Separate directory and repository scopes in the TUI (#56308)

## Why

The `Cwd` session filter included sessions from linked worktrees, leaving no way to restrict the picker to the current directory. Project grouping also combined tasks across linked worktrees.

## What changed

- Keep `Cwd` filtering and directory-scoped `resume --last` and `fork --last` lookups limited to the current directory.
- Add an explicit `Repo` picker filter when multiple local worktrees are available, including sessions from corresponding directories across linked worktrees. Cycle between `Cwd`, `Repo`, and `All` in either direction.
- Add `Directory` grouping to the agent command center alongside `Project`, `Status`, and `Model`. Keep this choice in memory without writing a configuration value older clients cannot read.

## Testing

Extend coverage for directory and repository filtering, pagination scope, linked-worktree grouping, directory aliases, grouping persistence, and adjacent selection after task removal. Update picker and command center snapshots.

GitOrigin-RevId: 59603c9c5136db43124102b3277d1d1dab36bb87
This commit is contained in:
Benjamin Carlsson
2026-10-23 00:29:14 +00:00
committed by copyberry
parent 75a4146411
commit 334527705c
18 changed files with 495 additions and 141 deletions
@@ -137,6 +137,7 @@ impl Renderable for AgentsOverviewView {
};
let grouping = match state.grouping {
AgentsOverviewGrouping::Project => "Project",
AgentsOverviewGrouping::Directory => "Directory",
AgentsOverviewGrouping::Status => "Status",
AgentsOverviewGrouping::Model => "Model",
};
+8 -2
View File
@@ -150,6 +150,10 @@ impl AgentsOverviewView {
/*max_width*/ None,
)
}
AgentsOverviewGrouping::Directory => format_directory_display(
self.rows[index].thread.cwd.as_path(),
/*max_width*/ None,
),
AgentsOverviewGrouping::Status => task.group.label().to_owned(),
AgentsOverviewGrouping::Model => model_name(&task.thread).to_owned(),
}
@@ -168,8 +172,10 @@ impl AgentsOverviewView {
} else {
format!("{count} of {total}")
};
let group = if state.grouping == AgentsOverviewGrouping::Project
&& !self.is_pinned(index)
let group = if matches!(
state.grouping,
AgentsOverviewGrouping::Project | AgentsOverviewGrouping::Directory
) && !self.is_pinned(index)
{
crate::text_formatting::center_truncate_path(
&group,
@@ -238,10 +238,20 @@ async fn archiving_selects_the_next_displayed_task() -> Result<()> {
let expected = match (grouping, filtered) {
(AgentsOverviewGrouping::Status, false) => vec![3, 2, 1, 4],
(AgentsOverviewGrouping::Status, true) => vec![3, 1, 4],
(AgentsOverviewGrouping::Project | AgentsOverviewGrouping::Model, false) => {
(
AgentsOverviewGrouping::Project
| AgentsOverviewGrouping::Directory
| AgentsOverviewGrouping::Model,
false,
) => {
vec![3, 4, 2, 1]
}
(AgentsOverviewGrouping::Project | AgentsOverviewGrouping::Model, true) => {
(
AgentsOverviewGrouping::Project
| AgentsOverviewGrouping::Directory
| AgentsOverviewGrouping::Model,
true,
) => {
vec![3, 4, 1]
}
};
@@ -311,6 +321,7 @@ async fn archiving_selects_the_next_displayed_task() -> Result<()> {
async fn external_removals_preserve_adjacent_selection() {
for grouping in [
AgentsOverviewGrouping::Project,
AgentsOverviewGrouping::Directory,
AgentsOverviewGrouping::Status,
AgentsOverviewGrouping::Model,
] {
@@ -362,7 +373,9 @@ async fn external_removals_preserve_adjacent_selection() {
);
let expected = match grouping {
AgentsOverviewGrouping::Status => [ids[0], ids[3]],
AgentsOverviewGrouping::Project | AgentsOverviewGrouping::Model => [ids[3], ids[0]],
AgentsOverviewGrouping::Project
| AgentsOverviewGrouping::Directory
| AgentsOverviewGrouping::Model => [ids[3], ids[0]],
};
let archive = ServerNotification::ThreadArchived(ThreadArchivedNotification {
thread_id: ids[2].to_string(),
@@ -438,6 +451,7 @@ async fn hiding_tasks_keeps_selection_adjacent_in_display_order() -> Result<()>
let mut selections = Vec::new();
for grouping in [
AgentsOverviewGrouping::Project,
AgentsOverviewGrouping::Directory,
AgentsOverviewGrouping::Status,
AgentsOverviewGrouping::Model,
] {
@@ -470,10 +484,20 @@ async fn hiding_tasks_keeps_selection_adjacent_in_display_order() -> Result<()>
// The preceding filtered case retains Other 2; rediscovered tasks append.
(AgentsOverviewGrouping::Status, false) => vec![3, 1, 4, 2],
(AgentsOverviewGrouping::Status, true) => vec![3, 1, 4],
(AgentsOverviewGrouping::Project | AgentsOverviewGrouping::Model, false) => {
(
AgentsOverviewGrouping::Project
| AgentsOverviewGrouping::Directory
| AgentsOverviewGrouping::Model,
false,
) => {
vec![3, 4, 2, 1]
}
(AgentsOverviewGrouping::Project | AgentsOverviewGrouping::Model, true) => {
(
AgentsOverviewGrouping::Project
| AgentsOverviewGrouping::Directory
| AgentsOverviewGrouping::Model,
true,
) => {
vec![3, 4, 1]
}
};
@@ -1,14 +1,43 @@
//! Command Center grouping, including restoration and client-local preference persistence.
//! The view owns the live choice; ordered app events save it through the existing config writer.
//! Directory is view-local so selecting it never writes a value that older clients reject.
use super::super::App;
use super::super::agents_overview::AgentsOverviewState;
use super::AgentsOverviewGrouping;
use super::AgentsOverviewView;
use crate::legacy_core::config::edit::ConfigEdit;
use crate::legacy_core::config::edit::ConfigEditsBuilder;
use codex_app_server_protocol::Thread;
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub(in crate::app) enum AgentsOverviewGrouping {
#[default]
Project,
Directory,
Status,
Model,
}
impl From<codex_config::types::AgentsOverviewGrouping> for AgentsOverviewGrouping {
fn from(grouping: codex_config::types::AgentsOverviewGrouping) -> Self {
match grouping {
codex_config::types::AgentsOverviewGrouping::Project => Self::Project,
codex_config::types::AgentsOverviewGrouping::Status => Self::Status,
codex_config::types::AgentsOverviewGrouping::Model => Self::Model,
}
}
}
impl AgentsOverviewGrouping {
pub(super) fn persisted(self) -> Option<codex_config::types::AgentsOverviewGrouping> {
match self {
Self::Project => Some(codex_config::types::AgentsOverviewGrouping::Project),
Self::Directory => None,
Self::Status => Some(codex_config::types::AgentsOverviewGrouping::Status),
Self::Model => Some(codex_config::types::AgentsOverviewGrouping::Model),
}
}
}
pub(super) fn model_name(thread: &Thread) -> &str {
thread
.model
@@ -56,6 +85,12 @@ impl AgentsOverviewView {
std::cmp::Reverse(self.rows[*index].thread.updated_at),
)
}),
AgentsOverviewGrouping::Directory => visible.sort_by_key(|index| {
(
&self.directory_group_keys[*index],
std::cmp::Reverse(self.rows[*index].thread.updated_at),
)
}),
AgentsOverviewGrouping::Status => {}
AgentsOverviewGrouping::Model => visible.sort_by_key(|index| {
(
@@ -81,6 +116,9 @@ impl AgentsOverviewView {
AgentsOverviewGrouping::Project => {
self.project_groups[left].key == self.project_groups[right].key
}
AgentsOverviewGrouping::Directory => {
self.directory_group_keys[left] == self.directory_group_keys[right]
}
AgentsOverviewGrouping::Status => self.rows[left].group == self.rows[right].group,
AgentsOverviewGrouping::Model => {
model_name(&self.rows[left].thread) == model_name(&self.rows[right].thread)
@@ -96,7 +134,7 @@ impl AgentsOverviewState {
.view_state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.grouping = grouping;
.grouping = grouping.into();
state
}
}
@@ -104,7 +142,7 @@ impl AgentsOverviewState {
impl App {
pub(in super::super) async fn persist_agents_overview_grouping(
&mut self,
grouping: AgentsOverviewGrouping,
grouping: codex_config::types::AgentsOverviewGrouping,
) {
self.local_settings.tui.agents_overview_grouping = grouping;
let edit = ConfigEdit::SetPath {
+158 -46
View File
@@ -9,6 +9,7 @@ use super::super::agents_overview_view::AgentsOverviewGrouping;
use super::*;
use crate::app::tests::make_test_app_with_channels;
use crate::chatwidget::tests::helpers::normalize_agent_center_snapshot;
use codex_config::types::AgentsOverviewGrouping as PersistedAgentsOverviewGrouping;
#[tokio::test]
async fn overview_worktree_creation_busy_state() {
@@ -1132,16 +1133,18 @@ async fn agents_overview_reasoning_uses_existing_events_and_expires_with_attachm
}
#[tokio::test]
async fn worktrees_overview_groups_linked_checkouts() {
async fn worktrees_overview_groups_by_project_or_directory() {
let mut app = make_test_app().await;
let root = tempfile::tempdir().unwrap();
let primary = root.path().join("primary");
let linked = root.path().join("linked");
let linked_subdir = linked.join("subdir/child");
let primary_subdir = primary.join("shared/child");
let linked_subdir = linked.join("shared/child");
let nested = primary.join("subdir");
let nested_child = nested.join("child");
let admin = primary.join(".git/worktrees/linked");
std::fs::create_dir_all(&admin).unwrap();
std::fs::create_dir_all(&primary_subdir).unwrap();
std::fs::create_dir_all(&linked_subdir).unwrap();
std::fs::create_dir_all(nested.join(".git")).unwrap();
std::fs::create_dir_all(&nested_child).unwrap();
@@ -1155,48 +1158,131 @@ async fn worktrees_overview_groups_linked_checkouts() {
.unwrap();
std::fs::write(linked.join(".git"), format!("gitdir: {}", admin.display())).unwrap();
let primary = dunce::canonicalize(primary).unwrap();
let threads = [primary.clone(), linked, linked_subdir, nested_child]
.into_iter()
.map(|cwd| {
let mut thread = overview_thread(
ThreadId::new(),
/*parent_thread_id*/ None,
"Task",
ThreadStatus::Idle,
);
thread.cwd = AbsolutePathBuf::from_absolute_path(cwd).unwrap();
thread
})
.collect::<Vec<_>>();
let headings = [primary.join(""), primary.join("subdir").join("child")].map(|path| {
crate::text_formatting::center_truncate_path(
&crate::status::format_directory_display(&path, /*max_width*/ None),
/*max_width*/ 64,
let primary_subdir = dunce::canonicalize(primary_subdir).unwrap();
let linked = dunce::canonicalize(linked).unwrap();
let linked_subdir = dunce::canonicalize(linked_subdir).unwrap();
let nested_child = dunce::canonicalize(nested_child).unwrap();
let threads = [
("Primary README", primary.clone()),
("Primary tree", primary.clone()),
("Linked README", linked.clone()),
("Linked tree", linked.clone()),
("Primary child", primary_subdir.clone()),
("Linked child", linked_subdir.clone()),
("Nested repository", nested_child.clone()),
]
.into_iter()
.map(|(title, cwd)| {
let mut thread = overview_thread(
ThreadId::new(),
/*parent_thread_id*/ None,
title,
ThreadStatus::Idle,
);
thread.cwd = AbsolutePathBuf::from_absolute_path(cwd).unwrap();
thread
})
.collect::<Vec<_>>();
let directory_headings = [
(&primary, "/tmp/worktree-root/primary"),
(&linked, "/tmp/worktree-root/linked"),
(&primary_subdir, "/tmp/worktree-root/primary/shared/child"),
(&linked_subdir, "/tmp/worktree-root/linked/shared/child"),
(&nested_child, "/tmp/worktree-root/primary/subdir/child"),
]
.map(|(path, stable)| {
(
crate::text_formatting::center_truncate_path(
&crate::status::format_directory_display(path, /*max_width*/ None),
/*max_width*/ 64,
),
stable,
)
});
let display_heading = &headings[0];
let grouped_heading = format!("{display_heading} 2");
let width = 160;
let view = app.agents_overview_view(threads, /*selected_thread_id*/ None);
app.chat_widget.show_bottom_pane_view(Box::new(view));
let popup = render_bottom_popup(&app.chat_widget, width);
assert!(popup.contains(&grouped_heading), "{popup}");
let grouping = popup
let project_popup = render_bottom_popup(&app.chat_widget, width);
let project_root = primary.join("");
let project_headings = [
(&project_root, "/tmp/worktree-root/primary"),
(&primary_subdir, "/tmp/worktree-root/primary/shared/child"),
(&nested_child, "/tmp/worktree-root/primary/subdir/child"),
]
.map(|(path, stable)| {
(
crate::text_formatting::center_truncate_path(
&crate::status::format_directory_display(path, /*max_width*/ None),
/*max_width*/ 64,
),
stable,
)
});
assert!(
project_popup.contains(&format!("{} 4", project_headings[0].0)),
"{project_popup}"
);
assert!(
project_popup.contains(&format!("{} 2", project_headings[1].0)),
"{project_popup}"
);
let mut project_grouping = project_popup
.lines()
.map(|line| line.split('│').next().unwrap_or(line).trim_end())
.collect::<Vec<_>>()
.join("\n")
.replace(&headings[1], "/tmp/worktree-root/primary/subdir/child")
.replace(display_heading, "/tmp/worktree-root/primary")
.replace('\\', "/");
.join("\n");
for (heading, stable) in project_headings.iter().rev() {
project_grouping = project_grouping.replace(heading, stable);
}
let project_grouping = project_grouping.replace('\\', "/");
app.agents_overview.view_state.lock().unwrap().grouping = AgentsOverviewGrouping::Directory;
let directory_popup = render_bottom_popup(&app.chat_widget, width);
for (heading, _) in &directory_headings[..2] {
assert!(
directory_popup.contains(&format!("{heading} 2")),
"{directory_popup}"
);
}
let mut directory_grouping = directory_popup
.lines()
.map(|line| line.split('│').next().unwrap_or(line).trim_end())
.collect::<Vec<_>>()
.join("\n");
for (heading, stable) in directory_headings.iter().rev() {
directory_grouping = directory_grouping.replace(heading, stable);
}
let directory_grouping = directory_grouping.replace('\\', "/");
insta::with_settings!({snapshot_path => "../snapshots"}, {
insta::assert_snapshot!(
"agents_overview_worktree_grouping",
normalize_agent_center_snapshot(grouping)
normalize_agent_center_snapshot(project_grouping)
);
insta::assert_snapshot!(
"agents_overview_directory_grouping",
normalize_agent_center_snapshot(directory_grouping)
);
});
}
#[cfg(unix)]
#[test]
fn overview_directory_group_keys_normalize_aliases() {
use super::super::agents_overview_view::directory_group_key;
let root = tempfile::tempdir().unwrap();
let checkout = root.path().join("checkout");
let alias = root.path().join("alias");
std::fs::create_dir(&checkout).unwrap();
std::os::unix::fs::symlink(&checkout, &alias).unwrap();
assert_eq!(
directory_group_key(&checkout, /*worktrees_enabled*/ true),
directory_group_key(&alias, /*worktrees_enabled*/ true),
);
let worktrees_enabled = false;
assert_eq!(directory_group_key(&alias, worktrees_enabled), alias);
}
#[tokio::test]
async fn overview_model_grouping_shows_details_and_preserves_selection() {
let mut app = make_test_app().await;
@@ -1221,7 +1307,7 @@ async fn overview_model_grouping_shows_details_and_preserves_selection() {
.collect();
let selected = ThreadId::from_u128(/*value*/ 1);
let mut view = app.agents_overview_view(threads, Some(selected));
for _ in 0..2 {
for _ in 0..3 {
view.handle_key_event(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE));
}
assert_eq!(
@@ -2971,18 +3057,40 @@ async fn overview_grouping_persists_across_config_reloads() -> Result<()> {
app.local_settings.user_config_path = AbsolutePathBuf::try_from(config_path.clone())?;
let mut server = crate::start_embedded_app_server_for_picker(&app.config).await?;
let mut tui = crate::tui::test_support::make_test_tui()?;
for expected in [
AgentsOverviewGrouping::Status,
AgentsOverviewGrouping::Model,
AgentsOverviewGrouping::Project,
for (selected, persisted) in [
(
AgentsOverviewGrouping::Directory,
PersistedAgentsOverviewGrouping::Project,
),
(
AgentsOverviewGrouping::Status,
PersistedAgentsOverviewGrouping::Status,
),
(
AgentsOverviewGrouping::Model,
PersistedAgentsOverviewGrouping::Model,
),
(
AgentsOverviewGrouping::Project,
PersistedAgentsOverviewGrouping::Project,
),
] {
let mut view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
view.handle_key_event(KeyCode::Char('g').into());
let event = std::iter::from_fn(|| events.try_recv().ok())
.find(|event| matches!(event, AppEvent::PersistAgentsOverviewGrouping(_)))
.expect("grouping change emits persistence event");
Box::pin(app.handle_event(&mut tui, &mut server, event)).await?;
drop(view);
assert_eq!(
app.agents_overview.view_state.lock().unwrap().grouping,
selected
);
if selected == AgentsOverviewGrouping::Directory {
assert!(events.try_recv().is_err());
assert!(!std::fs::read_to_string(&config_path)?.contains("agents_overview_grouping"));
} else {
let event = std::iter::from_fn(|| events.try_recv().ok())
.find(|event| matches!(event, AppEvent::PersistAgentsOverviewGrouping(_)))
.expect("persisted grouping change emits persistence event");
Box::pin(app.handle_event(&mut tui, &mut server, event)).await?;
}
let mut loader = codex_config::LoaderOverrides::without_managed_config_for_tests();
loader.user_config_path = Some(AbsolutePathBuf::try_from(config_path.clone())?);
@@ -2992,7 +3100,7 @@ async fn overview_grouping_persists_across_config_reloads() -> Result<()> {
.build()
.await?;
app.local_settings = crate::local_settings::LocalSettings::from(&config);
assert_eq!(app.local_settings.tui.agents_overview_grouping, expected);
assert_eq!(app.local_settings.tui.agents_overview_grouping, persisted);
assert!(!app.local_settings.tui.animations);
}
assert!(!home.path().join("config.toml").exists());
@@ -3003,7 +3111,7 @@ async fn overview_grouping_persists_across_config_reloads() -> Result<()> {
Box::pin(app.handle_event(
&mut tui,
&mut server,
AppEvent::PersistAgentsOverviewGrouping(AgentsOverviewGrouping::Status),
AppEvent::PersistAgentsOverviewGrouping(PersistedAgentsOverviewGrouping::Status),
))
.await?;
assert_eq!(
@@ -3012,7 +3120,7 @@ async fn overview_grouping_persists_across_config_reloads() -> Result<()> {
);
assert_eq!(
app.local_settings.tui.agents_overview_grouping,
AgentsOverviewGrouping::Status
PersistedAgentsOverviewGrouping::Status
);
assert!(
render_bottom_popup(&app.chat_widget, /*width*/ 100)
@@ -3047,7 +3155,7 @@ async fn command_center_new_actions_use_selection_and_leave_metadata_text_alone(
));
view.handle_key_event(KeyCode::Char('p').into());
assert!(rx.try_recv().is_err());
for _ in 0..3 {
for grouping_index in 0..3 {
view.handle_key_event(KeyCode::Char('n').into());
assert!(
matches!(rx.try_recv(), Ok(AppEvent::NewAgentsOverviewSession { cwd: Some(cwd) }) if cwd == target.cwd)
@@ -3057,10 +3165,14 @@ async fn command_center_new_actions_use_selection_and_leave_metadata_text_alone(
matches!(rx.try_recv(), Ok(AppEvent::NewAgentsOverviewWorktree { cwd: Some(cwd) }) if cwd == target.cwd)
);
view.handle_key_event(KeyCode::Char('g').into());
assert!(matches!(
rx.try_recv(),
Ok(AppEvent::PersistAgentsOverviewGrouping(_))
));
if grouping_index == 0 {
assert!(rx.try_recv().is_err());
} else {
assert!(matches!(
rx.try_recv(),
Ok(AppEvent::PersistAgentsOverviewGrouping(_))
));
}
}
view.handle_key_event(KeyCode::Char('r').into());
for character in "nwogrxfhap".chars() {
+21 -4
View File
@@ -8,7 +8,7 @@ pub(super) mod command_center;
#[path = "agents_overview_grouping.rs"]
mod grouping;
pub(super) use codex_config::types::AgentsOverviewGrouping;
pub(super) use grouping::AgentsOverviewGrouping;
use grouping::model_name;
use super::agents_overview::AGENTS_OVERVIEW_VIEW_ID;
@@ -57,6 +57,7 @@ use ratatui::widgets::Clear;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
@@ -140,6 +141,13 @@ impl AgentsOverviewProjectGroup {
}
}
pub(super) fn directory_group_key(cwd: &Path, worktrees_enabled: bool) -> PathBuf {
worktrees_enabled
.then(|| dunce::canonicalize(cwd).ok())
.flatten()
.unwrap_or_else(|| cwd.to_path_buf())
}
#[derive(Default)]
pub(super) struct AgentsOverviewViewState {
row_order: HashMap<ThreadId, (AgentsOverviewGroup, usize)>,
@@ -187,6 +195,7 @@ pub(super) struct AgentsOverviewView {
use_theme_colors: bool,
pub(super) rows: Vec<AgentsOverviewRow>,
project_groups: Vec<AgentsOverviewProjectGroup>,
directory_group_keys: Vec<PathBuf>,
pub(super) pinned_thread_ranks: Option<HashMap<ThreadId, usize>>,
pub(super) pin_action_pending: bool,
selected: usize,
@@ -241,6 +250,10 @@ impl AgentsOverviewView {
.iter()
.map(|row| AgentsOverviewProjectGroup::for_thread(&row.thread, worktrees_enabled))
.collect();
let directory_group_keys = rows
.iter()
.map(|row| directory_group_key(row.thread.cwd.as_path(), worktrees_enabled))
.collect();
let center_shortcut_keys = crate::keymap::keymap_action_ids()
.filter(|action| matches!(action.context, KeymapContext::List | KeymapContext::Agents))
.flat_map(|action| {
@@ -277,6 +290,7 @@ impl AgentsOverviewView {
use_theme_colors,
rows,
project_groups,
directory_group_keys,
pinned_thread_ranks: None,
pin_action_pending: false,
selected,
@@ -681,12 +695,15 @@ impl BottomPaneView for AgentsOverviewView {
if self.agents_keymap.toggle_grouping.is_pressed(key) {
let mut state = self.state();
state.grouping = match state.grouping {
AgentsOverviewGrouping::Project => AgentsOverviewGrouping::Status,
AgentsOverviewGrouping::Project => AgentsOverviewGrouping::Directory,
AgentsOverviewGrouping::Directory => AgentsOverviewGrouping::Status,
AgentsOverviewGrouping::Status => AgentsOverviewGrouping::Model,
AgentsOverviewGrouping::Model => AgentsOverviewGrouping::Project,
};
self.app_event_tx
.send(AppEvent::PersistAgentsOverviewGrouping(state.grouping));
if let Some(grouping) = state.grouping.persisted() {
self.app_event_tx
.send(AppEvent::PersistAgentsOverviewGrouping(grouping));
}
return;
}
if self.agents_keymap.toggle_pin.is_pressed(key) {
@@ -9,6 +9,13 @@ Project, filtered=false: › ○ Task 1
Project, filtered=true: › ○ Task 3
Project, filtered=true: › ○ Task 4
Project, filtered=true: › ○ Task 1
Directory, filtered=false: › ○ Task 3
Directory, filtered=false: › ○ Task 4
Directory, filtered=false: › ○ Other 2
Directory, filtered=false: › ○ Task 1
Directory, filtered=true: › ○ Task 3
Directory, filtered=true: › ○ Task 4
Directory, filtered=true: › ○ Task 1
Status, filtered=false: › ○ Task 3
Status, filtered=false: › ○ Task 1
Status, filtered=false: › ○ Task 4
+1 -1
View File
@@ -278,7 +278,7 @@ define_keymap_actions! {
archive => "Archive the selected task and its child agents.",
delete => "Permanently delete the selected task and its child agents.",
hide => "Hide the selected task until explicitly resumed.",
toggle_grouping => "Cycle task grouping by project, status, or model.",
toggle_grouping => "Cycle task grouping by project, directory, status, or model.",
}
approval "Approval" {
open_fullscreen => "Open approval details fullscreen.",
+1 -11
View File
@@ -43,7 +43,6 @@ use codex_app_server_protocol::AskForApproval;
use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::GetAccountResponse;
use codex_app_server_protocol::Thread as AppServerThread;
#[cfg(test)]
use codex_app_server_protocol::ThreadListCwdFilter;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadSortKey as AppServerThreadSortKey;
@@ -863,7 +862,6 @@ async fn lookup_session_target_with_app_server(
}
async fn lookup_latest_session_target_with_app_server(
uses_remote_filesystem: bool,
app_server: &mut AppServerSession,
config: &Config,
cwd_filter: Option<&Path>,
@@ -877,7 +875,6 @@ async fn lookup_latest_session_target_with_app_server(
] {
let response = app_server
.thread_list(latest_session_lookup_params(
uses_remote_filesystem,
model_provider.clone(),
cwd_filter,
include_non_interactive,
@@ -911,7 +908,6 @@ enum LatestSessionLookupMode {
}
fn latest_session_lookup_params(
uses_remote_filesystem: bool,
model_provider: Option<String>,
cwd_filter: Option<&Path>,
include_non_interactive: bool,
@@ -931,8 +927,7 @@ fn latest_session_lookup_params(
project_id: None,
parent_thread_id: None,
ancestor_thread_id: None,
cwd: cwd_filter
.map(|cwd| resume_picker::repository_cwd_filter(cwd, uses_remote_filesystem)),
cwd: cwd_filter.map(|cwd| ThreadListCwdFilter::One(cwd.to_string_lossy().into_owned())),
use_state_db_only: match lookup_mode {
LatestSessionLookupMode::StateDbOnly => true,
LatestSessionLookupMode::ScanAndRepair => false,
@@ -1550,10 +1545,6 @@ async fn run_ratatui_app(
.run_until(
&mut tui,
lookup_latest_session_target_with_app_server(
uses_remote_workspace_or_environment(
&app_server_target,
&environment_manager,
),
startup_app_server,
&config,
filter_cwd,
@@ -1652,7 +1643,6 @@ async fn run_ratatui_app(
.run_until(
&mut tui,
lookup_latest_session_target_with_app_server(
uses_remote_workspace_or_environment(&app_server_target, &environment_manager),
startup_app_server,
&config,
filter_cwd,
+1 -10
View File
@@ -942,7 +942,6 @@ async fn latest_session_lookup_params_keep_local_filters_for_embedded_sessions()
let cwd = temp_dir.path().join("project");
let params = latest_session_lookup_params(
/*uses_remote_filesystem*/ false,
/*model_provider*/ None,
Some(cwd.as_path()),
/*include_non_interactive*/ false,
@@ -957,7 +956,6 @@ async fn latest_session_lookup_params_keep_local_filters_for_embedded_sessions()
assert!(params.use_state_db_only);
let scan_params = latest_session_lookup_params(
/*uses_remote_filesystem*/ false,
/*model_provider*/ None,
Some(cwd.as_path()),
/*include_non_interactive*/ false,
@@ -972,7 +970,6 @@ async fn latest_session_lookup_params_honor_explicit_provider() -> color_eyre::R
let temp_dir = TempDir::new()?;
let cwd = temp_dir.path().join("project");
let params = latest_session_lookup_params(
/*uses_remote_filesystem*/ false,
Some("selected-provider".to_string()),
Some(cwd.as_path()),
/*include_non_interactive*/ false,
@@ -994,7 +991,6 @@ async fn latest_session_lookup_params_honor_explicit_provider() -> color_eyre::R
async fn latest_session_lookup_params_omit_local_filters_for_remote_sessions() -> std::io::Result<()>
{
let params = latest_session_lookup_params(
/*uses_remote_filesystem*/ true,
/*model_provider*/ None,
/*cwd_filter*/ None,
/*include_non_interactive*/ false,
@@ -1009,7 +1005,6 @@ async fn latest_session_lookup_params_omit_local_filters_for_remote_sessions() -
#[tokio::test]
async fn latest_session_lookup_params_can_include_non_interactive_sources() -> std::io::Result<()> {
let params = latest_session_lookup_params(
/*uses_remote_filesystem*/ true,
/*model_provider*/ None,
/*cwd_filter*/ None,
/*include_non_interactive*/ true,
@@ -1034,7 +1029,6 @@ async fn latest_session_lookup_params_keep_explicit_cwd_filter_for_remote_sessio
let cwd = Path::new("repo/on/server");
let params = latest_session_lookup_params(
/*uses_remote_filesystem*/ true,
/*model_provider*/ None,
Some(cwd),
/*include_non_interactive*/ false,
@@ -1164,7 +1158,6 @@ async fn fork_last_filters_latest_session_by_cwd_unless_show_all() -> color_eyre
/*show_all*/ false,
);
let scoped_target = lookup_latest_session_target_with_app_server(
/*uses_remote_filesystem*/ false,
&mut app_server,
&config,
filter_cwd,
@@ -1177,7 +1170,6 @@ async fn fork_last_filters_latest_session_by_cwd_unless_show_all() -> color_eyre
/*show_all*/ true,
);
let show_all_target = lookup_latest_session_target_with_app_server(
/*uses_remote_filesystem*/ false,
&mut app_server,
&config,
show_all_filter_cwd,
@@ -1187,7 +1179,7 @@ async fn fork_last_filters_latest_session_by_cwd_unless_show_all() -> color_eyre
.expect("expected global fork --last target");
app_server.shutdown().await?;
assert_eq!(scoped_target.thread_id, linked_thread_id);
assert_eq!(scoped_target.thread_id, project_thread_id);
assert_eq!(show_all_target.thread_id, other_thread_id);
Ok(())
}
@@ -1225,7 +1217,6 @@ async fn latest_session_lookup_falls_back_for_rollout_missing_from_state_db()
)?;
let target = lookup_latest_session_target_with_app_server(
/*uses_remote_filesystem*/ false,
&mut app_server,
&config,
Some(project_cwd.as_path()),
+144 -37
View File
@@ -186,6 +186,7 @@ struct PageLoadRequest {
request_token: usize,
search_token: Option<usize>,
mode: PageLoadMode,
filter_mode: SessionFilterMode,
cwd_filter: Option<PathBuf>,
status: SessionStatus,
provider_filter: ProviderFilter,
@@ -218,6 +219,7 @@ enum ProviderFilter {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SessionFilterMode {
Cwd,
Repo,
All,
}
@@ -230,11 +232,20 @@ impl SessionFilterMode {
}
}
fn toggle(self, filter_cwd: Option<&Path>) -> Self {
match self {
Self::Cwd => Self::All,
Self::All if filter_cwd.is_some() => Self::Cwd,
Self::All => Self::All,
fn change(
self,
direction: ToolbarValueDirection,
filter_cwd: Option<&Path>,
repo_filter_available: bool,
) -> Self {
match (self, direction) {
(Self::Cwd, ToolbarValueDirection::Next) if repo_filter_available => Self::Repo,
(Self::Cwd, ToolbarValueDirection::Next | ToolbarValueDirection::Previous)
| (Self::Repo, ToolbarValueDirection::Next) => Self::All,
(Self::Repo, ToolbarValueDirection::Previous) => Self::Cwd,
(Self::All, ToolbarValueDirection::Previous) if repo_filter_available => Self::Repo,
(Self::All, _) if filter_cwd.is_some() => Self::Cwd,
(Self::All, _) => Self::All,
}
}
}
@@ -252,6 +263,11 @@ enum ToolbarControl {
Sort,
}
enum ToolbarValueDirection {
Previous,
Next,
}
impl ToolbarControl {
fn previous(self, action: SessionPickerAction) -> Self {
match self {
@@ -570,6 +586,12 @@ async fn run_session_picker_with_loader(
picker_loader: PickerLoader,
bg_rx: mpsc::UnboundedReceiver<BackgroundEvent>,
) -> Result<SessionSelection> {
let (repo_filter_tx, repo_filter_rx) = mpsc::unbounded_channel();
if let Some(cwd) = options.local_filter_cwd.clone() {
tokio::task::spawn_blocking(move || {
let _ = repo_filter_tx.send(local_repo_filter_available(&cwd));
});
}
let alt = AltScreenGuard::enter(tui);
let mut state = PickerState::new(
options.backend_id,
@@ -577,9 +599,12 @@ async fn run_session_picker_with_loader(
picker_loader,
options.provider_filter,
options.show_all,
options.filter_cwd,
options.local_filter_cwd.clone(),
options.action,
);
state.filter_mode =
SessionFilterMode::from_show_all(options.show_all, options.filter_cwd.as_deref());
state.filter_cwd = options.filter_cwd;
state.local_filter_cwd = options.local_filter_cwd;
state.use_theme_colors = options.use_theme_colors;
state.copy_on_select = options.copy_on_select;
@@ -604,6 +629,7 @@ async fn run_session_picker_with_loader(
}
let mut tui_events = alt.tui.event_stream().fuse();
let mut background_events = UnboundedReceiverStream::new(bg_rx).fuse();
let mut repo_filter_events = UnboundedReceiverStream::new(repo_filter_rx).fuse();
loop {
tokio::select! {
@@ -658,6 +684,9 @@ async fn run_session_picker_with_loader(
return Ok(selection);
}
}
Some(available) = repo_filter_events.next() => {
state.set_repo_filter_available(available);
}
else => break,
}
}
@@ -735,6 +764,7 @@ fn spawn_app_server_page_loader(
let cwd_filter = page_cwd_filter.for_request(
request.cursor.as_ref(),
request.cwd_filter.as_deref(),
request.filter_mode,
uses_remote_filesystem,
);
let cursor = request.cursor.map(|PageCursor::AppServer(cursor)| cursor);
@@ -888,6 +918,7 @@ struct PickerState {
status: SessionStatus,
filter_cwd: Option<PathBuf>,
local_filter_cwd: Option<PathBuf>,
repo_filter_available: bool,
toolbar_focus: ToolbarControl,
density: SessionListDensity,
launch_context: SessionPickerLaunchContext,
@@ -1091,6 +1122,7 @@ impl PickerState {
local_filter_cwd: filter_cwd.clone(),
local_cwd_matches: HashMap::new(),
filter_cwd,
repo_filter_available: false,
toolbar_focus: ToolbarControl::Filter,
density: SessionListDensity::Comfortable,
launch_context: SessionPickerLaunchContext::Startup,
@@ -1143,6 +1175,11 @@ impl PickerState {
self.requester.schedule_frame();
}
fn set_repo_filter_available(&mut self, available: bool) {
self.repo_filter_available = available;
self.request_frame();
}
fn is_transcript_loading(&self) -> bool {
self.pending_transcript_open.is_some()
}
@@ -1421,11 +1458,12 @@ impl PickerState {
self.focus_previous_toolbar_control();
self.request_frame();
}
_ if allow_plain_char_navigation
&& (self.keymap.list.move_left.is_pressed(key)
|| self.keymap.list.move_right.is_pressed(key)) =>
{
self.change_focused_toolbar_value();
_ if allow_plain_char_navigation && self.keymap.list.move_left.is_pressed(key) => {
self.change_focused_toolbar_value(ToolbarValueDirection::Previous);
self.request_frame();
}
_ if allow_plain_char_navigation && self.keymap.list.move_right.is_pressed(key) => {
self.change_focused_toolbar_value(ToolbarValueDirection::Next);
self.request_frame();
}
KeyEvent {
@@ -1506,6 +1544,7 @@ impl PickerState {
request_token,
search_token,
mode,
filter_mode: self.filter_mode,
cwd_filter: self.active_cwd_filter(),
status: self.status,
provider_filter: self.provider_filter.clone(),
@@ -1546,6 +1585,7 @@ impl PickerState {
request_token,
search_token,
mode: PageLoadMode::StoreDefault,
filter_mode: self.filter_mode,
cwd_filter: self.active_cwd_filter(),
status: self.status,
provider_filter: self.provider_filter.clone(),
@@ -1716,11 +1756,13 @@ impl PickerState {
return false;
};
paths_match(row_cwd, filter_cwd)
|| codex_git_utils::repository_identity(row_cwd)
.zip(codex_git_utils::repository_identity(filter_cwd))
.is_some_and(|(row, filter)| {
row.common_dir == filter.common_dir && row.relative_cwd == filter.relative_cwd
})
|| (self.filter_mode == SessionFilterMode::Repo
&& codex_git_utils::repository_identity(row_cwd)
.zip(codex_git_utils::repository_identity(filter_cwd))
.is_some_and(|(row, filter)| {
row.common_dir == filter.common_dir
&& row.relative_cwd == filter.relative_cwd
}))
}
fn set_query(&mut self, new_query: String) {
@@ -1871,6 +1913,7 @@ impl PickerState {
request_token,
search_token,
mode,
filter_mode: self.filter_mode,
cwd_filter: self.active_cwd_filter(),
status: self.status,
provider_filter: self.provider_filter.clone(),
@@ -1910,8 +1953,12 @@ impl PickerState {
self.start_initial_load();
}
fn toggle_filter_mode(&mut self) {
let next_filter_mode = self.filter_mode.toggle(self.filter_cwd.as_deref());
fn change_filter_mode(&mut self, direction: ToolbarValueDirection) {
let next_filter_mode = self.filter_mode.change(
direction,
self.filter_cwd.as_deref(),
self.repo_filter_available,
);
if self.filter_mode == next_filter_mode {
return;
}
@@ -1929,7 +1976,7 @@ impl PickerState {
fn active_cwd_filter(&self) -> Option<PathBuf> {
match self.filter_mode {
SessionFilterMode::Cwd => self.filter_cwd.clone(),
SessionFilterMode::Cwd | SessionFilterMode::Repo => self.filter_cwd.clone(),
SessionFilterMode::All => None,
}
}
@@ -1942,10 +1989,10 @@ impl PickerState {
self.toolbar_focus = self.toolbar_focus.next(self.action);
}
fn change_focused_toolbar_value(&mut self) {
fn change_focused_toolbar_value(&mut self, direction: ToolbarValueDirection) {
match self.toolbar_focus {
ToolbarControl::Sort => self.toggle_sort_key(),
ToolbarControl::Filter => self.toggle_filter_mode(),
ToolbarControl::Filter => self.change_filter_mode(direction),
ToolbarControl::Status => self.toggle_status(),
}
}
@@ -2152,6 +2199,16 @@ pub(crate) fn repository_cwd_filter(
ThreadListCwdFilter::One(cwd.to_string_lossy().into_owned())
}
fn local_repo_filter_available(cwd: &Path) -> bool {
codex_git_utils::linked_worktree_cwds(cwd).is_some_and(|cwds| {
cwds.into_iter()
.filter_map(|cwd| dunce::canonicalize(cwd).ok())
.collect::<HashSet<_>>()
.len()
> 1
})
}
fn paths_match(a: &Path, b: &Path) -> bool {
path_utils::paths_match_after_normalization(a, b)
}
@@ -2289,19 +2346,27 @@ fn filter_control_spans(state: &PickerState, compact: bool) -> Vec<Span<'static>
),
];
}
vec![
let mut values = vec![
"Filter: ".set_style(secondary_text_style()),
toolbar_value(
filter_mode_label(SessionFilterMode::Cwd),
state.filter_mode == SessionFilterMode::Cwd,
filter_focused,
),
toolbar_value(
filter_mode_label(SessionFilterMode::All),
state.filter_mode == SessionFilterMode::All,
];
if state.repo_filter_available {
values.push(toolbar_value(
filter_mode_label(SessionFilterMode::Repo),
state.filter_mode == SessionFilterMode::Repo,
filter_focused,
),
]
));
}
values.push(toolbar_value(
filter_mode_label(SessionFilterMode::All),
state.filter_mode == SessionFilterMode::All,
filter_focused,
));
values
}
fn toolbar_value(label: &'static str, active: bool, focused: bool) -> Span<'static> {
@@ -2321,6 +2386,7 @@ fn toolbar_value(label: &'static str, active: bool, focused: bool) -> Span<'stat
fn filter_mode_label(filter_mode: SessionFilterMode) -> &'static str {
match filter_mode {
SessionFilterMode::Cwd => "Cwd",
SessionFilterMode::Repo => "Repo",
SessionFilterMode::All => "All",
}
}
@@ -3773,7 +3839,7 @@ mod tests {
}
#[tokio::test]
async fn linked_worktree_sessions_are_included_for_local_repositories() {
async fn linked_worktree_sessions_follow_directory_scope() {
let root = tempfile::tempdir().expect("temporary repository");
let primary = root.path().join("primary");
let linked = root.path().join("linked");
@@ -3802,6 +3868,7 @@ mod tests {
let mut row = make_row("session.jsonl", "2025-01-01T00:00:00Z", "Linked session");
row.cwd = Some(linked.clone());
let single = ThreadListCwdFilter::One(primary.display().to_string());
state.repo_filter_available = local_repo_filter_available(&primary);
state.start_initial_load();
state.ingest_page(page(
vec![row.clone()],
@@ -3809,6 +3876,14 @@ mod tests {
/*num_scanned_files*/ 1,
/*reached_scan_cap*/ false,
));
assert!(state.filtered_rows.is_empty());
state.change_filter_mode(ToolbarValueDirection::Next);
state.ingest_page(page(
vec![row.clone()],
/*next_cursor*/ None,
/*num_scanned_files*/ 1,
/*reached_scan_cap*/ false,
));
assert_eq!(state.filtered_rows, vec![row.clone()]);
state
.pagination
@@ -3887,6 +3962,22 @@ mod tests {
.handle_background_event(event)
.await
.expect("ingest app-server page");
assert!(
live_picker
.filtered_rows
.iter()
.all(|row| row.cwd.as_deref() == Some(primary.as_path()))
);
live_picker.filter_mode = SessionFilterMode::Repo;
live_picker.start_initial_load();
let event = tokio::time::timeout(std::time::Duration::from_secs(10), bg_rx.recv())
.await
.expect("app-server repo page response")
.expect("repo page event");
live_picker
.handle_background_event(event)
.await
.expect("ingest app-server repo page");
let linked_index = live_picker
.filtered_rows
.iter()
@@ -3906,18 +3997,15 @@ mod tests {
live_picker.handle_key(accept).await.expect("fork selection"),
Some(SessionSelection::Fork(SessionTarget { thread_id, .. })) if thread_id == linked_id
));
let mut local = crate::latest_session_lookup_params(
/*uses_remote_filesystem*/ false,
let local = crate::latest_session_lookup_params(
/*model_provider*/ None,
Some(&primary),
/*include_non_interactive*/ false,
crate::LatestSessionLookupMode::StateDbOnly,
);
assert!(matches!(local.cwd, Some(ThreadListCwdFilter::Many(_))));
local.cwd = Some(single.clone());
assert_eq!(local.cwd, Some(single.clone()));
assert_eq!(
crate::latest_session_lookup_params(
/*uses_remote_filesystem*/ true,
/*model_provider*/ None,
Some(&primary),
/*include_non_interactive*/ false,
@@ -5606,7 +5694,7 @@ session_picker_view = "dense"
use crate::test_backend::VT100Backend;
let loader = page_only_loader(|_| {});
let state = PickerState::new(
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
@@ -5615,6 +5703,7 @@ session_picker_view = "dense"
Some(PathBuf::from("/tmp/project")),
SessionPickerAction::Resume,
);
state.set_repo_filter_available(/*available*/ true);
let width: u16 = 100;
let backend = VT100Backend::new(width, /*height*/ 1);
@@ -6362,10 +6451,28 @@ session_picker_view = "dense"
.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE))
.await
.unwrap();
assert_eq!(state.filter_mode, SessionFilterMode::All);
state.set_repo_filter_available(/*available*/ true);
assert_eq!(state.filter_mode, SessionFilterMode::All);
let guard = recorded_requests.lock().unwrap();
assert_eq!(guard.len(), 2);
assert_eq!(guard[1].cwd_filter, None);
let cwd_filter = Some(Path::new("/tmp/project"));
for (key, filter_mode, expected_cwd) in [
(KeyCode::Left, SessionFilterMode::Repo, cwd_filter),
(KeyCode::Left, SessionFilterMode::Cwd, cwd_filter),
(KeyCode::Right, SessionFilterMode::Repo, cwd_filter),
(KeyCode::Right, SessionFilterMode::All, None),
] {
state
.handle_key(KeyEvent::new(key, KeyModifiers::NONE))
.await
.unwrap();
let guard = recorded_requests.lock().unwrap();
let request = guard.last().expect("filter reload request");
assert_eq!(
(request.filter_mode, request.cwd_filter.as_deref()),
(filter_mode, expected_cwd)
);
}
}
#[tokio::test]
@@ -218,7 +218,7 @@ fn archived_status_preserves_directory_filter_and_hides_archive_shortcut() {
@"Filter: Cwd Archived Sort: Updated "
);
state.toggle_filter_mode();
state.change_filter_mode(super::super::ToolbarValueDirection::Next);
assert_eq!(
*requested_filters.lock().unwrap(),
+11 -1
View File
@@ -3,6 +3,7 @@
use std::path::Path;
use super::PageCursor;
use super::SessionFilterMode;
use super::repository_cwd_filter;
use codex_app_server_protocol::ThreadListCwdFilter;
@@ -17,10 +18,19 @@ impl PageCwdFilter {
&mut self,
cursor: Option<&PageCursor>,
cwd: Option<&Path>,
filter_mode: SessionFilterMode,
uses_remote_filesystem: bool,
) -> Option<ThreadListCwdFilter> {
if cursor.is_none() {
self.current = cwd.map(|cwd| repository_cwd_filter(cwd, uses_remote_filesystem));
self.current = match filter_mode {
SessionFilterMode::Cwd => {
cwd.map(|cwd| ThreadListCwdFilter::One(cwd.to_string_lossy().into_owned()))
}
SessionFilterMode::Repo => {
cwd.map(|cwd| repository_cwd_filter(cwd, uses_remote_filesystem))
}
SessionFilterMode::All => None,
};
}
self.current.clone()
}
@@ -1,10 +1,10 @@
//! Checks that a cursor keeps its original checkout filter until the listing restarts.
//! Checks that a cursor keeps its original directory scope until the listing restarts.
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn linked_checkout_changes_only_affect_the_next_listing_cycle() {
fn directory_scope_changes_only_affect_the_next_listing_cycle() {
let root = tempfile::tempdir().expect("temporary repository");
let primary = root.path().join("primary");
let linked = root.path().join("linked");
@@ -20,6 +20,7 @@ fn linked_checkout_changes_only_affect_the_next_listing_cycle() {
filter.for_request(
/*cursor*/ None,
Some(&primary),
SessionFilterMode::Cwd,
/*uses_remote_filesystem*/ false,
),
single
@@ -41,11 +42,21 @@ fn linked_checkout_changes_only_affect_the_next_listing_cycle() {
filter.for_request(
Some(&cursor),
Some(&primary),
SessionFilterMode::Repo,
/*uses_remote_filesystem*/ false,
),
single
);
let both = Some(ThreadListCwdFilter::Many(vec![
assert_eq!(
filter.for_request(
/*cursor*/ None,
Some(&primary),
SessionFilterMode::Cwd,
/*uses_remote_filesystem*/ false,
),
single
);
let repo = Some(ThreadListCwdFilter::Many(vec![
primary.to_string_lossy().into_owned(),
linked.to_string_lossy().into_owned(),
]));
@@ -53,25 +64,37 @@ fn linked_checkout_changes_only_affect_the_next_listing_cycle() {
filter.for_request(
/*cursor*/ None,
Some(&primary),
SessionFilterMode::Repo,
/*uses_remote_filesystem*/ false,
),
both
repo
);
std::fs::remove_file(linked.join(".git")).expect("remove linked checkout registration");
assert_eq!(
filter.for_request(
Some(&cursor),
Some(&primary),
SessionFilterMode::Cwd,
/*uses_remote_filesystem*/ false,
),
both
repo
);
assert_eq!(
filter.for_request(
/*cursor*/ None,
Some(&primary),
SessionFilterMode::Repo,
/*uses_remote_filesystem*/ false,
),
single
);
assert_eq!(
filter.for_request(
/*cursor*/ None,
Some(&primary),
SessionFilterMode::All,
/*uses_remote_filesystem*/ false,
),
None
);
}
@@ -0,0 +1,28 @@
---
source: tui/src/app/agents_overview_tests.rs
expression: normalize_agent_center_snapshot(directory_grouping)
---
Agent command center Group: Directory g
All 7 Needs you 0 Working 0 Ready 7 Inactive 0 tab/⇧tab filter
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Tasks Status Updated
/tmp/worktree-root/linked 2
○ Linked README Ready 2m ago
○ Linked tree Ready 2m ago
/tmp/worktree-root/linked/shared/child 1
○ Linked child Ready 2m ago
/tmp/worktree-root/primary 2
› ○ Primary README Ready 2m ago
○ Primary tree Ready 2m ago
/tmp/worktree-root/primary/shared/child 1
○ Primary child Ready 2m ago
/tmp/worktree-root/primary/subdir/child 1
○ Nested repository Ready 2m ago
? help esc back ↑/↓ move enter open n new
@@ -1,23 +1,23 @@
---
source: tui/src/app/agents_overview_tests.rs
expression: normalize_agent_center_snapshot(grouping)
expression: normalize_agent_center_snapshot(project_grouping)
---
Agent command center Group: Project g
All 4 Needs you 0 Working 0 Ready 4 Inactive 0 tab/⇧tab filter
All 7 Needs you 0 Working 0 Ready 7 Inactive 0 tab/⇧tab filter
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Tasks Status Updated
/tmp/worktree-root/primary 2
› ○ Task Ready 2m ago
○ Task Ready 2m ago
/tmp/worktree-root/primary 4
› ○ Primary README Ready 2m ago
○ Primary tree Ready 2m ago
○ Linked README Ready 2m ago
○ Linked tree Ready 2m ago
/tmp/worktree-root/primary/shared/child 2
○ Primary child Ready 2m ago
○ Linked child Ready 2m ago
/tmp/worktree-root/primary/subdir/child 1
○ Task Ready 2m ago
/tmp/worktree-root/primary/subdir/child 1
○ Task Ready 2m ago
○ Nested repository Ready 2m ago
@@ -12,4 +12,4 @@ Stop | x | Agents stop Stop Stop the selected running task. x Default
Archive | a | Agents archive Archive Archive the selected task and its child agents. a Default
Delete | backspace | Agents delete Delete Permanently delete the selected task and its child agents. backspace Default
Hide | h | Agents hide Hide Hide the selected task until explicitly resumed. h Default
Toggle Grouping | g | Agents toggle_grouping Toggle Grouping Cycle task grouping by project, status, or model. g Default
Toggle Grouping | g | Agents toggle_grouping Toggle Grouping Cycle task grouping by project, directory, status, or model. g Default
@@ -2,4 +2,4 @@
source: tui/src/resume_picker.rs
expression: terminal.backend().to_string()
---
Filter: Cwd All Status: Active Archived Sort: Updated Created
Filter: Cwd Repo All Status: Active Archived Sort: Updated Created