- 1
//! Screen 1: Agent Studio view (Dual-Deck: Stream & Web Preview + Telemetry HUD). - 2
//! - 3
//! All content is derived from real API data: - 4
//! - Web preview URL comes from `launch_servers` (real `/sessions/{id}/launch`). - 5
//! - Tool execution cards come from `presentation_items` (real SSE). - 6
//! - Git diff inspector shows real diff data from presentation items. - 7
//! - Telemetry gauges show real `/health` values. - 8
//! - Sandbox name comes from real health report. - 9
- 10
use ratatui::buffer::Buffer; - 11
use ratatui::layout::{Constraint, Direction, Layout, Rect}; - 12
use ratatui::style::Modifier; - 13
use ratatui::text::{Line, Span}; - 14
use ratatui::widgets::{Block, Borders, Paragraph, Widget}; - 15
- 16
use crate::app::{DeckFocus, PresItem}; - 17
use crate::telemetry::TelemetryState; - 18
use crate::theme::{Symbols, Theme}; - 19
- 20
pub struct StudioView<'a> { - 21
pub telemetry: &'a TelemetryState, - 22
pub theme: &'a Theme, - 23
pub is_running: bool, - 24
pub deck_focus: DeckFocus, - 25
pub card_expanded: bool, - 26
pub presentation_items: &'a [PresItem], - 27
pub launch_url: Option<&'a str>, - 28
pub sandbox_name: &'a str, - 29
pub cwd: &'a str, - 30
} - 31
- 32
impl<'a> Widget for StudioView<'a> { - 33
fn render(self, area: Rect, buf: &mut Buffer) { - 34
let main_decks = Layout::default() - 35
.direction(Direction::Horizontal) - 36
.constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) - 37
.split(area); - 38
- 39
let left_border = if self.deck_focus == DeckFocus::Left { - 40
self.theme.style_border_focus() - 41
} else { - 42
self.theme.style_border() - 43
}; - 44
let right_border = if self.deck_focus == DeckFocus::Right { - 45
self.theme.style_border_focus() - 46
} else { - 47
self.theme.style_border() - 48
}; - 49
- 50
// ------------------------------------------------------------- - 51
// LEFT DECK: Stream, Previews & Tool Runs - 52
// ------------------------------------------------------------- - 53
let doc_height = if self.card_expanded { 11 } else { 7 }; - 54
let left_chunks = Layout::default() - 55
.direction(Direction::Vertical) - 56
.constraints([ - 57
Constraint::Length(9), - 58
Constraint::Length(doc_height), - 59
Constraint::Min(6), - 60
]) - 61
.split(main_decks[0]); - 62
- 63
// 1. Live Webpage Preview Card — real URL from launch_servers - 64
let preview_url = self.launch_url.unwrap_or("(no server running)"); - 65
let prev_block = Block::default() - 66
.borders(Borders::ALL) - 67
.border_style(left_border) - 68
.title(format!( - 69
" {} [PREVIEW] {preview_url} {} ", - 70
Symbols::BULLET, - 71
Symbols::EXTERNAL_LINK - 72
)) - 73
.title_style(self.theme.style_accent().add_modifier(Modifier::BOLD)); - 74
- 75
let prev_inner = prev_block.inner(left_chunks[0]); - 76
prev_block.render(left_chunks[0], buf); - 77
- 78
// Preview button - 79
if prev_inner.height >= 2 && prev_inner.width >= 10 { - 80
let btn_area = Rect { - 81
x: prev_inner.x + 2, - 82
y: prev_inner.y + prev_inner.height.saturating_sub(1), - 83
width: 32.min(prev_inner.width.saturating_sub(4)), - 84
height: 1, - 85
}; - 86
let btn_span = Span::styled( - 87
" [ [PREVIEW] in Browser (o) ] ", - 88
self.theme.style_tab_active(), - 89
); - 90
Paragraph::new(Line::from(vec![btn_span])).render(btn_area, buf); - 91
} - 92
- 93
// 2. Markdown Document Card — real presentation items - 94
let mut doc_lines: Vec<Line> = Vec::new(); - 95
// Show first text-like presentation item as the document card content - 96
let doc_item = self - 97
.presentation_items - 98
.iter() - 99
.find(|i| i.kind == "text" || i.kind == "markdown" || i.kind == "document"); - 100
if let Some(item) = doc_item { - 101
doc_lines.push(Line::from(vec![ - 102
Span::styled(item.kind.clone(), self.theme.style_card()), - 103
Span::styled(format!(" {}", item.title), self.theme.style_accent()), - 104
])); - 105
doc_lines.push(Line::from(vec![Span::styled( - 106
item.status.clone(), - 107
self.theme.style_card().add_modifier(Modifier::DIM), - 108
)])); - 109
} else { - 110
doc_lines.push(Line::from(vec![Span::styled( - 111
"Waiting for presentation events…", - 112
self.theme.style_card().add_modifier(Modifier::DIM), - 113
)])); - 114
} - 115
- 116
if self.card_expanded { - 117
doc_lines.push(Line::from(vec![ - 118
Span::styled(format!(" {} ", Symbols::CHECK), self.theme.style_ok()), - 119
Span::styled( - 120
format!("Sandbox: {} ", self.sandbox_name), - 121
self.theme.style_ok(), - 122
), - 123
])); - 124
doc_lines.push(Line::from(vec![ - 125
Span::styled(format!(" {} ", Symbols::BULLET), self.theme.style_accent()), - 126
Span::styled( - 127
format!("Workspace: {} ", self.cwd), - 128
self.theme.style_card().add_modifier(Modifier::DIM), - 129
), - 130
])); - 131
} - 132
- 133
let doc_caret = if self.card_expanded { - 134
Symbols::CARET_EXPANDED - 135
} else { - 136
Symbols::CARET_COLLAPSED - 137
}; - 138
- 139
let doc_block = Block::default() - 140
.borders(Borders::ALL) - 141
.border_style(left_border) - 142
.title(format!(" {} Markdown Document Card [Space] ", doc_caret)) - 143
.title_style(self.theme.style_card()); - 144
- 145
Paragraph::new(doc_lines) - 146
.block(doc_block) - 147
.render(left_chunks[1], buf); - 148
- 149
// 3. Tool Execution Card — real presentation items with 'tool' kind - 150
let tool_items: Vec<&PresItem> = self - 151
.presentation_items - 152
.iter() - 153
.filter(|i| i.kind == "tool" || i.kind == "bash" || i.kind == "tool_call") - 154
.collect(); - 155
- 156
let tool_lines: Vec<Line> = if tool_items.is_empty() { - 157
vec![Line::from(vec![Span::styled( - 158
"Waiting for tool execution events…", - 159
self.theme.style_card().add_modifier(Modifier::DIM), - 160
)])] - 161
} else { - 162
let mut lines = Vec::new(); - 163
for item in tool_items.iter().take(5) { - 164
let icon = if item.status == "running" || item.status == "active" { - 165
Symbols::STATUS_ACTIVE - 166
} else if item.status == "error" || item.status == "failed" { - 167
Symbols::CROSS - 168
} else { - 169
Symbols::CHECK - 170
}; - 171
let status_style = if item.status == "running" || item.status == "active" { - 172
self.theme.style_ok() - 173
} else if item.status == "error" || item.status == "failed" { - 174
self.theme.style_danger() - 175
} else { - 176
self.theme.style_ok() - 177
}; - 178
lines.push(Line::from(vec![ - 179
Span::styled(format!(" {} ", icon), status_style), - 180
Span::styled(item.title.clone(), self.theme.style_card()), - 181
Span::styled(format!(" [{}] ", item.status), status_style), - 182
])); - 183
} - 184
lines - 185
}; - 186
- 187
let spinner = - 188
Symbols::SPINNER_FRAMES[self.telemetry.tick_count % Symbols::SPINNER_FRAMES.len()]; - 189
let (title, title_style) = if self.is_running { - 190
( - 191
format!( - 192
" {} Agent Stream [{} RUNNING] ", - 193
Symbols::CARET_EXPANDED, - 194
spinner - 195
), - 196
self.theme.style_accent(), - 197
) - 198
} else { - 199
( - 200
format!(" {} Tool Execution History ", Symbols::CARET_EXPANDED), - 201
self.theme.style_ok(), - 202
) - 203
}; - 204
- 205
let tool_block = Block::default() - 206
.borders(Borders::ALL) - 207
.border_style(self.theme.style_border_focus()) - 208
.title(title) - 209
.title_style(title_style); - 210
- 211
Paragraph::new(tool_lines) - 212
.block(tool_block) - 213
.render(left_chunks[2], buf); - 214
- 215
// ------------------------------------------------------------- - 216
// RIGHT DECK: Telemetry HUD - 217
// ------------------------------------------------------------- - 218
let hud_chunks = Layout::default() - 219
.direction(Direction::Vertical) - 220
.constraints([ - 221
Constraint::Length(6), - 222
Constraint::Length(4), - 223
Constraint::Min(8), - 224
]) - 225
.split(main_decks[1]); - 226
- 227
// 1. System Metrics & Swarm Radar - 228
let gauge_chunks = Layout::default() - 229
.direction(Direction::Horizontal) - 230
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - 231
.split(hud_chunks[0]); - 232
- 233
// CPU & RSS Gauges (real values from health when available) - 234
let gauge_lines = vec![ - 235
Line::from(vec![ - 236
Span::styled("CPU UTIL: ", self.theme.style_card()), - 237
Span::styled( - 238
format!("{:.1}% ", self.telemetry.cpu_percent), - 239
self.theme.style_accent().add_modifier(Modifier::BOLD), - 240
), - 241
Span::styled("[■■■■□□□□]", self.theme.style_accent()), - 242
]), - 243
Line::from(vec![ - 244
Span::styled("MEMORY RSS:", self.theme.style_card()), - 245
Span::styled( - 246
format!("{:.1}MB ", self.telemetry.rss_mb), - 247
self.theme.style_ok().add_modifier(Modifier::BOLD), - 248
), - 249
Span::styled("[■■■■■■□□]", self.theme.style_ok()), - 250
]), - 251
Line::from(vec![ - 252
Span::styled("SANDBOX: ", self.theme.style_card()), - 253
Span::styled( - 254
self.sandbox_name, - 255
if self.sandbox_name == "none" { - 256
self.theme.style_danger() - 257
} else { - 258
self.theme.style_ok() - 259
}, - 260
), - 261
]), - 262
]; - 263
- 264
let gauge_block = Block::default() - 265
.borders(Borders::ALL) - 266
.border_style(right_border) - 267
.title(" System Metrics [Tab] ") - 268
.title_style(self.theme.style_card()); - 269
- 270
Paragraph::new(gauge_lines) - 271
.block(gauge_block) - 272
.render(gauge_chunks[0], buf); - 273
- 274
// Swarm Radar (real worker count from SSE) - 275
let radar_lines = vec![ - 276
Line::from(vec![ - 277
Span::styled( - 278
format!(" {} ACTIVE: ", Symbols::STATUS_ACTIVE), - 279
self.theme.style_accent(), - 280
), - 281
Span::styled( - 282
format!("{} agents", self.telemetry.active_workers), - 283
self.theme.style_card().add_modifier(Modifier::BOLD), - 284
), - 285
]), - 286
Line::from(vec![ - 287
Span::styled( - 288
format!(" {} PENDING: ", Symbols::STATUS_IDLE), - 289
self.theme.style_card(), - 290
), - 291
Span::styled( - 292
format!("{} tasks", self.telemetry.pending_tasks), - 293
self.theme.style_card(), - 294
), - 295
]), - 296
Line::from(vec![ - 297
Span::styled(" RADAR: ", self.theme.style_card()), - 298
Span::styled( - 299
format!("SWEEP {:.0}°", self.telemetry.radar_angle_deg), - 300
self.theme.style_ok(), - 301
), - 302
]), - 303
]; - 304
- 305
let radar_block = Block::default() - 306
.borders(Borders::ALL) - 307
.border_style(right_border) - 308
.title(" Agent Swarm Radar ") - 309
.title_style(self.theme.style_card()); - 310
- 311
Paragraph::new(radar_lines) - 312
.block(radar_block) - 313
.render(gauge_chunks[1], buf); - 314
- 315
// 2. Token Velocity Waveform (real samples from TurnEnd events) - 316
let mut wave_str = String::new(); - 317
for val in &self.telemetry.token_rate_history { - 318
let symbol = if *val > 150 { - 319
"▲" - 320
} else if *val > 100 { - 321
"∿" - 322
} else { - 323
"~" - 324
}; - 325
wave_str.push_str(symbol); - 326
} - 327
- 328
let max_rate = self.telemetry.max_rate.max(1); - 329
let rate_label = if wave_str.is_empty() { - 330
"no data".to_string() - 331
} else { - 332
format!("{max_rate} tok/s ") - 333
}; - 334
- 335
let wave_lines = vec![Line::from(vec![ - 336
Span::styled("RATE: ", self.theme.style_card()), - 337
Span::styled( - 338
rate_label, - 339
self.theme.style_accent().add_modifier(Modifier::BOLD), - 340
), - 341
Span::styled(wave_str, self.theme.style_accent()), - 342
])]; - 343
- 344
let wave_block = Block::default() - 345
.borders(Borders::ALL) - 346
.border_style(right_border) - 347
.title(" Token Burn-Rate ") - 348
.title_style(self.theme.style_card()); - 349
- 350
Paragraph::new(wave_lines) - 351
.block(wave_block) - 352
.render(hud_chunks[1], buf); - 353
- 354
// 3. Multi-File Diff Inspector — real diff data from presentation items - 355
let diff_items: Vec<&PresItem> = self - 356
.presentation_items - 357
.iter() - 358
.filter(|i| i.kind == "diff" || i.kind == "file_diff") - 359
.collect(); - 360
- 361
let diff_lines: Vec<Line> = if diff_items.is_empty() { - 362
vec![Line::from(vec![Span::styled( - 363
"Waiting for diff data…", - 364
self.theme.style_card().add_modifier(Modifier::DIM), - 365
)])] - 366
} else { - 367
let mut lines = Vec::new(); - 368
for item in diff_items.iter().take(5) { - 369
let status_icon = if item.status.contains("ok") || item.status.contains("done") { - 370
Symbols::CHECK - 371
} else { - 372
Symbols::STATUS_ACTIVE - 373
}; - 374
lines.push(Line::from(vec![ - 375
Span::styled( - 376
item.title.clone(), - 377
self.theme.style_card().add_modifier(Modifier::BOLD), - 378
), - 379
Span::styled(format!(" [{}] ", item.status), self.theme.style_ok()), - 380
Span::styled(format!(" {} ", status_icon), self.theme.style_ok()), - 381
])); - 382
// Show the summary from the status field - 383
lines.push(Line::from(vec![Span::styled( - 384
item.status.clone(), - 385
self.theme.style_card(), - 386
)])); - 387
} - 388
lines - 389
}; - 390
- 391
let diff_block = Block::default() - 392
.borders(Borders::ALL) - 393
.border_style(right_border) - 394
.title(" Multi-File Diff Inspector [Ctrl+K] ") - 395
.title_style(self.theme.style_card()); - 396
- 397
Paragraph::new(diff_lines) - 398
.block(diff_block) - 399
.render(hud_chunks[2], buf); - 400
} - 401
} - 402
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.