- 1
//! vak-terminal: Rich modern terminal surface for vak (`vak term`). - 2
//! Implements docs/design/55-rich-terminal-surface.md. - 3
//! - 4
//! Unlike the previous presentation prototype, this surface connects to a - 5
//! real vak server over HTTP/SSE. Every rendered value — health, sessions, - 6
//! model routes, MCP inventory, approval state, telemetry — is fetched - 7
//! live from the server and kept fresh by background SSE watchers. - 8
- 9
pub mod api; - 10
pub mod app; - 11
pub mod graphics; - 12
pub mod hil; - 13
pub mod repl; - 14
pub mod telemetry; - 15
pub mod theme; - 16
pub mod ui; - 17
- 18
use std::io::stdout; - 19
use std::sync::Arc; - 20
use std::time::{Duration, Instant}; - 21
- 22
use crossterm::event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream}; - 23
use crossterm::execute; - 24
use crossterm::terminal::{ - 25
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, - 26
}; - 27
use futures::StreamExt; - 28
use ratatui::Terminal; - 29
use ratatui::backend::CrosstermBackend; - 30
use tokio::sync::mpsc; - 31
- 32
use crate::api::{ApiClient, TerminalEvent}; - 33
use crate::app::TerminalApp; - 34
- 35
/// Error returned when the terminal cannot establish a connection to the - 36
/// server at startup. - 37
#[derive(Debug)] - 38
pub struct ConnectionError { - 39
pub message: String, - 40
pub server_url: String, - 41
} - 42
- 43
/// RAII Terminal Mode guard to ensure raw mode and alternate screen - 44
/// are ALWAYS restored even on panic or error (Invariant 6 & Rule 15). - 45
pub struct TerminalGuard; - 46
- 47
impl TerminalGuard { - 48
pub fn enter() -> std::io::Result<Self> { - 49
enable_raw_mode()?; - 50
execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?; - 51
Ok(Self) - 52
} - 53
} - 54
- 55
impl Drop for TerminalGuard { - 56
fn drop(&mut self) { - 57
let _ = execute!(stdout(), DisableMouseCapture, LeaveAlternateScreen); - 58
let _ = disable_raw_mode(); - 59
} - 60
} - 61
- 62
#[derive(Debug, Clone)] - 63
pub struct TerminalOptions { - 64
pub session_id: Option<String>, - 65
pub server_url: Option<String>, - 66
pub token: Option<String>, - 67
pub workspace_cwd: Option<std::path::PathBuf>, - 68
} - 69
- 70
/// Run the rich terminal surface, connected to a live vak server. - 71
/// - 72
/// The `--server`, `--token`, and `--session` flags are all required for a - 73
/// real connection. If no server URL is supplied, the terminal falls back - 74
/// to the local loopback default (`http://127.0.0.1:8901`) so that a - 75
/// locally-started `vak serve` works out of the box. - 76
pub async fn run_terminal(opts: TerminalOptions) -> std::io::Result<i32> { - 77
let _guard = TerminalGuard::enter()?; - 78
let backend = CrosstermBackend::new(stdout()); - 79
let mut terminal = Terminal::new(backend)?; - 80
terminal.clear()?; - 81
- 82
// --- Resolve server connection parameters --- - 83
let server_url = opts - 84
.server_url - 85
.unwrap_or_else(|| "http://127.0.0.1:8901".to_string()); - 86
let token = opts.token.unwrap_or_default(); - 87
- 88
// --- Create the real API client --- - 89
let api = Arc::new(ApiClient::new(server_url, &token)); - 90
- 91
// --- Fetch initial health — required to proceed --- - 92
let health = match api.health().await { - 93
Ok(h) => h, - 94
Err(e) => { - 95
eprintln!( - 96
"vak term: cannot connect to server at {}: {}", - 97
api.base_url(), - 98
e - 99
); - 100
eprintln!(" Is the vak server running? Try `vak serve` first."); - 101
return Err(std::io::Error::other(format!("connection error: {e}"))); - 102
} - 103
}; - 104
- 105
// --- Resolve session (attach, create, or pick from list) --- - 106
let session_id = if let Some(ref sid) = opts.session_id { - 107
sid.clone() - 108
} else { - 109
// Try to find a running session, or create one. - 110
match api.list_sessions().await { - 111
Ok(sessions) => { - 112
if let Some(s) = sessions - 113
.iter() - 114
.find(|s| s.running.unwrap_or(false) && !s.archived.unwrap_or(false)) - 115
{ - 116
s.session_id.clone() - 117
} else { - 118
match api.create_session().await { - 119
Ok(id) => id, - 120
Err(e) => { - 121
eprintln!("vak term: failed to create session: {e}"); - 122
return Err(std::io::Error::other(e.to_string())); - 123
} - 124
} - 125
} - 126
} - 127
Err(_) => match api.create_session().await { - 128
Ok(id) => id, - 129
Err(e) => { - 130
eprintln!("vak term: failed to create session: {e}"); - 131
return Err(std::io::Error::other(e.to_string())); - 132
} - 133
}, - 134
} - 135
}; - 136
- 137
// Attach to the session (marks it as the active terminal consumer). - 138
if let Err(e) = api.attach_session(&session_id).await { - 139
eprintln!("vak term: warning: could not attach to session {session_id}: {e}"); - 140
} - 141
- 142
// --- Fetch initial sessions list --- - 143
let sessions = api.list_sessions().await.unwrap_or_default(); - 144
- 145
// --- Fetch initial providers/model list --- - 146
let providers = api.list_providers().await.unwrap_or_default(); - 147
let models = if !providers.providers.is_empty() { - 148
let first = &providers.providers[0]; - 149
api.discover_models(&first.name).await.unwrap_or_default() - 150
} else { - 151
Vec::new() - 152
}; - 153
- 154
// --- Fetch initial MCP inventory --- - 155
let mcp_servers = api.get_mcp_servers().await.unwrap_or_default(); - 156
- 157
// --- Fetch initial gateway/approval policy --- - 158
let gateway = api.get_gateway_approvals().await.unwrap_or_default(); - 159
- 160
// --- Fetch initial config snapshot --- - 161
let config = api.get_config().await.unwrap_or_default(); - 162
- 163
// --- Fetch initial control state --- - 164
let control = api.get_control_state(&session_id).await.unwrap_or_default(); - 165
- 166
// --- Fetch initial launch servers (for the preview URL) --- - 167
let launch_servers = api - 168
.get_launch_servers(&session_id) - 169
.await - 170
.unwrap_or_default(); - 171
- 172
// --- Set up SSE background watchers --- - 173
let (tx, mut rx) = mpsc::unbounded_channel::<TerminalEvent>(); - 174
- 175
let agent_handle = api.spawn_agent_event_watcher(&session_id, tx.clone()); - 176
let _present_handle = api.spawn_presentation_watcher(&session_id, tx.clone()); - 177
let health_handle = api.clone().spawn_health_watcher(tx.clone()); - 178
- 179
// --- Build the app with real data --- - 180
let mut app = TerminalApp::new_with_api( - 181
session_id, - 182
health.model.clone(), - 183
health.clone(), - 184
api.clone(), - 185
) - 186
.with_workspace( - 187
opts.workspace_cwd - 188
.and_then(|p| p.file_name().and_then(|n| n.to_str()).map(String::from)) - 189
.unwrap_or_else(|| "default".into()), - 190
) - 191
.with_providers(providers) - 192
.with_models(models) - 193
.with_mcp_servers(mcp_servers) - 194
.with_gateway(gateway) - 195
.with_config(config) - 196
.with_control_state(control) - 197
.with_launch_servers(launch_servers) - 198
.with_sessions(sessions); - 199
- 200
let tick_rate = Duration::from_millis(50); - 201
let mut last_tick = Instant::now(); - 202
- 203
let mut event_stream = EventStream::new(); - 204
- 205
// --- Main event loop: crossterm input + SSE-driven updates --- - 206
loop { - 207
terminal.draw(|f| { - 208
f.render_widget(&app, f.area()); - 209
})?; - 210
- 211
let timeout = tick_rate.saturating_sub(last_tick.elapsed()); - 212
- 213
tokio::select! { - 214
// 1. Incoming SSE / health events from background tasks. - 215
maybe_terminal_ev = rx.recv() => { - 216
if let Some(ev) = maybe_terminal_ev { - 217
app.handle_terminal_event(ev); - 218
} - 219
} - 220
- 221
// 2. crossterm keyboard / mouse input. - 222
maybe_event = event_stream.next() => { - 223
if let Some(Ok(event)) = maybe_event { - 224
match event { - 225
Event::Key(key) => { - 226
app.handle_key(key); - 227
if app.should_quit || app.should_detach { - 228
break; - 229
} - 230
} - 231
Event::Mouse(mouse) => { - 232
app.handle_mouse(mouse); - 233
} - 234
Event::Resize(_, _) => { - 235
// Terminal resize automatically handles buffer redraw. - 236
} - 237
_ => {} - 238
} - 239
} - 240
} - 241
- 242
// 3. Periodic tick for animations. - 243
_ = tokio::time::sleep(timeout) => { - 244
app.on_tick(); - 245
last_tick = Instant::now(); - 246
} - 247
} - 248
} - 249
- 250
// Clean up background tasks. - 251
agent_handle.abort(); - 252
health_handle.abort(); - 253
- 254
Ok(0) - 255
} - 256
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.