mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(app): show project name in command center workspace search (#2345)
Searching the command center for a branch name like "master" returned a Workspaces list where every row looked identical: the title was the branch and the subtitle read "<hostname> · <branch>". On a single-host machine the hostname repeats on every row, so there was no way to tell which project each "master" workspace belonged to. Add the project name to the workspace subtitle (host · project · branch) and gate the host label behind multi-host, matching the Agents section. The host is dropped on single-host setups, so the subtitle reads "project · branch" and rows are distinguishable. Because searchText is derived from the subtitle, typing a project name now matches its workspaces too. Extract the shared join into joinSubtitleParts() and route both the workspace and agent subtitles through it, removing the duplicated filter/join the Agents section hand-rolled. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }) {
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{result.title}
|
||||
</Text>
|
||||
<Text style={styles.subtitle} numberOfLines={1}>
|
||||
<Text
|
||||
style={styles.subtitle}
|
||||
numberOfLines={1}
|
||||
testID="command-center-workspace-subtitle"
|
||||
>
|
||||
{result.subtitle}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user