Skip to content

feat(runtime): bridge user-input events and API to external GUI clients#2133

Open
gaord wants to merge 5 commits into
Hmbown:mainfrom
gaord:fix/user-input-runtime-bridge
Open

feat(runtime): bridge user-input events and API to external GUI clients#2133
gaord wants to merge 5 commits into
Hmbown:mainfrom
gaord:fix/user-input-runtime-bridge

Conversation

@gaord
Copy link
Copy Markdown
Contributor

@gaord gaord commented May 25, 2026

Summary

The TUI engine already emits EngineEvent::UserInputRequired and the Engine handle exposes submit_user_input / cancel_user_input, but the runtime API layer (used by external GUI clients like VSCode extensions) was missing the plumbing to propagate these events or accept responses. This meant request_user_input tool calls would hang indefinitely in GUI mode with no dialog appearing.

Changes

1. approval.rs — Timeout protection for await_user_input

  • Added a 5-minute timeout (USER_INPUT_TIMEOUT) around the rx_user_input.recv() call
  • On timeout, the engine emits a Status event and returns a ToolError
  • Prevents a disconnected GUI from stalling the agent loop forever

2. runtime_threads.rs — Event forwarding + manager methods + interrupt fix

  • Event forwarding: Handle EngineEvent::UserInputRequired in the event loop, emitting a "user_input.required" SSE event with the input ID and full request payload (questions, options)
  • Manager methods: Added submit_user_input() and cancel_user_input() on RuntimeThreadManager that delegate to the Engine handle
  • Interrupt fix: interrupt_turn() now immediately clears active_turn and emits "turn.completed" so the thread accepts new messages after /interrupt — this prevents persistent 409 "Thread already has an active turn" errors

3. runtime_api.rs — REST endpoint for user input submission

  • Added POST /v1/user-input/{thread_id}/{input_id} endpoint
  • Accepts { "answers": [{ "id": "...", "label": "...", "value": "..." }] }
  • Delivers responses to the engine via RuntimeThreadManager::submit_user_input()

Flow

Tool calls request_user_input
  → approval.rs sends Event::UserInputRequired
  → runtime_threads.rs forwards as SSE "user_input.required"
  → GUI displays dialog with option buttons
  → GUI calls POST /v1/user-input/{thread_id}/{input_id}
  → runtime_api.rs delivers to engine via channel
  → approval.rs receives answer, returns to tool

Testing

  • cargo check -p codewhale-tui passes
  • cargo clippy -p codewhale-tui --all-targets passes (no new warnings)
  • Verified end-to-end with VSCode extension (CodeWhale VSCode) that user input dialogs appear and responses are correctly delivered

Related

  • Mirrors the existing approval flow (approval.required SSE + POST /v1/approvals/{id}) that already works for tool authorization dialogs

Add SSE event forwarding for UserInputRequired, REST endpoint for submitting user input responses, timeout protection for await_user_input, and fix interrupt_turn to clear active_turn immediately.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a 300-second timeout for user input requests and adds a new API endpoint for submitting user input. It also updates the runtime thread manager to handle user input events and modifies turn interruption logic. Feedback suggests improving error handling consistency by using standard error mapping for missing threads and removing redundant turn finalization code that could cause duplicate events and data loss.

Comment thread crates/tui/src/runtime_api.rs Outdated
Comment thread crates/tui/src/runtime_threads.rs Outdated
Comment thread crates/tui/src/runtime_threads.rs Outdated
Comment thread crates/tui/src/runtime_threads.rs Outdated
gaord added 4 commits May 25, 2026 21:44
The monitor_turn loop already handles full turn finalization when the
engine shuts down after cancellation, including saving turn status,
usage, error, emitting turn.completed, and clearing active_turn.

Having interrupt_turn also save turn status and emit turn.completed
causes duplicate SSE events and loses usage/error data that
monitor_turn would have captured from TurnComplete.

Keep only the active_turn cleanup so the 409 error is resolved while
monitor_turn remains the single source of truth for turn completion.
- Change 'not loaded' to 'not found' in submit_user_input and
  cancel_user_input so map_thread_err correctly maps to 404
- Use map_thread_err in submit_user_input API endpoint for
  consistent error response (404 for missing thread, 409 for
  conflict, etc.) instead of always returning 500
Clearing active_turn immediately breaks is_interrupt_requested detection
in monitor_turn, causing turn status to be Completed instead of Interrupted.

Let monitor_turn handle the cleanup after it detects the interrupt flag
and performs full finalization with correct status, usage, and error.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant