Phase 3: pausable command lifecycle (pause / resume / cancel)#2732
Phase 3: pausable command lifecycle (pause / resume / cancel)#2732aboimpinto wants to merge 52 commits into
Conversation
…d, clear on turn end
…nel, works during active turns)
…ing...' initially
…ssage flow through as normal user message
… non-'continue' messages
…the user retypes fresh
…s model from trying alternatives)
…ally stops the turn
…of separate line)
…roke); other messages cancel + consume
…tale 100% from previous command)
…ded (pausable/paused flags extend the cancel window)
… prevent 'Command resumed' event overriding cancel status
… sync in dispatch_user_message; direct flag clear in cancel path
… app.pausable is true (new pausable command after cancel)
…x mock engine handle missing shared_paused
…Paused / Request was Resumed / Request was Cancelled
…ds race where late engine status overwrites cancel/resume; add guard in Event::Status handler; update test to verify guard
…in next turn; add test
…prompted to continue the goal
…stead of continuing
…ction to tell model NOT to continue
…o continue; quarry restored on resume
…>cancel->new command)
…l; restore from paused_quarry on cancel; clear quarry in dispatch_user_message when next message is sent
…ng pause (sidebar stays visible)
… state; continue/restore only on explicit 'continue'
…me; keep paused_quarry for later resume; show paused_quarry only when paused/cancelled
…ter unpausing with a non-continue message (user can still see what's on hold)
…nt.quarry is None for system prompt, paused_quarry preserved for WorkBench
…ersists start->pause->how-are-you->resume (no step loses the goal display)
…hile paused — tells model not to continue old command
… from ⏸ to ▶ after resume
…that bypasses dispatch_user_message); keep quarry on completion (sidebar shows ✓)
…tion; test catches exact scenario from log
|
Thanks @aboimpinto for taking the time to contribute. This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in Please read |
There was a problem hiding this comment.
Code Review
This pull request introduces a pause and resume lifecycle for user commands in the TUI, adding state management, UI indicators, and an engine-level pause gate that cleanly cancels turns when paused. Key feedback highlights a critical bug where synchronous git stash push calls block the main thread and lack a restore mechanism, alongside high false-positive risks in resume detection due to simple substring checks. Additionally, the reviewer recommends using the set_paused helper to simplify manual lock handling and warns against potential silent failures from using try_lock to clear previous command states.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
… manual_pattern_char_comparison, unnecessary_cast)
aboimpinto
left a comment
There was a problem hiding this comment.
All clippy warnings resolved (collapsible_if, unnecessary_map_or, manual_pattern_char_comparison, unnecessary_cast in shell.rs). The git stash snapshot mechanism is acknowledged as a best-effort prototype with known limitations (no restore, sync call). Resume detection was hardened with word-boundary checks (contains " continue "). Manual lock handling in CancelRequest uses direct flag set to avoid Op race. The try_lock for todos/plan clearing is best-effort (logs and continues on failure).
|
this is such a smart update @aboimpinto - wow. thank you for your time and effort! |
Phase 3 — Pausable Command Lifecycle (Pause / Resume / Cancel)
This PR builds on the custom slash command frontmatter from Phase 1 (PR #2326) and the hook gate from Phase 2. It adds
pausable: truelifecycle support for custom slash commands: pause with ESC, type other messages while paused, resume with "continue"/"resume", and cancel with ESC twice.What It Does
Frontmatter
pausable: truein~/.codewhale/commands/<name>.mdenables the pause lifecycle for a custom command.Pause / Resume / Cancel
pausablecommand. A sharedArc<Mutex<bool>>flag blocks all further tool calls at the engine's pause gate (turn loop).WorkBench lifecycle indicators
State isolation
goal_objectiveis cleared on pause, so the model does not see the old goal as active work to complete.Files Changed
How to Test
Create
~/.codewhale/commands/<name>.mdwithpausable: truein the frontmatter and a clear task description.Start the command in the TUI:
To cancel: while paused, press ESC again → WorkBench shows ✘ (cancelled)
Test Results
Issue Alignment
No
Closes/Fixeslines — these issues are broad EPIC/audit scopes. This PR implements thepausablefrontmatter slice and the pause/resume/cancel lifecycle for custom slash commands, but does not close the full universal hook layer (#1917) or the refactor issues.Builds On
Paulo Aboim Pinto
Greptile Summary
This PR implements Phase 3 of the pausable command lifecycle for custom slash commands: ESC once pauses a running
pausable: truecommand (blocking tool calls via a sharedArc<Mutex<bool>>flag), typing a non-resume message sends it through without restoring the goal, and "continue"/"resume" restores the saved quarry and resumes naturally. ESC twice cancels, and the WorkBench displays ▶ / ⏳ / ⏸ / ✘ / ✓ lifecycle indicators.shared_paused: Arc<StdMutex<bool>>toEngine/EngineHandlewith aset_paused()side-channel method; the turn loop's pause gate callscancel_token.cancel()before each tool call when the flag is set (safe becausereset_cancel_token()is already called at the start of every new turn).Appfields trackpaused,pausable,paused_cancelled,paused_quarry,paused_at, andactive_snapshot;dispatch_user_message/steer_user_message/submit_or_steer_messageall intercept resume patterns and restore the saved goal before dispatch.sidebar_work_summary.Confidence Score: 3/5
Pause does not take effect until all pending ToolCallBefore hook scripts complete; in hook-heavy environments this causes a multi-second delay and spurious external events. Several state-management issues from prior rounds are also still open.
The turn-loop pause gate fires cancel_token.cancel() but then falls through to the ToolCallBefore hook executor block unchanged. Because hooks are dispatched via spawn_blocking, they run to completion before the cancellation propagates, blocking the pause response and potentially triggering external side effects for tool calls that will never execute.
crates/tui/src/core/engine/turn_loop.rs (pause gate + hook interaction), crates/tui/src/tui/ui.rs (dead PauseCommand branch), crates/tui/src/commands/user_commands.rs (git stash leak, paused_quarry reset)
Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant UI as TUI / App State participant EH as EngineHandle participant TL as TurnLoop (Engine) User->>UI: /pausable-command UI->>UI: "try_dispatch_user_command (pausable=true)" UI->>EH: dispatch_user_message new turn EH->>TL: reset_cancel_token(), start turn TL-->>UI: TurnStarted event Note over TL: Tool call arrives User->>UI: ESC once UI->>UI: "PauseCommand: paused_quarry=quarry, quarry=None, paused=true" UI->>EH: set_paused(true) TL->>TL: "pause gate: is_paused=true, cancel_token.cancel()" Note over TL: ToolCallBefore hooks still run via spawn_blocking TL-->>UI: TurnCompleted Interrupted UI->>UI: "pausable=false" User->>UI: continue the command UI->>UI: "quarry=paused_quarry.take(), paused=false, set_paused(false)" UI->>EH: dispatch message EH->>TL: reset_cancel_token(), start fresh turn TL-->>UI: "TurnCompleted Completed, verdict=Hunted" alt ESC twice while paused User->>UI: ESC again UI->>UI: "CancelRequest: paused_cancelled=true, quarry=paused_quarry.take()" UI->>EH: set_paused(false), cancel() endComments Outside Diff (1)
crates/tui/src/core/engine/turn_loop.rs, line 1367-1415 (link)ToolCallBeforehooks still runAfter
self.cancel_token.cancel()is called in the pause gate,blocked_errorremainsNone. The nextif blocked_error.is_none() && hook_executor...check still passes, soToolCallBeforehooks are dispatched viatokio::task::spawn_blocking. Becausespawn_blockingtasks cannot be cancelled by the async runtime, the hooks run to completion before the cancelled turn can exit — meaning the pause does not take effect until every hook script finishes. In environments that use slow or side-effectfulToolCallBeforehooks (e.g. external webhooks, audit loggers, network calls), this can cause a multi-second delay in the pause response and spurious external events for tool calls that will never actually execute.Setting
blocked_errorto a non-Nonesentinel after the pause cancel would cause the existing hook guard to short-circuit, making the pause take effect immediately without running hooks.Reviews (4): Last reviewed commit: "fix: use set_paused in CancelRequest; us..." | Re-trigger Greptile