From 8d2ddef772a35f04e19c05ceef9138ce40c998b9 Mon Sep 17 00:00:00 2001 From: Jono Kemball Date: Fri, 24 Jul 2026 17:42:40 +1200 Subject: [PATCH 1/5] feat(web): show remote environment on command palette project rows Project rows in the command palette ("New thread in..." and project search) now show which remote T3 Code environment a project lives on, via a cloud/container badge plus the environment label. This mirrors the sidebar's existing project-header treatment, reusing the group's remoteEnvironmentLabels / allRemoteMembersAreDesktopLocal data. Local (this-device) projects have no remote, so nothing is rendered for them. Co-Authored-By: Claude Opus 4.8 --- .../src/components/CommandPalette.logic.ts | 30 +++++++------ apps/web/src/components/CommandPalette.tsx | 42 +++++++++++++++++++ 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index f69c38e1a0f3..bfb01f58e5dd 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -112,20 +112,26 @@ export function buildProjectActionItems(input: { icon: (project: Project) => ReactNode; runProject: (project: Project) => Promise; searchTerms?: (project: Project) => ReadonlyArray; + /** Optional content rendered inline after the title (e.g. a remote environment badge). */ + titleTrailingContent?: (project: Project) => ReactNode; shortcutCommand?: KeybindingCommand; }): CommandPaletteActionItem[] { - return input.projects.map((project) => ({ - kind: "action", - value: `${input.valuePrefix}:${project.environmentId}:${project.id}`, - searchTerms: [project.title, project.workspaceRoot, ...(input.searchTerms?.(project) ?? [])], - title: project.title, - description: project.workspaceRoot, - icon: input.icon(project), - ...(input.shortcutCommand !== undefined ? { shortcutCommand: input.shortcutCommand } : {}), - run: async () => { - await input.runProject(project); - }, - })); + return input.projects.map((project) => { + const titleTrailingContent = input.titleTrailingContent?.(project); + return { + kind: "action", + value: `${input.valuePrefix}:${project.environmentId}:${project.id}`, + searchTerms: [project.title, project.workspaceRoot, ...(input.searchTerms?.(project) ?? [])], + title: project.title, + description: project.workspaceRoot, + icon: input.icon(project), + ...(titleTrailingContent ? { titleTrailingContent } : {}), + ...(input.shortcutCommand !== undefined ? { shortcutCommand: input.shortcutCommand } : {}), + run: async () => { + await input.runProject(project); + }, + }; + }); } export type BuildThreadActionItemsThread = Pick< diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index aa7547c8ba60..36926ed30e3c 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -22,6 +22,8 @@ import { ArrowDownIcon, ArrowLeftIcon, ArrowUpIcon, + CloudIcon, + ContainerIcon, CornerLeftUpIcon, FolderIcon, FolderPlusIcon, @@ -131,6 +133,7 @@ import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore" import { buildSidebarProjectPickerEntries, buildSidebarProjectSnapshots, + type SidebarProjectSnapshot, } from "../sidebarProjectGrouping"; const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; @@ -158,6 +161,37 @@ function getEnvironmentBrowsePlatform(os: string | null | undefined): string { return typeof navigator === "undefined" ? "" : navigator.platform; } +// Badge showing which remote T3 Code connection a project lives on. Mirrors +// the sidebar's project-header treatment: a cloud (or container, for a +// desktop-local sandbox like WSL) icon plus the environment label(s). Local +// (this-device) projects have no remote, so nothing is rendered for them. +function renderProjectRemoteBadge(group: SidebarProjectSnapshot | undefined): ReactNode { + if (!group || group.remoteEnvironmentLabels.length === 0) { + return undefined; + } + const label = group.remoteEnvironmentLabels.join(", "); + const isSandbox = group.allRemoteMembersAreDesktopLocal; + return ( + + + {isSandbox ? ( + + ) : ( + + )} + {label} + + } + /> + + {isSandbox ? `Local sandbox: ${label}` : `Remote environment: ${label}`} + + + ); +} + interface AddProjectEnvironmentOption { readonly environmentId: EnvironmentId; readonly label: string; @@ -800,6 +834,10 @@ function OpenCommandPaletteDialog(props: { className={ITEM_ICON_CLASS} /> ), + titleTrailingContent: (project) => + renderProjectRemoteBadge( + projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`), + ), runProject: openProjectFromSearch, }), [openProjectFromSearch, pickerProjects, projectGroupByTargetKey], @@ -824,6 +862,10 @@ function OpenCommandPaletteDialog(props: { className={ITEM_ICON_CLASS} /> ), + titleTrailingContent: (project) => + renderProjectRemoteBadge( + projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`), + ), runProject: async (project) => { const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); const contextualRefBelongsToGroup = From a34d89693494ec55d65aa206ab084b63113fc457 Mon Sep 17 00:00:00 2001 From: Jono Kemball Date: Fri, 24 Jul 2026 18:25:08 +1200 Subject: [PATCH 2/5] fix(web): use server icon for command palette remote environment badge Switch the project-row remote badge from the v1 sidebar's cloud/container icons to SidebarV2's ServerIcon, dropping the desktop-local sandbox distinction, so it matches the sidebar that will become the default. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/components/CommandPalette.tsx | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 36926ed30e3c..f1c47ffd23ba 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -22,13 +22,12 @@ import { ArrowDownIcon, ArrowLeftIcon, ArrowUpIcon, - CloudIcon, - ContainerIcon, CornerLeftUpIcon, FolderIcon, FolderPlusIcon, LinkIcon, MessageSquareIcon, + ServerIcon, SettingsIcon, SquarePenIcon, } from "lucide-react"; @@ -161,33 +160,26 @@ function getEnvironmentBrowsePlatform(os: string | null | undefined): string { return typeof navigator === "undefined" ? "" : navigator.platform; } -// Badge showing which remote T3 Code connection a project lives on. Mirrors -// the sidebar's project-header treatment: a cloud (or container, for a -// desktop-local sandbox like WSL) icon plus the environment label(s). Local -// (this-device) projects have no remote, so nothing is rendered for them. +// Badge showing which remote T3 Code environment a project lives on: a server +// icon plus the environment label(s), matching SidebarV2's environment +// treatment. Local (this-device) projects have no remote, so nothing is +// rendered for them. function renderProjectRemoteBadge(group: SidebarProjectSnapshot | undefined): ReactNode { if (!group || group.remoteEnvironmentLabels.length === 0) { return undefined; } const label = group.remoteEnvironmentLabels.join(", "); - const isSandbox = group.allRemoteMembersAreDesktopLocal; return ( - {isSandbox ? ( - - ) : ( - - )} + {label} } /> - - {isSandbox ? `Local sandbox: ${label}` : `Remote environment: ${label}`} - + Remote environment: {label} ); } From 88009ac7827a94033c8f6384c3f420acbb89fd93 Mon Sep 17 00:00:00 2001 From: Jono Kemball Date: Fri, 24 Jul 2026 18:45:44 +1200 Subject: [PATCH 3/5] refactor(web): use server icon for remote environments app-wide Replace the CloudIcon remote-environment indicator with ServerIcon across the branch toolbar, environment selector, sidebar, and thread status indicators, matching SidebarV2 (soon the default) and the command palette badge. The desktop-local sandbox ContainerIcon distinction is unchanged. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/components/BranchToolbar.tsx | 6 +++--- .../src/components/BranchToolbarEnvironmentSelector.tsx | 8 ++++---- apps/web/src/components/Sidebar.tsx | 6 +++--- apps/web/src/components/ThreadStatusIndicators.tsx | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index e703427d4b63..bafef1bbb276 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -2,12 +2,12 @@ import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environ import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { ChevronDownIcon, - CloudIcon, FolderGit2Icon, FolderGitIcon, FolderIcon, HistoryIcon, MonitorIcon, + ServerIcon, } from "lucide-react"; import { memo, useCallback, useMemo } from "react"; @@ -103,7 +103,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ ? resolveEnvModeLabel("worktree") : resolveCurrentWorkspaceLabel(activeWorktreePath); const isLocked = envLocked || envModeLocked; - const EnvironmentIcon = activeEnvironment?.isPrimary ? MonitorIcon : CloudIcon; + const EnvironmentIcon = activeEnvironment?.isPrimary ? MonitorIcon : ServerIcon; const icon = showEnvironmentIndicator ? ( // Button's base styles apply `-mx-0.5` to descendant SVGs, which eats 4px // out of whatever gap we set. mx-0! cancels that so gap-0.5 reads as 2px. @@ -150,7 +150,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ onValueChange={(value) => onEnvironmentChange(value as EnvironmentId)} > {availableEnvironments.map((env) => { - const Icon = env.isPrimary ? MonitorIcon : CloudIcon; + const Icon = env.isPrimary ? MonitorIcon : ServerIcon; return ( ) : ( - + )} {activeEnvironment?.label ?? "Run on"} @@ -65,7 +65,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir {activeEnvironment?.isPrimary ? ( ) : ( - + )} @@ -78,7 +78,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir {env.isPrimary ? ( ) : ( - + )} {env.label} diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index a1d95eaa7340..d7ff447c06ae 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -2,12 +2,12 @@ import { ArchiveIcon, ArrowUpDownIcon, ChevronRightIcon, - CloudIcon, ContainerIcon, FolderPlusIcon, Globe2Icon, LoaderIcon, SearchIcon, + ServerIcon, SquarePenIcon, TerminalIcon, TriangleAlertIcon, @@ -836,7 +836,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr /> } > - + {threadEnvironmentLabel} @@ -2290,7 +2290,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec {project.allRemoteMembersAreDesktopLocal ? ( ) : ( - + )} diff --git a/apps/web/src/components/ThreadStatusIndicators.tsx b/apps/web/src/components/ThreadStatusIndicators.tsx index 3c58d6ea9898..a5bf09a32242 100644 --- a/apps/web/src/components/ThreadStatusIndicators.tsx +++ b/apps/web/src/components/ThreadStatusIndicators.tsx @@ -4,7 +4,7 @@ import { scopeThreadRef, } from "@t3tools/client-runtime/environment"; import type { VcsStatusResult } from "@t3tools/contracts"; -import { CloudIcon, FolderGit2Icon, GitPullRequestIcon, TerminalIcon } from "lucide-react"; +import { FolderGit2Icon, GitPullRequestIcon, ServerIcon, TerminalIcon } from "lucide-react"; import { useMemo } from "react"; import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments"; import { useProject } from "../state/entities"; @@ -337,7 +337,7 @@ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSumma /> } > - + {threadEnvironmentLabel} From b868761ddbb2912297ae85c680fe89c457efee48 Mon Sep 17 00:00:00 2001 From: Jono Kemball Date: Fri, 24 Jul 2026 20:49:09 +1200 Subject: [PATCH 4/5] feat(web): only badge command palette projects that are remote-only Restrict the remote-environment badge to projects that live purely on a remote environment. When a project is also present locally (this device), it needs no "from" hint, so mixed and local-only projects render nothing. This matches the sidebar's remote-only badge behaviour. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/components/CommandPalette.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index f1c47ffd23ba..fc97a7e71b47 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -162,10 +162,15 @@ function getEnvironmentBrowsePlatform(os: string | null | undefined): string { // Badge showing which remote T3 Code environment a project lives on: a server // icon plus the environment label(s), matching SidebarV2's environment -// treatment. Local (this-device) projects have no remote, so nothing is -// rendered for them. +// treatment. Only rendered for projects that live purely on a remote +// environment — when a project is also present locally (this device), it needs +// no "from" hint, so mixed and local-only projects render nothing. function renderProjectRemoteBadge(group: SidebarProjectSnapshot | undefined): ReactNode { - if (!group || group.remoteEnvironmentLabels.length === 0) { + if ( + !group || + group.environmentPresence !== "remote-only" || + group.remoteEnvironmentLabels.length === 0 + ) { return undefined; } const label = group.remoteEnvironmentLabels.join(", "); From fac616b93e26ff49c0fe5cae295cc2e82d2d2ae6 Mon Sep 17 00:00:00 2001 From: Jono Kemball Date: Mon, 7 Sep 2026 12:10:07 +1200 Subject: [PATCH 5/5] feat(web): show project environments in palette search results --- apps/web/src/components/CommandPalette.tsx | 70 +++++++++++++--------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index ec1e79dc2d7b..1f43aecc88c7 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -1061,6 +1061,33 @@ function OpenCommandPaletteDialog(props: { ], ); + const renderProjectDescription = useCallback( + (project: Project) => { + const location = projectEnvironmentLocationById.get(project.environmentId) ?? { + kind: "remote", + label: "Remote", + machine: "server" as const, + }; + return ( + + + {location.kind === "remote" ? ( + + ) : null} + {location.label} + + + {project.workspaceRoot} + + ); + }, + [projectEnvironmentLocationById], + ); + const projectSearchItems = useMemo( () => buildProjectActionItems({ @@ -1068,14 +1095,24 @@ function OpenCommandPaletteDialog(props: { valuePrefix: "project", searchTerms: (project) => { const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); - return ( - group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? [] - ); + const location = projectEnvironmentLocationById.get(project.environmentId); + return [ + ...(group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? + []), + ...(location ? [location.label] : []), + ]; }, + renderDescription: renderProjectDescription, icon: projectFavicon, runProject: openProjectFromSearch, }), - [openProjectFromSearch, pickerProjects, projectGroupByTargetKey], + [ + openProjectFromSearch, + pickerProjects, + projectEnvironmentLocationById, + projectGroupByTargetKey, + renderProjectDescription, + ], ); const projectThreadItems = useMemo( @@ -1093,29 +1130,7 @@ function OpenCommandPaletteDialog(props: { ...(location ? [location.label] : []), ]; }, - renderDescription: (project) => { - const location = projectEnvironmentLocationById.get(project.environmentId) ?? { - kind: "remote", - label: "Remote", - machine: "server" as const, - }; - return ( - - - {location.kind === "remote" ? ( - - ) : null} - {location.label} - - - {project.workspaceRoot} - - ); - }, + renderDescription: renderProjectDescription, icon: projectFavicon, runProject: async (project) => { const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); @@ -1140,6 +1155,7 @@ function OpenCommandPaletteDialog(props: { pickerProjects, projectEnvironmentLocationById, projectGroupByTargetKey, + renderProjectDescription, ], );