import { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { api } from '../../api'; import type { BackgroundTaskEntry, BackgroundTaskInfo, BackgroundTaskStatus } from '../../types'; import { formatAbsoluteTime, formatRelativeTime } from '../../util/time'; import { useTasks } from '../../hooks/useTasks'; import { CopyButton } from '../shared/CopyButton'; import { JsonViewer } from '../shared/JsonViewer'; import { formatBytes } from '../shared/SizePreview'; import { Pill, type PillTone } from '../shared/Pill'; interface TasksTabProps { sessionId: string; } const STATUS_TONE: Record = { running: 'info', completed: 'success', failed: 'error', timed_out: 'warning', killed: 'warning', lost: 'neutral', }; function kindTone(kind: BackgroundTaskInfo['kind']): PillTone { if (kind === 'agent') return 'subagent'; if (kind === 'question') return 'approval'; return 'tools'; } /** Tasks tab — background tasks (bash processes, subagents, pending * questions) persisted under the session's `tasks/` directory, plus their * `output.log`. None of this is reconstructable from the wire, so it is the * only place to inspect what a session spawned in the background. */ export function TasksTab({ sessionId }: TasksTabProps) { const { data, isLoading, error } = useTasks(sessionId); if (isLoading) { return
loading tasks…
; } if (error) { return (
{error.message}
); } const tasks = data?.tasks ?? []; return (
background tasks{tasks.length > 0 ? ` · ${tasks.length}` : ''}
{tasks.length === 0 ? (
no background tasks were persisted for this session
) : (
{tasks.map((entry) => ( ))}
)}
); } function TaskCard({ sessionId, entry }: { sessionId: string; entry: BackgroundTaskEntry }) { const { task } = entry; const [showLog, setShowLog] = useState(false); const [showRaw, setShowRaw] = useState(false); const duration = task.endedAt !== null && task.endedAt !== undefined ? task.endedAt - task.startedAt : null; return (
{/* Header line */}
{task.kind} {task.status} {task.taskId} {entry.agentId !== 'main' ? ( {entry.agentId} ) : null} {task.detached === false ? ( foreground ) : null} started {formatRelativeTime(task.startedAt)}
{/* Body fields */}
{task.description || (none)} {task.kind === 'process' ? ( <> {task.command} {task.pid} {task.exitCode ?? (running)} ) : null} {task.kind === 'agent' ? ( <> {task.agentId ? ( {task.agentId} → ) : ( (none) )} {task.subagentType ?? (none)} {task.stopCode !== undefined ? ( {task.stopCode} ) : null} ) : null} {task.kind === 'question' ? ( <> {task.questionCount} {task.toolCallId ?? (none)} ) : null} {duration === null ? (unfinished) : `${duration} ms`} {task.timeoutMs !== undefined ? ( {task.timeoutMs} ) : null} {task.stopReason ? {task.stopReason} : null} {task.endedAt === null || task.endedAt === undefined ? ( (running) ) : ( {formatRelativeTime(task.endedAt)} )}
{/* Toggles */}
{showLog && entry.outputExists ? ( ) : null} {showRaw ? (
) : null}
); } function TaskOutput({ sessionId, taskId }: { sessionId: string; taskId: string }) { // Progressive byte-window paging: fetch the first window on mount, then // append subsequent windows on demand via the server-provided exact // `nextOffset` cursor. Keeps arbitrarily large logs readable in full. const [content, setContent] = useState(''); const [cursor, setCursor] = useState(0); const [size, setSize] = useState(0); const [eof, setEof] = useState(false); const [loading, setLoading] = useState(false); const [err, setErr] = useState(null); const [started, setStarted] = useState(false); const loadFrom = useCallback( async (offset: number) => { setLoading(true); setErr(null); try { const w = await api.getTaskOutput(sessionId, taskId, offset); setContent((prev) => (offset === 0 ? w.content : prev + w.content)); setCursor(w.nextOffset); setSize(w.size); setEof(w.eof); } catch (error) { setErr(error instanceof Error ? error.message : String(error)); } finally { setLoading(false); } }, [sessionId, taskId], ); useEffect(() => { if (started) return; setStarted(true); void loadFrom(0); }, [started, loadFrom]); return (
output.log {formatBytes(Math.min(cursor, size))} / {formatBytes(size)} {!eof && cursor > 0 ? ( · more below ) : null}
{err !== null ? (
{err}
) : null}
        {content || (loading ? 'loading log…' : '(empty)')}
      
{!eof && cursor > 0 ? ( ) : null}
); } function Field({ label, children }: { label: string; children: import('react').ReactNode }) { return (
{label} {children}
); } function Dim({ children }: { children: import('react').ReactNode }) { return {children}; }