142 lines
4.3 KiB
TypeScript
142 lines
4.3 KiB
TypeScript
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',
|
|
}),
|
|
}
|
|
})
|
|
}
|