mod init;
mod input;
mod render;

use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::fs;
use std::io::{self, Read, Write};
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use dialoguer::Select;
use console::style;
use indicatif::{ProgressBar, ProgressStyle};

use api::{
    TernlangClient, AuthSource, ContentBlockDelta, InputContentBlock,
    InputMessage, MessageRequest, OutputContentBlock,
    StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
};
use commands::{render_slash_command_help, slash_command_specs, SlashCommand};
use compat_harness::{extract_manifest, UpstreamPaths};
use init::initialize_repo;
use render::TerminalRenderer;
use runtime::{
    clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
    parse_oauth_callback_request_target, save_oauth_credentials, ApiClient, ApiRequest,
    AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, ContentBlock,
    ConversationMessage, ConversationRuntime, MessageRole, OAuthAuthorizationRequest, OAuthConfig,
    PermissionMode, PermissionPolicy,
    ProjectContext, RuntimeError, Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
};
use serde_json::json;
use tools::{execute_tool, mvp_tool_specs, ToolSpec};

const DEFAULT_MODEL: &str = "gemini-2.5-pro-preview";
fn max_tokens_for_model(model: &str) -> u32 {
    if model.contains("haiku") {
        16_000
    } else {
        32_000
    }
}
const DEFAULT_DATE: &str = "2024-03-25";
const DEFAULT_OAUTH_CALLBACK_PORT: u16 = 4545;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const BUILD_TARGET: Option<&str> = option_env!("TARGET");
const GIT_SHA: Option<&str> = option_env!("GIT_SHA");

type AllowedToolSet = BTreeSet<String>;

fn main() {
    if let Err(error) = run() {
        eprintln!(
            "error: {error}

Run `claw --help` for usage."
        );
        std::process::exit(1);
    }
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = env::args().skip(1).collect();
    match parse_args(&args)? {
        CliAction::DumpManifests => dump_manifests(),
        CliAction::BootstrapPlan => print_bootstrap_plan(),
        CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
        CliAction::Version => print_version(),
        CliAction::ResumeSession {
            session_path,
            commands,
        } => resume_session(&session_path, &commands),
        CliAction::Prompt {
            prompt,
            model,
            output_format,
            allowed_tools,
            permission_mode,
        } => LiveCli::new(auto_select_model(&model), true, allowed_tools, permission_mode)?
            .run_turn_with_output(&prompt, output_format),
        CliAction::Login => run_login(),
        CliAction::Logout => run_logout(),
        CliAction::Init => run_init(),
        CliAction::Repl {
            model,
            allowed_tools,
            permission_mode,
        } => {
            let mut config_path = dirs::config_dir().unwrap_or_else(|| PathBuf::from("~/.config"));
            config_path.push("albert/config.toml");
            if !config_path.exists() {
                init::wake_sequence();
            }
            run_repl(auto_select_model(&model), allowed_tools, permission_mode)
        },
        CliAction::Help => {
            println!("{}", render_repl_help());
            Ok(())
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum CliAction {
    DumpManifests,
    BootstrapPlan,
    PrintSystemPrompt {
        cwd: PathBuf,
        date: String,
    },
    Version,
    ResumeSession {
        session_path: PathBuf,
        commands: Vec<String>,
    },
    Prompt {
        prompt: String,
        model: String,
        output_format: CliOutputFormat,
        allowed_tools: Option<AllowedToolSet>,
        permission_mode: PermissionMode,
    },
    Login,
    Logout,
    Init,
    Repl {
        model: String,
        allowed_tools: Option<AllowedToolSet>,
        permission_mode: PermissionMode,
    },
    // prompt-mode formatting is only supported for non-interactive runs
    Help,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CliOutputFormat {
    Text,
    Json,
}

impl CliOutputFormat {
    fn parse(value: &str) -> Result<Self, String> {
        match value {
            "text" => Ok(Self::Text),
            "json" => Ok(Self::Json),
            other => Err(format!(
                "unsupported value for --output-format: {other} (expected text or json)"
            )),
        }
    }
}

#[allow(clippy::too_many_lines)]
fn parse_args(args: &[String]) -> Result<CliAction, String> {
    let mut model = DEFAULT_MODEL.to_string();
    let mut output_format = CliOutputFormat::Text;
    let mut permission_mode = default_permission_mode();
    let mut wants_version = false;
    let mut allowed_tool_values = Vec::new();
    let mut rest = Vec::new();
    let mut index = 0;

    while index < args.len() {
        match args[index].as_str() {
            "--version" | "-V" => {
                wants_version = true;
                index += 1;
            }
            "--model" => {
                let value = args
                    .get(index + 1)
                    .ok_or_else(|| "missing value for --model".to_string())?;
                model = resolve_model_alias(value).to_string();
                index += 2;
            }
            flag if flag.starts_with("--model=") => {
                model = resolve_model_alias(&flag[8..]).to_string();
                index += 1;
            }
            "--output-format" => {
                let value = args
                    .get(index + 1)
                    .ok_or_else(|| "missing value for --output-format".to_string())?;
                output_format = CliOutputFormat::parse(value)?;
                index += 2;
            }
            "--permission-mode" => {
                let value = args
                    .get(index + 1)
                    .ok_or_else(|| "missing value for --permission-mode".to_string())?;
                permission_mode = parse_permission_mode_arg(value)?;
                index += 2;
            }
            flag if flag.starts_with("--output-format=") => {
                output_format = CliOutputFormat::parse(&flag[16..])?;
                index += 1;
            }
            flag if flag.starts_with("--permission-mode=") => {
                permission_mode = parse_permission_mode_arg(&flag[18..])?;
                index += 1;
            }
            "--dangerously-skip-permissions" => {
                permission_mode = PermissionMode::DangerFullAccess;
                index += 1;
            }
            "-p" => {
                // Claw Code compat: -p "prompt" = one-shot prompt
                let prompt = args[index + 1..].join(" ");
                if prompt.trim().is_empty() {
                    return Err("-p requires a prompt string".to_string());
                }
                return Ok(CliAction::Prompt {
                    prompt,
                    model: resolve_model_alias(&model).to_string(),
                    output_format,
                    allowed_tools: normalize_allowed_tools(&allowed_tool_values)?,
                    permission_mode,
                });
            }
            "--print" => {
                // Claw Code compat: --print makes output non-interactive
                output_format = CliOutputFormat::Text;
                index += 1;
            }
            "--allowedTools" | "--allowed-tools" => {
                let value = args
                    .get(index + 1)
                    .ok_or_else(|| "missing value for --allowedTools".to_string())?;
                allowed_tool_values.push(value.clone());
                index += 2;
            }
            flag if flag.starts_with("--allowedTools=") => {
                allowed_tool_values.push(flag[15..].to_string());
                index += 1;
            }
            flag if flag.starts_with("--allowed-tools=") => {
                allowed_tool_values.push(flag[16..].to_string());
                index += 1;
            }
            other => {
                rest.push(other.to_string());
                index += 1;
            }
        }
    }

    if wants_version {
        return Ok(CliAction::Version);
    }

    let allowed_tools = normalize_allowed_tools(&allowed_tool_values)?;

    if rest.is_empty() {
        return Ok(CliAction::Repl {
            model,
            allowed_tools,
            permission_mode,
        });
    }
    if matches!(rest.first().map(String::as_str), Some("--help" | "-h")) {
        return Ok(CliAction::Help);
    }
    if rest.first().map(String::as_str) == Some("--resume") {
        return parse_resume_args(&rest[1..]);
    }

    match rest[0].as_str() {
        "dump-manifests" => Ok(CliAction::DumpManifests),
        "bootstrap-plan" => Ok(CliAction::BootstrapPlan),
        "system-prompt" => parse_system_prompt_args(&rest[1..]),
        "login" => Ok(CliAction::Login),
        "logout" => Ok(CliAction::Logout),
        "init" => Ok(CliAction::Init),
        "prompt" => {
            let prompt = rest[1..].join(" ");
            if prompt.trim().is_empty() {
                return Err("prompt subcommand requires a prompt string".to_string());
            }
            Ok(CliAction::Prompt {
                prompt,
                model,
                output_format,
                allowed_tools,
                permission_mode,
            })
        }
        other if !other.starts_with('/') => Ok(CliAction::Prompt {
            prompt: rest.join(" "),
            model,
            output_format,
            allowed_tools,
            permission_mode,
        }),
        other => Err(format!("unknown subcommand: {other}")),
    }
}

fn resolve_model_alias(model: &str) -> &str {
    match model {
        "opus" => "claude-3-opus-20240229",
        "sonnet" => "claude-3-sonnet-20240229",
        "haiku" => "claude-3-haiku-20240307",
        "flash" => "gemini-2.0-flash",
        "pro" => "gemini-1.5-pro",
        _ => model,
    }
}

fn normalize_allowed_tools(values: &[String]) -> Result<Option<AllowedToolSet>, String> {
    if values.is_empty() {
        return Ok(None);
    }

    let canonical_names = mvp_tool_specs()
        .into_iter()
        .map(|spec| spec.name.to_string())
        .collect::<Vec<_>>();
    let mut name_map = canonical_names
        .iter()
        .map(|name| (normalize_tool_name(name), name.clone()))
        .collect::<BTreeMap<_, _>>();

    for (alias, canonical) in [
        ("read", "read_file"),
        ("write", "write_file"),
        ("edit", "edit_file"),
        ("glob", "glob_search"),
        ("grep", "grep_search"),
    ] {
        name_map.insert(alias.to_string(), canonical.to_string());
    }

    let mut allowed = AllowedToolSet::new();
    for value in values {
        for token in value
            .split(|ch: char| ch == ',' || ch.is_whitespace())
            .filter(|token| !token.is_empty())
        {
            let normalized = normalize_tool_name(token);
            let canonical = name_map.get(&normalized).ok_or_else(|| {
                format!(
                    "unsupported tool in --allowedTools: {token} (expected one of: {})",
                    canonical_names.join(", ")
                )
            })?;
            allowed.insert(canonical.clone());
        }
    }

    Ok(Some(allowed))
}

fn normalize_tool_name(value: &str) -> String {
    value.trim().replace('-', "_").to_ascii_lowercase()
}

fn parse_permission_mode_arg(value: &str) -> Result<PermissionMode, String> {
    normalize_permission_mode(value)
        .ok_or_else(|| {
            format!(
                "unsupported permission mode '{value}'. Use read-only, workspace-write, or danger-full-access."
            )
        })
        .map(permission_mode_from_label)
}

fn permission_mode_from_label(mode: &str) -> PermissionMode {
    match mode {
        "read-only" => PermissionMode::ReadOnly,
        "workspace-write" => PermissionMode::WorkspaceWrite,
        "danger-full-access" => PermissionMode::DangerFullAccess,
        other => panic!("unsupported permission mode label: {other}"),
    }
}

fn default_permission_mode() -> PermissionMode {
    env::var("RUSTY_TERNLANG_PERMISSION_MODE")
        .ok()
        .as_deref()
        .and_then(normalize_permission_mode)
        .map_or(PermissionMode::DangerFullAccess, permission_mode_from_label)
}

fn filter_tool_specs(allowed_tools: Option<&AllowedToolSet>) -> Vec<tools::ToolSpec> {
    mvp_tool_specs()
        .into_iter()
        .filter(|spec| allowed_tools.is_none_or(|allowed| allowed.contains(spec.name)))
        .collect()
}

fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
    let mut cwd = env::current_dir().map_err(|error| error.to_string())?;
    let mut date = DEFAULT_DATE.to_string();
    let mut index = 0;

    while index < args.len() {
        match args[index].as_str() {
            "--cwd" => {
                let value = args
                    .get(index + 1)
                    .ok_or_else(|| "missing value for --cwd".to_string())?;
                cwd = PathBuf::from(value);
                index += 2;
            }
            "--date" => {
                let value = args
                    .get(index + 1)
                    .ok_or_else(|| "missing value for --date".to_string())?;
                date.clone_from(value);
                index += 2;
            }
            other => return Err(format!("unknown system-prompt option: {other}")),
        }
    }

    Ok(CliAction::PrintSystemPrompt { cwd, date })
}

fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
    let session_path = args
        .first()
        .ok_or_else(|| "missing session path for --resume".to_string())
        .map(PathBuf::from)?;
    let commands = args[1..].to_vec();
    if commands
        .iter()
        .any(|command| !command.trim_start().starts_with('/'))
    {
        return Err("--resume trailing arguments must be slash commands".to_string());
    }
    Ok(CliAction::ResumeSession {
        session_path,
        commands,
    })
}

fn dump_manifests() -> Result<(), Box<dyn std::error::Error>> {
    let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
    let paths = UpstreamPaths::from_workspace_dir(&workspace_dir);
    let manifest = extract_manifest(&paths)?;
    println!("commands: {}", manifest.commands.entries().len());
    println!("tools: {}", manifest.tools.entries().len());
    println!("bootstrap phases: {}", manifest.bootstrap.phases().len());
    Ok(())
}

fn print_bootstrap_plan() -> Result<(), Box<dyn std::error::Error>> {
    for phase in runtime::BootstrapPlan::ternlang_cli_default().phases() {
        println!("- {phase:?}");
    }
    Ok(())
}

fn default_oauth_config() -> OAuthConfig {
    OAuthConfig {
        client_id: String::from("9d1c250a-e61b-44d9-88ed-5944d1962f5e"),
        authorize_url: String::from("https://console.anthropic.com/oauth/authorize"),
        token_url: String::from("https://api.anthropic.com/v1/oauth/token"),
        callback_port: None,
        manual_redirect_url: None,
        scopes: vec![
            String::from("user:profile"),
            String::from("user:inference"),
            String::from("user:sessions:ternlang_cli"),
        ],
    }
}

fn run_login() -> Result<(), Box<dyn std::error::Error>> {
    let cwd = env::current_dir()?;
    let config = ConfigLoader::default_for(&cwd).load()?;
    let default_oauth = default_oauth_config();
    let oauth = config.oauth().unwrap_or(&default_oauth);
    let callback_port = oauth.callback_port.unwrap_or(DEFAULT_OAUTH_CALLBACK_PORT);
    let redirect_uri = runtime::loopback_redirect_uri(callback_port);
    let pkce = generate_pkce_pair()?;
    let state = generate_state()?;
    let authorize_url =
        OAuthAuthorizationRequest::from_config(oauth, redirect_uri.clone(), state.clone(), &pkce)
            .build_url();

    println!("Starting Anthropic OAuth login...");
    println!("Listening for callback on {redirect_uri}");
    if let Err(error) = open_browser(&authorize_url) {
        eprintln!("warning: failed to open browser automatically: {error}");
        println!("Open this URL manually:
{authorize_url}");
    }

    let callback = wait_for_oauth_callback(callback_port)?;
    if let Some(error) = callback.error {
        let description = callback
            .error_description
            .unwrap_or_else(|| "authorization failed".to_string());
        return Err(io::Error::other(format!("{error}: {description}")).into());
    }
    let code = callback.code.ok_or_else(|| {
        io::Error::new(io::ErrorKind::InvalidData, "callback did not include code")
    })?;
    let returned_state = callback.state.ok_or_else(|| {
        io::Error::new(io::ErrorKind::InvalidData, "callback did not include state")
    })?;
    if returned_state != state {
        return Err(io::Error::new(io::ErrorKind::InvalidData, "oauth state mismatch").into());
    }

    let client = TernlangClient::from_auth(AuthSource::None).with_base_url(api::read_base_url());
    let exchange_request = api::OAuthTokenExchangeRequest {
        code,
        redirect_uri,
    };
    let runtime = tokio::runtime::Runtime::new()?;
    let token_set = runtime.block_on(client.exchange_oauth_code(api::OAuthConfig {}, &exchange_request))?;
    save_oauth_credentials(&runtime::OAuthTokenSet {
        access_token: token_set.access_token,
        refresh_token: token_set.refresh_token,
        expires_at: token_set.expires_at,
        scopes: token_set.scopes,
    })?;
    println!("Anthropic OAuth login complete.");
    Ok(())
}

fn run_logout() -> Result<(), Box<dyn std::error::Error>> {
    clear_oauth_credentials()?;
    println!("Anthropic OAuth credentials cleared.");
    Ok(())
}

fn open_browser(url: &str) -> io::Result<()> {
    let commands = if cfg!(target_os = "macos") {
        vec![("open", vec![url])]
    } else if cfg!(target_os = "windows") {
        vec![("cmd", vec!["/C", "start", "", url])]
    } else {
        vec![("xdg-open", vec![url])]
    };
    for (program, args) in commands {
        match Command::new(program).args(args).spawn() {
            Ok(_) => return Ok(()),
            Err(error) if error.kind() == io::ErrorKind::NotFound => {}
            Err(error) => return Err(error),
        }
    }
    Err(io::Error::new(
        io::ErrorKind::NotFound,
        "no supported browser opener command found",
    ))
}

fn wait_for_oauth_callback(
    port: u16,
) -> Result<runtime::OAuthCallbackParams, Box<dyn std::error::Error>> {
    let listener = TcpListener::bind(("127.0.0.1", port))?;
    let (mut stream, _) = listener.accept()?;
    let mut buffer = [0_u8; 4096];
    let bytes_read = stream.read(&mut buffer)?;
    let request = String::from_utf8_lossy(&buffer[..bytes_read]);
    let request_line = request.lines().next().ok_or_else(|| {
        io::Error::new(io::ErrorKind::InvalidData, "missing callback request line")
    })?;
    let target = request_line.split_whitespace().nth(1).ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidData,
            "missing callback request target",
        )
    })?;
    let callback = parse_oauth_callback_request_target(target)
        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
    let body = if callback.error.is_some() {
        "Anthropic OAuth login failed. You can close this window."
    } else {
        "Anthropic OAuth login succeeded. You can close this window."
    };
    let response = format!(
        "HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: {}
connection: close

{}",
        body.len(),
        body
    );
    stream.write_all(response.as_bytes())?;
    Ok(callback)
}

fn print_system_prompt(cwd: PathBuf, date: String) -> Result<(), Box<dyn std::error::Error>> {
    let sections = load_system_prompt(cwd, date, env::consts::OS, "unknown")?;
    println!("{}", sections.join("

"));
    Ok(())
}

fn print_version() -> Result<(), Box<dyn std::error::Error>> {
    println!("{}", render_version_report());
    Ok(())
}

fn resume_session(session_path: &Path, commands: &[String]) -> Result<(), Box<dyn std::error::Error>> {
    let mut session = Session::load_from_path(session_path)?;

    if commands.is_empty() {
        println!(
            "Restored session from {} ({} messages).",
            session_path.display(),
            session.messages.len()
        );
        return Ok(());
    }

    for raw_command in commands {
        let Some(command) = SlashCommand::parse(raw_command) else {
            eprintln!("unsupported resumed command: {raw_command}");
            std::process::exit(2);
        };
        let outcome = run_resume_command(session_path, &session, &command)?;
        session = outcome.session;
        if let Some(message) = outcome.message {
            println!("{message}");
        }
    }
    Ok(())
}

#[derive(Debug, Clone)]
struct ResumeCommandOutcome {
    session: Session,
    message: Option<String>,
}

#[derive(Debug, Clone)]
struct StatusContext {
    cwd: PathBuf,
    session_path: Option<PathBuf>,
    loaded_config_files: usize,
    discovered_config_files: usize,
    memory_file_count: usize,
    project_root: Option<PathBuf>,
    git_branch: Option<String>,
}

#[derive(Debug, Clone, Copy)]
struct StatusUsage {
    message_count: usize,
    turns: u32,
    latest: TokenUsage,
    cumulative: TokenUsage,
    estimated_tokens: usize,
}

fn format_model_report(model: &str, message_count: usize, turns: u32) -> String {
    format!(
        "Model
  Current model    {model}
  Session messages {message_count}
  Session turns    {turns}

Usage
  Inspect current model with /model
  Switch models with /model <name>"
    )
}

fn format_model_switch_report(previous: &str, next: &str, message_count: usize) -> String {
    format!(
        "Model updated
  Previous         {previous}
  Current          {next}
  Preserved msgs   {message_count}"
    )
}

fn format_permissions_report(mode: &str) -> String {
    let modes = [
        ("read-only", "Read/search tools only", mode == "read-only"),
        (
            "workspace-write",
            "Edit files inside the workspace",
            mode == "workspace-write",
        ),
        (
            "danger-full-access",
            "Unrestricted tool access",
            mode == "danger-full-access",
        ),
    ]
    .into_iter()
    .map(|(name, description, is_current)| {
        let marker = if is_current {
            "● current"
        } else {
            "○ available"
        };
        format!("  {name:<18} {marker:<11} {description}")
    })
    .collect::<Vec<_>>()
    .join(
        "
",
    );

    format!(
        "Permissions
  Active mode      {mode}
  Mode status      live session default

Modes
{modes}

Usage
  Inspect current mode with /permissions
  Switch modes with /permissions <mode>"
    )
}

fn format_permissions_switch_report(previous: &str, next: &str) -> String {
    format!(
        "Permissions updated
  Result           mode switched
  Previous mode    {previous}
  Active mode      {next}
  Applies to       subsequent tool calls
  Usage            /permissions to inspect current mode"
    )
}

fn format_cost_report(usage: TokenUsage) -> String {
    format!(
        "Cost
  Input tokens     {}
  Output tokens    {}
  Cache create     {}
  Cache read       {}
  Total tokens     {}",
        usage.input_tokens,
        usage.output_tokens,
        usage.cache_creation_input_tokens,
        usage.cache_read_input_tokens,
        usage.total_tokens(),
    )
}

fn format_resume_report(session_path: &str, message_count: usize, turns: u32) -> String {
    format!(
        "Session resumed
  Session file     {session_path}
  Messages         {message_count}
  Turns            {turns}"
    )
}

fn format_compact_report(removed: usize, resulting_messages: usize, skipped: bool) -> String {
    if skipped {
        format!(
            "Compact
  Result           skipped
  Reason           session below compaction threshold
  Messages kept    {resulting_messages}"
        )
    } else {
        format!(
            "Compact
  Result           compacted
  Messages removed {removed}
  Messages kept    {resulting_messages}"
        )
    }
}

fn format_auto_compaction_notice(removed: usize) -> String {
    format!("[auto-compacted: removed {removed} messages]")
}

fn parse_git_status_metadata(status: Option<&str>) -> (Option<PathBuf>, Option<String>) {
    let Some(status) = status else {
        return (None, None);
    };
    let branch = status.lines().next().and_then(|line| {
        line.strip_prefix("## ")
            .map(|line| {
                line.split(['.', ' '])
                    .next()
                    .unwrap_or_default()
                    .to_string()
            })
            .filter(|value| !value.is_empty())
    });
    let project_root = find_git_root().ok();
    (project_root, branch)
}

fn find_git_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let output = std::process::Command::new("git")
        .args(["rev-parse", "--show-toplevel"])
        .current_dir(env::current_dir()?)
        .output()?;
    if !output.status.success() {
        return Err("not a git repository".into());
    }
    let path = String::from_utf8(output.stdout)?.trim().to_string();
    if path.is_empty() {
        return Err("empty git root".into());
    }
    Ok(PathBuf::from(path))
}

#[allow(clippy::too_many_lines)]
fn run_resume_command(
    session_path: &Path,
    session: &Session,
    command: &SlashCommand,
) -> Result<ResumeCommandOutcome, Box<dyn std::error::Error>> {
    match command {
        SlashCommand::Help => Ok(ResumeCommandOutcome {
            session: session.clone(),
            message: Some(render_repl_help()),
        }),
        SlashCommand::Compact => {
            let result = runtime::compact_session(
                session,
                CompactionConfig {
                    max_estimated_tokens: 0,
                    ..CompactionConfig::default()
                },
            );
            let removed = result.removed_message_count;
            let kept = result.compacted_session.messages.len();
            let skipped = removed == 0;
            result.compacted_session.save_to_path(session_path)?;
            Ok(ResumeCommandOutcome {
                session: result.compacted_session,
                message: Some(format_compact_report(removed, kept, skipped)),
            })
        }
        SlashCommand::Clear { confirm } => {
            if !confirm {
                return Ok(ResumeCommandOutcome {
                    session: session.clone(),
                    message: Some(
                        "clear: confirmation required; rerun with /clear --confirm".to_string(),
                    ),
                });
            }
            let cleared = Session::new();
            cleared.save_to_path(session_path)?;
            Ok(ResumeCommandOutcome {
                session: cleared,
                message: Some(format!(
                    "Cleared resumed session file {}.",
                    session_path.display()
                )),
            })
        }
        SlashCommand::Status => {
            let tracker = UsageTracker::from_session(session);
            let usage = tracker.cumulative_usage();
            Ok(ResumeCommandOutcome {
                session: session.clone(),
                message: Some(format_status_report(
                    "restored-session",
                    StatusUsage {
                        message_count: session.messages.len(),
                        turns: tracker.turns(),
                        latest: tracker.current_turn_usage(),
                        cumulative: usage,
                        estimated_tokens: 0,
                    },
                    default_permission_mode().as_str(),
                    &status_context(Some(session_path))?,
                )),
            })
        }
        SlashCommand::Cost => {
            let usage = UsageTracker::from_session(session).cumulative_usage();
            Ok(ResumeCommandOutcome {
                session: session.clone(),
                message: Some(format_cost_report(usage)),
            })
        }
        SlashCommand::Config { section } => Ok(ResumeCommandOutcome {
            session: session.clone(),
            message: Some(render_config_report(section.as_deref())?),
        }),
        SlashCommand::Memory => Ok(ResumeCommandOutcome {
            session: session.clone(),
            message: Some(render_memory_report()?),
        }),
        SlashCommand::Init => Ok(ResumeCommandOutcome {
            session: session.clone(),
            message: Some(init_ternlang_md()?),
        }),
        SlashCommand::Diff => Ok(ResumeCommandOutcome {
            session: session.clone(),
            message: Some(render_diff_report()?),
        }),
        SlashCommand::Version => Ok(ResumeCommandOutcome {
            session: session.clone(),
            message: Some(render_version_report()),
        }),
        SlashCommand::Export { path } => {
            let export_path = resolve_export_path(path.as_deref(), session)?;
            fs::write(&export_path, render_export_text(session))?;
            Ok(ResumeCommandOutcome {
                session: session.clone(),
                message: Some(format!(
                    "Export
  Result           wrote transcript
  File             {}
  Messages         {}",
                    export_path.display(),
                    session.messages.len(),
                )),
            })
        }
        SlashCommand::Auth { .. }
        | SlashCommand::Bughunter { .. }
        | SlashCommand::Commit
        | SlashCommand::Pr { .. }
        | SlashCommand::Issue { .. }
        | SlashCommand::Ultraplan { .. }
        | SlashCommand::Teleport { .. }
        | SlashCommand::DebugToolCall
        | SlashCommand::Resume { .. }
        | SlashCommand::Session { .. }
        | SlashCommand::Plan { .. }
        | SlashCommand::Tdd { .. }
        | SlashCommand::Verify
        | SlashCommand::CodeReview { .. }
        | SlashCommand::BuildFix
        | SlashCommand::Aside { .. }
        | SlashCommand::Learn
        | SlashCommand::Refactor { .. }
        | SlashCommand::Checkpoint { .. }
        | SlashCommand::Docs { .. }
        | SlashCommand::Model { .. }
        | SlashCommand::Permissions { .. }
        | SlashCommand::Compress
        | SlashCommand::Loop { .. }
        | SlashCommand::Unknown(_) => Err("unsupported resumed slash command".into()),

    }
}

fn run_repl(
    model: String,
    allowed_tools: Option<AllowedToolSet>,
    permission_mode: PermissionMode,
) -> Result<(), Box<dyn std::error::Error>> {
    check_workspace_trust()?;
    let mut cli = LiveCli::new(model, true, allowed_tools, permission_mode)?;
    let mut editor = input::LineEditor::new("> ", slash_command_completion_candidates());
    println!("{}", cli.startup_banner());

    loop {
        print_status_bar(&cli.model, &cli.session.id, cli.runtime.session().messages.len());
        match editor.read_line()? {
            input::ReadOutcome::Submit(input) => {
                let raw = input.trim().to_string();
                if raw.is_empty() {
                    continue;
                }
                if matches!(raw.as_str(), "/exit" | "/quit") {
                    cli.persist_session()?;
                    break;
                }

                // Slash picker: bare "/" or unrecognized "/partial" → interactive picker
                let trimmed = if raw == "/" || (raw.starts_with('/') && SlashCommand::parse(&raw).is_none()) {
                    match show_slash_picker(&raw) {
                        Some(chosen) => chosen,
                        None => continue,
                    }
                } else {
                    raw
                };

                if let Some(command) = SlashCommand::parse(&trimmed) {
                    if cli.handle_repl_command(command)? {
                        cli.persist_session()?;
                    }
                    continue;
                }
                editor.push_history(input);

                // Extract [image: path] attachments and inject them as base64 context
                let (text_without_images, image_paths) = extract_image_attachments(&trimmed);
                let final_input = if image_paths.is_empty() {
                    trimmed
                } else {
                    let mut parts = Vec::new();
                    for path in &image_paths {
                        match load_image_as_base64(path) {
                            Ok((mime, b64)) => {
                                parts.push(format!("[ATTACHED IMAGE — mime:{mime}]\ndata:base64:{b64}"));
                                println!("{} {}", style("📎").cyan(), path);
                            }
                            Err(e) => eprintln!("{} {}", style("✘").red(), e),
                        }
                    }
                    parts.push(text_without_images.clone());
                    // Store combined prompt so borrow checker is happy below
                    // (we can't use a local String in the same block cleanly, use a heap allocation)
                    let combined = parts.join("\n\n");
                    // We need to run with the combined string; do it here and continue
                    cli.run_turn(&combined)?;
                    continue;
                };
                cli.run_turn(&final_input)?;
            }
            input::ReadOutcome::Cancel => {}
            input::ReadOutcome::Exit => {
                cli.persist_session()?;
                break;
            }
        }
    }

    Ok(())
}

#[derive(Debug, Clone)]
struct SessionHandle {
    id: String,
    path: PathBuf,
}

#[derive(Debug, Clone)]
struct ManagedSessionSummary {
    id: String,
    path: PathBuf,
    modified_epoch_secs: u64,
    message_count: usize,
}

struct LiveCli {
    model: String,
    allowed_tools: Option<AllowedToolSet>,
    permission_mode: PermissionMode,
    system_prompt: Vec<String>,
    runtime: ConversationRuntime<TernlangRuntimeClient, CliToolExecutor>,
    session: SessionHandle,
    stop_flag: Arc<AtomicBool>,
}

impl LiveCli {
    fn new(
        model: String,
        enable_tools: bool,
        allowed_tools: Option<AllowedToolSet>,
        permission_mode: PermissionMode,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        let system_prompt = build_system_prompt()?;
        let session = create_managed_session_handle()?;
        let runtime = build_runtime(
            Session::new(),
            model.clone(),
            system_prompt.clone(),
            enable_tools,
            true,
            allowed_tools.clone(),
            permission_mode,
        )?;
        let cli = Self {
            model,
            allowed_tools,
            permission_mode,
            system_prompt,
            runtime,
            session,
            stop_flag: Arc::new(AtomicBool::new(false)),
        };
        cli.persist_session()?;
        Ok(cli)
    }

    fn startup_banner(&self) -> String {
        let cwd = env::current_dir().map_or_else(
            |_| "<unknown>".to_string(),
            |path| path.display().to_string(),
        );
        format!(
            "
  █████╗  ██╗     ██████╗ ███████╗██████╗ ████████╗
  ██╔══██╗ ██║     ██╔══██╗██╔════╝██╔══██╗╚══██╔══╝
  ███████║ ██║     ██████╔╝█████╗  ██████╔╝   ██║   
  ██╔══██║ ██║     ██╔══██╗██╔══╝  ██╔══██╗   ██║   
  ██║  ██║ ███████╗██████╔╝███████╗██║  ██║   ██║   
  ╚═╝  ╚═╝ ╚══════╝╚═════╝ ╚══════╝╚═╝  ╚═╝   ╚═╝   
     ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

  Model            {}
  Permissions      {}
  Directory        {}
  Session          {}

  Type /help for commands · Shift+Enter for newline",
            self.model,
            self.permission_mode.as_str(),
            cwd,
            self.session.id,
        )
    }

    fn run_turn(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
        let pb = ProgressBar::new_spinner();
        pb.set_style(
            ProgressStyle::with_template("{spinner:.cyan.bold} {msg}")
                .unwrap()
                .tick_strings(&["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏","⠿"]),
        );
        pb.set_message("thinking...");
        pb.enable_steady_tick(Duration::from_millis(80));

        let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode, true);
        let result = self
            .runtime
            .run_turn(input.to_string(), Some(&mut permission_prompter));
        match result {
            Ok(summary) => {
                pb.finish_and_clear();
                let response_text = final_assistant_text(&summary);
                if !response_text.is_empty() {
                    typewriter_print(&TerminalRenderer::new().render_markdown(&response_text));
                }
                if let Some(event) = summary.auto_compaction {
                    println!(
                        "{}",
                        format_auto_compaction_notice(event.removed_message_count)
                    );
                }
                self.persist_session()?;

                // Self-reflection: score this turn and optionally save to memory
                let score = score_turn_importance(input, &response_text);
                if score >= 0.6 && !response_text.is_empty() {
                    // Distill a one-line summary from the key trigger phrase
                    let summary_text = distill_memory_summary(input, &response_text);
                    if let Err(e) = append_to_albert_memory(&summary_text) {
                        eprintln!("{} memory write failed: {e}", style("⚠").yellow());
                    } else {
                        println!("{}", style("  Noted.").dim().italic());
                    }
                }

                Ok(())
            }
            Err(error) => {
                pb.finish_and_clear();
                let error_str = error.to_string();
                // Auto-fallback on rate limit
                if error_str.contains("429") || error_str.contains("RESOURCE_EXHAUSTED") {
                    let retry_secs = parse_retry_after(&error_str).unwrap_or(30);
                    let fallback = fallback_model_for(&self.model);
                    eprintln!("{} Rate limited on {}. {}",
                        style("⚠").yellow().bold(),
                        self.model,
                        if retry_secs > 0 { format!("Retry in {retry_secs}s.") } else { String::new() }
                    );
                    if let Some(fb) = fallback {
                        eprintln!("{} Switching to {}...", style("→").cyan(), fb);
                        let _ = self.set_model(Some(fb.to_string()));
                        return self.run_turn(input);
                    }
                }
                // Clean error display — strip raw JSON, show first meaningful line
                let clean = clean_error_message(&error_str);
                eprintln!("{} {}", style("✘").red().bold(), clean);
                Err(Box::new(error))
            }
        }
    }

    fn run_turn_with_output(
        &mut self,
        input: &str,
        output_format: CliOutputFormat,
    ) -> Result<(), Box<dyn std::error::Error>> {
        match output_format {
            CliOutputFormat::Text => self.run_turn(input),
            CliOutputFormat::Json => self.run_prompt_json(input),
        }
    }

    fn run_prompt_json(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
        let session = self.runtime.session().clone();
        let mut runtime = build_runtime(
            session,
            self.model.clone(),
            self.system_prompt.clone(),
            true,
            false,
            self.allowed_tools.clone(),
            self.permission_mode,
        )?;
        let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode, false);
        let summary = runtime.run_turn(input.to_string(), Some(&mut permission_prompter))?;
        self.runtime = runtime;
        self.persist_session()?;
        println!(
            "{}",
            json!({
                "message": final_assistant_text(&summary),
                "model": self.model,
                "iterations": summary.iterations,
                "auto_compaction": summary.auto_compaction.map(|event| json!({
                    "removed_messages": event.removed_message_count,
                    "notice": format_auto_compaction_notice(event.removed_message_count),
                })),
                "tool_uses": collect_tool_uses(&summary),
                "tool_results": collect_tool_results(&summary),
                "usage": {
                    "input_tokens": summary.usage.input_tokens,
                    "output_tokens": summary.usage.output_tokens,
                    "cache_creation_input_tokens": summary.usage.cache_creation_input_tokens,
                    "cache_read_input_tokens": summary.usage.cache_read_input_tokens,
                }
            })
        );
        Ok(())
    }

    fn handle_repl_command(
        &mut self,
        command: SlashCommand,
    ) -> Result<bool, Box<dyn std::error::Error>> {
        Ok(match command {
            SlashCommand::Help => {
                println!("{}", render_repl_help());
                false
            }
            SlashCommand::Status => {
                self.print_status()?;
                false
            }
            SlashCommand::Bughunter { scope } => {
                self.run_bughunter(scope.as_deref())?;
                false
            }
            SlashCommand::Commit => {
                self.run_commit()?;
                true
            }
            SlashCommand::Pr { context } => {
                self.run_pr(context.as_deref())?;
                false
            }
            SlashCommand::Issue { context } => {
                self.run_issue(context.as_deref())?;
                false
            }
            SlashCommand::Ultraplan { task } => {
                self.run_ultraplan(task.as_deref())?;
                false
            }
            SlashCommand::Teleport { target } => {
                self.run_teleport(target.as_deref())?;
                false
            }
            SlashCommand::DebugToolCall => {
                self.run_debug_tool_call()?;
                false
            }
            SlashCommand::Compact => {
                self.compact()?;
                false
            }
            SlashCommand::Compress => {
                self.compress()?;
                false
            }
            SlashCommand::Model { model } => self.set_model(model)?,
            SlashCommand::Permissions { mode } => self.set_permissions(mode)?,
            SlashCommand::Clear { confirm } => self.clear_session(confirm)?,
            SlashCommand::Cost => {
                self.print_cost();
                false
            }
            SlashCommand::Resume { session_path } => self.resume_session(session_path)?,
            SlashCommand::Config { section } => {
                Self::print_config(section.as_deref())?;
                false
            }
            SlashCommand::Memory => {
                Self::print_memory()?;
                false
            }
            SlashCommand::Init => {
                run_init()?;
                false
            }
            SlashCommand::Diff => {
                Self::print_diff()?;
                false
            }
            SlashCommand::Version => {
                Self::print_version()?;
                false
            }
            SlashCommand::Export { path } => {
                self.export_session(path.as_deref())?;
                false
            }
            SlashCommand::Session { action, target } => {
                self.handle_session_command(action.as_deref(), target.as_deref())?
            }
            SlashCommand::Auth { provider } => {
                self.run_auth(provider.as_deref())?;
                false
            }
            SlashCommand::Plan { task } => {
                self.run_plan(task.as_deref())?;
                true
            }
            SlashCommand::Tdd { interface } => {
                println!("TDD loop engaged for {}. Scaffold -> Failing Test -> Implement.", interface.unwrap_or_else(|| "target".to_string()));
                false
            }
            SlashCommand::Verify => {
                println!("Running full verification: build, lint, test, and type-check...");
                false
            }
            SlashCommand::CodeReview { files } => {
                println!("Reviewing {}. Assessing quality, security, and maintainability.", files.unwrap_or_else(|| "changed files".to_string()));
                false
            }
            SlashCommand::BuildFix => {
                println!("Detecting build errors. Dispatching resolver agents...");
                false
            }
            SlashCommand::Aside { question } => {
                println!("Pivot: {}. Answering side question without losing context.", question.unwrap_or_else(|| "...".to_string()));
                false
            }
            SlashCommand::Learn => {
                println!("Extracting reusable patterns and learned instincts...");
                false
            }
            SlashCommand::Refactor { scope } => {
                println!("Refactoring {}. Removing dead code and consolidating duplicates.", scope.unwrap_or_else(|| "workspace".to_string()));
                false
            }
            SlashCommand::Checkpoint { label } => {
                println!("Checkpoint marked: {}.", label.unwrap_or_else(|| "manual".to_string()));
                false
            }
            SlashCommand::Docs { query } => {
                println!("Looking up docs for {}. Querying Context7...", query.unwrap_or_else(|| "project".to_string()));
                false
            }
            SlashCommand::Loop { mission } => {
                self.run_loop(mission.as_deref())?;
                false
            }
            SlashCommand::Unknown(name) => {
                eprintln!("unknown slash command: /{name}");
                false
            }
        })
    }

    fn persist_session(&self) -> Result<(), Box<dyn std::error::Error>> {
        self.runtime.session().save_to_path(&self.session.path)?;
        Ok(())
    }

    fn print_status(&self) -> Result<(), Box<dyn std::error::Error>> {
        let cumulative = self.runtime.usage().cumulative_usage();
        let latest = self.runtime.usage().current_turn_usage();
        println!(
            "{}",
            format_status_report(
                &self.model,
                StatusUsage {
                    message_count: self.runtime.session().messages.len(),
                    turns: self.runtime.usage().turns(),
                    latest,
                    cumulative,
                    estimated_tokens: self.runtime.estimated_tokens(),
                },
                self.permission_mode.as_str(),
                &status_context(Some(&self.session.path))?,
            )
        );
        Ok(())
    }

    fn set_model(&mut self, model: Option<String>) -> Result<bool, Box<dyn std::error::Error>> {
        let Some(model) = model else {
            // No arg → show interactive picker
            match show_model_picker(&self.model) {
                Some(chosen) => return self.set_model(Some(chosen)),
                None => {
                    println!(
                        "{}",
                        format_model_report(
                            &self.model,
                            self.runtime.session().messages.len(),
                            self.runtime.usage().turns(),
                        )
                    );
                    return Ok(false);
                }
            }
        };

        let model = resolve_model_alias(&model).to_string();

        if model == self.model {
            println!(
                "{}",
                format_model_report(
                    &self.model,
                    self.runtime.session().messages.len(),
                    self.runtime.usage().turns(),
                )
            );
            return Ok(false);
        }

        let previous = self.model.clone();
        let session = self.runtime.session().clone();
        let message_count = session.messages.len();
        self.runtime = build_runtime(
            session,
            model.clone(),
            self.system_prompt.clone(),
            true,
            true,
            self.allowed_tools.clone(),
            self.permission_mode,
        )?;
        self.model.clone_from(&model);
        println!(
            "{}",
            format_model_switch_report(&previous, &model, message_count)
        );
        Ok(true)
    }

    fn set_permissions(
        &mut self,
        mode: Option<String>,
    ) -> Result<bool, Box<dyn std::error::Error>> {
        let Some(mode) = mode else {
            println!(
                "{}",
                format_permissions_report(self.permission_mode.as_str())
            );
            return Ok(false);
        };

        let normalized = normalize_permission_mode(&mode).ok_or_else(|| {
            format!(
                "unsupported permission mode '{mode}'. Use read-only, workspace-write, or danger-full-access."
            )
        })?;

        if normalized == self.permission_mode.as_str() {
            println!("{}", format_permissions_report(normalized));
            return Ok(false);
        }

        let previous = self.permission_mode.as_str().to_string();
        let session = self.runtime.session().clone();
        self.permission_mode = permission_mode_from_label(normalized);
        self.runtime = build_runtime(
            session,
            self.model.clone(),
            self.system_prompt.clone(),
            true,
            true,
            self.allowed_tools.clone(),
            self.permission_mode,
        )?;
        println!(
            "{}",
            format_permissions_switch_report(&previous, normalized)
        );
        Ok(true)
    }

    fn clear_session(&mut self, confirm: bool) -> Result<bool, Box<dyn std::error::Error>> {
        if !confirm {
            println!(
                "clear: confirmation required; run /clear --confirm to start a fresh session."
            );
            return Ok(false);
        }

        self.session = create_managed_session_handle()?;
        self.runtime = build_runtime(
            Session::new(),
            self.model.clone(),
            self.system_prompt.clone(),
            true,
            true,
            self.allowed_tools.clone(),
            self.permission_mode,
        )?;
        println!(
            "Session cleared
  Mode             fresh session
  Preserved model  {}
  Permission mode  {}
  Session          {}",
            self.model,
            self.permission_mode.as_str(),
            self.session.id,
        );
        Ok(true)
    }

    fn print_cost(&self) {
        let cumulative = self.runtime.usage().cumulative_usage();
        println!("{}", format_cost_report(cumulative));
    }

    fn resume_session(
        &mut self,
        session_path: Option<String>,
    ) -> Result<bool, Box<dyn std::error::Error>> {
        let Some(session_ref) = session_path else {
            println!("Usage: /resume <session-path>");
            return Ok(false);
        };

        let handle = resolve_session_reference(&session_ref)?;
        let session = Session::load_from_path(&handle.path)?;
        let message_count = session.messages.len();
        self.runtime = build_runtime(
            session,
            self.model.clone(),
            self.system_prompt.clone(),
            true,
            true,
            self.allowed_tools.clone(),
            self.permission_mode,
        )?;
        self.session = handle;
        println!(
            "{}",
            format_resume_report(
                &self.session.path.display().to_string(),
                message_count,
                self.runtime.usage().turns(),
            )
        );
        Ok(true)
    }

    fn print_config(section: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
        println!("{}", render_config_report(section)?);
        Ok(())
    }

    fn print_memory() -> Result<(), Box<dyn std::error::Error>> {
        println!("{}", render_memory_report()?);
        Ok(())
    }

    fn print_diff() -> Result<(), Box<dyn std::error::Error>> {
        println!("{}", render_diff_report()?);
        Ok(())
    }

    fn print_version() -> Result<(), Box<dyn std::error::Error>> {
        println!("{}", render_version_report());
        Ok(())
    }

    fn export_session(
        &self,
        requested_path: Option<&str>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let export_path = resolve_export_path(requested_path, self.runtime.session())?;
        fs::write(&export_path, render_export_text(self.runtime.session()))?;
        println!(
            "Export
  Result           wrote transcript
  File             {}
  Messages         {}",
            export_path.display(),
            self.runtime.session().messages.len(),
        );
        Ok(())
    }

    fn handle_session_command(
        &mut self,
        action: Option<&str>,
        target: Option<&str>,
    ) -> Result<bool, Box<dyn std::error::Error>> {
        match action {
            None | Some("list") => {
                println!("{}", render_session_list(&self.session.id)?);
                Ok(false)
            }
            Some("switch") => {
                let Some(target) = target else {
                    println!("Usage: /session switch <session-id>");
                    return Ok(false);
                };
                let handle = resolve_session_reference(target)?;
                let session = Session::load_from_path(&handle.path)?;
                let message_count = session.messages.len();
                self.runtime = build_runtime(
                    session,
                    self.model.clone(),
                    self.system_prompt.clone(),
                    true,
                    true,
                    self.allowed_tools.clone(),
                    self.permission_mode,
                )?;
                self.session = handle;
                println!(
                    "Session switched
  Active session   {}
  File             {}
  Messages         {}",
                    self.session.id,
                    self.session.path.display(),
                    message_count,
                );
                Ok(true)
            }
            Some(other) => {
                println!("Unknown /session action '{other}'. Use /session list or /session switch <session-id>.");
                Ok(false)
            }
        }
    }

    fn compact(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        let result = self.runtime.compact(CompactionConfig::default());
        let removed = result.removed_message_count;
        let kept = result.compacted_session.messages.len();
        let skipped = removed == 0;
        self.runtime = build_runtime(
            result.compacted_session,
            self.model.clone(),
            self.system_prompt.clone(),
            true,
            true,
            self.allowed_tools.clone(),
            self.permission_mode,
        )?;
        self.persist_session()?;
        println!("{}", format_compact_report(removed, kept, skipped));
        Ok(())
    }

    fn compress(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        let result = self.runtime.compact(CompactionConfig {
            preserve_recent_messages: 2,
            max_estimated_tokens: 1, // Force aggressive
        });
        let removed = result.removed_message_count;
        let kept = result.compacted_session.messages.len();
        let skipped = removed == 0;
        self.runtime = build_runtime(
            result.compacted_session,
            self.model.clone(),
            self.system_prompt.clone(),
            true,
            true,
            self.allowed_tools.clone(),
            self.permission_mode,
        )?;
        self.persist_session()?;
        if skipped {
            println!("Compression skipped: session is empty or too short.");
        } else {
            println!(
                "Aggressively compressed {} messages. Albert's memory is now lean and sharp ({} kept).",
                removed, kept
            );
        }
        Ok(())
    }

    fn run_auth(&mut self, arg: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
        const KNOWN_PROVIDERS: &[&str] = &[
            "openai", "anthropic", "google", "xai", "huggingface", "ternlang", "azure", "aws", "ollama",
        ];
        const PROVIDER_MENU: &[&str] = &[
            "Anthropic (Claude)", "Google (Gemini)", "OpenAI (GPT-4o)",
            "XAI (Grok)", "HuggingFace", "Ollama (local, no key)", "Ternlang",
        ];
        const PROVIDER_KEYS: &[&str] = &[
            "anthropic", "google", "openai", "xai", "huggingface", "ollama", "ternlang",
        ];

        // If arg looks like an API key (not a known provider name), auto-detect and save
        if let Some(key) = arg {
            if !KNOWN_PROVIDERS.contains(&key.to_lowercase().as_str()) && key.len() > 8 {
                let provider = detect_provider_from_key(key).unwrap_or("ternlang");
                runtime::save_provider_config(provider, runtime::ProviderConfig {
                    api_key: Some(key.to_string()),
                    model: None,
                    base_url: None,
                })?;
                println!("  ✓ {} key saved.", provider_display_name(provider));
                return Ok(());
            }
        }

        // Resolve provider name or show interactive menu
        let provider = match arg.map(|s| s.to_lowercase()) {
            Some(ref p) if KNOWN_PROVIDERS.contains(&p.as_str()) => p.clone(),
            _ => {
                let sel = Select::new()
                    .with_prompt("  Choose provider")
                    .items(PROVIDER_MENU)
                    .default(0)
                    .interact()?;
                PROVIDER_KEYS[sel].to_string()
            }
        };

        if provider == "ollama" {
            runtime::save_provider_config("ollama", runtime::ProviderConfig {
                api_key: None,
                model: None,
                base_url: Some("http://localhost:11434".to_string()),
            })?;
            println!("  ✓ Ollama (local) configured — no key needed.");
            return Ok(());
        }

        print!("  Paste API key: ");
        io::stdout().flush()?;
        let mut api_key = String::new();
        io::stdin().read_line(&mut api_key)?;
        let api_key = api_key.trim().to_string();

        if api_key.is_empty() {
            println!("  Error: key cannot be empty.");
            return Ok(());
        }

        runtime::save_provider_config(&provider, runtime::ProviderConfig {
            api_key: Some(api_key),
            model: None,
            base_url: None,
        })?;

        println!("  ✓ {} key saved.", provider_display_name(&provider));
        Ok(())
    }

    fn run_internal_prompt_text(
        &self,
        prompt: &str,
        enable_tools: bool,
    ) -> Result<String, Box<dyn std::error::Error>> {
        let session = self.runtime.session().clone();
        let mut runtime = build_runtime(
            session,
            self.model.clone(),
            self.system_prompt.clone(),
            enable_tools,
            false,
            self.allowed_tools.clone(),
            self.permission_mode,
        )?;
        let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode, false);
        let summary = runtime.run_turn(prompt.to_string(), Some(&mut permission_prompter))?;
        Ok(final_assistant_text(&summary).trim().to_string())
    }

    fn run_bughunter(&self, scope: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
        let scope = scope.unwrap_or("the current repository");
        let prompt = format!(
            "You are /bughunter. Inspect {scope} and identify the most likely bugs or correctness issues. Prioritize concrete findings with file paths, severity, and suggested fixes. Use tools if needed."
        );
        println!("{}", self.run_internal_prompt_text(&prompt, true)?);
        Ok(())
    }

    fn run_ultraplan(&self, task: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
        let task = task.unwrap_or("the current repo work");
        let prompt = format!(
            "You are /ultraplan. Produce a deep multi-step execution plan for {task}. Include goals, risks, implementation sequence, verification steps, and rollback considerations. Use tools if needed."
        );
        println!("{}", self.run_internal_prompt_text(&prompt, true)?);
        Ok(())
    }

    fn run_teleport(&self, target: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
        let Some(target) = target.map(str::trim).filter(|value| !value.is_empty()) else {
            println!("Usage: /teleport <symbol-or-path>");
            return Ok(());
        };

        println!("{}", render_teleport_report(target)?);
        Ok(())
    }

    fn run_debug_tool_call(&self) -> Result<(), Box<dyn std::error::Error>> {
        println!("{}", render_last_tool_debug_report(self.runtime.session())?);
        Ok(())
    }

    fn run_commit(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        let status = git_output(&["status", "--short"])?;
        if status.trim().is_empty() {
            println!("Commit
  Result           skipped
  Reason           no workspace changes");
            return Ok(());
        }

        git_status_ok(&["add", "-A"])?;
        let staged_stat = git_output(&["diff", "--cached", "--stat"])?;
        let prompt = format!(
            "Generate a git commit message in plain text Lore format only. Base it on this staged diff summary:

{}

Recent conversation context:
{}",
            truncate_for_prompt(&staged_stat, 8_000),
            recent_user_context(self.runtime.session(), 6)
        );
        let message = sanitize_generated_message(&self.run_internal_prompt_text(&prompt, false)?);
        if message.trim().is_empty() {
            return Err("generated commit message was empty".into());
        }

        let path = write_temp_text_file("claw-commit-message.txt", &message)?;
        let output = Command::new("git")
            .args(["commit", "--file"])
            .arg(&path)
            .current_dir(env::current_dir()?)
            .output()?;
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
            return Err(format!("git commit failed: {stderr}").into());
        }

        println!(
            "Commit
  Result           created
  Message file     {}

{}",
            path.display(),
            message.trim()
        );
        Ok(())
    }

    fn run_pr(&self, context: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
        let staged = git_output(&["diff", "--stat"])?;
        let prompt = format!(
            "Generate a pull request title and body from this conversation and diff summary. Output plain text in this format exactly:
TITLE: <title>
BODY:
<body markdown>

Context hint: {}

Diff summary:
{}",
            context.unwrap_or("none"),
            truncate_for_prompt(&staged, 10_000)
        );
        let draft = sanitize_generated_message(&self.run_internal_prompt_text(&prompt, false)?);
        let (title, body) = parse_titled_body(&draft)
            .ok_or_else(|| "failed to parse generated PR title/body".to_string())?;

        if command_exists("gh") {
            let body_path = write_temp_text_file("claw-pr-body.md", &body)?;
            let output = Command::new("gh")
                .args(["pr", "create", "--title", &title, "--body-file"])
                .arg(&body_path)
                .current_dir(env::current_dir()?)
                .output()?;
            if output.status.success() {
                let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
                println!(
                    "PR
  Result           created
  Title            {title}
  URL              {}",
                    if stdout.is_empty() { "<unknown>" } else { &stdout }
                );
                return Ok(());
            }
        }

        println!("PR draft
  Title            {title}

{body}");
        Ok(())
    }

    fn run_issue(&self, context: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
        let prompt = format!(
            "Generate a GitHub issue title and body from this conversation. Output plain text in this format exactly:
TITLE: <title>
BODY:
<body markdown>

Context hint: {}

Conversation context:
{}",
            context.unwrap_or("none"),
            truncate_for_prompt(&recent_user_context(self.runtime.session(), 10), 10_000)
        );
        let draft = sanitize_generated_message(&self.run_internal_prompt_text(&prompt, false)?);
        let (title, body) = parse_titled_body(&draft)
            .ok_or_else(|| "failed to parse generated issue title/body".to_string())?;

        if command_exists("gh") {
            let body_path = write_temp_text_file("claw-issue-body.md", &body)?;
            let output = Command::new("gh")
                .args(["issue", "create", "--title", &title, "--body-file"])
                .arg(&body_path)
                .current_dir(env::current_dir()?)
                .output()?;
            if output.status.success() {
                let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
                println!(
                    "Issue
  Result           created
  Title            {title}
  URL              {}",
                    if stdout.is_empty() { "<unknown>" } else { &stdout }
                );
                return Ok(());
            }
        }

        println!("Issue draft
  Title            {title}

{body}");
        Ok(())
    }

    fn run_loop(&mut self, mission: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
        let mission_text = mission.unwrap_or("Complete the current objective.").to_string();
        println!("\n{} {}", style("🚀 MISSION STARTED:").bold().green(), style(&mission_text).cyan());
        println!("{}", style("  Press Ctrl+C once to abort after the current turn completes.").dim());

        // Reset and arm the stop flag for this loop run.
        self.stop_flag.store(false, Ordering::SeqCst);
        let flag = Arc::clone(&self.stop_flag);
        ctrlc::set_handler(move || {
            flag.store(true, Ordering::SeqCst);
        })
        .ok(); // ignore error if handler already set

        let mut turn_count = 0;
        let max_turns = 10;

        loop {
            if self.stop_flag.load(Ordering::SeqCst) {
                println!("\n{} Autopilot aborted by user after turn {}.", style("⛔ LOOP ABORTED").bold().red(), turn_count);
                self.stop_flag.store(false, Ordering::SeqCst);
                break;
            }

            turn_count += 1;
            if turn_count > max_turns {
                println!("\n{} Maximum turn limit reached ({}). Pausing autopilot for alignment.", style("⚠️").yellow(), max_turns);
                break;
            }

            println!("\n{} [Iteration {}/{}]", style("🌀 Autopilot").bold().magenta(), turn_count, max_turns);

            let loop_prompt = format!(
                "MISSION: {}\n\nContinue executing the mission. If the mission is fully complete, tested, and validated, end your response with 'MISSION COMPLETE'. Otherwise, continue with the next logical step. Spawn internal swarm reasoning if needed.",
                mission_text
            );

            self.run_turn(&loop_prompt)?;

            let last_message = self.runtime.session().messages.last();
            if let Some(msg) = last_message {
                let content = msg.blocks.iter()
                    .filter_map(|b| if let ContentBlock::Text { text } = b { Some(text.as_str()) } else { None })
                    .collect::<Vec<_>>()
                    .join(" ");
                if content.contains("MISSION COMPLETE") {
                    println!("\n{} Mission objectives achieved. Harness complete.", style("✨ MISSION SUCCESSFUL").bold().green());
                    break;
                }
            }

            thread::sleep(Duration::from_millis(500));
        }

        Ok(())
    }

    fn run_plan(&mut self, task: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
        let task = task.unwrap_or("").trim().to_string();
        let task = if task.is_empty() {
            "the current objective based on our conversation".to_string()
        } else {
            task
        };

        println!("\n{}", style("🗺  Generating plan...").bold().cyan());
        let plan_prompt = format!(
            "Create a precise numbered execution plan for: {task}\n\
            Format each step as: N. <action>\n\
            Include verification steps. Keep steps concrete and actionable. Max 8 steps.\n\
            Output ONLY the numbered steps, no preamble.",
        );
        let plan_text = self.run_internal_prompt_text(&plan_prompt, false)?;

        println!("\n{}\n{}", style("Plan").bold().green(), &plan_text);

        let steps = parse_numbered_steps(&plan_text);
        if steps.is_empty() {
            return self.run_turn(&format!("Execute this plan for: {task}\n\n{plan_text}"));
        }

        println!("\n{} {} steps", style("Executing").bold().yellow(), steps.len());
        for (i, step) in steps.iter().enumerate() {
            println!("\n{} [{}/{}] {}", style("▶").bold().cyan(), i + 1, steps.len(), step);
            self.run_turn(step)?;
        }

        println!("\n{}", style("✓ Plan complete").bold().green());
        Ok(())
    }
}

fn parse_numbered_steps(text: &str) -> Vec<String> {
    text.lines()
        .filter_map(|line| {
            let trimmed = line.trim();
            if trimmed.len() < 3 { return None; }
            if !trimmed.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) { return None; }
            let after_num = trimmed.trim_start_matches(|c: char| c.is_ascii_digit());
            let after_sep = after_num.trim_start_matches(|c: char| c == '.' || c == ')' || c == ':').trim();
            if after_sep.is_empty() { None } else { Some(after_sep.to_string()) }
        })
        .collect()
}

fn sessions_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let cwd = env::current_dir()?;
    let path = cwd.join(".claw").join("sessions");
    fs::create_dir_all(&path)?;
    Ok(path)
}

fn create_managed_session_handle() -> Result<SessionHandle, Box<dyn std::error::Error>> {
    let id = generate_session_id();
    let path = sessions_dir()?.join(format!("{id}.json"));
    Ok(SessionHandle { id, path })
}

fn generate_session_id() -> String {
    let millis = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis())
        .unwrap_or_default();
    format!("session-{millis}")
}

fn resolve_session_reference(reference: &str) -> Result<SessionHandle, Box<dyn std::error::Error>> {
    let direct = PathBuf::from(reference);
    let path = if direct.exists() {
        direct
    } else {
        sessions_dir()?.join(format!("{reference}.json"))
    };
    if !path.exists() {
        return Err(format!("session not found: {reference}").into());
    }
    let id = path
        .file_stem()
        .and_then(|value| value.to_str())
        .unwrap_or(reference)
        .to_string();
    Ok(SessionHandle { id, path })
}

fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::error::Error>> {
    let mut sessions = Vec::new();
    for entry in fs::read_dir(sessions_dir()?)? {
        let entry = entry?;
        let path = entry.path();
        if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
            continue;
        }
        let metadata = entry.metadata()?;
        let modified_epoch_secs = metadata
            .modified()
            .ok()
            .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
            .map(|duration| duration.as_secs())
            .unwrap_or_default();
        let message_count = Session::load_from_path(&path)
            .map(|session| session.messages.len())
            .unwrap_or_default();
        let id = path
            .file_stem()
            .and_then(|value| value.to_str())
            .unwrap_or("unknown")
            .to_string();
        sessions.push(ManagedSessionSummary {
            id,
            path,
            modified_epoch_secs,
            message_count,
        });
    }
    sessions.sort_by(|left, right| right.modified_epoch_secs.cmp(&left.modified_epoch_secs));
    Ok(sessions)
}

fn render_session_list(active_session_id: &str) -> Result<String, Box<dyn std::error::Error>> {
    let sessions = list_managed_sessions()?;
    let mut lines = vec![
        "Sessions".to_string(),
        format!("  Directory         {}", sessions_dir()?.display()),
    ];
    if sessions.is_empty() {
        lines.push("  No managed sessions saved yet.".to_string());
        return Ok(lines.join("
"));
    }
    for session in sessions {
        let marker = if session.id == active_session_id {
            "● current"
        } else {
            "○ saved"
        };
        lines.push(format!(
            "  {id:<20} {marker:<10} msgs={msgs:<4} modified={modified} path={path}",
            id = session.id,
            msgs = session.message_count,
            modified = session.modified_epoch_secs,
            path = session.path.display(),
        ));
    }
    Ok(lines.join("
"))
}

fn render_repl_help() -> String {
    [
        "REPL".to_string(),
        "  /exit                Quit the REPL".to_string(),
        "  /quit                Quit the REPL".to_string(),
        "  Up/Down              Navigate prompt history".to_string(),
        "  Tab                  Complete slash commands".to_string(),
        "  Ctrl-C               Clear input (or exit on empty prompt)".to_string(),
        "  Shift+Enter/Ctrl+J   Insert a newline".to_string(),
        String::new(),
        render_slash_command_help(),
    ]
    .join(
        "
",
    )
}

fn status_context(
    session_path: Option<&Path>,
) -> Result<StatusContext, Box<dyn std::error::Error>> {
    let cwd = env::current_dir()?;
    let loader = ConfigLoader::default_for(&cwd);
    let discovered_config_files = loader.discover().len();
    let runtime_config = loader.load()?;
    let project_context = ProjectContext::discover_with_git(&cwd, DEFAULT_DATE)?;
    let (project_root, git_branch) =
        parse_git_status_metadata(project_context.git_status.as_deref());
    Ok(StatusContext {
        cwd,
        session_path: session_path.map(Path::to_path_buf),
        loaded_config_files: runtime_config.loaded_entries().len(),
        discovered_config_files,
        memory_file_count: project_context.instruction_files.len(),
        project_root,
        git_branch,
    })
}

fn format_status_report(
    model: &str,
    usage: StatusUsage,
    permission_mode: &str,
    context: &StatusContext,
) -> String {
    [
        format!(
            "Status
  Model            {model}
  Permission mode  {permission_mode}
  Messages         {}
  Turns            {}
  Estimated tokens {}",
            usage.message_count, usage.turns, usage.estimated_tokens,
        ),
        format!(
            "Usage
  Latest total     {}
  Cumulative input {}
  Cumulative output {}
  Cumulative total {}",
            usage.latest.total_tokens(),
            usage.cumulative.input_tokens,
            usage.cumulative.output_tokens,
            usage.cumulative.total_tokens(),
        ),
        format!(
            "Workspace
  Cwd              {}
  Project root     {}
  Git branch       {}
  Session          {}
  Config files     loaded {}/{}
  Memory files     {}",
            context.cwd.display(),
            context
                .project_root
                .as_ref()
                .map_or_else(|| "unknown".to_string(), |path| path.display().to_string()),
            context.git_branch.as_deref().unwrap_or("unknown"),
            context.session_path.as_ref().map_or_else(
                || "live-repl".to_string(),
                |path| path.display().to_string()
            ),
            context.loaded_config_files,
            context.discovered_config_files,
            context.memory_file_count,
        ),
    ]
    .join(
        "

",
    )
}

fn render_config_report(section: Option<&str>) -> Result<String, Box<dyn std::error::Error>> {
    let cwd = env::current_dir()?;
    let loader = ConfigLoader::default_for(&cwd);
    let discovered = loader.discover();
    let runtime_config = loader.load()?;

    let mut lines = vec![
        format!(
            "Config
  Working directory {}
  Loaded files      {}
  Merged keys       {}",
            cwd.display(),
            runtime_config.loaded_entries().len(),
            runtime_config.merged().len()
        ),
        "Discovered files".to_string(),
    ];
    for entry in discovered {
        let source = match entry.source {
            ConfigSource::User => "user",
            ConfigSource::Project => "project",
            ConfigSource::Local => "local",
        };
        let status = if runtime_config
            .loaded_entries()
            .iter()
            .any(|loaded_entry| loaded_entry.path == entry.path)
        {
            "loaded"
        } else {
            "missing"
        };
        lines.push(format!(
            "  {source:<7} {status:<7} {}",
            entry.path.display()
        ));
    }

    if let Some(section) = section {
        lines.push(format!("Merged section: {section}"));
        let value = match section {
            "env" => runtime_config.get("env"),
            "hooks" => runtime_config.get("hooks"),
            "model" => runtime_config.get("model"),
            other => {
                lines.push(format!(
                    "  Unsupported config section '{other}'. Use env, hooks, or model."
                ));
                return Ok(lines.join(
                    "
",
                ));
            }
        };
        lines.push(format!(
            "  {}",
            match value {
                Some(value) => value.render(),
                None => "<unset>".to_string(),
            }
        ));
        return Ok(lines.join(
            "
",
        ));
    }

    lines.push("Merged JSON".to_string());
    lines.push(format!("  {}", runtime_config.as_json().render()));
    Ok(lines.join(
        "
",
    ))
}

fn render_memory_report() -> Result<String, Box<dyn std::error::Error>> {
    let cwd = env::current_dir()?;
    let project_context = ProjectContext::discover(&cwd, DEFAULT_DATE)?;
    let mut lines = vec![format!(
        "Memory
  Working directory {}
  Instruction files {}",
        cwd.display(),
        project_context.instruction_files.len()
    )];
    if project_context.instruction_files.is_empty() {
        lines.push("Discovered files".to_string());
        lines.push(
            "  No TERNLANG instruction files discovered in the current directory ancestry."
                .to_string(),
        );
    } else {
        lines.push("Discovered files".to_string());
        for (index, file) in project_context.instruction_files.iter().enumerate() {
            let preview = file.content.lines().next().unwrap_or("").trim();
            let preview = if preview.is_empty() {
                "<empty>"
            } else {
                preview
            };
            lines.push(format!("  {}. {}", index + 1, file.path.display(),));
            lines.push(format!(
                "     lines={} preview={}",
                file.content.lines().count(),
                preview
            ));
        }
    }
    Ok(lines.join(
        "
",
    ))
}

fn init_ternlang_md() -> Result<String, Box<dyn std::error::Error>> {
    let cwd = env::current_dir()?;
    Ok(initialize_repo(&cwd)?.render())
}

fn run_init() -> Result<(), Box<dyn std::error::Error>> {
    init::wake_sequence();
    println!("\n{}", init_ternlang_md()?);
    Ok(())
}

fn normalize_permission_mode(mode: &str) -> Option<&'static str> {
    match mode.trim() {
        "read-only" => Some("read-only"),
        "workspace-write" => Some("workspace-write"),
        "danger-full-access" => Some("danger-full-access"),
        _ => None,
    }
}

fn render_diff_report() -> Result<String, Box<dyn std::error::Error>> {
    let output = std::process::Command::new("git")
        .args(["diff", "--", ":(exclude).omx"])
        .current_dir(env::current_dir()?)
        .output()?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
        return Err(format!("git diff failed: {stderr}").into());
    }
    let diff = String::from_utf8(output.stdout)?;
    if diff.trim().is_empty() {
        return Ok(
            "Diff
  Result           clean working tree
  Detail           no current changes"
                .to_string(),
        );
    }
    Ok(format!("Diff

{}", diff.trim_end()))
}

fn render_version_report() -> String {
    format!(
        "Ternlang Code CLI version {}
target: {}
git sha: {}",
        VERSION,
        BUILD_TARGET.unwrap_or("unknown"),
        GIT_SHA.unwrap_or("unknown")
    )
}

struct CliPermissionPrompter {
    permission_mode: PermissionMode,
    interactive: bool,
}

impl CliPermissionPrompter {
    fn new(permission_mode: PermissionMode, interactive: bool) -> Self {
        Self {
            permission_mode,
            interactive,
        }
    }
}

impl runtime::PermissionPrompter for CliPermissionPrompter {
    fn decide(
        &mut self,
        request: &runtime::PermissionRequest,
    ) -> runtime::PermissionPromptDecision {
        let default = match self.permission_mode {
            PermissionMode::ReadOnly => runtime::PermissionPromptDecision::Deny {
                reason: "read-only mode".to_string(),
            },
            PermissionMode::WorkspaceWrite => {
                if request.tool_name.starts_with("shell") {
                    runtime::PermissionPromptDecision::Deny {
                        reason: "shell tools disabled in workspace-write mode".to_string(),
                    }
                } else {
                    runtime::PermissionPromptDecision::Allow
                }
            }
            PermissionMode::DangerFullAccess => return runtime::PermissionPromptDecision::Allow,
            PermissionMode::Allow => return runtime::PermissionPromptDecision::Allow,
            PermissionMode::Prompt => runtime::PermissionPromptDecision::Allow,
        };

        if !self.interactive || !matches!(default, runtime::PermissionPromptDecision::Allow) {
            return default;
        }

        let tool_input_string = request.input.to_string();
        if tool_input_string.len() > 256 {
            println!(
                "Request to use tool `{}` with large input. Preview:\n{}...",
                request.tool_name,
                &tool_input_string[..256]
            );
        } else {
            println!("Request to use tool `{}` with input:\n{}", request.tool_name, request.input);
        }

        loop {
            let choice = dialoguer::Select::new()
                .with_prompt("Allow this tool use?")
                .items(&["Allow once", "Deny"])
                .default(0)
                .interact_opt()
                .unwrap_or_default();

            match choice {
                Some(0) => return runtime::PermissionPromptDecision::Allow,
                Some(1) => {
                    return runtime::PermissionPromptDecision::Deny {
                        reason: "user denied".to_string(),
                    }
                }
                Some(_) => {
                    return runtime::PermissionPromptDecision::Deny {
                        reason: "invalid choice".to_string(),
                    }
                }
                None => {
                    // User cancelled with Esc, treat as denial
                    return runtime::PermissionPromptDecision::Deny {
                        reason: "user cancelled".to_string(),
                    };
                }
            }
        }
    }
}

fn detect_provider_from_key(key: &str) -> Option<&'static str> {
    if key.starts_with("sk-ant-") {
        Some("anthropic")
    } else if key.starts_with("AIza") {
        Some("google")
    } else if key.starts_with("sk-") {
        Some("openai")
    } else if key.starts_with("xai-") {
        Some("xai")
    } else if key.starts_with("hf_") {
        Some("huggingface")
    } else {
        None
    }
}

fn provider_display_name(provider: &str) -> &str {
    match provider {
        "anthropic"    => "Anthropic (Claude)",
        "google"       => "Google (Gemini)",
        "openai"       => "OpenAI",
        "xai"          => "XAI (Grok)",
        "huggingface"  => "HuggingFace",
        "ollama"       => "Ollama",
        _              => "Ternlang",
    }
}

/// If the user is on the built-in default and hasn't set a Gemini key,
/// switch to whatever provider they actually have a key for.
fn auto_select_model(model: &str) -> String {
    if model != DEFAULT_MODEL {
        return model.to_string();
    }
    let gemini_set = std::env::var("GEMINI_API_KEY").ok().filter(|v| !v.is_empty()).is_some()
        || std::env::var("GOOGLE_API_KEY").ok().filter(|v| !v.is_empty()).is_some();
    if gemini_set {
        return model.to_string();
    }
    if let Some((_, fallback_model)) = api::detect_provider_and_model_from_env() {
        return fallback_model.to_string();
    }
    model.to_string()
}

fn resolve_provider_for_model(model: &str) -> api::LlmProvider {
    if model.contains("gpt-") || model.contains("o1-") || model.contains("o3-") {
        api::LlmProvider::OpenAi
    } else if model.contains("claude-") || model.contains("opus") || model.contains("sonnet") || model.contains("haiku") {
        api::LlmProvider::Anthropic
    } else if model.contains("gemini-") {
        api::LlmProvider::Google
    } else if model.contains("llama") || model.contains("mistral") {
        api::LlmProvider::HuggingFace
    } else {
        api::LlmProvider::Ternlang
    }
}

fn build_runtime(
    session: Session,
    model: String,
    system_prompt: Vec<String>,
    enable_tools: bool,
    enable_stream_events: bool,
    allowed_tools: Option<AllowedToolSet>,
    permission_mode: PermissionMode,
) -> Result<ConversationRuntime<TernlangRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
{
    let cwd = env::current_dir()?;
    let _config = ConfigLoader::default_for(&cwd).load()?;

    let provider = resolve_provider_for_model(&model);
    let provider_config = runtime::load_provider_config(match provider {
        api::LlmProvider::Anthropic => "anthropic",
        api::LlmProvider::Google => "google",
        api::LlmProvider::OpenAi => "openai",
        api::LlmProvider::Xai => "xai",
        api::LlmProvider::HuggingFace => "huggingface",
        api::LlmProvider::Ollama => "ollama",
        _ => "ternlang",
    }).unwrap_or(None);

    let auth_source = if let Some(config) = provider_config {
        if let Some(key) = config.api_key {
            api::AuthSource::ApiKey(key)
        } else {
            api::resolve_auth_for_provider(provider).unwrap_or(api::AuthSource::None)
        }
    } else {
        api::resolve_auth_for_provider(provider).unwrap_or(api::AuthSource::None)
    };

    let client = TernlangClient::from_auth(auth_source).with_provider(provider);
    let api_client = TernlangRuntimeClient {
        client,
        model: model.clone(),
        max_tokens: max_tokens_for_model(&model),
        tools: if enable_tools {
            filter_tool_specs(allowed_tools.as_ref())
        } else {
            Vec::new()
        },
        event_tx: if enable_stream_events {
            Some(
                tokio::runtime::Runtime::new()?
                    .block_on(async {
                        let (tx, _) = tokio::sync::broadcast::channel(128);
                        tx
                    }),
            )
        } else {
            None
        },
    };

    let tool_executor = CliToolExecutor::new();
    let permission_policy = PermissionPolicy::new(permission_mode);
    Ok(ConversationRuntime::new(
        session,
        api_client,
        tool_executor,
        permission_policy,
        system_prompt,
    ))
}

fn build_system_prompt() -> Result<Vec<String>, Box<dyn std::error::Error>> {
    let cwd = env::current_dir()?;
    Ok(load_system_prompt(
        cwd,
        DEFAULT_DATE.to_string(),
        env::consts::OS,
        "unknown",
    )?)
}

#[derive(Clone)]
struct TernlangRuntimeClient {
    client: TernlangClient,
    model: String,
    max_tokens: u32,
    tools: Vec<ToolSpec>,
    event_tx: Option<tokio::sync::broadcast::Sender<AssistantEvent>>,
}

impl ApiClient for TernlangRuntimeClient {
    fn stream(
        &mut self,
        request: ApiRequest,
    ) -> Result<Vec<AssistantEvent>, RuntimeError> {
        let runtime = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .map_err(|e| RuntimeError::new(e.to_string()))?;
        runtime.block_on(self.stream_async(request)).map_err(|e| RuntimeError::new(e.to_string()))
    }
}

impl TernlangRuntimeClient {
    async fn stream_async(
        &mut self,
        request: ApiRequest,
    ) -> Result<Vec<AssistantEvent>, Box<dyn std::error::Error + Send + Sync>> {
        let mut stream = self
            .client
            .stream_message(&MessageRequest {
                model: self.model.clone(),
                max_tokens: Some(self.max_tokens),
                messages: request
                    .messages
                    .into_iter()
                    .map(map_conversation_message)
                    .collect(),
                system: Some(request.system_prompt.join("

")),
                tools: if self.tools.is_empty() {
                    None
                } else {
                    Some(self.tools.iter().map(map_tool_spec).collect())
                },
                tool_choice: if self.tools.is_empty() {
                    None
                } else {
                    Some(ToolChoice::Auto)
                },
                stream: true,
            })
            .await?;

        let mut events = Vec::new();
        while let Some(event) = stream.next_event().await? {
            if let Some(mapped) = map_stream_event(&event) {
                if let Some(tx) = &self.event_tx {
                    let _ = tx.send(mapped.clone());
                }
                events.push(mapped);
            }
        }
        Ok(events)
    }
}

fn map_conversation_message(message: ConversationMessage) -> InputMessage {
    InputMessage {
        role: match message.role {
            MessageRole::User => "user".to_string(),
            MessageRole::Assistant => "assistant".to_string(),
            MessageRole::Tool => "user".to_string(),
            MessageRole::System => "system".to_string(),
        },
        content: message
            .blocks
            .into_iter()
            .map(map_content_block)
            .collect(),
    }
}

fn map_content_block(block: ContentBlock) -> InputContentBlock {
    match block {
        ContentBlock::Text { text } => InputContentBlock::Text { text },
        ContentBlock::ToolUse { id, name, input } => InputContentBlock::ToolUse {
            id, name,
            input: serde_json::from_str(&input).unwrap_or(serde_json::Value::Null),
        },
        ContentBlock::ToolResult { tool_use_id, output, tool_name: _, is_error: _ } => InputContentBlock::ToolResult {
            tool_use_id,
            content: vec![ToolResultContentBlock::Text { text: output }],
            is_error: false,
        },
    }
}

fn map_tool_spec(spec: &ToolSpec) -> ToolDefinition {
    ToolDefinition {
        name: spec.name.to_string(),
        description: Some(spec.description.to_string()),
        input_schema: spec.input_schema.clone(),
    }
}

fn map_stream_event(event: &ApiStreamEvent) -> Option<AssistantEvent> {
    match event {
        ApiStreamEvent::MessageStart(payload) => Some(AssistantEvent::Usage(map_usage(payload.message.usage.clone()))),
        ApiStreamEvent::ContentBlockDelta(payload) => match &payload.delta {
            ContentBlockDelta::TextDelta { text } => Some(AssistantEvent::TextDelta(text.clone())),
            _ => None,
        },
        ApiStreamEvent::ContentBlockStart(payload) => match &payload.content_block {
            OutputContentBlock::ToolUse { id, name, input } => Some(AssistantEvent::ToolUse {
                id: id.clone(),
                name: name.clone(),
                input: input.to_string(),
            }),
            _ => None,
        },
        ApiStreamEvent::MessageDelta(payload) => {
            Some(AssistantEvent::Usage(map_usage(payload.usage.clone())))
        }
        ApiStreamEvent::MessageStop(_) => Some(AssistantEvent::MessageStop),
        _ => None,
    }
}

fn map_usage(usage: api::Usage) -> TokenUsage {
    TokenUsage {
        input_tokens: usage.input_tokens,
        output_tokens: usage.output_tokens,
        cache_creation_input_tokens: usage.cache_creation_input_tokens,
        cache_read_input_tokens: usage.cache_read_input_tokens,
    }
}

#[derive(Clone)]
struct CliToolExecutor {}

impl CliToolExecutor {
    fn new() -> Self {
        Self {}
    }
}

impl ToolExecutor for CliToolExecutor {
    fn execute(
        &mut self,
        tool_name: &str,
        input: &str,
    ) -> Result<runtime::ToolResult, ToolError> {
        let input_val: serde_json::Value = serde_json::from_str(input).unwrap_or(serde_json::Value::Null);
        let preview = tool_call_preview(tool_name, &input_val);
        println!("{} {}", style("→").cyan().bold(), style(&preview).dim());
        match execute_tool(tool_name, &input_val) {
            Ok(res) => Ok(runtime::ToolResult {
                output: res.output,
                state: res.state,
            }),
            Err(e) => {
                println!("{} {} failed", style("✘").red().bold(), style(tool_name).dim());
                Err(ToolError::new(e))
            }
        }
    }

    fn query_memory(&mut self, query: &str) -> Result<String, ToolError> {
        let input = json!({
            "action": "search_nodes",
            "query": query
        });
        match execute_tool("Memory", &input) {
            Ok(res) => Ok(res.output),
            Err(e) => Err(ToolError::new(e)),
        }
    }
}

fn typewriter_print(text: &str) {
    use std::io::Write;
    let stdout = std::io::stdout();
    let mut out = stdout.lock();
    let delay = if text.len() > 800 { 1 } else if text.len() > 300 { 2 } else { 3 };
    for ch in text.chars() {
        let _ = write!(out, "{ch}");
        let _ = out.flush();
        thread::sleep(Duration::from_millis(delay));
    }
    println!();
}

// ── Self-reflection memory ───────────────────────────────────────────────────

/// Heuristic score: how important is this turn to remember?
fn score_turn_importance(user_input: &str, response: &str) -> f32 {
    let combined = format!("{user_input} {response}").to_lowercase();
    let mut score: f32 = 0.0;

    // Explicit memory commands
    for kw in &["remember this", "remember:", "rule:", "never do", "always do",
                "from now on", "important:", "note:", "my name is", "i prefer"] {
        if combined.contains(kw) { score += 1.0; break; }
    }

    // New facts about user / project
    for kw in &["my email", "my api key", "my github", "the project is",
                "deadline", "we use", "the team", "our stack"] {
        if combined.contains(kw) { score += 0.7; break; }
    }

    // Successful novel task
    if response.len() > 400 && (response.contains("successfully") || response.contains("complete")) {
        score += 0.3;
    }

    // Errors or corrections
    if combined.contains("you were wrong") || combined.contains("that's incorrect")
        || combined.contains("you made a mistake") {
        score += 0.5; // corrections are valuable memories
    }

    score.clamp(0.0, 1.0)
}

fn distill_memory_summary(user_input: &str, response: &str) -> String {
    // Find the most relevant sentence from user input (first sentence that triggers memory)
    let trigger_kws = ["remember", "rule:", "never", "always", "from now on",
                       "important:", "note:", "my name", "i prefer", "deadline",
                       "we use", "the project", "you were wrong", "that's incorrect"];
    let user_lower = user_input.to_lowercase();
    for kw in &trigger_kws {
        if user_lower.contains(kw) {
            // Return first sentence containing the keyword
            if let Some(sentence) = user_input.split('.').find(|s| s.to_lowercase().contains(kw)) {
                return sentence.trim().to_string();
            }
        }
    }
    // Fallback: first 120 chars of user input
    let trimmed = user_input.trim();
    if trimmed.len() > 120 { trimmed[..120].to_string() } else { trimmed.to_string() }
}

fn append_to_albert_memory(summary: &str) -> std::io::Result<()> {
    let memory_path = dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join(".ternlang")
        .join("memory.md");

    if let Some(parent) = memory_path.parent() {
        fs::create_dir_all(parent)?;
    }

    if !memory_path.exists() {
        fs::write(&memory_path, "# Albert Memory\n\nAutomatic self-reflection log.\n\n")?;
    }

    let date = chrono::Local::now().format("%Y-%m-%d %H:%M").to_string();
    let entry = format!("- **{date}**: {summary}\n");

    let mut file = std::fs::OpenOptions::new()
        .append(true)
        .open(&memory_path)?;

    use std::io::Write;
    file.write_all(entry.as_bytes())
}

// ── Image input ──────────────────────────────────────────────────────────────

/// Parse `[image: /path/to/file.png]` tags from user input.
/// Returns (cleaned_text, list_of_image_paths)
fn extract_image_attachments(input: &str) -> (String, Vec<String>) {
    let mut paths = Vec::new();
    let mut cleaned = input.to_string();

    // Match [image: path] or [img: path] or [attach: path]
    let patterns = &["[image: ", "[img: ", "[attach: "];
    for prefix in patterns {
        loop {
            let Some(start) = cleaned.find(prefix) else { break };
            let rest = &cleaned[start + prefix.len()..];
            let Some(end_offset) = rest.find(']') else { break };
            let path = rest[..end_offset].trim().to_string();
            let full_tag = format!("{}{}{}", prefix, path, "]");
            paths.push(path);
            cleaned = cleaned.replacen(&full_tag, "", 1);
        }
    }
    (cleaned.trim().to_string(), paths)
}

/// Load an image file and return a base64-encoded data URI for inclusion in messages.
fn load_image_as_base64(path: &str) -> Result<(String, String), String> {
    use base64::{Engine as _, engine::general_purpose::STANDARD};
    let data = fs::read(path).map_err(|e| format!("cannot read image {path}: {e}"))?;
    let mime = if path.ends_with(".png") { "image/png" }
        else if path.ends_with(".jpg") || path.ends_with(".jpeg") { "image/jpeg" }
        else if path.ends_with(".gif") { "image/gif" }
        else if path.ends_with(".webp") { "image/webp" }
        else { "image/png" };
    Ok((mime.to_string(), STANDARD.encode(&data)))
}

fn parse_retry_after(error_str: &str) -> Option<u64> {
    // Look for "retryDelay":"28s" or "retry in 28s"
    let lower = error_str.to_lowercase();
    for pattern in &["retrydelay\":\"", "retry in "] {
        if let Some(pos) = lower.find(pattern) {
            let rest = &lower[pos + pattern.len()..];
            let num_str: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
            if let Ok(n) = num_str.parse::<u64>() {
                return Some(n);
            }
        }
    }
    None
}

fn fallback_model_for(model: &str) -> Option<&'static str> {
    let fallbacks: &[(&str, &str)] = &[
        ("gemini-2.5-flash",       "gemini-2.5-pro-preview"),
        ("gemini-2.5-pro-preview", "gemini-2.5-flash"),
        ("gemini-2.0-flash",       "gemini-2.5-flash"),
        ("gpt-4o",                 "gpt-4o-mini"),
        ("claude-opus-4-7",        "claude-sonnet-4-6"),
        ("claude-sonnet-4-6",      "claude-haiku-4-5"),
    ];
    fallbacks.iter().find(|(from, _)| *from == model).map(|(_, to)| *to)
}

fn clean_error_message(raw: &str) -> String {
    // Extract first "message": "..." from JSON error body, or first non-empty line
    if let Some(start) = raw.find("\"message\":") {
        let rest = &raw[start + 10..].trim_start();
        let rest = rest.trim_start_matches('"');
        let msg: String = rest.chars().take_while(|&c| c != '"').collect();
        if !msg.is_empty() {
            // Truncate at newline or quota detail
            let first_line = msg.lines().next().unwrap_or(&msg);
            return first_line.trim_end_matches('.').to_string();
        }
    }
    // Fallback: first line only
    raw.lines().next().unwrap_or(raw).to_string()
}

fn tool_call_preview(tool_name: &str, input: &serde_json::Value) -> String {
    let key_args = ["path", "command", "pattern", "url", "query", "file_path"];
    for key in &key_args {
        if let Some(val) = input.get(key).and_then(|v| v.as_str()) {
            let truncated = if val.len() > 60 { &val[..60] } else { val };
            return format!("{tool_name}({key}: {truncated:?})");
        }
    }
    format!("{tool_name}()")
}

fn final_assistant_text(summary: &runtime::TurnSummary) -> String {
    summary
        .assistant_messages
        .iter()
        .flat_map(|message| message.blocks.iter())
        .filter_map(|block| match block {
            ContentBlock::Text { text } => Some(text.as_str()),
            _ => None,
        })
        .collect::<Vec<_>>()
        .join("
")
}

fn collect_tool_uses(summary: &runtime::TurnSummary) -> serde_json::Value {
    serde_json::Value::Array(
        summary
            .assistant_messages
            .iter()
            .flat_map(|message| message.blocks.iter())
            .filter_map(|block| match block {
                ContentBlock::ToolUse { id, name, input } => Some(json!({
                    "id": id,
                    "name": name,
                    "input": input,
                })),
                _ => None,
            })
            .collect(),
    )
}

fn collect_tool_results(summary: &runtime::TurnSummary) -> serde_json::Value {
    serde_json::Value::Array(
        summary
            .tool_results
            .iter()
            .flat_map(|message| message.blocks.iter())
            .filter_map(|block| match block {
                ContentBlock::ToolResult { tool_use_id, output, tool_name: _, is_error: _ } => Some(json!({
                    "tool_use_id": tool_use_id,
                    "output": output,
                })),
                _ => None,
            })
            .collect(),
    )
}

fn slash_command_completion_candidates() -> Vec<String> {
    let mut candidates: Vec<String> = slash_command_specs()
        .iter()
        .map(|spec| format!("/{}", spec.name))
        .collect();
    candidates.sort();
    candidates
}

struct ModelEntry {
    id: &'static str,
    provider: &'static str,
    description: &'static str,
}

const KNOWN_MODELS: &[ModelEntry] = &[
    ModelEntry { id: "gemini-2.5-pro-preview",   provider: "Google",    description: "Most capable Gemini — complex reasoning" },
    ModelEntry { id: "gemini-2.5-flash",          provider: "Google",    description: "Fast & capable — recommended default" },
    ModelEntry { id: "gemini-2.5-flash-lite",     provider: "Google",    description: "Lightest Gemini — maximum speed" },
    ModelEntry { id: "gemini-2.0-flash",          provider: "Google",    description: "Previous Flash generation" },
    ModelEntry { id: "claude-opus-4-7",           provider: "Anthropic", description: "Most capable Claude" },
    ModelEntry { id: "claude-sonnet-4-6",         provider: "Anthropic", description: "Best balance of speed and capability" },
    ModelEntry { id: "claude-haiku-4-5",          provider: "Anthropic", description: "Fastest Claude" },
    ModelEntry { id: "gpt-4o",                    provider: "OpenAI",    description: "GPT-4o multimodal flagship" },
    ModelEntry { id: "gpt-4o-mini",               provider: "OpenAI",    description: "Efficient GPT-4o variant" },
    ModelEntry { id: "o3-mini",                   provider: "OpenAI",    description: "o3 reasoning — efficient" },
    ModelEntry { id: "grok-3",                    provider: "xAI",       description: "Grok 3 flagship" },
    ModelEntry { id: "grok-3-mini",               provider: "xAI",       description: "Efficient Grok variant" },
    ModelEntry { id: "llama-3.3-70b-versatile",   provider: "Ollama",    description: "Llama 3.3 70B — local or hosted" },
];

fn show_model_picker(current_model: &str) -> Option<String> {
    let items: Vec<String> = KNOWN_MODELS.iter().map(|m| {
        let active = if m.id == current_model { style("●").green().to_string() } else { " ".to_string() };
        format!("{} {:<32} {:>12}   {}",
            active,
            style(m.id).bold(),
            style(format!("({})", m.provider)).dim(),
            style(m.description).dim(),
        )
    }).collect();

    let current_idx = KNOWN_MODELS.iter().position(|m| m.id == current_model).unwrap_or(0);

    println!("\n{}", style("Select Model").bold().cyan());
    println!("{}", style("─".repeat(72)).dim());

    let result = dialoguer::Select::new()
        .items(&items)
        .default(current_idx)
        .interact_opt()
        .unwrap_or(None)?;

    let chosen = KNOWN_MODELS[result].id;
    if chosen == current_model {
        return None;
    }
    Some(chosen.to_string())
}

fn show_slash_picker(filter: &str) -> Option<String> {
    let filter_str = filter.trim_start_matches('/').to_lowercase();
    let specs: Vec<&commands::SlashCommandSpec> = slash_command_specs()
        .iter()
        .filter(|s| filter_str.is_empty() || s.name.starts_with(filter_str.as_str()))
        .collect();

    if specs.is_empty() {
        return None;
    }

    let items: Vec<String> = specs.iter().map(|s| {
        let hint = s.argument_hint.map(|h| format!(" {h}")).unwrap_or_default();
        format!("{:<22}  {}",
            style(format!("/{}{}", s.name, hint)).cyan().bold(),
            s.summary,
        )
    }).collect();

    println!("\n{}", style("─".repeat(60)).dim());
    let result = dialoguer::Select::new()
        .items(&items)
        .default(0)
        .interact_opt()
        .unwrap_or(None)?;

    Some(format!("/{}", specs[result].name))
}

fn print_status_bar(model: &str, session_id: &str, msg_count: usize) {
    let model_part  = style(format!("model:{model}")).cyan();
    let session_part = style(format!("session:{}", &session_id[..session_id.len().min(18)])).dim();
    let msgs_part   = style(format!("msgs:{msg_count}")).dim();
    let hint_part   = style("/ for commands").dim();
    println!("{model_part}  {session_part}  {msgs_part}  {hint_part}");
}

fn truncate_for_prompt(value: &str, max_chars: usize) -> String {
    if value.len() <= max_chars {
        return value.to_string();
    }
    let mut truncated = String::with_capacity(max_chars + 20);
    truncated.push_str(&value[..max_chars]);
    truncated.push_str("
... (truncated)");
    truncated
}

fn write_temp_text_file(name: &str, content: &str) -> Result<PathBuf, io::Error> {
    let path = env::temp_dir().join(name);
    fs::write(&path, content)?;
    Ok(path)
}

fn command_exists(name: &str) -> bool {
    let Ok(paths) = env::var("PATH") else {
        return false;
    };
    paths
        .split(':')
        .map(|path| Path::new(path).join(name))
        .any(|path| path.exists())
}

fn sanitize_generated_message(message: &str) -> String {
    let message = message.trim();
    if let Some(stripped) = message.strip_prefix("```text
") {
        return stripped.strip_suffix("```").unwrap_or(stripped).to_string();
    }
    if let Some(stripped) = message.strip_prefix("```") {
        return stripped.strip_suffix("```").unwrap_or(stripped).to_string();
    }
    message.to_string()
}

fn parse_titled_body(raw: &str) -> Option<(String, String)> {
    let Some(title_start) = raw.find("TITLE:") else {
        return None;
    };
    let Some(body_start) = raw.find("BODY:") else {
        return None;
    };
    let title = raw[title_start + 6..body_start].trim().to_string();
    let body = raw[body_start + 5..].trim().to_string();
    Some((title, body))
}

fn git_output(args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
    let output = std::process::Command::new("git")
        .args(args)
        .current_dir(env::current_dir()?)
        .output()?;
    Ok(String::from_utf8(output.stdout)?)
}

fn git_status_ok(args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
    let status = std::process::Command::new("git")
        .args(args)
        .current_dir(env::current_dir()?)
        .status()?;
    if !status.success() {
        return Err("git command failed".into());
    }
    Ok(())
}

fn recent_user_context(session: &Session, max_messages: usize) -> String {
    session
        .messages
        .iter()
        .filter(|message| message.role == MessageRole::User)
        .rev()
        .take(max_messages)
        .flat_map(|message| message.blocks.iter())
        .filter_map(|block| match block {
            ContentBlock::Text { text } => Some(text.as_str()),
            _ => None,
        })
        .collect::<Vec<_>>()
        .join("

")
}

fn resolve_export_path(requested_path: Option<&str>, _session: &Session) -> Result<PathBuf, Box<dyn std::error::Error>> {
    Ok(PathBuf::from(requested_path.unwrap_or("export.txt")))
}

fn render_export_text(_session: &Session) -> String {
    "".to_string()
}

fn render_teleport_report(_target: &str) -> Result<String, Box<dyn std::error::Error>> {
    Ok("".to_string())
}

fn render_last_tool_debug_report(_session: &Session) -> Result<String, Box<dyn std::error::Error>> {
    Ok("".to_string())
}

fn check_workspace_trust() -> Result<(), Box<dyn std::error::Error>> {
    let cwd = env::current_dir()?;
    
    println!("\n{}", style("────────────────────────────────────────────────────────────").dim());
    println!("Accessing workspace:");
    println!("\n  {}\n", style(cwd.display()).cyan());
    
    println!("Quick safety check: Is this a project you created or one you trust?");
    println!("(Like your own code, a well-known open source project, or work from your team).");
    println!("If not, take a moment to review what's in this folder first.\n");
    
    let options = vec!["Yes, I trust this folder", "No, exit"];
    let selection = Select::new()
        .with_prompt("Security guide")
        .items(&options)
        .default(0)
        .interact()?;
        
    if selection == 1 {
        println!("\nExiting for safety.");
        std::process::exit(0);
    }
    
    println!("{}", style("Trust verified. Let's build.").dim());
    Ok(())
}
