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', }), } }) }