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;