Skip to main content

vtcode_tui/core_tui/app/session/
palette.rs

1/// Palette management operations for Session
2///
3/// This module handles file palette interactions including:
4/// - Loading and closing palettes
5/// - Checking and handling triggers
6/// - Key event handling for palette navigation
7/// - Reference insertion
8use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
9use std::path::PathBuf;
10
11use super::{
12    AppSession,
13    agent_palette::{AgentEntry, AgentPalette, extract_agent_reference},
14    file_palette::{FilePalette, extract_file_reference},
15};
16use crate::core_tui::app::session::slash;
17use crate::core_tui::app::session::transient::TransientSurface;
18use crate::core_tui::app::types::AgentPaletteItem;
19
20impl AppSession {
21    pub(super) fn load_agent_palette(&mut self, agents: Vec<AgentPaletteItem>) {
22        let mut palette = AgentPalette::new();
23        palette.load_agents(
24            agents
25                .into_iter()
26                .map(|agent| AgentEntry {
27                    display_name: format!("@agent-{}", agent.name),
28                    name: agent.name,
29                    description: agent.description,
30                })
31                .collect(),
32        );
33        self.agent_palette = Some(palette);
34        self.agent_palette_active = false;
35        self.check_agent_reference_trigger();
36    }
37
38    pub fn check_agent_reference_trigger(&mut self) {
39        let cursor = self.core.input_manager.cursor();
40        let content = self.core.input_manager.content();
41        let trigger = extract_agent_reference(content, cursor);
42
43        if let Some(palette) = self.agent_palette.as_mut()
44            && let Some((_start, _end, query)) = trigger
45        {
46            palette.set_filter(query);
47            if !self.agent_palette_active {
48                self.ensure_inline_lists_visible_for_trigger();
49                self.agent_palette_active = true;
50                self.show_transient_surface(TransientSurface::AgentPalette);
51                self.mark_dirty();
52            }
53            return;
54        }
55
56        if self.agent_palette_active {
57            self.close_agent_palette();
58        }
59    }
60
61    pub(super) fn close_agent_palette(&mut self) {
62        self.agent_palette_active = false;
63        self.close_transient_surface(TransientSurface::AgentPalette);
64
65        if let Some(palette) = self.agent_palette.as_mut() {
66            palette.set_filter(String::new());
67        }
68    }
69
70    pub(super) fn handle_agent_palette_key(&mut self, key: &KeyEvent) -> bool {
71        if !self.agent_palette_visible() {
72            return false;
73        }
74
75        let Some(palette) = self.agent_palette.as_mut() else {
76            return false;
77        };
78
79        match key.code {
80            KeyCode::Up => {
81                palette.move_selection_up();
82                self.mark_dirty();
83                true
84            }
85            KeyCode::Down => {
86                palette.move_selection_down();
87                self.mark_dirty();
88                true
89            }
90            KeyCode::Tab => {
91                palette.select_best_match();
92                self.mark_dirty();
93                true
94            }
95            KeyCode::Enter => {
96                let selected_name = palette.get_selected().map(|entry| entry.name.clone());
97                if let Some(name) = selected_name {
98                    self.insert_agent_reference(&name);
99                    self.close_agent_palette();
100                    self.mark_dirty();
101                    true
102                } else {
103                    self.close_agent_palette();
104                    self.mark_dirty();
105                    false
106                }
107            }
108            KeyCode::Esc => {
109                self.close_agent_palette();
110                self.mark_dirty();
111                true
112            }
113            KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => {
114                palette.move_selection_down();
115                self.mark_dirty();
116                true
117            }
118            KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
119                palette.move_selection_up();
120                self.mark_dirty();
121                true
122            }
123            _ => false,
124        }
125    }
126
127    pub(crate) fn insert_agent_reference(&mut self, agent_name: &str) {
128        if let Some((start, end, _)) = extract_agent_reference(
129            self.core.input_manager.content(),
130            self.core.input_manager.cursor(),
131        ) {
132            let before = &self.core.input_manager.content()[..start];
133            let after = &self.core.input_manager.content()[end..];
134            let reference_alias = format!("@agent-{}", agent_name);
135            let new_content = format!("{}{} {}", before, reference_alias, after);
136            let new_cursor = start + reference_alias.len() + 1;
137
138            self.core.input_manager.set_content(new_content);
139            self.core.input_manager.set_cursor(new_cursor);
140            slash::update_slash_suggestions(self);
141        }
142    }
143
144    /// Load the file palette with files from the workspace
145    pub(super) fn load_file_palette(&mut self, files: Vec<String>, workspace: PathBuf) {
146        let mut palette = FilePalette::new(workspace);
147        palette.load_files(files);
148        self.file_palette = Some(palette);
149        self.file_palette_active = false;
150        self.check_file_reference_trigger();
151    }
152
153    /// Check if the current input should trigger the file palette
154    pub fn check_file_reference_trigger(&mut self) {
155        if self.agent_palette_visible() {
156            if self.file_palette_active {
157                self.close_file_palette();
158            }
159            return;
160        }
161
162        if let Some(palette) = self.file_palette.as_mut() {
163            if let Some((_start, _end, query)) = extract_file_reference(
164                self.core.input_manager.content(),
165                self.core.input_manager.cursor(),
166            ) {
167                palette.set_filter(query);
168                if !self.file_palette_active {
169                    self.ensure_inline_lists_visible_for_trigger();
170                    self.file_palette_active = true;
171                    self.show_transient_surface(TransientSurface::FilePalette);
172                    self.mark_dirty();
173                }
174            } else if self.file_palette_active {
175                self.close_file_palette();
176            }
177        }
178    }
179
180    /// Close the file palette and clean up resources
181    pub(super) fn close_file_palette(&mut self) {
182        self.file_palette_active = false;
183        self.close_transient_surface(TransientSurface::FilePalette);
184
185        // Clean up resources when closing to free memory
186        if let Some(palette) = self.file_palette.as_mut() {
187            palette.set_filter(String::new());
188        }
189    }
190
191    /// Handle key events for the file palette
192    ///
193    /// Returns true if the key was handled by the palette
194    pub(super) fn handle_file_palette_key(&mut self, key: &KeyEvent) -> bool {
195        if !self.file_palette_visible() {
196            return false;
197        }
198
199        let Some(palette) = self.file_palette.as_mut() else {
200            return false;
201        };
202
203        match key.code {
204            KeyCode::Up => {
205                palette.move_selection_up();
206                self.mark_dirty();
207                true
208            }
209            KeyCode::Down => {
210                palette.move_selection_down();
211                self.mark_dirty();
212                true
213            }
214            KeyCode::Left => {
215                palette.collapse_selected();
216                self.mark_dirty();
217                true
218            }
219            KeyCode::Right => {
220                palette.expand_selected();
221                self.mark_dirty();
222                true
223            }
224            KeyCode::Tab => {
225                palette.select_best_match();
226                self.mark_dirty();
227                true
228            }
229            KeyCode::Enter => {
230                if palette.selected_is_expandable_group() {
231                    palette.toggle_selected();
232                    self.mark_dirty();
233                    true
234                } else {
235                    let selected_path = palette.get_selected().map(|e| e.relative_path.clone());
236                    if let Some(path) = selected_path {
237                        self.insert_file_reference(&path);
238                        self.close_file_palette();
239                        self.mark_dirty();
240                        true
241                    } else {
242                        self.close_file_palette();
243                        self.mark_dirty();
244                        false
245                    }
246                }
247            }
248            KeyCode::Esc => {
249                self.close_file_palette();
250                self.mark_dirty();
251                true
252            }
253            KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => {
254                palette.move_selection_down();
255                self.mark_dirty();
256                true
257            }
258            KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
259                palette.move_selection_up();
260                self.mark_dirty();
261                true
262            }
263            _ => false,
264        }
265    }
266
267    /// Insert a file reference into the input at the current position
268    pub(crate) fn insert_file_reference(&mut self, file_path: &str) {
269        if let Some((start, end, _)) = extract_file_reference(
270            self.core.input_manager.content(),
271            self.core.input_manager.cursor(),
272        ) {
273            let before = &self.core.input_manager.content()[..start];
274            let after = &self.core.input_manager.content()[end..];
275            let reference_alias = format!("@{}", file_path);
276            let new_content = format!("{}{} {}", before, reference_alias, after);
277            let new_cursor = start + reference_alias.len() + 1;
278
279            self.core.input_manager.set_content(new_content);
280            self.core.input_manager.set_cursor(new_cursor);
281            slash::update_slash_suggestions(self);
282        }
283    }
284}