diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 60054a0c52869889aa02ffebd8d7c8c5f46f553f..63cfd455a7540be5650ae80056cc5366b5350468 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1303,6 +1303,10 @@ impl AcpThread { &self.session_id } + pub fn supports_truncate(&self, cx: &App) -> bool { + self.connection.truncate(&self.session_id, cx).is_some() + } + pub fn work_dirs(&self) -> Option<&PathList> { self.work_dirs.as_ref() } @@ -4362,6 +4366,7 @@ mod tests { #[derive(Clone, Default)] struct FakeAgentConnection { auth_methods: Vec, + supports_truncate: bool, sessions: Arc>>>, set_title_calls: Rc>>, on_user_message: Option< @@ -4380,12 +4385,18 @@ mod tests { fn new() -> Self { Self { auth_methods: Vec::new(), + supports_truncate: true, on_user_message: None, sessions: Arc::default(), set_title_calls: Default::default(), } } + fn without_truncate_support(mut self) -> Self { + self.supports_truncate = false; + self + } + #[expect(unused)] fn with_auth_methods(mut self, auth_methods: Vec) -> Self { self.auth_methods = auth_methods; @@ -4487,9 +4498,11 @@ mod tests { session_id: &acp::SessionId, _cx: &App, ) -> Option> { - Some(Rc::new(FakeAgentSessionEditor { - _session_id: session_id.clone(), - })) + self.supports_truncate.then(|| { + Rc::new(FakeAgentSessionEditor { + _session_id: session_id.clone(), + }) as Rc + }) } fn set_title( @@ -5044,6 +5057,37 @@ mod tests { ); } + #[gpui::test] + async fn test_send_assigns_message_id_without_truncate_support(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + + let connection = Rc::new(FakeAgentConnection::new().without_truncate_support()); + let thread = cx + .update(|cx| { + connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx) + }) + .await + .unwrap(); + + let response = thread + .update(cx, |thread, cx| thread.send_raw("test message", cx)) + .await; + + assert!(response.is_ok(), "send should not fail: {response:?}"); + thread.read_with(cx, |thread, _| { + let AgentThreadEntry::UserMessage(message) = &thread.entries[0] else { + panic!("expected first entry to be a user message") + }; + assert!( + message.id.is_some(), + "user message should always have an id" + ); + }); + } + #[gpui::test] async fn test_send_returns_cancelled_response_and_marks_tools_as_cancelled( cx: &mut TestAppContext, diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 2bcc8e4b82e5b2410b28b7e315bba5b1fa8876ed..d40749a0a2302e4851f4b3c4e4a91563d86a4ef8 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -1972,12 +1972,7 @@ impl ConversationView { .read(cx) .entries() .iter() - .any(|entry| { - matches!( - entry, - AgentThreadEntry::UserMessage(user_message) if user_message.id.is_some() - ) - }) + .any(|entry| matches!(entry, AgentThreadEntry::UserMessage(_))) }) } diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index bd0cfb13accd3b9c9fb9d634ce898b463abf9c6e..9a1c805d64059cf357d57bd680a04033f3534abf 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -754,6 +754,7 @@ impl ThreadView { ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { if let Some(AgentThreadEntry::UserMessage(user_message)) = self.thread.read(cx).entries().get(event.entry_index) + && self.thread.read(cx).supports_truncate(cx) && user_message.id.is_some() && !self.is_subagent() { @@ -764,6 +765,7 @@ impl ThreadView { ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => { if let Some(AgentThreadEntry::UserMessage(user_message)) = self.thread.read(cx).entries().get(event.entry_index) + && self.thread.read(cx).supports_truncate(cx) && user_message.id.is_some() && !self.is_subagent() { @@ -4495,7 +4497,8 @@ impl ThreadView { .is_some_and(|checkpoint| checkpoint.show); let is_subagent = self.is_subagent(); - let is_editable = message.id.is_some() && !is_subagent; + let can_rewind = self.thread.read(cx).supports_truncate(cx); + let is_editable = can_rewind && message.id.is_some() && !is_subagent; let agent_name = if is_subagent { "subagents".into() } else { diff --git a/crates/agent_ui/src/entry_view_state.rs b/crates/agent_ui/src/entry_view_state.rs index eeaf8f6935a2294d8d9a1fe71b8d8acd62ee43a2..071b92eb3ae6b68548539e58b16949d7c72bab80 100644 --- a/crates/agent_ui/src/entry_view_state.rs +++ b/crates/agent_ui/src/entry_view_state.rs @@ -72,6 +72,7 @@ impl EntryViewState { match thread_entry { AgentThreadEntry::UserMessage(message) => { + let can_rewind = thread.read(cx).supports_truncate(cx); let has_id = message.id.is_some(); let is_subagent = thread.read(cx).parent_session_id().is_some(); let chunks = message.chunks.clone(); @@ -101,7 +102,7 @@ impl EntryViewState { window, cx, ); - if !has_id || is_subagent { + if !can_rewind || !has_id || is_subagent { editor.set_read_only(true, cx); } editor.set_message(chunks, window, cx);