diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 2f86fb510cd0dd1726d024238bf98d990c39c601..6b5595a834b896ca0d8ee415970d5c0242e29874 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -214,12 +214,11 @@ pub fn init(cx: &mut App) { panel.update(cx, |panel, cx| panel.open_configuration(window, cx)); } }) - .register_action(|workspace, _action: &NewExternalAgentThread, window, cx| { + .register_action(|workspace, action: &NewExternalAgentThread, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { - let id = panel.create_thread("agent_panel", window, cx); - panel.activate_retained_thread(id, true, window, cx); + panel.new_external_agent_thread(action, window, cx); }); } }) @@ -1161,6 +1160,14 @@ impl AgentPanel { &self.connection_store } + pub fn selected_agent(&self, cx: &App) -> Agent { + if self.project.read(cx).is_via_collab() { + Agent::NativeAgent + } else { + self.selected_agent.clone() + } + } + pub fn open_thread( &mut self, session_id: acp::SessionId, @@ -1217,6 +1224,18 @@ impl AgentPanel { self.activate_draft(true, window, cx); } + pub fn new_external_agent_thread( + &mut self, + action: &NewExternalAgentThread, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(agent) = action.agent.clone() { + self.selected_agent = agent; + } + self.activate_draft(true, window, cx); + } + pub fn activate_draft(&mut self, focus: bool, window: &mut Window, cx: &mut Context) { let draft = self.ensure_draft(window, cx); if let BaseView::AgentThread { conversation_view } = &self.base_view { @@ -1242,33 +1261,22 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) -> Entity { - let desired_agent = if self.project.read(cx).is_via_collab() { - Agent::NativeAgent - } else { - self.selected_agent.clone() - }; + let desired_agent = self.selected_agent(cx); if let Some(draft) = &self.draft_thread { let agent_matches = *draft.read(cx).agent_key() == desired_agent; - let has_editor_content = draft.read(cx).root_thread_view().is_some_and(|tv| { - !tv.read(cx) - .message_editor - .read(cx) - .text(cx) - .trim() - .is_empty() - }); - if agent_matches || has_editor_content { + if agent_matches { return draft.clone(); } self.draft_thread = None; self._draft_editor_observation = None; } + let previous_content = self.active_initial_content(cx); let thread = self.create_agent_thread( desired_agent, None, None, None, - None, + previous_content, "agent_panel", window, cx, @@ -1308,11 +1316,7 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) -> ThreadId { - let agent = if self.project.read(cx).is_via_collab() { - Agent::NativeAgent - } else { - self.selected_agent.clone() - }; + let agent = self.selected_agent(cx); let thread = self.create_agent_thread(agent, None, None, None, None, source, window, cx); let thread_id = thread.conversation_view.read(cx).thread_id; self.retained_threads @@ -1414,36 +1418,6 @@ impl AgentPanel { }); } - fn take_active_initial_content( - &mut self, - cx: &mut Context, - ) -> Option { - self.active_thread_view(cx).and_then(|thread_view| { - thread_view.update(cx, |thread_view, cx| { - let draft_blocks = thread_view - .thread - .read(cx) - .draft_prompt() - .map(|draft| draft.to_vec()) - .filter(|draft| !draft.is_empty()); - - let draft_blocks = draft_blocks.or_else(|| { - let text = thread_view.message_editor.read(cx).text(cx); - if text.trim().is_empty() { - None - } else { - Some(vec![acp::ContentBlock::Text(acp::TextContent::new(text))]) - } - }); - - draft_blocks.map(|blocks| AgentInitialContent::ContentBlock { - blocks, - auto_submit: false, - }) - }) - }) - } - fn new_native_agent_thread_from_summary( &mut self, action: &NewNativeAgentThreadFromSummary, @@ -1501,13 +1475,7 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - let agent = agent_choice.unwrap_or_else(|| { - if self.project.read(cx).is_via_collab() { - Agent::NativeAgent - } else { - self.selected_agent.clone() - } - }); + let agent = agent_choice.unwrap_or_else(|| self.selected_agent(cx)); let thread = self.create_agent_thread( agent, resume_session_id, @@ -2423,18 +2391,17 @@ impl AgentPanel { let entry = entry.clone(); panel .update(cx, move |this, cx| { - if let Some(agent) = this.selected_agent() { - this.load_agent_thread( - agent, - entry.session_id.clone(), - entry.work_dirs.clone(), - entry.title.clone(), - true, - "agent_panel", - window, - cx, - ); - } + let agent = this.selected_agent(cx); + this.load_agent_thread( + agent, + entry.session_id.clone(), + entry.work_dirs.clone(), + entry.title.clone(), + true, + "agent_panel", + window, + cx, + ); }) .ok(); } @@ -2473,10 +2440,6 @@ impl AgentPanel { }) } - pub(crate) fn selected_agent(&self) -> Option { - Some(self.selected_agent.clone()) - } - fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context) { if let Some(extension_store) = ExtensionStore::try_global(cx) { let (manifests, extensions_dir) = { @@ -2521,31 +2484,6 @@ impl AgentPanel { ); } - pub fn new_agent_thread(&mut self, agent: Agent, window: &mut Window, cx: &mut Context) { - self.new_agent_thread_inner(agent, true, window, cx); - } - - fn new_agent_thread_inner( - &mut self, - agent: Agent, - focus: bool, - window: &mut Window, - cx: &mut Context, - ) { - let initial_content = self.take_active_initial_content(cx); - self.external_thread( - Some(agent), - None, - None, - None, - initial_content, - focus, - "agent_panel", - window, - cx, - ); - } - pub fn load_agent_thread( &mut self, agent: Agent, @@ -2980,11 +2918,6 @@ impl AgentPanel { return false; }; - let agent = if self.project.read(cx).is_via_collab() { - Agent::NativeAgent - } else { - agent - }; let thread = self.create_agent_thread( agent, None, @@ -3349,15 +3282,13 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.selected_agent = Agent::NativeAgent; - let id = panel.create_thread( - "agent_panel", + panel.new_external_agent_thread( + &NewExternalAgentThread { + agent: Some(Agent::NativeAgent), + }, window, cx, ); - panel.activate_retained_thread( - id, true, window, cx, - ); }); } }); @@ -3438,17 +3369,15 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.selected_agent = Agent::Custom { - id: agent_id.clone(), - }; - let id = panel.create_thread( - "agent_panel", + panel.new_external_agent_thread( + &NewExternalAgentThread { + agent: Some(Agent::Custom { + id: agent_id.clone(), + }), + }, window, cx, ); - panel.activate_retained_thread( - id, true, window, cx, - ); }); } }); @@ -5172,7 +5101,7 @@ mod tests { // Load thread A back via load_agent_thread — should promote from background. panel.update_in(&mut cx, |panel, window, cx| { panel.load_agent_thread( - panel.selected_agent().expect("selected agent must be set"), + panel.selected_agent(cx), session_id_a.clone(), None, None, @@ -5949,6 +5878,166 @@ mod tests { }); } + #[gpui::test] + async fn test_activate_draft_preserves_typed_content(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + cx.update(|cx| { + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + ::set_global(fs.clone(), cx); + }); + + let project = Project::test(fs.clone(), [], 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| { + let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx)); + workspace.add_panel(panel.clone(), window, cx); + panel + }); + + // Create a draft using the Stub agent, which connects synchronously. + panel.update_in(cx, |panel, window, cx| { + panel.selected_agent = Agent::Stub; + panel.activate_draft(true, window, cx); + }); + cx.run_until_parked(); + + let initial_draft_id = panel.read_with(cx, |panel, _cx| { + panel.draft_thread.as_ref().unwrap().entity_id() + }); + + // Type some text into the draft editor. + let thread_view = panel.read_with(cx, |panel, cx| panel.active_thread_view(cx).unwrap()); + let message_editor = thread_view.read_with(cx, |view, _cx| view.message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Don't lose me!", window, cx); + }); + + // Press cmd-n (activate_draft again with the same agent). + cx.dispatch_action(NewExternalAgentThread { agent: None }); + cx.run_until_parked(); + + // The draft entity should not have changed. + panel.read_with(cx, |panel, _cx| { + assert_eq!( + panel.draft_thread.as_ref().unwrap().entity_id(), + initial_draft_id, + "cmd-n should not replace the draft when already on it" + ); + }); + + // The editor content should be preserved. + let thread_id = panel.read_with(cx, |panel, cx| panel.active_thread_id(cx).unwrap()); + let text = panel.read_with(cx, |panel, cx| panel.editor_text(thread_id, cx)); + assert_eq!( + text.as_deref(), + Some("Don't lose me!"), + "typed content should be preserved when pressing cmd-n on the draft" + ); + } + + #[gpui::test] + async fn test_draft_content_carried_over_when_switching_agents(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + cx.update(|cx| { + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + ::set_global(fs.clone(), cx); + }); + + let project = Project::test(fs.clone(), [], 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| { + let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx)); + workspace.add_panel(panel.clone(), window, cx); + panel + }); + + // Create a draft with a custom stub server that connects synchronously. + panel.update_in(cx, |panel, window, cx| { + panel.open_draft_with_server( + Rc::new(StubAgentServer::new(StubAgentConnection::new())), + window, + cx, + ); + }); + cx.run_until_parked(); + + let initial_draft_id = panel.read_with(cx, |panel, _cx| { + panel.draft_thread.as_ref().unwrap().entity_id() + }); + + // Type text into the first draft's editor. + let thread_view = panel.read_with(cx, |panel, cx| panel.active_thread_view(cx).unwrap()); + let message_editor = thread_view.read_with(cx, |view, _cx| view.message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("carry me over", window, cx); + }); + + // Switch to a different agent. ensure_draft should extract the typed + // content from the old draft and pre-fill the new one. + cx.dispatch_action(NewExternalAgentThread { + agent: Some(Agent::Stub), + }); + cx.run_until_parked(); + + // A new draft should have been created for the Stub agent. + panel.read_with(cx, |panel, cx| { + let draft = panel.draft_thread.as_ref().expect("draft should exist"); + assert_ne!( + draft.entity_id(), + initial_draft_id, + "a new draft should have been created for the new agent" + ); + assert_eq!( + *draft.read(cx).agent_key(), + Agent::Stub, + "new draft should use the new agent" + ); + }); + + // The new draft's editor should contain the text typed in the old draft. + let thread_id = panel.read_with(cx, |panel, cx| panel.active_thread_id(cx).unwrap()); + let text = panel.read_with(cx, |panel, cx| panel.editor_text(thread_id, cx)); + assert_eq!( + text.as_deref(), + Some("carry me over"), + "content should be carried over to the new agent's draft" + ); + } + #[gpui::test] async fn test_rollback_all_succeed_returns_ok(cx: &mut TestAppContext) { init_test(cx);