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

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
This commit is contained in:
Ben Brandt authored and GitHub committed 2026-04-02 12:03:40 +02:00
1 parent f31bfc9173
commit 3dec65d9a9
1 file changed
+83 -21
+83 -21
View File
@@ -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 {