diff --git a/packages/app/e2e/command-center-workspaces.spec.ts b/packages/app/e2e/command-center-workspaces.spec.ts
index e03c21a3f..d515d3669 100644
--- a/packages/app/e2e/command-center-workspaces.spec.ts
+++ b/packages/app/e2e/command-center-workspaces.spec.ts
@@ -55,9 +55,15 @@ test.describe("Command center workspaces", () => {
);
await expect(row).toBeVisible({ timeout: 30_000 });
await expect(row).toContainText(WORKSPACE_TITLE);
- await expect(row).toContainText(PRIMARY_HOST_LABEL);
await expect(row).toContainText(WORKSPACE_BRANCH);
+ // The subtitle disambiguates by project: host · project · branch (multi-host).
+ const subtitle = row.getByTestId("command-center-workspace-subtitle");
+ await expect(subtitle).toContainText(PRIMARY_HOST_LABEL);
+ await expect(subtitle).toContainText(seeded.projectDisplayName);
+ await expect(subtitle).toContainText(WORKSPACE_BRANCH);
+
+ // The agent subtitle is unchanged by the shared-helper refactor.
const agentRow = panel.getByTestId(`command-center-agent-${getServerId()}:${agent.id}`);
await expect(agentRow).toContainText(AGENT_TITLE);
await expect(agentRow).toContainText(PRIMARY_HOST_LABEL);
@@ -89,6 +95,10 @@ test.describe("Command center workspaces", () => {
await expect(agentRow).toBeVisible();
await expect(row).not.toBeVisible();
+ // The project name is now part of the workspace searchText.
+ await input.fill(seeded.projectDisplayName);
+ await expect(row).toBeVisible();
+
await input.fill(AGENT_TITLE);
await expect(agentRow).toBeVisible();
await expect(row).not.toBeVisible();
@@ -103,4 +113,38 @@ test.describe("Command center workspaces", () => {
await seeded.cleanup();
}
});
+
+ test("single-host workspace subtitle omits the host and shows project · branch", async ({
+ page,
+ }) => {
+ const seeded = await seedWorkspace({
+ repoPrefix: "command-center-workspace-single-",
+ title: WORKSPACE_TITLE,
+ });
+
+ try {
+ execFileSync("git", ["checkout", "-b", WORKSPACE_BRANCH], {
+ cwd: seeded.repoPath,
+ stdio: "ignore",
+ });
+ const refreshed = await seeded.client.checkoutRefresh(seeded.repoPath);
+ if (!refreshed.success) {
+ throw new Error(`Failed to refresh checkout: ${JSON.stringify(refreshed.error)}`);
+ }
+
+ // No secondary host: with a single host, the host label is gated away.
+ await gotoAppShell(page);
+
+ const panel = await openCommandCenter(page);
+ const row = panel.getByTestId(
+ `command-center-workspace-${getServerId()}:${seeded.workspaceId}`,
+ );
+ await expect(row).toBeVisible({ timeout: 30_000 });
+
+ const subtitle = row.getByTestId("command-center-workspace-subtitle");
+ await expect(subtitle).toHaveText(`${seeded.projectDisplayName} · ${WORKSPACE_BRANCH}`);
+ } finally {
+ await seeded.cleanup();
+ }
+ });
});
diff --git a/packages/app/src/command-center/command-center.tsx b/packages/app/src/command-center/command-center.tsx
index 584c80bfc..11d59048c 100644
--- a/packages/app/src/command-center/command-center.tsx
+++ b/packages/app/src/command-center/command-center.tsx
@@ -52,6 +52,7 @@ import type { CommandCenterContribution, CommandCenterIconProps } from "./contri
import { useCommandCenterActions, useCommandCenterContributions } from "./provider";
import {
buildContributionSections,
+ joinSubtitleParts,
moveActiveResultId,
preserveActiveResultId,
projectCommandCenterRows,
@@ -203,7 +204,7 @@ function useBuiltInSections(open: boolean, query: string): CommandCenterResultSe
const { t } = useTranslation();
const { agents } = useAggregatedAgents();
const { projects } = useProjects({ enabled: open });
- const showAgentHost = useHosts().length > 1;
+ const showHost = useHosts().length > 1;
return useMemo(() => {
if (!open) return [];
@@ -213,9 +214,11 @@ function useBuiltInSections(open: boolean, query: string): CommandCenterResultSe
for (const workspace of host.workspaces) {
if (workspace.archivingAt) continue;
const title = workspace.title ?? workspace.name;
- const subtitle = workspace.currentBranch
- ? `${host.serverName} · ${workspace.currentBranch}`
- : host.serverName;
+ const subtitle = joinSubtitleParts([
+ showHost ? host.serverName : null,
+ project.projectName,
+ workspace.currentBranch,
+ ]);
const searchText = `${title} ${subtitle}`.toLowerCase();
allWorkspaces.push({
kind: "workspace",
@@ -251,13 +254,11 @@ function useBuiltInSections(open: boolean, query: string): CommandCenterResultSe
? workspaceTitleByKey.get(`${agent.serverId}:${agent.workspaceId}`)
: undefined;
const location = workspaceTitle ?? shortenPath(agent.cwd);
- const subtitle = [
- showAgentHost ? agent.serverLabel : null,
+ const subtitle = joinSubtitleParts([
+ showHost ? agent.serverLabel : null,
location,
formatTimeAgo(agent.lastActivityAt),
- ]
- .filter((part): part is string => Boolean(part))
- .join(" · ");
+ ]);
return {
kind: "agent",
id: `agent:${agent.serverId}:${agent.id}`,
@@ -282,7 +283,7 @@ function useBuiltInSections(open: boolean, query: string): CommandCenterResultSe
},
{ id: "agents", rank: 3, title: t("shell.commandCenter.agents"), results: agentResults },
];
- }, [agents, open, projects, query, showAgentHost, t]);
+ }, [agents, open, projects, query, showHost, t]);
}
interface CommandCenterState {
@@ -461,7 +462,11 @@ function ResultContent({ result }: { result: CommandCenterResult }) {
{result.title}
-
+
{result.subtitle}
diff --git a/packages/app/src/command-center/results.test.ts b/packages/app/src/command-center/results.test.ts
index 3405a6a51..0bc8b7626 100644
--- a/packages/app/src/command-center/results.test.ts
+++ b/packages/app/src/command-center/results.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import type { CommandCenterContribution } from "./contributions";
import {
buildContributionSections,
+ joinSubtitleParts,
moveActiveResultId,
preserveActiveResultId,
projectCommandCenterRows,
@@ -103,3 +104,35 @@ describe("Command Center result projection", () => {
expect(projection.offsets[151]).toBe(32 + 150 * 56);
});
});
+
+describe("joinSubtitleParts", () => {
+ it("joins all present parts with a middle dot", () => {
+ expect(joinSubtitleParts(["a", "b", "c"])).toBe("a · b · c");
+ });
+
+ it("drops null and undefined parts", () => {
+ expect(joinSubtitleParts(["host", null, "master"])).toBe("host · master");
+ expect(joinSubtitleParts(["host", undefined, "master"])).toBe("host · master");
+ });
+
+ it("drops empty strings (Boolean parity — agents subtitle refactor guard)", () => {
+ expect(joinSubtitleParts(["", "paseo", "master"])).toBe("paseo · master");
+ });
+
+ it("returns an empty string when every part is null or empty", () => {
+ expect(joinSubtitleParts([null, undefined, ""])).toBe("");
+ });
+
+ it("returns a single part unchanged, with no separator", () => {
+ expect(joinSubtitleParts([null, "paseo", null])).toBe("paseo");
+ });
+
+ it("builds the workspace subtitle in host · project · branch order", () => {
+ // Single-host: host gated away, project leads.
+ expect(joinSubtitleParts([null, "paseo", "master"])).toBe("paseo · master");
+ // Multi-host: host first, then project, then branch.
+ expect(joinSubtitleParts(["host", "paseo", "master"])).toBe("host · paseo · master");
+ // No branch: degrades to project (or host · project).
+ expect(joinSubtitleParts([null, "paseo", null])).toBe("paseo");
+ });
+});
diff --git a/packages/app/src/command-center/results.ts b/packages/app/src/command-center/results.ts
index ab2f3a837..da3ea5c25 100644
--- a/packages/app/src/command-center/results.ts
+++ b/packages/app/src/command-center/results.ts
@@ -63,6 +63,11 @@ function matchesQuery(searchText: string, query: string): boolean {
return !normalized || searchText.includes(normalized);
}
+/** Join non-empty subtitle parts with " · ", dropping null/undefined/empty. */
+export function joinSubtitleParts(parts: readonly (string | null | undefined)[]): string {
+ return parts.filter((part): part is string => Boolean(part)).join(" · ");
+}
+
function contributionSearchText(contribution: CommandCenterContribution): string {
const presentationText =
contribution.presentation.kind === "action"