feat: add diff stats and archive actions to workspace sidebar
This commit is contained in:
@@ -158,24 +158,35 @@ function SessionRow({
|
||||
<SessionBadge label="Attention" tone="danger" />
|
||||
) : null}
|
||||
</View>
|
||||
<View style={styles.rowMetaRow}>
|
||||
<Text style={styles.sessionMetaText} numberOfLines={1}>
|
||||
{isMobile && (
|
||||
<View style={styles.rowMetaRow}>
|
||||
<Text style={styles.sessionMetaText} numberOfLines={1}>
|
||||
{projectPath}
|
||||
</Text>
|
||||
<Text style={styles.sessionMetaSeparator}>·</Text>
|
||||
<Text style={styles.sessionMetaText}>{statusLabel}</Text>
|
||||
<Text style={styles.sessionMetaSeparator}>·</Text>
|
||||
<Text style={styles.sessionMetaText}>{timeAgo}</Text>
|
||||
{agent.serverLabel ? (
|
||||
<>
|
||||
<Text style={styles.sessionMetaSeparator}>·</Text>
|
||||
<Text style={styles.sessionMetaText} numberOfLines={1}>
|
||||
{agent.serverLabel}
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{!isMobile && (
|
||||
<>
|
||||
<Text style={styles.columnMeta} numberOfLines={1}>
|
||||
{projectPath}
|
||||
</Text>
|
||||
<Text style={styles.sessionMetaSeparator}>·</Text>
|
||||
<Text style={styles.sessionMetaText}>{statusLabel}</Text>
|
||||
<Text style={styles.sessionMetaSeparator}>·</Text>
|
||||
<Text style={styles.sessionMetaText}>{timeAgo}</Text>
|
||||
{agent.serverLabel ? (
|
||||
<>
|
||||
<Text style={styles.sessionMetaSeparator}>·</Text>
|
||||
<Text style={styles.sessionMetaText} numberOfLines={1}>
|
||||
{agent.serverLabel}
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.columnMetaFixed}>{statusLabel}</Text>
|
||||
<Text style={styles.columnMetaFixed}>{timeAgo}</Text>
|
||||
</>
|
||||
)}
|
||||
{isMobile && agent.requiresAttention ? (
|
||||
<View style={styles.rowTrailing}>
|
||||
<SessionBadge label="Attention" tone="danger" />
|
||||
@@ -202,7 +213,6 @@ function SessionTableSection({
|
||||
<View style={styles.sectionBlock}>
|
||||
<View style={styles.sectionHeading}>
|
||||
<Text style={styles.sectionTitle}>{section.title}</Text>
|
||||
<View style={styles.sectionLine} />
|
||||
</View>
|
||||
|
||||
<View style={styles.listCard}>
|
||||
@@ -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],
|
||||
|
||||
@@ -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<typeof useContextMenu> | 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 = (
|
||||
<>
|
||||
<View
|
||||
{...(dragHandleProps?.attributes as any)}
|
||||
{...(dragHandleProps?.listeners as any)}
|
||||
ref={dragHandleProps?.setActivatorNodeRef as any}
|
||||
style={styles.workspaceRowLeft}
|
||||
>
|
||||
<WorkspaceStatusIndicator bucket={workspace.statusBucket} loading={isArchiving} />
|
||||
<Text style={styles.workspaceBranchText} numberOfLines={1}>
|
||||
{workspace.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.workspaceRowRight}>
|
||||
{createdAtLabel ? (
|
||||
<Text style={styles.workspaceCreatedAtText} numberOfLines={1}>
|
||||
{createdAtLabel}
|
||||
</Text>
|
||||
) : null}
|
||||
{showShortcutBadge && shortcutNumber !== null ? (
|
||||
<View style={styles.shortcutBadge}>
|
||||
<Text style={styles.shortcutBadgeText}>{shortcutNumber}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
|
||||
const trigger = menuController ? (
|
||||
<ContextMenuTrigger
|
||||
enabledOnMobile={false}
|
||||
disabled={isArchiving}
|
||||
style={({ pressed, hovered = false }) => [
|
||||
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}
|
||||
</ContextMenuTrigger>
|
||||
) : (
|
||||
<Pressable
|
||||
disabled={isArchiving}
|
||||
style={({ pressed, hovered = false }) => [
|
||||
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}
|
||||
</Pressable>
|
||||
)
|
||||
|
||||
const content = trigger
|
||||
|
||||
return (
|
||||
<View style={styles.workspaceRowContainer}>
|
||||
{content}
|
||||
<Pressable
|
||||
disabled={isArchiving}
|
||||
style={({ pressed, hovered = false }) => [
|
||||
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}`}
|
||||
>
|
||||
<View
|
||||
{...(dragHandleProps?.attributes as any)}
|
||||
{...(dragHandleProps?.listeners as any)}
|
||||
ref={dragHandleProps?.setActivatorNodeRef as any}
|
||||
style={styles.workspaceRowLeft}
|
||||
>
|
||||
<WorkspaceStatusIndicator bucket={workspace.statusBucket} loading={isArchiving} />
|
||||
<Text style={styles.workspaceBranchText} numberOfLines={1}>
|
||||
{workspace.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.workspaceRowRight}>
|
||||
{workspace.diffStat ? (
|
||||
<View style={styles.diffStatRow}>
|
||||
<Text style={styles.diffStatAdditions}>+{workspace.diffStat.additions}</Text>
|
||||
<Text style={styles.diffStatDeletions}>-{workspace.diffStat.deletions}</Text>
|
||||
</View>
|
||||
) : createdAtLabel ? (
|
||||
<Text style={styles.workspaceCreatedAtText} numberOfLines={1}>
|
||||
{createdAtLabel}
|
||||
</Text>
|
||||
) : null}
|
||||
{showShortcutBadge && shortcutNumber !== null ? (
|
||||
<View style={styles.shortcutBadge}>
|
||||
<Text style={styles.shortcutBadgeText}>{shortcutNumber}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{onArchive ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
hitSlop={8}
|
||||
style={styles.kebabButton}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Workspace actions"
|
||||
testID={`sidebar-workspace-kebab-${workspace.workspaceKey}`}
|
||||
>
|
||||
<MoreVertical size={14} color={theme.colors.foregroundMuted} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" width={200}>
|
||||
{onCopyPath ? (
|
||||
<DropdownMenuItem
|
||||
testID={`sidebar-workspace-menu-copy-path-${workspace.workspaceKey}`}
|
||||
onSelect={onCopyPath}
|
||||
>
|
||||
Copy path
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem
|
||||
testID={`sidebar-workspace-menu-archive-${workspace.workspaceKey}`}
|
||||
status={archiveStatus}
|
||||
pendingLabel={archivePendingLabel}
|
||||
destructive
|
||||
onSelect={onArchive}
|
||||
>
|
||||
{archiveLabel ?? 'Archive'}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<WorkspaceRowInner
|
||||
workspace={workspace}
|
||||
selected={selected}
|
||||
shortcutNumber={shortcutNumber}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
onPress={onPress}
|
||||
drag={drag}
|
||||
isDragging={isDragging}
|
||||
isArchiving={isArchiving}
|
||||
dragHandleProps={dragHandleProps}
|
||||
menuController={contextMenu}
|
||||
/>
|
||||
<ContextMenuContent
|
||||
align="start"
|
||||
width={220}
|
||||
mobileMode="sheet"
|
||||
testID={`sidebar-workspace-context-${workspace.workspaceKey}`}
|
||||
>
|
||||
<ContextMenuItem
|
||||
testID={`sidebar-workspace-context-${workspace.workspaceKey}-archive`}
|
||||
status={isWorktree ? archiveStatus : isArchivingWorkspace ? 'pending' : 'idle'}
|
||||
pendingLabel={isWorktree ? 'Archiving...' : 'Hiding...'}
|
||||
destructive
|
||||
onSelect={isWorktree ? handleArchiveWorktree : handleArchiveWorkspace}
|
||||
>
|
||||
{isWorktree ? 'Archive worktree' : 'Hide from sidebar'}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
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 (
|
||||
<ContextMenu>
|
||||
<WorkspaceRowWithMenuContent
|
||||
workspace={workspace}
|
||||
selected={selected}
|
||||
shortcutNumber={shortcutNumber}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
onPress={onPress}
|
||||
drag={drag}
|
||||
isDragging={isDragging}
|
||||
dragHandleProps={dragHandleProps}
|
||||
/>
|
||||
</ContextMenu>
|
||||
<WorkspaceRowInner
|
||||
workspace={workspace}
|
||||
selected={selected}
|
||||
shortcutNumber={shortcutNumber}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
onPress={onPress}
|
||||
drag={drag}
|
||||
isDragging={isDragging}
|
||||
isArchiving={isArchiving}
|
||||
dragHandleProps={dragHandleProps}
|
||||
menuController={null}
|
||||
archiveLabel={isWorktree ? 'Archive worktree' : 'Hide from sidebar'}
|
||||
archiveStatus={isWorktree ? archiveStatus : isArchivingWorkspace ? 'pending' : 'idle'}
|
||||
archivePendingLabel={isWorktree ? 'Archiving...' : 'Hiding...'}
|
||||
onArchive={isWorktree ? handleArchiveWorktree : handleArchiveWorkspace}
|
||||
onCopyPath={handleCopyPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -33,6 +33,7 @@ function workspace(
|
||||
name: input.name,
|
||||
status: input.status,
|
||||
activityAt: input.activityAt,
|
||||
diffStat: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorks
|
||||
name: 'paseo',
|
||||
activityAt: null,
|
||||
statusBucket: 'done',
|
||||
diffStat: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ function workspace(serverId: string, cwd: string): SidebarWorkspaceEntry {
|
||||
name: cwd,
|
||||
activityAt: null,
|
||||
statusBucket: "done",
|
||||
diffStat: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AgentSession> {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1087,6 +1087,77 @@ export async function getCheckoutStatusLite(
|
||||
};
|
||||
}
|
||||
|
||||
export interface CheckoutShortstat {
|
||||
additions: number;
|
||||
deletions: number;
|
||||
}
|
||||
|
||||
export async function getCheckoutShortstat(
|
||||
cwd: string,
|
||||
context?: CheckoutContext
|
||||
): Promise<CheckoutShortstat | null> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user