diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx
index 830a86455..b5680f3ef 100644
--- a/packages/app/src/components/agent-list.tsx
+++ b/packages/app/src/components/agent-list.tsx
@@ -158,24 +158,35 @@ function SessionRow({
) : null}
-
-
+ {isMobile && (
+
+
+ {projectPath}
+
+ ·
+ {statusLabel}
+ ·
+ {timeAgo}
+ {agent.serverLabel ? (
+ <>
+ ·
+
+ {agent.serverLabel}
+
+ >
+ ) : null}
+
+ )}
+
+ {!isMobile && (
+ <>
+
{projectPath}
- ·
- {statusLabel}
- ·
- {timeAgo}
- {agent.serverLabel ? (
- <>
- ·
-
- {agent.serverLabel}
-
- >
- ) : null}
-
-
+ {statusLabel}
+ {timeAgo}
+ >
+ )}
{isMobile && agent.requiresAttention ? (
@@ -202,7 +213,6 @@ function SessionTableSection({
{section.title}
-
@@ -422,22 +432,24 @@ const styles = StyleSheet.create((theme) => ({
},
sectionTitle: {
fontSize: theme.fontSize.sm,
- fontWeight: '600',
+ fontWeight: theme.fontWeight.medium,
color: theme.colors.foregroundMuted,
- textTransform: 'uppercase',
- letterSpacing: 0.6,
- },
- sectionLine: {
- flex: 1,
- height: StyleSheet.hairlineWidth,
- backgroundColor: theme.colors.surface2,
},
listCard: {
- overflow: 'hidden',
- borderRadius: theme.borderRadius.lg,
+ overflow: {
+ xs: 'hidden' as const,
+ md: 'visible' as const,
+ },
+ borderRadius: {
+ xs: theme.borderRadius.lg,
+ md: 0,
+ },
},
rowDivider: {
- borderTopWidth: StyleSheet.hairlineWidth,
+ borderTopWidth: {
+ xs: StyleSheet.hairlineWidth,
+ md: 0,
+ },
borderTopColor: theme.colors.border,
},
row: {
@@ -445,8 +457,14 @@ const styles = StyleSheet.create((theme) => ({
alignItems: 'center',
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
- borderRadius: theme.borderRadius.lg,
- marginBottom: theme.spacing[1],
+ borderRadius: {
+ xs: theme.borderRadius.lg,
+ md: 0,
+ },
+ marginBottom: {
+ xs: theme.spacing[1],
+ md: 0,
+ },
},
rowLeading: {
marginRight: theme.spacing[3],
@@ -500,6 +518,21 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
opacity: 0.7,
},
+ columnMeta: {
+ fontSize: theme.fontSize.sm,
+ color: theme.colors.foregroundMuted,
+ flexShrink: 1,
+ minWidth: 60,
+ maxWidth: 200,
+ marginLeft: theme.spacing[4],
+ },
+ columnMetaFixed: {
+ fontSize: theme.fontSize.sm,
+ color: theme.colors.foregroundMuted,
+ flexShrink: 0,
+ width: 72,
+ textAlign: 'right' as const,
+ },
badge: {
paddingHorizontal: theme.spacing[2],
paddingVertical: theme.spacing[1],
@@ -514,10 +547,8 @@ const styles = StyleSheet.create((theme) => ({
},
badgeText: {
fontSize: theme.fontSize.xs,
- fontWeight: '600',
+ fontWeight: theme.fontWeight.medium,
color: theme.colors.foregroundMuted,
- textTransform: 'uppercase',
- letterSpacing: 0.4,
},
badgeTextWarning: {
color: theme.colors.palette.amber[500],
diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx
index 3a128d789..7f27146c4 100644
--- a/packages/app/src/components/sidebar-workspace-list.tsx
+++ b/packages/app/src/components/sidebar-workspace-list.tsx
@@ -23,7 +23,8 @@ import {
import { router, usePathname } from 'expo-router'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
import { type GestureType } from 'react-native-gesture-handler'
-import { ChevronDown, ChevronRight, Plus } from 'lucide-react-native'
+import * as Clipboard from 'expo-clipboard'
+import { ChevronDown, ChevronRight, MoreVertical, Plus } from 'lucide-react-native'
import { NestableScrollContainer } from 'react-native-draggable-flatlist'
import { DraggableList, type DraggableRenderItemInfo } from './draggable-list'
import type { DraggableListDragHandleProps } from './draggable-list.types'
@@ -50,6 +51,12 @@ import {
ContextMenuTrigger,
useContextMenu,
} from '@/components/ui/context-menu'
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+} from '@/components/ui/dropdown-menu'
import { SyncedLoader } from '@/components/synced-loader'
import { useToast } from '@/contexts/toast-context'
import { useCheckoutGitActionsStore } from '@/stores/checkout-git-actions-store'
@@ -112,6 +119,11 @@ interface WorkspaceRowInnerProps {
isArchiving: boolean
dragHandleProps?: DraggableListDragHandleProps
menuController: ReturnType | null
+ archiveLabel?: string
+ archiveStatus?: 'idle' | 'pending' | 'success'
+ archivePendingLabel?: string
+ onArchive?: () => void
+ onCopyPath?: () => void
}
function resolveWorkspaceCreatedAtLabel(workspace: SidebarWorkspaceEntry): string | null {
@@ -601,7 +613,13 @@ function WorkspaceRowInner({
isArchiving,
dragHandleProps,
menuController,
+ archiveLabel,
+ archiveStatus = 'idle',
+ archivePendingLabel,
+ onArchive,
+ onCopyPath,
}: WorkspaceRowInnerProps) {
+ const { theme } = useUnistyles()
const createdAtLabel = resolveWorkspaceCreatedAtLabel(workspace)
const interaction = useLongPressDragInteraction({
drag,
@@ -617,83 +635,89 @@ function WorkspaceRowInner({
onPress()
}, [interaction.didLongPressRef, onPress])
- const rowChildren = (
- <>
-
-
-
- {workspace.name}
-
-
-
- {createdAtLabel ? (
-
- {createdAtLabel}
-
- ) : null}
- {showShortcutBadge && shortcutNumber !== null ? (
-
- {shortcutNumber}
-
- ) : null}
-
- >
- )
-
- const trigger = menuController ? (
- [
- styles.workspaceRow,
- isDragging && styles.workspaceRowDragging,
- selected && styles.sidebarRowSelected,
- hovered && styles.workspaceRowHovered,
- pressed && styles.workspaceRowPressed,
- ]}
- onPressIn={interaction.handlePressIn}
- onTouchMove={interaction.handleTouchMove}
- onPressOut={interaction.handlePressOut}
- onPress={handlePress}
- testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
- >
- {rowChildren}
-
- ) : (
- [
- styles.workspaceRow,
- isDragging && styles.workspaceRowDragging,
- selected && styles.sidebarRowSelected,
- hovered && styles.workspaceRowHovered,
- pressed && styles.workspaceRowPressed,
- ]}
- onPressIn={interaction.handlePressIn}
- onTouchMove={interaction.handleTouchMove}
- onPressOut={interaction.handlePressOut}
- onPress={handlePress}
- testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
- >
- {rowChildren}
-
- )
-
- const content = trigger
-
return (
- {content}
+ [
+ styles.workspaceRow,
+ isDragging && styles.workspaceRowDragging,
+ selected && styles.sidebarRowSelected,
+ hovered && styles.workspaceRowHovered,
+ pressed && styles.workspaceRowPressed,
+ ]}
+ onPressIn={interaction.handlePressIn}
+ onTouchMove={interaction.handleTouchMove}
+ onPressOut={interaction.handlePressOut}
+ onPress={handlePress}
+ testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
+ >
+
+
+
+ {workspace.name}
+
+
+
+ {workspace.diffStat ? (
+
+ +{workspace.diffStat.additions}
+ -{workspace.diffStat.deletions}
+
+ ) : createdAtLabel ? (
+
+ {createdAtLabel}
+
+ ) : null}
+ {showShortcutBadge && shortcutNumber !== null ? (
+
+ {shortcutNumber}
+
+ ) : null}
+ {onArchive ? (
+
+
+
+
+
+ {onCopyPath ? (
+
+ Copy path
+
+ ) : null}
+
+ {archiveLabel ?? 'Archive'}
+
+
+
+ ) : null}
+
+
)
}
-function WorkspaceRowWithMenuContent({
+function WorkspaceRowWithMenu({
workspace,
selected,
shortcutNumber,
@@ -713,7 +737,6 @@ function WorkspaceRowWithMenuContent({
dragHandleProps?: DraggableListDragHandleProps
}) {
const toast = useToast()
- const contextMenu = useContextMenu()
const archiveWorktree = useCheckoutGitActionsStore((state) => state.archiveWorktree)
const [isArchivingWorkspace, setIsArchivingWorkspace] = useState(false)
const archiveStatus = useCheckoutGitActionsStore((state) =>
@@ -792,72 +815,29 @@ function WorkspaceRowWithMenuContent({
})()
}, [isArchivingWorkspace, toast, workspace.name, workspace.serverId, workspace.workspaceId])
- return (
- <>
-
-
-
- {isWorktree ? 'Archive worktree' : 'Hide from sidebar'}
-
-
- >
- )
-}
+ const handleCopyPath = useCallback(() => {
+ void Clipboard.setStringAsync(workspace.workspaceId)
+ toast.copied('Path copied')
+ }, [toast, workspace.workspaceId])
-function WorkspaceRowWithMenu({
- workspace,
- selected,
- shortcutNumber,
- showShortcutBadge,
- onPress,
- drag,
- isDragging,
- dragHandleProps,
-}: {
- workspace: SidebarWorkspaceEntry
- selected: boolean
- shortcutNumber: number | null
- showShortcutBadge: boolean
- onPress: () => void
- drag: () => void
- isDragging: boolean
- dragHandleProps?: DraggableListDragHandleProps
-}) {
return (
-
-
-
+
)
}
@@ -1751,6 +1731,27 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs,
flexShrink: 0,
},
+ diffStatRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ flexShrink: 0,
+ },
+ diffStatAdditions: {
+ fontSize: theme.fontSize.xs,
+ fontWeight: theme.fontWeight.normal,
+ color: theme.colors.palette.green[400],
+ },
+ diffStatDeletions: {
+ fontSize: theme.fontSize.xs,
+ fontWeight: theme.fontWeight.normal,
+ color: theme.colors.palette.red[500],
+ },
+ kebabButton: {
+ padding: 2,
+ borderRadius: 4,
+ marginLeft: 2,
+ },
shortcutBadge: {
minWidth: 18,
height: 18,
diff --git a/packages/app/src/hooks/use-sidebar-workspaces-list.test.ts b/packages/app/src/hooks/use-sidebar-workspaces-list.test.ts
index bbc69dcab..2afda94b0 100644
--- a/packages/app/src/hooks/use-sidebar-workspaces-list.test.ts
+++ b/packages/app/src/hooks/use-sidebar-workspaces-list.test.ts
@@ -33,6 +33,7 @@ function workspace(
name: input.name,
status: input.status,
activityAt: input.activityAt,
+ diffStat: null,
}
}
diff --git a/packages/app/src/hooks/use-sidebar-workspaces-list.ts b/packages/app/src/hooks/use-sidebar-workspaces-list.ts
index 671779d7f..712e1d82a 100644
--- a/packages/app/src/hooks/use-sidebar-workspaces-list.ts
+++ b/packages/app/src/hooks/use-sidebar-workspaces-list.ts
@@ -18,6 +18,7 @@ export interface SidebarWorkspaceEntry {
name: string
activityAt: Date | null
statusBucket: SidebarStateBucket
+ diffStat: { additions: number; deletions: number } | null
}
export interface SidebarProjectEntry {
@@ -129,6 +130,7 @@ export function buildSidebarProjectsFromWorkspaces(input: {
name: workspace.name,
activityAt: workspace.activityAt,
statusBucket: workspace.status,
+ diffStat: workspace.diffStat,
}
project.workspaces.push(row)
diff --git a/packages/app/src/screens/workspace/workspace-source-of-truth.test.ts b/packages/app/src/screens/workspace/workspace-source-of-truth.test.ts
index b4416b3d6..a6f5e0e7f 100644
--- a/packages/app/src/screens/workspace/workspace-source-of-truth.test.ts
+++ b/packages/app/src/screens/workspace/workspace-source-of-truth.test.ts
@@ -18,6 +18,7 @@ describe('workspace source of truth consumption', () => {
name: 'feat/workspace-sot',
status: 'running',
activityAt: new Date('2026-03-01T00:00:00.000Z'),
+ diffStat: null,
}
const header = resolveWorkspaceHeader({ workspace })
diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts
index c07ab1dab..1bbd1652b 100644
--- a/packages/app/src/stores/session-store.ts
+++ b/packages/app/src/stores/session-store.ts
@@ -120,6 +120,7 @@ export interface WorkspaceDescriptor {
name: string;
status: WorkspaceDescriptorPayload["status"];
activityAt: Date | null;
+ diffStat: { additions: number; deletions: number } | null;
}
export function normalizeWorkspaceDescriptor(
@@ -137,6 +138,7 @@ export function normalizeWorkspaceDescriptor(
status: payload.status,
activityAt:
activityAt && !Number.isNaN(activityAt.getTime()) ? activityAt : null,
+ diffStat: payload.diffStat ?? null,
};
}
diff --git a/packages/app/src/utils/sidebar-project-row-model.test.ts b/packages/app/src/utils/sidebar-project-row-model.test.ts
index d1cba0604..bb733e05f 100644
--- a/packages/app/src/utils/sidebar-project-row-model.test.ts
+++ b/packages/app/src/utils/sidebar-project-row-model.test.ts
@@ -14,6 +14,7 @@ function workspace(overrides: Partial = {}): SidebarWorks
name: 'paseo',
activityAt: null,
statusBucket: 'done',
+ diffStat: null,
...overrides,
}
}
diff --git a/packages/app/src/utils/sidebar-shortcuts.test.ts b/packages/app/src/utils/sidebar-shortcuts.test.ts
index 32155f78e..60c431676 100644
--- a/packages/app/src/utils/sidebar-shortcuts.test.ts
+++ b/packages/app/src/utils/sidebar-shortcuts.test.ts
@@ -12,6 +12,7 @@ function workspace(serverId: string, cwd: string): SidebarWorkspaceEntry {
name: cwd,
activityAt: null,
statusBucket: "done",
+ diffStat: null,
};
}
diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts
index 30a805885..fcc3080c3 100644
--- a/packages/server/src/server/agent/providers/claude-agent.ts
+++ b/packages/server/src/server/agent/providers/claude-agent.ts
@@ -1415,6 +1415,16 @@ export class ClaudeAgentClient implements AgentClient {
} catch {
this.claudePath = null;
}
+ if (this.claudePath) {
+ try {
+ const version = execSync(`${this.claudePath} --version`, { encoding: "utf8" }).trim();
+ this.logger.info({ claudePath: this.claudePath, version }, "Resolved Claude binary");
+ } catch {
+ this.logger.info({ claudePath: this.claudePath }, "Resolved Claude binary (version unknown)");
+ }
+ } else {
+ this.logger.warn("Claude binary not found via 'which claude'; SDK will use bundled binary");
+ }
}
async createSession(config: AgentSessionConfig): Promise {
diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts
index d1d97134d..67d26e789 100644
--- a/packages/server/src/server/session.ts
+++ b/packages/server/src/server/session.ts
@@ -137,6 +137,7 @@ import {
import { createAgentWorktree, runAsyncWorktreeBootstrap } from './worktree-bootstrap.js'
import {
getCheckoutDiff,
+ getCheckoutShortstat,
getCheckoutStatus,
listBranchSuggestions,
NotGitRepoError,
@@ -5253,6 +5254,13 @@ export class Session {
// Fall back to the persisted label if checkout metadata is unavailable.
}
+ let diffStat: { additions: number; deletions: number } | null = null
+ try {
+ diffStat = await getCheckoutShortstat(workspace.cwd)
+ } catch {
+ // Non-critical — leave null on failure.
+ }
+
return {
id: workspace.workspaceId,
projectId: workspace.projectId,
@@ -5263,6 +5271,7 @@ export class Session {
name: displayName,
status: 'done',
activityAt: null,
+ diffStat,
}
}
diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts
index 38a281e09..fb215f394 100644
--- a/packages/server/src/shared/messages.ts
+++ b/packages/server/src/shared/messages.ts
@@ -1490,6 +1490,10 @@ export const WorkspaceDescriptorPayloadSchema = z.object({
name: z.string(),
status: WorkspaceStateBucketSchema,
activityAt: z.string().nullable(),
+ diffStat: z.object({
+ additions: z.number(),
+ deletions: z.number(),
+ }).nullable().optional(),
})
export const AgentUpdateMessageSchema = z.object({
diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts
index 5f9082b8f..b2a876cf2 100644
--- a/packages/server/src/utils/checkout-git.ts
+++ b/packages/server/src/utils/checkout-git.ts
@@ -1087,6 +1087,77 @@ export async function getCheckoutStatusLite(
};
}
+export interface CheckoutShortstat {
+ additions: number;
+ deletions: number;
+}
+
+export async function getCheckoutShortstat(
+ cwd: string,
+ context?: CheckoutContext
+): Promise {
+ try {
+ await requireGitRepo(cwd);
+ } catch {
+ return null;
+ }
+
+ const configured = await getConfiguredBaseRefForCwd(cwd, context);
+ const baseRef = configured.baseRef ?? (await resolveBaseRef(cwd));
+ if (!baseRef) {
+ return null;
+ }
+
+ const currentBranch = await getCurrentBranch(cwd);
+ if (currentBranch === baseRef) {
+ return null;
+ }
+
+ let mergeBase: string;
+ try {
+ const { stdout } = await execAsync(`git merge-base HEAD ${baseRef}`, {
+ cwd,
+ env: READ_ONLY_GIT_ENV,
+ });
+ mergeBase = stdout.trim();
+ if (!mergeBase) {
+ return null;
+ }
+ } catch {
+ return null;
+ }
+
+ try {
+ const { stdout } = await execAsync(`git diff --shortstat ${mergeBase}`, {
+ cwd,
+ env: READ_ONLY_GIT_ENV,
+ });
+ const text = stdout.trim();
+ if (!text) {
+ return null;
+ }
+
+ let additions = 0;
+ let deletions = 0;
+ const addMatch = text.match(/(\d+)\s+insertion/);
+ if (addMatch) {
+ additions = Number.parseInt(addMatch[1]!, 10);
+ }
+ const delMatch = text.match(/(\d+)\s+deletion/);
+ if (delMatch) {
+ deletions = Number.parseInt(delMatch[1]!, 10);
+ }
+
+ if (additions === 0 && deletions === 0) {
+ return null;
+ }
+
+ return { additions, deletions };
+ } catch {
+ return null;
+ }
+}
+
export async function getCheckoutDiff(
cwd: string,
compare: CheckoutDiffCompare,