fix(webui): soften failed-tool activity summary with a subtle badge (#7302) (#7305)

When a tool call failed mid-run, the whole collapsed activity summary
("Activity - 13 tools, 4 failed") was painted in the danger color, so a
run the agent recovered from still read as an alarming error.

Drop the whole-row danger recolor from both summary rows (ActivityRun and
the collapsed ToolRun) and keep them in the neutral muted text. A recovered
failure is now flagged only by a small warning-tinted `alert` badge — an
informational cue, not a red banner. The collapsed tool-run summary text
omits the failure count, so its badge carries an sr-only note (reusing the
existing activity.failed strings) for assistive technology.

Adds an `alert` glyph to the icon set and a source-level regression test
pinning: no whole-row danger recolor, the gated warning badge in each row,
and the accessible note on the tool-run badge.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ron
2026-08-12 19:33:16 +00:00
committed by GitHub
parent 88fe2d01e9
commit ee3146af9b
4 changed files with 75 additions and 11 deletions

View File

@@ -131,6 +131,10 @@ const paths = {
arrowDown: (<><path d="M12 5v14" /><path d="m6 13 6 6 6-6" /></>),
retry: (<><path d="M3.5 12a8.5 8.5 0 1 1 2.6 6.1" /><path d="M3.2 18.5v-5h5" /></>),
alert: (<><path d="M12 5 3.5 19.5h17L12 5Z" /><path d="M12 10.5v3.6" /><path
d="M12 16.9h.01"
/></>),
};
export function Icon({ name, className = "", strokeWidth = 1.7 }) {

View File

@@ -36,15 +36,15 @@ export function ActivityRun({ activity, activeRunId = null }: ActivityRunProps)
onClick={() => setExpanded((value) => !value)}
aria-expanded={expanded ? "true" : "false"}
data-testid="activity-run-toggle"
className={[
"v2-button flex w-full min-w-0 items-center gap-2 border-0 bg-transparent px-1 py-1.5 text-left text-sm",
summary.hasError
? "text-[var(--v2-danger-text)]"
: "text-iron-400 hover:text-iron-200",
].join(" ")}
className="v2-button flex w-full min-w-0 items-center gap-2 border-0 bg-transparent px-1 py-1.5 text-left text-sm text-iron-400 hover:text-iron-200"
>
<Icon name="layers" className="h-4 w-4 shrink-0" />
<span className="min-w-0 truncate">{summary.label}</span>
{summary.hasError &&
(<Icon
name="alert"
className="h-3.5 w-3.5 shrink-0 text-[var(--v2-warning-text)]"
/>)}
<Icon
name="chevron"
className={["ml-auto h-3.5 w-3.5 shrink-0", expanded ? "rotate-180" : ""].join(" ")}

View File

@@ -14,6 +14,10 @@ const typingIndicatorSource = readFileSync(
new URL("./typing-indicator.tsx", import.meta.url),
"utf8",
);
const iconsSource = readFileSync(
new URL("../../../design-system/icons.tsx", import.meta.url),
"utf8",
);
test("tool activity cards keep long tool output inside the mobile viewport", () => {
assert.match(
@@ -67,6 +71,52 @@ test("tool activity cards keep long tool output inside the mobile viewport", ()
);
});
test("a failed tool call is flagged with a subtle badge, not a red banner (#7302)", () => {
// The whole-summary danger recolor is gone from the activity-run row: a
// recovered run should read as informational, never alarming.
assert.doesNotMatch(
activityRunSource,
/--v2-danger-text/,
"activity run summary should not paint the entire row with the danger color",
);
// The tool-run collapse row keeps its neutral text and drops the ternary that
// swapped in the danger color on failure.
assert.doesNotMatch(
toolActivitySource,
/hasError \? "text-\[var\(--v2-danger-text\)\]"/,
"tool run summary should not swap the whole row to the danger color",
);
// A subtle warning-tinted badge stands in for the red, gated on the failure
// flag in each summary row.
assert.match(
activityRunSource,
/summary\.hasError &&[\s\S]{0,200}name="alert"[\s\S]{0,120}text-\[var\(--v2-warning-text\)\]/,
"activity run summary should show a subtle warning badge when a tool failed",
);
assert.match(
toolActivitySource,
/hasError &&[\s\S]{0,320}name="alert"[\s\S]{0,120}text-\[var\(--v2-warning-text\)\]/,
"tool run summary should show a subtle warning badge when a tool failed",
);
// The collapsed tool-run summary text omits the failure count, so the badge
// carries an accessible note for assistive tech.
assert.match(
toolActivitySource,
/className="sr-only"[\s\S]{0,160}activity\.failed/,
"collapsed tool-run badge should expose the failure to assistive technology",
);
// The badge glyph must exist, or the Icon component silently falls back to
// the default `spark` icon.
assert.match(
iconsSource,
/\balert:\s*\(/,
"an `alert` glyph should exist for the failure badge",
);
});
test("activity run wrappers use mobile-safe width constraints", () => {
assert.match(
activityRunSource,

View File

@@ -64,7 +64,8 @@ function summarizeTools(tools, t) {
More than that → a collapsed summary line that expands to the full rows. */
export function ToolRun({ tools }) {
const t = useT();
const hasError = tools.some((tool) => tool.toolStatus === "error");
const failedCount = tools.filter((tool) => tool.toolStatus === "error").length;
const hasError = failedCount > 0;
const hasTerminalNotice = tools.some(
(tool) => tool.toolStatus === "error" || tool.toolStatus === "declined",
);
@@ -94,13 +95,22 @@ export function ToolRun({ tools }) {
type="button"
onClick={() => setExpanded((value) => !value)}
aria-expanded={expanded ? "true" : "false"}
className={[
"v2-button flex w-full min-w-0 items-center gap-2 border-0 bg-transparent px-1 py-1.5 text-left text-sm",
hasError ? "text-[var(--v2-danger-text)]" : "text-iron-400 hover:text-iron-200",
].join(" ")}
className="v2-button flex w-full min-w-0 items-center gap-2 border-0 bg-transparent px-1 py-1.5 text-left text-sm text-iron-400 hover:text-iron-200"
>
<Icon name="layers" className="h-4 w-4 shrink-0" />
<span className="min-w-0 truncate">{summary}</span>
{hasError &&
(<>
<span className="sr-only">
{t(failedCount === 1 ? "activity.failed" : "activity.failedPlural", {
count: failedCount,
})}
</span>
<Icon
name="alert"
className="h-3.5 w-3.5 shrink-0 text-[var(--v2-warning-text)]"
/>
</>)}
<Icon
name="chevron"
className={["ml-auto h-3.5 w-3.5 shrink-0", expanded ? "rotate-180" : ""].join(" ")}