Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 31 additions & 9 deletions rust/crates/rusty-claude-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1755,12 +1755,26 @@ fn parse_direct_slash_cli_action(
}
Ok(Some(command)) => Err({
let _ = command;
format!(
// #738: newline before remediation so split_error_hint populates hint field
"slash command {command_name} is interactive-only.\nStart `claw` and run it there, or use `claw --resume SESSION.jsonl {command_name}` / `claw --resume {latest} {command_name}` when the command is marked [resume] in /help.",
command_name = rest[0],
latest = LATEST_SESSION_REFERENCE,
)
let command_name = &rest[0];
// #829: only suggest --resume when the command is actually
// resume-safe. Non-resume-safe commands (e.g. /commit, /pr)
// previously suggested --resume, which just re-triggered
// interactive_only on a second invocation.
let bare_name = command_name.trim_start_matches('/');
let is_resume_safe = commands::resume_supported_slash_commands()
.iter()
.any(|spec| spec.name == bare_name);
if is_resume_safe {
format!(
// #738: newline before remediation so split_error_hint populates hint field
"interactive_only: slash command {command_name} requires a live session.\nStart `claw` and run it there, or use `claw --resume SESSION.jsonl {command_name}` / `claw --resume {latest} {command_name}`.",
latest = LATEST_SESSION_REFERENCE,
)
} else {
format!(
"interactive_only: slash command {command_name} requires a live REPL session.\nStart `claw` and run it there."
)
}
}),
Ok(None) => Err(format!("unknown subcommand: {}", rest[0])),
Err(error) => Err(error.to_string()),
Expand Down Expand Up @@ -13906,8 +13920,15 @@ mod tests {
);
let error = parse_args(&["/status".to_string()])
.expect_err("/status should remain REPL-only when invoked directly");
assert!(error.contains("interactive-only"));
assert!(error.contains("claw --resume SESSION.jsonl /status"));
// #829: prefix changed from "interactive-only" to "interactive_only:"
assert!(
error.contains("interactive_only:"),
"expected interactive_only: prefix, got: {error}"
);
assert!(
error.contains("claw --resume SESSION.jsonl /status"),
"expected --resume suggestion for resume-safe /status, got: {error}"
);
}

#[test]
Expand All @@ -13929,8 +13950,9 @@ mod tests {
for alias in ["/plugin", "/plugins", "/marketplace"] {
let error = parse_args(&[alias.to_string()])
.expect_err("valid plugin slash aliases are local/interactive, never prompts");
// #829: prefix changed from "interactive-only" to "interactive_only:"
assert!(
error.contains("interactive-only"),
error.contains("interactive_only:") || error.contains("interactive-only"),
"{alias} should reject as an interactive plugin command outside the REPL, got: {error}"
);
}
Expand Down
39 changes: 39 additions & 0 deletions rust/crates/rusty-claude-cli/tests/output_format_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4015,3 +4015,42 @@ fn approve_deny_outside_repl_emits_interactive_only() {
);
}
}

// #829: interactive_only hint must NOT suggest --resume for non-resume-safe commands
#[test]
fn non_resume_safe_interactive_only_hint_omits_resume_suggestion() {
let root = unique_temp_dir("non-resume-hint-829");
std::fs::create_dir_all(&root).expect("create temp dir");
// /commit, /pr, /issue, /bughunter, /ultraplan are not resume-safe
for cmd in &["/commit", "/pr", "/issue", "/bughunter", "/ultraplan"] {
let output = run_claw(&root, &["--output-format", "json", cmd], &[]);
let stdout = String::from_utf8_lossy(&output.stdout);
let j: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|_| panic!("{cmd} must emit JSON (#829), got: {stdout:?}"));
assert_eq!(
j["error_kind"], "interactive_only",
"{cmd} must emit interactive_only (#829): {j}"
);
let hint = j["hint"].as_str().unwrap_or("");
assert!(
!hint.contains("--resume"),
"{cmd} hint must not suggest --resume for non-resume-safe command (#829): hint={hint:?}"
);
}
}

// #829: resume-safe commands should still suggest --resume in the hint
#[test]
fn resume_safe_interactive_only_hint_includes_resume_suggestion() {
let root = unique_temp_dir("resume-hint-829");
std::fs::create_dir_all(&root).expect("create temp dir");
let output = run_claw(&root, &["--output-format", "json", "/diff"], &[]);
let stdout = String::from_utf8_lossy(&output.stdout);
let j: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|_| panic!("/diff must emit JSON (#829), got: {stdout:?}"));
let hint = j["hint"].as_str().unwrap_or("");
assert!(
hint.contains("--resume"),
"/diff hint must suggest --resume (it is resume-safe) (#829): hint={hint:?}"
);
}
Loading