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], 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/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx index 1c6fce104..65e208dca 100644 --- a/packages/app/src/components/combined-model-selector.tsx +++ b/packages/app/src/components/combined-model-selector.tsx @@ -644,7 +644,9 @@ export function CombinedModelSelector({ ) : ( <> - {triggerLabel} + + {triggerLabel} + )} @@ -714,6 +716,8 @@ export function CombinedModelSelector({ const styles = StyleSheet.create((theme) => ({ trigger: { height: 28, + minWidth: 0, + flexShrink: 1, flexDirection: "row", alignItems: "center", backgroundColor: "transparent", @@ -731,6 +735,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 9fe2400d7..2a3d24f6d 100644 --- a/packages/app/src/components/message-input.tsx +++ b/packages/app/src/components/message-input.tsx @@ -1333,11 +1333,15 @@ const styles = StyleSheet.create(((theme: any) => ({ marginHorizontal: -6, }, 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], diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index b182f82b5..85380d161 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -302,7 +302,7 @@ function WorkspaceStatusIndicator({ if (shouldShowSyncedLoader) { return ( - + ); } @@ -406,7 +406,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-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 98b12eda6..f3c034709 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -146,6 +146,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"; @@ -773,6 +785,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 = @@ -2053,27 +2071,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: { + xs: 1, + md: 0, + }, + flexDirection: { + xs: "column", + md: "row", + }, + alignItems: { + xs: "flex-start", + md: "center", + }, + justifyContent: "flex-start", + 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: { diff --git a/packages/app/src/screens/workspace/workspace-tab-presentation.tsx b/packages/app/src/screens/workspace/workspace-tab-presentation.tsx index c5eb42922..f443022c2 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 774831a2c..48a9addd4 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 diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index b7130ab48..33f35c2e4 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -113,6 +113,7 @@ import type { StoredAgentRecord } from "./agent/agent-storage.js"; import type { AgentStorage } from "./agent/agent-storage.js"; import { AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js"; import { + checkoutLiteFromGitSnapshot, normalizeWorkspaceId as normalizePersistedWorkspaceId, deriveWorkspaceId, deriveProjectRootPath, @@ -5716,8 +5717,14 @@ export class Session { if (!snapshot) { return base; } + + const checkout = checkoutLiteFromGitSnapshot(workspace.cwd, snapshot.git); + const displayName = deriveWorkspaceDisplayName({ cwd: workspace.cwd, checkout }); + return { ...base, + name: displayName, + diffStat: snapshot.git.diffStat ?? null, gitRuntime: this.buildWorkspaceGitRuntimePayload(snapshot) ?? undefined, 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 8725e5dd8..09bbcc9f8 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -1890,9 +1890,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: () => {}, @@ -1946,8 +1944,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", @@ -1998,9 +1995,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, @@ -2053,8 +2048,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> {