You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(tui): Fix duplicate tool execution messages in ACP (#88)
When agent text streams during an ACP tool call execution, the
incomplete ExecCell was being flushed to history. When the tool call
completed, a new ExecCell was created, resulting in duplicate entries
showing both "Running" and "Ran" states for the same command.
This fix tracks incomplete ExecCells that get flushed during streaming
in a pending map keyed by call_id. When ExecCommandEnd arrives, the
pending cell is retrieved and completed instead of creating a new one.
Changes:
- Add `pending_exec_cells` field to ChatWidget to track incomplete cells
- Modify `flush_active_cell` to save incomplete ExecCells to pending map
- Modify `handle_exec_end_now` to check pending map before creating new
- Add `pending_call_ids` method to ExecCell for tracking
- Add cleanup of pending cells in `on_task_complete`
- Add E2E test with interleaved text/tool call events to verify fix
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nori <contact@tilework.tech>
Copy file name to clipboardExpand all lines: codex-rs/tui/docs.md
+17Lines changed: 17 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -130,6 +130,23 @@ Most event types (exec begin/end, MCP calls, elicitation) are queued during acti
130
130
- The `InterruptManager` still contains `ExecApproval` and `ApplyPatchApproval` variants for completeness, but these methods are marked `#[allow(dead_code)]`
131
131
-`on_task_complete()` calls `flush_interrupt_queue()` for any remaining queued items
132
132
133
+
**Pending ExecCell Tracking:**
134
+
135
+
The `PendingExecCellTracker` (`chatwidget/pending_exec_cells.rs`) prevents duplicate ACP tool call messages in the chat history. The problem it solves:
136
+
137
+
1. Agent makes a tool call (e.g., `shell`) which creates an ExecCell in `active_cell`
138
+
2. Agent streams text *during* the tool call execution
139
+
3. Streaming text causes `flush_active_cell()`, which would normally push the incomplete ExecCell to history and clear `active_cell`
140
+
4. When `ExecCommandEnd` arrives, `handle_exec_end_now()` would create a *new* ExecCell since `active_cell` is empty
141
+
5. Result: duplicate entries for the same tool call
142
+
143
+
The tracker intercepts this by:
144
+
-`save_pending()`: Called during flush if the ExecCell has pending (incomplete) call_ids - saves the cell keyed by call_id instead of pushing to history
145
+
-`retrieve()`: Called in `handle_exec_end_now()` - retrieves and removes the saved cell, restoring it to `active_cell` for completion
146
+
-`drain_failed()`: Called in `on_task_complete()` - marks any uncompleted pending cells as failed and returns them for insertion into history
147
+
148
+
This follows the same encapsulation pattern as `InterruptManager`: self-contained state in its own module file with typed public methods instead of exposing raw data structures.
149
+
133
150
**ACP File Tracing:**
134
151
135
152
- The TUI calls `codex_acp::init_file_tracing()` at startup (`tui/src/lib.rs`) to write `.codex-acp.log` in the current directory. Every mock agent logs `ACP agent spawned (pid: ...)` there, which makes the agent-switching tests in `tui-pty-e2e` deterministic and ensures developers can inspect agent subprocess lifecycles during debugging.
0 commit comments