Skip to content

Phase 3: pausable command lifecycle (pause / resume / cancel)#2732

Draft
aboimpinto wants to merge 52 commits into
Hmbown:mainfrom
aboimpinto:feat/allowed-tools-phase3-fresh
Draft

Phase 3: pausable command lifecycle (pause / resume / cancel)#2732
aboimpinto wants to merge 52 commits into
Hmbown:mainfrom
aboimpinto:feat/allowed-tools-phase3-fresh

Conversation

@aboimpinto
Copy link
Copy Markdown
Contributor

@aboimpinto aboimpinto commented Jun 4, 2026

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: true lifecycle 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: true in ~/.codewhale/commands/<name>.md enables the pause lifecycle for a custom command.

Pause / Resume / Cancel

  • ESC once pauses a running pausable command. A shared Arc<Mutex<bool>> flag blocks all further tool calls at the engine's pause gate (turn loop).
  • Type any message while paused — the message goes through as a normal user message with no active goal in the system prompt. The WorkBench keeps showing the paused command so you remember it's on hold.
  • "continue"/"resume" (also "please continue...", "resume the command", etc.) restores the saved goal and the model continues naturally.
  • ESC twice cancels the paused command. The WorkBench shows the cancelled state with the original task name.
  • Cancellation notice injected into non-continue messages sent while paused, so the model sees "[The user paused: ...]" and does NOT continue the old command unprompted.

WorkBench lifecycle indicators

  • ▶ Play — command is running (yellow)
  • ⏳ Pausing — transitioning to paused (tools still draining)
  • ⏸ Paused — command is on hold, waiting for resume or cancel
  • ✘ Cancelled — command was cancelled by the user
  • ✓ Finished — command completed successfully (green)

State isolation

  • The system prompt goal_objective is cleared on pause, so the model does not see the old goal as active work to complete.
  • On resume the goal is restored. On cancel the quarry is restored for WorkBench display but cleared on the next user message.
  • Todos and plan state from the previous command are cleared when a new slash command starts.

Files Changed

crates/tui/src/commands/user_commands.rs  |  47 +-   pausable frontmatter parsing, state clear on new command
crates/tui/src/core/engine.rs             |  13 +-   shared_paused flag, set_paused on handle
crates/tui/src/core/engine/handle.rs      |  19 +-   set_paused() -> Arc<Mutex<bool>>
crates/tui/src/core/engine/turn_loop.rs   |  12 +-   pause gate before tool execution
crates/tui/src/core/ops.rs                |   2 +-   SetPaused variant (removed, was unused)
crates/tui/src/tui/app.rs                 |  20 +-   paused, pausable, paused_quarry fields
crates/tui/src/tui/composer_ui.rs         |  12 +-   EscapeAction::PauseCommand
crates/tui/src/tui/sidebar.rs             | 805 ++++-  WorkBench indicators, 80 lifecycle tests
crates/tui/src/tui/ui.rs                  | 226 +++-  dispatch interception, TurnCompleted verdict, cancellation notice
9 files changed, 1135 insertions(+), 21 deletions(-)

How to Test

Create ~/.codewhale/commands/<name>.md with pausable: true in the frontmatter and a clear task description.

Start the command in the TUI:

  1. Command starts → WorkBench shows (play, yellow)
  2. ESC → WorkBench shows (paused), tool calls are blocked
  3. Type a question (e.g. "how are you?") → model responds, does NOT continue the paused command, WorkBench keeps showing
  4. Type "can you please continue the paused command" → WorkBench shows again, model continues the task
  5. After completion → WorkBench shows (green checkmark)

To cancel: while paused, press ESC again → WorkBench shows (cancelled)

Test Results

cargo test -p codewhale-tui --bin codewhale-tui pause  -> 18 passed
cargo test -p codewhale-tui --bin codewhale-tui sidebar -> 80 passed (all sidebar tests)

Issue Alignment

Refs #1886, #1887, #1888, #1889, #1891, #1894, #1895, #1917

No Closes/Fixes lines — these issues are broad EPIC/audit scopes. This PR implements the pausable frontmatter 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: true command (blocking tool calls via a shared Arc<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.

  • Engine plumbing: adds shared_paused: Arc<StdMutex<bool>> to Engine/EngineHandle with a set_paused() side-channel method; the turn loop's pause gate calls cancel_token.cancel() before each tool call when the flag is set (safe because reset_cancel_token() is already called at the start of every new turn).
  • UI state machine: six new App fields track paused, pausable, paused_cancelled, paused_quarry, paused_at, and active_snapshot; dispatch_user_message / steer_user_message / submit_or_steer_message all intercept resume patterns and restore the saved goal before dispatch.
  • Sidebar indicators: 80 new unit tests cover the full pause/resume/cancel/complete lifecycle flows against 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

Filename Overview
crates/tui/src/core/engine/turn_loop.rs Adds pause gate before each tool call. Gate cancels the token but does not set blocked_error, so ToolCallBefore hooks still run via spawn_blocking for cancelled tool calls, delaying the pause and triggering spurious hook side effects.
crates/tui/src/tui/ui.rs Largest change: adds pause/resume/cancel intercept blocks in dispatch_user_message, steer_user_message, and submit_or_steer_message, plus ESC handler changes. Contains a dead branch in the PauseCommand handler and the app.pausable cleared-on-text-resume issue flagged in prior review.
crates/tui/src/commands/user_commands.rs Adds pausable frontmatter parsing, state reset on new command dispatch. Git stash is created for every pausable: true command but never popped; snapshot restore branch is unreachable (both flagged in prior review). paused_quarry also not cleared here (prior review).
crates/tui/src/core/engine/handle.rs Adds set_paused() using the same side-channel Arc<Mutex> pattern as cancel(). Straightforward and correct.
crates/tui/src/core/engine.rs Adds shared_paused: Arc<StdMutex> to both Engine and EngineHandle structs and wires it through initialization and mock helper. Clean plumbing change.
crates/tui/src/tui/app.rs Adds six new App fields for pause lifecycle state. All fields have sensible Default/false/None initialization. No issues found.
crates/tui/src/tui/composer_ui.rs Adds PauseCommand variant and updates next_escape_action. The dead-branch consequence lives in ui.rs, not here; the guard logic itself is correct for the intended ESC-once-pause, ESC-twice-cancel flow.
crates/tui/src/tui/sidebar.rs Adds pause lifecycle indicators and 80 new tests. Triply-duplicated pause_indicator and goal_objective logic across three sidebar_work_summary branches is verbose but consistent. Tests are thorough and well-scoped.
crates/tui/src/tools/shell.rs One-line allow(clippy::unnecessary_cast) addition to suppress a Windows-only lint. Unrelated to the pause feature; clean.

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()
    end
Loading

Comments Outside Diff (1)

  1. crates/tui/src/core/engine/turn_loop.rs, line 1367-1415 (link)

    P1 Pause gate cancels the token but ToolCallBefore hooks still run

    After self.cancel_token.cancel() is called in the pause gate, blocked_error remains None. The next if blocked_error.is_none() && hook_executor... check still passes, so ToolCallBefore hooks are dispatched via tokio::task::spawn_blocking. Because spawn_blocking tasks 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-effectful ToolCallBefore hooks (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_error to a non-None sentinel after the pause cancel would cause the existing hook guard to short-circuit, making the pause take effect immediately without running hooks.

    Fix in Codex Fix in Claude Code Fix in Cursor

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (4): Last reviewed commit: "fix: use set_paused in CancelRequest; us..." | Re-trigger Greptile

aboimpinto added 30 commits June 3, 2026 17:41
…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)
…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
aboimpinto added 19 commits June 3, 2026 22:58
…l; restore from paused_quarry on cancel; clear quarry in dispatch_user_message when next message is sent
… 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
…that bypasses dispatch_user_message); keep quarry on completion (sidebar shows ✓)
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

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 .github/APPROVED_CONTRIBUTORS will be closed automatically.

Please read CONTRIBUTING.md for the expected contribution shape. A maintainer can grant PR access by commenting /lgtm on a pull request.

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 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.

Comment thread crates/tui/src/commands/user_commands.rs
Comment thread crates/tui/src/tui/ui.rs
Comment thread crates/tui/src/tui/ui.rs Outdated
Comment thread crates/tui/src/tui/ui.rs
Comment thread crates/tui/src/commands/user_commands.rs Outdated
Comment thread crates/tui/src/tui/ui.rs Outdated
Comment thread crates/tui/src/commands/user_commands.rs Outdated
Comment thread crates/tui/src/commands/user_commands.rs
Comment thread crates/tui/src/tui/ui.rs
Comment thread crates/tui/src/commands/user_commands.rs
Copy link
Copy Markdown
Contributor Author

@aboimpinto aboimpinto left a comment

Choose a reason for hiding this comment

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

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).

Comment thread crates/tui/src/tui/ui.rs
@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented Jun 4, 2026

this is such a smart update @aboimpinto - wow. thank you for your time and effort!

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.

2 participants