use std::{ops::Range, sync::Arc}; use acp_thread::{AcpThread, AgentThreadEntry, AssistantMessageChunk};use agent::ThreadStore;use agent_client_protocol::schema::v1 as acp;use agent_settings::AgentSettings;use collections::{HashMap, HashSet};use editor::{ Editor, EditorEvent, EditorMode, MinimapVisibility, RestoreOnlyUnstagedDiffHunkDelegate, SizingBehavior,};use gpui::{ AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable, ScrollHandle, TextStyleRefinement, WeakEntity, Window,};use language::language_settings::SoftWrap;use project::{AgentId, Project, project_settings::DiagnosticSeverity};use rope::Point;use settings::{Settings as _, ThinkingBlockDisplay};use terminal_view::TerminalView;use theme_settings::ThemeSettings;use ui::{Context, TextSize};use workspace::Workspace; use crate::message_editor::{MessageEditor, MessageEditorEvent, SharedSessionCapabilities}; /// Maps an entry index through the removal of `removed` (a contiguous range of/// entries), returning `None` if the index referred to a removed entry.fn reindex_after_removal(index: usize, removed: &Range<usize>) -> Option<usize> { if index < removed.start { Some(index) } else if index < removed.end { None } else { Some(index - removed.len()) }} pub struct EntryViewState { workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, entries: Vec<Entry>, session_capabilities: SharedSessionCapabilities, agent_id: AgentId, expanded_thinking_blocks: HashSet<(usize, usize)>, auto_expanded_thinking_block: Option<(usize, usize)>, user_toggled_thinking_blocks: HashSet<(usize, usize)>, expanded_compactions: HashSet<usize>, expanded_tool_calls: HashSet<acp::ToolCallId>,} impl EntryViewState { pub fn new( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, session_capabilities: SharedSessionCapabilities, agent_id: AgentId, ) -> Self { Self { workspace, project, thread_store, entries: Vec::new(), session_capabilities, agent_id, expanded_thinking_blocks: HashSet::default(), auto_expanded_thinking_block: None, user_toggled_thinking_blocks: HashSet::default(), expanded_compactions: HashSet::default(), expanded_tool_calls: HashSet::default(), } } pub(crate) fn is_tool_call_expanded(&self, tool_call_id: &acp::ToolCallId) -> bool { self.expanded_tool_calls.contains(tool_call_id) } pub(crate) fn expand_tool_call(&mut self, tool_call_id: acp::ToolCallId) { self.expanded_tool_calls.insert(tool_call_id); } pub(crate) fn collapse_tool_call(&mut self, tool_call_id: &acp::ToolCallId) { self.expanded_tool_calls.remove(tool_call_id); } pub(crate) fn toggle_tool_call_expansion(&mut self, tool_call_id: &acp::ToolCallId) { if !self.expanded_tool_calls.remove(tool_call_id) { self.expanded_tool_calls.insert(tool_call_id.clone()); } } pub(crate) fn is_compaction_expanded(&self, entry_ix: usize) -> bool { self.expanded_compactions.contains(&entry_ix) } pub(crate) fn collapse_compaction(&mut self, entry_ix: usize) { self.expanded_compactions.remove(&entry_ix); } pub(crate) fn toggle_compaction_expansion(&mut self, entry_ix: usize) { if !self.expanded_compactions.remove(&entry_ix) { self.expanded_compactions.insert(entry_ix); } } pub(crate) fn clear_auto_expand_tracking(&mut self) { self.auto_expanded_thinking_block = None; } pub(crate) fn is_auto_expanded_thinking_block(&self, key: (usize, usize)) -> bool { self.auto_expanded_thinking_block == Some(key) } pub(crate) fn auto_expand_streaming_thought(&mut self, thread: &AcpThread, cx: &App) -> bool { let thinking_display = AgentSettings::get_global(cx).thinking_display; if !matches!( thinking_display, ThinkingBlockDisplay::Auto | ThinkingBlockDisplay::Preview ) { return false; } let last_ix = thread.entries().len().saturating_sub(1); let key = match thread.entries().get(last_ix) { Some(AgentThreadEntry::AssistantMessage(message)) => match message.chunks.last() { Some(AssistantMessageChunk::Thought { .. }) => { Some((last_ix, message.chunks.len() - 1)) } _ => None, }, _ => None, }; if let Some(key) = key { if self.auto_expanded_thinking_block != Some(key) { self.auto_expanded_thinking_block = Some(key); self.expanded_thinking_blocks.insert(key); return true; } } else if self.auto_expanded_thinking_block.is_some() { if thinking_display == ThinkingBlockDisplay::Auto && let Some(key) = self.auto_expanded_thinking_block && !self.user_toggled_thinking_blocks.contains(&key) { self.expanded_thinking_blocks.remove(&key); } self.auto_expanded_thinking_block = None; return true; } false } pub(crate) fn toggle_thinking_block_expansion(&mut self, key: (usize, usize), cx: &App) { match AgentSettings::get_global(cx).thinking_display { ThinkingBlockDisplay::Auto => { let is_open = self.expanded_thinking_blocks.contains(&key) || self.user_toggled_thinking_blocks.contains(&key); if is_open { self.expanded_thinking_blocks.remove(&key); self.user_toggled_thinking_blocks.remove(&key); } else { self.expanded_thinking_blocks.insert(key); self.user_toggled_thinking_blocks.insert(key); } } ThinkingBlockDisplay::Preview => { let is_user_expanded = self.user_toggled_thinking_blocks.contains(&key); let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key); if is_user_expanded { self.user_toggled_thinking_blocks.remove(&key); self.expanded_thinking_blocks.remove(&key); } else if is_in_expanded_set { self.user_toggled_thinking_blocks.insert(key); } else { self.expanded_thinking_blocks.insert(key); self.user_toggled_thinking_blocks.insert(key); } } ThinkingBlockDisplay::AlwaysExpanded => { if self.user_toggled_thinking_blocks.contains(&key) { self.user_toggled_thinking_blocks.remove(&key); } else { self.user_toggled_thinking_blocks.insert(key); } } ThinkingBlockDisplay::AlwaysCollapsed => { if self.user_toggled_thinking_blocks.contains(&key) { self.user_toggled_thinking_blocks.remove(&key); self.expanded_thinking_blocks.remove(&key); } else { self.expanded_thinking_blocks.insert(key); self.user_toggled_thinking_blocks.insert(key); } } } } pub(crate) fn thinking_block_state(&self, key: (usize, usize), cx: &App) -> (bool, bool) { let is_user_toggled = self.user_toggled_thinking_blocks.contains(&key); let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key); match AgentSettings::get_global(cx).thinking_display { ThinkingBlockDisplay::Auto => { let is_open = is_user_toggled || is_in_expanded_set; (is_open, false) } ThinkingBlockDisplay::Preview => { let is_open = is_user_toggled || is_in_expanded_set; let is_constrained = is_in_expanded_set && !is_user_toggled; (is_open, is_constrained) } ThinkingBlockDisplay::AlwaysExpanded => (!is_user_toggled, false), ThinkingBlockDisplay::AlwaysCollapsed => (is_user_toggled, false), } } pub fn entry(&self, index: usize) -> Option<&Entry> { self.entries.get(index) } pub fn sync_entry( &mut self, index: usize, thread: &Entity<AcpThread>, window: &mut Window, cx: &mut Context<Self>, ) { let Some(thread_entry) = thread.read(cx).entries().get(index) else { return; }; match thread_entry { AgentThreadEntry::UserMessage(message) => { let can_rewind = thread.read(cx).supports_truncate(cx); let has_client_id = message.client_id.is_some(); let is_subagent = thread.read(cx).parent_session_id().is_some(); let chunks = message.chunks.clone(); if let Some(Entry::UserMessage(editor)) = self.entries.get_mut(index) { if !editor.focus_handle(cx).is_focused(window) { // Only update if we are not editing. // If we are, cancelling the edit will set the message to the newest content. editor.update(cx, |editor, cx| { editor.set_message(chunks, window, cx); }); } } else { let message_editor = cx.new(|cx| { let mut editor = MessageEditor::new( self.workspace.clone(), self.project.clone(), self.thread_store.clone(), self.session_capabilities.clone(), self.agent_id.clone(), "Edit message - @ to include context", editor::EditorMode::AutoHeight { min_lines: 1, max_lines: None, }, window, cx, ); if !can_rewind || !has_client_id || is_subagent { editor.set_read_only(true, cx); } editor.set_message(chunks, window, cx); editor }); cx.subscribe(&message_editor, move |_, editor, event, cx| { cx.emit(EntryViewEvent { entry_index: index, view_event: ViewEvent::MessageEditorEvent(editor, event.clone()), }) }) .detach(); self.set_entry(index, Entry::UserMessage(message_editor)); } } AgentThreadEntry::ToolCall(tool_call) => { let id = tool_call.id.clone(); let terminals = tool_call.terminals().cloned().collect::<Vec<_>>(); let diffs = tool_call.diffs().cloned().collect::<Vec<_>>(); let views = if let Some(Entry::ToolCall(tool_call)) = self.entries.get_mut(index) { &mut tool_call.content } else { self.set_entry( index, Entry::ToolCall(ToolCallEntry { content: HashMap::default(), focus_handle: cx.focus_handle(), }), ); let Some(Entry::ToolCall(tool_call)) = self.entries.get_mut(index) else { unreachable!() }; &mut tool_call.content }; let is_tool_call_completed = matches!(tool_call.status, acp_thread::ToolCallStatus::Completed); for terminal in terminals { match views.entry(terminal.entity_id()) { collections::hash_map::Entry::Vacant(entry) => { let element = create_terminal( self.workspace.clone(), self.project.clone(), terminal.clone(), window, cx, ) .into_any(); cx.emit(EntryViewEvent { entry_index: index, view_event: ViewEvent::NewTerminal(id.clone()), }); entry.insert(element); } collections::hash_map::Entry::Occupied(_entry) => { if is_tool_call_completed && terminal.read(cx).output().is_none() { cx.emit(EntryViewEvent { entry_index: index, view_event: ViewEvent::TerminalMovedToBackground(id.clone()), }); } } } } for diff in diffs { views.entry(diff.entity_id()).or_insert_with(|| { let editor = create_editor_diff(diff.clone(), window, cx); cx.subscribe(&editor, { let diff = diff.clone(); let entry_index = index; move |_this, _editor, event: &EditorEvent, cx| { if let EditorEvent::OpenExcerptsRequested { selections_by_buffer, split, } = event { let multibuffer = diff.read(cx).multibuffer(); if let Some((buffer_id, (ranges, _))) = selections_by_buffer.iter().next() { if let Some(buffer) = multibuffer.read(cx).buffer(*buffer_id) { if let Some(range) = ranges.first() { let point = buffer.read(cx).offset_to_point(range.start.0); if let Some(path) = diff.read(cx).file_path(cx) { cx.emit(EntryViewEvent { entry_index, view_event: ViewEvent::OpenDiffLocation { path, position: point, split: *split, }, }); } } } } } } }) .detach(); cx.emit(EntryViewEvent { entry_index: index, view_event: ViewEvent::NewDiff(id.clone()), }); editor.into_any() }); } } AgentThreadEntry::Elicitation(_) => { if !matches!(self.entries.get(index), Some(Entry::Elicitation { .. })) { self.set_entry( index, Entry::Elicitation { focus_handle: cx.focus_handle(), }, ); } } AgentThreadEntry::AssistantMessage(message) => { let entry = if let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) { entry } else { self.set_entry( index, Entry::AssistantMessage(AssistantMessageEntry { scroll_handles_by_chunk_index: HashMap::default(), focus_handle: cx.focus_handle(), }), ); let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else { unreachable!() }; entry }; entry.sync(message); } AgentThreadEntry::CompletedPlan(_) => { if !matches!(self.entries.get(index), Some(Entry::CompletedPlan)) { self.set_entry(index, Entry::CompletedPlan); } } AgentThreadEntry::ContextCompaction(_) => { if !matches!(self.entries.get(index), Some(Entry::ContextCompaction)) { self.set_entry(index, Entry::ContextCompaction); } } }; } fn set_entry(&mut self, index: usize, entry: Entry) { if index == self.entries.len() { self.entries.push(entry); } else { self.entries[index] = entry; } } pub fn remove(&mut self, range: Range<usize>) { self.entries.drain(range.clone()); self.expanded_compactions = self .expanded_compactions .iter() .filter_map(|&entry_ix| reindex_after_removal(entry_ix, &range)) .collect(); self.expanded_thinking_blocks = self .expanded_thinking_blocks .iter() .filter_map(|&(entry_ix, chunk_ix)| { reindex_after_removal(entry_ix, &range).map(|entry_ix| (entry_ix, chunk_ix)) }) .collect(); self.user_toggled_thinking_blocks = self .user_toggled_thinking_blocks .iter() .filter_map(|&(entry_ix, chunk_ix)| { reindex_after_removal(entry_ix, &range).map(|entry_ix| (entry_ix, chunk_ix)) }) .collect(); self.auto_expanded_thinking_block = self.auto_expanded_thinking_block .and_then(|(entry_ix, chunk_ix)| { reindex_after_removal(entry_ix, &range).map(|entry_ix| (entry_ix, chunk_ix)) }); } pub fn agent_ui_font_size_changed(&mut self, cx: &mut App) { for entry in self.entries.iter() { match entry { Entry::UserMessage { .. } | Entry::AssistantMessage { .. } | Entry::Elicitation { .. } | Entry::CompletedPlan | Entry::ContextCompaction => {} Entry::ToolCall(ToolCallEntry { content, .. }) => { for view in content.values() { if let Ok(diff_editor) = view.clone().downcast::<Editor>() { diff_editor.update(cx, |diff_editor, cx| { diff_editor.set_text_style_refinement( diff_editor_text_style_refinement(cx), ); cx.notify(); }) } } } } } }} impl EventEmitter<EntryViewEvent> for EntryViewState {} pub struct EntryViewEvent { pub entry_index: usize, pub view_event: ViewEvent,} pub enum ViewEvent { NewDiff(acp::ToolCallId), NewTerminal(acp::ToolCallId), TerminalMovedToBackground(acp::ToolCallId), MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent), OpenDiffLocation {Lines 1–500 of 937. Load more
use std::{ops::Range, sync::Arc}; use acp_thread::{AcpThread, AgentThreadEntry, AssistantMessageChunk}; use agent::ThreadStore; use agent_client_protocol::schema::v1 as acp; use agent_settings::AgentSettings; use collections::{HashMap, HashSet}; use editor::{ Editor, EditorEvent, EditorMode, MinimapVisibility, RestoreOnlyUnstagedDiffHunkDelegate, SizingBehavior, }; use gpui::{ AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable, ScrollHandle, TextStyleRefinement, WeakEntity, Window, }; use language::language_settings::SoftWrap; use project::{AgentId, Project, project_settings::DiagnosticSeverity}; use rope::Point; use settings::{Settings as _, ThinkingBlockDisplay}; use terminal_view::TerminalView; use theme_settings::ThemeSettings; use ui::{Context, TextSize}; use workspace::Workspace; use crate::message_editor::{MessageEditor, MessageEditorEvent, SharedSessionCapabilities}; /// Maps an entry index through the removal of `removed` (a contiguous range of /// entries), returning `None` if the index referred to a removed entry. fn reindex_after_removal(index: usize, removed: &Range<usize>) -> Option<usize> { if index < removed.start { Some(index) } else if index < removed.end { None } else { Some(index - removed.len()) } } pub struct EntryViewState { workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, entries: Vec<Entry>, session_capabilities: SharedSessionCapabilities, agent_id: AgentId, expanded_thinking_blocks: HashSet<(usize, usize)>, auto_expanded_thinking_block: Option<(usize, usize)>, user_toggled_thinking_blocks: HashSet<(usize, usize)>, expanded_compactions: HashSet<usize>, expanded_tool_calls: HashSet<acp::ToolCallId>, } impl EntryViewState { pub fn new( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, session_capabilities: SharedSessionCapabilities, agent_id: AgentId, ) -> Self { Self { workspace, project, thread_store, entries: Vec::new(), session_capabilities, agent_id, expanded_thinking_blocks: HashSet::default(), auto_expanded_thinking_block: None, user_toggled_thinking_blocks: HashSet::default(), expanded_compactions: HashSet::default(), expanded_tool_calls: HashSet::default(), } } pub(crate) fn is_tool_call_expanded(&self, tool_call_id: &acp::ToolCallId) -> bool { self.expanded_tool_calls.contains(tool_call_id) } pub(crate) fn expand_tool_call(&mut self, tool_call_id: acp::ToolCallId) { self.expanded_tool_calls.insert(tool_call_id); } pub(crate) fn collapse_tool_call(&mut self, tool_call_id: &acp::ToolCallId) { self.expanded_tool_calls.remove(tool_call_id); } pub(crate) fn toggle_tool_call_expansion(&mut self, tool_call_id: &acp::ToolCallId) { if !self.expanded_tool_calls.remove(tool_call_id) { self.expanded_tool_calls.insert(tool_call_id.clone()); } } pub(crate) fn is_compaction_expanded(&self, entry_ix: usize) -> bool { self.expanded_compactions.contains(&entry_ix) } pub(crate) fn collapse_compaction(&mut self, entry_ix: usize) { self.expanded_compactions.remove(&entry_ix); } pub(crate) fn toggle_compaction_expansion(&mut self, entry_ix: usize) { if !self.expanded_compactions.remove(&entry_ix) { self.expanded_compactions.insert(entry_ix); } } pub(crate) fn clear_auto_expand_tracking(&mut self) { self.auto_expanded_thinking_block = None; } pub(crate) fn is_auto_expanded_thinking_block(&self, key: (usize, usize)) -> bool { self.auto_expanded_thinking_block == Some(key) } pub(crate) fn auto_expand_streaming_thought(&mut self, thread: &AcpThread, cx: &App) -> bool { let thinking_display = AgentSettings::get_global(cx).thinking_display; if !matches!( thinking_display, ThinkingBlockDisplay::Auto | ThinkingBlockDisplay::Preview ) { return false; } let last_ix = thread.entries().len().saturating_sub(1); let key = match thread.entries().get(last_ix) { Some(AgentThreadEntry::AssistantMessage(message)) => match message.chunks.last() { Some(AssistantMessageChunk::Thought { .. }) => { Some((last_ix, message.chunks.len() - 1)) } _ => None, }, _ => None, }; if let Some(key) = key { if self.auto_expanded_thinking_block != Some(key) { self.auto_expanded_thinking_block = Some(key); self.expanded_thinking_blocks.insert(key); return true; } } else if self.auto_expanded_thinking_block.is_some() { if thinking_display == ThinkingBlockDisplay::Auto && let Some(key) = self.auto_expanded_thinking_block && !self.user_toggled_thinking_blocks.contains(&key) { self.expanded_thinking_blocks.remove(&key); } self.auto_expanded_thinking_block = None; return true; } false } pub(crate) fn toggle_thinking_block_expansion(&mut self, key: (usize, usize), cx: &App) { match AgentSettings::get_global(cx).thinking_display { ThinkingBlockDisplay::Auto => { let is_open = self.expanded_thinking_blocks.contains(&key) || self.user_toggled_thinking_blocks.contains(&key); if is_open { self.expanded_thinking_blocks.remove(&key); self.user_toggled_thinking_blocks.remove(&key); } else { self.expanded_thinking_blocks.insert(key); self.user_toggled_thinking_blocks.insert(key); } } ThinkingBlockDisplay::Preview => { let is_user_expanded = self.user_toggled_thinking_blocks.contains(&key); let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key); if is_user_expanded { self.user_toggled_thinking_blocks.remove(&key); self.expanded_thinking_blocks.remove(&key); } else if is_in_expanded_set { self.user_toggled_thinking_blocks.insert(key); } else { self.expanded_thinking_blocks.insert(key); self.user_toggled_thinking_blocks.insert(key); } } ThinkingBlockDisplay::AlwaysExpanded => { if self.user_toggled_thinking_blocks.contains(&key) { self.user_toggled_thinking_blocks.remove(&key); } else { self.user_toggled_thinking_blocks.insert(key); } } ThinkingBlockDisplay::AlwaysCollapsed => { if self.user_toggled_thinking_blocks.contains(&key) { self.user_toggled_thinking_blocks.remove(&key); self.expanded_thinking_blocks.remove(&key); } else { self.expanded_thinking_blocks.insert(key); self.user_toggled_thinking_blocks.insert(key); } } } } pub(crate) fn thinking_block_state(&self, key: (usize, usize), cx: &App) -> (bool, bool) { let is_user_toggled = self.user_toggled_thinking_blocks.contains(&key); let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key); match AgentSettings::get_global(cx).thinking_display { ThinkingBlockDisplay::Auto => { let is_open = is_user_toggled || is_in_expanded_set; (is_open, false) } ThinkingBlockDisplay::Preview => { let is_open = is_user_toggled || is_in_expanded_set; let is_constrained = is_in_expanded_set && !is_user_toggled; (is_open, is_constrained) } ThinkingBlockDisplay::AlwaysExpanded => (!is_user_toggled, false), ThinkingBlockDisplay::AlwaysCollapsed => (is_user_toggled, false), } } pub fn entry(&self, index: usize) -> Option<&Entry> { self.entries.get(index) } pub fn sync_entry( &mut self, index: usize, thread: &Entity<AcpThread>, window: &mut Window, cx: &mut Context<Self>, ) { let Some(thread_entry) = thread.read(cx).entries().get(index) else { return; }; match thread_entry { AgentThreadEntry::UserMessage(message) => { let can_rewind = thread.read(cx).supports_truncate(cx); let has_client_id = message.client_id.is_some(); let is_subagent = thread.read(cx).parent_session_id().is_some(); let chunks = message.chunks.clone(); if let Some(Entry::UserMessage(editor)) = self.entries.get_mut(index) { if !editor.focus_handle(cx).is_focused(window) { // Only update if we are not editing. // If we are, cancelling the edit will set the message to the newest content. editor.update(cx, |editor, cx| { editor.set_message(chunks, window, cx); }); } } else { let message_editor = cx.new(|cx| { let mut editor = MessageEditor::new( self.workspace.clone(), self.project.clone(), self.thread_store.clone(), self.session_capabilities.clone(), self.agent_id.clone(), "Edit message - @ to include context", editor::EditorMode::AutoHeight { min_lines: 1, max_lines: None, }, window, cx, ); if !can_rewind || !has_client_id || is_subagent { editor.set_read_only(true, cx); } editor.set_message(chunks, window, cx); editor }); cx.subscribe(&message_editor, move |_, editor, event, cx| { cx.emit(EntryViewEvent { entry_index: index, view_event: ViewEvent::MessageEditorEvent(editor, event.clone()), }) }) .detach(); self.set_entry(index, Entry::UserMessage(message_editor)); } } AgentThreadEntry::ToolCall(tool_call) => { let id = tool_call.id.clone(); let terminals = tool_call.terminals().cloned().collect::<Vec<_>>(); let diffs = tool_call.diffs().cloned().collect::<Vec<_>>(); let views = if let Some(Entry::ToolCall(tool_call)) = self.entries.get_mut(index) { &mut tool_call.content } else { self.set_entry( index, Entry::ToolCall(ToolCallEntry { content: HashMap::default(), focus_handle: cx.focus_handle(), }), ); let Some(Entry::ToolCall(tool_call)) = self.entries.get_mut(index) else { unreachable!() }; &mut tool_call.content }; let is_tool_call_completed = matches!(tool_call.status, acp_thread::ToolCallStatus::Completed); for terminal in terminals { match views.entry(terminal.entity_id()) { collections::hash_map::Entry::Vacant(entry) => { let element = create_terminal( self.workspace.clone(), self.project.clone(), terminal.clone(), window, cx, ) .into_any(); cx.emit(EntryViewEvent { entry_index: index, view_event: ViewEvent::NewTerminal(id.clone()), }); entry.insert(element); } collections::hash_map::Entry::Occupied(_entry) => { if is_tool_call_completed && terminal.read(cx).output().is_none() { cx.emit(EntryViewEvent { entry_index: index, view_event: ViewEvent::TerminalMovedToBackground(id.clone()), }); } } } } for diff in diffs { views.entry(diff.entity_id()).or_insert_with(|| { let editor = create_editor_diff(diff.clone(), window, cx); cx.subscribe(&editor, { let diff = diff.clone(); let entry_index = index; move |_this, _editor, event: &EditorEvent, cx| { if let EditorEvent::OpenExcerptsRequested { selections_by_buffer, split, } = event { let multibuffer = diff.read(cx).multibuffer(); if let Some((buffer_id, (ranges, _))) = selections_by_buffer.iter().next() { if let Some(buffer) = multibuffer.read(cx).buffer(*buffer_id) { if let Some(range) = ranges.first() { let point = buffer.read(cx).offset_to_point(range.start.0); if let Some(path) = diff.read(cx).file_path(cx) { cx.emit(EntryViewEvent { entry_index, view_event: ViewEvent::OpenDiffLocation { path, position: point, split: *split, }, }); } } } } } } }) .detach(); cx.emit(EntryViewEvent { entry_index: index, view_event: ViewEvent::NewDiff(id.clone()), }); editor.into_any() }); } } AgentThreadEntry::Elicitation(_) => { if !matches!(self.entries.get(index), Some(Entry::Elicitation { .. })) { self.set_entry( index, Entry::Elicitation { focus_handle: cx.focus_handle(), }, ); } } AgentThreadEntry::AssistantMessage(message) => { let entry = if let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) { entry } else { self.set_entry( index, Entry::AssistantMessage(AssistantMessageEntry { scroll_handles_by_chunk_index: HashMap::default(), focus_handle: cx.focus_handle(), }), ); let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else { unreachable!() }; entry }; entry.sync(message); } AgentThreadEntry::CompletedPlan(_) => { if !matches!(self.entries.get(index), Some(Entry::CompletedPlan)) { self.set_entry(index, Entry::CompletedPlan); } } AgentThreadEntry::ContextCompaction(_) => { if !matches!(self.entries.get(index), Some(Entry::ContextCompaction)) { self.set_entry(index, Entry::ContextCompaction); } } }; } fn set_entry(&mut self, index: usize, entry: Entry) { if index == self.entries.len() { self.entries.push(entry); } else { self.entries[index] = entry; } } pub fn remove(&mut self, range: Range<usize>) { self.entries.drain(range.clone()); self.expanded_compactions = self .expanded_compactions .iter() .filter_map(|&entry_ix| reindex_after_removal(entry_ix, &range)) .collect(); self.expanded_thinking_blocks = self .expanded_thinking_blocks .iter() .filter_map(|&(entry_ix, chunk_ix)| { reindex_after_removal(entry_ix, &range).map(|entry_ix| (entry_ix, chunk_ix)) }) .collect(); self.user_toggled_thinking_blocks = self .user_toggled_thinking_blocks .iter() .filter_map(|&(entry_ix, chunk_ix)| { reindex_after_removal(entry_ix, &range).map(|entry_ix| (entry_ix, chunk_ix)) }) .collect(); self.auto_expanded_thinking_block = self.auto_expanded_thinking_block .and_then(|(entry_ix, chunk_ix)| { reindex_after_removal(entry_ix, &range).map(|entry_ix| (entry_ix, chunk_ix)) }); } pub fn agent_ui_font_size_changed(&mut self, cx: &mut App) { for entry in self.entries.iter() { match entry { Entry::UserMessage { .. } | Entry::AssistantMessage { .. } | Entry::Elicitation { .. } | Entry::CompletedPlan | Entry::ContextCompaction => {} Entry::ToolCall(ToolCallEntry { content, .. }) => { for view in content.values() { if let Ok(diff_editor) = view.clone().downcast::<Editor>() { diff_editor.update(cx, |diff_editor, cx| { diff_editor.set_text_style_refinement( diff_editor_text_style_refinement(cx), ); cx.notify(); }) } } } } } } } impl EventEmitter<EntryViewEvent> for EntryViewState {} pub struct EntryViewEvent { pub entry_index: usize, pub view_event: ViewEvent, } pub enum ViewEvent { NewDiff(acp::ToolCallId), NewTerminal(acp::ToolCallId), TerminalMovedToBackground(acp::ToolCallId), MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent), OpenDiffLocation {