From 0420fb1bfe18c3988af7ff363da324a9afd1f19d Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sat, 22 Aug 2026 14:15:50 +0530 Subject: [PATCH 1/9] feat(web): add toggle shortcut for settling threads Settling a thread had no keyboard shortcut. Add thread.settle.toggle (command ID thread.settle.toggle) with default mod+shift+s (when: !terminalFocus) and dispatch in ChatRouteGlobalShortcuts. The handler toggles the active thread via effectiveSettled classification (mirroring Sidebar partition) and calls settleThread/unsettleThread. No mobile changes; desktop inherits via web. --- apps/web/src/routes/_chat.tsx | 70 ++++++++++++++++++++++++++- packages/contracts/src/keybindings.ts | 1 + packages/shared/src/keybindings.ts | 1 + 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index e084e22c2cbb..e6f94e1138bc 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -1,16 +1,24 @@ import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; import { useAtomValue } from "@effect/atom-react"; import { useEffect, useMemo } from "react"; +import { scopedThreadKey } from "@t3tools/client-runtime/environment"; +import { effectiveSettled } from "@t3tools/client-runtime/state/thread-settled"; +import { + isAtomCommandInterrupted, + squashAtomCommandFailure, +} from "@t3tools/client-runtime/state/runtime"; import { isCommandPaletteOpen } from "../commandPaletteBus"; import { useClientSettings, useLegacySidebarEnabled } from "../hooks/useSettings"; import { openCommandPalette } from "../commandPaletteBus"; -import { useProjects } from "../state/entities"; +import { useProjects, useThreadShell } from "../state/entities"; import { usePrimaryEnvironmentId } from "../state/environments"; import { selectProjectGroupingSettings } from "../logicalProject"; import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping"; +import { threadChangeRequestSnapshotsAtom } from "../components/ThreadStatusIndicators"; import { dispatchPreviewAction } from "../components/preview/previewActionBus"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; +import { useThreadActions } from "../hooks/useThreadActions"; import { startNewThreadFromContext } from "../lib/chatThreadActions"; import { isPreviewFocused } from "../lib/previewFocus"; import { isTerminalFocused } from "../lib/terminalFocus"; @@ -20,13 +28,14 @@ import { isPreviewSupportedInRuntime } from "../previewStateStore"; import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore"; import { useThreadSelectionStore } from "../threadSelectionStore"; import { stackedThreadToast, toastManager } from "~/components/ui/toast"; -import { primaryServerKeybindingsAtom } from "~/state/server"; +import { environmentServerConfigsAtom, primaryServerKeybindingsAtom } from "~/state/server"; function ChatRouteGlobalShortcuts() { const clearSelection = useThreadSelectionStore((state) => state.clearSelection); const selectedThreadKeysSize = useThreadSelectionStore((state) => state.selectedThreadKeys.size); const { activeDraftThread, activeThread, defaultProjectRef, handleNewThread, routeThreadRef } = useHandleNewThread(); + const activeThreadShell = useThreadShell(routeThreadRef); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const legacySidebarEnabled = useLegacySidebarEnabled(); const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); @@ -55,6 +64,11 @@ function ChatRouteGlobalShortcuts() { ? selectActiveRightPanel(state.byThreadKey, routeThreadRef) === "preview" : false, ); + const { settleThread, unsettleThread } = useThreadActions(); + const serverConfigs = useAtomValue(environmentServerConfigsAtom); + const changeRequestSnapshotByKey = useAtomValue(threadChangeRequestSnapshotsAtom); + const autoSettleAfterDays = useClientSettings((settings) => settings.sidebarAutoSettleAfterDays); + const autoSettleOnMerge = useClientSettings((settings) => settings.sidebarAutoSettleOnMerge); useEffect(() => { const onWindowKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented) return; @@ -149,6 +163,51 @@ function ChatRouteGlobalShortcuts() { ? "zoom-out" : "reset-zoom"; dispatchPreviewAction(action); + return; + } + + if (command === "thread.settle.toggle") { + if (event.repeat) return; + event.preventDefault(); + event.stopPropagation(); + if (!routeThreadRef || !activeThreadShell) return; + const supportsSettlement = + serverConfigs.get(routeThreadRef.environmentId)?.environment.capabilities + .threadSettlement === true; + if (!supportsSettlement) return; + const threadKey = scopedThreadKey(routeThreadRef); + const snapshot = changeRequestSnapshotByKey.get(threadKey); + const changeRequest = + snapshot != null && + (activeThreadShell.worktreePath === null || snapshot.branch === activeThreadShell.branch) + ? snapshot.pr + : null; + // Pinned threads never classify as settled in the sidebar partition, + // so toggling a pinned thread always settles it rather than un-settling. + const isSettled = + activeThreadShell.pinnedAt == null && + effectiveSettled(activeThreadShell, { + now: `${new Date().toISOString().slice(0, 16)}:00.000Z`, + autoSettleAfterDays, + autoSettleOnMerge, + changeRequest, + }); + void (async () => { + const result = isSettled + ? await unsettleThread(routeThreadRef) + : await settleThread(routeThreadRef); + if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { + const error = squashAtomCommandFailure(result); + toastManager.add( + stackedThreadToast({ + type: "error", + title: isSettled ? "Failed to un-settle thread" : "Failed to settle thread", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + } + })(); + return; } }; @@ -159,6 +218,10 @@ function ChatRouteGlobalShortcuts() { }, [ activeDraftThread, activeThread, + activeThreadShell, + autoSettleAfterDays, + autoSettleOnMerge, + changeRequestSnapshotByKey, clearSelection, handleNewThread, keybindings, @@ -168,7 +231,10 @@ function ChatRouteGlobalShortcuts() { routeThreadRef, selectedThreadKeysSize, legacySidebarEnabled, + serverConfigs, + settleThread, terminalOpen, + unsettleThread, ]); return null; diff --git a/packages/contracts/src/keybindings.ts b/packages/contracts/src/keybindings.ts index 19276c41e7b6..050ef8c67a68 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -71,6 +71,7 @@ export const STATIC_KEYBINDING_COMMANDS = [ "chat.new", "chat.newLocal", "editor.openFavorite", + "thread.settle.toggle", ...MODEL_PICKER_KEYBINDING_COMMANDS, ...THREAD_KEYBINDING_COMMANDS, ] as const; diff --git a/packages/shared/src/keybindings.ts b/packages/shared/src/keybindings.ts index 158a9ffb1ac9..57b6d0121b26 100644 --- a/packages/shared/src/keybindings.ts +++ b/packages/shared/src/keybindings.ts @@ -44,6 +44,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" }, { key: "mod+shift+m", command: "modelPicker.toggle", when: "!terminalFocus" }, { key: "mod+o", command: "editor.openFavorite" }, + { key: "mod+shift+s", command: "thread.settle.toggle", when: "!terminalFocus" }, { key: "mod+shift+[", command: "thread.previous" }, { key: "mod+shift+]", command: "thread.next" }, ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ From 6fe4f8ad68c7639557a94ee63a7a6c1b63b2aea6 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sat, 22 Aug 2026 14:38:18 +0530 Subject: [PATCH 2/9] fix(web): exclude snoozed threads from settle toggle Sidebar partitions snoozed threads before settled ones, but the toggle only excluded pinned threads. A snoozed thread that also satisfies effectiveSettled was treated as settled and would be unsettle-d instead of settled. Mirror the partition ordering by checking effectiveSnoozed first. --- apps/web/src/routes/_chat.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index e6f94e1138bc..9cb1957c5b0f 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -2,7 +2,7 @@ import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; import { useAtomValue } from "@effect/atom-react"; import { useEffect, useMemo } from "react"; import { scopedThreadKey } from "@t3tools/client-runtime/environment"; -import { effectiveSettled } from "@t3tools/client-runtime/state/thread-settled"; +import { effectiveSettled, effectiveSnoozed } from "@t3tools/client-runtime/state/thread-settled"; import { isAtomCommandInterrupted, squashAtomCommandFailure, @@ -182,12 +182,18 @@ function ChatRouteGlobalShortcuts() { (activeThreadShell.worktreePath === null || snapshot.branch === activeThreadShell.branch) ? snapshot.pr : null; - // Pinned threads never classify as settled in the sidebar partition, - // so toggling a pinned thread always settles it rather than un-settling. + // Match Sidebar partition ordering: snoozed outranks settled, pinned + // never counts as settled. Without this, a snoozed thread that also + // satisfies effectiveSettled would be treated as settled and unsettled. + const nowIso = new Date().toISOString(); + const isSnoozed = + serverConfigs.get(routeThreadRef.environmentId)?.environment.capabilities.threadSnooze === + true && effectiveSnoozed(activeThreadShell, { now: nowIso }); const isSettled = + !isSnoozed && activeThreadShell.pinnedAt == null && effectiveSettled(activeThreadShell, { - now: `${new Date().toISOString().slice(0, 16)}:00.000Z`, + now: `${nowIso.slice(0, 16)}:00.000Z`, autoSettleAfterDays, autoSettleOnMerge, changeRequest, From 6f592f7e0e1fd0bea0831fe7487515e6e3aa9edb Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sat, 22 Aug 2026 21:01:56 +0530 Subject: [PATCH 3/9] fix(web): consume repeat events for settle toggle --- apps/web/src/routes/_chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index 9cb1957c5b0f..fca06ec82846 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -167,9 +167,9 @@ function ChatRouteGlobalShortcuts() { } if (command === "thread.settle.toggle") { - if (event.repeat) return; event.preventDefault(); event.stopPropagation(); + if (event.repeat) return; if (!routeThreadRef || !activeThreadShell) return; const supportsSettlement = serverConfigs.get(routeThreadRef.environmentId)?.environment.capabilities From f000b1b22fc13ab6652a1733c1dfedaa9cf4df7a Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sat, 22 Aug 2026 21:15:49 +0530 Subject: [PATCH 4/9] fix(web): settle toggle matches banner and menu classification The snoozed/pinned exclusions mirrored the sidebar list partition, but the surfaces the user sees on the open thread (parked-thread banner, header menu) classify with effectiveSettled alone. Classify the same way so the shortcut never disagrees with the visible Un-settle button. --- apps/web/src/routes/_chat.tsx | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index fca06ec82846..dc8d13181f58 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -2,7 +2,7 @@ import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; import { useAtomValue } from "@effect/atom-react"; import { useEffect, useMemo } from "react"; import { scopedThreadKey } from "@t3tools/client-runtime/environment"; -import { effectiveSettled, effectiveSnoozed } from "@t3tools/client-runtime/state/thread-settled"; +import { effectiveSettled } from "@t3tools/client-runtime/state/thread-settled"; import { isAtomCommandInterrupted, squashAtomCommandFailure, @@ -182,22 +182,15 @@ function ChatRouteGlobalShortcuts() { (activeThreadShell.worktreePath === null || snapshot.branch === activeThreadShell.branch) ? snapshot.pr : null; - // Match Sidebar partition ordering: snoozed outranks settled, pinned - // never counts as settled. Without this, a snoozed thread that also - // satisfies effectiveSettled would be treated as settled and unsettled. - const nowIso = new Date().toISOString(); - const isSnoozed = - serverConfigs.get(routeThreadRef.environmentId)?.environment.capabilities.threadSnooze === - true && effectiveSnoozed(activeThreadShell, { now: nowIso }); - const isSettled = - !isSnoozed && - activeThreadShell.pinnedAt == null && - effectiveSettled(activeThreadShell, { - now: `${nowIso.slice(0, 16)}:00.000Z`, - autoSettleAfterDays, - autoSettleOnMerge, - changeRequest, - }); + // Classify like ChatView's parked-thread banner and the header menu: + // effectiveSettled alone, minute-quantized so it cannot disagree + // with those surfaces within the same minute. + const isSettled = effectiveSettled(activeThreadShell, { + now: `${new Date().toISOString().slice(0, 16)}:00.000Z`, + autoSettleAfterDays, + autoSettleOnMerge, + changeRequest, + }); void (async () => { const result = isSettled ? await unsettleThread(routeThreadRef) From 6742b00c88e3a91699405bff7d3e98492bd39217 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sat, 22 Aug 2026 21:37:26 +0530 Subject: [PATCH 5/9] fix(web): resolve settle toggle PR from live VCS status The snapshot map is only written by sidebar v2 rows, so on the legacy sidebar (or before a row mounts) the shortcut classified with no PR while the banner and menu used the live one. Resolve through resolveDisplayedThreadPr over the same vcsEnvironment.status query ChatView uses, so all three surfaces agree. --- apps/web/src/routes/_chat.tsx | 45 +++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index dc8d13181f58..cef61e04b23d 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -11,11 +11,14 @@ import { import { isCommandPaletteOpen } from "../commandPaletteBus"; import { useClientSettings, useLegacySidebarEnabled } from "../hooks/useSettings"; import { openCommandPalette } from "../commandPaletteBus"; -import { useProjects, useThreadShell } from "../state/entities"; +import { useProjects, readProject, useThreadShell } from "../state/entities"; import { usePrimaryEnvironmentId } from "../state/environments"; import { selectProjectGroupingSettings } from "../logicalProject"; import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping"; -import { threadChangeRequestSnapshotsAtom } from "../components/ThreadStatusIndicators"; +import { + resolveDisplayedThreadPr, + threadChangeRequestSnapshotsAtom, +} from "../components/ThreadStatusIndicators"; import { dispatchPreviewAction } from "../components/preview/previewActionBus"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { useThreadActions } from "../hooks/useThreadActions"; @@ -27,6 +30,8 @@ import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../termina import { isPreviewSupportedInRuntime } from "../previewStateStore"; import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore"; import { useThreadSelectionStore } from "../threadSelectionStore"; +import { useEnvironmentQuery } from "../state/query"; +import { vcsEnvironment } from "../state/vcs"; import { stackedThreadToast, toastManager } from "~/components/ui/toast"; import { environmentServerConfigsAtom, primaryServerKeybindingsAtom } from "~/state/server"; @@ -69,6 +74,25 @@ function ChatRouteGlobalShortcuts() { const changeRequestSnapshotByKey = useAtomValue(threadChangeRequestSnapshotsAtom); const autoSettleAfterDays = useClientSettings((settings) => settings.sidebarAutoSettleAfterDays); const autoSettleOnMerge = useClientSettings((settings) => settings.sidebarAutoSettleOnMerge); + // PR resolution mirrors ChatView's banner exactly: live VCS status first, + // snapshot second. The snapshot alone (Sidebar-written) is missing on the + // legacy sidebar or before a row mounts, which would misclassify settle. + const gitStatusCwd = + activeThreadShell?.worktreePath ?? + (routeThreadRef && activeThreadShell + ? (readProject({ + environmentId: routeThreadRef.environmentId, + projectId: activeThreadShell.projectId, + })?.workspaceRoot ?? null) + : null); + const gitStatusQuery = useEnvironmentQuery( + routeThreadRef === null || gitStatusCwd === null + ? null + : vcsEnvironment.status({ + environmentId: routeThreadRef.environmentId, + input: { cwd: gitStatusCwd }, + }), + ); useEffect(() => { const onWindowKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented) return; @@ -176,12 +200,18 @@ function ChatRouteGlobalShortcuts() { .threadSettlement === true; if (!supportsSettlement) return; const threadKey = scopedThreadKey(routeThreadRef); - const snapshot = changeRequestSnapshotByKey.get(threadKey); + // Same PR resolution as ChatView's banner: resolveDisplayedThreadPr + // over live git status + the snapshot map. + const activeThreadPr = resolveDisplayedThreadPr({ + threadBranch: activeThreadShell.branch, + gitStatus: gitStatusQuery.data ?? null, + snapshot: changeRequestSnapshotByKey.get(threadKey), + retainTerminalOnBranchMismatch: activeThreadShell.worktreePath === null, + }); const changeRequest = - snapshot != null && - (activeThreadShell.worktreePath === null || snapshot.branch === activeThreadShell.branch) - ? snapshot.pr - : null; + activeThreadPr === null + ? null + : { state: activeThreadPr.state, updatedAt: activeThreadPr.updatedAt }; // Classify like ChatView's parked-thread banner and the header menu: // effectiveSettled alone, minute-quantized so it cannot disagree // with those surfaces within the same minute. @@ -223,6 +253,7 @@ function ChatRouteGlobalShortcuts() { changeRequestSnapshotByKey, clearSelection, handleNewThread, + gitStatusQuery, keybindings, defaultProjectRef, previewOpen, From ed7f8e4bf75a3b44d8b757498c6d98df8b89fbd5 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sat, 22 Aug 2026 21:51:17 +0530 Subject: [PATCH 6/9] fix(web): depend on git status data, not query object --- apps/web/src/routes/_chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index cef61e04b23d..3e665587dab1 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -253,7 +253,7 @@ function ChatRouteGlobalShortcuts() { changeRequestSnapshotByKey, clearSelection, handleNewThread, - gitStatusQuery, + gitStatusQuery.data, keybindings, defaultProjectRef, previewOpen, From 8c5633358c3bba2e48b4fb184579877736ad1f15 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 23 Aug 2026 13:59:27 +0530 Subject: [PATCH 7/9] fix(web): keep open PRs visible while VCS status loads --- apps/web/src/routes/_chat.tsx | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index 3e665587dab1..f107f9135fe6 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -200,14 +200,24 @@ function ChatRouteGlobalShortcuts() { .threadSettlement === true; if (!supportsSettlement) return; const threadKey = scopedThreadKey(routeThreadRef); - // Same PR resolution as ChatView's banner: resolveDisplayedThreadPr - // over live git status + the snapshot map. - const activeThreadPr = resolveDisplayedThreadPr({ - threadBranch: activeThreadShell.branch, - gitStatus: gitStatusQuery.data ?? null, - snapshot: changeRequestSnapshotByKey.get(threadKey), - retainTerminalOnBranchMismatch: activeThreadShell.worktreePath === null, - }); + const snapshot = changeRequestSnapshotByKey.get(threadKey); + // While VCS status is still loading, resolveDisplayedThreadPr drops + // non-terminal (open) snapshot PRs, which would let inactivity + // auto-settle classify a thread with an open PR as settled. Use the + // Sidebar's snapshot rule until the live status lands. + const activeThreadPr = + gitStatusQuery.data !== null || !gitStatusQuery.isPending + ? resolveDisplayedThreadPr({ + threadBranch: activeThreadShell.branch, + gitStatus: gitStatusQuery.data, + snapshot, + retainTerminalOnBranchMismatch: activeThreadShell.worktreePath === null, + }) + : snapshot != null && + (activeThreadShell.worktreePath === null || + snapshot.branch === activeThreadShell.branch) + ? snapshot.pr + : null; const changeRequest = activeThreadPr === null ? null From c55416627ebf674130ab3684119fd8f82c167753 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 23 Aug 2026 14:08:31 +0530 Subject: [PATCH 8/9] fix(web): tighten pending-status snapshot rule to matching or terminal PRs --- apps/web/src/routes/_chat.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index f107f9135fe6..6719bcb4ef4b 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -203,19 +203,23 @@ function ChatRouteGlobalShortcuts() { const snapshot = changeRequestSnapshotByKey.get(threadKey); // While VCS status is still loading, resolveDisplayedThreadPr drops // non-terminal (open) snapshot PRs, which would let inactivity - // auto-settle classify a thread with an open PR as settled. Use the - // Sidebar's snapshot rule until the live status lands. + // auto-settle classify a thread with an open PR as settled. Until the + // live status lands, accept matching-branch snapshots plus terminal + // snapshots retained across a mismatch (the same rule + // resolveDisplayedThreadPr applies once status arrives). + const gitStatus = gitStatusQuery.isPending ? null : gitStatusQuery.data; const activeThreadPr = - gitStatusQuery.data !== null || !gitStatusQuery.isPending + gitStatus !== null ? resolveDisplayedThreadPr({ threadBranch: activeThreadShell.branch, - gitStatus: gitStatusQuery.data, + gitStatus, snapshot, retainTerminalOnBranchMismatch: activeThreadShell.worktreePath === null, }) : snapshot != null && - (activeThreadShell.worktreePath === null || - snapshot.branch === activeThreadShell.branch) + (snapshot.branch === activeThreadShell.branch || + (activeThreadShell.worktreePath === null && + (snapshot.pr.state === "merged" || snapshot.pr.state === "closed"))) ? snapshot.pr : null; const changeRequest = From 7ca50c65d298f7fba6f17b5eb686244fc7d6917f Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 23 Aug 2026 14:15:18 +0530 Subject: [PATCH 9/9] fix(web): resolve settle toggle PR exactly like ChatView The pending-status snapshot fallback diverged from the banner and menu (both resolve unconditionally) and could go stale on a failed status query. Match ChatView's resolution exactly; the loading window behaves the same as the surfaces the user already sees. --- apps/web/src/routes/_chat.tsx | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index 6719bcb4ef4b..29d8d0e9169a 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -201,27 +201,15 @@ function ChatRouteGlobalShortcuts() { if (!supportsSettlement) return; const threadKey = scopedThreadKey(routeThreadRef); const snapshot = changeRequestSnapshotByKey.get(threadKey); - // While VCS status is still loading, resolveDisplayedThreadPr drops - // non-terminal (open) snapshot PRs, which would let inactivity - // auto-settle classify a thread with an open PR as settled. Until the - // live status lands, accept matching-branch snapshots plus terminal - // snapshots retained across a mismatch (the same rule - // resolveDisplayedThreadPr applies once status arrives). - const gitStatus = gitStatusQuery.isPending ? null : gitStatusQuery.data; - const activeThreadPr = - gitStatus !== null - ? resolveDisplayedThreadPr({ - threadBranch: activeThreadShell.branch, - gitStatus, - snapshot, - retainTerminalOnBranchMismatch: activeThreadShell.worktreePath === null, - }) - : snapshot != null && - (snapshot.branch === activeThreadShell.branch || - (activeThreadShell.worktreePath === null && - (snapshot.pr.state === "merged" || snapshot.pr.state === "closed"))) - ? snapshot.pr - : null; + // Resolve exactly like ChatView's banner and header menu + // (resolveDisplayedThreadPr over live VCS status + snapshots), so the + // shortcut can never disagree with the visible Un-settle button. + const activeThreadPr = resolveDisplayedThreadPr({ + threadBranch: activeThreadShell.branch, + gitStatus: gitStatusQuery.data, + snapshot, + retainTerminalOnBranchMismatch: activeThreadShell.worktreePath === null, + }); const changeRequest = activeThreadPr === null ? null