Skip to content

Commit fba5520

Browse files
committed
fix(web): scope command palette results to their environment
1 parent a3ca1a5 commit fba5520

3 files changed

Lines changed: 67 additions & 39 deletions

File tree

‎apps/web/src/components/CommandPalette.logic.test.ts‎

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,31 @@ describe("buildProjectActionItems", () => {
395395
});
396396

397397
describe("buildThreadActionItems", () => {
398+
it("selects the intended environment when thread IDs are shared", async () => {
399+
const local = makeThread();
400+
const remote = makeThread({ environmentId: EnvironmentId.make("remote-copy") });
401+
const runThread = vi.fn(async () => {});
402+
const items = buildThreadActionItems({
403+
threads: [local, remote],
404+
activeThread: remote,
405+
projectByKey: new Map([
406+
[`${local.environmentId}:${local.projectId}`, { title: "Local project" }],
407+
[`${remote.environmentId}:${remote.projectId}`, { title: "Remote project" }],
408+
]),
409+
sortOrder: "updated_at",
410+
icon: null,
411+
runThread,
412+
});
413+
const groups = [{ value: "threads", label: "Threads", items }];
414+
const selected = findHighlightedCommandPaletteItem(groups, items[1]!.value);
415+
if (selected?.kind === "action") await selected.run();
416+
expect(runThread).toHaveBeenCalledWith(remote);
417+
expect(new Set(buildCommandPaletteRows(groups).itemValues).size).toBe(2);
418+
expect(items[0]?.description).toBe("Local project");
419+
expect(items[1]?.description).toBe("Remote project · Current thread");
420+
expect(items[0]?.searchTerms).not.toContain("Remote project");
421+
});
422+
398423
it("orders threads by most recent activity and formats timestamps from updatedAt", () => {
399424
vi.useFakeTimers();
400425
vi.setSystemTime(new Date("2026-03-25T12:00:00.000Z"));
@@ -414,15 +439,15 @@ describe("buildThreadActionItems", () => {
414439
updatedAt: "2026-03-20T00:00:00.000Z",
415440
}),
416441
],
417-
projectTitleById: new Map([[PROJECT_ID, "Project"]]),
442+
projectByKey: new Map([[`${LOCAL_ENVIRONMENT_ID}:${PROJECT_ID}`, { title: "Project" }]]),
418443
sortOrder: "updated_at",
419444
icon: null,
420445
runThread: async (_thread) => undefined,
421446
});
422447

423448
expect(items.map((item) => item.value)).toEqual([
424-
"thread:thread-older",
425-
"thread:thread-newer",
449+
"thread:environment-local:thread-older",
450+
"thread:environment-local:thread-newer",
426451
]);
427452
expect(items[0]?.timestamp).toBe("1d ago");
428453
expect(items[1]?.timestamp).toBe("5d ago");
@@ -446,7 +471,7 @@ describe("buildThreadActionItems", () => {
446471
updatedAt: "2026-03-19T00:00:00.000Z",
447472
}),
448473
],
449-
projectTitleById: new Map([[PROJECT_ID, "Project"]]),
474+
projectByKey: new Map([[`${LOCAL_ENVIRONMENT_ID}:${PROJECT_ID}`, { title: "Project" }]]),
450475
sortOrder: "updated_at",
451476
icon: null,
452477
runThread: async (_thread) => undefined,
@@ -463,8 +488,8 @@ describe("buildThreadActionItems", () => {
463488
expect(groups).toHaveLength(1);
464489
expect(groups[0]?.value).toBe("threads-search");
465490
expect(groups[0]?.items.map((item) => item.value)).toEqual([
466-
"thread:thread-title-match",
467-
"thread:thread-context-match",
491+
"thread:environment-local:thread-title-match",
492+
"thread:environment-local:thread-context-match",
468493
]);
469494
});
470495

@@ -491,7 +516,7 @@ describe("buildThreadActionItems", () => {
491516
];
492517
const items = buildThreadActionItems({
493518
threads,
494-
projectTitleById: new Map([[PROJECT_ID, "T3 Code"]]),
519+
projectByKey: new Map([[`${LOCAL_ENVIRONMENT_ID}:${PROJECT_ID}`, { title: "T3 Code" }]]),
495520
sortOrder: "created_at",
496521
icon: null,
497522
getContentMatch: (thread) =>
@@ -510,9 +535,9 @@ describe("buildThreadActionItems", () => {
510535
});
511536

512537
expect(groups[0]?.items.map((item) => item.value)).toEqual([
513-
"thread:recent-title",
514-
"thread:old-prefix",
515-
"thread:recent-content",
538+
"thread:environment-local:recent-title",
539+
"thread:environment-local:old-prefix",
540+
"thread:environment-local:recent-content",
516541
]);
517542
});
518543

@@ -640,7 +665,7 @@ describe("buildThreadActionItems", () => {
640665
it("keeps message excerpts searchable without replacing thread metadata", () => {
641666
const [item] = buildThreadActionItems({
642667
threads: [makeThread({ branch: "feat/search" })],
643-
projectTitleById: new Map([[PROJECT_ID, "T3 Code"]]),
668+
projectByKey: new Map([[`${LOCAL_ENVIRONMENT_ID}:${PROJECT_ID}`, { title: "T3 Code" }]]),
644669
sortOrder: "updated_at",
645670
icon: null,
646671
getContentMatch: () => ({
@@ -673,7 +698,7 @@ describe("buildThreadActionItems", () => {
673698
});
674699
const items = buildThreadActionItems({
675700
threads: [idThread, titleThread],
676-
projectTitleById: new Map([[PROJECT_ID, "T3 Code"]]),
701+
projectByKey: new Map([[`${LOCAL_ENVIRONMENT_ID}:${PROJECT_ID}`, { title: "T3 Code" }]]),
677702
sortOrder: "updated_at",
678703
icon: null,
679704
runThread: async (_thread) => undefined,
@@ -689,15 +714,15 @@ describe("buildThreadActionItems", () => {
689714
});
690715

691716
expect(groups.flatMap((group) => group.items)).toEqual([
692-
expect.objectContaining({ value: `thread:${titleThread.id}` }),
693-
expect.objectContaining({ value: `thread:${idThread.id}` }),
717+
expect.objectContaining({ value: `thread:${titleThread.environmentId}:${titleThread.id}` }),
718+
expect.objectContaining({ value: `thread:${idThread.environmentId}:${idThread.id}` }),
694719
]);
695720
});
696721

697722
it("prefers renderDescription when provided", () => {
698723
const [item] = buildThreadActionItems({
699724
threads: [makeThread({ branch: "feat/search", worktreePath: "/tmp/wt" })],
700-
projectTitleById: new Map([[PROJECT_ID, "T3 Code"]]),
725+
projectByKey: new Map([[`${LOCAL_ENVIRONMENT_ID}:${PROJECT_ID}`, { title: "T3 Code" }]]),
701726
sortOrder: "updated_at",
702727
icon: null,
703728
renderDescription: (thread, { projectTitle }) =>
@@ -724,13 +749,13 @@ describe("buildThreadActionItems", () => {
724749
updatedAt: "2026-03-20T00:00:00.000Z",
725750
}),
726751
],
727-
projectTitleById: new Map([[PROJECT_ID, "Project"]]),
752+
projectByKey: new Map([[`${LOCAL_ENVIRONMENT_ID}:${PROJECT_ID}`, { title: "Project" }]]),
728753
sortOrder: "updated_at",
729754
icon: null,
730755
runThread: async (_thread) => undefined,
731756
});
732757

733-
expect(items.map((item) => item.value)).toEqual(["thread:thread-active"]);
758+
expect(items.map((item) => item.value)).toEqual(["thread:environment-local:thread-active"]);
734759
});
735760
});
736761

@@ -833,7 +858,7 @@ it.each([
833858
}),
834859
makeThread({ id: ThreadId.make("unrelated"), title: "Other work" }),
835860
],
836-
projectTitleById: new Map(),
861+
projectByKey: new Map(),
837862
sortOrder: "updated_at",
838863
icon: null,
839864
runThread: async () => undefined,

‎apps/web/src/components/CommandPalette.logic.ts‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,8 @@ export type BuildThreadActionItemsThread = Pick<
358358

359359
export function buildThreadActionItems<TThread extends BuildThreadActionItemsThread>(input: {
360360
threads: ReadonlyArray<TThread>;
361-
activeThreadId?: Thread["id"];
362-
projectTitleById: ReadonlyMap<Project["id"], string>;
361+
activeThread?: Pick<Thread, "environmentId" | "id">;
362+
projectByKey: ReadonlyMap<string, Pick<Project, "title">>;
363363
sortOrder: SidebarThreadSortOrder;
364364
icon: ReactNode;
365365
renderLeadingContent?: (thread: TThread) => ReactNode;
@@ -377,7 +377,9 @@ export function buildThreadActionItems<TThread extends BuildThreadActionItemsThr
377377
input.limit === undefined ? sortedThreads : sortedThreads.slice(0, input.limit);
378378

379379
return visibleThreads.map((thread) => {
380-
const projectTitle = input.projectTitleById.get(thread.projectId);
380+
const projectTitle = input.projectByKey.get(
381+
`${thread.environmentId}:${thread.projectId}`,
382+
)?.title;
381383
const descriptionParts: string[] = [];
382384

383385
if (projectTitle) {
@@ -386,7 +388,10 @@ export function buildThreadActionItems<TThread extends BuildThreadActionItemsThr
386388
if (thread.branch) {
387389
descriptionParts.push(`#${thread.branch}`);
388390
}
389-
if (thread.id === input.activeThreadId) {
391+
if (
392+
thread.id === input.activeThread?.id &&
393+
thread.environmentId === input.activeThread.environmentId
394+
) {
390395
descriptionParts.push("Current thread");
391396
}
392397

@@ -400,7 +405,7 @@ export function buildThreadActionItems<TThread extends BuildThreadActionItemsThr
400405
return Object.assign(
401406
{
402407
kind: "action" as const,
403-
value: `thread:${thread.id}`,
408+
value: `thread:${thread.environmentId}:${thread.id}`,
404409
searchTerms: [
405410
thread.title,
406411
...threadPullRequestSearchTerms(thread),

‎apps/web/src/components/CommandPalette.tsx‎

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ import {
4848
type EnvironmentId,
4949
type EnvironmentMachineKind,
5050
type FilesystemBrowseResult,
51-
type ProjectId,
5251
type SourceControlDiscoveryResult,
5352
SourceControlProviderKind,
5453
type SourceControlRepositoryInfo,
@@ -433,16 +432,10 @@ function useEnvironmentLookups() {
433432

434433
function useProjectLookups() {
435434
const projects = useProjects();
436-
const projectCwdById = new Map<ProjectId, string>(
437-
projects.map((project) => [project.id, project.workspaceRoot]),
438-
);
439435
const projectByKey = new Map(
440436
projects.map((project) => [`${project.environmentId}:${project.id}`, project]),
441437
);
442-
const projectTitleById = new Map<ProjectId, string>(
443-
projects.map((project) => [project.id, project.title]),
444-
);
445-
return { projectCwdById, projectByKey, projectTitleById };
438+
return { projectByKey };
446439
}
447440

448441
const selectCommandPaletteClientSettings = (settings: ClientSettings) => ({
@@ -1013,7 +1006,7 @@ function OpenCommandPaletteDialog(props: {
10131006
return ensureBrowseDirectoryPath(baseDirectory);
10141007
};
10151008

1016-
const { projectCwdById, projectByKey, projectTitleById } = useProjectLookups();
1009+
const { projectByKey } = useProjectLookups();
10171010

10181011
const activeThreadId = activeThread?.id;
10191012
const supportsThreadFind =
@@ -1025,9 +1018,11 @@ function OpenCommandPaletteDialog(props: {
10251018
const scratchTargetEnvironmentId = scratchEnvironmentId(
10261019
currentProjectEnvironmentId ?? primaryEnvironmentId,
10271020
);
1028-
const currentProjectCwd = currentProjectId
1029-
? (projectCwdById.get(currentProjectId) ?? null)
1030-
: null;
1021+
const currentProject =
1022+
currentProjectEnvironmentId && currentProjectId
1023+
? projectByKey.get(`${currentProjectEnvironmentId}:${currentProjectId}`)
1024+
: undefined;
1025+
const currentProjectCwd = currentProject?.workspaceRoot ?? null;
10311026
const currentProjectCwdForBrowse =
10321027
browseEnvironmentId && currentProjectEnvironmentId === browseEnvironmentId
10331028
? currentProjectCwd
@@ -1246,8 +1241,8 @@ function OpenCommandPaletteDialog(props: {
12461241

12471242
const allThreadItems = buildThreadActionItems({
12481243
threads,
1249-
...(activeThreadId ? { activeThreadId } : {}),
1250-
projectTitleById,
1244+
...(activeThread ? { activeThread } : {}),
1245+
projectByKey,
12511246
sortOrder: clientSettings.sidebarThreadSortOrder,
12521247
icon: <MessageSquareIcon className={ITEM_ICON_CLASS} />,
12531248
renderLeadingContent: (thread) => <ThreadRowLeadingStatus thread={thread} />,
@@ -1276,7 +1271,9 @@ function OpenCommandPaletteDialog(props: {
12761271
}
12771272
branch={thread.branch}
12781273
worktreePath={thread.worktreePath}
1279-
isCurrent={thread.id === activeThreadId}
1274+
isCurrent={
1275+
thread.id === activeThreadId && thread.environmentId === activeThread?.environmentId
1276+
}
12801277
driverKind={providerEntry?.driverKind ?? null}
12811278
providerDisplayName={
12821279
thread.runtime?.providerName ?? providerEntry?.displayName ?? modelInstanceId
@@ -1599,7 +1596,8 @@ function OpenCommandPaletteDialog(props: {
15991596
if (projects.length > 0) {
16001597
const activeProjectTitle =
16011598
projectPickerEntries.find((entry) => entry.isPreferred)?.group.displayName ??
1602-
(currentProjectId ? (projectTitleById.get(currentProjectId) ?? null) : null);
1599+
currentProject?.title ??
1600+
null;
16031601

16041602
if (activeProjectTitle) {
16051603
actionItems.push({

0 commit comments

Comments
 (0)