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:
Eric Traut
2026-09-20 16:43:59 +00:00
committed by copyberry
parent 569888ac65
commit eef77aa211
36 changed files with 393 additions and 86 deletions
@@ -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
}
}
+21 -1
View File
@@ -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;
+35 -15
View File
@@ -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)
+123
View File
@@ -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());
}
+12 -5
View File
@@ -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
+67 -38
View File
@@ -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;
+2 -1
View File
@@ -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}"));
}
}
@@ -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
@@ -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 │ "
@@ -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 │
@@ -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 │
@@ -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 │
@@ -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
@@ -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 │
@@ -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
@@ -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
@@ -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…
@@ -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
@@ -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);
@@ -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… │
@@ -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 │
@@ -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 │
@@ -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 │
+1
View File
@@ -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;
@@ -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 │
@@ -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 │
@@ -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
@@ -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 │
@@ -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 │
@@ -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