Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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 @@ -201,6 +201,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
assert.equal(defaultsByCommand.get("themeEditor.toggle"), "mod+alt+shift+t");
assert.equal(defaultsByCommand.get("filePicker.toggle"), "mod+p");
assert.equal(defaultsByCommand.get("projectSearch.toggle"), "mod+shift+f");
assert.equal(defaultsByCommand.get("thread.settle"), "mod+e");
assert.equal(defaultsByCommand.get("sidebar.toggle"), "mod+b");
assert.equal(defaultsByCommand.get("rightPanel.toggle"), "mod+alt+b");
assert.isFalse(defaultsByCommand.has("rightPanel.toggleMaximized"));
Expand Down
25 changes: 25 additions & 0 deletions apps/web/src/keybindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ const DEFAULT_BINDINGS = compile([
{ shortcut: modShortcut("o", { shiftKey: true }), command: "chat.new" },
{ shortcut: modShortcut("n", { shiftKey: true }), command: "chat.newLocal" },
{ shortcut: modShortcut("o"), command: "editor.openFavorite" },
{
shortcut: modShortcut("e"),
command: "thread.settle",
whenAst: whenNot(whenIdentifier("terminalFocus")),
},
{ shortcut: modShortcut("[", { shiftKey: true }), command: "thread.previous" },
{ shortcut: modShortcut("]", { shiftKey: true }), command: "thread.next" },
{ shortcut: modShortcut("1"), command: "thread.jump.1" },
Expand Down Expand Up @@ -365,6 +370,10 @@ describe("shortcutLabelForCommand", () => {
shortcutLabelForCommand(DEFAULT_BINDINGS, "thread.previous", "Linux"),
"Ctrl+Shift+[",
);
assert.strictEqual(
shortcutLabelForCommand(DEFAULT_BINDINGS, "thread.settle", "MacIntel"),
"⌘E",
);
assert.strictEqual(
shortcutLabelForCommand(DEFAULT_BINDINGS, "modelPicker.jump.3", {
platform: "MacIntel",
Expand Down Expand Up @@ -480,6 +489,22 @@ describe("model picker navigation helpers", () => {
});

describe("chat/editor shortcuts", () => {
it("matches thread.settle outside terminal focus", () => {
assert.strictEqual(
resolveShortcutCommand(event({ key: "e", metaKey: true }), DEFAULT_BINDINGS, {
platform: "MacIntel",
context: { terminalFocus: false },
}),
"thread.settle",
);
assert.isNull(
resolveShortcutCommand(event({ key: "e", ctrlKey: true }), DEFAULT_BINDINGS, {
platform: "Linux",
context: { terminalFocus: true },
}),
);
});

it("matches chat.new shortcut", () => {
assert.isTrue(
isChatNewShortcut(event({ key: "o", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
Expand Down
40 changes: 39 additions & 1 deletion apps/web/src/routes/_chat.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
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 {
isAtomCommandInterrupted,
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import { useEffect, useMemo, useRef } from "react";

import { isCommandPaletteOpen } from "../commandPaletteBus";
import { useClientSettings, useLegacySidebarEnabled } from "../hooks/useSettings";
Expand All @@ -11,6 +16,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 +33,8 @@ function ChatRouteGlobalShortcuts() {
const selectedThreadKeysSize = useThreadSelectionStore((state) => state.selectedThreadKeys.size);
const { activeDraftThread, activeThread, defaultProjectRef, handleNewThread, routeThreadRef } =
useHandleNewThread();
const { settleThread } = useThreadActions();
const settlingThreadKeysRef = useRef(new Set<string>());
const keybindings = useAtomValue(primaryServerKeybindingsAtom);
const legacySidebarEnabled = useLegacySidebarEnabled();
const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
Expand Down Expand Up @@ -108,6 +116,35 @@ function ChatRouteGlobalShortcuts() {
return;
}

if (command === "thread.settle") {
if (routeThreadRef === null) return;
event.preventDefault();
event.stopPropagation();
if (event.repeat) return;
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

const threadKey = scopedThreadKey(routeThreadRef);
if (settlingThreadKeysRef.current.has(threadKey)) return;
settlingThreadKeysRef.current.add(threadKey);
void (async () => {
try {
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.",
}),
);
}
} finally {
settlingThreadKeysRef.current.delete(threadKey);
}
})();
return;
}

if (command === "preview.toggle") {
event.preventDefault();
event.stopPropagation();
Expand Down Expand Up @@ -167,6 +204,7 @@ function ChatRouteGlobalShortcuts() {
projectGroupCount,
routeThreadRef,
selectedThreadKeysSize,
settleThread,
legacySidebarEnabled,
terminalOpen,
]);
Expand Down
2 changes: 2 additions & 0 deletions docs/user/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Examples: `mod+j`, `mod+shift+d`, `ctrl+l`, `cmd+k`.
Commands are IDs like `terminal.toggle`, `commandPalette.toggle`, `preview.refresh`, and
`chat.new`. Project scripts are addressable as `script.{id}.run`, for example `script.test.run`.

`thread.settle` settles the currently open thread and defaults to `mod+e` outside terminal focus.

`filePicker.toggle` opens file search for the active project and defaults to `mod+p`.
`projectSearch.toggle` searches inside the active project's files and defaults to `mod+shift+f`.
Repeating either shortcut closes that search, and switching shortcuts replaces the open search.
Expand Down
6 changes: 6 additions & 0 deletions packages/contracts/src/keybindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ it.effect("parses keybinding rules", () =>
command: "thread.previous",
});
assert.strictEqual(parsedThreadPrevious.command, "thread.previous");

const parsedThreadSettle = yield* decode(KeybindingRule, {
key: "mod+e",
command: "thread.settle",
});
assert.strictEqual(parsedThreadSettle.command, "thread.settle");
}),
);

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 @@ -35,6 +35,7 @@ export type ModelPickerJumpKeybindingCommand =
(typeof MODEL_PICKER_JUMP_KEYBINDING_COMMANDS)[number];

export const THREAD_KEYBINDING_COMMANDS = [
"thread.settle",
"thread.previous",
"thread.next",
...THREAD_JUMP_KEYBINDING_COMMANDS,
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+e", command: "thread.settle", when: "!terminalFocus" },
{ key: "mod+shift+[", command: "thread.previous" },
{ key: "mod+shift+]", command: "thread.next" },
...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
Expand Down
Loading