diff --git a/apps/web/src/components/CommandPalette.browser.test.tsx b/apps/web/src/components/CommandPalette.browser.test.tsx index 3883a3e2258e9..9faeab83c6ffc 100644 --- a/apps/web/src/components/CommandPalette.browser.test.tsx +++ b/apps/web/src/components/CommandPalette.browser.test.tsx @@ -1,13 +1,28 @@ -import { describe, expect, it } from "vite-plus/test"; +import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts"; +import { describe, expect, it, vi } from "vite-plus/test"; import { page } from "vite-plus/test/browser"; import { render } from "../../test/render"; import { openCommandPalette } from "../commandPaletteBus"; import { useCommandPaletteStore } from "../commandPaletteStore"; +import { makeThreadFixture } from "../test-fixtures"; +import type { Project, Thread } from "../types"; import { CommandPalette } from "./CommandPalette"; -function setup() { +const entities = vi.hoisted(() => ({ + projects: [] as ReadonlyArray, + threads: [] as ReadonlyArray, +})); +vi.mock("../state/entities", async (original) => ({ + ...(await original()), + useProjects: () => entities.projects, + useVisibleThreadShells: () => entities.threads, +})); + +function setup(state: { projects?: ReadonlyArray; threads?: ReadonlyArray } = {}) { useCommandPaletteStore.setState(useCommandPaletteStore.getInitialState(), true); + entities.projects = state.projects ?? []; + entities.threads = state.threads ?? []; } describe("CommandPalette open intents", () => { @@ -30,4 +45,50 @@ describe("CommandPalette open intents", () => { await expect.element(input).toHaveFocus(); await expect.element(input).toHaveValue("acme"); }); + + it("opens a searched project's latest listed thread, not a newer subagent", async () => { + const environmentId = EnvironmentId.make("local"); + const projectId = ProjectId.make("acme"); + const parentId = ThreadId.make("parent"); + setup({ + projects: [ + { + id: projectId, + environmentId, + title: "Acme", + workspaceRoot: "/work/acme", + repositoryIdentity: null, + defaultModelSelection: null, + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + scripts: [], + }, + ], + threads: [ + makeThreadFixture({ + id: parentId, + environmentId, + projectId, + latestUserMessageAt: "2026-03-02T00:00:00.000Z", + }), + makeThreadFixture({ + id: ThreadId.make("subagent"), + environmentId, + projectId, + updatedAt: "2026-03-03T00:00:00.000Z", + lineage: { + rootThreadId: parentId, + parentThreadId: parentId, + relationshipToParent: "subagent", + }, + }), + ], + }); + await using screen = await render({null}); + + openCommandPalette({ query: "Acme" }); + await page.getByRole("option", { name: /Acme/ }).first().click(); + + await vi.waitFor(() => expect(screen.router?.state.location.pathname).toBe("/local/parent")); + }); }); diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 068808707b4f5..ddac4fc4d7c17 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -212,6 +212,7 @@ import { type SearchOverlayMode, } from "./CommandPalette.logic"; import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; +import { isSidebarHiddenThread } from "./Sidebar.logic"; import { CommandPaletteContent } from "./CommandPaletteContent"; import { selectCommandPaletteClientSettings, @@ -493,7 +494,8 @@ function OpenCommandPaletteDialog(props: { if (activeThreadReferenceCopyTarget === null) return; await copyThreadReference(activeThreadReferenceCopyTarget); }; - const threads = useVisibleThreadShells(); + // Side chats and subagents open inside their parent, so opening a project never lands on one. + const threads = useVisibleThreadShells().filter((thread) => !isSidebarHiddenThread(thread)); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const providersByEnvironment = useEnvironmentProviders(); const { changeThemeItem, changeAppearanceItem, themeEditorItem } = useCommandPaletteThemeItems();