agent_ui: Skip restoring external threads without metadata (#52978)

Ben Brandt created

Make sure we don't try and load a thread we don't have metadata for and
therefore is unlikely to exist

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

Change summary

crates/agent_ui/src/agent_panel.rs | 104 +++++++++++++++++++++++++------
1 file changed, 83 insertions(+), 21 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -25,6 +25,7 @@ use zed_actions::agent::{
     ResolveConflictsWithAgent, ReviewBranchDiff,
 };
 
+use crate::thread_metadata_store::ThreadMetadataStore;
 use crate::{
     AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, CycleStartThreadIn,
     Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown,
@@ -753,28 +754,21 @@ impl AgentPanel {
                 .as_ref()
                 .and_then(|p| p.last_active_thread.as_ref())
             {
-                if thread_info.agent_type.is_native() {
-                    let session_id = acp::SessionId::new(thread_info.session_id.clone());
-                    let load_result = cx.update(|_window, cx| {
-                        let thread_store = ThreadStore::global(cx);
-                        thread_store.update(cx, |store, cx| store.load_thread(session_id, cx))
-                    });
-                    let thread_exists = if let Ok(task) = load_result {
-                        task.await.ok().flatten().is_some()
-                    } else {
-                        false
-                    };
-                    if thread_exists {
-                        Some(thread_info)
-                    } else {
-                        log::warn!(
-                            "last active thread {} not found in database, skipping restoration",
-                            thread_info.session_id
-                        );
-                        None
-                    }
-                } else {
+                let session_id = acp::SessionId::new(thread_info.session_id.clone());
+                let has_metadata = cx
+                    .update(|_window, cx| {
+                        let store = ThreadMetadataStore::global(cx);
+                        store.read(cx).entry(&session_id).is_some()
+                    })
+                    .unwrap_or(false);
+                if has_metadata {
                     Some(thread_info)
+                } else {
+                    log::warn!(
+                        "last active thread {} has no metadata, skipping restoration",
+                        thread_info.session_id
+                    );
+                    None
                 }
             } else {
                 None
@@ -4305,6 +4299,8 @@ mod tests {
             );
         });
 
+        send_message(&panel_a, cx);
+
         let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
 
         // --- Set up workspace B: ClaudeCode, no active thread ---
@@ -4364,6 +4360,72 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_non_native_thread_without_metadata_is_not_restored(cx: &mut TestAppContext) {
+        init_test(cx);
+        cx.update(|cx| {
+            cx.update_flags(true, vec!["agent-v2".to_string()]);
+            agent::ThreadStore::init_global(cx);
+            language_model::LanguageModelRegistry::test(cx);
+        });
+
+        let fs = FakeFs::new(cx.executor());
+        let project = Project::test(fs, [], cx).await;
+
+        let multi_workspace =
+            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+
+        let workspace = multi_workspace
+            .read_with(cx, |multi_workspace, _cx| {
+                multi_workspace.workspace().clone()
+            })
+            .unwrap();
+
+        workspace.update(cx, |workspace, _cx| {
+            workspace.set_random_database_id();
+        });
+
+        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
+
+        let panel = workspace.update_in(cx, |workspace, window, cx| {
+            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
+        });
+
+        panel.update_in(cx, |panel, window, cx| {
+            panel.open_external_thread_with_server(
+                Rc::new(StubAgentServer::default_response()),
+                window,
+                cx,
+            );
+        });
+
+        cx.run_until_parked();
+
+        panel.read_with(cx, |panel, cx| {
+            assert!(
+                panel.active_agent_thread(cx).is_some(),
+                "should have an active thread after connection"
+            );
+        });
+
+        // Serialize without ever sending a message, so no thread metadata exists.
+        panel.update(cx, |panel, cx| panel.serialize(cx));
+        cx.run_until_parked();
+
+        let async_cx = cx.update(|window, cx| window.to_async(cx));
+        let loaded = AgentPanel::load(workspace.downgrade(), async_cx)
+            .await
+            .expect("panel load should succeed");
+        cx.run_until_parked();
+
+        loaded.read_with(cx, |panel, _cx| {
+            assert!(
+                panel.active_conversation_view().is_none(),
+                "thread without metadata should not be restored"
+            );
+        });
+    }
+
     /// Extracts the text from a Text content block, panicking if it's not Text.
     fn expect_text_block(block: &acp::ContentBlock) -> &str {
         match block {