Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 5.2k
fix(web): clear woke state on explicit thread actions#5486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
7f3bbd1e9c57ee092478805c6acdFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -431,6 +431,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ||
| onUnsettle: (threadRef: ScopedThreadRef) => void; | ||
| onSnooze: (threadRef: ScopedThreadRef, preset: SnoozePreset) => void; | ||
| onUnsnooze: (threadRef: ScopedThreadRef) => void; | ||
| onAcknowledgeWoke: (threadRef: ScopedThreadRef, visitedAt: string) => void; | ||
| onChangeRequestState: (threadKey: string, state: "open" | "closed" | "merged" | null) => void; | ||
| }) { | ||
| const { | ||
| @@ -439,6 +440,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ||
| onCancelRename, | ||
| onCommitRename, | ||
| onContextMenu, | ||
| onAcknowledgeWoke, | ||
| onRenameTitleChange, | ||
| onSettle, | ||
| onSnooze, | ||
| @@ -542,6 +544,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ||
| className: "text-emerald-700 dark:text-emerald-300", | ||
| } | ||
| : null; | ||
| const isWokeStatus = topStatus?.icon === "woke"; | ||
| const gitCwd = thread.worktreePath ?? props.projectCwd; | ||
| const gitStatus = useEnvironmentQuery( | ||
| @@ -605,6 +608,15 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ||
| }, | ||
| [onThreadClick, threadRef], | ||
| ); | ||
| const handleAcknowledgeWokeClick = useCallback( | ||
| (event: ReactMouseEvent) => { | ||
| event.preventDefault(); | ||
| event.stopPropagation(); | ||
| if (props.wokeAt === null) return; | ||
| onAcknowledgeWoke(threadRef, props.wokeAt); | ||
| }, | ||
| [onAcknowledgeWoke, props.wokeAt, threadRef], | ||
| ); | ||
| const handleContextMenu = useCallback( | ||
| (event: ReactMouseEvent) => { | ||
| event.preventDefault(); | ||
| @@ -853,7 +865,12 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ||
| the time/jump label yields to the settle affordance. */} | ||
| {prBadge} | ||
| <span className="relative ml-auto flex h-6 min-w-8 shrink-0 items-center justify-end"> | ||
| <span className="inline-flex justify-end tabular-nums text-muted-foreground/55 transition-opacity group-hover/v2-row:opacity-0"> | ||
| <span | ||
| className={cn( | ||
| "inline-flex justify-end tabular-nums text-muted-foreground/55 transition-opacity", | ||
| !isWoke && "group-hover/v2-row:opacity-0", | ||
| )} | ||
| > | ||
| {variantAction === "unsnooze" && props.snoozeWakeLabelText !== null ? ( | ||
| // Snoozed rows show when they come BACK, not when they were | ||
| // last touched — the return ticket is the row's whole story. | ||
| @@ -863,14 +880,16 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ||
| ) : isWoke ? ( | ||
| // A wake can land straight in the settled tail (e.g. PR | ||
| // merged while snoozed); the signal must survive the trip. | ||
| <span | ||
| role="status" | ||
| aria-label="Woke from snooze" | ||
| className="inline-flex items-center gap-1 text-xs font-medium text-amber-700 dark:text-amber-300" | ||
| <button | ||
| type="button" | ||
| aria-label="Dismiss Woke notification" | ||
| title="Dismiss Woke notification" | ||
| onClick={handleAcknowledgeWokeClick} | ||
| className="inline-flex cursor-pointer items-center gap-1 rounded-sm text-xs font-medium text-amber-700 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring dark:text-amber-300" | ||
| > | ||
| <AlarmClockIcon aria-hidden className="size-3" /> | ||
| Woke | ||
| </span> | ||
| <span role="status">Woke</span> | ||
| </button> | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| ) : ( | ||
| <span className="text-xs"> | ||
| {variantAction === "unsettle" | ||
| @@ -885,7 +904,10 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ||
| type="button" | ||
| aria-label="Wake thread now" | ||
| onClick={handleUnsnoozeClick} | ||
| className="absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100" | ||
| className={cn( | ||
| "pointer-events-none absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 group-hover/v2-row:pointer-events-auto group-hover/v2-row:opacity-100", | ||
| isWoke && "group-hover/v2-row:static", | ||
| )} | ||
| > | ||
| <AlarmClockOffIcon className="size-3" /> | ||
| </button> | ||
| @@ -895,7 +917,10 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ||
| type="button" | ||
| aria-label="Un-settle thread" | ||
| onClick={handleUnsettleClick} | ||
| className="absolute inset-y-0 right-0 -mr-1 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-1.5 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100" | ||
| className={cn( | ||
| "pointer-events-none absolute inset-y-0 right-0 -mr-1 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-1.5 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 group-hover/v2-row:pointer-events-auto group-hover/v2-row:opacity-100", | ||
| isWoke && "group-hover/v2-row:static", | ||
| )} | ||
| > | ||
| <Undo2Icon className="mb-px size-3.5" /> | ||
| </button> | ||
| @@ -904,7 +929,10 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ||
| type="button" | ||
| aria-label="Settle thread" | ||
| onClick={handleSettleClick} | ||
| className="absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100" | ||
| className={cn( | ||
| "pointer-events-none absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 group-hover/v2-row:pointer-events-auto group-hover/v2-row:opacity-100", | ||
| isWoke && "group-hover/v2-row:static", | ||
| )} | ||
| > | ||
| <CheckIcon className="size-3" /> | ||
| </button> | ||
| @@ -972,39 +1000,56 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ||
| the hidden state out of flow lets the project label reclaim | ||
| space without either state overlapping it. */} | ||
| <span className="group/v2-status-slot relative ml-auto flex h-5 min-w-8 shrink-0 items-stretch justify-end text-xs"> | ||
| {/* pointer-events-none: while hovered this label is absolute | ||
| + opacity-0, which paints it ABOVE the in-flow settle/snooze | ||
| buttons; without it the invisible label eats their clicks. */} | ||
| {/* Read-only status labels yield to the hover actions. Woke is | ||
| itself an action, so it stays pointer-enabled and visible | ||
| while the other controls appear beside it. */} | ||
| <span | ||
| className={cn( | ||
| "pointer-events-none self-center justify-self-end tabular-nums text-muted-foreground/65 transition-opacity group-has-[:focus-visible]/v2-status-slot:absolute group-has-[:focus-visible]/v2-status-slot:right-0 group-has-[:focus-visible]/v2-status-slot:opacity-0 group-hover/v2-row:absolute group-hover/v2-row:right-0 group-hover/v2-row:opacity-0", | ||
| snoozeMenuOpen && "absolute right-0 opacity-0", | ||
| isWokeStatus | ||
| ? "pointer-events-auto" | ||
| : "pointer-events-none group-has-[:focus-visible]/v2-status-slot:absolute group-has-[:focus-visible]/v2-status-slot:right-0 group-has-[:focus-visible]/v2-status-slot:opacity-0 group-hover/v2-row:absolute group-hover/v2-row:right-0 group-hover/v2-row:opacity-0", | ||
| "self-center justify-self-end tabular-nums text-muted-foreground/65 transition-opacity", | ||
| snoozeMenuOpen && "pointer-events-none absolute right-0 opacity-0", | ||
| )} | ||
| > | ||
| {topStatus ? ( | ||
| <span | ||
| className={cn( | ||
| "inline-flex items-center gap-1 font-medium", | ||
| topStatus.className, | ||
| )} | ||
| > | ||
| {topStatus.icon === "working" ? ( | ||
| <CircleDashedIcon aria-hidden className="size-4 shrink-0" /> | ||
| ) : topStatus.icon === "done" ? ( | ||
| <CircleCheckIcon aria-hidden className="size-4 shrink-0" /> | ||
| ) : topStatus.icon === "woke" ? ( | ||
| isWokeStatus ? ( | ||
| <button | ||
| type="button" | ||
| aria-label="Dismiss Woke notification" | ||
| title="Dismiss Woke notification" | ||
| onClick={handleAcknowledgeWokeClick} | ||
| className={cn( | ||
| "inline-flex cursor-pointer items-center gap-1 rounded-sm font-medium outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring", | ||
| topStatus.className, | ||
| )} | ||
| > | ||
| <AlarmClockIcon aria-hidden className="size-4 shrink-0" /> | ||
| ) : null} | ||
| {/* The label alone is the live region: a role="status" | ||
| wrapper around the ticking duration would make | ||
| screen readers announce every second. */} | ||
| <span role="status">{topStatus.label}</span> | ||
| {status === "working" ? ( | ||
| <span aria-hidden> | ||
| <WorkingDuration startedAt={resolveWorkingStartedAt(thread)} /> | ||
| </span> | ||
| ) : null} | ||
| </span> | ||
| <span role="status">{topStatus.label}</span> | ||
| </button> | ||
| ) : ( | ||
| <span | ||
| className={cn( | ||
| "inline-flex items-center gap-1 font-medium", | ||
| topStatus.className, | ||
| )} | ||
| > | ||
| {topStatus.icon === "working" ? ( | ||
| <CircleDashedIcon aria-hidden className="size-4 shrink-0" /> | ||
| ) : topStatus.icon === "done" ? ( | ||
| <CircleCheckIcon aria-hidden className="size-4 shrink-0" /> | ||
| ) : null} | ||
| {/* The label alone is the live region: a role="status" | ||
| wrapper around the ticking duration would make | ||
| screen readers announce every second. */} | ||
| <span role="status">{topStatus.label}</span> | ||
| {status === "working" ? ( | ||
| <span aria-hidden> | ||
| <WorkingDuration startedAt={resolveWorkingStartedAt(thread)} /> | ||
| </span> | ||
| ) : null} | ||
| </span> | ||
| ) | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| ) : ( | ||
| threadTimeLabel(thread) | ||
| )} | ||
| @@ -1017,8 +1062,8 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ||
| // would keep the controls pinned over the status label | ||
| // once the pointer moves away (e.g. after a failed | ||
| // settle) instead of cross-fading back. | ||
| "absolute inset-y-0 right-0 flex items-stretch opacity-0 transition-opacity has-[:focus-visible]:static has-[:focus-visible]:opacity-100 group-hover/v2-row:static group-hover/v2-row:opacity-100", | ||
| snoozeMenuOpen && "static opacity-100", | ||
| "pointer-events-none absolute inset-y-0 right-0 flex items-stretch opacity-0 transition-opacity has-[:focus-visible]:pointer-events-auto has-[:focus-visible]:static has-[:focus-visible]:opacity-100 group-hover/v2-row:pointer-events-auto group-hover/v2-row:static group-hover/v2-row:opacity-100", | ||
| snoozeMenuOpen && "pointer-events-auto static opacity-100", | ||
| )} | ||
| > | ||
| {showSnoozeButton ? ( | ||
| @@ -1288,6 +1333,13 @@ export default function SidebarV2() { | ||
| const toggleThreadSelection = useThreadSelectionStore((s) => s.toggleThread); | ||
| const rangeSelectTo = useThreadSelectionStore((s) => s.rangeSelectTo); | ||
| const markThreadUnread = useUiStateStore((s) => s.markThreadUnread); | ||
| const markThreadVisited = useUiStateStore((s) => s.markThreadVisited); | ||
| const acknowledgeWoke = useCallback( | ||
| (threadRef: ScopedThreadRef, visitedAt: string) => { | ||
| markThreadVisited(scopedThreadKey(threadRef), visitedAt); | ||
| }, | ||
| [markThreadVisited], | ||
| ); | ||
| const routeTarget = useParams({ | ||
| strict: false, | ||
| select: (params) => resolveThreadRouteTarget(params), | ||
| @@ -3022,6 +3074,7 @@ export default function SidebarV2() { | ||
| onUnsettle={attemptUnsettle} | ||
| onSnooze={attemptSnooze} | ||
| onUnsnooze={attemptUnsnooze} | ||
| onAcknowledgeWoke={acknowledgeWoke} | ||
| onChangeRequestState={handleChangeRequestState} | ||
| /> | ||
| ); | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.