codetether_agent/tui/app/
navigation.rs1use crossterm::event::KeyModifiers;
2
3use crate::tui::app::model_picker::close_model_picker;
4use crate::tui::app::session_sync::return_to_chat;
5use crate::tui::app::state::App;
6use crate::tui::app::symbols::symbol_search_active;
7use crate::tui::models::{InputMode, ViewMode};
8
9pub fn handle_escape(app: &mut App) {
10 if symbol_search_active(app) {
11 app.state.symbol_search.close();
12 app.state.status = "Closed symbol search".to_string();
13 } else if app.state.show_help {
14 app.state.show_help = false;
15 app.state.status = "Closed help".to_string();
16 } else if app.state.model_picker_active {
17 close_model_picker(app);
18 app.state.status = "Closed model picker".to_string();
19 } else {
20 match app.state.view_mode {
21 ViewMode::Sessions => {
22 app.state.clear_session_filter();
23 return_to_chat(app);
24 }
25 ViewMode::FilePicker => {
26 app.state.file_picker_active = false;
27 return_to_chat(app);
28 }
29 ViewMode::Swarm if app.state.swarm.detail_mode => app.state.swarm.exit_detail(),
30 ViewMode::Ralph if app.state.ralph.detail_mode => app.state.ralph.exit_detail(),
31 ViewMode::Bus if app.state.bus_log.filter_input_mode => {
32 app.state.bus_log.exit_filter_mode();
33 app.state.status = "Protocol filter closed".to_string();
34 }
35 ViewMode::Bus if app.state.bus_log.detail_mode => app.state.bus_log.exit_detail(),
36 ViewMode::Chat => app.state.input_mode = InputMode::Normal,
37 _ => return_to_chat(app),
38 }
39 }
40}
41
42pub fn handle_tab(app: &mut App) {
43 if app.state.apply_selected_slash_suggestion() {
44 app.state.status = "Command autocompleted".to_string();
45 }
46}
47
48pub fn toggle_help(app: &mut App) {
49 app.state.show_help = !app.state.show_help;
50 app.state.help_scroll.offset = 0;
51 app.state.status = if app.state.show_help {
52 "Help".to_string()
53 } else {
54 "Closed help".to_string()
55 };
56}
57
58pub fn handle_up(app: &mut App, modifiers: KeyModifiers) {
59 if app.state.show_help {
60 app.state.help_scroll.scroll_up(1);
61 return;
62 }
63 if symbol_search_active(app) {
64 app.state.symbol_search.select_prev();
65 return;
66 }
67 if app.state.view_mode == ViewMode::Sessions {
68 app.state.sessions_select_prev();
69 return;
70 }
71 if app.state.view_mode == ViewMode::Model {
72 app.state.model_select_prev();
73 return;
74 }
75 if app.state.view_mode == ViewMode::Settings {
76 app.state.settings_select_prev();
77 return;
78 }
79 if app.state.slash_suggestions_visible() {
80 app.state.select_prev_slash_suggestion();
81 return;
82 }
83 if app.state.view_mode == ViewMode::Chat {
84 if modifiers.contains(KeyModifiers::CONTROL) {
85 let _ = app.state.history_prev();
86 } else if modifiers.contains(KeyModifiers::SHIFT) {
87 app.state.scroll_tool_preview_up(1);
88 } else {
89 app.state.scroll_up(1);
90 }
91 return;
92 }
93
94 match app.state.view_mode {
95 ViewMode::Swarm => {
96 if app.state.swarm.detail_mode {
97 app.state.swarm.detail_scroll_up(1);
98 } else {
99 app.state.swarm.select_prev();
100 }
101 }
102 ViewMode::Ralph => {
103 if app.state.ralph.detail_mode {
104 app.state.ralph.detail_scroll_up(1);
105 } else {
106 app.state.ralph.select_prev();
107 }
108 }
109 ViewMode::Bus if !app.state.bus_log.filter_input_mode => {
110 if app.state.bus_log.detail_mode {
111 app.state.bus_log.detail_scroll_up(1);
112 } else {
113 app.state.bus_log.select_prev();
114 }
115 }
116 _ => app.state.scroll_up(1),
117 }
118}
119
120pub fn handle_down(app: &mut App, modifiers: KeyModifiers) {
121 if app.state.show_help {
122 app.state.help_scroll.scroll_down(1, 200);
123 return;
124 }
125 if symbol_search_active(app) {
126 app.state.symbol_search.select_next();
127 return;
128 }
129 if app.state.view_mode == ViewMode::Sessions {
130 app.state.sessions_select_next();
131 return;
132 }
133 if app.state.view_mode == ViewMode::Model {
134 app.state.model_select_next();
135 return;
136 }
137 if app.state.view_mode == ViewMode::Settings {
138 app.state.settings_select_next();
139 return;
140 }
141 if app.state.slash_suggestions_visible() {
142 app.state.select_next_slash_suggestion();
143 return;
144 }
145 if app.state.view_mode == ViewMode::Chat {
146 if modifiers.contains(KeyModifiers::CONTROL) {
147 let _ = app.state.history_next();
148 } else if modifiers.contains(KeyModifiers::SHIFT) {
149 app.state.scroll_tool_preview_down(1);
150 } else {
151 app.state.scroll_down(1);
152 }
153 return;
154 }
155
156 match app.state.view_mode {
157 ViewMode::Swarm => {
158 if app.state.swarm.detail_mode {
159 app.state.swarm.detail_scroll_down(1);
160 } else {
161 app.state.swarm.select_next();
162 }
163 }
164 ViewMode::Ralph => {
165 if app.state.ralph.detail_mode {
166 app.state.ralph.detail_scroll_down(1);
167 } else {
168 app.state.ralph.select_next();
169 }
170 }
171 ViewMode::Bus if !app.state.bus_log.filter_input_mode => {
172 if app.state.bus_log.detail_mode {
173 app.state.bus_log.detail_scroll_down(1);
174 } else {
175 app.state.bus_log.select_next();
176 }
177 }
178 _ => app.state.scroll_down(1),
179 }
180}
181
182pub fn handle_page_up(app: &mut App) {
183 if app.state.show_help {
184 app.state.help_scroll.scroll_up(10);
185 return;
186 }
187
188 match app.state.view_mode {
189 ViewMode::Swarm if app.state.swarm.detail_mode => app.state.swarm.detail_scroll_up(10),
190 ViewMode::Ralph if app.state.ralph.detail_mode => app.state.ralph.detail_scroll_up(10),
191 ViewMode::Bus if app.state.bus_log.detail_mode => app.state.bus_log.detail_scroll_up(10),
192 ViewMode::Chat => app.state.scroll_up(10),
193 _ => {}
194 }
195}
196
197pub fn handle_page_down(app: &mut App) {
198 if app.state.show_help {
199 app.state.help_scroll.scroll_down(10, 200);
200 return;
201 }
202
203 match app.state.view_mode {
204 ViewMode::Swarm if app.state.swarm.detail_mode => app.state.swarm.detail_scroll_down(10),
205 ViewMode::Ralph if app.state.ralph.detail_mode => app.state.ralph.detail_scroll_down(10),
206 ViewMode::Bus if app.state.bus_log.detail_mode => app.state.bus_log.detail_scroll_down(10),
207 ViewMode::Protocol => {
208 app.state.protocol_scroll = app.state.protocol_scroll.saturating_add(10);
209 }
210 ViewMode::Chat => app.state.scroll_down(10),
211 _ => {}
212 }
213}
214
215pub fn handle_home(app: &mut App) {
216 if app.state.view_mode == ViewMode::Chat {
217 app.state.move_cursor_home();
218 }
219}
220
221pub fn handle_end(app: &mut App) {
222 if app.state.view_mode == ViewMode::Chat {
223 app.state.move_cursor_end();
224 }
225}
226
227pub fn handle_left(app: &mut App, modifiers: KeyModifiers) {
228 if app.state.view_mode == ViewMode::Chat {
229 if modifiers.contains(KeyModifiers::CONTROL) {
230 app.state.move_cursor_word_left();
231 } else {
232 app.state.move_cursor_left();
233 }
234 }
235}
236
237pub fn handle_right(app: &mut App, modifiers: KeyModifiers) {
238 if app.state.view_mode == ViewMode::Chat {
239 if modifiers.contains(KeyModifiers::CONTROL) {
240 app.state.move_cursor_word_right();
241 } else {
242 app.state.move_cursor_right();
243 }
244 }
245}
246
247pub fn handle_delete(app: &mut App) {
248 if app.state.view_mode == ViewMode::Chat {
249 app.state.delete_forward();
250 }
251}
252
253pub fn handle_symbol_enter(app: &mut App) {
254 if let Some(symbol) = app.state.symbol_search.selected_symbol() {
255 app.state.status = format!(
256 "Selected symbol {} {}",
257 symbol.name,
258 symbol
259 .line
260 .map(|line| format!("at line {line}"))
261 .unwrap_or_default()
262 );
263 }
264 app.state.symbol_search.close();
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn help_overlay_consumes_chat_arrow_navigation() {
273 let mut app = App::default();
274 app.state.show_help = true;
275 app.state.set_view_mode(ViewMode::Chat);
276 app.state.set_chat_max_scroll(25);
277 app.state.scroll_to_bottom();
278
279 handle_down(&mut app, KeyModifiers::NONE);
280
281 assert_eq!(app.state.help_scroll.offset, 1);
282 assert_eq!(app.state.chat_scroll, 1_000_000);
283 }
284}