File size: 4,399 Bytes
064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | import { logEvent } from '../services/analytics/index.js'
import { isTerminalTaskStatus } from '../Task.js'
import type { LocalAgentTaskState } from '../tasks/LocalAgentTask/LocalAgentTask.js'
// Inlined from framework.ts — importing creates a cycle through
// BackgroundTasksDialog. Keep in sync with PANEL_GRACE_MS there.
const PANEL_GRACE_MS = 30_000
import type { AppState } from './AppState.js'
// Inline type check instead of importing isLocalAgentTask — breaks the
// teammateViewHelpers → LocalAgentTask runtime edge that creates a cycle
// through BackgroundTasksDialog.
function isLocalAgent(task: unknown): task is LocalAgentTaskState {
return (
typeof task === 'object' &&
task !== null &&
'type' in task &&
task.type === 'local_agent'
)
}
/**
* Return the task released back to stub form: retain dropped, messages
* cleared, evictAfter set if terminal. Shared by exitTeammateView and
* the switch-away path in enterTeammateView.
*/
function release(task: LocalAgentTaskState): LocalAgentTaskState {
return {
...task,
retain: false,
messages: undefined,
diskLoaded: false,
evictAfter: isTerminalTaskStatus(task.status)
? Date.now() + PANEL_GRACE_MS
: undefined,
}
}
/**
* Transitions the UI to view a teammate's transcript.
* Sets viewingAgentTaskId and, for local_agent, retain: true (blocks eviction,
* enables stream-append, triggers disk bootstrap) and clears evictAfter.
* If switching from another agent, releases the previous one back to stub.
*/
export function enterTeammateView(
taskId: string,
setAppState: (updater: (prev: AppState) => AppState) => void,
): void {
logEvent('tengu_transcript_view_enter', {})
setAppState(prev => {
const task = prev.tasks[taskId]
const prevId = prev.viewingAgentTaskId
const prevTask = prevId !== undefined ? prev.tasks[prevId] : undefined
const switching =
prevId !== undefined &&
prevId !== taskId &&
isLocalAgent(prevTask) &&
prevTask.retain
const needsRetain =
isLocalAgent(task) && (!task.retain || task.evictAfter !== undefined)
const needsView =
prev.viewingAgentTaskId !== taskId ||
prev.viewSelectionMode !== 'viewing-agent'
if (!needsRetain && !needsView && !switching) return prev
let tasks = prev.tasks
if (switching || needsRetain) {
tasks = { ...prev.tasks }
if (switching) tasks[prevId] = release(prevTask)
if (needsRetain) {
tasks[taskId] = { ...task, retain: true, evictAfter: undefined }
}
}
return {
...prev,
viewingAgentTaskId: taskId,
viewSelectionMode: 'viewing-agent',
tasks,
}
})
}
/**
* Exit teammate transcript view and return to leader's view.
* Drops retain and clears messages back to stub form; if terminal,
* schedules eviction via evictAfter so the row lingers briefly.
*/
export function exitTeammateView(
setAppState: (updater: (prev: AppState) => AppState) => void,
): void {
logEvent('tengu_transcript_view_exit', {})
setAppState(prev => {
const id = prev.viewingAgentTaskId
const cleared = {
...prev,
viewingAgentTaskId: undefined,
viewSelectionMode: 'none' as const,
}
if (id === undefined) {
return prev.viewSelectionMode === 'none' ? prev : cleared
}
const task = prev.tasks[id]
if (!isLocalAgent(task) || !task.retain) return cleared
return {
...cleared,
tasks: { ...prev.tasks, [id]: release(task) },
}
})
}
/**
* Context-sensitive x: running → abort, terminal → dismiss.
* Dismiss sets evictAfter=0 so the filter hides immediately.
* If viewing the dismissed agent, also exits to leader.
*/
export function stopOrDismissAgent(
taskId: string,
setAppState: (updater: (prev: AppState) => AppState) => void,
): void {
setAppState(prev => {
const task = prev.tasks[taskId]
if (!isLocalAgent(task)) return prev
if (task.status === 'running') {
task.abortController?.abort()
return prev
}
if (task.evictAfter === 0) return prev
const viewingThis = prev.viewingAgentTaskId === taskId
return {
...prev,
tasks: {
...prev.tasks,
[taskId]: { ...release(task), evictAfter: 0 },
},
...(viewingThis && {
viewingAgentTaskId: undefined,
viewSelectionMode: 'none',
}),
}
})
}
|