feat: pending input queue, steer (/btw), tool system refactoring, and stability fixes#58
Conversation
- Add PendingInputItem/PendingInputKind protocol types with serialization - Add TurnState module for turn-scoped pending input management - Introduce TurnKind (Regular/Review/ManualCompaction) with TurnMetadata.kind - Add Role::System and Message::system() for system/developer role messages - Wire turn/start to queue inputs when turn is busy (instead of rejecting) - Add TurnKind validation in turn/steer handler (reject non-Regular) - Add InputQueueUpdated and SteerAccepted server events - Implement /btw slash command in TUI for explicit steer - Wire PendingInputPreview widget with live server event data - Add budget-limit steering injection in query loop - Add unit and integration tests for all new functionality
- Add Btw to supports_inline_args() so /btw <text> is routed as slash command - When busy, show input in PendingInputPreview instead of adding to history - Fix InputQueueUpdated to send all pending texts, not just the current input - Broadcast InputQueueUpdated (empty) when turn starts consuming the queue
Introduce a new layered tool architecture inspired by codex-rs: - JsonSchema, ToolSpec, ToolHandlerKind, ToolRegistryPlan data models - ToolHandler trait with per-tool handlers in src/handlers/ - ToolRuntime with RwLock concurrency gating (replaces ToolOrchestrator) - ToolRegistryBuilder for plan-driven registration - 73 unit tests passing, -Dwarnings clean across workspace Migrate all 16 built-in tools (bash, read, write, glob, grep, apply_patch, plan, question, task, todowrite, webfetch, websearch, skill, lsp, invalid, shell_command) to the new handler pattern. Update consumers: devo-core/query.rs, devo-server, devo-cli to use ToolRuntime.
…tyle - Use ◆ PENDING badge (yellow bold) for queued steers - Use ◆ BLOCKED badge (red bold) for rejected steers - Use ◆ QUEUED badge (cyan bold) for follow-up messages - Show item count and truncated preview with ↳ prefix - Add separator line and Esc hint for pending items
- Use steering_queue (independent StdMutex) instead of core_session to avoid deadlocking with the running query() loop - Add queue drain logic at end of execute_turn() to auto-submit the next queued input as a new turn, chaining until queue is empty - Fix pending input staying in queue indefinitely after turn completes
… submit - Add btw to built_in_slash_commands() so it appears in the / popup menu - Fix queue.clear() that removed ALL pending items instead of just one - Add broadcast_updated_queue() helper to sync TUI preview after dequeue - Broadcast InputQueueUpdated after auto-submit turn starts
…interactive processes Implement unified exec system inspired by codex-rs: - unified_exec/buffer.rs: HeadTailBuffer (capped output, head+tail preservation) - unified_exec/process.rs: PTY process lifecycle (spawn, write stdin, terminate) - unified_exec/store.rs: ProcessStore (cross-turn process tracking, LRU eviction) - handlers/exec_command.rs: ExecCommandHandler + WriteStdinHandler - ToolPlanConfig.use_unified_exec default true Protocol: exec_command returns session_id for ongoing interaction, write_stdin writes to stdin or polls output. Process survives across model turns via Arc<ProcessStore> shared state.
…ngInputPreview - Each pending message is now a UserHistoryCell with a cyan QUEUED badge - When server consumes the queue, InputQueueUpdated/TurnStarted triggers unqueue_oldest_pending() which removes the QUEUED badge - Remove PendingInputPreview widget and all related bottom pane plumbing - Remove pending_steers/rejected_steers tracking from ChatWidget - Add queued_count and unqueue_oldest_pending() for FIFO unqueue logic - Fix server: maybe_generate_final_title call signature, remove stale first_assistant_reply variable, add first_user_input to RuntimeSession
- Remove unsafe impl Send/Sync (auto-derived via Mutex wrapper) - Fix ProcessStore::get() to use read lock (was write lock) - Delete dead code: runtime/ (12 files, ~1.5k lines), spec.rs - Fix Box::leak memory leak in websearch (use OnceLock) - Fix HeadTailBuffer::collect() to return String (UTF-8 safe) - Add PTY idle timeout (30min) and catch_unwind for reader thread - Suppress dead_code on legacy orchestrator module - Add ToolPlanConfig::validate() placeholder
… when dequeued - Pending input cells render in bottom pane (above composer) with ┃ prefix + QUEUED badge - When InputQueueUpdated/TurnStarted fires, oldest pending cell is popped from bottom pane and added as normal UserHistoryCell to the main transcript - Remove queued field from UserHistoryCell, new_queued_user_prompt function - BottomPane tracks pending_cell_texts: Vec<String> with push/pop/clear methods
Coverage added for: - json_schema: builder methods, serialization, roundtrip - tool_spec: defaults, serde, enum variants - errors: all ToolExecutionError variants, ToolDispatchError - events: serde, roundtrip for all 3 variants - invocation: ToolName/ToolCallId newtypes, ToolContent variants, FunctionToolOutput, ToolOutput trait, from_output conversion - registry: empty builder, is_read_only, spec lookup, dispatch success - router: permission checker allow/deny, runtime deny mutating, concurrent+sequential ordering, empty batch, single tool - buffer: UTF-8 multibyte boundary, multiple pushes, zero push, total tracking, exact fit, truncation preserves tail - store: len, default, concurrent allocate+get (10 concurrent tasks), unique ID allocation, prune_exited - registry_plan: plan builder, config defaults, schema validation, exec/write_stdin/invalid/bash schemas - exec_command handler: format_exec_response for exit/running/truncated Total: 136 tests (up from 65), 0 failed, -Dwarnings clean
…-dequeue - Remove duplicate unqueue_oldest_pending() call from TurnStarted handler - InputQueueUpdated is the canonical source of truth for queue state changes - TurnStarted + InputQueueUpdated both arriving would previously pop 2 cells
- Fix formatting in errors/events/json_schema/router/buffer/store
- Fix permission test in devo-core (use PermissionChecker instead of
new_without_permissions for deny test case)
- Fix doc warning: Vec<u8> interpreted as HTML tag in buffer.rs
CI: cargo check -Dwarnings ✅ | cargo test 377 passed ✅
cargo fmt --check ✅ | cargo doc -Dwarnings ✅
- Respond to client BEFORE broadcasting InputQueueUpdated to avoid timeout - Add dedicated double-Esc interrupt handler in host event loop (mirrors Ctrl-C) - Clear pending_interrupt_esc on TurnFinished/TurnStarted - Add pending_interrupt_esc to InteractiveLoopState
The broadcast_updated_queue call can contend on self.sessions and self.connections locks. By spawning it as a background task and responding to the client immediately, we eliminate the timeout risk even under heavy server load.
- Split stdout output into two channels: high-pri (RPC responses) and low-pri (event notifications). The writer task always drains the high-priority channel before touching the low-priority one. - ServerRuntime gains a high_pri_tx field + set_high_pri_sender(). - handle_turn_start busy path sends the TurnStartResult via high_pri and returns Value::Null, which handle_incoming discards. - This guarantees that turn/start responses are never blocked by a backlog of TextDelta etc. events on the shared notification channel.
When the slash popup was visible and the user pressed Enter to submit '/btw help me xxx', the popup's Enter handler selected the 'btw' command and cleared the textarea - losing the arguments. Now, if the selected command supports inline args and there is text after the command name, Enter falls through to the default submission path which correctly dispatches the command with its arguments.
…/exec_command Tool execution titles (server-side): - Add tool_summary module with per-tool title construction - Add summary field to QueryEvent::ToolResult + ToolResultPayload - Wire through server runtime and TUI worker - 11 unit tests for summary formatting Streaming output: - Add ToolProgressSender type (mpsc::UnboundedSender<String>) - Extend ToolHandler trait: handle() now accepts Option<ToolProgressSender> - Update all 18 handler implementations + 6 test implementations - Wire PTY output chunks in shell_exec.rs to progress sender - Wire unified exec broadcast to progress sender in exec_command handler - Add execute_batch_streaming() method on ToolRuntime - Add QueryEvent::ToolProgress variant - Emit CommandExecutionOutputDelta events from server runtime CI: 147 tests passed, 0 failed, -Dwarnings clean, fmt clean, doc clean
…om pane Root cause: task.abort() kills execute_turn before it reaches drain_and_start_next_turn. Queued inputs stay in the steering_queue forever because nobody processes them after the interrupt. Fix: after broadcasting interrupt events, spawn a background task (spawn_next_turn_from_queue) that pops one queued item, creates a new TurnMetadata, broadcasts TurnStarted, and spawns execute_turn. The spawned task's natural completion will then drain the rest of the queue via the existing drain_and_start_next_turn chain.
E2E test coverage: - router: execute_single receives progress chunks via callback - router: execute_batch_streaming forwards all chunks to callback - router: empty batch streaming returns empty results - router: unknown tool in streaming returns error - shell_exec: non-TTY path sends progress via channel - shell_exec: progress=None does not crash - core/query: ToolResult summary field populated in QueryEvent (99th test) - tool_summary: 11 unit tests covering all tools CI: 153 devo-tools tests + 99 devo-core tests, all pass, -Dwarnings clean, fmt clean, doc clean
…to next turn Design change: - steer_input_queue: new separate queue for /btw turn-scoped inputs (drained by take_turn_pending_input into the current turn, discarded on end_turn) - pending_user_prompts / steering_queue: unchanged, for busy turn/start inputs that should start a new turn handle_turn_steer now pushes to steer_input_queue. The query loop merges steer_input_queue first, then pending_user_prompts. drain_and_start_next_turn only reads from steering_queue, never from steer_input_queue. end_turn clears the steer input queue.
Add WorkerEvent::ToolOutputDelta variant for real-time tool output. wire streaming through the full pipeline into the active viewport.
The host's TUI event loop had a dedicated Esc handler that returned early, preventing the bottom pane's internal two-press Esc logic from ever receiving the key. Removed the host-level handler and let the bottom pane handle Esc as it was designed to do. Bottom pane flow (bottom_pane/mod.rs:204): 1st Esc → is_task_running → pending_interrupt_esc = true → hint shown 2nd Esc → pending_interrupt_esc → AppEvent::Interrupt → worker.interrupt_turn()
Root cause: the single stdout task did write_all inside a tokio::select! branch. When the pipe buffer filled up (too many TextDelta events before the client could read them), write_all blocked, preventing the select loop from reading the next high-priority message. Fix: split into two tasks: - Producer: reads from high_pri + normal channels, serializes, pushes to an internal unbounded mpsc channel. NEVER blocks on stdout. - Writer: reads from the internal channel and writes to stdout. CAN block without affecting RPC response processing. - stdin reader remains the main task.
Root cause: commit_active_streams cleared active_reasoning_cell after committing to history, but buffered/delayed ReasoningDelta events arriving afterwards re-created the active reasoning cell via sync_active_reasoning_cell, making the same content appear in both history and the active viewport. Fix: take (clear) the reasoning and assistant text buffers BEFORE committing to history. Also clear cells and stream_controller before any history writes. Any stale delta events that arrive after this will append to an empty string (invisible until next TurnStarted).
Replace exit_code-based is_running() with dedicated AtomicBool terminated_flag. Background task now sets terminated_flag after process exit or forced kill. After kill+wait, re-poll try_wait() to retrieve the exit code. Fixes process_terminate_works test failure on Linux CI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
turn/startwhen busy,/btwinline steer,InputQueueUpdatedevents, QUEUED cells in bottom pane with auto-move to historystreaming output titles, unified exec (PTY process), registry plan
producer-consumer split to eliminate response timeouts under heavy event load
/btwusessteer_input_queue, busyturn/startusespending_user_prompts, preventing steer items from leaking into new turns