mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Add shared task pinning to the agent command center (#51500)
## What changed - Use `p` to pin or unpin the selected task, with a configurable `agents.toggle_pin` binding. - Display pinned tasks first in a `Pinned` group, preserving shared section order and applying search and status filters. - Load pins from the shared thread section, including tasks outside the recent-task window, and refresh after pin changes. Disable pinning when the server does not support shared sections. ## Testing Add coverage for shared pin/unpin state, paginated pin discovery and refreshing pins on reopen, shortcut dispatch and duplicate-action suppression, and pinned-group and custom-help snapshots. GitOrigin-RevId: d1a2cab6642d059c3c8df8480b5c59a10c493936
This commit is contained in:
@@ -503,6 +503,8 @@ pub struct TuiAgentsKeymap {
|
|||||||
pub delete: Option<KeybindingsSpec>,
|
pub delete: Option<KeybindingsSpec>,
|
||||||
/// Hide the selected task until explicitly resumed or the TUI restarts.
|
/// Hide the selected task until explicitly resumed or the TUI restarts.
|
||||||
pub hide: Option<KeybindingsSpec>,
|
pub hide: Option<KeybindingsSpec>,
|
||||||
|
/// Pin or unpin the selected task in the shared sidebar section.
|
||||||
|
pub toggle_pin: Option<KeybindingsSpec>,
|
||||||
/// Toggle grouping tasks by status or project.
|
/// Toggle grouping tasks by status or project.
|
||||||
pub toggle_grouping: Option<KeybindingsSpec>,
|
pub toggle_grouping: Option<KeybindingsSpec>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4615,7 +4615,8 @@
|
|||||||
"resume": null,
|
"resume": null,
|
||||||
"search": null,
|
"search": null,
|
||||||
"stop": null,
|
"stop": null,
|
||||||
"toggle_grouping": null
|
"toggle_grouping": null,
|
||||||
|
"toggle_pin": null
|
||||||
},
|
},
|
||||||
"approval": {
|
"approval": {
|
||||||
"approve": null,
|
"approve": null,
|
||||||
@@ -5040,6 +5041,14 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Toggle grouping tasks by status or project."
|
"description": "Toggle grouping tasks by status or project."
|
||||||
|
},
|
||||||
|
"toggle_pin": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/KeybindingsSpec"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Pin or unpin the selected task in the shared sidebar section."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@@ -5569,7 +5578,8 @@
|
|||||||
"resume": null,
|
"resume": null,
|
||||||
"search": null,
|
"search": null,
|
||||||
"stop": null,
|
"stop": null,
|
||||||
"toggle_grouping": null
|
"toggle_grouping": null,
|
||||||
|
"toggle_pin": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"approval": {
|
"approval": {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ impl AgentsOverviewView {
|
|||||||
&self.agents_keymap.archive,
|
&self.agents_keymap.archive,
|
||||||
&self.agents_keymap.delete,
|
&self.agents_keymap.delete,
|
||||||
&self.agents_keymap.hide,
|
&self.agents_keymap.hide,
|
||||||
|
&self.agents_keymap.toggle_pin,
|
||||||
&self.agents_keymap.toggle_grouping,
|
&self.agents_keymap.toggle_grouping,
|
||||||
].into_iter().any(|bindings| bindings.contains(binding)))
|
].into_iter().any(|bindings| bindings.contains(binding)))
|
||||||
})
|
})
|
||||||
@@ -89,8 +90,11 @@ impl AgentsOverviewView {
|
|||||||
("archive", &self.agents_keymap.archive, "Archive"),
|
("archive", &self.agents_keymap.archive, "Archive"),
|
||||||
("hide", &self.agents_keymap.hide, "Hide"),
|
("hide", &self.agents_keymap.hide, "Hide"),
|
||||||
("delete", &self.agents_keymap.delete, "Delete"),
|
("delete", &self.agents_keymap.delete, "Delete"),
|
||||||
|
("toggle_pin", &self.agents_keymap.toggle_pin, "Pin/unpin"),
|
||||||
] {
|
] {
|
||||||
if action != "new_worktree" || self.worktrees_enabled {
|
if (action != "new_worktree" || self.worktrees_enabled)
|
||||||
|
&& (action != "toggle_pin" || self.can_toggle_selected_pin())
|
||||||
|
{
|
||||||
tasks.push(self.agents_keymap.primary_hint(action, bindings), label);
|
tasks.push(self.agents_keymap.primary_hint(action, bindings), label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,15 +140,19 @@ impl AgentsOverviewView {
|
|||||||
let task = &self.rows[index];
|
let task = &self.rows[index];
|
||||||
let rect = row(viewport, offset as u16, /*height*/ 1);
|
let rect = row(viewport, offset as u16, /*height*/ 1);
|
||||||
if matches!(entry, CenterRow::Group(_)) {
|
if matches!(entry, CenterRow::Group(_)) {
|
||||||
let group = match state.grouping {
|
let group = if self.is_pinned(index) {
|
||||||
AgentsOverviewGrouping::Project => {
|
"Pinned".to_owned()
|
||||||
format_directory_display(
|
} else {
|
||||||
&self.project_groups[index].heading,
|
match state.grouping {
|
||||||
/*max_width*/ None,
|
AgentsOverviewGrouping::Project => {
|
||||||
)
|
format_directory_display(
|
||||||
|
&self.project_groups[index].heading,
|
||||||
|
/*max_width*/ None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AgentsOverviewGrouping::Status => task.group.label().to_owned(),
|
||||||
|
AgentsOverviewGrouping::Model => model_name(&task.thread).to_owned(),
|
||||||
}
|
}
|
||||||
AgentsOverviewGrouping::Status => task.group.label().to_owned(),
|
|
||||||
AgentsOverviewGrouping::Model => model_name(&task.thread).to_owned(),
|
|
||||||
};
|
};
|
||||||
let count = indices
|
let count = indices
|
||||||
.iter()
|
.iter()
|
||||||
@@ -164,7 +168,9 @@ impl AgentsOverviewView {
|
|||||||
} else {
|
} else {
|
||||||
format!("{count} of {total}")
|
format!("{count} of {total}")
|
||||||
};
|
};
|
||||||
let group = if state.grouping == AgentsOverviewGrouping::Project {
|
let group = if state.grouping == AgentsOverviewGrouping::Project
|
||||||
|
&& !self.is_pinned(index)
|
||||||
|
{
|
||||||
crate::text_formatting::center_truncate_path(
|
crate::text_formatting::center_truncate_path(
|
||||||
&group,
|
&group,
|
||||||
usize::from(rect.width)
|
usize::from(rect.width)
|
||||||
|
|||||||
@@ -75,8 +75,10 @@ async fn live_center_columns() {
|
|||||||
ThreadStatus::Idle,
|
ThreadStatus::Idle,
|
||||||
));
|
));
|
||||||
crate::chatwidget::activate_voice_for_thread(&mut app.chat_widget, child);
|
crate::chatwidget::activate_voice_for_thread(&mut app.chat_widget, child);
|
||||||
|
app.agents_overview.pinned_thread_ids = Some(vec![current, ThreadId::from_u128(/*value*/ 44)]);
|
||||||
let mut view = app.agents_overview_view(threads, Some(current));
|
let mut view = app.agents_overview_view(threads, Some(current));
|
||||||
insta::assert_snapshot!(screen(&view, /*width*/ 160, /*height*/ 22));
|
insta::assert_snapshot!(screen(&view, /*width*/ 160, /*height*/ 22));
|
||||||
|
view.pinned_thread_ranks = None;
|
||||||
let mut selected_status_styles = Vec::new();
|
let mut selected_status_styles = Vec::new();
|
||||||
for _ in 0..fixtures.len() {
|
for _ in 0..fixtures.len() {
|
||||||
let area = Rect::new(
|
let area = Rect::new(
|
||||||
@@ -466,15 +468,27 @@ page_up = []
|
|||||||
[agents]
|
[agents]
|
||||||
archive = []
|
archive = []
|
||||||
rename = 'z r'
|
rename = 'z r'
|
||||||
|
toggle_pin = 'z p'
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
app.keymap = RuntimeKeymap::from_config(&config).unwrap();
|
app.keymap = RuntimeKeymap::from_config(&config).unwrap();
|
||||||
let mut view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
app.agents_overview.pinned_thread_ids = Some(Vec::new());
|
||||||
|
let view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
||||||
insta::assert_snapshot!(
|
insta::assert_snapshot!(
|
||||||
"live_center_custom_footer",
|
"live_center_custom_footer",
|
||||||
screen(&view, /*width*/ 100, /*height*/ 12)
|
screen(&view, /*width*/ 100, /*height*/ 12)
|
||||||
);
|
);
|
||||||
|
let thread_id = ThreadId::from_u128(/*value*/ 42);
|
||||||
|
let mut view = app.agents_overview_view(
|
||||||
|
vec![overview_thread(
|
||||||
|
thread_id,
|
||||||
|
/*parent_thread_id*/ None,
|
||||||
|
"Task",
|
||||||
|
ThreadStatus::Idle,
|
||||||
|
)],
|
||||||
|
Some(thread_id),
|
||||||
|
);
|
||||||
view.handle_key_event(KeyCode::Char('?').into());
|
view.handle_key_event(KeyCode::Char('?').into());
|
||||||
insta::assert_snapshot!(
|
insta::assert_snapshot!(
|
||||||
"live_center_custom_help",
|
"live_center_custom_help",
|
||||||
|
|||||||
@@ -49,11 +49,16 @@ pub(super) struct AgentsOverviewState {
|
|||||||
pub(super) initialized: bool,
|
pub(super) initialized: bool,
|
||||||
pub(super) discovery: super::agents_overview_discovery::AgentsOverviewDiscovery,
|
pub(super) discovery: super::agents_overview_discovery::AgentsOverviewDiscovery,
|
||||||
pub(super) show_more_requested: bool,
|
pub(super) show_more_requested: bool,
|
||||||
|
pub(super) refresh_show_more: bool,
|
||||||
/// Vacancies left by lifecycle removals, filled without expanding the visible window.
|
/// Vacancies left by lifecycle removals, filled without expanding the visible window.
|
||||||
pub(super) refill_count: usize,
|
pub(super) refill_count: usize,
|
||||||
pub(super) request_id: Option<Uuid>,
|
pub(super) request_id: Option<Uuid>,
|
||||||
pub(super) refresh_pending: bool,
|
pub(super) refresh_pending: bool,
|
||||||
|
pub(super) pin_refresh_requested: bool,
|
||||||
pub(super) refresh_thread_ids: HashSet<ThreadId>,
|
pub(super) refresh_thread_ids: HashSet<ThreadId>,
|
||||||
|
pub(super) active_refresh_thread_ids: HashSet<ThreadId>,
|
||||||
|
pub(super) pinned_thread_ids: Option<Vec<ThreadId>>,
|
||||||
|
pub(super) pending_pin_change: Option<Uuid>,
|
||||||
pub(super) refresh_task: Option<tokio::task::AbortHandle>,
|
pub(super) refresh_task: Option<tokio::task::AbortHandle>,
|
||||||
pub(super) refresh_notifications: HashMap<ThreadId, Vec<ServerNotification>>,
|
pub(super) refresh_notifications: HashMap<ThreadId, Vec<ServerNotification>>,
|
||||||
pub(super) rendered_full_screen: bool,
|
pub(super) rendered_full_screen: bool,
|
||||||
@@ -170,6 +175,8 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.agents_overview.request_id = None;
|
self.agents_overview.request_id = None;
|
||||||
|
self.agents_overview.refresh_show_more = false;
|
||||||
|
self.agents_overview.active_refresh_thread_ids.clear();
|
||||||
self.agents_overview.refresh_task = None;
|
self.agents_overview.refresh_task = None;
|
||||||
let refill_succeeded = result
|
let refill_succeeded = result
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -185,6 +192,9 @@ impl App {
|
|||||||
}
|
}
|
||||||
match result {
|
match result {
|
||||||
Ok(refresh) => {
|
Ok(refresh) => {
|
||||||
|
if let Some(pinned_thread_ids) = refresh.pinned_thread_ids {
|
||||||
|
self.agents_overview.pinned_thread_ids = pinned_thread_ids;
|
||||||
|
}
|
||||||
self.agents_overview.initialized = refresh.recent_seed_complete;
|
self.agents_overview.initialized = refresh.recent_seed_complete;
|
||||||
if let Some(discovery) = refresh.discovery {
|
if let Some(discovery) = refresh.discovery {
|
||||||
if !discovery.has_more() {
|
if !discovery.has_more() {
|
||||||
@@ -380,7 +390,7 @@ impl App {
|
|||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
.vim_enabled = self.chat_widget.composer_is_vim_enabled();
|
.vim_enabled = self.chat_widget.composer_is_vim_enabled();
|
||||||
AgentsOverviewView::new(
|
let mut view = AgentsOverviewView::new(
|
||||||
rows,
|
rows,
|
||||||
selected_thread_id,
|
selected_thread_id,
|
||||||
self.config.features.enabled(Feature::Worktrees)
|
self.config.features.enabled(Feature::Worktrees)
|
||||||
@@ -392,7 +402,16 @@ impl App {
|
|||||||
self.app_event_tx.clone(),
|
self.app_event_tx.clone(),
|
||||||
self.keymap.clone(),
|
self.keymap.clone(),
|
||||||
Arc::clone(&self.agents_overview.view_state),
|
Arc::clone(&self.agents_overview.view_state),
|
||||||
)
|
);
|
||||||
|
view.pinned_thread_ranks = self.agents_overview.pinned_thread_ids.as_ref().map(|ids| {
|
||||||
|
ids.iter()
|
||||||
|
.copied()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(rank, id)| (id, rank))
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
view.pin_action_pending = self.agents_overview.pending_pin_change.is_some();
|
||||||
|
view
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn select_agents_overview_thread(
|
pub(super) async fn select_agents_overview_thread(
|
||||||
|
|||||||
@@ -12,10 +12,15 @@ use crate::chatwidget::ChatWidget;
|
|||||||
use crate::render::renderable::Renderable;
|
use crate::render::renderable::Renderable;
|
||||||
use crate::tui;
|
use crate::tui;
|
||||||
use crate::wrapping::word_wrap_lines;
|
use crate::wrapping::word_wrap_lines;
|
||||||
|
use codex_app_server_protocol::ClientRequest;
|
||||||
|
use codex_app_server_protocol::RequestId;
|
||||||
use codex_app_server_protocol::SessionSource;
|
use codex_app_server_protocol::SessionSource;
|
||||||
use codex_app_server_protocol::ThreadLoadedListParams;
|
use codex_app_server_protocol::ThreadLoadedListParams;
|
||||||
|
use codex_app_server_protocol::ThreadSectionMoveParams;
|
||||||
|
use codex_app_server_protocol::ThreadSectionMoveResponse;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use codex_protocol::protocol::SubAgentSource;
|
use codex_protocol::protocol::SubAgentSource;
|
||||||
|
use codex_state::PINNED_THREAD_SECTION_ID;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Stylize;
|
use ratatui::style::Stylize;
|
||||||
@@ -25,6 +30,7 @@ use std::collections::HashSet;
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use tokio_stream::Stream;
|
use tokio_stream::Stream;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
struct LifecycleHeader(Vec<Line<'static>>);
|
struct LifecycleHeader(Vec<Line<'static>>);
|
||||||
|
|
||||||
@@ -104,6 +110,90 @@ async fn run_lifecycle_modal<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
pub(super) fn toggle_agents_overview_pin(
|
||||||
|
&mut self,
|
||||||
|
app_server: &AppServerSession,
|
||||||
|
thread_id: ThreadId,
|
||||||
|
pinned: bool,
|
||||||
|
) {
|
||||||
|
if self.agents_overview.pending_pin_change.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let request_id = Uuid::new_v4();
|
||||||
|
self.agents_overview.pending_pin_change = Some(request_id);
|
||||||
|
let request_handle = app_server.request_handle();
|
||||||
|
let app_event_tx = self.app_event_tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let result = request_handle
|
||||||
|
.request_typed::<ThreadSectionMoveResponse>(ClientRequest::ThreadSectionMove {
|
||||||
|
request_id: RequestId::String(Uuid::new_v4().to_string()),
|
||||||
|
params: ThreadSectionMoveParams {
|
||||||
|
thread_id: thread_id.to_string(),
|
||||||
|
section_id: pinned.then(|| PINNED_THREAD_SECTION_ID.to_string()),
|
||||||
|
before_thread_id: None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|error| error.to_string());
|
||||||
|
app_event_tx.send(AppEvent::AgentsOverviewPinToggled {
|
||||||
|
request_id,
|
||||||
|
thread_id,
|
||||||
|
pinned,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn complete_agents_overview_pin(
|
||||||
|
&mut self,
|
||||||
|
app_server: &AppServerSession,
|
||||||
|
request_id: Uuid,
|
||||||
|
thread_id: ThreadId,
|
||||||
|
pinned: bool,
|
||||||
|
result: Result<(), String>,
|
||||||
|
) {
|
||||||
|
if self.agents_overview.pending_pin_change != Some(request_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.agents_overview.pending_pin_change = None;
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
if let Some(pinned_thread_ids) = self.agents_overview.pinned_thread_ids.as_mut() {
|
||||||
|
if pinned {
|
||||||
|
if !pinned_thread_ids.contains(&thread_id) {
|
||||||
|
pinned_thread_ids.push(thread_id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pinned_thread_ids.retain(|id| *id != thread_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
self.add_agents_overview_error(format!(
|
||||||
|
"Failed to {} task: {error}",
|
||||||
|
if pinned { "pin" } else { "unpin" }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let restore_show_more = self.agents_overview.refresh_show_more;
|
||||||
|
if let Some(refresh) = self.agents_overview.refresh_task.take() {
|
||||||
|
refresh.abort();
|
||||||
|
}
|
||||||
|
self.agents_overview.request_id = None;
|
||||||
|
self.agents_overview.refresh_show_more = false;
|
||||||
|
self.agents_overview.refresh_notifications.clear();
|
||||||
|
let refresh_thread_ids =
|
||||||
|
std::mem::take(&mut self.agents_overview.active_refresh_thread_ids);
|
||||||
|
self.agents_overview
|
||||||
|
.refresh_thread_ids
|
||||||
|
.extend(refresh_thread_ids);
|
||||||
|
self.agents_overview.refresh_pending = false;
|
||||||
|
self.agents_overview.show_more_requested |= restore_show_more;
|
||||||
|
self.refresh_agents_overview_threads(app_server);
|
||||||
|
self.repaint_agents_overview();
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn confirm_agents_overview_action(
|
pub(super) fn confirm_agents_overview_action(
|
||||||
&mut self,
|
&mut self,
|
||||||
thread_id: ThreadId,
|
thread_id: ThreadId,
|
||||||
@@ -227,6 +317,8 @@ impl App {
|
|||||||
refresh.abort();
|
refresh.abort();
|
||||||
}
|
}
|
||||||
self.agents_overview.request_id = None;
|
self.agents_overview.request_id = None;
|
||||||
|
self.agents_overview.refresh_show_more = false;
|
||||||
|
self.agents_overview.active_refresh_thread_ids.clear();
|
||||||
self.agents_overview
|
self.agents_overview
|
||||||
.view_state
|
.view_state
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::app::agents_overview_discovery::list_pinned_threads;
|
||||||
use crate::app_event::AgentsOverviewAction;
|
use crate::app_event::AgentsOverviewAction;
|
||||||
use codex_app_server_protocol::ClientRequest;
|
use codex_app_server_protocol::ClientRequest;
|
||||||
use codex_app_server_protocol::RequestId;
|
use codex_app_server_protocol::RequestId;
|
||||||
@@ -11,6 +12,51 @@ use core_test_support::streaming_sse::StreamingSseChunk;
|
|||||||
use core_test_support::streaming_sse::start_streaming_sse_server;
|
use core_test_support::streaming_sse::start_streaming_sse_server;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pin_action_updates_shared_section_state() -> 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 mut app_server = crate::start_embedded_app_server_for_picker(&app.config).await?;
|
||||||
|
let started = app_server.start_thread(&app.config).await?;
|
||||||
|
let thread_id = started.session.thread_id;
|
||||||
|
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||||
|
|
||||||
|
for pinned in [true, false] {
|
||||||
|
Box::pin(app.handle_event(
|
||||||
|
&mut tui,
|
||||||
|
&mut app_server,
|
||||||
|
AppEvent::ToggleAgentsOverviewPin { thread_id, pinned },
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
assert!(app.agents_overview.pending_pin_change.is_some());
|
||||||
|
let completed = rx.recv().await.expect("pin request completes");
|
||||||
|
assert!(matches!(
|
||||||
|
&completed,
|
||||||
|
AppEvent::AgentsOverviewPinToggled { .. }
|
||||||
|
));
|
||||||
|
app.agents_overview
|
||||||
|
.refresh_notifications
|
||||||
|
.insert(thread_id, Vec::new());
|
||||||
|
Box::pin(app.handle_event(&mut tui, &mut app_server, completed)).await?;
|
||||||
|
assert_eq!(app.agents_overview.pending_pin_change, None);
|
||||||
|
assert!(app.agents_overview.refresh_notifications.is_empty());
|
||||||
|
let threads = list_pinned_threads(&app_server.request_handle())
|
||||||
|
.await
|
||||||
|
.map_err(color_eyre::eyre::Report::msg)?
|
||||||
|
.expect("embedded app server supports shared thread sections");
|
||||||
|
assert_eq!(
|
||||||
|
threads
|
||||||
|
.iter()
|
||||||
|
.any(|thread| thread.id == started.session.thread_id.to_string()),
|
||||||
|
pinned
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
app_server.shutdown().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn archive_confirmation_number_keys_act_immediately() {
|
async fn archive_confirmation_number_keys_act_immediately() {
|
||||||
for key in ['1', '2'] {
|
for key in ['1', '2'] {
|
||||||
@@ -586,6 +632,7 @@ async fn hidden_task_stays_hidden_through_activity_and_seed_until_explicit_resum
|
|||||||
last_messages: HashMap::new(),
|
last_messages: HashMap::new(),
|
||||||
recent_seed_complete: true,
|
recent_seed_complete: true,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
pinned_thread_ids: None,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -865,6 +912,7 @@ async fn lifecycle_removes_background_and_current_tasks_without_losing_the_dashb
|
|||||||
last_messages: HashMap::new(),
|
last_messages: HashMap::new(),
|
||||||
recent_seed_complete: true,
|
recent_seed_complete: true,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
pinned_thread_ids: None,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -967,6 +1015,7 @@ async fn lifecycle_removes_background_and_current_tasks_without_losing_the_dashb
|
|||||||
last_messages: HashMap::new(),
|
last_messages: HashMap::new(),
|
||||||
recent_seed_complete: true,
|
recent_seed_complete: true,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
pinned_thread_ids: None,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
Box::pin(app.handle_event(&mut tui, &mut app_server, confirmed)).await?;
|
Box::pin(app.handle_event(&mut tui, &mut app_server, confirmed)).await?;
|
||||||
|
|||||||
@@ -12,11 +12,82 @@ use codex_app_server_protocol::ThreadListResponse;
|
|||||||
use codex_app_server_protocol::ThreadSortKey;
|
use codex_app_server_protocol::ThreadSortKey;
|
||||||
use codex_app_server_protocol::ThreadSourceKind;
|
use codex_app_server_protocol::ThreadSourceKind;
|
||||||
use codex_protocol::protocol::SubAgentSource;
|
use codex_protocol::protocol::SubAgentSource;
|
||||||
|
use codex_state::PINNED_THREAD_SECTION_ID;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const PAGE_SIZE: u32 = 10;
|
const PAGE_SIZE: u32 = 10;
|
||||||
|
|
||||||
|
pub(crate) async fn list_pinned_threads(
|
||||||
|
handle: &AppServerRequestHandle,
|
||||||
|
) -> Result<Option<Vec<Thread>>, String> {
|
||||||
|
let mut threads = Vec::new();
|
||||||
|
for source_kinds in [
|
||||||
|
Vec::new(),
|
||||||
|
vec![ThreadSourceKind::Exec, ThreadSourceKind::AppServer],
|
||||||
|
] {
|
||||||
|
let mut cursor = None;
|
||||||
|
loop {
|
||||||
|
let result = handle
|
||||||
|
.request_typed::<ThreadListResponse>(ClientRequest::ThreadList {
|
||||||
|
request_id: RequestId::String(Uuid::new_v4().to_string()),
|
||||||
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
|
cursor: cursor.clone(),
|
||||||
|
limit: Some(100),
|
||||||
|
sort_key: Some(ThreadSortKey::SectionPosition),
|
||||||
|
sort_direction: None,
|
||||||
|
model_providers: Some(Vec::new()),
|
||||||
|
source_kinds: Some(source_kinds.clone()),
|
||||||
|
archived: Some(false),
|
||||||
|
section_id: Some(Some(PINNED_THREAD_SECTION_ID.to_string())),
|
||||||
|
project_id: None,
|
||||||
|
parent_thread_id: None,
|
||||||
|
ancestor_thread_id: None,
|
||||||
|
cwd: None,
|
||||||
|
use_state_db_only: true,
|
||||||
|
search_term: None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(page) => {
|
||||||
|
threads.extend(page.data.into_iter().filter(|thread| {
|
||||||
|
is_overview_thread(thread) && supports_shared_pinning(&thread.source)
|
||||||
|
}));
|
||||||
|
let Some(next_cursor) = page.next_cursor else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
cursor = Some(next_cursor);
|
||||||
|
}
|
||||||
|
Err(TypedRequestError::Server { source, .. })
|
||||||
|
if matches!(source.code, -32602..=-32600) =>
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(threads))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_overview_thread(thread: &Thread) -> bool {
|
||||||
|
!thread.ephemeral
|
||||||
|
&& thread.parent_thread_id.is_none()
|
||||||
|
&& !matches!(
|
||||||
|
thread.source,
|
||||||
|
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn supports_shared_pinning(source: &SessionSource) -> bool {
|
||||||
|
matches!(
|
||||||
|
source,
|
||||||
|
SessionSource::Cli | SessionSource::VsCode | SessionSource::Exec | SessionSource::AppServer
|
||||||
|
) || matches!(source, SessionSource::Custom(source) if matches!(source.as_str(), "atlas" | "chatgpt"))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct SourcePage {
|
struct SourcePage {
|
||||||
cursor: Option<String>,
|
cursor: Option<String>,
|
||||||
@@ -128,14 +199,8 @@ impl SourcePage {
|
|||||||
Ok(page) => {
|
Ok(page) => {
|
||||||
self.exhausted = page.next_cursor.is_none();
|
self.exhausted = page.next_cursor.is_none();
|
||||||
self.cursor = page.next_cursor;
|
self.cursor = page.next_cursor;
|
||||||
self.rows.extend(page.data.into_iter().filter(|thread| {
|
self.rows
|
||||||
!thread.ephemeral
|
.extend(page.data.into_iter().filter(is_overview_thread));
|
||||||
&& thread.parent_thread_id.is_none()
|
|
||||||
&& !matches!(
|
|
||||||
thread.source,
|
|
||||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ use futures::SinkExt;
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
|
||||||
@@ -47,12 +50,21 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
let reopened_pin = overview_thread(
|
||||||
|
ThreadId::new(),
|
||||||
|
/*parent_thread_id*/ None,
|
||||||
|
"Pinned after reopen",
|
||||||
|
ThreadStatus::NotLoaded,
|
||||||
|
);
|
||||||
|
let reopened_pin_id = ThreadId::from_string(&reopened_pin.id).unwrap();
|
||||||
|
let show_reopened_pin = Arc::new(AtomicBool::new(false));
|
||||||
let expected_first_twenty: HashSet<_> = rows[1..21]
|
let expected_first_twenty: HashSet<_> = rows[1..21]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| ThreadId::from_string(&row.id).unwrap())
|
.map(|row| ThreadId::from_string(&row.id).unwrap())
|
||||||
.collect();
|
.collect();
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||||
let endpoint = crate::resolve_remote_addr(&format!("ws://{}", listener.local_addr()?))?;
|
let endpoint = crate::resolve_remote_addr(&format!("ws://{}", listener.local_addr()?))?;
|
||||||
|
let server_show_reopened_pin = Arc::clone(&show_reopened_pin);
|
||||||
let server = tokio::spawn(async move {
|
let server = tokio::spawn(async move {
|
||||||
let (stream, _) = listener.accept().await?;
|
let (stream, _) = listener.accept().await?;
|
||||||
let mut socket = tokio_tungstenite::accept_async(stream).await?;
|
let mut socket = tokio_tungstenite::accept_async(stream).await?;
|
||||||
@@ -66,6 +78,34 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
|||||||
"initialize" => json!({"userAgent": "overview-test/1.0"}),
|
"initialize" => json!({"userAgent": "overview-test/1.0"}),
|
||||||
"thread/loaded/list" => json!({"data": [], "nextCursor": null}),
|
"thread/loaded/list" => json!({"data": [], "nextCursor": null}),
|
||||||
"thread/list" => {
|
"thread/list" => {
|
||||||
|
if params["sortKey"] == "section_position" {
|
||||||
|
assert_eq!(params["sectionId"], codex_state::PINNED_THREAD_SECTION_ID);
|
||||||
|
let interactive = params["sourceKinds"] == json!([]);
|
||||||
|
let cursor = params["cursor"].as_str();
|
||||||
|
let reopened = server_show_reopened_pin.load(Ordering::SeqCst);
|
||||||
|
let data = if reopened && interactive && cursor.is_none() {
|
||||||
|
vec![reopened_pin.clone()]
|
||||||
|
} else if interactive && cursor == Some("pinned-page-2") {
|
||||||
|
vec![rows[1].clone()]
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
socket
|
||||||
|
.send(Message::Text(
|
||||||
|
json!({
|
||||||
|
"id": request.id,
|
||||||
|
"result": {
|
||||||
|
"data": data,
|
||||||
|
"nextCursor": (!reopened && interactive && cursor.is_none())
|
||||||
|
.then_some("pinned-page-2"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
assert_eq!(params["sortKey"], "recency_at");
|
assert_eq!(params["sortKey"], "recency_at");
|
||||||
assert_eq!(params["limit"], 10);
|
assert_eq!(params["limit"], 10);
|
||||||
{
|
{
|
||||||
@@ -113,6 +153,7 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
|||||||
app.refresh_agents_overview_threads(&session);
|
app.refresh_agents_overview_threads(&session);
|
||||||
finish_overview_refresh(&mut app, &session, &mut rx).await;
|
finish_overview_refresh(&mut app, &session, &mut rx).await;
|
||||||
assert_eq!(app.agents_overview.threads.len(), 10);
|
assert_eq!(app.agents_overview.threads.len(), 10);
|
||||||
|
assert_eq!(app.agents_overview.pinned_thread_ids, Some(vec![ids[1]]));
|
||||||
assert!(app.agents_overview.discovery.has_more());
|
assert!(app.agents_overview.discovery.has_more());
|
||||||
app.track_agents_overview_notification(&ServerNotification::ThreadArchived(
|
app.track_agents_overview_notification(&ServerNotification::ThreadArchived(
|
||||||
ThreadArchivedNotification {
|
ThreadArchivedNotification {
|
||||||
@@ -162,6 +203,17 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
|||||||
finish_overview_refresh(&mut app, &session, &mut rx).await;
|
finish_overview_refresh(&mut app, &session, &mut rx).await;
|
||||||
assert_eq!(app.agents_overview.threads, expected);
|
assert_eq!(app.agents_overview.threads, expected);
|
||||||
assert!(!app.agents_overview.discovery.has_more());
|
assert!(!app.agents_overview.discovery.has_more());
|
||||||
|
app.agents_overview.threads.clear();
|
||||||
|
app.agents_overview.pinned_thread_ids = Some(Vec::new());
|
||||||
|
app.agents_overview.refill_count = 0;
|
||||||
|
show_reopened_pin.store(true, Ordering::SeqCst);
|
||||||
|
app.refresh_agents_overview_threads(&session);
|
||||||
|
finish_overview_refresh(&mut app, &session, &mut rx).await;
|
||||||
|
assert_eq!(
|
||||||
|
app.agents_overview.pinned_thread_ids,
|
||||||
|
Some(vec![reopened_pin_id])
|
||||||
|
);
|
||||||
|
assert!(app.agents_overview.threads.contains_key(&reopened_pin_id));
|
||||||
session.shutdown().await?;
|
session.shutdown().await?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
server.await??,
|
server.await??,
|
||||||
|
|||||||
@@ -18,12 +18,65 @@ pub(super) fn model_name(thread: &Thread) -> &str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentsOverviewView {
|
impl AgentsOverviewView {
|
||||||
|
pub(super) fn is_pinned(&self, index: usize) -> bool {
|
||||||
|
self.pinned_thread_ranks
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|ranks| ranks.contains_key(&self.rows[index].thread_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn visible_indices(&self) -> Vec<usize> {
|
||||||
|
let state = self.state();
|
||||||
|
let search = state.search.to_lowercase();
|
||||||
|
let (_, status_group) = super::command_center::TASK_FILTERS[state.status_filter];
|
||||||
|
let (mut pinned, mut visible): (Vec<_>, Vec<_>) = self
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, row)| {
|
||||||
|
let searchable = format!(
|
||||||
|
"{} {} {}",
|
||||||
|
row.thread.name.as_deref().unwrap_or_default(),
|
||||||
|
row.thread.preview,
|
||||||
|
row.thread.cwd.display(),
|
||||||
|
)
|
||||||
|
.to_lowercase();
|
||||||
|
((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)
|
||||||
|
})
|
||||||
|
.partition(|index| self.is_pinned(*index));
|
||||||
|
if let Some(ranks) = &self.pinned_thread_ranks {
|
||||||
|
pinned.sort_by_key(|index| ranks.get(&self.rows[*index].thread_id).copied());
|
||||||
|
}
|
||||||
|
match state.grouping {
|
||||||
|
AgentsOverviewGrouping::Project => visible.sort_by_key(|index| {
|
||||||
|
(
|
||||||
|
&self.project_groups[*index].key,
|
||||||
|
std::cmp::Reverse(self.rows[*index].thread.updated_at),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
AgentsOverviewGrouping::Status => {}
|
||||||
|
AgentsOverviewGrouping::Model => visible.sort_by_key(|index| {
|
||||||
|
(
|
||||||
|
model_name(&self.rows[*index].thread),
|
||||||
|
std::cmp::Reverse(self.rows[*index].thread.updated_at),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
pinned.extend(visible);
|
||||||
|
pinned
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn same_group(
|
pub(super) fn same_group(
|
||||||
&self,
|
&self,
|
||||||
grouping: AgentsOverviewGrouping,
|
grouping: AgentsOverviewGrouping,
|
||||||
left: usize,
|
left: usize,
|
||||||
right: usize,
|
right: usize,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
if self.is_pinned(left) || self.is_pinned(right) {
|
||||||
|
return self.is_pinned(left) && self.is_pinned(right);
|
||||||
|
}
|
||||||
match grouping {
|
match grouping {
|
||||||
AgentsOverviewGrouping::Project => {
|
AgentsOverviewGrouping::Project => {
|
||||||
self.project_groups[left].key == self.project_groups[right].key
|
self.project_groups[left].key == self.project_groups[right].key
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ async fn shared_overview_keeps_rows_and_replays_changes_over_stale_reads() -> Re
|
|||||||
]),
|
]),
|
||||||
recent_seed_complete: true,
|
recent_seed_complete: true,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
pinned_thread_ids: None,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
retained_thread.name = Some("New name".to_string());
|
retained_thread.name = Some("New name".to_string());
|
||||||
@@ -396,6 +397,7 @@ async fn shared_overview_keeps_rows_and_replays_changes_over_stale_reads() -> Re
|
|||||||
threads: HashMap::from([(retained, None)]),
|
threads: HashMap::from([(retained, None)]),
|
||||||
recent_seed_complete: true,
|
recent_seed_complete: true,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
pinned_thread_ids: None,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert_eq!(app.agents_overview.threads, expected);
|
assert_eq!(app.agents_overview.threads, expected);
|
||||||
@@ -445,6 +447,7 @@ async fn shared_overview_keeps_rows_and_replays_changes_over_stale_reads() -> Re
|
|||||||
last_messages,
|
last_messages,
|
||||||
recent_seed_complete: true,
|
recent_seed_complete: true,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
pinned_thread_ids: None,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert!(app.agents_overview.last_messages.is_empty());
|
assert!(app.agents_overview.last_messages.is_empty());
|
||||||
@@ -2697,6 +2700,7 @@ async fn command_center_refresh_failure_is_inline_and_clears_on_success() -> Res
|
|||||||
last_messages: HashMap::new(),
|
last_messages: HashMap::new(),
|
||||||
recent_seed_complete: false,
|
recent_seed_complete: false,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
pinned_thread_ids: None,
|
||||||
}),
|
}),
|
||||||
] {
|
] {
|
||||||
let request_id = Uuid::new_v4();
|
let request_id = Uuid::new_v4();
|
||||||
@@ -2728,6 +2732,7 @@ async fn command_center_refresh_failure_is_inline_and_clears_on_success() -> Res
|
|||||||
last_messages: HashMap::new(),
|
last_messages: HashMap::new(),
|
||||||
recent_seed_complete: true,
|
recent_seed_complete: true,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
pinned_thread_ids: None,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert_eq!(render_bottom_popup(&app.chat_widget, /*width*/ 48), before);
|
assert_eq!(render_bottom_popup(&app.chat_widget, /*width*/ 48), before);
|
||||||
@@ -3023,7 +3028,18 @@ async fn command_center_new_actions_use_selection_and_leave_metadata_text_alone(
|
|||||||
ThreadStatus::Idle,
|
ThreadStatus::Idle,
|
||||||
);
|
);
|
||||||
target.cwd = test_path_buf("/tmp/checkout/subdir").abs();
|
target.cwd = test_path_buf("/tmp/checkout/subdir").abs();
|
||||||
|
app.agents_overview.pinned_thread_ids = Some(Vec::new());
|
||||||
let mut view = app.agents_overview_view(vec![target.clone()], Some(id));
|
let mut view = app.agents_overview_view(vec![target.clone()], Some(id));
|
||||||
|
view.handle_key_event(KeyCode::Char('p').into());
|
||||||
|
assert!(matches!(
|
||||||
|
rx.try_recv(),
|
||||||
|
Ok(AppEvent::ToggleAgentsOverviewPin {
|
||||||
|
thread_id,
|
||||||
|
pinned: true,
|
||||||
|
}) if thread_id == id
|
||||||
|
));
|
||||||
|
view.handle_key_event(KeyCode::Char('p').into());
|
||||||
|
assert!(rx.try_recv().is_err());
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
view.handle_key_event(KeyCode::Char('n').into());
|
view.handle_key_event(KeyCode::Char('n').into());
|
||||||
assert!(
|
assert!(
|
||||||
@@ -3040,13 +3056,13 @@ async fn command_center_new_actions_use_selection_and_leave_metadata_text_alone(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
view.handle_key_event(KeyCode::Char('r').into());
|
view.handle_key_event(KeyCode::Char('r').into());
|
||||||
for character in "nwogrxfha".chars() {
|
for character in "nwogrxfhap".chars() {
|
||||||
view.handle_key_event(KeyCode::Char(character).into());
|
view.handle_key_event(KeyCode::Char(character).into());
|
||||||
}
|
}
|
||||||
assert!(rx.try_recv().is_err());
|
assert!(rx.try_recv().is_err());
|
||||||
view.handle_key_event(KeyCode::Enter.into());
|
view.handle_key_event(KeyCode::Enter.into());
|
||||||
assert!(
|
assert!(
|
||||||
matches!(rx.try_recv(), Ok(AppEvent::RenameAgentsOverviewThread { name, .. }) if name.ends_with("nwogrxfha"))
|
matches!(rx.try_recv(), Ok(AppEvent::RenameAgentsOverviewThread { name, .. }) if name.ends_with("nwogrxfhap"))
|
||||||
);
|
);
|
||||||
let mut empty = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
let mut empty = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
|
||||||
empty.handle_key_event(KeyCode::Char('n').into());
|
empty.handle_key_event(KeyCode::Char('n').into());
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
use super::App;
|
use super::App;
|
||||||
use super::agents_overview::AGENTS_OVERVIEW_VIEW_ID;
|
use super::agents_overview::AGENTS_OVERVIEW_VIEW_ID;
|
||||||
use super::agents_overview_details::preview_agent_message;
|
use super::agents_overview_details::preview_agent_message;
|
||||||
|
use super::agents_overview_discovery::list_pinned_threads;
|
||||||
use super::app_server_event_targets::ServerNotificationThreadTarget;
|
use super::app_server_event_targets::ServerNotificationThreadTarget;
|
||||||
use super::app_server_event_targets::server_notification_thread_target;
|
use super::app_server_event_targets::server_notification_thread_target;
|
||||||
use crate::AppServerTarget;
|
use crate::AppServerTarget;
|
||||||
@@ -218,6 +219,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn refresh_agents_overview_threads(&mut self, app_server: &AppServerSession) {
|
pub(super) fn refresh_agents_overview_threads(&mut self, app_server: &AppServerSession) {
|
||||||
|
self.agents_overview.pin_refresh_requested = true;
|
||||||
self.agents_overview.refresh_thread_ids.extend(
|
self.agents_overview.refresh_thread_ids.extend(
|
||||||
self.agents_overview
|
self.agents_overview
|
||||||
.threads
|
.threads
|
||||||
@@ -266,16 +268,19 @@ impl App {
|
|||||||
&& self.agents_overview.refresh_thread_ids.is_empty()
|
&& self.agents_overview.refresh_thread_ids.is_empty()
|
||||||
&& !self.agents_overview.show_more_requested
|
&& !self.agents_overview.show_more_requested
|
||||||
&& !(self.agents_overview.refill_count > 0 && self.agents_overview.discovery.has_more())
|
&& !(self.agents_overview.refill_count > 0 && self.agents_overview.discovery.has_more())
|
||||||
|
&& !self.agents_overview.pin_refresh_requested
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.agents_overview.pin_refresh_requested = false;
|
||||||
let request_id = Uuid::new_v4();
|
let request_id = Uuid::new_v4();
|
||||||
self.agents_overview.request_id = Some(request_id);
|
self.agents_overview.request_id = Some(request_id);
|
||||||
let initialized = self.agents_overview.initialized;
|
let initialized = self.agents_overview.initialized;
|
||||||
let refill = self.agents_overview.refill_count;
|
let refill = self.agents_overview.refill_count;
|
||||||
let show_more =
|
let show_more =
|
||||||
refill == 0 && std::mem::take(&mut self.agents_overview.show_more_requested);
|
refill == 0 && std::mem::take(&mut self.agents_overview.show_more_requested);
|
||||||
|
self.agents_overview.refresh_show_more = show_more;
|
||||||
let discover =
|
let discover =
|
||||||
!initialized || show_more || (refill > 0 && self.agents_overview.discovery.has_more());
|
!initialized || show_more || (refill > 0 && self.agents_overview.discovery.has_more());
|
||||||
let limit = if initialized && refill > 0 {
|
let limit = if initialized && refill > 0 {
|
||||||
@@ -292,6 +297,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut thread_ids = std::mem::take(&mut self.agents_overview.refresh_thread_ids);
|
let mut thread_ids = std::mem::take(&mut self.agents_overview.refresh_thread_ids);
|
||||||
|
self.agents_overview.active_refresh_thread_ids = thread_ids.clone();
|
||||||
let request_handle = app_server.request_handle();
|
let request_handle = app_server.request_handle();
|
||||||
let app_event_tx = self.app_event_tx.clone();
|
let app_event_tx = self.app_event_tx.clone();
|
||||||
self.agents_overview
|
self.agents_overview
|
||||||
@@ -304,6 +310,23 @@ impl App {
|
|||||||
let mut threads = HashMap::new();
|
let mut threads = HashMap::new();
|
||||||
let mut last_messages = HashMap::new();
|
let mut last_messages = HashMap::new();
|
||||||
let mut recent_seed_complete = true;
|
let mut recent_seed_complete = true;
|
||||||
|
let pinned_thread_ids = match list_pinned_threads(&request_handle).await {
|
||||||
|
Ok(Some(pinned_threads)) => {
|
||||||
|
let mut ids = Vec::with_capacity(pinned_threads.len());
|
||||||
|
for thread in pinned_threads {
|
||||||
|
if let Ok(thread_id) = ThreadId::from_string(&thread.id) {
|
||||||
|
ids.push(thread_id);
|
||||||
|
threads.insert(thread_id, Some(thread));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Some(ids))
|
||||||
|
}
|
||||||
|
Ok(None) => Some(None),
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(%error, "failed to list pinned agent threads");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
if let Some(discovery) = &mut discovery {
|
if let Some(discovery) = &mut discovery {
|
||||||
let loaded = async {
|
let loaded = async {
|
||||||
if initialized {
|
if initialized {
|
||||||
@@ -423,6 +446,7 @@ impl App {
|
|||||||
last_messages,
|
last_messages,
|
||||||
recent_seed_complete,
|
recent_seed_complete,
|
||||||
discovery,
|
discovery,
|
||||||
|
pinned_thread_ids,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ async fn selected_usage_is_cached_and_account_changes_discard_old_results() -> R
|
|||||||
last_messages: HashMap::new(),
|
last_messages: HashMap::new(),
|
||||||
recent_seed_complete: true,
|
recent_seed_complete: true,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
pinned_thread_ids: None,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use grouping::model_name;
|
|||||||
|
|
||||||
use super::agents_overview::AGENTS_OVERVIEW_VIEW_ID;
|
use super::agents_overview::AGENTS_OVERVIEW_VIEW_ID;
|
||||||
use super::agents_overview_details::AgentsOverviewDetails;
|
use super::agents_overview_details::AgentsOverviewDetails;
|
||||||
|
use super::agents_overview_discovery::supports_shared_pinning;
|
||||||
use crate::app_event::AgentsOverviewAction;
|
use crate::app_event::AgentsOverviewAction;
|
||||||
use crate::app_event::AppEvent;
|
use crate::app_event::AppEvent;
|
||||||
use crate::app_event_sender::AppEventSender;
|
use crate::app_event_sender::AppEventSender;
|
||||||
@@ -54,6 +55,7 @@ use ratatui::text::Span;
|
|||||||
use ratatui::widgets::Clear;
|
use ratatui::widgets::Clear;
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::widgets::Widget;
|
use ratatui::widgets::Widget;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
@@ -181,6 +183,8 @@ pub(super) struct AgentsOverviewView {
|
|||||||
use_theme_colors: bool,
|
use_theme_colors: bool,
|
||||||
pub(super) rows: Vec<AgentsOverviewRow>,
|
pub(super) rows: Vec<AgentsOverviewRow>,
|
||||||
project_groups: Vec<AgentsOverviewProjectGroup>,
|
project_groups: Vec<AgentsOverviewProjectGroup>,
|
||||||
|
pub(super) pinned_thread_ranks: Option<HashMap<ThreadId, usize>>,
|
||||||
|
pub(super) pin_action_pending: bool,
|
||||||
selected: usize,
|
selected: usize,
|
||||||
state: Arc<Mutex<AgentsOverviewViewState>>,
|
state: Arc<Mutex<AgentsOverviewViewState>>,
|
||||||
app_event_tx: AppEventSender,
|
app_event_tx: AppEventSender,
|
||||||
@@ -251,6 +255,8 @@ impl AgentsOverviewView {
|
|||||||
use_theme_colors,
|
use_theme_colors,
|
||||||
rows,
|
rows,
|
||||||
project_groups,
|
project_groups,
|
||||||
|
pinned_thread_ranks: None,
|
||||||
|
pin_action_pending: false,
|
||||||
selected,
|
selected,
|
||||||
state,
|
state,
|
||||||
app_event_tx,
|
app_event_tx,
|
||||||
@@ -302,44 +308,12 @@ impl AgentsOverviewView {
|
|||||||
.filter(|_| self.visible_indices().contains(&self.selected))
|
.filter(|_| self.visible_indices().contains(&self.selected))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visible_indices(&self) -> Vec<usize> {
|
fn can_toggle_selected_pin(&self) -> bool {
|
||||||
let state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
|
!self.pin_action_pending
|
||||||
let search = state.search.to_lowercase();
|
&& self.pinned_thread_ranks.is_some()
|
||||||
let (_, status_group) = command_center::TASK_FILTERS[state.status_filter];
|
&& self
|
||||||
let mut visible = self
|
.selected_row()
|
||||||
.rows
|
.is_some_and(|row| supports_shared_pinning(&row.thread.source))
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(index, row)| {
|
|
||||||
let searchable = format!(
|
|
||||||
"{} {} {}",
|
|
||||||
row.thread.name.as_deref().unwrap_or_default(),
|
|
||||||
row.thread.preview,
|
|
||||||
row.thread.cwd.display(),
|
|
||||||
)
|
|
||||||
.to_lowercase();
|
|
||||||
((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 {
|
|
||||||
AgentsOverviewGrouping::Project => visible.sort_by_key(|index| {
|
|
||||||
(
|
|
||||||
&self.project_groups[*index].key,
|
|
||||||
std::cmp::Reverse(self.rows[*index].thread.updated_at),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
AgentsOverviewGrouping::Status => {}
|
|
||||||
AgentsOverviewGrouping::Model => visible.sort_by_key(|index| {
|
|
||||||
(
|
|
||||||
model_name(&self.rows[*index].thread),
|
|
||||||
std::cmp::Reverse(self.rows[*index].thread.updated_at),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
visible
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_selection(&mut self, forward: bool) {
|
fn move_selection(&mut self, forward: bool) {
|
||||||
@@ -682,6 +656,18 @@ impl BottomPaneView for AgentsOverviewView {
|
|||||||
.send(AppEvent::PersistAgentsOverviewGrouping(state.grouping));
|
.send(AppEvent::PersistAgentsOverviewGrouping(state.grouping));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if self.agents_keymap.toggle_pin.is_pressed(key) {
|
||||||
|
if self.can_toggle_selected_pin()
|
||||||
|
&& let (Some(ranks), Some(row)) = (&self.pinned_thread_ranks, self.selected_row())
|
||||||
|
{
|
||||||
|
let thread_id = row.thread_id;
|
||||||
|
let pinned = !ranks.contains_key(&thread_id);
|
||||||
|
self.pin_action_pending = true;
|
||||||
|
self.app_event_tx
|
||||||
|
.send(AppEvent::ToggleAgentsOverviewPin { thread_id, pinned });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.agents_keymap.new_task.is_pressed(key) {
|
if self.agents_keymap.new_task.is_pressed(key) {
|
||||||
self.app_event_tx.send(AppEvent::NewAgentsOverviewSession {
|
self.app_event_tx.send(AppEvent::NewAgentsOverviewSession {
|
||||||
cwd: self.selected_row().map(|row| row.thread.cwd.clone()),
|
cwd: self.selected_row().map(|row| row.thread.cwd.clone()),
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ impl App {
|
|||||||
task.abort();
|
task.abort();
|
||||||
}
|
}
|
||||||
self.agents_overview.request_id = None;
|
self.agents_overview.request_id = None;
|
||||||
|
self.agents_overview.refresh_show_more = false;
|
||||||
|
self.agents_overview.active_refresh_thread_ids.clear();
|
||||||
self.agents_overview.refresh_pending = false;
|
self.agents_overview.refresh_pending = false;
|
||||||
self.agents_overview.initialized = false;
|
self.agents_overview.initialized = false;
|
||||||
self.agents_overview.refresh_notifications.clear();
|
self.agents_overview.refresh_notifications.clear();
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ impl App {
|
|||||||
| AppEvent::FinishPromptRevert { .. }
|
| AppEvent::FinishPromptRevert { .. }
|
||||||
| AppEvent::ManagedWorktreeCreated(_)
|
| AppEvent::ManagedWorktreeCreated(_)
|
||||||
| AppEvent::AgentsOverviewWorktreeCreated(_)
|
| AppEvent::AgentsOverviewWorktreeCreated(_)
|
||||||
|
| AppEvent::AgentsOverviewPinToggled { .. }
|
||||||
| AppEvent::AppendMessageHistoryEntry { .. }
|
| AppEvent::AppendMessageHistoryEntry { .. }
|
||||||
| AppEvent::BeginInitialHistoryReplayBuffer
|
| AppEvent::BeginInitialHistoryReplayBuffer
|
||||||
| AppEvent::BeginThreadSwitchHistoryReplayBuffer
|
| AppEvent::BeginThreadSwitchHistoryReplayBuffer
|
||||||
@@ -2830,6 +2831,19 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AppEvent::ToggleAgentsOverviewPin { thread_id, pinned } => {
|
||||||
|
self.toggle_agents_overview_pin(app_server, thread_id, pinned);
|
||||||
|
}
|
||||||
|
AppEvent::AgentsOverviewPinToggled {
|
||||||
|
request_id,
|
||||||
|
thread_id,
|
||||||
|
pinned,
|
||||||
|
result,
|
||||||
|
} => {
|
||||||
|
self.complete_agents_overview_pin(
|
||||||
|
app_server, request_id, thread_id, pinned, result,
|
||||||
|
);
|
||||||
|
}
|
||||||
AppEvent::SuggestThreadName {
|
AppEvent::SuggestThreadName {
|
||||||
thread_id,
|
thread_id,
|
||||||
request_id,
|
request_id,
|
||||||
|
|||||||
@@ -214,8 +214,11 @@ impl App {
|
|||||||
task.abort();
|
task.abort();
|
||||||
}
|
}
|
||||||
self.agents_overview.request_id = None;
|
self.agents_overview.request_id = None;
|
||||||
|
self.agents_overview.refresh_show_more = false;
|
||||||
|
self.agents_overview.active_refresh_thread_ids.clear();
|
||||||
self.agents_overview.refresh_pending = false;
|
self.agents_overview.refresh_pending = false;
|
||||||
self.agents_overview.refresh_notifications.clear();
|
self.agents_overview.refresh_notifications.clear();
|
||||||
|
self.agents_overview.pending_pin_change = None;
|
||||||
self.agents_overview.pending_usage = None;
|
self.agents_overview.pending_usage = None;
|
||||||
self.agents_overview.usage_disabled = false;
|
self.agents_overview.usage_disabled = false;
|
||||||
self.agents_overview.usage.clear();
|
self.agents_overview.usage.clear();
|
||||||
@@ -357,6 +360,8 @@ impl App {
|
|||||||
self.sync_thread_title_progress();
|
self.sync_thread_title_progress();
|
||||||
self.agents_overview.dispatched_requests.clear();
|
self.agents_overview.dispatched_requests.clear();
|
||||||
self.agents_overview.request_id = None;
|
self.agents_overview.request_id = None;
|
||||||
|
self.agents_overview.refresh_show_more = false;
|
||||||
|
self.agents_overview.active_refresh_thread_ids.clear();
|
||||||
self.agents_overview.refresh_pending = false;
|
self.agents_overview.refresh_pending = false;
|
||||||
for input in self.agents_overview.input_states.values_mut() {
|
for input in self.agents_overview.input_states.values_mut() {
|
||||||
input.reconnect_pending = true;
|
input.reconnect_pending = true;
|
||||||
|
|||||||
+7
-7
@@ -6,15 +6,15 @@ expression: "screen(&view, 160, 22)"
|
|||||||
All 6 Needs you 2 Working 1 Ready 2 Inactive 1 tab/⇧tab filter
|
All 6 Needs you 2 Working 1 Ready 2 Inactive 1 tab/⇧tab filter
|
||||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Tasks Status Updated │ Task details
|
Tasks Status Updated │ Task details
|
||||||
/tmp/project 6 │
|
Pinned 2 │
|
||||||
› ○ Current task Ready now │ Current task
|
› ○ Current task Ready now │ Current task
|
||||||
○ A longer task title that uses the available column voice Ready 5m ago │ ○ Ready
|
● Needs an answer Needs input 2h ago │ ○ Ready
|
||||||
|
│
|
||||||
|
/tmp/project 4 │ Model: Unknown
|
||||||
|
○ A longer task title that uses the available column voice Ready 5m ago │ Reasoning: Unknown
|
||||||
! Failed task Error 1h ago │
|
! Failed task Error 1h ago │
|
||||||
● Needs an answer Needs input 2h ago │ Model: Unknown
|
● Working task Working 1d ago │ Project
|
||||||
● Working task Working 1d ago │ Reasoning: Unknown
|
○ Unloaded task Inactive 2d ago │ /tmp/project
|
||||||
○ Unloaded task Inactive 2d ago │
|
|
||||||
│ Project
|
|
||||||
│ /tmp/project
|
|
||||||
│
|
│
|
||||||
│ Prompt
|
│ Prompt
|
||||||
│ Current task
|
│ Current task
|
||||||
|
|||||||
+6
-7
@@ -1,22 +1,21 @@
|
|||||||
---
|
---
|
||||||
source: tui/src/app/agent_center_tests.rs
|
source: tui/src/app/agent_center_tests.rs
|
||||||
assertion_line: 450
|
|
||||||
expression: "screen(&view, 100, 24)"
|
expression: "screen(&view, 100, 24)"
|
||||||
---
|
---
|
||||||
Agent command center Group: Project g
|
Agent command center Group: Project g
|
||||||
All 0 Needs you 0 Working 0 Ready 0 Inactive 0 tab/⇧tab filter
|
All 1 Needs you 0 Working 0 Ready 1 Inactive 0 tab/⇧tab filter
|
||||||
────────────────────────────────────────────────────────────────────────────────────────────────
|
────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
Task shortcuts
|
Task shortcuts
|
||||||
|
|
||||||
Navigate Tasks View
|
Navigate Tasks View
|
||||||
k Up n New tab/⇧tab Filter
|
k Up n New tab/⇧tab Filter
|
||||||
j Down o Resume f3 Search
|
j Down o Resume f3 Search
|
||||||
enter Open f Fork g Group
|
enter Open f Fork g Group
|
||||||
pgdn Page down z r Rename
|
pgdn Page down z r Rename
|
||||||
⌃c Quit x Stop
|
⌃c Quit x Stop
|
||||||
h Hide
|
h Hide
|
||||||
backspace Delete
|
backspace Delete
|
||||||
|
z p Pin/unpin
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ async fn reconnect_daemon_command_center_after_socket_replacement_without_a_conv
|
|||||||
threads: HashMap::new(),
|
threads: HashMap::new(),
|
||||||
recent_seed_complete: false,
|
recent_seed_complete: false,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
pinned_thread_ids: None,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert_eq!(app.agents_overview.visible_thread_ids.len(), 2);
|
assert_eq!(app.agents_overview.visible_thread_ids.len(), 2);
|
||||||
@@ -436,6 +437,7 @@ async fn reconnect_daemon_command_center_after_socket_replacement_without_a_conv
|
|||||||
threads: stale_threads,
|
threads: stale_threads,
|
||||||
recent_seed_complete: true,
|
recent_seed_complete: true,
|
||||||
discovery: None,
|
discovery: None,
|
||||||
|
pinned_thread_ids: None,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert!(!app.agents_overview.visible_thread_ids.contains(&vanished));
|
assert!(!app.agents_overview.visible_thread_ids.contains(&vanished));
|
||||||
|
|||||||
@@ -634,6 +634,7 @@ pub(super) async fn start_recording_app_server_with_realtime_speech(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if request.method == "thread/list"
|
} else if request.method == "thread/list"
|
||||||
|
&& params.is_some_and(|params| params["sortKey"] == "recency_at")
|
||||||
&& std::mem::take(&mut reject_thread_list)
|
&& std::mem::take(&mut reject_thread_list)
|
||||||
{
|
{
|
||||||
JSONRPCMessage::Error(JSONRPCError {
|
JSONRPCMessage::Error(JSONRPCError {
|
||||||
@@ -3297,6 +3298,7 @@ async fn agents_overview_seeds_loaded_threads_when_recent_listing_is_unavailable
|
|||||||
let mut sort_keys = list_requests
|
let mut sort_keys = list_requests
|
||||||
.iter()
|
.iter()
|
||||||
.map(|params| params["sortKey"].as_str().unwrap())
|
.map(|params| params["sortKey"].as_str().unwrap())
|
||||||
|
.filter(|sort_key| *sort_key != "section_position")
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
sort_keys.sort_unstable();
|
sort_keys.sort_unstable();
|
||||||
assert_eq!(sort_keys, expected_sort_keys);
|
assert_eq!(sort_keys, expected_sort_keys);
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ pub(crate) struct AgentsOverviewThreadRefresh {
|
|||||||
pub(crate) last_messages: std::collections::HashMap<ThreadId, String>,
|
pub(crate) last_messages: std::collections::HashMap<ThreadId, String>,
|
||||||
pub(crate) recent_seed_complete: bool,
|
pub(crate) recent_seed_complete: bool,
|
||||||
pub(crate) discovery: Option<crate::app::agents_overview_discovery::AgentsOverviewDiscovery>,
|
pub(crate) discovery: Option<crate::app::agents_overview_discovery::AgentsOverviewDiscovery>,
|
||||||
|
pub(crate) pinned_thread_ids: Option<Option<Vec<ThreadId>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
@@ -335,6 +336,18 @@ pub(crate) enum AppEvent {
|
|||||||
thread_id: ThreadId,
|
thread_id: ThreadId,
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
|
/// Move a task into or out of the shared pinned section.
|
||||||
|
ToggleAgentsOverviewPin {
|
||||||
|
thread_id: ThreadId,
|
||||||
|
pinned: bool,
|
||||||
|
},
|
||||||
|
/// Finish moving a task into or out of the shared pinned section.
|
||||||
|
AgentsOverviewPinToggled {
|
||||||
|
request_id: Uuid,
|
||||||
|
thread_id: ThreadId,
|
||||||
|
pinned: bool,
|
||||||
|
result: Result<(), String>,
|
||||||
|
},
|
||||||
/// Generate an editable title suggestion for the active rename prompt.
|
/// Generate an editable title suggestion for the active rename prompt.
|
||||||
SuggestThreadName {
|
SuggestThreadName {
|
||||||
thread_id: ThreadId,
|
thread_id: ThreadId,
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ pub(crate) struct AgentsKeymap {
|
|||||||
pub(crate) archive: Vec<KeyBinding>,
|
pub(crate) archive: Vec<KeyBinding>,
|
||||||
pub(crate) delete: Vec<KeyBinding>,
|
pub(crate) delete: Vec<KeyBinding>,
|
||||||
pub(crate) hide: Vec<KeyBinding>,
|
pub(crate) hide: Vec<KeyBinding>,
|
||||||
|
pub(crate) toggle_pin: Vec<KeyBinding>,
|
||||||
pub(crate) toggle_grouping: Vec<KeyBinding>,
|
pub(crate) toggle_grouping: Vec<KeyBinding>,
|
||||||
chord_hints: Arc<RuntimeChordKeymap>,
|
chord_hints: Arc<RuntimeChordKeymap>,
|
||||||
}
|
}
|
||||||
@@ -1361,6 +1362,7 @@ impl RuntimeKeymap {
|
|||||||
archive: resolve_local!(keymap, defaults, agents, archive),
|
archive: resolve_local!(keymap, defaults, agents, archive),
|
||||||
delete: resolve_local!(keymap, defaults, agents, delete),
|
delete: resolve_local!(keymap, defaults, agents, delete),
|
||||||
hide: resolve_local!(keymap, defaults, agents, hide),
|
hide: resolve_local!(keymap, defaults, agents, hide),
|
||||||
|
toggle_pin: resolve_local!(keymap, defaults, agents, toggle_pin),
|
||||||
toggle_grouping: resolve_local!(keymap, defaults, agents, toggle_grouping),
|
toggle_grouping: resolve_local!(keymap, defaults, agents, toggle_grouping),
|
||||||
chord_hints: Arc::clone(&chords),
|
chord_hints: Arc::clone(&chords),
|
||||||
};
|
};
|
||||||
@@ -1380,6 +1382,7 @@ impl RuntimeKeymap {
|
|||||||
(keymap.agents.archive.as_ref(), &mut agents.archive),
|
(keymap.agents.archive.as_ref(), &mut agents.archive),
|
||||||
(keymap.agents.delete.as_ref(), &mut agents.delete),
|
(keymap.agents.delete.as_ref(), &mut agents.delete),
|
||||||
(keymap.agents.hide.as_ref(), &mut agents.hide),
|
(keymap.agents.hide.as_ref(), &mut agents.hide),
|
||||||
|
(keymap.agents.toggle_pin.as_ref(), &mut agents.toggle_pin),
|
||||||
(
|
(
|
||||||
keymap.agents.toggle_grouping.as_ref(),
|
keymap.agents.toggle_grouping.as_ref(),
|
||||||
&mut agents.toggle_grouping,
|
&mut agents.toggle_grouping,
|
||||||
@@ -1546,6 +1549,7 @@ impl RuntimeKeymap {
|
|||||||
(keymap.agents.archive.as_ref(), &mut agents.archive),
|
(keymap.agents.archive.as_ref(), &mut agents.archive),
|
||||||
(keymap.agents.delete.as_ref(), &mut agents.delete),
|
(keymap.agents.delete.as_ref(), &mut agents.delete),
|
||||||
(keymap.agents.hide.as_ref(), &mut agents.hide),
|
(keymap.agents.hide.as_ref(), &mut agents.hide),
|
||||||
|
(keymap.agents.toggle_pin.as_ref(), &mut agents.toggle_pin),
|
||||||
(
|
(
|
||||||
keymap.agents.toggle_grouping.as_ref(),
|
keymap.agents.toggle_grouping.as_ref(),
|
||||||
&mut agents.toggle_grouping,
|
&mut agents.toggle_grouping,
|
||||||
@@ -1920,6 +1924,7 @@ impl RuntimeKeymap {
|
|||||||
archive: default_bindings![plain(KeyCode::Char('a'))],
|
archive: default_bindings![plain(KeyCode::Char('a'))],
|
||||||
delete: default_bindings![plain(KeyCode::Backspace)],
|
delete: default_bindings![plain(KeyCode::Backspace)],
|
||||||
hide: default_bindings![plain(KeyCode::Char('h'))],
|
hide: default_bindings![plain(KeyCode::Char('h'))],
|
||||||
|
toggle_pin: default_bindings![plain(KeyCode::Char('p'))],
|
||||||
toggle_grouping: default_bindings![plain(KeyCode::Char('g'))],
|
toggle_grouping: default_bindings![plain(KeyCode::Char('g'))],
|
||||||
chord_hints: Arc::default(),
|
chord_hints: Arc::default(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -428,6 +428,7 @@ define_runtime_action_bindings! {
|
|||||||
archive,
|
archive,
|
||||||
delete,
|
delete,
|
||||||
hide,
|
hide,
|
||||||
|
toggle_pin,
|
||||||
toggle_grouping,
|
toggle_grouping,
|
||||||
],
|
],
|
||||||
"approval" => Approval, approval, approval [
|
"approval" => Approval, approval, approval [
|
||||||
|
|||||||
Reference in New Issue
Block a user