Skip to content

Shell-mode (!cmd) output is not displayed while the agent is Working #3639

Description

@probepark

Summary

Shell-mode commands entered while the agent is Working (streaming) are not shown in the transcript. Typing !pwd mid-turn runs the command, but the execution block flashes and then disappears — no command header, no output, no exit status. The same !pwd typed while idle renders normally.

Repro

  1. Start an interactive session and send any prompt so the agent is Working.
  2. While the spinner is active, type !pwd and press Enter.
  3. Observe: the shell block vanishes as soon as the command finishes; output is never shown for the rest of the turn.
  4. Type !pwd while idle → renders correctly.

Differential: $ (python/eval) commands typed mid-turn stay visible. Only ! (bash) commands disappear.

Root cause (evidence)

CommandController.handleBashCommand renders into two different parents depending on streaming state — packages/coding-agent/src/modes/controllers/command-controller.ts:1144:

const isDeferred = this.ctx.session.isStreaming;
this.ctx.bashComponent = new BashExecutionComponent(command, this.ctx.ui, excludeFromContext);

if (isDeferred) {
    this.ctx.pendingMessagesContainer.addChild(this.ctx.bashComponent);  // :1149
    this.ctx.pendingBashComponents.push(this.ctx.bashComponent);         // :1150
} else {
    this.ctx.chatContainer.addChild(this.ctx.bashComponent);             // :1152
}

Three defects on the deferred branch:

1. Completion detaches the component without ever re-parenting itcommand-controller.ts:1180:

const bashComponent = this.ctx.bashComponent;
if (isDeferred && bashComponent && this.ctx.pendingBashComponents.includes(bashComponent)) {
    this.ctx.pendingMessagesContainer.detachChild(bashComponent);   // :1182 — removed from screen
}
this.ctx.bashComponent = undefined;

The component is detached from pendingMessagesContainer and never added to chatContainer. It stays in pendingBashComponents (a plain array, not a rendered container), so it has no parent and is not drawn. For a fast command like pwd this happens within milliseconds of submit, which is why nothing is visible.

handlePythonCommand (command-controller.ts:1189) has no equivalent detach block — which matches the observed asymmetry between ! and $.

2. The only flush runs on the next non-streaming submitpackages/coding-agent/src/modes/controllers/input-controller.ts:949:

if (this.ctx.session.isStreaming) {
    ...
    this.ctx.updatePendingMessagesDisplay();
    return;                                        // :944 — flush is never reached while busy
}

// Normal message submission
// First, move any pending bash components to chat
this.ctx.flushPendingBashComponents();             // :949

flushPendingBashComponents() (packages/coding-agent/src/modes/utils/ui-helpers.ts:1241) is the only place that moves the parked components into the chat transcript, and it is only reachable from the non-streaming submit path. So the output stays invisible until the user sends another prompt after the turn ends.

3. Any pending-bar refresh during the turn disposes the live component. updatePendingMessagesDisplay() starts with a clear()ui-helpers.ts:981:

updatePendingMessagesDisplay(): void {
    this.ctx.pendingMessagesContainer.clear();     // :982

and Container.clear() disposes children — packages/tui/src/tui.ts:504:

clear(): void {
    for (const child of this.children) child.dispose?.();

updatePendingMessagesDisplay() has ~25 callsites and fires on ordinary mid-turn events, e.g. packages/coding-agent/src/modes/controllers/event-controller.ts:406 and :441 (queued/dequeued user messages). So a still-streaming ! command can be torn down mid-flight, stopping its loader and dropping buffered output. flushPendingBashComponents explicitly documents that these components must be detached rather than disposed (ui-helpers.ts:1242-1244), but clear() violates that.

Additionally, renderInitialMessages() (ui-helpers.ts:903-904) clears both pendingMessagesContainer and pendingBashComponents, permanently dropping anything parked there if a transcript rebuild happens before a flush.

Expected

A !command submitted while the agent is Working should render its header, streaming output, and exit status continuously, and should land in the chat transcript when it completes — same as the idle path.

Suggested fix

  • On completion of a deferred bash command, move the component into the chat transcript (detachChild + addChatChild) and remove it from pendingBashComponents, instead of just detaching it (command-controller.ts:1180-1183).
  • Keep in-flight execution components out of the container that updatePendingMessagesDisplay() clears, or have that method rebuild only the queued-message chips and re-attach live execution components (ui-helpers.ts:981-982).
  • Add regression coverage for ! and $ submitted mid-turn asserting the component is parented and visible during streaming and after completion.

Environment

  • Repo: gajae-code @ b6198e748
  • Affected: interactive TUI mode, bash (!, !!) shell input

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions