Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ import {
import { useTheme } from "../hooks/useTheme";
import { writeTextToClipboard } from "../hooks/useCopyToClipboard";
import { isCommandPaletteOpen } from "../commandPaletteBus";
import { settleAndAdvance } from "../settleAndAdvance";
import { subscribeSnapShotComposerFocus } from "../lib/desktopSnapShot";
import { useMediaQuery } from "../hooks/useMediaQuery";
import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
Expand Down Expand Up @@ -8109,6 +8110,8 @@ export default function ChatView(props: ChatViewProps) {
void handleUnsettleActiveThread();
return;
}
// Same as the sidebar Settle button: settle, then open the next thread.
if (settleAndAdvance(activeThreadRef)) return;

void settleThread(activeThreadRef).then((result) => {
if (result._tag !== "Failure" || isAtomCommandInterrupted(result)) return;
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ import { useAcknowledgeThreadWoke, useThreadActions } from "../hooks/useThreadAc
import { useHandleNewThread } from "../hooks/useHandleNewThread";
import { useTerminalFocus } from "../hooks/useTerminalFocus";
import { isCommandPaletteOpen, openCommandPalette } from "../commandPaletteBus";
import { registerSettleAndAdvance } from "../settleAndAdvance";
import { startNewThreadFromContext } from "../lib/chatThreadActions";
import { useClientSettings } from "../hooks/useSettings";
import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
Expand Down Expand Up @@ -3454,6 +3455,8 @@ export default function Sidebar() {
},
[planForwardNavigation, settleThread],
);
// The thread.settle shortcut parks the open thread through this same path.
useEffect(() => registerSettleAndAdvance(attemptSettle), [attemptSettle]);
// Post-settle navigation must skip threads settling in this same batch —
// they are all leaving the card block together. Rows that are already
// explicitly settled are skipped: nothing to do on a valid mixed selection.
Expand Down
37 changes: 37 additions & 0 deletions apps/web/src/settleAndAdvance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { scopeThreadRef } from "@t3tools/client-runtime/environment";
import { EnvironmentId, ThreadId, type ScopedThreadRef } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { registerSettleAndAdvance, settleAndAdvance } from "./settleAndAdvance";

const threadRef = scopeThreadRef(
EnvironmentId.make("environment-local"),
ThreadId.make("thread-1"),
);

describe("settleAndAdvance", () => {
it("hands the open thread to the mounted sidebar so it can move forward", () => {
const settled: ScopedThreadRef[] = [];
const unregister = registerSettleAndAdvance((ref) => settled.push(ref));

expect(settleAndAdvance(threadRef)).toBe(true);
expect(settled).toEqual([threadRef]);
unregister();
});

it("asks the caller to settle in place when no sidebar is mounted", () => {
expect(settleAndAdvance(threadRef)).toBe(false);
});

it("keeps a remounted sidebar's handler when the old one unmounts late", () => {
const settledBy: string[] = [];
const unregisterOld = registerSettleAndAdvance(() => settledBy.push("old"));
const unregisterNew = registerSettleAndAdvance(() => settledBy.push("new"));
unregisterOld();

expect(settleAndAdvance(threadRef)).toBe(true);
expect(settledBy).toEqual(["new"]);
unregisterNew();
expect(settleAndAdvance(threadRef)).toBe(false);
});
});
29 changes: 29 additions & 0 deletions apps/web/src/settleAndAdvance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ScopedThreadRef } from "@t3tools/contracts";

type SettleAndAdvance = (threadRef: ScopedThreadRef) => void;

// Only the mounted thread sidebar knows the card order that picks the next
// thread, so it registers its settle action here for other entry points.
let registeredSettleAndAdvance: SettleAndAdvance | null = null;

/**
* Called by the thread sidebar while it is mounted. Returns the unregister
* function, which leaves a newer registration in place.
*/
export function registerSettleAndAdvance(handler: SettleAndAdvance): () => void {
registeredSettleAndAdvance = handler;
return () => {
if (registeredSettleAndAdvance === handler) registeredSettleAndAdvance = null;
};
}

/**
* Settles the open thread the way the sidebar Settle button does: settle,
* then move to the next thread. Used by the `thread.settle` shortcut.
* Returns false when no sidebar is mounted, so the caller settles in place.
*/
export function settleAndAdvance(threadRef: ScopedThreadRef): boolean {
if (!registeredSettleAndAdvance) return false;
registeredSettleAndAdvance(threadRef);
return true;
}
Loading