holon 0.14.1

A headless, event-driven runtime for long-lived agents
Documentation
use std::{
    env,
    io::{self, Stdout},
    time::{Duration, Instant},
};

use crate::{
    client::LocalClient,
    config::{AltScreenMode, AppConfig},
    system::{workspace_access_mode_label, workspace_projection_label},
    tui_markdown::{render_markdown_text, render_markdown_text_spaced},
    types::{
        AgentListEntry, AgentSummary, MessageBody, OperatorMessageRecord, OperatorMessageStatus,
        ResolvedModelAvailability, TaskRecord, TrustLevel,
    },
};
use anyhow::{anyhow, Result};
use chrono::{DateTime, Local};
use crossterm::{
    event::{
        self, DisableBracketedPaste, EnableBracketedPaste, Event, KeyEvent, KeyEventKind,
        KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
    },
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
    backend::CrosstermBackend,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Modifier, Style},
    text::{Line, Span, Text},
    widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
    Frame, Terminal,
};
use serde_json::Value;

mod app;
mod chat;
mod composer;
mod input;
mod keymap;
pub(crate) mod logging;
mod model_picker;
mod overlay;
pub(crate) mod projection;
mod render;
mod runtime;
mod state;
mod view_model;

use app::TuiApp;
use chat::{chat_text_for_width, paragraph_max_scroll_unframed, ChatScrollState};
use composer::ComposerState;
use logging::TuiLogWriter;
use overlay::OverlayState;
use projection::{OperatorDisplayMode, TuiProjection};
use render::draw;
use runtime::{TuiConnectionState, TuiRuntimeMessage};

const INPUT_POLL_INTERVAL: Duration = Duration::from_millis(100);
pub async fn run_tui(
    config: AppConfig,
    no_alt_screen: bool,
    client: Option<LocalClient>,
) -> Result<()> {
    let client = match client {
        Some(client) => client,
        None => LocalClient::new(config.clone())?,
    };
    let log_writer = TuiLogWriter::new(config.log_root_dir())?;
    let mut app = TuiApp::new(client, log_writer);
    app.initialize().await;

    let mut terminal_guard = TerminalCleanupGuard::new();
    enable_raw_mode()?;
    terminal_guard.raw_mode_enabled = true;

    let alt_screen_enabled = determine_alt_screen_mode(no_alt_screen, config.tui_alternate_screen);
    let mut stdout = io::stdout();
    if alt_screen_enabled {
        execute!(stdout, EnterAlternateScreen)?;
        terminal_guard.alternate_screen_enabled = true;
    }
    if execute!(stdout, EnableBracketedPaste).is_ok() {
        terminal_guard.bracketed_paste_enabled = true;
    }
    if execute!(
        stdout,
        PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
    )
    .is_ok()
    {
        terminal_guard.keyboard_enhancement_enabled = true;
    }

    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;
    terminal.clear()?;

    let run_result = run_event_loop(&mut terminal, &mut app).await;
    terminal.show_cursor()?;
    run_result
}

fn determine_alt_screen_mode(no_alt_screen: bool, mode: AltScreenMode) -> bool {
    determine_alt_screen_mode_for_terminal(no_alt_screen, mode, env::var_os("ZELLIJ").is_some())
}

fn determine_alt_screen_mode_for_terminal(
    no_alt_screen: bool,
    mode: AltScreenMode,
    is_zellij: bool,
) -> bool {
    if no_alt_screen {
        return false;
    }

    match mode {
        AltScreenMode::Always => true,
        AltScreenMode::Never => false,
        AltScreenMode::Auto => !is_zellij,
    }
}

async fn run_event_loop(
    terminal: &mut Terminal<CrosstermBackend<Stdout>>,
    app: &mut TuiApp,
) -> Result<()> {
    loop {
        if let Err(err) = app.tick().await {
            app.status_line = format!("TUI runtime update failed: {err}");
        }
        terminal.draw(|frame| draw(frame, app))?;

        if app.should_quit {
            return Ok(());
        }

        if event::poll(INPUT_POLL_INTERVAL)? {
            match event::read()? {
                Event::Key(key)
                    if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
                {
                    if let Err(err) = app.handle_key(key).await {
                        app.status_line = format!("Action failed: {err}");
                    }
                }
                Event::Paste(text) => {
                    if let Err(err) = app.handle_paste(&text).await {
                        app.status_line = format!("Paste failed: {err}");
                    }
                }
                _ => {}
            }
        }
    }
}

struct TerminalCleanupGuard {
    raw_mode_enabled: bool,
    alternate_screen_enabled: bool,
    bracketed_paste_enabled: bool,
    keyboard_enhancement_enabled: bool,
}

impl TerminalCleanupGuard {
    fn new() -> Self {
        Self {
            raw_mode_enabled: false,
            alternate_screen_enabled: false,
            bracketed_paste_enabled: false,
            keyboard_enhancement_enabled: false,
        }
    }
}

impl Drop for TerminalCleanupGuard {
    fn drop(&mut self) {
        if self.keyboard_enhancement_enabled {
            let mut stdout = io::stdout();
            let _ = execute!(stdout, PopKeyboardEnhancementFlags);
        }
        if self.bracketed_paste_enabled {
            let mut stdout = io::stdout();
            let _ = execute!(stdout, DisableBracketedPaste);
        }
        if self.raw_mode_enabled {
            let _ = disable_raw_mode();
        }
        if self.alternate_screen_enabled {
            let mut stdout = io::stdout();
            let _ = execute!(stdout, LeaveAlternateScreen);
        }
    }
}

#[cfg(test)]
mod tests;