From be3d5ed78dbb8f0a0ee2be47ef8cf816ada1c9e4 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 14 Apr 2026 03:54:32 +0700 Subject: [PATCH 1/6] fix: use cached git service snapshots for fetchWorkspaces instead of shelling out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit describeWorkspaceRecordWithGitData was calling buildProjectPlacement → getCheckoutStatusLite which spawned 3-4 sequential git child processes per workspace. With 8 active workspaces and event loop pressure from running agents, this caused fetchWorkspaces to take 35-45s. Now uses workspaceGitService.getSnapshot() which returns instantly from cache (warm path) or awaits the git service's own concurrency-managed refresh (cold path). --- packages/server/src/server/session.ts | 22 +++------- .../src/server/session.workspaces.test.ts | 14 ++---- .../src/server/workspace-registry-model.ts | 44 +++++++++++++++++++ 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index e7d7d194a..914c64d1d 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -114,6 +114,7 @@ import type { import { AgentStorage, type StoredAgentRecord } from "./agent/agent-storage.js"; import { buildProjectPlacementForCwd, + checkoutLiteFromGitSnapshot, detectStaleWorkspaces, deriveProjectKind, deriveProjectRootPath, @@ -5406,26 +5407,17 @@ export class Session { projectRecord?: PersistedProjectRecord | null, ): Promise { const base = await this.describeWorkspaceRecord(workspace, projectRecord); - let displayName = workspace.displayName; - try { - const placement = await this.buildProjectPlacement(workspace.cwd); - displayName = deriveWorkspaceDisplayName({ - cwd: workspace.cwd, - checkout: placement.checkout, - }); - } catch { - // Fall back to the persisted label if checkout metadata is unavailable. - } + const snapshot = await this.workspaceGitService.getSnapshot(workspace.cwd); - let snapshot: WorkspaceGitRuntimeSnapshot | null = null; - snapshot = this.workspaceGitService.peekSnapshot(workspace.cwd); + const checkout = checkoutLiteFromGitSnapshot(workspace.cwd, snapshot.git); + const displayName = deriveWorkspaceDisplayName({ cwd: workspace.cwd, checkout }); return { ...base, name: displayName, - diffStat: snapshot?.git.diffStat ?? null, - gitRuntime: snapshot ? this.buildWorkspaceGitRuntimePayload(snapshot) : undefined, - githubRuntime: snapshot ? this.buildWorkspaceGitHubRuntimePayload(snapshot) : undefined, + diffStat: snapshot.git.diffStat ?? null, + gitRuntime: this.buildWorkspaceGitRuntimePayload(snapshot), + githubRuntime: this.buildWorkspaceGitHubRuntimePayload(snapshot), }; } diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 5dbb8b140..464c66fb6 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -1845,9 +1845,7 @@ describe("workspace aggregation", () => { }); const workspaceGitService = createNoopWorkspaceGitService(); workspaceGitService.peekSnapshot = vi.fn(() => runtimeSnapshot); - workspaceGitService.getSnapshot = vi.fn(async () => { - throw new Error("fetch_workspaces should not trigger per-workspace refreshes"); - }); + workspaceGitService.getSnapshot = vi.fn(async () => runtimeSnapshot); workspaceGitService.subscribe = vi.fn(async () => ({ initial: runtimeSnapshot, unsubscribe: () => {}, @@ -1901,8 +1899,7 @@ describe("workspace aggregation", () => { | { type: "fetch_workspaces_response"; payload: any } | undefined; - expect(workspaceGitService.peekSnapshot).toHaveBeenCalledWith("/tmp/repo"); - expect(workspaceGitService.getSnapshot).not.toHaveBeenCalled(); + expect(workspaceGitService.getSnapshot).toHaveBeenCalledWith("/tmp/repo"); expect(response?.payload.entries).toEqual([ expect.objectContaining({ id: "/tmp/repo", @@ -1953,9 +1950,7 @@ describe("workspace aggregation", () => { }); const workspaceGitService = createNoopWorkspaceGitService(); workspaceGitService.peekSnapshot = vi.fn(() => runtimeSnapshot); - workspaceGitService.getSnapshot = vi.fn(async () => { - throw new Error("workspace updates should use passive workspace git snapshots"); - }); + workspaceGitService.getSnapshot = vi.fn(async () => runtimeSnapshot); const session = createSessionForWorkspaceTests({ workspaceGitService, @@ -2008,8 +2003,7 @@ describe("workspace aggregation", () => { skipReconcile: true, }); - expect(workspaceGitService.peekSnapshot).toHaveBeenCalledWith("/tmp/repo"); - expect(workspaceGitService.getSnapshot).not.toHaveBeenCalled(); + expect(workspaceGitService.getSnapshot).toHaveBeenCalledWith("/tmp/repo"); expect(emitted).toContainEqual({ type: "workspace_update", payload: { diff --git a/packages/server/src/server/workspace-registry-model.ts b/packages/server/src/server/workspace-registry-model.ts index 43dde14fa..545095e71 100644 --- a/packages/server/src/server/workspace-registry-model.ts +++ b/packages/server/src/server/workspace-registry-model.ts @@ -137,6 +137,50 @@ export function deriveWorkspaceKind(checkout: ProjectCheckoutLitePayload): Persi return checkout.isPaseoOwnedWorktree ? "worktree" : "local_checkout"; } +export function checkoutLiteFromGitSnapshot( + cwd: string, + git: { + isGit: boolean; + currentBranch: string | null; + remoteUrl: string | null; + repoRoot: string | null; + isPaseoOwnedWorktree: boolean; + mainRepoRoot: string | null; + }, +): ProjectCheckoutLitePayload { + if (!git.isGit) { + return { + cwd, + isGit: false, + currentBranch: null, + remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }; + } + if (git.isPaseoOwnedWorktree && git.mainRepoRoot) { + return { + cwd, + isGit: true, + currentBranch: git.currentBranch, + remoteUrl: git.remoteUrl, + worktreeRoot: git.repoRoot ?? cwd, + isPaseoOwnedWorktree: true, + mainRepoRoot: git.mainRepoRoot, + }; + } + return { + cwd, + isGit: true, + currentBranch: git.currentBranch, + remoteUrl: git.remoteUrl, + worktreeRoot: git.repoRoot ?? cwd, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }; +} + export async function detectStaleWorkspaces( input: DetectStaleWorkspacesInput, ): Promise> { From 6ed166641f1c86b092e0d1c06f50dbbdaa2efc9e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 14 Apr 2026 10:26:28 +0700 Subject: [PATCH 2/6] Fix mobile model selector overflow --- packages/app/src/components/combined-model-selector.tsx | 8 +++++++- packages/app/src/components/message-input.tsx | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx index 2d4916025..588db0704 100644 --- a/packages/app/src/components/combined-model-selector.tsx +++ b/packages/app/src/components/combined-model-selector.tsx @@ -634,7 +634,9 @@ export function CombinedModelSelector({ ) : ( <> - {triggerLabel} + + {triggerLabel} + )} @@ -704,6 +706,8 @@ export function CombinedModelSelector({ const styles = StyleSheet.create((theme) => ({ trigger: { height: 28, + minWidth: 0, + flexShrink: 1, flexDirection: "row", alignItems: "center", backgroundColor: "transparent", @@ -721,6 +725,8 @@ const styles = StyleSheet.create((theme) => ({ opacity: 0.5, }, triggerText: { + minWidth: 0, + flexShrink: 1, color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, diff --git a/packages/app/src/components/message-input.tsx b/packages/app/src/components/message-input.tsx index fbe4cb3d5..2b5396493 100644 --- a/packages/app/src/components/message-input.tsx +++ b/packages/app/src/components/message-input.tsx @@ -1290,11 +1290,15 @@ const styles = StyleSheet.create(((theme: any) => ({ justifyContent: "space-between", }, leftButtonGroup: { + minWidth: 0, + flexShrink: 1, + flexGrow: 1, flexDirection: "row", alignItems: "flex-end", gap: theme.spacing[1], }, rightButtonGroup: { + flexShrink: 0, flexDirection: "row", alignItems: "center", gap: isWeb ? theme.spacing[2] : theme.spacing[1], From 9c0b5616fecec64f4bb5db8038e8ac6a254c1f1e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 14 Apr 2026 11:34:50 +0700 Subject: [PATCH 3/6] Refine compact workspace header layout --- .../app/src/components/branch-switcher.tsx | 8 ++- .../screens/workspace/workspace-screen.tsx | 68 +++++++++++++++---- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/packages/app/src/components/branch-switcher.tsx b/packages/app/src/components/branch-switcher.tsx index 432849389..f9ce26bf9 100644 --- a/packages/app/src/components/branch-switcher.tsx +++ b/packages/app/src/components/branch-switcher.tsx @@ -4,6 +4,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { ChevronDown, GitBranch } from "lucide-react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox"; +import { useIsCompactFormFactor } from "@/constants/layout"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { useToast } from "@/contexts/toast-context"; import { useBranchSwitcher } from "@/hooks/use-branch-switcher"; @@ -24,6 +25,7 @@ export function BranchSwitcher({ isGitCheckout, }: BranchSwitcherProps) { const { theme } = useUnistyles(); + const isCompact = useIsCompactFormFactor(); const anchorRef = useRef(null); const client = useHostRuntimeClient(serverId); const isConnected = useHostRuntimeIsConnected(serverId); @@ -65,7 +67,7 @@ export function BranchSwitcher({ {title} - + {!isCompact ? : null} ({ flexDirection: "row", alignItems: "center", gap: theme.spacing[1], + marginLeft: { + xs: -theme.spacing[2], + md: 0, + }, paddingVertical: theme.spacing[1], paddingHorizontal: theme.spacing[2], borderRadius: theme.borderRadius.md, diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index c16090e9f..cb8745ecb 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -141,6 +141,18 @@ function decodeSegment(value: string): string { } } +function areHeaderLabelsEquivalent( + a: string | null | undefined, + b: string | null | undefined, +): boolean { + const normalizedA = trimNonEmpty(a)?.toLocaleLowerCase(); + const normalizedB = trimNonEmpty(b)?.toLocaleLowerCase(); + if (!normalizedA || !normalizedB) { + return false; + } + return normalizedA === normalizedB; +} + function getFallbackTabOptionLabel(tab: WorkspaceTabDescriptor): string { if (tab.target.kind === "draft") { return "New Agent"; @@ -747,6 +759,12 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) ? resolveWorkspaceHeader({ workspace: workspaceDescriptor }) : null; const isWorkspaceHeaderLoading = workspaceHeader === null; + const workspaceHeaderTitle = workspaceHeader?.title ?? ""; + const workspaceHeaderSubtitle = workspaceHeader?.subtitle ?? ""; + const shouldShowWorkspaceHeaderSubtitle = !areHeaderLabelsEquivalent( + workspaceHeaderTitle, + workspaceHeaderSubtitle, + ); const isGitCheckout = checkoutQuery.data?.isGit ?? false; const currentBranchName = @@ -1949,27 +1967,29 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) {isWorkspaceHeaderLoading ? ( - <> + - + ) : ( - <> + - - {workspaceHeader.subtitle} - - + {shouldShowWorkspaceHeaderSubtitle ? ( + + {workspaceHeaderSubtitle} + + ) : null} + )} ({ flexShrink: 1, }, headerTitleContainer: { + flex: 1, flexShrink: 1, minWidth: 0, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, + headerTitleTextGroup: { + minWidth: 0, + flexShrink: 1, + flexGrow: 1, + flexDirection: { + xs: "column", + md: "row", + }, + alignItems: { + xs: "flex-start", + md: "center", + }, + justifyContent: "center", + gap: { + xs: 0, + md: theme.spacing[2], + }, + }, headerProjectTitle: { color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.base, + fontSize: { + xs: theme.fontSize.sm, + md: theme.fontSize.base, + }, flexShrink: 1, }, headerTitleSkeleton: { From 5329a12222e72c5da103c69c095dfda4d0f766aa Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 14 Apr 2026 11:36:50 +0700 Subject: [PATCH 4/6] Fix compact status bar model overflow --- packages/app/src/components/agent-status-bar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx index f79392b9d..e817f9dce 100644 --- a/packages/app/src/components/agent-status-bar.tsx +++ b/packages/app/src/components/agent-status-bar.tsx @@ -1225,6 +1225,8 @@ const styles = StyleSheet.create((theme) => ({ }, prefsButton: { height: 28, + minWidth: 0, + flexShrink: 1, flexDirection: "row", alignItems: "center", gap: theme.spacing[1], From 8d854a634240af62ef425bddcc6786c48744c60b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 14 Apr 2026 12:23:56 +0700 Subject: [PATCH 5/6] Fix desktop workspace header alignment --- packages/app/src/screens/workspace/workspace-screen.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index cb8745ecb..6054887d6 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -2321,7 +2321,10 @@ const styles = StyleSheet.create((theme) => ({ headerTitleTextGroup: { minWidth: 0, flexShrink: 1, - flexGrow: 1, + flexGrow: { + xs: 1, + md: 0, + }, flexDirection: { xs: "column", md: "row", @@ -2330,7 +2333,7 @@ const styles = StyleSheet.create((theme) => ({ xs: "flex-start", md: "center", }, - justifyContent: "center", + justifyContent: "flex-start", gap: { xs: 0, md: theme.spacing[2], From 202fa3e8f32b2488fff88986cfdff454357d6fa5 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 14 Apr 2026 13:33:16 +0700 Subject: [PATCH 6/6] Improve synced loader visibility in light mode --- packages/app/src/components/sidebar-workspace-list.tsx | 4 ++-- packages/app/src/components/synced-loader.tsx | 2 +- .../app/src/screens/workspace/workspace-tab-presentation.tsx | 2 +- packages/app/src/styles/theme.ts | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index 0e88ce3d8..6b9eca15a 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -236,7 +236,7 @@ function WorkspaceStatusIndicator({ if (shouldShowSyncedLoader) { return ( - + ); } @@ -340,7 +340,7 @@ function ProjectLeadingVisual({ if (shouldShowSyncedLoader) { return ( - + ); } diff --git a/packages/app/src/components/synced-loader.tsx b/packages/app/src/components/synced-loader.tsx index 7697f3553..d0257de4d 100644 --- a/packages/app/src/components/synced-loader.tsx +++ b/packages/app/src/components/synced-loader.tsx @@ -16,7 +16,7 @@ const DOT_COUNT = DOT_SEQUENCE.length; const GRID_ROWS = 3; const GRID_COLUMNS = 2; const SNAKE_SEGMENT_OFFSETS = [0, -1, -2, -3, -4] as const; -const SNAKE_OPACITIES = [1, 0.72, 0.46, 0.22, 0] as const; +const SNAKE_OPACITIES = [1, 0.78, 0.56, 0.34, 0] as const; const sharedStepProgress = makeMutable(0); let sharedLoopStarted = false; diff --git a/packages/app/src/screens/workspace/workspace-tab-presentation.tsx b/packages/app/src/screens/workspace/workspace-tab-presentation.tsx index 694639e14..3cd9b817e 100644 --- a/packages/app/src/screens/workspace/workspace-tab-presentation.tsx +++ b/packages/app/src/screens/workspace/workspace-tab-presentation.tsx @@ -134,7 +134,7 @@ export function WorkspaceTabIcon({ if (shouldShowLoader) { return ( - + ); } diff --git a/packages/app/src/styles/theme.ts b/packages/app/src/styles/theme.ts index 29f413fd7..8cbc8bd29 100644 --- a/packages/app/src/styles/theme.ts +++ b/packages/app/src/styles/theme.ts @@ -83,6 +83,7 @@ export const baseColors = { // Amber scale amber: { 500: "#f59e0b", + 700: "#b45309", }, // Yellow scale