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) -> Option { if index < removed.start { Some(index) } else if index < removed.end { None } else { Some(index - removed.len()) } } pub struct EntryViewState { workspace: WeakEntity, project: WeakEntity, thread_store: Option>, entries: Vec, 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, expanded_tool_calls: HashSet, } impl EntryViewState { pub fn new( workspace: WeakEntity, project: WeakEntity, thread_store: Option>, 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, window: &mut Window, cx: &mut Context, ) { 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::>(); let diffs = tool_call.diffs().cloned().collect::>(); 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) { 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::() { diff_editor.update(cx, |diff_editor, cx| { diff_editor.set_text_style_refinement( diff_editor_text_style_refinement(cx), ); cx.notify(); }) } } } } } } } impl EventEmitter 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, MessageEditorEvent), OpenDiffLocation { path: String, position: Point, split: bool, }, } #[derive(Debug)] pub struct AssistantMessageEntry { scroll_handles_by_chunk_index: HashMap, focus_handle: FocusHandle, } impl AssistantMessageEntry { pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option { 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, focus_handle: FocusHandle, } #[derive(Debug)] pub enum Entry { UserMessage(Entity), AssistantMessage(AssistantMessageEntry), ToolCall(ToolCallEntry), Elicitation { focus_handle: FocusHandle }, CompletedPlan, ContextCompaction, } impl Entry { pub fn focus_handle(&self, cx: &App) -> Option { match self { Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)), Self::AssistantMessage(message) => Some(message.focus_handle.clone()), Self::ToolCall(tool_call) => Some(tool_call.focus_handle.clone()), Self::Elicitation { focus_handle } => Some(focus_handle.clone()), Self::CompletedPlan | Self::ContextCompaction => None, } } pub fn message_editor(&self) -> Option<&Entity> { match self { Self::UserMessage(editor) => Some(editor), Self::AssistantMessage(_) | Self::ToolCall(_) | Self::Elicitation { .. } | Self::CompletedPlan | Self::ContextCompaction => None, } } pub fn editor_for_diff(&self, diff: &Entity) -> Option> { self.content_map()? .get(&diff.entity_id()) .cloned() .and_then(|entity| entity.downcast::().ok()) } pub fn terminal( &self, terminal: &Entity, ) -> Option> { self.content_map()? .get(&terminal.entity_id()) .cloned() .and_then(|entity| entity.downcast::().ok()) } pub fn scroll_handle_for_assistant_message_chunk( &self, chunk_ix: usize, ) -> Option { match self { Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix), Self::UserMessage(_) | Self::ToolCall(_) | Self::Elicitation { .. } | Self::CompletedPlan | Self::ContextCompaction => None, } } fn content_map(&self) -> Option<&HashMap> { 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(_) | Self::Elicitation { .. } | Self::CompletedPlan | Self::ContextCompaction => false, } } } impl Focusable for ToolCallEntry { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl Focusable for Entry { fn focus_handle(&self, cx: &App) -> FocusHandle { match self { Self::UserMessage(editor) => editor.read(cx).focus_handle(cx), Self::AssistantMessage(message) => message.focus_handle.clone(), Self::ToolCall(tool_call) => tool_call.focus_handle.clone(), Self::Elicitation { focus_handle } => focus_handle.clone(), Self::CompletedPlan | Self::ContextCompaction => cx.focus_handle(), } } } fn create_terminal( workspace: WeakEntity, project: WeakEntity, terminal: Entity, window: &mut Window, cx: &mut App, ) -> Entity { 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, window: &mut Window, cx: &mut App, ) -> Entity { 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_diagnostics(cx); editor.set_max_diagnostics_severity(DiagnosticSeverity::Off, cx); 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.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_bookmarks(false, cx); 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_diff_hunk_delegate(Some(Arc::new(RestoreOnlyUnstagedDiffHunkDelegate)), 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 std::sync::Arc; use acp_thread::{AgentConnection, StubAgentConnection}; use agent_client_protocol::schema::v1 as acp; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use editor::RowInfo; use fs::FakeFs; use gpui::{AppContext as _, TestAppContext}; use parking_lot::RwLock; use crate::entry_view_state::{Entry, EntryViewState}; use crate::message_editor::SessionCapabilities; 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}; #[test] fn test_reindex_after_removal() { use super::reindex_after_removal; // Entries before the removed range keep their index. assert_eq!(reindex_after_removal(0, &(2..4)), Some(0)); assert_eq!(reindex_after_removal(1, &(2..4)), Some(1)); // Entries inside the removed range are dropped. assert_eq!(reindex_after_removal(2, &(2..4)), None); assert_eq!(reindex_after_removal(3, &(2..4)), None); // Entries after the removed range slide down by its length. assert_eq!(reindex_after_removal(4, &(2..4)), Some(2)); assert_eq!(reindex_after_removal(5, &(2..4)), Some(3)); // An empty removal range leaves indices untouched. assert_eq!(reindex_after_removal(3, &(2..2)), Some(3)); } #[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"))]), cx, ) }) .await .unwrap(); let session_id = thread.update(cx, |thread, _| thread.session_id().clone()); cx.update(|_, cx| { connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx) }); let thread_store = None; let view_state = cx.new(|_cx| { EntryViewState::new( workspace.downgrade(), project.downgrade(), thread_store, Arc::new(RwLock::new(SessionCapabilities::default())), "Test Agent".into(), ) }); view_state.update_in(cx, |view_state, window, cx| { view_state.sync_entry(0, &thread, window, cx) }); let diff = thread.read_with(cx, |thread, _| { thread .entries() .get(0) .unwrap() .diffs() .next() .unwrap() .clone() }); cx.run_until_parked(); let diff_editor = view_state.read_with(cx, |view_state, _cx| { view_state.entry(0).unwrap().editor_for_diff(&diff).unwrap() }); assert_eq!( diff_editor.read_with(cx, |editor, cx| editor.text(cx)), "hi world\nhello world" ); let row_infos = diff_editor.read_with(cx, |editor, cx| { let multibuffer = editor.buffer().read(cx); multibuffer .snapshot(cx) .row_infos(MultiBufferRow(0)) .collect::>() }); assert_matches!( row_infos.as_slice(), [ RowInfo { multibuffer_row: Some(MultiBufferRow(0)), diff_status: Some(DiffHunkStatus { kind: DiffHunkStatusKind::Deleted, .. }), .. }, RowInfo { multibuffer_row: Some(MultiBufferRow(1)), diff_status: Some(DiffHunkStatus { kind: DiffHunkStatusKind::Added, .. }), .. } ] ); } #[gpui::test] async fn test_elicitation_preserves_entry_index(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree("/project", json!({})).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 connection = Rc::new(StubAgentConnection::new()); let thread = cx .update(|_, cx| { connection.clone().new_session( project.clone(), PathList::new(&[Path::new(path!("/project"))]), cx, ) }) .await .unwrap(); let session_id = thread.update(cx, |thread, _| thread.session_id().clone()); let _response_task = thread.update(cx, |thread, cx| { thread .request_elicitation( acp::CreateElicitationRequest::new( acp::ElicitationFormMode::new( acp::ElicitationSessionScope::new(session_id.clone()), acp::ElicitationSchema::new().string("name", true), ), "Provide a name", ), cx, ) .unwrap() }); cx.update(|_, cx| { connection.send_update( session_id, acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new( acp::ContentBlock::Text(acp::TextContent::new("hello")), )), cx, ); }); let view_state = cx.new(|_cx| { EntryViewState::new( workspace.downgrade(), project.downgrade(), None, Arc::new(RwLock::new(SessionCapabilities::default())), "Test Agent".into(), ) }); view_state.update_in(cx, |view_state, window, cx| { view_state.sync_entry(0, &thread, window, cx); view_state.sync_entry(1, &thread, window, cx); }); view_state.read_with(cx, |view_state, _cx| { assert!(matches!( view_state.entry(0), Some(Entry::Elicitation { .. }) )); assert!(matches!( view_state.entry(1), Some(Entry::AssistantMessage(_)) )); }); } fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let mut settings_store = SettingsStore::test(cx); settings_store.register_setting::(); cx.set_global(settings_store); theme_settings::init(theme::LoadThemes::JustBase, cx); release_channel::init(semver::Version::new(0, 0, 0), cx); }); } }