import type { AgentConfigInfo } from "../api/agents"; import type { PipelineStageItem, PipelineState, StatusEvent, } from "../api/client"; import { AgentPanel } from "./AgentPanel"; import { LozengeFlyProvider } from "./LozengeFlyContext"; import type { LogEntry } from "./ServerLogsPanel"; import { ServerLogsPanel } from "./ServerLogsPanel"; import { StagePanel } from "./StagePanel"; import { WorkItemDetailPanel } from "./WorkItemDetailPanel"; /** Format a structured StatusEvent into a human-readable display string. * This conversion happens at render time, not at the WebSocket boundary, * so the original StatusEvent structure is preserved in state. */ function formatStatusEventMessage(event: StatusEvent): string { const name = event.story_name || event.story_id; switch (event.type) { case "stage_transition": return `${name} — ${event.from_stage} → ${event.to_stage}`; case "merge_failure": return `✗ ${name} — ${event.reason}`; case "story_blocked": return `⊘ ${name} — BLOCKED: ${event.reason}`; case "rate_limit_warning": return `⚠ ${name} — ${event.agent_name} hit an API rate limit`; case "rate_limit_hard_block": return `⊗ ${name} — ${event.agent_name} hard rate-limited until ${event.reset_at}`; } } interface ChatPipelinePanelProps { isNarrowScreen: boolean; pipeline: PipelineState; pipelineVersion: number; agentConfigVersion: number; agentStateVersion: number; storyTokenCosts: Map; agentRoster: AgentConfigInfo[]; busyAgentNames: Set; selectedWorkItemId: string | null; serverLogs: LogEntry[]; /** Structured pipeline status events forwarded from the status broadcaster. */ statusEvents: Array<{ receivedAt: string; event: StatusEvent }>; onSelectWorkItem: (id: string) => void; onCloseWorkItem: () => void; onStartAgent: (storyId: string, agentName?: string) => void; onStopAgent: (storyId: string, agentName: string) => void; onDeleteItem: (item: PipelineStageItem) => void; } export function ChatPipelinePanel({ isNarrowScreen, pipeline, pipelineVersion, agentConfigVersion, agentStateVersion, storyTokenCosts, agentRoster, busyAgentNames, selectedWorkItemId, serverLogs, statusEvents, onSelectWorkItem, onCloseWorkItem, onStartAgent, onStopAgent, onDeleteItem, }: ChatPipelinePanelProps) { // Convert structured status events to LogEntry format for display in the // existing log area. Structure is preserved in the statusEvents array itself. const statusLogEntries: LogEntry[] = statusEvents.map( ({ receivedAt, event }) => ({ timestamp: receivedAt, level: event.type === "merge_failure" || event.type === "story_blocked" || event.type === "rate_limit_hard_block" ? "WARN" : "INFO", message: formatStatusEventMessage(event), }), ); const combinedLogs = [...statusLogEntries, ...serverLogs]; return (
{selectedWorkItemId ? ( ) : ( <> {(() => { const mergesInFlight = new Set( pipeline.deterministic_merges_in_flight ?? [], ); return ( <> onSelectWorkItem(item.story_id)} onStopAgent={onStopAgent} onDeleteItem={onDeleteItem} mergesInFlight={mergesInFlight} /> onSelectWorkItem(item.story_id)} onStopAgent={onStopAgent} onDeleteItem={onDeleteItem} mergesInFlight={mergesInFlight} isMergeStage /> onSelectWorkItem(item.story_id)} onStopAgent={onStopAgent} onDeleteItem={onDeleteItem} mergesInFlight={mergesInFlight} /> onSelectWorkItem(item.story_id)} agentRoster={agentRoster} busyAgentNames={busyAgentNames} onStartAgent={onStartAgent} onStopAgent={onStopAgent} onDeleteItem={onDeleteItem} mergesInFlight={mergesInFlight} /> onSelectWorkItem(item.story_id)} agentRoster={agentRoster} busyAgentNames={busyAgentNames} onStartAgent={onStartAgent} onStopAgent={onStopAgent} onDeleteItem={onDeleteItem} mergesInFlight={mergesInFlight} /> ); })()} )}
); }