huskies: merge 643_story_web_ui_consumer_for_the_unified_status_broadcaster
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import type { AgentConfigInfo } from "../api/agents";
|
||||
import type { PipelineStageItem, PipelineState } from "../api/client";
|
||||
import type {
|
||||
PipelineStageItem,
|
||||
PipelineState,
|
||||
StatusEvent,
|
||||
} from "../api/client";
|
||||
import { AgentPanel } from "./AgentPanel";
|
||||
import { LozengeFlyProvider } from "./LozengeFlyContext";
|
||||
import type { LogEntry } from "./ServerLogsPanel";
|
||||
@@ -7,6 +11,25 @@ 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;
|
||||
@@ -18,6 +41,8 @@ interface ChatPipelinePanelProps {
|
||||
busyAgentNames: Set<string>;
|
||||
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;
|
||||
@@ -36,12 +61,28 @@ export function ChatPipelinePanel({
|
||||
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 (
|
||||
<div
|
||||
data-testid="chat-right-column"
|
||||
@@ -115,7 +156,7 @@ export function ChatPipelinePanel({
|
||||
onStopAgent={onStopAgent}
|
||||
onDeleteItem={onDeleteItem}
|
||||
/>
|
||||
<ServerLogsPanel logs={serverLogs} />
|
||||
<ServerLogsPanel logs={combinedLogs} />
|
||||
</>
|
||||
)}
|
||||
</LozengeFlyProvider>
|
||||
|
||||
Reference in New Issue
Block a user