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::Chat => app.state.scroll_down(10),
208 _ => {}
209 }
210}
211
212pub fn handle_home(app: &mut App) {
213 if app.state.view_mode == ViewMode::Chat {
214 app.state.move_cursor_home();
215 }
216}
217
218pub fn handle_end(app: &mut App) {
219 if app.state.view_mode == ViewMode::Chat {
220 app.state.move_cursor_end();
221 }
222}
223
224pub fn handle_left(app: &mut App, modifiers: KeyModifiers) {
225 if app.state.view_mode == ViewMode::Chat {
226 if modifiers.contains(KeyModifiers::CONTROL) {
227 app.state.move_cursor_word_left();
228 } else {
229 app.state.move_cursor_left();
230 }
231 }
232}
233
234pub fn handle_right(app: &mut App, modifiers: KeyModifiers) {
235 if app.state.view_mode == ViewMode::Chat {
236 if modifiers.contains(KeyModifiers::CONTROL) {
237 app.state.move_cursor_word_right();
238 } else {
239 app.state.move_cursor_right();
240 }
241 }
242}
243
244pub fn handle_delete(app: &mut App) {
245 if app.state.view_mode == ViewMode::Chat {
246 app.state.delete_forward();
247 }
248}
249
250pub fn handle_symbol_enter(app: &mut App) {
251 if let Some(symbol) = app.state.symbol_search.selected_symbol() {
252 app.state.status = format!(
253 "Selected symbol {} {}",
254 symbol.name,
255 symbol
256 .line
257 .map(|line| format!("at line {line}"))
258 .unwrap_or_default()
259 );
260 }
261 app.state.symbol_search.close();
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn help_overlay_consumes_chat_arrow_navigation() {
270 let mut app = App::default();
271 app.state.show_help = true;
272 app.state.set_view_mode(ViewMode::Chat);
273 app.state.set_chat_max_scroll(25);
274 app.state.scroll_to_bottom();
275
276 handle_down(&mut app, KeyModifiers::NONE);
277
278 assert_eq!(app.state.help_scroll.offset, 1);
279 assert_eq!(app.state.chat_scroll, 1_000_000);
280 }
281}