diff --git a/web/src/main.tsx b/web/src/main.tsx index 38a2b0f..5085bdb 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -3009,7 +3009,7 @@ export function LiveSessionPanel({ const model = metadataString(activeSession.metadata, "model") || metadataString(activeSession.metadata, "brain") || metadataString(activeWorker?.metadata, "model") || metadataString(activeWorker?.metadata, "brain"); const provider = metadataString(activeSession.metadata, "provider") || metadataString(activeWorker?.metadata, "provider"); const branch = metadataString(activeSession.metadata, "branch") || metadataString(activeWorker?.metadata, "branch") || primaryPullRequest?.branch; - const latestOutput = activeSession.currentAction || tail?.completion?.summary || tail?.completion?.error || (latestEvent ? eventDisplayText(latestEvent) : ""); + const latestOutput = readableWorkerOutputText(activeSession.currentAction) || tail?.completion?.summary || tail?.completion?.error || (latestEvent ? eventDisplayText(latestEvent) : ""); const location = activeSession.workspaceCwd || activeSession.remoteWorkDir || activeSession.workspaceRoot || activeNode?.remoteWorkDir || ""; const scratch = activeSession.sharedWorkerDir || activeSession.sharedArtifactsDir || activeSession.sharedRoot || ""; const target = [activeSession.targetKind && humanizeKey(activeSession.targetKind), activeSession.targetId].filter(Boolean).join(" "); @@ -3103,15 +3103,16 @@ export function LiveSessionPanel({ )} {canCancel && ( -
+
- Session steering - {activeSession.id.slice(0, 8)} - {activeSession.workerId.slice(0, 8)} + Live session steering + session {activeSession.id.slice(0, 8)} + worker {activeSession.workerId.slice(0, 8)} + {activeSession.role && {humanizeKey(activeSession.role)}} {target && {target}}
setMessage(event.target.value)} placeholder="Steer this exact session..." required /> -
@@ -3273,7 +3274,13 @@ export function ManagerPullRequestSummary({ ))} )} -
+ +
+ PR follow-up steering + {selectedPR.repo}{selectedPR.number ? `#${selectedPR.number}` : ""} + {selectedPR.state} + {selectedPR.branch && head {selectedPR.branch}} +
setSteering(event.target.value)} placeholder="Steer this PR..." required /> )} - steerItem(event, item)}> + steerItem(event, item)}> +
+ Work item steering + {humanizeKey(item.kind)} + {item.id.slice(0, 8)} + {item.status} + {item.targetKind && item.targetId && {humanizeKey(item.targetKind)} {item.targetId.slice(0, 8)}} +
setSteering((current) => ({ ...current, [item.id]: event.target.value }))} placeholder="Steer this work item..." required />
@@ -4965,7 +4985,7 @@ function isWorkerProgressEvent(event: EventRecord): boolean { return false; } const payload = asRecord(event.payload); - const raw = asRecord(payload.raw ?? payload.rawResult); + const raw = workerRawPayload(payload); if (isClaudeRaw(raw)) { return isClaudeProgressRaw(raw); } @@ -4979,13 +4999,14 @@ function isWorkerProgressEvent(event: EventRecord): boolean { function workerEventLabel(event: EventRecord): string { if (event.type === "worker.output") { const payload = event.payload as { kind?: string; stream?: string; raw?: unknown; rawResult?: unknown }; - const raw = asRecord(payload.raw ?? payload.rawResult); + const raw = workerRawPayload(asRecord(payload)); const item = asRecord(raw.item); const claudeLabel = isClaudeRaw(raw) ? claudeWorkerEventLabel(raw, payload.kind) : ""; if (claudeLabel) return claudeLabel; if (item.type === "command_execution") return `command:${payload.kind ?? "log"}`; if (item.type === "agent_message") return `message:${payload.kind ?? "result"}`; if (item.type === "file_change") return `file:${String(item.status ?? payload.kind ?? "log")}`; + if (item.type === "mcp_tool_call") return `tool:${payloadValue(item.tool) || payloadValue(item.name) || payload.kind || "call"}`; if (raw.type === "turn.completed") return "usage"; if (raw.type === "thread.started") return "thread"; if (raw.type === "turn.started") return "turn"; @@ -4999,7 +5020,7 @@ function isClaudeThinkingEvent(event: EventRecord): boolean { return false; } const payload = asRecord(event.payload); - const raw = asRecord(payload.raw ?? payload.rawResult); + const raw = workerRawPayload(payload); return isClaudeRaw(raw) && payloadValue(raw.type) === "assistant" && payloadValue(claudeMessageContent(raw)[0]?.type) === "thinking"; } @@ -6011,7 +6032,7 @@ function structuredWorkerEvent(event: EventRecord): React.ReactNode { return null; } const payload = asRecord(event.payload); - const raw = asRecord(payload.raw ?? payload.rawResult); + const raw = workerRawPayload(payload); const item = asRecord(raw.item); if (event.type === "worker.created") { @@ -6045,6 +6066,9 @@ function structuredWorkerEvent(event: EventRecord): React.ReactNode { if (item.type === "file_change") { return ; } + if (item.type === "mcp_tool_call") { + return ; + } if (raw.type === "turn.completed") { return ; } @@ -6382,6 +6406,73 @@ function FileChangeCard({ payload, item, raw }: { payload: Record; raw: Record }) { + const server = payloadValue(item.server); + const tool = payloadValue(item.tool || item.name) || "tool"; + const status = payloadValue(item.status) || payloadValue(raw.type).replace(/^item\./, "") || "recorded"; + const result = asRecord(item.result); + const args = item.arguments ?? item.input; + const summary = mcpToolResultSummary(result); + return ( +
+
+ {server ? `${server}.${tool}` : tool} + {status} + tool call +
+

{summary || "Tool call recorded."}

+ + +
+ ); +} + +function mcpToolCallSummary(item: Record): string { + const server = payloadValue(item.server); + const tool = payloadValue(item.tool || item.name) || "tool"; + const status = payloadValue(item.status); + const summary = mcpToolResultSummary(asRecord(item.result)); + return [server ? `${server}.${tool}` : tool, status, summary].filter(Boolean).join(" | "); +} + +function mcpToolResultSummary(result: Record): string { + const error = payloadValue(result.error); + if (error) { + return error.length > 160 ? `${error.slice(0, 157)}...` : error; + } + const structured = asRecord(result.structuredContent ?? result.structured_content); + const structuredSummary = collectionResultSummary(structured); + if (structuredSummary) { + return structuredSummary; + } + const content = Array.isArray(result.content) ? result.content.map(asRecord) : []; + for (const part of content) { + const text = payloadValue(part.text); + if (!text) continue; + const parsed = asRecord(parseJSONPayload(text)); + const parsedSummary = collectionResultSummary(parsed); + if (parsedSummary) { + return parsedSummary; + } + return summarizeText(text); + } + return ""; +} + +function collectionResultSummary(value: Record): string { + for (const [key, item] of Object.entries(value)) { + if (Array.isArray(item)) { + const label = item.length === 1 ? humanizeKey(key).replace(/s$/, "") : humanizeKey(key).toLowerCase(); + return `${item.length} ${label}`; + } + } + const structuredContent = asRecord(value.structured_content ?? value.structuredContent); + if (Object.keys(structuredContent).length > 0) { + return collectionResultSummary(structuredContent); + } + return ""; +} + function UsageCard({ raw }: { raw: Record }) { const usage = asRecord(raw.usage); return ( @@ -6495,6 +6586,16 @@ function RawPayloadDetails({ value }: { value: unknown }) { ); } +function PayloadDetails({ label, value }: { label: string; value: unknown }) { + if (value === undefined || value === null || prettyPayload(value) === "{}") return null; + return ( +
+ {label} +
{prettyPayload(value)}
+
+ ); +} + function parseJSONPayload(value: string): unknown { const trimmed = value.trim(); if (!trimmed || (trimmed[0] !== "{" && trimmed[0] !== "[")) { @@ -6507,6 +6608,25 @@ function parseJSONPayload(value: string): unknown { } } +function workerRawPayload(payload: Record): Record { + const explicit = asRecord(payload.raw ?? payload.rawResult); + if (Object.keys(explicit).length > 0) { + return explicit; + } + return asRecord(parseJSONPayload(payloadValue(payload.text))); +} + +function readableWorkerOutputText(value: string | undefined): string { + const text = payloadValue(value); + if (!text) { + return ""; + } + if (parseJSONPayload(text) === undefined) { + return text; + } + return compactEventDisplay({ id: 0, at: "", type: "worker.output", payload: { text } }) || "Structured worker output"; +} + function summarizeText(value: string): string { const trimmed = value.trim(); if (!trimmed) return "empty output"; @@ -6571,6 +6691,9 @@ function eventDisplayText(event: EventRecord): string { .map((file) => file.path) .join(", ")}${changedFiles.length > 4 ? "..." : ""}` : undefined; + if (event.type === "worker.output" && parseJSONPayload(payloadValue(payload.text)) !== undefined) { + return changeText ? `Structured worker output | ${changeText}` : "Structured worker output"; + } const workspaceText = payload.cwd || payload.root ? `${payload.mode ?? "workspace"} ${payload.cwd ?? payload.root}` : undefined; const primaryText = payload.text ?? @@ -6658,7 +6781,7 @@ function compactEventDisplay(event: EventRecord): string { return payloadValue(payload.result) || payloadValue(payload.cleanupPolicy || payload.policy) || "Workspace cleanup recorded"; } if (event.type === "worker.output") { - const raw = asRecord(payload.raw ?? payload.rawResult); + const raw = workerRawPayload(payload); const claudeText = isClaudeRaw(raw) ? claudeEventDisplayText(payload, raw) : ""; if (claudeText) return claudeText; const item = asRecord(raw.item); @@ -6671,6 +6794,9 @@ function compactEventDisplay(event: EventRecord): string { if (item.type === "file_change") { return payloadValue(item.path || item.file || payload.text) || "File changed"; } + if (item.type === "mcp_tool_call") { + return mcpToolCallSummary(item); + } if (raw.type === "thread.started") return "Thread started"; if (raw.type === "turn.started") return "Turn started"; if (raw.type === "turn.completed") return "Turn completed"; diff --git a/web/src/test/LiveSessionPanel.test.tsx b/web/src/test/LiveSessionPanel.test.tsx index 99d7720..b15fcb1 100644 --- a/web/src/test/LiveSessionPanel.test.tsx +++ b/web/src/test/LiveSessionPanel.test.tsx @@ -125,6 +125,53 @@ describe("LiveSessionPanel", () => { expect(screen.getByText("modified web/src/main.tsx")).toBeInTheDocument(); }); + it("summarizes codex json tool output instead of dumping raw protocol text", async () => { + vi.mocked(api.getSessionTail).mockResolvedValue({ + sessionId: "session-1", + workerId: "worker-1", + taskId: "task-1", + status: "running", + lastEventId: 22, + events: [], + session: baseSession, + worker: baseWorker, + node: baseNode, + pullRequests: [], + changedFiles: [], + }); + const rawToolOutput = JSON.stringify({ + type: "item.completed", + item: { + id: "item_52", + type: "mcp_tool_call", + server: "codex_apps", + tool: "github_search_prs", + status: "completed", + arguments: { query: "Manager OR objective" }, + result: { + structured_content: { + issues: [{ number: 427 }, { number: 275 }], + }, + }, + }, + }); + + renderPanel([ + { + id: 22, + at: "2026-06-10T00:00:02Z", + type: "worker.output", + taskId: "task-1", + workerId: "worker-1", + payload: { kind: "log", stream: "stdout", text: rawToolOutput }, + }, + ]); + + expect(screen.getAllByText("codex_apps.github_search_prs | completed | 2 issues")).not.toHaveLength(0); + expect(screen.queryByText(rawToolOutput)).not.toBeInTheDocument(); + await waitFor(() => expect(api.getSessionTail).toHaveBeenCalledWith("session-1", { after: 22, limit: 50 })); + }); + it("polls incrementally after receiving the initial latest tail", async () => { vi.useFakeTimers(); try {