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
24const 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");