Skip to content
Closed
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
1 change: 1 addition & 0 deletions apps/server/src/keybindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {

assert.equal(defaultsByCommand.get("thread.previous"), "mod+shift+[");
assert.equal(defaultsByCommand.get("thread.next"), "mod+shift+]");
assert.equal(defaultsByCommand.get("thread.settle"), "mod+shift+s");
assert.equal(defaultsByCommand.get("thread.jump.1"), "mod+1");
assert.equal(defaultsByCommand.get("thread.jump.9"), "mod+9");
assert.equal(defaultsByCommand.get("modelPicker.toggle"), "mod+shift+m");
Expand Down
29 changes: 29 additions & 0 deletions apps/web/src/keybindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ const DEFAULT_BINDINGS = compile([
{ shortcut: modShortcut("o"), command: "editor.openFavorite" },
{ shortcut: modShortcut("[", { shiftKey: true }), command: "thread.previous" },
{ shortcut: modShortcut("]", { shiftKey: true }), command: "thread.next" },
{
shortcut: modShortcut("s", { shiftKey: true }),
command: "thread.settle",
whenAst: whenNot(whenIdentifier("terminalFocus")),
},
{ shortcut: modShortcut("1"), command: "thread.jump.1" },
{ shortcut: modShortcut("2"), command: "thread.jump.2" },
{ shortcut: modShortcut("3"), command: "thread.jump.3" },
Expand Down Expand Up @@ -497,6 +502,30 @@ describe("chat/editor shortcuts", () => {
);
});

it("matches thread.settle shortcut outside terminal focus", () => {
assert.strictEqual(
resolveShortcutCommand(event({ key: "s", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
platform: "MacIntel",
context: { terminalFocus: false },
}),
"thread.settle",
);
assert.notStrictEqual(
resolveShortcutCommand(event({ key: "s", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
platform: "MacIntel",
context: { terminalFocus: true },
}),
"thread.settle",
);
assert.strictEqual(
resolveShortcutCommand(event({ key: "s", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
platform: "Linux",
context: { terminalFocus: false },
}),
"thread.settle",
);
});

it("matches commandPalette.toggle shortcut outside terminal focus", () => {
assert.strictEqual(
resolveShortcutCommand(event({ key: "k", metaKey: true }), DEFAULT_BINDINGS, {
Expand Down
29 changes: 29 additions & 0 deletions apps/web/src/routes/_chat.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
import { useAtomValue } from "@effect/atom-react";
import {
isAtomCommandInterrupted,
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import { useEffect, useMemo } from "react";

import { isCommandPaletteOpen } from "../commandPaletteBus";
Expand All @@ -11,6 +15,7 @@ import { selectProjectGroupingSettings } from "../logicalProject";
import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping";
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 @@ -27,6 +32,7 @@ function ChatRouteGlobalShortcuts() {
const selectedThreadKeysSize = useThreadSelectionStore((state) => state.selectedThreadKeys.size);
const { activeDraftThread, activeThread, defaultProjectRef, handleNewThread, routeThreadRef } =
useHandleNewThread();
const { settleThread } = useThreadActions();
const keybindings = useAtomValue(primaryServerKeybindingsAtom);
const sidebarV2Enabled = useSidebarV2Enabled();
const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
Expand Down Expand Up @@ -108,6 +114,28 @@ function ChatRouteGlobalShortcuts() {
return;
}

if (command === "thread.settle") {
event.preventDefault();
event.stopPropagation();
if (!routeThreadRef) return;
// Settle intentionally stays on the thread so the user can keep
// reading; only failures surface, matching the sidebar's behavior.
void (async () => {
const result = await settleThread(routeThreadRef);
if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
toastManager.add(
stackedThreadToast({
type: "error",
title: "Failed to settle thread",
description: error instanceof Error ? error.message : "An error occurred.",
}),
);
}
})();
return;
}

if (command === "preview.toggle") {
event.preventDefault();
event.stopPropagation();
Expand Down Expand Up @@ -167,6 +195,7 @@ function ChatRouteGlobalShortcuts() {
projectGroupCount,
routeThreadRef,
selectedThreadKeysSize,
settleThread,
sidebarV2Enabled,
terminalOpen,
]);
Expand Down
4 changes: 3 additions & 1 deletion docs/user/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ See the full schema for more details: [`packages/contracts/src/keybindings.ts`](
{ "key": "mod+n", "command": "chat.new", "when": "!terminalFocus" },
{ "key": "mod+shift+o", "command": "chat.new", "when": "!terminalFocus" },
{ "key": "mod+shift+n", "command": "chat.newLocal", "when": "!terminalFocus" },
{ "key": "mod+o", "command": "editor.openFavorite" }
{ "key": "mod+o", "command": "editor.openFavorite" },
{ "key": "mod+shift+s", "command": "thread.settle", "when": "!terminalFocus" }
]
```

Expand Down Expand Up @@ -67,6 +68,7 @@ Invalid rules are ignored. Invalid config files are ignored. Warnings are logged
- `chat.new`: create a new chat thread preserving the active thread's branch/worktree state
- `chat.newLocal`: create a new chat thread for the active project in a new environment (local/worktree determined by app settings (default `local`))
- `editor.openFavorite`: open current project/worktree in the last-used editor
- `thread.settle`: settle the active thread (blocked while the thread still needs attention)
- `script.{id}.run`: run a project script by id (for example `script.test.run`)

### Key Syntax
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 @@ -37,6 +37,7 @@ export type ModelPickerJumpKeybindingCommand =
export const THREAD_KEYBINDING_COMMANDS = [
"thread.previous",
"thread.next",
"thread.settle",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep new keybinding commands backward-compatible

When a newer server starts, it backfills thread.settle into its persisted defaults and includes that rule in ServerConfig.keybindings; an older desktop/mobile/web client still decodes this field with the previous closed KeybindingCommand literal union, so the unknown command causes initial config decoding to fail and prevents that client from connecting. This affects common remote version-skew scenarios, so the wire decoder needs to tolerate/filter commands unknown to the client, or the server must avoid sending them to older clients.

AGENTS.md reference: AGENTS.md:L19-L21

Useful? React with 👍 / 👎.

...THREAD_JUMP_KEYBINDING_COMMANDS,
] as const;
export type ThreadKeybindingCommand = (typeof THREAD_KEYBINDING_COMMANDS)[number];
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 @@ -43,6 +43,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
{ key: "mod+o", command: "editor.openFavorite" },
{ key: "mod+shift+[", command: "thread.previous" },
{ key: "mod+shift+]", command: "thread.next" },
{ key: "mod+shift+s", command: "thread.settle", when: "!terminalFocus" },
...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
key: `mod+${index + 1}`,
command,
Expand Down
Loading