Skip to content
59 changes: 59 additions & 0 deletions apps/web/src/components/CommandPalette.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,32 @@ import { CommandPalette } from "./CommandPalette";
const entities = vi.hoisted(() => ({
projects: [] as ReadonlyArray<Project>,
threads: [] as ReadonlyArray<Thread>,
handleNewThread: vi.fn(async () => undefined),
}));
vi.mock("../state/server", async (original) => {
const actual = await original<typeof import("../state/server")>();
const { Atom } = await import("effect/reactivity");
const { compileResolvedKeybindingsConfig } = await import("@t3tools/shared/keybindings");
const { THREAD_JUMP_KEYBINDING_COMMANDS } = await import("@t3tools/contracts");
// The default jump bindings are desktop-only; these hold in the browser too.
const jumpBindings = compileResolvedKeybindingsConfig(
THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
key: `mod+${index + 1}`,
command,
})),
);
return { ...actual, primaryServerKeybindingsAtom: Atom.make(jumpBindings) };
});
vi.mock("../hooks/useHandleNewThread", async (original) => {
const actual = await original<typeof import("../hooks/useHandleNewThread")>();
return {
...actual,
useHandleNewThread: () => ({
...actual.useHandleNewThread(),
handleNewThread: entities.handleNewThread,
}),
};
});
vi.mock("../state/entities", async (original) => ({
...(await original<typeof import("../state/entities")>()),
useProjects: () => entities.projects,
Expand All @@ -23,6 +48,21 @@ function setup(state: { projects?: ReadonlyArray<Project>; threads?: ReadonlyArr
useCommandPaletteStore.setState(useCommandPaletteStore.getInitialState(), true);
entities.projects = state.projects ?? [];
entities.threads = state.threads ?? [];
entities.handleNewThread.mockClear();
}

function makeProject(title: string): Project {
return {
id: ProjectId.make(title),
environmentId: EnvironmentId.make("local"),
title,
workspaceRoot: `/work/${title}`,
repositoryIdentity: null,
defaultModelSelection: null,
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-01T00:00:00.000Z",
scripts: [],
};
}

describe("CommandPalette open intents", () => {
Expand Down Expand Up @@ -174,4 +214,23 @@ describe("CommandPalette open intents", () => {

await vi.waitFor(() => expect(screen.router?.state.location.pathname).toBe("/local/parent"));
});

it("numbers filtered New thread in... rows from 1 and jumps to them", async () => {
using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64");
setup({ projects: [makeProject("server"), makeProject("lima")] });
await using _screen = await render(<CommandPalette>{null}</CommandPalette>);

openCommandPalette({ open: "new-thread-in" });
await userEvent.fill(page.getByPlaceholder("Search..."), "li");
await expect.element(page.getByRole("option", { name: /server/ })).not.toBeInTheDocument();
await expect.element(page.getByRole("option", { name: /lima/ })).toMatchTextContent("Ctrl+1");
await userEvent.keyboard("{Control>}1{/Control}");

await vi.waitFor(() =>
expect(entities.handleNewThread).toHaveBeenCalledWith({
environmentId: EnvironmentId.make("local"),
projectId: ProjectId.make("lima"),
}),
);
});
});
19 changes: 17 additions & 2 deletions apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@ function OpenCommandPaletteDialog(props: {
rowOrderKey("new-thread-in"),
true,
);
// Jump shortcuts follow the rows as shown, so a pin cannot move a number to another row.
// Root search suggestions copy these numbers; the picker renumbers its shown rows below.
const projectJumpItems = new Map(
enumerateCommandPaletteItems(
openingOrderThreadItems.filter((item) => item.value !== NEW_THREAD_NO_PROJECT_VALUE),
Expand Down Expand Up @@ -1780,7 +1780,7 @@ function OpenCommandPaletteDialog(props: {

const isLinkedThreadSearch =
linkedThreadSearch?.linkedThreads !== undefined && deferredQuery === linkedThreadSearch.query;
const filteredGroups = filterCommandPaletteGroups({
const searchedGroups = filterCommandPaletteGroups({
activeGroups,
query: deferredQuery,
isInSubmenu: currentView !== null,
Expand Down Expand Up @@ -1818,6 +1818,21 @@ function OpenCommandPaletteDialog(props: {
threadMessageItems: isLinkedThreadSearch ? [] : threadMessageItems,
now: searchNow,
});
// Jump shortcuts number the project rows as shown, so a pin cannot move a number to another row.
const filteredGroups = searchedGroups.map((group) => {
if (group.value !== NEW_THREAD_IN_GROUP) return group;
const jumpItems = new Map(
enumerateCommandPaletteItems(
group.items.filter(
(item): item is CommandPaletteActionItem =>
item.kind === "action" &&
item.value !== NEW_THREAD_NO_PROJECT_VALUE &&
item.value !== addProjectAction.value,
),
).map((item) => [item.value, item] as const),
);
return { ...group, items: group.items.map((item) => jumpItems.get(item.value) ?? item) };
});

const handleAddProjectForEnvironment = async (input: {
readonly environmentId: EnvironmentId;
Expand Down
Loading