Skip to content

Commit cc0b7d5

Browse files
echobtfactorydroid
andauthored
fix(cortex-tui): clean up agents TUI and improve navigation (#225)
- Remove all emojis from agent selection interface - Remove @ prefix from agent names for cleaner display - Fix arrow key navigation to skip separators and disabled items - Add test for navigation skipping behavior Co-authored-by: Droid Agent <droid@factory.ai>
1 parent 5c7ef58 commit cc0b7d5

2 files changed

Lines changed: 91 additions & 38 deletions

File tree

cortex-tui/src/interactive/builders/agents.rs

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ pub fn build_agents_selector(
281281
let mut items = Vec::new();
282282

283283
// Add "Create New Agent" action at the top
284-
let create_item = InteractiveItem::new("__create__", "Create New Agent")
284+
let create_item = InteractiveItem::new("__create__", "Create New Agent")
285285
.with_description("Create a new custom agent (AI-assisted or manual)")
286286
.with_shortcut('n');
287287
items.push(create_item);
@@ -314,12 +314,8 @@ pub fn build_agents_selector(
314314
let suffix = if agent.is_subagent { " (subagent)" } else { "" };
315315
let description = format!("{}{}", agent.description, suffix);
316316

317-
let item = InteractiveItem::new(
318-
format!("builtin:{}", agent.name),
319-
format!("@{}", agent.name),
320-
)
321-
.with_description(description)
322-
.with_icon('🤖');
317+
let item = InteractiveItem::new(format!("builtin:{}", agent.name), agent.name.clone())
318+
.with_description(description);
323319
items.push(item);
324320
}
325321

@@ -347,12 +343,9 @@ pub fn build_agents_selector(
347343
.unwrap_or_default();
348344
let description = format!("{}{}", agent.description, model_info);
349345

350-
let item = InteractiveItem::new(
351-
format!("project:{}", agent.name),
352-
format!("@{}", display_name),
353-
)
354-
.with_description(description)
355-
.with_icon('📁');
346+
let item =
347+
InteractiveItem::new(format!("project:{}", agent.name), display_name.clone())
348+
.with_description(description);
356349
items.push(item);
357350
}
358351
}
@@ -381,12 +374,8 @@ pub fn build_agents_selector(
381374
.unwrap_or_default();
382375
let description = format!("{}{}", agent.description, model_info);
383376

384-
let item = InteractiveItem::new(
385-
format!("global:{}", agent.name),
386-
format!("@{}", display_name),
387-
)
388-
.with_description(description)
389-
.with_icon('🌐');
377+
let item = InteractiveItem::new(format!("global:{}", agent.name), display_name.clone())
378+
.with_description(description);
390379
items.push(item);
391380
}
392381
}
@@ -445,10 +434,10 @@ pub enum AgentCreationMethod {
445434
/// Build an interactive state for agent creation - step 1: choose location
446435
pub fn build_agent_location_selector() -> InteractiveState {
447436
let items = vec![
448-
InteractiveItem::new("project", "📁 Project Agent")
437+
InteractiveItem::new("project", "Project Agent")
449438
.with_description("Create in .cortex/agents/ - available only in this project")
450439
.with_shortcut('p'),
451-
InteractiveItem::new("global", "🌐 Global Agent")
440+
InteractiveItem::new("global", "Global Agent")
452441
.with_description("Create in ~/.config/cortex/agents/ - available everywhere")
453442
.with_shortcut('g'),
454443
];
@@ -473,10 +462,10 @@ pub fn build_agent_method_selector(location: AgentLocation) -> InteractiveState
473462
};
474463

475464
let items = vec![
476-
InteractiveItem::new(format!("ai:{}", location_str), "AI-Assisted")
465+
InteractiveItem::new(format!("ai:{}", location_str), "AI-Assisted")
477466
.with_description("Describe what you want and AI will generate the agent configuration")
478467
.with_shortcut('a'),
479-
InteractiveItem::new(format!("manual:{}", location_str), "📝 Manual")
468+
InteractiveItem::new(format!("manual:{}", location_str), "Manual")
480469
.with_description("Configure the agent settings manually")
481470
.with_shortcut('m'),
482471
];
@@ -529,19 +518,19 @@ impl PermissionPreset {
529518
/// Build an interactive state for permission selection during agent creation
530519
pub fn build_permission_selector(suggested: Option<PermissionPreset>) -> InteractiveState {
531520
let items = vec![
532-
InteractiveItem::new("readonly", "🔒 Read-only")
521+
InteractiveItem::new("readonly", "Read-only")
533522
.with_description(PermissionPreset::ReadOnly.description())
534523
.with_current(suggested == Some(PermissionPreset::ReadOnly))
535524
.with_shortcut('r'),
536-
InteractiveItem::new("standard", "📝 Standard")
525+
InteractiveItem::new("standard", "Standard")
537526
.with_description(PermissionPreset::Standard.description())
538527
.with_current(suggested == Some(PermissionPreset::Standard))
539528
.with_shortcut('s'),
540-
InteractiveItem::new("full", "Full Access")
529+
InteractiveItem::new("full", "Full Access")
541530
.with_description(PermissionPreset::FullAccess.description())
542531
.with_current(suggested == Some(PermissionPreset::FullAccess))
543532
.with_shortcut('f'),
544-
InteractiveItem::new("custom", "⚙️ Custom")
533+
InteractiveItem::new("custom", "Custom")
545534
.with_description(PermissionPreset::Custom.description())
546535
.with_current(suggested == Some(PermissionPreset::Custom))
547536
.with_shortcut('c'),
@@ -667,7 +656,7 @@ pub fn build_agent_confirm_selector(config: &NewAgentConfig) -> InteractiveState
667656

668657
// Show configuration summary
669658
items.push(
670-
InteractiveItem::new("__info_name__", format!("Name: @{}", config.name)).as_separator(),
659+
InteractiveItem::new("__info_name__", format!("Name: {}", config.name)).as_separator(),
671660
);
672661

673662
if let Some(ref dn) = config.display_name {
@@ -707,19 +696,19 @@ pub fn build_agent_confirm_selector(config: &NewAgentConfig) -> InteractiveState
707696

708697
// Action items
709698
items.push(
710-
InteractiveItem::new("confirm", "Create Agent")
699+
InteractiveItem::new("confirm", "Create Agent")
711700
.with_description("Save the agent configuration")
712701
.with_shortcut('y'),
713702
);
714703

715704
items.push(
716-
InteractiveItem::new("edit_permissions", "🔒 Edit Permissions")
705+
InteractiveItem::new("edit_permissions", "Edit Permissions")
717706
.with_description("Modify the permission settings")
718707
.with_shortcut('p'),
719708
);
720709

721710
items.push(
722-
InteractiveItem::new("cancel", "Cancel")
711+
InteractiveItem::new("cancel", "Cancel")
723712
.with_description("Discard and go back")
724713
.with_shortcut('n'),
725714
);

cortex-tui/src/interactive/state.rs

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,25 +134,53 @@ impl InteractiveState {
134134
self.filtered_indices.get(self.selected).copied()
135135
}
136136

137-
/// Move selection up.
137+
/// Move selection up, skipping separators and disabled items.
138138
pub fn select_prev(&mut self) {
139139
if self.filtered_indices.is_empty() {
140140
return;
141141
}
142-
if self.selected == 0 {
143-
self.selected = self.filtered_indices.len() - 1;
144-
} else {
145-
self.selected -= 1;
142+
let len = self.filtered_indices.len();
143+
let start = self.selected;
144+
loop {
145+
if self.selected == 0 {
146+
self.selected = len - 1;
147+
} else {
148+
self.selected -= 1;
149+
}
150+
// Stop if we've wrapped around completely
151+
if self.selected == start {
152+
break;
153+
}
154+
// Stop if we found a selectable item
155+
if let Some(item) = self.selected_item() {
156+
if !item.is_separator && !item.disabled {
157+
break;
158+
}
159+
}
146160
}
147161
self.ensure_visible();
148162
}
149163

150-
/// Move selection down.
164+
/// Move selection down, skipping separators and disabled items.
151165
pub fn select_next(&mut self) {
152166
if self.filtered_indices.is_empty() {
153167
return;
154168
}
155-
self.selected = (self.selected + 1) % self.filtered_indices.len();
169+
let len = self.filtered_indices.len();
170+
let start = self.selected;
171+
loop {
172+
self.selected = (self.selected + 1) % len;
173+
// Stop if we've wrapped around completely
174+
if self.selected == start {
175+
break;
176+
}
177+
// Stop if we found a selectable item
178+
if let Some(item) = self.selected_item() {
179+
if !item.is_separator && !item.disabled {
180+
break;
181+
}
182+
}
183+
}
156184
self.ensure_visible();
157185
}
158186

@@ -443,4 +471,40 @@ mod tests {
443471
assert!(!state.is_checked(1));
444472
assert_eq!(state.checked.len(), 1);
445473
}
474+
475+
#[test]
476+
fn test_navigation_skips_separators() {
477+
let items = vec![
478+
InteractiveItem::new("1", "Item 1"),
479+
InteractiveItem::new("sep", "─────").as_separator(),
480+
InteractiveItem::new("2", "Item 2"),
481+
InteractiveItem::new("disabled", "Disabled").with_disabled(true),
482+
InteractiveItem::new("3", "Item 3"),
483+
];
484+
let mut state =
485+
InteractiveState::new("Test", items, InteractiveAction::Custom("test".into()));
486+
487+
// Should start at 0 (Item 1)
488+
assert_eq!(state.selected, 0);
489+
490+
// select_next should skip separator (1) and go to Item 2 (2)
491+
state.select_next();
492+
assert_eq!(state.selected, 2); // Skipped separator at index 1
493+
494+
// select_next should skip disabled item (3) and go to Item 3 (4)
495+
state.select_next();
496+
assert_eq!(state.selected, 4); // Skipped disabled at index 3
497+
498+
// select_next should wrap around and go back to Item 1 (0)
499+
state.select_next();
500+
assert_eq!(state.selected, 0);
501+
502+
// select_prev should wrap around to Item 3 (4)
503+
state.select_prev();
504+
assert_eq!(state.selected, 4);
505+
506+
// select_prev should skip disabled (3) and go to Item 2 (2)
507+
state.select_prev();
508+
assert_eq!(state.selected, 2);
509+
}
446510
}

0 commit comments

Comments
 (0)