import React, { useCallback, useEffect, useRef, useState } from 'react' import type { Session } from '../types/kanban' import type { AgentInfo } from '../types/visual' import { IconStop, IconBoard, IconTool, IconCheck } from './SvgIcons' import { getSessionRuntimeStatus, isSessionWorking } from '../lib/sessionRuntime' import { getWorkItemRoleLabel } from '../lib/workItemIdentity' interface TaskHeaderBarProps { session: Session agents: AgentInfo[] onTitleChange: (taskId: string, title: string) => void onViewOnBoard?: () => void onStop?: () => void onComplete?: () => void onResume?: () => void } function relativeTime(ts: number): string { const diff = Date.now() - ts if (diff < 60_000) return 'just now' if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago` if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago` return `${Math.floor(diff / 86_400_000)}d ago` } const STATUS_META: Record = { running: { color: 'var(--green)', label: 'Running' }, idle: { color: 'var(--accent)', label: 'Idle' }, done: { color: 'var(--green)', label: 'Done' }, pending: { color: 'var(--text-secondary)', label: 'Pending' }, failed: { color: 'var(--red)', label: 'Failed' }, cancelled: { color: 'var(--text-secondary)', label: 'Cancelled' }, blocked: { color: 'var(--yellow)', label: 'Blocked' }, awaiting_human: { color: 'var(--yellow)', label: 'Waiting for review' }, awaiting_manager_review: { color: 'var(--yellow)', label: 'Manager review' }, awaiting_review: { color: 'var(--yellow)', label: 'Waiting for review' }, awaiting_peer: { color: 'var(--yellow)', label: 'Waiting for peer' }, } const HUMAN_REVIEW_STATUSES = new Set([ 'awaiting_human', 'awaiting_manager_review', 'awaiting_review', 'awaiting_peer', ]) const EXECUTION_AGENT_LABELS: Record = { native: 'Native', codex: 'Codex', claude_code: 'Claude Code', cursor: 'Cursor', opencode: 'OpenCode', } export function TaskHeaderBar({ session, agents, onTitleChange, onViewOnBoard, onStop, onComplete, onResume }: TaskHeaderBarProps) { const [editing, setEditing] = useState(false) const [draft, setDraft] = useState(session.title) const inputRef = useRef(null) useEffect(() => { if (!editing) setDraft(session.title) }, [session.title, editing]) const commitTitle = useCallback(() => { const trimmed = draft.trim() if (trimmed && trimmed !== session.title) { onTitleChange(session.taskId, trimmed) } else { setDraft(session.title) } setEditing(false) }, [draft, session.title, session.taskId, onTitleChange]) const startEditing = useCallback(() => { setDraft(session.title) setEditing(true) setTimeout(() => inputRef.current?.select(), 0) }, [session.title]) const assignees = session.assigneeIds .map(id => agents.find(a => a.agent_id === id)) .filter(Boolean) as AgentInfo[] const execMode = String(session.execMode ?? '').trim().toLowerCase() const isCompanyHeaderSession = execMode === 'company' || execMode === 'org' || execMode === 'custom' || !!session.isCompanyRuntime || !!session.workItemProjectionId || !!session.workItemRoleId || !!session.workItemRoleName const showAssigneeAvatars = assignees.length > 0 && !isCompanyHeaderSession const runtimeControlState = session.runtimeControlState ?? (session.status === 'running' ? 'running' : 'idle') const isSuspending = runtimeControlState === 'suspending' const isResuming = runtimeControlState === 'resuming' const isSuspended = runtimeControlState === 'suspended' const isRunning = session.status === 'running' && !isSuspending && !isSuspended && !isResuming const isAwaitingReview = HUMAN_REVIEW_STATUSES.has(session.status) const canStop = (session.canStop ?? session.status === 'running') && !isSuspending && !isSuspended && !isResuming const canResume = ( session.canResume ?? (isSuspended || (!isAwaitingReview && !isRunning && session.status !== 'done' && session.status !== 'pending')) ) && !isSuspending && !isResuming const meta = STATUS_META[session.status] ?? STATUS_META.pending const statusLabel = isSuspending ? 'Stopping' : isSuspended ? 'Suspended' : isResuming ? 'Resuming' : meta.label const roleLabel = getWorkItemRoleLabel(session) const runtimeStatus = getSessionRuntimeStatus(session) const isWorking = isSessionWorking(session) const liveTool = session.displayTool || session.currentTool // Sticky tool label tied to the RUN lifecycle (not the transient agentStatus). // The native runtime reports an 'idle'/'reflecting' state with no current_tool // between consecutive tool calls; reacting to that blanks the pill for a frame // and makes the command flicker once per call. Instead, keep showing the last // non-empty command for as long as the session is running, and drop it only // when the run stops — so the pill holds steady and just swaps to the next tool. const [stickyTool, setStickyTool] = useState(liveTool || undefined) useEffect(() => { if (!isRunning) { setStickyTool(undefined); return } if (liveTool) setStickyTool(liveTool) }, [isRunning, liveTool]) const hasApprovalMetrics = typeof session.pendingPermissionCount === 'number' && session.pendingPermissionCount > 0 const showRuntimeMetrics = hasApprovalMetrics const statusDotColor = isSuspending ? 'var(--yellow)' : isSuspended ? 'var(--text-secondary)' : runtimeStatus === 'tool_active' ? 'var(--green)' : runtimeStatus === 'reflecting' ? 'var(--yellow)' : meta.color return (
{editing ? ( setDraft(e.target.value)} onBlur={commitTitle} onKeyDown={e => { if (e.key === 'Enter') commitTitle() if (e.key === 'Escape') { setDraft(session.title); setEditing(false) } }} /> ) : ( {session.title} )} {statusLabel} {isRunning && stickyTool && ( {stickyTool} {session.currentTool && typeof session.toolElapsedMs === 'number' && session.toolElapsedMs > 0 && ( {session.toolElapsedMs}ms )} )} {session.lastToolSummary && ( {session.lastToolSummary.slice(0, 48)} )} {/* Projection id (e.g. "attention::seat::team::cto::cto::review::f42cf81f") is an internal debug identifier — useful in the Info tab but adds noise to the header bar. Surface it via title-tooltip on the role pill rather than as a wide chip. */} {roleLabel && ( {roleLabel} )} {session.employeeAssignment?.name && ( 👤 {session.employeeAssignment.name} )} {session.selectedExecutionAgent && ( {EXECUTION_AGENT_LABELS[session.selectedExecutionAgent] ?? session.selectedExecutionAgent} )}
{showAssigneeAvatars && (
{assignees.slice(0, 3).map(a => ( {a.name.charAt(0).toUpperCase()} ))}
)} {relativeTime(session.createdAt)} {(canStop || isSuspending) && onStop && ( )} {canResume && onResume && session.status !== 'done' && ( )} {onComplete && ( )} {onViewOnBoard && ( )}
{showRuntimeMetrics && (
{hasApprovalMetrics && (
Approvals {session.pendingPermissionCount}
)}
)}
) }