* feat(command-center): add workspace file search Use the existing workspace-aware fuzzy search so file lookup follows the file explorer's ignore and hidden-directory rules. Cmd/Ctrl+P opens a removable Files scope while the normal command center also includes file matches. * fix(command-center): stabilize file search results * fix(shortcuts): preserve project picker command * fix(command-center): keep file results while searching * feat(command-center): integrate the files scope into the search row The scope was a 24px pill with 12px text sitting inside the 16px input line, so it read as a badge dropped into the query rather than the scope of the search. Make it a leading cell of the search row instead: folder icon, input-sized label, and a hairline divider before the caret. It stretches to the row's cross size and bleeds through the header padding with negative margins, so its height is driven entirely by the input and the panel's own radius clips its corner. Toggling the scope cannot resize the header, which the new spec pins. * fix(command-center): restore the files scope as a chip The previous commit turned the scope into a flush leading cell of the header. Put the chip back: rounded, bordered, folder icon and 14px label, no divider. It stretches to the row instead of carrying a height, so it is exactly as tall as the input's line box and cannot resize the header. * refactor(shortcuts): regroup and reorder the shortcut help sections Panels was a junk drawer: the command center, file search, the shortcuts sheet itself, new terminal, the sidebars, settings, focus mode and theme cycling. The two shortcuts people reach for most, Cmd+K and Cmd+P, sat inside it below three other sections. Navigation held both workspace and tab movement. Sections are now General, Projects & Workspaces, Tabs & Panes, Layout, and Agent Input. General leads with the command center and file search. Tab movement joins Tabs & Panes, new terminal joins the tabs it creates, and Layout keeps only the sidebar and focus toggles. Row order is now data rather than a side effect of binding-definition order, so the row someone opens the sheet to find sits at the top of its section. A test fails when a new shortcut has no place in that order instead of letting it sink to the bottom unnoticed. * Keep file search failures in view
414 lines
14 KiB
TypeScript
414 lines
14 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
routeKeyboardShortcut,
|
|
type ShortcutAction,
|
|
type ShortcutRoutingContext,
|
|
} from "./route-shortcut";
|
|
|
|
const SIDEBAR_TARGETS = [
|
|
{ serverId: "srv", workspaceId: "ws-1" },
|
|
{ serverId: "srv", workspaceId: "ws-2" },
|
|
{ serverId: "srv", workspaceId: "ws-3" },
|
|
{ serverId: "srv", workspaceId: "ws-4" },
|
|
{ serverId: "srv", workspaceId: "ws-5" },
|
|
] as const;
|
|
|
|
function makeCtx(overrides: Partial<ShortcutRoutingContext> = {}): ShortcutRoutingContext {
|
|
return {
|
|
pathname: "/h/srv/workspace/ws-2",
|
|
isMobile: false,
|
|
sidebarShortcutTargets: SIDEBAR_TARGETS,
|
|
navigationActiveWorkspace: null,
|
|
commandCenterOpen: false,
|
|
shortcutsDialogOpen: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("routeKeyboardShortcut — dispatch passthroughs", () => {
|
|
it.each([
|
|
["agent.interrupt", { id: "agent.interrupt", scope: "global" }],
|
|
["workspace.tab.new", { id: "workspace.tab.new", scope: "workspace" }],
|
|
["workspace.new", { id: "workspace.new", scope: "sidebar" }],
|
|
["workspace.project.pick", { id: "workspace.project.pick", scope: "workspace" }],
|
|
["workspace.archive", { id: "workspace.archive", scope: "sidebar" }],
|
|
["workspace.pin", { id: "workspace.pin", scope: "sidebar" }],
|
|
["worktree.new", { id: "worktree.new", scope: "sidebar" }],
|
|
["workspace.terminal.new", { id: "workspace.terminal.new", scope: "workspace" }],
|
|
["workspace.tab.close.current", { id: "workspace.tab.close-current", scope: "workspace" }],
|
|
["sidebar.toggle.right", { id: "sidebar.toggle.right", scope: "sidebar" }],
|
|
["workspace.pane.split.right", { id: "workspace.pane.split.right", scope: "workspace" }],
|
|
["workspace.pane.split.down", { id: "workspace.pane.split.down", scope: "workspace" }],
|
|
["workspace.pane.focus.left", { id: "workspace.pane.focus.left", scope: "workspace" }],
|
|
["workspace.pane.focus.right", { id: "workspace.pane.focus.right", scope: "workspace" }],
|
|
["workspace.pane.focus.up", { id: "workspace.pane.focus.up", scope: "workspace" }],
|
|
["workspace.pane.focus.down", { id: "workspace.pane.focus.down", scope: "workspace" }],
|
|
["workspace.pane.move-tab.left", { id: "workspace.pane.move-tab.left", scope: "workspace" }],
|
|
["workspace.pane.move-tab.right", { id: "workspace.pane.move-tab.right", scope: "workspace" }],
|
|
["workspace.pane.move-tab.up", { id: "workspace.pane.move-tab.up", scope: "workspace" }],
|
|
["workspace.pane.move-tab.down", { id: "workspace.pane.move-tab.down", scope: "workspace" }],
|
|
["workspace.pane.close", { id: "workspace.pane.close", scope: "workspace" }],
|
|
["view.toggle.focus", { id: "workspace.focus.toggle", scope: "workspace" }],
|
|
])("%s → dispatch %j", (action, expected) => {
|
|
expect(routeKeyboardShortcut({ action, payload: null }, makeCtx())).toEqual({
|
|
kind: "dispatch",
|
|
action: expected,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("routeKeyboardShortcut — workspace.tab.navigate", () => {
|
|
it("forwards index payloads to the workspace.tab.navigate-index dispatch", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.tab.navigate.index", payload: { index: 3 } },
|
|
makeCtx(),
|
|
),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "dispatch",
|
|
action: { id: "workspace.tab.navigate-index", scope: "workspace", index: 3 },
|
|
});
|
|
});
|
|
|
|
it("returns none when index payload is missing", () => {
|
|
expect(
|
|
routeKeyboardShortcut({ action: "workspace.tab.navigate.index", payload: null }, makeCtx()),
|
|
).toEqual<ShortcutAction>({ kind: "none" });
|
|
});
|
|
|
|
it("forwards delta payloads to the workspace.tab.navigate-relative dispatch", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.tab.navigate.relative", payload: { delta: -1 } },
|
|
makeCtx(),
|
|
),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "dispatch",
|
|
action: { id: "workspace.tab.navigate-relative", scope: "workspace", delta: -1 },
|
|
});
|
|
});
|
|
|
|
it("returns none when delta payload is missing", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.tab.navigate.relative", payload: null },
|
|
makeCtx(),
|
|
),
|
|
).toEqual<ShortcutAction>({ kind: "none" });
|
|
});
|
|
});
|
|
|
|
describe("routeKeyboardShortcut — workspace.navigate.index", () => {
|
|
it("navigates to the sidebar target at index-1", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.navigate.index", payload: { index: 4 } },
|
|
makeCtx(),
|
|
),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "navigate-workspace",
|
|
serverId: "srv",
|
|
workspaceId: "ws-4",
|
|
});
|
|
});
|
|
|
|
it("returns none when the target index is out of range", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.navigate.index", payload: { index: 99 } },
|
|
makeCtx(),
|
|
),
|
|
).toEqual<ShortcutAction>({ kind: "none" });
|
|
});
|
|
|
|
it("returns none when the index payload is missing", () => {
|
|
expect(
|
|
routeKeyboardShortcut({ action: "workspace.navigate.index", payload: null }, makeCtx()),
|
|
).toEqual<ShortcutAction>({ kind: "none" });
|
|
});
|
|
|
|
it("returns none when there are no sidebar targets", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.navigate.index", payload: { index: 1 } },
|
|
makeCtx({ sidebarShortcutTargets: [] }),
|
|
),
|
|
).toEqual<ShortcutAction>({ kind: "none" });
|
|
});
|
|
});
|
|
|
|
describe("routeKeyboardShortcut — workspace.navigate.relative", () => {
|
|
const STATUS_VISUAL_TARGETS = [
|
|
{ serverId: "srv", workspaceId: "needs-input" },
|
|
{ serverId: "srv", workspaceId: "running-new" },
|
|
{ serverId: "srv", workspaceId: "running-old" },
|
|
{ serverId: "srv", workspaceId: "done" },
|
|
] as const;
|
|
|
|
it("uses the retained navigation workspace selection over a stale pathname", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.navigate.relative", payload: { delta: 1 } },
|
|
makeCtx({
|
|
pathname: "/h/srv/workspace/ws-2",
|
|
navigationActiveWorkspace: { serverId: "srv", workspaceId: "ws-4" },
|
|
}),
|
|
),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "navigate-workspace",
|
|
serverId: "srv",
|
|
workspaceId: "ws-5",
|
|
});
|
|
});
|
|
|
|
it("moves from status row 2 to row 3 using status visual target order", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.navigate.relative", payload: { delta: 1 } },
|
|
makeCtx({
|
|
pathname: "/h/srv/workspace/ui-expose-archive-worktrees-on-merge",
|
|
sidebarShortcutTargets: STATUS_VISUAL_TARGETS,
|
|
navigationActiveWorkspace: { serverId: "srv", workspaceId: "running-new" },
|
|
}),
|
|
),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "navigate-workspace",
|
|
serverId: "srv",
|
|
workspaceId: "running-old",
|
|
});
|
|
});
|
|
|
|
it("moves backward from status row 2 to row 1 using status visual target order", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.navigate.relative", payload: { delta: -1 } },
|
|
makeCtx({
|
|
pathname: "/h/srv/workspace/ui-expose-archive-worktrees-on-merge",
|
|
sidebarShortcutTargets: STATUS_VISUAL_TARGETS,
|
|
navigationActiveWorkspace: { serverId: "srv", workspaceId: "running-new" },
|
|
}),
|
|
),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "navigate-workspace",
|
|
serverId: "srv",
|
|
workspaceId: "needs-input",
|
|
});
|
|
});
|
|
|
|
it("moves backward from status row 3 to row 2 using status visual target order", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.navigate.relative", payload: { delta: -1 } },
|
|
makeCtx({
|
|
pathname: "/h/srv/workspace/running-old",
|
|
sidebarShortcutTargets: STATUS_VISUAL_TARGETS,
|
|
navigationActiveWorkspace: { serverId: "srv", workspaceId: "running-old" },
|
|
}),
|
|
),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "navigate-workspace",
|
|
serverId: "srv",
|
|
workspaceId: "running-new",
|
|
});
|
|
});
|
|
|
|
it("falls back to the pathname workspace when no retained selection exists", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.navigate.relative", payload: { delta: -1 } },
|
|
makeCtx({ pathname: "/h/srv/workspace/ws-3", navigationActiveWorkspace: null }),
|
|
),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "navigate-workspace",
|
|
serverId: "srv",
|
|
workspaceId: "ws-2",
|
|
});
|
|
});
|
|
|
|
it("wraps from the last target forward to the first", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.navigate.relative", payload: { delta: 1 } },
|
|
makeCtx({ navigationActiveWorkspace: { serverId: "srv", workspaceId: "ws-5" } }),
|
|
),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "navigate-workspace",
|
|
serverId: "srv",
|
|
workspaceId: "ws-1",
|
|
});
|
|
});
|
|
|
|
it("returns none when there are no sidebar targets", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.navigate.relative", payload: { delta: 1 } },
|
|
makeCtx({ sidebarShortcutTargets: [] }),
|
|
),
|
|
).toEqual<ShortcutAction>({ kind: "none" });
|
|
});
|
|
|
|
it("returns none when the delta payload is missing", () => {
|
|
expect(
|
|
routeKeyboardShortcut({ action: "workspace.navigate.relative", payload: null }, makeCtx()),
|
|
).toEqual<ShortcutAction>({ kind: "none" });
|
|
});
|
|
|
|
it("falls back to the first target when the current workspace is not in the sidebar", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "workspace.navigate.relative", payload: { delta: 1 } },
|
|
makeCtx({
|
|
pathname: "/settings/general",
|
|
navigationActiveWorkspace: { serverId: "other", workspaceId: "ws-x" },
|
|
}),
|
|
),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "navigate-workspace",
|
|
serverId: "srv",
|
|
workspaceId: "ws-1",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("routeKeyboardShortcut — message-input.action", () => {
|
|
it.each([
|
|
["focus", "message-input.focus"],
|
|
["send", "message-input.send"],
|
|
["dictation-toggle", "message-input.dictation-toggle"],
|
|
["dictation-cancel", "message-input.dictation-cancel"],
|
|
["dictation-confirm", "message-input.dictation-confirm"],
|
|
["voice-toggle", "message-input.voice-toggle"],
|
|
["voice-mute-toggle", "message-input.voice-mute-toggle"],
|
|
["mode-cycle", "message-input.mode-cycle"],
|
|
] as const)("kind=%s → dispatch %s", (kind, id) => {
|
|
expect(
|
|
routeKeyboardShortcut({ action: "message-input.action", payload: { kind } }, makeCtx()),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "dispatch",
|
|
action: { id, scope: "message-input" },
|
|
});
|
|
});
|
|
|
|
it("returns none when kind is missing", () => {
|
|
expect(
|
|
routeKeyboardShortcut({ action: "message-input.action", payload: null }, makeCtx()),
|
|
).toEqual<ShortcutAction>({ kind: "none" });
|
|
});
|
|
});
|
|
|
|
describe("routeKeyboardShortcut — settings.toggle", () => {
|
|
it("pushes to the settings root when not currently in settings", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "settings.toggle", payload: null },
|
|
makeCtx({ pathname: "/h/srv/workspace/ws-2" }),
|
|
),
|
|
).toEqual<ShortcutAction>({ kind: "router-push", route: "/settings" });
|
|
});
|
|
|
|
it("navigates to the last workspace when leaving settings on desktop", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "settings.toggle", payload: null },
|
|
makeCtx({
|
|
pathname: "/settings/general",
|
|
isMobile: false,
|
|
}),
|
|
),
|
|
).toEqual<ShortcutAction>({ kind: "navigate-last-workspace" });
|
|
});
|
|
|
|
it("falls back to router.back() on mobile", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "settings.toggle", payload: null },
|
|
makeCtx({
|
|
pathname: "/settings/general",
|
|
isMobile: true,
|
|
}),
|
|
),
|
|
).toEqual<ShortcutAction>({ kind: "router-back" });
|
|
});
|
|
});
|
|
|
|
describe("routeKeyboardShortcut — callbacks and pickers", () => {
|
|
it.each([
|
|
["sidebar.toggle.left", "toggle-agent-list"],
|
|
["sidebar.toggle.both", "toggle-both-sidebars"],
|
|
["theme.cycle", "cycle-theme"],
|
|
] as const)("%s → callback %s", (action, name) => {
|
|
expect(routeKeyboardShortcut({ action, payload: null }, makeCtx())).toEqual<ShortcutAction>({
|
|
kind: "callback",
|
|
name,
|
|
});
|
|
});
|
|
|
|
it("agent.new → open-project-picker", () => {
|
|
expect(
|
|
routeKeyboardShortcut({ action: "agent.new", payload: null }, makeCtx()),
|
|
).toEqual<ShortcutAction>({ kind: "open-project-picker" });
|
|
});
|
|
});
|
|
|
|
describe("routeKeyboardShortcut — toggle dialogs", () => {
|
|
it("opens the command center scoped to files from a workspace", () => {
|
|
expect(
|
|
routeKeyboardShortcut({ action: "command-center.files", payload: null }, makeCtx()),
|
|
).toEqual<ShortcutAction>({ kind: "command-center-toggle", nextOpen: true, scope: "files" });
|
|
});
|
|
|
|
it("leaves the file-search shortcut to the project-picker host outside a workspace", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "command-center.files", payload: null },
|
|
makeCtx({ pathname: "/settings" }),
|
|
),
|
|
).toEqual<ShortcutAction>({
|
|
kind: "dispatch",
|
|
action: { id: "workspace.project.pick", scope: "workspace" },
|
|
});
|
|
});
|
|
|
|
it("opens the command center when closed", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "command-center.toggle", payload: null },
|
|
makeCtx({ commandCenterOpen: false }),
|
|
),
|
|
).toEqual<ShortcutAction>({ kind: "command-center-toggle", nextOpen: true });
|
|
});
|
|
|
|
it("closes the command center when open", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "command-center.toggle", payload: null },
|
|
makeCtx({ commandCenterOpen: true }),
|
|
),
|
|
).toEqual<ShortcutAction>({ kind: "command-center-toggle", nextOpen: false });
|
|
});
|
|
|
|
it("toggles the shortcuts dialog", () => {
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "shortcuts.dialog.toggle", payload: null },
|
|
makeCtx({ shortcutsDialogOpen: false }),
|
|
),
|
|
).toEqual<ShortcutAction>({ kind: "shortcuts-dialog-toggle", nextOpen: true });
|
|
|
|
expect(
|
|
routeKeyboardShortcut(
|
|
{ action: "shortcuts.dialog.toggle", payload: null },
|
|
makeCtx({ shortcutsDialogOpen: true }),
|
|
),
|
|
).toEqual<ShortcutAction>({ kind: "shortcuts-dialog-toggle", nextOpen: false });
|
|
});
|
|
});
|
|
|
|
describe("routeKeyboardShortcut — unknown actions", () => {
|
|
it("returns none for unknown action ids", () => {
|
|
expect(
|
|
routeKeyboardShortcut({ action: "totally.made.up", payload: null }, makeCtx()),
|
|
).toEqual<ShortcutAction>({ kind: "none" });
|
|
});
|
|
});
|