Fixed the interrupt mechanism to show "[Interrupted by user]" message when ESC is pressed: - Removed duplicate UI cleanup from ESC key handler that interfered with event processing - Added centralized interrupted event emission in exception handler when abort signal is detected - Removed duplicate event emissions from API call methods to prevent multiple messages - Added abort signal support to preflight reasoning check for proper cancellation - Simplified abort detection to only check signal state, not error messages
3.6 KiB
Analysis: Interrupted Message Not Showing in TUI
Problem Summary
When pressing ESC to interrupt the agent while it's working, the "interrupted" message is not appearing in the TUI interface.
Research Findings
Interrupt Handling Flow
-
ESC Key Detection (TuiRenderer line 110)
- ESC key is detected as
\x1bin theonGlobalKeyPresshandler - Only triggers when
this.currentLoadingAnimationis active (agent is processing)
- ESC key is detected as
-
Immediate UI Cleanup (TuiRenderer lines 112-128)
- Calls
this.onInterruptCallback()(which callsagent.interrupt()) - Stops loading animation and clears status container
- Re-enables text editor submission
- Requests UI render
- Calls
-
Agent Interruption (Agent.ts line 615-617)
agent.interrupt()callsthis.abortController?.abort()- This triggers AbortSignal in ongoing API calls
-
Interrupted Event Generation (Agent.ts multiple locations)
- When signal is aborted, code checks
signal?.aborted - Emits
{ type: "interrupted" }event viaeventReceiver?.on() - Throws
new Error("Interrupted")to exit processing
- When signal is aborted, code checks
-
Message Display (TuiRenderer line 272-283)
- Handles
"interrupted"event - Adds red "[Interrupted by user]" message to chat container
- Requests render
- Handles
Root Cause Analysis
The issue appears to be a race condition with duplicate cleanup:
-
When ESC is pressed, the key handler immediately (lines 115-120):
- Stops the loading animation
- Clears the status container
- Sets
currentLoadingAnimation = null
-
Later, when the "interrupted" event arrives (lines 273-277), it tries to:
- Stop the loading animation again (but it's already null)
- Clear the status container again (already cleared)
-
The comment on line 123 says "Don't show message here - the interrupted event will handle it", but the event handler at line 280 does add the message to the chat container.
The Actual Problem
Looking closely at the code flow:
- ESC handler clears animation and calls
agent.interrupt()(synchronous) - Agent aborts the controller (synchronous)
- API call code detects abort and emits "interrupted" event (asynchronous)
- TUI renderer receives "interrupted" event and adds message (asynchronous)
The issue is likely that:
- The interrupted event IS being emitted and handled
- The message IS being added to the chat container
- But the UI render might not be properly triggered or the differential rendering isn't detecting the change
Additional Issues Found
-
Duplicate Animation Cleanup: The loading animation is stopped twice - once in the ESC handler and once in the interrupted event handler. This is redundant but shouldn't cause the missing message.
-
Render Request Timing: The ESC handler requests a render immediately after clearing the UI, then the interrupted event handler adds the message but doesn't explicitly request another render (it relies on the Container's automatic render request).
-
Container Change Detection: Recent commit
192d8d2fixed container change detection issues. The interrupted message addition might not be triggering proper change detection.
Solution Approach
The fix needs to ensure the interrupted message is properly displayed. Options:
- Add explicit render request after adding the interrupted message
- Remove duplicate cleanup in the ESC handler and let the event handler do all the work
- Ensure proper change detection when adding the message to the chat container
The cleanest solution is likely option 2 - let the interrupted event handler do all the UI updates to avoid race conditions and ensure proper sequencing.