use crate::{
commands::CommandRegistry,
config::{CliConfigOverrides, ColorChoice, EffectiveConfig, McPaths},
instructions::discover_agents_with_additional_markdown,
providers::OPENAI_CODEX_PROVIDER,
sessions::SessionManager,
skills::{SkillRoots, discover_skills, filter_enabled_skills},
tui::TuiSessionConfig,
};
use anyhow::{Error, Result};
use clap::{Parser, Subcommand};
use std::{env, io, path::Path};
mod diagnostics;
mod dump_prompt;
mod interactive;
mod mcp;
mod output;
mod print;
mod sessions;
mod validation;
use output::CliOutputSink;
use validation::{
validate_dump_prompt_launch, validate_session_launch, validate_subcommand_launch,
validate_tui_launch,
};
pub(crate) type AppResult<T> = std::result::Result<T, AppError>;
#[derive(Debug)]
pub(crate) enum AppError {
Usage(Error),
Runtime(Error),
}
impl AppError {
pub(crate) fn usage(error: impl Into<Error>) -> Self {
Self::Usage(error.into())
}
pub(crate) fn runtime(error: impl Into<Error>) -> Self {
Self::Runtime(error.into())
}
pub(crate) fn exit_code(&self) -> std::process::ExitCode {
match self {
Self::Usage(_) => std::process::ExitCode::from(2),
Self::Runtime(_) => std::process::ExitCode::from(1),
}
}
pub(crate) fn into_error(self) -> Error {
match self {
Self::Usage(error) | Self::Runtime(error) => error,
}
}
pub(crate) fn as_error(&self) -> &Error {
match self {
Self::Usage(error) | Self::Runtime(error) => error,
}
}
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_error().fmt(f)
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.as_error().source()
}
}
impl From<Error> for AppError {
fn from(error: Error) -> Self {
Self::Runtime(error)
}
}
fn runtime<T>(result: Result<T>) -> AppResult<T> {
result.map_err(AppError::runtime)
}
fn prepare_session_for_launch(
session: crate::sessions::Session,
) -> AppResult<crate::sessions::Session> {
let replay = session
.read_events_tolerant_bounded(
crate::context::REPLAY_JSONL_MAX_LINES,
crate::context::REPLAY_JSONL_MAX_BYTES,
)
.map_err(|_| {
AppError::runtime(anyhow::anyhow!(
"cannot attach session: history is unreadable or exceeds bounded replay limits; use --compact or start a new session"
))
})?;
if !replay.diagnostics.is_empty() {
return Err(AppError::runtime(anyhow::anyhow!(
"cannot attach session: history contains unreadable events; use --compact or start a new session"
)));
}
Ok(session)
}
#[derive(Debug, Parser)]
#[command(
name = "magi-code",
about = "Local CLI coding agent MVP foundation",
version,
after_long_help = "Slash commands (interactive shell / Mission Control): /help, /login, /logout, /setmodel, /models, /new, /changes, /rewind [--to <turn>] [--dry-run], /compact, /prune-sessions, /sessions, /skills, /tools, /subagents, /mcp, /system-prompt, /quit"
)]
pub struct CliArgs {
#[arg(long, help = "Run a single prompt and print the assistant response")]
pub print: bool,
#[arg(
long,
help = "Launch the opt-in Mission Control TUI for interactive terminal sessions"
)]
pub tui: bool,
#[arg(
long = "tui-experimental",
help = "Launch experimental workspace-only TUI (provider/session execution unavailable)"
)]
pub tui_experimental: bool,
#[arg(
long,
help = "Print provider-ready runtime system prompt and tool schemas as JSON, then exit"
)]
pub dump_prompt: bool,
#[arg(long, help = "Override model name")]
pub model: Option<String>,
#[arg(long, help = "Override provider name")]
pub provider: Option<String>,
#[arg(
long,
help = "Deprecated: override provider API key for this process; prefer provider-specific environment variables or /login because argv can be exposed"
)]
pub api_key: Option<String>,
#[arg(
long,
value_enum,
value_name = "WHEN",
help = "Control ANSI color: auto, always, or never"
)]
pub color: Option<ColorChoice>,
#[arg(long, help = "Do not create or attach to a session")]
pub no_session: bool,
#[arg(long = "continue", help = "Continue the most recent session")]
pub continue_session: bool,
#[arg(long, value_name = "SESSION_ID", help = "Resume a specific session")]
pub resume: Option<String>,
#[command(subcommand)]
pub command: Option<CliCommand>,
#[arg(value_name = "PROMPT", trailing_var_arg = true)]
pub prompt: Vec<String>,
}
#[derive(Debug, Subcommand)]
pub enum CliCommand {
#[command(about = "Repair or inspect session storage")]
Sessions {
#[command(subcommand)]
command: SessionsCommand,
},
#[command(about = "Diagnose configured MCP servers")]
Mcp {
#[command(subcommand)]
command: McpCommand,
},
}
#[derive(Debug, Subcommand)]
pub enum SessionsCommand {
#[command(about = "Repair recognized legacy session permissions")]
RepairPermissions {
#[arg(long, conflicts_with = "dry_run", help = "Apply without confirmation")]
yes: bool,
#[arg(
long,
conflicts_with = "yes",
help = "Preview without changing permissions"
)]
dry_run: bool,
},
}
#[derive(Debug, Subcommand)]
pub enum McpCommand {
#[command(about = "List configured MCP servers and connection status")]
List,
#[command(about = "Test one configured MCP server")]
Test { server: String },
#[command(about = "Authenticate one OAuth MCP server")]
Login { server: String },
#[command(about = "Delete local OAuth token for one MCP server")]
Logout { server: String },
}
pub fn run(args: CliArgs) -> AppResult<()> {
if let Some(command) = &args.command {
validate_subcommand_launch(&args)?;
return runtime(run_subcommand(command));
}
let cwd = env::current_dir().map_err(AppError::runtime)?;
if args.dump_prompt {
validate_dump_prompt_launch(&args)?;
let provider = args.provider.as_deref().expect("validated dump provider");
let target = provider.parse().map_err(AppError::runtime)?;
let paths = runtime(McPaths::resolve())?;
let stdout = io::stdout();
let mut writer = stdout.lock();
return runtime(dump_prompt::run_dump_prompt(
&mut writer,
paths,
&cwd,
target,
));
}
validate_tui_launch(&args)?;
validate_session_launch(&args)?;
if args.api_key.is_some() {
eprintln!(
"warning: --api-key is deprecated because command-line arguments can be exposed by process listings; prefer provider-specific environment variables or /login"
);
}
let paths = runtime(McPaths::resolve())?;
runtime(paths.ensure_runtime_dirs())?;
runtime(crate::config::ensure_settings_schema_files(&paths))?;
if args.tui_experimental {
return runtime(crate::tui2::run_tui(paths));
}
let (config, settings) = runtime(crate::config::load_config_with_settings(
paths.clone(),
CliConfigOverrides {
provider: args.provider.clone(),
model: args.model.clone(),
api_key: args.api_key.clone(),
color: args.color,
},
))?;
let instructions = runtime(discover_agents_with_additional_markdown(
&paths.user_agents,
&cwd,
&settings.instructions.additional_markdown_paths,
))?;
let mut skill_roots = SkillRoots::from_paths(&paths.root, &cwd);
skill_roots.additional_paths = settings.skills.additional_paths.clone();
let discovered_skills = discover_skills(&skill_roots);
let disabled_skills = crate::config::disabled_skill_names_from_settings(&settings);
let skills = filter_enabled_skills(&discovered_skills, &disabled_skills);
let commands = CommandRegistry::mvp();
let manager = SessionManager::new(paths.sessions.clone());
let active_session = if args.no_session {
None
} else if let Some(id) = args.resume {
let id = crate::sessions::validate_session_id(id).map_err(AppError::usage)?;
let session = manager.open_existing(id).map_err(|_| {
AppError::runtime(anyhow::anyhow!(
"cannot resume session: session is missing, unreadable, or unsafe; use --continue or start a new session"
))
})?;
Some(prepare_session_for_launch(session)?)
} else if args.continue_session {
match manager.most_recent() {
Ok(Some(session)) => Some(prepare_session_for_launch(session)?),
Ok(None) => Some(runtime(manager.create())?),
Err(_) => {
return Err(AppError::runtime(anyhow::anyhow!(
"cannot continue session: history discovery failed; use --resume with a valid session or start a new session"
)));
}
}
} else {
Some(runtime(manager.create())?)
};
if args.print || !args.prompt.is_empty() {
runtime(print::run_print_mode(
&config,
&instructions,
&skills,
&settings,
&args.prompt,
active_session.as_ref(),
&cwd,
))?;
return Ok(());
}
if args.tui {
let discovered_skills_for_tui = discovered_skills.metadata_only();
let tui_config = TuiSessionConfig {
config,
instructions,
discovered_skills: discovered_skills_for_tui,
skills,
commands,
manager,
active_session,
cwd,
};
crate::rendering::highlight::prewarm();
return runtime(crate::tui::run_tui(tui_config));
}
print_summary(
&cwd,
&config,
&instructions,
&skills,
&commands,
active_session.as_ref().map(|session| session.id()),
&args.prompt,
);
runtime(interactive::run_interactive(
config,
instructions,
skills,
commands,
manager,
active_session,
cwd,
))
}
fn run_subcommand(command: &CliCommand) -> Result<()> {
match command {
CliCommand::Sessions { command } => match command {
SessionsCommand::RepairPermissions { yes, dry_run } => {
let paths = McPaths::resolve()?;
sessions::run(&paths.sessions, *yes, *dry_run)
}
},
CliCommand::Mcp { command } => {
let stdout = io::stdout();
let stderr = io::stderr();
let mut stdout = stdout.lock();
let mut stderr = stderr.lock();
match command {
McpCommand::List => mcp::run_list(&mut stdout, &mut stderr),
McpCommand::Test { server } => mcp::run_test(server, &mut stdout, &mut stderr),
McpCommand::Login { server } => mcp::run_login(server, &mut stdout, &mut stderr),
McpCommand::Logout { server } => mcp::run_logout(server, &mut stdout, &mut stderr),
}
}
}
}
fn print_summary(
cwd: &Path,
config: &EffectiveConfig,
instructions: &[crate::instructions::InstructionFile],
skills: &crate::skills::SkillDiscovery,
commands: &CommandRegistry,
session_id: Option<&str>,
prompt: &[String],
) {
println!("magi-code MVP foundation");
println!("cwd: {}", cwd.display());
println!("mc_home: {}", config.paths.root.display());
println!(
"provider: {}",
config.provider.as_deref().unwrap_or(OPENAI_CODEX_PROVIDER)
);
println!(
"model: {}",
config
.model
.as_deref()
.unwrap_or(crate::providers::DEFAULT_CODEX_MODEL)
);
println!(
"auth: {}",
if config.auth_state().is_ready() {
"<set>"
} else {
"<unset>"
}
);
println!("session: {}", session_id.unwrap_or("<disabled>"));
println!("instructions_loaded: {}", instructions.len());
println!("skills_loaded: {}", skills.skills.len());
println!("skill_diagnostics: {}", skills.diagnostics.len());
println!("commands_registered: {}", commands.list().len());
if !prompt.is_empty() {
println!("prompt: {}", prompt.join(" "));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
agent::{AgentSession, cancellation::AgentCancellation},
providers::{Provider, ProviderEvent, ProviderRequest, ToolCall},
shell::{ShellState, run_repl},
};
use serde_json::json;
use std::sync::Mutex;
use tempfile::TempDir;
struct ScriptedProvider {
responses: Mutex<Vec<Vec<ProviderEvent>>>,
}
impl ScriptedProvider {
fn new(responses: Vec<Vec<ProviderEvent>>) -> Self {
Self {
responses: Mutex::new(responses.into_iter().rev().collect()),
}
}
}
impl Provider for ScriptedProvider {
fn stream_cancellable(
&self,
_request: ProviderRequest,
_cancellation: &AgentCancellation,
on_event: &mut dyn FnMut(ProviderEvent) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
let events = self.responses.lock().unwrap().pop().unwrap_or_default();
for event in events {
on_event(event)?;
}
Ok(())
}
}
#[test]
fn color_help_and_values_parse() {
let help = <CliArgs as clap::CommandFactory>::command()
.render_long_help()
.to_string();
assert!(help.contains("--color"));
assert!(help.contains("auto"));
assert!(help.contains("always"));
assert!(help.contains("never"));
let args = CliArgs::parse_from(["magi-code", "--color", "always"]);
assert_eq!(args.color, Some(ColorChoice::Always));
}
#[test]
fn clap_definition_is_valid() {
<CliArgs as clap::CommandFactory>::command().debug_assert();
}
#[test]
fn api_key_help_warns_but_still_parses_for_compatibility() {
let help = <CliArgs as clap::CommandFactory>::command()
.render_long_help()
.to_string();
assert!(help.contains("--api-key"));
assert!(help.contains("Deprecated"));
let args = CliArgs::parse_from(["magi-code", "--api-key", "placeholder"]);
assert_eq!(args.api_key.as_deref(), Some("placeholder"));
}
#[test]
fn tui_launch_rejects_print_and_trailing_prompt_before_tty_gate() {
let args = CliArgs::parse_from(["magi-code", "--tui", "--print"]);
let err = validate_tui_launch(&args).unwrap_err().to_string();
assert!(err.contains("--tui cannot be combined with --print"));
let args = CliArgs::parse_from(["magi-code", "--tui", "hello"]);
let err = validate_tui_launch(&args).unwrap_err().to_string();
assert!(err.contains("does not support trailing PROMPT"));
}
#[test]
fn tui_experimental_launch_rejects_print_and_trailing_prompt_before_tty_gate() {
let args = CliArgs::parse_from(["magi-code", "--tui-experimental", "--print"]);
let err = validate_tui_launch(&args).unwrap_err().to_string();
assert!(err.contains("--tui-experimental cannot be combined with --print"));
let args = CliArgs::parse_from(["magi-code", "--tui-experimental", "hello"]);
let err = validate_tui_launch(&args).unwrap_err().to_string();
assert!(err.contains("--tui-experimental does not support trailing PROMPT"));
}
#[test]
fn tui_experimental_launch_rejects_tui_conflict_before_tty_gate() {
let args = CliArgs::parse_from(["magi-code", "--tui", "--tui-experimental"]);
let err = validate_tui_launch(&args).unwrap_err().to_string();
assert!(err.contains("--tui and --tui-experimental cannot be combined"));
}
#[test]
fn tui_experimental_launch_rejects_runtime_configuration_flags() {
let cases = [
(vec!["--provider", "local"], "--provider"),
(vec!["--model", "model"], "--model"),
(vec!["--api-key", "secret-value"], "--api-key"),
(vec!["--continue"], "--continue"),
(vec!["--resume", "session-id"], "--resume"),
(vec!["--no-session"], "--no-session"),
];
for (flags, expected_flag) in cases {
let mut argv = vec!["magi-code", "--tui-experimental"];
argv.extend(flags);
let args = CliArgs::parse_from(argv);
let error = validate_tui_launch(&args).unwrap_err().to_string();
assert!(error.contains("--tui-experimental"), "{error}");
assert!(error.contains(expected_flag), "{error}");
assert!(error.contains("--tui"), "{error}");
}
}
#[test]
fn print_mode_successful_provider_prompt_does_not_duplicate_session_events() {
let temp = TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap();
let provider = ScriptedProvider::new(vec![vec![
ProviderEvent::TextDelta("hello".to_string()),
ProviderEvent::Done,
]]);
let agent = AgentSession::new("model", &[], &crate::skills::SkillDiscovery::default());
let output =
print::run_print_mode_with_runner(Some(&session), temp.path(), |session, cwd| {
agent.run_print_with_tools(&provider, "say hi", None, session, cwd)
})
.unwrap();
assert_eq!(output.text, "hello");
let events = session.read_events().unwrap();
let user_inputs = events
.iter()
.filter(|event| event.event_type == "user_input")
.count();
let assistant_outputs = events
.iter()
.filter(|event| event.event_type == "assistant_output")
.count();
assert_eq!(user_inputs, 1);
assert_eq!(assistant_outputs, 1);
assert_eq!(events[0].event_type, "user_input");
assert_eq!(events.last().unwrap().event_type, "assistant_output");
}
#[test]
fn print_mode_streams_without_duplicate_final_output() {
let temp = TempDir::new().unwrap();
let provider = ScriptedProvider::new(vec![vec![
ProviderEvent::TextDelta("hel".to_string()),
ProviderEvent::TextDelta("lo".to_string()),
ProviderEvent::Done,
]]);
let agent = AgentSession::new("model", &[], &crate::skills::SkillDiscovery::default());
let mut output = Vec::new();
let run_output = print::run_print_mode_streaming_with_runner(
&mut output,
None,
temp.path(),
|session, cwd, sink| {
agent.run_print_with_tools_streaming_output(
&provider,
"say hi",
None,
session,
cwd,
Some(sink),
)
},
)
.unwrap();
assert_eq!(run_output.text, "hello");
assert_eq!(String::from_utf8(output).unwrap(), "hello\n");
}
#[test]
fn print_mode_streams_visible_read_tool_block() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("file.txt"), "hello").unwrap();
let provider = ScriptedProvider::new(vec![
vec![
ProviderEvent::ToolCall(ToolCall {
id: "call_1".to_string(),
name: "read".to_string(),
arguments: json!({"path":"file.txt:raw"}),
}),
ProviderEvent::Done,
],
vec![
ProviderEvent::TextDelta("done".to_string()),
ProviderEvent::Done,
],
]);
let tools = crate::tools::ToolRuntime::new(temp.path()).unwrap();
let agent = AgentSession::new("model", &[], &crate::skills::SkillDiscovery::default());
let mut output = Vec::new();
let run_output = print::run_print_mode_streaming_with_runner(
&mut output,
None,
temp.path(),
|session, cwd, sink| {
agent.run_print_with_tools_streaming_output(
&provider,
"read file",
Some(&tools),
session,
cwd,
Some(sink),
)
},
)
.unwrap();
let text = String::from_utf8(output).unwrap();
assert_eq!(run_output.text, "done");
assert!(!text.contains("TOOL START"));
assert!(text.contains("STATUS: success"));
assert!(text.contains("PATH: file.txt"));
assert!(text.contains("OUTPUT:\nhello"));
assert!(text.contains("OUTPUT:\nhello\ndone\n"));
assert_eq!(text.matches("STATUS: success").count(), 1);
}
#[test]
fn print_mode_keeps_newlines_around_tool_blocks_without_duplicate_final_answer() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("file.txt"), "hello").unwrap();
let provider = ScriptedProvider::new(vec![
vec![
ProviderEvent::TextDelta("before".to_string()),
ProviderEvent::ToolCall(ToolCall {
id: "call_1".to_string(),
name: "read".to_string(),
arguments: json!({"path":"file.txt:raw"}),
}),
ProviderEvent::Done,
],
vec![
ProviderEvent::TextDelta("after".to_string()),
ProviderEvent::Done,
],
]);
let tools = crate::tools::ToolRuntime::new(temp.path()).unwrap();
let agent = AgentSession::new("model", &[], &crate::skills::SkillDiscovery::default());
let mut output = Vec::new();
print::run_print_mode_streaming_with_runner(
&mut output,
None,
temp.path(),
|session, cwd, sink| {
agent.run_print_with_tools_streaming_output(
&provider,
"read file",
Some(&tools),
session,
cwd,
Some(sink),
)
},
)
.unwrap();
let text = String::from_utf8(output).unwrap();
assert!(text.contains("before\nSTATUS: success"));
assert!(text.contains("OUTPUT:\nhello\n\n\nafter\n"));
assert_eq!(text.matches("after").count(), 1);
}
#[test]
fn print_mode_streams_multiple_tool_blocks_in_order() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("file.txt"), "hello").unwrap();
std::fs::write(temp.path().join("other.txt"), "world").unwrap();
let provider = ScriptedProvider::new(vec![
vec![
ProviderEvent::ToolCall(ToolCall {
id: "call_1".to_string(),
name: "read".to_string(),
arguments: json!({"path":"file.txt:raw"}),
}),
ProviderEvent::Done,
],
vec![
ProviderEvent::ToolCall(ToolCall {
id: "call_2".to_string(),
name: "read".to_string(),
arguments: json!({"path":"other.txt"}),
}),
ProviderEvent::Done,
],
vec![
ProviderEvent::TextDelta("done".to_string()),
ProviderEvent::Done,
],
]);
let tools = crate::tools::ToolRuntime::new(temp.path()).unwrap();
let agent = AgentSession::new("model", &[], &crate::skills::SkillDiscovery::default());
let mut output = Vec::new();
print::run_print_mode_streaming_with_runner(
&mut output,
None,
temp.path(),
|session, cwd, sink| {
agent.run_print_with_tools_streaming_output(
&provider,
"read twice",
Some(&tools),
session,
cwd,
Some(sink),
)
},
)
.unwrap();
let text = String::from_utf8(output).unwrap();
let first = text.find("PATH: file.txt").unwrap();
let second = text.find("PATH: other.txt").unwrap();
assert!(first < second);
assert_eq!(text.matches("STATUS: success").count(), 2);
}
#[test]
fn print_mode_streams_more_than_eight_tool_iterations_without_diagnostic() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("file.txt"), "hello").unwrap();
let mut responses = (0..9)
.map(|index| {
vec![
ProviderEvent::ToolCall(ToolCall {
id: format!("call_{index}"),
name: "read".to_string(),
arguments: json!({"path":"file.txt:raw"}),
}),
ProviderEvent::Done,
]
})
.collect::<Vec<_>>();
responses.push(vec![
ProviderEvent::TextDelta("done".to_string()),
ProviderEvent::Done,
]);
let provider = ScriptedProvider::new(responses);
let tools = crate::tools::ToolRuntime::new(temp.path()).unwrap();
let agent = AgentSession::new("model", &[], &crate::skills::SkillDiscovery::default());
let mut output = Vec::new();
let run_output = print::run_print_mode_streaming_with_runner(
&mut output,
None,
temp.path(),
|session, cwd, sink| {
agent.run_print_with_tools_streaming_output(
&provider,
"loop",
Some(&tools),
session,
cwd,
Some(sink),
)
},
)
.unwrap();
let text = String::from_utf8(output).unwrap();
assert_eq!(run_output.text, "done");
assert!(text.contains("done"));
assert!(!text.contains("magi-code diagnostic"));
}
#[test]
fn print_mode_streaming_error_uses_newline_hygiene_without_replay() {
let temp = TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap();
let mut output = Vec::new();
let error = print::run_print_mode_streaming_with_runner(
&mut output,
Some(&session),
temp.path(),
|_session, _cwd, sink| {
sink.assistant_delta("partial")?;
anyhow::bail!("provider failed")
},
)
.unwrap_err()
.to_string();
assert_eq!(error, "provider failed");
assert_eq!(String::from_utf8(output).unwrap(), "partial\n");
let events = session.read_events().unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_type, "diagnostic");
}
#[test]
fn interactive_repl_wires_agent_tool_blocks_to_shell_writer() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("file.txt"), "hello").unwrap();
let provider = ScriptedProvider::new(vec![
vec![
ProviderEvent::ToolCall(ToolCall {
id: "call_1".to_string(),
name: "read".to_string(),
arguments: json!({"path":"file.txt:raw"}),
}),
ProviderEvent::Done,
],
vec![
ProviderEvent::TextDelta("done".to_string()),
ProviderEvent::Done,
],
]);
let tools = crate::tools::ToolRuntime::new(temp.path()).unwrap();
let agent = AgentSession::new("model", &[], &crate::skills::SkillDiscovery::default());
let commands = CommandRegistry::mvp();
let mut state = ShellState::new(
SessionManager::new(temp.path().join("sessions")),
None,
temp.path().to_path_buf(),
"model".to_string(),
crate::config::AuthState::Missing {
provider: OPENAI_CODEX_PROVIDER.to_string(),
},
);
let input = b"read file\n/quit\n";
let mut reader = &input[..];
let mut output = Vec::new();
run_repl(
&mut reader,
&mut output,
&commands,
&mut state,
|_state, prompt, writer| {
let mut sink = CliOutputSink::new(writer);
let result = agent.run_print_with_tools_streaming_output(
&provider,
prompt,
Some(&tools),
None,
temp.path(),
Some(&mut sink),
);
sink.finish_line()?;
result.map(|_| String::new())
},
)
.unwrap();
let text = String::from_utf8(output).unwrap();
assert!(!text.contains("TOOL START"));
assert!(text.contains("STATUS: success"));
assert!(text.contains("PATH: file.txt"));
assert!(text.contains("OUTPUT:\nhello"));
assert!(text.contains("OUTPUT:\nhello\ndone\n> bye\n"));
assert_eq!(text.matches("STATUS: success").count(), 1);
assert_eq!(text.matches("done").count(), 1);
}
#[test]
fn interactive_rich_repl_renders_transcript_header_once_and_prompts_once() {
let temp = TempDir::new().unwrap();
let provider = ScriptedProvider::new(vec![
vec![
ProviderEvent::TextDelta("second".to_string()),
ProviderEvent::Done,
],
vec![
ProviderEvent::TextDelta("first".to_string()),
ProviderEvent::Done,
],
]);
let agent = AgentSession::new("model", &[], &crate::skills::SkillDiscovery::default());
let commands = CommandRegistry::mvp();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap();
let mut state = ShellState::new(
manager,
Some(session),
temp.path().to_path_buf(),
"model".to_string(),
crate::config::AuthState::Missing {
provider: OPENAI_CODEX_PROVIDER.to_string(),
},
);
let input = b"one\ntwo\n/quit\n";
let mut reader = &input[..];
let mut output = Vec::new();
let mut header_rendered = false;
run_repl(
&mut reader,
&mut output,
&commands,
&mut state,
|state, prompt, writer| {
let mut sink = CliOutputSink::with_mode(
writer,
crate::output::OutputMode::Rich(crate::output::RichStyle {
color: false,
unicode: false,
}),
)
.suppress_session_header(header_rendered);
crate::agent::AgentOutputSink::output_event(
&mut sink,
crate::output::OutputEvent::SessionHeader {
session_id: state
.current_session
.as_ref()
.map(|session| session.id().to_string()),
model: "model".to_string(),
cwd: temp.path().to_path_buf(),
},
)?;
let result = agent.run_print_with_tools_streaming_output(
&provider,
prompt,
None,
state.current_session.as_ref(),
temp.path(),
Some(&mut sink),
);
sink.finish_line()?;
header_rendered = true;
result.map(|_| String::new())
},
)
.unwrap();
let text = String::from_utf8(output).unwrap();
assert_eq!(text.matches("magi-code ยท session").count(), 1);
assert_eq!(text.matches("You\n one").count(), 1);
assert_eq!(text.matches("You\n two").count(), 1);
assert_eq!(text.matches("Assistant").count(), 2);
assert_eq!(text.matches("first").count(), 1);
assert_eq!(text.matches("second").count(), 1);
assert!(!text.contains("\x1b["));
}
#[test]
fn print_mode_missing_key_returns_actionable_diagnostic_and_persists_events() {
let temp = TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap();
let config = EffectiveConfig {
provider: Some(OPENAI_CODEX_PROVIDER.to_string()),
model: None,
no_color: false,
file_autocomplete_respects_gitignore: true,
custom_providers: std::collections::BTreeMap::new(),
thinking_level: crate::thinking::ThinkingLevel::Default,
api_key: None,
auth: None,
paths: McPaths::from_root(temp.path().join("mc")),
};
let err = print::run_print_mode(
&config,
&[],
&crate::skills::SkillDiscovery::default(),
&crate::config::Settings::default(),
&["hello".to_string()],
Some(&session),
temp.path(),
)
.unwrap_err()
.to_string();
assert!(err.contains("missing auth"));
let events = session.read_events().unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_type, "diagnostic");
}
}