mirror of
https://github.com/openai/codex.git
synced 2026-10-14 16:38:43 +00:00
Add status filter tabs to the agent command center (#46839)
## What changed - Replace status totals with counted tabs for All, Needs you, Working, Ready, and Inactive tasks. - Cycle filters with `Tab` and `Shift+Tab`, respecting configured task and list bindings. Combine status filtering with search and retain the active filter across refreshes. - Keep a task visible while renaming it if its status leaves the active filter. Preserve the rename target and draft on failure, and reconcile selection when editing ends. ## Testing Add coverage for filter navigation, narrow tab rendering, configured shortcut precedence, and rename cancellation, submission, and failure recovery after a status change. GitOrigin-RevId: 935a8eadbd3fa970a573614ba653c13ff6541167
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
//! Status-tab hints reflect shortcuts that are not claimed by configured actions.
|
||||
|
||||
use super::*;
|
||||
use crate::key_hint;
|
||||
use crate::style::footer_hint_label_style;
|
||||
|
||||
pub(super) fn hint_line(items: &[(String, String)]) -> Line<'static> {
|
||||
let mut line = Line::default();
|
||||
for (key, label) in items.iter().filter(|(key, _)| !key.is_empty()) {
|
||||
if !line.spans.is_empty() {
|
||||
line.spans.push(" ".dim());
|
||||
}
|
||||
line.spans.extend(key_hint::key_label_spans(key));
|
||||
line.spans.push(Span::styled(
|
||||
format!(" {label}"),
|
||||
footer_hint_label_style().not_bold(),
|
||||
));
|
||||
}
|
||||
line
|
||||
}
|
||||
|
||||
impl AgentsOverviewView {
|
||||
pub(super) fn center_filter_hint(&self) -> String {
|
||||
[key_hint::plain(KeyCode::Tab), key_hint::shift(KeyCode::Tab)]
|
||||
.into_iter()
|
||||
.filter(|hint| {
|
||||
let (code, modifiers) = hint.parts();
|
||||
!self
|
||||
.center_shortcut_keys
|
||||
.is_pressed(KeyEvent::new(code, modifiers))
|
||||
})
|
||||
.map(|hint| hint.display_label())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Status-tab shortcuts yield to configured task and list bindings.
|
||||
|
||||
use super::*;
|
||||
|
||||
impl AgentsOverviewView {
|
||||
pub(in crate::app::agents_overview_view) fn command_center_key(
|
||||
&mut self,
|
||||
key: KeyEvent,
|
||||
) -> bool {
|
||||
if self.state().editing_metadata() {
|
||||
return false;
|
||||
}
|
||||
let mut state = self.state();
|
||||
if self.center_shortcut_keys.is_pressed(key) {
|
||||
return false;
|
||||
}
|
||||
if key.code == KeyCode::Tab
|
||||
&& !key
|
||||
.modifiers
|
||||
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
|
||||
{
|
||||
let step = if key.modifiers.contains(KeyModifiers::SHIFT) {
|
||||
TASK_FILTERS.len() - 1
|
||||
} else {
|
||||
1
|
||||
};
|
||||
state.status_filter = (state.status_filter + step) % TASK_FILTERS.len();
|
||||
state.scroll = 0;
|
||||
drop(state);
|
||||
self.reconcile_command_center_selection();
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,27 @@
|
||||
//! Shared grouped rows and viewport state for task rendering and paging.
|
||||
//! Status filters and viewport state survive refreshes of the shared task projection.
|
||||
|
||||
mod hints;
|
||||
mod input;
|
||||
mod navigation;
|
||||
mod render;
|
||||
mod rows;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Counts and filtering use the same status groups.
|
||||
pub(super) const TASK_FILTERS: &[(&str, Option<AgentsOverviewGroup>)] = &[
|
||||
("All", None),
|
||||
("Needs you", Some(AgentsOverviewGroup::NeedsYou)),
|
||||
("Working", Some(AgentsOverviewGroup::Working)),
|
||||
("Ready", Some(AgentsOverviewGroup::Ready)),
|
||||
("Inactive", Some(AgentsOverviewGroup::Finished)),
|
||||
];
|
||||
|
||||
impl AgentsOverviewView {
|
||||
pub(in crate::app::agents_overview_view) fn reconcile_command_center_selection(&mut self) {
|
||||
let visible = self.visible_indices();
|
||||
if !visible.contains(&self.selected) {
|
||||
self.selected = visible.first().copied().unwrap_or(usize::MAX);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ impl AgentsOverviewView {
|
||||
pub(in crate::app::agents_overview_view) fn page_selection(&mut self, action: ListAction) {
|
||||
let indices = self.visible_indices();
|
||||
let mut state = self.state();
|
||||
if state.renaming || indices.is_empty() {
|
||||
if state.rename_target.is_some() || indices.is_empty() {
|
||||
return;
|
||||
}
|
||||
let forward = action == ListAction::PageDown;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Task rows, metadata editing and read-only details share a wide/narrow layout.
|
||||
|
||||
use super::hints::hint_line;
|
||||
use super::*;
|
||||
use crate::bottom_pane::render_filled_tab_bar;
|
||||
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
@@ -94,12 +96,12 @@ impl Renderable for AgentsOverviewView {
|
||||
let layout = self.center_layout(area);
|
||||
let state = self.state();
|
||||
if state.editing_metadata() && !layout.search.is_empty() {
|
||||
let input = if state.renaming {
|
||||
let input = if state.rename_target.is_some() {
|
||||
&state.input
|
||||
} else {
|
||||
&state.search
|
||||
};
|
||||
let prefix = if state.renaming {
|
||||
let prefix = if state.rename_target.is_some() {
|
||||
"Rename › "
|
||||
} else {
|
||||
"Search › "
|
||||
@@ -128,6 +130,11 @@ impl Renderable for AgentsOverviewView {
|
||||
.map(ShortcutHint::display_label)
|
||||
.unwrap_or_default();
|
||||
let state = self.state();
|
||||
let filter_keys = if state.editing_metadata() {
|
||||
String::new()
|
||||
} else {
|
||||
self.center_filter_hint()
|
||||
};
|
||||
let grouping = match state.grouping {
|
||||
AgentsOverviewGrouping::Project => "Project",
|
||||
AgentsOverviewGrouping::Status => "Status",
|
||||
@@ -146,18 +153,31 @@ impl Renderable for AgentsOverviewView {
|
||||
inset(layout.header),
|
||||
buf,
|
||||
);
|
||||
let (needs_you, working, ready) = self.rows.iter().fold((0, 0, 0), |counts, row| {
|
||||
let (needs_you, working, ready) = counts;
|
||||
match row.group {
|
||||
AgentsOverviewGroup::NeedsYou => (needs_you + 1, working, ready),
|
||||
AgentsOverviewGroup::Working => (needs_you, working + 1, ready),
|
||||
AgentsOverviewGroup::Ready => (needs_you, working, ready + 1),
|
||||
AgentsOverviewGroup::Finished => counts,
|
||||
}
|
||||
});
|
||||
line(
|
||||
format!("{needs_you} need input {working} working {ready} ready").dim(),
|
||||
inset(row(layout.header, /*offset*/ 1, /*height*/ 1)),
|
||||
let labels = TASK_FILTERS
|
||||
.iter()
|
||||
.map(|(label, group)| {
|
||||
let count = self
|
||||
.rows
|
||||
.iter()
|
||||
.filter(|row| group.is_none_or(|group| group == row.group))
|
||||
.count();
|
||||
format!("{label} {count}")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut tabs = inset(row(layout.header, /*offset*/ 1, /*height*/ 1));
|
||||
let filter_hint = hint_line(&[(filter_keys, "filter".into())]);
|
||||
if tabs.width >= 80 && filter_hint.width() > 0 {
|
||||
let hint_width = filter_hint.width() as u16;
|
||||
filter_hint.render(
|
||||
Rect::new(tabs.right() - hint_width, tabs.y, hint_width, tabs.height),
|
||||
buf,
|
||||
);
|
||||
tabs.width = tabs.width.saturating_sub(hint_width + 2);
|
||||
}
|
||||
render_filled_tab_bar(
|
||||
&labels.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||
state.status_filter,
|
||||
tabs,
|
||||
buf,
|
||||
);
|
||||
line(
|
||||
@@ -168,7 +188,7 @@ impl Renderable for AgentsOverviewView {
|
||||
buf,
|
||||
);
|
||||
if state.editing_metadata() {
|
||||
let (label, input) = if state.renaming {
|
||||
let (label, input) = if state.rename_target.is_some() {
|
||||
("Rename › ", &state.input)
|
||||
} else {
|
||||
("Search › ", &state.search)
|
||||
|
||||
@@ -164,3 +164,126 @@ async fn live_center_metadata_clips_at_grapheme_boundaries() {
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_center_rename_retains_target_when_status_leaves_filter() -> Result<()> {
|
||||
let mut app = make_test_app().await;
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
app.app_event_tx = AppEventSender::new(tx);
|
||||
let id = ThreadId::new();
|
||||
let other = ThreadId::new();
|
||||
let mut threads = vec![
|
||||
overview_thread(
|
||||
id,
|
||||
/*parent_thread_id*/ None,
|
||||
"Target",
|
||||
ThreadStatus::Idle,
|
||||
),
|
||||
overview_thread(
|
||||
other,
|
||||
/*parent_thread_id*/ None,
|
||||
"Other",
|
||||
ThreadStatus::Idle,
|
||||
),
|
||||
];
|
||||
for key in [KeyCode::Esc, KeyCode::Enter] {
|
||||
threads[0].status = ThreadStatus::Idle;
|
||||
let mut view = app.agents_overview_view(threads.clone(), Some(id));
|
||||
// Reuse the Ready tab after the first iteration.
|
||||
if key == KeyCode::Esc {
|
||||
for _ in 0..3 {
|
||||
view.handle_key_event(KeyCode::Tab.into());
|
||||
}
|
||||
}
|
||||
view.handle_key_event(KeyCode::Char('r').into());
|
||||
threads[0].status = ThreadStatus::SystemError;
|
||||
view = app.agents_overview_view(threads.clone(), Some(id));
|
||||
view.handle_key_event(KeyCode::Char('!').into());
|
||||
view.handle_key_event(key.into());
|
||||
assert_eq!(view.rows[view.selected_index().unwrap()].thread_id, other);
|
||||
if key == KeyCode::Enter {
|
||||
let rename = rx.try_recv().unwrap();
|
||||
assert!(
|
||||
matches!(&rename, AppEvent::RenameAgentsOverviewThread { thread_id, name } if *thread_id == id && name == "Target!")
|
||||
);
|
||||
app.agents_overview.threads = threads
|
||||
.iter()
|
||||
.map(|thread| {
|
||||
(
|
||||
ThreadId::from_string(&thread.id).unwrap(),
|
||||
Some(thread.clone()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
app.agents_overview.visible_thread_ids = view.thread_ids();
|
||||
app.chat_widget.show_bottom_pane_view(Box::new(view));
|
||||
let mut server = crate::start_embedded_app_server_for_picker(&app.config).await?;
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
// Synthetic IDs have no rollout: the server rejects the rename.
|
||||
Box::pin(app.handle_event(&mut tui, &mut server, rename)).await?;
|
||||
let mut retry = app.agents_overview_view(threads.clone(), Some(other));
|
||||
assert_eq!(retry.rows[retry.selected_index().unwrap()].thread_id, id);
|
||||
retry.handle_key_event(KeyCode::Enter.into());
|
||||
assert!(std::iter::from_fn(|| rx.try_recv().ok()).any(|event| matches!(event, AppEvent::RenameAgentsOverviewThread { thread_id, name } if thread_id == id && name == "Target!")));
|
||||
server.shutdown().await?;
|
||||
} else {
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_center_navigation_and_complete_hints() {
|
||||
let app = make_test_app().await;
|
||||
let ready = ThreadId::from_u128(/*value*/ 42);
|
||||
let needs_you = ThreadId::from_u128(/*value*/ 43);
|
||||
let unloaded = ThreadId::from_u128(/*value*/ 44);
|
||||
let mut view = app.agents_overview_view(
|
||||
vec![
|
||||
overview_thread(
|
||||
ready,
|
||||
/*parent_thread_id*/ None,
|
||||
"Ready task",
|
||||
ThreadStatus::Idle,
|
||||
),
|
||||
overview_thread(
|
||||
needs_you,
|
||||
/*parent_thread_id*/ None,
|
||||
"Needs input",
|
||||
ThreadStatus::SystemError,
|
||||
),
|
||||
overview_thread(
|
||||
unloaded,
|
||||
/*parent_thread_id*/ None,
|
||||
"Unloaded task",
|
||||
ThreadStatus::NotLoaded,
|
||||
),
|
||||
],
|
||||
Some(ready),
|
||||
);
|
||||
let selected = |view: &AgentsOverviewView| view.rows[view.selected_index().unwrap()].thread_id;
|
||||
view.handle_key_event(KeyCode::Tab.into());
|
||||
assert_eq!(selected(&view), needs_you);
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT));
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT));
|
||||
assert_eq!(selected(&view), unloaded);
|
||||
insta::assert_snapshot!(
|
||||
"live_center_status_filter",
|
||||
screen(&view, /*width*/ 40, /*height*/ 12)
|
||||
);
|
||||
view.handle_key_event(KeyCode::Tab.into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_center_fixed_shortcuts_yield_to_configured_actions() {
|
||||
let mut app = make_test_app().await;
|
||||
let config: TuiKeymap = toml::from_str("[agents]\nresume = 'tab'").unwrap();
|
||||
app.keymap = RuntimeKeymap::from_config(&config).unwrap();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
app.app_event_tx = AppEventSender::new(tx);
|
||||
let mut view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
||||
view.handle_key_event(KeyCode::Tab.into());
|
||||
assert!(matches!(rx.try_recv(), Ok(AppEvent::OpenResumePicker)));
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
@@ -211,9 +211,16 @@ impl App {
|
||||
};
|
||||
let selected_thread_id = self
|
||||
.agents_overview
|
||||
.visible_thread_ids
|
||||
.get(selected)
|
||||
.copied();
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.rename_target
|
||||
.or_else(|| {
|
||||
self.agents_overview
|
||||
.visible_thread_ids
|
||||
.get(selected)
|
||||
.copied()
|
||||
});
|
||||
let threads = self
|
||||
.agents_overview
|
||||
.threads
|
||||
@@ -226,7 +233,7 @@ impl App {
|
||||
if selected_thread_id
|
||||
.is_some_and(|thread_id| !self.agents_overview.visible_thread_ids.contains(&thread_id))
|
||||
&& let Ok(mut state) = self.agents_overview.view_state.lock()
|
||||
&& state.renaming
|
||||
&& state.rename_target.is_some()
|
||||
{
|
||||
self.chat_widget.add_info_message(
|
||||
format!(
|
||||
@@ -235,7 +242,7 @@ impl App {
|
||||
),
|
||||
/*hint*/ None,
|
||||
);
|
||||
state.renaming = false;
|
||||
state.rename_target = None;
|
||||
state.input.clear();
|
||||
}
|
||||
self.chat_widget
|
||||
|
||||
@@ -269,7 +269,10 @@ async fn hiding_rename_target_does_not_transfer_draft_to_neighbor() -> Result<()
|
||||
Box::pin(app.handle_event(&mut tui, &mut app_server, hide)).await?;
|
||||
{
|
||||
let state = app.agents_overview.view_state.lock().unwrap();
|
||||
assert_eq!((state.renaming, state.input.as_str()), (false, ""));
|
||||
assert_eq!(
|
||||
(state.rename_target.is_some(), state.input.as_str()),
|
||||
(false, "")
|
||||
);
|
||||
}
|
||||
app.chat_widget.handle_key_event(KeyCode::Enter.into());
|
||||
assert!(
|
||||
|
||||
@@ -23,7 +23,11 @@ impl AgentsOverviewView {
|
||||
.primary_hint(ListAction::Accept)
|
||||
.map(crate::key_hint::ShortcutHint::display_label)
|
||||
.unwrap_or_default(),
|
||||
if state.renaming { "rename" } else { "open" }
|
||||
if state.rename_target.is_some() {
|
||||
"rename"
|
||||
} else {
|
||||
"open"
|
||||
}
|
||||
))
|
||||
} else if state.connection_notice.is_some() {
|
||||
Some("ctrl+c quit · actions paused until the list is refreshed".into())
|
||||
|
||||
@@ -215,7 +215,14 @@ async fn overview_right_preserves_editors_and_offline_state() {
|
||||
);
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
|
||||
view.handle_key_event(KeyCode::Right.into());
|
||||
assert!(app.agents_overview.view_state.lock().unwrap().renaming);
|
||||
assert!(
|
||||
app.agents_overview
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.rename_target
|
||||
.is_some()
|
||||
);
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
app.agents_overview
|
||||
.view_state
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
//! Dashboard for inspecting and managing the TUI's retained daemon tasks.
|
||||
//! Search and rename input survive metadata refreshes; root Escape never exits.
|
||||
//! Search, rename, status filters and selection survive metadata refreshes.
|
||||
|
||||
#[path = "agent_center/mod.rs"]
|
||||
pub(super) mod command_center;
|
||||
|
||||
#[path = "agents_overview_grouping.rs"]
|
||||
mod grouping;
|
||||
#[path = "agents_overview_render.rs"]
|
||||
mod render;
|
||||
|
||||
#[path = "agents_overview_grouping.rs"]
|
||||
mod grouping;
|
||||
|
||||
pub(super) use grouping::AgentsOverviewGrouping;
|
||||
use grouping::model_name;
|
||||
|
||||
@@ -36,6 +37,7 @@ use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_protocol::ThreadId;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::layout::Layout;
|
||||
@@ -134,6 +136,7 @@ impl AgentsOverviewProjectGroup {
|
||||
pub(super) struct AgentsOverviewViewState {
|
||||
scroll: usize,
|
||||
page_height: usize,
|
||||
status_filter: usize,
|
||||
pub(super) input: String,
|
||||
pub(super) key_chord_hint: Option<Vec<(String, String)>>,
|
||||
pub(super) creating_worktree: bool,
|
||||
@@ -144,14 +147,14 @@ pub(super) struct AgentsOverviewViewState {
|
||||
search: String,
|
||||
searching: bool,
|
||||
pub(super) grouping: AgentsOverviewGrouping,
|
||||
pub(super) renaming: bool,
|
||||
pub(super) rename_target: Option<ThreadId>,
|
||||
// The picker can finish this retained view when it selects the already active session.
|
||||
pub(super) completion: Option<ViewCompletion>,
|
||||
}
|
||||
|
||||
impl AgentsOverviewViewState {
|
||||
pub(super) fn editing_metadata(&self) -> bool {
|
||||
self.searching || self.renaming
|
||||
self.searching || self.rename_target.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +167,7 @@ pub(super) struct AgentsOverviewView {
|
||||
app_event_tx: AppEventSender,
|
||||
keymap: ListKeymap,
|
||||
agents_keymap: AgentsKeymap,
|
||||
center_shortcut_keys: Vec<crate::key_hint::KeyBinding>,
|
||||
worktrees_enabled: bool,
|
||||
}
|
||||
|
||||
@@ -177,7 +181,11 @@ impl AgentsOverviewView {
|
||||
keymap: RuntimeKeymap,
|
||||
state: Arc<Mutex<AgentsOverviewViewState>>,
|
||||
) -> Self {
|
||||
let selected = selected_thread_id
|
||||
let selected = state
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.rename_target
|
||||
.or(selected_thread_id)
|
||||
.and_then(|thread_id| rows.iter().position(|row| row.thread_id == thread_id))
|
||||
.or_else(|| rows.iter().position(|row| row.is_current))
|
||||
.unwrap_or(0);
|
||||
@@ -185,6 +193,26 @@ impl AgentsOverviewView {
|
||||
.iter()
|
||||
.map(|row| AgentsOverviewProjectGroup::for_thread(&row.thread, worktrees_enabled))
|
||||
.collect();
|
||||
let center_shortcut_keys = crate::keymap::keymap_action_ids()
|
||||
.filter(|action| matches!(action.context, KeymapContext::List | KeymapContext::Agents))
|
||||
.flat_map(|action| {
|
||||
crate::keymap::bindings_for_action(
|
||||
&keymap,
|
||||
action.context.config_name(),
|
||||
action.action,
|
||||
)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.copied()
|
||||
})
|
||||
.chain(keymap.chords.bindings.iter().filter_map(|binding| {
|
||||
matches!(
|
||||
binding.action.context,
|
||||
KeymapContext::List | KeymapContext::Agents
|
||||
)
|
||||
.then_some(binding.chord.prefix)
|
||||
}))
|
||||
.collect();
|
||||
let mut view = Self {
|
||||
use_theme_colors,
|
||||
rows,
|
||||
@@ -194,13 +222,11 @@ impl AgentsOverviewView {
|
||||
app_event_tx,
|
||||
keymap: keymap.list,
|
||||
agents_keymap: keymap.agents,
|
||||
center_shortcut_keys,
|
||||
worktrees_enabled,
|
||||
};
|
||||
view.state().completion = None;
|
||||
let visible = view.visible_indices();
|
||||
if !visible.contains(&view.selected) {
|
||||
view.selected = visible.first().copied().unwrap_or(usize::MAX);
|
||||
}
|
||||
view.reconcile_command_center_selection();
|
||||
view
|
||||
}
|
||||
|
||||
@@ -229,6 +255,7 @@ impl AgentsOverviewView {
|
||||
fn visible_indices(&self) -> Vec<usize> {
|
||||
let state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let search = state.search.to_lowercase();
|
||||
let (_, status_group) = command_center::TASK_FILTERS[state.status_filter];
|
||||
let mut visible = self
|
||||
.rows
|
||||
.iter()
|
||||
@@ -241,7 +268,10 @@ impl AgentsOverviewView {
|
||||
row.thread.cwd.display(),
|
||||
)
|
||||
.to_lowercase();
|
||||
(search.is_empty() || searchable.contains(&search)).then_some(index)
|
||||
((search.is_empty() || searchable.contains(&search))
|
||||
&& (state.rename_target == Some(row.thread_id)
|
||||
|| status_group.is_none_or(|group| group == row.group)))
|
||||
.then_some(index)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
match state.grouping {
|
||||
@@ -263,7 +293,7 @@ impl AgentsOverviewView {
|
||||
}
|
||||
|
||||
fn move_selection(&mut self, forward: bool) {
|
||||
if self.state().renaming {
|
||||
if self.state().rename_target.is_some() {
|
||||
return;
|
||||
}
|
||||
let visible = self.visible_indices();
|
||||
@@ -283,7 +313,7 @@ impl AgentsOverviewView {
|
||||
|
||||
fn activate(&mut self) {
|
||||
let input = self.state().input.clone();
|
||||
if self.state().renaming && !input.trim().is_empty() {
|
||||
if self.state().rename_target.is_some() && !input.trim().is_empty() {
|
||||
if let Some(row) = self.selected_row() {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::RenameAgentsOverviewThread {
|
||||
@@ -291,9 +321,13 @@ impl AgentsOverviewView {
|
||||
name: input.trim().to_string(),
|
||||
});
|
||||
}
|
||||
self.state().renaming = false;
|
||||
self.state().rename_target = None;
|
||||
self.state().input.clear();
|
||||
} else if let Some(row) = self.selected_row().filter(|_| !self.state().renaming) {
|
||||
self.reconcile_command_center_selection();
|
||||
} else if let Some(row) = self
|
||||
.selected_row()
|
||||
.filter(|_| self.state().rename_target.is_none())
|
||||
{
|
||||
self.app_event_tx
|
||||
.send(AppEvent::SelectAgentsOverviewThread {
|
||||
thread_id: row.thread_id,
|
||||
@@ -453,17 +487,11 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
let mut state = self.state();
|
||||
if state.editing_metadata() {
|
||||
state.searching = false;
|
||||
state.renaming = false;
|
||||
state.rename_target = None;
|
||||
state.search.clear();
|
||||
state.input.clear();
|
||||
drop(state);
|
||||
if self.selected >= self.rows.len() {
|
||||
self.selected = self
|
||||
.visible_indices()
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(usize::MAX);
|
||||
}
|
||||
self.reconcile_command_center_selection();
|
||||
return CancellationEvent::Handled;
|
||||
}
|
||||
CancellationEvent::NotHandled
|
||||
@@ -478,10 +506,18 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
false
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) {
|
||||
fn handle_key_event(&mut self, mut key: KeyEvent) {
|
||||
// Terminals encode Shift-Tab as either BackTab or Tab with the shift modifier.
|
||||
if key.code == KeyCode::BackTab {
|
||||
key.code = KeyCode::Tab;
|
||||
key.modifiers.insert(KeyModifiers::SHIFT);
|
||||
}
|
||||
if key.kind == crossterm::event::KeyEventKind::Release {
|
||||
return;
|
||||
}
|
||||
if self.command_center_key(key) {
|
||||
return;
|
||||
}
|
||||
if key.code == KeyCode::Esc {
|
||||
self.on_ctrl_c();
|
||||
return;
|
||||
@@ -506,7 +542,7 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
|
||||
if self.agents_keymap.search.is_pressed(key) {
|
||||
let mut state = self.state();
|
||||
if !state.renaming {
|
||||
if state.rename_target.is_none() {
|
||||
state.searching = !state.searching;
|
||||
if !state.searching {
|
||||
state.search.clear();
|
||||
@@ -522,7 +558,7 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
Some(ListAction::MoveUp) => self.move_selection(/*forward*/ false),
|
||||
Some(ListAction::MoveDown) => self.move_selection(/*forward*/ true),
|
||||
Some(action @ (ListAction::PageUp | ListAction::PageDown)) => {
|
||||
self.page_selection(action);
|
||||
self.page_selection(action)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -563,7 +599,7 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
state.input = row.thread.name.clone().unwrap_or_default();
|
||||
state.search.clear();
|
||||
state.searching = false;
|
||||
state.renaming = true;
|
||||
state.rename_target = Some(row.thread_id);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -586,16 +622,9 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
if self.agents_keymap.hide.is_pressed(key) {
|
||||
if let Some(row) = self.selected_row() {
|
||||
let thread_id = row.thread_id;
|
||||
let visible = self.visible_indices();
|
||||
if !self.state().renaming
|
||||
&& let Some(position) = visible.iter().position(|index| *index == self.selected)
|
||||
&& let Some(next) = visible
|
||||
.get(position + 1)
|
||||
.or_else(|| visible.get(position.saturating_sub(1)))
|
||||
{
|
||||
// Preserve a neighboring row when hiding rebuilds the view.
|
||||
self.selected = *next;
|
||||
}
|
||||
// Keep an adjacent task selected when hiding rebuilds the view.
|
||||
let forward = self.visible_indices().last() != Some(&self.selected);
|
||||
self.move_selection(forward);
|
||||
self.app_event_tx
|
||||
.send(AppEvent::HideAgentsOverviewThread { thread_id });
|
||||
}
|
||||
@@ -613,7 +642,7 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
}
|
||||
|
||||
if let Some(action) = self.keymap.action_for(key) {
|
||||
if self.state().renaming
|
||||
if self.state().rename_target.is_some()
|
||||
&& matches!(action, ListAction::JumpTop | ListAction::JumpBottom)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -2536,8 +2536,9 @@ impl App {
|
||||
Err(error) => {
|
||||
if let Ok(mut state) = self.agents_overview.view_state.lock() {
|
||||
state.input = name;
|
||||
state.renaming = true;
|
||||
state.rename_target = Some(thread_id);
|
||||
}
|
||||
self.repaint_agents_overview();
|
||||
self.add_agents_overview_error(format!("Failed to rename task: {error}"));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_actions_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget,\n48).replace(&test_path_display(\"/tmp/project\"), \"/tmp/project\")"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 0 working 1 ready
|
||||
All 1 Needs you 0 Working 0 ›
|
||||
────────────────────────────────────────────
|
||||
Search tasks · f
|
||||
Tasks
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: normalize_agent_center_snapshot(&cached)
|
||||
---
|
||||
" Agent command center Group: Project g "
|
||||
" 0 need input 0 working 1 ready "
|
||||
" All 1 Needs you 0 Working 0 Ready 1 Inactive 0 tab/shift+tab filter "
|
||||
" ──────────────────────────────────────────────────────────────────────────────────────────── "
|
||||
" Search tasks · f │ Task details "
|
||||
" Tasks │ "
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: "normalize_agent_center_snapshot(render_bottom_popup(&app.chat_widget, 96))"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 0 working 1 ready
|
||||
All 1 Needs you 0 Working 0 Ready 1 Inactive 0 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f │ Task details
|
||||
Tasks │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: "normalize_agent_center_snapshot(render_bottom_popup(&app.chat_widget, 96))"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 0 working 1 ready
|
||||
All 1 Needs you 0 Working 0 Ready 1 Inactive 0 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f │ Task details
|
||||
Tasks │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget,\n100).replace(&test_path_display(\"/tmp/project\"),\n\"/tmp/project\").replace(\"fwd del\", \"del\")"
|
||||
---
|
||||
Agent command center Group: Model g
|
||||
0 need input 0 working 4 ready
|
||||
All 4 Needs you 0 Working 0 Ready 4 Inactive 0 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f │ Task details
|
||||
Tasks │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: "age.replace_all(&rendered, \" [age]\")"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 0 working 0 ready
|
||||
All 10 Needs you 0 Working 0 Ready 0 Inactive 10
|
||||
────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f
|
||||
Tasks Status Updated
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agent_center_tests.rs
|
||||
expression: "screen(&view, 160, 22)"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
2 need input 1 working 2 ready
|
||||
All 6 Needs you 2 Working 1 Ready 2 Inactive 1 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f │ Task details
|
||||
Tasks Status Updated │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agent_center_tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
Agent command center Group: Projec…
|
||||
0 need input 0 working 1 ready
|
||||
All 1 Needs you 0 Working 0 ›
|
||||
────────────────────────────────────
|
||||
Rename › 界 界 界 界 界 界 界 日 本 語 é 👩💻
|
||||
Tasks
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
---
|
||||
source: tui/src/app/agent_center_tests.rs
|
||||
expression: "screen(&view, 40, 12)"
|
||||
---
|
||||
Agent command center Group: Projec…
|
||||
‹ Working 0 Ready 1 Inactive 1
|
||||
────────────────────────────────────
|
||||
Search tasks · f
|
||||
↑ Tasks
|
||||
› ○ Unloaded task
|
||||
|
||||
↑↓ navigate o resume enter open
|
||||
n new w new worktree f search
|
||||
g group: project r rename x stop
|
||||
h hide a archive del delete
|
||||
ctrl+c quit
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget, 12)"
|
||||
---
|
||||
Agent c…
|
||||
0 need …
|
||||
All … ›
|
||||
────────
|
||||
Search …
|
||||
No tas…
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: "normalize_agent_center_snapshot(render_bottom_popup(&app.chat_widget, 80,))"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 0 working 0 ready
|
||||
All 0 Needs you 0 Working 0 Ready 0 Inactive 0
|
||||
────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f
|
||||
No tasks yet
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_usage_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{project} 2\"),\n&group).replace(&project, \"/tmp/project\").replace(\"fwd del\", \"del\")"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
1 need input 0 working 1 ready
|
||||
All 2 Needs you 1 Working 0 Ready 1 Inactive 0 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f │ Task details
|
||||
Tasks │
|
||||
|
||||
@@ -144,7 +144,12 @@ async fn reconnect_daemon_command_center_after_socket_replacement_without_a_conv
|
||||
app.agents_overview.visible_thread_ids = view.thread_ids();
|
||||
app.chat_widget.show_bottom_pane_view(Box::new(view));
|
||||
app.agents_overview.view_state.lock().unwrap().input = "Keep this task draft".into();
|
||||
app.agents_overview.view_state.lock().unwrap().renaming = true;
|
||||
app.agents_overview.view_state.lock().unwrap().rename_target =
|
||||
Some(if previous_thread.is_some() {
|
||||
vanished
|
||||
} else {
|
||||
selected
|
||||
});
|
||||
let draft = |app: &App| app.agents_overview.view_state.lock().unwrap().input.clone();
|
||||
let stale_request = Uuid::new_v4();
|
||||
app.agents_overview.request_id = Some(stale_request);
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/tests/disconnect_tests.rs
|
||||
expression: searching
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 0 working 0 ready
|
||||
All 0 Needs you 0 Working 0 Ready 0 Inactive 0
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search › search query! │
|
||||
Reconnecting… │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/tests/navigation_reconnect_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget, 100)"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 1 working 1 ready
|
||||
All 2 Needs you 0 Working 1 Ready 1 Inactive 0
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Rename › Keep this task draft! │ Task details
|
||||
Tasks │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/tests/navigation_reconnect_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget, 100)"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 1 working 1 ready
|
||||
All 2 Needs you 0 Working 1 Ready 1 Inactive 0
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Rename › Keep this task draft! │ Task details
|
||||
Tasks │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/tests/navigation_reconnect_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget, 100)"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 1 working 1 ready
|
||||
All 2 Needs you 0 Working 1 Ready 1 Inactive 0 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f │ Task details
|
||||
Tasks │
|
||||
|
||||
@@ -200,6 +200,7 @@ pub(crate) use selection_popup_common::menu_surface_padding_height;
|
||||
pub(crate) use selection_popup_common::render_menu_surface;
|
||||
mod selection_row_layout;
|
||||
mod selection_tabs;
|
||||
pub(crate) use selection_tabs::render_filled_tab_bar;
|
||||
mod startup;
|
||||
mod textarea;
|
||||
pub(crate) use textarea::TextArea;
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{project} 3\"),\n&normalized_project_group).replace(&project, \"/tmp/project\")"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
1 need input 1 working 0 ready
|
||||
All 3 Needs you 1 Working 1 Ready 0 Inactive 1 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f │ Task details
|
||||
Tasks │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{project} 2\"),\n&normalized_group).replace(&project, \"/tmp/project\")"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
1 need input 0 working 0 ready
|
||||
All 2 Needs you 1 Working 0 Ready 0 Inactive 1 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f │ Task details
|
||||
Tasks │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 0 working 0 ready
|
||||
All 0 Needs you 0 Working 0 ›
|
||||
────────────────────────────────────────────
|
||||
Search tasks · f
|
||||
No tasks yet
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{project} 2\"),\n&normalized_group).replace(&project, \"/tmp/project\")"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
1 need input 0 working 0 ready
|
||||
All 2 Needs you 1 Working 0 Ready 0 Inactive 1 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f │ Task details
|
||||
Tasks │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{project} 1\"),\n&normalized_group).replace(&project, \"/tmp/project\")"
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 1 working 0 ready
|
||||
All 1 Needs you 0 Working 1 Ready 0 Inactive 0 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f │ Task details
|
||||
Tasks │
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ source: tui/src/app/agents_overview_tests.rs
|
||||
expression: normalize_agent_center_snapshot(grouping)
|
||||
---
|
||||
Agent command center Group: Project g
|
||||
0 need input 0 working 4 ready
|
||||
All 4 Needs you 0 Working 0 Ready 4 Inactive 0 tab/shift+tab filter
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Search tasks · f
|
||||
Tasks Status Updated
|
||||
|
||||
Reference in New Issue
Block a user