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

@@ -1,6 +1,7 @@
//! Key routing and composer-adjacent UI interaction for `ChatWidget`.
use super::*;
use crate::bottom_pane::BottomPaneView;
impl ChatWidget {
pub(crate) fn keymap_contexts(&self) -> crate::keymap::KeymapContextSet {
@@ -241,6 +242,21 @@ impl ChatWidget {
self.request_redraw();
}
pub(crate) fn show_bottom_pane_view(&mut self, view: Box<dyn BottomPaneView>) {
self.bottom_pane.show_view(view);
self.refresh_plan_mode_nudge();
self.request_redraw();
}
pub(crate) fn replace_bottom_pane_view_if_present(
&mut self,
view_id: &'static str,
view: Box<dyn BottomPaneView>,
) {
self.bottom_pane.replace_view_if_present(view_id, view);
self.refresh_plan_mode_nudge();
}
pub(crate) fn selected_index_for_present_view(&self, view_id: &'static str) -> Option<usize> {
self.bottom_pane.selected_index_for_present_view(view_id)
}
+10
View File
@@ -7,6 +7,16 @@ use std::cell::Cell;
impl ChatWidget {
pub(crate) fn as_renderable(&self) -> RenderableItem<'_> {
if self
.bottom_pane
.selected_index_for_active_view(crate::app::AGENTS_OVERVIEW_VIEW_ID)
.is_some()
{
return self
.bottom_pane
.as_renderable_with_composer_right_reserve(/*composer_right_reserve*/ 0);
}
let active_cell_right_reserve = self.ambient_pet_wrap_reserved_cols();
let active_cell_renderable = match &self.transcript.active_cell {
Some(cell) => RenderableItem::Owned(Box::new(TranscriptAreaRenderable {
@@ -313,6 +313,9 @@ impl ChatWidget {
SlashCommand::Side | SlashCommand::Btw => {
self.request_empty_side_conversation(cmd);
}
SlashCommand::Agents => {
self.app_event_tx.send(AppEvent::OpenAgentsOverview);
}
SlashCommand::MultiAgents => {
self.app_event_tx.send(AppEvent::OpenAgentPicker);
}
@@ -1139,6 +1142,7 @@ impl ChatWidget {
| SlashCommand::Side
| SlashCommand::Btw
| SlashCommand::Keymap
| SlashCommand::Agents
| SlashCommand::MultiAgents
| SlashCommand::Permissions
| SlashCommand::ElevateSandbox