import { useEffect, useMemo, useRef, useState } from 'react' import { Bot, Check, ChevronRight, Circle, FileText, LoaderCircle, Terminal, Users, X } from 'lucide-react' import { getVisibleActivitySections, type ActivityRow, type ActivitySectionId, type SessionActivityModel } from './sessionActivityModel' import { useTranslation } from '../../i18n' import type { BackgroundAgentTask } from '../../types/chat' import type { TeamMember } from '../../types/team' import { formatTokenCount } from '../../lib/formatTokenCount' export type OpenSubagentPayload = { sessionId: string toolUseId: string title: string } type SessionActivityPanelPlacement = 'overlay' | 'rail' type TranslationFn = ReturnType function fallbackStatusLabel(status: ActivityRow['status']): string { const label = String(status).replace(/[_-]/g, ' ').replace(/\s+/g, ' ').trim() if (!label) return '' return `${label.charAt(0).toUpperCase()}${label.slice(1)}` } function getActivityStatusLabel(status: ActivityRow['status'], t: TranslationFn): string { switch (status) { case 'pending': return t('session.activity.status.pending') case 'in_progress': return t('session.activity.status.inProgress') case 'completed': return t('session.activity.status.completed') case 'running': return t('session.activity.status.running') case 'failed': return t('session.activity.status.failed') case 'stopped': return t('session.activity.status.stopped') case 'idle': return t('session.activity.status.idle') case 'error': return t('session.activity.status.error') default: return fallbackStatusLabel(status) } } function getSectionTitle(sectionId: ActivitySectionId, t: TranslationFn): string { switch (sectionId) { case 'tasks': return t('session.activity.section.tasks') case 'team': return t('session.activity.section.team') case 'backgroundTasks': return t('session.activity.section.backgroundTasks') case 'subagents': return t('session.activity.section.subagents') case 'sources': return t('session.activity.section.sources') case 'output': return t('subagentRun.output') } } function getSectionRowsClassName(sectionId: ActivitySectionId, rowCount: number): string { const base = 'space-y-1' if (rowCount === 0) return base switch (sectionId) { case 'tasks': return `${base} max-h-44 overflow-y-auto overscroll-contain pr-1` case 'team': return `${base} max-h-36 overflow-y-auto overscroll-contain pr-1` case 'backgroundTasks': return `${base} max-h-40 overflow-y-auto overscroll-contain pr-1` case 'subagents': return `${base} max-h-40 overflow-y-auto overscroll-contain pr-1` case 'sources': return `${base} max-h-28 overflow-y-auto overscroll-contain pr-1` case 'output': return `${base} max-h-28 overflow-y-auto overscroll-contain pr-1` } } function getTaskTypeLabel(taskType: BackgroundAgentTask['taskType'] | undefined, t: TranslationFn): string { if (taskType?.includes('agent')) return t('chat.backgroundTasks.type.agent') if (taskType === 'local_bash') return t('chat.backgroundTasks.type.bash') if (taskType === 'local_workflow') return t('chat.backgroundTasks.type.workflow') return t('chat.backgroundTasks.type.task') } function formatBackgroundDuration(ms: number | undefined, t: TranslationFn): string | undefined { if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return undefined const totalSeconds = Math.max(1, Math.round(ms / 1000)) if (totalSeconds < 60) return t('chat.duration.seconds', { seconds: totalSeconds }) const minutes = Math.floor(totalSeconds / 60) const seconds = totalSeconds % 60 return t('chat.duration.minutesSeconds', { minutes, seconds }) } function hasBackgroundTaskDetails(row: ActivityRow): boolean { return Boolean( row.description || row.summary || row.outputFile || row.taskType || row.workflowName || row.usage?.totalTokens || row.usage?.durationMs, ) } function isActivityTriggerTarget(target: EventTarget | null): boolean { return target instanceof Element && target.closest('[data-session-activity-trigger="true"]') !== null } function isBackgroundTaskStatus(status: ActivityRow['status']): status is BackgroundAgentTask['status'] { return status === 'running' || status === 'completed' || status === 'failed' || status === 'stopped' } function getFinishedBackgroundTaskKeys(model: SessionActivityModel): string[] { const keys = new Set() for (const sectionId of ['backgroundTasks', 'subagents'] as const) { for (const row of model.sections[sectionId].rows) { if (row.dismissKey && isBackgroundTaskStatus(row.status) && row.status !== 'running') { keys.add(row.dismissKey) } } } return Array.from(keys) } function TaskStatusMarker({ status, t }: { status: ActivityRow['status']; t: TranslationFn }) { if (status === 'completed') { return ( ) } if (status === 'in_progress' || status === 'running') { return ( ) } return ( ) } function getRowIcon(row: ActivityRow) { switch (row.section) { case 'team': return Users case 'backgroundTasks': return Terminal case 'subagents': return Bot case 'sources': case 'output': return FileText case 'tasks': return Circle } } function getStatusTone(status: ActivityRow['status']) { if (status === 'running' || status === 'in_progress') { return 'bg-[var(--color-accent)]' } if (status === 'completed' || status === 'idle') { return 'bg-[var(--color-success)]' } if (status === 'failed' || status === 'error' || status === 'stopped') { return 'bg-[var(--color-error)]' } return 'bg-[var(--color-text-tertiary)]' } function ActivityRowView({ row, sessionId, onOpenSubagent, onOpenMember, onOpenBackgroundTask, selected, }: { row: ActivityRow sessionId: string onOpenSubagent: (payload: OpenSubagentPayload) => void onOpenMember?: (member: TeamMember) => void onOpenBackgroundTask?: (row: ActivityRow) => void selected?: boolean }) { const t = useTranslation() const isTask = row.section === 'tasks' const label = row.taskHistory ? t('session.activity.tasks.earlier') : row.label const detail = row.taskHistory ? t('session.activity.tasks.earlierSummary', { completed: row.taskHistory.completed, total: row.taskHistory.total, turns: row.taskHistory.turnCount, }) : isTask && row.description && row.description !== row.label ? row.description : isTask && row.summary && row.summary !== row.label ? row.summary : undefined const content = ( <> {isTask ? ( ) : ( {(() => { const Icon = getRowIcon(row) return )} {label} {detail ? ( {detail} ) : null} {isTask ? null : ( )} {!isTask && row.openable ? (