Skip to content

Commit 18103ff

Browse files
committed
fix: Fix memory tool_info, improve shortcuts and key management
1 parent 12c1c7e commit 18103ff

File tree

4 files changed

+182
-62
lines changed

4 files changed

+182
-62
lines changed

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ fn init_panic_hook() {
7676
fn init_tui() -> io::Result<DefaultTerminal> {
7777
enable_raw_mode()?;
7878
execute!(stdout(), EnterAlternateScreen)?;
79+
execute!(stdout(), crossterm::event::EnableMouseCapture)?;
7980
Terminal::new(CrosstermBackend::new(stdout()))
8081
}
8182

src/tui/app.rs

Lines changed: 110 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright © 2025 Huly Labs. Use of this source code is governed by the MIT license.
2-
use std::collections::HashSet;
2+
use std::collections::{HashMap, HashSet};
33
use std::path::PathBuf;
44

55
use crate::agent::event::{AgentCommandStatus, AgentState, AgentStatus};
@@ -13,6 +13,8 @@ use crate::{
1313
},
1414
};
1515
use crossterm::event::KeyEventKind;
16+
use ratatui::layout::Position;
17+
use ratatui::prelude::Rect;
1618
use ratatui::{
1719
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
1820
widgets::ScrollbarState,
@@ -25,7 +27,7 @@ use tui_widget_list::ListState;
2527

2628
use super::filetree::FileTreeState;
2729

28-
#[derive(Debug)]
30+
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
2931
#[repr(u8)]
3032
pub enum FocusedComponent {
3133
/// Input text area field
@@ -69,6 +71,7 @@ pub struct UiState<'a> {
6971
pub history_state: ListState,
7072
pub history_opened_state: HashSet<usize>,
7173
pub throbber_state: throbber_widgets_tui::ThrobberState,
74+
pub widget_areas: HashMap<FocusedComponent, Rect>,
7275
}
7376

7477
#[derive(Debug)]
@@ -92,8 +95,9 @@ impl UiState<'_> {
9295
terminal_scroll_position: 0,
9396
tree_state: FileTreeState::new(workspace),
9497
history_state: ListState::default(),
95-
history_opened_state: HashSet::new(),
98+
history_opened_state: HashSet::default(),
9699
throbber_state: throbber_widgets_tui::ThrobberState::default(),
100+
widget_areas: HashMap::default(),
97101
}
98102
}
99103
}
@@ -130,54 +134,85 @@ impl App<'_> {
130134

131135
match self.events.next().await? {
132136
UiEvent::Tick => self.tick(),
133-
UiEvent::Crossterm(event) => {
134-
if let crossterm::event::Event::Key(KeyEvent {
135-
code: KeyCode::Tab,
136-
kind: KeyEventKind::Press,
137-
..
138-
}) = event
139-
{
140-
self.ui.focus = (self.ui.focus as u8 + 1u8).into();
141-
self.ui.tree_state.focused =
142-
matches!(self.ui.focus, FocusedComponent::Tree);
143-
} else {
144-
if let crossterm::event::Event::Key(key_event) = event {
145-
self.handle_global_key_events(key_event)?
146-
}
147-
match self.ui.focus {
148-
FocusedComponent::Input => {
149-
if Self::handle_text_input(&mut self.ui.textarea, &event)
150-
&& !self.ui.textarea.is_empty()
151-
{
152-
self.ui.textarea.select_all();
153-
self.ui.textarea.cut();
154-
self.model.last_error = None;
155-
self.agent_sender
156-
.send(agent::AgentControlEvent::SendMessage(
157-
self.ui.textarea.yank_text(),
158-
))
159-
.unwrap();
137+
UiEvent::Crossterm(event) => match event {
138+
crossterm::event::Event::Key(key_event) => {
139+
if !self.handle_global_key_events(key_event)? {
140+
match self.ui.focus {
141+
FocusedComponent::Input => {
142+
if Self::handle_text_input(&mut self.ui.textarea, &event)
143+
&& !self.ui.textarea.is_empty()
144+
{
145+
self.ui.textarea.select_all();
146+
self.ui.textarea.cut();
147+
self.model.last_error = None;
148+
self.agent_sender
149+
.send(agent::AgentControlEvent::SendMessage(
150+
self.ui.textarea.yank_text(),
151+
))
152+
.unwrap();
153+
}
154+
}
155+
FocusedComponent::Tree => {
156+
Self::handle_tree_input(&mut self.ui.tree_state, &event);
157+
}
158+
FocusedComponent::History => {
159+
Self::handle_list_input(
160+
&mut self.ui.history_state,
161+
&mut self.ui.history_opened_state,
162+
&event,
163+
);
164+
}
165+
FocusedComponent::Terminal => {
166+
Self::handle_scroll_input(
167+
&mut self.ui.terminal_scroll_position,
168+
&event,
169+
);
160170
}
161171
}
162-
FocusedComponent::Tree => {
163-
Self::handle_tree_input(&mut self.ui.tree_state, &event);
164-
}
165-
FocusedComponent::History => {
166-
Self::handle_list_input(
167-
&mut self.ui.history_state,
168-
&mut self.ui.history_opened_state,
169-
&event,
170-
);
172+
}
173+
}
174+
crossterm::event::Event::Mouse(mouse_event) => {
175+
if matches!(mouse_event.kind, crossterm::event::MouseEventKind::Down(_)) {
176+
let focus = self.ui.widget_areas.iter().find_map(|(k, v)| {
177+
if v.contains(Position {
178+
x: mouse_event.column,
179+
y: mouse_event.row,
180+
}) {
181+
Some(k)
182+
} else {
183+
None
184+
}
185+
});
186+
if let Some(focus) = focus {
187+
self.ui.focus = focus.clone();
188+
self.ui.tree_state.focused =
189+
matches!(self.ui.focus, FocusedComponent::Tree);
171190
}
172-
FocusedComponent::Terminal => {
173-
Self::handle_scroll_input(
174-
&mut self.ui.terminal_scroll_position,
175-
&event,
176-
);
191+
match self.ui.focus {
192+
FocusedComponent::Input => {
193+
Self::handle_text_input(&mut self.ui.textarea, &event);
194+
}
195+
FocusedComponent::Tree => {
196+
Self::handle_tree_input(&mut self.ui.tree_state, &event);
197+
}
198+
FocusedComponent::History => {
199+
Self::handle_list_input(
200+
&mut self.ui.history_state,
201+
&mut self.ui.history_opened_state,
202+
&event,
203+
);
204+
}
205+
FocusedComponent::Terminal => {
206+
Self::handle_scroll_input(
207+
&mut self.ui.terminal_scroll_position,
208+
&event,
209+
);
210+
}
177211
}
178212
}
179213
}
180-
}
214+
_ => {}
215+
},
181216
UiEvent::App(app_event) => match app_event {
182217
AppEvent::Quit => self.quit(),
183218
AppEvent::Agent(evt) => match evt {
@@ -308,9 +343,9 @@ impl App<'_> {
308343
}
309344
}
310345

311-
pub fn handle_global_key_events(&mut self, key_event: KeyEvent) -> color_eyre::Result<()> {
346+
pub fn handle_global_key_events(&mut self, key_event: KeyEvent) -> color_eyre::Result<bool> {
312347
if key_event.kind != KeyEventKind::Press {
313-
return Ok(());
348+
return Ok(false);
314349
}
315350

316351
match key_event.code {
@@ -328,9 +363,35 @@ impl App<'_> {
328363
.agent_sender
329364
.send(AgentControlEvent::CancelTask)
330365
.unwrap(),
331-
_ => {}
366+
KeyCode::BackTab => {
367+
let mut focus = self.ui.focus.clone() as u8;
368+
if focus == 0 {
369+
focus = FocusedComponent::Terminal as u8;
370+
} else {
371+
focus = focus - 1;
372+
}
373+
self.ui.focus = focus.into();
374+
}
375+
KeyCode::Tab => {
376+
self.ui.focus = (self.ui.focus.clone() as u8 + 1u8).into();
377+
}
378+
KeyCode::Char('1') | KeyCode::Char('2') | KeyCode::Char('3') | KeyCode::Char('4')
379+
if key_event.modifiers == KeyModifiers::ALT =>
380+
{
381+
match key_event.code {
382+
KeyCode::Char('1') => self.ui.focus = FocusedComponent::Input.into(),
383+
KeyCode::Char('2') => self.ui.focus = FocusedComponent::History.into(),
384+
KeyCode::Char('3') => self.ui.focus = FocusedComponent::Tree.into(),
385+
KeyCode::Char('4') => self.ui.focus = FocusedComponent::Terminal.into(),
386+
_ => {}
387+
};
388+
}
389+
_ => {
390+
return Ok(false);
391+
}
332392
}
333-
Ok(())
393+
self.ui.tree_state.focused = matches!(self.ui.focus, FocusedComponent::Tree);
394+
Ok(true)
334395
}
335396

336397
pub fn current_task_text(&self) -> String {

src/tui/tool_info.rs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,25 @@ use crate::tools::web_fetch::WebFetchTool;
1919
use crate::tools::web_search::WebSearchTool;
2020
use crate::tools::write_to_file::WriteToFileTool;
2121

22-
fn array_info<'a>(name: &'a str, child_name: &'a str, args: &'a serde_json::Value) -> &'a str {
22+
fn array_info<'a>(name: &'a str, child_name: &'a str, args: &'a serde_json::Value) -> String {
2323
args.get(name)
2424
.and_then(|v| {
2525
v.as_array().and_then(|a| {
2626
if a.is_empty() {
27-
Some("")
27+
Some("".to_string())
2828
} else {
2929
a.first().and_then(|f| {
30-
if child_name.is_empty() {
31-
f.as_str()
30+
let name = if child_name.is_empty() {
31+
f.as_str().map(|s| s.to_string())
3232
} else {
33-
Some(array_info(child_name, "", f))
33+
f.get(child_name)
34+
.and_then(|child| child.as_str())
35+
.map(|s| s.to_string())
36+
};
37+
if a.len() > 1 {
38+
name.map(|name| format!("{name}...({})", a.len() - 1))
39+
} else {
40+
name
3441
}
3542
})
3643
}
@@ -143,3 +150,39 @@ pub fn get_tool_call_info(name: &str, args: &serde_json::Value) -> (String, Stri
143150
};
144151
(icon.to_string(), info)
145152
}
153+
154+
#[cfg(test)]
155+
mod tests {
156+
use serde_json::json;
157+
158+
use super::*;
159+
160+
#[test]
161+
fn test_array_info_object() {
162+
let args = json!({
163+
"entities": [
164+
{
165+
"entityType": "person",
166+
"name": "default_user",
167+
"observations": [
168+
"Name is Test"
169+
]
170+
}
171+
]
172+
});
173+
let res = array_info("entities", "name", &args);
174+
assert_eq!(res, "default_user");
175+
}
176+
177+
#[test]
178+
fn test_array_info_simple() {
179+
let args = json!({
180+
"entities": [
181+
"default_user",
182+
"default_user1",
183+
]
184+
});
185+
let res = array_info("entities", "", &args);
186+
assert_eq!(res, "default_user...(1)");
187+
}
188+
}

src/tui/widgets/mod.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ impl Widget for &mut App<'_> {
192192

193193
list.render(left_panel[1], buf, &mut self.ui.history_state);
194194
render_scrollbar(left_panel[1], buf, &mut self.ui.history_scroll_state);
195+
self.ui
196+
.widget_areas
197+
.insert(FocusedComponent::History, left_panel[1]);
195198

196199
// Error message
197200
if let Some(error) = self.model.last_error.as_ref() {
@@ -240,14 +243,15 @@ impl Widget for &mut App<'_> {
240243
.set_placeholder_text("Type your message here...");
241244

242245
// Render the textarea
243-
self.ui.textarea.render(
244-
left_panel[if self.model.last_error.is_some() {
245-
4
246-
} else {
247-
3
248-
}],
249-
buf,
250-
);
246+
let input_layout_idx = if self.model.last_error.is_some() {
247+
4
248+
} else {
249+
3
250+
};
251+
self.ui.textarea.render(left_panel[input_layout_idx], buf);
252+
self.ui
253+
.widget_areas
254+
.insert(FocusedComponent::Input, left_panel[input_layout_idx]);
251255

252256
// Right panel (file tree + terminal)
253257
let right_panel = Layout::default()
@@ -260,6 +264,9 @@ impl Widget for &mut App<'_> {
260264

261265
// File tree
262266
FileTreeWidget.render(right_panel[0], buf, &mut self.ui.tree_state);
267+
self.ui
268+
.widget_areas
269+
.insert(FocusedComponent::Tree, right_panel[0]);
263270

264271
// Terminal output
265272
let terminal_block = Block::bordered()
@@ -296,6 +303,9 @@ impl Widget for &mut App<'_> {
296303
.style(theme.text_style())
297304
.scroll((self.ui.terminal_scroll_position, 0))
298305
.render(right_panel[1], buf);
306+
self.ui
307+
.widget_areas
308+
.insert(FocusedComponent::Terminal, right_panel[1]);
299309

300310
// Status bar with shortcuts
301311
let status_block = Block::bordered()
@@ -311,6 +321,11 @@ impl Widget for &mut App<'_> {
311321
Span::styled(": Pause/Resume Task | ", theme.inactive_style()),
312322
Span::styled("⇥", theme.highlight_style()),
313323
Span::styled(": Change Focus | ", theme.inactive_style()),
324+
#[cfg(target_os = "macos")]
325+
Span::styled("⌥[1-4]", theme.highlight_style()),
326+
#[cfg(not(target_os = "macos"))]
327+
Span::styled("Alt+[1-4]", theme.highlight_style()),
328+
Span::styled(": Focus Panel | ", theme.inactive_style()),
314329
Span::styled("↑↓", theme.highlight_style()),
315330
Span::styled(": Navigate | ", theme.inactive_style()),
316331
Span::styled("Enter", theme.highlight_style()),

0 commit comments

Comments
 (0)