import { useEffect, useState } from 'react'
import { useTaskStore } from '../../stores/taskStore'
import { useChatStore } from '../../stores/chatStore'
import { useTabStore } from '../../stores/tabStore'
import { useTranslation } from '../../i18n'
import { parseRunOutput } from '../../lib/parseRunOutput'
import type { TaskRun } from '../../types/task'
import { MarkdownRenderer } from '../markdown/MarkdownRenderer'
function RunOutput({ run }: { run: TaskRun }) {
const t = useTranslation()
// Show error prominently if present
if (run.error) {
return (
{run.error}
)
}
const text = parseRunOutput(run.output || '')
if (!text) {
return (
{run.sessionId ? t('tasks.outputHintSession') : t('tasks.noOutputText')}
)
}
return (
)
}
type Props = {
taskId: string
onClose: () => void
refreshKey?: number
}
const STATUS_CONFIG: Record = {
running: { icon: 'sync', color: 'var(--color-warning)' },
completed: { icon: 'check_circle', color: 'var(--color-success)' },
failed: { icon: 'error', color: 'var(--color-error)' },
timeout: { icon: 'timer_off', color: 'var(--color-error)' },
}
export function TaskRunsPanel({ taskId, onClose, refreshKey }: Props) {
const t = useTranslation()
const { fetchTaskRuns } = useTaskStore()
const connectToSession = useChatStore((s) => s.connectToSession)
const openTab = useTabStore((s) => s.openTab)
const [runs, setRuns] = useState([])
const [loading, setLoading] = useState(true)
const [expandedId, setExpandedId] = useState(null)
const openSession = (sessionId: string, taskName?: string) => {
openTab(sessionId, taskName || 'Task Run')
connectToSession(sessionId)
}
const refresh = () => {
fetchTaskRuns(taskId).then((r) => {
setRuns(r)
setLoading(false)
}).catch(() => setLoading(false))
}
// Initial fetch + re-fetch when refreshKey changes
useEffect(() => {
setLoading(true)
refresh()
}, [taskId, fetchTaskRuns, refreshKey])
// Auto-poll while any run is "running" or shortly after a manual trigger.
// Uses faster 1s polling for the first 10s after refreshKey changes, then 3s.
const hasRunning = runs.some((r) => r.status === 'running')
useEffect(() => {
if (!hasRunning && refreshKey === 0) return // no reason to poll initially
// Start with fast polling (1s) to give snappy feedback after "Run Now"
let interval = 1000
let timer = setInterval(refresh, interval)
// After 10s, switch to slower 3s polling if still running
const slowDown = setTimeout(() => {
clearInterval(timer)
if (hasRunning) {
timer = setInterval(refresh, 3000)
}
}, 10000)
// If nothing is running and initial window passes, stop entirely
const stopTimer = hasRunning ? undefined : setTimeout(() => clearInterval(timer), 12000)
return () => {
clearInterval(timer)
clearTimeout(slowDown)
if (stopTimer) clearTimeout(stopTimer)
}
}, [hasRunning, taskId, refreshKey])
return (
{/* Header */}
{t('tasks.logsTitle')}
close
{/* Content */}
{loading ? (
) : runs.length === 0 ? (
{t('tasks.noLogs')}
) : (
{runs.map((run) => {
const cfg = STATUS_CONFIG[run.status] || STATUS_CONFIG.failed!
const isExpanded = expandedId === run.id
return (
{/* Status icon */}
{cfg.icon}
{/* Status text */}
{t(`tasks.runStatus.${run.status}` as any)} {/* dynamic key */}
{/* Time */}
{new Date(run.startedAt).toLocaleString()}
{/* Duration */}
{run.durationMs != null && (
{t('tasks.duration', { s: Math.round(run.durationMs / 1000) })}
)}
{/* Open session — only after run completes (session is empty while running) */}
{run.sessionId && run.status !== 'running' && (
openSession(run.sessionId!, run.taskName)}
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-[var(--color-brand)] bg-[var(--color-brand)]/8 hover:bg-[var(--color-brand)]/15 rounded-[var(--radius-sm)] transition-colors"
>
open_in_new
{t('tasks.openSession')}
)}
{/* Summary toggle */}
{(run.output || run.error) && (
setExpandedId(isExpanded ? null : run.id)}
className="text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
>
{isExpanded ? t('tasks.hideOutput') : t('tasks.viewOutput')}
)}
{/* Expanded output */}
{isExpanded && (
)}
)
})}
)}
)
}