- 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) - 1188
if let Some(ref hil) = self.hil_state { - 1189
let modal_view = HilModalView { - 1190
state: hil, - 1191
theme: &self.theme, - 1192
remember: hil.remember, - 1193
}; - 1194
modal_view.render(area, buf); - 1195
} - 1196
} - 1197
} - 1198
- 1199
impl Widget for &TerminalApp { - 1200
fn render(self, area: Rect, buf: &mut Buffer) { - 1201
self.render_to(area, buf); - 1202
} - 1203
} - 1204
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.