pub mod app;
pub mod backend;
pub mod bash;
pub mod commands;
pub mod completion;
pub mod cost;
pub mod events;
pub mod input_state;
pub mod keymap;
pub mod model;
pub mod runtime_builder;
pub mod skill_commands;
pub mod ui;
pub use cost::UsageStats;
pub use input_state::{InputMode, PromptInputState};
pub use model::{AppScreen, DiffHunk, DiffLine, DiffLineKind, TranscriptBlock};
use std::io::{self, Write as _};
use std::time::Duration;
use unicode_width::UnicodeWidthStr as _;
use crossterm::event::{self, Event, KeyEventKind};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use ratatui::prelude::*;
use ratatui::widgets::{Paragraph, Wrap};
use ratatui::{Terminal, TerminalOptions, Viewport};
use crate::tui::app::App;
use crate::tui::backend::Backend;
use crate::tui::events::UserAction;
const INLINE_HEIGHT_NORMAL: u16 = 10;
const INLINE_HEIGHT_EXPANDED: u16 = 40;
fn print_startup_banner(workspace: &std::path::Path) {
const CYAN: &str = "\x1b[36m";
const BOLD: &str = "\x1b[1m";
const DARK_GRAY: &str = "\x1b[90m";
const DIM: &str = "\x1b[2m";
const RESET: &str = "\x1b[0m";
println!("{CYAN}{BOLD}╦═╗╔═╗╔═╗╦ ╦╦═╗╔═╗╦╦ ╦╔═╗{RESET}");
println!("{CYAN}{BOLD}╠╦╝║╣ ║ ║ ║╠╦╝╚═╗║╚╗╔╝║╣ {RESET}");
println!("{CYAN}{BOLD}╩╚═╚═╝╚═╝╚═╝╩╚═╚═╝╩ ╚╝ ╚═╝{RESET}");
let model = crate::tui::cost::detect_model_name();
println!("{DIM} v{} · {model}{RESET}", env!("CARGO_PKG_VERSION"));
println!("{DARK_GRAY} ……………………………………………………………………………………………………{RESET}");
let mut shown = 0;
if let Ok(mut sessions) = crate::session::SessionReader::list_sessions(workspace) {
sessions.reverse();
for dir in &sessions {
if shown >= 3 {
break;
}
if let Ok(meta) = crate::session::SessionReader::load_meta(dir) {
if let Some(ref prompt) = meta.last_prompt {
let short: String = prompt.chars().take(60).collect();
let ellipsis = if prompt.chars().count() > 60 {
"…"
} else {
""
};
println!("{DARK_GRAY} › {short}{ellipsis}{RESET}");
shown += 1;
}
}
}
}
println!();
let _ = io::stdout().flush();
}
struct RawModeGuard;
impl Drop for RawModeGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
let _ = writeln!(io::stdout());
}
}
fn make_inline_terminal(
height: u16,
) -> io::Result<ratatui::Terminal<CrosstermBackend<io::Stdout>>> {
Terminal::with_options(
CrosstermBackend::new(io::stdout()),
TerminalOptions {
viewport: Viewport::Inline(height),
},
)
}
pub async fn run() -> io::Result<()> {
run_with_backend(Backend::spawn()).await
}
pub async fn run_with_backend(backend: Backend) -> io::Result<()> {
let _quiet_guard = crate::logging::suppress_tracing_for_tui();
let workspace = crate::config::Config::from_env()
.map(|c| c.workspace)
.unwrap_or_else(|_| std::path::PathBuf::from("."));
print_startup_banner(&workspace);
enable_raw_mode()?;
let _guard = RawModeGuard;
let mut terminal = make_inline_terminal(INLINE_HEIGHT_NORMAL)?;
let mut current_inline_height = INLINE_HEIGHT_NORMAL;
let mut backend = backend;
let mut app = App::new();
app.permission_hook_enabled = backend.permission_enabled.clone();
loop {
app.flush_ready_blocks();
let queued: Vec<Vec<Line<'static>>> = app.print_queue.drain(..).collect();
for lines in queued {
let h = (lines.len() as u16).max(1);
terminal.insert_before(h, |buf| {
let area = buf.area;
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.render(area, buf);
let mut i = 0;
while i < buf.content.len() {
let w = buf.content[i].symbol().width();
if w >= 2 {
for j in 1..w {
if i + j < buf.content.len() {
buf.content[i + j].set_symbol("");
}
}
}
i += 1;
}
})?;
}
let needs_expanded = !app.modals.is_empty() || app.pending_permission.is_some();
let desired_height = if needs_expanded {
INLINE_HEIGHT_EXPANDED
} else {
INLINE_HEIGHT_NORMAL
};
if desired_height != current_inline_height {
terminal = make_inline_terminal(desired_height)?;
current_inline_height = desired_height;
}
terminal.draw(|frame| ui::chat::render(frame, &app))?;
app.spinner_frame = app.spinner_frame.wrapping_add(1);
tokio::select! {
_ = tokio::time::sleep(Duration::from_millis(50)) => {
while event::poll(Duration::ZERO)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
if let Some(action) = keymap::dispatch(&mut app, key) {
let _ = backend.action_tx.send(action);
}
}
}
}
}
Some(ui_event) = backend.event_rx.recv() => {
app.handle_ui_event(ui_event);
}
Some(perm_req) = backend.perm_rx.recv() => {
app.set_pending_permission(perm_req);
}
}
if app.should_quit {
break;
}
}
let _ = backend.action_tx.send(UserAction::Shutdown);
Ok(())
}