fix: use numeric workspace ID for worktree setup tracking and improve service list UI

This commit is contained in:
Mohamed Boudra
2026-04-05 18:16:31 +07:00
parent 2dd597f5f5
commit 8e482b1613
4 changed files with 64 additions and 18 deletions
@@ -365,15 +365,26 @@ function WorkspaceHoverCardContent({
backgroundColor: backgroundColor:
service.status === "running" service.status === "running"
? theme.colors.palette.green[500] ? theme.colors.palette.green[500]
: theme.colors.palette.red[500], : theme.colors.foregroundMuted,
}, },
]} ]}
/> />
<Text style={styles.serviceName} numberOfLines={1}> <Text
style={[
styles.serviceName,
{ color: service.status === "running" ? theme.colors.foreground : theme.colors.foregroundMuted },
]}
numberOfLines={1}
>
{service.serviceName} {service.serviceName}
</Text> </Text>
{service.url ? ( {service.url ? (
<ExternalLink size={12} color={theme.colors.foregroundMuted} /> <Text style={styles.serviceUrl} numberOfLines={1}>
{service.url.replace(/^https?:\/\//, "")}
</Text>
) : null}
{service.url ? (
<ExternalLink size={11} color={theme.colors.foregroundMuted} />
) : null} ) : null}
</Pressable> </Pressable>
))} ))}
@@ -482,8 +493,12 @@ const styles = StyleSheet.create((theme) => ({
flexShrink: 0, flexShrink: 0,
}, },
serviceName: { serviceName: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
flexShrink: 0,
},
serviceUrl: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
flex: 1, flex: 1,
minWidth: 0, minWidth: 0,
}, },
@@ -22,6 +22,7 @@ import {
EllipsisVertical, EllipsisVertical,
PanelRight, PanelRight,
RotateCw, RotateCw,
Settings,
SquarePen, SquarePen,
SquareTerminal, SquareTerminal,
X, X,
@@ -1363,6 +1364,23 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
} }
}, [currentBranchName, toast]); }, [currentBranchName, toast]);
const handleOpenSetupTab = useCallback(() => {
if (!persistenceKey) {
return;
}
const target = normalizeWorkspaceTabTarget({
kind: "setup",
workspaceId: normalizedWorkspaceId,
});
if (!target) {
return;
}
const tabId = openWorkspaceTab(persistenceKey, target);
if (tabId) {
focusWorkspaceTab(persistenceKey, tabId);
}
}, [focusWorkspaceTab, normalizedWorkspaceId, openWorkspaceTab, persistenceKey]);
const handleBulkCloseTabs = useCallback( const handleBulkCloseTabs = useCallback(
async (input: { tabsToClose: WorkspaceTabDescriptor[]; title: string; logLabel: string }) => { async (input: { tabsToClose: WorkspaceTabDescriptor[]; title: string; logLabel: string }) => {
const { tabsToClose, title, logLabel } = input; const { tabsToClose, title, logLabel } = input;
@@ -2021,6 +2039,14 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
Copy branch name Copy branch name
</DropdownMenuItem> </DropdownMenuItem>
) : null} ) : null}
<DropdownMenuSeparator />
<DropdownMenuItem
testID="workspace-header-show-setup"
leading={<Settings size={16} color={theme.colors.foregroundMuted} />}
onSelect={handleOpenSetupTab}
>
Show setup
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</View> </View>
@@ -146,6 +146,7 @@ describe("createPaseoWorktreeInBackground", () => {
{ {
requestCwd: repoDir, requestCwd: repoDir,
repoRoot: repoDir, repoRoot: repoDir,
workspaceId: 42,
worktree: { worktree: {
branchName: "feature-no-setup", branchName: "feature-no-setup",
worktreePath, worktreePath,
@@ -160,7 +161,7 @@ describe("createPaseoWorktreeInBackground", () => {
); );
expect(progressMessages).toHaveLength(2); expect(progressMessages).toHaveLength(2);
expect(progressMessages[0]?.payload).toMatchObject({ expect(progressMessages[0]?.payload).toMatchObject({
workspaceId: worktreePath, workspaceId: "42",
status: "running", status: "running",
error: null, error: null,
detail: { detail: {
@@ -172,7 +173,7 @@ describe("createPaseoWorktreeInBackground", () => {
}, },
}); });
expect(progressMessages[1]?.payload).toMatchObject({ expect(progressMessages[1]?.payload).toMatchObject({
workspaceId: worktreePath, workspaceId: "42",
status: "completed", status: "completed",
error: null, error: null,
detail: { detail: {
@@ -183,7 +184,7 @@ describe("createPaseoWorktreeInBackground", () => {
commands: [], commands: [],
}, },
}); });
expect(snapshots.get(worktreePath)).toMatchObject({ expect(snapshots.get("42")).toMatchObject({
status: "completed", status: "completed",
error: null, error: null,
detail: { detail: {
@@ -274,7 +275,7 @@ describe("createPaseoWorktreeInBackground", () => {
expect(progressMessages[1]?.payload.status).toBe("failed"); expect(progressMessages[1]?.payload.status).toBe("failed");
expect(progressMessages[1]?.payload.error).toContain("Failed to parse paseo.json"); expect(progressMessages[1]?.payload.error).toContain("Failed to parse paseo.json");
expect(progressMessages[1]?.payload.detail.commands).toEqual([]); expect(progressMessages[1]?.payload.detail.commands).toEqual([]);
expect(snapshots.get(worktreePath)).toMatchObject({ expect(snapshots.get("101")).toMatchObject({
status: "failed", status: "failed",
error: expect.stringContaining("Failed to parse paseo.json"), error: expect.stringContaining("Failed to parse paseo.json"),
}); });
@@ -323,6 +324,7 @@ describe("createPaseoWorktreeInBackground", () => {
{ {
requestCwd: repoDir, requestCwd: repoDir,
repoRoot: repoDir, repoRoot: repoDir,
workspaceId: 43,
worktree: { worktree: {
branchName: "feature-running-setup", branchName: "feature-running-setup",
worktreePath, worktreePath,
@@ -337,7 +339,7 @@ describe("createPaseoWorktreeInBackground", () => {
); );
expect(progressMessages.length).toBeGreaterThan(1); expect(progressMessages.length).toBeGreaterThan(1);
expect(progressMessages[0]?.payload).toMatchObject({ expect(progressMessages[0]?.payload).toMatchObject({
workspaceId: worktreePath, workspaceId: "43",
status: "running", status: "running",
error: null, error: null,
detail: { detail: {
@@ -368,7 +370,7 @@ describe("createPaseoWorktreeInBackground", () => {
}); });
expect(progressMessages.at(-1)?.payload).toMatchObject({ expect(progressMessages.at(-1)?.payload).toMatchObject({
workspaceId: worktreePath, workspaceId: "43",
status: "completed", status: "completed",
error: null, error: null,
detail: { detail: {
@@ -385,7 +387,7 @@ describe("createPaseoWorktreeInBackground", () => {
status: "completed", status: "completed",
exitCode: 0, exitCode: 0,
}); });
expect(snapshots.get(worktreePath)).toMatchObject({ expect(snapshots.get("43")).toMatchObject({
status: "completed", status: "completed",
error: null, error: null,
}); });
@@ -439,6 +441,7 @@ describe("createPaseoWorktreeInBackground", () => {
{ {
requestCwd: repoDir, requestCwd: repoDir,
repoRoot: repoDir, repoRoot: repoDir,
workspaceId: 44,
worktree: { worktree: {
branchName: "reused-worktree", branchName: "reused-worktree",
worktreePath: existingWorktree.worktreePath, worktreePath: existingWorktree.worktreePath,
@@ -453,12 +456,12 @@ describe("createPaseoWorktreeInBackground", () => {
); );
expect(progressMessages).toHaveLength(2); expect(progressMessages).toHaveLength(2);
expect(progressMessages[0]?.payload).toMatchObject({ expect(progressMessages[0]?.payload).toMatchObject({
workspaceId: existingWorktree.worktreePath, workspaceId: "44",
status: "running", status: "running",
error: null, error: null,
}); });
expect(progressMessages[1]?.payload).toMatchObject({ expect(progressMessages[1]?.payload).toMatchObject({
workspaceId: existingWorktree.worktreePath, workspaceId: "44",
status: "completed", status: "completed",
error: null, error: null,
detail: { detail: {
@@ -487,7 +490,7 @@ describe("createPaseoWorktreeInBackground", () => {
readFileSync(path.join(existingWorktree.worktreePath, "README.md"), "utf8"), readFileSync(path.join(existingWorktree.worktreePath, "README.md"), "utf8"),
).toContain("hello"); ).toContain("hello");
expect(() => readFileSync(path.join(existingWorktree.worktreePath, "setup-ran.txt"), "utf8")).toThrow(); expect(() => readFileSync(path.join(existingWorktree.worktreePath, "setup-ran.txt"), "utf8")).toThrow();
expect(snapshots.get(existingWorktree.worktreePath)).toMatchObject({ expect(snapshots.get("44")).toMatchObject({
status: "completed", status: "completed",
error: null, error: null,
}); });
@@ -544,6 +547,7 @@ describe("createPaseoWorktreeInBackground", () => {
{ {
requestCwd: repoDir, requestCwd: repoDir,
repoRoot: repoDir, repoRoot: repoDir,
workspaceId: 45,
worktree: { worktree: {
branchName: "feature-service-failure", branchName: "feature-service-failure",
worktreePath, worktreePath,
@@ -572,7 +576,7 @@ describe("createPaseoWorktreeInBackground", () => {
}), }),
"Failed to spawn worktree services after workspace setup completed", "Failed to spawn worktree services after workspace setup completed",
); );
expect(snapshots.get(worktreePath)).toMatchObject({ expect(snapshots.get("45")).toMatchObject({
status: "completed", status: "completed",
error: null, error: null,
}); });
@@ -625,6 +629,7 @@ describe("createPaseoWorktreeInBackground", () => {
{ {
requestCwd: repoDir, requestCwd: repoDir,
repoRoot: repoDir, repoRoot: repoDir,
workspaceId: 46,
worktree: { worktree: {
branchName: "feature-socket-mode", branchName: "feature-socket-mode",
worktreePath, worktreePath,
@@ -646,7 +651,7 @@ describe("createPaseoWorktreeInBackground", () => {
expect(terminalManager.terminals[0]?.env?.PORT).toEqual(expect.any(String)); expect(terminalManager.terminals[0]?.env?.PORT).toEqual(expect.any(String));
expect(terminalManager.terminals[0]?.env?.PASEO_SERVICE_URL).toBeUndefined(); expect(terminalManager.terminals[0]?.env?.PASEO_SERVICE_URL).toBeUndefined();
expect(terminalManager.terminals[0]?.sent).toEqual(["npm run dev\r"]); expect(terminalManager.terminals[0]?.sent).toEqual(["npm run dev\r"]);
expect(snapshots.get(worktreePath)).toMatchObject({ expect(snapshots.get("46")).toMatchObject({
status: "completed", status: "completed",
error: null, error: null,
}); });
@@ -644,7 +644,7 @@ export async function handleWorkspaceSetupStatusRequest(
dependencies: HandleWorkspaceSetupStatusRequestDependencies, dependencies: HandleWorkspaceSetupStatusRequestDependencies,
request: Extract<SessionInboundMessage, { type: "workspace_setup_status_request" }>, request: Extract<SessionInboundMessage, { type: "workspace_setup_status_request" }>,
): Promise<void> { ): Promise<void> {
const workspaceId = normalizePersistedWorkspaceId(request.workspaceId); const workspaceId = request.workspaceId;
dependencies.emit({ dependencies.emit({
type: "workspace_setup_status_response", type: "workspace_setup_status_response",
payload: { payload: {
@@ -669,7 +669,7 @@ export async function createPaseoWorktreeInBackground(
let setupResults: WorktreeSetupCommandResult[] = []; let setupResults: WorktreeSetupCommandResult[] = [];
let setupStarted = false; let setupStarted = false;
const progressAccumulator = createWorktreeSetupProgressAccumulator(); const progressAccumulator = createWorktreeSetupProgressAccumulator();
const workspaceId = normalizePersistedWorkspaceId(worktree.worktreePath); const workspaceId = String(options.workspaceId);
const emitSetupProgress = (status: "running" | "completed" | "failed", error: string | null) => { const emitSetupProgress = (status: "running" | "completed" | "failed", error: string | null) => {
const snapshot: WorkspaceSetupSnapshot = { const snapshot: WorkspaceSetupSnapshot = {