#![expect(dead_code)] use std::io; use std::io::Write; use anyhow::Result; use crossterm::execute; use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; use ratatui::backend::CrosstermBackend; use ratatui::Terminal; mod app; mod controller; mod dto; mod ipc; mod model; mod security; mod service; mod tool; mod resources; mod view; fn main() -> Result<()> { let args: Vec = std::env::args().collect(); let is_daemon = args.iter().any(|a| a == "--daemon"); let attach_session = args.iter() .position(|a| a == "--attach") .and_then(|i| args.get(i + 1).cloned()); tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .init(); if is_daemon && attach_session.is_some() { anyhow::bail!("--daemon and --attach are mutually exclusive"); } if is_daemon { return run_daemon(); } if let Some(session_id) = attach_session { return run_attach(&session_id); } run_single_process() } fn run_single_process() -> Result<()> { let store = model::store::Store::new(); store.ensure_dirs()?; let session_id = uuid::Uuid::new_v4().to_string(); let session_dir = store.base_dir.join("sessions").join(&session_id); std::fs::create_dir_all(&session_dir)?; let workspace_roots = vec![std::env::current_dir()?]; let mut state = app::state::rest::AppStateRest::new( workspace_roots.clone(), session_dir, store.memory_dir, ); state.sessions = model::session::Session::list(&store.base_dir); let _rt = tokio::runtime::Runtime::new()?; enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; terminal.clear()?; let run_result = run_loop(&mut state, &mut terminal); let mut restore_stdout = io::stdout(); let _ = execute!(restore_stdout, LeaveAlternateScreen); let _ = disable_raw_mode(); if let Err(e) = run_result { let _ = writeln!(restore_stdout, "error: {}", e); let _ = restore_stdout.flush(); } let _ = state.settings.save(); Ok(()) } fn key_code_to_action(code: crossterm::event::KeyCode) -> Option { use crossterm::event::KeyCode; match code { KeyCode::Char(c) => Some(ipc::protocol::KeyAction::Char(c)), KeyCode::Enter => Some(ipc::protocol::KeyAction::Enter), KeyCode::Esc => Some(ipc::protocol::KeyAction::Escape), KeyCode::Backspace => Some(ipc::protocol::KeyAction::Backspace), KeyCode::Delete => Some(ipc::protocol::KeyAction::Delete), KeyCode::Tab => Some(ipc::protocol::KeyAction::Tab), KeyCode::Up => Some(ipc::protocol::KeyAction::Up), KeyCode::Down => Some(ipc::protocol::KeyAction::Down), KeyCode::Left => Some(ipc::protocol::KeyAction::Left), KeyCode::Right => Some(ipc::protocol::KeyAction::Right), KeyCode::Home => Some(ipc::protocol::KeyAction::Home), KeyCode::End => Some(ipc::protocol::KeyAction::End), KeyCode::PageUp => Some(ipc::protocol::KeyAction::PageUp), KeyCode::PageDown => Some(ipc::protocol::KeyAction::PageDown), KeyCode::F(n) => Some(ipc::protocol::KeyAction::Function(n)), _ => None, } } fn key_action_to_code(action: &ipc::protocol::KeyAction) -> crossterm::event::KeyCode { use crossterm::event::KeyCode; match action { ipc::protocol::KeyAction::Char(c) => KeyCode::Char(*c), ipc::protocol::KeyAction::Enter => KeyCode::Enter, ipc::protocol::KeyAction::Escape => KeyCode::Esc, ipc::protocol::KeyAction::Backspace => KeyCode::Backspace, ipc::protocol::KeyAction::Delete => KeyCode::Delete, ipc::protocol::KeyAction::Tab => KeyCode::Tab, ipc::protocol::KeyAction::Up => KeyCode::Up, ipc::protocol::KeyAction::Down => KeyCode::Down, ipc::protocol::KeyAction::Left => KeyCode::Left, ipc::protocol::KeyAction::Right => KeyCode::Right, ipc::protocol::KeyAction::Home => KeyCode::Home, ipc::protocol::KeyAction::End => KeyCode::End, ipc::protocol::KeyAction::PageUp => KeyCode::PageUp, ipc::protocol::KeyAction::PageDown => KeyCode::PageDown, ipc::protocol::KeyAction::Function(n) => KeyCode::F(*n), } } fn send_daemon_update(conn: &mut ipc::conn::Connection, state: &app::state::rest::AppStateRest) -> Result<()> { use ipc::protocol::{DaemonFrame, MessageEntry, ToastEntry, StatePayload}; let messages: Vec = state.transcript_cache.messages.iter().map(|m| { MessageEntry { role: format!("{:?}", m.role), content: m.content.clone(), timestamp: m.timestamp, } }).collect(); let toasts: Vec = state.misc.toasts.iter().map(|t| { ToastEntry { kind: format!("{:?}", t.kind), message: t.message.clone(), created_at: t.created_at, lifetime_ms: t.lifetime_ms, } }).collect(); let overlay = if state.misc.overlay.is_active() { Some(format!("{:?}", state.misc.overlay)) } else { None }; let frame = DaemonFrame::StateUpdate(Box::new(StatePayload { mode: state.mode.name().to_string(), session_id: state.session_id.clone(), messages, edit_count: state.edit_log.len() as u32, message_count: state.transcript_cache.messages.len(), overlay, toasts, dirty: state.dirty, input_buffer: state.input.buffer.clone(), input_cursor: state.input.cursor, })); conn.send(&frame) } fn apply_client_update( state: &mut app::state::rest::AppStateRest, payload: ipc::protocol::StatePayload, ) { use app::state::types::{AgentMode, Overlay, Toast, ToastKind}; state.mode = match payload.mode.as_str() { "Auto" => AgentMode::Auto, "Normal" => AgentMode::Normal, "Plan" => AgentMode::Plan, "Yolo" => AgentMode::Yolo, _ => state.mode, }; state.session_id = payload.session_id; state.dirty = payload.dirty; state.transcript_cache.messages = payload.messages.into_iter().map(|m| { app::state::rest::ChatMessageDisplay { role: match m.role.as_str() { "User" => crate::dto::chat::message::Role::User, "Assistant" => crate::dto::chat::message::Role::Assistant, "System" => crate::dto::chat::message::Role::System, "Tool" => crate::dto::chat::message::Role::Tool, _ => crate::dto::chat::message::Role::User, }, content: m.content, timestamp: m.timestamp, } }).collect(); state.transcript_cache.dirty = true; state.misc.overlay = match payload.overlay.as_deref() { Some("Help") => Overlay::Help, Some("Settings") => Overlay::Settings, Some("Agents") => Overlay::Agents, Some("Bash") => Overlay::Bash, Some("QuitConfirm") => Overlay::QuitConfirm, Some("Workflow") => Overlay::Workflow, Some("Onboard") => Overlay::Onboard, Some("OnboardProvider") => Overlay::OnboardProvider, Some("KeyInput") => Overlay::KeyInput, Some("Editor") => Overlay::Editor, Some("Effort") => Overlay::Effort, Some("Mcp") => Overlay::Mcp, Some("Security") => Overlay::Security, Some("Todo") => Overlay::Todo, Some("Rewind") => Overlay::Rewind, Some("Learning") => Overlay::Learning, Some("Usage") => Overlay::Usage, Some("Loading") => Overlay::Loading, _ => Overlay::None, }; state.misc.toasts = payload.toasts.into_iter().map(|t| { Toast { kind: match t.kind.as_str() { "Info" => ToastKind::Info, "Success" => ToastKind::Success, "Warning" => ToastKind::Warning, "Error" => ToastKind::Error, "Lesson" => ToastKind::Lesson, _ => ToastKind::Info, }, message: t.message, created_at: t.created_at, lifetime_ms: t.lifetime_ms, } }).collect(); state.input.buffer = payload.input_buffer; state.input.cursor = payload.input_cursor; } fn run_daemon() -> Result<()> { use app::runtime::actions::{Action, apply_action}; use ipc::protocol::ClientRequest; let store = model::store::Store::new(); store.ensure_dirs()?; let session_id = uuid::Uuid::new_v4().to_string(); let session_dir = store.base_dir.join("sessions").join(&session_id); std::fs::create_dir_all(&session_dir)?; let workspace_roots = vec![std::env::current_dir()?]; let mut state = app::state::rest::AppStateRest::new( workspace_roots.clone(), session_dir, store.memory_dir, ); state.sessions = model::session::Session::list(&store.base_dir); let _rt = tokio::runtime::Runtime::new()?; let run_dir = store.base_dir.join("run"); std::fs::create_dir_all(&run_dir)?; let socket_path = run_dir.join(format!("{}.sock", session_id)); let addr = socket_path.to_string_lossy().to_string(); let server = ipc::server::IpcServer::bind_unix(&addr)?; eprintln!("daemon: listening on {}", addr); let mut conn = match server.accept() { Ok(c) => c, Err(e) => { eprintln!("daemon: accept error: {}", e); let _ = std::fs::remove_file(&socket_path); return Err(e); } }; eprintln!("daemon: client connected"); let mut running = true; while running { match conn.receive::()? { Some(req) => { match req { ClientRequest::Tick => { apply_action(&mut state, Action::Tick); } ClientRequest::KeyPress { key, ctrl, alt, shift } => { let mut modifiers = crossterm::event::KeyModifiers::NONE; if ctrl { modifiers |= crossterm::event::KeyModifiers::CONTROL; } if alt { modifiers |= crossterm::event::KeyModifiers::ALT; } if shift { modifiers |= crossterm::event::KeyModifiers::SHIFT; } let key_event = crossterm::event::KeyEvent::new( key_action_to_code(&key), modifiers, ); let actions = controller::input::handle_key(key_event, &mut state); for action in actions { apply_action(&mut state, action); } apply_action(&mut state, Action::Tick); } ClientRequest::Submit(text) => { state.input.buffer = text; let enter_event = crossterm::event::KeyEvent::new( crossterm::event::KeyCode::Enter, crossterm::event::KeyModifiers::NONE, ); let actions = controller::input::handle_key(enter_event, &mut state); for action in actions { apply_action(&mut state, action); } apply_action(&mut state, Action::Tick); } ClientRequest::Resize(w, h) => { apply_action(&mut state, Action::Resize(w, h)); apply_action(&mut state, Action::Tick); } ClientRequest::Close => { running = false; } } send_daemon_update(&mut conn, &state)?; } None => { running = false; } } } let _ = std::fs::remove_file(&socket_path); let _ = state.settings.save(); Ok(()) } fn run_attach(session_id: &str) -> Result<()> { use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers}; use ipc::protocol::ClientRequest; let store = model::store::Store::new(); let socket_path = store.base_dir.join("run").join(format!("{}.sock", session_id)); let addr = socket_path.to_string_lossy().to_string(); let mut client = ipc::client::IpcClient::connect_unix(&addr)?; enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; terminal.clear()?; let workspace_roots = vec![std::env::current_dir()?]; let session_dir = store.base_dir.join("sessions").join(session_id); std::fs::create_dir_all(&session_dir)?; let mut client_state = app::state::rest::AppStateRest::new( workspace_roots, session_dir, store.memory_dir, ); client_state.session_id = session_id.to_string(); let _rt = tokio::runtime::Runtime::new()?; loop { if client_state.quit { let _ = client.send(&ClientRequest::Close); break; } let now_ms = chrono::Utc::now().timestamp_millis(); client_state.misc.drain_expired_toasts(now_ms); if crossterm::event::poll(std::time::Duration::from_millis(50))? { match crossterm::event::read()? { Event::Key(key) => { if key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); let alt = key.modifiers.contains(KeyModifiers::ALT); let shift = key.modifiers.contains(KeyModifiers::SHIFT); if key.code == KeyCode::Char('c') && ctrl { client_state.quit = true; continue; } if let Some(key_action) = key_code_to_action(key.code) { client.send(&ClientRequest::KeyPress { key: key_action, ctrl, alt, shift, })?; } } } Event::Resize(w, h) => { client.send(&ClientRequest::Resize(w, h))?; } _ => {} } } else { client.send(&ClientRequest::Tick)?; } match client.receive::()? { Some(ipc::protocol::DaemonFrame::StateUpdate(payload)) => { apply_client_update(&mut client_state, *payload); } Some(ipc::protocol::DaemonFrame::StreamToken(_token)) => {} Some(ipc::protocol::DaemonFrame::SystemNote { kind: _, message }) => { client_state.push_toast( app::state::types::Toast::new( app::state::types::ToastKind::Info, message, ), ); } Some(ipc::protocol::DaemonFrame::Closed) => { client_state.quit = true; } None => { client_state.quit = true; } } terminal.draw(|f| { view::draw(f, &client_state); })?; } let _ = execute!(io::stdout(), LeaveAlternateScreen); let _ = disable_raw_mode(); let _ = client_state.settings.save(); core::mem::drop(_rt); Ok(()) } fn run_loop( state: &mut app::state::rest::AppStateRest, terminal: &mut Terminal>, ) -> Result<()> { let result = run_loop_inner(state, terminal); if let Err(ref _e) = result { let _ = terminal.clear(); let _ = disable_raw_mode(); let _ = execute!(io::stdout(), LeaveAlternateScreen); } result } fn run_loop_inner( state: &mut app::state::rest::AppStateRest, terminal: &mut Terminal>, ) -> Result<()> { use std::time::Duration; use crossterm::event::{Event, KeyEventKind}; use controller::input::handle_key; use app::runtime::actions::{Action, apply_action}; loop { if state.quit { break; } let now_ms = chrono::Utc::now().timestamp_millis(); state.misc.drain_expired_toasts(now_ms); terminal.draw(|f| { view::draw(f, state); state.dirty = false; })?; if crossterm::event::poll(Duration::from_millis(50))? { match crossterm::event::read()? { Event::Key(key) => { if key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat { let actions = handle_key(key, state); for action in actions { apply_action(state, action); } } } Event::Resize(w, h) => { apply_action(state, Action::Resize(w, h)); } _ => {} } } apply_action(state, Action::Tick); } terminal.clear()?; Ok(()) }