authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-08-18 20:33:04+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-19 00:03:04+05:30
logfda740ad7b28334a6c579d801db22ad8f5914b5f
tree3ac73673805d905428e5c15da15e819d8127293d
parent3b5d476eb6af9d14efe037e9d163848d6fd048d7
signature Signed by PGP key B5690EEEBB952194

feat(web): show project location in new thread picker (#7392)

Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>

3 files changed, 66 insertions(+), 12 deletions(-)

apps/web/src/components/CommandPalette.logic.ts+2-1
...@@ -150,6 +150,7 @@ export function buildProjectActionItems(input: {...@@ -150,6 +150,7 @@ export function buildProjectActionItems(input: {
150 icon: (project: Project) => ReactNode;150 icon: (project: Project) => ReactNode;
151 runProject: (project: Project) => Promise<void>;151 runProject: (project: Project) => Promise<void>;
152 searchTerms?: (project: Project) => ReadonlyArray<string>;152 searchTerms?: (project: Project) => ReadonlyArray<string>;
153 renderDescription?: (project: Project) => ReactNode;
153 shortcutCommand?: KeybindingCommand;154 shortcutCommand?: KeybindingCommand;
154}): CommandPaletteActionItem[] {155}): CommandPaletteActionItem[] {
155 return input.projects.map((project) => ({156 return input.projects.map((project) => ({
...@@ -157,7 +158,7 @@ export function buildProjectActionItems(input: {...@@ -157,7 +158,7 @@ export function buildProjectActionItems(input: {
157 value: `${input.valuePrefix}:${project.environmentId}:${project.id}`,158 value: `${input.valuePrefix}:${project.environmentId}:${project.id}`,
158 searchTerms: [project.title, project.workspaceRoot, ...(input.searchTerms?.(project) ?? [])],159 searchTerms: [project.title, project.workspaceRoot, ...(input.searchTerms?.(project) ?? [])],
159 title: project.title,160 title: project.title,
160 description: project.workspaceRoot,161 description: input.renderDescription?.(project) ?? project.workspaceRoot,
161 icon: input.icon(project),162 icon: input.icon(project),
162 ...(input.shortcutCommand !== undefined ? { shortcutCommand: input.shortcutCommand } : {}),163 ...(input.shortcutCommand !== undefined ? { shortcutCommand: input.shortcutCommand } : {}),
163 run: async () => {164 run: async () => {
apps/web/src/components/CommandPalette.tsx+56-3
...@@ -41,6 +41,7 @@ import {...@@ -41,6 +41,7 @@ import {
41 LinkIcon,41 LinkIcon,
42 MessageSquareIcon,42 MessageSquareIcon,
43 PaletteIcon,43 PaletteIcon,
44 ServerIcon,
44 SettingsIcon,45 SettingsIcon,
45 SquarePenIcon,46 SquarePenIcon,
46 TextSearchIcon,47 TextSearchIcon,
...@@ -131,7 +132,11 @@ import { ProjectFavicon } from "./ProjectFavicon";...@@ -131,7 +132,11 @@ import { ProjectFavicon } from "./ProjectFavicon";
131import { ProjectFilePicker } from "./files/ProjectFilePicker";132import { ProjectFilePicker } from "./files/ProjectFilePicker";
132import { ProjectContentSearchDialog } from "./search/ProjectContentSearchDialog";133import { ProjectContentSearchDialog } from "./search/ProjectContentSearchDialog";
133import { toggleThemeEditorForTheme } from "./settings/themeEditorStore";134import { toggleThemeEditorForTheme } from "./settings/themeEditorStore";
134import { ThreadCommandSubtitle } from "./ThreadCommandSubtitle";135import {
136 COMMAND_PALETTE_META_ICON_CLASS,
137 CommandPaletteMetaDot,
138 ThreadCommandSubtitle,
139} from "./ThreadCommandSubtitle";
135import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators";140import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators";
136import { primaryServerKeybindingsAtom, primaryServerProvidersAtom } from "../state/server";141import { primaryServerKeybindingsAtom, primaryServerProvidersAtom } from "../state/server";
137import {142import {
...@@ -657,6 +662,27 @@ function OpenCommandPaletteDialog(props: {...@@ -657,6 +662,27 @@ function OpenCommandPaletteDialog(props: {
657 ),662 ),
658 [environments],663 [environments],
659 );664 );
665 const projectEnvironmentLocationById = useMemo(
666 () =>
667 new Map(
668 environments.map((environment) => {
669 const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget";
670 const isLocal = isPrimary || isDesktopLocalConnectionTarget(environment.entry.target);
671 return [
672 environment.environmentId,
673 {
674 kind: isLocal ? "local" : "remote",
675 label: isPrimary
676 ? "Local"
677 : isLocal
678 ? `${environment.label} (Local)`
679 : environment.label,
680 },
681 ] as const;
682 }),
683 ),
684 [environments],
685 );
660 const orderedProjects = useMemo(686 const orderedProjects = useMemo(
661 () =>687 () =>
662 orderItemsByPreferredIds({688 orderItemsByPreferredIds({
...@@ -1011,8 +1037,29 @@ function OpenCommandPaletteDialog(props: {...@@ -1011,8 +1037,29 @@ function OpenCommandPaletteDialog(props: {
1011 valuePrefix: "new-thread-in",1037 valuePrefix: "new-thread-in",
1012 searchTerms: (project) => {1038 searchTerms: (project) => {
1013 const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`);1039 const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`);
1040 const location = projectEnvironmentLocationById.get(project.environmentId);
1041 return [
1042 ...(group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ??
1043 []),
1044 ...(location ? [location.label] : []),
1045 ];
1046 },
1047 renderDescription: (project) => {
1048 const location = projectEnvironmentLocationById.get(project.environmentId) ?? {
1049 kind: "remote",
1050 label: "Remote",
1051 };
1014 return (1052 return (
1015 group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? []1053 <span className="flex min-w-0 items-center gap-1">
1054 <span className="inline-flex min-w-0 items-center gap-1">
1055 {location.kind === "remote" ? (
1056 <ServerIcon aria-hidden className={COMMAND_PALETTE_META_ICON_CLASS} />
1057 ) : null}
1058 <span className="truncate">{location.label}</span>
1059 </span>
1060 <CommandPaletteMetaDot />
1061 <span className="truncate">{project.workspaceRoot}</span>
1062 </span>
1016 );1063 );
1017 },1064 },
1018 icon: projectFavicon,1065 icon: projectFavicon,
...@@ -1033,7 +1080,13 @@ function OpenCommandPaletteDialog(props: {...@@ -1033,7 +1080,13 @@ function OpenCommandPaletteDialog(props: {
1033 },1080 },
1034 }),1081 }),
1035 ),1082 ),
1036 [contextualProjectRef, handleNewThread, pickerProjects, projectGroupByTargetKey],1083 [
1084 contextualProjectRef,
1085 handleNewThread,
1086 pickerProjects,
1087 projectEnvironmentLocationById,
1088 projectGroupByTargetKey,
1089 ],
1037 );1090 );
10381091
1039 const allThreadItems = useMemo(1092 const allThreadItems = useMemo(
apps/web/src/components/ThreadCommandSubtitle.tsx+8-8
...@@ -18,20 +18,20 @@ export type ThreadCommandSubtitleVariant =...@@ -18,20 +18,20 @@ export type ThreadCommandSubtitleVariant =
18export const THREAD_COMMAND_SUBTITLE_VARIANT: ThreadCommandSubtitleVariant =18export const THREAD_COMMAND_SUBTITLE_VARIANT: ThreadCommandSubtitleVariant =
19 "favicon-workspace-harness";19 "favicon-workspace-harness";
2020
21const META_ICON_CLASS = "size-3 shrink-0 text-muted-foreground/70";21export const COMMAND_PALETTE_META_ICON_CLASS = "size-3 shrink-0 text-muted-foreground/70";
2222
23function Dot() {23export function CommandPaletteMetaDot() {
24 return <span className="shrink-0 text-muted-foreground/50">·</span>;24 return <span className="shrink-0 text-muted-foreground/50">·</span>;
25}25}
2626
27function WorkspaceIcon(props: { variant: ThreadCommandSubtitleVariant; isWorktree: boolean }) {27function WorkspaceIcon(props: { variant: ThreadCommandSubtitleVariant; isWorktree: boolean }) {
28 if (props.isWorktree) {28 if (props.isWorktree) {
29 return <FolderGit2Icon className={META_ICON_CLASS} aria-hidden />;29 return <FolderGit2Icon className={COMMAND_PALETTE_META_ICON_CLASS} aria-hidden />;
30 }30 }
31 if (props.variant === "favicon-branch-harness") {31 if (props.variant === "favicon-branch-harness") {
32 return <GitBranchIcon className={META_ICON_CLASS} aria-hidden />;32 return <GitBranchIcon className={COMMAND_PALETTE_META_ICON_CLASS} aria-hidden />;
33 }33 }
34 return <FolderIcon className={META_ICON_CLASS} aria-hidden />;34 return <FolderIcon className={COMMAND_PALETTE_META_ICON_CLASS} aria-hidden />;
35}35}
3636
37export function ThreadCommandSubtitle(props: {37export function ThreadCommandSubtitle(props: {
...@@ -82,7 +82,7 @@ export function ThreadCommandSubtitle(props: {...@@ -82,7 +82,7 @@ export function ThreadCommandSubtitle(props: {
8282
83 {branchLabel ? (83 {branchLabel ? (
84 <>84 <>
85 {projectLabel ? <Dot /> : null}85 {projectLabel ? <CommandPaletteMetaDot /> : null}
86 <span className="inline-flex min-w-0 items-center gap-1">86 <span className="inline-flex min-w-0 items-center gap-1">
87 <WorkspaceIcon variant={variant} isWorktree={isWorktree} />87 <WorkspaceIcon variant={variant} isWorktree={isWorktree} />
88 <span className="min-w-0 truncate">{branchLabel}</span>88 <span className="min-w-0 truncate">{branchLabel}</span>
...@@ -92,7 +92,7 @@ export function ThreadCommandSubtitle(props: {...@@ -92,7 +92,7 @@ export function ThreadCommandSubtitle(props: {
9292
93 {showHarness && props.driverKind ? (93 {showHarness && props.driverKind ? (
94 <>94 <>
95 {projectLabel || branchLabel ? <Dot /> : null}95 {projectLabel || branchLabel ? <CommandPaletteMetaDot /> : null}
96 <ProviderInstanceIcon96 <ProviderInstanceIcon
97 driverKind={props.driverKind}97 driverKind={props.driverKind}
98 displayName={props.providerDisplayName ?? props.driverKind}98 displayName={props.providerDisplayName ?? props.driverKind}
...@@ -103,7 +103,7 @@ export function ThreadCommandSubtitle(props: {...@@ -103,7 +103,7 @@ export function ThreadCommandSubtitle(props: {
103103
104 {props.isCurrent ? (104 {props.isCurrent ? (
105 <>105 <>
106 {projectLabel || branchLabel || showHarness ? <Dot /> : null}106 {projectLabel || branchLabel || showHarness ? <CommandPaletteMetaDot /> : null}
107 <span className="shrink-0">Current thread</span>107 <span className="shrink-0">Current thread</span>
108 </>108 </>
109 ) : null}109 ) : null}