feat: rename sidebar-agent-list to sidebar-workspace-list and emit workspace updates on agent removal

This commit is contained in:
Mohamed Boudra
2026-03-04 10:01:11 +07:00
parent e9b57a5ac6
commit bf1cded8a1
4 changed files with 232 additions and 25 deletions
@@ -0,0 +1,895 @@
import { View, Text, Pressable, Image, Platform, ActivityIndicator, Alert } from 'react-native'
import { useQueries } from '@tanstack/react-query'
import {
useCallback,
useMemo,
useState,
useEffect,
useRef,
type ReactElement,
type MutableRefObject,
} from 'react'
import { router, usePathname, useSegments } from 'expo-router'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
import { Gesture, GestureDetector, type GestureType } from 'react-native-gesture-handler'
import { ChevronDown, ChevronRight } from 'lucide-react-native'
import { DraggableList, type DraggableRenderItemInfo } from './draggable-list'
import { getHostRuntimeStore, isHostRuntimeConnected } from '@/runtime/host-runtime'
import { getIsTauri } from '@/constants/layout'
import { projectIconQueryKey } from '@/hooks/use-project-icon-query'
import {
buildHostWorkspaceRoute,
parseHostWorkspaceRouteFromPathname,
} from '@/utils/host-routes'
import {
type SidebarProjectEntry,
type SidebarWorkspaceEntry,
type SidebarStateBucket,
} from '@/hooks/use-sidebar-workspaces-list'
import { useSidebarOrderStore } from '@/stores/sidebar-order-store'
import { useKeyboardShortcutsStore } from '@/stores/keyboard-shortcuts-store'
import { formatTimeAgo } from '@/utils/time'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from '@/components/ui/context-menu'
import { useToast } from '@/contexts/toast-context'
import {
buildSidebarWorkspaceViewModel,
type SidebarWorkspaceTreeRow,
} from '@/utils/sidebar-shortcuts'
import { useCheckoutGitActionsStore } from '@/stores/checkout-git-actions-store'
const PASEO_WORKTREE_PATH_MARKER = '/.paseo/worktrees'
function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null {
if (!icon) {
return null
}
return `data:${icon.mimeType};base64,${icon.data}`
}
interface SidebarWorkspaceListProps {
isOpen?: boolean
projects: SidebarProjectEntry[]
serverId: string | null
isRefreshing?: boolean
onRefresh?: () => void
onWorkspacePress?: () => void
listFooterComponent?: ReactElement | null
/** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */
parentGestureRef?: MutableRefObject<GestureType | undefined>
}
interface ProjectRowProps {
project: SidebarProjectEntry
displayName: string
iconDataUri: string | null
collapsed: boolean
onToggle: () => void
onLongPress: () => void
}
interface WorkspaceRowProps {
workspace: SidebarWorkspaceEntry
selected: boolean
shortcutNumber: number | null
showShortcutBadge: boolean
onPress: () => void
drag: () => void
isArchiving: boolean
useContextMenuTrigger?: boolean
}
function resolveWorkspaceCreatedAtLabel(workspace: SidebarWorkspaceEntry): string | null {
if (!workspace.activityAt) {
return null
}
return formatTimeAgo(workspace.activityAt)
}
function resolveStatusDotColor(input: { theme: ReturnType<typeof useUnistyles>['theme']; bucket: SidebarStateBucket }) {
const { theme, bucket } = input
return bucket === 'needs_input'
? theme.colors.palette.amber[500]
: bucket === 'failed'
? theme.colors.palette.red[500]
: bucket === 'running'
? theme.colors.palette.blue[500]
: bucket === 'attention'
? theme.colors.palette.green[500]
: theme.colors.border
}
function isPaseoOwnedWorktreePath(path: string): boolean {
const normalizedPath = path.replace(/\\/g, '/')
const markerIndex = normalizedPath.indexOf(PASEO_WORKTREE_PATH_MARKER)
if (markerIndex <= 0) {
return false
}
const nextChar = normalizedPath[markerIndex + PASEO_WORKTREE_PATH_MARKER.length]
return !nextChar || nextChar === '/'
}
function WorkspaceStatusIndicator({
bucket,
loading = false,
}: {
bucket: SidebarWorkspaceEntry['statusBucket']
loading?: boolean
}) {
const { theme } = useUnistyles()
const color = resolveStatusDotColor({ theme, bucket })
return (
<View style={styles.workspaceStatusDot}>
{loading ? (
<ActivityIndicator size={8} color={theme.colors.foregroundMuted} />
) : (
<View style={[styles.workspaceStatusDotFill, { backgroundColor: color }]} />
)}
</View>
)
}
function ProjectRow({
project,
displayName,
iconDataUri,
collapsed,
onToggle,
onLongPress,
}: ProjectRowProps) {
const didLongPressRef = useRef(false)
const handlePress = useCallback(() => {
if (didLongPressRef.current) {
didLongPressRef.current = false
return
}
onToggle()
}, [onToggle])
const handleLongPress = useCallback(() => {
didLongPressRef.current = true
onLongPress()
}, [onLongPress])
return (
<Pressable
style={({ pressed, hovered = false }) => [
styles.projectRow,
hovered && styles.projectRowHovered,
pressed && styles.projectRowPressed,
]}
onPress={handlePress}
onLongPress={handleLongPress}
delayLongPress={200}
testID={`sidebar-project-row-${project.projectKey}`}
>
<View style={styles.projectRowLeft}>
{collapsed ? (
<ChevronRight size={14} color="#9ca3af" />
) : (
<ChevronDown size={14} color="#9ca3af" />
)}
{iconDataUri ? (
<Image source={{ uri: iconDataUri }} style={styles.projectIcon} />
) : (
<View style={styles.projectIconFallback}>
<Text style={styles.projectIconFallbackText}>
{displayName.charAt(0).toUpperCase()}
</Text>
</View>
)}
<Text style={styles.projectTitle} numberOfLines={1}>
{displayName}
</Text>
</View>
</Pressable>
)
}
function WorkspaceRow({
workspace,
selected,
shortcutNumber,
showShortcutBadge,
onPress,
drag,
isArchiving,
useContextMenuTrigger = true,
}: WorkspaceRowProps) {
const { theme } = useUnistyles()
const createdAtLabel = resolveWorkspaceCreatedAtLabel(workspace)
const didLongPressRef = useRef(false)
const handlePress = useCallback(() => {
if (didLongPressRef.current) {
didLongPressRef.current = false
return
}
onPress()
}, [onPress])
const handleLongPress = useCallback(() => {
didLongPressRef.current = true
}, [])
const moveMonitorGesture = useMemo(() => {
return Platform.OS === 'web' ? null : Gesture.Pan().manualActivation(true).runOnJS(true)
}, [])
const rowChildren = (
<>
<View 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 = useContextMenuTrigger ? (
<ContextMenuTrigger
enabledOnMobile={false}
disabled={isArchiving}
style={({ pressed, hovered = false }) => [
styles.workspaceRow,
selected && styles.workspaceRowSelected,
hovered && styles.workspaceRowHovered,
pressed && styles.workspaceRowPressed,
]}
onPress={handlePress}
onLongPress={handleLongPress}
delayLongPress={200}
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
>
{rowChildren}
</ContextMenuTrigger>
) : (
<Pressable
disabled={isArchiving}
style={({ pressed, hovered = false }) => [
styles.workspaceRow,
selected && styles.workspaceRowSelected,
hovered && styles.workspaceRowHovered,
pressed && styles.workspaceRowPressed,
]}
onPress={handlePress}
onLongPress={handleLongPress}
delayLongPress={200}
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
>
{rowChildren}
</Pressable>
)
const content = moveMonitorGesture ? (
<GestureDetector gesture={moveMonitorGesture}>{trigger}</GestureDetector>
) : (
trigger
)
return (
<View style={styles.workspaceRowContainer}>
{content}
{isArchiving ? (
<View style={styles.workspaceArchivingOverlay} testID={`sidebar-workspace-archiving-${workspace.workspaceKey}`}>
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
<Text style={styles.workspaceArchivingText}>Archiving</Text>
</View>
) : null}
</View>
)
}
function WorkspaceRowWithMenu({
workspace,
selected,
shortcutNumber,
showShortcutBadge,
onPress,
drag,
}: {
workspace: SidebarWorkspaceEntry
selected: boolean
shortcutNumber: number | null
showShortcutBadge: boolean
onPress: () => void
drag: () => void
}) {
const toast = useToast()
const archiveWorktree = useCheckoutGitActionsStore((state) => state.archiveWorktree)
const archiveStatus = useCheckoutGitActionsStore((state) =>
state.getStatus({
serverId: workspace.serverId,
cwd: workspace.workspaceId,
actionId: 'archive-worktree',
})
)
const isArchiving = archiveStatus === 'pending'
const isPaseoOwnedWorktree = isPaseoOwnedWorktreePath(workspace.workspaceId)
const handleArchiveWorktree = useCallback(() => {
if (isArchiving) {
return
}
Alert.alert(
'Archive worktree?',
`Archive "${workspace.name}"?\n\nThis removes the worktree from the sidebar.`,
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Archive',
style: 'destructive',
onPress: () => {
void archiveWorktree({
serverId: workspace.serverId,
cwd: workspace.workspaceId,
worktreePath: workspace.workspaceId,
}).catch((error) => {
const message = error instanceof Error ? error.message : 'Failed to archive worktree'
toast.error(message)
})
},
},
],
{ cancelable: true }
)
}, [
archiveWorktree,
isArchiving,
toast,
workspace.name,
workspace.serverId,
workspace.workspaceId,
])
if (!isPaseoOwnedWorktree) {
return (
<WorkspaceRow
workspace={workspace}
selected={selected}
shortcutNumber={shortcutNumber}
showShortcutBadge={showShortcutBadge}
onPress={onPress}
drag={drag}
isArchiving={false}
useContextMenuTrigger={false}
/>
)
}
return (
<ContextMenu>
<WorkspaceRow
workspace={workspace}
selected={selected}
shortcutNumber={shortcutNumber}
showShortcutBadge={showShortcutBadge}
onPress={onPress}
drag={drag}
isArchiving={isArchiving}
/>
<ContextMenuContent align="start" width={220} testID={`sidebar-workspace-context-${workspace.workspaceKey}`}>
<ContextMenuItem
testID={`sidebar-workspace-context-${workspace.workspaceKey}-archive`}
status={archiveStatus}
pendingLabel="Archiving..."
destructive
onSelect={handleArchiveWorktree}
>
Archive worktree
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
function mergeWithRemainder(input: {
currentOrder: string[]
reorderedVisibleKeys: string[]
}): string[] {
const reorderedSet = new Set(input.reorderedVisibleKeys)
const remainder = input.currentOrder.filter((key) => !reorderedSet.has(key))
return [...input.reorderedVisibleKeys, ...remainder]
}
function hasVisibleOrderChanged(input: {
currentOrder: string[]
reorderedVisibleKeys: string[]
}): boolean {
const currentVisible = input.currentOrder.filter((key) =>
input.reorderedVisibleKeys.includes(key)
)
if (currentVisible.length !== input.reorderedVisibleKeys.length) {
return true
}
return input.reorderedVisibleKeys.some((key, index) => currentVisible[index] !== key)
}
export function SidebarWorkspaceList({
isOpen = true,
projects,
serverId,
isRefreshing = false,
onRefresh,
onWorkspacePress,
listFooterComponent,
parentGestureRef,
}: SidebarWorkspaceListProps) {
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
const showDesktopWebScrollbar = Platform.OS === 'web' && !isMobile
const segments = useSegments()
const pathname = usePathname()
const shouldReplaceWorkspaceNavigation = segments[0] === 'h'
const [collapsedProjectKeys, setCollapsedProjectKeys] = useState<Set<string>>(new Set())
const [canonicalResyncNonce, setCanonicalResyncNonce] = useState(0)
const isTauri = getIsTauri()
const altDown = useKeyboardShortcutsStore((state) => state.altDown)
const cmdOrCtrlDown = useKeyboardShortcutsStore((state) => state.cmdOrCtrlDown)
const setSidebarShortcutWorkspaceTargets = useKeyboardShortcutsStore(
(state) => state.setSidebarShortcutWorkspaceTargets
)
const showShortcutBadges = altDown || (isTauri && cmdOrCtrlDown)
const getProjectOrder = useSidebarOrderStore((state) => state.getProjectOrder)
const setProjectOrder = useSidebarOrderStore((state) => state.setProjectOrder)
const getWorkspaceOrder = useSidebarOrderStore((state) => state.getWorkspaceOrder)
const setWorkspaceOrder = useSidebarOrderStore((state) => state.setWorkspaceOrder)
const activeWorkspaceSelection = useMemo(() => {
if (!pathname) {
return null
}
const parsed = parseHostWorkspaceRouteFromPathname(pathname)
if (!parsed) {
return null
}
return {
serverId: parsed.serverId,
workspaceId: parsed.workspaceId,
}
}, [pathname])
useEffect(() => {
setCollapsedProjectKeys((prev) => {
const validProjectKeys = new Set(projects.map((project) => project.projectKey))
const next = new Set<string>()
for (const key of prev) {
if (validProjectKeys.has(key)) {
next.add(key)
}
}
return next
})
}, [projects])
const projectIconRequests = useMemo(() => {
if (!isOpen || !serverId) {
return []
}
const unique = new Map<string, { serverId: string; cwd: string }>()
for (const project of projects) {
const cwd = project.iconWorkingDir.trim()
if (!cwd) {
continue
}
unique.set(`${serverId}:${cwd}`, { serverId, cwd })
}
return Array.from(unique.values())
}, [isOpen, projects, serverId])
const projectIconQueries = useQueries({
queries: projectIconRequests.map((request) => ({
queryKey: projectIconQueryKey(request.serverId, request.cwd),
queryFn: async () => {
const client = getHostRuntimeStore().getClient(request.serverId)
if (!client) {
return null
}
const result = await client.requestProjectIcon(request.cwd)
return result.icon
},
select: toProjectIconDataUri,
enabled: Boolean(
isOpen &&
getHostRuntimeStore().getClient(request.serverId) &&
isHostRuntimeConnected(getHostRuntimeStore().getSnapshot(request.serverId)) &&
request.cwd
),
staleTime: Infinity,
gcTime: 1000 * 60 * 60,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
})),
})
const projectIconByProjectKey = useMemo(() => {
const iconByServerAndCwd = new Map<string, string | null>()
for (let index = 0; index < projectIconRequests.length; index += 1) {
const request = projectIconRequests[index]
if (!request) {
continue
}
iconByServerAndCwd.set(
`${request.serverId}:${request.cwd}`,
projectIconQueries[index]?.data ?? null
)
}
const byProject = new Map<string, string | null>()
for (const project of projects) {
const cwd = project.iconWorkingDir.trim()
if (!cwd || !serverId) {
byProject.set(project.projectKey, null)
continue
}
byProject.set(project.projectKey, iconByServerAndCwd.get(`${serverId}:${cwd}`) ?? null)
}
return byProject
}, [projectIconQueries, projectIconRequests, projects, serverId])
const viewModel = useMemo(
() =>
buildSidebarWorkspaceViewModel({
projects,
collapsedProjectKeys,
getProjectDisplayName: (project) => project.projectName,
}),
[canonicalResyncNonce, collapsedProjectKeys, projects]
)
useEffect(() => {
setSidebarShortcutWorkspaceTargets(viewModel.shortcutTargets)
}, [setSidebarShortcutWorkspaceTargets, viewModel.shortcutTargets])
useEffect(() => {
return () => {
setSidebarShortcutWorkspaceTargets([])
}
}, [setSidebarShortcutWorkspaceTargets])
const toggleProjectCollapsed = useCallback((projectKey: string) => {
setCollapsedProjectKeys((prev) => {
const next = new Set(prev)
if (next.has(projectKey)) {
next.delete(projectKey)
} else {
next.add(projectKey)
}
return next
})
}, [])
const renderRow = useCallback(
({ item, drag }: DraggableRenderItemInfo<SidebarWorkspaceTreeRow>) => {
if (item.kind === 'project') {
return (
<ProjectRow
project={item.project}
displayName={item.displayName}
iconDataUri={projectIconByProjectKey.get(item.project.projectKey) ?? null}
collapsed={collapsedProjectKeys.has(item.project.projectKey)}
onToggle={() => toggleProjectCollapsed(item.project.projectKey)}
onLongPress={drag}
/>
)
}
const workspaceRoute = buildHostWorkspaceRoute(serverId ?? '', item.workspace.workspaceId)
const navigate = shouldReplaceWorkspaceNavigation ? router.replace : router.push
const isSelected =
Boolean(serverId) &&
activeWorkspaceSelection?.serverId === serverId &&
activeWorkspaceSelection.workspaceId === item.workspace.workspaceId
return (
<WorkspaceRowWithMenu
workspace={item.workspace}
selected={isSelected}
shortcutNumber={item.shortcutNumber}
showShortcutBadge={showShortcutBadges}
onPress={() => {
if (!serverId) {
return
}
onWorkspacePress?.()
navigate(workspaceRoute as any)
}}
drag={drag}
/>
)
},
[
activeWorkspaceSelection,
collapsedProjectKeys,
onWorkspacePress,
projectIconByProjectKey,
serverId,
showShortcutBadges,
shouldReplaceWorkspaceNavigation,
toggleProjectCollapsed,
]
)
const keyExtractor = useCallback((entry: SidebarWorkspaceTreeRow) => entry.rowKey, [])
const handleDragEnd = useCallback(
(reorderedRows: SidebarWorkspaceTreeRow[]) => {
if (!serverId) {
return
}
let didPersistOrderChange = false
const reorderedProjectKeys = reorderedRows
.filter(
(row): row is Extract<SidebarWorkspaceTreeRow, { kind: 'project' }> =>
row.kind === 'project'
)
.map((row) => row.project.projectKey)
const currentProjectOrder = getProjectOrder(serverId)
if (
hasVisibleOrderChanged({
currentOrder: currentProjectOrder,
reorderedVisibleKeys: reorderedProjectKeys,
})
) {
didPersistOrderChange = true
setProjectOrder(
serverId,
mergeWithRemainder({
currentOrder: currentProjectOrder,
reorderedVisibleKeys: reorderedProjectKeys,
})
)
}
const workspaceRowsByProject = new Map<string, string[]>()
for (const row of reorderedRows) {
if (row.kind !== 'workspace') {
continue
}
const list = workspaceRowsByProject.get(row.projectKey) ?? []
list.push(row.workspace.workspaceKey)
workspaceRowsByProject.set(row.projectKey, list)
}
for (const [projectKey, reorderedWorkspaceKeys] of workspaceRowsByProject.entries()) {
const currentWorkspaceOrder = getWorkspaceOrder(serverId, projectKey)
if (
!hasVisibleOrderChanged({
currentOrder: currentWorkspaceOrder,
reorderedVisibleKeys: reorderedWorkspaceKeys,
})
) {
continue
}
didPersistOrderChange = true
setWorkspaceOrder(
serverId,
projectKey,
mergeWithRemainder({
currentOrder: currentWorkspaceOrder,
reorderedVisibleKeys: reorderedWorkspaceKeys,
})
)
}
// If persisted ordering did not change, force a local resync so draggable UI state
// snaps back to canonical Project -> Workspaces grouping.
if (!didPersistOrderChange) {
setCanonicalResyncNonce((prev) => prev + 1)
}
},
[getProjectOrder, getWorkspaceOrder, serverId, setProjectOrder, setWorkspaceOrder]
)
return (
<View style={styles.container}>
<DraggableList
data={viewModel.rows}
style={styles.list}
contentContainerStyle={styles.listContent}
testID="sidebar-project-workspace-list-scroll"
keyExtractor={keyExtractor}
renderItem={renderRow}
onDragEnd={handleDragEnd}
showsVerticalScrollIndicator={false}
enableDesktopWebScrollbar={showDesktopWebScrollbar}
ListFooterComponent={listFooterComponent}
ListEmptyComponent={<Text style={styles.emptyText}>No projects yet</Text>}
refreshing={isRefreshing}
onRefresh={onRefresh}
simultaneousGestureRef={parentGestureRef}
/>
</View>
)
}
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
},
list: {
flex: 1,
},
listContent: {
paddingHorizontal: theme.spacing[2],
paddingTop: theme.spacing[2],
paddingBottom: theme.spacing[4],
},
emptyText: {
color: theme.colors.foregroundMuted,
textAlign: 'center',
marginTop: theme.spacing[8],
marginHorizontal: theme.spacing[2],
},
projectRow: {
minHeight: 36,
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.lg,
marginBottom: theme.spacing[1],
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: theme.spacing[2],
},
projectRowHovered: {
backgroundColor: theme.colors.surface1,
},
projectRowPressed: {
backgroundColor: theme.colors.surface2,
},
projectRowLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing[2],
flex: 1,
minWidth: 0,
},
projectIcon: {
width: theme.iconSize.sm,
height: theme.iconSize.sm,
borderRadius: theme.borderRadius.sm,
},
projectIconFallback: {
width: theme.iconSize.sm,
height: theme.iconSize.sm,
borderRadius: theme.borderRadius.sm,
borderWidth: 1,
borderColor: theme.colors.border,
alignItems: 'center',
justifyContent: 'center',
},
projectIconFallbackText: {
color: theme.colors.foregroundMuted,
fontSize: 9,
},
projectTitle: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
flex: 1,
minWidth: 0,
},
workspaceRow: {
minHeight: 36,
marginBottom: theme.spacing[1],
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.lg,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: theme.spacing[2],
},
workspaceRowLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing[2],
flex: 1,
minWidth: 0,
},
workspaceRowRight: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing[2],
flexShrink: 0,
},
workspaceRowHovered: {
backgroundColor: theme.colors.surface1,
},
workspaceRowPressed: {
backgroundColor: theme.colors.surface2,
},
workspaceRowSelected: {
backgroundColor: theme.colors.surface2,
},
workspaceRowContainer: {
position: 'relative',
},
workspaceStatusDot: {
width: 8,
height: 8,
borderRadius: theme.borderRadius.full,
flexShrink: 0,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
workspaceStatusDotFill: {
width: 8,
height: 8,
borderRadius: theme.borderRadius.full,
},
workspaceArchivingOverlay: {
...StyleSheet.absoluteFillObject,
borderRadius: theme.borderRadius.lg,
backgroundColor: `${theme.colors.surface0}cc`,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
gap: theme.spacing[2],
zIndex: 1,
},
workspaceArchivingText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
fontWeight: '600',
},
workspaceBranchText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
flex: 1,
minWidth: 0,
},
workspaceCreatedAtText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
flexShrink: 0,
},
shortcutBadge: {
minWidth: 18,
height: 18,
paddingHorizontal: theme.spacing[1],
borderRadius: theme.borderRadius.full,
borderWidth: 1,
borderColor: theme.colors.border,
backgroundColor: theme.colors.surface2,
alignItems: 'center',
justifyContent: 'center',
},
shortcutBadgeText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
fontWeight: '600',
lineHeight: 14,
},
}))