diff --git a/crates/agent_ui/src/agent_connection_store.rs b/crates/agent_ui/src/agent_connection_store.rs index 79644eb26886c4ea23b9440473193a8f15bec977..545fedae278c7bb6747984833a83abf5fdb01602 100644 --- a/crates/agent_ui/src/agent_connection_store.rs +++ b/crates/agent_ui/src/agent_connection_store.rs @@ -24,7 +24,7 @@ pub enum AgentConnectionEntry { #[derive(Clone)] pub struct AgentConnectedState { pub connection: Rc, - pub history: Entity, + pub history: Option>, } impl AgentConnectionEntry { @@ -38,7 +38,7 @@ impl AgentConnectionEntry { pub fn history(&self) -> Option<&Entity> { match self { - AgentConnectionEntry::Connected(state) => Some(&state.history), + AgentConnectionEntry::Connected(state) => state.history.as_ref(), _ => None, } } @@ -163,7 +163,9 @@ impl AgentConnectionStore { let connect_task = server.connect(delegate, cx); let connect_task = cx.spawn(async move |_this, cx| match connect_task.await { Ok(connection) => cx.update(|cx| { - let history = cx.new(|cx| ThreadHistory::new(connection.session_list(cx), cx)); + let history = connection + .session_list(cx) + .map(|session_list| cx.new(|cx| ThreadHistory::new(session_list, cx))); Ok(AgentConnectedState { connection, history, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 2dd120dffd6ba50455c0f5ab31b679b91e667006..0fa332532f312e9d1815fdefa03abd8cdb32185f 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1461,13 +1461,9 @@ impl AgentPanel { .read(cx) .history()? .clone(); - if history.read(cx).has_session_list() { - Some(History::AgentThreads { - view: self.create_thread_history_view(agent, history, window, cx), - }) - } else { - None - } + Some(History::AgentThreads { + view: self.create_thread_history_view(agent, history, window, cx), + }) } } } @@ -4650,16 +4646,13 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { let Some(panel) = workspace.read(cx).panel::(cx) else { return; }; - let Some(history) = panel + let history = panel .read(cx) .connection_store() .read(cx) .entry(&crate::Agent::NativeAgent) .and_then(|s| s.read(cx).history()) - else { - log::error!("No connection entry found for native agent"); - return; - }; + .map(|h| h.downgrade()); let project = workspace.read(cx).project().downgrade(); let panel = panel.read(cx); let thread_store = panel.thread_store().clone(); @@ -4669,7 +4662,7 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { project, thread_store, None, - history.downgrade(), + history, initial_prompt, window, cx, diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs index d8c45755413ffb14433e3eeb4309e869de195a75..ca59796966ff4ea5a5ad71f38cf65793a7cb0f3e 100644 --- a/crates/agent_ui/src/completion_provider.rs +++ b/crates/agent_ui/src/completion_provider.rs @@ -223,7 +223,7 @@ pub struct PromptCompletionProvider { source: Arc, editor: WeakEntity, mention_set: Entity, - history: WeakEntity, + history: Option>, prompt_store: Option>, workspace: WeakEntity, } @@ -233,7 +233,7 @@ impl PromptCompletionProvider { source: T, editor: WeakEntity, mention_set: Entity, - history: WeakEntity, + history: Option>, prompt_store: Option>, workspace: WeakEntity, ) -> Self { @@ -920,7 +920,7 @@ impl PromptCompletionProvider { } Some(PromptContextType::Thread) => { - if let Some(history) = self.history.upgrade() { + if let Some(history) = self.history.as_ref().and_then(|h| h.upgrade()) { let sessions = history .read(cx) .sessions() @@ -1146,7 +1146,7 @@ impl PromptCompletionProvider { return Task::ready(recent); } - if let Some(history) = self.history.upgrade() { + if let Some(history) = self.history.as_ref().and_then(|h| h.upgrade()) { const RECENT_COUNT: usize = 2; recent.extend( history diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 277b3412e9862e6458d1e6999a6fdede7bd1ec37..8a8b7803078aa7c5886251fede5f19155155f4ba 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -421,7 +421,7 @@ pub struct ConnectedServerState { active_id: Option, threads: HashMap>, connection: Rc, - history: Entity, + history: Option>, conversation: Entity, _connection_entry_subscription: Subscription, } @@ -816,7 +816,7 @@ impl ConversationView { conversation: Entity, resumed_without_history: bool, initial_content: Option, - history: Entity, + history: Option>, window: &mut Window, cx: &mut Context, ) -> Entity { @@ -833,7 +833,7 @@ impl ConversationView { self.workspace.clone(), self.project.downgrade(), self.thread_store.clone(), - history.downgrade(), + history.as_ref().map(|h| h.downgrade()), self.prompt_store.clone(), prompt_capabilities.clone(), available_commands.clone(), @@ -1082,7 +1082,7 @@ impl ConversationView { threads: HashMap::default(), connection, conversation: cx.new(|_cx| Conversation::default()), - history: cx.new(|cx| ThreadHistory::new(None, cx)), + history: None, _connection_entry_subscription: Subscription::new(|| {}), }), cx, @@ -2213,7 +2213,7 @@ impl ConversationView { let Some(connected) = self.as_connected() else { return; }; - let history = connected.history.downgrade(); + let history = connected.history.as_ref().map(|h| h.downgrade()); let Some(thread) = connected.active_view() else { return; }; @@ -2601,7 +2601,7 @@ impl ConversationView { } pub fn history(&self) -> Option<&Entity> { - self.as_connected().map(|c| &c.history) + self.as_connected().and_then(|c| c.history.as_ref()) } pub fn delete_history_entry(&mut self, session_id: &acp::SessionId, cx: &mut Context) { @@ -2609,9 +2609,10 @@ impl ConversationView { return; }; - let task = connected - .history - .update(cx, |history, cx| history.delete_session(&session_id, cx)); + let Some(history) = &connected.history else { + return; + }; + let task = history.update(cx, |history, cx| history.delete_session(&session_id, cx)); task.detach_and_log_err(cx); if let Some(store) = ThreadMetadataStore::try_global(cx) { @@ -2902,60 +2903,14 @@ pub(crate) mod tests { let session_a = AgentSessionInfo::new(SessionId::new("session-a")); let session_b = AgentSessionInfo::new(SessionId::new("session-b")); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], 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 thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); - let connection_store = - cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx))); - - let conversation_view = cx.update(|window, cx| { - cx.new(|cx| { - ConversationView::new( - Rc::new(StubAgentServer::default_response()), - connection_store, - Agent::Custom { id: "Test".into() }, - None, - None, - None, - None, - workspace.downgrade(), - project, - Some(thread_store), - None, - window, - cx, - ) - }) - }); - - // Wait for connection to establish - cx.run_until_parked(); - - let history = cx.update(|_window, cx| { - conversation_view - .read(cx) - .history() - .expect("Missing history") - .clone() - }); - - // Initially empty because StubAgentConnection.session_list() returns None - active_thread(&conversation_view, cx).read_with(cx, |view, _cx| { - assert_eq!(view.recent_history_entries.len(), 0); - }); - - // Now set the session list - this simulates external agents providing their history - let list_a: Rc = - Rc::new(StubSessionList::new(vec![session_a.clone()])); - history.update(cx, |history, cx| { - history.set_session_list(Some(list_a), cx); - }); - cx.run_until_parked(); + // Use a connection that provides a session list so ThreadHistory is created + let (conversation_view, history, cx) = setup_thread_view_with_history( + StubAgentServer::new(SessionHistoryConnection::new(vec![session_a.clone()])), + cx, + ) + .await; + // Initially has session_a from the connection's session list active_thread(&conversation_view, cx).read_with(cx, |view, _cx| { assert_eq!(view.recent_history_entries.len(), 1); assert_eq!( @@ -2964,11 +2919,11 @@ pub(crate) mod tests { ); }); - // Update to a different session list + // Swap to a different session list let list_b: Rc = Rc::new(StubSessionList::new(vec![session_b.clone()])); history.update(cx, |history, cx| { - history.set_session_list(Some(list_b), cx); + history.set_session_list(list_b, cx); }); cx.run_until_parked(); @@ -2986,19 +2941,12 @@ pub(crate) mod tests { init_test(cx); let session = AgentSessionInfo::new(SessionId::new("history-session")); - let (conversation_view, history, cx) = setup_thread_view_with_history( + let (conversation_view, _history, cx) = setup_thread_view_with_history( StubAgentServer::new(SessionHistoryConnection::new(vec![session.clone()])), cx, ) .await; - history.read_with(cx, |history, _cx| { - assert!( - history.has_session_list(), - "session list should be attached after thread creation" - ); - }); - active_thread(&conversation_view, cx).read_with(cx, |view, _cx| { assert_eq!(view.recent_history_entries.len(), 1); assert_eq!( diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 63442e7aa9c526172966b5d6a340205a9cfc7780..fd76c34402a5ba71a8e5feea03dc6398e6a43b4b 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -228,8 +228,8 @@ pub struct ThreadView { pub hovered_recent_history_item: Option, pub show_external_source_prompt_warning: bool, pub show_codex_windows_warning: bool, - pub history: Entity, - pub _history_subscription: Subscription, + pub history: Option>, + pub _history_subscription: Option, } impl Focusable for ThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { @@ -273,7 +273,7 @@ impl ThreadView { resumed_without_history: bool, project: WeakEntity, thread_store: Option>, - history: Entity, + history: Option>, prompt_store: Option>, initial_content: Option, mut subscriptions: Vec, @@ -284,8 +284,10 @@ impl ThreadView { let placeholder = placeholder_text(agent_display_name.as_ref(), false); - let history_subscription = cx.observe(&history, |this, history, cx| { - this.update_recent_history_from_cache(&history, cx); + let history_subscription = history.as_ref().map(|h| { + cx.observe(h, |this, history, cx| { + this.update_recent_history_from_cache(&history, cx); + }) }); let mut should_auto_submit = false; @@ -296,7 +298,7 @@ impl ThreadView { workspace.clone(), project.clone(), thread_store, - history.downgrade(), + history.as_ref().map(|h| h.downgrade()), prompt_store, prompt_capabilities.clone(), available_commands.clone(), @@ -392,7 +394,10 @@ impl ThreadView { })); })); - let recent_history_entries = history.read(cx).get_recent_sessions(3); + let recent_history_entries = history + .as_ref() + .map(|h| h.read(cx).get_recent_sessions(3)) + .unwrap_or_default(); let mut this = Self { id, @@ -7502,7 +7507,10 @@ impl ThreadView { ), ) .child(v_flex().p_1().pr_1p5().gap_1().children({ - let supports_delete = self.history.read(cx).supports_delete(); + let supports_delete = self + .history + .as_ref() + .map_or(false, |h| h.read(cx).supports_delete()); recent_history .into_iter() .enumerate() diff --git a/crates/agent_ui/src/entry_view_state.rs b/crates/agent_ui/src/entry_view_state.rs index 92075616547d7917119b42cf762557ce163d0a2a..e6b1124bccf5498ff7851303988d09c74a362972 100644 --- a/crates/agent_ui/src/entry_view_state.rs +++ b/crates/agent_ui/src/entry_view_state.rs @@ -26,7 +26,7 @@ pub struct EntryViewState { workspace: WeakEntity, project: WeakEntity, thread_store: Option>, - history: WeakEntity, + history: Option>, prompt_store: Option>, entries: Vec, prompt_capabilities: Rc>, @@ -39,7 +39,7 @@ impl EntryViewState { workspace: WeakEntity, project: WeakEntity, thread_store: Option>, - history: WeakEntity, + history: Option>, prompt_store: Option>, prompt_capabilities: Rc>, available_commands: Rc>>, @@ -510,14 +510,14 @@ mod tests { }); let thread_store = None; - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); + let history: Option> = None; let view_state = cx.new(|_cx| { EntryViewState::new( workspace.downgrade(), project.downgrade(), thread_store, - history.downgrade(), + history, None, Default::default(), Default::default(), diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 1fc66f6079fa146440a1f5a594d9f160e4580ab2..74f48c204957a76cc79bc71aac0526fde6f3ae5c 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -278,15 +278,11 @@ impl InlineAssistant { let prompt_store = agent_panel.prompt_store().as_ref().cloned(); let thread_store = agent_panel.thread_store().clone(); - let Some(history) = agent_panel + let history = agent_panel .connection_store() .read(cx) .entry(&crate::Agent::NativeAgent) - .and_then(|s| s.read(cx).history().cloned()) - else { - log::error!("No connection entry found for native agent"); - return; - }; + .and_then(|s| s.read(cx).history().cloned()); let handle_assist = |window: &mut Window, cx: &mut Context| match inline_assist_target { @@ -298,7 +294,7 @@ impl InlineAssistant { workspace.project().downgrade(), thread_store, prompt_store, - history.downgrade(), + history.as_ref().map(|h| h.downgrade()), action.prompt.clone(), window, cx, @@ -313,7 +309,7 @@ impl InlineAssistant { workspace.project().downgrade(), thread_store, prompt_store, - history.downgrade(), + history.as_ref().map(|h| h.downgrade()), action.prompt.clone(), window, cx, @@ -495,7 +491,7 @@ impl InlineAssistant { project: WeakEntity, thread_store: Entity, prompt_store: Option>, - history: WeakEntity, + history: Option>, initial_prompt: Option, window: &mut Window, codegen_ranges: &[Range], @@ -634,7 +630,7 @@ impl InlineAssistant { project: WeakEntity, thread_store: Entity, prompt_store: Option>, - history: WeakEntity, + history: Option>, initial_prompt: Option, window: &mut Window, cx: &mut App, @@ -679,7 +675,7 @@ impl InlineAssistant { workspace: Entity, thread_store: Entity, prompt_store: Option>, - history: WeakEntity, + history: Option>, window: &mut Window, cx: &mut App, ) -> InlineAssistId { @@ -1983,8 +1979,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { .read(cx) .entry(&crate::Agent::NativeAgent) .and_then(|e| e.read(cx).history()) - .context("no history found for native agent")? - .downgrade(); + .map(|h| h.downgrade()); anyhow::Ok((panel.thread_store().clone(), history)) })??; @@ -2155,7 +2150,7 @@ pub mod test { setup(cx); - let (_editor, buffer, _history) = cx.update(|window, cx| { + let (_editor, buffer) = cx.update(|window, cx| { let buffer = cx.new(|cx| Buffer::local("", cx)); let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let editor = cx.new(|cx| Editor::for_multibuffer(multibuffer, None, window, cx)); @@ -2172,7 +2167,6 @@ pub mod test { }); let thread_store = cx.new(|cx| ThreadStore::new(cx)); - let history = cx.new(|cx| crate::ThreadHistory::new(None, cx)); // Add editor to workspace workspace.update(cx, |workspace, cx| { @@ -2188,7 +2182,7 @@ pub mod test { project.downgrade(), thread_store, None, - history.downgrade(), + None, Some(prompt), window, cx, @@ -2198,7 +2192,7 @@ pub mod test { inline_assistant.start_assist(assist_id, window, cx); }); - (editor, buffer, history) + (editor, buffer) }); cx.run_until_parked(); diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index fa68c86fe9fa39319b7d6adb1c7ae50544ae4f00..43e6b1ad393a8ca1d568bc8dc8df6b0fa9d977db 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -64,7 +64,7 @@ pub struct PromptEditor { pub editor: Entity, mode: PromptEditorMode, mention_set: Entity, - history: WeakEntity, + history: Option>, prompt_store: Option>, workspace: WeakEntity, model_selector: Entity, @@ -1227,7 +1227,7 @@ impl PromptEditor { fs: Arc, thread_store: Entity, prompt_store: Option>, - history: WeakEntity, + history: Option>, project: WeakEntity, workspace: WeakEntity, window: &mut Window, @@ -1386,7 +1386,7 @@ impl PromptEditor { fs: Arc, thread_store: Entity, prompt_store: Option>, - history: WeakEntity, + history: Option>, project: WeakEntity, workspace: WeakEntity, window: &mut Window, diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index fd625db07b0c34cdf90a9913f574d38df32e97f8..ccbd249d7cfa159c042921a11cfc18c0f09472a0 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -110,7 +110,7 @@ impl MessageEditor { workspace: WeakEntity, project: WeakEntity, thread_store: Option>, - history: WeakEntity, + history: Option>, prompt_store: Option>, prompt_capabilities: Rc>, available_commands: Rc>>, @@ -1789,7 +1789,6 @@ mod tests { let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = None; - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let message_editor = cx.update(|window, cx| { cx.new(|cx| { @@ -1797,7 +1796,7 @@ mod tests { workspace.downgrade(), project.downgrade(), thread_store.clone(), - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -1902,7 +1901,6 @@ mod tests { 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 history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let workspace_handle = workspace.downgrade(); let message_editor = workspace.update_in(cx, |_, window, cx| { cx.new(|cx| { @@ -1910,7 +1908,7 @@ mod tests { workspace_handle.clone(), project.downgrade(), thread_store.clone(), - history.downgrade(), + None, None, prompt_capabilities.clone(), available_commands.clone(), @@ -2057,7 +2055,6 @@ mod tests { let mut cx = VisualTestContext::from_window(window.into(), cx); let thread_store = None; - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); let available_commands = Rc::new(RefCell::new(vec![ acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"), @@ -2075,7 +2072,7 @@ mod tests { workspace_handle, project.downgrade(), thread_store.clone(), - history.downgrade(), + None, None, prompt_capabilities.clone(), available_commands.clone(), @@ -2291,7 +2288,6 @@ mod tests { } let thread_store = cx.new(|cx| ThreadStore::new(cx)); - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { @@ -2301,7 +2297,7 @@ mod tests { workspace_handle, project.downgrade(), Some(thread_store), - history.downgrade(), + None, None, prompt_capabilities.clone(), Default::default(), @@ -2786,7 +2782,6 @@ mod tests { let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let message_editor = cx.update(|window, cx| { cx.new(|cx| { @@ -2794,7 +2789,7 @@ mod tests { workspace.downgrade(), project.downgrade(), thread_store.clone(), - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -2886,7 +2881,6 @@ mod tests { let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let session_id = acp::SessionId::new("thread-123"); let title = Some("Previous Conversation".into()); @@ -2897,7 +2891,7 @@ mod tests { workspace.downgrade(), project.downgrade(), thread_store.clone(), - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -2961,7 +2955,6 @@ mod tests { let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = None; - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let message_editor = cx.update(|window, cx| { cx.new(|cx| { @@ -2969,7 +2962,7 @@ mod tests { workspace.downgrade(), project.downgrade(), thread_store.clone(), - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -3017,7 +3010,6 @@ mod tests { let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = None; - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let message_editor = cx.update(|window, cx| { cx.new(|cx| { @@ -3025,7 +3017,7 @@ mod tests { workspace.downgrade(), project.downgrade(), thread_store.clone(), - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -3071,7 +3063,6 @@ mod tests { let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let message_editor = cx.update(|window, cx| { cx.new(|cx| { @@ -3079,7 +3070,7 @@ mod tests { workspace.downgrade(), project.downgrade(), thread_store.clone(), - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -3126,7 +3117,6 @@ mod tests { let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let message_editor = cx.update(|window, cx| { cx.new(|cx| { @@ -3134,7 +3124,7 @@ mod tests { workspace.downgrade(), project.downgrade(), thread_store.clone(), - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -3190,7 +3180,6 @@ mod tests { let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| { let workspace_handle = cx.weak_entity(); @@ -3199,7 +3188,7 @@ mod tests { workspace_handle, project.downgrade(), thread_store.clone(), - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -3349,7 +3338,6 @@ mod tests { }); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); // Create a new `MessageEditor`. The `EditorMode::full()` has to be used // to ensure we have a fixed viewport, so we can eventually actually @@ -3361,7 +3349,7 @@ mod tests { workspace_handle, project.downgrade(), thread_store.clone(), - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -3469,7 +3457,6 @@ mod tests { let mut cx = VisualTestContext::from_window(window.into(), cx); let thread_store = cx.new(|cx| ThreadStore::new(cx)); - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { let workspace_handle = cx.weak_entity(); @@ -3478,7 +3465,7 @@ mod tests { workspace_handle, project.downgrade(), Some(thread_store), - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -3551,7 +3538,6 @@ mod tests { let mut cx = VisualTestContext::from_window(window.into(), cx); let thread_store = cx.new(|cx| ThreadStore::new(cx)); - let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { let workspace_handle = cx.weak_entity(); @@ -3560,7 +3546,7 @@ mod tests { workspace_handle, project.downgrade(), Some(thread_store), - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -3635,7 +3621,6 @@ mod tests { 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 history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let message_editor = cx.update(|window, cx| { cx.new(|cx| { @@ -3643,7 +3628,7 @@ mod tests { workspace.downgrade(), project.downgrade(), None, - history.downgrade(), + None, None, Default::default(), Default::default(), @@ -3787,7 +3772,6 @@ mod tests { 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 history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx))); let message_editor = cx.update(|window, cx| { cx.new(|cx| { @@ -3795,7 +3779,7 @@ mod tests { workspace.downgrade(), project.downgrade(), None, - history.downgrade(), + None, None, Default::default(), Default::default(), diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs index 3df3c1faaed9e02b659bc75b09257e81e96ebc03..281edc838c959ee47216811812ebb53f0467f3c0 100644 --- a/crates/agent_ui/src/terminal_inline_assistant.rs +++ b/crates/agent_ui/src/terminal_inline_assistant.rs @@ -64,7 +64,7 @@ impl TerminalInlineAssistant { project: WeakEntity, thread_store: Entity, prompt_store: Option>, - history: WeakEntity, + history: Option>, initial_prompt: Option, window: &mut Window, cx: &mut App, diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index 48d0b11b00103bbcf8399e6f7f77f8804051a465..d2aa38e13ceae1d088e8b078ce741c42f4c31206 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/crates/agent_ui/src/thread_history.rs @@ -5,59 +5,48 @@ use std::rc::Rc; use ui::prelude::*; pub struct ThreadHistory { - session_list: Option>, + session_list: Rc, sessions: Vec, _refresh_task: Task<()>, _watch_task: Option>, } impl ThreadHistory { - pub fn new(session_list: Option>, cx: &mut Context) -> Self { + pub fn new(session_list: Rc, cx: &mut Context) -> Self { let mut this = Self { - session_list: None, + session_list, sessions: Vec::new(), _refresh_task: Task::ready(()), _watch_task: None, }; - this.set_session_list_impl(session_list, cx); + + this.start_watching(cx); this } #[cfg(any(test, feature = "test-support"))] pub fn set_session_list( &mut self, - session_list: Option>, + session_list: Rc, cx: &mut Context, ) { - self.set_session_list_impl(session_list, cx); - } - - fn set_session_list_impl( - &mut self, - session_list: Option>, - cx: &mut Context, - ) { - if let (Some(current), Some(next)) = (&self.session_list, &session_list) - && Rc::ptr_eq(current, next) - { + if Rc::ptr_eq(&self.session_list, &session_list) { return; } self.session_list = session_list; self.sessions.clear(); self._refresh_task = Task::ready(()); + self.start_watching(cx); + } - let Some(session_list) = self.session_list.as_ref() else { - self._watch_task = None; - cx.notify(); - return; - }; - let Some(rx) = session_list.watch(cx) else { + fn start_watching(&mut self, cx: &mut Context) { + let Some(rx) = self.session_list.watch(cx) else { self._watch_task = None; self.refresh_sessions(false, cx); return; }; - session_list.notify_refresh(); + self.session_list.notify_refresh(); self._watch_task = Some(cx.spawn(async move |this, cx| { while let Ok(first_update) = rx.recv().await { @@ -132,10 +121,7 @@ impl ThreadHistory { } fn refresh_sessions(&mut self, load_all_pages: bool, cx: &mut Context) { - let Some(session_list) = self.session_list.clone() else { - cx.notify(); - return; - }; + let session_list = self.session_list.clone(); self._refresh_task = cx.spawn(async move |this, cx| { let mut cursor: Option = None; @@ -196,14 +182,8 @@ impl ThreadHistory { self.sessions.is_empty() } - pub fn has_session_list(&self) -> bool { - self.session_list.is_some() - } - pub fn refresh(&mut self, _cx: &mut Context) { - if let Some(session_list) = &self.session_list { - session_list.notify_refresh(); - } + self.session_list.notify_refresh(); } pub fn session_for_id(&self, session_id: &acp::SessionId) -> Option { @@ -222,10 +202,7 @@ impl ThreadHistory { } pub fn supports_delete(&self) -> bool { - self.session_list - .as_ref() - .map(|sl| sl.supports_delete()) - .unwrap_or(false) + self.session_list.supports_delete() } pub(crate) fn delete_session( @@ -233,19 +210,11 @@ impl ThreadHistory { session_id: &acp::SessionId, cx: &mut App, ) -> Task> { - if let Some(session_list) = self.session_list.as_ref() { - session_list.delete_session(session_id, cx) - } else { - Task::ready(Ok(())) - } + self.session_list.delete_session(session_id, cx) } pub(crate) fn delete_sessions(&self, cx: &mut App) -> Task> { - if let Some(session_list) = self.session_list.as_ref() { - session_list.delete_sessions(cx) - } else { - Task::ready(Ok(())) - } + self.session_list.delete_sessions(cx) } } @@ -425,7 +394,7 @@ mod tests { vec![test_session("session-2", "Second")], )); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); history.update(cx, |history, _cx| { @@ -447,7 +416,7 @@ mod tests { vec![test_session("session-2", "Second")], )); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); session_list.clear_requested_cursors(); @@ -482,7 +451,7 @@ mod tests { vec![test_session("session-2", "Second")], )); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); history.update(cx, |history, cx| history.refresh_full_history(cx)); @@ -513,7 +482,7 @@ mod tests { vec![test_session("session-2", "Second")], )); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); history.update(cx, |history, cx| history.refresh_full_history(cx)); @@ -542,7 +511,7 @@ mod tests { vec![test_session("session-2", "Second")], )); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); history.update(cx, |history, cx| history.refresh_full_history(cx)); @@ -585,7 +554,7 @@ mod tests { .with_async_responses(), ); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); session_list.clear_requested_cursors(); @@ -616,7 +585,7 @@ mod tests { }]; let session_list = Rc::new(TestSessionList::new(sessions)); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); session_list.send_update(SessionListUpdate::SessionInfo { @@ -649,7 +618,7 @@ mod tests { }]; let session_list = Rc::new(TestSessionList::new(sessions)); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); session_list.send_update(SessionListUpdate::SessionInfo { @@ -679,7 +648,7 @@ mod tests { }]; let session_list = Rc::new(TestSessionList::new(sessions)); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); session_list.send_update(SessionListUpdate::SessionInfo { @@ -712,7 +681,7 @@ mod tests { }]; let session_list = Rc::new(TestSessionList::new(sessions)); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); session_list.send_update(SessionListUpdate::SessionInfo { @@ -749,7 +718,7 @@ mod tests { }]; let session_list = Rc::new(TestSessionList::new(sessions)); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); session_list.send_update(SessionListUpdate::SessionInfo { @@ -783,7 +752,7 @@ mod tests { }]; let session_list = Rc::new(TestSessionList::new(sessions)); - let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx)); + let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx)); cx.run_until_parked(); session_list.send_update(SessionListUpdate::SessionInfo { diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs index 237a6c539c6669df0df535ae91a7ba9fa99acf9f..503dea7286a49ddec10e186c9fdafe765134e078 100644 --- a/crates/agent_ui/src/threads_archive_view.rs +++ b/crates/agent_ui/src/threads_archive_view.rs @@ -91,6 +91,22 @@ fn fuzzy_match_positions(query: &str, text: &str) -> Option> { } } +fn archive_empty_state_message( + has_history: bool, + is_empty: bool, + has_query: bool, +) -> Option<&'static str> { + if !is_empty { + None + } else if !has_history { + Some("This agent does not support viewing archived threads.") + } else if has_query { + Some("No threads match your search.") + } else { + Some("No archived threads yet.") + } +} + pub enum ThreadsArchiveViewEvent { Close, OpenThread { @@ -171,6 +187,7 @@ impl ThreadsArchiveView { fn set_selected_agent(&mut self, agent: Agent, window: &mut Window, cx: &mut Context) { self.selected_agent = agent.clone(); self.is_loading = true; + self.reset_history_subscription(); self.history = None; self.items.clear(); self.selection = None; @@ -193,25 +210,33 @@ impl ThreadsArchiveView { cx.notify(); } - fn set_history(&mut self, history: Entity, cx: &mut Context) { - self._history_subscription = cx.observe(&history, |this, _, cx| { - this.update_items(cx); - }); - history.update(cx, |history, cx| { - history.refresh_full_history(cx); - }); - self.history = Some(history); + fn reset_history_subscription(&mut self) { + self._history_subscription = Subscription::new(|| {}); + } + + fn set_history(&mut self, history: Option>, cx: &mut Context) { + self.reset_history_subscription(); + + if let Some(history) = &history { + self._history_subscription = cx.observe(history, |this, _, cx| { + this.update_items(cx); + }); + history.update(cx, |history, cx| { + history.refresh_full_history(cx); + }); + } + self.history = history; self.is_loading = false; self.update_items(cx); cx.notify(); } fn update_items(&mut self, cx: &mut Context) { - let Some(history) = self.history.as_ref() else { - return; - }; - - let sessions = history.read(cx).sessions().to_vec(); + let sessions = self + .history + .as_ref() + .map(|h| h.read(cx).sessions().to_vec()) + .unwrap_or_default(); let query = self.filter_editor.read(cx).text(cx).to_lowercase(); let today = Local::now().naive_local().date(); @@ -696,6 +721,12 @@ impl Focusable for ThreadsArchiveView { } } +impl ThreadsArchiveView { + fn empty_state_message(&self, is_empty: bool, has_query: bool) -> Option<&'static str> { + archive_empty_state_message(self.history.is_some(), is_empty, has_query) + } +} + impl Render for ThreadsArchiveView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let is_empty = self.items.is_empty(); @@ -713,24 +744,13 @@ impl Render for ThreadsArchiveView { .with_rotate_animation(2), ) .into_any_element() - } else if is_empty && has_query { + } else if let Some(message) = self.empty_state_message(is_empty, has_query) { v_flex() .flex_1() .justify_center() .items_center() .child( - Label::new("No threads match your search.") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element() - } else if is_empty { - v_flex() - .flex_1() - .justify_center() - .items_center() - .child( - Label::new("No archived threads yet.") + Label::new(message) .size(LabelSize::Small) .color(Color::Muted), ) @@ -768,3 +788,38 @@ impl Render for ThreadsArchiveView { .child(content) } } + +#[cfg(test)] +mod tests { + use super::archive_empty_state_message; + + #[test] + fn empty_state_message_returns_none_when_archive_has_items() { + assert_eq!(archive_empty_state_message(false, false, false), None); + assert_eq!(archive_empty_state_message(true, false, true), None); + } + + #[test] + fn empty_state_message_distinguishes_unsupported_history() { + assert_eq!( + archive_empty_state_message(false, true, false), + Some("This agent does not support viewing archived threads.") + ); + assert_eq!( + archive_empty_state_message(false, true, true), + Some("This agent does not support viewing archived threads.") + ); + } + + #[test] + fn empty_state_message_distinguishes_empty_history_and_search_results() { + assert_eq!( + archive_empty_state_message(true, true, false), + Some("No archived threads yet.") + ); + assert_eq!( + archive_empty_state_message(true, true, true), + Some("No threads match your search.") + ); + } +}