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
- Start an interactive session and send any prompt so the agent is Working.
- While the spinner is active, type
!pwd and press Enter.
- Observe: the shell block vanishes as soon as the command finishes; output is never shown for the rest of the turn.
- 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 it — command-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 submit — packages/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
Summary
Shell-mode commands entered while the agent is Working (streaming) are not shown in the transcript. Typing
!pwdmid-turn runs the command, but the execution block flashes and then disappears — no command header, no output, no exit status. The same!pwdtyped while idle renders normally.Repro
!pwdand press Enter.!pwdwhile idle → renders correctly.Differential:
$(python/eval) commands typed mid-turn stay visible. Only!(bash) commands disappear.Root cause (evidence)
CommandController.handleBashCommandrenders into two different parents depending on streaming state —packages/coding-agent/src/modes/controllers/command-controller.ts:1144:Three defects on the deferred branch:
1. Completion detaches the component without ever re-parenting it —
command-controller.ts:1180:The component is detached from
pendingMessagesContainerand never added tochatContainer. It stays inpendingBashComponents(a plain array, not a rendered container), so it has no parent and is not drawn. For a fast command likepwdthis 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 submit —
packages/coding-agent/src/modes/controllers/input-controller.ts: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 aclear()—ui-helpers.ts:981:and
Container.clear()disposes children —packages/tui/src/tui.ts:504:updatePendingMessagesDisplay()has ~25 callsites and fires on ordinary mid-turn events, e.g.packages/coding-agent/src/modes/controllers/event-controller.ts:406and:441(queued/dequeued user messages). So a still-streaming!command can be torn down mid-flight, stopping its loader and dropping buffered output.flushPendingBashComponentsexplicitly documents that these components must be detached rather than disposed (ui-helpers.ts:1242-1244), butclear()violates that.Additionally,
renderInitialMessages()(ui-helpers.ts:903-904) clears bothpendingMessagesContainerandpendingBashComponents, permanently dropping anything parked there if a transcript rebuild happens before a flush.Expected
A
!commandsubmitted 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
detachChild+addChatChild) and remove it frompendingBashComponents, instead of just detaching it (command-controller.ts:1180-1183).updatePendingMessagesDisplay()clears, or have that method rebuild only the queued-message chips and re-attach live execution components (ui-helpers.ts:981-982).!and$submitted mid-turn asserting the component is parented and visible during streaming and after completion.Environment
gajae-code@b6198e748!,!!) shell input