Detailed changes
@@ -302,6 +302,20 @@ impl MessageEditor {
}
}
+ pub fn set_command_state(
+ &mut self,
+ prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
+ available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+ cached_user_commands: Rc<RefCell<collections::HashMap<String, UserSlashCommand>>>,
+ cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
+ _cx: &mut Context<Self>,
+ ) {
+ self.prompt_capabilities = prompt_capabilities;
+ self.available_commands = available_commands;
+ self.cached_user_commands = cached_user_commands;
+ self.cached_user_command_errors = cached_user_command_errors;
+ }
+
fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> {
let available_commands = self.available_commands.borrow();
if available_commands.is_empty() {
@@ -87,6 +87,9 @@ use crate::{
const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);
const TOKEN_THRESHOLD: u64 = 250;
+mod active_thread;
+pub use active_thread::*;
+
pub struct QueuedMessage {
pub content: Vec<acp::ContentBlock>,
pub tracked_buffers: Vec<Entity<Buffer>>,
@@ -326,86 +329,45 @@ pub struct AcpThreadView {
agent_server_store: Entity<AgentServerStore>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
+ thread_store: Option<Entity<ThreadStore>>,
+ prompt_store: Option<Entity<PromptStore>>,
thread_state: ThreadState,
- permission_dropdown_handle: PopoverMenuHandle<ContextMenu>,
- /// Tracks the selected granularity index for each tool call's permission dropdown.
- /// The index corresponds to the position in the allow_options list.
- /// Default is the last option (index pointing to "Only this time").
- selected_permission_granularity: HashMap<acp::ToolCallId, usize>,
- login: Option<task::SpawnInTerminal>,
+ login: Option<task::SpawnInTerminal>, // is some <=> Active | Unauthenticated
recent_history_entries: Vec<AgentSessionInfo>,
history: Entity<AcpThreadHistory>,
_history_subscription: Subscription,
hovered_recent_history_item: Option<usize>,
- entry_view_state: Entity<EntryViewState>,
message_editor: Entity<MessageEditor>,
focus_handle: FocusHandle,
- model_selector: Option<Entity<AcpModelSelectorPopover>>,
- config_options_view: Option<Entity<ConfigOptionsView>>,
- profile_selector: Option<Entity<ProfileSelector>>,
notifications: Vec<WindowHandle<AgentNotification>>,
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
- thread_retry_status: Option<RetryStatus>,
- thread_error: Option<ThreadError>,
- thread_error_markdown: Option<Entity<Markdown>>,
- command_load_errors: Vec<CommandLoadError>,
- command_load_errors_dismissed: bool,
slash_command_registry: Option<Entity<SlashCommandRegistry>>,
- token_limit_callout_dismissed: bool,
- thread_feedback: ThreadFeedbackState,
- list_state: ListState,
auth_task: Option<Task<()>>,
- /// Tracks which tool calls have their content/output expanded.
- /// Used for showing/hiding tool call results, terminal output, etc.
- expanded_tool_calls: HashSet<acp::ToolCallId>,
- expanded_tool_call_raw_inputs: HashSet<acp::ToolCallId>,
- expanded_thinking_blocks: HashSet<(usize, usize)>,
- expanded_subagents: HashSet<acp::SessionId>,
- subagent_scroll_handles: RefCell<HashMap<acp::SessionId, ScrollHandle>>,
- edits_expanded: bool,
- plan_expanded: bool,
- queue_expanded: bool,
- editor_expanded: bool,
- should_be_following: bool,
- editing_message: Option<usize>,
- local_queued_messages: Vec<QueuedMessage>,
- queued_message_editors: Vec<Entity<MessageEditor>>,
- queued_message_editor_subscriptions: Vec<Subscription>,
- last_synced_queue_length: usize,
- discarded_partial_edits: HashSet<acp::ToolCallId>,
- prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
- available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
- cached_user_commands: Rc<RefCell<HashMap<String, UserSlashCommand>>>,
- cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
- is_loading_contents: bool,
- new_server_version_available: Option<SharedString>,
- resume_thread_metadata: Option<AgentSessionInfo>,
- resumed_without_history: bool,
- _cancel_task: Option<Task<()>>,
- _subscriptions: [Subscription; 5],
+ _subscriptions: [Subscription; 4],
show_codex_windows_warning: bool,
in_flight_prompt: Option<Vec<acp::ContentBlock>>,
- skip_queue_processing_count: usize,
- user_interrupted_generation: bool,
- can_fast_track_queue: bool,
- turn_tokens: Option<u64>,
- last_turn_tokens: Option<u64>,
- turn_started_at: Option<Instant>,
- last_turn_duration: Option<Duration>,
- turn_generation: usize,
- _turn_timer_task: Option<Task<()>>,
- hovered_edited_file_buttons: Option<usize>,
add_context_menu_handle: PopoverMenuHandle<ContextMenu>,
}
+impl AcpThreadView {
+ pub fn as_active_thread(&self) -> Option<&ActiveThreadState> {
+ match &self.thread_state {
+ ThreadState::Active(active) => Some(active),
+ _ => None,
+ }
+ }
+
+ pub fn as_active_thread_mut(&mut self) -> Option<&mut ActiveThreadState> {
+ match &mut self.thread_state {
+ ThreadState::Active(active) => Some(active),
+ _ => None,
+ }
+ }
+}
+
enum ThreadState {
Loading(Entity<LoadingView>),
- Ready {
- thread: Entity<AcpThread>,
- title_editor: Option<Entity<Editor>>,
- mode_selector: Option<Entity<ModeSelector>>,
- _subscriptions: Vec<Subscription>,
- },
+ Active(ActiveThreadState),
LoadError(LoadError),
Unauthenticated {
connection: Rc<dyn AgentConnection>,
@@ -439,7 +401,6 @@ impl AcpThreadView {
let available_commands = Rc::new(RefCell::new(vec![]));
let cached_user_commands = Rc::new(RefCell::new(collections::HashMap::default()));
let cached_user_command_errors = Rc::new(RefCell::new(Vec::new()));
- let mut command_load_errors = Vec::new();
let agent_server_store = project.read(cx).agent_server_store().clone();
let agent_display_name = agent_server_store
@@ -475,28 +436,10 @@ impl AcpThreadView {
editor
});
- let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
-
- let entry_view_state = cx.new(|_| {
- EntryViewState::new(
- workspace.clone(),
- project.downgrade(),
- thread_store.clone(),
- history.downgrade(),
- prompt_store.clone(),
- prompt_capabilities.clone(),
- available_commands.clone(),
- cached_user_commands.clone(),
- cached_user_command_errors.clone(),
- agent.name(),
- )
- });
-
let subscriptions = [
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
- cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
cx.subscribe_in(
&agent_server_store,
window,
@@ -550,7 +493,6 @@ impl AcpThreadView {
&mut errors,
&server_command_names,
);
- command_load_errors = errors.clone();
*cached_user_commands.borrow_mut() = commands;
*cached_user_command_errors.borrow_mut() = errors;
@@ -569,100 +511,78 @@ impl AcpThreadView {
agent_server_store,
workspace: workspace.clone(),
project: project.clone(),
- entry_view_state,
- permission_dropdown_handle: PopoverMenuHandle::default(),
- selected_permission_granularity: HashMap::default(),
+ thread_store,
+ prompt_store,
thread_state: Self::initial_state(
agent.clone(),
- resume_thread.clone(),
+ resume_thread,
workspace.clone(),
project.clone(),
+ prompt_capabilities,
+ available_commands,
+ cached_user_commands,
+ cached_user_command_errors,
window,
cx,
),
login: None,
message_editor,
- model_selector: None,
- config_options_view: None,
- profile_selector: None,
notifications: Vec::new(),
notification_subscriptions: HashMap::default(),
- list_state,
- thread_retry_status: None,
- thread_error: None,
- thread_error_markdown: None,
- command_load_errors,
- command_load_errors_dismissed: false,
slash_command_registry,
- token_limit_callout_dismissed: false,
- thread_feedback: Default::default(),
auth_task: None,
- expanded_tool_calls: HashSet::default(),
- expanded_tool_call_raw_inputs: HashSet::default(),
- expanded_thinking_blocks: HashSet::default(),
- expanded_subagents: HashSet::default(),
- subagent_scroll_handles: RefCell::new(HashMap::default()),
- editing_message: None,
- local_queued_messages: Vec::new(),
- queued_message_editors: Vec::new(),
- queued_message_editor_subscriptions: Vec::new(),
- last_synced_queue_length: 0,
- edits_expanded: false,
- plan_expanded: false,
- queue_expanded: true,
- discarded_partial_edits: HashSet::default(),
- prompt_capabilities,
- available_commands,
- cached_user_commands,
- cached_user_command_errors,
- editor_expanded: false,
- should_be_following: false,
recent_history_entries,
history,
_history_subscription: history_subscription,
hovered_recent_history_item: None,
- is_loading_contents: false,
_subscriptions: subscriptions,
- _cancel_task: None,
focus_handle: cx.focus_handle(),
- new_server_version_available: None,
- resume_thread_metadata: resume_thread,
- resumed_without_history: false,
show_codex_windows_warning,
in_flight_prompt: None,
- skip_queue_processing_count: 0,
- user_interrupted_generation: false,
- can_fast_track_queue: false,
- turn_tokens: None,
- last_turn_tokens: None,
- turn_started_at: None,
- last_turn_duration: None,
- turn_generation: 0,
- _turn_timer_task: None,
- hovered_edited_file_buttons: None,
add_context_menu_handle: PopoverMenuHandle::default(),
}
}
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
+ let available_commands = Rc::new(RefCell::new(vec![]));
+ let cached_user_commands = Rc::new(RefCell::new(collections::HashMap::default()));
+ let cached_user_command_errors = Rc::new(RefCell::new(Vec::new()));
+
+ let resume_thread_metadata = if let ThreadState::Active(ActiveThreadState {
+ resume_thread_metadata,
+ ..
+ }) = &self.thread_state
+ {
+ resume_thread_metadata.clone()
+ } else {
+ None
+ };
+
+ self.message_editor.update(cx, |editor, cx| {
+ editor.set_command_state(
+ prompt_capabilities.clone(),
+ available_commands.clone(),
+ cached_user_commands.clone(),
+ cached_user_command_errors.clone(),
+ cx,
+ );
+ });
+
self.thread_state = Self::initial_state(
self.agent.clone(),
- self.resume_thread_metadata.clone(),
+ resume_thread_metadata,
self.workspace.clone(),
self.project.clone(),
+ prompt_capabilities,
+ available_commands,
+ cached_user_commands,
+ cached_user_command_errors,
window,
cx,
);
- self.available_commands.replace(vec![]);
self.refresh_cached_user_commands(cx);
- self.new_server_version_available.take();
self.recent_history_entries.clear();
- self.turn_tokens = None;
- self.last_turn_tokens = None;
- self.turn_started_at = None;
- self.last_turn_duration = None;
- self._turn_timer_task = None;
- self.resumed_without_history = false;
cx.notify();
}
@@ -671,6 +591,10 @@ impl AcpThreadView {
resume_thread: Option<AgentSessionInfo>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
+ prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
+ available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+ cached_user_commands: Rc<RefCell<HashMap<String, UserSlashCommand>>>,
+ cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> ThreadState {
@@ -795,16 +719,30 @@ impl AcpThreadView {
Ok(thread) => {
let action_log = thread.read(cx).action_log().clone();
- this.resumed_without_history = resumed_without_history;
- this.prompt_capabilities
- .replace(thread.read(cx).prompt_capabilities());
+ prompt_capabilities.replace(thread.read(cx).prompt_capabilities());
+
+ let entry_view_state = cx.new(|_| {
+ EntryViewState::new(
+ this.workspace.clone(),
+ this.project.downgrade(),
+ this.thread_store.clone(),
+ this.history.downgrade(),
+ this.prompt_store.clone(),
+ prompt_capabilities.clone(),
+ available_commands.clone(),
+ cached_user_commands.clone(),
+ cached_user_command_errors.clone(),
+ this.agent.name(),
+ )
+ });
let count = thread.read(cx).entries().len();
- this.entry_view_state.update(cx, |view_state, cx| {
+ let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
+ entry_view_state.update(cx, |view_state, cx| {
for ix in 0..count {
view_state.sync_entry(ix, &thread, window, cx);
}
- this.list_state.splice_focusable(
+ list_state.splice_focusable(
0..0,
(0..count).map(|ix| view_state.entry(ix)?.focus_handle(cx)),
);
@@ -829,20 +767,22 @@ impl AcpThreadView {
let config_options_provider =
connection.session_config_options(&session_id, cx);
+ let config_options_view;
let mode_selector;
+ let model_selector;
if let Some(config_options) = config_options_provider {
// Use config options - don't create mode_selector or model_selector
let agent_server = this.agent.clone();
let fs = this.project.read(cx).fs().clone();
- this.config_options_view = Some(cx.new(|cx| {
+ config_options_view = Some(cx.new(|cx| {
ConfigOptionsView::new(config_options, agent_server, fs, window, cx)
}));
- this.model_selector = None;
+ model_selector = None;
mode_selector = None;
} else {
// Fall back to legacy mode/model selectors
- this.config_options_view = None;
- this.model_selector =
+ config_options_view = None;
+ model_selector =
connection.model_selector(&session_id).map(|selector| {
let agent_server = this.agent.clone();
let fs = this.project.read(cx).fs().clone();
@@ -879,6 +819,11 @@ impl AcpThreadView {
let mut subscriptions = vec![
cx.subscribe_in(&thread, window, Self::handle_thread_event),
cx.observe(&action_log, |_, _, cx| cx.notify()),
+ cx.subscribe_in(
+ &entry_view_state,
+ window,
+ Self::handle_entry_view_event,
+ ),
];
let title_editor =
@@ -898,23 +843,39 @@ impl AcpThreadView {
None
};
- this.thread_state = ThreadState::Ready {
+ let profile_selector: Option<Rc<agent::NativeAgentConnection>> =
+ connection.clone().downcast();
+ let profile_selector = profile_selector
+ .and_then(|native_connection| native_connection.thread(&session_id, cx))
+ .map(|native_thread| {
+ cx.new(|cx| {
+ ProfileSelector::new(
+ <dyn Fs>::global(cx),
+ Arc::new(native_thread),
+ this.focus_handle(cx),
+ cx,
+ )
+ })
+ });
+
+ this.thread_state = ThreadState::Active(ActiveThreadState::new(
thread,
+ workspace.clone(),
+ entry_view_state,
title_editor,
+ config_options_view,
mode_selector,
- _subscriptions: subscriptions,
- };
-
- this.profile_selector = this.as_native_thread(cx).map(|thread| {
- cx.new(|cx| {
- ProfileSelector::new(
- <dyn Fs>::global(cx),
- Arc::new(thread.clone()),
- this.focus_handle(cx),
- cx,
- )
- })
- });
+ model_selector,
+ profile_selector,
+ list_state,
+ prompt_capabilities,
+ available_commands,
+ cached_user_commands,
+ cached_user_command_errors,
+ resumed_without_history,
+ resume_thread.clone(),
+ subscriptions,
+ ));
if this.focus_handle.contains_focused(window, cx) {
this.message_editor.focus_handle(cx).focus(window, cx);
@@ -934,7 +895,13 @@ impl AcpThreadView {
while let Ok(new_version) = new_version_available_rx.recv().await {
if let Some(new_version) = new_version {
this.update(cx, |this, cx| {
- this.new_server_version_available = Some(new_version.into());
+ if let ThreadState::Active(ActiveThreadState {
+ new_server_version_available,
+ ..
+ }) = &mut this.thread_state
+ {
+ *new_server_version_available = Some(new_version.into());
+ }
cx.notify();
})
.ok();
@@ -1054,12 +1021,20 @@ impl AcpThreadView {
// If we're in a LoadError state OR have a thread_error set (which can happen
// when agent.connect() fails during loading), retry loading the thread.
// This handles the case where a thread is restored before authentication completes.
- let should_retry =
- matches!(&self.thread_state, ThreadState::LoadError(_)) || self.thread_error.is_some();
+ let should_retry = match &self.thread_state {
+ ThreadState::LoadError(_)
+ | ThreadState::Active(ActiveThreadState {
+ thread_error: Some(_),
+ ..
+ }) => true,
+ _ => false,
+ };
if should_retry {
- self.thread_error = None;
- self.thread_error_markdown = None;
+ if let Some(active) = self.as_active_thread_mut() {
+ active.thread_error = None;
+ active.thread_error_markdown = None;
+ }
self.reset(window, cx);
}
}
@@ -1068,27 +1043,11 @@ impl AcpThreadView {
&self.workspace
}
- pub fn thread(&self) -> Option<&Entity<AcpThread>> {
- match &self.thread_state {
- ThreadState::Ready { thread, .. } => Some(thread),
- ThreadState::Unauthenticated { .. }
- | ThreadState::Loading { .. }
- | ThreadState::LoadError { .. } => None,
- }
- }
-
- pub fn mode_selector(&self) -> Option<&Entity<ModeSelector>> {
- match &self.thread_state {
- ThreadState::Ready { mode_selector, .. } => mode_selector.as_ref(),
- ThreadState::Unauthenticated { .. }
- | ThreadState::Loading { .. }
- | ThreadState::LoadError { .. } => None,
- }
- }
-
pub fn title(&self, cx: &App) -> SharedString {
match &self.thread_state {
- ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
+ ThreadState::Active(ActiveThreadState { .. }) | ThreadState::Unauthenticated { .. } => {
+ "New Thread".into()
+ }
ThreadState::Loading(loading_view) => loading_view.read(cx).title.clone(),
ThreadState::LoadError(error) => match error {
LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
@@ -1101,21 +1060,9 @@ impl AcpThreadView {
}
}
- pub fn title_editor(&self) -> Option<Entity<Editor>> {
- if let ThreadState::Ready { title_editor, .. } = &self.thread_state {
- title_editor.clone()
- } else {
- None
- }
- }
-
pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
- self.thread_error.take();
- self.thread_retry_status.take();
- self.user_interrupted_generation = true;
-
- if let Some(thread) = self.thread() {
- self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
+ if let Some(active) = self.as_active_thread_mut() {
+ active.cancel_generation(cx);
}
}
@@ -1180,7 +1127,7 @@ impl AcpThreadView {
return;
}
- let Some(thread) = self.thread() else {
+ let Some(active) = self.as_active_thread() else {
return;
};
@@ -1194,7 +1141,7 @@ impl AcpThreadView {
let thread_store = session_list.thread_store().clone();
let client = self.project.read(cx).client();
- let session_id = thread.read(cx).session_id().clone();
+ let session_id = active.thread.read(cx).session_id().clone();
cx.spawn_in(window, async move |this, cx| {
let response = client
@@ -1222,7 +1169,9 @@ impl AcpThreadView {
};
this.update_in(cx, |this, window, cx| {
- this.resume_thread_metadata = Some(thread_metadata);
+ if let Some(active) = this.as_active_thread_mut() {
+ active.resume_thread_metadata = Some(thread_metadata);
+ }
this.reset(window, cx);
})?;
@@ -1253,35 +1202,17 @@ impl AcpThreadView {
_window: &mut Window,
cx: &mut Context<Self>,
) {
- self.set_editor_is_expanded(!self.editor_expanded, cx);
- cx.stop_propagation();
- cx.notify();
+ let editor = self.message_editor.clone();
+ if let Some(active) = self.as_active_thread_mut() {
+ active.expand_message_editor(editor, cx);
+ }
}
fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
- self.editor_expanded = is_expanded;
- self.message_editor.update(cx, |editor, cx| {
- if is_expanded {
- editor.set_mode(
- EditorMode::Full {
- scale_ui_elements_with_buffer_font_size: false,
- show_active_line_background: false,
- sizing_behavior: SizingBehavior::ExcludeOverscrollMargin,
- },
- cx,
- )
- } else {
- let agent_settings = AgentSettings::get_global(cx);
- editor.set_mode(
- EditorMode::AutoHeight {
- min_lines: agent_settings.message_editor_min_lines,
- max_lines: Some(agent_settings.set_message_editor_max_lines()),
- },
- cx,
- )
- }
- });
- cx.notify();
+ let editor = self.message_editor.clone();
+ if let Some(active) = self.as_active_thread_mut() {
+ active.set_editor_is_expanded(is_expanded, editor, cx);
+ }
}
pub fn handle_title_editor_event(
@@ -1291,25 +1222,8 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let Some(thread) = self.thread() else { return };
-
- match event {
- EditorEvent::BufferEdited => {
- let new_title = title_editor.read(cx).text(cx);
- thread.update(cx, |thread, cx| {
- thread
- .set_title(new_title.into(), cx)
- .detach_and_log_err(cx);
- })
- }
- EditorEvent::Blurred => {
- if title_editor.read(cx).text(cx).is_empty() {
- title_editor.update(cx, |editor, cx| {
- editor.set_text("New Thread", window, cx);
- });
- }
- }
- _ => {}
+ if let Some(active) = self.as_active_thread_mut() {
+ active.handle_title_editor_event(title_editor, event, window, cx);
}
}
@@ -1341,35 +1255,45 @@ impl AcpThreadView {
match &event.view_event {
ViewEvent::NewDiff(tool_call_id) => {
if AgentSettings::get_global(cx).expand_edit_card {
- self.expanded_tool_calls.insert(tool_call_id.clone());
+ if let Some(active) = self.as_active_thread_mut() {
+ active.expanded_tool_calls.insert(tool_call_id.clone());
+ }
}
}
ViewEvent::NewTerminal(tool_call_id) => {
if AgentSettings::get_global(cx).expand_terminal_card {
- self.expanded_tool_calls.insert(tool_call_id.clone());
+ if let Some(active) = self.as_active_thread_mut() {
+ active.expanded_tool_calls.insert(tool_call_id.clone());
+ }
}
}
ViewEvent::TerminalMovedToBackground(tool_call_id) => {
- self.expanded_tool_calls.remove(tool_call_id);
+ if let Some(active) = self.as_active_thread_mut() {
+ active.expanded_tool_calls.remove(tool_call_id);
+ }
}
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
- if let Some(thread) = self.thread()
+ if let Some(active) = self.as_active_thread()
&& let Some(AgentThreadEntry::UserMessage(user_message)) =
- thread.read(cx).entries().get(event.entry_index)
+ active.thread.read(cx).entries().get(event.entry_index)
&& user_message.id.is_some()
{
- self.editing_message = Some(event.entry_index);
+ if let Some(active) = self.as_active_thread_mut() {
+ active.editing_message = Some(event.entry_index);
+ }
cx.notify();
}
}
ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => {
- if let Some(thread) = self.thread()
+ if let Some(active) = self.as_active_thread()
&& let Some(AgentThreadEntry::UserMessage(user_message)) =
- thread.read(cx).entries().get(event.entry_index)
+ active.thread.read(cx).entries().get(event.entry_index)
&& user_message.id.is_some()
{
if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) {
- self.editing_message = None;
+ if let Some(active) = self.as_active_thread_mut() {
+ active.editing_message = None;
+ }
cx.notify();
}
}
@@ -1389,165 +1313,43 @@ impl AcpThreadView {
}
fn retry_generation(&mut self, cx: &mut Context<Self>) {
- self.thread_error.take();
- let Some(thread) = self.thread() else {
- return;
+ if let Some(active) = self.as_active_thread_mut() {
+ active.retry_generation(cx);
};
- if !thread.read(cx).can_retry(cx) {
- return;
- }
-
- let task = thread.update(cx, |thread, cx| thread.retry(cx));
- cx.spawn(async move |this, cx| {
- let result = task.await;
-
- this.update(cx, |this, cx| {
- if let Err(err) = result {
- this.handle_thread_error(err, cx);
- }
- })
- })
- .detach();
}
fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let Some(thread) = self.thread() else { return };
-
- if self.is_loading_contents {
- return;
- }
-
- let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
- let is_generating = thread.read(cx).status() != ThreadStatus::Idle;
-
- let has_queued = self.has_queued_messages();
- if is_editor_empty && self.can_fast_track_queue && has_queued {
- self.can_fast_track_queue = false;
- self.send_queued_message_at_index(0, true, window, cx);
- return;
- }
-
- if is_editor_empty {
- return;
- }
-
- if is_generating {
- self.queue_message(window, cx);
- return;
- }
-
- let text = self.message_editor.read(cx).text(cx);
- let text = text.trim();
- if text == "/login" || text == "/logout" {
- let ThreadState::Ready { thread, .. } = &self.thread_state else {
- return;
- };
-
- let connection = thread.read(cx).connection().clone();
- let can_login = !connection.auth_methods().is_empty() || self.login.is_some();
- // Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
- let logout_supported = text == "/logout"
- && self
- .available_commands
- .borrow()
- .iter()
- .any(|command| command.name == "logout");
- if can_login && !logout_supported {
- self.message_editor
- .update(cx, |editor, cx| editor.clear(window, cx));
+ let message_editor = self.message_editor.clone();
+ let agent = self.agent.clone();
+ let login = self.login.clone();
- let this = cx.weak_entity();
- let agent = self.agent.clone();
- window.defer(cx, |window, cx| {
- Self::handle_auth_required(
- this,
- AuthRequired::new(),
- agent,
- connection,
- window,
- cx,
- );
- });
- cx.notify();
- return;
- }
+ if let Some(active) = self.as_active_thread_mut() {
+ active.send(message_editor, agent, login, window, cx);
}
-
- self.send_impl(self.message_editor.clone(), window, cx)
}
fn interrupt_and_send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let Some(thread) = self.thread() else {
- return;
- };
-
- if self.is_loading_contents {
- return;
- }
-
- if thread.read(cx).status() == ThreadStatus::Idle {
- self.send_impl(self.message_editor.clone(), window, cx);
- return;
- }
-
- self.stop_current_and_send_new_message(window, cx);
- }
-
- fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let Some(thread) = self.thread().cloned() else {
- return;
+ let message_editor = self.message_editor.clone();
+ if let Some(active) = self.as_active_thread_mut() {
+ active.interrupt_and_send(message_editor, window, cx);
};
-
- self.skip_queue_processing_count = 0;
- self.user_interrupted_generation = true;
-
- let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx));
-
- cx.spawn_in(window, async move |this, cx| {
- cancelled.await;
-
- this.update_in(cx, |this, window, cx| {
- this.send_impl(this.message_editor.clone(), window, cx);
- })
- .ok();
- })
- .detach();
}
fn start_turn(&mut self, cx: &mut Context<Self>) -> usize {
- self.turn_generation += 1;
- let generation = self.turn_generation;
- self.turn_started_at = Some(Instant::now());
- self.last_turn_duration = None;
- self.last_turn_tokens = None;
- self.turn_tokens = Some(0);
- self._turn_timer_task = Some(cx.spawn(async move |this, cx| {
- loop {
- cx.background_executor().timer(Duration::from_secs(1)).await;
- if this.update(cx, |_, cx| cx.notify()).is_err() {
- break;
- }
- }
- }));
- generation
+ self.as_active_thread_mut()
+ .map(|active| active.start_turn(cx))
+ .unwrap_or(0)
}
fn stop_turn(&mut self, generation: usize) {
- if self.turn_generation != generation {
- return;
+ if let Some(active) = self.as_active_thread_mut() {
+ active.stop_turn(generation);
}
- self.last_turn_duration = self.turn_started_at.take().map(|started| started.elapsed());
- self.last_turn_tokens = self.turn_tokens.take();
- self._turn_timer_task = None;
}
fn update_turn_tokens(&mut self, cx: &App) {
- if let Some(thread) = self.thread() {
- if let Some(usage) = thread.read(cx).token_usage() {
- if let Some(ref mut tokens) = self.turn_tokens {
- *tokens += usage.output_tokens;
- }
- }
+ if let Some(active) = self.as_active_thread_mut() {
+ active.update_turn_tokens(cx);
}
}
@@ -1577,11 +1379,23 @@ impl AcpThreadView {
)
});
- self.thread_error.take();
- self.editing_message.take();
- self.thread_feedback.clear();
+ if let ThreadState::Active(ActiveThreadState {
+ thread_error,
+ thread_feedback,
+ editing_message,
+ ..
+ }) = &mut self.thread_state
+ {
+ thread_error.take();
+ thread_feedback.clear();
+ editing_message.take();
+ }
- if self.should_be_following {
+ if let ThreadState::Active(ActiveThreadState {
+ should_be_following: true,
+ ..
+ }) = &self.thread_state
+ {
self.workspace
.update(cx, |workspace, cx| {
workspace.follow(CollaboratorId::Agent, window, cx);
@@ -1614,159 +1428,9 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let Some(thread) = self.thread() else {
- return;
+ if let Some(active) = self.as_active_thread_mut() {
+ active.send_content(contents_task, window, cx);
};
- let session_id = thread.read(cx).session_id().clone();
- let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
- let thread = thread.downgrade();
-
- self.is_loading_contents = true;
- let model_id = self.current_model_id(cx);
- let mode_id = self.current_mode_id(cx);
- let guard = cx.new(|_| ());
- cx.observe_release(&guard, |this, _guard, cx| {
- this.is_loading_contents = false;
- cx.notify();
- })
- .detach();
-
- let task = cx.spawn_in(window, async move |this, cx| {
- let Some((contents, tracked_buffers)) = contents_task.await? else {
- return Ok(());
- };
-
- let generation = this.update_in(cx, |this, _window, cx| {
- this.in_flight_prompt = Some(contents.clone());
- let generation = this.start_turn(cx);
- this.set_editor_is_expanded(false, cx);
- this.scroll_to_bottom(cx);
- generation
- })?;
-
- let _stop_turn = defer({
- let this = this.clone();
- let mut cx = cx.clone();
- move || {
- this.update(&mut cx, |this, cx| {
- this.stop_turn(generation);
- cx.notify();
- })
- .ok();
- }
- });
- let turn_start_time = Instant::now();
- let send = thread.update(cx, |thread, cx| {
- thread.action_log().update(cx, |action_log, cx| {
- for buffer in tracked_buffers {
- action_log.buffer_read(buffer, cx)
- }
- });
- drop(guard);
-
- telemetry::event!(
- "Agent Message Sent",
- agent = agent_telemetry_id,
- session = session_id,
- model = model_id,
- mode = mode_id
- );
-
- thread.send(contents, cx)
- })?;
- let res = send.await;
- let turn_time_ms = turn_start_time.elapsed().as_millis();
- drop(_stop_turn);
- let status = if res.is_ok() {
- this.update(cx, |this, _| this.in_flight_prompt.take()).ok();
- "success"
- } else {
- "failure"
- };
- telemetry::event!(
- "Agent Turn Completed",
- agent = agent_telemetry_id,
- session = session_id,
- model = model_id,
- mode = mode_id,
- status,
- turn_time_ms,
- );
- res
- });
-
- cx.spawn(async move |this, cx| {
- if let Err(err) = task.await {
- this.update(cx, |this, cx| {
- this.handle_thread_error(err, cx);
- })
- .ok();
- } else {
- this.update(cx, |this, cx| {
- this.should_be_following = this
- .workspace
- .update(cx, |workspace, _| {
- workspace.is_being_followed(CollaboratorId::Agent)
- })
- .unwrap_or_default();
- })
- .ok();
- }
- })
- .detach();
- }
-
- fn queue_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let is_idle = self
- .thread()
- .map(|t| t.read(cx).status() == acp_thread::ThreadStatus::Idle)
- .unwrap_or(true);
-
- if is_idle {
- self.send_impl(self.message_editor.clone(), window, cx);
- return;
- }
-
- let full_mention_content = self.as_native_thread(cx).is_some_and(|thread| {
- let thread = thread.read(cx);
- AgentSettings::get_global(cx)
- .profiles
- .get(thread.profile())
- .is_some_and(|profile| profile.tools.is_empty())
- });
-
- let cached_commands = self.cached_slash_commands(cx);
- let cached_errors = self.cached_slash_command_errors(cx);
- let contents = self.message_editor.update(cx, |message_editor, cx| {
- message_editor.contents_with_cache(
- full_mention_content,
- Some(cached_commands),
- Some(cached_errors),
- cx,
- )
- });
-
- let message_editor = self.message_editor.clone();
-
- cx.spawn_in(window, async move |this, cx| {
- let (content, tracked_buffers) = contents.await?;
-
- if content.is_empty() {
- return Ok::<(), anyhow::Error>(());
- }
-
- this.update_in(cx, |this, window, cx| {
- this.add_to_queue(content, tracked_buffers, cx);
- // Enable fast-track: user can press Enter again to send this queued message immediately
- this.can_fast_track_queue = true;
- message_editor.update(cx, |message_editor, cx| {
- message_editor.clear(window, cx);
- });
- cx.notify();
- })?;
- Ok(())
- })
- .detach_and_log_err(cx);
}
fn send_queued_message_at_index(
@@ -0,0 +1,1224 @@
+use super::*;
+
+pub struct ActiveThreadState {
+ pub thread: Entity<AcpThread>,
+ pub workspace: WeakEntity<Workspace>,
+ pub entry_view_state: Entity<EntryViewState>,
+ pub title_editor: Option<Entity<Editor>>,
+ pub config_options_view: Option<Entity<ConfigOptionsView>>,
+ pub mode_selector: Option<Entity<ModeSelector>>,
+ pub model_selector: Option<Entity<AcpModelSelectorPopover>>,
+ pub profile_selector: Option<Entity<ProfileSelector>>,
+ pub permission_dropdown_handle: PopoverMenuHandle<ContextMenu>,
+ pub thread_retry_status: Option<RetryStatus>,
+ pub(super) thread_error: Option<ThreadError>,
+ pub thread_error_markdown: Option<Entity<Markdown>>,
+ pub token_limit_callout_dismissed: bool,
+ pub(super) thread_feedback: ThreadFeedbackState,
+ pub list_state: ListState,
+ pub prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
+ pub available_commands: Rc<RefCell<Vec<agent_client_protocol::AvailableCommand>>>,
+ pub cached_user_commands: Rc<RefCell<HashMap<String, UserSlashCommand>>>,
+ pub cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
+ /// Tracks which tool calls have their content/output expanded.
+ /// Used for showing/hiding tool call results, terminal output, etc.
+ pub expanded_tool_calls: HashSet<agent_client_protocol::ToolCallId>,
+ pub expanded_tool_call_raw_inputs: HashSet<agent_client_protocol::ToolCallId>,
+ pub expanded_thinking_blocks: HashSet<(usize, usize)>,
+ pub expanded_subagents: HashSet<agent_client_protocol::SessionId>,
+ pub subagent_scroll_handles: RefCell<HashMap<agent_client_protocol::SessionId, ScrollHandle>>,
+ pub edits_expanded: bool,
+ pub plan_expanded: bool,
+ pub queue_expanded: bool,
+ pub editor_expanded: bool,
+ pub should_be_following: bool,
+ pub editing_message: Option<usize>,
+ pub local_queued_messages: Vec<QueuedMessage>,
+ pub queued_message_editors: Vec<Entity<MessageEditor>>,
+ pub queued_message_editor_subscriptions: Vec<Subscription>,
+ pub last_synced_queue_length: usize,
+ pub turn_fields: TurnFields,
+ pub command_load_errors_dismissed: bool,
+ pub discarded_partial_edits: HashSet<agent_client_protocol::ToolCallId>,
+ pub is_loading_contents: bool,
+ pub new_server_version_available: Option<SharedString>,
+ pub resumed_without_history: bool,
+ /// Tracks the selected granularity index for each tool call's permission dropdown.
+ /// The index corresponds to the position in the allow_options list.
+ /// Default is the last option (index pointing to "Only this time").
+ pub selected_permission_granularity: HashMap<agent_client_protocol::ToolCallId, usize>,
+ pub resume_thread_metadata: Option<AgentSessionInfo>,
+ pub _cancel_task: Option<Task<()>>,
+ pub skip_queue_processing_count: usize,
+ pub user_interrupted_generation: bool,
+ pub can_fast_track_queue: bool,
+ pub hovered_edited_file_buttons: Option<usize>,
+ pub _subscriptions: Vec<Subscription>,
+}
+
+#[derive(Default)]
+pub struct TurnFields {
+ pub _turn_timer_task: Option<Task<()>>,
+ pub last_turn_duration: Option<Duration>,
+ pub last_turn_tokens: Option<u64>,
+ pub turn_generation: usize,
+ pub turn_started_at: Option<Instant>,
+ pub turn_tokens: Option<u64>,
+}
+
+impl ActiveThreadState {
+ pub fn new(
+ thread: Entity<AcpThread>,
+ workspace: WeakEntity<Workspace>,
+ entry_view_state: Entity<EntryViewState>,
+ title_editor: Option<Entity<Editor>>,
+ config_options_view: Option<Entity<ConfigOptionsView>>,
+ mode_selector: Option<Entity<ModeSelector>>,
+ model_selector: Option<Entity<AcpModelSelectorPopover>>,
+ profile_selector: Option<Entity<ProfileSelector>>,
+ list_state: ListState,
+ prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
+ available_commands: Rc<RefCell<Vec<agent_client_protocol::AvailableCommand>>>,
+ cached_user_commands: Rc<RefCell<HashMap<String, UserSlashCommand>>>,
+ cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
+ resumed_without_history: bool,
+ resume_thread_metadata: Option<AgentSessionInfo>,
+ subscriptions: Vec<Subscription>,
+ ) -> Self {
+ Self {
+ thread,
+ workspace,
+ entry_view_state,
+ title_editor,
+ config_options_view,
+ mode_selector,
+ model_selector,
+ profile_selector,
+ list_state,
+ prompt_capabilities,
+ available_commands,
+ cached_user_commands,
+ cached_user_command_errors,
+ resumed_without_history,
+ resume_thread_metadata,
+ command_load_errors_dismissed: false,
+ _subscriptions: subscriptions,
+ permission_dropdown_handle: PopoverMenuHandle::default(),
+ thread_retry_status: None,
+ thread_error: None,
+ thread_error_markdown: None,
+ token_limit_callout_dismissed: false,
+ thread_feedback: Default::default(),
+ expanded_tool_calls: HashSet::default(),
+ expanded_tool_call_raw_inputs: HashSet::default(),
+ expanded_thinking_blocks: HashSet::default(),
+ expanded_subagents: HashSet::default(),
+ subagent_scroll_handles: RefCell::new(HashMap::default()),
+ edits_expanded: false,
+ plan_expanded: false,
+ queue_expanded: true,
+ editor_expanded: false,
+ should_be_following: false,
+ editing_message: None,
+ local_queued_messages: Vec::new(),
+ queued_message_editors: Vec::new(),
+ queued_message_editor_subscriptions: Vec::new(),
+ last_synced_queue_length: 0,
+ turn_fields: TurnFields::default(),
+ discarded_partial_edits: HashSet::default(),
+ is_loading_contents: false,
+ new_server_version_available: None,
+ selected_permission_granularity: HashMap::default(),
+ _cancel_task: None,
+ skip_queue_processing_count: 0,
+ user_interrupted_generation: false,
+ can_fast_track_queue: false,
+ hovered_edited_file_buttons: None,
+ }
+ }
+
+ pub(crate) fn as_native_connection(
+ &self,
+ cx: &App,
+ ) -> Option<Rc<agent::NativeAgentConnection>> {
+ let acp_thread = self.thread.read(cx);
+ acp_thread.connection().clone().downcast()
+ }
+
+ pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
+ let acp_thread = self.thread.read(cx);
+ self.as_native_connection(cx)?
+ .thread(acp_thread.session_id(), cx)
+ }
+
+ pub fn current_model_id(&self, cx: &App) -> Option<String> {
+ let selector = self.model_selector.as_ref()?;
+ let model = selector.read(cx).active_model(cx)?;
+ Some(model.id.to_string())
+ }
+
+ pub fn current_mode_id(&self, cx: &App) -> Option<Arc<str>> {
+ if let Some(thread) = self.as_native_thread(cx) {
+ Some(thread.read(cx).profile().0.clone())
+ } else {
+ let mode_selector = self.mode_selector.as_ref()?;
+ Some(mode_selector.read(cx).mode().0)
+ }
+ }
+
+ pub fn has_queued_messages(&self) -> bool {
+ !self.local_queued_messages.is_empty()
+ }
+
+ pub fn is_imported_thread(&self, cx: &App) -> bool {
+ let Some(thread) = self.as_native_thread(cx) else {
+ return false;
+ };
+ thread.read(cx).is_imported()
+ }
+
+ // turns
+
+ pub fn start_turn(&mut self, cx: &mut Context<AcpThreadView>) -> usize {
+ self.turn_fields.turn_generation += 1;
+ let generation = self.turn_fields.turn_generation;
+ self.turn_fields.turn_started_at = Some(Instant::now());
+ self.turn_fields.last_turn_duration = None;
+ self.turn_fields.last_turn_tokens = None;
+ self.turn_fields.turn_tokens = Some(0);
+ self.turn_fields._turn_timer_task = Some(cx.spawn(async move |this, cx| {
+ loop {
+ cx.background_executor().timer(Duration::from_secs(1)).await;
+ if this.update(cx, |_, cx| cx.notify()).is_err() {
+ break;
+ }
+ }
+ }));
+ generation
+ }
+
+ pub fn stop_turn(&mut self, generation: usize) {
+ if self.turn_fields.turn_generation != generation {
+ return;
+ }
+ self.turn_fields.last_turn_duration = self
+ .turn_fields
+ .turn_started_at
+ .take()
+ .map(|started| started.elapsed());
+ self.turn_fields.last_turn_tokens = self.turn_fields.turn_tokens.take();
+ self.turn_fields._turn_timer_task = None;
+ }
+
+ pub fn update_turn_tokens(&mut self, cx: &App) {
+ if let Some(usage) = self.thread.read(cx).token_usage() {
+ if let Some(tokens) = &mut self.turn_fields.turn_tokens {
+ *tokens += usage.output_tokens;
+ }
+ }
+ }
+
+ // sending
+
+ pub fn send(
+ &mut self,
+ message_editor: Entity<MessageEditor>,
+ agent: Rc<dyn AgentServer>,
+ login: Option<task::SpawnInTerminal>,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ let thread = &self.thread;
+
+ if self.is_loading_contents {
+ return;
+ }
+
+ let is_editor_empty = message_editor.read(cx).is_empty(cx);
+ let is_generating = thread.read(cx).status() != ThreadStatus::Idle;
+
+ let has_queued = self.has_queued_messages();
+ if is_editor_empty && self.can_fast_track_queue && has_queued {
+ self.can_fast_track_queue = false;
+ self.send_queued_message_at_index(0, true, window, cx);
+ return;
+ }
+
+ if is_editor_empty {
+ return;
+ }
+
+ if is_generating {
+ self.queue_message(message_editor, window, cx);
+ return;
+ }
+
+ let text = message_editor.read(cx).text(cx);
+ let text = text.trim();
+ if text == "/login" || text == "/logout" {
+ let connection = thread.read(cx).connection().clone();
+ let can_login = !connection.auth_methods().is_empty() || login.is_some();
+ // Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
+ let logout_supported = text == "/logout"
+ && self
+ .available_commands
+ .borrow()
+ .iter()
+ .any(|command| command.name == "logout");
+ if can_login && !logout_supported {
+ message_editor.update(cx, |editor, cx| editor.clear(window, cx));
+
+ let this = cx.weak_entity();
+ let agent = agent.clone();
+ window.defer(cx, |window, cx| {
+ AcpThreadView::handle_auth_required(
+ this,
+ AuthRequired::new(),
+ agent,
+ connection,
+ window,
+ cx,
+ );
+ });
+ cx.notify();
+ return;
+ }
+ }
+
+ self.send_impl(message_editor, window, cx)
+ }
+
+ pub fn send_impl(
+ &mut self,
+ message_editor: Entity<MessageEditor>,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ let full_mention_content = self.as_native_thread(cx).is_some_and(|thread| {
+ // Include full contents when using minimal profile
+ let thread = thread.read(cx);
+ AgentSettings::get_global(cx)
+ .profiles
+ .get(thread.profile())
+ .is_some_and(|profile| profile.tools.is_empty())
+ });
+
+ let cached_commands = &self.cached_user_commands;
+ let cached_errors = &self.cached_user_command_errors;
+ let contents = message_editor.update(cx, |message_editor, cx| {
+ message_editor.contents_with_cache(
+ full_mention_content,
+ Some(cached_commands.borrow().clone()),
+ Some(cached_errors.borrow().clone()),
+ cx,
+ )
+ });
+
+ self.thread_error.take();
+ self.thread_feedback.clear();
+ self.editing_message.take();
+
+ if self.should_be_following {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ workspace.follow(CollaboratorId::Agent, window, cx);
+ })
+ .ok();
+ }
+
+ let contents_task = cx.spawn_in(window, async move |_this, cx| {
+ let (contents, tracked_buffers) = contents.await?;
+
+ if contents.is_empty() {
+ return Ok(None);
+ }
+
+ let _ = cx.update(|window, cx| {
+ message_editor.update(cx, |message_editor, cx| {
+ message_editor.clear(window, cx);
+ });
+ });
+
+ Ok(Some((contents, tracked_buffers)))
+ });
+
+ self.send_content(contents_task, window, cx);
+ }
+
+ pub fn send_content(
+ &mut self,
+ contents_task: Task<anyhow::Result<Option<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>>,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ let session_id = self.thread.read(cx).session_id().clone();
+ let agent_telemetry_id = self.thread.read(cx).connection().telemetry_id();
+ let thread = self.thread.downgrade();
+
+ self.is_loading_contents = true;
+
+ let model_id = self.current_model_id(cx);
+ let mode_id = self.current_mode_id(cx);
+ let guard = cx.new(|_| ());
+ cx.observe_release(&guard, |this, _guard, cx| {
+ if let ThreadState::Active(ActiveThreadState {
+ is_loading_contents,
+ ..
+ }) = &mut this.thread_state
+ {
+ *is_loading_contents = false;
+ }
+ cx.notify();
+ })
+ .detach();
+
+ let task = cx.spawn_in(window, async move |this, cx| {
+ let Some((contents, tracked_buffers)) = contents_task.await? else {
+ return Ok(());
+ };
+
+ let generation = this.update_in(cx, |this, _window, cx| {
+ this.in_flight_prompt = Some(contents.clone());
+ let generation = this.start_turn(cx);
+ this.set_editor_is_expanded(false, cx);
+ this.scroll_to_bottom(cx);
+ generation
+ })?;
+
+ let _stop_turn = defer({
+ let this = this.clone();
+ let mut cx = cx.clone();
+ move || {
+ this.update(&mut cx, |this, cx| {
+ this.stop_turn(generation);
+ cx.notify();
+ })
+ .ok();
+ }
+ });
+ let turn_start_time = Instant::now();
+ let send = thread.update(cx, |thread, cx| {
+ thread.action_log().update(cx, |action_log, cx| {
+ for buffer in tracked_buffers {
+ action_log.buffer_read(buffer, cx)
+ }
+ });
+ drop(guard);
+
+ telemetry::event!(
+ "Agent Message Sent",
+ agent = agent_telemetry_id,
+ session = session_id,
+ model = model_id,
+ mode = mode_id
+ );
+
+ thread.send(contents, cx)
+ })?;
+ let res = send.await;
+ let turn_time_ms = turn_start_time.elapsed().as_millis();
+ drop(_stop_turn);
+ let status = if res.is_ok() {
+ this.update(cx, |this, _| this.in_flight_prompt.take()).ok();
+ "success"
+ } else {
+ "failure"
+ };
+ telemetry::event!(
+ "Agent Turn Completed",
+ agent = agent_telemetry_id,
+ session = session_id,
+ model = model_id,
+ mode = mode_id,
+ status,
+ turn_time_ms,
+ );
+ res
+ });
+
+ cx.spawn(async move |this, cx| {
+ if let Err(err) = task.await {
+ this.update(cx, |this, cx| {
+ this.handle_thread_error(err, cx);
+ })
+ .ok();
+ } else {
+ this.update(cx, |this, cx| {
+ if let ThreadState::Active(ActiveThreadState {
+ should_be_following,
+ ..
+ }) = &mut this.thread_state
+ {
+ *should_be_following = this
+ .workspace
+ .update(cx, |workspace, _| {
+ workspace.is_being_followed(CollaboratorId::Agent)
+ })
+ .unwrap_or_default();
+ }
+ })
+ .ok();
+ }
+ })
+ .detach();
+ }
+
+ pub fn interrupt_and_send(
+ &mut self,
+ message_editor: Entity<MessageEditor>,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ let thread = &self.thread;
+
+ if self.is_loading_contents {
+ return;
+ }
+
+ if thread.read(cx).status() == ThreadStatus::Idle {
+ self.send_impl(message_editor, window, cx);
+ return;
+ }
+
+ self.stop_current_and_send_new_message(window, cx);
+ }
+
+ pub fn stop_current_and_send_new_message(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ let thread = self.thread.clone();
+ self.skip_queue_processing_count = 0;
+ self.user_interrupted_generation = true;
+
+ let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx));
+
+ cx.spawn_in(window, async move |this, cx| {
+ cancelled.await;
+
+ this.update_in(cx, |this, window, cx| {
+ this.send_impl(this.message_editor.clone(), window, cx);
+ })
+ .ok();
+ })
+ .detach();
+ }
+
+ // generation
+
+ pub fn cancel_generation(&mut self, cx: &mut Context<AcpThreadView>) {
+ self.thread_retry_status.take();
+ self.thread_error.take();
+ self.user_interrupted_generation = true;
+ self._cancel_task = Some(self.thread.update(cx, |thread, cx| thread.cancel(cx)));
+ }
+
+ pub fn retry_generation(&mut self, cx: &mut Context<AcpThreadView>) {
+ self.thread_error.take();
+
+ let thread = &self.thread;
+ if !thread.read(cx).can_retry(cx) {
+ return;
+ }
+
+ let task = thread.update(cx, |thread, cx| thread.retry(cx));
+ cx.spawn(async move |this, cx| {
+ let result = task.await;
+
+ this.update(cx, |this, cx| {
+ if let Err(err) = result {
+ this.handle_thread_error(err, cx);
+ }
+ })
+ })
+ .detach();
+ }
+
+ pub fn regenerate(
+ &mut self,
+ entry_ix: usize,
+ message_editor: Entity<MessageEditor>,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ if self.is_loading_contents {
+ return;
+ }
+ let thread = self.thread.clone();
+
+ let Some(user_message_id) = thread.update(cx, |thread, _| {
+ thread.entries().get(entry_ix)?.user_message()?.id.clone()
+ }) else {
+ return;
+ };
+
+ cx.spawn_in(window, async move |this, cx| {
+ // Check if there are any edits from prompts before the one being regenerated.
+ //
+ // If there are, we keep/accept them since we're not regenerating the prompt that created them.
+ //
+ // If editing the prompt that generated the edits, they are auto-rejected
+ // through the `rewind` function in the `acp_thread`.
+ let has_earlier_edits = thread.read_with(cx, |thread, _| {
+ thread
+ .entries()
+ .iter()
+ .take(entry_ix)
+ .any(|entry| entry.diffs().next().is_some())
+ });
+
+ if has_earlier_edits {
+ thread.update(cx, |thread, cx| {
+ thread.action_log().update(cx, |action_log, cx| {
+ action_log.keep_all_edits(None, cx);
+ });
+ });
+ }
+
+ thread
+ .update(cx, |thread, cx| thread.rewind(user_message_id, cx))
+ .await?;
+ this.update_in(cx, |this, window, cx| {
+ this.send_impl(message_editor, window, cx);
+ this.focus_handle(cx).focus(window, cx);
+ })?;
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ // message queueing
+
+ pub fn queue_message(
+ &mut self,
+ message_editor: Entity<MessageEditor>,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ let is_idle = self.thread.read(cx).status() == acp_thread::ThreadStatus::Idle;
+
+ if is_idle {
+ self.send_impl(message_editor.clone(), window, cx);
+ return;
+ }
+
+ let full_mention_content = self.as_native_thread(cx).is_some_and(|thread| {
+ let thread = thread.read(cx);
+ AgentSettings::get_global(cx)
+ .profiles
+ .get(thread.profile())
+ .is_some_and(|profile| profile.tools.is_empty())
+ });
+
+ let cached_commands = self.cached_user_commands.borrow().clone();
+ let cached_errors = self.cached_user_command_errors.borrow().clone();
+ let contents = message_editor.update(cx, |message_editor, cx| {
+ message_editor.contents_with_cache(
+ full_mention_content,
+ Some(cached_commands),
+ Some(cached_errors),
+ cx,
+ )
+ });
+
+ cx.spawn_in(window, async move |this, cx| {
+ let (content, tracked_buffers) = contents.await?;
+
+ if content.is_empty() {
+ return Ok::<(), anyhow::Error>(());
+ }
+
+ this.update_in(cx, |this, window, cx| {
+ this.add_to_queue(content, tracked_buffers, cx);
+ // Enable fast-track: user can press Enter again to send this queued message immediately
+ this.set_can_fast_track_queue(true);
+ message_editor.update(cx, |message_editor, cx| {
+ message_editor.clear(window, cx);
+ });
+ cx.notify();
+ })?;
+ Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ pub fn remove_from_queue(
+ &mut self,
+ index: usize,
+ cx: &mut Context<AcpThreadView>,
+ ) -> Option<QueuedMessage> {
+ if index < self.local_queued_messages.len() {
+ let removed = self.local_queued_messages.remove(index);
+ self.sync_queue_flag_to_native_thread(cx);
+ Some(removed)
+ } else {
+ None
+ }
+ }
+
+ pub fn sync_queue_flag_to_native_thread(&self, cx: &mut Context<AcpThreadView>) {
+ if let Some(native_thread) = self.as_native_thread(cx) {
+ let has_queued = self.has_queued_messages();
+ native_thread.update(cx, |thread, _| {
+ thread.set_has_queued_message(has_queued);
+ });
+ }
+ }
+
+ pub fn send_queued_message_at_index(
+ &mut self,
+ index: usize,
+ is_send_now: bool,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ let Some(queued) = self.remove_from_queue(index, cx) else {
+ return;
+ };
+ let content = queued.content;
+ let tracked_buffers = queued.tracked_buffers;
+
+ // Only increment skip count for "Send Now" operations (out-of-order sends)
+ // Normal auto-processing from the Stopped handler doesn't need to skip.
+ // We only skip the Stopped event from the cancelled generation, NOT the
+ // Stopped event from the newly sent message (which should trigger queue processing).
+ if is_send_now {
+ let is_generating =
+ self.thread.read(cx).status() == acp_thread::ThreadStatus::Generating;
+ self.skip_queue_processing_count += if is_generating { 1 } else { 0 };
+ }
+
+ let cancelled = self.thread.update(cx, |thread, cx| thread.cancel(cx));
+
+ let workspace = self.workspace.clone();
+
+ let should_be_following = self.should_be_following;
+ let contents_task = cx.spawn_in(window, async move |_this, cx| {
+ cancelled.await;
+ if should_be_following {
+ workspace
+ .update_in(cx, |workspace, window, cx| {
+ workspace.follow(CollaboratorId::Agent, window, cx);
+ })
+ .ok();
+ }
+
+ Ok(Some((content, tracked_buffers)))
+ });
+
+ self.send_content(contents_task, window, cx);
+ }
+
+ // editor methods
+
+ pub fn expand_message_editor(
+ &mut self,
+ message_editor: Entity<MessageEditor>,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ self.set_editor_is_expanded(!self.editor_expanded, message_editor, cx);
+ cx.stop_propagation();
+ cx.notify();
+ }
+
+ pub fn set_editor_is_expanded(
+ &mut self,
+ is_expanded: bool,
+ message_editor: Entity<MessageEditor>,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ self.editor_expanded = is_expanded;
+ message_editor.update(cx, |editor, cx| {
+ if is_expanded {
+ editor.set_mode(
+ EditorMode::Full {
+ scale_ui_elements_with_buffer_font_size: false,
+ show_active_line_background: false,
+ sizing_behavior: SizingBehavior::ExcludeOverscrollMargin,
+ },
+ cx,
+ )
+ } else {
+ let agent_settings = AgentSettings::get_global(cx);
+ editor.set_mode(
+ EditorMode::AutoHeight {
+ min_lines: agent_settings.message_editor_min_lines,
+ max_lines: Some(agent_settings.set_message_editor_max_lines()),
+ },
+ cx,
+ )
+ }
+ });
+ cx.notify();
+ }
+
+ pub fn handle_title_editor_event(
+ &mut self,
+ title_editor: &Entity<Editor>,
+ event: &EditorEvent,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ let thread = &self.thread;
+
+ match event {
+ EditorEvent::BufferEdited => {
+ let new_title = title_editor.read(cx).text(cx);
+ thread.update(cx, |thread, cx| {
+ thread
+ .set_title(new_title.into(), cx)
+ .detach_and_log_err(cx);
+ })
+ }
+ EditorEvent::Blurred => {
+ if title_editor.read(cx).text(cx).is_empty() {
+ title_editor.update(cx, |editor, cx| {
+ editor.set_text("New Thread", window, cx);
+ });
+ }
+ }
+ _ => {}
+ }
+ }
+
+ pub fn cancel_editing(
+ &mut self,
+ focus_handle: FocusHandle,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ if let Some(index) = self.editing_message.take()
+ && let Some(editor) = &self
+ .entry_view_state
+ .read(cx)
+ .entry(index)
+ .and_then(|e| e.message_editor())
+ .cloned()
+ {
+ editor.update(cx, |editor, cx| {
+ if let Some(user_message) = self
+ .thread
+ .read(cx)
+ .entries()
+ .get(index)
+ .and_then(|e| e.user_message())
+ {
+ editor.set_message(user_message.chunks.clone(), window, cx);
+ }
+ })
+ };
+ focus_handle.focus(window, cx);
+ cx.notify();
+ }
+
+ // tool permissions
+
+ pub fn authorize_tool_call(
+ &mut self,
+ tool_call_id: acp::ToolCallId,
+ option_id: acp::PermissionOptionId,
+ option_kind: acp::PermissionOptionKind,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ let thread = &self.thread;
+ let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
+
+ telemetry::event!(
+ "Agent Tool Call Authorized",
+ agent = agent_telemetry_id,
+ session = thread.read(cx).session_id(),
+ option = option_kind
+ );
+
+ thread.update(cx, |thread, cx| {
+ thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
+ });
+ if self.should_be_following {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ workspace.follow(CollaboratorId::Agent, window, cx);
+ })
+ .ok();
+ }
+ cx.notify();
+ }
+
+ pub fn authorize_pending_tool_call(
+ &mut self,
+ kind: acp::PermissionOptionKind,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) -> Option<()> {
+ let thread = self.thread.read(cx);
+ let tool_call = thread.first_tool_awaiting_confirmation()?;
+ let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else {
+ return None;
+ };
+ let option = options.first_option_of_kind(kind)?;
+
+ self.authorize_tool_call(
+ tool_call.id.clone(),
+ option.option_id.clone(),
+ option.kind,
+ window,
+ cx,
+ );
+
+ Some(())
+ }
+
+ pub fn handle_select_permission_granularity(
+ &mut self,
+ action: &SelectPermissionGranularity,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
+ self.selected_permission_granularity
+ .insert(tool_call_id, action.index);
+
+ cx.notify();
+ }
+
+ // edits
+
+ pub fn keep_all(&mut self, cx: &mut Context<AcpThreadView>) {
+ let thread = &self.thread;
+ let telemetry = ActionLogTelemetry::from(thread.read(cx));
+ let action_log = thread.read(cx).action_log().clone();
+ action_log.update(cx, |action_log, cx| {
+ action_log.keep_all_edits(Some(telemetry), cx)
+ });
+ }
+
+ pub fn reject_all(&mut self, cx: &mut Context<AcpThreadView>) {
+ let thread = &self.thread;
+ let telemetry = ActionLogTelemetry::from(thread.read(cx));
+ let action_log = thread.read(cx).action_log().clone();
+ action_log
+ .update(cx, |action_log, cx| {
+ action_log.reject_all_edits(Some(telemetry), cx)
+ })
+ .detach();
+ }
+
+ pub fn open_edited_buffer(
+ &mut self,
+ buffer: &Entity<Buffer>,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ let thread = &self.thread;
+
+ let Some(diff) =
+ AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
+ else {
+ return;
+ };
+
+ diff.update(cx, |diff, cx| {
+ diff.move_to_path(PathKey::for_buffer(buffer, cx), window, cx)
+ })
+ }
+
+ // thread stuff
+
+ pub fn sync_thread(
+ &mut self,
+ project: Entity<Project>,
+ window: &mut Window,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ if !self.is_imported_thread(cx) {
+ return;
+ }
+
+ let Some(session_list) = self
+ .as_native_connection(cx)
+ .and_then(|connection| connection.session_list(cx))
+ .and_then(|list| list.downcast::<NativeAgentSessionList>())
+ else {
+ return;
+ };
+ let thread_store = session_list.thread_store().clone();
+
+ let client = project.read(cx).client();
+ let session_id = self.thread.read(cx).session_id().clone();
+
+ cx.spawn_in(window, async move |this, cx| {
+ let response = client
+ .request(proto::GetSharedAgentThread {
+ session_id: session_id.to_string(),
+ })
+ .await?;
+
+ let shared_thread = SharedThread::from_bytes(&response.thread_data)?;
+
+ let db_thread = shared_thread.to_db_thread();
+
+ thread_store
+ .update(&mut cx.clone(), |store, cx| {
+ store.save_thread(session_id.clone(), db_thread, cx)
+ })
+ .await?;
+
+ let thread_metadata = AgentSessionInfo {
+ session_id,
+ cwd: None,
+ title: Some(format!("🔗 {}", response.title).into()),
+ updated_at: Some(chrono::Utc::now()),
+ meta: None,
+ };
+
+ this.update_in(cx, |this, window, cx| {
+ if let ThreadState::Active(ActiveThreadState {
+ resume_thread_metadata,
+ ..
+ }) = &mut this.thread_state
+ {
+ *resume_thread_metadata = Some(thread_metadata);
+ }
+ this.reset(window, cx);
+ })?;
+
+ this.update_in(cx, |this, _window, cx| {
+ if let Some(workspace) = this.workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ struct ThreadSyncedToast;
+ workspace.show_toast(
+ Toast::new(
+ NotificationId::unique::<ThreadSyncedToast>(),
+ "Thread synced with latest version",
+ )
+ .autohide(),
+ cx,
+ );
+ });
+ }
+ })?;
+
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ pub fn restore_checkpoint(
+ &mut self,
+ message_id: &UserMessageId,
+ cx: &mut Context<AcpThreadView>,
+ ) {
+ self.thread
+ .update(cx, |thread, cx| {
+ thread.restore_checkpoint(message_id.clone(), cx)
+ })
+ .detach_and_log_err(cx);
+ }
+
+ pub fn clear_thread_error(&mut self, cx: &mut Context<AcpThreadView>) {
+ self.thread_error = None;
+ self.thread_error_markdown = None;
+ self.token_limit_callout_dismissed = true;
+ cx.notify();
+ }
+
+ // other
+
+ pub fn refresh_cached_user_commands_from_registry(
+ &mut self,
+ registry: &Entity<SlashCommandRegistry>,
+ cx: &App,
+ ) {
+ let (mut commands, mut errors) = registry.read_with(cx, |registry, _| {
+ (registry.commands().clone(), registry.errors().to_vec())
+ });
+ let server_command_names = self
+ .available_commands
+ .borrow()
+ .iter()
+ .map(|command| command.name.clone())
+ .collect::<HashSet<_>>();
+ user_slash_command::apply_server_command_conflicts_to_map(
+ &mut commands,
+ &mut errors,
+ &server_command_names,
+ );
+
+ self.command_load_errors_dismissed = false;
+ *self.cached_user_commands.borrow_mut() = commands;
+ *self.cached_user_command_errors.borrow_mut() = errors;
+ }
+
+ pub fn render_command_load_errors(
+ &self,
+ cx: &mut Context<AcpThreadView>,
+ ) -> Option<impl IntoElement> {
+ let errors = self.cached_user_command_errors.borrow();
+
+ if self.command_load_errors_dismissed || errors.is_empty() {
+ return None;
+ }
+
+ let workspace = self.workspace.clone();
+
+ let error_count = errors.len();
+ let title = if error_count == 1 {
+ "Failed to load slash command"
+ } else {
+ "Failed to load slash commands"
+ };
+
+ Some(
+ v_flex()
+ .w_full()
+ .p_2()
+ .gap_1()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .bg(cx.theme().colors().surface_background)
+ .child(
+ h_flex()
+ .justify_between()
+ .child(
+ h_flex()
+ .gap_1()
+ .child(
+ Icon::new(IconName::Warning)
+ .size(IconSize::Small)
+ .color(Color::Warning),
+ )
+ .child(
+ Label::new(title)
+ .size(LabelSize::Small)
+ .color(Color::Warning),
+ ),
+ )
+ .child(
+ IconButton::new("dismiss-command-errors", IconName::Close)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .tooltip(Tooltip::text("Dismiss"))
+ .on_click(cx.listener(|this, _, _, cx| {
+ this.clear_command_load_errors(cx);
+ })),
+ ),
+ )
+ .children(errors.iter().enumerate().map({
+ move |(i, error)| {
+ let path = error.path.clone();
+ let workspace = workspace.clone();
+ let file_name = error
+ .path
+ .file_name()
+ .map(|n| n.to_string_lossy().to_string())
+ .unwrap_or_else(|| error.path.display().to_string());
+
+ h_flex()
+ .id(ElementId::Name(format!("command-error-{i}").into()))
+ .gap_1()
+ .px_1()
+ .py_0p5()
+ .rounded_sm()
+ .cursor_pointer()
+ .hover(|style| style.bg(cx.theme().colors().element_hover))
+ .tooltip(Tooltip::text(format!(
+ "Click to open {}\n\n{}",
+ error.path.display(),
+ error.message
+ )))
+ .on_click({
+ move |_, window, cx| {
+ if let Some(workspace) = workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ workspace
+ .open_abs_path(
+ path.clone(),
+ OpenOptions::default(),
+ window,
+ cx,
+ )
+ .detach_and_log_err(cx);
+ });
+ }
+ }
+ })
+ .child(
+ Label::new(format!("• {}: {}", file_name, error.message))
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ }
+ })),
+ )
+ }
+
+ pub fn render_thread_retry_status_callout(&self) -> Option<Callout> {
+ let state = self.thread_retry_status.as_ref()?;
+
+ let next_attempt_in = state
+ .duration
+ .saturating_sub(Instant::now().saturating_duration_since(state.started_at));
+ if next_attempt_in.is_zero() {
+ return None;
+ }
+
+ let next_attempt_in_secs = next_attempt_in.as_secs() + 1;
+
+ let retry_message = if state.max_attempts == 1 {
+ if next_attempt_in_secs == 1 {
+ "Retrying. Next attempt in 1 second.".to_string()
+ } else {
+ format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.")
+ }
+ } else if next_attempt_in_secs == 1 {
+ format!(
+ "Retrying. Next attempt in 1 second (Attempt {} of {}).",
+ state.attempt, state.max_attempts,
+ )
+ } else {
+ format!(
+ "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).",
+ state.attempt, state.max_attempts,
+ )
+ };
+
+ Some(
+ Callout::new()
+ .severity(Severity::Warning)
+ .title(state.last_error.clone())
+ .description(retry_message),
+ )
+ }
+
+ pub fn handle_open_rules(&mut self, window: &mut Window, cx: &mut Context<AcpThreadView>) {
+ let Some(thread) = self.as_native_thread(cx) else {
+ return;
+ };
+ let project_context = thread.read(cx).project_context().read(cx);
+
+ let project_entry_ids = project_context
+ .worktrees
+ .iter()
+ .flat_map(|worktree| worktree.rules_file.as_ref())
+ .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id))
+ .collect::<Vec<_>>();
+
+ self.workspace
+ .update(cx, move |workspace, cx| {
+ // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
+ // files clear. For example, if rules file 1 is already open but rules file 2 is not,
+ // this would open and focus rules file 2 in a tab that is not next to rules file 1.
+ let project = workspace.project().read(cx);
+ let project_paths = project_entry_ids
+ .into_iter()
+ .flat_map(|entry_id| project.path_for_entry(entry_id, cx))
+ .collect::<Vec<_>>();
+ for project_path in project_paths {
+ workspace
+ .open_path(project_path, None, true, window, cx)
+ .detach_and_log_err(cx);
+ }
+ })
+ .ok();
+ }
+}
@@ -149,7 +149,12 @@ pub fn init(cx: &mut App) {
let thread = workspace
.panel::<AgentPanel>(cx)
.and_then(|panel| panel.read(cx).active_thread_view().cloned())
- .and_then(|thread_view| thread_view.read(cx).thread().cloned());
+ .and_then(|thread_view| {
+ thread_view
+ .read(cx)
+ .as_active_thread()
+ .map(|r| r.thread.clone())
+ });
if let Some(thread) = thread {
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
@@ -1221,9 +1226,10 @@ impl AgentPanel {
pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
match &self.active_view {
- ActiveView::ExternalAgentThread { thread_view, .. } => {
- thread_view.read(cx).thread().cloned()
- }
+ ActiveView::ExternalAgentThread { thread_view, .. } => thread_view
+ .read(cx)
+ .as_active_thread()
+ .map(|r| r.thread.clone()),
_ => None,
}
}
@@ -1638,7 +1644,11 @@ impl AgentPanel {
.as_native_thread(cx)
.map_or(false, |t| t.read(cx).is_generating_title());
- if let Some(title_editor) = thread_view.read(cx).title_editor() {
+ if let Some(title_editor) = thread_view
+ .read(cx)
+ .as_active_thread()
+ .and_then(|ready| ready.title_editor.clone())
+ {
let container = div()
.w_full()
.on_action({
@@ -237,7 +237,7 @@ enum ProfilePickerEntry {
Profile(ProfileMatchEntry),
}
-pub(crate) struct ProfilePickerDelegate {
+pub struct ProfilePickerDelegate {
fs: Arc<dyn Fs>,
provider: Arc<dyn ProfileProvider>,
foreground: ForegroundExecutor,
@@ -142,8 +142,8 @@ impl AgentThreadPane {
fn title(&self, cx: &App) -> SharedString {
if let Some(active_thread_view) = &self.thread_view {
let thread_view = active_thread_view.view.read(cx);
- if let Some(thread) = thread_view.thread() {
- let title = thread.read(cx).title();
+ if let Some(ready) = thread_view.as_active_thread() {
+ let title = ready.thread.read(cx).title();
if !title.is_empty() {
return title;
}
@@ -2011,7 +2011,7 @@ impl Interactivity {
origin: hitbox.origin,
size: text.size(FONT_SIZE),
};
- if self.source_location.is_some()
+ if let Some(source_location) = self.source_location
&& text_bounds.contains(&window.mouse_position())
&& window.modifiers().secondary()
{
@@ -2042,7 +2042,6 @@ impl Interactivity {
window.on_mouse_event({
let hitbox = hitbox.clone();
- let location = self.source_location.unwrap();
move |e: &crate::MouseDownEvent, phase, window, cx| {
if text_bounds.contains(&e.position)
&& phase.capture()
@@ -2055,9 +2054,9 @@ impl Interactivity {
eprintln!(
"This element was created at:\n{}:{}:{}",
- dir.join(location.file()).to_string_lossy(),
- location.line(),
- location.column()
+ dir.join(source_location.file()).to_string_lossy(),
+ source_location.line(),
+ source_location.column()
);
}
}
@@ -1806,7 +1806,12 @@ fn run_subagent_visual_tests(
.ok_or_else(|| anyhow::anyhow!("No active thread view"))?;
let thread = cx
- .read(|cx| thread_view.read(cx).thread().cloned())
+ .read(|cx| {
+ thread_view
+ .read(cx)
+ .as_active_thread()
+ .map(|active| active.thread.clone())
+ })
.ok_or_else(|| anyhow::anyhow!("Thread not available"))?;
// Send the message to trigger the subagent response
@@ -2211,7 +2216,12 @@ fn run_agent_thread_view_test(
.ok_or_else(|| anyhow::anyhow!("No active thread view"))?;
let thread = cx
- .read(|cx| thread_view.read(cx).thread().cloned())
+ .read(|cx| {
+ thread_view
+ .read(cx)
+ .as_active_thread()
+ .map(|active| active.thread.clone())
+ })
.ok_or_else(|| anyhow::anyhow!("Thread not available"))?;
// Send the message to trigger the image response