337 lines · 10.1 KB · Rust
RawDownload
/// Codex TUI for secure-exec VM.////// Full terminal UI using ratatui + crossterm backend, rendering through/// the WasmVM PTY. This is the interactive entry point — for headless/// (scriptable) usage, see codex-exec.////// Uses wasi-spawn for process spawning via host_process FFI and/// wasi-http for HTTP/HTTPS requests via host_net TCP/TLS imports.////// crossterm is patched for WASI support (see std-patches/crates/crossterm/):///   - Terminal raw mode tracked in-process (host PTY handles discipline)///   - Terminal size from COLUMNS/LINES env vars///   - Event source reads stdin directly and parses ANSI escape sequences///   - IsTty returns true (PTY slave FDs are terminals)use std::io; use ratatui::{    backend::CrosstermBackend,    crossterm::{        event::{self, Event, KeyCode, KeyEvent, KeyModifiers},        execute,        terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},    },    layout::{Constraint, Layout},    style::{Color, Modifier, Style},    text::{Line, Span, Text},    widgets::{Block, Borders, Paragraph, Wrap},    Frame, Terminal,}; // Validate WASI stub crates compile by referencing key typesuse codex_network_proxy::NetworkProxy;use codex_otel::SessionTelemetry; const VERSION: &str = env!("CARGO_PKG_VERSION");const MAX_INPUT_CHARS: usize = 8192;const MAX_MESSAGES: usize = 200; fn main() {    let args: Vec<String> = std::env::args().collect();     // Handle --help    if args.iter().any(|a| a == "--help" || a == "-h") {        print_help();        return;    }     // Handle --version    if args.iter().any(|a| a == "--version" || a == "-V") {        println!("codex {}", VERSION);        return;    }     // Handle --model flag (accept but note it's stored for future use)    let model = args        .windows(2)        .find(|w| w[0] == "--model")        .map(|w| w[1].clone());     // Built-in HTTP test subcommand (validates wasi-http integration)    if args.get(1).map(|s| s.as_str()) == Some("--http-test") {        return http_test(&args[2..]);    }     // Stub validation subcommand (validates WASI stub crates)    if args.get(1).map(|s| s.as_str()) == Some("--stub-test") {        return stub_test();    }     // Run the TUI    if let Err(e) = run_tui(model.as_deref()) {        eprintln!("codex: TUI error: {}", e);        std::process::exit(1);    }} /// Main TUI event loop using ratatui + crossterm.fn run_tui(model: Option<&str>) -> io::Result<()> {    let _terminal_guard = TerminalGuard::enter()?;     let stdout = io::stdout();    let backend = CrosstermBackend::new(stdout);    let mut terminal = Terminal::new(backend)?;     // App state    let mut input = String::new();    let mut messages: Vec<String> = Vec::new();    let mut should_quit = false;     // Draw initial frame    terminal.draw(|f| draw_ui(f, &input, &messages, model))?;     // Event loop    while !should_quit {        match event::read()? {            Event::Key(key) => {                handle_key_event(key, &mut input, &mut messages, &mut should_quit);                terminal.draw(|f| draw_ui(f, &input, &messages, model))?;            }            Event::Resize(_, _) => {                terminal.draw(|f| draw_ui(f, &input, &messages, model))?;            }            _ => {}        }    }     Ok(())} /// Handle a key event, updating app state.fn handle_key_event(    key: KeyEvent,    input: &mut String,    messages: &mut Vec<String>,    should_quit: &mut bool,) {    match (key.code, key.modifiers) {        // Ctrl+C quits        (KeyCode::Char('c'), m) if m.contains(KeyModifiers::CONTROL) => {            *should_quit = true;        }        // 'q' on empty input quits        (KeyCode::Char('q'), KeyModifiers::NONE) if input.is_empty() => {            *should_quit = true;        }        // Enter submits the input        (KeyCode::Enter, _) => {            if !input.is_empty() {                let prompt = input.clone();                push_message(messages, format!("> {}", prompt));                push_message(                    messages,                    "codex: agent loop is under development".to_string(),                );                push_message(                    messages,                    format!("codex: prompt received ({} chars)", prompt.len()),                );                input.clear();            }        }        // Backspace deletes last char        (KeyCode::Backspace, _) => {            input.pop();        }        // Esc clears input        (KeyCode::Esc, _) => {            input.clear();        }        // Regular character input        (KeyCode::Char(c), _) => {            if input.chars().count() < MAX_INPUT_CHARS {                input.push(c);            }        }        _ => {}    }} fn push_message(messages: &mut Vec<String>, message: String) {    messages.push(message);    if messages.len() > MAX_MESSAGES {        messages.drain(..messages.len() - MAX_MESSAGES);    }} struct TerminalGuard {    raw_mode_enabled: bool,    alternate_screen_enabled: bool,} impl TerminalGuard {    fn enter() -> io::Result<Self> {        terminal::enable_raw_mode()?;         if let Err(error) = execute!(io::stdout(), EnterAlternateScreen) {            let _ = terminal::disable_raw_mode();            return Err(error);        }         Ok(Self {            raw_mode_enabled: true,            alternate_screen_enabled: true,        })    }} impl Drop for TerminalGuard {    fn drop(&mut self) {        if self.alternate_screen_enabled {            let _ = execute!(io::stdout(), LeaveAlternateScreen);            self.alternate_screen_enabled = false;        }         if self.raw_mode_enabled {            let _ = terminal::disable_raw_mode();            self.raw_mode_enabled = false;        }    }} /// Draw the TUI layout.fn draw_ui(f: &mut Frame, input: &str, messages: &[String], model: Option<&str>) {    let area = f.area();     let chunks = Layout::vertical([        Constraint::Length(3), // Header        Constraint::Min(5),    // Messages        Constraint::Length(3), // Input    ])    .split(area);     // Header    let model_text = model.unwrap_or("default");    let header = Paragraph::new(Line::from(vec![        Span::styled(            "Codex ",            Style::default()                .fg(Color::Cyan)                .add_modifier(Modifier::BOLD),        ),        Span::styled(VERSION, Style::default().fg(Color::DarkGray)),        Span::raw("  "),        Span::styled(            format!("model: {}", model_text),            Style::default().fg(Color::Yellow),        ),        Span::raw("  "),        Span::styled("WasmVM", Style::default().fg(Color::Green)),    ]))    .block(Block::default().borders(Borders::ALL).title(" codex "));    f.render_widget(header, chunks[0]);     // Messages area    let msg_lines: Vec<Line> = messages        .iter()        .map(|m| {            if m.starts_with("> ") {                Line::from(Span::styled(m.as_str(), Style::default().fg(Color::Cyan)))            } else {                Line::from(Span::raw(m.as_str()))            }        })        .collect();     let welcome = if messages.is_empty() {        vec![            Line::from(""),            Line::from(Span::styled(                "Welcome to Codex on WasmVM!",                Style::default()                    .fg(Color::Cyan)                    .add_modifier(Modifier::BOLD),            )),            Line::from(""),            Line::from("Type a prompt and press Enter to submit."),            Line::from("Press 'q' (on empty input) or Ctrl+C to exit."),        ]    } else {        msg_lines    };     let messages_widget = Paragraph::new(Text::from(welcome))        .block(Block::default().borders(Borders::ALL).title(" messages "))        .wrap(Wrap { trim: false });    f.render_widget(messages_widget, chunks[1]);     // Input area    let input_widget = Paragraph::new(Line::from(vec![        Span::styled("❯ ", Style::default().fg(Color::Green)),        Span::raw(input),    ]))    .block(Block::default().borders(Borders::ALL).title(" input "));    f.render_widget(input_widget, chunks[2]);} fn print_help() {    println!(        "codex {} — interactive Codex TUI for secure-exec VM",        VERSION    );    println!();    println!("USAGE:");    println!("    codex [OPTIONS]");    println!();    println!("OPTIONS:");    println!("    -h, --help         Print this help message");    println!("    -V, --version      Print version information");    println!("    --model MODEL      Select model for completions");    println!("    --http-test URL    Test HTTP client via host_net");    println!("    --stub-test        Validate WASI stub crates");    println!();    println!("DESCRIPTION:");    println!("    Interactive TUI for the Codex agent, rendered through");    println!("    the WasmVM PTY using ratatui + crossterm backend.");    println!("    For headless mode, use codex-exec instead.");} fn stub_test() {    let proxy = NetworkProxy;    let mut env = std::collections::HashMap::new();    proxy.apply_to_env(&mut env);    println!("network-proxy: NetworkProxy is zero-size, apply_to_env is no-op");     let telemetry = SessionTelemetry::new();    telemetry.counter("test.counter", 1, &[]);    telemetry.histogram("test.histogram", 42, &[]);    println!("otel: SessionTelemetry metrics are no-ops");     let global = codex_otel::metrics::global();    assert!(global.is_none(), "global metrics should be None on WASI");    println!("otel: global() returns None (no exporter on WASI)");     println!("stub-test: all stubs validated successfully");} fn http_test(args: &[String]) {    if args.is_empty() {        eprintln!("usage: codex --http-test <url>");        std::process::exit(1);    }     let url = &args[0];    match wasi_http::get(url) {        Ok(resp) => {            println!("status: {}", resp.status);            match resp.text() {                Ok(body) => println!("body: {}", body),                Err(e) => eprintln!("body decode error: {}", e),            }        }        Err(e) => {            eprintln!("http error: {}", e);            std::process::exit(1);        }    }}