use std::{cell::RefCell, ops::Range, rc::Rc}; use super::thread_history::ThreadHistory;use acp_thread::{AcpThread, AgentThreadEntry};use agent::ThreadStore;use agent_client_protocol::{self as acp, ToolCallId};use collections::HashMap;use editor::{Editor, EditorEvent, EditorMode, MinimapVisibility, 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};use prompt_store::PromptStore;use rope::Point;use settings::Settings as _;use terminal_view::TerminalView;use theme::ThemeSettings;use ui::{Context, TextSize};use workspace::Workspace; use crate::message_editor::{MessageEditor, MessageEditorEvent}; pub struct EntryViewState { workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, history: WeakEntity<ThreadHistory>, prompt_store: Option<Entity<PromptStore>>, entries: Vec<Entry>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_id: AgentId,} impl EntryViewState { pub fn new( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, history: WeakEntity<ThreadHistory>, prompt_store: Option<Entity<PromptStore>>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_id: AgentId, ) -> Self { Self { workspace, project, thread_store, history, prompt_store, entries: Vec::new(), prompt_capabilities, available_commands, agent_id, } } 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 has_id = message.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.history.clone(), self.prompt_store.clone(), self.prompt_capabilities.clone(), self.available_commands.clone(), self.agent_id.clone(), "Edit message - @ to include context", editor::EditorMode::AutoHeight { min_lines: 1, max_lines: None, }, window, cx, ); if !has_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(), }), ); 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::AssistantMessage(message) => { let entry = if let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) { entry } else { self.set_entry( index, Entry::AssistantMessage(AssistantMessageEntry::default()), ); let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else { unreachable!() }; entry }; entry.sync(message); } }; } 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); } 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::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(ToolCallId), NewTerminal(ToolCallId), TerminalMovedToBackground(ToolCallId), MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent), OpenDiffLocation { path: String, position: Point, split: bool, },} #[derive(Default, Debug)]pub struct AssistantMessageEntry { scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,} impl AssistantMessageEntry { pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> { self.scroll_handles_by_chunk_index.get(&ix).cloned() } pub fn sync(&mut self, message: &acp_thread::AssistantMessage) { if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() { let ix = message.chunks.len() - 1; let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default(); handle.scroll_to_bottom(); } }} #[derive(Debug)]pub struct ToolCallEntry { content: HashMap<EntityId, AnyEntity>,} #[derive(Debug)]pub enum Entry { UserMessage(Entity<MessageEditor>), AssistantMessage(AssistantMessageEntry), ToolCall(ToolCallEntry),} impl Entry { pub fn focus_handle(&self, cx: &App) -> Option<FocusHandle> { match self { Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)), Self::AssistantMessage(_) | Self::ToolCall(_) => None, } } pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> { match self { Self::UserMessage(editor) => Some(editor), Self::AssistantMessage(_) | Self::ToolCall(_) => None, } } pub fn editor_for_diff(&self, diff: &Entity<acp_thread::Diff>) -> Option<Entity<Editor>> { self.content_map()? .get(&diff.entity_id()) .cloned() .map(|entity| entity.downcast::<Editor>().unwrap()) } pub fn terminal( &self, terminal: &Entity<acp_thread::Terminal>, ) -> Option<Entity<TerminalView>> { self.content_map()? .get(&terminal.entity_id()) .cloned() .map(|entity| entity.downcast::<TerminalView>().unwrap()) } pub fn scroll_handle_for_assistant_message_chunk( &self, chunk_ix: usize, ) -> Option<ScrollHandle> { match self { Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix), Self::UserMessage(_) | Self::ToolCall(_) => None, } } fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> { match self { Self::ToolCall(ToolCallEntry { content }) => Some(content), _ => None, } } #[cfg(test)] pub fn has_content(&self) -> bool { match self { Self::ToolCall(ToolCallEntry { content }) => !content.is_empty(), Self::UserMessage(_) | Self::AssistantMessage(_) => false, } }} fn create_terminal( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, terminal: Entity<acp_thread::Terminal>, window: &mut Window, cx: &mut App,) -> Entity<TerminalView> { cx.new(|cx| { let mut view = TerminalView::new( terminal.read(cx).inner().clone(), workspace, None, project, window, cx, ); view.set_embedded_mode(Some(1000), cx); view })} fn create_editor_diff( diff: Entity<acp_thread::Diff>, window: &mut Window, cx: &mut App,) -> Entity<Editor> { cx.new(|cx| { let mut editor = Editor::new( EditorMode::Full { scale_ui_elements_with_buffer_font_size: false, show_active_line_background: false, sizing_behavior: SizingBehavior::SizeByContent, }, diff.read(cx).multibuffer().clone(), None, window, cx, ); editor.set_show_gutter(false, cx); editor.disable_inline_diagnostics(); editor.disable_expand_excerpt_buttons(cx); editor.set_show_vertical_scrollbar(false, cx); editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); editor.set_soft_wrap_mode(SoftWrap::None, cx); editor.scroll_manager.set_forbid_vertical_scroll(true); editor.set_show_indent_guides(false, cx); editor.set_read_only(true); editor.set_delegate_open_excerpts(true); editor.set_show_breakpoints(false, cx); editor.set_show_code_actions(false, cx); editor.set_show_git_diff_gutter(false, cx); editor.set_expand_all_diff_hunks(cx); editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); editor })} fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { TextStyleRefinement { font_size: Some( TextSize::Small .rems(cx) .to_pixels(ThemeSettings::get_global(cx).agent_ui_font_size(cx)) .into(), ), ..Default::default() }} #[cfg(test)]mod tests { use std::path::Path; use std::rc::Rc; use acp_thread::{AgentConnection, StubAgentConnection}; use agent_client_protocol as acp; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use editor::RowInfo; use fs::FakeFs; use gpui::{AppContext as _, TestAppContext}; use crate::entry_view_state::EntryViewState; use multi_buffer::MultiBufferRow; use pretty_assertions::assert_matches; use project::Project; use serde_json::json; use settings::SettingsStore; use util::path; use workspace::{MultiWorkspace, PathList}; #[gpui::test] async fn test_diff_sync(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project", json!({ "hello.txt": "hi world" }), ) .await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; let (multi_workspace, cx) = cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let tool_call = acp::ToolCall::new("tool", "Tool call") .status(acp::ToolCallStatus::InProgress) .content(vec![acp::ToolCallContent::Diff( acp::Diff::new("/project/hello.txt", "hello world").old_text("hi world"), )]); let connection = Rc::new(StubAgentConnection::new()); let thread = cx .update(|_, cx| { connection.clone().new_session( project.clone(), PathList::new(&[Path::new(path!("/project"))]),Lines 1–500 of 590. Load more
use std::{cell::RefCell, ops::Range, rc::Rc}; use super::thread_history::ThreadHistory; use acp_thread::{AcpThread, AgentThreadEntry}; use agent::ThreadStore; use agent_client_protocol::{self as acp, ToolCallId}; use collections::HashMap; use editor::{Editor, EditorEvent, EditorMode, MinimapVisibility, 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}; use prompt_store::PromptStore; use rope::Point; use settings::Settings as _; use terminal_view::TerminalView; use theme::ThemeSettings; use ui::{Context, TextSize}; use workspace::Workspace; use crate::message_editor::{MessageEditor, MessageEditorEvent}; pub struct EntryViewState { workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, history: WeakEntity<ThreadHistory>, prompt_store: Option<Entity<PromptStore>>, entries: Vec<Entry>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_id: AgentId, } impl EntryViewState { pub fn new( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>, history: WeakEntity<ThreadHistory>, prompt_store: Option<Entity<PromptStore>>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_id: AgentId, ) -> Self { Self { workspace, project, thread_store, history, prompt_store, entries: Vec::new(), prompt_capabilities, available_commands, agent_id, } } 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 has_id = message.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.history.clone(), self.prompt_store.clone(), self.prompt_capabilities.clone(), self.available_commands.clone(), self.agent_id.clone(), "Edit message - @ to include context", editor::EditorMode::AutoHeight { min_lines: 1, max_lines: None, }, window, cx, ); if !has_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(), }), ); 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::AssistantMessage(message) => { let entry = if let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) { entry } else { self.set_entry( index, Entry::AssistantMessage(AssistantMessageEntry::default()), ); let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else { unreachable!() }; entry }; entry.sync(message); } }; } 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); } 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::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(ToolCallId), NewTerminal(ToolCallId), TerminalMovedToBackground(ToolCallId), MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent), OpenDiffLocation { path: String, position: Point, split: bool, }, } #[derive(Default, Debug)] pub struct AssistantMessageEntry { scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>, } impl AssistantMessageEntry { pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> { self.scroll_handles_by_chunk_index.get(&ix).cloned() } pub fn sync(&mut self, message: &acp_thread::AssistantMessage) { if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() { let ix = message.chunks.len() - 1; let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default(); handle.scroll_to_bottom(); } } } #[derive(Debug)] pub struct ToolCallEntry { content: HashMap<EntityId, AnyEntity>, } #[derive(Debug)] pub enum Entry { UserMessage(Entity<MessageEditor>), AssistantMessage(AssistantMessageEntry), ToolCall(ToolCallEntry), } impl Entry { pub fn focus_handle(&self, cx: &App) -> Option<FocusHandle> { match self { Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)), Self::AssistantMessage(_) | Self::ToolCall(_) => None, } } pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> { match self { Self::UserMessage(editor) => Some(editor), Self::AssistantMessage(_) | Self::ToolCall(_) => None, } } pub fn editor_for_diff(&self, diff: &Entity<acp_thread::Diff>) -> Option<Entity<Editor>> { self.content_map()? .get(&diff.entity_id()) .cloned() .map(|entity| entity.downcast::<Editor>().unwrap()) } pub fn terminal( &self, terminal: &Entity<acp_thread::Terminal>, ) -> Option<Entity<TerminalView>> { self.content_map()? .get(&terminal.entity_id()) .cloned() .map(|entity| entity.downcast::<TerminalView>().unwrap()) } pub fn scroll_handle_for_assistant_message_chunk( &self, chunk_ix: usize, ) -> Option<ScrollHandle> { match self { Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix), Self::UserMessage(_) | Self::ToolCall(_) => None, } } fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> { match self { Self::ToolCall(ToolCallEntry { content }) => Some(content), _ => None, } } #[cfg(test)] pub fn has_content(&self) -> bool { match self { Self::ToolCall(ToolCallEntry { content }) => !content.is_empty(), Self::UserMessage(_) | Self::AssistantMessage(_) => false, } } } fn create_terminal( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, terminal: Entity<acp_thread::Terminal>, window: &mut Window, cx: &mut App, ) -> Entity<TerminalView> { cx.new(|cx| { let mut view = TerminalView::new( terminal.read(cx).inner().clone(), workspace, None, project, window, cx, ); view.set_embedded_mode(Some(1000), cx); view }) } fn create_editor_diff( diff: Entity<acp_thread::Diff>, window: &mut Window, cx: &mut App, ) -> Entity<Editor> { cx.new(|cx| { let mut editor = Editor::new( EditorMode::Full { scale_ui_elements_with_buffer_font_size: false, show_active_line_background: false, sizing_behavior: SizingBehavior::SizeByContent, }, diff.read(cx).multibuffer().clone(), None, window, cx, ); editor.set_show_gutter(false, cx); editor.disable_inline_diagnostics(); editor.disable_expand_excerpt_buttons(cx); editor.set_show_vertical_scrollbar(false, cx); editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); editor.set_soft_wrap_mode(SoftWrap::None, cx); editor.scroll_manager.set_forbid_vertical_scroll(true); editor.set_show_indent_guides(false, cx); editor.set_read_only(true); editor.set_delegate_open_excerpts(true); editor.set_show_breakpoints(false, cx); editor.set_show_code_actions(false, cx); editor.set_show_git_diff_gutter(false, cx); editor.set_expand_all_diff_hunks(cx); editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); editor }) } fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { TextStyleRefinement { font_size: Some( TextSize::Small .rems(cx) .to_pixels(ThemeSettings::get_global(cx).agent_ui_font_size(cx)) .into(), ), ..Default::default() } } #[cfg(test)] mod tests { use std::path::Path; use std::rc::Rc; use acp_thread::{AgentConnection, StubAgentConnection}; use agent_client_protocol as acp; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use editor::RowInfo; use fs::FakeFs; use gpui::{AppContext as _, TestAppContext}; use crate::entry_view_state::EntryViewState; use multi_buffer::MultiBufferRow; use pretty_assertions::assert_matches; use project::Project; use serde_json::json; use settings::SettingsStore; use util::path; use workspace::{MultiWorkspace, PathList}; #[gpui::test] async fn test_diff_sync(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project", json!({ "hello.txt": "hi world" }), ) .await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; let (multi_workspace, cx) = cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let tool_call = acp::ToolCall::new("tool", "Tool call") .status(acp::ToolCallStatus::InProgress) .content(vec![acp::ToolCallContent::Diff( acp::Diff::new("/project/hello.txt", "hello world").old_text("hi world"), )]); let connection = Rc::new(StubAgentConnection::new()); let thread = cx .update(|_, cx| { connection.clone().new_session( project.clone(), PathList::new(&[Path::new(path!("/project"))]),