- 188
health: HealthReport, - 189
api: Arc<ApiClient>, - 190
) -> Self { - 191
let providers = ProviderList { - 192
current: health.provider.clone(), - 193
current_model: health.model.clone(), - 194
current_configured: true, - 195
providers: Vec::new(), - 196
}; - 197
- 198
Self { - 199
active_tab: ActiveTab::Studio, - 200
deck_focus: DeckFocus::Left, - 201
theme: ThemeKind::default().theme(), - 202
telemetry: TelemetryState::new(), - 203
composer: ReplComposer::new(), - 204
hil_state: None, - 205
session_id: session_id.into(), - 206
model_name: model_name.into(), - 207
host_endpoint: host_endpoint.into(), - 208
selected_permission_mode: health.permission_mode.clone(), - 209
approval_mode: health.permission_mode.clone(), - 210
workspace_name: health.cwd.clone(), - 211
scroll_offset: 0, - 212
card_expanded: false, - 213
alert_acknowledged: false, - 214
skill_promoted: false, - 215
watchdog_restarted: false, - 216
pending_chat_status: None, - 217
should_quit: false, - 218
should_detach: false, - 219
- 220
api, - 221
health, - 222
providers, - 223
models: Vec::new(), - 224
mcp_servers: HashMap::new(), - 225
gateway: GatewayApprovals { - 226
mode: "deny".into(), - 227
approver: None, - 228
timeout_secs: 30, - 229
enabled: false, - 230
forwarding: false, - 231
candidates: Vec::new(), - 232
}, - 233
config: ConfigSnapshot::default(), - 234
control_state: ControlState::default(), - 235
launch_servers: Vec::new(), - 236
sessions: Vec::new(), - 237
incidents: Vec::new(), - 238
audit_entries: Vec::new(), - 239
merkle_nodes: Vec::new(), - 240
pending_chats: Vec::new(), - 241
presentation_items: Vec::new(), - 242
mcp_rows: Vec::new(), - 243
inbox_entries: Vec::new(), - 244
connected: false, - 245
} - 246
} - 247
- 248
/// Build a TerminalApp from a real API client — the host endpoint is - 249
/// derived from the client's base URL so the header always shows where - 250
/// the terminal is connected to. - 251
pub fn new_with_api( - 252
session_id: impl Into<String>, - 253
model_name: impl Into<String>, - 254
health: HealthReport, - 255
api: Arc<ApiClient>, - 256
) -> Self { - 257
let host = api.base_url().to_string(); - 258
Self::new(session_id, model_name, host, health, api) - 259
} - 260
- 261
pub fn with_workspace(mut self, name: impl Into<String>) -> Self { - 262
self.workspace_name = name.into(); - 263
self - 264
} - 265
- 266
pub fn with_providers(mut self, providers: ProviderList) -> Self { - 267
self.providers = providers; - 268
self - 269
} - 270
- 271
pub fn with_models(mut self, models: Vec<String>) -> Self { - 272
self.models = models; - 273
self - 274
} - 275
- 276
pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerDef>) -> Self { - 277
self.mcp_servers = servers.clone(); - 278
self.mcp_rows = servers - 279
.keys() - 280
.map(|name| McpRow { - 281
name: name.clone(), - 282
tool_count: 0, - 283
memory_mb: 0.0, - 284
status: McpStatus::Ok, - 285
}) - 286
.collect(); - 287
self - 288
} - 289
- 290
pub fn with_gateway(mut self, gateway: GatewayApprovals) -> Self { - 291
self.gateway = gateway; - 292
self - 293
} - 294
- 295
pub fn with_config(mut self, config: ConfigSnapshot) -> Self { - 296
self.config = config; - 297
self - 298
} - 299
- 300
pub fn with_control_state(mut self, state: ControlState) -> Self { - 301
self.control_state = state; - 302
self - 303
} - 304
- 305
pub fn with_launch_servers(mut self, servers: Vec<LaunchServer>) -> Self { - 306
self.launch_servers = servers; - 307
self - 308
} - 309
- 310
pub fn with_sessions(mut self, sessions: Vec<SessionInfo>) -> Self { - 311
self.sessions = sessions; - 312
self - 313
} - 314
- 315
/// Advance tick animations (50ms) and derive a token-rate sample when - 316
/// no fresh agent event has arrived. - 317
pub fn on_tick(&mut self) { - 318
self.telemetry.tick(); - 319
} - 320
- 321
/// Cycle to the next theme. - 322
pub fn cycle_theme(&mut self) { - 323
let next_kind = self.theme.kind.next(); - 324
self.theme = next_kind.theme(); - 325
} - 326
- 327
/// Process a real event from the SSE watcher channel. - 328
pub fn handle_terminal_event(&mut self, event: TerminalEvent) { - 329
match event { - 330
TerminalEvent::Connected => { - 331
self.connected = true; - 332
self.telemetry.connected = true; - 333
self.telemetry.last_error = None; - 334
} - 335
TerminalEvent::Error(msg) => { - 336
self.connected = false; - 337
self.telemetry.set_connection_error(msg); - 338
} - 339
TerminalEvent::Health(report) => { - 340
self.connected = true; - 341
self.telemetry.update_from_health(&report); - 342
self.telemetry.clear_error(); - 343
self.health = report.clone(); - 344
self.model_name = report.model.clone(); - 345
self.selected_permission_mode = report.permission_mode.clone(); - 346
} - 347
TerminalEvent::Agent { event, seq } => { - 348
self.handle_agent_event(&event, seq); - 349
} - 350
TerminalEvent::Presentation { frame, seq } => { - 351
self.handle_presentation_event(&frame, seq); - 352
} - 353
} - 354
} - 355
- 356
/// Dispatch a real `AgentEvent` from the SSE stream. - 357
fn handle_agent_event(&mut self, event_json: &serde_json::Value, _seq: u64) { - 358
if let Some(approval) = crate::hil::extract_approval_request(event_json) { - 359
let hil = HilApprovalState::from_approval_event( - 360
self.session_id.clone(), - 361
approval.request_id.clone(), - 362
approval.tool.clone(), - 363
approval.args_json.clone(), - 364
approval.reason.clone(), - 365
self.health.cwd.clone(), - 366
None, - 367
); - 368
self.hil_state = Some(hil); - 369
self.pending_chats.push(PendingChatEntry { - 370
surface: "gateway".into(), - 371
sender: approval.tool.clone(), - 372
preview: approval.reason.clone(), - 373
request_id: approval.request_id.clone(), - 374
}); - 375
return; - 376
} - 377
- 378
if let Some(turn_end) = event_json.get("TurnEnd") - 379
&& let Some(usage) = turn_end.get("usage") - 380
{ - 381
let input = usage - 382
.get("input_tokens") - 383
.and_then(|v| v.as_u64()) - 384
.unwrap_or(0); - 385
let output = usage - 386
.get("output_tokens") - 387
.and_then(|v| v.as_u64()) - 388
.unwrap_or(0); - 389
let total = (input + output) as u32; - 390
self.telemetry.push_token_rate(total); - 391
} - 392
- 393
if event_json.get("WorkerStarted").is_some() { - 394
self.telemetry - 395
.set_active_workers(self.telemetry.active_workers.saturating_add(1)); - 396
} - 397
if event_json.get("WorkerFinished").is_some() { - 398
self.telemetry - 399
.set_active_workers(self.telemetry.active_workers.saturating_sub(1)); - 400
} - 401
- 402
if event_json.get("RunFinished").is_some() { - 403
self.control_state.running = false; - 404
} - 405
- 406
if let Some(fallback) = event_json.get("RouteFallback") { - 407
let to_provider = fallback - 408
.get("to_provider") - 409
.and_then(|v| v.as_str()) - 410
.unwrap_or("unknown"); - 411
let to_model = fallback - 412
.get("to_model") - 413
.and_then(|v| v.as_str()) - 414
.unwrap_or("unknown"); - 415
self.incidents.push(IncidentEntry { - 416
timestamp: chrono::Local::now().format("%H:%M:%S").to_string(), - 417
severity: IncidentSeverity::Warn, - 418
message: format!("Route fallback to {to_provider}:{to_model}"), - 419
}); - 420
} - 421
- 422
if let Some(err) = event_json.get("ProviderError") { - 423
let provider = err.get("provider").and_then(|v| v.as_str()).unwrap_or("?"); - 424
let msg = err.get("error").and_then(|v| v.as_str()).unwrap_or("?"); - 425
self.incidents.push(IncidentEntry { - 426
timestamp: chrono::Local::now().format("%H:%M:%S").to_string(), - 427
severity: IncidentSeverity::Critical, - 428
message: format!("{provider}: {msg}"), - 429
}); - 430
} - 431
} - 432
- 433
/// Dispatch a real presentation `OutputStreamFrame` from the SSE stream. - 434
fn handle_presentation_event(&mut self, frame_json: &serde_json::Value, _seq: u64) { - 435
if let Some(snapshot) = frame_json.get("snapshot") { - 436
if let Some(items) = snapshot.get("items").and_then(|v| v.as_array()) { - 437
let mut new_items = Vec::new(); - 438
for item in items { - 439
let id = item - 440
.get("id") - 441
.and_then(|v| v.as_str()) - 442
.unwrap_or("") - 443
.to_string(); - 444
let kind = item - 445
.get("kind") - 446
.and_then(|v| v.as_str()) - 447
.unwrap_or("unknown") - 448
.to_string(); - 449
let title = item - 450
.get("title") - 451
.and_then(|v| v.as_str()) - 452
.unwrap_or("") - 453
.to_string(); - 454
let status = item - 455
.get("status") - 456
.and_then(|v| v.as_str()) - 457
.unwrap_or("unknown") - 458
.to_string(); - 459
new_items.push(PresItem { - 460
id: id.clone(), - 461
kind, - 462
title, - 463
status, - 464
}); - 465
} - 466
self.presentation_items = new_items; - 467
} - 468
- 469
if let Some(diag) = snapshot.get("diagnostics").and_then(|v| v.as_array()) { - 470
for d in diag { - 471
if let Some(text) = d.as_str() { - 472
self.incidents.push(IncidentEntry { - 473
timestamp: chrono::Local::now().format("%H:%M:%S").to_string(), - 474
severity: IncidentSeverity::Info, - 475
message: text.to_string(), - 476
}); - 477
} - 478
} - 479
} - 480
} - 481
- 482
if let Some(delta) = frame_json.get("delta").and_then(|v| v.as_object()) - 483
&& let Some(item) = delta.get("item").and_then(|v| v.as_object()) - 484
{ - 485
let id = item - 486
.get("id") - 487
.and_then(|v| v.as_str()) - 488
.unwrap_or("") - 489
.to_string(); - 490
let kind = item - 491
.get("kind") - 492
.and_then(|v| v.as_str()) - 493
.unwrap_or("unknown") - 494
.to_string(); - 495
let title = item - 496
.get("title") - 497
.and_then(|v| v.as_str()) - 498
.unwrap_or("") - 499
.to_string(); - 500
let status = item - 501
.get("status") - 502
.and_then(|v| v.as_str()) - 503
.unwrap_or("unknown") - 504
.to_string(); - 505
if let Some(existing) = self.presentation_items.iter_mut().find(|i| i.id == id) { - 506
existing.kind = kind; - 507
existing.title = title; - 508
existing.status = status; - 509
} else { - 510
self.presentation_items.push(PresItem { - 511
id, - 512
kind, - 513
title, - 514
status, - 515
}); - 516
} - 517
} - 518
} - 519
- 520
/// Handle keyboard input. - 521
pub fn handle_key(&mut self, key: crossterm::event::KeyEvent) { - 522
use crossterm::event::KeyCode; - 523
- 524
// 1. If HIL modal is open, it captures inputs - 525
if let Some(ref mut hil) = self.hil_state { - 526
if hil.is_editing { - 527
match key.code { - 528
KeyCode::Enter => { - 529
hil.is_editing = false; - 530
} - 531
KeyCode::Esc => { - 532
hil.cancel_editing(); - 533
} - 534
KeyCode::Backspace => { - 535
hil.delete_backspace(); - 536
} - 537
KeyCode::Char(c) => { - 538
hil.insert_char(c); - 539
} - 540
_ => {} - 541
} - 542
return; - 543
} - 544
- 545
match key.code { - 546
KeyCode::Char('y') | KeyCode::Enter => { - 547
self.dispatch_approval(HilOutcome::ApproveOnce { - 548
modified_command: None, - 549
}); - 550
} - 551
KeyCode::Char('a') => { - 552
self.dispatch_approval(HilOutcome::AlwaysForSession); - 553
} - 554
KeyCode::Char('d') => { - 555
self.dispatch_approval(HilOutcome::Deny); - 556
} - 557
KeyCode::Char('e') => { - 558
hil.start_editing(); - 559
} - 560
KeyCode::Char('r') => { - 561
hil.toggle_remember(); - 562
} - 563
KeyCode::Esc => { - 564
self.hil_state = None; - 565
} - 566
_ => {} - 567
} - 568
return; - 569
} - 570
- 571
// 2. Global Hotkeys (Ctrl+D, Ctrl+C, Ctrl+K) - 572
if key - 573
.modifiers - 574
.contains(crossterm::event::KeyModifiers::CONTROL) - 575
{ - 576
match key.code { - 577
KeyCode::Char('d') => { - 578
self.should_detach = true; - 579
return; - 580
} - 581
KeyCode::Char('c') => { - 582
self.should_quit = true; - 583
return; - 584
} - 585
KeyCode::Char('k') => { - 586
self.composer.buffer = "/".into(); - 587
self.composer.cursor_pos = 1; - 588
self.composer.slash_palette_open = true; - 589
return; - 590
} - 591
_ => {} - 592
} - 593
} - 594
- 595
// F2 cycles theme - 596
if key.code == KeyCode::F(2) { - 597
self.cycle_theme(); - 598
return; - 599
} - 600
- 601
// Tab switches deck focus or completes slash commands - 602
if key.code == KeyCode::Tab { - 603
if self.composer.slash_palette_open { - 604
self.composer.complete_selected_slash(); - 605
return; - 606
} - 607
self.deck_focus = match self.deck_focus { - 608
DeckFocus::Left => DeckFocus::Right, - 609
DeckFocus::Right => DeckFocus::Left, - 610
}; - 611
return; - 612
} - 613
- 614
if key.code == KeyCode::BackTab { - 615
self.deck_focus = match self.deck_focus { - 616
DeckFocus::Left => DeckFocus::Right, - 617
DeckFocus::Right => DeckFocus::Left, - 618
}; - 619
return; - 620
} - 621
- 622
// Number keys 1-4 and hotkeys (when composer buffer is empty) - 623
if self.composer.buffer.is_empty() { - 624
match key.code { - 625
KeyCode::Char('1') => { - 626
self.active_tab = ActiveTab::Studio; - 627
return; - 628
} - 629
KeyCode::Char('2') => { - 630
self.active_tab = ActiveTab::Observability; - 631
return; - 632
} - 633
KeyCode::Char('3') => { - 634
self.active_tab = ActiveTab::Admin; - 635
return; - 636
} - 637
KeyCode::Char('4') => { - 638
self.active_tab = ActiveTab::Inbox; - 639
return; - 640
} - 641
KeyCode::Char(' ') => { - 642
self.card_expanded = !self.card_expanded; - 643
return; - 644
} - 645
KeyCode::Char('m') if self.active_tab == ActiveTab::Admin => { - 646
self.cycle_permission_mode(); - 647
return; - 648
} - 649
KeyCode::Char('p') if self.active_tab == ActiveTab::Admin => { - 650
self.cycle_approval_mode(); - 651
return; - 652
} - 653
KeyCode::Char('y') if self.active_tab == ActiveTab::Admin => { - 654
self.pending_chat_status = Some(true); - 655
if let Some(_chat) = self.pending_chats.first() { - 656
self.dispatch_approval_from_chat(true, false); - 657
} - 658
return; - 659
} - 660
KeyCode::Char('d') if self.active_tab == ActiveTab::Admin => { - 661
self.pending_chat_status = Some(false); - 662
if let Some(_chat) = self.pending_chats.first() { - 663
self.dispatch_approval_from_chat(false, false); - 664
} - 665
return; - 666
} - 667
KeyCode::Char('a') if self.active_tab == ActiveTab::Inbox => { - 668
self.alert_acknowledged = true; - 669
return; - 670
} - 671
KeyCode::Char('p') if self.active_tab == ActiveTab::Inbox => { - 672
self.skill_promoted = true; - 673
return; - 674
} - 675
KeyCode::Char('r') if self.active_tab == ActiveTab::Inbox => { - 676
self.watchdog_restarted = true; - 677
return; - 678
} - 679
_ => {} - 680
} - 681
} - 682
- 683
// 3. Quick Action Palette navigation - 684
if self.composer.slash_palette_open { - 685
match key.code { - 686
KeyCode::Down => { - 687
self.composer.select_next_slash(); - 688
return; - 689
} - 690
KeyCode::Up => { - 691
self.composer.select_prev_slash(); - 692
return; - 693
} - 694
KeyCode::Esc => { - 695
self.composer.slash_palette_open = false; - 696
return; - 697
} - 698
_ => {} - 699
} - 700
} - 701
- 702
// 4. Composer typing - 703
match key.code { - 704
KeyCode::Enter => { - 705
if let Some(cmd) = self.composer.submit() { - 706
self.execute_user_command(&cmd); - 707
} - 708
} - 709
KeyCode::Char(c) => { - 710
self.composer.insert_char(c); - 711
} - 712
KeyCode::Backspace => { - 713
self.composer.delete_backspace(); - 714
} - 715
KeyCode::Left => { - 716
self.composer.move_cursor_left(); - 717
} - 718
KeyCode::Right => { - 719
self.composer.move_cursor_right(); - 720
} - 721
KeyCode::Home => { - 722
self.composer.move_to_start(); - 723
} - 724
KeyCode::End => { - 725
self.composer.move_to_end(); - 726
} - 727
KeyCode::Up => { - 728
self.composer.history_up(); - 729
} - 730
KeyCode::Down => { - 731
self.composer.history_down(); - 732
} - 733
KeyCode::Esc => { - 734
self.composer.clear(); - 735
} - 736
_ => {} - 737
} - 738
} - 739
- 740
/// Execute a slash command or free text, wired to the REAL API. - 741
fn execute_user_command(&mut self, cmd: &str) { - 742
let rt = tokio::runtime::Handle::current(); - 743
let api = self.api.clone(); - 744
let session_id = self.session_id.clone(); - 745
- 746
if cmd == "/model" { - 747
// Cycle through REAL discovered models and PATCH the config. - 748
if !self.models.is_empty() { - 749
let idx = self - 750
.models - 751
.iter() - 752
.position(|m| m == &self.model_name) - 753
.unwrap_or(0); - 754
let next = (idx + 1) % self.models.len(); - 755
let new_model = self.models[next].clone(); - 756
self.model_name = new_model.clone(); - 757
rt.spawn(async move { - 758
let patch = crate::api::ConfigPatch { - 759
model: Some(new_model), - 760
..Default::default() - 761
}; - 762
if let Err(e) = api.patch_config(&patch).await { - 763
eprintln!("failed to set model: {e}"); - 764
} - 765
}); - 766
} - 767
return; - 768
} - 769
- 770
if cmd == "/ops" { - 771
self.active_tab = ActiveTab::Observability; - 772
return; - 773
} - 774
if cmd == "/admin" { - 775
self.active_tab = ActiveTab::Admin; - 776
return; - 777
} - 778
if cmd == "/inbox" { - 779
self.active_tab = ActiveTab::Inbox; - 780
return; - 781
} - 782
if cmd == "/diff" { - 783
self.active_tab = ActiveTab::Studio; - 784
self.deck_focus = DeckFocus::Right; - 785
return; - 786
} - 787
- 788
if cmd == "/preview" { - 789
// Open the REAL launch server URL (from /sessions/{id}/launch). - 790
if let Some(port) = self - 791
.launch_servers - 792
.iter() - 793
.find(|s| s.running && s.port.is_some()) - 794
.and_then(|s| s.port) - 795
{ - 796
let url = format!("http://localhost:{port}"); - 797
let _ = std::process::Command::new("open").arg(&url).spawn(); - 798
} - 799
return; - 800
} - 801
- 802
if cmd == "/hil" || cmd == "/gate" { - 803
// Show the first pending approval from the REAL queue (SSE-derived). - 804
if let Some(chat) = self.pending_chats.first() { - 805
let hil = HilApprovalState::from_approval_event( - 806
self.session_id.clone(), - 807
chat.request_id.clone(), - 808
chat.sender.clone(), - 809
"{}".to_string(), - 810
chat.preview.clone(), - 811
self.health.cwd.clone(), - 812
None, - 813
); - 814
self.hil_state = Some(hil); - 815
} - 816
return; - 817
} - 818
- 819
if cmd == "/theme" { - 820
self.cycle_theme(); - 821
return; - 822
} - 823
if cmd == "/clear" { - 824
self.composer.clear(); - 825
self.scroll_offset = 0; - 826
return; - 827
} - 828
if cmd == "/help" { - 829
self.scroll_offset = 0; - 830
return; - 831
} - 832
if cmd == "/quit" || cmd == "/exit" { - 833
self.should_quit = true; - 834
return; - 835
} - 836
if cmd == "/detach" { - 837
self.should_detach = true; - 838
return; - 839
} - 840
- 841
// Any other text: send it as a steering message to the running session. - 842
let cmd_owned = cmd.to_string(); - 843
rt.spawn(async move { - 844
if let Err(e) = api.send_steering(&session_id, &cmd_owned).await { - 845
eprintln!("failed to send steering: {e}"); - 846
} - 847
}); - 848
} - 849
- 850
/// Dispatch a real HIL approval to the server via POST and clear the modal. - 851
pub fn dispatch_approval(&mut self, outcome: HilOutcome) { - 852
if let Some(hil) = self.hil_state.take() { - 853
let api = self.api.clone(); - 854
let session_id = hil.session_id.clone(); - 855
let req_id = hil.request_id.clone(); - 856
let approve = matches!( - 857
outcome, - 858
HilOutcome::ApproveOnce { .. } | HilOutcome::AlwaysForSession - 859
); - 860
let remember = matches!(outcome, HilOutcome::AlwaysForSession); - 861
- 862
let rt = tokio::runtime::Handle::current(); - 863
rt.spawn(async move { - 864
match api - 865
.answer_approval(&session_id, &req_id, approve, remember) - 866
.await - 867
{ - 868
Ok(ans) => { - 869
if let Some(rule) = ans.learned_rule { - 870
eprintln!("learned rule: {rule}"); - 871
} - 872
if let Some(err) = ans.learn_error { - 873
eprintln!("could not learn rule: {err}"); - 874
} - 875
} - 876
Err(e) => { - 877
eprintln!("approval dispatch failed: {e}"); - 878
} - 879
} - 880
}); - 881
} - 882
} - 883
- 884
/// Dispatch an approval from a pending chat entry in the Admin view. - 885
fn dispatch_approval_from_chat(&mut self, approve: bool, remember: bool) { - 886
if let Some(chat) = self.pending_chats.first() { - 887
let api = self.api.clone(); - 888
let session_id = chat.request_id.clone(); // The request_id IS the approval target - 889
let req_id = chat.request_id.clone(); - 890
let _ = session_id; // We use the session_id for the URL and req_id for the path - 891
let api_clone = api.clone(); - 892
let sid = self.session_id.clone(); - 893
let rt = tokio::runtime::Handle::current(); - 894
rt.spawn(async move { - 895
if let Err(e) = api_clone - 896
.answer_approval(&sid, &req_id, approve, remember) - 897
.await - 898
{ - 899
eprintln!("approval dispatch failed: {e}"); - 900
} - 901
}); - 902
} - 903
} - 904
- 905
/// Cycle permission mode and push to the real server. - 906
/// Order: ReadOnly → FullAccess → WorkspaceWrite → ReadOnly - 907
fn cycle_permission_mode(&mut self) { - 908
let next = match self.selected_permission_mode.as_str() { - 909
"ReadOnly" => "FullAccess", - 910
"FullAccess" => "WorkspaceWrite", - 911
_ => "ReadOnly", - 912
}; - 913
self.selected_permission_mode = next.to_string(); - 914
let api = self.api.clone(); - 915
let mode = next.to_string(); - 916
let rt = tokio::runtime::Handle::current(); - 917
rt.spawn(async move { - 918
if let Err(e) = api.set_permission_mode(&mode).await { - 919
eprintln!("failed to set permission mode: {e}"); - 920
} - 921
}); - 922
} - 923
- 924
/// Toggle approval mode. - 925
fn cycle_approval_mode(&mut self) { - 926
let next = if self.approval_mode == "Ask" { - 927
"AutoApprove" - 928
} else { - 929
"Ask" - 930
}; - 931
self.approval_mode = next.to_string(); - 932
} - 933
- 934
/// Refresh pending chats from the current queue. - 935
pub fn refresh_pending_chats(&mut self) { - 936
self.pending_chat_status = if self.pending_chats.is_empty() { - 937
None - 938
} else { - 939
Some(true) - 940
}; - 941
} - 942
- 943
/// Handle mouse clicks and scrolling. - 944
pub fn handle_mouse(&mut self, mouse: crossterm::event::MouseEvent) { - 945
use crossterm::event::{MouseButton, MouseEventKind}; - 946
- 947
match mouse.kind { - 948
MouseEventKind::Down(MouseButton::Left) => { - 949
let x = mouse.column; - 950
let y = mouse.row; - 951
- 952
// 1. Top navigation tab bar (row 0) - 953
if y == 0 { - 954
if (1..=19).contains(&x) { - 955
self.active_tab = ActiveTab::Studio; - 956
} else if (20..=48).contains(&x) { - 957
self.active_tab = ActiveTab::Observability; - 958
} else if (49..=80).contains(&x) { - 959
self.active_tab = ActiveTab::Admin; - 960
} else if (81..=114).contains(&x) { - 961
self.active_tab = ActiveTab::Inbox; - 962
} - 963
return; - 964
} - 965
- 966
// 2. If HIL modal is active, check click on modal buttons - 967
if let Some(ref _hil) = self.hil_state { - 968
if (10..=16).contains(&y) { - 969
if (15..=32).contains(&x) { - 970
self.dispatch_approval(HilOutcome::ApproveOnce { - 971
modified_command: None, - 972
}); - 973
return; - 974
} else if (33..=56).contains(&x) { - 975
self.hil_state = None; - 976
return; - 977
} else if (57..=70).contains(&x) { - 978
if let Some(hil) = &mut self.hil_state { - 979
hil.start_editing(); - 980
} - 981
return; - 982
} - 983
} - 984
return; - 985
} - 986
- 987
if y > 0 && y < 26 { - 988
self.deck_focus = if x < 60 { - 989
DeckFocus::Left - 990
} else { - 991
DeckFocus::Right - 992
}; - 993
} - 994
- 995
// 3. Screen-specific interactive clicks - 996
match self.active_tab { - 997
ActiveTab::Admin => { - 998
if x < 45 { - 999
if (8..=10).contains(&y) { - 1000
self.selected_permission_mode = "WorkspaceWrite".into(); - 1001
let api = self.api.clone(); - 1002
let rt = tokio::runtime::Handle::current(); - 1003
rt.spawn(async move { - 1004
let _ = api.set_permission_mode("WorkspaceWrite").await; - 1005
}); - 1006
} else if (11..=13).contains(&y) { - 1007
self.selected_permission_mode = "ReadOnly".into(); - 1008
let api = self.api.clone(); - 1009
let rt = tokio::runtime::Handle::current(); - 1010
rt.spawn(async move { - 1011
let _ = api.set_permission_mode("ReadOnly").await; - 1012
}); - 1013
} else if (14..=16).contains(&y) { - 1014
// FullAccess: explicit human trust decision — - 1015
// do NOT auto-escalate from a click. Send to - 1016
// approval gate instead. - 1017
self.dispatch_approval(HilOutcome::ApproveOnce { - 1018
modified_command: None, - 1019
}); - 1020
} - 1021
} - 1022
if (17..=19).contains(&y) { - 1023
if (15..=25).contains(&x) { - 1024
self.approval_mode = "Ask".into(); - 1025
} else if (26..=45).contains(&x) { - 1026
self.approval_mode = "AutoApprove".into(); - 1027
} - 1028
} - 1029
// Pending Authorization Queue buttons - 1030
if x >= 55 && (16..=20).contains(&y) { - 1031
if (55..=75).contains(&x) { - 1032
self.pending_chat_status = Some(true); - 1033
self.dispatch_approval_from_chat(true, false); - 1034
} else if (76..=95).contains(&x) { - 1035
self.pending_chat_status = Some(false); - 1036
self.dispatch_approval_from_chat(false, false); - 1037
} - 1038
} - 1039
} - 1040
ActiveTab::Inbox => { - 1041
if (1..=4).contains(&y) && (60..=75).contains(&x) { - 1042
self.alert_acknowledged = true; - 1043
} - 1044
if (5..=9).contains(&y) && (40..=65).contains(&x) { - 1045
self.watchdog_restarted = true; - 1046
} - 1047
if (15..=20).contains(&y) && x >= 60 { - 1048
self.skill_promoted = true; - 1049
} - 1050
} - 1051
ActiveTab::Studio => { - 1052
if (8..=11).contains(&y) - 1053
&& (2..=32).contains(&x) - 1054
&& let Some(port) = self - 1055
.launch_servers - 1056
.iter() - 1057
.find(|s| s.running && s.port.is_some()) - 1058
.and_then(|s| s.port) - 1059
{ - 1060
let url = format!("http://localhost:{port}"); - 1061
let _ = std::process::Command::new("open").arg(&url).spawn(); - 1062
} - 1063
if (11..=15).contains(&y) && x < 60 { - 1064
self.card_expanded = !self.card_expanded; - 1065
} - 1066
} - 1067
ActiveTab::Observability => {} - 1068
} - 1069
} - 1070
MouseEventKind::ScrollDown => { - 1071
self.scroll_offset = self.scroll_offset.saturating_add(1); - 1072
} - 1073
MouseEventKind::ScrollUp => { - 1074
self.scroll_offset = self.scroll_offset.saturating_sub(1); - 1075
} - 1076
_ => {} - 1077
} - 1078
} - 1079
- 1080
pub fn quit_requested(&self) -> bool { - 1081
self.should_quit - 1082
} - 1083
- 1084
pub fn detach_requested(&self) -> bool { - 1085
self.should_detach - 1086
} - 1087
- 1088
/// Render the full terminal UI. All data comes from the real - 1089
/// API-backed fields, never from hardcoded values. - 1090
pub fn render_to(&self, area: Rect, buf: &mut Buffer) { - 1091
let chunks = Layout::default() - 1092
.direction(Direction::Vertical) - 1093
.constraints([ - 1094
Constraint::Length(1), // Header tab bar - 1095
Constraint::Min(10), // Active deck view - 1096
Constraint::Length(4), // Pinned bottom composer - 1097
]) - 1098
.split(area); - 1099
- 1100
// 1. Header View (real session, model, server, spend) - 1101
let header = HeaderView { - 1102
active_tab: self.active_tab, - 1103
session_name: &self.session_id, - 1104
model_name: &self.model_name, - 1105
host_endpoint: &self.host_endpoint, - 1106
spend_usd: 0.0, - 1107
connected: self.connected, - 1108
error: self.telemetry.last_error.clone(), - 1109
theme: &self.theme, - 1110
}; - 1111
header.render(chunks[0], buf); - 1112
- 1113
// 2. Active Screen View - 1114
match self.active_tab { - 1115
ActiveTab::Studio => { - 1116
let launch_url = self - 1117
.launch_servers - 1118
.iter() - 1119
.find(|s| s.running && s.port.is_some()) - 1120
.and_then(|s| s.port.map(|port| format!("http://localhost:{port}"))); - 1121
let view = StudioView { - 1122
telemetry: &self.telemetry, - 1123
theme: &self.theme, - 1124
is_running: self.control_state.running, - 1125
deck_focus: self.deck_focus, - 1126
card_expanded: self.card_expanded, - 1127
presentation_items: &self.presentation_items, - 1128
launch_url: launch_url.as_deref(), - 1129
sandbox_name: &self.health.sandbox, - 1130
cwd: &self.health.cwd, - 1131
}; - 1132
view.render(chunks[1], buf); - 1133
} - 1134
ActiveTab::Observability => { - 1135
let view = OpsView { - 1136
telemetry: &self.telemetry, - 1137
theme: &self.theme, - 1138
deck_focus: self.deck_focus, - 1139
incidents: &self.incidents, - 1140
audit_entries: &self.audit_entries, - 1141
merkle_nodes: &self.merkle_nodes, - 1142
health: &self.health, - 1143
}; - 1144
view.render(chunks[1], buf); - 1145
} - 1146
ActiveTab::Admin => { - 1147
let view = AdminView { - 1148
theme: &self.theme, - 1149
selected_permission_mode: &self.selected_permission_mode, - 1150
approval_mode: &self.approval_mode, - 1151
workspace_name: &self.workspace_name, - 1152
deck_focus: self.deck_focus, - 1153
pending_chat_status: self.pending_chat_status, - 1154
mcp_rows: &self.mcp_rows, - 1155
mcp_servers: &self.mcp_servers, - 1156
gateway: &self.gateway, - 1157
sessions: &self.sessions, - 1158
pending_chats: &self.pending_chats, - 1159
health: &self.health, - 1160
connected: self.connected, - 1161
}; - 1162
view.render(chunks[1], buf); - 1163
} - 1164
ActiveTab::Inbox => { - 1165
let view = InboxView { - 1166
theme: &self.theme, - 1167
deck_focus: self.deck_focus, - 1168
alert_acknowledged: self.alert_acknowledged, - 1169
skill_promoted: self.skill_promoted, - 1170
watchdog_restarted: self.watchdog_restarted, - 1171
inbox_entries: &self.inbox_entries, - 1172
health: &self.health, - 1173
}; - 1174
view.render(chunks[1], buf); - 1175
} - 1176
} - 1177
- 1178
// 3. Pinned Bottom Composer - 1179
let composer_view = ComposerView { - 1180
composer: &self.composer, - 1181
theme: &self.theme, - 1182
connected: self.connected, - 1183
error: self.telemetry.last_error.clone(), - 1184
}; - 1185
composer_view.render(chunks[2], buf); - 1186
- 1187
// 4. Floating HIL Approval Modal (if active)
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.