Skip to main content

viridian_mini_tui/
lib.rs

1use anyhow::{Context, Result};
2use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
3use crossterm::{cursor, execute, queue, terminal};
4use mini_agent_core::{
5    Agent, AgentEvent, BUILT_IN_PROVIDER_NAMES, Config, DEFAULT_CONTEXT_WINDOW_TOKENS,
6    DEFAULT_SYSTEM_PROMPT, ModelMessage, ModelRole, Plugin, PluginKind, ProviderConfig,
7    compose_prompt, estimate_messages_tokens, list_models, load_plugin,
8};
9use serde::{Deserialize, Serialize};
10use std::io::{self, Stdout, Write};
11use std::path::PathBuf;
12use std::sync::Arc;
13use std::sync::OnceLock;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::mpsc::{self, Receiver};
16use std::thread;
17use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
18use syntect::easy::HighlightLines;
19use syntect::highlighting::{Theme, ThemeSet};
20use syntect::parsing::SyntaxSet;
21use syntect::util::as_24_bit_terminal_escaped;
22use unicode_width::UnicodeWidthChar;
23
24// waverows from @agilek/cli-loaders / unicode-animations.
25const SPINNER: [&str; 16] = [
26    "⠖⠉⠉⠑",
27    "⡠⠖⠉⠉",
28    "⣠⡠⠖⠉",
29    "⣄⣠⡠⠖",
30    "⠢⣄⣠⡠",
31    "⠙⠢⣄⣠",
32    "⠉⠙⠢⣄",
33    "⠊⠉⠙⠢",
34    "⠜⠊⠉⠙",
35    "⡤⠜⠊⠉",
36    "⣀⡤⠜⠊",
37    "⢤⣀⡤⠜",
38    "⠣⢤⣀⡤",
39    "⠑⠣⢤⣀",
40    "⠉⠑⠣⢤",
41    "⠋⠉⠑⠣",
42];
43const BANNER: &str = r"         _      _ 
44  __ _  (_)__  (_)
45 /  ' \/ / _ \/ / 
46/_/_/_/_/_//_/_/  
47                  ";
48const RESET: &str = "\x1b[0m";
49const BOLD: &str = "\x1b[1m";
50const RED: &str = "\x1b[31m";
51const YELLOW: &str = "\x1b[33m";
52const CYAN: &str = "\x1b[36m";
53const BRIGHT_BLACK: &str = "\x1b[90m";
54const BG_USER: &str = "\x1b[40m";
55const INPUT_FRAME: &str = BRIGHT_BLACK;
56const BOLD_CYAN: &str = "\x1b[1;36m";
57const BOLD_WHITE: &str = "\x1b[1;97m";
58const MESSAGE_INDENT: usize = 3;
59const OUTPUT_HEAD_LINES: usize = 24;
60const OUTPUT_TAIL_LINES: usize = 8;
61const STREAM_UNSTABLE_ROWS: usize = 8;
62const SLASH_COMMANDS: [&str; 10] = [
63    "/help",
64    "/provider",
65    "/model",
66    "/model add",
67    "/mode",
68    "/effort",
69    "/session",
70    "/resume",
71    "/compact",
72    "/compact status",
73];
74
75enum AgentUpdate {
76    Event(AgentEvent),
77    Done(Box<Agent>, Result<()>),
78}
79
80struct RunningAgent {
81    receiver: Receiver<AgentUpdate>,
82    interrupted: Arc<AtomicBool>,
83}
84
85#[derive(Clone, Copy, PartialEq, Eq)]
86enum Role {
87    User,
88    Assistant,
89    Command,
90    Output,
91    Local,
92}
93
94struct Message {
95    role: Role,
96    text: String,
97    output: Option<String>,
98}
99
100#[derive(Deserialize, Serialize)]
101struct StoredSession {
102    id: String,
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    title: Option<String>,
105    mode: String,
106    system: String,
107    config: Config,
108    messages: Vec<ModelMessage>,
109}
110
111#[derive(Clone, Copy)]
112enum SelectionCommand {
113    CommandPalette,
114    Provider,
115    Model,
116    Mode,
117    Effort,
118    Resume,
119}
120
121struct SelectionItem {
122    label: String,
123    value: String,
124}
125
126struct Selection {
127    title: String,
128    command: SelectionCommand,
129    items: Vec<SelectionItem>,
130    selected: usize,
131}
132
133struct App {
134    messages: Vec<Message>,
135    history: Vec<String>,
136    history_index: Option<usize>,
137    provider: String,
138    model: String,
139    mode: String,
140    effort: Option<String>,
141    context_window_tokens: usize,
142    context_percent: Option<usize>,
143    input: Vec<char>,
144    cursor: usize,
145    spinner: usize,
146    printed_messages: usize,
147    streaming_text: String,
148    streaming_started: bool,
149    stream_message_cutoff: Option<usize>,
150    streaming_rows: Vec<String>,
151    streaming_committed_rows: usize,
152    stream_final_skip_rows: Option<usize>,
153    previous_bottom_rows: u16,
154    rendered_width: Option<u16>,
155    needs_full_redraw: bool,
156    running_since: Option<Instant>,
157    session_id: String,
158    session_title: Option<String>,
159    selection: Option<Selection>,
160    plugins: Vec<Plugin>,
161    cwd: PathBuf,
162    append_system_prompt: Option<String>,
163    ignore_plugin_errors: bool,
164    agent: Option<Agent>,
165    running: Option<RunningAgent>,
166}
167
168pub struct RunOptions {
169    pub system_prompt: String,
170    pub config: Config,
171    pub mode: String,
172    pub plugins: Vec<Plugin>,
173    pub cwd: PathBuf,
174    pub append_system_prompt: Option<String>,
175    pub ignore_plugin_errors: bool,
176    pub resume: Option<String>,
177    pub session_id: Option<String>,
178}
179
180include!("runtime.rs");
181include!("sessions.rs");
182include!("render.rs");
183include!("commands.rs");
184include!("text.rs");