| ... | @@ -203,6 +203,7 @@ import { | ... | @@ -203,6 +203,7 @@ import { |
| 203 | composerDraftHasUserContent, | 203 | composerDraftHasUserContent, |
| 204 | DraftId, | 204 | DraftId, |
| 205 | useComposerDraftStore, | 205 | useComposerDraftStore, |
| | 206 | useThreadHasUnsentDraft, |
| 206 | type ComposerThreadDraftState, | 207 | type ComposerThreadDraftState, |
| 207 | type DraftSessionState, | 208 | type DraftSessionState, |
| 208 | } from "../composerDraftStore"; | 209 | } from "../composerDraftStore"; |
| ... | @@ -487,6 +488,11 @@ function SortablePinnedThreadRow(props: { | ... | @@ -487,6 +488,11 @@ function SortablePinnedThreadRow(props: { |
| 487 | return props.children({ listeners, setNodeRef, transform, transition, isDragging }); | 488 | return props.children({ listeners, setNodeRef, transform, transition, isDragging }); |
| 488 | } | 489 | } |
| 489 | | 490 | |
| | 491 | // Unsent work shares one look: the new-thread draft rows and thread rows |
| | 492 | // with unsent composer text both use this tint and pen so they read alike. |
| | 493 | const draftSurfaceClassName = "bg-amber-400/[0.04] hover:bg-amber-400/[0.08]"; |
| | 494 | const draftPenClassName = "size-3 shrink-0 text-amber-600 dark:text-amber-300/80"; |
| | 495 | |
| 490 | // One unsent draft session the user has invested content in. Two lines, | 496 | // One unsent draft session the user has invested content in. Two lines, |
| 491 | // nothing else: project name, then the typed prompt. All the draft's | 497 | // nothing else: project name, then the typed prompt. All the draft's |
| 492 | // settings (model, env mode, branch, worktree) still travel with it — | 498 | // settings (model, env mode, branch, worktree) still travel with it — |
| ... | @@ -552,19 +558,14 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { | ... | @@ -552,19 +558,14 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { |
| 552 | data-testid="sidebar-draft-row" | 558 | data-testid="sidebar-draft-row" |
| 553 | className={cn( | 559 | className={cn( |
| 554 | "group/sidebar-row relative w-full cursor-pointer overflow-hidden rounded-md text-left text-sidebar-foreground outline-none select-none", | 560 | "group/sidebar-row relative w-full cursor-pointer overflow-hidden rounded-md text-left text-sidebar-foreground outline-none select-none", |
| 555 | props.isActive | 561 | props.isActive ? "bg-sidebar-row-active" : draftSurfaceClassName, |
| 556 | ? "bg-sidebar-row-active" | | |
| 557 | : "bg-amber-400/[0.04] hover:bg-amber-400/[0.08]", | | |
| 558 | )} | 562 | )} |
| 559 | onClick={handleActivate} | 563 | onClick={handleActivate} |
| 560 | onKeyDown={handleKeyDown} | 564 | onKeyDown={handleKeyDown} |
| 561 | > | 565 | > |
| 562 | <div className="relative z-10 px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]"> | 566 | <div className="relative z-10 px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]"> |
| 563 | <div className="flex h-5 min-w-0 items-center gap-1.5"> | 567 | <div className="flex h-5 min-w-0 items-center gap-1.5"> |
| 564 | <SquarePenIcon | 568 | <SquarePenIcon aria-hidden className={draftPenClassName} /> |
| 565 | aria-hidden | | |
| 566 | className="size-3 shrink-0 text-amber-600 dark:text-amber-300/80" | | |
| 567 | /> | | |
| 568 | <ProjectFavicon | 569 | <ProjectFavicon |
| 569 | environmentId={session.environmentId} | 570 | environmentId={session.environmentId} |
| 570 | cwd={props.projectCwd ?? ""} | 571 | cwd={props.projectCwd ?? ""} |
| ... | @@ -827,6 +828,19 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -827,6 +828,19 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 827 | }); | 828 | }); |
| 828 | const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds); | 829 | const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds); |
| 829 | const terminalProcessCount = runningTerminalIds.length; | 830 | const terminalProcessCount = runningTerminalIds.length; |
| | 831 | // Unsent composer text on this thread. The open thread shows its own |
| | 832 | // composer, so the marker only decorates rows you have navigated away from. |
| | 833 | const hasUnsentDraft = useThreadHasUnsentDraft(threadRef) && !props.isActive; |
| | 834 | const clearComposerContent = useComposerDraftStore((store) => store.clearComposerContent); |
| | 835 | const handleDiscardDraftClick = useCallback( |
| | 836 | (event: ReactMouseEvent) => { |
| | 837 | event.preventDefault(); |
| | 838 | event.stopPropagation(); |
| | 839 | releaseComposerDraftUploads(threadRef); |
| | 840 | clearComposerContent(threadRef); |
| | 841 | }, |
| | 842 | [clearComposerContent, threadRef], |
| | 843 | ); |
| 830 | | 844 | |
| 831 | const gitCwd = thread.worktreePath ?? props.projectCwd; | 845 | const gitCwd = thread.worktreePath ?? props.projectCwd; |
| 832 | const linkedPullRequestStatus = useLinkedThreadPullRequest( | 846 | const linkedPullRequestStatus = useLinkedThreadPullRequest( |
| ... | @@ -1163,9 +1177,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1163,9 +1177,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1163 | ? "bg-sidebar-row-active text-sidebar-foreground" | 1177 | ? "bg-sidebar-row-active text-sidebar-foreground" |
| 1164 | : isSelected | 1178 | : isSelected |
| 1165 | ? "bg-sidebar-row-selected text-sidebar-foreground" | 1179 | ? "bg-sidebar-row-selected text-sidebar-foreground" |
| 1166 | : shouldRecede | 1180 | : hasUnsentDraft |
| 1167 | ? "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground" | 1181 | ? cn(draftSurfaceClassName, "text-sidebar-foreground") |
| 1168 | : "bg-transparent text-sidebar-foreground hover:bg-sidebar-row-hover", | 1182 | : shouldRecede |
| | 1183 | ? "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground" |
| | 1184 | : "bg-transparent text-sidebar-foreground hover:bg-sidebar-row-hover", |
| 1169 | isInFlight && | 1185 | isInFlight && |
| 1170 | !props.isActive && | 1186 | !props.isActive && |
| 1171 | !isSelected && | 1187 | !isSelected && |
| ... | @@ -1251,6 +1267,25 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1251,6 +1267,25 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1251 | <TerminalIcon className={cn("size-3.5", terminalStatus.pulse && "animate-status-pulse")} /> | 1267 | <TerminalIcon className={cn("size-3.5", terminalStatus.pulse && "animate-status-pulse")} /> |
| 1252 | </span> | 1268 | </span> |
| 1253 | ) : null; | 1269 | ) : null; |
| | 1270 | // Same pen the new-thread draft rows lead with, so both kinds of unsent |
| | 1271 | // work read the same way in the list. |
| | 1272 | const draftIndicator = hasUnsentDraft ? ( |
| | 1273 | <Tooltip> |
| | 1274 | <TooltipTrigger |
| | 1275 | render={ |
| | 1276 | <span |
| | 1277 | role="img" |
| | 1278 | aria-label="Unsent draft" |
| | 1279 | data-testid={`sidebar-draft-indicator-${thread.id}`} |
| | 1280 | className="inline-flex shrink-0 items-center" |
| | 1281 | /> |
| | 1282 | } |
| | 1283 | > |
| | 1284 | <SquarePenIcon aria-hidden className={draftPenClassName} /> |
| | 1285 | </TooltipTrigger> |
| | 1286 | <TooltipPopup side="top">Unsent draft</TooltipPopup> |
| | 1287 | </Tooltip> |
| | 1288 | ) : null; |
| 1254 | const pinIndicator = props.isPinned ? ( | 1289 | const pinIndicator = props.isPinned ? ( |
| 1255 | props.pinningSupported ? ( | 1290 | props.pinningSupported ? ( |
| 1256 | <Tooltip> | 1291 | <Tooltip> |
| ... | @@ -1318,6 +1353,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1318,6 +1353,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1318 | className="size-4" | 1353 | className="size-4" |
| 1319 | /> | 1354 | /> |
| 1320 | </span> | 1355 | </span> |
| | 1356 | {draftIndicator} |
| 1321 | {title} | 1357 | {title} |
| 1322 | {pinIndicator} | 1358 | {pinIndicator} |
| 1323 | {terminalStatusIcon} | 1359 | {terminalStatusIcon} |
| ... | @@ -1465,6 +1501,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1465,6 +1501,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1465 | > | 1501 | > |
| 1466 | <div className="relative z-10 h-[4.875rem] px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]"> | 1502 | <div className="relative z-10 h-[4.875rem] px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]"> |
| 1467 | <div className="flex h-5 min-w-0 items-center gap-1.5"> | 1503 | <div className="flex h-5 min-w-0 items-center gap-1.5"> |
| | 1504 | {draftIndicator} |
| 1468 | <ProjectFavicon | 1505 | <ProjectFavicon |
| 1469 | environmentId={thread.environmentId} | 1506 | environmentId={thread.environmentId} |
| 1470 | cwd={props.projectCwd ?? ""} | 1507 | cwd={props.projectCwd ?? ""} |
| ... | @@ -1551,7 +1588,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1551,7 +1588,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1551 | threadTimeLabel(thread) | 1588 | threadTimeLabel(thread) |
| 1552 | )} | 1589 | )} |
| 1553 | </span> | 1590 | </span> |
| 1554 | {props.settlementSupported || showSnoozeButton ? ( | 1591 | {props.settlementSupported || showSnoozeButton || hasUnsentDraft ? ( |
| 1555 | <span | 1592 | <span |
| 1556 | className={cn( | 1593 | className={cn( |
| 1557 | // focus-visible, not focus-within: a mouse click leaves | 1594 | // focus-visible, not focus-within: a mouse click leaves |
| ... | @@ -1563,6 +1600,23 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1563,6 +1600,23 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1563 | snoozeMenuOpen && "pointer-events-auto static opacity-100", | 1600 | snoozeMenuOpen && "pointer-events-auto static opacity-100", |
| 1564 | )} | 1601 | )} |
| 1565 | > | 1602 | > |
| | 1603 | {hasUnsentDraft ? ( |
| | 1604 | <Tooltip> |
| | 1605 | <TooltipTrigger |
| | 1606 | render={ |
| | 1607 | <button |
| | 1608 | type="button" |
| | 1609 | aria-label="Discard draft" |
| | 1610 | onClick={handleDiscardDraftClick} |
| | 1611 | className="inline-flex cursor-pointer items-center rounded-md bg-transparent px-1.5 text-xs text-muted-foreground hover:text-foreground" |
| | 1612 | /> |
| | 1613 | } |
| | 1614 | > |
| | 1615 | <XIcon className="size-3.5" /> |
| | 1616 | </TooltipTrigger> |
| | 1617 | <TooltipPopup side="top">Discard draft</TooltipPopup> |
| | 1618 | </Tooltip> |
| | 1619 | ) : null} |
| 1566 | {showSnoozeButton ? ( | 1620 | {showSnoozeButton ? ( |
| 1567 | <SnoozePopoverButton | 1621 | <SnoozePopoverButton |
| 1568 | open={snoozeMenuOpen} | 1622 | open={snoozeMenuOpen} |