mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
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:
@@ -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 }) {
|
||||
|
||||
@@ -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(" ")}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(" ")}
|
||||
|
||||
Reference in New Issue
Block a user