mod acp;
mod app;
mod atif;
mod background_wake;
mod capabilities;
mod capability_settings;
mod clipboard_paste;
mod codex_auth;
mod codex_driver;
mod config_schema;
mod config_service;
mod connectors;
mod extensions;
mod goal;
mod hooks_config;
mod host_ui;
mod image_input;
mod into;
mod mcp_config;
mod mcp_oauth;
mod mcp_oauth_login;
mod oauth_flow;
mod paste_attachment;
mod presentation;
mod runtime;
mod session;
mod session_log;
mod session_tasks_view;
mod settings;
#[cfg(test)]
mod test_env;
mod tools;
mod transcript;
mod user_ask;
mod version;
mod workspace_host;
mod worktree;
#[cfg(test)]
mod streaming_tests;
#[cfg(test)]
mod mcp_e2e_tests;
#[cfg(test)]
mod agent_scenarios;
use crate::capabilities::ClientUiContext;
use anyhow::{Context, Result};
extern crate everruns_integrations_daytona;
extern crate everruns_integrations_parallel;
use app::{App, COMPOSER_VIEWPORT_HEIGHT, maybe_reanchor_inline_viewport};
use clap::{Args, Parser, Subcommand};
use crossterm::event::{
DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags,
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use crossterm::{execute, queue};
use everruns_core::command::ExecuteCommandRequest;
use everruns_core::message::{ContentPart, MessageRole};
use everruns_core::typed_id::SessionId;
use mcp_config::{McpConfigScope, McpConfigStore};
use ratatui::backend::CrosstermBackend;
use ratatui::{Terminal, TerminalOptions, Viewport};
use runtime::{BuiltRuntime, ProviderChoice, ResolvedProviderChoice, resolve_for_settings};
use settings::SettingsStore;
use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Parser, Debug)]
#[command(
name = "yolop",
version = version::VERSION_DETAILS,
about = "Yolop coding agent — embedded terminal agent built on everruns-runtime"
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(short = 'C', long = "cwd")]
cwd: Option<PathBuf>,
#[arg(long, value_enum)]
provider: Option<ProviderArg>,
#[arg(short, long)]
model: Option<String>,
#[arg(long)]
reasoning_effort: Option<String>,
#[arg(short = 'p', long)]
print: Option<String>,
#[arg(
long = "image",
short = 'i',
value_name = "FILE",
value_delimiter = ',',
num_args = 1..
)]
images: Vec<PathBuf>,
#[arg(long, conflicts_with = "print")]
acp: bool,
#[arg(long)]
session: Option<String>,
#[arg(long)]
session_dir: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
trajectory_out: Option<PathBuf>,
#[arg(long)]
fullscreen: bool,
}
#[derive(Subcommand, Debug)]
enum Commands {
Version,
Into(IntoCommand),
Worktree(WorktreeArgs),
Mcp(McpArgs),
#[command(hide = true)]
TuikaGallery,
}
#[derive(Args, Debug)]
struct McpArgs {
#[command(subcommand)]
command: McpCommand,
}
#[derive(Subcommand, Debug)]
enum McpCommand {
List {
#[arg(long, default_value = "effective")]
scope: McpScopeArg,
#[arg(short = 'C', long = "cwd")]
cwd: Option<PathBuf>,
},
Add {
#[arg(long, default_value = "global")]
scope: McpScopeArg,
name: String,
#[arg(long = "type")]
transport_type: String,
#[arg(long)]
command: Option<String>,
#[arg(long, value_delimiter = ',', num_args = 0..)]
args: Vec<String>,
#[arg(long)]
url: Option<String>,
#[arg(long = "header", value_parser = parse_key_value)]
headers: Vec<(String, String)>,
#[arg(long)]
auth_mode: Option<String>,
#[arg(long)]
oauth_provider_id: Option<String>,
#[arg(long, default_value_t = false)]
disabled: bool,
#[arg(short = 'C', long = "cwd")]
cwd: Option<PathBuf>,
},
Remove {
#[arg(long, default_value = "global")]
scope: McpScopeArg,
name: String,
#[arg(short = 'C', long = "cwd")]
cwd: Option<PathBuf>,
},
Enable {
#[arg(long, default_value = "global")]
scope: McpScopeArg,
name: String,
#[arg(long, default_value_t = false)]
disable: bool,
#[arg(short = 'C', long = "cwd")]
cwd: Option<PathBuf>,
},
}
#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
enum McpScopeArg {
Global,
Workspace,
Effective,
}
fn parse_key_value(value: &str) -> Result<(String, String), String> {
let (key, val) = value
.split_once('=')
.ok_or_else(|| "expected KEY=VALUE".to_string())?;
if key.trim().is_empty() {
return Err("header key cannot be empty".to_string());
}
Ok((key.trim().to_string(), val.to_string()))
}
#[derive(Args, Debug)]
struct WorktreeArgs {
#[command(subcommand)]
command: WorktreeCommand,
}
#[derive(Subcommand, Debug)]
enum WorktreeCommand {
List,
Prune {
#[arg(long)]
dry_run: bool,
#[arg(long)]
session_dir: Option<PathBuf>,
},
}
#[derive(Args, Debug)]
struct IntoCommand {
#[command(subcommand)]
target: IntoTarget,
}
#[derive(Subcommand, Debug)]
enum IntoTarget {
Paseo(PaseoIntoArgs),
Zed(ZedIntoArgs),
}
#[derive(Args, Debug)]
struct PaseoIntoArgs {
#[arg(long)]
force: bool,
}
#[derive(Args, Debug)]
struct ZedIntoArgs {
#[arg(long)]
force: bool,
}
#[derive(clap::ValueEnum, Debug, Clone, Copy)]
enum ProviderArg {
Anthropic,
Codex,
Openai,
Google,
Openrouter,
Ollama,
Custom,
#[value(name = "llmsim", alias = "sim")]
Sim,
}
fn provider_name_for_arg(arg: ProviderArg) -> &'static str {
match arg {
ProviderArg::Anthropic => "anthropic",
ProviderArg::Codex => "codex",
ProviderArg::Openai => "openai",
ProviderArg::Google => "google",
ProviderArg::Openrouter => "openrouter",
ProviderArg::Ollama => "ollama",
ProviderArg::Custom => "custom",
ProviderArg::Sim => "llmsim",
}
}
fn pick_provider(cli: &Cli, settings: &SettingsStore) -> (ProviderChoice, Vec<String>) {
let snapshot = settings.snapshot();
let cli_reasoning_effort = runtime::normalize_reasoning_effort(cli.reasoning_effort.clone());
let mut notes = Vec::new();
let resolved = if let Some(arg) = cli.provider {
resolve_for_settings(provider_name_for_arg(arg), &snapshot)
.expect("ProviderArg names are always valid")
} else if let Some(saved) = snapshot.default_provider.as_deref() {
match resolve_for_settings(saved, &snapshot) {
Ok(resolved) => resolved,
Err(err) => {
eprintln!("yolop: ignoring saved provider `{saved}`: {err}");
let auto = ProviderChoice::from_env_or_settings(&snapshot);
resolve_for_settings(auto.provider_name(), &snapshot).unwrap_or(
ResolvedProviderChoice {
choice: auto,
source: runtime::ModelResolutionSource::ProviderDefault,
notes: vec![],
},
)
}
}
} else {
let auto = ProviderChoice::from_env_or_settings(&snapshot);
resolve_for_settings(auto.provider_name(), &snapshot).unwrap_or(ResolvedProviderChoice {
choice: auto,
source: runtime::ModelResolutionSource::ProviderDefault,
notes: vec![],
})
};
notes.extend(resolved.notes);
let base = resolved.choice;
let selected = if let Some(model) = cli.model.clone() {
let spec = match cli_reasoning_effort.clone() {
Some(effort) => format!("{model} {effort}"),
None => model,
};
match base.resolve_model_spec(&spec) {
Ok(selected) => selected,
Err(err) => {
notes.push(format!("model override ignored: {err}"));
base
}
}
} else {
base
};
let selected = match (selected, cli_reasoning_effort) {
(
ProviderChoice::Anthropic {
model,
reasoning_effort,
},
effort,
) => ProviderChoice::Anthropic {
model,
reasoning_effort: effort.or(reasoning_effort),
},
(
ProviderChoice::OpenAi {
model,
reasoning_effort,
},
effort,
) => ProviderChoice::OpenAi {
model,
reasoning_effort: effort.or(reasoning_effort),
},
(
ProviderChoice::Codex {
model,
reasoning_effort,
},
effort,
) => ProviderChoice::Codex {
model,
reasoning_effort: effort.or(reasoning_effort),
},
(
ProviderChoice::Google {
model,
base_url,
reasoning_effort,
},
effort,
) => ProviderChoice::Google {
model,
base_url,
reasoning_effort: effort.or(reasoning_effort),
},
(
ProviderChoice::OpenRouter {
model,
base_url,
reasoning_effort,
},
effort,
) => ProviderChoice::OpenRouter {
model,
base_url,
reasoning_effort: effort.or(reasoning_effort),
},
(
ProviderChoice::Ollama {
model,
base_url,
reasoning_effort,
},
effort,
) => ProviderChoice::Ollama {
model,
base_url,
reasoning_effort: effort.or(reasoning_effort),
},
(
ProviderChoice::Custom {
model,
reasoning_effort,
},
effort,
) => ProviderChoice::Custom {
model,
reasoning_effort: effort.or(reasoning_effort),
},
(other, _) => other,
};
(selected, notes)
}
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("error")),
)
.with_writer(io::stderr)
.init();
let cli = Cli::parse();
if let Some(command) = cli.command {
return run_command(command);
}
let settings_path = settings::default_settings_path().unwrap_or_else(|| {
eprintln!(
"yolop: no platform config dir resolvable — settings will not persist across runs"
);
std::path::PathBuf::from("/dev/null/yolop/settings.toml")
});
let settings = Arc::new(SettingsStore::open(settings_path));
let (mut provider, mut notes) = pick_provider(&cli, &settings);
let snapshot = settings.snapshot();
let (reconciled, catalog_notes) =
capabilities::model_discovery::reconcile_provider_with_catalog(provider, &snapshot).await;
provider = reconciled;
notes.extend(catalog_notes);
for note in notes {
eprintln!("yolop: {note}");
}
let resume_session_id = match cli.session.as_deref() {
Some(raw) => Some(
raw.parse()
.map_err(|e| anyhow::anyhow!("invalid --session id `{raw}`: {e}"))?,
),
None => None,
};
let sessions_dir = match cli.session_dir.clone() {
Some(p) => p,
None => session_log::default_sessions_dir()?,
};
let cwd = resolve_workspace_root(cli.cwd.clone(), resume_session_id, &sessions_dir)?;
if cli.acp {
if cli.trajectory_out.is_some() {
eprintln!("yolop: --trajectory-out is ignored in --acp mode");
}
return acp::run_stdio(provider, settings, sessions_dir).await;
}
let interactive = cli.print.is_none();
let runtime = runtime::build_with_options(
cwd,
provider,
resume_session_id,
sessions_dir,
settings,
runtime::BuildOptions {
client_commands: interactive,
client_ui: if interactive {
ClientUiContext::Tui
} else {
ClientUiContext::Print
},
session_kind: if interactive {
session_log::SessionKind::Interactive
} else {
session_log::SessionKind::Print
},
initial_prompt: cli.print.clone(),
..Default::default()
},
)
.await?;
if let Some(prompt) = cli.print {
let image_parts = image_input::load_image_parts(&cli.images)?;
return run_print_mode(runtime, prompt, image_parts, cli.trajectory_out).await;
}
let pending_images = image_input::load_image_parts(&cli.images)?;
run_tui(runtime, pending_images, cli.trajectory_out, cli.fullscreen).await
}
fn resolve_workspace_root(
cli_cwd: Option<PathBuf>,
resume_session_id: Option<SessionId>,
sessions_dir: &Path,
) -> Result<PathBuf> {
if let Some(cwd) = cli_cwd {
return Ok(cwd);
}
if let Some(session_id) = resume_session_id {
let session_dir = session_log::session_dir_path(sessions_dir, session_id);
if let Some(saved) = session_log::read_session_workspace(&session_dir)? {
return Ok(saved);
}
}
std::env::current_dir().context("resolve current workspace directory")
}
fn run_mcp_command(command: McpCommand) -> Result<()> {
use crate::mcp_config::{McpServerEntry, McpServerSummary};
use everruns_core::{McpServerTransportType, ScopedMcpServer};
use std::collections::HashMap;
fn store(cwd: Option<PathBuf>) -> Result<McpConfigStore> {
let workspace_root = match cwd {
Some(path) => path,
None => std::env::current_dir().context("resolve current workspace directory")?,
};
Ok(McpConfigStore::default_for_workspace(&workspace_root))
}
fn write_scope(scope: McpScopeArg) -> Result<McpConfigScope> {
match scope {
McpScopeArg::Global => Ok(McpConfigScope::Global),
McpScopeArg::Workspace => Ok(McpConfigScope::Workspace),
McpScopeArg::Effective => anyhow::bail!(
"effective scope is read-only; use --scope global or --scope workspace"
),
}
}
match command {
McpCommand::List { scope, cwd } => {
let store = store(cwd)?;
let servers: Vec<McpServerSummary> = match scope {
McpScopeArg::Global => store
.effective()
.map_err(anyhow::Error::msg)?
.servers
.into_iter()
.filter(|server| server.scope == McpConfigScope::Global)
.collect(),
McpScopeArg::Workspace => store
.effective()
.map_err(anyhow::Error::msg)?
.servers
.into_iter()
.filter(|server| server.scope == McpConfigScope::Workspace)
.collect(),
McpScopeArg::Effective => store
.effective()
.map_err(anyhow::Error::msg)?
.servers
.into_iter()
.filter(|server| server.effective)
.collect(),
};
if servers.is_empty() {
println!("no MCP servers configured");
} else {
for server in servers {
let enabled = if server.enabled {
"enabled"
} else {
"disabled"
};
println!(
"{} {} {}",
server.name,
mcp_scope_label(server.scope),
enabled
);
}
}
Ok(())
}
McpCommand::Add {
scope,
name,
transport_type,
command,
args,
url,
headers,
auth_mode,
oauth_provider_id,
disabled,
cwd,
} => {
let transport = transport_type.to_ascii_lowercase();
let server = match transport.as_str() {
"stdio" => ScopedMcpServer {
transport_type: McpServerTransportType::Stdio,
command: Some(command.context("--command is required for stdio MCP servers")?),
args,
env: HashMap::new(),
..ScopedMcpServer::default()
},
"http" | "sse" => ScopedMcpServer {
transport_type: McpServerTransportType::Http,
url: url.context("--url is required for remote MCP servers")?,
headers: headers.into_iter().collect(),
auth_mode: auth_mode
.as_deref()
.map(parse_mcp_auth_mode)
.transpose()?
.unwrap_or_default(),
oauth_provider_id,
..ScopedMcpServer::default()
},
other => anyhow::bail!(
"unsupported MCP transport `{other}`; expected stdio, http, or sse"
),
};
let store = store(cwd)?;
let _summary = store
.upsert(
write_scope(scope)?,
&name,
McpServerEntry {
enabled: !disabled,
server,
},
)
.map_err(anyhow::Error::msg)?;
let action = "saved";
println!(
"{action} MCP server `{name}` in {} scope",
mcp_scope_label(write_scope(scope)?)
);
println!(
"restart or start a new yolop session for MCP connection changes to take effect"
);
Ok(())
}
McpCommand::Remove { scope, name, cwd } => {
let store = store(cwd)?;
let removed = store
.remove(write_scope(scope)?, &name)
.map_err(anyhow::Error::msg)?;
if removed {
println!(
"removed MCP server `{name}` from {} scope",
mcp_scope_label(write_scope(scope)?)
);
println!(
"restart or start a new yolop session for MCP connection changes to take effect"
);
} else {
println!(
"MCP server `{name}` was not configured in {} scope",
mcp_scope_label(write_scope(scope)?)
);
}
Ok(())
}
McpCommand::Enable {
scope,
name,
disable,
cwd,
} => {
let store = store(cwd)?;
store
.set_enabled(write_scope(scope)?, &name, !disable)
.map_err(anyhow::Error::msg)?;
println!(
"{} MCP server `{name}` in {} scope",
if disable { "disabled" } else { "enabled" },
mcp_scope_label(write_scope(scope)?)
);
println!(
"restart or start a new yolop session for MCP connection changes to take effect"
);
Ok(())
}
}
}
fn mcp_scope_label(scope: McpConfigScope) -> &'static str {
match scope {
McpConfigScope::Global => "global",
McpConfigScope::Workspace => "workspace",
}
}
fn parse_mcp_auth_mode(value: &str) -> Result<everruns_core::McpServerAuthMode> {
match value.to_ascii_lowercase().as_str() {
"none" => Ok(everruns_core::McpServerAuthMode::None),
"bearer" | "api_key" | "api-key" => Ok(everruns_core::McpServerAuthMode::ApiKey),
"oauth" | "o_auth" => Ok(everruns_core::McpServerAuthMode::OAuth),
other => anyhow::bail!(
"unsupported auth mode `{other}`; expected none, bearer/api_key, or oauth"
),
}
}
fn run_worktree_command(command: WorktreeCommand) -> Result<()> {
match command {
WorktreeCommand::List => {
let paths = worktree::list_worktree_paths_on_disk()?;
if paths.is_empty() {
println!("no yolop worktrees found on disk");
} else {
for path in paths {
println!("{}", path.display());
}
}
Ok(())
}
WorktreeCommand::Prune {
dry_run,
session_dir,
} => {
let sessions_dir = match session_dir {
Some(path) => path,
None => session_log::default_sessions_dir()?,
};
let report = worktree::prune_orphan_worktrees(&sessions_dir, dry_run)?;
let action = if dry_run { "would remove" } else { "removed" };
for path in &report.removed {
println!("{action}: {}", path.display());
}
println!(
"kept {} referenced worktree(s); {} orphan(s) {action}",
report.kept,
report.removed.len()
);
for err in &report.errors {
eprintln!("error: {err}");
}
if report.errors.is_empty() {
Ok(())
} else {
std::process::exit(1);
}
}
}
}
fn run_command(command: Commands) -> Result<()> {
match command {
Commands::Version => {
println!("{}", version::VERSION_LINE);
Ok(())
}
Commands::Worktree(args) => run_worktree_command(args.command),
Commands::Mcp(args) => run_mcp_command(args.command),
Commands::TuikaGallery => run_tuika_gallery(),
Commands::Into(into) => match into.target {
IntoTarget::Paseo(args) => {
let command = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("yolop"));
let result = into::into_paseo(into::PaseoIntoOptions {
settings_path: None,
agent_name: "yolop".to_string(),
command,
force: args.force,
})?;
match result.status {
into::IntoStatus::Unchanged => {
println!(
"yolop: Paseo already has `{}` ACP provider configured at {}",
result.agent_name,
result.settings_path.display()
);
}
into::IntoStatus::Created => {
println!(
"yolop: added `{}` ACP provider to {}",
result.agent_name,
result.settings_path.display()
);
}
into::IntoStatus::Updated => {
println!(
"yolop: updated `{}` ACP provider in {}",
result.agent_name,
result.settings_path.display()
);
}
}
println!("yolop: Paseo command: {} --acp", result.command);
println!("yolop: restart the Paseo daemon to reload this configuration");
Ok(())
}
IntoTarget::Zed(args) => {
let command = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("yolop"));
let result = into::into_zed(into::ZedIntoOptions {
settings_path: None,
agent_name: "yolop".to_string(),
command,
force: args.force,
})?;
match result.status {
into::IntoStatus::Unchanged => {
println!(
"yolop: Zed already has `{}` configured at {}",
result.agent_name,
result.settings_path.display()
);
}
into::IntoStatus::Created => {
println!(
"yolop: added `{}` ACP agent to {}",
result.agent_name,
result.settings_path.display()
);
}
into::IntoStatus::Updated => {
println!(
"yolop: updated `{}` ACP agent in {}",
result.agent_name,
result.settings_path.display()
);
}
}
println!("yolop: Zed command: {} --acp", result.command);
Ok(())
}
},
}
}
fn run_tuika_gallery() -> Result<()> {
use crossterm::event::{self, Event as CtEvent, KeyCode as CtKeyCode, KeyEventKind};
use std::time::Duration;
let mut raw = RawModeGuard::new()?;
let mut alt = tuika::AltScreen::enter()?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)?;
let theme = tuika::Theme::default();
let mut progress = tuika::TerminalProgress::new();
progress.indeterminate();
let mut frame: u64 = 0;
loop {
terminal.draw(|f| {
let area = f.area();
let root = build_gallery(frame, &theme);
tuika::paint(f.buffer_mut(), area, &theme, root.as_ref(), &[]);
})?;
if event::poll(Duration::from_millis(80))?
&& let CtEvent::Key(key) = event::read()?
&& key.kind != KeyEventKind::Release
&& matches!(key.code, CtKeyCode::Char('q') | CtKeyCode::Esc)
{
break;
}
frame = frame.wrapping_add(1);
}
progress.clear();
let _ = terminal.clear();
drop(terminal);
alt.leave();
raw.disable()?;
Ok(())
}
fn build_gallery(frame: u64, theme: &tuika::Theme) -> tuika::Element {
use ratatui::text::{Line, Span};
use tuika::{Loader, ProgressBar, Spinner, SpinnerStyle, Text};
let labeled_spinner = |style: SpinnerStyle, label: &str| -> tuika::Element {
tuika::view! {
row(gap = 1) {
fixed(1) { node(Spinner::new(frame).style(style)) }
text(label.to_string())
}
}
};
let animated = tuika::anim::ping_pong(frame, 120);
tuika::view! {
col(
background = ratatui::style::Style::default().bg(theme.background),
padding = tuika::Padding::all(1),
gap = 1
) {
fixed(5) {
boxed(title = Line::from(Span::styled(" spinners ", theme.accent_style()))) {
col {
fixed(1) { node(labeled_spinner(SpinnerStyle::Braille, "Braille")) }
fixed(1) { node(labeled_spinner(SpinnerStyle::Line, "Line")) }
fixed(1) { node(labeled_spinner(SpinnerStyle::Dots, "Dots")) }
}
}
}
fixed(6) {
boxed(title = Line::from(Span::styled(" progress ", theme.accent_style()))) {
col {
fixed(1) { node(ProgressBar::determinate(0.25).percent(true)) }
fixed(1) { node(ProgressBar::determinate(0.60).percent(true)) }
fixed(1) { node(ProgressBar::determinate(animated).percent(true)) }
fixed(1) { node(ProgressBar::indeterminate(frame)) }
}
}
}
fixed(3) {
boxed(title = Line::from(Span::styled(" loader ", theme.accent_style()))) {
node(Loader::new(frame, "working…").hint("esc to quit"))
}
}
grow(1) { spacer() }
fixed(1) {
node(Text::new(vec![Line::from(Span::styled(
"tuika gallery — native progress is live in the taskbar/top bar · press q to quit",
theme.muted_style(),
))]))
}
}
}
}
async fn run_tui(
runtime: BuiltRuntime,
pending_images: Vec<ContentPart>,
trajectory_out: Option<PathBuf>,
fullscreen: bool,
) -> Result<()> {
let trajectory_handles = runtime.handles.clone();
let trajectory_model = runtime.model.clone();
let mut raw_mode = RawModeGuard::new()?;
let mut keyboard_enhancements = KeyboardEnhancementGuard::new();
let mut bracketed_paste = BracketedPasteGuard::new();
let alt_screen = if fullscreen {
Some(tuika::AltScreen::enter()?)
} else {
None
};
let stdout = io::stdout();
let backend = CrosstermBackend::new(stdout);
let viewport = if fullscreen {
Viewport::Fullscreen
} else {
Viewport::Inline(COMPOSER_VIEWPORT_HEIGHT)
};
let mut terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
if !fullscreen && let Err(err) = maybe_reanchor_inline_viewport(&mut terminal) {
tracing::warn!("inline viewport anchoring failed, starting unanchored: {err:#}");
}
let mut app = App::new(runtime, pending_images);
app.enable_native_progress();
if fullscreen {
app.set_render_mode(app::RenderMode::Fullscreen);
}
let result = app.run(&mut terminal).await;
let show_resume_hint = app.should_show_resume_hint();
let session_id = app.session_id();
if let Err(err) = terminal.clear() {
tracing::warn!("terminal clear failed: {err:#}");
}
if let Err(err) = terminal.show_cursor() {
tracing::warn!("cursor restore failed: {err:#}");
}
drop(terminal);
if let Some(mut alt) = alt_screen {
alt.leave();
}
bracketed_paste.disable();
keyboard_enhancements.disable();
raw_mode.disable()?;
write_trajectory_if_requested(
&trajectory_handles,
&trajectory_model,
trajectory_out.as_deref(),
)
.await;
if show_resume_hint {
println!();
print_resume_divider();
println!("Resume with yolop --session {session_id}");
println!();
print_centered_ukraine_banner();
}
result
}
struct RawModeGuard {
active: bool,
}
impl RawModeGuard {
fn new() -> Result<Self> {
enable_raw_mode()?;
Ok(Self { active: true })
}
fn disable(&mut self) -> Result<()> {
if self.active {
disable_raw_mode()?;
self.active = false;
}
Ok(())
}
}
impl Drop for RawModeGuard {
fn drop(&mut self) {
if self.active {
let _ = disable_raw_mode();
self.active = false;
}
}
}
struct KeyboardEnhancementGuard {
active: bool,
}
impl KeyboardEnhancementGuard {
fn new() -> Self {
let flags = KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
let mut stdout = io::stdout();
let active = execute!(stdout, PushKeyboardEnhancementFlags(flags)).is_ok();
Self { active }
}
fn disable(&mut self) {
if self.active {
let mut stdout = io::stdout();
let _ = queue!(stdout, PopKeyboardEnhancementFlags);
let _ = stdout.flush();
self.active = false;
}
}
}
impl Drop for KeyboardEnhancementGuard {
fn drop(&mut self) {
self.disable();
}
}
struct BracketedPasteGuard {
active: bool,
}
impl BracketedPasteGuard {
fn new() -> Self {
let mut stdout = io::stdout();
let active = execute!(stdout, EnableBracketedPaste).is_ok();
Self { active }
}
fn disable(&mut self) {
if self.active {
let mut stdout = io::stdout();
let _ = execute!(stdout, DisableBracketedPaste);
self.active = false;
}
}
}
impl Drop for BracketedPasteGuard {
fn drop(&mut self) {
self.disable();
}
}
fn print_resume_divider() {
let width = crossterm::terminal::size()
.map(|(width, _)| width as usize)
.unwrap_or(80)
.max(1);
println!("\x1b[38;2;45;91;158m{}\x1b[0m", "─".repeat(width));
}
fn print_centered_ukraine_banner() {
let text = ">> Зроблено в Україні <<";
let width = crossterm::terminal::size()
.map(|(width, _)| width as usize)
.unwrap_or(0);
let pad = width.saturating_sub(text.chars().count()) / 2;
println!(
"{}\x1b[38;2;45;91;158m>> Зроблено в \x1b[38;2;126;94;19mУкраїні <<\x1b[0m",
" ".repeat(pad)
);
}
async fn write_trajectory_if_requested(
handles: &runtime::RuntimeHandles,
model: &runtime::ModelState,
path: Option<&Path>,
) {
let Some(path) = path else { return };
let events = match handles.runtime.events().await {
Ok(events) => events,
Err(err) => {
eprintln!("yolop: trajectory export failed to read session events: {err}");
return;
}
};
let trajectory = atif::trajectory_from_events(
atif::AgentInfo {
name: "yolop".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
model_name: Some(model.model_id()),
},
handles.session_id,
&events,
);
if let Err(err) = atif::write_trajectory_file(path, &trajectory) {
eprintln!(
"yolop: failed to write trajectory to {}: {err}",
path.display()
);
}
}
async fn run_print_mode(
runtime: BuiltRuntime,
prompt: String,
images: Vec<ContentPart>,
trajectory_out: Option<PathBuf>,
) -> Result<()> {
let BuiltRuntime {
handles,
startup,
model,
goal_store,
user_ask_store,
user_ask_enabled,
worktree,
..
} = runtime;
let color = io::stdout().is_terminal();
if let Err(err) = worktree.ensure_before_turn(prompt.trim()) {
eprintln!("worktree: {err}");
}
let _ = (&startup.session_dir, &startup.session_log_path);
let trimmed = prompt.trim();
if let Some(goal_args) = trimmed.strip_prefix("/goal") {
let success = run_print_goal(
&handles,
&worktree,
&model,
&goal_store,
goal_args.trim(),
color,
)
.await?;
write_trajectory_if_requested(&handles, &model, trajectory_out.as_deref()).await;
if !success {
std::process::exit(1);
}
return Ok(());
}
if user_ask_enabled
&& let Err(err) = user_ask_store.record_user_prompt(handles.session_id, trimmed)
{
eprintln!("user ask: {err}");
}
let turn = collect_print_turn(&handles, &worktree, &model, trimmed, images).await?;
print_final_output(&turn.output);
if !turn.result.success {
write_trajectory_if_requested(&handles, &model, trajectory_out.as_deref()).await;
std::process::exit(1);
}
if user_ask_enabled && user_ask_store.is_active(handles.session_id) {
let evaluation = handles
.runtime
.execute_command(
handles.session_id,
ExecuteCommandRequest {
name: "ask".to_string(),
arguments: Some(user_ask::USER_ASK_EVALUATE_ARG.to_string()),
controls: None,
},
)
.await?;
if evaluation.success {
if let Ok(parsed) = user_ask::parse_evaluation_response(&evaluation.message)
&& parsed.outcome == user_ask::AskOutcome::Blocked
{
handles.report_herdr_state(capabilities::herdr::HerdrState::Blocked);
}
} else {
eprintln!("user ask evaluation failed: {}", evaluation.message);
}
}
write_trajectory_if_requested(&handles, &model, trajectory_out.as_deref()).await;
Ok(())
}
async fn run_print_goal(
handles: &runtime::RuntimeHandles,
worktree: &crate::worktree::WorktreeManager,
model: &runtime::ModelState,
goal_store: &goal::GoalStore,
arguments: &str,
color: bool,
) -> Result<bool> {
let session_id = handles.session_id;
let request = ExecuteCommandRequest {
name: "goal".to_string(),
arguments: if arguments.is_empty() {
None
} else {
Some(arguments.to_string())
},
controls: None,
};
let result = handles.runtime.execute_command(session_id, request).await?;
if !result.success {
eprintln!("goal command failed: {}", result.message);
return Ok(false);
}
if !goal_store.take_pending_turn(session_id) {
if !result.message.is_empty() {
println!("{}", paint(color, "90", &result.message));
}
return Ok(true);
}
let Some(mut turn_prompt) = goal_store.active_condition(session_id) else {
return Ok(true);
};
loop {
let turn = collect_print_turn(handles, worktree, model, &turn_prompt, vec![]).await?;
if !turn.result.success {
print_final_output(&turn.output);
return Ok(false);
}
if !goal_store.is_active(session_id) {
print_final_output(&turn.output);
return Ok(true);
}
let evaluation = handles
.runtime
.execute_command(
session_id,
ExecuteCommandRequest {
name: "goal".to_string(),
arguments: Some(goal::GOAL_EVALUATE_ARG.to_string()),
controls: None,
},
)
.await?;
if !evaluation.success {
eprintln!("goal evaluation failed: {}", evaluation.message);
return Ok(false);
}
let parsed = goal::parse_evaluation_response(&evaluation.message)?;
if parsed.met {
print_final_output(&turn.output);
return Ok(true);
}
turn_prompt = goal_store
.continuation_prompt(session_id)
.unwrap_or_else(|| turn_prompt.clone());
}
}
struct PrintTurn {
result: everruns_runtime::TurnResult,
output: Vec<String>,
}
async fn collect_print_turn(
handles: &runtime::RuntimeHandles,
worktree: &crate::worktree::WorktreeManager,
model: &runtime::ModelState,
prompt: &str,
images: Vec<ContentPart>,
) -> Result<PrintTurn> {
if let Err(err) = worktree.ensure_before_turn(prompt) {
eprintln!("worktree: {err}");
}
let before_msgs = handles
.runtime
.messages(handles.session_id)
.await
.map(|m| m.len())
.unwrap_or(0);
let input = model.input_message_with_images(prompt, images);
let result = handles.runtime.run_turn(handles.session_id, input).await?;
let messages = handles
.runtime
.messages(handles.session_id)
.await
.unwrap_or_default();
let mut output = Vec::new();
for msg in messages.iter().skip(before_msgs) {
if msg.role == MessageRole::Agent
&& !msg.has_tool_calls()
&& let Some(text) = msg.text()
{
let t = text.trim();
if !t.is_empty() {
output.push(t.to_string());
}
}
}
if !result.success
&& let Some(err) = &result.error
{
eprintln!("turn error: {err}");
}
Ok(PrintTurn { result, output })
}
fn print_final_output(output: &[String]) {
for (index, text) in output.iter().enumerate() {
if index > 0 {
println!();
}
println!("{text}");
}
}
fn paint(enabled: bool, code: &str, text: &str) -> String {
if enabled {
format!("\x1b[{code}m{text}\x1b[0m")
} else {
text.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cli_with_reasoning_effort(reasoning_effort: Option<&str>) -> Cli {
Cli {
command: None,
cwd: None,
provider: Some(ProviderArg::Openrouter),
model: Some("nvidia/nemotron-3-super-120b-a12b".to_string()),
reasoning_effort: reasoning_effort.map(str::to_string),
print: None,
images: vec![],
acp: false,
session: None,
session_dir: None,
trajectory_out: None,
fullscreen: false,
}
}
#[test]
fn pick_provider_normalizes_cli_reasoning_effort() {
let tmp = tempfile::tempdir().expect("settings tempdir");
let settings = SettingsStore::open(tmp.path().join("settings.toml"));
let (provider, _notes) =
pick_provider(&cli_with_reasoning_effort(Some(" HIGH ")), &settings);
assert_eq!(
provider.label(),
"openrouter/nvidia/nemotron-3-super-120b-a12b high"
);
}
#[test]
fn pick_provider_ignores_blank_cli_reasoning_effort() {
let tmp = tempfile::tempdir().expect("settings tempdir");
let settings = SettingsStore::open(tmp.path().join("settings.toml"));
let (provider, _notes) = pick_provider(&cli_with_reasoning_effort(Some(" ")), &settings);
assert_eq!(
provider.label(),
"openrouter/nvidia/nemotron-3-super-120b-a12b"
);
}
#[test]
fn pick_provider_applies_saved_model_for_saved_provider() {
let _guard = crate::test_env::lock();
unsafe {
std::env::remove_var("EVERRUNS_CLI_MODEL");
}
let tmp = tempfile::tempdir().expect("settings tempdir");
let path = tmp.path().join("settings.toml");
std::fs::write(
&path,
"provider = \"openai\"\n\n[models]\nopenai = \"gpt-5.4 high\"\n",
)
.expect("write settings");
let settings = SettingsStore::open(path);
let cli = Cli {
command: None,
cwd: None,
provider: None,
model: None,
reasoning_effort: None,
print: None,
images: vec![],
acp: false,
session: None,
session_dir: None,
trajectory_out: None,
fullscreen: false,
};
let (provider, _notes) = pick_provider(&cli, &settings);
assert_eq!(provider.label(), "openai/gpt-5.4 high");
}
#[test]
fn resolve_workspace_root_uses_saved_session_workspace() {
let sessions = tempfile::tempdir().expect("sessions tempdir");
let workspace = tempfile::tempdir().expect("workspace tempdir");
let session_id = SessionId::from_seed(42);
let session_dir = session_log::session_dir_path(sessions.path(), session_id);
session_log::write_session_workspace(
&session_dir,
&session_log::SessionWorkspaceMetadata::new(workspace.path().to_path_buf(), None),
)
.expect("write workspace metadata");
let resolved =
resolve_workspace_root(None, Some(session_id), sessions.path()).expect("resolve");
assert_eq!(resolved, workspace.path());
}
#[test]
fn resolve_workspace_root_prefers_explicit_cwd() {
let sessions = tempfile::tempdir().expect("sessions tempdir");
let saved = tempfile::tempdir().expect("saved workspace tempdir");
let explicit = tempfile::tempdir().expect("explicit workspace tempdir");
let session_id = SessionId::from_seed(43);
let session_dir = session_log::session_dir_path(sessions.path(), session_id);
session_log::write_session_workspace(
&session_dir,
&session_log::SessionWorkspaceMetadata::new(saved.path().to_path_buf(), None),
)
.expect("write workspace metadata");
let resolved = resolve_workspace_root(
Some(explicit.path().to_path_buf()),
Some(session_id),
sessions.path(),
)
.expect("resolve");
assert_eq!(resolved, explicit.path());
}
#[test]
fn inline_viewport_anchor_fills_space_above_bottom_target() {
assert_eq!(app::rows_below_inline_viewport(4, 18, 60), 38);
}
#[test]
fn inline_viewport_anchor_does_not_scroll_when_already_low_enough() {
assert_eq!(app::rows_below_inline_viewport(42, 18, 60), 0);
assert_eq!(app::rows_below_inline_viewport(50, 18, 60), 0);
}
#[test]
fn inline_viewport_anchor_handles_small_terminals() {
assert_eq!(app::rows_below_inline_viewport(0, 18, 10), 0);
}
}