diff --git a/web/packages/superagent-ui/README.md b/web/packages/superagent-ui/README.md index bed790f..3a7f3f2 100644 --- a/web/packages/superagent-ui/README.md +++ b/web/packages/superagent-ui/README.md @@ -79,6 +79,10 @@ negative legacy timing is omitted. Running durations update once per second but are excluded from live-region announcements. Stream timestamps remain ordering metadata and are never rendered as stream duration. +Collapsed Work logs show separate succeeded and failed operation counts. Active +and unknown counts appear when those states are present, so one failed operation +does not make the entire log look unsuccessful. + Set `isExecutionLive={false}` for terminal history. Unpaired legacy calls then show `unknown` and do not start the elapsed-time interval. Collapsed Work logs do not mount tool result bodies or call `renderToolCall` until expanded. diff --git a/web/packages/superagent-ui/src/ConversationView.test.tsx b/web/packages/superagent-ui/src/ConversationView.test.tsx index 05fa645..c4b347d 100644 --- a/web/packages/superagent-ui/src/ConversationView.test.tsx +++ b/web/packages/superagent-ui/src/ConversationView.test.tsx @@ -7,7 +7,10 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { ConversationView } from "./ConversationView.js"; +import { + ConversationView, + type ConversationViewProps, +} from "./ConversationView.js"; const base = Date.parse("2026-09-23T12:00:00Z"); @@ -16,6 +19,41 @@ afterEach(() => { }); describe("ConversationView", () => { + it("summarizes successful and failed operations in a collapsed work log", () => { + const { container } = render( + , + ); + + expect(container).toHaveTextContent("3 operations"); + expect(container).toHaveTextContent("2 succeeded"); + expect(container).toHaveTextContent("1 failed"); + expect(screen.getByText("Work log").closest("details")).not.toHaveAttribute( + "open", + ); + }); + + it("keeps a zero failure count visible for successful work", () => { + const { container } = render( + , + ); + + expect(container).toHaveTextContent("2 succeeded"); + expect(container).toHaveTextContent("0 failed"); + }); + it("updates a running tool duration without announcing every tick", () => { vi.useFakeTimers(); vi.setSystemTime(base + 2_000); @@ -203,3 +241,47 @@ function toggleWorkLog(open: boolean) { details.open = open; fireEvent(details, new Event("toggle")); } + +function toolTurn( + tools: readonly { id: string; name: string; content: string }[], +): ConversationViewProps["messages"] { + return [ + { + sequence: 1, + message: { + role: "user", + content: "run tools", + toolCalls: [], + toolCallId: null, + toolName: null, + createdAt: new Date(base).toISOString(), + }, + }, + { + sequence: 2, + message: { + role: "assistant", + content: "", + toolCalls: tools.map(({ id, name }) => ({ + id, + name, + argumentsJson: "{}", + })), + toolCallId: null, + toolName: null, + createdAt: new Date(base + 500).toISOString(), + }, + }, + ...tools.map(({ id, name, content }, index) => ({ + sequence: index + 3, + message: { + role: "tool" as const, + content, + toolCalls: [], + toolCallId: id, + toolName: name, + createdAt: new Date(base + 1_000 + index * 500).toISOString(), + }, + })), + ]; +} diff --git a/web/packages/superagent-ui/src/ConversationView.tsx b/web/packages/superagent-ui/src/ConversationView.tsx index 3bf6449..22db309 100644 --- a/web/packages/superagent-ui/src/ConversationView.tsx +++ b/web/packages/superagent-ui/src/ConversationView.tsx @@ -160,18 +160,20 @@ function WorkLog({ 0, ); if (operationCount === 0) return null; - const failed = - turn.models.some((item) => item.status === "failed") || - turn.tools.some((group) => - group.calls.some((item) => item.status === "failed"), - ); - const running = - turn.models.some((item) => item.status === "running") || - turn.tools.some((group) => - group.calls.some( - (item) => item.status === "running" || item.status === "waiting", - ), - ); + const statuses = [ + ...turn.models.map((item) => item.status), + ...turn.tools.flatMap((group) => group.calls.map((item) => item.status)), + ]; + const succeededCount = statuses.filter( + (status) => status === "complete", + ).length; + const failedCount = statuses.filter((status) => status === "failed").length; + const activeCount = statuses.filter( + (status) => status === "running" || status === "waiting", + ).length; + const unknownCount = statuses.filter((status) => status === "unknown").length; + const failed = failedCount > 0; + const running = activeCount > 0; return (
{operationCount} operation{operationCount === 1 ? "" : "s"} - {failed ? : null} + + {succeededCount} succeeded + + + {failedCount} failed + + {activeCount > 0 ? ( + + {activeCount} active + + ) : null} + {unknownCount > 0 ? ( + + {unknownCount} unknown + + ) : null} diff --git a/web/packages/superagent-ui/styles.css b/web/packages/superagent-ui/styles.css index 5896534..7879d60 100644 --- a/web/packages/superagent-ui/styles.css +++ b/web/packages/superagent-ui/styles.css @@ -882,6 +882,15 @@ gap: 10px; } +.sa-work-log__summary-count { + color: var(--sa-muted-color, #667066); + font-size: 11px; +} + +.sa-work-log__summary-count--failed { + color: var(--sa-danger-color, #9b2c25); +} + .sa-work-log__body { display: flex; flex-direction: column;