agent_ui: Only create ThreadHistory if agent supports it (#51759)

Ben Brandt created

Rather than always having a ThreadHistory with an internal option,
changed it to be an Option<ThreadHistory> so we can better distingiuish
between an empty list and unsupported

Release Notes:

- N/A

Change summary

crates/agent_ui/src/agent_connection_store.rs        |   8 
crates/agent_ui/src/agent_panel.rs                   |  19 -
crates/agent_ui/src/completion_provider.rs           |   8 
crates/agent_ui/src/conversation_view.rs             |  92 ++---------
crates/agent_ui/src/conversation_view/thread_view.rs |  24 ++-
crates/agent_ui/src/entry_view_state.rs              |   8 
crates/agent_ui/src/inline_assistant.rs              |  28 +--
crates/agent_ui/src/inline_prompt_editor.rs          |   6 
crates/agent_ui/src/message_editor.rs                |  50 ++----
crates/agent_ui/src/terminal_inline_assistant.rs     |   2 
crates/agent_ui/src/thread_history.rs                |  89 +++-------
crates/agent_ui/src/threads_archive_view.rs          | 107 ++++++++++---
12 files changed, 197 insertions(+), 244 deletions(-)

Detailed changes

crates/agent_ui/src/agent_connection_store.rs 🔗

@@ -24,7 +24,7 @@ pub enum AgentConnectionEntry {
 #[derive(Clone)]
 pub struct AgentConnectedState {
     pub connection: Rc<dyn AgentConnection>,
-    pub history: Entity<ThreadHistory>,
+    pub history: Option<Entity<ThreadHistory>>,
 }
 
 impl AgentConnectionEntry {
@@ -38,7 +38,7 @@ impl AgentConnectionEntry {
 
     pub fn history(&self) -> Option<&Entity<ThreadHistory>> {
         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,

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::<AgentPanel>(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,

crates/agent_ui/src/completion_provider.rs 🔗

@@ -223,7 +223,7 @@ pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> {
     source: Arc<T>,
     editor: WeakEntity<Editor>,
     mention_set: Entity<MentionSet>,
-    history: WeakEntity<ThreadHistory>,
+    history: Option<WeakEntity<ThreadHistory>>,
     prompt_store: Option<Entity<PromptStore>>,
     workspace: WeakEntity<Workspace>,
 }
@@ -233,7 +233,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
         source: T,
         editor: WeakEntity<Editor>,
         mention_set: Entity<MentionSet>,
-        history: WeakEntity<ThreadHistory>,
+        history: Option<WeakEntity<ThreadHistory>>,
         prompt_store: Option<Entity<PromptStore>>,
         workspace: WeakEntity<Workspace>,
     ) -> Self {
@@ -920,7 +920,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
             }
 
             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<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
             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

crates/agent_ui/src/conversation_view.rs 🔗

@@ -421,7 +421,7 @@ pub struct ConnectedServerState {
     active_id: Option<acp::SessionId>,
     threads: HashMap<acp::SessionId, Entity<ThreadView>>,
     connection: Rc<dyn AgentConnection>,
-    history: Entity<ThreadHistory>,
+    history: Option<Entity<ThreadHistory>>,
     conversation: Entity<Conversation>,
     _connection_entry_subscription: Subscription,
 }
@@ -816,7 +816,7 @@ impl ConversationView {
         conversation: Entity<Conversation>,
         resumed_without_history: bool,
         initial_content: Option<AgentInitialContent>,
-        history: Entity<ThreadHistory>,
+        history: Option<Entity<ThreadHistory>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Entity<ThreadView> {
@@ -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<ThreadHistory>> {
-        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<Self>) {
@@ -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<dyn AgentSessionList> =
-            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<dyn AgentSessionList> =
             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!(

crates/agent_ui/src/conversation_view/thread_view.rs 🔗

@@ -228,8 +228,8 @@ pub struct ThreadView {
     pub hovered_recent_history_item: Option<usize>,
     pub show_external_source_prompt_warning: bool,
     pub show_codex_windows_warning: bool,
-    pub history: Entity<ThreadHistory>,
-    pub _history_subscription: Subscription,
+    pub history: Option<Entity<ThreadHistory>>,
+    pub _history_subscription: Option<Subscription>,
 }
 impl Focusable for ThreadView {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
@@ -273,7 +273,7 @@ impl ThreadView {
         resumed_without_history: bool,
         project: WeakEntity<Project>,
         thread_store: Option<Entity<ThreadStore>>,
-        history: Entity<ThreadHistory>,
+        history: Option<Entity<ThreadHistory>>,
         prompt_store: Option<Entity<PromptStore>>,
         initial_content: Option<AgentInitialContent>,
         mut subscriptions: Vec<Subscription>,
@@ -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()

crates/agent_ui/src/entry_view_state.rs 🔗

@@ -26,7 +26,7 @@ pub struct EntryViewState {
     workspace: WeakEntity<Workspace>,
     project: WeakEntity<Project>,
     thread_store: Option<Entity<ThreadStore>>,
-    history: WeakEntity<ThreadHistory>,
+    history: Option<WeakEntity<ThreadHistory>>,
     prompt_store: Option<Entity<PromptStore>>,
     entries: Vec<Entry>,
     prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
@@ -39,7 +39,7 @@ impl EntryViewState {
         workspace: WeakEntity<Workspace>,
         project: WeakEntity<Project>,
         thread_store: Option<Entity<ThreadStore>>,
-        history: WeakEntity<ThreadHistory>,
+        history: Option<WeakEntity<ThreadHistory>>,
         prompt_store: Option<Entity<PromptStore>>,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
@@ -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<gpui::WeakEntity<crate::ThreadHistory>> = None;
 
         let view_state = cx.new(|_cx| {
             EntryViewState::new(
                 workspace.downgrade(),
                 project.downgrade(),
                 thread_store,
-                history.downgrade(),
+                history,
                 None,
                 Default::default(),
                 Default::default(),

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<Workspace>| 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<Project>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
-        history: WeakEntity<ThreadHistory>,
+        history: Option<WeakEntity<ThreadHistory>>,
         initial_prompt: Option<String>,
         window: &mut Window,
         codegen_ranges: &[Range<Anchor>],
@@ -634,7 +630,7 @@ impl InlineAssistant {
         project: WeakEntity<Project>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
-        history: WeakEntity<ThreadHistory>,
+        history: Option<WeakEntity<ThreadHistory>>,
         initial_prompt: Option<String>,
         window: &mut Window,
         cx: &mut App,
@@ -679,7 +675,7 @@ impl InlineAssistant {
         workspace: Entity<Workspace>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
-        history: WeakEntity<ThreadHistory>,
+        history: Option<WeakEntity<ThreadHistory>>,
         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();

crates/agent_ui/src/inline_prompt_editor.rs 🔗

@@ -64,7 +64,7 @@ pub struct PromptEditor<T> {
     pub editor: Entity<Editor>,
     mode: PromptEditorMode,
     mention_set: Entity<MentionSet>,
-    history: WeakEntity<ThreadHistory>,
+    history: Option<WeakEntity<ThreadHistory>>,
     prompt_store: Option<Entity<PromptStore>>,
     workspace: WeakEntity<Workspace>,
     model_selector: Entity<AgentModelSelector>,
@@ -1227,7 +1227,7 @@ impl PromptEditor<BufferCodegen> {
         fs: Arc<dyn Fs>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
-        history: WeakEntity<ThreadHistory>,
+        history: Option<WeakEntity<ThreadHistory>>,
         project: WeakEntity<Project>,
         workspace: WeakEntity<Workspace>,
         window: &mut Window,
@@ -1386,7 +1386,7 @@ impl PromptEditor<TerminalCodegen> {
         fs: Arc<dyn Fs>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
-        history: WeakEntity<ThreadHistory>,
+        history: Option<WeakEntity<ThreadHistory>>,
         project: WeakEntity<Project>,
         workspace: WeakEntity<Workspace>,
         window: &mut Window,

crates/agent_ui/src/message_editor.rs 🔗

@@ -110,7 +110,7 @@ impl MessageEditor {
         workspace: WeakEntity<Workspace>,
         project: WeakEntity<Project>,
         thread_store: Option<Entity<ThreadStore>>,
-        history: WeakEntity<ThreadHistory>,
+        history: Option<WeakEntity<ThreadHistory>>,
         prompt_store: Option<Entity<PromptStore>>,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
@@ -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(),

crates/agent_ui/src/terminal_inline_assistant.rs 🔗

@@ -64,7 +64,7 @@ impl TerminalInlineAssistant {
         project: WeakEntity<Project>,
         thread_store: Entity<ThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
-        history: WeakEntity<ThreadHistory>,
+        history: Option<WeakEntity<ThreadHistory>>,
         initial_prompt: Option<String>,
         window: &mut Window,
         cx: &mut App,

crates/agent_ui/src/thread_history.rs 🔗

@@ -5,59 +5,48 @@ use std::rc::Rc;
 use ui::prelude::*;
 
 pub struct ThreadHistory {
-    session_list: Option<Rc<dyn AgentSessionList>>,
+    session_list: Rc<dyn AgentSessionList>,
     sessions: Vec<AgentSessionInfo>,
     _refresh_task: Task<()>,
     _watch_task: Option<Task<()>>,
 }
 
 impl ThreadHistory {
-    pub fn new(session_list: Option<Rc<dyn AgentSessionList>>, cx: &mut Context<Self>) -> Self {
+    pub fn new(session_list: Rc<dyn AgentSessionList>, cx: &mut Context<Self>) -> 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<Rc<dyn AgentSessionList>>,
+        session_list: Rc<dyn AgentSessionList>,
         cx: &mut Context<Self>,
     ) {
-        self.set_session_list_impl(session_list, cx);
-    }
-
-    fn set_session_list_impl(
-        &mut self,
-        session_list: Option<Rc<dyn AgentSessionList>>,
-        cx: &mut Context<Self>,
-    ) {
-        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<Self>) {
+        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<Self>) {
-        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<String> = 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<Self>) {
-        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<AgentSessionInfo> {
@@ -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<anyhow::Result<()>> {
-        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<anyhow::Result<()>> {
-        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 {

crates/agent_ui/src/threads_archive_view.rs 🔗

@@ -91,6 +91,22 @@ fn fuzzy_match_positions(query: &str, text: &str) -> Option<Vec<usize>> {
     }
 }
 
+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>) {
         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<ThreadHistory>, cx: &mut Context<Self>) {
-        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<Entity<ThreadHistory>>, cx: &mut Context<Self>) {
+        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<Self>) {
-        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<Self>) -> 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.")
+        );
+    }
+}