Harvest pausable custom command MVP from #2732#2803
Conversation
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>
There was a problem hiding this comment.
Hmbown has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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);
}| 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") | ||
| } |
There was a problem hiding this comment.
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")
}| 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)) | ||
| } |
There was a problem hiding this comment.
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))
}| app.paused_quarry = app | ||
| .paused_quarry | ||
| .clone() | ||
| .or_else(|| app.hunt.quarry.clone()); |
There was a problem hiding this comment.
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();
}
Summary
Harvests the narrow pausable custom slash-command MVP from #2732 by @aboimpinto without merging the broad PR as-is.
pausable: truefrontmatter for custom slash commands.is_resume_messagedetection 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 pausablecargo test -p codewhale-tui --bin codewhale-tui paused_commandcargo test -p codewhale-tui --bin codewhale-tui resume_message_helper_is_strictcargo test -p codewhale-tui --bin codewhale-tui new_user_command_clears_stale_paused_statecargo check -p codewhale-tui --bin codewhale-tuipython3 scripts/check-coauthor-trailers.py --range origin/codex/v0.9.0-stewardship..HEAD --check-authorsgit diff --checkCredit
Harvested from #2732 by @aboimpinto. Changelog and commit trailer preserve contributor credit.