feat: add worktree archiving with context menu and improved logging

This commit is contained in:
Mohamed Boudra
2026-03-03 17:35:48 +07:00
parent f418f3ab02
commit 888af29485
11 changed files with 529 additions and 24 deletions
+3 -1
View File
@@ -98,6 +98,7 @@ function AgentListRow({
const timeAgo = formatTimeAgo(agent.lastActivityAt);
const agentKey = `${agent.serverId}:${agent.id}`;
const isSelected = selectedAgentId === agentKey;
const archivedLabel = agent.archivedAt ? "Archived" : null;
const checkoutQuery = useCheckoutStatusCacheOnly({
serverId: agent.serverId,
cwd: agent.cwd,
@@ -137,7 +138,8 @@ function AgentListRow({
<Text style={styles.secondaryRow} numberOfLines={1}>
{shortenPath(projectPath)}
{branchLabel ? ` · ${branchLabel}` : ""} · {timeAgo}
{branchLabel ? ` · ${branchLabel}` : ""}
{archivedLabel ? ` · ${archivedLabel}` : ""} · {timeAgo}
</Text>
</View>
)}
@@ -1,4 +1,4 @@
import { View, Text, Pressable, Image, Platform } from 'react-native'
import { View, Text, Pressable, Image, Platform, Alert, ActivityIndicator, StatusBar } from 'react-native'
import { useQueries } from '@tanstack/react-query'
import {
useCallback,
@@ -11,8 +11,8 @@ import {
} from 'react'
import { router, usePathname, useSegments } from 'expo-router'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
import { type GestureType } from 'react-native-gesture-handler'
import { ChevronDown, ChevronRight } from 'lucide-react-native'
import { Gesture, GestureDetector, type GestureType } from 'react-native-gesture-handler'
import { Archive, ChevronDown, ChevronRight } from 'lucide-react-native'
import { DraggableList, type DraggableRenderItemInfo } from './draggable-list'
import { getHostRuntimeStore, isHostRuntimeConnected } from '@/runtime/host-runtime'
import { projectIconQueryKey } from '@/hooks/use-project-icon-query'
@@ -27,8 +27,17 @@ import {
type SidebarWorkspaceEntry,
} from '@/hooks/use-sidebar-agents-list'
import { useSidebarOrderStore } from '@/stores/sidebar-order-store'
import { useCheckoutGitActionsStore } from '@/stores/checkout-git-actions-store'
import { formatTimeAgo } from '@/utils/time'
import type { SidebarStateBucket } from '@/utils/sidebar-agent-state'
import { confirmDialog } from '@/utils/confirm-dialog'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
useContextMenu,
} from '@/components/ui/context-menu'
type SidebarTreeRow =
| {
@@ -76,7 +85,7 @@ interface WorkspaceRowProps {
workspace: SidebarWorkspaceEntry
selected: boolean
onPress: () => void
onLongPress: () => void
drag: () => void
}
function deriveProjectDisplayName(input: { projectKey: string; projectName: string }): string {
@@ -135,10 +144,25 @@ function resolveStatusDotColor(input: { theme: ReturnType<typeof useUnistyles>['
: theme.colors.border
}
function WorkspaceStatusDot({ bucket }: { bucket: SidebarWorkspaceEntry['statusBucket'] }) {
function WorkspaceStatusIndicator({
bucket,
loading = false,
}: {
bucket: SidebarWorkspaceEntry['statusBucket']
loading?: boolean
}) {
const { theme } = useUnistyles()
const color = resolveStatusDotColor({ theme, bucket })
return <View style={[styles.workspaceStatusDot, { backgroundColor: color }]} />
return (
<View style={styles.workspaceStatusDot}>
{loading ? (
<ActivityIndicator size={8} color={theme.colors.foregroundMuted} />
) : (
<View style={[styles.workspaceStatusDotFill, { backgroundColor: color }]} />
)}
</View>
)
}
function ProjectRow({
@@ -201,10 +225,71 @@ function ProjectRow({
)
}
function WorkspaceRow({ workspace, selected, onPress, onLongPress }: WorkspaceRowProps) {
const didLongPressRef = useRef(false)
function WorkspaceRow({ workspace, selected, onPress, drag }: WorkspaceRowProps) {
const { theme } = useUnistyles()
const createdAtLabel = resolveWorkspaceCreatedAtLabel(workspace)
const { setAnchorRect, setOpen } = useContextMenu()
const didLongPressRef = useRef(false)
const didLongPressCleanupTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const longPressArmedRef = useRef(false)
const longPressCancelledRef = useRef(false)
const didStartDragRef = useRef(false)
const touchStartRef = useRef<{ x: number; y: number } | null>(null)
const archiveStatus = useCheckoutGitActionsStore((state) =>
state.getStatus({
serverId: workspace.serverId,
cwd: workspace.cwd,
actionId: 'archive-worktree',
})
)
const runArchiveWorktree = useCheckoutGitActionsStore((state) => state.archiveWorktree)
const isArchiving = archiveStatus === 'pending'
const openContextMenuAtTouchStart = useCallback(() => {
const point = touchStartRef.current
if (!point) {
return
}
const statusBarHeight = Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0
setAnchorRect({
x: point.x,
y: point.y + statusBarHeight,
width: 0,
height: 0,
})
setOpen(true)
}, [setAnchorRect, setOpen])
const handleArchiveWorktree = useCallback(() => {
if (!workspace.isPaseoOwnedWorktree) {
return
}
void confirmDialog({
title: 'Archive worktree?',
message: `Archive this worktree?\n\n${workspace.cwd}`,
confirmLabel: 'Archive',
cancelLabel: 'Cancel',
destructive: true,
})
.then((confirmed) => {
if (!confirmed) {
return
}
return runArchiveWorktree({
serverId: workspace.serverId,
cwd: workspace.cwd,
worktreePath: workspace.cwd,
})
})
.catch((err) => {
const message = err instanceof Error ? err.message : 'Failed to archive worktree'
Alert.alert('Error', message)
})
}, [runArchiveWorktree, workspace.cwd, workspace.isPaseoOwnedWorktree, workspace.serverId])
const handlePress = useCallback(() => {
if (didLongPressRef.current) {
didLongPressRef.current = false
@@ -214,25 +299,115 @@ function WorkspaceRow({ workspace, selected, onPress, onLongPress }: WorkspaceRo
}, [onPress])
const handleLongPress = useCallback(() => {
if (longPressCancelledRef.current) {
return
}
didLongPressRef.current = true
onLongPress()
}, [onLongPress])
longPressArmedRef.current = true
}, [])
return (
<Pressable
const moveMonitorGesture = useMemo(() => {
if (Platform.OS === 'web') {
return null
}
const CANCEL_SLOP_PX = 10
const DRAG_SLOP_PX = 8
return Gesture.Pan()
.manualActivation(true)
.runOnJS(true)
.onTouchesDown((event) => {
const touch = event.changedTouches[0]
if (!touch) {
return
}
touchStartRef.current = { x: touch.absoluteX, y: touch.absoluteY }
})
.onTouchesMove((event, stateManager) => {
const touch = event.changedTouches[0]
if (!touch || event.numberOfTouches !== 1) {
stateManager.fail()
return
}
const start = touchStartRef.current
if (!start) {
touchStartRef.current = { x: touch.absoluteX, y: touch.absoluteY }
return
}
const dx = touch.absoluteX - start.x
const dy = touch.absoluteY - start.y
const distance = Math.sqrt(dx * dx + dy * dy)
if (!longPressArmedRef.current) {
if (distance > CANCEL_SLOP_PX) {
longPressCancelledRef.current = true
stateManager.fail()
}
return
}
if (didStartDragRef.current) {
return
}
if (distance > DRAG_SLOP_PX) {
didStartDragRef.current = true
drag()
stateManager.fail()
}
})
}, [drag])
const trigger = (
<ContextMenuTrigger
enabledOnMobile={false}
style={({ pressed, hovered = false }) => [
styles.workspaceRow,
selected && styles.workspaceRowSelected,
hovered && styles.workspaceRowHovered,
pressed && styles.workspaceRowPressed,
]}
onPressIn={(event) => {
if (didLongPressCleanupTimerRef.current) {
clearTimeout(didLongPressCleanupTimerRef.current)
didLongPressCleanupTimerRef.current = null
}
longPressCancelledRef.current = false
longPressArmedRef.current = false
didStartDragRef.current = false
touchStartRef.current = { x: event.nativeEvent.pageX, y: event.nativeEvent.pageY }
}}
onPressOut={() => {
if (Platform.OS === 'web') {
return
}
if (!longPressArmedRef.current || didStartDragRef.current) {
longPressCancelledRef.current = false
longPressArmedRef.current = false
didStartDragRef.current = false
touchStartRef.current = null
return
}
openContextMenuAtTouchStart()
didLongPressCleanupTimerRef.current = setTimeout(() => {
didLongPressRef.current = false
didLongPressCleanupTimerRef.current = null
}, 0)
longPressCancelledRef.current = false
longPressArmedRef.current = false
didStartDragRef.current = false
touchStartRef.current = null
}}
onPress={handlePress}
onLongPress={handleLongPress}
delayLongPress={200}
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
>
<View style={styles.workspaceRowLeft}>
<WorkspaceStatusDot bucket={workspace.statusBucket} />
<WorkspaceStatusIndicator bucket={workspace.statusBucket} loading={isArchiving} />
<Text style={styles.workspaceBranchText} numberOfLines={1}>
{resolveWorkspaceBranchLabel(workspace)}
</Text>
@@ -242,7 +417,54 @@ function WorkspaceRow({ workspace, selected, onPress, onLongPress }: WorkspaceRo
{createdAtLabel}
</Text>
) : null}
</Pressable>
</ContextMenuTrigger>
)
return (
<>
{moveMonitorGesture ? (
<GestureDetector gesture={moveMonitorGesture}>{trigger}</GestureDetector>
) : (
trigger
)}
<ContextMenuContent
align="start"
width={220}
testID={`sidebar-workspace-context-${workspace.workspaceKey}`}
>
<ContextMenuItem
leading={<Archive size={16} color={theme.colors.foregroundMuted} />}
destructive
disabled={!workspace.isPaseoOwnedWorktree}
status={workspace.isPaseoOwnedWorktree ? archiveStatus : 'idle'}
pendingLabel="Archiving…"
successLabel="Archived"
onSelect={handleArchiveWorktree}
testID={`sidebar-workspace-context-${workspace.workspaceKey}-archive`}
>
Archive worktree
</ContextMenuItem>
</ContextMenuContent>
</>
)
}
function WorkspaceRowWithMenu({
workspace,
selected,
onPress,
drag,
}: {
workspace: SidebarWorkspaceEntry
selected: boolean
onPress: () => void
drag: () => void
}) {
return (
<ContextMenu>
<WorkspaceRow workspace={workspace} selected={selected} onPress={onPress} drag={drag} />
</ContextMenu>
)
}
@@ -452,7 +674,7 @@ export function SidebarAgentList({
activeWorkspaceSelection.workspaceId === item.workspace.cwd
return (
<WorkspaceRow
<WorkspaceRowWithMenu
workspace={item.workspace}
selected={isSelected}
onPress={() => {
@@ -462,7 +684,7 @@ export function SidebarAgentList({
onWorkspacePress?.()
navigate(workspaceRoute as any)
}}
onLongPress={drag}
drag={drag}
/>
)
},
@@ -671,6 +893,14 @@ const styles = StyleSheet.create((theme) => ({
height: 8,
borderRadius: theme.borderRadius.full,
flexShrink: 0,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
workspaceStatusDotFill: {
width: 8,
height: 8,
borderRadius: theme.borderRadius.full,
},
workspaceBranchText: {
color: theme.colors.foreground,
@@ -58,6 +58,10 @@ function useContextMenuContext(componentName: string): ContextMenuContextValue {
return ctx;
}
export function useContextMenu(): ContextMenuContextValue {
return useContextMenuContext("useContextMenu");
}
function useControllableOpenState({
open,
defaultOpen,
@@ -55,6 +55,9 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
}
const serverLabel = serverLabelById.get(serverId) ?? serverId;
for (const agent of agents.values()) {
if (agent.archivedAt) {
continue;
}
const nextAgent: AggregatedAgent = {
id: agent.id,
serverId,
@@ -74,9 +74,6 @@ export function useAllAgentsList(options?: {
serverId,
serverLabel,
});
if (aggregated.archivedAt) {
continue;
}
if (aggregated.labels.ui !== "true") {
continue;
}
+1 -1
View File
@@ -1113,7 +1113,7 @@ export class HostRuntimeStore {
controller.markAgentDirectorySyncLoading();
try {
const payload = await client.fetchAgents({
filter: input.filter ?? { labels: { ui: "true" } },
filter: input.filter ?? { labels: { ui: "true" }, includeArchived: true },
...(input.subscribe ? { subscribe: input.subscribe } : {}),
...(input.page ? { page: input.page } : {}),
});
@@ -86,6 +86,49 @@ function invalidateWorktreeList() {
predicate: (query) =>
Array.isArray(query.queryKey) && query.queryKey[0] === "paseoWorktreeList",
});
void queryClient.invalidateQueries({
predicate: (query) =>
Array.isArray(query.queryKey) && query.queryKey[0] === "sidebarPaseoWorktreeList",
});
}
function removeWorktreeFromCachedLists(input: {
serverId: string;
worktreePath: string;
}): void {
const serverId = input.serverId.trim();
const worktreePath = input.worktreePath.trim();
if (!serverId || !worktreePath) {
return;
}
const removeFromList = (current: unknown) => {
if (!Array.isArray(current)) {
return current;
}
const filtered = current.filter((entry) => entry?.worktreePath !== worktreePath);
return filtered.length === current.length ? current : filtered;
};
queryClient.setQueriesData(
{
predicate: (query) =>
Array.isArray(query.queryKey) &&
query.queryKey[0] === "paseoWorktreeList" &&
query.queryKey[1] === serverId,
},
removeFromList
);
queryClient.setQueriesData(
{
predicate: (query) =>
Array.isArray(query.queryKey) &&
query.queryKey[0] === "sidebarPaseoWorktreeList" &&
query.queryKey[1] === serverId,
},
removeFromList
);
}
const successTimers = new Map<string, ReturnType<typeof setTimeout>>();
@@ -267,6 +310,7 @@ export const useCheckoutGitActionsStore = create<CheckoutGitActionsStoreState>()
if (payload.error) {
throw new Error(payload.error.message);
}
removeWorktreeFromCachedLists({ serverId, worktreePath });
invalidateWorktreeList();
},
});
@@ -801,6 +801,15 @@ export class AgentManager {
async closeAgent(agentId: string): Promise<void> {
const agent = this.requireAgent(agentId);
this.logger.trace(
{
agentId,
lifecycle: agent.lifecycle,
hasPendingRun: Boolean(agent.pendingRun),
pendingPermissions: agent.pendingPermissions.size,
},
"closeAgent: start"
);
this.agents.delete(agentId);
this.liveEventPumps.delete(agentId);
this.liveEventBacklog.delete(agentId);
@@ -816,6 +825,7 @@ export class AgentManager {
};
await session.close();
this.emitState(closedAgent);
this.logger.trace({ agentId }, "closeAgent: completed");
}
async setAgentMode(agentId: string, modeId: string): Promise<void> {
@@ -1005,7 +1015,24 @@ export class AgentManager {
options?: AgentRunOptions
): AsyncGenerator<AgentStreamEvent> {
const existingAgent = this.requireAgent(agentId);
this.logger.trace(
{
agentId,
lifecycle: existingAgent.lifecycle,
hasPendingRun: Boolean(existingAgent.pendingRun),
promptType: typeof prompt === "string" ? "string" : "structured",
hasRunOptions: Boolean(options),
},
"streamAgent: requested"
);
if (existingAgent.pendingRun) {
this.logger.trace(
{
agentId,
lifecycle: existingAgent.lifecycle,
},
"streamAgent: rejected because pendingRun already exists"
);
throw new Error(`Agent ${agentId} already has an active run`);
}
@@ -1015,12 +1042,31 @@ export class AgentManager {
let finalized = false;
const finalize = (error?: string) => {
this.logger.trace(
{
agentId,
error,
alreadyFinalized: finalized,
lifecycle: agent.lifecycle,
hasPendingRun: Boolean(agent.pendingRun),
},
"streamAgent.finalize: invoked"
);
if (finalized) {
return;
}
finalized = true;
if (agent.pendingRun !== streamForwarder) {
this.logger.trace(
{
agentId,
error,
lifecycle: agent.lifecycle,
hasPendingRun: Boolean(agent.pendingRun),
},
"streamAgent.finalize: skipped because pendingRun no longer points to streamForwarder"
);
if (error) {
agent.lastError = error;
}
@@ -1043,6 +1089,15 @@ export class AgentManager {
mutableAgent.cwd
);
}
this.logger.trace(
{
agentId,
lifecycle: mutableAgent.lifecycle,
hasPendingRun: Boolean(mutableAgent.pendingRun),
terminalError,
},
"streamAgent.finalize: applying terminal state"
);
this.emitState(mutableAgent);
this.flushLiveEventBacklog(mutableAgent);
};
@@ -1054,6 +1109,22 @@ export class AgentManager {
try {
for await (const event of iterator) {
self.handleStreamEvent(agent, event);
if (
event.type === "turn_started" ||
event.type === "turn_completed" ||
event.type === "turn_failed" ||
event.type === "turn_canceled"
) {
self.logger.trace(
{
agentId,
eventType: event.type,
lifecycle: agent.lifecycle,
hasPendingRun: Boolean(agent.pendingRun),
},
"streamAgent: forwarded terminal/turn event"
);
}
yield event;
}
} catch (error) {
@@ -1079,6 +1150,14 @@ export class AgentManager {
// deterministically order idle->running transitions.
this.touchUpdatedAt(agent);
self.emitState(agent);
this.logger.trace(
{
agentId,
lifecycle: agent.lifecycle,
hasPendingRun: Boolean(agent.pendingRun),
},
"streamAgent: started"
);
return streamForwarder;
}
@@ -1743,6 +1822,14 @@ export class AgentManager {
}
break;
case "turn_completed":
this.logger.trace(
{
agentId: agent.id,
lifecycle: agent.lifecycle,
hasPendingRun: Boolean(agent.pendingRun),
},
"handleStreamEvent: turn_completed"
);
agent.lastUsage = event.usage;
agent.lastError = undefined;
if (!agent.pendingRun && agent.lifecycle !== "idle") {
@@ -1774,6 +1861,14 @@ export class AgentManager {
this.emitState(agent);
break;
case "turn_canceled":
this.logger.trace(
{
agentId: agent.id,
lifecycle: agent.lifecycle,
hasPendingRun: Boolean(agent.pendingRun),
},
"handleStreamEvent: turn_canceled"
);
if (!agent.pendingRun) {
(agent as ActiveManagedAgent).lifecycle = "idle";
}
@@ -1792,6 +1887,14 @@ export class AgentManager {
this.emitState(agent);
break;
case "turn_started":
this.logger.trace(
{
agentId: agent.id,
lifecycle: agent.lifecycle,
hasPendingRun: Boolean(agent.pendingRun),
},
"handleStreamEvent: turn_started"
);
if (!agent.pendingRun) {
(agent as ActiveManagedAgent).lifecycle = "running";
this.emitState(agent);
@@ -2143,6 +2246,14 @@ export class AgentManager {
// Keep consuming provider events even during an active foreground run,
// then replay them immediately once that run settles.
if (latest.pendingRun) {
this.logger.trace(
{
agentId: latest.id,
eventType: event.type,
backlogSize: (this.liveEventBacklog.get(latest.id)?.length ?? 0) + 1,
},
"Live event pump: queued event because pendingRun is active"
);
this.enqueueLiveEvent(latest.id, event);
continue;
}
@@ -1906,6 +1906,16 @@ class ClaudeAgentSession implements AgentSession {
}
async close(): Promise<void> {
this.logger.trace(
{
claudeSessionId: this.claudeSessionId,
turnState: this.turnState,
hasQuery: Boolean(this.query),
hasInput: Boolean(this.input),
hasActiveForegroundTurn: Boolean(this.activeForegroundTurn),
},
"Claude session close: start"
);
this.closed = true;
this.rejectAllPendingPermissions(new Error("Claude session closed"));
this.cancelCurrentTurn?.();
@@ -1922,6 +1932,10 @@ class ClaudeAgentSession implements AgentSession {
await this.awaitWithTimeout(this.query?.return?.(), "close query return");
this.query = null;
this.input = null;
this.logger.trace(
{ claudeSessionId: this.claudeSessionId, turnState: this.turnState },
"Claude session close: completed"
);
}
async listCommands(): Promise<AgentSlashCommand[]> {
@@ -2235,8 +2249,11 @@ class ClaudeAgentSession implements AgentSession {
label: string
): Promise<void> {
if (!promise) {
this.logger.trace({ label }, "Claude query operation skipped (no promise)");
return;
}
const startedAt = Date.now();
this.logger.trace({ label }, "Claude query operation wait start");
try {
await Promise.race([
promise,
@@ -2244,6 +2261,10 @@ class ClaudeAgentSession implements AgentSession {
setTimeout(() => reject(new Error("timeout")), 3_000);
}),
]);
this.logger.trace(
{ label, durationMs: Date.now() - startedAt },
"Claude query operation settled"
);
} catch (error) {
this.logger.warn({ err: error, label }, "Claude query operation did not settle cleanly");
}
@@ -2464,6 +2485,23 @@ class ClaudeAgentSession implements AgentSession {
}
private emitRunEvent(run: RunRecord, event: AgentStreamEvent): void {
if (
event.type === "turn_started" ||
event.type === "turn_completed" ||
event.type === "turn_failed" ||
event.type === "turn_canceled"
) {
this.logger.trace(
{
runId: run.id,
owner: run.owner,
runState: run.state,
eventType: event.type,
routedTo: run.owner === "foreground" && run.queue ? "foreground_queue" : "live_queue",
},
"Claude run event emitted"
);
}
if (run.owner === "foreground" && run.queue) {
run.queue.push(event);
if (
@@ -2494,6 +2532,16 @@ class ClaudeAgentSession implements AgentSession {
this.activeForegroundTurn = null;
this.preReplayMetadataSeen = false;
}
this.logger.trace(
{
runId: run.id,
owner: run.owner,
eventType: event.type,
runState: run.state,
hasActiveForegroundTurn: Boolean(this.activeForegroundTurn),
},
"Claude run terminal event handled"
);
this.transitionTurnStateFromActiveRuns(`run ${run.id} terminal`);
}
@@ -2789,6 +2837,13 @@ class ClaudeAgentSession implements AgentSession {
}
if (next.done) {
this.logger.trace(
{
claudeSessionId: this.claudeSessionId,
activeRunCount: this.runTracker.listActiveRuns().length,
},
"Claude query pump next() returned done"
);
this.input?.end();
await this.awaitWithTimeout(q.return?.(), "query pump return on done");
if (this.query === q) {
@@ -2834,6 +2889,18 @@ class ClaudeAgentSession implements AgentSession {
run: route.run,
message,
});
this.logger.trace(
{
claudeSessionId: this.claudeSessionId,
messageType: message.type,
routeReason: route.reason,
runId: route.run?.id ?? null,
runOwner: route.run?.owner ?? null,
suppressTerminalEvents,
metadataOnly,
},
"Claude query pump routed SDK message"
);
if (route.run) {
this.transitionTurnStateFromActiveRuns(`routed via ${route.reason}`);
this.runTracker.bindIdentifiers(route.run, identifiers);
@@ -2898,6 +2965,16 @@ class ClaudeAgentSession implements AgentSession {
// Pre-replay success results are stale in practice (leftover from an
// earlier query segment) and must not end the current foreground run.
if (resultSubtype === "success") {
this.logger.trace(
{
runId: run.id,
runOwner: run.owner,
runState: run.state,
promptReplaySeen: run.promptReplaySeen,
resultSubtype,
},
"Suppressing pre-replay foreground success result terminal event"
);
return true;
}
@@ -2917,6 +2994,7 @@ class ClaudeAgentSession implements AgentSession {
message: SDKMessage,
identifiers: EventIdentifiers
): void {
const previousState = run.state;
if (
message.type === "user" &&
identifiers.messageId &&
@@ -2941,8 +3019,25 @@ class ClaudeAgentSession implements AgentSession {
if (message.type === "result") {
this.runTracker.transition(run, "finalizing");
} else {
return;
}
if (run.state !== previousState) {
this.logger.trace(
{
runId: run.id,
owner: run.owner,
messageType: message.type,
previousState,
nextState: run.state,
taskId: identifiers.taskId,
parentMessageId: identifiers.parentMessageId,
messageId: identifiers.messageId,
},
"Updated Claude run lifecycle from SDK message"
);
}
}
private shouldSuppressLocalReplayActivity(message: SDKMessage): boolean {
+16
View File
@@ -736,10 +736,15 @@ export class Session {
private async interruptAgentIfRunning(agentId: string): Promise<void> {
const snapshot = this.agentManager.getAgent(agentId)
if (!snapshot) {
this.sessionLogger.trace({ agentId }, 'interruptAgentIfRunning: agent not found')
throw new Error(`Agent ${agentId} not found`)
}
if (snapshot.lifecycle !== 'running' && !snapshot.pendingRun) {
this.sessionLogger.trace(
{ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) },
'interruptAgentIfRunning: skipping because agent is not running'
)
return
}
@@ -787,9 +792,18 @@ export class Session {
prompt: AgentPromptInput,
runOptions?: AgentRunOptions
): { ok: true } | { ok: false; error: string } {
this.sessionLogger.trace(
{
agentId,
promptType: typeof prompt === 'string' ? 'string' : 'structured',
hasRunOptions: Boolean(runOptions),
},
'startAgentStream: requested'
)
let iterator: AsyncGenerator<AgentStreamEvent>
try {
iterator = this.agentManager.streamAgent(agentId, prompt, runOptions)
this.sessionLogger.trace({ agentId }, 'startAgentStream: streamAgent returned iterator')
} catch (error) {
this.handleAgentRunError(agentId, error, 'Failed to start agent run')
const message =
@@ -802,7 +816,9 @@ export class Session {
for await (const _ of iterator) {
// Events are forwarded via the session's AgentManager subscription.
}
this.sessionLogger.trace({ agentId }, 'startAgentStream: iterator drained')
} catch (error) {
this.sessionLogger.trace({ agentId, err: error }, 'startAgentStream: iterator threw')
this.handleAgentRunError(agentId, error, 'Agent stream failed')
}
})()
+6 -3
View File
@@ -10,8 +10,11 @@ DAEMON_PORT=$(get-port 6767 6768 6769 6770 6771 6772 6773)
METRO_PORT=$(get-port 8081 8082 8083 8084 8085 8086 8087)
# Use a temporary PASEO_HOME to avoid conflicts between dev instances
# export PASEO_HOME=$(mktemp -d "${TMPDIR:-/tmp}/paseo-dev.XXXXXX")
# trap "rm -rf '$PASEO_HOME'" EXIT
if [ -z "${PASEO_HOME}" ]; then
export PASEO_HOME
PASEO_HOME="$(mktemp -d "${TMPDIR:-/tmp}/paseo-dev.XXXXXX")"
trap "rm -rf '$PASEO_HOME'" EXIT
fi
# Build CORS origins for this Expo instance
CORS_ORIGINS="http://localhost:${METRO_PORT},http://127.0.0.1:${METRO_PORT}"
@@ -24,7 +27,7 @@ echo " Paseo Dev"
echo "══════════════════════════════════════════════════════"
echo " Daemon: http://localhost:${DAEMON_PORT}"
echo " Metro: http://localhost:${METRO_PORT}"
# echo " Home: ${PASEO_HOME}"
echo " Home: ${PASEO_HOME}"
echo "══════════════════════════════════════════════════════"
# Export for child processes (overrides .env values)