Skip to content

Commit 713d184

Browse files
committed
fix(web): align command palette search and shortcut
1 parent 17073d5 commit 713d184

6 files changed

Lines changed: 110 additions & 17 deletions

File tree

KEYBINDINGS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ See the full schema for more details: [`packages/contracts/src/keybindings.ts`](
2323
{ "key": "mod+d", "command": "terminal.split", "when": "terminalFocus" },
2424
{ "key": "mod+n", "command": "terminal.new", "when": "terminalFocus" },
2525
{ "key": "mod+w", "command": "terminal.close", "when": "terminalFocus" },
26-
{ "key": "mod+k", "command": "commandPalette.toggle", "when": "!terminalFocus" },
26+
{ "key": "mod+shift+p", "command": "commandPalette.toggle", "when": "!terminalFocus" },
2727
{ "key": "mod+n", "command": "chat.new", "when": "!terminalFocus" },
2828
{ "key": "mod+shift+o", "command": "chat.new", "when": "!terminalFocus" },
2929
{ "key": "mod+shift+n", "command": "chat.newLocal", "when": "!terminalFocus" },

apps/server/src/keybindings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
7070
{ key: "mod+n", command: "terminal.new", when: "terminalFocus" },
7171
{ key: "mod+w", command: "terminal.close", when: "terminalFocus" },
7272
{ key: "mod+d", command: "diff.toggle", when: "!terminalFocus" },
73-
{ key: "mod+k", command: "commandPalette.toggle", when: "!terminalFocus" },
73+
{ key: "mod+shift+p", command: "commandPalette.toggle", when: "!terminalFocus" },
7474
{ key: "mod+n", command: "chat.new", when: "!terminalFocus" },
7575
{ key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" },
7676
{ key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" },

apps/web/src/components/ChatView.browser.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,58 @@ describe("ChatView timeline estimator parity (full app)", () => {
11981198
}
11991199
});
12001200

1201+
it("filters command palette results as the user types", async () => {
1202+
const mounted = await mountChatView({
1203+
viewport: DEFAULT_VIEWPORT,
1204+
snapshot: createSnapshotForTargetUser({
1205+
targetMessageId: "msg-user-command-palette-search-test" as MessageId,
1206+
targetText: "command palette search test",
1207+
}),
1208+
configureFixture: (nextFixture) => {
1209+
nextFixture.serverConfig = {
1210+
...nextFixture.serverConfig,
1211+
keybindings: [
1212+
{
1213+
command: "commandPalette.toggle",
1214+
shortcut: {
1215+
key: "k",
1216+
metaKey: false,
1217+
ctrlKey: false,
1218+
shiftKey: false,
1219+
altKey: false,
1220+
modKey: true,
1221+
},
1222+
whenAst: {
1223+
type: "not",
1224+
node: { type: "identifier", name: "terminalFocus" },
1225+
},
1226+
},
1227+
],
1228+
};
1229+
},
1230+
});
1231+
1232+
try {
1233+
const useMetaForMod = isMacPlatform(navigator.platform);
1234+
window.dispatchEvent(
1235+
new KeyboardEvent("keydown", {
1236+
key: "k",
1237+
metaKey: useMetaForMod,
1238+
ctrlKey: !useMetaForMod,
1239+
bubbles: true,
1240+
cancelable: true,
1241+
}),
1242+
);
1243+
1244+
await expect.element(page.getByTestId("command-palette")).toBeInTheDocument();
1245+
await page.getByPlaceholder("Search commands and threads...").fill("settings");
1246+
await expect.element(page.getByText("Open settings")).toBeInTheDocument();
1247+
await expect.element(page.getByText("New thread")).not.toBeInTheDocument();
1248+
} finally {
1249+
await mounted.cleanup();
1250+
}
1251+
});
1252+
12011253
it("creates a fresh draft after the previous draft thread is promoted", async () => {
12021254
const mounted = await mountChatView({
12031255
viewport: DEFAULT_VIEWPORT,

apps/web/src/components/CommandPalette.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ import { type KeybindingCommand } from "@t3tools/contracts";
44
import { useQuery } from "@tanstack/react-query";
55
import { useNavigate } from "@tanstack/react-router";
66
import { MessageSquareIcon, SettingsIcon, SquarePenIcon } from "lucide-react";
7-
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react";
7+
import {
8+
createContext,
9+
useCallback,
10+
useContext,
11+
useDeferredValue,
12+
useMemo,
13+
useState,
14+
type ReactNode,
15+
} from "react";
816
import { useAppSettings } from "../appSettings";
917
import { useHandleNewThread } from "../hooks/useHandleNewThread";
1018
import {
@@ -63,6 +71,10 @@ function iconClassName() {
6371
return "size-4 text-muted-foreground/80";
6472
}
6573

74+
function normalizeSearchText(value: string): string {
75+
return value.trim().toLowerCase().replace(/\s+/g, " ");
76+
}
77+
6678
export function useCommandPalette() {
6779
const context = useContext(CommandPaletteContext);
6880
if (!context) {
@@ -108,6 +120,8 @@ function CommandPaletteDialog() {
108120
function OpenCommandPaletteDialog() {
109121
const navigate = useNavigate();
110122
const { setOpen } = useCommandPalette();
123+
const [query, setQuery] = useState("");
124+
const deferredQuery = useDeferredValue(query);
111125
const { settings } = useAppSettings();
112126
const { activeDraftThread, activeThread, handleNewThread, projects } = useHandleNewThread();
113127
const threads = useStore((store) => store.threads);
@@ -118,7 +132,7 @@ function OpenCommandPaletteDialog() {
118132
[projects],
119133
);
120134

121-
const groups = useMemo<CommandPaletteGroup[]>(() => {
135+
const allGroups = useMemo<CommandPaletteGroup[]>(() => {
122136
const actionItems: CommandPaletteItem[] = [];
123137
if (projects.length > 0) {
124138
const activeProjectTitle =
@@ -248,6 +262,25 @@ function OpenCommandPaletteDialog() {
248262
threads,
249263
]);
250264

265+
const filteredGroups = useMemo(() => {
266+
const normalizedQuery = normalizeSearchText(deferredQuery);
267+
if (normalizedQuery.length === 0) {
268+
return allGroups;
269+
}
270+
271+
return allGroups
272+
.map((group) => ({
273+
...group,
274+
items: group.items.filter((item) => {
275+
const haystack = normalizeSearchText(
276+
[item.label, item.title, item.description ?? ""].join(" "),
277+
);
278+
return haystack.includes(normalizedQuery);
279+
}),
280+
}))
281+
.filter((group) => group.items.length > 0);
282+
}, [allGroups, deferredQuery]);
283+
251284
const executeItem = useCallback(
252285
(item: CommandPaletteItem) => {
253286
setOpen(false);
@@ -268,11 +301,11 @@ function OpenCommandPaletteDialog() {
268301
className="overflow-hidden p-0"
269302
data-testid="command-palette"
270303
>
271-
<Command aria-label="Command palette" items={groups}>
304+
<Command aria-label="Command palette" mode="none" onValueChange={setQuery} value={query}>
272305
<CommandInput placeholder="Search commands and threads..." />
273306
<CommandPanel className="max-h-[min(28rem,70vh)]">
274307
<CommandList>
275-
{groups.map((group) => (
308+
{filteredGroups.map((group) => (
276309
<CommandGroup items={group.items} key={group.value}>
277310
<CommandGroupLabel>{group.label}</CommandGroupLabel>
278311
<CommandCollection>

apps/web/src/keybindings.test.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ const DEFAULT_BINDINGS = compile([
9999
whenAst: whenNot(whenIdentifier("terminalFocus")),
100100
},
101101
{
102-
shortcut: modShortcut("k"),
102+
shortcut: modShortcut("p", { shiftKey: true }),
103103
command: "commandPalette.toggle",
104104
whenAst: whenNot(whenIdentifier("terminalFocus")),
105105
},
@@ -245,7 +245,7 @@ describe("shortcutLabelForCommand", () => {
245245
assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "diff.toggle", "Linux"), "Ctrl+D");
246246
assert.strictEqual(
247247
shortcutLabelForCommand(DEFAULT_BINDINGS, "commandPalette.toggle", "MacIntel"),
248-
"⌘K",
248+
"⇧⌘P",
249249
);
250250
assert.strictEqual(
251251
shortcutLabelForCommand(DEFAULT_BINDINGS, "editor.openFavorite", "Linux"),
@@ -296,16 +296,24 @@ describe("chat/editor shortcuts", () => {
296296

297297
it("matches commandPalette.toggle shortcut outside terminal focus", () => {
298298
assert.isTrue(
299-
isCommandPaletteToggleShortcut(event({ key: "k", metaKey: true }), DEFAULT_BINDINGS, {
300-
platform: "MacIntel",
301-
context: { terminalFocus: false },
302-
}),
299+
isCommandPaletteToggleShortcut(
300+
event({ key: "p", metaKey: true, shiftKey: true }),
301+
DEFAULT_BINDINGS,
302+
{
303+
platform: "MacIntel",
304+
context: { terminalFocus: false },
305+
},
306+
),
303307
);
304308
assert.isFalse(
305-
isCommandPaletteToggleShortcut(event({ key: "k", metaKey: true }), DEFAULT_BINDINGS, {
306-
platform: "MacIntel",
307-
context: { terminalFocus: true },
308-
}),
309+
isCommandPaletteToggleShortcut(
310+
event({ key: "p", metaKey: true, shiftKey: true }),
311+
DEFAULT_BINDINGS,
312+
{
313+
platform: "MacIntel",
314+
context: { terminalFocus: true },
315+
},
316+
),
309317
);
310318
});
311319

packages/contracts/src/keybindings.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ it.effect("parses keybinding rules", () =>
4242
assert.strictEqual(parsedDiffToggle.command, "diff.toggle");
4343

4444
const parsedCommandPalette = yield* decode(KeybindingRule, {
45-
key: "mod+k",
45+
key: "mod+shift+p",
4646
command: "commandPalette.toggle",
4747
});
4848
assert.strictEqual(parsedCommandPalette.command, "commandPalette.toggle");

0 commit comments

Comments
 (0)