| author | |
| committer | |
| log | 6edd44f405bf874a85d8d4f7559149a42bf6c547 |
| tree | d1a3394935ca8ef0ae0178b66508a464053da581 |
| parent | e6626ee1643f62c53ca0c2680b46cf11cab9942d |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>4 files changed, 72 insertions(+), 0 deletions(-)
apps/web/src/components/ChatView.tsx+3| ... | ... | @@ -242,6 +242,7 @@ import { |
| 242 | 242 | import { useTheme } from "../hooks/useTheme"; |
| 243 | 243 | import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; |
| 244 | 244 | import { isCommandPaletteOpen } from "../commandPaletteBus"; |
| 245 | import { settleAndAdvance } from "../settleAndAdvance"; | |
| 245 | 246 | import { subscribeSnapShotComposerFocus } from "../lib/desktopSnapShot"; |
| 246 | 247 | import { useMediaQuery } from "../hooks/useMediaQuery"; |
| 247 | 248 | import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; |
| ... | ... | @@ -8109,6 +8110,8 @@ export default function ChatView(props: ChatViewProps) { |
| 8109 | 8110 | void handleUnsettleActiveThread(); |
| 8110 | 8111 | return; |
| 8111 | 8112 | } |
| 8113 | // Same as the sidebar Settle button: settle, then open the next thread. | |
| 8114 | if (settleAndAdvance(activeThreadRef)) return; | |
| 8112 | 8115 | |
| 8113 | 8116 | void settleThread(activeThreadRef).then((result) => { |
| 8114 | 8117 | if (result._tag !== "Failure" || isAtomCommandInterrupted(result)) return; |
apps/web/src/components/Sidebar.tsx+3| ... | ... | @@ -145,6 +145,7 @@ import { useAcknowledgeThreadWoke, useThreadActions } from "../hooks/useThreadAc |
| 145 | 145 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; |
| 146 | 146 | import { useTerminalFocus } from "../hooks/useTerminalFocus"; |
| 147 | 147 | import { isCommandPaletteOpen, openCommandPalette } from "../commandPaletteBus"; |
| 148 | import { registerSettleAndAdvance } from "../settleAndAdvance"; | |
| 148 | 149 | import { startNewThreadFromContext } from "../lib/chatThreadActions"; |
| 149 | 150 | import { useClientSettings } from "../hooks/useSettings"; |
| 150 | 151 | import { useCopyToClipboard } from "../hooks/useCopyToClipboard"; |
| ... | ... | @@ -3454,6 +3455,8 @@ export default function Sidebar() { |
| 3454 | 3455 | }, |
| 3455 | 3456 | [planForwardNavigation, settleThread], |
| 3456 | 3457 | ); |
| 3458 | // The thread.settle shortcut parks the open thread through this same path. | |
| 3459 | useEffect(() => registerSettleAndAdvance(attemptSettle), [attemptSettle]); | |
| 3457 | 3460 | // Post-settle navigation must skip threads settling in this same batch — |
| 3458 | 3461 | // they are all leaving the card block together. Rows that are already |
| 3459 | 3462 | // explicitly settled are skipped: nothing to do on a valid mixed selection. |
apps/web/src/settleAndAdvance.test.ts created+37| ... | ... | @@ -0,0 +1,37 @@ |
| 1 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; | |
| 2 | import { EnvironmentId, ThreadId, type ScopedThreadRef } from "@t3tools/contracts"; | |
| 3 | import { describe, expect, it } from "vite-plus/test"; | |
| 4 | ||
| 5 | import { registerSettleAndAdvance, settleAndAdvance } from "./settleAndAdvance"; | |
| 6 | ||
| 7 | const threadRef = scopeThreadRef( | |
| 8 | EnvironmentId.make("environment-local"), | |
| 9 | ThreadId.make("thread-1"), | |
| 10 | ); | |
| 11 | ||
| 12 | describe("settleAndAdvance", () => { | |
| 13 | it("hands the open thread to the mounted sidebar so it can move forward", () => { | |
| 14 | const settled: ScopedThreadRef[] = []; | |
| 15 | const unregister = registerSettleAndAdvance((ref) => settled.push(ref)); | |
| 16 | ||
| 17 | expect(settleAndAdvance(threadRef)).toBe(true); | |
| 18 | expect(settled).toEqual([threadRef]); | |
| 19 | unregister(); | |
| 20 | }); | |
| 21 | ||
| 22 | it("asks the caller to settle in place when no sidebar is mounted", () => { | |
| 23 | expect(settleAndAdvance(threadRef)).toBe(false); | |
| 24 | }); | |
| 25 | ||
| 26 | it("keeps a remounted sidebar's handler when the old one unmounts late", () => { | |
| 27 | const settledBy: string[] = []; | |
| 28 | const unregisterOld = registerSettleAndAdvance(() => settledBy.push("old")); | |
| 29 | const unregisterNew = registerSettleAndAdvance(() => settledBy.push("new")); | |
| 30 | unregisterOld(); | |
| 31 | ||
| 32 | expect(settleAndAdvance(threadRef)).toBe(true); | |
| 33 | expect(settledBy).toEqual(["new"]); | |
| 34 | unregisterNew(); | |
| 35 | expect(settleAndAdvance(threadRef)).toBe(false); | |
| 36 | }); | |
| 37 | }); |
apps/web/src/settleAndAdvance.ts created+29| ... | ... | @@ -0,0 +1,29 @@ |
| 1 | import type { ScopedThreadRef } from "@t3tools/contracts"; | |
| 2 | ||
| 3 | type SettleAndAdvance = (threadRef: ScopedThreadRef) => void; | |
| 4 | ||
| 5 | // Only the mounted thread sidebar knows the card order that picks the next | |
| 6 | // thread, so it registers its settle action here for other entry points. | |
| 7 | let registeredSettleAndAdvance: SettleAndAdvance | null = null; | |
| 8 | ||
| 9 | /** | |
| 10 | * Called by the thread sidebar while it is mounted. Returns the unregister | |
| 11 | * function, which leaves a newer registration in place. | |
| 12 | */ | |
| 13 | export function registerSettleAndAdvance(handler: SettleAndAdvance): () => void { | |
| 14 | registeredSettleAndAdvance = handler; | |
| 15 | return () => { | |
| 16 | if (registeredSettleAndAdvance === handler) registeredSettleAndAdvance = null; | |
| 17 | }; | |
| 18 | } | |
| 19 | ||
| 20 | /** | |
| 21 | * Settles the open thread the way the sidebar Settle button does: settle, | |
| 22 | * then move to the next thread. Used by the `thread.settle` shortcut. | |
| 23 | * Returns false when no sidebar is mounted, so the caller settles in place. | |
| 24 | */ | |
| 25 | export function settleAndAdvance(threadRef: ScopedThreadRef): boolean { | |
| 26 | if (!registeredSettleAndAdvance) return false; | |
| 27 | registeredSettleAndAdvance(threadRef); | |
| 28 | return true; | |
| 29 | } |