Files
zesdex/src/main.rs
T

505 lines
17 KiB
Rust
Raw Normal View History

#![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<String> = 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<ipc::protocol::KeyAction> {
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<MessageEntry> = state.transcript_cache.messages.iter().map(|m| {
MessageEntry {
role: format!("{:?}", m.role),
content: m.content.clone(),
timestamp: m.timestamp,
}
}).collect();
let toasts: Vec<ToastEntry> = 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::<ClientRequest>()? {
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::<ipc::protocol::DaemonFrame>()? {
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<CrosstermBackend<io::Stdout>>,
) -> 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<CrosstermBackend<io::Stdout>>,
) -> 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(())
}