use super::CommandResult;
use crate::tui::app::{App, AppAction};
const USAGE: &str = "Usage: /preview-request [json] [--prompt <text>] | base-prompt \
(flags first; --prompt takes the rest)";
pub fn preview_request(_app: &mut App, arg: Option<&str>) -> CommandResult {
match parse_args(arg.unwrap_or_default()) {
Ok(PreviewArgs {
json,
base_prompt_only,
hypothetical_prompt,
}) => CommandResult::action(AppAction::PreviewOutboundRequest {
json,
base_prompt_only,
hypothetical_prompt,
}),
Err(message) => CommandResult::message(message),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PreviewArgs {
json: bool,
base_prompt_only: bool,
hypothetical_prompt: Option<String>,
}
fn parse_args(raw: &str) -> Result<PreviewArgs, String> {
const PROMPT_FLAG: &str = "--prompt";
let mut json = false;
let mut base_prompt_only = false;
let mut rest = raw;
loop {
let trimmed = rest.trim_start();
if trimmed.is_empty() {
return Ok(PreviewArgs {
json,
base_prompt_only,
hypothetical_prompt: None,
});
}
let token_end = trimmed.find(char::is_whitespace).unwrap_or(trimmed.len());
let (token, remainder) = trimmed.split_at(token_end);
if token == PROMPT_FLAG {
if base_prompt_only {
return Err(format!(
"`base-prompt` cannot be combined with `--prompt`. {USAGE}"
));
}
let Some(delimiter) = remainder.chars().next().filter(|ch| ch.is_whitespace()) else {
return Err(format!("`--prompt` needs text after it. {USAGE}"));
};
let prompt = &remainder[delimiter.len_utf8()..];
if prompt.trim().is_empty() {
return Err(format!("`--prompt` needs text after it. {USAGE}"));
}
return Ok(PreviewArgs {
json,
base_prompt_only,
hypothetical_prompt: Some(prompt.to_string()),
});
}
match token {
"json" | "--json" => {
if base_prompt_only {
return Err(format!(
"`base-prompt` cannot be combined with JSON. {USAGE}"
));
}
json = true;
}
"manifest" | "--manifest" => json = false,
"prompt" => {}
"base-prompt" | "--base-prompt" => {
if json {
return Err(format!(
"`base-prompt` cannot be combined with JSON. {USAGE}"
));
}
base_prompt_only = true;
}
_ => {
return Err(format!(
"Unknown argument. Flags come before `--prompt`, which takes the rest of the line as prompt text. {USAGE}"
));
}
}
rest = remainder;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
fn args(raw: &str) -> Result<PreviewArgs, String> {
parse_args(raw)
}
#[test]
fn default_invocation_requests_the_human_manifest() {
assert_eq!(
args("").unwrap(),
PreviewArgs {
json: false,
base_prompt_only: false,
hypothetical_prompt: None
}
);
assert_eq!(args("manifest").unwrap(), args("").unwrap());
}
#[test]
fn json_flag_is_accepted_in_both_spellings() {
assert!(args("json").unwrap().json);
assert!(args("--json").unwrap().json);
}
#[test]
fn base_prompt_mode_is_explicit_and_cannot_mix_with_body_preview() {
assert!(!args("prompt").unwrap().base_prompt_only);
for alias in ["base-prompt", "--base-prompt"] {
let parsed = args(alias).expect("base-prompt mode parses");
assert!(parsed.base_prompt_only, "{alias}");
assert_eq!(parsed.hypothetical_prompt, None, "{alias}");
assert!(!parsed.json, "{alias}");
}
for invalid in [
"json base-prompt",
"base-prompt json",
"base-prompt --prompt hi",
] {
assert!(args(invalid).is_err(), "{invalid}");
}
}
#[test]
fn hypothetical_prompt_is_captured_verbatim_for_auto_resolution() {
assert_eq!(
args("--prompt refactor the parser").unwrap(),
PreviewArgs {
json: false,
base_prompt_only: false,
hypothetical_prompt: Some("refactor the parser".to_string()),
}
);
assert_eq!(
args("json --prompt fix the failing test").unwrap(),
PreviewArgs {
json: true,
base_prompt_only: false,
hypothetical_prompt: Some("fix the failing test".to_string()),
}
);
}
#[test]
fn the_prompt_keeps_the_users_bytes() {
for prompt in [
"keep two spaces",
"line one\nline two",
"tabs\tand\tmore",
"trailing space ",
] {
let raw = format!("--prompt {prompt}");
let parsed = args(&raw).expect("prompt parses");
assert_eq!(
parsed.hypothetical_prompt.as_deref(),
Some(prompt),
"`{prompt:?}` must survive the parser byte for byte"
);
}
assert_eq!(
args("--prompt padded start")
.unwrap()
.hypothetical_prompt
.as_deref(),
Some(" padded start")
);
}
#[test]
fn flags_after_the_prompt_are_prompt_text_not_flags() {
let parsed = args("--prompt fix it json").expect("parses");
assert!(
!parsed.json,
"a trailing `json` is part of the prompt, and the manifest stays human"
);
assert_eq!(parsed.hypothetical_prompt.as_deref(), Some("fix it json"));
let parsed = args("json --prompt fix it").expect("parses");
assert!(parsed.json);
assert_eq!(parsed.hypothetical_prompt.as_deref(), Some("fix it"));
}
#[test]
fn unknown_arguments_before_the_prompt_are_rejected_not_guessed() {
for raw in ["nope", "json nope", "--nope --prompt hi", "manifest -x"] {
let err = args(raw).expect_err("an unknown argument must not parse");
assert!(err.contains("Unknown argument"), "{raw}: {err}");
assert!(err.contains("--prompt"), "{raw}: {err}");
}
}
#[test]
fn unknown_argument_diagnostic_is_bounded_and_never_echoes_input() {
let hostile = format!(
"sk-live-{}-/Users/alice/private/config\nsecond-line",
"a".repeat(10_000)
);
let err = args(&hostile).expect_err("hostile input must be rejected");
assert!(err.contains("Unknown argument"), "{err}");
assert!(err.contains("--prompt"), "{err}");
assert!(err.len() < 256, "diagnostic was not bounded: {}", err.len());
for forbidden in ["sk-live", "/Users/alice", "second-line"] {
assert!(!err.contains(forbidden), "{forbidden} leaked in {err}");
}
}
#[test]
fn empty_hypothetical_prompt_is_rejected() {
for raw in ["--prompt", "--prompt ", "json --prompt"] {
let err = args(raw).expect_err("bare --prompt is an error");
assert!(err.contains("needs text"), "{raw}: {err}");
assert!(err.contains("/preview-request"), "{raw}: {err}");
}
}
#[test]
fn leading_and_repeated_whitespace_between_flags_is_ignored() {
assert_eq!(args(" json --manifest ").unwrap(), args("").unwrap());
}
#[test]
fn unknown_argument_is_rejected_without_touching_state() {
let options = crate::test_support::test_tui_options(std::path::PathBuf::from(
"/tmp/test-workspace-preview-request",
));
let mut app = App::new(options, &Config::default());
let messages_before = app.api_messages.len();
let history_before = app.history.len();
let result = preview_request(&mut app, Some("nope"));
assert!(!result.is_error);
assert!(
result
.message
.as_deref()
.is_some_and(|message| message.contains("/preview-request")),
"{result:?}"
);
assert!(result.action.is_none());
assert_eq!(app.api_messages.len(), messages_before);
assert_eq!(app.history.len(), history_before);
}
#[test]
fn command_delegates_to_the_engine_and_mutates_nothing() {
let options = crate::test_support::test_tui_options(std::path::PathBuf::from(
"/tmp/test-workspace-preview-request-pure",
));
let mut app = App::new(options, &Config::default());
app.api_messages.push(crate::models::Message {
role: "user".to_string(),
content: vec![crate::models::ContentBlock::Text {
text: "hello".to_string(),
cache_control: None,
}],
});
let result = preview_request(&mut app, Some("json"));
assert!(result.message.is_none(), "{result:?}");
assert!(matches!(
result.action,
Some(AppAction::PreviewOutboundRequest { json: true, .. })
));
assert_eq!(app.api_messages.len(), 1);
assert!(app.history.is_empty());
}
#[test]
fn base_prompt_provenance_is_runtime_not_a_source_path() {
let label = crate::prompts::base_prompt_origin().label();
assert!(!label.contains("crates/"), "{label}");
assert!(!label.contains(".rs"), "{label}");
assert!(
label.contains("bundled") || label.contains("override"),
"{label}"
);
}
#[test]
fn this_command_contains_no_prompt_dumping_path() {
let source = include_str!("preview_request.rs");
for forbidden in [
"effective_base_prompt_text",
"system_prompt_text",
"compose_default_static_layers",
] {
assert!(
!source.contains(&format!("{forbidden}(")),
"`{forbidden}` must not be callable from the command layer"
);
}
}
}