diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx
index ced54e761..cf4eb428c 100644
--- a/packages/app/src/components/left-sidebar.tsx
+++ b/packages/app/src/components/left-sidebar.tsx
@@ -13,7 +13,7 @@ import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyl
import { Plus, Settings, Users } from 'lucide-react-native'
import { router, usePathname } from 'expo-router'
import { usePanelStore } from '@/stores/panel-store'
-import { SidebarAgentList } from './sidebar-agent-list'
+import { SidebarWorkspaceList } from './sidebar-workspace-list'
import { SidebarAgentListSkeleton } from './sidebar-agent-list-skeleton'
import { useSidebarWorkspacesList } from '@/hooks/use-sidebar-workspaces-list'
import { useSidebarAnimation } from '@/contexts/sidebar-animation-context'
@@ -365,7 +365,7 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
{isInitialLoad ? (
) : (
-
) : (
- void
drag: () => void
+ isArchiving: boolean
+ useContextMenuTrigger?: boolean
}
function resolveWorkspaceCreatedAtLabel(workspace: SidebarWorkspaceEntry): string | null {
@@ -95,17 +103,33 @@ function resolveStatusDotColor(input: { theme: ReturnType['
: 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 (
-
+ {loading ? (
+
+ ) : (
+
+ )}
)
}
@@ -177,7 +201,10 @@ function WorkspaceRow({
showShortcutBadge,
onPress,
drag,
+ isArchiving,
+ useContextMenuTrigger = true,
}: WorkspaceRowProps) {
+ const { theme } = useUnistyles()
const createdAtLabel = resolveWorkspaceCreatedAtLabel(workspace)
const didLongPressRef = useRef(false)
@@ -197,22 +224,10 @@ function WorkspaceRow({
return Platform.OS === 'web' ? null : Gesture.Pan().manualActivation(true).runOnJS(true)
}, [])
- const trigger = (
- [
- styles.workspaceRow,
- selected && styles.workspaceRowSelected,
- hovered && styles.workspaceRowHovered,
- pressed && styles.workspaceRowPressed,
- ]}
- onPress={handlePress}
- onLongPress={handleLongPress}
- delayLongPress={200}
- testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
- >
+ const rowChildren = (
+ <>
-
+
{workspace.name}
@@ -229,14 +244,61 @@ function WorkspaceRow({
) : null}
-
+ >
)
- return moveMonitorGesture ? (
+ const trigger = useContextMenuTrigger ? (
+ [
+ styles.workspaceRow,
+ selected && styles.workspaceRowSelected,
+ hovered && styles.workspaceRowHovered,
+ pressed && styles.workspaceRowPressed,
+ ]}
+ onPress={handlePress}
+ onLongPress={handleLongPress}
+ delayLongPress={200}
+ testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
+ >
+ {rowChildren}
+
+ ) : (
+ [
+ styles.workspaceRow,
+ selected && styles.workspaceRowSelected,
+ hovered && styles.workspaceRowHovered,
+ pressed && styles.workspaceRowPressed,
+ ]}
+ onPress={handlePress}
+ onLongPress={handleLongPress}
+ delayLongPress={200}
+ testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
+ >
+ {rowChildren}
+
+ )
+
+ const content = moveMonitorGesture ? (
{trigger}
) : (
trigger
)
+
+ return (
+
+ {content}
+ {isArchiving ? (
+
+
+ Archiving
+
+ ) : null}
+
+ )
}
function WorkspaceRowWithMenu({
@@ -254,6 +316,72 @@ function WorkspaceRowWithMenu({
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 (
+
+ )
+ }
+
return (
+
+
+ Archive worktree
+
+
)
}
@@ -290,7 +430,7 @@ function hasVisibleOrderChanged(input: {
return input.reorderedVisibleKeys.some((key, index) => currentVisible[index] !== key)
}
-export function SidebarAgentList({
+export function SidebarWorkspaceList({
isOpen = true,
projects,
serverId,
@@ -299,7 +439,7 @@ export function SidebarAgentList({
onWorkspacePress,
listFooterComponent,
parentGestureRef,
-}: SidebarAgentListProps) {
+}: SidebarWorkspaceListProps) {
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
const showDesktopWebScrollbar = Platform.OS === 'web' && !isMobile
const segments = useSegments()
@@ -692,6 +832,9 @@ const styles = StyleSheet.create((theme) => ({
workspaceRowSelected: {
backgroundColor: theme.colors.surface2,
},
+ workspaceRowContainer: {
+ position: 'relative',
+ },
workspaceStatusDot: {
width: 8,
height: 8,
@@ -706,6 +849,21 @@ const styles = StyleSheet.create((theme) => ({
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,
diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts
index f336801d5..4f94730dc 100644
--- a/packages/server/src/server/session.ts
+++ b/packages/server/src/server/session.ts
@@ -4544,10 +4544,12 @@ export class Session {
}
const removedAgents = new Set()
+ const affectedWorkspaceCwds = new Set([targetPath])
const agents = this.agentManager.listAgents()
for (const agent of agents) {
if (this.isPathWithinRoot(targetPath, agent.cwd)) {
removedAgents.add(agent.id)
+ affectedWorkspaceCwds.add(agent.cwd)
try {
await this.agentManager.closeAgent(agent.id)
} catch {
@@ -4565,6 +4567,7 @@ export class Session {
for (const record of registryRecords) {
if (this.isPathWithinRoot(targetPath, record.cwd)) {
removedAgents.add(record.id)
+ affectedWorkspaceCwds.add(record.cwd)
try {
await this.agentStorage.remove(record.id)
} catch {
@@ -4591,6 +4594,8 @@ export class Session {
})
}
+ await this.emitWorkspaceUpdatesForCwds(affectedWorkspaceCwds)
+
return Array.from(removedAgents)
}
@@ -5671,6 +5676,25 @@ export class Session {
})
}
+ private async emitWorkspaceUpdatesForCwds(cwds: Iterable): Promise {
+ if (!this.workspaceUpdatesSubscription) {
+ return
+ }
+
+ const uniqueWorkspaceCwds = new Set()
+ for (const cwd of cwds) {
+ const normalized = this.normalizeWorkspaceId(cwd)
+ if (!normalized) {
+ continue
+ }
+ uniqueWorkspaceCwds.add(normalized)
+ }
+
+ for (const workspaceCwd of uniqueWorkspaceCwds) {
+ await this.emitWorkspaceUpdateForCwd(workspaceCwd)
+ }
+ }
+
private async handleFetchAgents(
request: Extract
): Promise {
diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts
index e8b23439c..22e657e25 100644
--- a/packages/server/src/server/session.workspaces.test.ts
+++ b/packages/server/src/server/session.workspaces.test.ts
@@ -264,4 +264,29 @@ describe('workspace aggregation', () => {
id: '/tmp/repo',
})
})
+
+ test('workspace update fanout for multiple cwd values is deduplicated', async () => {
+ const session = createSessionForWorkspaceTests() as any
+ session.workspaceUpdatesSubscription = {
+ subscriptionId: 'sub-dedupe',
+ filter: undefined,
+ isBootstrapping: false,
+ pendingUpdatesByWorkspaceId: new Map(),
+ }
+
+ const emitWorkspaceUpdateForCwd = vi.fn(async () => {})
+ session.emitWorkspaceUpdateForCwd = emitWorkspaceUpdateForCwd
+
+ await session.emitWorkspaceUpdatesForCwds([
+ '/tmp/repo',
+ '/tmp/repo/',
+ ' /tmp/repo ',
+ '/tmp/repo/sub',
+ '/tmp/repo/sub/',
+ ])
+
+ expect(emitWorkspaceUpdateForCwd).toHaveBeenCalledTimes(2)
+ expect(emitWorkspaceUpdateForCwd).toHaveBeenNthCalledWith(1, '/tmp/repo')
+ expect(emitWorkspaceUpdateForCwd).toHaveBeenNthCalledWith(2, '/tmp/repo/sub')
+ })
})