sidebar: Debounce Sidebar::update_entries

This commit is contained in:
Lukas Wirth 2026-05-27 11:11:22 +02:00
parent f839f8e108
commit a1907cc7cf
6 changed files with 69 additions and 29 deletions

View file

@ -74,9 +74,10 @@ pub use crate::agent_panel::{
};
use crate::agent_registry_ui::AgentRegistryPage;
pub use crate::inline_assistant::InlineAssistant;
pub use crate::message_editor::MessageEditorEvent;
pub use crate::thread_metadata_store::ThreadId;
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use conversation_view::ConversationView;
pub use conversation_view::{ConversationView, StateChange};
pub use external_source_prompt::ExternalSourcePrompt;
pub(crate) use mode_selector::ModeSelector;
pub(crate) use model_selector::ModelSelector;

View file

@ -456,6 +456,10 @@ pub(crate) struct RootThreadUpdated;
impl EventEmitter<RootThreadUpdated> for ConversationView {}
pub struct StateChange;
impl EventEmitter<StateChange> for ConversationView {}
fn resolve_outcome_from_selection(
options: &PermissionOptions,
selection: Option<&thread_view::PermissionSelection>,
@ -829,6 +833,7 @@ impl ConversationView {
}
self.server_state = state;
cx.emit(StateChange);
cx.emit(AcpServerViewEvent::ActiveThreadChanged);
if matches!(&self.server_state, ServerState::Connected(_)) {
cx.emit(RootThreadUpdated);
@ -1338,6 +1343,7 @@ impl ConversationView {
};
if let Some(connected) = this.as_connected_mut() {
connected.auth_state = auth_state;
cx.emit(StateChange);
if let Some(view) = connected.active_view()
&& view
.read(cx)
@ -1832,6 +1838,7 @@ impl ConversationView {
pending_auth_method.replace(method.clone());
let project = self.project.clone();
cx.emit(StateChange);
cx.notify();
self.auth_task = Some(cx.spawn_in(window, {
async move |this, cx| {
@ -1877,6 +1884,7 @@ impl ConversationView {
}) = this.as_connected_mut()
{
pending_auth_method.take();
cx.emit(StateChange);
}
if let Some(active) = this.root_thread_view() {
active.update(cx, |active, cx| {
@ -1898,6 +1906,7 @@ impl ConversationView {
pending_auth_method.replace(method.clone());
let authenticate = connection.authenticate(method, cx);
cx.emit(StateChange);
cx.notify();
self.auth_task = Some(cx.spawn_in(window, {
async move |this, cx| {
@ -1925,6 +1934,7 @@ impl ConversationView {
}) = this.as_connected_mut()
{
pending_auth_method.take();
cx.emit(StateChange);
}
if let Some(active) = this.root_thread_view() {
active.update(cx, |active, cx| active.handle_thread_error(err, cx));
@ -3009,6 +3019,7 @@ impl ConversationView {
pending_auth_method: None,
_subscription: None,
};
cx.emit(StateChange);
if let Some(view) = connected.active_view()
&& view
.read(cx)

View file

@ -1002,6 +1002,7 @@ impl ThreadView {
MessageEditorEvent::LostFocus => {}
MessageEditorEvent::SlashAutocompleteOpened => {}
MessageEditorEvent::InputAttempted { .. } => {}
MessageEditorEvent::Edited => {}
}
}
@ -1142,6 +1143,7 @@ impl ThreadView {
}
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::SlashAutocompleteOpened) => {
}
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Edited) => {}
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::InputAttempted { .. }) => {}
ViewEvent::OpenDiffLocation {
path,

View file

@ -206,6 +206,7 @@ pub enum MessageEditorEvent {
Cancel,
Focus,
LostFocus,
Edited,
/// Emitted when the user opens slash-command autocomplete in this
/// editor. Used by `ThreadView` to fire the global-skills scan
/// trigger; see `NativeAgent::ensure_skills_scan_started`.
@ -559,6 +560,7 @@ impl MessageEditor {
if let EditorEvent::Edited { .. } = event
&& !editor.read(cx).read_only(cx)
{
cx.emit(MessageEditorEvent::Edited);
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
this.mention_set

View file

@ -11474,8 +11474,11 @@ pub enum EditorEvent {
RestoreRequested {
hunks: Vec<MultiBufferDiffHunk>,
},
/// Emitted when an underlying buffer changes, including edits made through another editor.
BufferEdited,
/// Emitted when this editor creates, undoes, or redoes an edit transaction.
Edited {
/// The transaction that changed the editor's buffer.
transaction_id: clock::Lamport,
},
Reparsed(BufferId),

View file

@ -10,7 +10,6 @@ use agent_ui::terminal_thread_metadata_store::{
use agent_ui::thread_metadata_store::{
ThreadMetadata, ThreadMetadataStore, WorktreePaths, worktree_info_from_thread_paths,
};
use agent_ui::thread_worktree_archive;
use agent_ui::threads_archive_view::{
ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp,
fuzzy_match_positions,
@ -21,6 +20,7 @@ use agent_ui::{
NewThread, RenameSelectedThread, TerminalId, ThreadId, ThreadImportModal,
channels_with_threads, import_threads_from_other_channels,
};
use agent_ui::{MessageEditorEvent, StateChange, thread_worktree_archive};
use chrono::{DateTime, Utc};
use editor::Editor;
use feature_flags::{
@ -686,6 +686,7 @@ pub struct Sidebar {
project_header_menu_ix: Option<usize>,
_subscriptions: Vec<gpui::Subscription>,
_draft_editor_observations: Vec<gpui::Subscription>,
update_task: Option<Task<()>>,
/// For the thread import banners, if there is just one we show "Import
/// Threads" but if we are showing both the external agents and other
/// channels import banners then we change the text to disambiguate the
@ -720,15 +721,15 @@ impl Sidebar {
MultiWorkspaceEvent::ActiveWorkspaceChanged { .. } => {
this.sync_active_entry_from_active_workspace(cx);
this.replace_archived_panel_thread(window, cx);
this.update_entries(cx);
this.schedule_update_entries(false, cx);
}
MultiWorkspaceEvent::WorkspaceAdded(workspace) => {
this.subscribe_to_workspace(workspace, window, cx);
this.update_entries(cx);
this.schedule_update_entries(false, cx);
}
MultiWorkspaceEvent::WorkspaceRemoved(_)
| MultiWorkspaceEvent::ProjectGroupsChanged => {
this.update_entries(cx);
this.schedule_update_entries(false, cx);
}
},
)
@ -740,10 +741,7 @@ impl Sidebar {
if !query.is_empty() {
this.selection.take();
}
this.update_entries(cx);
if !query.is_empty() {
this.select_first_entry();
}
this.schedule_update_entries(!query.is_empty(), cx);
}
})
.detach();
@ -758,14 +756,14 @@ impl Sidebar {
.detach();
cx.observe(&ThreadMetadataStore::global(cx), |this, _store, cx| {
this.update_entries(cx);
this.schedule_update_entries(false, cx);
})
.detach();
cx.observe(
&TerminalThreadMetadataStore::global(cx),
|this, _store, cx| {
this.update_entries(cx);
this.schedule_update_entries(false, cx);
},
)
.detach();
@ -778,7 +776,7 @@ impl Sidebar {
this.subscribe_to_workspace(workspace, window, cx);
}
}
this.update_entries(cx);
this.schedule_update_entries(false, cx);
});
Self {
@ -808,6 +806,7 @@ impl Sidebar {
project_header_menu_ix: None,
_subscriptions: Vec::new(),
_draft_editor_observations: Vec::new(),
update_task: None,
import_banners_use_verbose_labels: None,
}
}
@ -862,11 +861,11 @@ impl Sidebar {
ProjectEvent::WorktreeAdded(_)
| ProjectEvent::WorktreeRemoved(_)
| ProjectEvent::WorktreeOrderChanged => {
this.update_entries(cx);
this.schedule_update_entries(false, cx);
}
ProjectEvent::WorktreePathsChanged { old_worktree_paths } => {
this.move_entry_paths(project, old_worktree_paths, cx);
this.update_entries(cx);
this.schedule_update_entries(false, cx);
}
_ => {}
},
@ -887,7 +886,7 @@ impl Sidebar {
_,
)
) {
this.update_entries(cx);
this.schedule_update_entries(false, cx);
}
},
)
@ -900,7 +899,7 @@ impl Sidebar {
if let workspace::Event::PanelAdded(view) = event {
if let Ok(agent_panel) = view.clone().downcast::<AgentPanel>() {
this.subscribe_to_agent_panel(workspace, &agent_panel, window, cx);
this.update_entries(cx);
this.schedule_update_entries(false, cx);
}
}
},
@ -992,7 +991,7 @@ impl Sidebar {
| AgentPanelEvent::ActiveViewFocused
| AgentPanelEvent::EntryChanged => {
this.sync_active_entry_from_panel(agent_panel, cx);
this.update_entries(cx);
this.schedule_update_entries(false, cx);
}
AgentPanelEvent::TerminalClosed { metadata } => {
if let Some(workspace) = workspace.upgrade() {
@ -1002,7 +1001,7 @@ impl Sidebar {
}
AgentPanelEvent::ThreadInteracted { thread_id } => {
this.record_thread_interacted(thread_id, cx);
this.update_entries(cx);
this.schedule_update_entries(false, cx);
}
},
)
@ -1858,6 +1857,24 @@ impl Sidebar {
};
}
fn schedule_update_entries(&mut self, select_first_after_update: bool, cx: &mut Context<Self>) {
if self.update_task.is_some() && !select_first_after_update {
return;
}
self.update_task = Some(cx.spawn(async move |this, cx| {
this.update(cx, |this, cx| {
this.update_task = None;
this.update_entries(cx);
if select_first_after_update {
this.select_first_entry();
cx.notify();
}
})
.ok();
}));
}
/// Rebuilds the sidebar's visible entries from already-cached state.
fn update_entries(&mut self, cx: &mut Context<Self>) {
let Some(multi_workspace) = self.multi_workspace.upgrade() else {
@ -1939,8 +1956,8 @@ impl Sidebar {
/// Re-establishes subscriptions to each visible draft's message editor
/// so we rebuild entries (and their displayed titles) as the user types.
fn refresh_draft_editor_observations(&mut self, cx: &mut Context<Self>) {
self._draft_editor_observations.clear();
let Some(multi_workspace) = self.multi_workspace.upgrade() else {
self._draft_editor_observations.clear();
return;
};
@ -1951,23 +1968,27 @@ impl Sidebar {
.flat_map(|panel| panel.read(cx).conversation_views())
.collect();
let mut subscriptions = Vec::with_capacity(draft_conversation_views.len());
for cv in draft_conversation_views {
if let Some(thread_view) = cv.read(cx).active_thread() {
let editor = thread_view.read(cx).message_editor.clone();
subscriptions.push(cx.observe(&editor, |this, _editor, cx| {
this.update_entries(cx);
}));
self._draft_editor_observations.push(cx.subscribe(
&editor,
|this, _editor, event, cx| match event {
MessageEditorEvent::Edited => this.schedule_update_entries(false, cx),
_ => (),
},
));
}
// Also observe the ConversationView itself so that editor
// Also subscribe to the ConversationView itself so that editor
// replacements during lifecycle transitions (Loading →
// Connected) re-wire the editor observation above.
subscriptions.push(cx.observe(&cv, |this, _cv, cx| {
this.refresh_draft_editor_observations(cx);
this.update_entries(cx);
}));
self._draft_editor_observations.push(cx.subscribe(
&cv,
|this, _cv, _event: &StateChange, cx| {
this.schedule_update_entries(false, cx);
},
));
}
self._draft_editor_observations = subscriptions;
}
fn select_first_entry(&mut self) {