feat: add worktree archiving with context menu and improved logging
This commit is contained in:
@@ -98,6 +98,7 @@ function AgentListRow({
|
|||||||
const timeAgo = formatTimeAgo(agent.lastActivityAt);
|
const timeAgo = formatTimeAgo(agent.lastActivityAt);
|
||||||
const agentKey = `${agent.serverId}:${agent.id}`;
|
const agentKey = `${agent.serverId}:${agent.id}`;
|
||||||
const isSelected = selectedAgentId === agentKey;
|
const isSelected = selectedAgentId === agentKey;
|
||||||
|
const archivedLabel = agent.archivedAt ? "Archived" : null;
|
||||||
const checkoutQuery = useCheckoutStatusCacheOnly({
|
const checkoutQuery = useCheckoutStatusCacheOnly({
|
||||||
serverId: agent.serverId,
|
serverId: agent.serverId,
|
||||||
cwd: agent.cwd,
|
cwd: agent.cwd,
|
||||||
@@ -137,7 +138,8 @@ function AgentListRow({
|
|||||||
|
|
||||||
<Text style={styles.secondaryRow} numberOfLines={1}>
|
<Text style={styles.secondaryRow} numberOfLines={1}>
|
||||||
{shortenPath(projectPath)}
|
{shortenPath(projectPath)}
|
||||||
{branchLabel ? ` · ${branchLabel}` : ""} · {timeAgo}
|
{branchLabel ? ` · ${branchLabel}` : ""}
|
||||||
|
{archivedLabel ? ` · ${archivedLabel}` : ""} · {timeAgo}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</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 { useQueries } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { router, usePathname, useSegments } from 'expo-router'
|
import { router, usePathname, useSegments } from 'expo-router'
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
||||||
import { type GestureType } from 'react-native-gesture-handler'
|
import { Gesture, GestureDetector, type GestureType } from 'react-native-gesture-handler'
|
||||||
import { ChevronDown, ChevronRight } from 'lucide-react-native'
|
import { Archive, ChevronDown, ChevronRight } from 'lucide-react-native'
|
||||||
import { DraggableList, type DraggableRenderItemInfo } from './draggable-list'
|
import { DraggableList, type DraggableRenderItemInfo } from './draggable-list'
|
||||||
import { getHostRuntimeStore, isHostRuntimeConnected } from '@/runtime/host-runtime'
|
import { getHostRuntimeStore, isHostRuntimeConnected } from '@/runtime/host-runtime'
|
||||||
import { projectIconQueryKey } from '@/hooks/use-project-icon-query'
|
import { projectIconQueryKey } from '@/hooks/use-project-icon-query'
|
||||||
@@ -27,8 +27,17 @@ import {
|
|||||||
type SidebarWorkspaceEntry,
|
type SidebarWorkspaceEntry,
|
||||||
} from '@/hooks/use-sidebar-agents-list'
|
} from '@/hooks/use-sidebar-agents-list'
|
||||||
import { useSidebarOrderStore } from '@/stores/sidebar-order-store'
|
import { useSidebarOrderStore } from '@/stores/sidebar-order-store'
|
||||||
|
import { useCheckoutGitActionsStore } from '@/stores/checkout-git-actions-store'
|
||||||
import { formatTimeAgo } from '@/utils/time'
|
import { formatTimeAgo } from '@/utils/time'
|
||||||
import type { SidebarStateBucket } from '@/utils/sidebar-agent-state'
|
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 =
|
type SidebarTreeRow =
|
||||||
| {
|
| {
|
||||||
@@ -76,7 +85,7 @@ interface WorkspaceRowProps {
|
|||||||
workspace: SidebarWorkspaceEntry
|
workspace: SidebarWorkspaceEntry
|
||||||
selected: boolean
|
selected: boolean
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
onLongPress: () => void
|
drag: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function deriveProjectDisplayName(input: { projectKey: string; projectName: string }): string {
|
function deriveProjectDisplayName(input: { projectKey: string; projectName: string }): string {
|
||||||
@@ -135,10 +144,25 @@ function resolveStatusDotColor(input: { theme: ReturnType<typeof useUnistyles>['
|
|||||||
: theme.colors.border
|
: theme.colors.border
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkspaceStatusDot({ bucket }: { bucket: SidebarWorkspaceEntry['statusBucket'] }) {
|
function WorkspaceStatusIndicator({
|
||||||
|
bucket,
|
||||||
|
loading = false,
|
||||||
|
}: {
|
||||||
|
bucket: SidebarWorkspaceEntry['statusBucket']
|
||||||
|
loading?: boolean
|
||||||
|
}) {
|
||||||
const { theme } = useUnistyles()
|
const { theme } = useUnistyles()
|
||||||
const color = resolveStatusDotColor({ theme, bucket })
|
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({
|
function ProjectRow({
|
||||||
@@ -201,10 +225,71 @@ function ProjectRow({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkspaceRow({ workspace, selected, onPress, onLongPress }: WorkspaceRowProps) {
|
function WorkspaceRow({ workspace, selected, onPress, drag }: WorkspaceRowProps) {
|
||||||
const didLongPressRef = useRef(false)
|
const { theme } = useUnistyles()
|
||||||
const createdAtLabel = resolveWorkspaceCreatedAtLabel(workspace)
|
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(() => {
|
const handlePress = useCallback(() => {
|
||||||
if (didLongPressRef.current) {
|
if (didLongPressRef.current) {
|
||||||
didLongPressRef.current = false
|
didLongPressRef.current = false
|
||||||
@@ -214,25 +299,115 @@ function WorkspaceRow({ workspace, selected, onPress, onLongPress }: WorkspaceRo
|
|||||||
}, [onPress])
|
}, [onPress])
|
||||||
|
|
||||||
const handleLongPress = useCallback(() => {
|
const handleLongPress = useCallback(() => {
|
||||||
|
if (longPressCancelledRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
didLongPressRef.current = true
|
didLongPressRef.current = true
|
||||||
onLongPress()
|
longPressArmedRef.current = true
|
||||||
}, [onLongPress])
|
}, [])
|
||||||
|
|
||||||
return (
|
const moveMonitorGesture = useMemo(() => {
|
||||||
<Pressable
|
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 }) => [
|
style={({ pressed, hovered = false }) => [
|
||||||
styles.workspaceRow,
|
styles.workspaceRow,
|
||||||
selected && styles.workspaceRowSelected,
|
selected && styles.workspaceRowSelected,
|
||||||
hovered && styles.workspaceRowHovered,
|
hovered && styles.workspaceRowHovered,
|
||||||
pressed && styles.workspaceRowPressed,
|
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}
|
onPress={handlePress}
|
||||||
onLongPress={handleLongPress}
|
onLongPress={handleLongPress}
|
||||||
delayLongPress={200}
|
delayLongPress={200}
|
||||||
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
|
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
|
||||||
>
|
>
|
||||||
<View style={styles.workspaceRowLeft}>
|
<View style={styles.workspaceRowLeft}>
|
||||||
<WorkspaceStatusDot bucket={workspace.statusBucket} />
|
<WorkspaceStatusIndicator bucket={workspace.statusBucket} loading={isArchiving} />
|
||||||
<Text style={styles.workspaceBranchText} numberOfLines={1}>
|
<Text style={styles.workspaceBranchText} numberOfLines={1}>
|
||||||
{resolveWorkspaceBranchLabel(workspace)}
|
{resolveWorkspaceBranchLabel(workspace)}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -242,7 +417,54 @@ function WorkspaceRow({ workspace, selected, onPress, onLongPress }: WorkspaceRo
|
|||||||
{createdAtLabel}
|
{createdAtLabel}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : 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
|
activeWorkspaceSelection.workspaceId === item.workspace.cwd
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceRow
|
<WorkspaceRowWithMenu
|
||||||
workspace={item.workspace}
|
workspace={item.workspace}
|
||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -462,7 +684,7 @@ export function SidebarAgentList({
|
|||||||
onWorkspacePress?.()
|
onWorkspacePress?.()
|
||||||
navigate(workspaceRoute as any)
|
navigate(workspaceRoute as any)
|
||||||
}}
|
}}
|
||||||
onLongPress={drag}
|
drag={drag}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -671,6 +893,14 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
height: 8,
|
height: 8,
|
||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
workspaceStatusDotFill: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: theme.borderRadius.full,
|
||||||
},
|
},
|
||||||
workspaceBranchText: {
|
workspaceBranchText: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ function useContextMenuContext(componentName: string): ContextMenuContextValue {
|
|||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useContextMenu(): ContextMenuContextValue {
|
||||||
|
return useContextMenuContext("useContextMenu");
|
||||||
|
}
|
||||||
|
|
||||||
function useControllableOpenState({
|
function useControllableOpenState({
|
||||||
open,
|
open,
|
||||||
defaultOpen,
|
defaultOpen,
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
|
|||||||
}
|
}
|
||||||
const serverLabel = serverLabelById.get(serverId) ?? serverId;
|
const serverLabel = serverLabelById.get(serverId) ?? serverId;
|
||||||
for (const agent of agents.values()) {
|
for (const agent of agents.values()) {
|
||||||
|
if (agent.archivedAt) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const nextAgent: AggregatedAgent = {
|
const nextAgent: AggregatedAgent = {
|
||||||
id: agent.id,
|
id: agent.id,
|
||||||
serverId,
|
serverId,
|
||||||
|
|||||||
@@ -74,9 +74,6 @@ export function useAllAgentsList(options?: {
|
|||||||
serverId,
|
serverId,
|
||||||
serverLabel,
|
serverLabel,
|
||||||
});
|
});
|
||||||
if (aggregated.archivedAt) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (aggregated.labels.ui !== "true") {
|
if (aggregated.labels.ui !== "true") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1113,7 +1113,7 @@ export class HostRuntimeStore {
|
|||||||
controller.markAgentDirectorySyncLoading();
|
controller.markAgentDirectorySyncLoading();
|
||||||
try {
|
try {
|
||||||
const payload = await client.fetchAgents({
|
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.subscribe ? { subscribe: input.subscribe } : {}),
|
||||||
...(input.page ? { page: input.page } : {}),
|
...(input.page ? { page: input.page } : {}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -86,6 +86,49 @@ function invalidateWorktreeList() {
|
|||||||
predicate: (query) =>
|
predicate: (query) =>
|
||||||
Array.isArray(query.queryKey) && query.queryKey[0] === "paseoWorktreeList",
|
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>>();
|
const successTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
@@ -267,6 +310,7 @@ export const useCheckoutGitActionsStore = create<CheckoutGitActionsStoreState>()
|
|||||||
if (payload.error) {
|
if (payload.error) {
|
||||||
throw new Error(payload.error.message);
|
throw new Error(payload.error.message);
|
||||||
}
|
}
|
||||||
|
removeWorktreeFromCachedLists({ serverId, worktreePath });
|
||||||
invalidateWorktreeList();
|
invalidateWorktreeList();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -801,6 +801,15 @@ export class AgentManager {
|
|||||||
|
|
||||||
async closeAgent(agentId: string): Promise<void> {
|
async closeAgent(agentId: string): Promise<void> {
|
||||||
const agent = this.requireAgent(agentId);
|
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.agents.delete(agentId);
|
||||||
this.liveEventPumps.delete(agentId);
|
this.liveEventPumps.delete(agentId);
|
||||||
this.liveEventBacklog.delete(agentId);
|
this.liveEventBacklog.delete(agentId);
|
||||||
@@ -816,6 +825,7 @@ export class AgentManager {
|
|||||||
};
|
};
|
||||||
await session.close();
|
await session.close();
|
||||||
this.emitState(closedAgent);
|
this.emitState(closedAgent);
|
||||||
|
this.logger.trace({ agentId }, "closeAgent: completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAgentMode(agentId: string, modeId: string): Promise<void> {
|
async setAgentMode(agentId: string, modeId: string): Promise<void> {
|
||||||
@@ -1005,7 +1015,24 @@ export class AgentManager {
|
|||||||
options?: AgentRunOptions
|
options?: AgentRunOptions
|
||||||
): AsyncGenerator<AgentStreamEvent> {
|
): AsyncGenerator<AgentStreamEvent> {
|
||||||
const existingAgent = this.requireAgent(agentId);
|
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) {
|
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`);
|
throw new Error(`Agent ${agentId} already has an active run`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,12 +1042,31 @@ export class AgentManager {
|
|||||||
|
|
||||||
let finalized = false;
|
let finalized = false;
|
||||||
const finalize = (error?: string) => {
|
const finalize = (error?: string) => {
|
||||||
|
this.logger.trace(
|
||||||
|
{
|
||||||
|
agentId,
|
||||||
|
error,
|
||||||
|
alreadyFinalized: finalized,
|
||||||
|
lifecycle: agent.lifecycle,
|
||||||
|
hasPendingRun: Boolean(agent.pendingRun),
|
||||||
|
},
|
||||||
|
"streamAgent.finalize: invoked"
|
||||||
|
);
|
||||||
if (finalized) {
|
if (finalized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
finalized = true;
|
finalized = true;
|
||||||
|
|
||||||
if (agent.pendingRun !== streamForwarder) {
|
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) {
|
if (error) {
|
||||||
agent.lastError = error;
|
agent.lastError = error;
|
||||||
}
|
}
|
||||||
@@ -1043,6 +1089,15 @@ export class AgentManager {
|
|||||||
mutableAgent.cwd
|
mutableAgent.cwd
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.logger.trace(
|
||||||
|
{
|
||||||
|
agentId,
|
||||||
|
lifecycle: mutableAgent.lifecycle,
|
||||||
|
hasPendingRun: Boolean(mutableAgent.pendingRun),
|
||||||
|
terminalError,
|
||||||
|
},
|
||||||
|
"streamAgent.finalize: applying terminal state"
|
||||||
|
);
|
||||||
this.emitState(mutableAgent);
|
this.emitState(mutableAgent);
|
||||||
this.flushLiveEventBacklog(mutableAgent);
|
this.flushLiveEventBacklog(mutableAgent);
|
||||||
};
|
};
|
||||||
@@ -1054,6 +1109,22 @@ export class AgentManager {
|
|||||||
try {
|
try {
|
||||||
for await (const event of iterator) {
|
for await (const event of iterator) {
|
||||||
self.handleStreamEvent(agent, event);
|
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;
|
yield event;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1079,6 +1150,14 @@ export class AgentManager {
|
|||||||
// deterministically order idle->running transitions.
|
// deterministically order idle->running transitions.
|
||||||
this.touchUpdatedAt(agent);
|
this.touchUpdatedAt(agent);
|
||||||
self.emitState(agent);
|
self.emitState(agent);
|
||||||
|
this.logger.trace(
|
||||||
|
{
|
||||||
|
agentId,
|
||||||
|
lifecycle: agent.lifecycle,
|
||||||
|
hasPendingRun: Boolean(agent.pendingRun),
|
||||||
|
},
|
||||||
|
"streamAgent: started"
|
||||||
|
);
|
||||||
|
|
||||||
return streamForwarder;
|
return streamForwarder;
|
||||||
}
|
}
|
||||||
@@ -1743,6 +1822,14 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "turn_completed":
|
case "turn_completed":
|
||||||
|
this.logger.trace(
|
||||||
|
{
|
||||||
|
agentId: agent.id,
|
||||||
|
lifecycle: agent.lifecycle,
|
||||||
|
hasPendingRun: Boolean(agent.pendingRun),
|
||||||
|
},
|
||||||
|
"handleStreamEvent: turn_completed"
|
||||||
|
);
|
||||||
agent.lastUsage = event.usage;
|
agent.lastUsage = event.usage;
|
||||||
agent.lastError = undefined;
|
agent.lastError = undefined;
|
||||||
if (!agent.pendingRun && agent.lifecycle !== "idle") {
|
if (!agent.pendingRun && agent.lifecycle !== "idle") {
|
||||||
@@ -1774,6 +1861,14 @@ export class AgentManager {
|
|||||||
this.emitState(agent);
|
this.emitState(agent);
|
||||||
break;
|
break;
|
||||||
case "turn_canceled":
|
case "turn_canceled":
|
||||||
|
this.logger.trace(
|
||||||
|
{
|
||||||
|
agentId: agent.id,
|
||||||
|
lifecycle: agent.lifecycle,
|
||||||
|
hasPendingRun: Boolean(agent.pendingRun),
|
||||||
|
},
|
||||||
|
"handleStreamEvent: turn_canceled"
|
||||||
|
);
|
||||||
if (!agent.pendingRun) {
|
if (!agent.pendingRun) {
|
||||||
(agent as ActiveManagedAgent).lifecycle = "idle";
|
(agent as ActiveManagedAgent).lifecycle = "idle";
|
||||||
}
|
}
|
||||||
@@ -1792,6 +1887,14 @@ export class AgentManager {
|
|||||||
this.emitState(agent);
|
this.emitState(agent);
|
||||||
break;
|
break;
|
||||||
case "turn_started":
|
case "turn_started":
|
||||||
|
this.logger.trace(
|
||||||
|
{
|
||||||
|
agentId: agent.id,
|
||||||
|
lifecycle: agent.lifecycle,
|
||||||
|
hasPendingRun: Boolean(agent.pendingRun),
|
||||||
|
},
|
||||||
|
"handleStreamEvent: turn_started"
|
||||||
|
);
|
||||||
if (!agent.pendingRun) {
|
if (!agent.pendingRun) {
|
||||||
(agent as ActiveManagedAgent).lifecycle = "running";
|
(agent as ActiveManagedAgent).lifecycle = "running";
|
||||||
this.emitState(agent);
|
this.emitState(agent);
|
||||||
@@ -2143,6 +2246,14 @@ export class AgentManager {
|
|||||||
// Keep consuming provider events even during an active foreground run,
|
// Keep consuming provider events even during an active foreground run,
|
||||||
// then replay them immediately once that run settles.
|
// then replay them immediately once that run settles.
|
||||||
if (latest.pendingRun) {
|
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);
|
this.enqueueLiveEvent(latest.id, event);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1906,6 +1906,16 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
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.closed = true;
|
||||||
this.rejectAllPendingPermissions(new Error("Claude session closed"));
|
this.rejectAllPendingPermissions(new Error("Claude session closed"));
|
||||||
this.cancelCurrentTurn?.();
|
this.cancelCurrentTurn?.();
|
||||||
@@ -1922,6 +1932,10 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
await this.awaitWithTimeout(this.query?.return?.(), "close query return");
|
await this.awaitWithTimeout(this.query?.return?.(), "close query return");
|
||||||
this.query = null;
|
this.query = null;
|
||||||
this.input = null;
|
this.input = null;
|
||||||
|
this.logger.trace(
|
||||||
|
{ claudeSessionId: this.claudeSessionId, turnState: this.turnState },
|
||||||
|
"Claude session close: completed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listCommands(): Promise<AgentSlashCommand[]> {
|
async listCommands(): Promise<AgentSlashCommand[]> {
|
||||||
@@ -2235,8 +2249,11 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
label: string
|
label: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!promise) {
|
if (!promise) {
|
||||||
|
this.logger.trace({ label }, "Claude query operation skipped (no promise)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const startedAt = Date.now();
|
||||||
|
this.logger.trace({ label }, "Claude query operation wait start");
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
promise,
|
promise,
|
||||||
@@ -2244,6 +2261,10 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
setTimeout(() => reject(new Error("timeout")), 3_000);
|
setTimeout(() => reject(new Error("timeout")), 3_000);
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
this.logger.trace(
|
||||||
|
{ label, durationMs: Date.now() - startedAt },
|
||||||
|
"Claude query operation settled"
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn({ err: error, label }, "Claude query operation did not settle cleanly");
|
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 {
|
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) {
|
if (run.owner === "foreground" && run.queue) {
|
||||||
run.queue.push(event);
|
run.queue.push(event);
|
||||||
if (
|
if (
|
||||||
@@ -2494,6 +2532,16 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.activeForegroundTurn = null;
|
this.activeForegroundTurn = null;
|
||||||
this.preReplayMetadataSeen = false;
|
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`);
|
this.transitionTurnStateFromActiveRuns(`run ${run.id} terminal`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2789,6 +2837,13 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (next.done) {
|
if (next.done) {
|
||||||
|
this.logger.trace(
|
||||||
|
{
|
||||||
|
claudeSessionId: this.claudeSessionId,
|
||||||
|
activeRunCount: this.runTracker.listActiveRuns().length,
|
||||||
|
},
|
||||||
|
"Claude query pump next() returned done"
|
||||||
|
);
|
||||||
this.input?.end();
|
this.input?.end();
|
||||||
await this.awaitWithTimeout(q.return?.(), "query pump return on done");
|
await this.awaitWithTimeout(q.return?.(), "query pump return on done");
|
||||||
if (this.query === q) {
|
if (this.query === q) {
|
||||||
@@ -2834,6 +2889,18 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
run: route.run,
|
run: route.run,
|
||||||
message,
|
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) {
|
if (route.run) {
|
||||||
this.transitionTurnStateFromActiveRuns(`routed via ${route.reason}`);
|
this.transitionTurnStateFromActiveRuns(`routed via ${route.reason}`);
|
||||||
this.runTracker.bindIdentifiers(route.run, identifiers);
|
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
|
// Pre-replay success results are stale in practice (leftover from an
|
||||||
// earlier query segment) and must not end the current foreground run.
|
// earlier query segment) and must not end the current foreground run.
|
||||||
if (resultSubtype === "success") {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2917,6 +2994,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
message: SDKMessage,
|
message: SDKMessage,
|
||||||
identifiers: EventIdentifiers
|
identifiers: EventIdentifiers
|
||||||
): void {
|
): void {
|
||||||
|
const previousState = run.state;
|
||||||
if (
|
if (
|
||||||
message.type === "user" &&
|
message.type === "user" &&
|
||||||
identifiers.messageId &&
|
identifiers.messageId &&
|
||||||
@@ -2941,8 +3019,25 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
|
|
||||||
if (message.type === "result") {
|
if (message.type === "result") {
|
||||||
this.runTracker.transition(run, "finalizing");
|
this.runTracker.transition(run, "finalizing");
|
||||||
|
} else {
|
||||||
return;
|
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 {
|
private shouldSuppressLocalReplayActivity(message: SDKMessage): boolean {
|
||||||
|
|||||||
@@ -736,10 +736,15 @@ export class Session {
|
|||||||
private async interruptAgentIfRunning(agentId: string): Promise<void> {
|
private async interruptAgentIfRunning(agentId: string): Promise<void> {
|
||||||
const snapshot = this.agentManager.getAgent(agentId)
|
const snapshot = this.agentManager.getAgent(agentId)
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
|
this.sessionLogger.trace({ agentId }, 'interruptAgentIfRunning: agent not found')
|
||||||
throw new Error(`Agent ${agentId} not found`)
|
throw new Error(`Agent ${agentId} not found`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.lifecycle !== 'running' && !snapshot.pendingRun) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,9 +792,18 @@ export class Session {
|
|||||||
prompt: AgentPromptInput,
|
prompt: AgentPromptInput,
|
||||||
runOptions?: AgentRunOptions
|
runOptions?: AgentRunOptions
|
||||||
): { ok: true } | { ok: false; error: string } {
|
): { 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>
|
let iterator: AsyncGenerator<AgentStreamEvent>
|
||||||
try {
|
try {
|
||||||
iterator = this.agentManager.streamAgent(agentId, prompt, runOptions)
|
iterator = this.agentManager.streamAgent(agentId, prompt, runOptions)
|
||||||
|
this.sessionLogger.trace({ agentId }, 'startAgentStream: streamAgent returned iterator')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleAgentRunError(agentId, error, 'Failed to start agent run')
|
this.handleAgentRunError(agentId, error, 'Failed to start agent run')
|
||||||
const message =
|
const message =
|
||||||
@@ -802,7 +816,9 @@ export class Session {
|
|||||||
for await (const _ of iterator) {
|
for await (const _ of iterator) {
|
||||||
// Events are forwarded via the session's AgentManager subscription.
|
// Events are forwarded via the session's AgentManager subscription.
|
||||||
}
|
}
|
||||||
|
this.sessionLogger.trace({ agentId }, 'startAgentStream: iterator drained')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.sessionLogger.trace({ agentId, err: error }, 'startAgentStream: iterator threw')
|
||||||
this.handleAgentRunError(agentId, error, 'Agent stream failed')
|
this.handleAgentRunError(agentId, error, 'Agent stream failed')
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|||||||
+6
-3
@@ -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)
|
METRO_PORT=$(get-port 8081 8082 8083 8084 8085 8086 8087)
|
||||||
|
|
||||||
# Use a temporary PASEO_HOME to avoid conflicts between dev instances
|
# Use a temporary PASEO_HOME to avoid conflicts between dev instances
|
||||||
# export PASEO_HOME=$(mktemp -d "${TMPDIR:-/tmp}/paseo-dev.XXXXXX")
|
if [ -z "${PASEO_HOME}" ]; then
|
||||||
# trap "rm -rf '$PASEO_HOME'" EXIT
|
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
|
# Build CORS origins for this Expo instance
|
||||||
CORS_ORIGINS="http://localhost:${METRO_PORT},http://127.0.0.1:${METRO_PORT}"
|
CORS_ORIGINS="http://localhost:${METRO_PORT},http://127.0.0.1:${METRO_PORT}"
|
||||||
@@ -24,7 +27,7 @@ echo " Paseo Dev"
|
|||||||
echo "══════════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════════"
|
||||||
echo " Daemon: http://localhost:${DAEMON_PORT}"
|
echo " Daemon: http://localhost:${DAEMON_PORT}"
|
||||||
echo " Metro: http://localhost:${METRO_PORT}"
|
echo " Metro: http://localhost:${METRO_PORT}"
|
||||||
# echo " Home: ${PASEO_HOME}"
|
echo " Home: ${PASEO_HOME}"
|
||||||
echo "══════════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
|
||||||
# Export for child processes (overrides .env values)
|
# Export for child processes (overrides .env values)
|
||||||
|
|||||||
Reference in New Issue
Block a user