Skip to content
Open
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
110 changes: 108 additions & 2 deletions apps/web/src/routes/_chat.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
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, readProject, useThreadShell } from "../state/entities";
import { usePrimaryEnvironmentId } from "../state/environments";
import { selectProjectGroupingSettings } from "../logicalProject";
import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping";
import {
resolveDisplayedThreadPr,
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";
Expand All @@ -19,14 +30,17 @@ 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 { 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);
Expand Down Expand Up @@ -55,6 +69,30 @@ 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);
// 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;
Expand Down Expand Up @@ -149,6 +187,66 @@ function ChatRouteGlobalShortcuts() {
? "zoom-out"
: "reset-zoom";
dispatchPreviewAction(action);
return;
}

if (command === "thread.settle.toggle") {
event.preventDefault();
event.stopPropagation();
Comment thread
cursor[bot] marked this conversation as resolved.
if (event.repeat) return;
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);
// 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 &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium routes/_chat.tsx:216

While VCS status is pending, a snapshot from a previous branch is treated as the current PR whenever activeThreadShell.worktreePath === null, so effectiveSettled can choose the wrong settle/un-settle direction. Apply the same branch-mismatch rule as resolveDisplayedThreadPr—only matching-branch snapshots, or terminal snapshots explicitly retained across a mismatch, should be passed as changeRequest.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/routes/_chat.tsx around line 216:

While VCS status is pending, a snapshot from a previous branch is treated as the current PR whenever `activeThreadShell.worktreePath === null`, so `effectiveSettled` can choose the wrong settle/un-settle direction. Apply the same branch-mismatch rule as `resolveDisplayedThreadPr`—only matching-branch snapshots, or terminal snapshots explicitly retained across a mismatch, should be passed as `changeRequest`.

(activeThreadShell.worktreePath === null ||
snapshot.branch === activeThreadShell.branch)
? snapshot.pr
: null;
const changeRequest =
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.
const isSettled = effectiveSettled(activeThreadShell, {
now: `${new Date().toISOString().slice(0, 16)}:00.000Z`,
autoSettleAfterDays,
autoSettleOnMerge,
changeRequest,
});
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
void (async () => {
const result = isSettled
? await unsettleThread(routeThreadRef)
: await settleThread(routeThreadRef);
Comment thread
cursor[bot] marked this conversation as resolved.
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;
}
};

Expand All @@ -159,16 +257,24 @@ function ChatRouteGlobalShortcuts() {
}, [
activeDraftThread,
activeThread,
activeThreadShell,
autoSettleAfterDays,
autoSettleOnMerge,
changeRequestSnapshotByKey,
clearSelection,
handleNewThread,
gitStatusQuery.data,
keybindings,
defaultProjectRef,
previewOpen,
projectGroupCount,
routeThreadRef,
selectedThreadKeysSize,
legacySidebarEnabled,
serverConfigs,
settleThread,
terminalOpen,
unsettleThread,
]);

return null;
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
{ 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) => ({
Expand Down
Loading