mirror of
https://github.com/openai/codex.git
synced 2026-10-19 08:16:48 +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>,
|
||||
/// Hide the selected task until explicitly resumed or the TUI restarts.
|
||||
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.
|
||||
pub toggle_grouping: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
@@ -4615,7 +4615,8 @@
|
||||
"resume": null,
|
||||
"search": null,
|
||||
"stop": null,
|
||||
"toggle_grouping": null
|
||||
"toggle_grouping": null,
|
||||
"toggle_pin": null
|
||||
},
|
||||
"approval": {
|
||||
"approve": null,
|
||||
@@ -5040,6 +5041,14 @@
|
||||
}
|
||||
],
|
||||
"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"
|
||||
@@ -5569,7 +5578,8 @@
|
||||
"resume": null,
|
||||
"search": null,
|
||||
"stop": null,
|
||||
"toggle_grouping": null
|
||||
"toggle_grouping": null,
|
||||
"toggle_pin": null
|
||||
}
|
||||
},
|
||||
"approval": {
|
||||
|
||||
@@ -35,6 +35,7 @@ impl AgentsOverviewView {
|
||||
&self.agents_keymap.archive,
|
||||
&self.agents_keymap.delete,
|
||||
&self.agents_keymap.hide,
|
||||
&self.agents_keymap.toggle_pin,
|
||||
&self.agents_keymap.toggle_grouping,
|
||||
].into_iter().any(|bindings| bindings.contains(binding)))
|
||||
})
|
||||
@@ -89,8 +90,11 @@ impl AgentsOverviewView {
|
||||
("archive", &self.agents_keymap.archive, "Archive"),
|
||||
("hide", &self.agents_keymap.hide, "Hide"),
|
||||
("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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,15 +140,19 @@ impl AgentsOverviewView {
|
||||
let task = &self.rows[index];
|
||||
let rect = row(viewport, offset as u16, /*height*/ 1);
|
||||
if matches!(entry, CenterRow::Group(_)) {
|
||||
let group = match state.grouping {
|
||||
AgentsOverviewGrouping::Project => {
|
||||
format_directory_display(
|
||||
&self.project_groups[index].heading,
|
||||
/*max_width*/ None,
|
||||
)
|
||||
let group = if self.is_pinned(index) {
|
||||
"Pinned".to_owned()
|
||||
} else {
|
||||
match state.grouping {
|
||||
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
|
||||
.iter()
|
||||
@@ -164,7 +168,9 @@ impl AgentsOverviewView {
|
||||
} else {
|
||||
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(
|
||||
&group,
|
||||
usize::from(rect.width)
|
||||
|
||||
@@ -75,8 +75,10 @@ async fn live_center_columns() {
|
||||
ThreadStatus::Idle,
|
||||
));
|
||||
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));
|
||||
insta::assert_snapshot!(screen(&view, /*width*/ 160, /*height*/ 22));
|
||||
view.pinned_thread_ranks = None;
|
||||
let mut selected_status_styles = Vec::new();
|
||||
for _ in 0..fixtures.len() {
|
||||
let area = Rect::new(
|
||||
@@ -466,15 +468,27 @@ page_up = []
|
||||
[agents]
|
||||
archive = []
|
||||
rename = 'z r'
|
||||
toggle_pin = 'z p'
|
||||
"#,
|
||||
)
|
||||
.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!(
|
||||
"live_center_custom_footer",
|
||||
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());
|
||||
insta::assert_snapshot!(
|
||||
"live_center_custom_help",
|
||||
|
||||
@@ -49,11 +49,16 @@ pub(super) struct AgentsOverviewState {
|
||||
pub(super) initialized: bool,
|
||||
pub(super) discovery: super::agents_overview_discovery::AgentsOverviewDiscovery,
|
||||
pub(super) show_more_requested: bool,
|
||||
pub(super) refresh_show_more: bool,
|
||||
/// Vacancies left by lifecycle removals, filled without expanding the visible window.
|
||||
pub(super) refill_count: usize,
|
||||
pub(super) request_id: Option<Uuid>,
|
||||
pub(super) refresh_pending: bool,
|
||||
pub(super) pin_refresh_requested: bool,
|
||||
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_notifications: HashMap<ThreadId, Vec<ServerNotification>>,
|
||||
pub(super) rendered_full_screen: bool,
|
||||
@@ -170,6 +175,8 @@ impl App {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
let refill_succeeded = result
|
||||
.as_ref()
|
||||
@@ -185,6 +192,9 @@ impl App {
|
||||
}
|
||||
match result {
|
||||
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;
|
||||
if let Some(discovery) = refresh.discovery {
|
||||
if !discovery.has_more() {
|
||||
@@ -380,7 +390,7 @@ impl App {
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.vim_enabled = self.chat_widget.composer_is_vim_enabled();
|
||||
AgentsOverviewView::new(
|
||||
let mut view = AgentsOverviewView::new(
|
||||
rows,
|
||||
selected_thread_id,
|
||||
self.config.features.enabled(Feature::Worktrees)
|
||||
@@ -392,7 +402,16 @@ impl App {
|
||||
self.app_event_tx.clone(),
|
||||
self.keymap.clone(),
|
||||
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(
|
||||
|
||||
@@ -12,10 +12,15 @@ use crate::chatwidget::ChatWidget;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::tui;
|
||||
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::ThreadLoadedListParams;
|
||||
use codex_app_server_protocol::ThreadSectionMoveParams;
|
||||
use codex_app_server_protocol::ThreadSectionMoveResponse;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_state::PINNED_THREAD_SECTION_ID;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Stylize;
|
||||
@@ -25,6 +30,7 @@ use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use tokio_stream::Stream;
|
||||
use tokio_stream::StreamExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
struct LifecycleHeader(Vec<Line<'static>>);
|
||||
|
||||
@@ -104,6 +110,90 @@ async fn run_lifecycle_modal<T>(
|
||||
}
|
||||
|
||||
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(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
@@ -227,6 +317,8 @@ impl App {
|
||||
refresh.abort();
|
||||
}
|
||||
self.agents_overview.request_id = None;
|
||||
self.agents_overview.refresh_show_more = false;
|
||||
self.agents_overview.active_refresh_thread_ids.clear();
|
||||
self.agents_overview
|
||||
.view_state
|
||||
.lock()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::app::agents_overview_discovery::list_pinned_threads;
|
||||
use crate::app_event::AgentsOverviewAction;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
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 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]
|
||||
async fn archive_confirmation_number_keys_act_immediately() {
|
||||
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(),
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
pinned_thread_ids: None,
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -865,6 +912,7 @@ async fn lifecycle_removes_background_and_current_tasks_without_losing_the_dashb
|
||||
last_messages: HashMap::new(),
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
pinned_thread_ids: None,
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -967,6 +1015,7 @@ async fn lifecycle_removes_background_and_current_tasks_without_losing_the_dashb
|
||||
last_messages: HashMap::new(),
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
pinned_thread_ids: None,
|
||||
}),
|
||||
});
|
||||
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::ThreadSourceKind;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_state::PINNED_THREAD_SECTION_ID;
|
||||
use std::collections::VecDeque;
|
||||
use uuid::Uuid;
|
||||
|
||||
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)]
|
||||
struct SourcePage {
|
||||
cursor: Option<String>,
|
||||
@@ -128,14 +199,8 @@ impl SourcePage {
|
||||
Ok(page) => {
|
||||
self.exhausted = page.next_cursor.is_none();
|
||||
self.cursor = page.next_cursor;
|
||||
self.rows.extend(page.data.into_iter().filter(|thread| {
|
||||
!thread.ephemeral
|
||||
&& thread.parent_thread_id.is_none()
|
||||
&& !matches!(
|
||||
thread.source,
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
|
||||
)
|
||||
}));
|
||||
self.rows
|
||||
.extend(page.data.into_iter().filter(is_overview_thread));
|
||||
return true;
|
||||
}
|
||||
Err(error) => {
|
||||
|
||||
@@ -7,6 +7,9 @@ use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
@@ -47,12 +50,21 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
||||
)
|
||||
})
|
||||
.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]
|
||||
.iter()
|
||||
.map(|row| ThreadId::from_string(&row.id).unwrap())
|
||||
.collect();
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
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 (stream, _) = listener.accept().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"}),
|
||||
"thread/loaded/list" => json!({"data": [], "nextCursor": null}),
|
||||
"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["limit"], 10);
|
||||
{
|
||||
@@ -113,6 +153,7 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
||||
app.refresh_agents_overview_threads(&session);
|
||||
finish_overview_refresh(&mut app, &session, &mut rx).await;
|
||||
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());
|
||||
app.track_agents_overview_notification(&ServerNotification::ThreadArchived(
|
||||
ThreadArchivedNotification {
|
||||
@@ -162,6 +203,17 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
||||
finish_overview_refresh(&mut app, &session, &mut rx).await;
|
||||
assert_eq!(app.agents_overview.threads, expected);
|
||||
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?;
|
||||
assert_eq!(
|
||||
server.await??,
|
||||
|
||||
@@ -18,12 +18,65 @@ pub(super) fn model_name(thread: &Thread) -> &str {
|
||||
}
|
||||
|
||||
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(
|
||||
&self,
|
||||
grouping: AgentsOverviewGrouping,
|
||||
left: usize,
|
||||
right: usize,
|
||||
) -> bool {
|
||||
if self.is_pinned(left) || self.is_pinned(right) {
|
||||
return self.is_pinned(left) && self.is_pinned(right);
|
||||
}
|
||||
match grouping {
|
||||
AgentsOverviewGrouping::Project => {
|
||||
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,
|
||||
discovery: None,
|
||||
pinned_thread_ids: None,
|
||||
}),
|
||||
);
|
||||
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)]),
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
pinned_thread_ids: None,
|
||||
}),
|
||||
);
|
||||
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,
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
pinned_thread_ids: None,
|
||||
}),
|
||||
);
|
||||
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(),
|
||||
recent_seed_complete: false,
|
||||
discovery: None,
|
||||
pinned_thread_ids: None,
|
||||
}),
|
||||
] {
|
||||
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(),
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
pinned_thread_ids: None,
|
||||
}),
|
||||
);
|
||||
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,
|
||||
);
|
||||
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));
|
||||
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 {
|
||||
view.handle_key_event(KeyCode::Char('n').into());
|
||||
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());
|
||||
for character in "nwogrxfha".chars() {
|
||||
for character in "nwogrxfhap".chars() {
|
||||
view.handle_key_event(KeyCode::Char(character).into());
|
||||
}
|
||||
assert!(rx.try_recv().is_err());
|
||||
view.handle_key_event(KeyCode::Enter.into());
|
||||
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);
|
||||
empty.handle_key_event(KeyCode::Char('n').into());
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use super::App;
|
||||
use super::agents_overview::AGENTS_OVERVIEW_VIEW_ID;
|
||||
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::server_notification_thread_target;
|
||||
use crate::AppServerTarget;
|
||||
@@ -218,6 +219,7 @@ impl App {
|
||||
}
|
||||
|
||||
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
|
||||
.threads
|
||||
@@ -266,16 +268,19 @@ impl App {
|
||||
&& self.agents_overview.refresh_thread_ids.is_empty()
|
||||
&& !self.agents_overview.show_more_requested
|
||||
&& !(self.agents_overview.refill_count > 0 && self.agents_overview.discovery.has_more())
|
||||
&& !self.agents_overview.pin_refresh_requested
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.agents_overview.pin_refresh_requested = false;
|
||||
let request_id = Uuid::new_v4();
|
||||
self.agents_overview.request_id = Some(request_id);
|
||||
let initialized = self.agents_overview.initialized;
|
||||
let refill = self.agents_overview.refill_count;
|
||||
let show_more =
|
||||
refill == 0 && std::mem::take(&mut self.agents_overview.show_more_requested);
|
||||
self.agents_overview.refresh_show_more = show_more;
|
||||
let discover =
|
||||
!initialized || show_more || (refill > 0 && self.agents_overview.discovery.has_more());
|
||||
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);
|
||||
self.agents_overview.active_refresh_thread_ids = thread_ids.clone();
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
self.agents_overview
|
||||
@@ -304,6 +310,23 @@ impl App {
|
||||
let mut threads = HashMap::new();
|
||||
let mut last_messages = HashMap::new();
|
||||
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 {
|
||||
let loaded = async {
|
||||
if initialized {
|
||||
@@ -423,6 +446,7 @@ impl App {
|
||||
last_messages,
|
||||
recent_seed_complete,
|
||||
discovery,
|
||||
pinned_thread_ids,
|
||||
})
|
||||
}
|
||||
.await;
|
||||
|
||||
@@ -103,6 +103,7 @@ async fn selected_usage_is_cached_and_account_changes_discard_old_results() -> R
|
||||
last_messages: HashMap::new(),
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
pinned_thread_ids: None,
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -12,6 +12,7 @@ use grouping::model_name;
|
||||
|
||||
use super::agents_overview::AGENTS_OVERVIEW_VIEW_ID;
|
||||
use super::agents_overview_details::AgentsOverviewDetails;
|
||||
use super::agents_overview_discovery::supports_shared_pinning;
|
||||
use crate::app_event::AgentsOverviewAction;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
@@ -54,6 +55,7 @@ use ratatui::text::Span;
|
||||
use ratatui::widgets::Clear;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Widget;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
@@ -181,6 +183,8 @@ pub(super) struct AgentsOverviewView {
|
||||
use_theme_colors: bool,
|
||||
pub(super) rows: Vec<AgentsOverviewRow>,
|
||||
project_groups: Vec<AgentsOverviewProjectGroup>,
|
||||
pub(super) pinned_thread_ranks: Option<HashMap<ThreadId, usize>>,
|
||||
pub(super) pin_action_pending: bool,
|
||||
selected: usize,
|
||||
state: Arc<Mutex<AgentsOverviewViewState>>,
|
||||
app_event_tx: AppEventSender,
|
||||
@@ -251,6 +255,8 @@ impl AgentsOverviewView {
|
||||
use_theme_colors,
|
||||
rows,
|
||||
project_groups,
|
||||
pinned_thread_ranks: None,
|
||||
pin_action_pending: false,
|
||||
selected,
|
||||
state,
|
||||
app_event_tx,
|
||||
@@ -302,44 +308,12 @@ impl AgentsOverviewView {
|
||||
.filter(|_| self.visible_indices().contains(&self.selected))
|
||||
}
|
||||
|
||||
fn visible_indices(&self) -> Vec<usize> {
|
||||
let state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let search = state.search.to_lowercase();
|
||||
let (_, status_group) = command_center::TASK_FILTERS[state.status_filter];
|
||||
let mut visible = self
|
||||
.rows
|
||||
.iter()
|
||||
.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 can_toggle_selected_pin(&self) -> bool {
|
||||
!self.pin_action_pending
|
||||
&& self.pinned_thread_ranks.is_some()
|
||||
&& self
|
||||
.selected_row()
|
||||
.is_some_and(|row| supports_shared_pinning(&row.thread.source))
|
||||
}
|
||||
|
||||
fn move_selection(&mut self, forward: bool) {
|
||||
@@ -682,6 +656,18 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
.send(AppEvent::PersistAgentsOverviewGrouping(state.grouping));
|
||||
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) {
|
||||
self.app_event_tx.send(AppEvent::NewAgentsOverviewSession {
|
||||
cwd: self.selected_row().map(|row| row.thread.cwd.clone()),
|
||||
|
||||
@@ -78,6 +78,8 @@ impl App {
|
||||
task.abort();
|
||||
}
|
||||
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.initialized = false;
|
||||
self.agents_overview.refresh_notifications.clear();
|
||||
|
||||
@@ -67,6 +67,7 @@ impl App {
|
||||
| AppEvent::FinishPromptRevert { .. }
|
||||
| AppEvent::ManagedWorktreeCreated(_)
|
||||
| AppEvent::AgentsOverviewWorktreeCreated(_)
|
||||
| AppEvent::AgentsOverviewPinToggled { .. }
|
||||
| AppEvent::AppendMessageHistoryEntry { .. }
|
||||
| AppEvent::BeginInitialHistoryReplayBuffer
|
||||
| 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 {
|
||||
thread_id,
|
||||
request_id,
|
||||
|
||||
@@ -214,8 +214,11 @@ impl App {
|
||||
task.abort();
|
||||
}
|
||||
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_notifications.clear();
|
||||
self.agents_overview.pending_pin_change = None;
|
||||
self.agents_overview.pending_usage = None;
|
||||
self.agents_overview.usage_disabled = false;
|
||||
self.agents_overview.usage.clear();
|
||||
@@ -357,6 +360,8 @@ impl App {
|
||||
self.sync_thread_title_progress();
|
||||
self.agents_overview.dispatched_requests.clear();
|
||||
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;
|
||||
for input in self.agents_overview.input_states.values_mut() {
|
||||
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
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Tasks Status Updated │ Task details
|
||||
/tmp/project 6 │
|
||||
Pinned 2 │
|
||||
› ○ 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 │
|
||||
● Needs an answer Needs input 2h ago │ Model: Unknown
|
||||
● Working task Working 1d ago │ Reasoning: Unknown
|
||||
○ Unloaded task Inactive 2d ago │
|
||||
│ Project
|
||||
│ /tmp/project
|
||||
● Working task Working 1d ago │ Project
|
||||
○ Unloaded task Inactive 2d ago │ /tmp/project
|
||||
│
|
||||
│ Prompt
|
||||
│ Current task
|
||||
|
||||
+6
-7
@@ -1,22 +1,21 @@
|
||||
---
|
||||
source: tui/src/app/agent_center_tests.rs
|
||||
assertion_line: 450
|
||||
expression: "screen(&view, 100, 24)"
|
||||
---
|
||||
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
|
||||
|
||||
Navigate Tasks View
|
||||
k Up n New tab/⇧tab Filter
|
||||
j Down o Resume f3 Search
|
||||
enter Open f Fork g Group
|
||||
Navigate Tasks View
|
||||
k Up n New tab/⇧tab Filter
|
||||
j Down o Resume f3 Search
|
||||
enter Open f Fork g Group
|
||||
pgdn Page down z r Rename
|
||||
⌃c Quit x Stop
|
||||
h Hide
|
||||
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(),
|
||||
recent_seed_complete: false,
|
||||
discovery: None,
|
||||
pinned_thread_ids: None,
|
||||
}),
|
||||
);
|
||||
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,
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
pinned_thread_ids: None,
|
||||
}),
|
||||
);
|
||||
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"
|
||||
&& params.is_some_and(|params| params["sortKey"] == "recency_at")
|
||||
&& std::mem::take(&mut reject_thread_list)
|
||||
{
|
||||
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
|
||||
.iter()
|
||||
.map(|params| params["sortKey"].as_str().unwrap())
|
||||
.filter(|sort_key| *sort_key != "section_position")
|
||||
.collect::<Vec<_>>();
|
||||
sort_keys.sort_unstable();
|
||||
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) recent_seed_complete: bool,
|
||||
pub(crate) discovery: Option<crate::app::agents_overview_discovery::AgentsOverviewDiscovery>,
|
||||
pub(crate) pinned_thread_ids: Option<Option<Vec<ThreadId>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -335,6 +336,18 @@ pub(crate) enum AppEvent {
|
||||
thread_id: ThreadId,
|
||||
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.
|
||||
SuggestThreadName {
|
||||
thread_id: ThreadId,
|
||||
|
||||
@@ -432,6 +432,7 @@ pub(crate) struct AgentsKeymap {
|
||||
pub(crate) archive: Vec<KeyBinding>,
|
||||
pub(crate) delete: Vec<KeyBinding>,
|
||||
pub(crate) hide: Vec<KeyBinding>,
|
||||
pub(crate) toggle_pin: Vec<KeyBinding>,
|
||||
pub(crate) toggle_grouping: Vec<KeyBinding>,
|
||||
chord_hints: Arc<RuntimeChordKeymap>,
|
||||
}
|
||||
@@ -1361,6 +1362,7 @@ impl RuntimeKeymap {
|
||||
archive: resolve_local!(keymap, defaults, agents, archive),
|
||||
delete: resolve_local!(keymap, defaults, agents, delete),
|
||||
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),
|
||||
chord_hints: Arc::clone(&chords),
|
||||
};
|
||||
@@ -1380,6 +1382,7 @@ impl RuntimeKeymap {
|
||||
(keymap.agents.archive.as_ref(), &mut agents.archive),
|
||||
(keymap.agents.delete.as_ref(), &mut agents.delete),
|
||||
(keymap.agents.hide.as_ref(), &mut agents.hide),
|
||||
(keymap.agents.toggle_pin.as_ref(), &mut agents.toggle_pin),
|
||||
(
|
||||
keymap.agents.toggle_grouping.as_ref(),
|
||||
&mut agents.toggle_grouping,
|
||||
@@ -1546,6 +1549,7 @@ impl RuntimeKeymap {
|
||||
(keymap.agents.archive.as_ref(), &mut agents.archive),
|
||||
(keymap.agents.delete.as_ref(), &mut agents.delete),
|
||||
(keymap.agents.hide.as_ref(), &mut agents.hide),
|
||||
(keymap.agents.toggle_pin.as_ref(), &mut agents.toggle_pin),
|
||||
(
|
||||
keymap.agents.toggle_grouping.as_ref(),
|
||||
&mut agents.toggle_grouping,
|
||||
@@ -1920,6 +1924,7 @@ impl RuntimeKeymap {
|
||||
archive: default_bindings![plain(KeyCode::Char('a'))],
|
||||
delete: default_bindings![plain(KeyCode::Backspace)],
|
||||
hide: default_bindings![plain(KeyCode::Char('h'))],
|
||||
toggle_pin: default_bindings![plain(KeyCode::Char('p'))],
|
||||
toggle_grouping: default_bindings![plain(KeyCode::Char('g'))],
|
||||
chord_hints: Arc::default(),
|
||||
},
|
||||
|
||||
@@ -428,6 +428,7 @@ define_runtime_action_bindings! {
|
||||
archive,
|
||||
delete,
|
||||
hide,
|
||||
toggle_pin,
|
||||
toggle_grouping,
|
||||
],
|
||||
"approval" => Approval, approval, approval [
|
||||
|
||||
Reference in New Issue
Block a user