Skip to content

Harvest pausable custom command MVP from #2732#2803

Merged
Hmbown merged 1 commit into
codex/v0.9.0-stewardshipfrom
codex/harvest-2732-pausable-command-mvp
Jun 5, 2026
Merged

Harvest pausable custom command MVP from #2732#2803
Hmbown merged 1 commit into
codex/v0.9.0-stewardshipfrom
codex/harvest-2732-pausable-command-mvp

Conversation

@Hmbown

@Hmbown Hmbown commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Harvests the narrow pausable custom slash-command MVP from #2732 by @aboimpinto without merging the broad PR as-is.

  • Parse pausable: true frontmatter for custom slash commands.
  • Add minimal app/engine pause state plus an engine pause gate before tool execution.
  • Preserve paused command context across separate messages and resume only on explicit continue/resume wording.
  • Clear stale paused state on new custom-command dispatch, terminal completion, and cancel.
  • Centralize strict is_resume_message detection with negative coverage for deferred/negated phrases.

Explicitly out of scope

This harvest intentionally does not include any git stash, active_snapshot, rollback, restore, or worktree-mutation behavior from #2732.

Tests

  • cargo test -p codewhale-tui --bin codewhale-tui pausable
  • cargo test -p codewhale-tui --bin codewhale-tui paused_command
  • cargo test -p codewhale-tui --bin codewhale-tui resume_message_helper_is_strict
  • cargo test -p codewhale-tui --bin codewhale-tui new_user_command_clears_stale_paused_state
  • cargo check -p codewhale-tui --bin codewhale-tui
  • python3 scripts/check-coauthor-trailers.py --range origin/codex/v0.9.0-stewardship..HEAD --check-authors
  • git diff --check

Credit

Harvested from #2732 by @aboimpinto. Changelog and commit trailer preserve contributor credit.

Harvested from PR #2732 by @aboimpinto.

Parse pausable frontmatter for custom slash commands, add a narrow engine pause gate before tool execution, and preserve paused command state across separate messages until explicit resume, cancel, terminal completion, or a new command.

Also centralize strict resume-message detection with negative coverage for deferred or negated phrases, and keep rollback/stash/worktree mutation behavior out of this slice.

Co-authored-by: aboimpinto <1231687+aboimpinto@users.noreply.github.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmbown has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

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

Copy link
Copy Markdown
Contributor

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 implements a pausable custom slash-command MVP, allowing commands with pausable: true to pause before tool execution, preserve state, and resume upon explicit user instruction. The changes span frontmatter parsing, engine-level pause gates, and TUI escape action updates, accompanied by comprehensive tests. The review feedback suggests key improvements for robustness and performance, specifically handling poisoned mutexes in the turn loop to prevent bypassing the pause gate, and optimizing string allocations and redundant clones in the message parsing and command state helper functions.

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 on lines +1262 to +1268
if self.shared_paused.lock().is_ok_and(|paused| *paused) {
let _ = self
.tx_event
.send(Event::status("Request was Paused"))
.await;
return (TurnOutcomeStatus::Interrupted, None);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If the shared_paused mutex is poisoned (e.g., due to a panic in another thread), lock() will return an Err. Using is_ok_and will silently evaluate to false, causing the engine to bypass the pause gate and continue executing tools. To ensure robust and consistent behavior across the codebase, handle the poisoned mutex state by calling into_inner() on the poison error, matching the pattern used in engine.rs and handle.rs.

            let is_paused = match self.shared_paused.lock() {
                Ok(paused) => *paused,
                Err(poisoned) => *poisoned.into_inner(),
            };
            if is_paused {
                let _ = self
                    .tx_event
                    .send(Event::status("Request was Paused"))
                    .await;
                return (TurnOutcomeStatus::Interrupted, None);
            }

Comment thread crates/tui/src/tui/ui.rs
Comment on lines +4985 to +5050
fn is_resume_message(message: &str) -> bool {
let words: Vec<String> = message
.to_ascii_lowercase()
.split(|ch: char| !ch.is_ascii_alphanumeric())
.filter(|word| !word.is_empty())
.map(str::to_string)
.collect();
if words.is_empty() {
return false;
}
let text = words.join(" ");
let has_resume_verb = words
.iter()
.any(|word| matches!(word.as_str(), "continue" | "resume"));
if !has_resume_verb {
return false;
}

let blockers = [
"do not continue",
"do not resume",
"don t continue",
"don t resume",
"dont continue",
"dont resume",
"not continue",
"not resume",
"continue yet",
"resume yet",
"will continue",
"will resume",
"continue tomorrow",
"resume tomorrow",
"continue later",
"resume later",
];
if blockers.iter().any(|blocker| text.contains(blocker)) {
return false;
}
if matches!(
words.first().map(String::as_str),
Some("how" | "what" | "when" | "where" | "why")
) {
return false;
}

if words.len() == 1 {
return true;
}

let context_words = [
"please", "now", "paused", "pause", "command", "task", "work", "request", "goal",
"previous", "last", "same", "it", "that", "this", "go", "ahead",
];
if words
.iter()
.any(|word| context_words.contains(&word.as_str()))
{
return true;
}

text.starts_with("can you continue")
|| text.starts_with("can you resume")
|| text.starts_with("could you continue")
|| text.starts_with("could you resume")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The current implementation of is_resume_message allocates a new String for every single word in the user's message by calling .map(str::to_string). We can avoid these unnecessary allocations by collecting references (&str) pointing into the lowercase string instead.

fn is_resume_message(message: &str) -> bool {
    let lowercase = message.to_ascii_lowercase();
    let words: Vec<&str> = lowercase
        .split(|ch: char| !ch.is_ascii_alphanumeric())
        .filter(|word| !word.is_empty())
        .collect();
    if words.is_empty() {
        return false;
    }
    let text = words.join(" ");
    let has_resume_verb = words
        .iter()
        .any(|word| matches!(*word, "continue" | "resume"));
    if !has_resume_verb {
        return false;
    }

    let blockers = [
        "do not continue",
        "do not resume",
        "don t continue",
        "don t resume",
        "dont continue",
        "dont resume",
        "not continue",
        "not resume",
        "continue yet",
        "resume yet",
        "will continue",
        "will resume",
        "continue tomorrow",
        "resume tomorrow",
        "continue later",
        "resume later",
    ];
    if blockers.iter().any(|blocker| text.contains(blocker)) {
        return false;
    }
    if matches!(
        words.first().copied(),
        Some("how" | "what" | "when" | "where" | "why")
    ) {
        return false;
    }

    if words.len() == 1 {
        return true;
    }

    let context_words = [
        "please", "now", "paused", "pause", "command", "task", "work", "request", "goal",
        "previous", "last", "same", "it", "that", "this", "go", "ahead",
    ];
    if words
        .iter()
        .any(|word| context_words.contains(word))
    {
        return true;
    }

    text.starts_with("can you continue")
        || text.starts_with("can you resume")
        || text.starts_with("could you continue")
        || text.starts_with("could you resume")
}

Comment thread crates/tui/src/tui/ui.rs
Comment on lines +5079 to +5095
let Some(quarry) = app
.paused_quarry
.clone()
.or_else(|| app.hunt.quarry.clone())
else {
app.pausable = false;
return None;
};
let title = paused_quarry_title(&quarry).to_string();
if is_resume_message(user_message) {
app.hunt.quarry = Some(app.paused_quarry.take().unwrap_or(quarry));
app.pausable = true;
Some(paused_command_note(&title, true))
} else {
app.hunt.quarry = None;
Some(paused_command_note(&title, false))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In prepare_paused_command_message, the code unconditionally clones app.paused_quarry (or app.hunt.quarry) into quarry at the start. If the message is not a resume message, this cloned string is completely discarded. If it is a resume message, it is also discarded unless app.paused_quarry was None. Since these quarry strings can be quite large, we can avoid these unnecessary clones by working with references first and only cloning when absolutely necessary.

    let quarry_ref = app.paused_quarry.as_ref().or(app.hunt.quarry.as_ref());
    let Some(quarry) = quarry_ref else {
        app.pausable = false;
        return None;
    };
    let title = paused_quarry_title(quarry).to_string();
    if is_resume_message(user_message) {
        app.hunt.quarry = Some(app.paused_quarry.take().unwrap_or_else(|| quarry.clone()));
        app.pausable = true;
        Some(paused_command_note(&title, true))
    } else {
        app.hunt.quarry = None;
        Some(paused_command_note(&title, false))
    }

Comment thread crates/tui/src/tui/ui.rs
Comment on lines +5099 to +5102
app.paused_quarry = app
.paused_quarry
.clone()
.or_else(|| app.hunt.quarry.clone());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In pause_pausable_command, the code unconditionally clones app.paused_quarry and re-assigns it. If app.paused_quarry is already Some, this is a completely redundant clone and allocation. We can simplify this and avoid the redundant clone by only cloning app.hunt.quarry if app.paused_quarry is None.

    if app.paused_quarry.is_none() {
        app.paused_quarry = app.hunt.quarry.clone();
    }

@Hmbown Hmbown merged commit 8fe8900 into codex/v0.9.0-stewardship Jun 5, 2026
2 checks passed
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