Add an agents overview dashboard to the TUI (#39094)

## What changed

- Add `/agents` to open a full-screen dashboard of loaded root sessions from the shared app server, with subagent status reflected in each root session.
- Support search, navigation, and grouping sessions by project or status.
- Refresh the dashboard when relevant thread notifications arrive, while preserving its selection and view preferences.
- Show a clear message when the dashboard is unavailable with the embedded app server.

## Testing

- Add coverage for dashboard rendering, status grouping, persisted view state, and restoring the conversation viewport after closing the dashboard.
- Cover `/agents` command discovery and dispatch from parent-owned threads.

GitOrigin-RevId: 6651f4c47358116e1809da3ac8844a5842026a95
This commit is contained in:
Eric Traut authored and copyberry committed 2026-08-17 22:50:25 +00:00
1 parent 83d015375e
commit 4617d4d21d
19 files changed
+949 -4

No files matched your search

@@ -384,6 +384,7 @@ fn parent_owned_command_is_allowed(command: SlashCommand, args: &str) -> bool {
| SlashCommand::App
| SlashCommand::Side
| SlashCommand::Btw
| SlashCommand::Agents
| SlashCommand::MultiAgents
| SlashCommand::Vim
| SlashCommand::Keymap
@@ -4976,6 +4977,7 @@ mod tests {
#[test]
fn parent_owned_thread_allows_bare_navigation_commands() {
for (command, expected) in [
("/agents", SlashCommand::Agents),
("/subagents", SlashCommand::MultiAgents),
("/side", SlashCommand::Side),
("/btw", SlashCommand::Btw),
@@ -4996,13 +4998,13 @@ mod tests {
fn parent_owned_thread_allows_safe_command_selected_from_prefix() {
let (mut composer, _rx) = new_test_composer();
composer.set_parent_owned_thread();
type_chars_humanlike(&mut composer, &['/', 's', 'u', 'b']);
type_chars_humanlike(&mut composer, &['/', 'a', 'g']);
let result = composer
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
.0;
assert_eq!(result, InputResult::Command(SlashCommand::MultiAgents));
assert_eq!(result, InputResult::Command(SlashCommand::Agents));
}
#[test]
+16 -1
View File
@@ -1222,6 +1222,21 @@ impl BottomPane {
popup_consts::standard_popup_hint_line_for_keymap(&self.keymap.list)
}
pub(crate) fn replace_view_if_present(
&mut self,
view_id: &'static str,
view: Box<dyn BottomPaneView>,
) {
if let Some(index) = self
.view_stack
.iter()
.rposition(|existing| existing.view_id() == Some(view_id))
{
self.view_stack[index] = view;
self.request_redraw();
}
}
pub(crate) fn list_keymap(&self) -> crate::keymap::ListKeymap {
self.keymap.list.clone()
}
@@ -1383,7 +1398,7 @@ impl BottomPane {
.lines()
.next()
.and_then(parse_slash_name)
.is_some_and(|(name, _, _)| name == "subagents");
.is_some_and(|(name, _, _)| matches!(name, "agents" | "subagents"));
self.keymap.chat.interrupt_turn.is_pressed(key_event)
&& self.is_task_running
@@ -303,6 +303,7 @@ mod tests {
commands,
vec![
SlashCommand::Ide,
SlashCommand::Agents,
SlashCommand::Copy,
SlashCommand::Export,
SlashCommand::Raw,
@@ -23,6 +23,7 @@ expression: commands
/app - continue this session in the Desktop app
/init - create an AGENTS.md file with instructions for Codex
/compact - summarize conversation to prevent hitting the context limit
/agents - view and switch between all active agent sessions
/side - start a side conversation in an ephemeral fork
/copy - copy last response as markdown
/export - export the conversation as markdown