1pub mod async_ops;
2pub mod help;
3pub mod metadata;
4pub mod panels;
5pub mod pickers;
6pub mod profiles;
7pub mod state;
8pub mod sync_ops;
9pub mod types;
10
11pub use types::*;
13
14use crate::config::Config;
15use crate::config::LogEntry;
16use crate::config::physical_cores;
17use std::collections::VecDeque;
18
19pub use types::App;
21
22impl App {
23 pub fn new(config: Config) -> Self {
24 let active_panel = config.active_panel;
25 let left_pct = config.left_pct;
26 let mut log = VecDeque::new();
27 log.push_back(LogEntry::new(
28 "Starting llm-manager...",
29 crate::config::LogLevel::Info,
30 ));
31 let settings: crate::models::ModelSettings =
32 crate::models::ModelSettings::from_config(&config);
33 let settings_clone = settings.clone();
34 let server_mode = config.default.server_mode;
35 let router_max_models = config.default.router_max_models;
36 Self {
37 running: true,
38 config,
39 models: Vec::new(),
40 selected_model_idx: None,
41 models_mode: types::ModelsMode::List,
42 settings: settings_clone,
43 model_settings_cache: settings.clone(),
44 model_states: Default::default(),
45 metrics: Default::default(),
46 max_threads: physical_cores(),
47 cancelled: None,
48 server_mode,
49 router_max_models,
50 ws_server_handle: None,
51 background_tasks: Default::default(),
52
53 settings_state: SettingsState {
54 settings_selected_idx: 0,
55 server_settings_selected_idx: 0,
56 server_settings_scroll_offset: 0,
57 settings_edit_buffer: String::new(),
58 settings_scroll_offset: 0,
59 settings_render_cache: None,
60 expert_mode: false,
61 },
62 picker: PickerState {
63 host_picker_entries: Vec::new(),
64 host_picker_selected: 0,
65 backend_picker_entries: Vec::new(),
66 backend_picker_selected: 0,
67 prompt_picker_entries: Vec::new(),
68 prompt_picker_selected: 0,
69 profile_picker_entries: Vec::new(),
70 profile_picker_selected: 0,
71 profiles_scroll_offset: 0,
72 system_prompt_presets_scroll_offset: 0,
73 rpc_workers_selected_idx: 0,
74 editing_rpc_worker: None,
75 rpc_workers_scroll_offset: 0,
76 readme_scroll_offset: 0,
77 },
78 download: DownloadState {
79 download_progress: Vec::new(),
80 download_tx: None,
81 download_rx: None,
82 download_scroll_state: Default::default(),
83 downloading: false,
84 },
85 server: ServerState {
86 server_handle: None,
87 metrics_task_handle: None,
88 sync_task_handle: None,
89 spawn_task_handle: None,
90 bench_tune_task_handle: None,
91 server_log_rx: None,
92 metrics_rx: None,
93 sync_rx: None,
94 spawn_log_tx: None,
95 metrics_model_name: std::sync::Arc::new(std::sync::Mutex::new(None)),
96 loaded_model_names: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
97 api_proxy_handle: None,
98 metrics_tx: None,
99 running_ws_port: None,
100 running_ws_auth: None,
101 running_ws_tls: None,
102 running_api_port: None,
103 running_api_server_port: None,
104 running_api_model: None,
105 running_ws_tls_cfg: None,
106 running_ws_tls_cert_path: None,
107 running_ws_tls_key_path: None,
108 cmd_display: None,
109 spawned_settings: None,
110 spawned_model_name: None,
111 spawned_model_state: None,
112 spawned_context_length: 0,
113 server_exit_rx: None,
114 server_exit_tx: None,
115 api_shutdown_tx: None,
116 },
117 bench_tune: BenchTuneState {
118 bench_tune_progress: None,
119 bench_tune_results: Vec::new(),
120 bench_tune_running: false,
121 bench_tune_config: None,
122 bench_tune_rx: None,
123 bench_tune_tx: None,
124 bench_tune_output_view: None,
125 bench_tune_cancel_tx: None,
126 bench_tune_output_scroll: 0,
127 bench_tune_output_h_scroll: 0,
128 bench_tune_result_row: 0,
129 bench_tune_table_state: Default::default(),
130 bench_tune_output_index: 0,
131 },
132 log: LogState {
133 log_entries: log,
134 log_expanded: false,
135 log_scroll_offset: 0,
136 log_follow: true,
137 log_total_lines: 0,
138 },
139 loading: LoadingState {
140 loading_phases: Default::default(),
141 last_active_phase: None,
142 loading_progress: 0.0,
143 progress_target: 0.0,
144 load_progress: Default::default(),
145 last_spinner_time: None,
146 loading_spinner: 0,
147 model_total_layers: 0,
148 model_hidden_size: 0,
149 model_n_ctx_train: 0,
150 model_n_head: 0,
151 model_n_kv_head: 0,
152 vram_estimate: 0,
153 last_metadata_parse: (std::path::PathBuf::new(), std::time::SystemTime::now()),
154 health_poll_handle: None,
155 loading_completion_rx: None,
156 },
157 pending: PendingOperations {
158 pending_download: None,
159 pending_deletion: None,
160 pending_backend_deletion: None,
161 pending_spawn: None,
162 pending_api_load: None,
163 pending_api_unload: None,
164 pending_kill: None,
165 backend_resolving: false,
166 backend_resolve_handle: None,
167 },
168 search: SearchState {
169 local_filter: String::new(),
170 filtering_local: false,
171 search_results_idx: None,
172 search_table_state: Default::default(),
173 files_table_state: Default::default(),
174 readme_cache: None,
175 gguf_metadata_cache: Default::default(),
176 pending_search_load: None,
177 search_loading: false,
178 search_input: None,
179 },
180 ui: UIState {
181 active_panel,
182 global_mode: types::GlobalMode::Normal,
183 panel_visibility: 0b111111,
184 panel_help: false,
185 panel_help_offset: 0,
186 last_error_message: None,
187 list_state: Default::default(),
188 resize_state: None,
189 left_pct,
190 needs_full_redraw: false,
191 needs_redraw: true,
192 text_scrolls: Default::default(),
193 },
194 edit: EditState {
195 edit_cursor_pos: 0,
196 editing_n_predict: false,
197 n_predict_edit_buffer: String::new(),
198 editing_iters: false,
199 iters_edit_buffer: String::new(),
200 tags_editing: false,
201 tags_edit_buffer: String::new(),
202 tags_selected_idx: None,
203 tags_insert_mode: false,
204 editing_preset: None,
205 },
206 }
207 }
208}
209
210impl App {
211 const SCROLL_TICK_MS: u64 = 870;
212 const SCROLL_HOLD_FRAMES: u8 = 5;
213
214 pub fn tick_text_scrolls(&mut self) {
215 let now = std::time::Instant::now();
216 let mut changed = false;
217
218 for (_, state) in self.ui.text_scrolls.iter_mut() {
219 if !state.visible {
221 continue;
222 }
223
224 if state.max_offset == 0 {
225 if state.offset != 0 {
226 state.offset = 0;
227 changed = true;
228 }
229 continue;
230 }
231
232 if now.duration_since(state.last_tick) >= std::time::Duration::from_millis(Self::SCROLL_TICK_MS) {
233 if state.offset > state.max_offset {
235 state.offset = state.max_offset;
236 state.direction = -1;
237 state.hold_count = Self::SCROLL_HOLD_FRAMES;
238 }
239
240 let prev_offset = state.offset;
241
242 if state.offset == 0 && state.direction == -1 {
243 state.direction = 1;
244 state.hold_count = Self::SCROLL_HOLD_FRAMES;
245 } else if state.offset == state.max_offset && state.direction == 1 {
246 state.direction = -1;
247 state.hold_count = Self::SCROLL_HOLD_FRAMES;
248 }
249
250 if state.hold_count > 0 {
251 state.hold_count -= 1;
252 } else {
253 state.offset = if state.direction > 0 {
254 state.offset.saturating_add(1)
255 } else {
256 state.offset.saturating_sub(1)
257 };
258 }
259
260 state.last_tick = now;
261 if state.offset != prev_offset {
262 changed = true;
263 }
264 }
265 }
266
267 if changed {
268 self.ui.needs_redraw = true;
269 }
270 }
271
272 pub fn init_scrolls_for_models(&mut self) {
273 use std::time::Instant;
274 for model in &self.models {
275 let key = model.display_name.clone();
276 let max_offset = model.display_name.chars().count().saturating_sub(20);
277 self.ui.text_scrolls.insert(key, TextScrollState {
278 offset: 0,
279 last_tick: Instant::now(),
280 direction: 1,
281 hold_count: 0,
282 max_offset,
283 visible: false,
284 });
285 }
286 }
287
288 #[allow(dead_code)]
289 pub fn get_scroll_state(&self, key: &str) -> Option<&TextScrollState> {
290 self.ui.text_scrolls.get(key)
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use crate::models::LoadProgress;
298
299 fn make_app() -> App {
300 let config = crate::config::Config {
301 models_dirs: vec![],
302 llama_server: std::path::PathBuf::new(),
303 default: crate::config::DefaultParams::default(),
304 model_overrides: crate::config::ModelConfigStore::new(),
305 profiles: crate::config::ProfileStore::new(),
306 system_prompt_presets: crate::config::PresetStore::new(),
307 rpc_workers: Vec::new(),
308 search_limit: 50,
309 active_panel: types::ActivePanel::Models,
310 left_pct: 55,
311 };
312 let mut app = App::new(config);
313 app.loading.loading_phases.clear();
314 app.loading.last_active_phase = None;
315 app.loading.loading_progress = 0.0;
316 app.loading.progress_target = 0.0;
317 app.loading.load_progress = LoadProgress {
318 layers_total: None,
319 layers_loaded: None,
320 tensors_total: None,
321 tensors_loaded: 0,
322 buffers: vec![],
323 };
324 app.loading.last_spinner_time = None;
325 app
326 }
327
328 #[test]
329 fn test_progress_server_starting() {
330 let mut app = make_app();
331 app.loading
332 .loading_phases
333 .insert(LoadingPhase::ServerStarting);
334 app.loading.last_active_phase = Some(LoadingPhase::ServerStarting);
335 app.compute_progress();
336 assert!((app.loading.loading_progress - 0.08).abs() < 0.001);
337 }
338
339 #[test]
340 fn test_progress_with_layers() {
341 let mut app = make_app();
342 app.loading
343 .loading_phases
344 .insert(LoadingPhase::ServerStarting);
345 app.loading
346 .loading_phases
347 .insert(LoadingPhase::LoadingModel);
348 app.loading.loading_phases.insert(LoadingPhase::LoadingMeta);
349 app.loading
350 .loading_phases
351 .insert(LoadingPhase::LoadingTensors);
352 app.loading.last_active_phase = Some(LoadingPhase::LoadingTensors);
353 app.loading.load_progress.layers_loaded = Some(16);
354 app.loading.load_progress.layers_total = Some(32);
355 app.compute_progress();
356 assert!((app.loading.loading_progress - 0.57).abs() < 0.01);
357 }
358
359 #[test]
360 fn test_progress_complete() {
361 let mut app = make_app();
362 app.loading.loading_phases.insert(LoadingPhase::Complete);
363 app.loading.last_active_phase = Some(LoadingPhase::Complete);
364 app.compute_progress();
365 assert!((app.loading.loading_progress - 1.0).abs() < 0.001);
366 }
367
368 #[test]
369 fn test_progress_all_phases() {
370 let mut app = make_app();
371 app.loading
372 .loading_phases
373 .insert(LoadingPhase::ServerStarting);
374 app.loading
375 .loading_phases
376 .insert(LoadingPhase::LoadingModel);
377 app.loading.loading_phases.insert(LoadingPhase::LoadingMeta);
378 app.loading
379 .loading_phases
380 .insert(LoadingPhase::LoadingTensors);
381 app.loading
382 .loading_phases
383 .insert(LoadingPhase::ServerListening);
384 app.loading.last_active_phase = Some(LoadingPhase::ServerListening);
385 app.compute_progress();
386 assert!((app.loading.loading_progress - 0.98).abs() < 0.01);
387 }
388}