mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Search task history from the command center (#60286)
## Why Command center search only filtered tasks already loaded for browsing, leaving older tasks undiscoverable without manually loading more history. ## What changed - Scan history on the active backend for case-insensitive matches in task names, previews, and working directories, using a separate paging cursor for each query. - Add loading, retry, and `Show more matches` states. Cancel superseded searches and ignore stale results. - Keep history matches separate from browse results and restore the previous selection when leaving search. - Preserve the original task preview for search while displaying the latest prompt in task details. ## Testing Extend discovery and command center tests to cover older history matches, fresh queries after browse pagination is exhausted, selection restoration, actions on search results, loading and retry states, and separate original previews and latest prompts. GitOrigin-RevId: 9864ee006a14bd232e9cfd7ab2ee099c84667597
This commit is contained in:
@@ -20,7 +20,7 @@ pub(super) const TASK_FILTERS: &[(&str, Option<AgentsOverviewGroup>)] = &[
|
||||
impl AgentsOverviewView {
|
||||
pub(in crate::app::agents_overview_view) fn selectable_indices(&self) -> Vec<usize> {
|
||||
let mut indices = self.visible_indices();
|
||||
if self.state().has_more {
|
||||
if self.state().more_available() {
|
||||
indices.push(usize::MAX);
|
||||
}
|
||||
indices
|
||||
|
||||
@@ -154,12 +154,14 @@ impl Renderable for AgentsOverviewView {
|
||||
inset(layout.header),
|
||||
buf,
|
||||
);
|
||||
let history = state.history.as_ref();
|
||||
let labels = TASK_FILTERS
|
||||
.iter()
|
||||
.map(|(label, group)| {
|
||||
let count = self
|
||||
.rows
|
||||
.iter()
|
||||
.filter(|row| state.includes_row(row))
|
||||
.filter(|row| group.is_none_or(|group| group == row.group))
|
||||
.count();
|
||||
format!("{label} {count}")
|
||||
@@ -229,7 +231,8 @@ impl Renderable for AgentsOverviewView {
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
(state.refresh_failed && !state.loading).then(|| "Error loading tasks".to_owned())
|
||||
let failed = state.refresh_failed || history.is_some_and(|s| s.failed);
|
||||
(failed && !state.loading).then(|| "Error loading tasks".to_owned())
|
||||
})
|
||||
.or_else(|| state.server_version_notice.clone());
|
||||
if let Some(notice) = notice.filter(|_| !state.help && state.key_chord_hint.is_none()) {
|
||||
|
||||
@@ -50,6 +50,8 @@ impl AgentsOverviewView {
|
||||
line(
|
||||
if state.connection_notice.is_some() {
|
||||
"Reconnecting…"
|
||||
} else if state.history.is_some() {
|
||||
"No matching tasks"
|
||||
} else if state.loading && self.rows.is_empty() {
|
||||
"Loading tasks…"
|
||||
} else if state.refresh_failed {
|
||||
@@ -121,7 +123,15 @@ impl AgentsOverviewView {
|
||||
},
|
||||
style,
|
||||
);
|
||||
let label = if state.loading {
|
||||
let label = if let Some(search) = &state.history {
|
||||
if search.loading {
|
||||
"Searching history…"
|
||||
} else if search.failed {
|
||||
"Show more (retry)"
|
||||
} else {
|
||||
"Show more matches"
|
||||
}
|
||||
} else if state.loading {
|
||||
"Loading more…"
|
||||
} else if state.refresh_failed {
|
||||
"Show more (retry)"
|
||||
@@ -160,6 +170,7 @@ impl AgentsOverviewView {
|
||||
})
|
||||
.count();
|
||||
let total = (0..self.rows.len())
|
||||
.filter(|&i| state.includes_row(&self.rows[i]))
|
||||
.filter(|&candidate| self.same_group(state.grouping, candidate, index))
|
||||
.count();
|
||||
let count = if count == total {
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::*;
|
||||
use assert_matches::assert_matches;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn screen(view: &AgentsOverviewView, width: u16, height: u16) -> String {
|
||||
pub(super) fn screen(view: &AgentsOverviewView, width: u16, height: u16) -> String {
|
||||
let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height);
|
||||
let mut buf = ratatui::buffer::Buffer::empty(area);
|
||||
view.render(area, &mut buf);
|
||||
@@ -819,6 +819,9 @@ async fn backspace_edits_search_and_rename_without_deleting_tasks() {
|
||||
view.handle_paste("!".into());
|
||||
view.handle_key_event(KeyCode::Backspace.into());
|
||||
assert_eq!(screen(&view, /*width*/ 88, /*height*/ 16), initial);
|
||||
if key == '/' {
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::SearchAgentsOverview(..)));
|
||||
}
|
||||
assert!(rx.try_recv().is_err());
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
}
|
||||
@@ -895,10 +898,15 @@ async fn live_center_show_more_navigation_search_and_loading() {
|
||||
}
|
||||
view.handle_key_event(KeyCode::Char('/').into());
|
||||
view.handle_paste("Older task".into());
|
||||
let id = assert_matches!(rx.try_recv(), Ok(AppEvent::SearchAgentsOverview(id, _)) => id);
|
||||
insta::assert_snapshot!(screen(&view, /*width*/ 80, /*height*/ 12).lines().nth(6).unwrap(), @" › Searching history…");
|
||||
app.apply_agents_overview_search_page(id, Vec::new(), Default::default(), /*ok*/ false);
|
||||
insta::assert_snapshot!(
|
||||
"live_center_show_more_search_retry",
|
||||
screen(&view, /*width*/ 80, /*height*/ 12)
|
||||
);
|
||||
view.handle_key_event(KeyCode::Enter.into());
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::ShowMoreAgentsOverview));
|
||||
let id = assert_matches!(rx.try_recv(), Ok(AppEvent::SearchAgentsOverview(id, _)) => id);
|
||||
app.apply_agents_overview_search_page(id, Vec::new(), Default::default(), /*ok*/ true);
|
||||
insta::assert_snapshot!(screen(&view, /*width*/ 80, /*height*/ 12).lines().nth(6).unwrap(), @" › Show more matches");
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ pub(crate) const AGENTS_OVERVIEW_VIEW_ID: &str = "agents-overview";
|
||||
pub(super) struct AgentsOverviewState {
|
||||
/// Missing metadata records a local resume until the next metadata refresh.
|
||||
pub(super) threads: HashMap<ThreadId, Option<Thread>>,
|
||||
/// Identifies the query that populated the cached search results.
|
||||
pub(super) search_id: Uuid,
|
||||
pub(super) search_matches: HashMap<ThreadId, Thread>,
|
||||
/// Lifecycle removals take precedence over delayed tool registration responses.
|
||||
pub(super) removed_threads: HashSet<ThreadId>,
|
||||
/// Local visibility only; activity and metadata refreshes never reveal hidden roots.
|
||||
@@ -173,7 +176,8 @@ impl App {
|
||||
if self.agents_overview.removed_threads.contains(&thread_id) {
|
||||
continue;
|
||||
}
|
||||
if let Some((mut thread, unread)) = thread {
|
||||
if let Some((mut thread, unread, latest_prompt)) = thread {
|
||||
self.agents_overview.search_matches.remove(&thread_id);
|
||||
if unread {
|
||||
self.agents_overview.unread.insert(thread_id);
|
||||
} else {
|
||||
@@ -197,6 +201,11 @@ impl App {
|
||||
self.agents_overview.refill_count =
|
||||
self.agents_overview.refill_count.saturating_sub(1);
|
||||
}
|
||||
self.agents_overview
|
||||
.activity
|
||||
.entry(thread_id)
|
||||
.or_default()
|
||||
.latest_prompt = latest_prompt;
|
||||
thread.turns.clear();
|
||||
self.agents_overview.threads.insert(thread_id, Some(thread));
|
||||
} else {
|
||||
@@ -216,6 +225,9 @@ impl App {
|
||||
&& let Ok(thread_id) = ThreadId::from_string(&reverted.thread_id)
|
||||
{
|
||||
// Discard stale read results without clearing activity received after the revert.
|
||||
if let Some(activity) = self.agents_overview.activity.get_mut(&thread_id) {
|
||||
activity.latest_prompt = None;
|
||||
}
|
||||
self.agents_overview.last_messages.remove(&thread_id);
|
||||
if let Some(usage) = self.agents_overview.usage.get_mut(&thread_id) {
|
||||
usage.tokens = None;
|
||||
@@ -268,6 +280,18 @@ impl App {
|
||||
.cloned()
|
||||
.collect();
|
||||
let view = self.agents_overview_view(threads, selected_thread_id);
|
||||
let matches = &mut self.agents_overview.search_matches;
|
||||
if !matches.is_empty() {
|
||||
let visible: HashSet<_> = view
|
||||
.visible_indices()
|
||||
.into_iter()
|
||||
.map(|i| view.rows[i].thread_id)
|
||||
.collect();
|
||||
matches.retain(|id, _| {
|
||||
visible.contains(id) || self.agents_overview.hidden_threads.contains(id)
|
||||
});
|
||||
}
|
||||
view.load_search_matches(/*additional*/ 0);
|
||||
self.agents_overview.visible_thread_ids = view.thread_ids();
|
||||
let next_thread_id = view
|
||||
.selected_index()
|
||||
@@ -304,6 +328,35 @@ impl App {
|
||||
mut threads: Vec<Thread>,
|
||||
selected_thread_id: Option<ThreadId>,
|
||||
) -> AgentsOverviewView {
|
||||
{
|
||||
let overview = &self.agents_overview;
|
||||
let mut state = overview
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
state.vim_enabled = self.chat_widget.composer_is_vim_enabled();
|
||||
if state
|
||||
.history
|
||||
.as_ref()
|
||||
.is_some_and(|search| search.id == overview.search_id)
|
||||
{
|
||||
let query = state.search.to_lowercase();
|
||||
threads.extend(
|
||||
overview
|
||||
.search_matches
|
||||
.values()
|
||||
.filter(|thread| {
|
||||
super::agents_overview_discovery::matches_query(thread, &query)
|
||||
})
|
||||
.cloned(),
|
||||
);
|
||||
} else if let Some(target) = state
|
||||
.rename_target
|
||||
.and_then(|id| overview.search_matches.get(&id))
|
||||
{
|
||||
threads.push(target.clone());
|
||||
}
|
||||
}
|
||||
threads.retain(|thread| !thread.ephemeral);
|
||||
for thread in &mut threads {
|
||||
if thread.parent_thread_id.is_none()
|
||||
@@ -349,6 +402,11 @@ impl App {
|
||||
continue;
|
||||
}
|
||||
rows.push(AgentsOverviewRow {
|
||||
search_id: self
|
||||
.agents_overview
|
||||
.search_matches
|
||||
.contains_key(&thread_id)
|
||||
.then_some(self.agents_overview.search_id),
|
||||
unread: self.agents_overview.unread.contains(&thread_id),
|
||||
details: self.agents_overview_details(root, &children),
|
||||
thread: root.clone(),
|
||||
@@ -359,11 +417,6 @@ impl App {
|
||||
});
|
||||
}
|
||||
|
||||
self.agents_overview
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.vim_enabled = self.chat_widget.composer_is_vim_enabled();
|
||||
let mut view = AgentsOverviewView::new(
|
||||
rows,
|
||||
selected_thread_id,
|
||||
|
||||
@@ -190,6 +190,7 @@ impl App {
|
||||
.threads
|
||||
.get(&thread_id)
|
||||
.and_then(Option::as_ref)
|
||||
.or_else(|| self.agents_overview.search_matches.get(&thread_id))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -137,9 +137,11 @@ async fn lifecycle_shortcuts_target_filtered_task_in_any_state() {
|
||||
);
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE));
|
||||
for character in "Target".chars() {
|
||||
view.handle_key_event(KeyCode::Char(character).into());
|
||||
}
|
||||
view.handle_paste("Target".into());
|
||||
assert!(matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::SearchAgentsOverview(..))
|
||||
));
|
||||
for (key, expected) in [
|
||||
(5, AgentsOverviewAction::Archive),
|
||||
(6, AgentsOverviewAction::Delete),
|
||||
@@ -664,7 +666,7 @@ async fn hidden_task_stays_hidden_through_activity_and_seed_until_explicit_resum
|
||||
request_id,
|
||||
Ok(AgentsOverviewThreadRefresh {
|
||||
cleared_unread: Vec::new(),
|
||||
threads: HashMap::from([(id, Some((thread.clone(), false)))]),
|
||||
threads: HashMap::from([(id, Some((thread.clone(), false, None)))]),
|
||||
last_messages: HashMap::new(),
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
@@ -948,7 +950,7 @@ async fn lifecycle_removes_background_and_current_tasks_without_losing_the_dashb
|
||||
cleared_unread: Vec::new(),
|
||||
threads: stale_threads
|
||||
.into_iter()
|
||||
.map(|(id, thread)| (id, thread.map(|thread| (thread, false))))
|
||||
.map(|(id, thread)| (id, thread.map(|thread| (thread, false, None))))
|
||||
.collect(),
|
||||
last_messages: HashMap::new(),
|
||||
recent_seed_complete: true,
|
||||
@@ -1126,6 +1128,12 @@ async fn fork_shortcut_respects_metadata_editing() {
|
||||
for editor in ['r', '/'] {
|
||||
view.handle_key_event(KeyCode::Char(editor).into());
|
||||
view.handle_key_event(KeyCode::Char('f').into());
|
||||
if editor == '/' {
|
||||
assert!(matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::SearchAgentsOverview(..))
|
||||
));
|
||||
}
|
||||
assert!(rx.try_recv().is_err());
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ pub(super) fn preview_markdown(text: &str) -> String {
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(super) struct AgentsOverviewDetails {
|
||||
pub(super) latest_prompt: Option<String>,
|
||||
pub(super) lines: Vec<Line<'static>>,
|
||||
pub(super) usage_lines: Vec<Line<'static>>,
|
||||
pub(super) last_message: Option<(String, AbsolutePathBuf)>,
|
||||
@@ -50,6 +51,7 @@ pub(super) fn preview_text(text: &str) -> String {
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct AgentsOverviewActivity {
|
||||
pub(super) latest_prompt: Option<String>,
|
||||
reasoning: Option<ReasoningPreview>,
|
||||
last_message: Option<String>,
|
||||
}
|
||||
@@ -219,10 +221,17 @@ impl App {
|
||||
.or_else(|| self.agents_overview.last_messages.get(&thread_id))
|
||||
.filter(|message| !message.trim().is_empty())
|
||||
.map(|message| (message.clone(), source.cwd.clone()));
|
||||
let root_id = ThreadId::from_string(&root.id).ok();
|
||||
AgentsOverviewDetails {
|
||||
latest_prompt: root_id.and_then(|id| {
|
||||
self.agents_overview
|
||||
.activity
|
||||
.get(&id)?
|
||||
.latest_prompt
|
||||
.clone()
|
||||
}),
|
||||
lines,
|
||||
usage_lines: ThreadId::from_string(&root.id)
|
||||
.ok()
|
||||
usage_lines: root_id
|
||||
.and_then(|id| self.agents_overview.usage.get(&id))
|
||||
.map(|usage| {
|
||||
super::agents_overview_usage::usage_lines(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Demand-driven history discovery. Each source retains its cursor and unshown rows.
|
||||
//! One activation requests at most one page per source, then publishes up to ten tasks.
|
||||
//! Each new query scans the active backend from the start with its own paging cursor.
|
||||
|
||||
use super::App;
|
||||
use crate::app_event::AppEvent;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_client::TypedRequestError;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
@@ -14,6 +16,8 @@ use codex_app_server_protocol::ThreadSourceKind;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_state::PINNED_THREAD_SECTION_ID;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::PoisonError;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const PAGE_SIZE: u32 = 10;
|
||||
@@ -110,6 +114,7 @@ struct SourcePage {
|
||||
rows: VecDeque<(Thread, bool)>,
|
||||
exhausted: bool,
|
||||
sort_key: ThreadSortKey,
|
||||
page_size: u32,
|
||||
}
|
||||
|
||||
impl Default for SourcePage {
|
||||
@@ -119,6 +124,7 @@ impl Default for SourcePage {
|
||||
rows: VecDeque::new(),
|
||||
exhausted: false,
|
||||
sort_key: ThreadSortKey::RecencyAt,
|
||||
page_size: PAGE_SIZE,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,7 +198,7 @@ impl SourcePage {
|
||||
attached_to_thread_id: None,
|
||||
originators: None,
|
||||
cursor: self.cursor.clone(),
|
||||
limit: Some(PAGE_SIZE),
|
||||
limit: Some(self.page_size),
|
||||
sort_key: Some(self.sort_key),
|
||||
sort_direction: None,
|
||||
model_providers: Some(Vec::new()),
|
||||
@@ -218,6 +224,9 @@ impl SourcePage {
|
||||
self.cursor = None;
|
||||
}
|
||||
Ok(page) => {
|
||||
if page.next_cursor.is_some() && page.next_cursor == self.cursor {
|
||||
return false;
|
||||
}
|
||||
self.exhausted = page.next_cursor.is_none();
|
||||
self.cursor = page.next_cursor.clone();
|
||||
self.rows.extend(
|
||||
@@ -233,3 +242,108 @@ impl SourcePage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct HistorySearch {
|
||||
pub(super) id: Uuid,
|
||||
pub(super) cursor: AgentsOverviewDiscovery,
|
||||
/// Requested number of matches visible under the active status filter.
|
||||
pub(super) target: usize,
|
||||
pub(super) loading: bool,
|
||||
pub(super) failed: bool,
|
||||
pub(super) task: Option<AbortOnDropHandle<()>>,
|
||||
}
|
||||
|
||||
pub(super) fn matches_query(thread: &Thread, query: &str) -> bool {
|
||||
let name = thread.name.as_deref().unwrap_or_default();
|
||||
let cwd = thread.cwd.display();
|
||||
format!("{name} {} {cwd}", thread.preview)
|
||||
.to_lowercase()
|
||||
.contains(query)
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(super) fn search_agents_overview(
|
||||
&mut self,
|
||||
handle: AppServerRequestHandle,
|
||||
id: Uuid,
|
||||
limit: usize,
|
||||
) {
|
||||
let shared = &self.agents_overview.view_state;
|
||||
let mut state = shared.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let query = state.search.to_lowercase();
|
||||
let Some(search) = state.history.as_mut().filter(|search| search.id == id) else {
|
||||
return;
|
||||
};
|
||||
let first = self.agents_overview.search_id != id;
|
||||
let mut cursor = std::mem::take(&mut search.cursor);
|
||||
if first {
|
||||
self.agents_overview.search_matches.clear();
|
||||
self.agents_overview.search_id = id;
|
||||
}
|
||||
for source in &mut cursor.sources {
|
||||
source.page_size = 100;
|
||||
}
|
||||
if matches!(handle, AppServerRequestHandle::Cloud(_)) {
|
||||
cursor.sources[1].exhausted = true;
|
||||
}
|
||||
let tx = self.app_event_tx.clone();
|
||||
search.task = Some(AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
if first {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(180)).await;
|
||||
}
|
||||
loop {
|
||||
let (mut rows, ok) = cursor.next_batch(&handle, limit).await;
|
||||
rows.retain(|(thread, _)| matches_query(thread, &query));
|
||||
if rows.is_empty() && ok && cursor.has_more() {
|
||||
continue;
|
||||
}
|
||||
tx.send(AppEvent::AgentsOverviewSearchPage {
|
||||
id,
|
||||
rows,
|
||||
cursor,
|
||||
ok,
|
||||
});
|
||||
return;
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
pub(super) fn apply_agents_overview_search_page(
|
||||
&mut self,
|
||||
id: Uuid,
|
||||
rows: Vec<(Thread, bool)>,
|
||||
cursor: AgentsOverviewDiscovery,
|
||||
ok: bool,
|
||||
) {
|
||||
let overview = &mut self.agents_overview;
|
||||
let mut state = overview
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner);
|
||||
let Some(search) = state.history.as_mut().filter(|s| s.id == id) else {
|
||||
return;
|
||||
};
|
||||
for (thread, unread) in rows {
|
||||
if let Ok(id) = codex_protocol::ThreadId::from_string(&thread.id)
|
||||
&& !overview.threads.get(&id).is_some_and(Option::is_some)
|
||||
&& !overview.removed_threads.contains(&id)
|
||||
&& !overview.hidden_threads.contains(&id)
|
||||
&& let std::collections::hash_map::Entry::Vacant(entry) =
|
||||
overview.search_matches.entry(id)
|
||||
{
|
||||
entry.insert(thread);
|
||||
if unread {
|
||||
overview.unread.insert(id);
|
||||
} else {
|
||||
overview.unread.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
search.cursor = cursor;
|
||||
search.loading = false;
|
||||
search.failed = !ok;
|
||||
drop(state);
|
||||
self.repaint_agents_overview();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Demand-driven discovery, cursor retry, and visible Show more behavior.
|
||||
|
||||
use super::*;
|
||||
use crate::app_event::AgentsOverviewAction;
|
||||
use crate::app_server_session::ThreadParamsMode;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use futures::SinkExt;
|
||||
@@ -32,6 +33,9 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
||||
if mixed_sources && (index == 0 || index >= 21) {
|
||||
thread.source = SessionSource::Exec;
|
||||
}
|
||||
if index == 23 {
|
||||
thread.name = None;
|
||||
}
|
||||
thread.updated_at += 24 - index;
|
||||
thread.recency_at = Some(thread.updated_at);
|
||||
thread
|
||||
@@ -107,22 +111,26 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
assert_eq!(params["sortKey"], "recency_at");
|
||||
assert_eq!(params["limit"], 10);
|
||||
let limit = params["limit"].as_u64().unwrap() as usize;
|
||||
assert!(params["searchTerm"].is_null());
|
||||
{
|
||||
let interactive = params["sourceKinds"] == json!([]);
|
||||
let source_rows: Vec<_> = rows
|
||||
let mut source_rows: Vec<_> = rows
|
||||
.iter()
|
||||
.filter(|row| matches!(row.source, SessionSource::Exec) != interactive)
|
||||
.collect();
|
||||
if interactive && server_show_reopened_pin.load(Ordering::SeqCst) {
|
||||
source_rows.insert(0, &reopened_pin);
|
||||
}
|
||||
let cursor = params["cursor"].as_str().unwrap_or("0").parse::<usize>()?;
|
||||
if interactive {
|
||||
if interactive && limit == 10 {
|
||||
cursors.push(cursor);
|
||||
}
|
||||
if !mixed_sources && interactive && cursors == [0, 10] {
|
||||
if limit == 10 && !mixed_sources && interactive && cursors == [0, 10] {
|
||||
socket.send(Message::Text(json!({"id": request.id, "error": {"code": -32603, "message": "transient"}}).to_string().into())).await?;
|
||||
continue;
|
||||
}
|
||||
let end = (cursor + 10).min(source_rows.len());
|
||||
let end = (cursor + limit).min(source_rows.len());
|
||||
json!({"data": source_rows[cursor..end], "nextCursor": (end < source_rows.len()).then(|| end.to_string())})
|
||||
}
|
||||
}
|
||||
@@ -133,7 +141,15 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
||||
.unwrap();
|
||||
json!({"thread": thread, "readState": {"revision": "read", "firstUnread": null}})
|
||||
}
|
||||
"thread/turns/list" if params["threadId"] == rows[23].id => json!({"data": [{
|
||||
"id":"latest", "status":"completed", "items":[{"type":"userMessage","id":"prompt",
|
||||
"content":[{"type":"text","text":"A later prompt","textElements":[]}]}]
|
||||
}], "nextCursor":null}),
|
||||
"thread/turns/list" => json!({"data": [], "nextCursor": null}),
|
||||
"thread/archive" | "thread/delete" => {
|
||||
socket.send(Message::Text(json!({"id": request.id, "error":{"code":-32603,"message":"try again"}}).to_string().into())).await?;
|
||||
continue;
|
||||
}
|
||||
method => panic!("unexpected request: {method}"),
|
||||
};
|
||||
socket
|
||||
@@ -146,7 +162,7 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
||||
}
|
||||
Ok::<_, color_eyre::Report>(cursors)
|
||||
});
|
||||
let session = AppServerSession::new(
|
||||
let mut session = AppServerSession::new(
|
||||
crate::backend_id::BackendId::new(),
|
||||
crate::connect_remote_app_server(endpoint).await?,
|
||||
ThreadParamsMode::Remote,
|
||||
@@ -160,6 +176,39 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
||||
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());
|
||||
let browse = app.agents_overview.threads.clone();
|
||||
let browse_rows = || browse.values().flatten().cloned().collect();
|
||||
let mut view = app.agents_overview_view(browse_rows(), Some(ids[4]));
|
||||
view.handle_key_event(KeyCode::Char('/').into());
|
||||
view.handle_paste("TASK 23".into());
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
tui.pause_events();
|
||||
finish_history_search(&mut app, &mut tui, &mut session, &mut rx).await?;
|
||||
assert_eq!(app.agents_overview.search_matches.len(), 1);
|
||||
view = app.agents_overview_view(browse_rows(), /*selected_thread_id*/ None);
|
||||
assert_eq!(view.thread_ids()[view.selected_index().unwrap()], ids[23]);
|
||||
let action = if mixed_sources {
|
||||
AgentsOverviewAction::Archive
|
||||
} else {
|
||||
AgentsOverviewAction::Delete
|
||||
};
|
||||
Box::pin(app.run_agents_overview_action(&mut tui, &mut session, ids[23], action)).await?;
|
||||
finish_overview_refresh(&mut app, &session, &mut rx).await;
|
||||
app.chat_widget.handle_key_event(KeyCode::Enter.into());
|
||||
assert!(render_bottom_popup(&app.chat_widget, /*width*/ 100).contains("Task 23"));
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
assert_eq!(view.thread_ids()[view.selected_index().unwrap()], ids[4]);
|
||||
assert_eq!(app.agents_overview.threads, browse);
|
||||
view.handle_key_event(KeyCode::Char('/').into());
|
||||
view.handle_paste("TASK 22".into());
|
||||
assert!(super::command_center::screen(&view, /*width*/ 100, /*height*/ 16).contains("All 10 "));
|
||||
finish_history_search(&mut app, &mut tui, &mut session, &mut rx).await?;
|
||||
view = app.agents_overview_view(browse_rows(), /*selected_thread_id*/ None);
|
||||
let rendered = super::command_center::screen(&view, /*width*/ 100, /*height*/ 16);
|
||||
assert!(rendered.contains("All 11 "));
|
||||
assert!(rendered.contains("1 of 10"), "{rendered}"); // The pinned task has its own group.
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
|
||||
app.track_agents_overview_notification(&ServerNotification::ThreadArchived(
|
||||
ThreadArchivedNotification {
|
||||
thread_id: ids[0].to_string(),
|
||||
@@ -211,10 +260,28 @@ 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());
|
||||
let old = app.agents_overview.threads[&ids[23]].as_ref().unwrap();
|
||||
assert_eq!(old.preview, "Task 23");
|
||||
assert_eq!(
|
||||
app.agents_overview_details(old, &HashMap::new())
|
||||
.latest_prompt
|
||||
.as_deref(),
|
||||
Some("A later prompt")
|
||||
);
|
||||
// Another CLI inserts a matching session ahead of the exhausted browse cursor.
|
||||
show_reopened_pin.store(true, Ordering::SeqCst);
|
||||
view.handle_key_event(KeyCode::Char('/').into());
|
||||
view.handle_paste("Pinned after reopen".into());
|
||||
finish_history_search(&mut app, &mut tui, &mut session, &mut rx).await?;
|
||||
assert!(
|
||||
app.agents_overview
|
||||
.search_matches
|
||||
.contains_key(&reopened_pin_id)
|
||||
);
|
||||
view.handle_key_event(KeyCode::Esc.into());
|
||||
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!(
|
||||
@@ -233,3 +300,26 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn finish_history_search(
|
||||
app: &mut App,
|
||||
tui: &mut crate::tui::Tui,
|
||||
session: &mut AppServerSession,
|
||||
rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
|
||||
) -> Result<()> {
|
||||
while app
|
||||
.agents_overview
|
||||
.view_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.history
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.loading)
|
||||
{
|
||||
let event = tokio::time::timeout(Duration::from_secs(10), rx.recv())
|
||||
.await?
|
||||
.expect("search event");
|
||||
app.handle_event(tui, session, event).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use super::super::App;
|
||||
use super::super::agents_overview::AgentsOverviewState;
|
||||
use super::super::agents_overview_discovery::matches_query;
|
||||
use super::AgentsOverviewView;
|
||||
use crate::legacy_core::config::edit::ConfigEdit;
|
||||
use crate::legacy_core::config::edit::ConfigEditsBuilder;
|
||||
@@ -53,7 +54,7 @@ impl AgentsOverviewView {
|
||||
.is_some_and(|ranks| ranks.contains_key(&self.rows[index].thread_id))
|
||||
}
|
||||
|
||||
pub(super) fn visible_indices(&self) -> Vec<usize> {
|
||||
pub(in crate::app) 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];
|
||||
@@ -62,16 +63,10 @@ impl AgentsOverviewView {
|
||||
.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)))
|
||||
(state.rename_target == Some(row.thread_id)
|
||||
|| (state.includes_row(row)
|
||||
&& (search.is_empty() || matches_query(&row.thread, &search))
|
||||
&& status_group.is_none_or(|group| group == row.group)))
|
||||
.then_some(index)
|
||||
})
|
||||
.partition(|index| self.is_pinned(*index));
|
||||
|
||||
@@ -384,9 +384,9 @@ async fn shared_overview_keeps_rows_and_replays_changes_over_stale_reads() -> Re
|
||||
cleared_unread: Vec::new(),
|
||||
last_messages: HashMap::new(),
|
||||
threads: HashMap::from([
|
||||
(retained, Some((retained_thread.clone(), false))),
|
||||
(archived, Some((archived_thread, false))),
|
||||
(deleted, Some((deleted_thread, false))),
|
||||
(retained, Some((retained_thread.clone(), false, None))),
|
||||
(archived, Some((archived_thread, false, None))),
|
||||
(deleted, Some((deleted_thread, false, None))),
|
||||
]),
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
@@ -1438,6 +1438,10 @@ async fn shared_overview_shows_only_root_sessions() {
|
||||
);
|
||||
action_view.handle_key_event(KeyCode::Char('/').into());
|
||||
assert!(action_view.handle_paste("Repair authentication".into()));
|
||||
assert!(matches!(
|
||||
event_rx.try_recv(),
|
||||
Ok(AppEvent::SearchAgentsOverview(..))
|
||||
));
|
||||
action_view.handle_key_event(KeyCode::Enter.into());
|
||||
assert!(
|
||||
matches!(event_rx.try_recv(), Ok(AppEvent::SelectAgentsOverviewThread { thread_id }) if thread_id == second_root)
|
||||
@@ -1580,6 +1584,10 @@ async fn filtered_dashboard_actions_use_configured_shortcuts() {
|
||||
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::F(6), KeyModifiers::NONE));
|
||||
assert!(view.handle_paste("Second task".to_string()));
|
||||
assert!(matches!(
|
||||
event_rx.try_recv(),
|
||||
Ok(AppEvent::SearchAgentsOverview(..))
|
||||
));
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::F(8), KeyModifiers::NONE));
|
||||
assert!(matches!(
|
||||
event_rx.try_recv(),
|
||||
|
||||
@@ -57,6 +57,7 @@ fn detail_thread_ids<'a>(threads: impl Iterator<Item = &'a Thread>) -> Vec<Threa
|
||||
impl App {
|
||||
pub(super) fn remove_agents_overview_thread(&mut self, thread_id: ThreadId) {
|
||||
self.prepare_agents_overview_removal(&std::collections::HashSet::from([thread_id]));
|
||||
self.agents_overview.search_matches.remove(&thread_id);
|
||||
self.agents_overview.removed_threads.insert(thread_id);
|
||||
self.agents_overview.unread.remove(&thread_id);
|
||||
if let Some(Some(thread)) = self.agents_overview.threads.remove(&thread_id)
|
||||
@@ -100,7 +101,8 @@ impl App {
|
||||
.agents_overview
|
||||
.threads
|
||||
.get_mut(&thread_id)
|
||||
.and_then(Option::as_mut);
|
||||
.and_then(Option::as_mut)
|
||||
.or_else(|| self.agents_overview.search_matches.get_mut(&thread_id));
|
||||
match notification {
|
||||
ServerNotification::ThreadReadStateChanged(change) => {
|
||||
if change.read_state.first_unread.is_some() {
|
||||
@@ -127,6 +129,7 @@ impl App {
|
||||
self.agents_overview.removed_threads.remove(&thread_id);
|
||||
let mut thread = started.thread.clone();
|
||||
thread.turns.clear();
|
||||
self.agents_overview.search_matches.remove(&thread_id);
|
||||
self.agents_overview.threads.insert(thread_id, Some(thread));
|
||||
}
|
||||
ServerNotification::ThreadArchived(_) | ServerNotification::ThreadDeleted(_) => {
|
||||
@@ -333,7 +336,7 @@ impl App {
|
||||
for (thread, is_unread) in pinned_threads {
|
||||
if let Ok(thread_id) = ThreadId::from_string(&thread.id) {
|
||||
ids.push(thread_id);
|
||||
threads.insert(thread_id, Some((thread, is_unread)));
|
||||
threads.insert(thread_id, Some((thread, is_unread, None)));
|
||||
}
|
||||
}
|
||||
Some(Some(ids))
|
||||
@@ -377,7 +380,7 @@ impl App {
|
||||
thread_ids.extend(detail_thread_ids(recent.iter().map(|(thread, _)| thread)));
|
||||
for (thread, is_unread) in recent {
|
||||
if let Ok(thread_id) = ThreadId::from_string(&thread.id) {
|
||||
threads.insert(thread_id, Some((thread, is_unread)));
|
||||
threads.insert(thread_id, Some((thread, is_unread, None)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,7 +420,8 @@ impl App {
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(mut response) => {
|
||||
Ok(response) => {
|
||||
let mut latest_prompt = None;
|
||||
let mut last_message = None;
|
||||
if let Ok(turns) = request_handle
|
||||
.request_typed::<ThreadTurnsListResponse>(
|
||||
@@ -441,9 +445,13 @@ impl App {
|
||||
content, ..
|
||||
}) = turn.item_payloads().next()
|
||||
{
|
||||
response.thread.preview =
|
||||
ChatWidget::user_message_display_from_inputs(content)
|
||||
.message;
|
||||
latest_prompt =
|
||||
Some(super::agents_overview_details::preview_markdown(
|
||||
&ChatWidget::user_message_display_from_inputs(
|
||||
content,
|
||||
)
|
||||
.message,
|
||||
));
|
||||
}
|
||||
last_message =
|
||||
turn.item_payloads().rev().find_map(|item| match item {
|
||||
@@ -456,7 +464,11 @@ impl App {
|
||||
let unread = response
|
||||
.read_state
|
||||
.is_some_and(|state| state.first_unread.is_some());
|
||||
Some((thread_id, (response.thread, unread), last_message))
|
||||
Some((
|
||||
thread_id,
|
||||
(response.thread, unread, latest_prompt),
|
||||
last_message,
|
||||
))
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(%thread_id, %error, "failed to read agent thread");
|
||||
|
||||
@@ -105,7 +105,9 @@ impl App {
|
||||
return;
|
||||
}
|
||||
self.agents_overview.pending_usage = None;
|
||||
if self.agents_overview.threads.contains_key(&thread_id) {
|
||||
if self.agents_overview.threads.contains_key(&thread_id)
|
||||
|| self.agents_overview.search_matches.contains_key(&thread_id)
|
||||
{
|
||||
let usage = self.agents_overview.usage.entry(thread_id).or_default();
|
||||
usage.fetched_at = Some(Instant::now());
|
||||
match result {
|
||||
|
||||
@@ -100,7 +100,7 @@ async fn selected_usage_is_cached_and_account_changes_discard_old_results() -> R
|
||||
request_id,
|
||||
Ok(AgentsOverviewThreadRefresh {
|
||||
cleared_unread: Vec::new(),
|
||||
threads: HashMap::from([(selected, Some((threads[0].clone(), false)))]),
|
||||
threads: HashMap::from([(selected, Some((threads[0].clone(), false, None)))]),
|
||||
last_messages: HashMap::new(),
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
|
||||
@@ -13,6 +13,7 @@ use grouping::model_name;
|
||||
|
||||
use super::agents_overview::AGENTS_OVERVIEW_VIEW_ID;
|
||||
use super::agents_overview_details::AgentsOverviewDetails;
|
||||
use super::agents_overview_discovery::HistorySearch;
|
||||
use super::agents_overview_discovery::supports_shared_pinning;
|
||||
use crate::app_event::AgentsOverviewAction;
|
||||
use crate::app_event::AppEvent;
|
||||
@@ -107,6 +108,7 @@ fn is_subagent(thread: &Thread) -> bool {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct AgentsOverviewRow {
|
||||
pub(super) search_id: Option<uuid::Uuid>,
|
||||
pub(super) details: AgentsOverviewDetails,
|
||||
pub(super) thread: Thread,
|
||||
pub(super) thread_id: ThreadId,
|
||||
@@ -173,7 +175,9 @@ pub(super) struct AgentsOverviewViewState {
|
||||
/// Browse only while the server holds its lifecycle lock.
|
||||
pub(super) removal: Option<AgentsOverviewAction>,
|
||||
pub(super) server_version_notice: Option<String>,
|
||||
search: String,
|
||||
pub(super) search: String,
|
||||
pub(super) history: Option<HistorySearch>,
|
||||
browse_selection: Option<ThreadId>,
|
||||
searching: bool,
|
||||
pub(super) grouping: AgentsOverviewGrouping,
|
||||
pub(super) rename_target: Option<ThreadId>,
|
||||
@@ -192,6 +196,29 @@ impl AgentsOverviewViewState {
|
||||
self.input = input;
|
||||
}
|
||||
|
||||
pub(super) fn restart_history_search(&mut self) {
|
||||
self.history =
|
||||
(!self.search.is_empty() && self.connection_notice.is_none()).then(|| HistorySearch {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
target: self.history.as_ref().map_or(10, |search| search.target),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
fn includes_row(&self, row: &AgentsOverviewRow) -> bool {
|
||||
row.search_id.is_none()
|
||||
|| row.search_id == self.history.as_ref().map(|search| search.id)
|
||||
|| self.rename_target == Some(row.thread_id)
|
||||
}
|
||||
|
||||
fn more_available(&self) -> bool {
|
||||
self.history
|
||||
.as_ref()
|
||||
.map_or(self.has_more && !self.searching, |search| {
|
||||
search.cursor.has_more()
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn editing_metadata(&self) -> bool {
|
||||
self.searching || self.rename_target.is_some()
|
||||
}
|
||||
@@ -362,7 +389,36 @@ impl AgentsOverviewView {
|
||||
self.state().show_more_selected = self.selected == usize::MAX;
|
||||
}
|
||||
|
||||
pub(super) fn load_search_matches(&self, additional: usize) {
|
||||
if self.state().history.is_none() {
|
||||
return;
|
||||
}
|
||||
let visible = self.visible_indices().len();
|
||||
let mut state = self.state();
|
||||
if state.more_available()
|
||||
&& let Some(search) = &mut state.history
|
||||
&& !search.loading
|
||||
{
|
||||
if additional > 0 {
|
||||
search.target = visible + additional;
|
||||
search.failed = false;
|
||||
}
|
||||
if search.failed || visible >= search.target {
|
||||
return;
|
||||
}
|
||||
search.loading = true;
|
||||
self.app_event_tx.send(AppEvent::SearchAgentsOverview(
|
||||
search.id,
|
||||
search.target - visible,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn activate(&mut self) {
|
||||
if self.selected == usize::MAX && self.state().searching {
|
||||
self.load_search_matches(/*additional*/ 10);
|
||||
return;
|
||||
}
|
||||
if self.selected == usize::MAX && self.state().has_more {
|
||||
if !self.state().loading {
|
||||
self.state().loading = true;
|
||||
@@ -392,6 +448,7 @@ impl AgentsOverviewView {
|
||||
});
|
||||
if self.state().searching {
|
||||
let mut state = self.state();
|
||||
state.history = None;
|
||||
state.search.clear();
|
||||
state.searching = false;
|
||||
}
|
||||
@@ -403,9 +460,12 @@ impl AgentsOverviewView {
|
||||
let searching = state.searching;
|
||||
if searching {
|
||||
edit(&mut state.search);
|
||||
state.restart_history_search();
|
||||
}
|
||||
drop(state);
|
||||
if searching {
|
||||
self.load_search_matches(/*additional*/ 10);
|
||||
self.state().show_more_selected = false;
|
||||
self.selected = self
|
||||
.selectable_indices()
|
||||
.first()
|
||||
@@ -493,7 +553,12 @@ impl AgentsOverviewView {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let preview = super::agents_overview_details::preview_markdown(&row.thread.preview);
|
||||
let preview = super::agents_overview_details::preview_markdown(
|
||||
row.details
|
||||
.latest_prompt
|
||||
.as_deref()
|
||||
.unwrap_or(&row.thread.preview),
|
||||
);
|
||||
let prompt_start = crate::wrapping::word_wrap_lines(lines.clone(), width).len();
|
||||
lines.extend([Line::default(), Line::from("Prompt".dim())]);
|
||||
let prompt = crate::markdown_render::render_markdown_lines_with_width_and_cwd(
|
||||
@@ -588,11 +653,19 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
fn on_ctrl_c(&mut self) -> CancellationEvent {
|
||||
let mut state = self.state();
|
||||
if state.editing_metadata() {
|
||||
let restore = state.searching.then_some(state.browse_selection);
|
||||
state.history = None;
|
||||
state.searching = false;
|
||||
state.rename_target = None;
|
||||
state.search.clear();
|
||||
state.input.set_text_clearing_elements("");
|
||||
drop(state);
|
||||
if let Some(selected) = restore {
|
||||
self.selected = selected
|
||||
.and_then(|id| self.rows.iter().position(|row| row.thread_id == id))
|
||||
.unwrap_or(usize::MAX);
|
||||
self.state().show_more_selected = self.selected == usize::MAX;
|
||||
}
|
||||
self.reconcile_command_center_selection();
|
||||
return CancellationEvent::Handled;
|
||||
}
|
||||
@@ -650,9 +723,12 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
if self.agents_keymap.search.is_pressed(key) {
|
||||
let mut state = self.state();
|
||||
if state.rename_target.is_none() {
|
||||
state.searching = !state.searching;
|
||||
if !state.searching {
|
||||
state.search.clear();
|
||||
if state.searching {
|
||||
drop(state);
|
||||
self.on_ctrl_c();
|
||||
} else {
|
||||
state.browse_selection = self.rows.get(self.selected).map(|row| row.thread_id);
|
||||
state.searching = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -740,6 +816,7 @@ impl BottomPaneView for AgentsOverviewView {
|
||||
row.thread.name.as_deref().unwrap_or_default(),
|
||||
&self.editor_keymap,
|
||||
);
|
||||
state.history = None;
|
||||
state.search.clear();
|
||||
state.searching = false;
|
||||
state.rename_target = Some(row.thread_id);
|
||||
|
||||
@@ -386,6 +386,7 @@ impl App {
|
||||
let destination = backends.session_at_mut(destination_slot);
|
||||
self.store_active_thread_receiver().await;
|
||||
self.clear_active_thread().await;
|
||||
self.agents_overview.search_matches.clear();
|
||||
self.agents_overview.pending_usage = None;
|
||||
self.agents_overview.usage_disabled = false;
|
||||
self.app_event_tx = AppEventSender::new(self.app_event_tx.app_event_tx.clone());
|
||||
@@ -393,6 +394,7 @@ impl App {
|
||||
self.reconnect = Default::default();
|
||||
if let Ok(mut state) = self.agents_overview.view_state.lock() {
|
||||
state.connection_notice = None;
|
||||
state.restart_history_search();
|
||||
}
|
||||
self.rate_limit_hard_stop_generation = self.rate_limit_hard_stop_generation.wrapping_add(1);
|
||||
self.rate_limit_refresh_state.invalidate_recovery();
|
||||
|
||||
@@ -415,6 +415,7 @@ impl App {
|
||||
if !thread.ephemeral
|
||||
&& !self.agents_overview.removed_threads.contains(&thread_id)
|
||||
&& !self.agents_overview.threads.get(&thread_id).is_some_and(Option::is_some) {
|
||||
self.agents_overview.search_matches.remove(&thread_id);
|
||||
self.agents_overview.threads.insert(thread_id, Some(thread));
|
||||
self.agents_overview.refresh_thread_ids.insert(thread_id);
|
||||
}
|
||||
@@ -3192,6 +3193,10 @@ impl App {
|
||||
}
|
||||
}
|
||||
AppEvent::OpenAgentsOverview => self.open_agents_overview(app_server),
|
||||
AppEvent::SearchAgentsOverview(id, limit) => self.search_agents_overview(app_server.request_handle(), id, limit),
|
||||
AppEvent::AgentsOverviewSearchPage { id, rows, cursor, ok } => {
|
||||
self.apply_agents_overview_search_page(id, rows, cursor, ok);
|
||||
}
|
||||
AppEvent::ShowMoreAgentsOverview => self.show_more_agents_overview(app_server),
|
||||
AppEvent::RefreshViewedThread { backend_id, thread_id, revision } => {
|
||||
if app_server.backend_id() == backend_id {
|
||||
|
||||
@@ -378,6 +378,7 @@ impl App {
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
state.connection_notice = None;
|
||||
state.restart_history_search();
|
||||
if state.creating_worktree {
|
||||
state.creating_worktree = false;
|
||||
self.pending_managed_worktree_creation = false;
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ expression: "screen(&view, area.width, area.height)"
|
||||
/tmp/project 1
|
||||
› ○ Task to find Ready 2m ago
|
||||
|
||||
|
||||
Searching history…
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -438,7 +438,7 @@ async fn reconnect_daemon_command_center_after_socket_replacement_without_a_conv
|
||||
last_messages: HashMap::new(),
|
||||
threads: stale_threads
|
||||
.into_iter()
|
||||
.map(|(id, thread)| (id, thread.map(|thread| (thread, false))))
|
||||
.map(|(id, thread)| (id, thread.map(|thread| (thread, false, None))))
|
||||
.collect(),
|
||||
recent_seed_complete: true,
|
||||
discovery: None,
|
||||
|
||||
@@ -281,7 +281,7 @@ pub(crate) enum RecapTrigger {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AgentsOverviewThreadRefresh {
|
||||
pub(crate) cleared_unread: Vec<ThreadId>,
|
||||
pub(crate) threads: std::collections::HashMap<ThreadId, Option<(Thread, bool)>>,
|
||||
pub(crate) threads: std::collections::HashMap<ThreadId, Option<(Thread, bool, Option<String>)>>,
|
||||
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>,
|
||||
@@ -367,6 +367,13 @@ pub(crate) enum AppEvent {
|
||||
/// Open the live command center for recent and locally retained root sessions.
|
||||
OpenAgentsOverview,
|
||||
ShowMoreAgentsOverview,
|
||||
SearchAgentsOverview(Uuid, usize),
|
||||
AgentsOverviewSearchPage {
|
||||
id: Uuid,
|
||||
rows: Vec<(Thread, bool)>,
|
||||
cursor: crate::app::agents_overview_discovery::AgentsOverviewDiscovery,
|
||||
ok: bool,
|
||||
},
|
||||
RefreshViewedThread {
|
||||
backend_id: crate::backend_id::BackendId,
|
||||
thread_id: ThreadId,
|
||||
|
||||
Reference in New Issue
Block a user