Skip to content

fix(chat): show why a settled subagent failed in its detail panel - #192

Merged
vastsa merged 5 commits into
vastsa:mainfrom
Yi-111-a:subagent-error-card-161
Sep 10, 2026
Merged

fix(chat): show why a settled subagent failed in its detail panel#192
vastsa merged 5 commits into
vastsa:mainfrom
Yi-111-a:subagent-error-card-161

Conversation

@Yi-111-a

@Yi-111-a Yi-111-a commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

When a delegate ends failed, timed_out, or aborted, its detail panel shows a step stream that just stops. The status capsule says Failed, the elapsed time is there, and nothing says why. A delegate that dies before emitting any message row has no other carrier for its reason, so the panel is a list of steps and no explanation.

Reported as #161.

This renders the reason as an error card at the foot of the dock.

Why the reason was missing

The reason already exists at runtime — SubagentRunResult.error: { code, message } — and delegationSummary in packages/agent-runtime/src/runtime.ts already spreads it onto the delegation roster entry. It simply had no reader on this surface:

  • Task returns the moment a delegate starts (ADR 0089), so its own tool result is pinned at running forever, no matter how the delegate ended. SubagentDetail renders that row.
  • SubagentRun (the delegate's own row collection) carries only agentName + items — no error field.
  • The roster entries are attached to the lifecycle rows (TaskWait / TaskList / TaskStop), which are deliberately not topology nodes.

So this is a presentation gap, not a runtime gap: the error was already being delivered to the renderer and then discarded. No runtime, IPC, storage, or tool-result change is needed.

What changed

  • collectDelegationFailures(items) in src/lib/subagent-topology.ts walks the lifecycle rows' details.delegations[] and details.stopped[] and returns delegationId → { code, message }. It is deliberately the same shape as the existing collectDelegationStatuses (same iteration, same last-write-wins rule, same "the Task row is not a carrier" exclusion), so the status and the reason can never disagree about which row wins.
  • SubagentDetail gains an optional delegationFailures prop and renders a SubagentFailureCard at the foot of the panel. SubagentPanel computes the map from selected.turnActivityItems — the same array it already feeds to collectDelegationStatuses and collectDelegationTimings.
  • The card reuses the transcript's existing error-card visual language and vocabulary rather than inventing a subagent-specific one: error.code uses the same errors.<code> lookup (AssistantErrorMessage), the summary falls back to the localized chat.subagentStatus.* outcome when a code is not registered, and the raw provider message sits behind a disclosure with a copy control.

No new i18n keys. All eight locales already carry chat.subagentStatus.* (all eight outcomes), chat.showErrorDetails / chat.hideErrorDetails, and chat.copyErrorDetails — the card only reuses them.

Two details worth calling out, since both would be easy to get wrong:

  • The card follows a non-success terminal outcome, not the error field. A delegation can carry an error and still be completed, and a still-running delegate has no terminal outcome to explain. The gate is failure && outcome !== "completed" && outcome !== "running".
  • The disclosure control is in the heading, not inside the collapsed region. The parent error card puts its toggle in message-error-actions for exactly this reason; hiding the details must not take away the control that brings them back.

Documentation

  • spec/04-ux/08-component-spec.md §5.7 gains the failure card, the summary fallback, and the terminal-outcome gate.
  • E2E-198 (the subagent dock scenario) gains step 6 and the matching expectation, and its status records the new unit coverage.
  • spec/08-meta/decisions-log.md records D382, mirrored in docs/zh-CN/.

Note on the zh-CN mirrors: docs/zh-CN/spec/04-ux/08-component-spec.md is missing the entire §5.7, and docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md does not have E2E-198 at all — both predate this change. I did not translate a whole pre-existing missing section here, since that would be an unrelated change (AGENTS.md R2); happy to do it as a separate PR if useful. The decisions log mirror is current, so its entry is synchronized.

Validation

  • node --test test/subagent-topology.test.mjs test/subagent-panel.test.mjs23 passed, 0 failed.
    • 5 new behavioural tests in subagent-topology.test.mjs: a failure read from a TaskWait roster entry, one read from TaskStop's stopped[], last-write-wins across a repeated row, entries that report no error at all (status only, error: {}, whitespace-only message, missing delegationId), and the Task row that must never carry one.
    • 1 new source-contract test in subagent-panel.test.mjs, following that file's existing pattern for this panel: the prop is wired, the gate is the terminal outcome, and the toggle precedes the region it collapses.
  • node scripts/check-style-tokens.mjsstyle tokens OK.
  • The rest of apps/desktop/test/*.test.mjs reports 24 failures on this machine, all of them ERR_MODULE_NOT_FOUND: Cannot find package '@pi-desktop/shared' — this checkout has no node_modules because pnpm install cannot complete on this network (metadata timeouts; it has been retrying for several minutes). Those files are unrelated to this change and fail identically on unmodified main. CI is the authoritative check for pnpm typecheck / lint / the full workspace suite.

If the toggle placement or the summary fallback should follow a different convention in this panel, say so and I will adjust.


Numbering note: the decision is D382. D381 is taken by #191 (the selectable Windows
shell change), which itself moved up from D380 after upstream 8ce8ed36 claimed it.
Both decisions logs follow. The sibling PRs are #191 at D381 and #193 at D383.

A delegate that ended failed, timed_out, or aborted showed only an outcome
capsule and an elapsed time. The reason existed but had no reader: the runtime
reports `error: { code, message }` on the delegation roster entry, while a node
and its dock render the `Task` row's own result, which ADR 0089 fixes at
`running` the moment the delegate starts. A delegate that died before emitting
a message row therefore left a panel of steps and no explanation (issue vastsa#161).

Collect the failure from the lifecycle rows' `details.delegations[]` and
`details.stopped[]` with the same last-write-wins rule as the settled status,
and close the dock with an error card that reuses the transcript error card's
visual language and its `errors.<code>` vocabulary, falling back to the
localized `chat.subagentStatus.*` outcome when a code is not registered.

The card follows a non-success terminal outcome rather than the error field
alone, so a completed or still-running delegate never shows one. Its disclosure
control sits in the heading so it stays reachable while the details are
collapsed, and a code-only error stays a one-line card instead of opening onto
an empty box.

No runtime, IPC, storage, or tool-result change — the runtime already reported
the error and this stops discarding it — and no new i18n keys.

Verified: node --test test/subagent-topology.test.mjs test/subagent-panel.test.mjs
(23 passed, 0 failed).
Synchronize the surfaces that describe a settled delegate:

- spec 04-ux 08-component-spec §5.7 gains the failure card, its summary
  fallback, and the non-success terminal-outcome gate.
- E2E-198 gains step 6 and the matching expectation, and its status records the
  new unit coverage.
- The decisions log records D381, and the zh-CN mirror of the decisions log
  carries the same entry.

No behavior change beyond the code commit.
@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown

@Yi-111-a is attempting to deploy a commit to the vastsa's projects Team on Vercel.

A member of the Team first needs to authorize it.

D381 is now taken by the selectable Windows shell change (vastsa#191), so this
entry moves to D382 and the two PRs stay distinct. Both decisions logs
follow.
Keep D381 (PowerShell 7 catalog) and D382 (settled-subagent failure card)
as consecutive decisions-log entries after vastsa#191 landed on main.
collectDelegationFailures skipped Task rows the same way
collectDelegationStatuses does, but read item.message after the
predicate. That guard narrows a tool item to never, so typecheck
failed. Pull the message off first, matching the existing collector.

@vastsa vastsa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Principle

Sound. Merging this PR.

Issue #161 is real on current main: a delegate that ends failed / timed_out / aborted shows a Failed capsule and a step stream, and nothing else. SubagentRunResult.error already travels on the lifecycle roster (delegationSummary spreads record.result?.error), but the dock renders the Task row, which ADR 0089 pins at running. This is a presentation gap, not a runtime/IPC/storage change.

The approach is the right one:

  • Collect error: { code, message } from the same lifecycle rows, with the same last-write-wins rule, as collectDelegationStatuses.
  • Gate the card on a non-success terminal outcome, not on the error field alone.
  • Reuse the transcript error card (summary, code, disclosure + copy) and existing i18n keys.
  • Topology node and sidebar share SubagentDetail via the dock, so both entry points get the card.

Landing

main had moved (#191 / D381), so the decisions logs conflicted. I merged main into this branch (keep D381 then D382) and copied the tool-row message before the isDelegationActivityItem guard — the same never narrowing that collectDelegationStatuses already documents. Typecheck now passes.

Validation

  • node --test test/subagent-topology.test.mjs test/subagent-panel.test.mjs — 23 passed
  • node scripts/check-style-tokens.mjs — OK
  • CI after the landing commits: Docs locale pair, JS typecheck/lint/test, Rust host-core — pass. Vercel is unrelated deploy-auth.

Follow-up (not merge blockers)

  • Optional issue ask (failed step/tool name) is still out of scope.
  • zh-CN is missing the pre-existing §5.7 / E2E-198 sections; correctly not invented here.
  • A reconstructed leftover aborted with no roster error still has no card.

Thank you — clean diagnosis and a small, matching collector.

@vastsa
vastsa merged commit 4d96757 into vastsa:main Sep 10, 2026
3 of 4 checks passed
@vastsa

vastsa commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Merged as 4d967576 on main, preserving the author commits plus two landing commits on top (93dfb5d7 merge of main for the D381/D382 decisions-log conflict, 79fa90f4 typecheck fix for the Task-row guard).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants