Skip to content

🤖 feat: show task kind and spawn intent in single-task task_await summary - #3793

Merged
ibetitsmike merged 3 commits into
mainfrom
mike/task-await-summary-detail
Aug 4, 2026
Merged

🤖 feat: show task kind and spawn intent in single-task task_await summary#3793
ibetitsmike merged 3 commits into
mainfrom
mike/task-await-summary-detail

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

For a single-task task_await, the collapsed transcript row now shows what actually finished: the task kind (bash, workflow, workspace, or the sub-agent type) plus the spawn intent or title, e.g. 1 task completed · bash · Resolving pagination feedback and requesting another review.

Background

The collapsed row previously said only 1 task completed, which conveys nothing about what finished, forcing the user to expand every await row to see which background work landed.

Implementation

  • Kind is derived from the task handle (bash: → bash, wfr_ → workflow, wst_ → workspace) or from the spawning task call's agentId/subagent_type.
  • task_await results don't carry model_intent, so the existing render-time computeTaskReportLinking pass (which already links spawn titles) now also indexes background bash spawns by taskId and captures their model_intent + script.
  • The intent is sanitized via a shared sanitizeDisplayableModelIntent helper (extracted from buildBashCollapsedSummary) so an intent that merely restates the command is rejected, falling back to the result title (bash display_name or agent report title).
  • Multi-task summaries stay count-only to avoid noise; the detail renders in the existing truncating muted span so long text clips gracefully at narrow widths.

Scope note: only the completed state is enriched; the Waiting for N tasks state could get the same suffix via the same helper if desired.

Validation

  • 5 new tests in TaskToolCall.test.tsx (bash intent, command-echo intent fallback, title fallback, agent type, multi-task stays count-only), red-green verified against the summary wiring and the echo-intent fallback.
  • Sandbox UAT with seeded real sessions: verified bash and explore awaits at desktop and 375px mobile widths (no overflow), plus expanded-row rendering.

Generated with mux • Model: anthropic:claude-fable-5 • Thinking: xhigh

Review findings: reuse the intent-vs-command comparison from
buildBashCollapsedSummary via a shared sanitizeDisplayableModelIntent
helper, and drop the unused BashTaskSpawnInfo.displayName field (bash
task_await results already carry display_name as their title).
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aaaa78529e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/Tools/TaskToolCall.tsx Outdated
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

Mux is acting on Mike's behalf.

@coder-agents-review

coder-agents-review Bot commented Aug 4, 2026

Copy link
Copy Markdown

Chat: Review posted | View chat
Requested: 2026-08-04 15:05 UTC by @ibetitsmike

Review history
  • R1 (2026-08-04): 22 reviewers, 10 Nit, 4 Note, 6 P2, 12 P3, COMMENT. Review

deep-review v0.9.0 | Round 1 | d8e92cb..aaaa785

Last posted: Round 1, 32 findings (6 P2, 12 P3, 10 Nit, 4 Note), COMMENT. Review

Finding inventory

Finding inventory: coder/mux PR #3793

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Open taskReportLinking.ts:14 BashTaskSpawnInfo.modelIntent optional but producer skips intent-less spawns, so bashSpawnByTaskId is a partial index R1 Netero Yes
CRF-2 P3 Open TaskToolCall.tsx:1351 workflow and workspace kind branches have no test R1 Netero, Bisky Note Yes
CRF-3 P3 Open TaskToolCall.tsx:464 isWorkflowRunTaskHandleId duplicates the wfr_ prefix owned by src/node/services/tools/taskId.ts R1 Netero Yes
CRF-4 Nit Open taskReportLinking.ts:146 First-pass loop comment still describes a task-only loop R1 Netero, Gon Yes
CRF-5 Note Open TaskToolCall.test.tsx:519 Multi-task count-only test passes on base code (no red-green value) R1 Netero, Bisky Yes
CRF-6 Note Open CI Test / Unit failing on CI; not reproducible locally, needs someone with log access R1 Netero Yes (body)
CRF-7 P2 Open TaskToolCall.tsx:1402 Nonzero exitCode ignored: failed background bash renders as green 1 task completed · bash · <intent> R1 Mafuuu P2, Chopper P2, Meruem P2 Yes
CRF-8 P2 Open taskReportLinking.ts:158 bash: taskIds are reused across restarts/compaction; last-wins index attributes a newer spawn's intent to an older await row R1 Komugi P2, Mafuuu P2, Nami P2, Knov P3, Melody P3, Razor P3, Hisoka P4 Yes
CRF-9 P2 Open TaskToolCall.tsx:1361 Title fallback has no command-echo guard, so a rejected intent returns as the (filename-sanitized) command R1 Hisoka P2, Bisky P3, Pariston P3, Luffy P3, Melody Note Yes
CRF-10 P2 Open TaskToolCall.tsx:1402 Adjacent task_await polls collapse into Checked task status N times, so the new detail never renders in that shape R1 Melody P2, Kite P3 Yes
CRF-11 P2 Open TaskToolCall.tsx:1357 Collapsed chain omits the workspace-title rung the expanded row uses, so title-less results fall back to bare 1 task completed R1 Pariston P2, Zoro P3, Chopper P3, Robin P3, Meruem P3, Melody P3, Kite P3 Yes
CRF-12 P3 Open taskReportLinking.ts:96 Agent type rendered verbatim from model args, bypassing normalizeAgentId/AgentIdSchema R1 Bisky P3, Robin P3, Hisoka Nit, Zoro Note Yes
CRF-13 P3 Open TaskToolCall.tsx:1436 aria-label carries summaryTitle only, so the new detail (and callError) is sighted-only R1 Mafu-san P3, Leorio P3, Knov P3 Yes
CRF-14 P3 Open TaskToolCall.tsx:1393 Waiting for 1 task keeps the exact problem the PR opens with; scope note has no ticket R1 Leorio P3, Nami P3, Luffy P3 Yes
CRF-15 P3 Open TaskToolCall.tsx:1381 Single interrupted task stays count-only and is not force-expanded R1 Pariston P3, Mafu-san P3, Mafuuu Note, Chopper Note Yes
CRF-16 P2 Open TaskToolCall.test.tsx:484 Sub-agent test asserts a string both title sources produce; mutation-proved it cannot fail for the precedence it appears to cover R1 Mafu-san Yes
CRF-17 P3 Open TaskToolCall.tsx:1349 Fifth in-file spelling of task-kind resolution; the new one already disagrees with the waiting-row one R1 Mafu-san P3, Zoro P3, Knov Nit, Chopper Nit Yes
CRF-18 P3 Open TaskToolCall.tsx:1344 results[0] dereferenced through an unchecked length guard, 55 lines above its only consumer R1 Meruem P3, Nami Nit, Mafuuu Nit, Kite Nit, Zoro Nit, Hisoka Note Yes
CRF-19 P3 Open taskReportLinking.ts:52 Two required fields added to TaskReportLinking; the hand-built fixture at TaskToolCall.test.tsx:226 is now incomplete and typecheck cannot see it R1 Meruem Yes
CRF-20 P3 Open taskReportLinking.ts:46 Three parallel spawn maps keyed by the same taskId; BashTaskSpawnInfo exports an intermediate with a "" placeholder R1 Meruem P3, Zoro P3 Yes
CRF-21 P3 Open taskReportLinking.ts:154 Background bash spawned inside code_execution is a nested call, never a top-level message, so it is never indexed R1 Hisoka Yes
CRF-22 Nit Open TaskToolCall.test.tsx:385 No viewport-pinned story covers the new detail; the 375px claim rests on an unreproducible sandbox session R1 Melody, Chopper, Mafu-san Yes
CRF-23 Nit Open TaskToolCall.tsx:1342 Second clause of the new comment narrates the code below it R1 Gon P2 (adjusted to Nit) Yes
CRF-24 Nit Open taskReportLinking.ts:139 computeTaskReportLinking docblock describes report linking only, but the function returns two more indexes R1 Gon Yes
CRF-25 Nit Open TaskToolCall.tsx:1349 kind is a third name for agentType and collides with handleKind R1 Gon Yes
CRF-26 Nit Open taskReportLinking.ts:112 getBashSpawnTaskId re-implements getTaskIdsFromToolResult's singular branch and its name promises bash-specific logic R1 Gon, Robin, Zoro Yes
CRF-27 Nit Open taskReportLinking.ts:49 bashSpawnByTaskId doc claims all background bash spawn args; it holds two fields for the intent-carrying subset R1 Leorio Yes
CRF-28 Nit Open bashCollapsedSummary.ts:90 Doc names one of the two ways sanitizeDisplayableModelIntent returns undefined R1 Leorio Yes
CRF-29 Note Open TaskToolCall.tsx:1359 The bash detail ignores the user's bashCollapsedSummaryMode setting R1 Nami Yes
CRF-30 Nit Open TaskToolCall.test.tsx:400 Fixture taskId bash:pr-ready-watcher-a1b2 implies entropy the backend never adds, which hides CRF-8 R1 Nami Yes
CRF-31 Note Open TaskToolCall.tsx:1363 Model text is spliced into a ·-delimited status string, so an intent can fabricate row structure R1 Kurapika Yes
CRF-32 Note Open TaskToolCall.tsx:1361 Detail capitalization depends on which fallback wins (intent capitalized, titles raw) R1 Leorio Yes
CRF-33 Nit Open TaskToolCall.test.tsx:74 createToolMessage is a second local DisplayedMessage factory with hardcoded ids R1 Robin, Bisky Yes
CRF-34 Note Dropped by orchestrator (reviewer requested no change; ~20 in-repo siblings, house style) TaskToolCall.tsx:1363 (part): part is string predicate has been inferred since TS 5.5 R1 Ging-TS No
CRF-35 Note Dropped by orchestrator (concerns unchanged code outside the diff; captured in vault) ChatPane.tsx:526 ChatPane fails React Compiler, so the manual useMemo around computeTaskReportLinking is load-bearing R1 Ging-React No
CRF-36 Note Dropped by orchestrator (subsumed by CRF-11 and CRF-17) TaskToolCall.tsx:1356 Collapsed header labels a bash task by intent while the expanded row labels it by displayName R1 Razor No

Drop/downgrade gate, keep-arguments written before each decision:

  • CRF-34 keep-argument: it flags a compiler-version-dependent redundancy that a future sweep would want listed. Dropped because the reviewer explicitly did not request a change and the explicit form is the established convention at ~20 sites; posting it changes no reader decision on this PR.
  • CRF-35 keep-argument: it warns a future reader not to strip a useMemo that looks redundant, which would be a real regression. Dropped from the review because it concerns ChatPane.tsx code this diff does not touch; recorded in the vault instead so the knowledge survives.
  • CRF-36 keep-argument: header and row naming the same task differently is a real user-visible inconsistency. Dropped because the fix is exactly the single-identity-resolution change CRF-11 and CRF-17 already ask for; a separate comment would triple-count one root cause.
  • CRF-23 downgrade keep-argument (Gon P2): AGENTS.md mandates substantive, concise comments, and this PR has three comment defects, which is a pattern rather than a one-off. Adjusted to Nit because the review vocabulary reserves Nit for convention and style violations where the code works, and no behavior or reader decision depends on the second clause. The finding itself is posted unchanged.

Severity escalations applied during cross-check:

  • CRF-8 raised to P2 (Komugi, Mafuuu, Nami) over the P3/P4 reports: Komugi reproduced the misattribution deterministically through the compaction path and Razor proved the last-wins index at unit level. Higher severity wins on disagreement.
  • CRF-9 held at P2 (Hisoka) over three P3s, with the reachability corrected: display_name is required by the bash schema (toolDefinitions.ts:1537, z.string() with no nullish), so the "display_name omitted" path Hisoka, Luffy and Pariston described is not reachable through a schema-valid tool call. The reachable path is a display_name that restates the command, which sanitizeBashDisplayName still rewrites / | : > < to _ in, so the mangled-pseudo-command outcome stands.
  • CRF-10 held at P2 (Melody) over Kite's P3: the orchestrator verified the mechanism in transcriptRenderProjection.ts:181-228 and ChatPane.tsx:1584-1611. When a run of 2+ adjacent task_await messages settles cleanly, defaultExpanded is false, member rows return null, and the head renders only OperationalBundleMessage, so no individual summary renders at all.
  • CRF-11 held at P2 (Pariston) over six P3s: three reviewers independently rendered the collapsed row falling back to bare 1 task completed or bare workspace while the expanded row one click below names the task. Combined with Knov's 64-message display window, the enrichment silently reverts to pre-PR behavior in reachable, common cases.

Contested and acknowledged

None.

Round log

Round 1

Netero first pass: 3 P3, 1 Nit, 2 Notes. No P0-P2, panel proceeded. Panel of 21 trigger-matched reviewers plus Kurapika as wildcard. 6 P2, 13 P3, 9 Nit, 5 Note after cross-check; 3 dropped. Reviewed against d8e92cb..aaaa785.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

Codex review: the collapsed row should describe the task's spawn intent;
the report heading is only a fallback.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: b7e05300e3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ibetitsmike
ibetitsmike enabled auto-merge August 4, 2026 15:13
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 8241e56 Aug 4, 2026
21 of 22 checks passed
@ibetitsmike
ibetitsmike deleted the mike/task-await-summary-detail branch August 4, 2026 15:27
mux-bot Bot added a commit that referenced this pull request Aug 4, 2026
The module reads every field out of persisted tool args/results typed as
`unknown`, and #3789/#3793 grew that from four to seven copies of the same
"is a string, is not blank, use the trimmed form" check. Collapse those seven
into a single coerceNonBlankString helper.

The two task_await result reads are deliberately left alone: they validate a
field is non-blank but then store the raw, untrimmed value, so routing them
through the helper would change what gets persisted.
@mux-bot mux-bot Bot mentioned this pull request Aug 4, 2026

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The extraction is the best part of this diff: sanitizeDisplayableModelIntent removes the inline copy rather than adding a fifth intent sanitizer, so the "this intent just restates the command" rule now has one implementation shared by the bash row and the await row. Three of the five new tests drive the real computeTaskReportLinking over hand-built DisplayedMessage inputs instead of stubbing a TaskReportLinking, which is why the wiring is genuinely exercised; four of the five are red on base. The kind enumeration was checked against every set it reads from and is complete, the taskId-implies-background-spawn invariant holds against BashToolResultSchema, and the detail renders inside a min-w-0 flex-1 truncate span so long intents clip by construction. make typecheck is clean on both projects and the touched suites pass.

Round 1, 22 reviewers plus a first pass. 6 P2, 13 P3, 9 Nit, 5 Note.

The P2s cluster into four things the collapsed row now gets wrong or fails to show. Two are wrong claims: a background bash that exited nonzero renders as a green 1 task completed · bash · <intent> (CRF-7), and a reused bash: taskId lets a newer spawn's intent label an older row (CRF-8, reproduced deterministically through the compaction path). Two are the feature not arriving: adjacent task_await polls collapse into Checked task status N times and no per-row summary renders at all (CRF-10, verified in transcriptRenderProjection.ts and ChatPane.tsx), and the description chain omits the workspace-title rung the expanded row uses, so title-less results fall back to bare 1 task completed (CRF-11). CRF-9 is the guard you added being bypassed by the fallback directly beneath it. CRF-16 is the sub-agent test: it asserts a string that both candidate title sources produce, mutation-proved, which is also the precedence question the Codex thread raised.

One correction to reviewer evidence, since it changes CRF-9's reachability: display_name is required in the bash schema (toolDefinitions.ts:1537, z.string() with no nullish), so the "model omitted display_name" path three reviewers described is not reachable through a schema-valid call. The reachable path is a display_name that restates the command, and sanitizeBashDisplayName still rewrites / | : > < to _ in authored names, so the mangled-pseudo-command outcome stands.

Two things need a human decision rather than an agent one. The PR's scope note ("the Waiting for N tasks state could get the same suffix ... if desired") has no ticket, which makes it a drop, not a deferral: CRF-14 and CRF-15 are the waiting and interrupted rows still carrying the exact problem the Background section opens with. Either extend them or file the ticket. Separately, CI reports Test / Unit failing (run 92014224880). Neither the first pass nor the orchestrator could attribute it to this diff: the touched suite is 18/18, and the full local run is 10281 pass with 15 failures that are all sandbox git-wrapper artifacts in diffParser.test.ts and friends. Someone with CI log access needs to confirm that before merge; we are not claiming it is unrelated, only that we could not reproduce it (CRF-6).

From Luffy, who liked this one: "OI, THIS IS THE GOOD KIND OF CHANGE! 'One task completed' was a row that told you nothing and made you click."


src/browser/features/Tools/TaskToolCall.tsx:1436

P3 [CRF-13] The new detail is excluded from the row's accessible name, so the information this PR adds is sighted-only. (Mafu-san P3, Leorio P3, Knov P3)

aria-label={${summaryTitle}. Show task wait details} names the row with the count alone. A screen reader user hears "1 task completed. Show task wait details" and still has to expand the row, which is the exact cost the PR sets out to remove.

The gap is class-wide, not new: the sharpest pre-existing instance is the failure case, where the label says "Task wait failed" while the entire diagnosis (callError) lives in summaryDetail. This PR adds a third summaryDetail producer, so the new text joins the error text on the wrong side of that divide. One line covers all of them:

aria-label={`${[summaryTitle, summaryDetail].filter(Boolean).join(" · ")}. Show task wait details`}

Unverified: which string an actual screen reader speaks, since ToolHeader renders a bare div with no role. Either way the code holds two descriptions of one row and only one is complete. Note the existing test at TaskToolCall.test.tsx:553 queries by that label, so the change is observable.

🤖

src/browser/features/Tools/TaskToolCall.tsx:1393

P3 [CRF-14] The row now names what finished but still says nothing about what you are waiting on, which is the state the user actually sits in. (Leorio P3, Nami P3, Luffy P3)

While the await is in flight the row says Waiting for 1 task, and while partially done Still waiting for 1 task · 1 completed. Same disease, and worse, because unlike the failure path this state is not force-expanded, and it is the state the row spends its entire lifetime in before the answer arrives. Someone watching an await hang for four minutes gets a spinner, an elapsed counter, and the word "task".

The data is already computed in this render: for status === "executing" with a single task_ids entry, awaitedRows[0].title is resolved from backgroundProcesses (proc.displayName ?? proc.id) or from workspace metadata, and agentType from resolvePersistedAgentId, at lines 1291-1336. It is computed, then thrown away for the collapsed row.

The PR says this state "could get the same suffix via the same helper if desired." A scope note with no ticket is a drop, not a deferral, and no agent gets to accept it: either extend the single-task path to the waiting and still-waiting branches, or file the ticket and link it here.

🤖

src/browser/features/Tools/TaskToolCall.tsx:1381

P3 [CRF-15] A single interrupted task is the one collapsed terminal state with no identity and no auto-expand, and it is left untouched. (Pariston P3, Mafu-san P3)

forceExpanded (line 1413) opens the row for callError, status === "failed", and failedCount > 0. isInterruptedTaskAwaitResult results are deliberately excluded from failedCount, so the interrupted branch stays collapsed showing 1 task interrupted and nothing else. Verified: a single {status: "interrupted", taskId: "task-1"} with a linked task spawn renders 1 task interrupted with no detail, even though the title and the agent type are sitting in taskReportLinking.

Failed rows are fine because they force-expand. Interrupted rows do not, so this is the collapsed state where a user asks "which one got interrupted?" and gets no answer. singleTaskDetail already computes kind independently of status; widening the guard to firstResult.status === "completed" || isInterruptedTaskAwaitResult(firstResult) is a two-line change. The scope note names only the waiting state, which suggests this branch was not considered rather than excluded.

🤖

🤖 This review was automatically generated with Coder Agents.

summaryTone = "waiting";
} else if (completedCount > 0) {
summaryTitle = `${formatTasks(completedCount)} completed`;
summaryDetail = singleTaskDetail;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 [CRF-7] A background bash that exited nonzero or was killed renders as 1 task completed · bash · <intent> with a green check, asserting the work succeeded. (Mafuuu P2, Chopper P2, Meruem P2)

task_await reports a finished background process as status: "completed" regardless of exit code (src/node/services/tools/task_await.ts:523-536 returns status: "completed" plus exitCode), so completedCount > 0 and summaryTone = "success". Before this PR the row said 1 task completed, which claims nothing about the goal. Now it claims the goal was reached.

Verified by render, twice independently: a completed result { taskId: "bash:deploy-a1b2", title: "Deploy", exitCode: 1 } with intent "deploying the service to staging" produces 1 task completed · bash · Deploying the service to staging with the green CircleCheck. The expanded row already prints exit {exitCode} at line 1592, so collapsed and expanded now disagree about whether the work succeeded, and the collapsed one is what gets read. The field that contradicts the claim is on the object this code already reads.

When firstResult.exitCode is a nonzero number, either append exit N and drop the success tone, or suppress the intent.

🤖

const taskId = getBashSpawnTaskId(msg.result);
const spawnInfo = taskId ? getBashSpawnInfoFromArgs(msg.args) : null;
if (taskId && spawnInfo) {
bashSpawnByTaskId.set(taskId, spawnInfo);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 [CRF-8] bash: taskIds are only unique among live processes, so this last-wins index attaches a newer spawn's model_intent to an older completed await row. (Komugi P2, Mafuuu P2, Nami P2, Knov P3, Razor P3)

The key is bash:<processId> and processId is fully deterministic from display_name (resolveBashDisplayName, no entropy). Uniqueness comes only from generateUniqueProcessId (backgroundProcessManager.ts:692), which appends " (1)" only while the base ID is still present in the in-memory this.processes map. That map is emptied for the workspace on compaction (agentSession.ts:3054) and on session dispose (agentSession.ts:625), and it is never rehydrated (backgroundProcessManager.ts:228-230). The transcript is not dropped at those points.

Forced, not inferred: two spawns with taskId: "bash:Running tests" (different scripts, different intents) plus the earlier task's own task_await result render the later intent on the earlier row, passing on the first run, so the misattribution is deterministic once the IDs collide. Razor proved the index half at unit level (bashSpawnByTaskId.size === 1, holding the second spawn).

The row's own result carries the right answer and loses to a cross-message guess, on exactly the question this feature exists to answer. The cheapest correct fix is to fail closed: on a duplicate key, delete the entry (and remember the id) so the lookup misses and the row falls back to its own firstResult.title, which task_await fills per result. Resolving the nearest spawn preceding the await by historySequence is the stronger version.

🤖

(bashSpawn
? sanitizeDisplayableModelIntent(bashSpawn.modelIntent, bashSpawn.script)
: undefined) ??
trimToNonEmptyString(firstResult.title) ??

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 [CRF-9] The title fallback has no command-echo guard, so the row prints the command that sanitizeDisplayableModelIntent just rejected. (Hisoka P2, Bisky P3, Pariston P3, Luffy P3)

sanitizeDisplayableModelIntent exists to stop the row from restating the command. When it returns undefined, the very next fallback is firstResult.title, and for a bash task that title is proc.displayName ?? proc.id (task_await.ts:527), which is resolveBashDisplayName(script, display_name). So the guard is bypassed by the path that trips it.

Verified by render: with script, display_name and model_intent all ./scripts/wait_pr_ready.sh 27330, the collapsed row reads bash · ./scripts/wait_pr_ready.sh 27330. And because sanitizeBashDisplayName rewrites < > : " / \ | ? * to _, an echoing display name renders as a mangled pseudo-command: bun run make lint 2>&1 | tail -20 comes out as bun run make lint 2_&1 _ tail -20. It looks like a command and is not one.

One correction to the reviewer evidence: display_name is required in the bash schema (toolDefinitions.ts:1537), so the "model omitted display_name" variant is not reachable through a schema-valid call. The echo variant is, and bashCollapsedSummary.ts:99 already knows models do this: getIntentOnlyFallback compares display_name against the command before using it. Run the title fallback through the same normalizeForComparison check and let the detail be bash alone when it matches.

Test 2 dodged this by giving its fixture a friendly display_name ("Repo State"): it proved the intent guard, then landed on a title that happened to be clean.

🤖

summaryTone = "waiting";
} else if (completedCount > 0) {
summaryTitle = `${formatTasks(completedCount)} completed`;
summaryDetail = singleTaskDetail;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 [CRF-10] A run of two or more adjacent task_await rows renders a different summary producer that has no detail at all, so the enrichment never appears in that shape. (Melody P2, Kite P3)

computeTaskAwaitPollGroupInfos groups every run of 2+ adjacent task_await tool messages (transcriptRenderProjection.ts:181-200), and ChatPane.tsx:1592-1594 returns null for non-head members and renders the member row only when expanded, so while collapsed no individual task_await summary renders, including the head's. The completed poll is normally the last element of such a run, so in exactly the case the PR targets the user sees Checked task status 3 times.

Orchestrator verified the mechanism: for a cleanly settled group defaultExpanded = needsAttention is false, summarizeOperationalBundle returns Checked task status N times with details: "", and the head branch at ChatPane.tsx:1584-1611 renders only OperationalBundleMessage. Frequency is not measured; the mechanism is. Your own comment at line 1409 says task_await "commonly appears several times during one turn".

Either give the task_await branch of summarizeOperationalBundle the same single-completed-task suffix (extract the derivation so one helper owns the string), or exclude the completed poll from the group. If neither, the PR description should stop promising the collapsed transcript row shows what finished, because for bundled polls it does not.

🤖

firstResult.handleKind === "workspace_turn"
? "workspace"
: taskReportLinking?.spawnAgentTypeByTaskId.get(completedTaskId);
const description =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 [CRF-11] The collapsed row builds a weaker title chain than the expanded row, so the exact case this PR targets can still read 1 task completed. (Pariston P2, Zoro P3, Chopper P3, Robin P3, Meruem P3, Melody P3, Kite P3)

TaskAwaitResult resolves a completed row's title as result.title ?? spawnTitle ?? workspaceTitle, where workspaceTitle comes from getTaskToolWorkspaceTitle(findWorkspaceForTaskTarget(workspaceMetadata, taskId, resultWorkspaceId)) (lines 1505-1514). The new collapsed chain is bashIntent ?? result.title ?? spawnTitle and drops the metadata leg.

Three reviewers rendered it independently:

  • completed task-1, no result.title, no linking, metadata titled "Pagination exploration": collapsed 1 task completed, expanded Pagination exploration.
  • completed wst_abc with workspaceId: "ws-9", metadata titled "Shipping the pagination fixes": collapsed 1 task completed · workspace, expanded shows the title.

title is optional on the workspace-turn result because WorkspaceTurnTaskHandleRecord.title is optional (taskHandleStore.ts:45), and both spawnTitleByTaskId and spawnAgentTypeByTaskId are populated from the same task message, so when it is out of the window the row loses kind and description together. Knov measured that window: computeTaskReportLinking runs on deferredMessages, capped at MAX_DISPLAYED_MESSAGES = 64, and tool is not in ALWAYS_KEEP_MESSAGE_TYPES.

workspaceMetadata is already in scope at line 1285. The fix worth making is one resolveAwaitResultTitle(result, linking, workspaceMetadata) consumed by both the collapsed detail and fallbackTitle at 1514, so a new rung can only be added in one place.

🤖

: taskReportLinking?.spawnAgentTypeByTaskId.get(completedTaskId);
const description =
(bashSpawn
? sanitizeDisplayableModelIntent(bashSpawn.modelIntent, bashSpawn.script)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note [CRF-29] The bash detail ignores the user's "Collapsed bash summaries" setting. (Nami)

A user who set that select to command (GeneralSection.tsx:589, key bashCollapsedSummaryMode, default intent-command) is telling Mux they want the raw command, not model prose. This row calls sanitizeDisplayableModelIntent directly and never consults useBashCollapsedSummaryMode(), so they get prose. BashTaskSpawnInfo.script is already captured, so honoring the mode is a lookup plus a branch.

Held at Note deliberately: the setting's own copy scopes itself to "collapsed bash tools", and a task_await row is not one. A decision, not necessarily a change.

🤖

: undefined) ??
trimToNonEmptyString(firstResult.title) ??
trimToNonEmptyString(taskReportLinking?.spawnTitleByTaskId.get(completedTaskId));
const detail = [kind, description].filter((part): part is string => part != null).join(" · ");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note [CRF-31] Model-controlled text is spliced into a ·-delimited status string, so an intent can fabricate row structure. (Kurapika)

detail = [kind, description].join(" · ") and the render at line 1470 prepends another ·. Nothing rejects · inside description, so an intent such as Reviewing docs · 0 failed · approved renders as 1 task completed · bash · Reviewing docs · 0 failed · approved, claiming a status the await never reported.

No security finding: the sink is a React text child inside a truncating span, no dangerouslySetInnerHTML, no markdown, no URL construction. The blast radius is one muted line the user can expand, and the class already exists wherever bash intents render, so it is not this PR's invention. Recorded because this is the only place in the diff where model text is spliced into a delimiter-structured status string.

🤖

(bashSpawn
? sanitizeDisplayableModelIntent(bashSpawn.modelIntent, bashSpawn.script)
: undefined) ??
trimToNonEmptyString(firstResult.title) ??

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note [CRF-32] Capitalization of the detail slot depends on which fallback wins. (Leorio)

The intent path runs through capitalize inside sanitizeModelIntent, so it renders bash · Watching PR 27330 until it is ready. The two title fallbacks render raw, so a task titled run integration tests renders bash · run integration tests in the same slot. getIntentOnlyFallback capitalizes its display_name fallback for exactly this reason, but task titles render raw everywhere else in this file, so both conventions have a claim.

Cosmetic either way. If you want the slot self-consistent, wrap the fallbacks in capitalize.

🤖

status: "completed",
args: { task_ids: ["task-1", "task-2"] },
result: {
results: [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note [CRF-5] keeps multi-task completion summaries count-only is green on base code. (Netero, Bisky)

Verified by red-green: with the three production files reverted to base, the file ran 14 pass / 4 fail. The four failures are the four other new tests; this one passed.

It is a boundary guard for the deliberate "multi stays count-only" decision, not a proof of this diff, so it carries no red-green value today. Worth knowing when reading the "5 new tests ... red-green verified" claim in the PR body: four were verified red, one cannot be, and a fifth (CRF-16) cannot fail for the behavior it names.

🤖

@@ -87,18 +146,35 @@ export function computeTaskReportLinking(messages: DisplayedMessage[]): TaskRepo
// First pass: record which taskIds have a visible `task` tool call (and capture spawn titles).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit [CRF-4] The first-pass comment still describes a task-only loop. (Netero, Gon)

// First pass: record which taskIds have a visible \task` tool call (and capture spawn titles).sits directly above a loop that now also branches onmsg.toolName === "bash"` and captures spawn agent types.

The comment was accurate before this diff. Extend it to name the bash-spawn indexing.

🤖

ibetitsmike added a commit that referenced this pull request Aug 5, 2026
…aries (#3796)

## Summary

Multi-task `task_await` completion rows previously said only `N tasks
completed`, hiding what actually finished. The collapsed row now lists
each completed task's kind and spawn intent/title: a single detail stays
inline in the header (unchanged from #3793), while multiple details
render as stacked truncated lines below it.

## Background

#3793 added the `kind · intent/title` detail for single-task awaits but
deliberately kept multi-task summaries count-only. In practice that
forces expanding every multi-task await row to see which background work
landed. A comma-joined inline list was considered first but truncates
quickly and reads poorly because `,` and `·` separators compete, so
multiple details stack as one line per task.

## Implementation

- The single-task detail computation now runs for every completed
result, reusing the same kind derivation (`bash:`/`wfr_`/`wst_` handles
or spawn `agentId`) and `sanitizeDisplayableModelIntent` fallback chain
per task.
- With exactly one detail it remains the inline header suffix; with
more, a stacked list renders below the header, gated to collapsed +
success state since the expanded view already shows per-task result
rows.
- Tasks with no resolvable kind/intent/title are skipped rather than
rendering empty lines.

## Validation

- Storybook screenshots (built Storybook, dark theme) comparing inline
comma-joined vs stacked layouts for 1/2/3-task completions; stacked
layout verified to align with header text and truncate per line.

---

_Generated with `mux` • Model: `anthropic:claude-fable-5` • Thinking:
`xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
mux-bot Bot added a commit that referenced this pull request Aug 5, 2026
The module reads every field out of persisted tool args/results typed as
`unknown`, and #3789/#3793 grew that from four to seven copies of the same
"is a string, is not blank, use the trimmed form" check. Collapse those seven
into a single coerceNonBlankString helper.

The two task_await result reads are deliberately left alone: they validate a
field is non-blank but then store the raw, untrimmed value, so routing them
through the helper would change what gets persisted.
mux-bot Bot added a commit that referenced this pull request Aug 6, 2026
The module reads every field out of persisted tool args/results typed as
`unknown`, and #3789/#3793 grew that from four to seven copies of the same
"is a string, is not blank, use the trimmed form" check. Collapse those seven
into a single coerceNonBlankString helper.

The two task_await result reads are deliberately left alone: they validate a
field is non-blank but then store the raw, untrimmed value, so routing them
through the helper would change what gets persisted.
mux-bot Bot added a commit that referenced this pull request Aug 6, 2026
The module reads every field out of persisted tool args/results typed as
`unknown`, and #3789/#3793 grew that from four to seven copies of the same
"is a string, is not blank, use the trimmed form" check. Collapse those seven
into a single coerceNonBlankString helper.

The two task_await result reads are deliberately left alone: they validate a
field is non-blank but then store the raw, untrimmed value, so routing them
through the helper would change what gets persisted.
mux-bot Bot added a commit that referenced this pull request Aug 7, 2026
The module reads every field out of persisted tool args/results typed as
`unknown`, and #3789/#3793 grew that from four to seven copies of the same
"is a string, is not blank, use the trimmed form" check. Collapse those seven
into a single coerceNonBlankString helper.

The two task_await result reads are deliberately left alone: they validate a
field is non-blank but then store the raw, untrimmed value, so routing them
through the helper would change what gets persisted.
mux-bot Bot added a commit that referenced this pull request Aug 7, 2026
The module reads every field out of persisted tool args/results typed as
`unknown`, and #3789/#3793 grew that from four to seven copies of the same
"is a string, is not blank, use the trimmed form" check. Collapse those seven
into a single coerceNonBlankString helper.

The two task_await result reads are deliberately left alone: they validate a
field is non-blank but then store the raw, untrimmed value, so routing them
through the helper would change what gets persisted.
mux-bot Bot added a commit that referenced this pull request Aug 7, 2026
The module reads every field out of persisted tool args/results typed as
`unknown`, and #3789/#3793 grew that from four to seven copies of the same
"is a string, is not blank, use the trimmed form" check. Collapse those seven
into a single coerceNonBlankString helper.

The two task_await result reads are deliberately left alone: they validate a
field is non-blank but then store the raw, untrimmed value, so routing them
through the helper would change what gets persisted.
mux-bot Bot added a commit that referenced this pull request Aug 8, 2026
The module reads every field out of persisted tool args/results typed as
`unknown`, and #3789/#3793 grew that from four to seven copies of the same
"is a string, is not blank, use the trimmed form" check. Collapse those seven
into a single coerceNonBlankString helper.

The two task_await result reads are deliberately left alone: they validate a
field is non-blank but then store the raw, untrimmed value, so routing them
through the helper would change what gets persisted.
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.

1 participant