feat: label process_output rows with the process command - #28300
Conversation
The process_output tool result carried only output, exit code, and running state, so chat transcripts rendered every poll as a generic "Process output" row with no indication of which process produced it. Return the process command from the agent's process output endpoint and propagate it through the chatd tool result. The UI now renders "Checking <command>" while the process runs and "Checked <command>" once it exits, mirroring the execute tool's verb-first label grammar. Results without a command (older agents, historical transcripts) fall back to the previous generic label. Also align the process output body with the execute tool: tinted rounded panel with a scrollable max-h-64 viewport instead of the bordered box, and drop the bespoke secondary expand/collapse button in favor of the standard header toggle plus scrolling.
Mirroring the execute tool, process_output now takes an optional model_intent describing why the agent is checking the process. The chat header composes it with the command (e.g. "Waiting for the dev server to be ready on npm start"), reusing the execute intent sanitizer to strip redundant command references. Calls without an intent keep the previous Checking/Checked labels.
go generate emits third-party import grouping; make gen applies gci with the repo's coder/cdr.dev prefix group on top. Run the formatter so the gen freshness check passes.
execute and process_output rows were visually indistinguishable: same
terminal icon, same body chrome, and a static layers icon that failed
to communicate that a process was still running in the background.
- process_output now leads with the activity (pulse) icon so act and
observe rows read differently at scan distance.
- The layers icon is replaced by a persistent status chip on the
backgrounded execute row: pulsing dot + ticking elapsed time while
the process is alive, exit state once an observation arrives. State
is derived in messageParsing by correlating process_output results
and process_signal calls with the execute row's background_process_id.
- The misleading spawn-duration suffix ("for 0ms") is suppressed on
backgrounded execute rows; the chip carries the live state instead.
- process_output now shows a completion chip on clean exits (exit 0)
instead of rendering no right-edge state at all.
- process_output no longer renders an "exit 0" badge on clean exits; the Checked verb carries success and only failures earn the red badge, matching the execute tool's quiet-success convention. Failed checks now read "Failed <command>". - The background process chip collapses to a bare check icon with a tooltip on clean exit instead of spelling out "exit 0", so the execute row's copy button stays aligned with the process_output row's copy button.
The chip annotation only read process_output results, so a process checked via process_list (or never polled) stayed "running" forever. Also populate startedAtMs from the execute message timestamp so the running chip shows elapsed time.
The execute row's custom grid added gap-x-2 between the header button and the actions column, insetting its copy button 8px from the right edge shared by every other tool row. Drop the gap; the header button already truncates.
Following dogfooding feedback: the chip answered a question readers weren't asking. Live state was derivable-but-stale the moment it rendered (a transcript records what was observed, not what is true), success states were noise, and the ticking pill mostly advertised that the design was trying too hard. The transcript shows what the agent saw, when it saw it; cross-process liveness dashboards are a different feature. Removes BackgroundProcessChip, the backgroundProcess annotation pass in messageParsing, and their tests and stories. Keeps the durability fixes that stood on their own: the activity icon for process_output, suppressing the misleading spawn-duration on backgrounded execute rows, and quiet-exit labeling on process_output.
Backgrounded execute calls with a model intent now read "<intent> in
the background using <command>" so the launch mode is visible in the
row; the intent description tells the model the framing exists so it
does not write "background" itself.
process_output stops appending the command after the intent
("<intent> on <command>"): the intent is written to be
self-sufficient, and the appended command rendered as a stutter
against the execute row's "using <command>" summary above it. Rows
without an intent keep the command-based Checking/Checked/Failed
labels.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d344b1cf72
ℹ️ 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".
Two review fixes: - process_output results now serialize running:true when the process outlived the poll's wait, instead of only embedding it in the note string. The chat UI reads the flag so a completed poll over a live process renders "Checking <command>" (and keeps its running spinner) instead of "Checked <command>". - Backgrounded execute calls without a model intent now render "Started <command> in the background" instead of "Ran <command>", restoring a background-specific label for the no-intent case. - Update the execute schema test for the reworded model_intent description from the background framing change.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e87ab3c8b5
ℹ️ 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".
A process_output poll that timed out with running:true kept the row labeled "Checking <command>" and suppressed the killed-by-signal icon even when the transcript recorded a subsequent successful process_signal for the same process. The snapshot was true at poll time; the kill is newer information and wins.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 29b731575b
ℹ️ 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".
The ProcessOutputRunningThenKilled story queried the indicator by lucide's private CSS class, violating FE10. Wrap the icon in an aria-labeled span (mirroring the execute tool's existing pattern) and query it by role and accessible name instead.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 356c8b6032
ℹ️ 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".
background_process_id is set on both intentional run_in_background launches and foreground commands that exceeded their timeout (so the caller can re-attach), and the UI derived "backgrounded" from its presence alone. A timed-out foreground command was therefore labeled "Started ... in the background" and lost its meaningful wall_duration_ms suffix. ExecuteResult now carries an explicit backgrounded flag set only on the intentional path, and the frontend derives the background label and duration suppression from it.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a07711d0a
ℹ️ 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".
Transcripts recorded before the backgrounded result flag existed carry the launch intent in the persisted call args (run_in_background: true). Falling back to the args when the result predates the flag keeps legacy rows labeled as background launches while foreground timeouts (backgrounded omitted, not false) still render as failures with their duration.
|
@codex review |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 221d924e08
ℹ️ 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".
The execute tool promotes "cmd &" to background mode and strips the ampersand locally, but the persisted tool-call args keep the original command with no run_in_background. Those legacy rows have neither the backgrounded flag nor the args flag, so they lost the background label and gained a meaningless spawn-duration suffix. Detect the trailing-ampersand form (excluding && and |&) as another legacy fallback. Also drop a story comment that restated its assertion.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae3fa9738c
ℹ️ 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".
SIGTERM is catchable: the agent delivers it and returns without waiting for exit, so a successful terminate must not flip a poll row's recorded running state to "Checked". Only an uncatchable kill overrides the snapshot. Also drops story comments that restated their assertions.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3672b2cf62
ℹ️ 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".
run_in_background in the persisted args records intent, not outcome: a failed StartProcess returns an error result with neither flag nor process ID, and the legacy fallback alone labeled it "Started ... in the background". Gate the args fallback on a nonempty background_process_id so only launches that produced a process count.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 919e7ae769
ℹ️ 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".
Tool output viewports were plain scrollable divs with no tab stop, so
keyboard-only users in browsers that do not auto-focus overflow
containers could not reach content below the fold. Add
viewportTabIndex={0} to every tool ScrollArea so the regions accept
focus and scroll by keyboard. The shared ScrollArea already forwards
viewportTabIndex; a positive default there would break the one
consumer that opts out with -1, so call sites opt in explicitly.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fa1ec43ce4
ℹ️ 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".
site/AGENTS.md requires every tabIndex={0} element to carry a
semantic role; the scroll-area sweep created fourteen anonymous
generic focus stops. ScrollArea now accepts viewportAriaLabel and
renders role="region" with it on the viewport when set, and each
tool output region supplies an accessible name (process output,
command output, diffs by path, file contents by name, subagent
prompt/response/report, advisor, models, summary, build log).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e3640605c1
ℹ️ 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".
Twelve stories had accreted across review rounds, several asserting the same label in different fixtures. Kept the four that pin distinct mechanisms: quiet clean exit, intent as label, running flag from a timed-out poll, and the kill-override. Dropped duplicates whose behavior is already covered by the sanitizer unit tests or by another story's play function, and merged the two signal stories into one (the terminate non-override is the default path the running-flag story already proves).

Every background-process poll in an agent chat transcript rendered as a generic "Process output" row. The row carried no indication of which process it belonged to, so following an agent's work meant expanding each poll and pattern-matching the output against an
executerow above.The agent's process output endpoint already loads the process's info; it just never serialized the command. This PR returns
commandfromGET /api/v0/processes/{id}/output, propagates it through the chatdprocess_outputtool result, and renders verb-first labels mirroring theexecutetool: "Checkingnpm start" while the process runs, "Checkednpm start" once it exits. Results without a command (older agents, historical transcripts) fall back to the previous generic label, and the field rides into the model's context too, so polled results are self-describing when several background processes are in flight.Also aligns the output body with the
executetool's styling: tinted rounded panel with a scrollablemax-h-64viewport, replacing the bordered box and the bespoke secondary expand/collapse button. Collapse/expand is now only the standard header toggle; long output scrolls like every other tool.Like
execute,process_outputnow accepts an optionalmodel_intentso the agent can say why it is checking: "Waiting for the dev server to be ready onnpm start". The execute intent sanitizer is reused to strip redundant command references; calls without an intent keep the Checking/Checked labels.commandis additive andomitempty: new coderd against old agent sees the field absent and falls back; old coderd against new agent ignores it.Implementation plan (generated during development)
Problem
process_outputrenders as a collapsible row labeled "Process output" with the raw output. The reader cannot tell which background process a poll belongs to without expanding it, and the row used a bespoke two-level expansion (header toggle + 54px clamp with a secondary chevron button) unlike any other tool.Approach
Backend enrichment (chosen over frontend-only correlation in
messageParsing.ts, which fails when the originalexecuterow is absent:process_list-discovered processes, compacted transcripts, subagent chats):codersdk/workspacesdk: addCommandtoProcessOutputResponse.agent/agentproc/api.go: populate fromproc.info()inhandleProcessOutput(already loaded for the TOCTOU-safe read).coderd/x/chatd/chattool/execute.go: addCommandtoExecuteResult, copy from the response inProcessOutput().ProcessOutputRendererreadsresult.command;ProcessOutputToolrendersChecking <cmd>/Checked <cmd>, falling back to "Process output".ExecuteTool:rounded-xl bg-surface-secondary/60,max-h-64scroll viewport; remove the secondary toggle and the now-orphanedCOLLAPSED_OUTPUT_HEIGHT.model_intentarg onprocess_output(mirrorsexecute), composed into the label as<intent> on <command>after sanitizing redundant command references.Label decision: the tool's purpose is a status check ("check the result later"), so the label is verb-first with tense carrying the outcome, paralleling
execute's "Ran …" / "Failed to run …". When the model suppliesmodel_intent, the intent phrase replaces the Checking/Checked verb, asexecutedoes with "Ran". Duration suffix andstarted_at/exited_atwere considered and dropped (not consumed by the UI; keeps API surface minimal).🤖 Generated by Coder Agents