Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:24:07.677Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

acp_thread.rs

10209 lines · 372.1 KB · rust
1mod connection;
2mod diff;
3mod mention;
4mod terminal;
5pub use ::terminal::HeadlessTerminal;
6use action_log::{ActionLog, ActionLogTelemetry};
7use agent_client_protocol::schema::{MaybeUndefined, v1 as acp};
8use anyhow::{Context as _, Result, anyhow};
9use collections::HashSet;
10pub use connection::*;
11pub use diff::*;
12use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
13use futures::{FutureExt, channel::oneshot, future::BoxFuture};
14use gpui::{
15    AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
16    WeakEntity,
17};
18use itertools::Itertools;
19use language::language_settings::FormatOnSave;
20use language::{
21    Anchor, Buffer, BufferEditSource, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff,
22};
23use markdown::{Markdown, MarkdownOptions};
24pub use mention::*;
25use project::lsp_store::{FormatTrigger, LspFormatTarget};
26use project::{
27    AgentLocation, Project,
28    git_store::{GitStoreCheckpoint, GitStoreEvent, RepositoryEvent},
29};
30use serde::{Deserialize, Serialize};
31use serde_json::to_string_pretty;
32use std::collections::HashMap;
33use std::error::Error;
34use std::fmt::{Formatter, Write};
35use std::ops::Range;
36use std::process::ExitStatus;
37use std::rc::Rc;
38use std::time::{Duration, Instant};
39use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
40use task::{Shell, ShellBuilder};
41pub use terminal::*;
42use text::Bias;
43use ui::App;
44use util::markdown::MarkdownEscaped;
45use util::path_list::PathList;
46use util::{
47    ResultExt, get_default_system_shell_preferring_bash,
48    paths::{PathStyle, is_absolute},
49};
50use uuid::Uuid;
51
52/// Returned when the model stops because it exhausted its output token budget.
53#[derive(Debug)]
54pub struct MaxOutputTokensError;
55
56impl std::fmt::Display for MaxOutputTokensError {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "output token limit reached")
59    }
60}
61
62impl std::error::Error for MaxOutputTokensError {}
63
64/// Key used in ACP ToolCall meta to store the tool's programmatic name.
65/// This is a workaround since ACP's ToolCall doesn't have a dedicated name field.
66pub const TOOL_NAME_META_KEY: &str = "tool_name";
67
68/// Helper to extract tool name from ACP meta
69pub fn tool_name_from_meta(meta: &Option<acp::Meta>) -> Option<SharedString> {
70    meta.as_ref()
71        .and_then(|m| m.get(TOOL_NAME_META_KEY))
72        .and_then(|v| v.as_str())
73        .map(|s| SharedString::from(s.to_owned()))
74}
75
76/// Helper to create meta with tool name
77pub fn meta_with_tool_name(tool_name: &str) -> acp::Meta {
78    acp::Meta::from_iter([(TOOL_NAME_META_KEY.into(), tool_name.into())])
79}
80
81/// Key used in ACP `AvailableCommand` meta to record which source produced a
82/// slash command, so the completion popup can group commands by category.
83pub const COMMAND_CATEGORY_META_KEY: &str = "command_category";
84
85/// The source category of a slash command, used to group commands in the
86/// completion popup. Only the native Omega Agent executor annotates its commands; commands
87/// from external ACP agents carry no category and are grouped on their own.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum CommandCategory {
90    /// Built-in Omega Agent commands (e.g. `/compact`).
91    Native,
92    /// Commands sourced from MCP server prompts.
93    Mcp,
94}
95
96impl CommandCategory {
97    fn as_str(self) -> &'static str {
98        match self {
99            Self::Native => "native",
100            Self::Mcp => "mcp",
101        }
102    }
103
104    fn from_str(value: &str) -> Option<Self> {
105        match value {
106            "native" => Some(Self::Native),
107            "mcp" => Some(Self::Mcp),
108            _ => None,
109        }
110    }
111}
112
113pub fn meta_with_command_category(category: CommandCategory) -> acp::Meta {
114    acp::Meta::from_iter([(COMMAND_CATEGORY_META_KEY.into(), category.as_str().into())])
115}
116
117pub fn command_category_from_meta(meta: &Option<acp::Meta>) -> Option<CommandCategory> {
118    meta.as_ref()
119        .and_then(|m| m.get(COMMAND_CATEGORY_META_KEY))
120        .and_then(|v| v.as_str())
121        .and_then(CommandCategory::from_str)
122}
123
124/// Key used in ACP ToolCall meta to store the session id and message indexes
125pub const SUBAGENT_SESSION_INFO_META_KEY: &str = "subagent_session_info";
126
127pub const SANDBOX_AUTHORIZATION_META_KEY: &str = "sandbox_authorization";
128
129/// Stable `PermissionOption` ids for the sandbox-escalation approval prompt.
130///
131/// These are shared across the option construction (in the agent), the outcome
132/// dispatch, and the UI so the distinct grant lifetimes stay in sync. Note
133/// that `AllowThread` and `AllowAlways` both use
134/// `PermissionOptionKind::AllowAlways`; the id is what distinguishes them.
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub enum SandboxPermission {
137    AllowOnce,
138    AllowThread,
139    AllowAlways,
140    Deny,
141}
142
143impl SandboxPermission {
144    pub fn as_id(self) -> &'static str {
145        match self {
146            Self::AllowOnce => "allow",
147            Self::AllowThread => "allow_thread",
148            Self::AllowAlways => "allow_always",
149            Self::Deny => "deny",
150        }
151    }
152
153    pub fn from_id(id: &str) -> Option<Self> {
154        match id {
155            "allow" => Some(Self::AllowOnce),
156            "allow_thread" => Some(Self::AllowThread),
157            "allow_always" => Some(Self::AllowAlways),
158            "deny" => Some(Self::Deny),
159            _ => None,
160        }
161    }
162}
163
164#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
165pub struct SandboxAuthorizationDetails {
166    #[serde(default)]
167    pub command: Option<String>,
168    /// Specific hosts the command requested network access to, in canonical
169    /// form (`github.com`, `*.npmjs.org`). Empty when no specific hosts were
170    /// requested (see `network_all_hosts`).
171    #[serde(default)]
172    pub network_hosts: Vec<String>,
173    /// Whether the command requested access to any host ("arbitrary network
174    /// access"). The `network` alias deserializes the field this replaced —
175    /// a plain bool meaning "network access" — so details persisted by older
176    /// builds still render the network request.
177    #[serde(default, alias = "network")]
178    pub network_all_hosts: bool,
179
180    #[serde(default)]
181    pub allow_fs_write_all: bool,
182    #[serde(default)]
183    pub unsandboxed: bool,
184    #[serde(default)]
185    pub write_paths: Vec<PathBuf>,
186    /// The agent-provided justification for requesting these permissions,
187    /// shown to the user (attributed to the agent) in the approval prompt.
188    #[serde(default)]
189    pub reason: String,
190}
191
192pub fn meta_with_sandbox_authorization(details: SandboxAuthorizationDetails) -> acp::Meta {
193    acp::Meta::from_iter([(
194        SANDBOX_AUTHORIZATION_META_KEY.into(),
195        serde_json::to_value(details).unwrap_or_default(),
196    )])
197}
198
199pub fn sandbox_authorization_details_from_meta(
200    meta: &Option<acp::Meta>,
201) -> Option<SandboxAuthorizationDetails> {
202    meta.as_ref()
203        .and_then(|m| m.get(SANDBOX_AUTHORIZATION_META_KEY))
204        .and_then(|v| serde_json::from_value(v.clone()).ok())
205}
206
207pub const SANDBOX_FALLBACK_AUTHORIZATION_META_KEY: &str = "sandbox_fallback_authorization";
208
209/// Stable `PermissionOption` id for the "Retry" choice in the sandbox
210/// *fallback* prompt (shown when the OS sandbox can't be created on this
211/// system). The remaining choices reuse the [`SandboxPermission`] ids.
212pub const SANDBOX_FALLBACK_RETRY_OPTION_ID: &str = "retry";
213
214/// Details shown when the OS sandbox could not be created for a command and
215/// the user is asked whether to run it without a sandbox. Distinct from
216/// [`SandboxAuthorizationDetails`] (a model-requested *escalation*): here the
217/// sandbox itself failed, so the prompt explains why and offers a retry.
218#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
219pub struct SandboxFallbackAuthorizationDetails {
220    #[serde(default)]
221    pub command: Option<String>,
222    /// Human-readable reason the OS sandbox could not be created (for example,
223    /// "bwrap not found on PATH"), shown to the user so they can decide
224    /// whether to run the command without a sandbox.
225    #[serde(default)]
226    pub reason: String,
227    /// Slug of the sandboxing docs section that best explains how to fix this
228    /// failure (see [`crate::LinuxWslSandboxError::docs_section`]), rendered as a
229    /// "Learn more" link. `None` when the cause is unknown.
230    #[serde(default)]
231    pub docs_section: Option<String>,
232}
233
234pub fn meta_with_sandbox_fallback_authorization(
235    details: SandboxFallbackAuthorizationDetails,
236) -> acp::Meta {
237    acp::Meta::from_iter([(
238        SANDBOX_FALLBACK_AUTHORIZATION_META_KEY.into(),
239        serde_json::to_value(details).unwrap_or_default(),
240    )])
241}
242
243pub fn sandbox_fallback_authorization_details_from_meta(
244    meta: &Option<acp::Meta>,
245) -> Option<SandboxFallbackAuthorizationDetails> {
246    meta.as_ref()
247        .and_then(|m| m.get(SANDBOX_FALLBACK_AUTHORIZATION_META_KEY))
248        .and_then(|v| serde_json::from_value(v.clone()).ok())
249}
250
251/// Meta key recording why the OS sandbox was not applied to a terminal tool
252/// call, even though sandboxing was active for the thread. The value is a
253/// serialized [`SandboxNotAppliedReason`]. Surfaced as a warning in the UI and
254/// used to explain the situation to both the user and the agent.
255pub const SANDBOX_NOT_APPLIED_META_KEY: &str = "sandbox_not_applied";
256
257pub fn meta_with_sandbox_not_applied(reason: &SandboxNotAppliedReason) -> acp::Meta {
258    acp::Meta::from_iter([(
259        SANDBOX_NOT_APPLIED_META_KEY.into(),
260        serde_json::to_value(reason).unwrap_or_default(),
261    )])
262}
263
264pub fn sandbox_not_applied_from_meta(meta: &Option<acp::Meta>) -> Option<SandboxNotAppliedReason> {
265    meta.as_ref()
266        .and_then(|m| m.get(SANDBOX_NOT_APPLIED_META_KEY))
267        .and_then(|v| serde_json::from_value(v.clone()).ok())
268}
269
270#[derive(Clone, Debug, Deserialize, Serialize)]
271pub struct SubagentSessionInfo {
272    /// The session id of the subagent sessiont that was spawned
273    pub session_id: acp::SessionId,
274    /// The index of the message of the start of the "turn" run by this tool call
275    pub message_start_index: usize,
276    /// The index of the output of the message that the subagent has returned
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub message_end_index: Option<usize>,
279}
280
281/// Helper to extract subagent session id from ACP meta
282pub fn subagent_session_info_from_meta(meta: &Option<acp::Meta>) -> Option<SubagentSessionInfo> {
283    meta.as_ref()
284        .and_then(|m| m.get(SUBAGENT_SESSION_INFO_META_KEY))
285        .and_then(|v| serde_json::from_value(v.clone()).ok())
286}
287
288#[derive(Debug)]
289pub struct UserMessage {
290    pub protocol_id: Option<acp::MessageId>,
291    pub client_id: Option<ClientUserMessageId>,
292    pub is_optimistic: bool,
293    pub content: ContentBlock,
294    pub chunks: Vec<acp::ContentBlock>,
295    pub checkpoint: Option<Checkpoint>,
296    pub indented: bool,
297}
298
299#[derive(Debug)]
300pub struct Checkpoint {
301    git_checkpoint: GitStoreCheckpoint,
302    pub show: bool,
303}
304
305impl UserMessage {
306    fn to_markdown(&self, cx: &App) -> String {
307        let mut markdown = String::new();
308        if self
309            .checkpoint
310            .as_ref()
311            .is_some_and(|checkpoint| checkpoint.show)
312        {
313            writeln!(markdown, "## User (checkpoint)").unwrap();
314        } else {
315            writeln!(markdown, "## User").unwrap();
316        }
317        writeln!(markdown).unwrap();
318        writeln!(markdown, "{}", self.content.to_markdown(cx)).unwrap();
319        writeln!(markdown).unwrap();
320        markdown
321    }
322}
323
324#[derive(Debug, PartialEq)]
325pub struct AssistantMessage {
326    pub chunks: Vec<AssistantMessageChunk>,
327    pub indented: bool,
328    pub is_subagent_output: bool,
329}
330
331impl AssistantMessage {
332    pub fn to_markdown(&self, cx: &App) -> String {
333        format!(
334            "## Assistant\n\n{}\n\n",
335            self.chunks
336                .iter()
337                .map(|chunk| chunk.to_markdown(cx))
338                .join("\n\n")
339        )
340    }
341}
342
343#[derive(Debug, PartialEq)]
344pub enum AssistantMessageChunk {
345    Message {
346        id: Option<acp::MessageId>,
347        block: ContentBlock,
348    },
349    Thought {
350        id: Option<acp::MessageId>,
351        block: ContentBlock,
352    },
353}
354
355impl AssistantMessageChunk {
356    pub fn from_str(
357        chunk: &str,
358        language_registry: &Arc<LanguageRegistry>,
359        path_style: PathStyle,
360        cx: &mut App,
361    ) -> Self {
362        Self::Message {
363            id: None,
364            block: ContentBlock::new(chunk.into(), language_registry, path_style, cx),
365        }
366    }
367
368    fn to_markdown(&self, cx: &App) -> String {
369        match self {
370            Self::Message { block, .. } => block.to_markdown(cx).to_string(),
371            Self::Thought { block, .. } => {
372                format!("<thinking>\n{}\n</thinking>", block.to_markdown(cx))
373            }
374        }
375    }
376}
377
378fn can_merge_message_chunks(
379    existing: Option<&acp::MessageId>,
380    incoming: Option<&acp::MessageId>,
381) -> bool {
382    match (existing, incoming) {
383        (Some(existing), Some(incoming)) => existing == incoming,
384        _ => true,
385    }
386}
387
388#[derive(Debug)]
389pub enum AgentThreadEntry {
390    UserMessage(UserMessage),
391    AssistantMessage(AssistantMessage),
392    ToolCall(ToolCall),
393    Elicitation(ElicitationEntryId),
394    CompletedPlan(Vec<PlanEntry>),
395    ContextCompaction(ContextCompaction),
396    /// OMEGA-DELTA-0045. A note the supervising host wrote into the thread so
397    /// the owner reading it can see something that happened *to* the run
398    /// rather than inside a model turn.
399    SystemNote(SystemNote),
400}
401
402/// OMEGA-DELTA-0045. Identity of a host-authored system note.
403///
404/// The engine supplies it, so a retried `append_system_note` — after a host
405/// restart, or after a response the engine never saw — lands on the note that
406/// is already there instead of writing a second copy of the same disclosure.
407#[derive(Debug, Clone, PartialEq, Eq, Hash)]
408pub struct SystemNoteId(pub Arc<str>);
409
410/// OMEGA-DELTA-0045. A host-authored line in the thread.
411///
412/// This exists because of the rc11 defect: a cross-provider handoff changed
413/// which model was spending the owner's budget and left nothing in the thread
414/// the owner reads. `AgentThreadEntry` had no variant an owner-visible,
415/// non-model disclosure could be, so the host had nowhere to put one and
416/// refused instead. The refusal was honest; the missing seam was the defect.
417///
418/// The text is plain, not Markdown, and not model output: it is written by the
419/// host and rendered as-is, so nothing a provider emits can style, hide, or
420/// impersonate it.
421#[derive(Debug, Clone)]
422pub struct SystemNote {
423    pub id: SystemNoteId,
424    pub text: SharedString,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Hash)]
428pub struct ElicitationEntryId(pub Arc<str>);
429
430#[derive(Debug)]
431pub struct Elicitation {
432    pub id: ElicitationEntryId,
433    pub request: acp::CreateElicitationRequest,
434    pub status: ElicitationStatus,
435}
436
437#[derive(Debug)]
438pub enum ElicitationStatus {
439    Pending {
440        respond_tx: oneshot::Sender<acp::CreateElicitationResponse>,
441    },
442    Accepted,
443    Declined,
444    Canceled,
445    Completed,
446}
447
448#[derive(Clone, Debug)]
449pub enum ElicitationStoreEvent {
450    ElicitationRequested(ElicitationEntryId),
451    ElicitationResponded(ElicitationEntryId),
452    ElicitationUpdated(ElicitationEntryId),
453}
454
455#[derive(Default)]
456pub struct ElicitationStore {
457    elicitations: Vec<Elicitation>,
458}
459
460impl EventEmitter<ElicitationStoreEvent> for ElicitationStore {}
461
462impl ElicitationStore {
463    pub fn elicitations(&self) -> &[Elicitation] {
464        &self.elicitations
465    }
466
467    fn validate_request(request: &acp::CreateElicitationRequest) -> Result<(), acp::Error> {
468        if let acp::ElicitationMode::Url(mode) = &request.mode {
469            url::Url::parse(&mode.url)
470                .map_err(|_| acp::Error::invalid_params().data("invalid elicitation URL"))?;
471        }
472
473        Ok(())
474    }
475
476    fn insert_pending_elicitation(
477        &mut self,
478        request: acp::CreateElicitationRequest,
479    ) -> (
480        ElicitationEntryId,
481        oneshot::Receiver<acp::CreateElicitationResponse>,
482    ) {
483        let (respond_tx, response_rx) = oneshot::channel();
484        let id = ElicitationEntryId(Uuid::new_v4().to_string().into());
485        self.elicitations.push(Elicitation {
486            id: id.clone(),
487            request,
488            status: ElicitationStatus::Pending { respond_tx },
489        });
490        (id, response_rx)
491    }
492
493    fn response_task<T>(
494        id: ElicitationEntryId,
495        response_rx: oneshot::Receiver<acp::CreateElicitationResponse>,
496        cx: &mut Context<T>,
497        emit_responded: impl FnOnce(&mut T, &mut Context<T>, ElicitationEntryId) + 'static,
498    ) -> Task<acp::CreateElicitationResponse>
499    where
500        T: 'static,
501    {
502        cx.spawn(async move |this, cx| {
503            let response = response_rx.await.unwrap_or_else(|oneshot::Canceled| {
504                acp::CreateElicitationResponse::new(acp::ElicitationAction::Cancel)
505            });
506            this.update(cx, |this, cx| emit_responded(this, cx, id))
507                .ok();
508            response
509        })
510    }
511
512    fn respond_to_elicitation_entry(
513        elicitation: &mut Elicitation,
514        response: acp::CreateElicitationResponse,
515    ) -> bool {
516        if !matches!(elicitation.status, ElicitationStatus::Pending { .. }) {
517            return false;
518        }
519        let ElicitationStatus::Pending { respond_tx } = mem::replace(
520            &mut elicitation.status,
521            elicitation_status_for_response(&response),
522        ) else {
523            return false;
524        };
525        respond_tx.send(response).ok();
526        true
527    }
528
529    fn complete_url_elicitation_entry(elicitation: &mut Elicitation) -> bool {
530        let previous_status = mem::replace(&mut elicitation.status, ElicitationStatus::Completed);
531        match previous_status {
532            ElicitationStatus::Pending { respond_tx } => {
533                respond_tx
534                    .send(acp::CreateElicitationResponse::new(
535                        acp::ElicitationAction::Accept(acp::ElicitationAcceptAction::new()),
536                    ))
537                    .ok();
538                true
539            }
540            ElicitationStatus::Accepted => true,
541            ElicitationStatus::Completed => false,
542            previous_status @ (ElicitationStatus::Declined | ElicitationStatus::Canceled) => {
543                elicitation.status = previous_status;
544                false
545            }
546        }
547    }
548
549    fn cancel_elicitation_entry(
550        elicitation: &mut Elicitation,
551        cancel_accepted_url_elicitations: bool,
552    ) -> bool {
553        match mem::replace(&mut elicitation.status, ElicitationStatus::Canceled) {
554            ElicitationStatus::Pending { respond_tx } => {
555                respond_tx
556                    .send(acp::CreateElicitationResponse::new(
557                        acp::ElicitationAction::Cancel,
558                    ))
559                    .ok();
560                true
561            }
562            ElicitationStatus::Accepted
563                if cancel_accepted_url_elicitations
564                    && matches!(&elicitation.request.mode, acp::ElicitationMode::Url(_)) =>
565            {
566                true
567            }
568            previous_status => {
569                elicitation.status = previous_status;
570                false
571            }
572        }
573    }
574
575    fn respond_to_elicitation_by_id(
576        &mut self,
577        id: &ElicitationEntryId,
578        response: acp::CreateElicitationResponse,
579    ) -> bool {
580        let Some((_, elicitation)) = self.elicitation_mut(id) else {
581            return false;
582        };
583        Self::respond_to_elicitation_entry(elicitation, response)
584    }
585
586    fn complete_url_elicitation_by_id(&mut self, id: &ElicitationEntryId) -> bool {
587        let Some((_, elicitation)) = self.elicitation_mut(id) else {
588            return false;
589        };
590        Self::complete_url_elicitation_entry(elicitation)
591    }
592
593    fn cancel_elicitation_by_id(
594        &mut self,
595        id: &ElicitationEntryId,
596        cancel_accepted_url_elicitations: bool,
597    ) -> bool {
598        let Some((_, elicitation)) = self.elicitation_mut(id) else {
599            return false;
600        };
601        Self::cancel_elicitation_entry(elicitation, cancel_accepted_url_elicitations)
602    }
603
604    pub fn request_elicitation(
605        &mut self,
606        request: acp::CreateElicitationRequest,
607        cx: &mut Context<Self>,
608    ) -> Result<Task<acp::CreateElicitationResponse>, acp::Error> {
609        self.request_elicitation_with_id(request, cx)
610            .map(|(_, task)| task)
611    }
612
613    pub fn request_elicitation_with_id(
614        &mut self,
615        request: acp::CreateElicitationRequest,
616        cx: &mut Context<Self>,
617    ) -> Result<(ElicitationEntryId, Task<acp::CreateElicitationResponse>), acp::Error> {
618        Self::validate_request(&request)?;
619        let (id, response_rx) = self.insert_pending_elicitation(request);
620        cx.emit(ElicitationStoreEvent::ElicitationRequested(id.clone()));
621        cx.notify();
622
623        let task = Self::response_task(id.clone(), response_rx, cx, |_store, cx, id| {
624            cx.emit(ElicitationStoreEvent::ElicitationResponded(id));
625            cx.notify();
626        });
627
628        Ok((id, task))
629    }
630
631    pub fn respond_to_elicitation(
632        &mut self,
633        id: &ElicitationEntryId,
634        response: acp::CreateElicitationResponse,
635        cx: &mut Context<Self>,
636    ) {
637        if !self.respond_to_elicitation_by_id(id, response) {
638            return;
639        }
640
641        cx.emit(ElicitationStoreEvent::ElicitationUpdated(id.clone()));
642        cx.notify();
643    }
644
645    pub fn complete_url_elicitation(
646        &mut self,
647        elicitation_id: &acp::ElicitationId,
648        cx: &mut Context<Self>,
649    ) {
650        let Some(entry_id) = self.entry_id_for_url_elicitation(elicitation_id) else {
651            return;
652        };
653        if !self.complete_url_elicitation_by_id(&entry_id) {
654            return;
655        }
656
657        cx.emit(ElicitationStoreEvent::ElicitationUpdated(entry_id));
658        cx.notify();
659    }
660
661    pub fn cancel_elicitation(&mut self, id: &ElicitationEntryId, cx: &mut Context<Self>) {
662        if !self.cancel_elicitation_by_id(id, true) {
663            return;
664        }
665
666        cx.emit(ElicitationStoreEvent::ElicitationUpdated(id.clone()));
667        cx.notify();
668    }
669
670    pub fn cancel_all(&mut self, cx: &mut Context<Self>) {
671        let canceled_ids = self.cancel_pending(|_| true);
672        for id in canceled_ids {
673            cx.emit(ElicitationStoreEvent::ElicitationUpdated(id));
674        }
675        cx.notify();
676    }
677
678    pub fn clear(&mut self, cx: &mut Context<Self>) {
679        let canceled_ids = self.cancel_pending(|_| true);
680        self.elicitations.clear();
681        for id in canceled_ids {
682            cx.emit(ElicitationStoreEvent::ElicitationUpdated(id));
683        }
684        cx.notify();
685    }
686
687    pub fn clear_resolved(&mut self, cx: &mut Context<Self>) -> Vec<ElicitationEntryId> {
688        let mut cleared_ids = Vec::new();
689        self.elicitations.retain(|elicitation| {
690            let keep = matches!(
691                (&elicitation.status, &elicitation.request.mode),
692                (ElicitationStatus::Pending { .. }, _)
693                    | (ElicitationStatus::Accepted, acp::ElicitationMode::Url(_))
694            );
695            if !keep {
696                cleared_ids.push(elicitation.id.clone());
697            }
698            keep
699        });
700
701        if !cleared_ids.is_empty() {
702            for id in &cleared_ids {
703                cx.emit(ElicitationStoreEvent::ElicitationUpdated(id.clone()));
704            }
705            cx.notify();
706        }
707
708        cleared_ids
709    }
710
711    pub fn cancel_request(&mut self, request_id: &acp::RequestId, cx: &mut Context<Self>) {
712        let canceled_ids = self.cancel_pending(|elicitation| {
713            matches!(
714                elicitation.request.scope(),
715                acp::ElicitationScope::Request(scope) if &scope.request_id == request_id
716            )
717        });
718        for id in canceled_ids {
719            cx.emit(ElicitationStoreEvent::ElicitationUpdated(id));
720        }
721        cx.notify();
722    }
723
724    pub fn elicitation(&self, id: &ElicitationEntryId) -> Option<(usize, &Elicitation)> {
725        self.elicitations
726            .iter()
727            .enumerate()
728            .rev()
729            .find_map(|(index, elicitation)| {
730                (&elicitation.id == id).then_some((index, elicitation))
731            })
732    }
733
734    fn entry_id_for_url_elicitation(
735        &self,
736        elicitation_id: &acp::ElicitationId,
737    ) -> Option<ElicitationEntryId> {
738        self.elicitations.iter().rev().find_map(|elicitation| {
739            if let acp::ElicitationMode::Url(mode) = &elicitation.request.mode
740                && &mode.elicitation_id == elicitation_id
741            {
742                Some(elicitation.id.clone())
743            } else {
744                None
745            }
746        })
747    }
748
749    fn elicitation_mut(&mut self, id: &ElicitationEntryId) -> Option<(usize, &mut Elicitation)> {
750        self.elicitations
751            .iter_mut()
752            .enumerate()
753            .rev()
754            .find_map(|(index, elicitation)| {
755                (&elicitation.id == id).then_some((index, elicitation))
756            })
757    }
758
759    fn cancel_pending(
760        &mut self,
761        mut should_cancel: impl FnMut(&Elicitation) -> bool,
762    ) -> Vec<ElicitationEntryId> {
763        let mut canceled_ids = Vec::new();
764        for elicitation in &mut self.elicitations {
765            if should_cancel(elicitation) && Self::cancel_elicitation_entry(elicitation, true) {
766                canceled_ids.push(elicitation.id.clone());
767            }
768        }
769        canceled_ids
770    }
771}
772
773#[derive(Debug, Clone, PartialEq, Eq)]
774pub struct ContextCompactionId(pub Arc<str>);
775
776#[derive(Debug, Clone, Copy, PartialEq, Eq)]
777pub enum ContextCompactionStatus {
778    InProgress,
779    Completed,
780    Canceled,
781}
782
783/// A point in the thread where the conversation history was compacted to free
784/// up room in the model's context window. The summary can be expanded to inspect
785/// what the model retained.
786#[derive(Debug)]
787pub struct ContextCompaction {
788    pub id: ContextCompactionId,
789    pub status: ContextCompactionStatus,
790    /// The compaction summary, streamed in as the model produces it. This is
791    /// `None` for provider-native compaction, which produces no summary to show.
792    pub summary: Option<Entity<Markdown>>,
793}
794
795impl ContextCompaction {
796    pub fn is_in_progress(&self) -> bool {
797        self.status == ContextCompactionStatus::InProgress
798    }
799}
800
801#[derive(Debug)]
802pub struct ContextCompactionUpdate {
803    pub id: ContextCompactionId,
804    pub summary_delta: String,
805    pub status: Option<ContextCompactionStatus>,
806}
807
808impl AgentThreadEntry {
809    pub fn is_indented(&self) -> bool {
810        match self {
811            Self::UserMessage(message) => message.indented,
812            Self::AssistantMessage(message) => message.indented,
813            Self::ToolCall(_) => false,
814            Self::Elicitation(_) => false,
815            Self::CompletedPlan(_) => false,
816            Self::ContextCompaction(_) => false,
817            Self::SystemNote(_) => false,
818        }
819    }
820
821    pub fn to_markdown(&self, cx: &App) -> String {
822        match self {
823            Self::UserMessage(message) => message.to_markdown(cx),
824            Self::AssistantMessage(message) => message.to_markdown(cx),
825            Self::ToolCall(tool_call) => tool_call.to_markdown(cx),
826            Self::Elicitation(_) => "## Input Requested\n\n".to_string(),
827            Self::CompletedPlan(entries) => {
828                let mut md = String::from("## Plan\n\n");
829                for entry in entries {
830                    let source = entry.content.read(cx).source().to_string();
831                    md.push_str(&format!("- [x] {}\n", source));
832                }
833                md
834            }
835            Self::ContextCompaction(_) => "--- Context Compacted ---\n\n".to_string(),
836            Self::SystemNote(note) => format!("--- {} ---\n\n", note.text),
837        }
838    }
839
840    pub fn user_message(&self) -> Option<&UserMessage> {
841        if let AgentThreadEntry::UserMessage(message) = self {
842            Some(message)
843        } else {
844            None
845        }
846    }
847
848    pub fn diffs(&self) -> impl Iterator<Item = &Entity<Diff>> {
849        if let AgentThreadEntry::ToolCall(call) = self {
850            itertools::Either::Left(call.diffs())
851        } else {
852            itertools::Either::Right(std::iter::empty())
853        }
854    }
855
856    pub fn terminals(&self) -> impl Iterator<Item = &Entity<Terminal>> {
857        if let AgentThreadEntry::ToolCall(call) = self {
858            itertools::Either::Left(call.terminals())
859        } else {
860            itertools::Either::Right(std::iter::empty())
861        }
862    }
863
864    pub fn location(&self, ix: usize) -> Option<(acp::ToolCallLocation, AgentLocation)> {
865        if let AgentThreadEntry::ToolCall(ToolCall {
866            locations,
867            resolved_locations,
868            ..
869        }) = self
870        {
871            Some((
872                locations.get(ix)?.clone(),
873                resolved_locations.get(ix)?.clone()?,
874            ))
875        } else {
876            None
877        }
878    }
879}
880
881#[derive(Debug)]
882pub struct ToolCall {
883    pub id: acp::ToolCallId,
884    pub label: Entity<Markdown>,
885    pub kind: acp::ToolKind,
886    pub content: Vec<ToolCallContent>,
887    pub status: ToolCallStatus,
888    pub locations: Vec<acp::ToolCallLocation>,
889    pub resolved_locations: Vec<Option<AgentLocation>>,
890    pub raw_input: Option<serde_json::Value>,
891    pub raw_input_markdown: Option<Entity<Markdown>>,
892    pub raw_output: Option<serde_json::Value>,
893    pub tool_name: Option<SharedString>,
894    pub subagent_session_info: Option<SubagentSessionInfo>,
895    pub sandbox_authorization_details: Option<SandboxAuthorizationDetails>,
896    pub sandbox_fallback_authorization_details: Option<SandboxFallbackAuthorizationDetails>,
897    /// Why this terminal command ran without the OS sandbox even though
898    /// sandboxing was active (see [`SANDBOX_NOT_APPLIED_META_KEY`]). `None` when
899    /// the command was sandboxed normally (or sandboxing was off).
900    pub sandbox_not_applied: Option<SandboxNotAppliedReason>,
901}
902
903impl ToolCall {
904    fn from_acp(
905        tool_call: acp::ToolCall,
906        status: ToolCallStatus,
907        language_registry: Arc<LanguageRegistry>,
908        path_style: PathStyle,
909        terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
910        cx: &mut App,
911    ) -> Result<Self> {
912        let title = if tool_call.kind == acp::ToolKind::Execute {
913            tool_call.title
914        } else if tool_call.kind == acp::ToolKind::Edit {
915            MarkdownEscaped(tool_call.title.as_str()).to_string()
916        } else if let Some((first_line, _)) = tool_call.title.split_once("\n") {
917            first_line.to_owned() + "…"
918        } else {
919            tool_call.title
920        };
921        let mut content = Vec::with_capacity(tool_call.content.len());
922        for item in tool_call.content {
923            if let Some(item) = ToolCallContent::from_acp(
924                item,
925                language_registry.clone(),
926                path_style,
927                terminals,
928                cx,
929            )? {
930                content.push(item);
931            }
932        }
933
934        let raw_input_markdown = tool_call
935            .raw_input
936            .as_ref()
937            .and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
938
939        let tool_name = tool_name_from_meta(&tool_call.meta);
940
941        let subagent_session_info = subagent_session_info_from_meta(&tool_call.meta);
942        let sandbox_authorization_details =
943            sandbox_authorization_details_from_meta(&tool_call.meta);
944        let sandbox_fallback_authorization_details =
945            sandbox_fallback_authorization_details_from_meta(&tool_call.meta);
946        let sandbox_not_applied = sandbox_not_applied_from_meta(&tool_call.meta);
947
948        let label = if tool_call.kind == acp::ToolKind::Execute {
949            cx.new(|cx| Markdown::new_text(title.into(), cx))
950        } else {
951            cx.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx))
952        };
953
954        let result = Self {
955            id: tool_call.tool_call_id,
956            label,
957            kind: tool_call.kind,
958            content,
959            locations: tool_call.locations,
960            resolved_locations: Vec::default(),
961            status,
962            raw_input: tool_call.raw_input,
963            raw_input_markdown,
964            raw_output: tool_call.raw_output,
965            tool_name,
966            subagent_session_info,
967            sandbox_authorization_details,
968            sandbox_fallback_authorization_details,
969            sandbox_not_applied,
970        };
971        Ok(result)
972    }
973
974    fn update_fields(
975        &mut self,
976        fields: acp::ToolCallUpdateFields,
977        meta: Option<acp::Meta>,
978        language_registry: Arc<LanguageRegistry>,
979        path_style: PathStyle,
980        terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
981        cx: &mut App,
982    ) -> Result<()> {
983        let acp::ToolCallUpdateFields {
984            kind,
985            status,
986            title,
987            content,
988            locations,
989            raw_input,
990            raw_output,
991            ..
992        } = fields;
993
994        if let Some(kind) = kind {
995            self.kind = kind;
996        }
997
998        if let Some(status) = status {
999            self.update_acp_status(status);
1000        }
1001
1002        if let Some(subagent_session_info) = subagent_session_info_from_meta(&meta) {
1003            self.subagent_session_info = Some(subagent_session_info);
1004        }
1005        if let Some(sandbox_authorization_details) = sandbox_authorization_details_from_meta(&meta)
1006        {
1007            self.sandbox_authorization_details = Some(sandbox_authorization_details);
1008        }
1009        if let Some(sandbox_fallback_authorization_details) =
1010            sandbox_fallback_authorization_details_from_meta(&meta)
1011        {
1012            self.sandbox_fallback_authorization_details =
1013                Some(sandbox_fallback_authorization_details);
1014        }
1015        if let Some(sandbox_not_applied) = sandbox_not_applied_from_meta(&meta) {
1016            self.sandbox_not_applied = Some(sandbox_not_applied);
1017        }
1018
1019        if let Some(title) = title {
1020            if self.kind == acp::ToolKind::Execute {
1021                for terminal in self.terminals() {
1022                    terminal.update(cx, |terminal, cx| {
1023                        terminal.update_command_label(&title, cx);
1024                    });
1025                }
1026            }
1027            self.label.update(cx, |label, cx| {
1028                if self.kind == acp::ToolKind::Execute {
1029                    label.replace(title, cx);
1030                } else if self.kind == acp::ToolKind::Edit {
1031                    label.replace(MarkdownEscaped(&title).to_string(), cx)
1032                } else if let Some((first_line, _)) = title.split_once("\n") {
1033                    label.replace(first_line.to_owned() + "…", cx);
1034                } else {
1035                    label.replace(title, cx);
1036                }
1037            });
1038        }
1039
1040        if let Some(content) = content {
1041            let mut new_content_len = content.len();
1042            let mut content = content.into_iter();
1043
1044            // Reuse existing content if we can
1045            for (old, new) in self.content.iter_mut().zip(content.by_ref()) {
1046                let valid_content =
1047                    old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?;
1048                if !valid_content {
1049                    new_content_len -= 1;
1050                }
1051            }
1052            for new in content {
1053                if let Some(new) = ToolCallContent::from_acp(
1054                    new,
1055                    language_registry.clone(),
1056                    path_style,
1057                    terminals,
1058                    cx,
1059                )? {
1060                    self.content.push(new);
1061                } else {
1062                    new_content_len -= 1;
1063                }
1064            }
1065            self.content.truncate(new_content_len);
1066        }
1067
1068        if let Some(locations) = locations {
1069            self.locations = locations;
1070        }
1071
1072        if let Some(raw_input) = raw_input {
1073            self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
1074            self.raw_input = Some(raw_input);
1075        }
1076
1077        if let Some(raw_output) = raw_output {
1078            if self.content.is_empty()
1079                && let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx)
1080            {
1081                self.content
1082                    .push(ToolCallContent::ContentBlock(ContentBlock::Markdown {
1083                        markdown,
1084                    }));
1085            }
1086            self.raw_output = Some(raw_output);
1087        }
1088        Ok(())
1089    }
1090
1091    fn update_status(&mut self, status: ToolCallStatus) {
1092        match status {
1093            ToolCallStatus::Pending => self.update_acp_status(acp::ToolCallStatus::Pending),
1094            ToolCallStatus::InProgress => self.update_acp_status(acp::ToolCallStatus::InProgress),
1095            ToolCallStatus::Completed => self.update_acp_status(acp::ToolCallStatus::Completed),
1096            ToolCallStatus::Failed => self.update_acp_status(acp::ToolCallStatus::Failed),
1097            status @ (ToolCallStatus::WaitingForConfirmation { .. }
1098            | ToolCallStatus::Rejected
1099            | ToolCallStatus::Canceled) => self.status = status,
1100        }
1101    }
1102
1103    fn update_acp_status(&mut self, status: acp::ToolCallStatus) {
1104        if let ToolCallStatus::WaitingForConfirmation { current_status, .. } = &mut self.status
1105            && matches!(
1106                status,
1107                acp::ToolCallStatus::Pending | acp::ToolCallStatus::InProgress
1108            )
1109        {
1110            *current_status = status;
1111        } else {
1112            self.status = status.into();
1113        }
1114    }
1115
1116    pub fn diffs(&self) -> impl Iterator<Item = &Entity<Diff>> {
1117        self.content.iter().filter_map(|content| match content {
1118            ToolCallContent::Diff(diff) => Some(diff),
1119            ToolCallContent::ContentBlock(_) => None,
1120            ToolCallContent::Terminal(_) => None,
1121        })
1122    }
1123
1124    pub fn terminals(&self) -> impl Iterator<Item = &Entity<Terminal>> {
1125        self.content.iter().filter_map(|content| match content {
1126            ToolCallContent::Terminal(terminal) => Some(terminal),
1127            ToolCallContent::ContentBlock(_) => None,
1128            ToolCallContent::Diff(_) => None,
1129        })
1130    }
1131
1132    pub fn is_subagent(&self) -> bool {
1133        self.tool_name.as_ref().is_some_and(|s| s == "spawn_agent")
1134            || self.subagent_session_info.is_some()
1135    }
1136
1137    pub fn to_markdown(&self, cx: &App) -> String {
1138        let mut markdown = format!(
1139            "**Tool Call: {}**\nStatus: {}\n\n",
1140            self.label.read(cx).source(),
1141            self.status
1142        );
1143        for content in &self.content {
1144            markdown.push_str(content.to_markdown(cx).as_str());
1145            markdown.push_str("\n\n");
1146        }
1147        markdown
1148    }
1149
1150    async fn resolve_location(
1151        location: acp::ToolCallLocation,
1152        project: WeakEntity<Project>,
1153        cx: &mut AsyncApp,
1154    ) -> Option<ResolvedLocation> {
1155        let buffer = project
1156            .update(cx, |project, cx| {
1157                if let Some(path) = project.project_path_for_absolute_path(&location.path, cx) {
1158                    Some(project.open_buffer(path, cx))
1159                } else if is_absolute(
1160                    location.path.to_string_lossy().as_ref(),
1161                    project.path_style(cx),
1162                ) {
1163                    Some(project.open_local_buffer(&location.path, cx))
1164                } else {
1165                    None
1166                }
1167            })
1168            .ok()??;
1169        let buffer = buffer.await.log_err()?;
1170        let position = buffer.update(cx, |buffer, _| {
1171            let snapshot = buffer.snapshot();
1172            if let Some(row) = location.line {
1173                let column = snapshot.indent_size_for_line(row).len;
1174                let point = snapshot.clip_point(Point::new(row, column), Bias::Left);
1175                snapshot.anchor_before(point)
1176            } else {
1177                Anchor::min_for_buffer(snapshot.remote_id())
1178            }
1179        });
1180
1181        Some(ResolvedLocation { buffer, position })
1182    }
1183
1184    fn resolve_locations(
1185        &self,
1186        project: Entity<Project>,
1187        cx: &mut App,
1188    ) -> Task<Vec<Option<ResolvedLocation>>> {
1189        let locations = self.locations.clone();
1190        project.update(cx, |_, cx| {
1191            cx.spawn(async move |project, cx| {
1192                let mut new_locations = Vec::new();
1193                for location in locations {
1194                    new_locations.push(Self::resolve_location(location, project.clone(), cx).await);
1195                }
1196                new_locations
1197            })
1198        })
1199    }
1200}
1201
1202// Separate so we can hold a strong reference to the buffer
1203// for saving on the thread
1204#[derive(Clone, Debug, PartialEq, Eq)]
1205struct ResolvedLocation {
1206    buffer: Entity<Buffer>,
1207    position: Anchor,
1208}
1209
1210impl From<&ResolvedLocation> for AgentLocation {
1211    fn from(value: &ResolvedLocation) -> Self {
1212        Self {
1213            buffer: value.buffer.downgrade(),
1214            position: value.position,
1215        }
1216    }
1217}
1218
1219#[derive(Debug, Clone)]
1220pub enum SelectedPermissionParams {
1221    Terminal { patterns: Vec<String> },
1222}
1223
1224#[derive(Debug, Clone)]
1225pub struct SelectedPermissionOutcome {
1226    pub option_id: acp::PermissionOptionId,
1227    pub option_kind: acp::PermissionOptionKind,
1228    pub params: Option<SelectedPermissionParams>,
1229}
1230
1231impl SelectedPermissionOutcome {
1232    pub fn new(option_id: acp::PermissionOptionId, option_kind: acp::PermissionOptionKind) -> Self {
1233        Self {
1234            option_id,
1235            option_kind,
1236            params: None,
1237        }
1238    }
1239
1240    pub fn params(mut self, params: Option<SelectedPermissionParams>) -> Self {
1241        self.params = params;
1242        self
1243    }
1244}
1245
1246impl From<SelectedPermissionOutcome> for acp::SelectedPermissionOutcome {
1247    fn from(value: SelectedPermissionOutcome) -> Self {
1248        Self::new(value.option_id)
1249    }
1250}
1251
1252#[derive(Debug)]
1253pub enum RequestPermissionOutcome {
1254    Cancelled,
1255    Selected(SelectedPermissionOutcome),
1256}
1257
1258impl From<RequestPermissionOutcome> for acp::RequestPermissionOutcome {
1259    fn from(value: RequestPermissionOutcome) -> Self {
1260        match value {
1261            RequestPermissionOutcome::Cancelled => Self::Cancelled,
1262            RequestPermissionOutcome::Selected(outcome) => Self::Selected(outcome.into()),
1263        }
1264    }
1265}
1266
1267/// What a `WaitingForConfirmation` prompt represents semantically.
1268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1269pub enum AuthorizationKind {
1270    /// The user is granting or denying permission for the tool call to
1271    /// proceed. The selected `PermissionOptionKind` determines whether the
1272    /// tool call transitions to `InProgress` (allow) or `Rejected` (reject).
1273    /// This is the default for tool authorization prompts.
1274    PermissionGrant,
1275    /// The user is choosing between actions for the tool to take next
1276    /// (for example, "Save" vs "Discard" before editing a dirty buffer).
1277    /// The tool call always transitions to `InProgress` regardless of the
1278    /// selected `PermissionOptionKind`; the caller interprets the chosen
1279    /// `option_id` to decide what to do.
1280    ActionChoice,
1281}
1282
1283#[derive(Debug)]
1284pub enum ToolCallStatus {
1285    /// The tool call hasn't started running yet, but we start showing it to
1286    /// the user.
1287    Pending,
1288    /// The tool call is waiting for confirmation from the user.
1289    WaitingForConfirmation {
1290        current_status: acp::ToolCallStatus,
1291        options: PermissionOptions,
1292        respond_tx: oneshot::Sender<SelectedPermissionOutcome>,
1293        kind: AuthorizationKind,
1294    },
1295    /// The tool call is currently running.
1296    InProgress,
1297    /// The tool call completed successfully.
1298    Completed,
1299    /// The tool call failed.
1300    Failed,
1301    /// The user rejected the tool call.
1302    Rejected,
1303    /// The user canceled generation so the tool call was canceled.
1304    Canceled,
1305}
1306
1307impl From<acp::ToolCallStatus> for ToolCallStatus {
1308    fn from(status: acp::ToolCallStatus) -> Self {
1309        match status {
1310            acp::ToolCallStatus::Pending => Self::Pending,
1311            acp::ToolCallStatus::InProgress => Self::InProgress,
1312            acp::ToolCallStatus::Completed => Self::Completed,
1313            acp::ToolCallStatus::Failed => Self::Failed,
1314            _ => Self::Pending,
1315        }
1316    }
1317}
1318
1319impl ToolCallStatus {
1320    fn as_acp_status(&self) -> Option<acp::ToolCallStatus> {
1321        match self {
1322            ToolCallStatus::Pending => Some(acp::ToolCallStatus::Pending),
1323            ToolCallStatus::WaitingForConfirmation { current_status, .. } => Some(*current_status),
1324            ToolCallStatus::InProgress => Some(acp::ToolCallStatus::InProgress),
1325            ToolCallStatus::Completed => Some(acp::ToolCallStatus::Completed),
1326            ToolCallStatus::Failed => Some(acp::ToolCallStatus::Failed),
1327            ToolCallStatus::Rejected | ToolCallStatus::Canceled => None,
1328        }
1329    }
1330
1331    fn status_after_permission_grant(status: acp::ToolCallStatus) -> ToolCallStatus {
1332        match ToolCallStatus::from(status) {
1333            ToolCallStatus::Pending => ToolCallStatus::InProgress,
1334            status => status,
1335        }
1336    }
1337}
1338
1339impl Display for ToolCallStatus {
1340    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1341        write!(
1342            f,
1343            "{}",
1344            match self {
1345                ToolCallStatus::Pending => "Pending",
1346                ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation",
1347                ToolCallStatus::InProgress => "In Progress",
1348                ToolCallStatus::Completed => "Completed",
1349                ToolCallStatus::Failed => "Failed",
1350                ToolCallStatus::Rejected => "Rejected",
1351                ToolCallStatus::Canceled => "Canceled",
1352            }
1353        )
1354    }
1355}
1356
1357fn elicitation_status_for_response(response: &acp::CreateElicitationResponse) -> ElicitationStatus {
1358    match &response.action {
1359        acp::ElicitationAction::Accept(_) => ElicitationStatus::Accepted,
1360        acp::ElicitationAction::Decline => ElicitationStatus::Declined,
1361        acp::ElicitationAction::Cancel => ElicitationStatus::Canceled,
1362        _ => ElicitationStatus::Canceled,
1363    }
1364}
1365
1366#[derive(Debug, PartialEq, Clone)]
1367pub enum ContentBlock {
1368    Empty,
1369    Markdown {
1370        markdown: Entity<Markdown>,
1371    },
1372    EmbeddedResource {
1373        resource: acp::EmbeddedResource,
1374        markdown: Option<Entity<Markdown>>,
1375    },
1376    ResourceLink {
1377        resource_link: acp::ResourceLink,
1378    },
1379    Image {
1380        image: Arc<gpui::Image>,
1381        dimensions: Option<gpui::Size<u32>>,
1382    },
1383}
1384
1385impl ContentBlock {
1386    pub fn new(
1387        block: acp::ContentBlock,
1388        language_registry: &Arc<LanguageRegistry>,
1389        path_style: PathStyle,
1390        cx: &mut App,
1391    ) -> Self {
1392        let mut this = Self::Empty;
1393        this.append(block, language_registry, path_style, cx);
1394        this
1395    }
1396
1397    pub fn new_combined(
1398        blocks: impl IntoIterator<Item = acp::ContentBlock>,
1399        language_registry: Arc<LanguageRegistry>,
1400        path_style: PathStyle,
1401        cx: &mut App,
1402    ) -> Self {
1403        let mut this = Self::Empty;
1404        for block in blocks {
1405            this.append(block, &language_registry, path_style, cx);
1406        }
1407        this
1408    }
1409
1410    pub fn new_tool_call_content(
1411        block: acp::ContentBlock,
1412        language_registry: &Arc<LanguageRegistry>,
1413        path_style: PathStyle,
1414        cx: &mut App,
1415    ) -> Self {
1416        match block {
1417            acp::ContentBlock::Resource(resource) => {
1418                if let Some((image, dimensions)) = Self::decode_embedded_resource_image(&resource) {
1419                    Self::Image { image, dimensions }
1420                } else {
1421                    let markdown =
1422                        Self::embedded_resource_markdown(&resource, language_registry, cx);
1423                    Self::EmbeddedResource { resource, markdown }
1424                }
1425            }
1426            block => Self::new(block, language_registry, path_style, cx),
1427        }
1428    }
1429
1430    pub fn append(
1431        &mut self,
1432        block: acp::ContentBlock,
1433        language_registry: &Arc<LanguageRegistry>,
1434        path_style: PathStyle,
1435        cx: &mut App,
1436    ) {
1437        match (&mut *self, &block) {
1438            (ContentBlock::Empty, acp::ContentBlock::ResourceLink(resource_link)) => {
1439                *self = ContentBlock::ResourceLink {
1440                    resource_link: resource_link.clone(),
1441                };
1442            }
1443            (ContentBlock::Empty, acp::ContentBlock::Image(image_content)) => {
1444                if let Some((image, dimensions)) = Self::decode_image(image_content) {
1445                    *self = ContentBlock::Image { image, dimensions };
1446                } else {
1447                    let new_content = Self::image_md(image_content);
1448                    *self = Self::create_markdown_block(new_content, language_registry, cx);
1449                }
1450            }
1451            (ContentBlock::Empty, _) => {
1452                let new_content = Self::block_string_contents(&block, path_style);
1453                *self = Self::create_markdown_block(new_content, language_registry, cx);
1454            }
1455            (ContentBlock::Markdown { markdown }, _) => {
1456                let new_content = Self::block_string_contents(&block, path_style);
1457                markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx));
1458            }
1459            (ContentBlock::ResourceLink { resource_link }, _) => {
1460                let existing_content = Self::resource_link_md(&resource_link.uri, path_style);
1461                let new_content = Self::block_string_contents(&block, path_style);
1462                let combined = format!("{}\n{}", existing_content, new_content);
1463                *self = Self::create_markdown_block(combined, language_registry, cx);
1464            }
1465            (ContentBlock::EmbeddedResource { resource, .. }, _) => {
1466                let existing_content =
1467                    Self::embedded_resource_string_contents(resource, path_style);
1468                let new_content = Self::block_string_contents(&block, path_style);
1469                let combined = format!("{}\n{}", existing_content, new_content);
1470                *self = Self::create_markdown_block(combined, language_registry, cx);
1471            }
1472            (ContentBlock::Image { .. }, _) => {
1473                let new_content = Self::block_string_contents(&block, path_style);
1474                let combined = format!("`Image`\n{}", new_content);
1475                *self = Self::create_markdown_block(combined, language_registry, cx);
1476            }
1477        }
1478    }
1479
1480    /// Updates a Markdown block in place from a streaming text `block`, reusing
1481    /// the existing `Markdown` entity rather than recreating it. Appends only the
1482    /// new suffix when the update is a continuation (the common streaming case),
1483    /// otherwise re-sets the source. Returns `false` when an in-place update isn't
1484    /// applicable, so the caller can fall back to replacing the block wholesale.
1485    ///
1486    /// Recreating the entity on every streamed snapshot causes the rendered
1487    /// element to tear down and rebuild, which flickers badly.
1488    pub fn update_text_in_place(&mut self, block: &acp::ContentBlock, cx: &mut App) -> bool {
1489        let ContentBlock::Markdown { markdown } = self else {
1490            return false;
1491        };
1492        let acp::ContentBlock::Text(text_content) = block else {
1493            return false;
1494        };
1495        let new_content = &text_content.text;
1496        markdown.update(cx, |markdown, cx| {
1497            let current = markdown.source().to_string();
1498            match new_content.strip_prefix(&current) {
1499                Some("") => {}
1500                Some(suffix) => markdown.append(suffix, cx),
1501                None => markdown.reset(new_content.clone().into(), cx),
1502            }
1503        });
1504        true
1505    }
1506
1507    fn decode_image(
1508        image_content: &acp::ImageContent,
1509    ) -> Option<(Arc<gpui::Image>, Option<gpui::Size<u32>>)> {
1510        Self::decode_image_data(&image_content.data, &image_content.mime_type)
1511    }
1512
1513    fn decode_embedded_resource_image(
1514        resource: &acp::EmbeddedResource,
1515    ) -> Option<(Arc<gpui::Image>, Option<gpui::Size<u32>>)> {
1516        let acp::EmbeddedResourceResource::BlobResourceContents(blob) = &resource.resource else {
1517            return None;
1518        };
1519        let mime_type = blob.mime_type.as_deref()?;
1520        Self::decode_image_data(&blob.blob, mime_type)
1521    }
1522
1523    fn decode_image_data(
1524        data: &str,
1525        mime_type: &str,
1526    ) -> Option<(Arc<gpui::Image>, Option<gpui::Size<u32>>)> {
1527        use base64::Engine as _;
1528
1529        let bytes = base64::engine::general_purpose::STANDARD
1530            .decode(data.as_bytes())
1531            .ok()?;
1532        let format = gpui::ImageFormat::from_mime_type(mime_type)?;
1533        let dimensions = Self::image_dimensions(&bytes, format);
1534        Some((Arc::new(gpui::Image::from_bytes(format, bytes)), dimensions))
1535    }
1536
1537    fn image_dimensions(bytes: &[u8], format: gpui::ImageFormat) -> Option<gpui::Size<u32>> {
1538        let format = match format {
1539            gpui::ImageFormat::Png => image::ImageFormat::Png,
1540            gpui::ImageFormat::Jpeg => image::ImageFormat::Jpeg,
1541            gpui::ImageFormat::Webp => image::ImageFormat::WebP,
1542            gpui::ImageFormat::Gif => image::ImageFormat::Gif,
1543            gpui::ImageFormat::Svg => return None,
1544            gpui::ImageFormat::Bmp => image::ImageFormat::Bmp,
1545            gpui::ImageFormat::Tiff => image::ImageFormat::Tiff,
1546            gpui::ImageFormat::Ico => image::ImageFormat::Ico,
1547            gpui::ImageFormat::Pnm => image::ImageFormat::Pnm,
1548        };
1549
1550        image::ImageReader::with_format(std::io::Cursor::new(bytes), format)
1551            .into_dimensions()
1552            .ok()
1553            .map(|(width, height)| gpui::Size { width, height })
1554    }
1555
1556    fn create_markdown_block(
1557        content: String,
1558        language_registry: &Arc<LanguageRegistry>,
1559        cx: &mut App,
1560    ) -> ContentBlock {
1561        ContentBlock::Markdown {
1562            markdown: Self::create_markdown(content, language_registry, cx),
1563        }
1564    }
1565
1566    fn create_markdown(
1567        content: String,
1568        language_registry: &Arc<LanguageRegistry>,
1569        cx: &mut App,
1570    ) -> Entity<Markdown> {
1571        cx.new(|cx| {
1572            Markdown::new_with_options(
1573                content.into(),
1574                Some(language_registry.clone()),
1575                None,
1576                MarkdownOptions {
1577                    render_mermaid_diagrams: true,
1578                    render_metadata_blocks: true,
1579                    ..Default::default()
1580                },
1581                cx,
1582            )
1583        })
1584    }
1585
1586    fn embedded_resource_markdown(
1587        resource: &acp::EmbeddedResource,
1588        language_registry: &Arc<LanguageRegistry>,
1589        cx: &mut App,
1590    ) -> Option<Entity<Markdown>> {
1591        match &resource.resource {
1592            acp::EmbeddedResourceResource::TextResourceContents(text) => Some(
1593                Self::create_markdown(Self::text_resource_markdown(text), language_registry, cx),
1594            ),
1595            acp::EmbeddedResourceResource::BlobResourceContents(_) => None,
1596            _ => None,
1597        }
1598    }
1599
1600    fn text_resource_markdown(resource: &acp::TextResourceContents) -> String {
1601        match text_resource_render_mode(resource.mime_type.as_deref()) {
1602            TextResourceRenderMode::Markdown => resource.text.clone(),
1603            TextResourceRenderMode::CodeBlock(language) => {
1604                Self::fenced_code_block(&resource.text, language)
1605            }
1606        }
1607    }
1608
1609    pub fn text_content<'a>(&'a self, cx: &'a App) -> Option<&'a str> {
1610        match self {
1611            ContentBlock::Markdown { markdown } => Some(markdown.read(cx).source()),
1612            ContentBlock::EmbeddedResource { resource, .. } => match &resource.resource {
1613                acp::EmbeddedResourceResource::TextResourceContents(text) => Some(&text.text),
1614                acp::EmbeddedResourceResource::BlobResourceContents(_) => None,
1615                _ => None,
1616            },
1617            ContentBlock::Empty
1618            | ContentBlock::ResourceLink { .. }
1619            | ContentBlock::Image { .. } => None,
1620        }
1621    }
1622
1623    fn fenced_code_block(text: &str, language: Option<&str>) -> String {
1624        let fence_len = text
1625            .as_bytes()
1626            .chunk_by(|left, right| left == right)
1627            .filter(|chunk| chunk.first() == Some(&b'`'))
1628            .map(|chunk| chunk.len() + 1)
1629            .max()
1630            .unwrap_or(3)
1631            .max(3);
1632        let fence = "`".repeat(fence_len);
1633
1634        let mut markdown = String::new();
1635        markdown.push_str(&fence);
1636        if let Some(language) = language {
1637            markdown.push_str(language);
1638        }
1639        markdown.push('\n');
1640        markdown.push_str(text);
1641        if !text.ends_with('\n') {
1642            markdown.push('\n');
1643        }
1644        markdown.push_str(&fence);
1645        markdown
1646    }
1647
1648    fn embedded_resource_string_contents(
1649        resource: &acp::EmbeddedResource,
1650        path_style: PathStyle,
1651    ) -> String {
1652        match &resource.resource {
1653            acp::EmbeddedResourceResource::TextResourceContents(text) => {
1654                Self::resource_link_md(&text.uri, path_style)
1655            }
1656            acp::EmbeddedResourceResource::BlobResourceContents(blob) => {
1657                Self::resource_link_md(&blob.uri, path_style)
1658            }
1659            _ => String::new(),
1660        }
1661    }
1662
1663    fn embedded_resource_text(resource: &acp::EmbeddedResource) -> &str {
1664        match &resource.resource {
1665            acp::EmbeddedResourceResource::TextResourceContents(text) => &text.text,
1666            acp::EmbeddedResourceResource::BlobResourceContents(blob) => &blob.uri,
1667            _ => "",
1668        }
1669    }
1670
1671    fn embedded_resource_label(resource: &acp::EmbeddedResource) -> &str {
1672        match &resource.resource {
1673            acp::EmbeddedResourceResource::TextResourceContents(text) => &text.uri,
1674            acp::EmbeddedResourceResource::BlobResourceContents(blob) => &blob.uri,
1675            _ => "",
1676        }
1677    }
1678
1679    pub fn embedded_resource(&self) -> Option<(&acp::EmbeddedResource, Option<&Entity<Markdown>>)> {
1680        match self {
1681            ContentBlock::EmbeddedResource { resource, markdown } => {
1682                Some((resource, markdown.as_ref()))
1683            }
1684            _ => None,
1685        }
1686    }
1687
1688    pub fn visible_content(&self, cx: &App) -> bool {
1689        match self {
1690            ContentBlock::Empty => false,
1691            ContentBlock::Markdown { markdown } => !markdown.read(cx).source().trim().is_empty(),
1692            ContentBlock::EmbeddedResource { resource, markdown } => match markdown {
1693                Some(markdown) => !markdown.read(cx).source().trim().is_empty(),
1694                None => !Self::embedded_resource_text(resource).trim().is_empty(),
1695            },
1696            ContentBlock::ResourceLink { .. } | ContentBlock::Image { .. } => true,
1697        }
1698    }
1699
1700    fn block_string_contents(block: &acp::ContentBlock, path_style: PathStyle) -> String {
1701        match block {
1702            acp::ContentBlock::Text(text_content) => text_content.text.clone(),
1703            acp::ContentBlock::ResourceLink(resource_link) => {
1704                Self::resource_link_md(&resource_link.uri, path_style)
1705            }
1706            acp::ContentBlock::Resource(acp::EmbeddedResource {
1707                resource:
1708                    acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents {
1709                        uri,
1710                        ..
1711                    }),
1712                ..
1713            }) => Self::resource_link_md(uri, path_style),
1714            acp::ContentBlock::Image(image) => Self::image_md(image),
1715            _ => String::new(),
1716        }
1717    }
1718
1719    fn resource_link_md(uri: &str, path_style: PathStyle) -> String {
1720        if let Some(uri) = MentionUri::parse(uri, path_style).log_err() {
1721            uri.as_link().to_string()
1722        } else {
1723            uri.to_string()
1724        }
1725    }
1726
1727    fn image_md(_image: &acp::ImageContent) -> String {
1728        "`Image`".into()
1729    }
1730
1731    pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
1732        match self {
1733            ContentBlock::Empty => "",
1734            ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
1735            ContentBlock::EmbeddedResource { resource, markdown } => {
1736                if let Some(markdown) = markdown {
1737                    markdown.read(cx).source()
1738                } else {
1739                    Self::embedded_resource_label(resource)
1740                }
1741            }
1742            ContentBlock::ResourceLink { resource_link } => &resource_link.uri,
1743            ContentBlock::Image { .. } => "`Image`",
1744        }
1745    }
1746
1747    pub fn markdown(&self) -> Option<&Entity<Markdown>> {
1748        match self {
1749            ContentBlock::Empty => None,
1750            ContentBlock::Markdown { markdown } => Some(markdown),
1751            ContentBlock::EmbeddedResource { markdown, .. } => markdown.as_ref(),
1752            ContentBlock::ResourceLink { .. } => None,
1753            ContentBlock::Image { .. } => None,
1754        }
1755    }
1756
1757    pub fn resource_link(&self) -> Option<&acp::ResourceLink> {
1758        match self {
1759            ContentBlock::ResourceLink { resource_link } => Some(resource_link),
1760            _ => None,
1761        }
1762    }
1763
1764    pub fn image(&self) -> Option<(&Arc<gpui::Image>, Option<gpui::Size<u32>>)> {
1765        match self {
1766            ContentBlock::Image { image, dimensions } => Some((image, *dimensions)),
1767            _ => None,
1768        }
1769    }
1770}
1771
1772enum TextResourceRenderMode {
1773    Markdown,
1774    CodeBlock(Option<&'static str>),
1775}
1776
1777fn text_resource_render_mode(mime_type: Option<&str>) -> TextResourceRenderMode {
1778    let Some(mime_type) = mime_type else {
1779        return TextResourceRenderMode::CodeBlock(None);
1780    };
1781    let Ok(mime) = mime_type.parse::<mime::Mime>() else {
1782        return TextResourceRenderMode::CodeBlock(None);
1783    };
1784
1785    let type_ = mime.type_().as_str();
1786    let subtype = mime.subtype().as_str();
1787    let suffix = mime.suffix().map(|suffix| suffix.as_str());
1788
1789    if matches!(
1790        (type_, subtype),
1791        ("text", "markdown") | ("text", "x-markdown")
1792    ) {
1793        return TextResourceRenderMode::Markdown;
1794    }
1795
1796    let language = match (type_, subtype, suffix) {
1797        (_, "json", _) | (_, _, Some("json")) => Some("json"),
1798        (_, "xml", _) | (_, _, Some("xml")) => Some("xml"),
1799        ("text", "html", _) => Some("html"),
1800        ("text", "css", _) => Some("css"),
1801        ("text", "csv", _) => Some("csv"),
1802        ("text", "tab-separated-values", _) => Some("tsv"),
1803        ("text", "javascript", _) | ("application", "javascript", _) => Some("javascript"),
1804        ("application", "x-javascript", _) => Some("javascript"),
1805        ("text", "typescript", _) | ("application", "typescript", _) => Some("typescript"),
1806        ("text", "x-shellscript", _) | ("application", "x-shellscript", _) => Some("sh"),
1807        ("application", "x-sh", _) => Some("sh"),
1808        ("text", "x-python", _) => Some("python"),
1809        ("text", "x-rust", _) => Some("rust"),
1810        ("text", "x-go", _) => Some("go"),
1811        ("text", "x-ruby", _) => Some("ruby"),
1812        ("text", "x-c", _) => Some("c"),
1813        // `mime` parses `text/x-c++` as subtype `x-c+` with an empty suffix.
1814        ("text", "x-c+", Some("")) => Some("cpp"),
1815        ("text", "plain", _) => None,
1816        ("text", _, _) => None,
1817        ("application", "graphql", _) => Some("graphql"),
1818        ("application", "toml", _) => Some("toml"),
1819        ("application", "yaml", _) | ("application", "x-yaml", _) => Some("yaml"),
1820        (_, _, Some("yaml" | "yml")) => Some("yaml"),
1821        _ => return TextResourceRenderMode::CodeBlock(None),
1822    };
1823
1824    TextResourceRenderMode::CodeBlock(language)
1825}
1826
1827#[derive(Debug)]
1828pub enum ToolCallContent {
1829    ContentBlock(ContentBlock),
1830    Diff(Entity<Diff>),
1831    Terminal(Entity<Terminal>),
1832}
1833
1834impl ToolCallContent {
1835    pub fn from_acp(
1836        content: acp::ToolCallContent,
1837        language_registry: Arc<LanguageRegistry>,
1838        path_style: PathStyle,
1839        terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
1840        cx: &mut App,
1841    ) -> Result<Option<Self>> {
1842        match content {
1843            acp::ToolCallContent::Content(acp::Content { content, .. }) => Ok(Some(
1844                Self::ContentBlock(ContentBlock::new_tool_call_content(
1845                    content,
1846                    &language_registry,
1847                    path_style,
1848                    cx,
1849                )),
1850            )),
1851            acp::ToolCallContent::Diff(diff) => Ok(Some(Self::Diff(cx.new(|cx| {
1852                Diff::finalized(
1853                    diff.path.to_string_lossy().into_owned(),
1854                    diff.old_text,
1855                    diff.new_text,
1856                    language_registry,
1857                    cx,
1858                )
1859            })))),
1860            acp::ToolCallContent::Terminal(acp::Terminal { terminal_id, .. }) => terminals
1861                .get(&terminal_id)
1862                .cloned()
1863                .map(|terminal| Some(Self::Terminal(terminal)))
1864                .ok_or_else(|| anyhow::anyhow!("Terminal with id `{}` not found", terminal_id)),
1865            _ => Ok(None),
1866        }
1867    }
1868
1869    pub fn update_from_acp(
1870        &mut self,
1871        new: acp::ToolCallContent,
1872        language_registry: Arc<LanguageRegistry>,
1873        path_style: PathStyle,
1874        terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
1875        cx: &mut App,
1876    ) -> Result<bool> {
1877        // Update streaming text in place so the rendered markdown element is
1878        // reused across snapshots instead of being recreated (which flickers).
1879        if let (
1880            Self::ContentBlock(block),
1881            acp::ToolCallContent::Content(acp::Content { content, .. }),
1882        ) = (&mut *self, &new)
1883            && block.update_text_in_place(content, cx)
1884        {
1885            return Ok(true);
1886        }
1887
1888        let needs_update = match (&self, &new) {
1889            (Self::Diff(old_diff), acp::ToolCallContent::Diff(new_diff)) => {
1890                old_diff.read(cx).needs_update(
1891                    new_diff.old_text.as_deref().unwrap_or(""),
1892                    &new_diff.new_text,
1893                    cx,
1894                )
1895            }
1896            _ => true,
1897        };
1898
1899        if let Some(update) = Self::from_acp(new, language_registry, path_style, terminals, cx)? {
1900            if needs_update {
1901                *self = update;
1902            }
1903            Ok(true)
1904        } else {
1905            Ok(false)
1906        }
1907    }
1908
1909    pub fn to_markdown(&self, cx: &App) -> String {
1910        match self {
1911            Self::ContentBlock(content) => content.to_markdown(cx).to_string(),
1912            Self::Diff(diff) => diff.read(cx).to_markdown(cx),
1913            Self::Terminal(terminal) => terminal.read(cx).to_markdown(cx),
1914        }
1915    }
1916
1917    pub fn image(&self) -> Option<(&Arc<gpui::Image>, Option<gpui::Size<u32>>)> {
1918        match self {
1919            Self::ContentBlock(content) => content.image(),
1920            _ => None,
1921        }
1922    }
1923}
1924
1925#[derive(Debug, PartialEq)]
1926pub enum ToolCallUpdate {
1927    UpdateFields(acp::ToolCallUpdate),
1928    UpdateDiff(ToolCallUpdateDiff),
1929    UpdateTerminal(ToolCallUpdateTerminal),
1930}
1931
1932impl ToolCallUpdate {
1933    fn id(&self) -> &acp::ToolCallId {
1934        match self {
1935            Self::UpdateFields(update) => &update.tool_call_id,
1936            Self::UpdateDiff(diff) => &diff.id,
1937            Self::UpdateTerminal(terminal) => &terminal.id,
1938        }
1939    }
1940}
1941
1942impl From<acp::ToolCallUpdate> for ToolCallUpdate {
1943    fn from(update: acp::ToolCallUpdate) -> Self {
1944        Self::UpdateFields(update)
1945    }
1946}
1947
1948impl From<ToolCallUpdateDiff> for ToolCallUpdate {
1949    fn from(diff: ToolCallUpdateDiff) -> Self {
1950        Self::UpdateDiff(diff)
1951    }
1952}
1953
1954#[derive(Debug, PartialEq)]
1955pub struct ToolCallUpdateDiff {
1956    pub id: acp::ToolCallId,
1957    pub diff: Entity<Diff>,
1958}
1959
1960impl From<ToolCallUpdateTerminal> for ToolCallUpdate {
1961    fn from(terminal: ToolCallUpdateTerminal) -> Self {
1962        Self::UpdateTerminal(terminal)
1963    }
1964}
1965
1966#[derive(Debug, PartialEq)]
1967pub struct ToolCallUpdateTerminal {
1968    pub id: acp::ToolCallId,
1969    pub terminal: Entity<Terminal>,
1970}
1971
1972#[derive(Debug, Default)]
1973pub struct Plan {
1974    pub entries: Vec<PlanEntry>,
1975}
1976
1977#[derive(Debug)]
1978pub struct PlanStats<'a> {
1979    pub in_progress_entry: Option<&'a PlanEntry>,
1980    pub pending: u32,
1981    pub completed: u32,
1982}
1983
1984impl Plan {
1985    pub fn is_empty(&self) -> bool {
1986        self.entries.is_empty()
1987    }
1988
1989    pub fn stats(&self) -> PlanStats<'_> {
1990        let mut stats = PlanStats {
1991            in_progress_entry: None,
1992            pending: 0,
1993            completed: 0,
1994        };
1995
1996        for entry in &self.entries {
1997            match &entry.status {
1998                acp::PlanEntryStatus::Pending => {
1999                    stats.pending += 1;
2000                }
2001                acp::PlanEntryStatus::InProgress => {
2002                    stats.in_progress_entry = stats.in_progress_entry.or(Some(entry));
2003                    stats.pending += 1;
2004                }
2005                acp::PlanEntryStatus::Completed => {
2006                    stats.completed += 1;
2007                }
2008                _ => {}
2009            }
2010        }
2011
2012        stats
2013    }
2014}
2015
2016#[derive(Debug)]
2017pub struct PlanEntry {
2018    pub content: Entity<Markdown>,
2019    pub priority: acp::PlanEntryPriority,
2020    pub status: acp::PlanEntryStatus,
2021}
2022
2023impl PlanEntry {
2024    pub fn from_acp(entry: acp::PlanEntry, cx: &mut App) -> Self {
2025        Self {
2026            content: cx.new(|cx| Markdown::new(entry.content.into(), None, None, cx)),
2027            priority: entry.priority,
2028            status: entry.status,
2029        }
2030    }
2031}
2032
2033#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2034pub struct TokenUsage {
2035    pub max_tokens: u64,
2036    pub used_tokens: u64,
2037    pub input_tokens: u64,
2038    pub output_tokens: u64,
2039    pub max_output_tokens: Option<u64>,
2040}
2041
2042#[derive(Debug, Clone)]
2043pub struct SessionCost {
2044    pub amount: f64,
2045    pub currency: SharedString,
2046}
2047
2048pub const TOKEN_USAGE_WARNING_THRESHOLD: f32 = 0.8;
2049
2050impl TokenUsage {
2051    pub fn ratio(&self) -> TokenUsageRatio {
2052        #[cfg(debug_assertions)]
2053        let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
2054            .unwrap_or(TOKEN_USAGE_WARNING_THRESHOLD.to_string())
2055            .parse()
2056            .unwrap();
2057        #[cfg(not(debug_assertions))]
2058        let warning_threshold: f32 = TOKEN_USAGE_WARNING_THRESHOLD;
2059
2060        // When the maximum is unknown because there is no selected model,
2061        // avoid showing the token limit warning.
2062        if self.max_tokens == 0 {
2063            TokenUsageRatio::Normal
2064        } else if self.used_tokens >= self.max_tokens {
2065            TokenUsageRatio::Exceeded
2066        } else if self.used_tokens as f32 / self.max_tokens as f32 >= warning_threshold {
2067            TokenUsageRatio::Warning
2068        } else {
2069            TokenUsageRatio::Normal
2070        }
2071    }
2072}
2073
2074#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2075pub enum TokenUsageRatio {
2076    Normal,
2077    Warning,
2078    Exceeded,
2079}
2080
2081#[derive(Debug, Clone)]
2082pub struct RetryStatus {
2083    pub last_error: SharedString,
2084    pub attempt: usize,
2085    pub max_attempts: usize,
2086    pub started_at: Instant,
2087    pub duration: Duration,
2088    pub meta: Option<acp::Meta>,
2089}
2090
2091pub const REFUSAL_FALLBACK_MODEL_META_KEY: &str = "refusal_fallback_model";
2092
2093pub fn meta_with_refusal_fallback(model_name: &str) -> acp::Meta {
2094    acp::Meta::from_iter([(REFUSAL_FALLBACK_MODEL_META_KEY.into(), model_name.into())])
2095}
2096
2097pub fn refusal_fallback_model_from_meta(meta: &Option<acp::Meta>) -> Option<SharedString> {
2098    meta.as_ref()
2099        .and_then(|m| m.get(REFUSAL_FALLBACK_MODEL_META_KEY))
2100        .and_then(|v| v.as_str())
2101        .map(|s| SharedString::from(s.to_owned()))
2102}
2103
2104struct RunningTurn {
2105    id: u32,
2106    send_task: Task<()>,
2107}
2108
2109pub struct AcpThread {
2110    session_id: acp::SessionId,
2111    work_dirs: Option<PathList>,
2112    parent_session_id: Option<acp::SessionId>,
2113    title: Option<SharedString>,
2114    provisional_title: Option<SharedString>,
2115    entries: Vec<AgentThreadEntry>,
2116    elicitations: ElicitationStore,
2117    plan: Plan,
2118    project: Entity<Project>,
2119    action_log: Entity<ActionLog>,
2120    _git_store_subscription: Subscription,
2121    update_last_checkpoint_if_changed_task: Option<Task<Result<()>>>,
2122    shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
2123    turn_id: u32,
2124    running_turn: Option<RunningTurn>,
2125    connection: Rc<dyn AgentConnection>,
2126    token_usage: Option<TokenUsage>,
2127    cost: Option<SessionCost>,
2128    prompt_capabilities: acp::PromptCapabilities,
2129    available_commands: Vec<acp::AvailableCommand>,
2130    _observe_prompt_capabilities: Task<anyhow::Result<()>>,
2131    terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
2132    pending_terminal_output: HashMap<acp::TerminalId, Vec<Vec<u8>>>,
2133    pending_terminal_exit: HashMap<acp::TerminalId, acp::TerminalExitStatus>,
2134    had_error: bool,
2135    /// The user's unsent prompt text, persisted so it can be restored when reloading the thread.
2136    draft_prompt: Option<Vec<acp::ContentBlock>>,
2137    /// The initial scroll position for the thread view, set during session registration.
2138    ui_scroll_position: Option<gpui::ListOffset>,
2139    /// Buffer for smooth text streaming. Holds text that has been received from
2140    /// the model but not yet revealed in the UI. A timer task drains this buffer
2141    /// gradually to create a fluid typing effect instead of choppy chunk-at-a-time
2142    /// updates.
2143    streaming_text_buffer: Option<StreamingTextBuffer>,
2144}
2145
2146struct StreamingTextBuffer {
2147    /// Text received from the model but not yet appended to the Markdown source.
2148    pending: String,
2149    /// The number of bytes to reveal per timer turn.
2150    bytes_to_reveal_per_tick: usize,
2151    /// The Markdown entity being streamed into.
2152    target: Entity<Markdown>,
2153    /// Timer task that periodically moves text from `pending` into `source`.
2154    _reveal_task: Task<()>,
2155}
2156
2157impl StreamingTextBuffer {
2158    /// The number of milliseconds between each timer tick, controlling how quickly
2159    /// text is revealed.
2160    const TASK_UPDATE_MS: u64 = 16;
2161    /// The time in milliseconds to reveal the entire pending text.
2162    const REVEAL_TARGET: f32 = 200.0;
2163}
2164
2165impl From<&AcpThread> for ActionLogTelemetry {
2166    fn from(value: &AcpThread) -> Self {
2167        Self {
2168            agent_telemetry_id: value.connection().telemetry_id(),
2169            session_id: value.session_id.0.clone(),
2170        }
2171    }
2172}
2173
2174#[derive(Debug)]
2175pub enum AcpThreadEvent {
2176    StatusChanged,
2177    PromptUpdated,
2178    NewEntry,
2179    TitleUpdated,
2180    TokenUsageUpdated,
2181    EntryUpdated(usize),
2182    EntriesRemoved(Range<usize>),
2183    ToolAuthorizationRequested(acp::ToolCallId),
2184    ToolAuthorizationReceived(acp::ToolCallId),
2185    ElicitationRequested(ElicitationEntryId),
2186    ElicitationResponded(ElicitationEntryId),
2187    Retry(RetryStatus),
2188    SubagentSpawned(acp::SessionId),
2189    Stopped(acp::StopReason),
2190    Error,
2191    LoadError(LoadError),
2192    PromptCapabilitiesUpdated,
2193    Refusal,
2194    AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
2195    ModeUpdated(acp::SessionModeId),
2196    ConfigOptionsUpdated(Vec<acp::SessionConfigOption>),
2197    WorkingDirectoriesUpdated,
2198}
2199
2200impl EventEmitter<AcpThreadEvent> for AcpThread {}
2201
2202#[derive(Debug, Clone)]
2203pub enum TerminalProviderEvent {
2204    Created {
2205        terminal_id: acp::TerminalId,
2206        label: String,
2207        cwd: Option<PathBuf>,
2208        output_byte_limit: Option<u64>,
2209        terminal: Entity<::terminal::Terminal>,
2210    },
2211    Output {
2212        terminal_id: acp::TerminalId,
2213        data: Vec<u8>,
2214    },
2215    TitleChanged {
2216        terminal_id: acp::TerminalId,
2217        title: String,
2218    },
2219    Exit {
2220        terminal_id: acp::TerminalId,
2221        status: acp::TerminalExitStatus,
2222    },
2223}
2224
2225#[derive(Debug, Clone)]
2226pub enum TerminalProviderCommand {
2227    WriteInput {
2228        terminal_id: acp::TerminalId,
2229        bytes: Vec<u8>,
2230    },
2231    Resize {
2232        terminal_id: acp::TerminalId,
2233        cols: u16,
2234        rows: u16,
2235    },
2236    Close {
2237        terminal_id: acp::TerminalId,
2238    },
2239}
2240
2241#[derive(PartialEq, Eq, Debug)]
2242pub enum ThreadStatus {
2243    Idle,
2244    Generating,
2245}
2246
2247#[derive(Debug, Clone)]
2248pub enum LoadError {
2249    Unsupported {
2250        command: SharedString,
2251        current_version: SharedString,
2252        minimum_version: SharedString,
2253    },
2254    FailedToInstall(SharedString),
2255    Exited {
2256        status: ExitStatus,
2257        stderr: Option<SharedString>,
2258    },
2259    /// The agent no longer holds the session this thread names.
2260    ///
2261    /// Omega's thread record outlives the external agent's session store, so
2262    /// reopening an old thread after the agent has forgotten it is ordinary
2263    /// rather than exceptional. It surfaced as "Failed to Launch: Resource not
2264    /// found: <uuid>", which names a thing the reader cannot act on and reads
2265    /// like the agent failed to start. Nothing failed to launch: the
2266    /// conversation is gone and a new one is the way forward.
2267    SessionGone,
2268    Other(SharedString),
2269}
2270
2271impl Display for LoadError {
2272    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2273        match self {
2274            LoadError::Unsupported {
2275                command: path,
2276                current_version,
2277                minimum_version,
2278            } => {
2279                write!(
2280                    f,
2281                    "version {current_version} from {path} is not supported (need at least {minimum_version})"
2282                )
2283            }
2284            LoadError::FailedToInstall(msg) => write!(f, "Failed to install: {msg}"),
2285            LoadError::Exited { status, .. } => write!(f, "Server exited with status {status}"),
2286            LoadError::SessionGone => write!(
2287                f,
2288                "the agent no longer has this conversation; start a new thread to keep going"
2289            ),
2290            LoadError::Other(msg) => write!(f, "{msg}"),
2291        }
2292    }
2293}
2294
2295impl Error for LoadError {}
2296
2297impl AcpThread {
2298    pub fn new(
2299        parent_session_id: Option<acp::SessionId>,
2300        title: Option<SharedString>,
2301        work_dirs: Option<PathList>,
2302        connection: Rc<dyn AgentConnection>,
2303        project: Entity<Project>,
2304        action_log: Entity<ActionLog>,
2305        session_id: acp::SessionId,
2306        mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
2307        cx: &mut Context<Self>,
2308    ) -> Self {
2309        let prompt_capabilities = prompt_capabilities_rx.borrow().clone();
2310        let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
2311            loop {
2312                let caps = prompt_capabilities_rx.recv().await?;
2313                this.update(cx, |this, cx| {
2314                    this.prompt_capabilities = caps;
2315                    cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated);
2316                })?;
2317            }
2318        });
2319
2320        let git_store = project.read(cx).git_store().clone();
2321        let _git_store_subscription = cx.subscribe(&git_store, |this, _, event, cx| {
2322            if matches!(
2323                event,
2324                GitStoreEvent::RepositoryUpdated(
2325                    _,
2326                    RepositoryEvent::StatusesChanged | RepositoryEvent::HeadChanged,
2327                    _
2328                )
2329            ) {
2330                this.update_last_checkpoint_if_changed_task =
2331                    Some(this.update_last_checkpoint_if_changed(cx));
2332            }
2333        });
2334
2335        Self {
2336            parent_session_id,
2337            work_dirs,
2338            action_log,
2339            _git_store_subscription,
2340            update_last_checkpoint_if_changed_task: None,
2341            shared_buffers: Default::default(),
2342            entries: Default::default(),
2343            elicitations: ElicitationStore::default(),
2344            plan: Default::default(),
2345            title,
2346            provisional_title: None,
2347            project,
2348            running_turn: None,
2349            turn_id: 0,
2350            connection,
2351            session_id,
2352            token_usage: None,
2353            cost: None,
2354            prompt_capabilities,
2355            available_commands: Vec::new(),
2356            _observe_prompt_capabilities: task,
2357            terminals: HashMap::default(),
2358            pending_terminal_output: HashMap::default(),
2359            pending_terminal_exit: HashMap::default(),
2360            had_error: false,
2361            draft_prompt: None,
2362            ui_scroll_position: None,
2363            streaming_text_buffer: None,
2364        }
2365    }
2366
2367    pub fn parent_session_id(&self) -> Option<&acp::SessionId> {
2368        self.parent_session_id.as_ref()
2369    }
2370
2371    pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
2372        self.prompt_capabilities.clone()
2373    }
2374
2375    pub fn available_commands(&self) -> &[acp::AvailableCommand] {
2376        &self.available_commands
2377    }
2378
2379    pub fn is_draft_thread(&self) -> bool {
2380        self.entries().is_empty()
2381    }
2382
2383    pub fn draft_prompt(&self) -> Option<&[acp::ContentBlock]> {
2384        self.draft_prompt.as_deref()
2385    }
2386
2387    pub fn set_draft_prompt(
2388        &mut self,
2389        prompt: Option<Vec<acp::ContentBlock>>,
2390        cx: &mut Context<Self>,
2391    ) {
2392        cx.emit(AcpThreadEvent::PromptUpdated);
2393        self.draft_prompt = prompt;
2394    }
2395
2396    pub fn ui_scroll_position(&self) -> Option<gpui::ListOffset> {
2397        self.ui_scroll_position
2398    }
2399
2400    pub fn set_ui_scroll_position(&mut self, position: Option<gpui::ListOffset>) {
2401        self.ui_scroll_position = position;
2402    }
2403
2404    pub fn connection(&self) -> &Rc<dyn AgentConnection> {
2405        &self.connection
2406    }
2407
2408    /// Replace the connection facade while keeping the established ACP
2409    /// session.
2410    ///
2411    /// An adapter can delegate session creation to an inner ACP connection and
2412    /// then install its policy-enforcing facade here. All later prompts,
2413    /// cancellation, and executor disclosure must go through that facade.
2414    pub fn replace_connection(&mut self, connection: Rc<dyn AgentConnection>) {
2415        self.connection = connection;
2416    }
2417
2418    pub fn action_log(&self) -> &Entity<ActionLog> {
2419        &self.action_log
2420    }
2421
2422    pub fn project(&self) -> &Entity<Project> {
2423        &self.project
2424    }
2425
2426    pub fn title(&self) -> Option<SharedString> {
2427        self.title
2428            .clone()
2429            .or_else(|| self.provisional_title.clone())
2430    }
2431
2432    pub fn has_provisional_title(&self) -> bool {
2433        self.provisional_title.is_some()
2434    }
2435
2436    pub fn entries(&self) -> &[AgentThreadEntry] {
2437        &self.entries
2438    }
2439
2440    pub fn is_compacting(&self) -> bool {
2441        self.entries.last().is_some_and(|entry| {
2442            matches!(
2443                entry,
2444                AgentThreadEntry::ContextCompaction(compaction) if compaction.is_in_progress()
2445            )
2446        })
2447    }
2448
2449    pub fn invalidate_mermaid_caches(&self, cx: &mut App) {
2450        for entry in &self.entries {
2451            let chunks = match entry {
2452                AgentThreadEntry::AssistantMessage(message) => &message.chunks,
2453                _ => continue,
2454            };
2455            for chunk in chunks {
2456                let block = match chunk {
2457                    AssistantMessageChunk::Message { block, .. } => block,
2458                    AssistantMessageChunk::Thought { block, .. } => block,
2459                };
2460                if let Some(markdown) = block.markdown() {
2461                    markdown.update(cx, |markdown, cx| {
2462                        markdown.invalidate_mermaid_cache(cx);
2463                    });
2464                }
2465            }
2466        }
2467    }
2468
2469    pub fn session_id(&self) -> &acp::SessionId {
2470        &self.session_id
2471    }
2472
2473    pub fn supports_truncate(&self, cx: &App) -> bool {
2474        self.connection.truncate(&self.session_id, cx).is_some()
2475    }
2476
2477    pub fn work_dirs(&self) -> Option<&PathList> {
2478        self.work_dirs.as_ref()
2479    }
2480
2481    pub fn set_work_dirs(&mut self, work_dirs: PathList, cx: &mut Context<Self>) {
2482        self.work_dirs = Some(work_dirs);
2483        cx.emit(AcpThreadEvent::WorkingDirectoriesUpdated)
2484    }
2485
2486    pub fn status(&self) -> ThreadStatus {
2487        if self.running_turn.is_some() {
2488            ThreadStatus::Generating
2489        } else {
2490            ThreadStatus::Idle
2491        }
2492    }
2493
2494    pub fn had_error(&self) -> bool {
2495        self.had_error
2496    }
2497
2498    pub fn is_waiting_for_confirmation(&self) -> bool {
2499        for entry in self.entries.iter().rev() {
2500            match entry {
2501                AgentThreadEntry::UserMessage(_) => return false,
2502                AgentThreadEntry::ToolCall(ToolCall {
2503                    status: ToolCallStatus::WaitingForConfirmation { .. },
2504                    ..
2505                }) => return true,
2506                AgentThreadEntry::Elicitation(elicitation_id)
2507                    if self.elicitations.elicitation(elicitation_id).is_some_and(
2508                        |(_, elicitation)| {
2509                            matches!(elicitation.status, ElicitationStatus::Pending { .. })
2510                        },
2511                    ) =>
2512                {
2513                    return true;
2514                }
2515                AgentThreadEntry::ToolCall(_)
2516                | AgentThreadEntry::Elicitation(_)
2517                | AgentThreadEntry::AssistantMessage(_)
2518                | AgentThreadEntry::CompletedPlan(_)
2519                | AgentThreadEntry::ContextCompaction(_)
2520                | AgentThreadEntry::SystemNote(_) => {}
2521            }
2522        }
2523        false
2524    }
2525
2526    pub fn token_usage(&self) -> Option<&TokenUsage> {
2527        self.token_usage.as_ref()
2528    }
2529
2530    pub fn cost(&self) -> Option<&SessionCost> {
2531        self.cost.as_ref()
2532    }
2533
2534    pub fn has_pending_edit_tool_calls(&self) -> bool {
2535        for entry in self.entries.iter().rev() {
2536            match entry {
2537                AgentThreadEntry::UserMessage(_) => return false,
2538                AgentThreadEntry::ToolCall(
2539                    call @ ToolCall {
2540                        status: ToolCallStatus::InProgress | ToolCallStatus::Pending,
2541                        ..
2542                    },
2543                ) if call.diffs().next().is_some() => {
2544                    return true;
2545                }
2546                AgentThreadEntry::ToolCall(_)
2547                | AgentThreadEntry::Elicitation(_)
2548                | AgentThreadEntry::AssistantMessage(_)
2549                | AgentThreadEntry::CompletedPlan(_)
2550                | AgentThreadEntry::ContextCompaction(_)
2551                | AgentThreadEntry::SystemNote(_) => {}
2552            }
2553        }
2554
2555        false
2556    }
2557
2558    pub fn has_in_progress_tool_calls(&self) -> bool {
2559        for entry in self.entries.iter().rev() {
2560            match entry {
2561                AgentThreadEntry::UserMessage(_) => return false,
2562                AgentThreadEntry::ToolCall(ToolCall {
2563                    status: ToolCallStatus::InProgress | ToolCallStatus::Pending,
2564                    ..
2565                }) => {
2566                    return true;
2567                }
2568                AgentThreadEntry::ToolCall(_)
2569                | AgentThreadEntry::Elicitation(_)
2570                | AgentThreadEntry::AssistantMessage(_)
2571                | AgentThreadEntry::CompletedPlan(_)
2572                | AgentThreadEntry::ContextCompaction(_)
2573                | AgentThreadEntry::SystemNote(_) => {}
2574            }
2575        }
2576
2577        false
2578    }
2579
2580    pub fn used_tools_since_last_user_message(&self) -> bool {
2581        for entry in self.entries.iter().rev() {
2582            match entry {
2583                AgentThreadEntry::UserMessage(..) => return false,
2584                AgentThreadEntry::AssistantMessage(..)
2585                | AgentThreadEntry::CompletedPlan(..)
2586                | AgentThreadEntry::ContextCompaction(_)
2587                | AgentThreadEntry::SystemNote(_)
2588                | AgentThreadEntry::Elicitation(_) => continue,
2589                AgentThreadEntry::ToolCall(..) => return true,
2590            }
2591        }
2592
2593        false
2594    }
2595
2596    pub fn handle_session_update(
2597        &mut self,
2598        update: acp::SessionUpdate,
2599        cx: &mut Context<Self>,
2600    ) -> Result<(), acp::Error> {
2601        match update {
2602            acp::SessionUpdate::UserMessageChunk(acp::ContentChunk {
2603                content,
2604                message_id,
2605                ..
2606            }) => {
2607                // We optimistically add the full user prompt before calling `prompt`.
2608                // Some ACP servers echo user chunks back over updates. Skip echoed
2609                // chunks only when they match the local optimistic message.
2610                let already_in_user_message = self
2611                    .entries
2612                    .last_mut()
2613                    .and_then(|entry| match entry {
2614                        AgentThreadEntry::UserMessage(message) => Some(message),
2615                        _ => None,
2616                    })
2617                    .is_some_and(|message| {
2618                        let already_in_user_message = message.is_optimistic
2619                            && message.chunks.contains(&content)
2620                            && can_merge_message_chunks(
2621                                message.protocol_id.as_ref(),
2622                                message_id.as_ref(),
2623                            );
2624                        if already_in_user_message && message.protocol_id.is_none() {
2625                            message.protocol_id = message_id.clone();
2626                        }
2627                        already_in_user_message
2628                    });
2629                if !already_in_user_message {
2630                    self.push_user_content_block_from_agent(message_id, content, cx);
2631                }
2632            }
2633            acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
2634                content,
2635                message_id,
2636                ..
2637            }) => {
2638                self.push_assistant_content_block_with_message_id(
2639                    message_id, content, false, false, cx,
2640                );
2641            }
2642            acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk {
2643                content,
2644                message_id,
2645                ..
2646            }) => {
2647                self.push_assistant_content_block_with_message_id(
2648                    message_id, content, true, false, cx,
2649                );
2650            }
2651            acp::SessionUpdate::ToolCall(tool_call) => {
2652                self.upsert_tool_call(tool_call, cx)?;
2653            }
2654            acp::SessionUpdate::ToolCallUpdate(tool_call_update) => {
2655                self.update_tool_call(tool_call_update, cx)?;
2656            }
2657            acp::SessionUpdate::Plan(plan) => {
2658                self.update_plan(plan, cx);
2659            }
2660            acp::SessionUpdate::SessionInfoUpdate(info_update) => {
2661                if let MaybeUndefined::Value(title) = info_update.title {
2662                    let had_provisional = self.provisional_title.take().is_some();
2663                    let title: SharedString = title.into();
2664                    if self.title.as_ref() != Some(&title) {
2665                        self.title = Some(title);
2666                        cx.emit(AcpThreadEvent::TitleUpdated);
2667                    } else if had_provisional {
2668                        cx.emit(AcpThreadEvent::TitleUpdated);
2669                    }
2670                }
2671            }
2672            acp::SessionUpdate::AvailableCommandsUpdate(acp::AvailableCommandsUpdate {
2673                available_commands,
2674                ..
2675            }) => {
2676                self.available_commands = available_commands.clone();
2677                cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands));
2678            }
2679            acp::SessionUpdate::CurrentModeUpdate(acp::CurrentModeUpdate {
2680                current_mode_id,
2681                ..
2682            }) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)),
2683            acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate {
2684                config_options,
2685                ..
2686            }) => cx.emit(AcpThreadEvent::ConfigOptionsUpdated(config_options)),
2687            acp::SessionUpdate::UsageUpdate(update) => {
2688                let usage = self.token_usage.get_or_insert_with(Default::default);
2689                usage.max_tokens = update.size;
2690                usage.used_tokens = update.used;
2691                if let Some(cost) = update.cost {
2692                    self.cost = Some(SessionCost {
2693                        amount: cost.amount,
2694                        currency: cost.currency.into(),
2695                    });
2696                }
2697                cx.emit(AcpThreadEvent::TokenUsageUpdated);
2698            }
2699            _ => {}
2700        }
2701        Ok(())
2702    }
2703
2704    pub fn push_user_content_block(
2705        &mut self,
2706        client_id: Option<ClientUserMessageId>,
2707        chunk: acp::ContentBlock,
2708        cx: &mut Context<Self>,
2709    ) {
2710        self.push_user_content_block_with_indent(client_id, chunk, false, cx)
2711    }
2712
2713    pub fn push_user_content_block_with_indent(
2714        &mut self,
2715        client_id: Option<ClientUserMessageId>,
2716        chunk: acp::ContentBlock,
2717        indented: bool,
2718        cx: &mut Context<Self>,
2719    ) {
2720        self.push_user_content_block_with_protocol_id(
2721            client_id.clone(),
2722            client_id.is_some(),
2723            None,
2724            chunk,
2725            indented,
2726            cx,
2727        )
2728    }
2729
2730    fn push_user_content_block_from_agent(
2731        &mut self,
2732        id: Option<acp::MessageId>,
2733        chunk: acp::ContentBlock,
2734        cx: &mut Context<Self>,
2735    ) {
2736        self.push_user_content_block_with_protocol_id(None, false, id, chunk, false, cx)
2737    }
2738
2739    fn push_user_content_block_with_protocol_id(
2740        &mut self,
2741        incoming_client_id: Option<ClientUserMessageId>,
2742        is_optimistic: bool,
2743        protocol_id: Option<acp::MessageId>,
2744        chunk: acp::ContentBlock,
2745        indented: bool,
2746        cx: &mut Context<Self>,
2747    ) {
2748        let language_registry = self.project.read(cx).languages().clone();
2749        let path_style = self.project.read(cx).path_style(cx);
2750        let entries_len = self.entries.len();
2751
2752        if let Some(last_entry) = self.entries.last_mut()
2753            && let AgentThreadEntry::UserMessage(UserMessage {
2754                protocol_id: existing_protocol_id,
2755                client_id: existing_client_id,
2756                content,
2757                chunks,
2758                is_optimistic: existing_is_optimistic,
2759                indented: existing_indented,
2760                ..
2761            }) = last_entry
2762            && *existing_indented == indented
2763            && can_merge_message_chunks(existing_protocol_id.as_ref(), protocol_id.as_ref())
2764            && !(*existing_is_optimistic
2765                && !is_optimistic
2766                && existing_protocol_id.is_none()
2767                && protocol_id.is_some())
2768        {
2769            Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
2770            if let Some(incoming_client_id) = incoming_client_id {
2771                *existing_client_id = Some(incoming_client_id);
2772            }
2773            *existing_is_optimistic |= is_optimistic;
2774            if existing_protocol_id.is_none() {
2775                *existing_protocol_id = protocol_id;
2776            }
2777            content.append(chunk.clone(), &language_registry, path_style, cx);
2778            chunks.push(chunk);
2779            let idx = entries_len - 1;
2780            cx.emit(AcpThreadEvent::EntryUpdated(idx));
2781        } else {
2782            let content = ContentBlock::new(chunk.clone(), &language_registry, path_style, cx);
2783            self.push_entry(
2784                AgentThreadEntry::UserMessage(UserMessage {
2785                    protocol_id,
2786                    client_id: incoming_client_id,
2787                    is_optimistic,
2788                    content,
2789                    chunks: vec![chunk],
2790                    checkpoint: None,
2791                    indented,
2792                }),
2793                cx,
2794            );
2795        }
2796    }
2797
2798    pub fn push_assistant_content_block(
2799        &mut self,
2800        chunk: acp::ContentBlock,
2801        is_thought: bool,
2802        cx: &mut Context<Self>,
2803    ) {
2804        self.push_assistant_content_block_with_indent(chunk, is_thought, false, cx)
2805    }
2806
2807    pub fn push_assistant_content_block_with_indent(
2808        &mut self,
2809        chunk: acp::ContentBlock,
2810        is_thought: bool,
2811        indented: bool,
2812        cx: &mut Context<Self>,
2813    ) {
2814        self.push_assistant_content_block_with_message_id(None, chunk, is_thought, indented, cx)
2815    }
2816
2817    fn push_assistant_content_block_with_message_id(
2818        &mut self,
2819        message_id: Option<acp::MessageId>,
2820        chunk: acp::ContentBlock,
2821        is_thought: bool,
2822        indented: bool,
2823        cx: &mut Context<Self>,
2824    ) {
2825        let path_style = self.project.read(cx).path_style(cx);
2826
2827        // For text chunks going to an existing Markdown block, buffer for smooth
2828        // streaming instead of appending all at once which may feel more choppy.
2829        if let acp::ContentBlock::Text(text_content) = &chunk {
2830            if let Some(markdown) =
2831                self.streaming_markdown_target(message_id.as_ref(), is_thought, indented)
2832            {
2833                let entries_len = self.entries.len();
2834                cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
2835                self.buffer_streaming_text(&markdown, text_content.text.clone(), cx);
2836                return;
2837            }
2838        }
2839
2840        let language_registry = self.project.read(cx).languages().clone();
2841        let entries_len = self.entries.len();
2842        if let Some(last_entry) = self.entries.last_mut()
2843            && let AgentThreadEntry::AssistantMessage(AssistantMessage {
2844                chunks,
2845                indented: existing_indented,
2846                is_subagent_output: _,
2847            }) = last_entry
2848            && *existing_indented == indented
2849        {
2850            let idx = entries_len - 1;
2851            Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
2852            cx.emit(AcpThreadEvent::EntryUpdated(idx));
2853            match (chunks.last_mut(), is_thought) {
2854                (
2855                    Some(AssistantMessageChunk::Message {
2856                        id: existing_id,
2857                        block,
2858                    }),
2859                    false,
2860                )
2861                | (
2862                    Some(AssistantMessageChunk::Thought {
2863                        id: existing_id,
2864                        block,
2865                    }),
2866                    true,
2867                ) if can_merge_message_chunks(existing_id.as_ref(), message_id.as_ref()) => {
2868                    if existing_id.is_none() {
2869                        *existing_id = message_id;
2870                    }
2871                    block.append(chunk, &language_registry, path_style, cx)
2872                }
2873                _ => {
2874                    let block = ContentBlock::new(chunk, &language_registry, path_style, cx);
2875                    if is_thought {
2876                        chunks.push(AssistantMessageChunk::Thought {
2877                            id: message_id,
2878                            block,
2879                        })
2880                    } else {
2881                        chunks.push(AssistantMessageChunk::Message {
2882                            id: message_id,
2883                            block,
2884                        })
2885                    }
2886                }
2887            }
2888        } else {
2889            let block = ContentBlock::new(chunk, &language_registry, path_style, cx);
2890            let chunk = if is_thought {
2891                AssistantMessageChunk::Thought {
2892                    id: message_id,
2893                    block,
2894                }
2895            } else {
2896                AssistantMessageChunk::Message {
2897                    id: message_id,
2898                    block,
2899                }
2900            };
2901
2902            self.push_entry(
2903                AgentThreadEntry::AssistantMessage(AssistantMessage {
2904                    chunks: vec![chunk],
2905                    indented,
2906                    is_subagent_output: false,
2907                }),
2908                cx,
2909            );
2910        }
2911    }
2912
2913    fn streaming_markdown_target(
2914        &mut self,
2915        message_id: Option<&acp::MessageId>,
2916        is_thought: bool,
2917        indented: bool,
2918    ) -> Option<Entity<Markdown>> {
2919        let last_entry = self.entries.last_mut()?;
2920        if let AgentThreadEntry::AssistantMessage(AssistantMessage {
2921            chunks,
2922            indented: existing_indented,
2923            ..
2924        }) = last_entry
2925            && *existing_indented == indented
2926            && let [.., chunk] = chunks.as_mut_slice()
2927        {
2928            match (chunk, is_thought) {
2929                (
2930                    AssistantMessageChunk::Message {
2931                        id: existing_id,
2932                        block: ContentBlock::Markdown { markdown },
2933                    },
2934                    false,
2935                )
2936                | (
2937                    AssistantMessageChunk::Thought {
2938                        id: existing_id,
2939                        block: ContentBlock::Markdown { markdown },
2940                    },
2941                    true,
2942                ) if can_merge_message_chunks(existing_id.as_ref(), message_id) => {
2943                    if existing_id.is_none() {
2944                        *existing_id = message_id.cloned();
2945                    }
2946                    Some(markdown.clone())
2947                }
2948                _ => None,
2949            }
2950        } else {
2951            None
2952        }
2953    }
2954
2955    /// Add text to the streaming buffer. If the target changed (e.g. switching
2956    /// from thoughts to message text), flush the old buffer first.
2957    fn buffer_streaming_text(
2958        &mut self,
2959        markdown: &Entity<Markdown>,
2960        text: String,
2961        cx: &mut Context<Self>,
2962    ) {
2963        if let Some(buffer) = &mut self.streaming_text_buffer {
2964            if buffer.target.entity_id() == markdown.entity_id() {
2965                buffer.pending.push_str(&text);
2966
2967                buffer.bytes_to_reveal_per_tick = (buffer.pending.len() as f32
2968                    / StreamingTextBuffer::REVEAL_TARGET
2969                    * StreamingTextBuffer::TASK_UPDATE_MS as f32)
2970                    .ceil() as usize;
2971                return;
2972            }
2973            Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
2974        }
2975
2976        let target = markdown.clone();
2977        let _reveal_task = self.start_streaming_reveal(cx);
2978        let pending_len = text.len();
2979        let bytes_to_reveal = (pending_len as f32 / StreamingTextBuffer::REVEAL_TARGET
2980            * StreamingTextBuffer::TASK_UPDATE_MS as f32)
2981            .ceil() as usize;
2982        self.streaming_text_buffer = Some(StreamingTextBuffer {
2983            pending: text,
2984            bytes_to_reveal_per_tick: bytes_to_reveal,
2985            target,
2986            _reveal_task,
2987        });
2988    }
2989
2990    /// Flush all buffered streaming text into the Markdown entity immediately.
2991    fn flush_streaming_text(
2992        streaming_text_buffer: &mut Option<StreamingTextBuffer>,
2993        cx: &mut Context<Self>,
2994    ) {
2995        if let Some(buffer) = streaming_text_buffer.take() {
2996            if !buffer.pending.is_empty() {
2997                buffer
2998                    .target
2999                    .update(cx, |markdown, cx| markdown.append(&buffer.pending, cx));
3000            }
3001        }
3002    }
3003
3004    /// Spawns a foreground task that periodically drains
3005    /// `streaming_text_buffer.pending` into the target `Markdown` entity,
3006    /// producing smooth, continuous text output.
3007    fn start_streaming_reveal(&self, cx: &mut Context<Self>) -> Task<()> {
3008        cx.spawn(async move |this, cx| {
3009            loop {
3010                cx.background_executor()
3011                    .timer(Duration::from_millis(StreamingTextBuffer::TASK_UPDATE_MS))
3012                    .await;
3013
3014                let should_continue = this
3015                    .update(cx, |this, cx| {
3016                        let Some(buffer) = &mut this.streaming_text_buffer else {
3017                            return false;
3018                        };
3019
3020                        if buffer.pending.is_empty() {
3021                            return true;
3022                        }
3023
3024                        let pending_len = buffer.pending.len();
3025
3026                        let byte_boundary = buffer
3027                            .pending
3028                            .ceil_char_boundary(buffer.bytes_to_reveal_per_tick)
3029                            .min(pending_len);
3030
3031                        buffer.target.update(cx, |markdown: &mut Markdown, cx| {
3032                            markdown.append(&buffer.pending[..byte_boundary], cx);
3033                            buffer.pending.drain(..byte_boundary);
3034                        });
3035
3036                        true
3037                    })
3038                    .unwrap_or(false);
3039
3040                if !should_continue {
3041                    break;
3042                }
3043            }
3044        })
3045    }
3046
3047    fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
3048        Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
3049        self.entries.push(entry);
3050        cx.emit(AcpThreadEvent::NewEntry);
3051    }
3052
3053    /// OMEGA-DELTA-0045. Append a host-authored note to the thread the owner
3054    /// reads. Returns `false` when a note with this id is already present.
3055    ///
3056    /// Idempotent on the id rather than last-write-wins: the note is a record
3057    /// that something happened, so a retry must not be able to rewrite what
3058    /// the owner was already shown, and must not double it either.
3059    pub fn push_system_note(&mut self, note: SystemNote, cx: &mut Context<Self>) -> bool {
3060        if self
3061            .entries
3062            .iter()
3063            .any(|entry| matches!(entry, AgentThreadEntry::SystemNote(existing) if existing.id == note.id))
3064        {
3065            return false;
3066        }
3067        self.push_entry(AgentThreadEntry::SystemNote(note), cx);
3068        true
3069    }
3070
3071    pub fn push_context_compaction(
3072        &mut self,
3073        compaction: ContextCompaction,
3074        cx: &mut Context<Self>,
3075    ) {
3076        if let Some(ix) =
3077            self.entries
3078                .iter()
3079                .enumerate()
3080                .rev()
3081                .find_map(|(ix, entry)| match entry {
3082                    AgentThreadEntry::ContextCompaction(c) if &c.id == &compaction.id => Some(ix),
3083                    _ => None,
3084                })
3085        {
3086            self.entries[ix] = AgentThreadEntry::ContextCompaction(compaction);
3087            cx.emit(AcpThreadEvent::EntryUpdated(ix));
3088        } else {
3089            self.push_entry(AgentThreadEntry::ContextCompaction(compaction), cx);
3090        }
3091    }
3092
3093    pub fn update_context_compaction(
3094        &mut self,
3095        update: ContextCompactionUpdate,
3096        cx: &mut Context<Self>,
3097    ) {
3098        let language_registry = self.project.read(cx).languages().clone();
3099        let Some((ix, compaction)) =
3100            self.entries
3101                .iter_mut()
3102                .enumerate()
3103                .rev()
3104                .find_map(|(ix, entry)| match entry {
3105                    AgentThreadEntry::ContextCompaction(c) if &c.id == &update.id => Some((ix, c)),
3106                    _ => None,
3107                })
3108        else {
3109            return;
3110        };
3111
3112        if !update.summary_delta.is_empty() {
3113            if compaction.summary.is_none() {
3114                compaction.summary = Some(cx.new(|cx| {
3115                    Markdown::new(
3116                        update.summary_delta.into(),
3117                        Some(language_registry),
3118                        None,
3119                        cx,
3120                    )
3121                }));
3122            } else if let Some(summary) = compaction.summary.clone() {
3123                summary.update(cx, |markdown, cx| {
3124                    markdown.append(&update.summary_delta, cx)
3125                });
3126            }
3127        }
3128
3129        if let Some(status) = update.status {
3130            compaction.status = status;
3131        }
3132
3133        cx.emit(AcpThreadEvent::EntryUpdated(ix));
3134    }
3135
3136    pub fn can_set_title(&mut self, cx: &mut Context<Self>) -> bool {
3137        self.connection.set_title(&self.session_id, cx).is_some()
3138    }
3139
3140    pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) -> Task<Result<()>> {
3141        let had_provisional = self.provisional_title.take().is_some();
3142        if self.title.as_ref() != Some(&title) {
3143            self.title = Some(title.clone());
3144            cx.emit(AcpThreadEvent::TitleUpdated);
3145            if let Some(set_title) = self.connection.set_title(&self.session_id, cx) {
3146                return set_title.run(title, cx);
3147            }
3148        } else if had_provisional {
3149            cx.emit(AcpThreadEvent::TitleUpdated);
3150        }
3151        Task::ready(Ok(()))
3152    }
3153
3154    /// Sets a provisional display title without propagating back to the
3155    /// underlying agent connection. This is used for quick preview titles
3156    /// (e.g. first 20 chars of the user message) that should be shown
3157    /// immediately but replaced once the LLM generates a proper title via
3158    /// `set_title`.
3159    pub fn set_provisional_title(&mut self, title: SharedString, cx: &mut Context<Self>) {
3160        self.provisional_title = Some(title);
3161        cx.emit(AcpThreadEvent::TitleUpdated);
3162    }
3163
3164    pub fn subagent_spawned(&mut self, session_id: acp::SessionId, cx: &mut Context<Self>) {
3165        cx.emit(AcpThreadEvent::SubagentSpawned(session_id));
3166    }
3167
3168    pub fn update_token_usage(&mut self, usage: Option<TokenUsage>, cx: &mut Context<Self>) {
3169        if usage.is_none() {
3170            self.cost = None;
3171        }
3172        self.token_usage = usage;
3173        cx.emit(AcpThreadEvent::TokenUsageUpdated);
3174    }
3175
3176    pub fn update_retry_status(&mut self, status: RetryStatus, cx: &mut Context<Self>) {
3177        cx.emit(AcpThreadEvent::Retry(status));
3178    }
3179
3180    pub fn update_tool_call(
3181        &mut self,
3182        update: impl Into<ToolCallUpdate>,
3183        cx: &mut Context<Self>,
3184    ) -> Result<()> {
3185        let update = update.into();
3186        let languages = self.project.read(cx).languages().clone();
3187        let path_style = self.project.read(cx).path_style(cx);
3188
3189        let ix = match self.index_for_tool_call(update.id()) {
3190            Some(ix) => ix,
3191            None => {
3192                // Tool call not found - create a failed tool call entry
3193                let failed_tool_call = ToolCall {
3194                    id: update.id().clone(),
3195                    label: cx.new(|cx| Markdown::new("Tool call not found".into(), None, None, cx)),
3196                    kind: acp::ToolKind::Fetch,
3197                    content: vec![ToolCallContent::ContentBlock(ContentBlock::new(
3198                        "Tool call not found".into(),
3199                        &languages,
3200                        path_style,
3201                        cx,
3202                    ))],
3203                    status: ToolCallStatus::Failed,
3204                    locations: Vec::new(),
3205                    resolved_locations: Vec::new(),
3206                    raw_input: None,
3207                    raw_input_markdown: None,
3208                    raw_output: None,
3209                    tool_name: None,
3210                    subagent_session_info: None,
3211                    sandbox_authorization_details: None,
3212                    sandbox_fallback_authorization_details: None,
3213                    sandbox_not_applied: None,
3214                };
3215                self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
3216                return Ok(());
3217            }
3218        };
3219        let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
3220            unreachable!()
3221        };
3222
3223        match update {
3224            ToolCallUpdate::UpdateFields(update) => {
3225                let location_updated = update.fields.locations.is_some();
3226                call.update_fields(
3227                    update.fields,
3228                    update.meta,
3229                    languages,
3230                    path_style,
3231                    &self.terminals,
3232                    cx,
3233                )?;
3234                if location_updated {
3235                    self.resolve_locations(update.tool_call_id, cx);
3236                }
3237            }
3238            ToolCallUpdate::UpdateDiff(update) => {
3239                call.content.clear();
3240                call.content.push(ToolCallContent::Diff(update.diff));
3241            }
3242            ToolCallUpdate::UpdateTerminal(update) => {
3243                call.content.clear();
3244                call.content
3245                    .push(ToolCallContent::Terminal(update.terminal));
3246            }
3247        }
3248
3249        cx.emit(AcpThreadEvent::EntryUpdated(ix));
3250
3251        Ok(())
3252    }
3253
3254    /// Updates a tool call if id matches an existing entry, otherwise inserts a new one.
3255    pub fn upsert_tool_call(
3256        &mut self,
3257        tool_call: acp::ToolCall,
3258        cx: &mut Context<Self>,
3259    ) -> Result<(), acp::Error> {
3260        let status = tool_call.status.into();
3261        self.upsert_tool_call_inner(tool_call.into(), status, cx)
3262    }
3263
3264    /// Fails if id does not match an existing entry.
3265    pub fn upsert_tool_call_inner(
3266        &mut self,
3267        update: acp::ToolCallUpdate,
3268        status: ToolCallStatus,
3269        cx: &mut Context<Self>,
3270    ) -> Result<(), acp::Error> {
3271        let language_registry = self.project.read(cx).languages().clone();
3272        let path_style = self.project.read(cx).path_style(cx);
3273        let id = update.tool_call_id.clone();
3274
3275        let agent_telemetry_id = self.connection().telemetry_id();
3276        let session = self.session_id();
3277        let parent_session_id = self.parent_session_id();
3278        if let ToolCallStatus::Completed | ToolCallStatus::Failed = status {
3279            let status = if matches!(status, ToolCallStatus::Completed) {
3280                "completed"
3281            } else {
3282                "failed"
3283            };
3284            telemetry::event!(
3285                "Agent Tool Call Completed",
3286                agent_telemetry_id,
3287                session,
3288                parent_session_id,
3289                status
3290            );
3291        }
3292
3293        if let Some(ix) = self.index_for_tool_call(&id) {
3294            let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
3295                unreachable!()
3296            };
3297
3298            call.update_fields(
3299                update.fields,
3300                update.meta,
3301                language_registry,
3302                path_style,
3303                &self.terminals,
3304                cx,
3305            )?;
3306            call.update_status(status);
3307
3308            cx.emit(AcpThreadEvent::EntryUpdated(ix));
3309        } else {
3310            let call = ToolCall::from_acp(
3311                update.try_into()?,
3312                status,
3313                language_registry,
3314                self.project.read(cx).path_style(cx),
3315                &self.terminals,
3316                cx,
3317            )?;
3318            self.push_entry(AgentThreadEntry::ToolCall(call), cx);
3319        };
3320
3321        self.resolve_locations(id, cx);
3322        Ok(())
3323    }
3324
3325    fn index_for_tool_call(&self, id: &acp::ToolCallId) -> Option<usize> {
3326        self.entries
3327            .iter()
3328            .enumerate()
3329            .rev()
3330            .find_map(|(index, entry)| {
3331                if let AgentThreadEntry::ToolCall(tool_call) = entry
3332                    && &tool_call.id == id
3333                {
3334                    Some(index)
3335                } else {
3336                    None
3337                }
3338            })
3339    }
3340
3341    fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> {
3342        // The tool call we are looking for is typically the last one, or very close to the end.
3343        // At the moment, it doesn't seem like a hashmap would be a good fit for this use case.
3344        self.entries
3345            .iter_mut()
3346            .enumerate()
3347            .rev()
3348            .find_map(|(index, tool_call)| {
3349                if let AgentThreadEntry::ToolCall(tool_call) = tool_call
3350                    && &tool_call.id == id
3351                {
3352                    Some((index, tool_call))
3353                } else {
3354                    None
3355                }
3356            })
3357    }
3358
3359    pub fn tool_call(&self, id: &acp::ToolCallId) -> Option<(usize, &ToolCall)> {
3360        self.entries
3361            .iter()
3362            .enumerate()
3363            .rev()
3364            .find_map(|(index, tool_call)| {
3365                if let AgentThreadEntry::ToolCall(tool_call) = tool_call
3366                    && &tool_call.id == id
3367                {
3368                    Some((index, tool_call))
3369                } else {
3370                    None
3371                }
3372            })
3373    }
3374
3375    pub fn tool_call_for_subagent(&self, session_id: &acp::SessionId) -> Option<&ToolCall> {
3376        self.entries.iter().find_map(|entry| match entry {
3377            AgentThreadEntry::ToolCall(tool_call) => {
3378                if let Some(subagent_session_info) = &tool_call.subagent_session_info
3379                    && &subagent_session_info.session_id == session_id
3380                {
3381                    Some(tool_call)
3382                } else {
3383                    None
3384                }
3385            }
3386            _ => None,
3387        })
3388    }
3389
3390    pub fn resolve_locations(&mut self, id: acp::ToolCallId, cx: &mut Context<Self>) {
3391        let project = self.project.clone();
3392        let should_update_agent_location = self.parent_session_id.is_none();
3393        let Some((_, tool_call)) = self.tool_call_mut(&id) else {
3394            return;
3395        };
3396        let task = tool_call.resolve_locations(project, cx);
3397        cx.spawn(async move |this, cx| {
3398            let resolved_locations = task.await;
3399
3400            this.update(cx, |this, cx| {
3401                let project = this.project.clone();
3402
3403                for location in resolved_locations.iter().flatten() {
3404                    this.shared_buffers
3405                        .insert(location.buffer.clone(), location.buffer.read(cx).snapshot());
3406                }
3407                let Some((ix, tool_call)) = this.tool_call_mut(&id) else {
3408                    return;
3409                };
3410
3411                if let Some(Some(location)) = resolved_locations.last() {
3412                    project.update(cx, |project, cx| {
3413                        let should_ignore = if let Some(agent_location) = project
3414                            .agent_location()
3415                            .filter(|agent_location| agent_location.buffer == location.buffer)
3416                        {
3417                            let snapshot = location.buffer.read(cx).snapshot();
3418                            let old_position = agent_location.position.to_point(&snapshot);
3419                            let new_position = location.position.to_point(&snapshot);
3420
3421                            // ignore this so that when we get updates from the edit tool
3422                            // the position doesn't reset to the startof line
3423                            old_position.row == new_position.row
3424                                && old_position.column > new_position.column
3425                        } else {
3426                            false
3427                        };
3428                        if !should_ignore && should_update_agent_location {
3429                            project.set_agent_location(Some(location.into()), cx);
3430                        }
3431                    });
3432                }
3433
3434                let resolved_locations = resolved_locations
3435                    .iter()
3436                    .map(|l| l.as_ref().map(|l| AgentLocation::from(l)))
3437                    .collect::<Vec<_>>();
3438
3439                if tool_call.resolved_locations != resolved_locations {
3440                    tool_call.resolved_locations = resolved_locations;
3441                    cx.emit(AcpThreadEvent::EntryUpdated(ix));
3442                }
3443            })
3444        })
3445        .detach();
3446    }
3447
3448    pub fn request_tool_call_authorization(
3449        &mut self,
3450        tool_call: acp::ToolCallUpdate,
3451        options: PermissionOptions,
3452        kind: AuthorizationKind,
3453        cx: &mut Context<Self>,
3454    ) -> Result<Task<RequestPermissionOutcome>> {
3455        let (tx, rx) = oneshot::channel();
3456
3457        let current_status = self
3458            .tool_call(&tool_call.tool_call_id)
3459            .and_then(|(_, tool_call)| tool_call.status.as_acp_status())
3460            .or(tool_call.fields.status)
3461            .unwrap_or(acp::ToolCallStatus::Pending);
3462        let status = ToolCallStatus::WaitingForConfirmation {
3463            current_status,
3464            options,
3465            respond_tx: tx,
3466            kind,
3467        };
3468
3469        let tool_call_id = tool_call.tool_call_id.clone();
3470        self.upsert_tool_call_inner(tool_call, status, cx)?;
3471        cx.emit(AcpThreadEvent::ToolAuthorizationRequested(
3472            tool_call_id.clone(),
3473        ));
3474
3475        Ok(cx.spawn(async move |this, cx| {
3476            let outcome = match rx.await {
3477                Ok(outcome) => RequestPermissionOutcome::Selected(outcome),
3478                Err(oneshot::Canceled) => RequestPermissionOutcome::Cancelled,
3479            };
3480            this.update(cx, |_this, cx| {
3481                cx.emit(AcpThreadEvent::ToolAuthorizationReceived(tool_call_id))
3482            })
3483            .ok();
3484            outcome
3485        }))
3486    }
3487
3488    pub fn cancel_tool_call_authorization(&mut self, id: &acp::ToolCallId, cx: &mut Context<Self>) {
3489        let Some((ix, call)) = self.tool_call_mut(id) else {
3490            return;
3491        };
3492        if !matches!(call.status, ToolCallStatus::WaitingForConfirmation { .. }) {
3493            return;
3494        }
3495
3496        call.status = ToolCallStatus::Canceled;
3497        cx.emit(AcpThreadEvent::EntryUpdated(ix));
3498        cx.emit(AcpThreadEvent::ToolAuthorizationReceived(id.clone()));
3499    }
3500
3501    pub fn authorize_tool_call(
3502        &mut self,
3503        id: acp::ToolCallId,
3504        outcome: SelectedPermissionOutcome,
3505        cx: &mut Context<Self>,
3506    ) {
3507        let Some((ix, call)) = self.tool_call_mut(&id) else {
3508            return;
3509        };
3510
3511        let new_status =
3512            match &call.status {
3513                ToolCallStatus::WaitingForConfirmation {
3514                    kind: AuthorizationKind::ActionChoice,
3515                    ..
3516                } => ToolCallStatus::InProgress,
3517                ToolCallStatus::WaitingForConfirmation { current_status, .. } => {
3518                    match outcome.option_kind {
3519                        acp::PermissionOptionKind::RejectOnce
3520                        | acp::PermissionOptionKind::RejectAlways => ToolCallStatus::Rejected,
3521                        acp::PermissionOptionKind::AllowOnce
3522                        | acp::PermissionOptionKind::AllowAlways => {
3523                            ToolCallStatus::status_after_permission_grant(*current_status)
3524                        }
3525                        _ => ToolCallStatus::status_after_permission_grant(*current_status),
3526                    }
3527                }
3528                _ => match outcome.option_kind {
3529                    acp::PermissionOptionKind::RejectOnce
3530                    | acp::PermissionOptionKind::RejectAlways => ToolCallStatus::Rejected,
3531                    acp::PermissionOptionKind::AllowOnce
3532                    | acp::PermissionOptionKind::AllowAlways => ToolCallStatus::InProgress,
3533                    _ => ToolCallStatus::InProgress,
3534                },
3535            };
3536
3537        let curr_status = mem::replace(&mut call.status, new_status);
3538
3539        if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status {
3540            respond_tx.send(outcome).ok();
3541        }
3542
3543        cx.emit(AcpThreadEvent::EntryUpdated(ix));
3544    }
3545
3546    pub fn request_elicitation(
3547        &mut self,
3548        request: acp::CreateElicitationRequest,
3549        cx: &mut Context<Self>,
3550    ) -> Result<Task<acp::CreateElicitationResponse>, acp::Error> {
3551        self.request_elicitation_with_id(request, cx)
3552            .map(|(_, task)| task)
3553    }
3554
3555    pub fn request_elicitation_with_id(
3556        &mut self,
3557        request: acp::CreateElicitationRequest,
3558        cx: &mut Context<Self>,
3559    ) -> Result<(ElicitationEntryId, Task<acp::CreateElicitationResponse>), acp::Error> {
3560        ElicitationStore::validate_request(&request)?;
3561
3562        let (id, response_rx) = self.elicitations.insert_pending_elicitation(request);
3563        self.push_entry(AgentThreadEntry::Elicitation(id.clone()), cx);
3564        cx.emit(AcpThreadEvent::ElicitationRequested(id.clone()));
3565
3566        let task =
3567            ElicitationStore::response_task(id.clone(), response_rx, cx, |_thread, cx, id| {
3568                cx.emit(AcpThreadEvent::ElicitationResponded(id))
3569            });
3570
3571        Ok((id, task))
3572    }
3573
3574    pub fn respond_to_elicitation(
3575        &mut self,
3576        id: &ElicitationEntryId,
3577        response: acp::CreateElicitationResponse,
3578        cx: &mut Context<Self>,
3579    ) {
3580        let Some(ix) = self.elicitation_entry_ix(id) else {
3581            return;
3582        };
3583        if !self.elicitations.respond_to_elicitation_by_id(id, response) {
3584            return;
3585        }
3586
3587        cx.emit(AcpThreadEvent::EntryUpdated(ix));
3588    }
3589
3590    pub fn complete_url_elicitation(
3591        &mut self,
3592        elicitation_id: &acp::ElicitationId,
3593        cx: &mut Context<Self>,
3594    ) {
3595        let Some(entry_id) = self
3596            .elicitations
3597            .entry_id_for_url_elicitation(elicitation_id)
3598        else {
3599            return;
3600        };
3601        let Some(ix) = self.elicitation_entry_ix(&entry_id) else {
3602            return;
3603        };
3604        if !self.elicitations.complete_url_elicitation_by_id(&entry_id) {
3605            return;
3606        }
3607
3608        cx.emit(AcpThreadEvent::EntryUpdated(ix));
3609    }
3610
3611    pub fn cancel_elicitation(&mut self, id: &ElicitationEntryId, cx: &mut Context<Self>) {
3612        let Some(ix) = self.elicitation_entry_ix(id) else {
3613            return;
3614        };
3615        if !self.elicitations.cancel_elicitation_by_id(id, true) {
3616            return;
3617        }
3618
3619        cx.emit(AcpThreadEvent::EntryUpdated(ix));
3620    }
3621
3622    fn elicitation_entry_ix(&self, id: &ElicitationEntryId) -> Option<usize> {
3623        self.entries
3624            .iter()
3625            .enumerate()
3626            .rev()
3627            .find_map(|(index, entry)| {
3628                matches!(entry, AgentThreadEntry::Elicitation(elicitation_id) if elicitation_id == id)
3629                    .then_some(index)
3630            })
3631    }
3632
3633    pub fn elicitation(&self, id: &ElicitationEntryId) -> Option<(usize, &Elicitation)> {
3634        let index = self.elicitation_entry_ix(id)?;
3635        let (_, elicitation) = self.elicitations.elicitation(id)?;
3636        Some((index, elicitation))
3637    }
3638
3639    pub fn plan(&self) -> &Plan {
3640        &self.plan
3641    }
3642
3643    pub fn update_plan(&mut self, request: acp::Plan, cx: &mut Context<Self>) {
3644        let new_entries_len = request.entries.len();
3645        let mut new_entries = request.entries.into_iter();
3646
3647        // Reuse existing markdown to prevent flickering
3648        for (old, new) in self.plan.entries.iter_mut().zip(new_entries.by_ref()) {
3649            let PlanEntry {
3650                content,
3651                priority,
3652                status,
3653            } = old;
3654            content.update(cx, |old, cx| {
3655                old.replace(new.content, cx);
3656            });
3657            *priority = new.priority;
3658            *status = new.status;
3659        }
3660        for new in new_entries {
3661            self.plan.entries.push(PlanEntry::from_acp(new, cx))
3662        }
3663        self.plan.entries.truncate(new_entries_len);
3664
3665        cx.notify();
3666    }
3667
3668    pub fn snapshot_completed_plan(&mut self, cx: &mut Context<Self>) {
3669        if !self.plan.is_empty() && self.plan.stats().pending == 0 {
3670            let completed_entries = std::mem::take(&mut self.plan.entries);
3671            self.push_entry(AgentThreadEntry::CompletedPlan(completed_entries), cx);
3672        }
3673    }
3674
3675    fn clear_completed_plan_entries(&mut self, cx: &mut Context<Self>) {
3676        self.plan
3677            .entries
3678            .retain(|entry| !matches!(entry.status, acp::PlanEntryStatus::Completed));
3679        cx.notify();
3680    }
3681
3682    pub fn clear_plan(&mut self, cx: &mut Context<Self>) {
3683        self.plan.entries.clear();
3684        cx.notify();
3685    }
3686
3687    #[cfg(any(test, feature = "test-support"))]
3688    pub fn send_raw(
3689        &mut self,
3690        message: &str,
3691        cx: &mut Context<Self>,
3692    ) -> BoxFuture<'static, Result<Option<acp::PromptResponse>>> {
3693        self.send(vec![message.into()], cx)
3694    }
3695
3696    pub fn send(
3697        &mut self,
3698        message: Vec<acp::ContentBlock>,
3699        cx: &mut Context<Self>,
3700    ) -> BoxFuture<'static, Result<Option<acp::PromptResponse>>> {
3701        self.send_inner(message, true, cx)
3702    }
3703
3704    /// Sends a prompt without displaying a user-message bubble for it.
3705    /// This is used for native slash commands (e.g. `/compact`) that run a turn
3706    /// which produces its own thread entry (like the compaction summary). The
3707    /// typed command isn't sent to the model as an ordinary user turn.
3708    pub fn send_command(
3709        &mut self,
3710        message: Vec<acp::ContentBlock>,
3711        cx: &mut Context<Self>,
3712    ) -> BoxFuture<'static, Result<Option<acp::PromptResponse>>> {
3713        self.send_inner(message, false, cx)
3714    }
3715
3716    fn send_inner(
3717        &mut self,
3718        message: Vec<acp::ContentBlock>,
3719        push_user_message: bool,
3720        cx: &mut Context<Self>,
3721    ) -> BoxFuture<'static, Result<Option<acp::PromptResponse>>> {
3722        let block = ContentBlock::new_combined(
3723            message.clone(),
3724            self.project.read(cx).languages().clone(),
3725            self.project.read(cx).path_style(cx),
3726            cx,
3727        );
3728        let request = acp::PromptRequest::new(self.session_id.clone(), message.clone());
3729        let git_store = self.project.read(cx).git_store().clone();
3730
3731        let client_user_message_ids = self.connection.client_user_message_ids(cx);
3732        let client_id = client_user_message_ids
3733            .as_ref()
3734            .map(|client_user_message_ids| client_user_message_ids.new_id());
3735
3736        self.run_turn(cx, async move |this, cx| {
3737            if push_user_message {
3738                this.update(cx, |this, cx| {
3739                    this.push_entry(
3740                        AgentThreadEntry::UserMessage(UserMessage {
3741                            protocol_id: None,
3742                            client_id: client_id.clone(),
3743                            is_optimistic: true,
3744                            content: block,
3745                            chunks: message,
3746                            checkpoint: None,
3747                            indented: false,
3748                        }),
3749                        cx,
3750                    );
3751                })
3752                .ok();
3753
3754                let old_checkpoint = git_store
3755                    .update(cx, |git, cx| git.checkpoint(cx))
3756                    .await
3757                    .context("failed to get old checkpoint")
3758                    .log_err();
3759                this.update(cx, |this, _cx| {
3760                    if let Some((_ix, message)) = this.last_user_message() {
3761                        message.checkpoint = old_checkpoint.map(|git_checkpoint| Checkpoint {
3762                            git_checkpoint,
3763                            show: false,
3764                        });
3765                    }
3766                })
3767                .ok();
3768            }
3769
3770            this.update(cx, |this, cx| {
3771                if let (Some(prompt), Some(client_id)) = (client_user_message_ids, client_id) {
3772                    prompt.prompt(client_id, request, cx)
3773                } else {
3774                    this.connection.prompt(request, cx)
3775                }
3776            })?
3777            .await
3778        })
3779    }
3780
3781    pub fn can_retry(&self, cx: &App) -> bool {
3782        self.connection.retry(&self.session_id, cx).is_some()
3783    }
3784
3785    pub fn retry(
3786        &mut self,
3787        cx: &mut Context<Self>,
3788    ) -> BoxFuture<'static, Result<Option<acp::PromptResponse>>> {
3789        self.run_turn(cx, async move |this, cx| {
3790            this.update(cx, |this, cx| {
3791                this.connection
3792                    .retry(&this.session_id, cx)
3793                    .map(|retry| retry.run(cx))
3794            })?
3795            .context("retrying a session is not supported")?
3796            .await
3797        })
3798    }
3799
3800    fn run_turn(
3801        &mut self,
3802        cx: &mut Context<Self>,
3803        f: impl 'static + AsyncFnOnce(WeakEntity<Self>, &mut AsyncApp) -> Result<acp::PromptResponse>,
3804    ) -> BoxFuture<'static, Result<Option<acp::PromptResponse>>> {
3805        self.clear_completed_plan_entries(cx);
3806        self.had_error = false;
3807
3808        let (tx, rx) = oneshot::channel();
3809        let cancel_task = self.cancel(cx);
3810
3811        self.turn_id += 1;
3812        let turn_id = self.turn_id;
3813        self.running_turn = Some(RunningTurn {
3814            id: turn_id,
3815            send_task: cx.spawn(async move |this, cx| {
3816                cancel_task.await;
3817                tx.send(f(this, cx).await).ok();
3818            }),
3819        });
3820        cx.emit(AcpThreadEvent::StatusChanged);
3821
3822        cx.spawn(async move |this, cx| {
3823            let response = rx.await;
3824
3825            this.update(cx, |this, cx| this.update_last_checkpoint(cx))?
3826                .await?;
3827
3828            this.update(cx, |this, cx| {
3829                if this.parent_session_id.is_none() {
3830                    this.project
3831                        .update(cx, |project, cx| project.set_agent_location(None, cx));
3832                }
3833
3834                let is_same_turn = this
3835                    .running_turn
3836                    .as_ref()
3837                    .is_some_and(|turn| turn_id == turn.id);
3838
3839                // If the user submitted a follow up message, running_turn might
3840                // already point to a different turn. Therefore we only want to
3841                // take the task if it's the same turn. We do this before the
3842                // dropped-tx guard below so the panel exits its generating
3843                // state even when the send_task is cancelled before tx.send().
3844                if is_same_turn {
3845                    this.running_turn.take();
3846                }
3847
3848                let Ok(response) = response else {
3849                    if is_same_turn {
3850                        cx.emit(AcpThreadEvent::StatusChanged);
3851                    }
3852                    // tx dropped, just return
3853                    return Ok(None);
3854                };
3855
3856                match response {
3857                    Ok(r) => {
3858                        Self::flush_streaming_text(&mut this.streaming_text_buffer, cx);
3859
3860                        if r.stop_reason == acp::StopReason::MaxTokens {
3861                            if is_same_turn {
3862                                cx.emit(AcpThreadEvent::StatusChanged);
3863                            }
3864                            this.had_error = true;
3865                            cx.emit(AcpThreadEvent::Error);
3866                            log::error!("Max tokens reached. Usage: {:?}", this.token_usage);
3867
3868                            let exceeded_max_output_tokens =
3869                                this.token_usage.as_ref().is_some_and(|u| {
3870                                    u.max_output_tokens
3871                                        .is_some_and(|max| u.output_tokens >= max)
3872                                });
3873
3874                            if exceeded_max_output_tokens {
3875                                log::error!(
3876                                    "Max output tokens reached. Usage: {:?}",
3877                                    this.token_usage
3878                                );
3879                            } else {
3880                                log::error!("Max tokens reached. Usage: {:?}", this.token_usage);
3881                            }
3882                            if is_same_turn {
3883                                this.cancel_pending_turn_entries(cx);
3884                            }
3885                            return Err(anyhow!(MaxOutputTokensError));
3886                        }
3887
3888                        let canceled = matches!(r.stop_reason, acp::StopReason::Cancelled);
3889                        if canceled && is_same_turn {
3890                            this.cancel_pending_turn_entries(cx);
3891                        }
3892
3893                        if !canceled {
3894                            this.snapshot_completed_plan(cx);
3895                        }
3896
3897                        // Handle refusal - distinguish between user prompt and tool call refusals
3898                        if let acp::StopReason::Refusal = r.stop_reason {
3899                            this.had_error = true;
3900                            if let Some((user_msg_ix, _)) = this.last_user_message() {
3901                                // Check if there's a completed tool call with results after the last user message
3902                                // This indicates the refusal is in response to tool output, not the user's prompt
3903                                let has_completed_tool_call_after_user_msg =
3904                                    this.entries.iter().skip(user_msg_ix + 1).any(|entry| {
3905                                        if let AgentThreadEntry::ToolCall(tool_call) = entry {
3906                                            // Check if the tool call has completed and has output
3907                                            matches!(tool_call.status, ToolCallStatus::Completed)
3908                                                && tool_call.raw_output.is_some()
3909                                        } else {
3910                                            false
3911                                        }
3912                                    });
3913
3914                                if has_completed_tool_call_after_user_msg {
3915                                    // Refusal is due to tool output - don't truncate, just notify
3916                                    // The model refused based on what the tool returned
3917                                    cx.emit(AcpThreadEvent::Refusal);
3918                                } else {
3919                                    // User prompt was refused - truncate back to before the user message
3920                                    let range = user_msg_ix..this.entries.len();
3921                                    if range.start < range.end {
3922                                        this.entries.truncate(user_msg_ix);
3923                                        cx.emit(AcpThreadEvent::EntriesRemoved(range));
3924                                    }
3925                                    cx.emit(AcpThreadEvent::Refusal);
3926                                }
3927                            } else {
3928                                // No user message found, treat as general refusal
3929                                cx.emit(AcpThreadEvent::Refusal);
3930                            }
3931                        }
3932
3933                        if cx.has_flag::<AcpBetaFeatureFlag>()
3934                            && let Some(response_usage) = &r.usage
3935                        {
3936                            let usage = this.token_usage.get_or_insert_with(Default::default);
3937                            usage.input_tokens = response_usage.input_tokens;
3938                            usage.output_tokens = response_usage.output_tokens;
3939                            cx.emit(AcpThreadEvent::TokenUsageUpdated);
3940                        }
3941
3942                        if is_same_turn {
3943                            cx.emit(AcpThreadEvent::StatusChanged);
3944                        }
3945                        cx.emit(AcpThreadEvent::Stopped(r.stop_reason));
3946                        Ok(Some(r))
3947                    }
3948                    Err(e) => {
3949                        if is_same_turn {
3950                            cx.emit(AcpThreadEvent::StatusChanged);
3951                        }
3952                        Self::flush_streaming_text(&mut this.streaming_text_buffer, cx);
3953                        if is_same_turn {
3954                            this.cancel_pending_turn_entries(cx);
3955                        }
3956                        this.had_error = true;
3957                        cx.emit(AcpThreadEvent::Error);
3958                        log::error!("Error in run turn: {:?}", e);
3959                        Err(e)
3960                    }
3961                }
3962            })?
3963        })
3964        .boxed()
3965    }
3966
3967    pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<()> {
3968        Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
3969        self.cancel_outstanding_elicitations(cx);
3970
3971        let Some(turn) = self.running_turn.take() else {
3972            return Task::ready(());
3973        };
3974        self.mark_pending_entries_as_canceled(cx);
3975        self.connection.cancel(&self.session_id, cx);
3976        cx.emit(AcpThreadEvent::StatusChanged);
3977
3978        // Wait for the send task to complete
3979        cx.background_spawn(turn.send_task)
3980    }
3981
3982    fn cancel_pending_turn_entries(&mut self, cx: &mut Context<Self>) {
3983        self.mark_pending_entries_as_canceled(cx);
3984        self.cancel_outstanding_elicitations(cx);
3985    }
3986
3987    fn mark_pending_entries_as_canceled(&mut self, cx: &mut Context<Self>) {
3988        for (ix, entry) in self.entries.iter_mut().enumerate() {
3989            match entry {
3990                AgentThreadEntry::ToolCall(call) => {
3991                    let cancel = matches!(
3992                        call.status,
3993                        ToolCallStatus::Pending
3994                            | ToolCallStatus::WaitingForConfirmation { .. }
3995                            | ToolCallStatus::InProgress
3996                    );
3997                    if cancel {
3998                        call.status = ToolCallStatus::Canceled;
3999                        cx.emit(AcpThreadEvent::EntryUpdated(ix));
4000                    }
4001                }
4002                AgentThreadEntry::ContextCompaction(compaction) => {
4003                    if compaction.status == ContextCompactionStatus::InProgress {
4004                        compaction.status = ContextCompactionStatus::Canceled;
4005                        cx.emit(AcpThreadEvent::EntryUpdated(ix));
4006                    }
4007                }
4008                _ => {}
4009            }
4010        }
4011    }
4012
4013    fn cancel_outstanding_elicitations(&mut self, cx: &mut Context<Self>) {
4014        for ix in 0..self.entries.len() {
4015            let Some(AgentThreadEntry::Elicitation(elicitation_id)) = self.entries.get(ix) else {
4016                continue;
4017            };
4018            if self
4019                .elicitations
4020                .cancel_elicitation_by_id(elicitation_id, true)
4021            {
4022                cx.emit(AcpThreadEvent::EntryUpdated(ix));
4023            }
4024        }
4025    }
4026
4027    /// Restores the git working tree to the state at the given checkpoint (if one exists)
4028    pub fn restore_checkpoint(
4029        &mut self,
4030        client_id: ClientUserMessageId,
4031        cx: &mut Context<Self>,
4032    ) -> Task<Result<()>> {
4033        let Some((_, message)) = self.user_message_mut(&client_id) else {
4034            return Task::ready(Err(anyhow!("message not found")));
4035        };
4036
4037        let checkpoint = message
4038            .checkpoint
4039            .as_ref()
4040            .map(|c| c.git_checkpoint.clone());
4041
4042        // Cancel any in-progress generation before restoring
4043        let cancel_task = self.cancel(cx);
4044        let rewind = self.rewind(client_id.clone(), cx);
4045        let git_store = self.project.read(cx).git_store().clone();
4046
4047        cx.spawn(async move |_, cx| {
4048            cancel_task.await;
4049            rewind.await?;
4050            if let Some(checkpoint) = checkpoint {
4051                git_store
4052                    .update(cx, |git, cx| git.restore_checkpoint(checkpoint, cx))
4053                    .await?;
4054            }
4055
4056            Ok(())
4057        })
4058    }
4059
4060    /// Rewinds this thread to before the entry at `index`, removing it and all
4061    /// subsequent entries while rejecting any action_log changes made from that point.
4062    /// Unlike `restore_checkpoint`, this method does not restore from git.
4063    pub fn rewind(
4064        &mut self,
4065        client_id: ClientUserMessageId,
4066        cx: &mut Context<Self>,
4067    ) -> Task<Result<()>> {
4068        let Some(truncate) = self.connection.truncate(&self.session_id, cx) else {
4069            return Task::ready(Err(anyhow!("not supported")));
4070        };
4071
4072        Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
4073        let telemetry = ActionLogTelemetry::from(&*self);
4074        cx.spawn(async move |this, cx| {
4075            cx.update(|cx| truncate.run(client_id.clone(), cx)).await?;
4076            this.update(cx, |this, cx| {
4077                if let Some((ix, _)) = this.user_message_mut(&client_id) {
4078                    // Collect all terminals from entries that will be removed
4079                    let terminals_to_remove: Vec<acp::TerminalId> = this.entries[ix..]
4080                        .iter()
4081                        .flat_map(|entry| entry.terminals())
4082                        .filter_map(|terminal| terminal.read(cx).id().clone().into())
4083                        .collect();
4084
4085                    let range = ix..this.entries.len();
4086                    this.entries.truncate(ix);
4087                    cx.emit(AcpThreadEvent::EntriesRemoved(range));
4088
4089                    // Kill and remove the terminals
4090                    for terminal_id in terminals_to_remove {
4091                        if let Some(terminal) = this.terminals.remove(&terminal_id) {
4092                            terminal.update(cx, |terminal, cx| {
4093                                terminal.kill(cx);
4094                            });
4095                        }
4096                    }
4097                }
4098                this.action_log().update(cx, |action_log, cx| {
4099                    action_log.reject_all_edits(Some(telemetry), cx)
4100                })
4101            })?
4102            .await;
4103            Ok(())
4104        })
4105    }
4106
4107    fn update_last_checkpoint_if_changed(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
4108        let Some(turn_id) = self.running_turn.as_ref().map(|turn| turn.id) else {
4109            return Task::ready(Ok(()));
4110        };
4111
4112        let git_store = self.project.read(cx).git_store().clone();
4113
4114        let Some((client_id, checkpoint)) = self.last_user_message().and_then(|(_, message)| {
4115            let id = message.client_id.clone()?;
4116            let checkpoint = message.checkpoint.as_ref()?;
4117            Some((id, checkpoint))
4118        }) else {
4119            return Task::ready(Ok(()));
4120        };
4121        if checkpoint.show {
4122            return Task::ready(Ok(()));
4123        }
4124        let old_checkpoint = checkpoint.git_checkpoint.clone();
4125
4126        let new_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx));
4127        cx.spawn(async move |this, cx| {
4128            let Some(new_checkpoint) = new_checkpoint
4129                .await
4130                .context("failed to get new checkpoint")
4131                .log_err()
4132            else {
4133                return Ok(());
4134            };
4135
4136            let Some(equal) = git_store
4137                .update(cx, |git, cx| {
4138                    git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
4139                })
4140                .await
4141                .context("failed to compare checkpoints")
4142                .log_err()
4143            else {
4144                return Ok(());
4145            };
4146
4147            if equal {
4148                return Ok(());
4149            }
4150
4151            this.update(cx, |this, cx| {
4152                if !this
4153                    .running_turn
4154                    .as_ref()
4155                    .is_some_and(|turn| turn.id == turn_id)
4156                {
4157                    return;
4158                }
4159
4160                let Some((ix, message)) = this.last_user_message() else {
4161                    return;
4162                };
4163                if message.client_id.as_ref() != Some(&client_id) {
4164                    return;
4165                }
4166                if let Some(checkpoint) = message.checkpoint.as_mut()
4167                    && !checkpoint.show
4168                {
4169                    checkpoint.show = true;
4170                    cx.emit(AcpThreadEvent::EntryUpdated(ix));
4171                }
4172            })?;
4173
4174            Ok(())
4175        })
4176    }
4177
4178    fn update_last_checkpoint(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
4179        let git_store = self.project.read(cx).git_store().clone();
4180
4181        let Some((_, message)) = self.last_user_message() else {
4182            return Task::ready(Ok(()));
4183        };
4184        let Some(client_id) = message.client_id.clone() else {
4185            return Task::ready(Ok(()));
4186        };
4187        let Some(checkpoint) = message.checkpoint.as_ref() else {
4188            return Task::ready(Ok(()));
4189        };
4190        let old_checkpoint = checkpoint.git_checkpoint.clone();
4191
4192        let new_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx));
4193        cx.spawn(async move |this, cx| {
4194            let Some(new_checkpoint) = new_checkpoint
4195                .await
4196                .context("failed to get new checkpoint")
4197                .log_err()
4198            else {
4199                return Ok(());
4200            };
4201
4202            let Some(equal) = git_store
4203                .update(cx, |git, cx| {
4204                    git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
4205                })
4206                .await
4207                .context("failed to compare checkpoints")
4208                .log_err()
4209            else {
4210                return Ok(());
4211            };
4212
4213            this.update(cx, |this, cx| {
4214                if let Some((ix, message)) = this.user_message_mut(&client_id) {
4215                    if let Some(checkpoint) = message.checkpoint.as_mut() {
4216                        checkpoint.show = !equal;
4217                        cx.emit(AcpThreadEvent::EntryUpdated(ix));
4218                    }
4219                }
4220            })?;
4221
4222            Ok(())
4223        })
4224    }
4225
4226    fn last_user_message(&mut self) -> Option<(usize, &mut UserMessage)> {
4227        self.entries
4228            .iter_mut()
4229            .enumerate()
4230            .rev()
4231            .find_map(|(ix, entry)| {
4232                if let AgentThreadEntry::UserMessage(message) = entry {
4233                    Some((ix, message))
4234                } else {
4235                    None
4236                }
4237            })
4238    }
4239
4240    fn user_message_mut(
4241        &mut self,
4242        client_id: &ClientUserMessageId,
4243    ) -> Option<(usize, &mut UserMessage)> {
4244        self.entries.iter_mut().enumerate().find_map(|(ix, entry)| {
4245            if let AgentThreadEntry::UserMessage(message) = entry {
4246                if message.client_id.as_ref() == Some(client_id) {
4247                    Some((ix, message))
4248                } else {
4249                    None
4250                }
4251            } else {
4252                None
4253            }
4254        })
4255    }
4256
4257    pub fn read_text_file(
4258        &self,
4259        path: PathBuf,
4260        line: Option<u32>,
4261        limit: Option<u32>,
4262        reuse_shared_snapshot: bool,
4263        cx: &mut Context<Self>,
4264    ) -> Task<Result<String, acp::Error>> {
4265        // Args are 1-based, move to 0-based
4266        let line = line.unwrap_or_default().saturating_sub(1);
4267        let limit = limit.unwrap_or(u32::MAX);
4268        let project = self.project.clone();
4269        let action_log = self.action_log.clone();
4270        let should_update_agent_location = self.parent_session_id.is_none();
4271        cx.spawn(async move |this, cx| {
4272            let load = project.update(cx, |project, cx| {
4273                let path = project
4274                    .project_path_for_absolute_path(&path, cx)
4275                    .ok_or_else(|| {
4276                        acp::Error::resource_not_found(Some(path.display().to_string()))
4277                    })?;
4278                Ok::<_, acp::Error>(project.open_buffer(path, cx))
4279            })?;
4280
4281            let buffer = load.await?;
4282
4283            let snapshot = if reuse_shared_snapshot {
4284                this.read_with(cx, |this, _| {
4285                    this.shared_buffers.get(&buffer.clone()).cloned()
4286                })
4287                .log_err()
4288                .flatten()
4289            } else {
4290                None
4291            };
4292
4293            let snapshot = if let Some(snapshot) = snapshot {
4294                snapshot
4295            } else {
4296                action_log.update(cx, |action_log, cx| {
4297                    action_log.buffer_read(buffer.clone(), cx);
4298                });
4299
4300                let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
4301                this.update(cx, |this, _| {
4302                    this.shared_buffers.insert(buffer.clone(), snapshot.clone());
4303                })?;
4304                snapshot
4305            };
4306
4307            let max_point = snapshot.max_point();
4308            let start_position = Point::new(line, 0);
4309
4310            if start_position > max_point {
4311                return Err(acp::Error::invalid_params().data(format!(
4312                    "Attempting to read beyond the end of the file, line {}:{}",
4313                    max_point.row + 1,
4314                    max_point.column
4315                )));
4316            }
4317
4318            let start = snapshot.anchor_before(start_position);
4319            let end = snapshot.anchor_before(Point::new(line.saturating_add(limit), 0));
4320
4321            if should_update_agent_location {
4322                project.update(cx, |project, cx| {
4323                    project.set_agent_location(
4324                        Some(AgentLocation {
4325                            buffer: buffer.downgrade(),
4326                            position: start,
4327                        }),
4328                        cx,
4329                    );
4330                });
4331            }
4332
4333            Ok(snapshot.text_for_range(start..end).collect::<String>())
4334        })
4335    }
4336
4337    pub fn write_text_file(
4338        &self,
4339        path: PathBuf,
4340        content: String,
4341        cx: &mut Context<Self>,
4342    ) -> Task<Result<()>> {
4343        let project = self.project.clone();
4344        let action_log = self.action_log.clone();
4345        let should_update_agent_location = self.parent_session_id.is_none();
4346        cx.spawn(async move |this, cx| {
4347            let load = project.update(cx, |project, cx| {
4348                let path = project
4349                    .project_path_for_absolute_path(&path, cx)
4350                    .context("invalid path")?;
4351                anyhow::Ok(project.open_buffer(path, cx))
4352            });
4353            let buffer = load?.await?;
4354            let snapshot = this.update(cx, |this, cx| {
4355                this.shared_buffers
4356                    .get(&buffer)
4357                    .cloned()
4358                    .unwrap_or_else(|| buffer.read(cx).snapshot())
4359            })?;
4360            let edits = cx
4361                .background_executor()
4362                .spawn(async move {
4363                    let old_text = snapshot.text();
4364                    text_diff(old_text.as_str(), &content)
4365                        .into_iter()
4366                        .map(|(range, replacement)| {
4367                            (snapshot.anchor_range_inside(range), replacement)
4368                        })
4369                        .collect::<Vec<_>>()
4370                })
4371                .await;
4372
4373            if should_update_agent_location {
4374                project.update(cx, |project, cx| {
4375                    project.set_agent_location(
4376                        Some(AgentLocation {
4377                            buffer: buffer.downgrade(),
4378                            position: edits
4379                                .last()
4380                                .map(|(range, _)| range.end)
4381                                .unwrap_or(Anchor::min_for_buffer(buffer.read(cx).remote_id())),
4382                        }),
4383                        cx,
4384                    );
4385                });
4386            }
4387
4388            let format_on_save = cx.update(|cx| {
4389                action_log.update(cx, |action_log, cx| {
4390                    action_log.buffer_read(buffer.clone(), cx);
4391                });
4392
4393                let format_on_save = buffer.update(cx, |buffer, cx| {
4394                    buffer.start_transaction();
4395                    buffer.edit(edits, None, cx);
4396                    buffer.end_transaction_with_source(BufferEditSource::Agent, cx);
4397
4398                    let settings =
4399                        language::language_settings::LanguageSettings::for_buffer(buffer, cx);
4400
4401                    settings.format_on_save != FormatOnSave::Off
4402                });
4403                action_log.update(cx, |action_log, cx| {
4404                    action_log.buffer_edited(buffer.clone(), cx);
4405                });
4406                format_on_save
4407            });
4408
4409            if format_on_save {
4410                let format_task = project.update(cx, |project, cx| {
4411                    project.format(
4412                        HashSet::from_iter([buffer.clone()]),
4413                        LspFormatTarget::Buffers,
4414                        false,
4415                        FormatTrigger::Save,
4416                        cx,
4417                    )
4418                });
4419                format_task.await.log_err();
4420
4421                action_log.update(cx, |action_log, cx| {
4422                    action_log.buffer_edited(buffer.clone(), cx);
4423                });
4424            }
4425
4426            project
4427                .update(cx, |project, cx| project.save_buffer(buffer, cx))
4428                .await
4429        })
4430    }
4431
4432    pub fn create_terminal(
4433        &self,
4434        command: String,
4435        args: Vec<String>,
4436        extra_env: Vec<acp::EnvVariable>,
4437        cwd: Option<PathBuf>,
4438        output_byte_limit: Option<u64>,
4439        sandbox_wrap: Option<SandboxWrap>,
4440        cx: &mut Context<Self>,
4441    ) -> Task<Result<Entity<Terminal>>> {
4442        let env = match &cwd {
4443            Some(dir) => self.project.update(cx, |project, cx| {
4444                project.environment().update(cx, |env, cx| {
4445                    env.directory_environment(dir.as_path().into(), cx)
4446                })
4447            }),
4448            None => Task::ready(None).shared(),
4449        };
4450        let env = cx.spawn(async move |_, _| {
4451            let mut env = env.await.unwrap_or_default();
4452            // Disables paging for `git` and hopefully other commands
4453            env.insert("PAGER".into(), "".into());
4454            for var in extra_env {
4455                env.insert(var.name, var.value);
4456            }
4457            env
4458        });
4459
4460        let project = self.project.clone();
4461        let language_registry = project.read(cx).languages().clone();
4462        let is_windows = project.read(cx).path_style(cx).is_windows();
4463        // Headless hosts (e.g. the eval CLI) have no controlling TTY, so PTY
4464        // setup fails with `ENOTTY`. Run the command non-interactively and
4465        // without a PTY in that case.
4466        let headless = HeadlessTerminal::is_enabled(cx);
4467
4468        let terminal_id = acp::TerminalId::new(Uuid::new_v4().to_string());
4469        let terminal_task = cx.spawn({
4470            let terminal_id = terminal_id.clone();
4471            async move |_this, cx| {
4472                let env = env.await;
4473                let shell = project
4474                    .update(cx, |project, cx| {
4475                        project
4476                            .remote_client()
4477                            .and_then(|r| r.read(cx).default_system_shell())
4478                    })
4479                    .unwrap_or_else(|| get_default_system_shell_preferring_bash());
4480
4481                // The sandbox owns the network proxy (for restricted-network
4482                // policies) and injects the child's proxy env vars, returning
4483                // the env to spawn with. On Windows, restricted host access is
4484                // rejected inside the sandbox before command preparation.
4485                #[cfg(target_os = "windows")]
4486                let (task_command, task_args, task_env, sandbox, spawn_cwd) =
4487                    if sandbox_wrap.is_some() {
4488                        let (task_command, task_args) = task::ShellBuilder::new(
4489                            &Shell::Program("/bin/sh".to_string()),
4490                            false,
4491                        )
4492                        .non_interactive()
4493                        .redirect_stdin_to_dev_null()
4494                        .build(Some(command.clone()), &args);
4495                        let wrap = cx.background_spawn(prepare_sandbox_wrap(
4496                            task_command,
4497                            task_args,
4498                            cwd.clone(),
4499                            sandbox_wrap,
4500                            env,
4501                        ));
4502                        let timeout = cx.background_executor().timer(WSL_SANDBOX_WRAP_TIMEOUT);
4503                        let (task_command, task_args, task_env, sandbox) = futures::select_biased! {
4504                            result = wrap.fuse() => result?,
4505                            _ = timeout.fuse() => return Err(anyhow::Error::new(
4506                                sandbox::SandboxError::WslUnavailable(format!(
4507                                    "WSL did not respond within {} seconds while preparing the sandboxed command",
4508                                    WSL_SANDBOX_WRAP_TIMEOUT.as_secs()
4509                                )),
4510                            )),
4511                        };
4512                        (task_command, task_args, task_env, sandbox, None)
4513                    } else {
4514                        // No sandbox wrap means we're running unsandboxed, and
4515                        // on Windows that deliberately changes the shell: the
4516                        // sandboxed path runs under WSL's Linux bash, but this
4517                        // fallback uses the host's `shell` against the native cwd.
4518                        let mut builder = ShellBuilder::new(&Shell::Program(shell), is_windows);
4519                        if headless {
4520                            builder = builder.non_interactive();
4521                        }
4522                        let (task_command, task_args) = builder
4523                            .redirect_stdin_to_dev_null()
4524                            .build(Some(command.clone()), &args);
4525                        (task_command, task_args, env, None, cwd.clone())
4526                    };
4527
4528                #[cfg(not(target_os = "windows"))]
4529                let (task_command, task_args, task_env, sandbox, spawn_cwd) = {
4530                    let mut builder = ShellBuilder::new(&Shell::Program(shell), is_windows);
4531                    if headless {
4532                        builder = builder.non_interactive();
4533                    }
4534                    let (task_command, task_args) = builder
4535                        .redirect_stdin_to_dev_null()
4536                        .build(Some(command.clone()), &args);
4537                    let (task_command, task_args, task_env, sandbox) = cx
4538                        .background_spawn(prepare_sandbox_wrap(
4539                            task_command,
4540                            task_args,
4541                            cwd.clone(),
4542                            sandbox_wrap,
4543                            env,
4544                        ))
4545                        .await?;
4546                    (task_command, task_args, task_env, sandbox, cwd.clone())
4547                };
4548                let terminal = project
4549                    .update(cx, |project, cx| {
4550                        project.create_terminal_task(
4551                            task::SpawnInTerminal {
4552                                command: Some(task_command),
4553                                args: task_args,
4554                                cwd: spawn_cwd,
4555                                env: task_env,
4556                                ..Default::default()
4557                            },
4558                            cx,
4559                        )
4560                    })
4561                    .await?;
4562
4563                anyhow::Ok(cx.new(|cx| {
4564                    Terminal::new(
4565                        terminal_id,
4566                        &format!("{} {}", command, args.join(" ")),
4567                        cwd,
4568                        output_byte_limit.map(|l| l as usize),
4569                        terminal,
4570                        language_registry,
4571                        sandbox,
4572                        cx,
4573                    )
4574                }))
4575            }
4576        });
4577
4578        cx.spawn(async move |this, cx| {
4579            let terminal = terminal_task.await?;
4580            this.update(cx, |this, _cx| {
4581                this.terminals.insert(terminal_id, terminal.clone());
4582                terminal
4583            })
4584        })
4585    }
4586
4587    pub fn kill_terminal(
4588        &mut self,
4589        terminal_id: acp::TerminalId,
4590        cx: &mut Context<Self>,
4591    ) -> Result<()> {
4592        self.terminals
4593            .get(&terminal_id)
4594            .context("Terminal not found")?
4595            .update(cx, |terminal, cx| {
4596                terminal.kill(cx);
4597            });
4598
4599        Ok(())
4600    }
4601
4602    pub fn release_terminal(
4603        &mut self,
4604        terminal_id: acp::TerminalId,
4605        cx: &mut Context<Self>,
4606    ) -> Result<()> {
4607        self.terminals
4608            .remove(&terminal_id)
4609            .context("Terminal not found")?
4610            .update(cx, |terminal, cx| {
4611                terminal.kill(cx);
4612            });
4613
4614        Ok(())
4615    }
4616
4617    pub fn terminal(&self, terminal_id: acp::TerminalId) -> Result<Entity<Terminal>> {
4618        self.terminals
4619            .get(&terminal_id)
4620            .context("Terminal not found")
4621            .cloned()
4622    }
4623
4624    pub fn to_markdown(&self, cx: &App) -> String {
4625        self.entries
4626            .iter()
4627            .map(|entry| match entry {
4628                AgentThreadEntry::Elicitation(elicitation_id) => self
4629                    .elicitations
4630                    .elicitation(elicitation_id)
4631                    .map(|(_, elicitation)| {
4632                        format!("## Input Requested\n\n{}\n\n", elicitation.request.message)
4633                    })
4634                    .unwrap_or_else(|| entry.to_markdown(cx)),
4635                _ => entry.to_markdown(cx),
4636            })
4637            .collect()
4638    }
4639
4640    pub fn emit_load_error(&mut self, error: LoadError, cx: &mut Context<Self>) {
4641        cx.emit(AcpThreadEvent::LoadError(error));
4642    }
4643
4644    pub fn register_terminal_created(
4645        &mut self,
4646        terminal_id: acp::TerminalId,
4647        command_label: String,
4648        working_dir: Option<PathBuf>,
4649        output_byte_limit: Option<u64>,
4650        terminal: Entity<::terminal::Terminal>,
4651        cx: &mut Context<Self>,
4652    ) -> Entity<Terminal> {
4653        let language_registry = self.project.read(cx).languages().clone();
4654
4655        let entity = cx.new(|cx| {
4656            Terminal::new(
4657                terminal_id.clone(),
4658                &command_label,
4659                working_dir.clone(),
4660                output_byte_limit.map(|l| l as usize),
4661                terminal,
4662                language_registry,
4663                // External terminal providers manage their own sandboxing
4664                // (if any). We don't wrap their commands.
4665                None,
4666                cx,
4667            )
4668        });
4669        self.terminals.insert(terminal_id.clone(), entity.clone());
4670        entity
4671    }
4672
4673    pub fn mark_as_subagent_output(&mut self, cx: &mut Context<Self>) {
4674        for entry in self.entries.iter_mut().rev() {
4675            if let AgentThreadEntry::AssistantMessage(assistant_message) = entry {
4676                assistant_message.is_subagent_output = true;
4677                cx.notify();
4678                return;
4679            }
4680        }
4681    }
4682
4683    pub fn on_terminal_provider_event(
4684        &mut self,
4685        event: TerminalProviderEvent,
4686        cx: &mut Context<Self>,
4687    ) {
4688        match event {
4689            TerminalProviderEvent::Created {
4690                terminal_id,
4691                label,
4692                cwd,
4693                output_byte_limit,
4694                terminal,
4695            } => {
4696                let entity = self.register_terminal_created(
4697                    terminal_id.clone(),
4698                    label,
4699                    cwd,
4700                    output_byte_limit,
4701                    terminal,
4702                    cx,
4703                );
4704
4705                if let Some(mut chunks) = self.pending_terminal_output.remove(&terminal_id) {
4706                    for data in chunks.drain(..) {
4707                        entity.update(cx, |term, cx| {
4708                            term.inner().update(cx, |inner, cx| {
4709                                inner.write_output(&data, cx);
4710                            })
4711                        });
4712                    }
4713                }
4714
4715                if let Some(_status) = self.pending_terminal_exit.remove(&terminal_id) {
4716                    entity.update(cx, |term, cx| {
4717                        term.inner().update(cx, |inner, _| inner.shrink_to_used());
4718                        cx.notify();
4719                    });
4720                }
4721
4722                cx.notify();
4723            }
4724            TerminalProviderEvent::Output { terminal_id, data } => {
4725                if let Some(entity) = self.terminals.get(&terminal_id) {
4726                    entity.update(cx, |term, cx| {
4727                        term.inner().update(cx, |inner, cx| {
4728                            inner.write_output(&data, cx);
4729                        })
4730                    });
4731                } else {
4732                    self.pending_terminal_output
4733                        .entry(terminal_id)
4734                        .or_default()
4735                        .push(data);
4736                }
4737            }
4738            TerminalProviderEvent::TitleChanged { terminal_id, title } => {
4739                if let Some(entity) = self.terminals.get(&terminal_id) {
4740                    entity.update(cx, |term, cx| {
4741                        term.inner().update(cx, |inner, cx| {
4742                            inner.breadcrumb_text = title;
4743                            cx.emit(::terminal::Event::BreadcrumbsChanged);
4744                        })
4745                    });
4746                }
4747            }
4748            TerminalProviderEvent::Exit {
4749                terminal_id,
4750                status,
4751            } => {
4752                if let Some(entity) = self.terminals.get(&terminal_id) {
4753                    entity.update(cx, |term, cx| {
4754                        term.inner().update(cx, |inner, _| inner.shrink_to_used());
4755                        cx.notify();
4756                    });
4757                } else {
4758                    self.pending_terminal_exit.insert(terminal_id, status);
4759                }
4760            }
4761        }
4762    }
4763}
4764
4765fn markdown_for_raw_output(
4766    raw_output: &serde_json::Value,
4767    language_registry: &Arc<LanguageRegistry>,
4768    cx: &mut App,
4769) -> Option<Entity<Markdown>> {
4770    match raw_output {
4771        serde_json::Value::Null => None,
4772        serde_json::Value::Bool(value) => Some(cx.new(|cx| {
4773            Markdown::new(
4774                value.to_string().into(),
4775                Some(language_registry.clone()),
4776                None,
4777                cx,
4778            )
4779        })),
4780        serde_json::Value::Number(value) => Some(cx.new(|cx| {
4781            Markdown::new(
4782                value.to_string().into(),
4783                Some(language_registry.clone()),
4784                None,
4785                cx,
4786            )
4787        })),
4788        serde_json::Value::String(value) => Some(cx.new(|cx| {
4789            Markdown::new(
4790                value.clone().into(),
4791                Some(language_registry.clone()),
4792                None,
4793                cx,
4794            )
4795        })),
4796        value => Some(cx.new(|cx| {
4797            let pretty_json = to_string_pretty(value).unwrap_or_else(|_| value.to_string());
4798
4799            Markdown::new(
4800                format!("```json\n{}\n```", pretty_json).into(),
4801                Some(language_registry.clone()),
4802                None,
4803                cx,
4804            )
4805        })),
4806    }
4807}
4808
4809#[cfg(test)]
4810mod tests {
4811    use super::*;
4812    use anyhow::anyhow;
4813    use feature_flags::FeatureFlag as _;
4814    use futures::stream::StreamExt as _;
4815    use futures::{channel::mpsc, future::LocalBoxFuture, select};
4816    use gpui::UpdateGlobal as _;
4817    use gpui::{App, AsyncApp, TestAppContext, WeakEntity};
4818    use indoc::indoc;
4819    use project::{AgentId, FakeFs, Fs, RemoveOptions};
4820    use rand::{distr, prelude::*};
4821    use serde_json::json;
4822    use settings::SettingsStore;
4823    use std::{
4824        any::Any,
4825        cell::RefCell,
4826        path::Path,
4827        rc::Rc,
4828        sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
4829        time::Duration,
4830    };
4831    use util::{path, path_list::PathList};
4832
4833    #[test]
4834    fn command_category_meta_round_trips() {
4835        // Exhaustive list of variants. The match below has no wildcard arm, so
4836        // adding a `CommandCategory` variant fails to compile here until it's
4837        // covered, keeping the `as_str`/`from_str` wire contract in sync.
4838        let all = [CommandCategory::Native, CommandCategory::Mcp];
4839        for category in all {
4840            match category {
4841                CommandCategory::Native | CommandCategory::Mcp => {}
4842            }
4843            let meta = meta_with_command_category(category);
4844            assert_eq!(command_category_from_meta(&Some(meta)), Some(category));
4845        }
4846
4847        // Absent meta and unknown categories both decode to `None`.
4848        assert_eq!(command_category_from_meta(&None), None);
4849        let unknown =
4850            acp::Meta::from_iter([(COMMAND_CATEGORY_META_KEY.into(), "future-category".into())]);
4851        assert_eq!(command_category_from_meta(&Some(unknown)), None);
4852    }
4853
4854    #[test]
4855    fn client_user_message_id_serializes_as_string() {
4856        let serialized =
4857            serde_json::to_value(ClientUserMessageId::new()).expect("serialize client message id");
4858        assert!(
4859            serialized.is_string(),
4860            "expected string, got {serialized:?}"
4861        );
4862
4863        let deserialized: ClientUserMessageId =
4864            serde_json::from_value(json!("client-id")).expect("deserialize client message id");
4865        assert_eq!(
4866            serde_json::to_value(deserialized).expect("serialize client message id"),
4867            json!("client-id")
4868        );
4869    }
4870
4871    fn init_test(cx: &mut TestAppContext) {
4872        env_logger::try_init().ok();
4873        cx.update(|cx| {
4874            let mut settings_store = SettingsStore::test(cx);
4875            settings_store.register_setting::<feature_flags::FeatureFlagsSettings>();
4876            cx.set_global(settings_store);
4877        });
4878    }
4879
4880    fn enable_acp_beta(cx: &mut TestAppContext) {
4881        cx.update(|cx| {
4882            cx.update_flags(false, vec![AcpBetaFeatureFlag::NAME.to_string()]);
4883        });
4884    }
4885
4886    fn set_acp_beta_override(value: &str, cx: &mut TestAppContext) {
4887        cx.update(|cx| {
4888            SettingsStore::update_global(cx, |store, cx| {
4889                store.update_user_settings(cx, |content| {
4890                    content
4891                        .feature_flags
4892                        .get_or_insert_default()
4893                        .insert(AcpBetaFeatureFlag::NAME.to_string(), value.to_string());
4894                });
4895            });
4896        });
4897    }
4898
4899    #[test]
4900    fn text_resource_markdown_uses_mime_type_for_code_blocks() {
4901        let shell = acp::TextResourceContents::new("echo 'hello from exec test'", "tool://preview")
4902            .mime_type("text/x-shellscript".to_string());
4903        assert_eq!(
4904            ContentBlock::text_resource_markdown(&shell),
4905            "```sh\necho 'hello from exec test'\n```"
4906        );
4907
4908        let markdown = acp::TextResourceContents::new("**approval** requested", "tool://preview")
4909            .mime_type("text/markdown".to_string());
4910        assert_eq!(
4911            ContentBlock::text_resource_markdown(&markdown),
4912            "**approval** requested"
4913        );
4914
4915        let plain = acp::TextResourceContents::new("plain preview", "tool://preview")
4916            .mime_type("text/plain".to_string());
4917        assert_eq!(
4918            ContentBlock::text_resource_markdown(&plain),
4919            "```\nplain preview\n```"
4920        );
4921
4922        let cpp = acp::TextResourceContents::new("int main() {}", "tool://preview")
4923            .mime_type("text/x-c++; charset=utf-8".to_string());
4924        assert_eq!(
4925            ContentBlock::text_resource_markdown(&cpp),
4926            "```cpp\nint main() {}\n```"
4927        );
4928
4929        let untyped = acp::TextResourceContents::new("# plain preview", "tool://preview");
4930        assert_eq!(
4931            ContentBlock::text_resource_markdown(&untyped),
4932            "```\n# plain preview\n```"
4933        );
4934    }
4935
4936    #[gpui::test]
4937    async fn test_tool_call_content_preserves_embedded_text_resource(
4938        cx: &mut gpui::TestAppContext,
4939    ) {
4940        init_test(cx);
4941
4942        cx.update(|cx| {
4943            let language_registry =
4944                Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
4945            let content = acp::ContentBlock::Resource(acp::EmbeddedResource::new(
4946                acp::EmbeddedResourceResource::TextResourceContents(
4947                    acp::TextResourceContents::new("echo 'hello from exec test'", "tool://preview")
4948                        .mime_type("text/x-shellscript".to_string()),
4949                ),
4950            ));
4951
4952            let block = ContentBlock::new_tool_call_content(
4953                content,
4954                &language_registry,
4955                PathStyle::local(),
4956                cx,
4957            );
4958
4959            let ContentBlock::EmbeddedResource { resource, markdown } = &block else {
4960                panic!("expected embedded resource block, got {block:?}");
4961            };
4962            match &resource.resource {
4963                acp::EmbeddedResourceResource::TextResourceContents(text) => {
4964                    assert_eq!(text.text, "echo 'hello from exec test'");
4965                    assert_eq!(text.uri, "tool://preview");
4966                    assert_eq!(text.mime_type.as_deref(), Some("text/x-shellscript"));
4967                }
4968                other => panic!("expected text resource contents, got {other:?}"),
4969            }
4970
4971            let markdown = markdown
4972                .as_ref()
4973                .expect("text resources should have renderable markdown")
4974                .read(cx)
4975                .source()
4976                .to_string();
4977            assert_eq!(markdown, "```sh\necho 'hello from exec test'\n```");
4978            assert_eq!(
4979                block.to_markdown(cx),
4980                "```sh\necho 'hello from exec test'\n```"
4981            );
4982            assert_eq!(block.text_content(cx), Some("echo 'hello from exec test'"));
4983
4984            let untyped = ContentBlock::new_tool_call_content(
4985                acp::ContentBlock::Resource(acp::EmbeddedResource::new(
4986                    acp::EmbeddedResourceResource::TextResourceContents(
4987                        acp::TextResourceContents::new("# plain preview", "tool://preview"),
4988                    ),
4989                )),
4990                &language_registry,
4991                PathStyle::local(),
4992                cx,
4993            );
4994            assert_eq!(untyped.to_markdown(cx), "```\n# plain preview\n```");
4995            assert_eq!(untyped.text_content(cx), Some("# plain preview"));
4996        });
4997    }
4998
4999    #[gpui::test]
5000    async fn test_tool_call_content_renders_embedded_image_blob_resource(
5001        cx: &mut gpui::TestAppContext,
5002    ) {
5003        init_test(cx);
5004
5005        cx.update(|cx| {
5006            let language_registry =
5007                Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
5008            let image_blob = acp::ContentBlock::Resource(acp::EmbeddedResource::new(
5009                acp::EmbeddedResourceResource::BlobResourceContents(
5010                    acp::BlobResourceContents::new(
5011                        "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
5012                        "tool://preview.png",
5013                    )
5014                    .mime_type("image/png".to_string()),
5015                ),
5016            ));
5017
5018            let block = ContentBlock::new_tool_call_content(
5019                image_blob,
5020                &language_registry,
5021                PathStyle::local(),
5022                cx,
5023            );
5024
5025            let ContentBlock::Image { image, dimensions } = &block else {
5026                panic!("expected image block, got {block:?}");
5027            };
5028            assert_eq!(image.format(), gpui::ImageFormat::Png);
5029            assert_eq!(
5030                dimensions.as_ref().map(|size| (size.width, size.height)),
5031                Some((1, 1))
5032            );
5033            assert_eq!(block.to_markdown(cx), "`Image`");
5034            assert_eq!(block.text_content(cx), None);
5035        });
5036    }
5037
5038    #[gpui::test]
5039    async fn test_tool_call_content_falls_back_for_non_image_blob_resource(
5040        cx: &mut gpui::TestAppContext,
5041    ) {
5042        init_test(cx);
5043
5044        cx.update(|cx| {
5045            let language_registry =
5046                Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
5047            let archive_blob = acp::ContentBlock::Resource(acp::EmbeddedResource::new(
5048                acp::EmbeddedResourceResource::BlobResourceContents(
5049                    acp::BlobResourceContents::new("not an image", "tool://archive.bin")
5050                        .mime_type("application/octet-stream".to_string()),
5051                ),
5052            ));
5053
5054            let block = ContentBlock::new_tool_call_content(
5055                archive_blob,
5056                &language_registry,
5057                PathStyle::local(),
5058                cx,
5059            );
5060
5061            let ContentBlock::EmbeddedResource { resource, markdown } = &block else {
5062                panic!("expected embedded resource block, got {block:?}");
5063            };
5064            assert!(markdown.is_none());
5065            match &resource.resource {
5066                acp::EmbeddedResourceResource::BlobResourceContents(blob) => {
5067                    assert_eq!(blob.uri, "tool://archive.bin");
5068                    assert_eq!(blob.mime_type.as_deref(), Some("application/octet-stream"));
5069                }
5070                other => panic!("expected blob resource contents, got {other:?}"),
5071            }
5072            assert_eq!(block.to_markdown(cx), "tool://archive.bin");
5073            assert_eq!(block.text_content(cx), None);
5074
5075            let invalid_image_blob = acp::ContentBlock::Resource(acp::EmbeddedResource::new(
5076                acp::EmbeddedResourceResource::BlobResourceContents(
5077                    acp::BlobResourceContents::new("not-base64", "tool://preview.png")
5078                        .mime_type("image/png".to_string()),
5079                ),
5080            ));
5081            let invalid = ContentBlock::new_tool_call_content(
5082                invalid_image_blob,
5083                &language_registry,
5084                PathStyle::local(),
5085                cx,
5086            );
5087            let ContentBlock::EmbeddedResource { resource, markdown } = &invalid else {
5088                panic!("expected embedded resource block, got {invalid:?}");
5089            };
5090            assert!(markdown.is_none());
5091            assert_eq!(
5092                ContentBlock::embedded_resource_label(resource),
5093                "tool://preview.png"
5094            );
5095            assert_eq!(invalid.to_markdown(cx), "tool://preview.png");
5096        });
5097    }
5098
5099    #[test]
5100    fn sandbox_authorization_details_deserialize_legacy_network_bool() {
5101        // Older builds persisted `network: bool`; the `alias` on
5102        // `network_all_hosts` must keep those details rendering as a
5103        // network request rather than silently dropping it.
5104        let details: SandboxAuthorizationDetails =
5105            serde_json::from_value(json!({ "network": true })).unwrap();
5106        assert!(details.network_all_hosts);
5107        assert!(details.network_hosts.is_empty());
5108
5109        let details: SandboxAuthorizationDetails =
5110            serde_json::from_value(json!({ "network": false })).unwrap();
5111        assert!(!details.network_all_hosts);
5112    }
5113
5114    #[gpui::test]
5115    async fn test_terminal_output_buffered_before_created_renders(cx: &mut gpui::TestAppContext) {
5116        init_test(cx);
5117
5118        let fs = FakeFs::new(cx.executor());
5119        let project = Project::test(fs, [], cx).await;
5120        let connection = Rc::new(FakeAgentConnection::new());
5121        let thread = cx
5122            .update(|cx| {
5123                connection.new_session(
5124                    project,
5125                    PathList::new(&[std::path::Path::new(path!("/test"))]),
5126                    cx,
5127                )
5128            })
5129            .await
5130            .unwrap();
5131
5132        let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
5133
5134        // Send Output BEFORE Created - should be buffered by acp_thread
5135        thread.update(cx, |thread, cx| {
5136            thread.on_terminal_provider_event(
5137                TerminalProviderEvent::Output {
5138                    terminal_id: terminal_id.clone(),
5139                    data: b"hello buffered".to_vec(),
5140                },
5141                cx,
5142            );
5143        });
5144
5145        // Create a display-only terminal and then send Created
5146        let lower = cx.new(|cx| {
5147            let builder = ::terminal::TerminalBuilder::new_display_only(
5148                ::terminal::terminal_settings::CursorShape::default(),
5149                ::terminal::terminal_settings::AlternateScroll::On,
5150                None,
5151                0,
5152                cx.background_executor(),
5153                PathStyle::local(),
5154            );
5155            builder.subscribe(cx)
5156        });
5157
5158        thread.update(cx, |thread, cx| {
5159            thread.on_terminal_provider_event(
5160                TerminalProviderEvent::Created {
5161                    terminal_id: terminal_id.clone(),
5162                    label: "Buffered Test".to_string(),
5163                    cwd: None,
5164                    output_byte_limit: None,
5165                    terminal: lower.clone(),
5166                },
5167                cx,
5168            );
5169        });
5170
5171        // After Created, buffered Output should have been flushed into the renderer
5172        let content = thread.read_with(cx, |thread, cx| {
5173            let term = thread.terminal(terminal_id.clone()).unwrap();
5174            term.read_with(cx, |t, cx| t.inner().read(cx).get_content())
5175        });
5176
5177        assert!(
5178            content.contains("hello buffered"),
5179            "expected buffered output to render, got: {content}"
5180        );
5181    }
5182
5183    #[gpui::test]
5184    async fn test_terminal_exit_preserves_visible_scrollback(cx: &mut gpui::TestAppContext) {
5185        init_test(cx);
5186
5187        let fs = FakeFs::new(cx.executor());
5188        let project = Project::test(fs, [], cx).await;
5189        let connection = Rc::new(FakeAgentConnection::new());
5190        let thread = cx
5191            .update(|cx| {
5192                connection.new_session(
5193                    project,
5194                    PathList::new(&[std::path::Path::new(path!("/test"))]),
5195                    cx,
5196                )
5197            })
5198            .await
5199            .unwrap();
5200
5201        let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
5202        let lower = cx.new(|cx| {
5203            let builder = ::terminal::TerminalBuilder::new_display_only(
5204                ::terminal::terminal_settings::CursorShape::default(),
5205                ::terminal::terminal_settings::AlternateScroll::On,
5206                None,
5207                0,
5208                cx.background_executor(),
5209                PathStyle::local(),
5210            );
5211            builder.subscribe(cx)
5212        });
5213
5214        thread.update(cx, |thread, cx| {
5215            thread.on_terminal_provider_event(
5216                TerminalProviderEvent::Created {
5217                    terminal_id: terminal_id.clone(),
5218                    label: "Buffered Test".to_string(),
5219                    cwd: None,
5220                    output_byte_limit: None,
5221                    terminal: lower.clone(),
5222                },
5223                cx,
5224            );
5225        });
5226
5227        let mut output = String::new();
5228        for line in 0..15_000 {
5229            output.push_str(&format!("line {line}\n"));
5230        }
5231
5232        thread.update(cx, |thread, cx| {
5233            thread.on_terminal_provider_event(
5234                TerminalProviderEvent::Output {
5235                    terminal_id: terminal_id.clone(),
5236                    data: output.into_bytes(),
5237                },
5238                cx,
5239            );
5240            thread.on_terminal_provider_event(
5241                TerminalProviderEvent::Exit {
5242                    terminal_id: terminal_id.clone(),
5243                    status: acp::TerminalExitStatus::new().exit_code(0),
5244                },
5245                cx,
5246            );
5247        });
5248
5249        let content = thread.read_with(cx, |thread, cx| {
5250            let term = thread.terminal(terminal_id.clone()).unwrap();
5251            term.read_with(cx, |term, cx| term.inner().read(cx).get_content())
5252        });
5253
5254        assert!(
5255            content.contains("line 14999"),
5256            "expected output to remain visible after terminal exit, got: {content}"
5257        );
5258    }
5259
5260    #[gpui::test]
5261    async fn test_terminal_output_and_exit_buffered_before_created(cx: &mut gpui::TestAppContext) {
5262        init_test(cx);
5263
5264        let fs = FakeFs::new(cx.executor());
5265        let project = Project::test(fs, [], cx).await;
5266        let connection = Rc::new(FakeAgentConnection::new());
5267        let thread = cx
5268            .update(|cx| {
5269                connection.new_session(
5270                    project,
5271                    PathList::new(&[std::path::Path::new(path!("/test"))]),
5272                    cx,
5273                )
5274            })
5275            .await
5276            .unwrap();
5277
5278        let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
5279
5280        // Send Output BEFORE Created
5281        thread.update(cx, |thread, cx| {
5282            thread.on_terminal_provider_event(
5283                TerminalProviderEvent::Output {
5284                    terminal_id: terminal_id.clone(),
5285                    data: b"pre-exit data".to_vec(),
5286                },
5287                cx,
5288            );
5289        });
5290
5291        // Send Exit BEFORE Created
5292        thread.update(cx, |thread, cx| {
5293            thread.on_terminal_provider_event(
5294                TerminalProviderEvent::Exit {
5295                    terminal_id: terminal_id.clone(),
5296                    status: acp::TerminalExitStatus::new().exit_code(0),
5297                },
5298                cx,
5299            );
5300        });
5301
5302        // Now create a display-only lower-level terminal and send Created
5303        let lower = cx.new(|cx| {
5304            let builder = ::terminal::TerminalBuilder::new_display_only(
5305                ::terminal::terminal_settings::CursorShape::default(),
5306                ::terminal::terminal_settings::AlternateScroll::On,
5307                None,
5308                0,
5309                cx.background_executor(),
5310                PathStyle::local(),
5311            );
5312            builder.subscribe(cx)
5313        });
5314
5315        thread.update(cx, |thread, cx| {
5316            thread.on_terminal_provider_event(
5317                TerminalProviderEvent::Created {
5318                    terminal_id: terminal_id.clone(),
5319                    label: "Buffered Exit Test".to_string(),
5320                    cwd: None,
5321                    output_byte_limit: None,
5322                    terminal: lower.clone(),
5323                },
5324                cx,
5325            );
5326        });
5327
5328        // Output should be present after Created (flushed from buffer)
5329        let content = thread.read_with(cx, |thread, cx| {
5330            let term = thread.terminal(terminal_id.clone()).unwrap();
5331            term.read_with(cx, |t, cx| t.inner().read(cx).get_content())
5332        });
5333
5334        assert!(
5335            content.contains("pre-exit data"),
5336            "expected pre-exit data to render, got: {content}"
5337        );
5338    }
5339
5340    /// Test that killing a terminal via Terminal::kill properly:
5341    /// 1. Causes wait_for_exit to complete (doesn't hang forever)
5342    /// 2. The underlying terminal still has the output that was written before the kill
5343    ///
5344    /// This test verifies that the fix to kill_active_task (which now also kills
5345    /// the shell process in addition to the foreground process) properly allows
5346    /// wait_for_exit to complete instead of hanging indefinitely.
5347    #[cfg(unix)]
5348    #[gpui::test]
5349    async fn test_terminal_kill_allows_wait_for_exit_to_complete(cx: &mut gpui::TestAppContext) {
5350        use std::collections::HashMap;
5351        use task::Shell;
5352        use util::shell_builder::ShellBuilder;
5353
5354        init_test(cx);
5355        cx.executor().allow_parking();
5356
5357        let fs = FakeFs::new(cx.executor());
5358        let project = Project::test(fs, [], cx).await;
5359        let connection = Rc::new(FakeAgentConnection::new());
5360        let thread = cx
5361            .update(|cx| {
5362                connection.new_session(
5363                    project.clone(),
5364                    PathList::new(&[Path::new(path!("/test"))]),
5365                    cx,
5366                )
5367            })
5368            .await
5369            .unwrap();
5370
5371        let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
5372
5373        // Create a real PTY terminal that runs a command which prints output then sleeps
5374        // We use printf instead of echo and chain with && sleep to ensure proper execution
5375        let (completion_tx, _completion_rx) = async_channel::unbounded();
5376        let (program, args) = ShellBuilder::new(&Shell::System, false).build(
5377            Some("printf 'output_before_kill\\n' && sleep 60".to_owned()),
5378            &[],
5379        );
5380
5381        let builder = cx
5382            .update(|cx| {
5383                ::terminal::TerminalBuilder::new(
5384                    None,
5385                    None,
5386                    task::Shell::WithArguments {
5387                        program,
5388                        args,
5389                        title_override: None,
5390                    },
5391                    HashMap::default(),
5392                    ::terminal::terminal_settings::CursorShape::default(),
5393                    ::terminal::terminal_settings::AlternateScroll::On,
5394                    None,
5395                    vec![],
5396                    0,
5397                    false,
5398                    0,
5399                    Some(completion_tx),
5400                    cx,
5401                    vec![],
5402                    PathStyle::local(),
5403                )
5404            })
5405            .await
5406            .unwrap();
5407
5408        let lower_terminal = cx.new(|cx| builder.subscribe(cx));
5409
5410        // Create the acp_thread Terminal wrapper
5411        thread.update(cx, |thread, cx| {
5412            thread.on_terminal_provider_event(
5413                TerminalProviderEvent::Created {
5414                    terminal_id: terminal_id.clone(),
5415                    label: "printf output_before_kill && sleep 60".to_string(),
5416                    cwd: None,
5417                    output_byte_limit: None,
5418                    terminal: lower_terminal.clone(),
5419                },
5420                cx,
5421            );
5422        });
5423
5424        // Poll until the printf command produces output, rather than using a
5425        // fixed sleep which is flaky on loaded machines.
5426        let deadline = std::time::Instant::now() + Duration::from_secs(10);
5427        loop {
5428            let has_output = thread.read_with(cx, |thread, cx| {
5429                let term = thread
5430                    .terminals
5431                    .get(&terminal_id)
5432                    .expect("terminal not found");
5433                let content = term.read(cx).inner().read(cx).get_content();
5434                content.contains("output_before_kill")
5435            });
5436            if has_output {
5437                break;
5438            }
5439            assert!(
5440                std::time::Instant::now() < deadline,
5441                "Timed out waiting for printf output to appear in terminal",
5442            );
5443            cx.executor().timer(Duration::from_millis(50)).await;
5444        }
5445
5446        // Get the acp_thread Terminal and kill it
5447        let wait_for_exit = thread.update(cx, |thread, cx| {
5448            let term = thread.terminals.get(&terminal_id).unwrap();
5449            let wait_for_exit = term.read(cx).wait_for_exit();
5450            term.update(cx, |term, cx| {
5451                term.kill(cx);
5452            });
5453            wait_for_exit
5454        });
5455
5456        // KEY ASSERTION: wait_for_exit should complete within a reasonable time (not hang).
5457        // Before the fix to kill_active_task, this would hang forever because
5458        // only the foreground process was killed, not the shell, so the PTY
5459        // child never exited and wait_for_completed_task never completed.
5460        let exit_result = futures::select! {
5461            result = futures::FutureExt::fuse(wait_for_exit) => Some(result),
5462            _ = futures::FutureExt::fuse(cx.background_executor.timer(Duration::from_secs(5))) => None,
5463        };
5464
5465        assert!(
5466            exit_result.is_some(),
5467            "wait_for_exit should complete after kill, but it timed out. \
5468            This indicates kill_active_task is not properly killing the shell process."
5469        );
5470
5471        // Give the system a chance to process any pending updates
5472        cx.run_until_parked();
5473
5474        // Verify that the underlying terminal still has the output that was
5475        // written before the kill. This verifies that killing doesn't lose output.
5476        let inner_content = thread.read_with(cx, |thread, cx| {
5477            let term = thread.terminals.get(&terminal_id).unwrap();
5478            term.read(cx).inner().read(cx).get_content()
5479        });
5480
5481        assert!(
5482            inner_content.contains("output_before_kill"),
5483            "Underlying terminal should contain output from before kill, got: {}",
5484            inner_content
5485        );
5486    }
5487
5488    #[gpui::test]
5489    async fn test_push_user_content_block(cx: &mut gpui::TestAppContext) {
5490        init_test(cx);
5491
5492        let fs = FakeFs::new(cx.executor());
5493        let project = Project::test(fs, [], cx).await;
5494        let connection = Rc::new(FakeAgentConnection::new());
5495        let thread = cx
5496            .update(|cx| {
5497                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
5498            })
5499            .await
5500            .unwrap();
5501
5502        // Test creating a new user message
5503        thread.update(cx, |thread, cx| {
5504            thread.push_user_content_block(None, "Hello, ".into(), cx);
5505        });
5506
5507        thread.update(cx, |thread, cx| {
5508            assert_eq!(thread.entries.len(), 1);
5509            if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] {
5510                assert_eq!(user_msg.protocol_id, None);
5511                assert_eq!(user_msg.client_id, None);
5512                assert_eq!(user_msg.content.to_markdown(cx), "Hello, ");
5513            } else {
5514                panic!("Expected UserMessage");
5515            }
5516        });
5517
5518        // Test appending to existing user message
5519        let message_1_id = ClientUserMessageId::new();
5520        thread.update(cx, |thread, cx| {
5521            thread.push_user_content_block(Some(message_1_id.clone()), "world!".into(), cx);
5522        });
5523
5524        thread.update(cx, |thread, cx| {
5525            assert_eq!(thread.entries.len(), 1);
5526            if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] {
5527                assert_eq!(user_msg.protocol_id, None);
5528                assert_eq!(user_msg.client_id, Some(message_1_id));
5529                assert_eq!(user_msg.content.to_markdown(cx), "Hello, world!");
5530            } else {
5531                panic!("Expected UserMessage");
5532            }
5533        });
5534
5535        // Test creating new user message after assistant message
5536        thread.update(cx, |thread, cx| {
5537            thread.push_assistant_content_block("Assistant response".into(), false, cx);
5538        });
5539
5540        let message_2_id = ClientUserMessageId::new();
5541        thread.update(cx, |thread, cx| {
5542            thread.push_user_content_block(
5543                Some(message_2_id.clone()),
5544                "New user message".into(),
5545                cx,
5546            );
5547        });
5548
5549        thread.update(cx, |thread, cx| {
5550            assert_eq!(thread.entries.len(), 3);
5551            if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[2] {
5552                assert_eq!(user_msg.protocol_id, None);
5553                assert_eq!(user_msg.client_id, Some(message_2_id));
5554                assert_eq!(user_msg.content.to_markdown(cx), "New user message");
5555            } else {
5556                panic!("Expected UserMessage at index 2");
5557            }
5558        });
5559    }
5560
5561    #[gpui::test]
5562    async fn test_user_message_chunks_use_protocol_message_id_boundaries(
5563        cx: &mut gpui::TestAppContext,
5564    ) {
5565        init_test(cx);
5566
5567        let fs = FakeFs::new(cx.executor());
5568        let project = Project::test(fs, [], cx).await;
5569        let connection = Rc::new(FakeAgentConnection::new());
5570        let thread = cx
5571            .update(|cx| {
5572                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
5573            })
5574            .await
5575            .unwrap();
5576
5577        thread.update(cx, |thread, cx| {
5578            thread
5579                .handle_session_update(
5580                    acp::SessionUpdate::UserMessageChunk(
5581                        acp::ContentChunk::new("First ".into()).message_id("msg_user_1"),
5582                    ),
5583                    cx,
5584                )
5585                .unwrap();
5586            thread
5587                .handle_session_update(
5588                    acp::SessionUpdate::UserMessageChunk(
5589                        acp::ContentChunk::new("message".into()).message_id("msg_user_1"),
5590                    ),
5591                    cx,
5592                )
5593                .unwrap();
5594            thread
5595                .handle_session_update(
5596                    acp::SessionUpdate::UserMessageChunk(
5597                        acp::ContentChunk::new("Second message".into()).message_id("msg_user_2"),
5598                    ),
5599                    cx,
5600                )
5601                .unwrap();
5602            thread
5603                .handle_session_update(
5604                    acp::SessionUpdate::UserMessageChunk(
5605                        acp::ContentChunk::new("Echo".into()).message_id("msg_user_3"),
5606                    ),
5607                    cx,
5608                )
5609                .unwrap();
5610            thread
5611                .handle_session_update(
5612                    acp::SessionUpdate::UserMessageChunk(
5613                        acp::ContentChunk::new("Echo".into()).message_id("msg_user_3"),
5614                    ),
5615                    cx,
5616                )
5617                .unwrap();
5618        });
5619
5620        thread.update(cx, |thread, cx| {
5621            assert_eq!(thread.entries.len(), 3);
5622
5623            let AgentThreadEntry::UserMessage(first_message) = &thread.entries[0] else {
5624                panic!("expected first entry to be a user message")
5625            };
5626            assert_eq!(first_message.content.to_markdown(cx), "First message");
5627            assert_eq!(
5628                first_message
5629                    .protocol_id
5630                    .as_ref()
5631                    .map(ToString::to_string)
5632                    .as_deref(),
5633                Some("msg_user_1")
5634            );
5635
5636            let AgentThreadEntry::UserMessage(second_message) = &thread.entries[1] else {
5637                panic!("expected second entry to be a user message")
5638            };
5639            assert_eq!(second_message.content.to_markdown(cx), "Second message");
5640            assert_eq!(
5641                second_message
5642                    .protocol_id
5643                    .as_ref()
5644                    .map(ToString::to_string)
5645                    .as_deref(),
5646                Some("msg_user_2")
5647            );
5648
5649            let AgentThreadEntry::UserMessage(third_message) = &thread.entries[2] else {
5650                panic!("expected third entry to be a user message")
5651            };
5652            assert_eq!(third_message.content.to_markdown(cx), "EchoEcho");
5653            assert_eq!(
5654                third_message
5655                    .protocol_id
5656                    .as_ref()
5657                    .map(ToString::to_string)
5658                    .as_deref(),
5659                Some("msg_user_3")
5660            );
5661        });
5662    }
5663
5664    #[gpui::test]
5665    async fn test_protocol_user_chunk_does_not_merge_into_optimistic_prompt(
5666        cx: &mut gpui::TestAppContext,
5667    ) {
5668        init_test(cx);
5669
5670        let fs = FakeFs::new(cx.executor());
5671        let project = Project::test(fs, [], cx).await;
5672        let connection = Rc::new(FakeAgentConnection::new());
5673        let thread = cx
5674            .update(|cx| {
5675                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
5676            })
5677            .await
5678            .unwrap();
5679
5680        thread.update(cx, |thread, cx| {
5681            thread.push_user_content_block_with_protocol_id(
5682                None,
5683                true,
5684                None,
5685                "Typed prompt".into(),
5686                false,
5687                cx,
5688            );
5689            thread
5690                .handle_session_update(
5691                    acp::SessionUpdate::UserMessageChunk(
5692                        acp::ContentChunk::new("Agent user chunk".into())
5693                            .message_id("agent_user_chunk"),
5694                    ),
5695                    cx,
5696                )
5697                .unwrap();
5698        });
5699
5700        thread.update(cx, |thread, cx| {
5701            assert_eq!(thread.entries.len(), 2);
5702
5703            let AgentThreadEntry::UserMessage(optimistic_message) = &thread.entries[0] else {
5704                panic!("expected first entry to be optimistic user message")
5705            };
5706            assert!(optimistic_message.is_optimistic);
5707            assert_eq!(optimistic_message.content.to_markdown(cx), "Typed prompt");
5708            assert!(optimistic_message.protocol_id.is_none());
5709            assert!(optimistic_message.client_id.is_none());
5710
5711            let AgentThreadEntry::UserMessage(agent_message) = &thread.entries[1] else {
5712                panic!("expected second entry to be protocol user chunk")
5713            };
5714            assert!(!agent_message.is_optimistic);
5715            assert_eq!(agent_message.content.to_markdown(cx), "Agent user chunk");
5716            assert_eq!(
5717                agent_message
5718                    .protocol_id
5719                    .as_ref()
5720                    .map(ToString::to_string)
5721                    .as_deref(),
5722                Some("agent_user_chunk")
5723            );
5724        });
5725    }
5726
5727    #[gpui::test]
5728    async fn test_assistant_chunks_use_protocol_message_id_boundaries(
5729        cx: &mut gpui::TestAppContext,
5730    ) {
5731        init_test(cx);
5732
5733        let fs = FakeFs::new(cx.executor());
5734        let project = Project::test(fs, [], cx).await;
5735        let connection = Rc::new(FakeAgentConnection::new());
5736        let thread = cx
5737            .update(|cx| {
5738                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
5739            })
5740            .await
5741            .unwrap();
5742
5743        thread.update(cx, |thread, cx| {
5744            thread
5745                .handle_session_update(
5746                    acp::SessionUpdate::AgentThoughtChunk(
5747                        acp::ContentChunk::new("Thinking ".into()).message_id("msg_thought_1"),
5748                    ),
5749                    cx,
5750                )
5751                .unwrap();
5752            thread
5753                .handle_session_update(
5754                    acp::SessionUpdate::AgentThoughtChunk(
5755                        acp::ContentChunk::new("hard".into()).message_id("msg_thought_1"),
5756                    ),
5757                    cx,
5758                )
5759                .unwrap();
5760            thread
5761                .handle_session_update(
5762                    acp::SessionUpdate::AgentThoughtChunk(
5763                        acp::ContentChunk::new("A separate thought".into())
5764                            .message_id("msg_thought_2"),
5765                    ),
5766                    cx,
5767                )
5768                .unwrap();
5769            thread
5770                .handle_session_update(
5771                    acp::SessionUpdate::AgentMessageChunk(
5772                        acp::ContentChunk::new("Answer ".into()).message_id("msg_agent_1"),
5773                    ),
5774                    cx,
5775                )
5776                .unwrap();
5777            thread
5778                .handle_session_update(
5779                    acp::SessionUpdate::AgentMessageChunk(
5780                        acp::ContentChunk::new("done".into()).message_id("msg_agent_1"),
5781                    ),
5782                    cx,
5783                )
5784                .unwrap();
5785            thread
5786                .handle_session_update(
5787                    acp::SessionUpdate::AgentMessageChunk(
5788                        acp::ContentChunk::new("Follow-up".into()).message_id("msg_agent_2"),
5789                    ),
5790                    cx,
5791                )
5792                .unwrap();
5793        });
5794
5795        thread.update(cx, |thread, cx| {
5796            assert_eq!(thread.entries.len(), 1);
5797            let AgentThreadEntry::AssistantMessage(message) = &thread.entries[0] else {
5798                panic!("expected assistant entry")
5799            };
5800            assert_eq!(message.chunks.len(), 4);
5801
5802            let AssistantMessageChunk::Thought { id, block } = &message.chunks[0] else {
5803                panic!("expected first chunk to be a thought")
5804            };
5805            assert_eq!(block.to_markdown(cx), "Thinking hard");
5806            assert_eq!(
5807                id.as_ref().map(ToString::to_string).as_deref(),
5808                Some("msg_thought_1")
5809            );
5810
5811            let AssistantMessageChunk::Thought { id, block } = &message.chunks[1] else {
5812                panic!("expected second chunk to be a thought")
5813            };
5814            assert_eq!(block.to_markdown(cx), "A separate thought");
5815            assert_eq!(
5816                id.as_ref().map(ToString::to_string).as_deref(),
5817                Some("msg_thought_2")
5818            );
5819
5820            let AssistantMessageChunk::Message { id, block } = &message.chunks[2] else {
5821                panic!("expected third chunk to be a message")
5822            };
5823            assert_eq!(block.to_markdown(cx), "Answer done");
5824            assert_eq!(
5825                id.as_ref().map(ToString::to_string).as_deref(),
5826                Some("msg_agent_1")
5827            );
5828
5829            let AssistantMessageChunk::Message { id, block } = &message.chunks[3] else {
5830                panic!("expected fourth chunk to be a message")
5831            };
5832            assert_eq!(block.to_markdown(cx), "Follow-up");
5833            assert_eq!(
5834                id.as_ref().map(ToString::to_string).as_deref(),
5835                Some("msg_agent_2")
5836            );
5837        });
5838    }
5839
5840    #[gpui::test]
5841    async fn test_thinking_concatenation(cx: &mut gpui::TestAppContext) {
5842        init_test(cx);
5843
5844        let fs = FakeFs::new(cx.executor());
5845        let project = Project::test(fs, [], cx).await;
5846        let connection = Rc::new(FakeAgentConnection::new().on_user_message(
5847            |_, thread, mut cx| {
5848                async move {
5849                    thread.update(&mut cx, |thread, cx| {
5850                        thread
5851                            .handle_session_update(
5852                                acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
5853                                    "Thinking ".into(),
5854                                )),
5855                                cx,
5856                            )
5857                            .unwrap();
5858                        thread
5859                            .handle_session_update(
5860                                acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
5861                                    "hard!".into(),
5862                                )),
5863                                cx,
5864                            )
5865                            .unwrap();
5866                    })?;
5867                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
5868                }
5869                .boxed_local()
5870            },
5871        ));
5872
5873        let thread = cx
5874            .update(|cx| {
5875                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
5876            })
5877            .await
5878            .unwrap();
5879
5880        thread
5881            .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx))
5882            .await
5883            .unwrap();
5884
5885        let output = thread.read_with(cx, |thread, cx| thread.to_markdown(cx));
5886        assert_eq!(
5887            output,
5888            indoc! {r#"
5889            ## User
5890
5891            Hello from Zed!
5892
5893            ## Assistant
5894
5895            <thinking>
5896            Thinking hard!
5897            </thinking>
5898
5899            "#}
5900        );
5901    }
5902
5903    /// `send_command` runs the turn (the connection receives the typed command)
5904    /// but never echoes a user-message bubble, so commands like `/compact` don't
5905    /// show a fake user message implying the text was sent to the model.
5906    #[gpui::test]
5907    async fn test_send_command_does_not_echo_user_message(cx: &mut gpui::TestAppContext) {
5908        init_test(cx);
5909
5910        let fs = FakeFs::new(cx.executor());
5911        let project = Project::test(fs, [], cx).await;
5912
5913        let received_prompt: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
5914        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
5915            let received_prompt = received_prompt.clone();
5916            move |request, thread, mut cx| {
5917                let received_prompt = received_prompt.clone();
5918                async move {
5919                    if let Some(acp::ContentBlock::Text(text)) = request.prompt.first() {
5920                        *received_prompt.borrow_mut() = Some(text.text.clone());
5921                    }
5922                    // Simulate a native command producing its own thread entry
5923                    // (here a compaction) rather than echoing a user message.
5924                    thread.update(&mut cx, |thread, cx| {
5925                        thread.push_context_compaction(
5926                            ContextCompaction {
5927                                id: ContextCompactionId("c1".into()),
5928                                status: ContextCompactionStatus::Completed,
5929                                summary: None,
5930                            },
5931                            cx,
5932                        );
5933                    })?;
5934                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
5935                }
5936                .boxed_local()
5937            }
5938        }));
5939
5940        let thread = cx
5941            .update(|cx| {
5942                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
5943            })
5944            .await
5945            .unwrap();
5946
5947        cx.update(|cx| {
5948            thread.update(cx, |thread, cx| {
5949                thread.send_command(vec!["/compact".into()], cx)
5950            })
5951        })
5952        .await
5953        .unwrap();
5954
5955        // The command turn ran: the connection received the typed command.
5956        assert_eq!(received_prompt.borrow().as_deref(), Some("/compact"));
5957
5958        thread.update(cx, |thread, _cx| {
5959            assert!(
5960                !thread
5961                    .entries
5962                    .iter()
5963                    .any(|entry| matches!(entry, AgentThreadEntry::UserMessage(_))),
5964                "send_command must not echo a user message"
5965            );
5966            // The command's own entry (here a compaction) is still shown.
5967            assert!(
5968                thread
5969                    .entries
5970                    .iter()
5971                    .any(|entry| matches!(entry, AgentThreadEntry::ContextCompaction(_))),
5972                "the command's own thread entry should still be present"
5973            );
5974        });
5975    }
5976
5977    #[gpui::test]
5978    async fn test_ignore_echoed_user_message_chunks_during_active_turn(
5979        cx: &mut gpui::TestAppContext,
5980    ) {
5981        init_test(cx);
5982
5983        let fs = FakeFs::new(cx.executor());
5984        let project = Project::test(fs, [], cx).await;
5985        let connection = Rc::new(
5986            FakeAgentConnection::new()
5987                .without_truncate_support()
5988                .on_user_message(|request, thread, mut cx| {
5989                    async move {
5990                        let prompt = request.prompt.first().cloned().unwrap_or_else(|| "".into());
5991
5992                        thread.update(&mut cx, |thread, cx| {
5993                            thread
5994                                .handle_session_update(
5995                                    acp::SessionUpdate::UserMessageChunk(acp::ContentChunk::new(
5996                                        prompt,
5997                                    )),
5998                                    cx,
5999                                )
6000                                .unwrap();
6001                        })?;
6002
6003                        Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
6004                    }
6005                    .boxed_local()
6006                }),
6007        );
6008
6009        let thread = cx
6010            .update(|cx| {
6011                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
6012            })
6013            .await
6014            .unwrap();
6015
6016        thread
6017            .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx))
6018            .await
6019            .unwrap();
6020
6021        let output = thread.read_with(cx, |thread, cx| thread.to_markdown(cx));
6022        assert_eq!(output.matches("Hello from Zed!").count(), 1);
6023        thread.read_with(cx, |thread, _cx| {
6024            let Some(AgentThreadEntry::UserMessage(message)) = thread.entries.first() else {
6025                panic!("expected optimistic user message");
6026            };
6027            assert_eq!(message.protocol_id, None);
6028            assert_eq!(message.client_id, None);
6029            assert!(message.is_optimistic);
6030        });
6031    }
6032
6033    #[gpui::test]
6034    async fn test_edits_concurrently_to_user(cx: &mut TestAppContext) {
6035        init_test(cx);
6036
6037        let fs = FakeFs::new(cx.executor());
6038        fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\n"}))
6039            .await;
6040        let project = Project::test(fs.clone(), [], cx).await;
6041        let (read_file_tx, read_file_rx) = oneshot::channel::<()>();
6042        let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx)));
6043        let connection = Rc::new(FakeAgentConnection::new().on_user_message(
6044            move |_, thread, mut cx| {
6045                let read_file_tx = read_file_tx.clone();
6046                async move {
6047                    let content = thread
6048                        .update(&mut cx, |thread, cx| {
6049                            thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx)
6050                        })
6051                        .unwrap()
6052                        .await
6053                        .unwrap();
6054                    assert_eq!(content, "one\ntwo\nthree\n");
6055                    read_file_tx.take().unwrap().send(()).unwrap();
6056                    thread
6057                        .update(&mut cx, |thread, cx| {
6058                            thread.write_text_file(
6059                                path!("/tmp/foo").into(),
6060                                "one\ntwo\nthree\nfour\nfive\n".to_string(),
6061                                cx,
6062                            )
6063                        })
6064                        .unwrap()
6065                        .await
6066                        .unwrap();
6067                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
6068                }
6069                .boxed_local()
6070            },
6071        ));
6072
6073        let (worktree, pathbuf) = project
6074            .update(cx, |project, cx| {
6075                project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
6076            })
6077            .await
6078            .unwrap();
6079        let buffer = project
6080            .update(cx, |project, cx| {
6081                project.open_buffer((worktree.read(cx).id(), pathbuf), cx)
6082            })
6083            .await
6084            .unwrap();
6085
6086        let thread = cx
6087            .update(|cx| {
6088                connection.new_session(project, PathList::new(&[Path::new(path!("/tmp"))]), cx)
6089            })
6090            .await
6091            .unwrap();
6092
6093        let request = thread.update(cx, |thread, cx| {
6094            thread.send_raw("Extend the count in /tmp/foo", cx)
6095        });
6096        read_file_rx.await.ok();
6097        buffer.update(cx, |buffer, cx| {
6098            buffer.edit([(0..0, "zero\n".to_string())], None, cx);
6099        });
6100        cx.run_until_parked();
6101        assert_eq!(
6102            buffer.read_with(cx, |buffer, _| buffer.text()),
6103            "zero\none\ntwo\nthree\nfour\nfive\n"
6104        );
6105        assert_eq!(
6106            String::from_utf8(fs.read_file_sync(path!("/tmp/foo")).unwrap()).unwrap(),
6107            "zero\none\ntwo\nthree\nfour\nfive\n"
6108        );
6109        request.await.unwrap();
6110    }
6111
6112    #[gpui::test]
6113    async fn test_reading_from_line(cx: &mut TestAppContext) {
6114        init_test(cx);
6115
6116        let fs = FakeFs::new(cx.executor());
6117        fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\nfour\n"}))
6118            .await;
6119        let project = Project::test(fs.clone(), [], cx).await;
6120        project
6121            .update(cx, |project, cx| {
6122                project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
6123            })
6124            .await
6125            .unwrap();
6126
6127        let connection = Rc::new(FakeAgentConnection::new());
6128
6129        let thread = cx
6130            .update(|cx| {
6131                connection.new_session(project, PathList::new(&[Path::new(path!("/tmp"))]), cx)
6132            })
6133            .await
6134            .unwrap();
6135
6136        // Whole file
6137        let content = thread
6138            .update(cx, |thread, cx| {
6139                thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx)
6140            })
6141            .await
6142            .unwrap();
6143
6144        assert_eq!(content, "one\ntwo\nthree\nfour\n");
6145
6146        // Only start line
6147        let content = thread
6148            .update(cx, |thread, cx| {
6149                thread.read_text_file(path!("/tmp/foo").into(), Some(3), None, false, cx)
6150            })
6151            .await
6152            .unwrap();
6153
6154        assert_eq!(content, "three\nfour\n");
6155
6156        // Only limit
6157        let content = thread
6158            .update(cx, |thread, cx| {
6159                thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx)
6160            })
6161            .await
6162            .unwrap();
6163
6164        assert_eq!(content, "one\ntwo\n");
6165
6166        // Range
6167        let content = thread
6168            .update(cx, |thread, cx| {
6169                thread.read_text_file(path!("/tmp/foo").into(), Some(2), Some(2), false, cx)
6170            })
6171            .await
6172            .unwrap();
6173
6174        assert_eq!(content, "two\nthree\n");
6175
6176        // Invalid
6177        let err = thread
6178            .update(cx, |thread, cx| {
6179                thread.read_text_file(path!("/tmp/foo").into(), Some(6), Some(2), false, cx)
6180            })
6181            .await
6182            .unwrap_err();
6183
6184        assert_eq!(
6185            err.to_string(),
6186            "Invalid params: \"Attempting to read beyond the end of the file, line 5:0\""
6187        );
6188    }
6189
6190    #[gpui::test]
6191    async fn test_reading_empty_file(cx: &mut TestAppContext) {
6192        init_test(cx);
6193
6194        let fs = FakeFs::new(cx.executor());
6195        fs.insert_tree(path!("/tmp"), json!({"foo": ""})).await;
6196        let project = Project::test(fs.clone(), [], cx).await;
6197        project
6198            .update(cx, |project, cx| {
6199                project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
6200            })
6201            .await
6202            .unwrap();
6203
6204        let connection = Rc::new(FakeAgentConnection::new());
6205
6206        let thread = cx
6207            .update(|cx| {
6208                connection.new_session(project, PathList::new(&[Path::new(path!("/tmp"))]), cx)
6209            })
6210            .await
6211            .unwrap();
6212
6213        // Whole file
6214        let content = thread
6215            .update(cx, |thread, cx| {
6216                thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx)
6217            })
6218            .await
6219            .unwrap();
6220
6221        assert_eq!(content, "");
6222
6223        // Only start line
6224        let content = thread
6225            .update(cx, |thread, cx| {
6226                thread.read_text_file(path!("/tmp/foo").into(), Some(1), None, false, cx)
6227            })
6228            .await
6229            .unwrap();
6230
6231        assert_eq!(content, "");
6232
6233        // Only limit
6234        let content = thread
6235            .update(cx, |thread, cx| {
6236                thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx)
6237            })
6238            .await
6239            .unwrap();
6240
6241        assert_eq!(content, "");
6242
6243        // Range
6244        let content = thread
6245            .update(cx, |thread, cx| {
6246                thread.read_text_file(path!("/tmp/foo").into(), Some(1), Some(1), false, cx)
6247            })
6248            .await
6249            .unwrap();
6250
6251        assert_eq!(content, "");
6252
6253        // Invalid
6254        let err = thread
6255            .update(cx, |thread, cx| {
6256                thread.read_text_file(path!("/tmp/foo").into(), Some(5), Some(2), false, cx)
6257            })
6258            .await
6259            .unwrap_err();
6260
6261        assert_eq!(
6262            err.to_string(),
6263            "Invalid params: \"Attempting to read beyond the end of the file, line 1:0\""
6264        );
6265    }
6266    #[gpui::test]
6267    async fn test_reading_non_existing_file(cx: &mut TestAppContext) {
6268        init_test(cx);
6269
6270        let fs = FakeFs::new(cx.executor());
6271        fs.insert_tree(path!("/tmp"), json!({})).await;
6272        let project = Project::test(fs.clone(), [], cx).await;
6273        project
6274            .update(cx, |project, cx| {
6275                project.find_or_create_worktree(path!("/tmp"), true, cx)
6276            })
6277            .await
6278            .unwrap();
6279
6280        let connection = Rc::new(FakeAgentConnection::new());
6281
6282        let thread = cx
6283            .update(|cx| {
6284                connection.new_session(project, PathList::new(&[Path::new(path!("/tmp"))]), cx)
6285            })
6286            .await
6287            .unwrap();
6288
6289        // Out of project file
6290        let err = thread
6291            .update(cx, |thread, cx| {
6292                thread.read_text_file(path!("/foo").into(), None, None, false, cx)
6293            })
6294            .await
6295            .unwrap_err();
6296
6297        assert_eq!(err.code, acp::ErrorCode::ResourceNotFound);
6298    }
6299
6300    #[gpui::test]
6301    async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) {
6302        init_test(cx);
6303
6304        let fs = FakeFs::new(cx.executor());
6305        let project = Project::test(fs, [], cx).await;
6306        let id = acp::ToolCallId::new("test");
6307
6308        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
6309            let id = id.clone();
6310            move |_, thread, mut cx| {
6311                let id = id.clone();
6312                async move {
6313                    thread
6314                        .update(&mut cx, |thread, cx| {
6315                            thread.handle_session_update(
6316                                acp::SessionUpdate::ToolCall(
6317                                    acp::ToolCall::new(id.clone(), "Label")
6318                                        .kind(acp::ToolKind::Fetch)
6319                                        .status(acp::ToolCallStatus::InProgress),
6320                                ),
6321                                cx,
6322                            )
6323                        })
6324                        .unwrap()
6325                        .unwrap();
6326                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
6327                }
6328                .boxed_local()
6329            }
6330        }));
6331
6332        let thread = cx
6333            .update(|cx| {
6334                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
6335            })
6336            .await
6337            .unwrap();
6338
6339        let request = thread.update(cx, |thread, cx| {
6340            thread.send_raw("Fetch https://example.com", cx)
6341        });
6342
6343        run_until_first_tool_call(&thread, cx).await;
6344
6345        thread.read_with(cx, |thread, _| {
6346            assert!(matches!(
6347                thread.entries[1],
6348                AgentThreadEntry::ToolCall(ToolCall {
6349                    status: ToolCallStatus::InProgress,
6350                    ..
6351                })
6352            ));
6353        });
6354
6355        thread.update(cx, |thread, cx| thread.cancel(cx)).await;
6356
6357        thread.read_with(cx, |thread, _| {
6358            assert!(matches!(
6359                &thread.entries[1],
6360                AgentThreadEntry::ToolCall(ToolCall {
6361                    status: ToolCallStatus::Canceled,
6362                    ..
6363                })
6364            ));
6365        });
6366
6367        thread
6368            .update(cx, |thread, cx| {
6369                thread.handle_session_update(
6370                    acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
6371                        id,
6372                        acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
6373                    )),
6374                    cx,
6375                )
6376            })
6377            .unwrap();
6378
6379        request.await.unwrap();
6380
6381        thread.read_with(cx, |thread, _| {
6382            assert!(matches!(
6383                thread.entries[1],
6384                AgentThreadEntry::ToolCall(ToolCall {
6385                    status: ToolCallStatus::Completed,
6386                    ..
6387                })
6388            ));
6389        });
6390    }
6391
6392    #[gpui::test]
6393    async fn test_tool_call_location_resolves_external_file(cx: &mut TestAppContext) {
6394        init_test(cx);
6395
6396        let fs = FakeFs::new(cx.executor());
6397        fs.insert_tree(
6398            path!("/tmp/skills/test-skill"),
6399            json!({ "SKILL.md": "skill body" }),
6400        )
6401        .await;
6402        let project = Project::test(fs, [], cx).await;
6403        let connection = Rc::new(FakeAgentConnection::new());
6404        let thread = cx
6405            .update(|cx| {
6406                connection.new_session(project, PathList::new(&[Path::new(path!("/project"))]), cx)
6407            })
6408            .await
6409            .unwrap();
6410
6411        let skill_path = std::path::PathBuf::from(path!("/tmp/skills/test-skill/SKILL.md"));
6412        thread
6413            .update(cx, |thread, cx| {
6414                thread.handle_session_update(
6415                    acp::SessionUpdate::ToolCall(
6416                        acp::ToolCall::new("write_file", "Write SKILL.md")
6417                            .kind(acp::ToolKind::Edit)
6418                            .status(acp::ToolCallStatus::Completed)
6419                            .locations(vec![acp::ToolCallLocation::new(skill_path.clone())]),
6420                    ),
6421                    cx,
6422                )
6423            })
6424            .unwrap();
6425
6426        cx.run_until_parked();
6427
6428        thread.read_with(cx, |thread, cx| {
6429            let (tool_call_location, agent_location) = thread.entries[0]
6430                .location(0)
6431                .expect("external tool-call location should resolve");
6432            assert_eq!(tool_call_location.path, skill_path);
6433
6434            let buffer = agent_location
6435                .buffer
6436                .upgrade()
6437                .expect("resolved location should keep an open buffer");
6438            assert_eq!(buffer.read(cx).text(), "skill body");
6439        });
6440    }
6441
6442    #[gpui::test]
6443    async fn test_duplicate_tool_call_update_preserves_open_permission_request_until_authorized(
6444        cx: &mut TestAppContext,
6445    ) {
6446        init_test(cx);
6447
6448        let fs = FakeFs::new(cx.executor());
6449        let project = Project::test(fs, [], cx).await;
6450        let connection = Rc::new(FakeAgentConnection::new());
6451        let thread = cx
6452            .update(|cx| {
6453                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
6454            })
6455            .await
6456            .unwrap();
6457
6458        let tool_call_id = acp::ToolCallId::new("toolu_01duplicate");
6459        let allow_option_id = acp::PermissionOptionId::new("allow");
6460        let permission_task = thread
6461            .update(cx, |thread, cx| {
6462                thread.request_tool_call_authorization(
6463                    acp::ToolCall::new(tool_call_id.clone(), "Original title")
6464                        .kind(acp::ToolKind::Execute)
6465                        .status(acp::ToolCallStatus::Pending)
6466                        .content(vec!["original content".into()])
6467                        .into(),
6468                    PermissionOptions::Flat(vec![acp::PermissionOption::new(
6469                        allow_option_id.clone(),
6470                        "Allow",
6471                        acp::PermissionOptionKind::AllowOnce,
6472                    )]),
6473                    AuthorizationKind::PermissionGrant,
6474                    cx,
6475                )
6476            })
6477            .unwrap();
6478
6479        thread
6480            .update(cx, |thread, cx| {
6481                thread.handle_session_update(
6482                    acp::SessionUpdate::ToolCall(
6483                        acp::ToolCall::new(tool_call_id.clone(), "Updated title")
6484                            .kind(acp::ToolKind::Execute)
6485                            .status(acp::ToolCallStatus::Pending)
6486                            .content(vec!["updated content".into()]),
6487                    ),
6488                    cx,
6489                )
6490            })
6491            .unwrap();
6492
6493        thread.read_with(cx, |thread, cx| {
6494            let (_, tool_call) = thread
6495                .tool_call(&tool_call_id)
6496                .expect("tool call should exist");
6497            assert_eq!(tool_call.label.read(cx).source(), "Updated title");
6498            assert!(matches!(
6499                tool_call.status,
6500                ToolCallStatus::WaitingForConfirmation { .. }
6501            ));
6502            assert_eq!(tool_call.content.len(), 1);
6503            assert_eq!(tool_call.content[0].to_markdown(cx), "updated content");
6504        });
6505
6506        thread
6507            .update(cx, |thread, cx| {
6508                thread.handle_session_update(
6509                    acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
6510                        tool_call_id.clone(),
6511                        acp::ToolCallUpdateFields::new()
6512                            .status(acp::ToolCallStatus::InProgress)
6513                            .title("Updated again")
6514                            .content(vec!["updated again".into()]),
6515                    )),
6516                    cx,
6517                )
6518            })
6519            .unwrap();
6520
6521        thread.read_with(cx, |thread, cx| {
6522            let (_, tool_call) = thread
6523                .tool_call(&tool_call_id)
6524                .expect("tool call should exist");
6525            assert_eq!(tool_call.label.read(cx).source(), "Updated again");
6526            assert!(matches!(
6527                tool_call.status,
6528                ToolCallStatus::WaitingForConfirmation { .. }
6529            ));
6530            assert_eq!(tool_call.content.len(), 1);
6531            assert_eq!(tool_call.content[0].to_markdown(cx), "updated again");
6532        });
6533
6534        let selected_outcome = SelectedPermissionOutcome::new(
6535            allow_option_id.clone(),
6536            acp::PermissionOptionKind::AllowOnce,
6537        );
6538        thread.update(cx, |thread, cx| {
6539            thread.authorize_tool_call(tool_call_id.clone(), selected_outcome, cx);
6540        });
6541
6542        thread.read_with(cx, |thread, _cx| {
6543            let (_, tool_call) = thread
6544                .tool_call(&tool_call_id)
6545                .expect("tool call should exist");
6546            assert!(matches!(tool_call.status, ToolCallStatus::InProgress));
6547        });
6548
6549        match permission_task.await {
6550            RequestPermissionOutcome::Selected(outcome) => {
6551                assert_eq!(outcome.option_id, allow_option_id);
6552                assert_eq!(outcome.option_kind, acp::PermissionOptionKind::AllowOnce);
6553            }
6554            RequestPermissionOutcome::Cancelled => {
6555                panic!("permission request should remain open after duplicate tool call update")
6556            }
6557        }
6558
6559        thread
6560            .update(cx, |thread, cx| {
6561                thread.handle_session_update(
6562                    acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
6563                        tool_call_id.clone(),
6564                        acp::ToolCallUpdateFields::new()
6565                            .status(acp::ToolCallStatus::Completed)
6566                            .title("Completed")
6567                            .content(vec!["done".into()]),
6568                    )),
6569                    cx,
6570                )
6571            })
6572            .unwrap();
6573
6574        thread.read_with(cx, |thread, cx| {
6575            let (_, tool_call) = thread
6576                .tool_call(&tool_call_id)
6577                .expect("tool call should exist");
6578            assert_eq!(tool_call.label.read(cx).source(), "Completed");
6579            assert!(matches!(tool_call.status, ToolCallStatus::Completed));
6580            assert_eq!(tool_call.content.len(), 1);
6581            assert_eq!(tool_call.content[0].to_markdown(cx), "done");
6582        });
6583    }
6584
6585    #[gpui::test]
6586    async fn test_permission_request_tracks_agent_status_until_resolved(cx: &mut TestAppContext) {
6587        init_test(cx);
6588
6589        let fs = FakeFs::new(cx.executor());
6590        let project = Project::test(fs, [], cx).await;
6591        let connection = Rc::new(FakeAgentConnection::new());
6592        let thread = cx
6593            .update(|cx| {
6594                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
6595            })
6596            .await
6597            .unwrap();
6598
6599        let tool_call_id = acp::ToolCallId::new("toolu_01auto_resolve");
6600        let permission_task = thread
6601            .update(cx, |thread, cx| {
6602                thread.request_tool_call_authorization(
6603                    acp::ToolCall::new(tool_call_id.clone(), "Original title")
6604                        .kind(acp::ToolKind::Execute)
6605                        .status(acp::ToolCallStatus::Pending)
6606                        .into(),
6607                    PermissionOptions::Flat(vec![acp::PermissionOption::new(
6608                        acp::PermissionOptionId::new("allow"),
6609                        "Allow",
6610                        acp::PermissionOptionKind::AllowOnce,
6611                    )]),
6612                    AuthorizationKind::PermissionGrant,
6613                    cx,
6614                )
6615            })
6616            .unwrap();
6617
6618        thread
6619            .update(cx, |thread, cx| {
6620                thread.handle_session_update(
6621                    acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
6622                        tool_call_id.clone(),
6623                        acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress),
6624                    )),
6625                    cx,
6626                )
6627            })
6628            .unwrap();
6629
6630        thread.read_with(cx, |thread, _cx| {
6631            let (_, tool_call) = thread
6632                .tool_call(&tool_call_id)
6633                .expect("tool call should exist");
6634            assert!(matches!(
6635                tool_call.status,
6636                ToolCallStatus::WaitingForConfirmation {
6637                    current_status: acp::ToolCallStatus::InProgress,
6638                    ..
6639                }
6640            ));
6641        });
6642
6643        thread.update(cx, |thread, cx| {
6644            thread.authorize_tool_call(
6645                tool_call_id.clone(),
6646                SelectedPermissionOutcome::new(
6647                    acp::PermissionOptionId::new("allow"),
6648                    acp::PermissionOptionKind::AllowOnce,
6649                ),
6650                cx,
6651            );
6652        });
6653
6654        thread.read_with(cx, |thread, _cx| {
6655            let (_, tool_call) = thread
6656                .tool_call(&tool_call_id)
6657                .expect("tool call should exist");
6658            assert!(matches!(tool_call.status, ToolCallStatus::InProgress));
6659        });
6660
6661        match permission_task.await {
6662            RequestPermissionOutcome::Selected(outcome) => {
6663                assert_eq!(outcome.option_id, acp::PermissionOptionId::new("allow"));
6664                assert_eq!(outcome.option_kind, acp::PermissionOptionKind::AllowOnce);
6665            }
6666            RequestPermissionOutcome::Cancelled => {
6667                panic!("resolved permission request should select an outcome")
6668            }
6669        }
6670    }
6671
6672    #[gpui::test]
6673    async fn test_permission_request_sets_waiting_status_on_existing_tool_call(
6674        cx: &mut TestAppContext,
6675    ) {
6676        init_test(cx);
6677
6678        let fs = FakeFs::new(cx.executor());
6679        let project = Project::test(fs, [], cx).await;
6680        let connection = Rc::new(FakeAgentConnection::new());
6681        let thread = cx
6682            .update(|cx| {
6683                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
6684            })
6685            .await
6686            .unwrap();
6687
6688        let tool_call_id = acp::ToolCallId::new("toolu_01existing_permission");
6689        thread
6690            .update(cx, |thread, cx| {
6691                thread.handle_session_update(
6692                    acp::SessionUpdate::ToolCall(
6693                        acp::ToolCall::new(tool_call_id.clone(), "Running title")
6694                            .kind(acp::ToolKind::Execute)
6695                            .status(acp::ToolCallStatus::InProgress),
6696                    ),
6697                    cx,
6698                )
6699            })
6700            .unwrap();
6701
6702        let permission_task = thread
6703            .update(cx, |thread, cx| {
6704                thread.request_tool_call_authorization(
6705                    acp::ToolCall::new(tool_call_id.clone(), "Needs permission")
6706                        .kind(acp::ToolKind::Execute)
6707                        .status(acp::ToolCallStatus::Pending)
6708                        .into(),
6709                    PermissionOptions::Flat(vec![acp::PermissionOption::new(
6710                        acp::PermissionOptionId::new("allow"),
6711                        "Allow",
6712                        acp::PermissionOptionKind::AllowOnce,
6713                    )]),
6714                    AuthorizationKind::PermissionGrant,
6715                    cx,
6716                )
6717            })
6718            .unwrap();
6719
6720        thread.read_with(cx, |thread, cx| {
6721            let (_, tool_call) = thread
6722                .tool_call(&tool_call_id)
6723                .expect("tool call should exist");
6724            assert_eq!(tool_call.label.read(cx).source(), "Needs permission");
6725            assert!(matches!(
6726                tool_call.status,
6727                ToolCallStatus::WaitingForConfirmation {
6728                    current_status: acp::ToolCallStatus::InProgress,
6729                    ..
6730                }
6731            ));
6732        });
6733
6734        thread.update(cx, |thread, cx| {
6735            thread.authorize_tool_call(
6736                tool_call_id.clone(),
6737                SelectedPermissionOutcome::new(
6738                    acp::PermissionOptionId::new("allow"),
6739                    acp::PermissionOptionKind::AllowOnce,
6740                ),
6741                cx,
6742            );
6743        });
6744
6745        match permission_task.await {
6746            RequestPermissionOutcome::Selected(outcome) => {
6747                assert_eq!(outcome.option_id, acp::PermissionOptionId::new("allow"));
6748                assert_eq!(outcome.option_kind, acp::PermissionOptionKind::AllowOnce);
6749            }
6750            RequestPermissionOutcome::Cancelled => {
6751                panic!("permission request should resolve after authorization")
6752            }
6753        }
6754    }
6755
6756    #[gpui::test]
6757    async fn test_cancel_tool_call_authorization_resolves_permission_request(
6758        cx: &mut TestAppContext,
6759    ) {
6760        init_test(cx);
6761
6762        let fs = FakeFs::new(cx.executor());
6763        let project = Project::test(fs, [], cx).await;
6764        let connection = Rc::new(FakeAgentConnection::new());
6765        let thread = cx
6766            .update(|cx| {
6767                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
6768            })
6769            .await
6770            .unwrap();
6771
6772        let tool_call_id = acp::ToolCallId::new("toolu_01cancelled_permission");
6773        let permission_task = thread
6774            .update(cx, |thread, cx| {
6775                thread.request_tool_call_authorization(
6776                    acp::ToolCall::new(tool_call_id.clone(), "Needs permission")
6777                        .kind(acp::ToolKind::Execute)
6778                        .status(acp::ToolCallStatus::Pending)
6779                        .into(),
6780                    PermissionOptions::Flat(vec![acp::PermissionOption::new(
6781                        acp::PermissionOptionId::new("allow"),
6782                        "Allow",
6783                        acp::PermissionOptionKind::AllowOnce,
6784                    )]),
6785                    AuthorizationKind::PermissionGrant,
6786                    cx,
6787                )
6788            })
6789            .unwrap();
6790
6791        thread.update(cx, |thread, cx| {
6792            thread.cancel_tool_call_authorization(&tool_call_id, cx);
6793        });
6794
6795        thread.read_with(cx, |thread, _cx| {
6796            let (_, tool_call) = thread
6797                .tool_call(&tool_call_id)
6798                .expect("tool call should exist");
6799            assert!(matches!(tool_call.status, ToolCallStatus::Canceled));
6800        });
6801
6802        match permission_task.await {
6803            RequestPermissionOutcome::Cancelled => {}
6804            RequestPermissionOutcome::Selected(_) => {
6805                panic!("cancelled permission request should not select an outcome")
6806            }
6807        }
6808    }
6809
6810    #[gpui::test]
6811    async fn test_terminal_tool_call_update_closes_open_permission_request(
6812        cx: &mut TestAppContext,
6813    ) {
6814        init_test(cx);
6815
6816        let fs = FakeFs::new(cx.executor());
6817        let project = Project::test(fs, [], cx).await;
6818        let connection = Rc::new(FakeAgentConnection::new());
6819        let thread = cx
6820            .update(|cx| {
6821                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
6822            })
6823            .await
6824            .unwrap();
6825
6826        let tool_call_id = acp::ToolCallId::new("toolu_01completed_while_waiting");
6827        let permission_task = thread
6828            .update(cx, |thread, cx| {
6829                thread.request_tool_call_authorization(
6830                    acp::ToolCall::new(tool_call_id.clone(), "Needs permission")
6831                        .kind(acp::ToolKind::Execute)
6832                        .status(acp::ToolCallStatus::Pending)
6833                        .into(),
6834                    PermissionOptions::Flat(vec![acp::PermissionOption::new(
6835                        acp::PermissionOptionId::new("allow"),
6836                        "Allow",
6837                        acp::PermissionOptionKind::AllowOnce,
6838                    )]),
6839                    AuthorizationKind::PermissionGrant,
6840                    cx,
6841                )
6842            })
6843            .unwrap();
6844
6845        thread
6846            .update(cx, |thread, cx| {
6847                thread.handle_session_update(
6848                    acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
6849                        tool_call_id.clone(),
6850                        acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
6851                    )),
6852                    cx,
6853                )
6854            })
6855            .unwrap();
6856
6857        thread.read_with(cx, |thread, _cx| {
6858            let (_, tool_call) = thread
6859                .tool_call(&tool_call_id)
6860                .expect("tool call should exist");
6861            assert!(matches!(tool_call.status, ToolCallStatus::Completed));
6862        });
6863
6864        match permission_task.await {
6865            RequestPermissionOutcome::Cancelled => {}
6866            RequestPermissionOutcome::Selected(_) => {
6867                panic!("terminal tool call update should close pending permission request")
6868            }
6869        }
6870    }
6871
6872    #[gpui::test]
6873    async fn test_no_pending_edits_if_tool_calls_are_completed(cx: &mut TestAppContext) {
6874        init_test(cx);
6875        let fs = FakeFs::new(cx.background_executor.clone());
6876        fs.insert_tree(path!("/test"), json!({})).await;
6877        let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
6878
6879        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
6880            move |_, thread, mut cx| {
6881                async move {
6882                    thread
6883                        .update(&mut cx, |thread, cx| {
6884                            thread.handle_session_update(
6885                                acp::SessionUpdate::ToolCall(
6886                                    acp::ToolCall::new("test", "Label")
6887                                        .kind(acp::ToolKind::Edit)
6888                                        .status(acp::ToolCallStatus::Completed)
6889                                        .content(vec![acp::ToolCallContent::Diff(acp::Diff::new(
6890                                            "/test/test.txt",
6891                                            "foo",
6892                                        ))]),
6893                                ),
6894                                cx,
6895                            )
6896                        })
6897                        .unwrap()
6898                        .unwrap();
6899                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
6900                }
6901                .boxed_local()
6902            }
6903        }));
6904
6905        let thread = cx
6906            .update(|cx| {
6907                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
6908            })
6909            .await
6910            .unwrap();
6911
6912        cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Hi".into()], cx)))
6913            .await
6914            .unwrap();
6915
6916        assert!(cx.read(|cx| !thread.read(cx).has_pending_edit_tool_calls()));
6917    }
6918
6919    #[gpui::test(iterations = 10)]
6920    async fn test_checkpoints(cx: &mut TestAppContext) {
6921        init_test(cx);
6922        let fs = FakeFs::new(cx.background_executor.clone());
6923        fs.insert_tree(
6924            path!("/test"),
6925            json!({
6926                ".git": {}
6927            }),
6928        )
6929        .await;
6930        let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
6931
6932        let simulate_changes = Arc::new(AtomicBool::new(true));
6933        let next_filename = Arc::new(AtomicUsize::new(0));
6934        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
6935            let simulate_changes = simulate_changes.clone();
6936            let next_filename = next_filename.clone();
6937            let fs = fs.clone();
6938            move |request, thread, mut cx| {
6939                let fs = fs.clone();
6940                let simulate_changes = simulate_changes.clone();
6941                let next_filename = next_filename.clone();
6942                async move {
6943                    if simulate_changes.load(SeqCst) {
6944                        let filename = format!("/test/file-{}", next_filename.fetch_add(1, SeqCst));
6945                        fs.write(Path::new(&filename), b"").await?;
6946                    }
6947
6948                    let acp::ContentBlock::Text(content) = &request.prompt[0] else {
6949                        panic!("expected text content block");
6950                    };
6951                    thread.update(&mut cx, |thread, cx| {
6952                        thread
6953                            .handle_session_update(
6954                                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
6955                                    content.text.to_uppercase().into(),
6956                                )),
6957                                cx,
6958                            )
6959                            .unwrap();
6960                    })?;
6961                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
6962                }
6963                .boxed_local()
6964            }
6965        }));
6966        let thread = cx
6967            .update(|cx| {
6968                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
6969            })
6970            .await
6971            .unwrap();
6972
6973        cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Lorem".into()], cx)))
6974            .await
6975            .unwrap();
6976        thread.read_with(cx, |thread, cx| {
6977            assert_eq!(
6978                thread.to_markdown(cx),
6979                indoc! {"
6980                    ## User (checkpoint)
6981
6982                    Lorem
6983
6984                    ## Assistant
6985
6986                    LOREM
6987
6988                "}
6989            );
6990        });
6991        assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]);
6992
6993        cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["ipsum".into()], cx)))
6994            .await
6995            .unwrap();
6996        thread.read_with(cx, |thread, cx| {
6997            assert_eq!(
6998                thread.to_markdown(cx),
6999                indoc! {"
7000                    ## User (checkpoint)
7001
7002                    Lorem
7003
7004                    ## Assistant
7005
7006                    LOREM
7007
7008                    ## User (checkpoint)
7009
7010                    ipsum
7011
7012                    ## Assistant
7013
7014                    IPSUM
7015
7016                "}
7017            );
7018        });
7019        assert_eq!(
7020            fs.files(),
7021            vec![
7022                Path::new(path!("/test/file-0")),
7023                Path::new(path!("/test/file-1"))
7024            ]
7025        );
7026
7027        // Checkpoint isn't stored when there are no changes.
7028        simulate_changes.store(false, SeqCst);
7029        cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["dolor".into()], cx)))
7030            .await
7031            .unwrap();
7032        thread.read_with(cx, |thread, cx| {
7033            assert_eq!(
7034                thread.to_markdown(cx),
7035                indoc! {"
7036                    ## User (checkpoint)
7037
7038                    Lorem
7039
7040                    ## Assistant
7041
7042                    LOREM
7043
7044                    ## User (checkpoint)
7045
7046                    ipsum
7047
7048                    ## Assistant
7049
7050                    IPSUM
7051
7052                    ## User
7053
7054                    dolor
7055
7056                    ## Assistant
7057
7058                    DOLOR
7059
7060                "}
7061            );
7062        });
7063        assert_eq!(
7064            fs.files(),
7065            vec![
7066                Path::new(path!("/test/file-0")),
7067                Path::new(path!("/test/file-1"))
7068            ]
7069        );
7070
7071        // Rewinding the conversation truncates the history and restores the checkpoint.
7072        thread
7073            .update(cx, |thread, cx| {
7074                let AgentThreadEntry::UserMessage(message) = &thread.entries[2] else {
7075                    panic!("unexpected entries {:?}", thread.entries)
7076                };
7077                thread.restore_checkpoint(message.client_id.clone().unwrap(), cx)
7078            })
7079            .await
7080            .unwrap();
7081        thread.read_with(cx, |thread, cx| {
7082            assert_eq!(
7083                thread.to_markdown(cx),
7084                indoc! {"
7085                    ## User (checkpoint)
7086
7087                    Lorem
7088
7089                    ## Assistant
7090
7091                    LOREM
7092
7093                "}
7094            );
7095        });
7096        assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]);
7097    }
7098
7099    #[gpui::test(iterations = 10)]
7100    async fn test_checkpoint_shows_when_file_changes_during_pending_message(
7101        cx: &mut TestAppContext,
7102    ) {
7103        init_test(cx);
7104        let fs = FakeFs::new(cx.background_executor.clone());
7105        fs.insert_tree(
7106            path!("/test"),
7107            json!({
7108                ".git": {}
7109            }),
7110        )
7111        .await;
7112        let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
7113
7114        let (request_started_tx, request_started_rx) = oneshot::channel::<()>();
7115        let request_started_tx = Rc::new(RefCell::new(Some(request_started_tx)));
7116        let (write_file_tx, write_file_rx) = oneshot::channel::<()>();
7117        let write_file_rx = Rc::new(RefCell::new(Some(write_file_rx)));
7118        let (file_written_tx, file_written_rx) = oneshot::channel::<()>();
7119        let file_written_tx = Rc::new(RefCell::new(Some(file_written_tx)));
7120        let (finish_response_tx, finish_response_rx) = oneshot::channel::<()>();
7121        let finish_response_tx = Rc::new(RefCell::new(Some(finish_response_tx)));
7122        let finish_response_rx = Rc::new(RefCell::new(Some(finish_response_rx)));
7123        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
7124            let request_started_tx = request_started_tx.clone();
7125            let write_file_rx = write_file_rx.clone();
7126            let file_written_tx = file_written_tx.clone();
7127            let finish_response_rx = finish_response_rx.clone();
7128            move |_request, thread, mut cx| {
7129                let write_file_rx = write_file_rx.borrow_mut().take();
7130                let finish_response_rx = finish_response_rx.borrow_mut().take();
7131                let request_started_tx = request_started_tx.borrow_mut().take();
7132                let file_written_tx = file_written_tx.borrow_mut().take();
7133                async move {
7134                    if let Some(request_started_tx) = request_started_tx {
7135                        request_started_tx.send(()).ok();
7136                    }
7137                    if let Some(write_file_rx) = write_file_rx {
7138                        write_file_rx.await.ok();
7139                    }
7140
7141                    thread
7142                        .update(&mut cx, |thread, cx| {
7143                            thread.write_text_file(
7144                                PathBuf::from(path!("/test/file")),
7145                                String::new(),
7146                                cx,
7147                            )
7148                        })?
7149                        .await?;
7150
7151                    if let Some(file_written_tx) = file_written_tx {
7152                        file_written_tx.send(()).ok();
7153                    }
7154                    if let Some(finish_response_rx) = finish_response_rx {
7155                        finish_response_rx.await.ok();
7156                    }
7157
7158                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
7159                }
7160                .boxed_local()
7161            }
7162        }));
7163        let thread = cx
7164            .update(|cx| {
7165                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
7166            })
7167            .await
7168            .unwrap();
7169
7170        let send = thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx));
7171        let send_task = cx.background_executor.spawn(send);
7172        request_started_rx.await.unwrap();
7173        cx.run_until_parked();
7174
7175        thread.read_with(cx, |thread, cx| {
7176            assert_eq!(
7177                thread.to_markdown(cx),
7178                indoc! {"
7179                    ## User
7180
7181                    hello
7182
7183                "}
7184            );
7185        });
7186
7187        write_file_tx.send(()).ok();
7188        file_written_rx.await.unwrap();
7189        cx.run_until_parked();
7190
7191        thread.read_with(cx, |thread, cx| {
7192            assert_eq!(
7193                thread.to_markdown(cx),
7194                indoc! {"
7195                    ## User (checkpoint)
7196
7197                    hello
7198
7199                "}
7200            );
7201        });
7202
7203        finish_response_tx
7204            .borrow_mut()
7205            .take()
7206            .unwrap()
7207            .send(())
7208            .ok();
7209        send_task.await.unwrap();
7210    }
7211
7212    #[gpui::test]
7213    async fn test_tool_result_refusal(cx: &mut TestAppContext) {
7214        use std::sync::atomic::AtomicUsize;
7215        init_test(cx);
7216
7217        let fs = FakeFs::new(cx.executor());
7218        let project = Project::test(fs, None, cx).await;
7219
7220        // Create a connection that simulates refusal after tool result
7221        let prompt_count = Arc::new(AtomicUsize::new(0));
7222        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
7223            let prompt_count = prompt_count.clone();
7224            move |_request, thread, mut cx| {
7225                let count = prompt_count.fetch_add(1, SeqCst);
7226                async move {
7227                    if count == 0 {
7228                        // First prompt: Generate a tool call with result
7229                        thread.update(&mut cx, |thread, cx| {
7230                            thread
7231                                .handle_session_update(
7232                                    acp::SessionUpdate::ToolCall(
7233                                        acp::ToolCall::new("tool1", "Test Tool")
7234                                            .kind(acp::ToolKind::Fetch)
7235                                            .status(acp::ToolCallStatus::Completed)
7236                                            .raw_input(serde_json::json!({"query": "test"}))
7237                                            .raw_output(serde_json::json!({"result": "inappropriate content"})),
7238                                    ),
7239                                    cx,
7240                                )
7241                                .unwrap();
7242                        })?;
7243
7244                        // Now return refusal because of the tool result
7245                        Ok(acp::PromptResponse::new(acp::StopReason::Refusal))
7246                    } else {
7247                        Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
7248                    }
7249                }
7250                .boxed_local()
7251            }
7252        }));
7253
7254        let thread = cx
7255            .update(|cx| {
7256                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
7257            })
7258            .await
7259            .unwrap();
7260
7261        // Track if we see a Refusal event
7262        let saw_refusal_event = Arc::new(std::sync::Mutex::new(false));
7263        let saw_refusal_event_captured = saw_refusal_event.clone();
7264        thread.update(cx, |_thread, cx| {
7265            cx.subscribe(
7266                &thread,
7267                move |_thread, _event_thread, event: &AcpThreadEvent, _cx| {
7268                    if matches!(event, AcpThreadEvent::Refusal) {
7269                        *saw_refusal_event_captured.lock().unwrap() = true;
7270                    }
7271                },
7272            )
7273            .detach();
7274        });
7275
7276        // Send a user message - this will trigger tool call and then refusal
7277        let send_task = thread.update(cx, |thread, cx| thread.send(vec!["Hello".into()], cx));
7278        cx.background_executor.spawn(send_task).detach();
7279        cx.run_until_parked();
7280
7281        // Verify that:
7282        // 1. A Refusal event WAS emitted (because it's a tool result refusal, not user prompt)
7283        // 2. The user message was NOT truncated
7284        assert!(
7285            *saw_refusal_event.lock().unwrap(),
7286            "Refusal event should be emitted for tool result refusals"
7287        );
7288
7289        thread.read_with(cx, |thread, _| {
7290            let entries = thread.entries();
7291            assert!(entries.len() >= 2, "Should have user message and tool call");
7292
7293            // Verify user message is still there
7294            assert!(
7295                matches!(entries[0], AgentThreadEntry::UserMessage(_)),
7296                "User message should not be truncated"
7297            );
7298
7299            // Verify tool call is there with result
7300            if let AgentThreadEntry::ToolCall(tool_call) = &entries[1] {
7301                assert!(
7302                    tool_call.raw_output.is_some(),
7303                    "Tool call should have output"
7304                );
7305            } else {
7306                panic!("Expected tool call at index 1");
7307            }
7308        });
7309    }
7310
7311    #[gpui::test]
7312    async fn test_user_prompt_refusal_emits_event(cx: &mut TestAppContext) {
7313        init_test(cx);
7314
7315        let fs = FakeFs::new(cx.executor());
7316        let project = Project::test(fs, None, cx).await;
7317
7318        let refuse_next = Arc::new(AtomicBool::new(false));
7319        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
7320            let refuse_next = refuse_next.clone();
7321            move |_request, _thread, _cx| {
7322                if refuse_next.load(SeqCst) {
7323                    async move { Ok(acp::PromptResponse::new(acp::StopReason::Refusal)) }
7324                        .boxed_local()
7325                } else {
7326                    async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) }
7327                        .boxed_local()
7328                }
7329            }
7330        }));
7331
7332        let thread = cx
7333            .update(|cx| {
7334                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
7335            })
7336            .await
7337            .unwrap();
7338
7339        // Track if we see a Refusal event
7340        let saw_refusal_event = Arc::new(std::sync::Mutex::new(false));
7341        let saw_refusal_event_captured = saw_refusal_event.clone();
7342        thread.update(cx, |_thread, cx| {
7343            cx.subscribe(
7344                &thread,
7345                move |_thread, _event_thread, event: &AcpThreadEvent, _cx| {
7346                    if matches!(event, AcpThreadEvent::Refusal) {
7347                        *saw_refusal_event_captured.lock().unwrap() = true;
7348                    }
7349                },
7350            )
7351            .detach();
7352        });
7353
7354        // Send a message that will be refused
7355        refuse_next.store(true, SeqCst);
7356        cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx)))
7357            .await
7358            .unwrap();
7359
7360        // Verify that a Refusal event WAS emitted for user prompt refusal
7361        assert!(
7362            *saw_refusal_event.lock().unwrap(),
7363            "Refusal event should be emitted for user prompt refusals"
7364        );
7365
7366        // Verify the message was truncated (user prompt refusal)
7367        thread.read_with(cx, |thread, cx| {
7368            assert_eq!(thread.to_markdown(cx), "");
7369        });
7370    }
7371
7372    #[gpui::test]
7373    async fn test_refusal(cx: &mut TestAppContext) {
7374        init_test(cx);
7375        let fs = FakeFs::new(cx.background_executor.clone());
7376        fs.insert_tree(path!("/"), json!({})).await;
7377        let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
7378
7379        let refuse_next = Arc::new(AtomicBool::new(false));
7380        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
7381            let refuse_next = refuse_next.clone();
7382            move |request, thread, mut cx| {
7383                let refuse_next = refuse_next.clone();
7384                async move {
7385                    if refuse_next.load(SeqCst) {
7386                        return Ok(acp::PromptResponse::new(acp::StopReason::Refusal));
7387                    }
7388
7389                    let acp::ContentBlock::Text(content) = &request.prompt[0] else {
7390                        panic!("expected text content block");
7391                    };
7392                    thread.update(&mut cx, |thread, cx| {
7393                        thread
7394                            .handle_session_update(
7395                                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
7396                                    content.text.to_uppercase().into(),
7397                                )),
7398                                cx,
7399                            )
7400                            .unwrap();
7401                    })?;
7402                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
7403                }
7404                .boxed_local()
7405            }
7406        }));
7407        let thread = cx
7408            .update(|cx| {
7409                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
7410            })
7411            .await
7412            .unwrap();
7413
7414        cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx)))
7415            .await
7416            .unwrap();
7417        thread.read_with(cx, |thread, cx| {
7418            assert_eq!(
7419                thread.to_markdown(cx),
7420                indoc! {"
7421                    ## User
7422
7423                    hello
7424
7425                    ## Assistant
7426
7427                    HELLO
7428
7429                "}
7430            );
7431        });
7432
7433        // Simulate refusing the second message. The message should be truncated
7434        // when a user prompt is refused.
7435        refuse_next.store(true, SeqCst);
7436        cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx)))
7437            .await
7438            .unwrap();
7439        thread.read_with(cx, |thread, cx| {
7440            assert_eq!(
7441                thread.to_markdown(cx),
7442                indoc! {"
7443                    ## User
7444
7445                    hello
7446
7447                    ## Assistant
7448
7449                    HELLO
7450
7451                "}
7452            );
7453        });
7454    }
7455
7456    async fn new_test_thread(cx: &mut TestAppContext) -> Entity<AcpThread> {
7457        let fs = FakeFs::new(cx.executor());
7458        let project = Project::test(fs, [], cx).await;
7459        let connection = Rc::new(FakeAgentConnection::new());
7460        cx.update(|cx| {
7461            connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
7462        })
7463        .await
7464        .unwrap()
7465    }
7466
7467    fn only_thread_elicitation(thread: &AcpThread) -> (ElicitationEntryId, &Elicitation) {
7468        let [entry] = thread.entries() else {
7469            panic!("expected one elicitation entry, got {:?}", thread.entries());
7470        };
7471        let AgentThreadEntry::Elicitation(id) = entry else {
7472            panic!("expected one elicitation entry, got {:?}", thread.entries());
7473        };
7474        let Some((_, elicitation)) = thread.elicitation(id) else {
7475            panic!("missing elicitation entry");
7476        };
7477        (id.clone(), elicitation)
7478    }
7479
7480    fn latest_thread_elicitation(thread: &AcpThread) -> (ElicitationEntryId, &Elicitation) {
7481        let Some(AgentThreadEntry::Elicitation(id)) = thread.entries().last() else {
7482            panic!("expected latest entry to be an elicitation");
7483        };
7484        let Some((_, elicitation)) = thread.elicitation(id) else {
7485            panic!("missing elicitation entry");
7486        };
7487        (id.clone(), elicitation)
7488    }
7489
7490    #[gpui::test]
7491    async fn test_elicitation_is_available_without_acp_beta_flag(cx: &mut TestAppContext) {
7492        init_test(cx);
7493        cx.update(|cx| {
7494            cx.update_flags(false, vec![]);
7495        });
7496        set_acp_beta_override("off", cx);
7497        let thread = new_test_thread(cx).await;
7498        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
7499
7500        let result = thread.update(cx, |thread, cx| {
7501            thread.request_elicitation(
7502                acp::CreateElicitationRequest::new(
7503                    acp::ElicitationFormMode::new(
7504                        acp::ElicitationSessionScope::new(session_id),
7505                        acp::ElicitationSchema::new().string("name", true),
7506                    ),
7507                    "Provide a name",
7508                ),
7509                cx,
7510            )
7511        });
7512
7513        assert!(result.is_ok());
7514        thread.read_with(cx, |thread, _| {
7515            assert!(matches!(
7516                thread.entries(),
7517                [AgentThreadEntry::Elicitation(_)]
7518            ));
7519        });
7520    }
7521
7522    #[gpui::test]
7523    async fn test_form_elicitation_accepts_response(cx: &mut TestAppContext) {
7524        init_test(cx);
7525        enable_acp_beta(cx);
7526        let thread = new_test_thread(cx).await;
7527        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
7528        let tool_call_id = acp::ToolCallId::new("tool-1");
7529
7530        let response_task = thread.update(cx, |thread, cx| {
7531            thread
7532                .request_elicitation(
7533                    acp::CreateElicitationRequest::new(
7534                        acp::ElicitationFormMode::new(
7535                            acp::ElicitationSessionScope::new(session_id.clone())
7536                                .tool_call_id(tool_call_id.clone()),
7537                            acp::ElicitationSchema::new().string("name", true),
7538                        ),
7539                        "Provide a name",
7540                    ),
7541                    cx,
7542                )
7543                .unwrap()
7544        });
7545
7546        let elicitation_id = thread.read_with(cx, |thread, _| {
7547            let (elicitation_id, elicitation) = only_thread_elicitation(thread);
7548            let acp::ElicitationScope::Session(scope) = elicitation.request.scope() else {
7549                panic!("expected session-scoped elicitation");
7550            };
7551            assert_eq!(scope.tool_call_id.as_ref(), Some(&tool_call_id));
7552            elicitation_id
7553        });
7554
7555        let expected_content = std::collections::BTreeMap::from([(
7556            "name".to_string(),
7557            acp::ElicitationContentValue::from("Ada"),
7558        )]);
7559        thread.update(cx, |thread, cx| {
7560            thread.respond_to_elicitation(
7561                &elicitation_id,
7562                acp::CreateElicitationResponse::new(acp::ElicitationAction::Accept(
7563                    acp::ElicitationAcceptAction::new().content(expected_content.clone()),
7564                )),
7565                cx,
7566            );
7567        });
7568
7569        let response = response_task.await;
7570        assert_eq!(
7571            response.action,
7572            acp::ElicitationAction::Accept(
7573                acp::ElicitationAcceptAction::new().content(expected_content)
7574            )
7575        );
7576        thread.read_with(cx, |thread, _| {
7577            let Some((_, elicitation)) = thread.elicitation(&elicitation_id) else {
7578                panic!("missing elicitation entry");
7579            };
7580            assert!(matches!(elicitation.status, ElicitationStatus::Accepted));
7581        });
7582    }
7583
7584    #[gpui::test]
7585    async fn test_url_elicitation_can_be_completed(cx: &mut TestAppContext) {
7586        init_test(cx);
7587        enable_acp_beta(cx);
7588        let thread = new_test_thread(cx).await;
7589        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
7590        let url_elicitation_id = acp::ElicitationId::new("url-1");
7591
7592        let response_task = thread.update(cx, |thread, cx| {
7593            thread
7594                .request_elicitation(
7595                    acp::CreateElicitationRequest::new(
7596                        acp::ElicitationUrlMode::new(
7597                            acp::ElicitationSessionScope::new(session_id),
7598                            url_elicitation_id.clone(),
7599                            "https://example.com/complete",
7600                        ),
7601                        "Complete this in the browser",
7602                    ),
7603                    cx,
7604                )
7605                .unwrap()
7606        });
7607
7608        let entry_id = thread.read_with(cx, |thread, _| {
7609            let (entry_id, _) = only_thread_elicitation(thread);
7610            entry_id
7611        });
7612
7613        thread.update(cx, |thread, cx| {
7614            thread.complete_url_elicitation(&url_elicitation_id, cx);
7615        });
7616        assert!(matches!(
7617            response_task.await.action,
7618            acp::ElicitationAction::Accept(_)
7619        ));
7620        thread.update(cx, |thread, cx| {
7621            thread.respond_to_elicitation(
7622                &entry_id,
7623                acp::CreateElicitationResponse::new(acp::ElicitationAction::Decline),
7624                cx,
7625            );
7626        });
7627        thread.read_with(cx, |thread, _| {
7628            let Some((_, elicitation)) = thread.elicitation(&entry_id) else {
7629                panic!("missing elicitation entry");
7630            };
7631            assert!(matches!(elicitation.status, ElicitationStatus::Completed));
7632        });
7633    }
7634
7635    #[gpui::test]
7636    async fn test_idle_cancel_cancels_accepted_url_elicitation(cx: &mut TestAppContext) {
7637        init_test(cx);
7638        enable_acp_beta(cx);
7639        let thread = new_test_thread(cx).await;
7640        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
7641        let url_elicitation_id = acp::ElicitationId::new("url-1");
7642
7643        let response_task = thread.update(cx, |thread, cx| {
7644            thread
7645                .request_elicitation(
7646                    acp::CreateElicitationRequest::new(
7647                        acp::ElicitationUrlMode::new(
7648                            acp::ElicitationSessionScope::new(session_id),
7649                            url_elicitation_id.clone(),
7650                            "https://example.com/complete",
7651                        ),
7652                        "Complete this in the browser",
7653                    ),
7654                    cx,
7655                )
7656                .unwrap()
7657        });
7658
7659        let entry_id = thread.read_with(cx, |thread, _| {
7660            let (entry_id, _) = only_thread_elicitation(thread);
7661            entry_id
7662        });
7663
7664        thread.update(cx, |thread, cx| {
7665            thread.respond_to_elicitation(
7666                &entry_id,
7667                acp::CreateElicitationResponse::new(acp::ElicitationAction::Accept(
7668                    acp::ElicitationAcceptAction::new(),
7669                )),
7670                cx,
7671            );
7672        });
7673        assert!(matches!(
7674            response_task.await.action,
7675            acp::ElicitationAction::Accept(_)
7676        ));
7677
7678        thread.update(cx, |thread, cx| {
7679            thread.cancel(cx).detach();
7680        });
7681        thread.read_with(cx, |thread, _| {
7682            let Some((_, elicitation)) = thread.elicitation(&entry_id) else {
7683                panic!("missing elicitation entry");
7684            };
7685            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
7686        });
7687
7688        thread.update(cx, |thread, cx| {
7689            thread.complete_url_elicitation(&url_elicitation_id, cx);
7690        });
7691        thread.read_with(cx, |thread, _| {
7692            let Some((_, elicitation)) = thread.elicitation(&entry_id) else {
7693                panic!("missing elicitation entry");
7694            };
7695            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
7696        });
7697    }
7698
7699    #[gpui::test]
7700    async fn test_cancel_accepted_url_elicitation_marks_canceled(cx: &mut TestAppContext) {
7701        init_test(cx);
7702        enable_acp_beta(cx);
7703        let thread = new_test_thread(cx).await;
7704        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
7705        let url_elicitation_id = acp::ElicitationId::new("url-1");
7706
7707        let response_task = thread.update(cx, |thread, cx| {
7708            thread
7709                .request_elicitation(
7710                    acp::CreateElicitationRequest::new(
7711                        acp::ElicitationUrlMode::new(
7712                            acp::ElicitationSessionScope::new(session_id),
7713                            url_elicitation_id.clone(),
7714                            "https://example.com/complete",
7715                        ),
7716                        "Complete this in the browser",
7717                    ),
7718                    cx,
7719                )
7720                .unwrap()
7721        });
7722
7723        let entry_id = thread.read_with(cx, |thread, _| {
7724            let (entry_id, _) = only_thread_elicitation(thread);
7725            entry_id
7726        });
7727
7728        thread.update(cx, |thread, cx| {
7729            thread.respond_to_elicitation(
7730                &entry_id,
7731                acp::CreateElicitationResponse::new(acp::ElicitationAction::Accept(
7732                    acp::ElicitationAcceptAction::new(),
7733                )),
7734                cx,
7735            );
7736        });
7737        assert!(matches!(
7738            response_task.await.action,
7739            acp::ElicitationAction::Accept(_)
7740        ));
7741        thread.read_with(cx, |thread, _| {
7742            let Some((_, elicitation)) = thread.elicitation(&entry_id) else {
7743                panic!("missing elicitation entry");
7744            };
7745            assert!(matches!(elicitation.status, ElicitationStatus::Accepted));
7746        });
7747
7748        thread.update(cx, |thread, cx| {
7749            thread.cancel(cx).detach();
7750        });
7751        thread.read_with(cx, |thread, _| {
7752            let Some((_, elicitation)) = thread.elicitation(&entry_id) else {
7753                panic!("missing elicitation entry");
7754            };
7755            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
7756        });
7757
7758        thread.update(cx, |thread, cx| {
7759            thread.complete_url_elicitation(&url_elicitation_id, cx);
7760        });
7761        thread.read_with(cx, |thread, _| {
7762            let Some((_, elicitation)) = thread.elicitation(&entry_id) else {
7763                panic!("missing elicitation entry");
7764            };
7765            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
7766        });
7767    }
7768
7769    #[gpui::test]
7770    async fn test_turn_cancel_cancels_accepted_url_elicitation_from_previous_turn(
7771        cx: &mut TestAppContext,
7772    ) {
7773        init_test(cx);
7774        enable_acp_beta(cx);
7775        let fs = FakeFs::new(cx.executor());
7776        let project = Project::test(fs, [], cx).await;
7777        let prompt_count = Rc::new(RefCell::new(0usize));
7778        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
7779            let prompt_count = prompt_count.clone();
7780            move |_request, _thread, _cx| {
7781                let stop_reason = {
7782                    let mut prompt_count = prompt_count.borrow_mut();
7783                    let stop_reason = if *prompt_count == 0 {
7784                        acp::StopReason::EndTurn
7785                    } else {
7786                        acp::StopReason::Cancelled
7787                    };
7788                    *prompt_count += 1;
7789                    stop_reason
7790                };
7791
7792                async move { Ok(acp::PromptResponse::new(stop_reason)) }.boxed_local()
7793            }
7794        }));
7795        let thread = cx
7796            .update(|cx| {
7797                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
7798            })
7799            .await
7800            .expect("new session should succeed");
7801
7802        let response = thread
7803            .update(cx, |thread, cx| thread.send(vec!["first turn".into()], cx))
7804            .await
7805            .expect("first turn should succeed")
7806            .expect("first turn should return a response");
7807        assert_eq!(response.stop_reason, acp::StopReason::EndTurn);
7808
7809        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
7810        let url_elicitation_id = acp::ElicitationId::new("url-1");
7811        let response_task = thread.update(cx, |thread, cx| {
7812            thread
7813                .request_elicitation(
7814                    acp::CreateElicitationRequest::new(
7815                        acp::ElicitationUrlMode::new(
7816                            acp::ElicitationSessionScope::new(session_id),
7817                            url_elicitation_id.clone(),
7818                            "https://example.com/complete",
7819                        ),
7820                        "Complete this in the browser",
7821                    ),
7822                    cx,
7823                )
7824                .expect("url elicitation should be accepted")
7825        });
7826
7827        let entry_id = thread.read_with(cx, |thread, _| {
7828            let (entry_id, _) = latest_thread_elicitation(thread);
7829            entry_id
7830        });
7831
7832        thread.update(cx, |thread, cx| {
7833            thread.respond_to_elicitation(
7834                &entry_id,
7835                acp::CreateElicitationResponse::new(acp::ElicitationAction::Accept(
7836                    acp::ElicitationAcceptAction::new(),
7837                )),
7838                cx,
7839            );
7840        });
7841        assert!(matches!(
7842            response_task.await.action,
7843            acp::ElicitationAction::Accept(_)
7844        ));
7845
7846        let response = thread
7847            .update(cx, |thread, cx| thread.send(vec!["second turn".into()], cx))
7848            .await
7849            .expect("second turn should succeed")
7850            .expect("second turn should return a response");
7851        assert_eq!(response.stop_reason, acp::StopReason::Cancelled);
7852        thread.read_with(cx, |thread, _| {
7853            let Some((_, elicitation)) = thread.elicitation(&entry_id) else {
7854                panic!("missing elicitation entry");
7855            };
7856            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
7857        });
7858
7859        thread.update(cx, |thread, cx| {
7860            thread.complete_url_elicitation(&url_elicitation_id, cx);
7861        });
7862        thread.read_with(cx, |thread, _| {
7863            let Some((_, elicitation)) = thread.elicitation(&entry_id) else {
7864                panic!("missing elicitation entry");
7865            };
7866            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
7867        });
7868    }
7869
7870    #[gpui::test]
7871    async fn test_request_scoped_elicitation_store_accepts_response(cx: &mut TestAppContext) {
7872        init_test(cx);
7873        enable_acp_beta(cx);
7874        let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
7875
7876        let response_task = store.update(cx, |store, cx| {
7877            store
7878                .request_elicitation(
7879                    acp::CreateElicitationRequest::new(
7880                        acp::ElicitationFormMode::new(
7881                            acp::ElicitationRequestScope::new(acp::RequestId::Number(1)),
7882                            acp::ElicitationSchema::new().string("name", true),
7883                        ),
7884                        "Provide a name",
7885                    ),
7886                    cx,
7887                )
7888                .unwrap()
7889        });
7890
7891        let elicitation_id = store.read_with(cx, |store, _| {
7892            let [elicitation] = store.elicitations() else {
7893                panic!(
7894                    "expected one elicitation entry, got {:?}",
7895                    store.elicitations()
7896                );
7897            };
7898            let acp::ElicitationScope::Request(scope) = elicitation.request.scope() else {
7899                panic!("expected request-scoped elicitation");
7900            };
7901            assert_eq!(scope.request_id, acp::RequestId::Number(1));
7902            elicitation.id.clone()
7903        });
7904
7905        store.update(cx, |store, cx| {
7906            store.respond_to_elicitation(
7907                &elicitation_id,
7908                acp::CreateElicitationResponse::new(acp::ElicitationAction::Decline),
7909                cx,
7910            );
7911        });
7912
7913        assert_eq!(response_task.await.action, acp::ElicitationAction::Decline);
7914        store.read_with(cx, |store, _| {
7915            let Some((_, elicitation)) = store.elicitation(&elicitation_id) else {
7916                panic!("missing elicitation entry");
7917            };
7918            assert!(matches!(elicitation.status, ElicitationStatus::Declined));
7919        });
7920    }
7921
7922    #[gpui::test]
7923    async fn test_request_elicitation_store_ignores_duplicate_response(cx: &mut TestAppContext) {
7924        init_test(cx);
7925        enable_acp_beta(cx);
7926        let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
7927
7928        let response_task = store.update(cx, |store, cx| {
7929            store
7930                .request_elicitation(
7931                    acp::CreateElicitationRequest::new(
7932                        acp::ElicitationFormMode::new(
7933                            acp::ElicitationRequestScope::new(acp::RequestId::Number(1)),
7934                            acp::ElicitationSchema::new().string("name", true),
7935                        ),
7936                        "Provide a name",
7937                    ),
7938                    cx,
7939                )
7940                .unwrap()
7941        });
7942
7943        let elicitation_id = store.read_with(cx, |store, _| {
7944            let [elicitation] = store.elicitations() else {
7945                panic!(
7946                    "expected one elicitation entry, got {:?}",
7947                    store.elicitations()
7948                );
7949            };
7950            elicitation.id.clone()
7951        });
7952
7953        store.update(cx, |store, cx| {
7954            store.respond_to_elicitation(
7955                &elicitation_id,
7956                acp::CreateElicitationResponse::new(acp::ElicitationAction::Decline),
7957                cx,
7958            );
7959            store.respond_to_elicitation(
7960                &elicitation_id,
7961                acp::CreateElicitationResponse::new(acp::ElicitationAction::Accept(
7962                    acp::ElicitationAcceptAction::new(),
7963                )),
7964                cx,
7965            );
7966        });
7967
7968        assert_eq!(response_task.await.action, acp::ElicitationAction::Decline);
7969        store.read_with(cx, |store, _| {
7970            let Some((_, elicitation)) = store.elicitation(&elicitation_id) else {
7971                panic!("missing elicitation entry");
7972            };
7973            assert!(matches!(elicitation.status, ElicitationStatus::Declined));
7974        });
7975    }
7976
7977    #[gpui::test]
7978    async fn test_cancel_session_elicitation_by_id_resolves_cancel(cx: &mut TestAppContext) {
7979        init_test(cx);
7980        enable_acp_beta(cx);
7981        let thread = new_test_thread(cx).await;
7982        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
7983
7984        let (elicitation_id, response_task) = thread.update(cx, |thread, cx| {
7985            thread
7986                .request_elicitation_with_id(
7987                    acp::CreateElicitationRequest::new(
7988                        acp::ElicitationFormMode::new(
7989                            acp::ElicitationSessionScope::new(session_id),
7990                            acp::ElicitationSchema::new().string("name", true),
7991                        ),
7992                        "Provide a name",
7993                    ),
7994                    cx,
7995                )
7996                .unwrap()
7997        });
7998
7999        thread.update(cx, |thread, cx| {
8000            thread.cancel_elicitation(&elicitation_id, cx);
8001        });
8002
8003        assert_eq!(response_task.await.action, acp::ElicitationAction::Cancel);
8004        thread.read_with(cx, |thread, _| {
8005            let Some((_, elicitation)) = thread.elicitation(&elicitation_id) else {
8006                panic!("missing elicitation entry");
8007            };
8008            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
8009        });
8010    }
8011
8012    #[gpui::test]
8013    async fn test_cancel_pending_session_elicitation_resolves_cancel(cx: &mut TestAppContext) {
8014        init_test(cx);
8015        enable_acp_beta(cx);
8016        let thread = new_test_thread(cx).await;
8017        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
8018
8019        let response_task = thread.update(cx, |thread, cx| {
8020            thread
8021                .request_elicitation(
8022                    acp::CreateElicitationRequest::new(
8023                        acp::ElicitationFormMode::new(
8024                            acp::ElicitationSessionScope::new(session_id),
8025                            acp::ElicitationSchema::new().string("name", true),
8026                        ),
8027                        "Provide a name",
8028                    ),
8029                    cx,
8030                )
8031                .unwrap()
8032        });
8033
8034        let elicitation_id = thread.read_with(cx, |thread, _| {
8035            let (elicitation_id, _) = only_thread_elicitation(thread);
8036            elicitation_id
8037        });
8038
8039        thread.update(cx, |thread, cx| {
8040            thread.cancel(cx).detach();
8041        });
8042
8043        assert_eq!(response_task.await.action, acp::ElicitationAction::Cancel);
8044        thread.read_with(cx, |thread, _| {
8045            let Some((_, elicitation)) = thread.elicitation(&elicitation_id) else {
8046                panic!("missing elicitation entry");
8047            };
8048            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
8049        });
8050    }
8051
8052    fn request_test_session_elicitation(
8053        thread: WeakEntity<AcpThread>,
8054        session_id: acp::SessionId,
8055        cx: &mut AsyncApp,
8056    ) -> Result<Task<acp::CreateElicitationResponse>> {
8057        thread.update(cx, |thread, cx| {
8058            thread
8059                .request_elicitation(
8060                    acp::CreateElicitationRequest::new(
8061                        acp::ElicitationFormMode::new(
8062                            acp::ElicitationSessionScope::new(session_id),
8063                            acp::ElicitationSchema::new().string("name", true),
8064                        ),
8065                        "Provide a name",
8066                    ),
8067                    cx,
8068                )
8069                .map_err(|error| anyhow!(error))
8070        })?
8071    }
8072
8073    #[gpui::test]
8074    async fn test_prompt_error_cancels_pending_session_elicitation(cx: &mut TestAppContext) {
8075        init_test(cx);
8076        enable_acp_beta(cx);
8077        let fs = FakeFs::new(cx.executor());
8078        let project = Project::test(fs, [], cx).await;
8079        let elicitation_action = Rc::new(RefCell::new(None));
8080        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
8081            let elicitation_action = elicitation_action.clone();
8082            move |request, thread, mut cx| {
8083                let elicitation_action = elicitation_action.clone();
8084                async move {
8085                    let response_task =
8086                        request_test_session_elicitation(thread, request.session_id, &mut cx)?;
8087                    cx.spawn(async move |_cx| {
8088                        let response = response_task.await;
8089                        *elicitation_action.borrow_mut() = Some(response.action);
8090                    })
8091                    .detach();
8092
8093                    Err(anyhow!("prompt failed"))
8094                }
8095                .boxed_local()
8096            }
8097        }));
8098        let thread = cx
8099            .update(|cx| {
8100                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
8101            })
8102            .await
8103            .expect("new session should succeed");
8104
8105        let result = thread
8106            .update(cx, |thread, cx| thread.send(vec!["hello".into()], cx))
8107            .await;
8108
8109        assert!(result.is_err());
8110        cx.run_until_parked();
8111        assert_eq!(
8112            *elicitation_action.borrow(),
8113            Some(acp::ElicitationAction::Cancel)
8114        );
8115        thread.read_with(cx, |thread, _| {
8116            let Some(elicitation) = thread.entries().iter().find_map(|entry| match entry {
8117                AgentThreadEntry::Elicitation(id) => {
8118                    thread.elicitation(id).map(|(_, elicitation)| elicitation)
8119                }
8120                _ => None,
8121            }) else {
8122                panic!("expected an elicitation entry");
8123            };
8124            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
8125        });
8126    }
8127
8128    #[gpui::test]
8129    async fn test_max_tokens_cancels_pending_session_elicitation(cx: &mut TestAppContext) {
8130        init_test(cx);
8131        enable_acp_beta(cx);
8132        let fs = FakeFs::new(cx.executor());
8133        let project = Project::test(fs, [], cx).await;
8134        let elicitation_action = Rc::new(RefCell::new(None));
8135        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
8136            let elicitation_action = elicitation_action.clone();
8137            move |request, thread, mut cx| {
8138                let elicitation_action = elicitation_action.clone();
8139                async move {
8140                    let response_task =
8141                        request_test_session_elicitation(thread, request.session_id, &mut cx)?;
8142                    cx.spawn(async move |_cx| {
8143                        let response = response_task.await;
8144                        *elicitation_action.borrow_mut() = Some(response.action);
8145                    })
8146                    .detach();
8147
8148                    Ok(acp::PromptResponse::new(acp::StopReason::MaxTokens))
8149                }
8150                .boxed_local()
8151            }
8152        }));
8153        let thread = cx
8154            .update(|cx| {
8155                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
8156            })
8157            .await
8158            .expect("new session should succeed");
8159
8160        let result = thread
8161            .update(cx, |thread, cx| thread.send(vec!["hello".into()], cx))
8162            .await;
8163
8164        assert!(result.is_err());
8165        cx.run_until_parked();
8166        assert_eq!(
8167            *elicitation_action.borrow(),
8168            Some(acp::ElicitationAction::Cancel)
8169        );
8170        thread.read_with(cx, |thread, _| {
8171            let Some(elicitation) = thread.entries().iter().find_map(|entry| match entry {
8172                AgentThreadEntry::Elicitation(id) => {
8173                    thread.elicitation(id).map(|(_, elicitation)| elicitation)
8174                }
8175                _ => None,
8176            }) else {
8177                panic!("expected an elicitation entry");
8178            };
8179            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
8180        });
8181    }
8182
8183    #[gpui::test]
8184    async fn test_cancel_request_scoped_elicitation_resolves_cancel(cx: &mut TestAppContext) {
8185        init_test(cx);
8186        enable_acp_beta(cx);
8187        let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
8188
8189        let (elicitation_id, response_task) = store.update(cx, |store, cx| {
8190            store
8191                .request_elicitation_with_id(
8192                    acp::CreateElicitationRequest::new(
8193                        acp::ElicitationFormMode::new(
8194                            acp::ElicitationRequestScope::new(acp::RequestId::Number(1)),
8195                            acp::ElicitationSchema::new().string("name", true),
8196                        ),
8197                        "Provide a name",
8198                    ),
8199                    cx,
8200                )
8201                .unwrap()
8202        });
8203
8204        store.update(cx, |store, cx| {
8205            store.cancel_elicitation(&elicitation_id, cx);
8206        });
8207
8208        assert_eq!(response_task.await.action, acp::ElicitationAction::Cancel);
8209        store.read_with(cx, |store, _| {
8210            let Some((_, elicitation)) = store.elicitation(&elicitation_id) else {
8211                panic!("missing elicitation entry");
8212            };
8213            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
8214        });
8215    }
8216
8217    #[gpui::test]
8218    async fn test_request_elicitation_store_cancel_all_resolves_cancel(cx: &mut TestAppContext) {
8219        init_test(cx);
8220        enable_acp_beta(cx);
8221        let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
8222
8223        let response_task = store.update(cx, |store, cx| {
8224            store
8225                .request_elicitation(
8226                    acp::CreateElicitationRequest::new(
8227                        acp::ElicitationFormMode::new(
8228                            acp::ElicitationRequestScope::new(acp::RequestId::Number(1)),
8229                            acp::ElicitationSchema::new().string("name", true),
8230                        ),
8231                        "Provide a name",
8232                    ),
8233                    cx,
8234                )
8235                .unwrap()
8236        });
8237
8238        store.update(cx, |store, cx| {
8239            store.cancel_all(cx);
8240        });
8241
8242        assert_eq!(response_task.await.action, acp::ElicitationAction::Cancel);
8243    }
8244
8245    #[gpui::test]
8246    async fn test_request_elicitation_store_clear_removes_answered_and_cancels_pending(
8247        cx: &mut TestAppContext,
8248    ) {
8249        init_test(cx);
8250        enable_acp_beta(cx);
8251        let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
8252
8253        let first_response_task = store.update(cx, |store, cx| {
8254            store
8255                .request_elicitation(
8256                    acp::CreateElicitationRequest::new(
8257                        acp::ElicitationFormMode::new(
8258                            acp::ElicitationRequestScope::new(acp::RequestId::Number(1)),
8259                            acp::ElicitationSchema::new().string("name", true),
8260                        ),
8261                        "Provide a name",
8262                    ),
8263                    cx,
8264                )
8265                .unwrap()
8266        });
8267        let second_response_task = store.update(cx, |store, cx| {
8268            store
8269                .request_elicitation(
8270                    acp::CreateElicitationRequest::new(
8271                        acp::ElicitationFormMode::new(
8272                            acp::ElicitationRequestScope::new(acp::RequestId::Number(2)),
8273                            acp::ElicitationSchema::new().string("account", true),
8274                        ),
8275                        "Provide an account",
8276                    ),
8277                    cx,
8278                )
8279                .unwrap()
8280        });
8281
8282        let first_elicitation_id = store.read_with(cx, |store, _| {
8283            let [first, _second] = store.elicitations() else {
8284                panic!("expected two elicitations, got {:?}", store.elicitations());
8285            };
8286            first.id.clone()
8287        });
8288
8289        store.update(cx, |store, cx| {
8290            store.respond_to_elicitation(
8291                &first_elicitation_id,
8292                acp::CreateElicitationResponse::new(acp::ElicitationAction::Decline),
8293                cx,
8294            );
8295            store.clear(cx);
8296        });
8297
8298        assert_eq!(
8299            first_response_task.await.action,
8300            acp::ElicitationAction::Decline
8301        );
8302        assert_eq!(
8303            second_response_task.await.action,
8304            acp::ElicitationAction::Cancel
8305        );
8306        store.read_with(cx, |store, _| assert!(store.elicitations().is_empty()));
8307    }
8308
8309    #[gpui::test]
8310    async fn test_request_elicitation_store_clear_resolved_preserves_outstanding(
8311        cx: &mut TestAppContext,
8312    ) {
8313        init_test(cx);
8314        enable_acp_beta(cx);
8315        let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
8316        let url_elicitation_id = acp::ElicitationId::new("url-1");
8317
8318        let accepted_response_task = store.update(cx, |store, cx| {
8319            store
8320                .request_elicitation(
8321                    acp::CreateElicitationRequest::new(
8322                        acp::ElicitationFormMode::new(
8323                            acp::ElicitationRequestScope::new(acp::RequestId::Number(1)),
8324                            acp::ElicitationSchema::new().string("name", true),
8325                        ),
8326                        "Provide a name",
8327                    ),
8328                    cx,
8329                )
8330                .unwrap()
8331        });
8332        let pending_response_task = store.update(cx, |store, cx| {
8333            store
8334                .request_elicitation(
8335                    acp::CreateElicitationRequest::new(
8336                        acp::ElicitationFormMode::new(
8337                            acp::ElicitationRequestScope::new(acp::RequestId::Number(2)),
8338                            acp::ElicitationSchema::new().string("account", true),
8339                        ),
8340                        "Provide an account",
8341                    ),
8342                    cx,
8343                )
8344                .unwrap()
8345        });
8346        let accepted_url_response_task = store.update(cx, |store, cx| {
8347            store
8348                .request_elicitation(
8349                    acp::CreateElicitationRequest::new(
8350                        acp::ElicitationUrlMode::new(
8351                            acp::ElicitationRequestScope::new(acp::RequestId::Number(3)),
8352                            url_elicitation_id,
8353                            "https://example.com/complete",
8354                        ),
8355                        "Complete this in the browser",
8356                    ),
8357                    cx,
8358                )
8359                .unwrap()
8360        });
8361
8362        let (accepted_id, pending_id, accepted_url_id) = store.read_with(cx, |store, _| {
8363            let [accepted, pending, accepted_url] = store.elicitations() else {
8364                panic!(
8365                    "expected three request-scoped elicitations, got {:?}",
8366                    store.elicitations()
8367                );
8368            };
8369            (
8370                accepted.id.clone(),
8371                pending.id.clone(),
8372                accepted_url.id.clone(),
8373            )
8374        });
8375
8376        store.update(cx, |store, cx| {
8377            store.respond_to_elicitation(
8378                &accepted_id,
8379                acp::CreateElicitationResponse::new(acp::ElicitationAction::Accept(
8380                    acp::ElicitationAcceptAction::new(),
8381                )),
8382                cx,
8383            );
8384            store.respond_to_elicitation(
8385                &accepted_url_id,
8386                acp::CreateElicitationResponse::new(acp::ElicitationAction::Accept(
8387                    acp::ElicitationAcceptAction::new(),
8388                )),
8389                cx,
8390            );
8391        });
8392        assert!(matches!(
8393            accepted_response_task.await.action,
8394            acp::ElicitationAction::Accept(_)
8395        ));
8396        assert!(matches!(
8397            accepted_url_response_task.await.action,
8398            acp::ElicitationAction::Accept(_)
8399        ));
8400
8401        let cleared_ids = store.update(cx, |store, cx| store.clear_resolved(cx));
8402        assert_eq!(cleared_ids, vec![accepted_id]);
8403        store.read_with(cx, |store, _| {
8404            let [pending, accepted_url] = store.elicitations() else {
8405                panic!(
8406                    "expected pending and accepted url elicitations, got {:?}",
8407                    store.elicitations()
8408                );
8409            };
8410            assert_eq!(pending.id, pending_id);
8411            assert!(matches!(pending.status, ElicitationStatus::Pending { .. }));
8412            assert_eq!(accepted_url.id, accepted_url_id);
8413            assert!(matches!(accepted_url.status, ElicitationStatus::Accepted));
8414        });
8415
8416        store.update(cx, |store, cx| store.clear(cx));
8417        assert_eq!(
8418            pending_response_task.await.action,
8419            acp::ElicitationAction::Cancel
8420        );
8421    }
8422
8423    #[gpui::test]
8424    async fn test_request_url_elicitation_store_can_be_completed(cx: &mut TestAppContext) {
8425        init_test(cx);
8426        enable_acp_beta(cx);
8427        let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
8428        let url_elicitation_id = acp::ElicitationId::new("url-1");
8429
8430        let response_task = store.update(cx, |store, cx| {
8431            store
8432                .request_elicitation(
8433                    acp::CreateElicitationRequest::new(
8434                        acp::ElicitationUrlMode::new(
8435                            acp::ElicitationRequestScope::new(acp::RequestId::Number(1)),
8436                            url_elicitation_id.clone(),
8437                            "https://example.com/complete",
8438                        ),
8439                        "Complete this in the browser",
8440                    ),
8441                    cx,
8442                )
8443                .unwrap()
8444        });
8445
8446        let entry_id = store.read_with(cx, |store, _| {
8447            let [elicitation] = store.elicitations() else {
8448                panic!(
8449                    "expected one request-scoped elicitation, got {:?}",
8450                    store.elicitations()
8451                );
8452            };
8453            elicitation.id.clone()
8454        });
8455
8456        store.update(cx, |store, cx| {
8457            store.complete_url_elicitation(&url_elicitation_id, cx);
8458        });
8459
8460        assert!(matches!(
8461            response_task.await.action,
8462            acp::ElicitationAction::Accept(_)
8463        ));
8464        store.update(cx, |store, cx| {
8465            store.respond_to_elicitation(
8466                &entry_id,
8467                acp::CreateElicitationResponse::new(acp::ElicitationAction::Decline),
8468                cx,
8469            );
8470        });
8471        store.read_with(cx, |store, _| {
8472            let Some((_, elicitation)) = store.elicitation(&entry_id) else {
8473                panic!("missing elicitation entry");
8474            };
8475            assert!(matches!(elicitation.status, ElicitationStatus::Completed));
8476        });
8477    }
8478
8479    #[gpui::test]
8480    async fn test_request_url_elicitation_store_cancel_all_cancels_accepted_url(
8481        cx: &mut TestAppContext,
8482    ) {
8483        init_test(cx);
8484        enable_acp_beta(cx);
8485        let store = cx.update(|cx| cx.new(|_| ElicitationStore::default()));
8486        let url_elicitation_id = acp::ElicitationId::new("url-1");
8487
8488        let response_task = store.update(cx, |store, cx| {
8489            store
8490                .request_elicitation(
8491                    acp::CreateElicitationRequest::new(
8492                        acp::ElicitationUrlMode::new(
8493                            acp::ElicitationRequestScope::new(acp::RequestId::Number(1)),
8494                            url_elicitation_id.clone(),
8495                            "https://example.com/complete",
8496                        ),
8497                        "Complete this in the browser",
8498                    ),
8499                    cx,
8500                )
8501                .unwrap()
8502        });
8503
8504        let entry_id = store.read_with(cx, |store, _| {
8505            let [elicitation] = store.elicitations() else {
8506                panic!(
8507                    "expected one elicitation entry, got {:?}",
8508                    store.elicitations()
8509                );
8510            };
8511            elicitation.id.clone()
8512        });
8513
8514        store.update(cx, |store, cx| {
8515            store.respond_to_elicitation(
8516                &entry_id,
8517                acp::CreateElicitationResponse::new(acp::ElicitationAction::Accept(
8518                    acp::ElicitationAcceptAction::new(),
8519                )),
8520                cx,
8521            );
8522        });
8523        assert!(matches!(
8524            response_task.await.action,
8525            acp::ElicitationAction::Accept(_)
8526        ));
8527        store.update(cx, |store, cx| {
8528            store.cancel_all(cx);
8529        });
8530        store.read_with(cx, |store, _| {
8531            let Some((_, elicitation)) = store.elicitation(&entry_id) else {
8532                panic!("missing elicitation entry");
8533            };
8534            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
8535        });
8536
8537        store.update(cx, |store, cx| {
8538            store.complete_url_elicitation(&url_elicitation_id, cx);
8539        });
8540        store.read_with(cx, |store, _| {
8541            let Some((_, elicitation)) = store.elicitation(&entry_id) else {
8542                panic!("missing elicitation entry");
8543            };
8544            assert!(matches!(elicitation.status, ElicitationStatus::Canceled));
8545        });
8546    }
8547
8548    #[gpui::test]
8549    async fn test_cancel_pending_elicitations_preserves_responded_statuses(
8550        cx: &mut TestAppContext,
8551    ) {
8552        init_test(cx);
8553        enable_acp_beta(cx);
8554        let thread = new_test_thread(cx).await;
8555        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
8556
8557        let response_task = thread.update(cx, |thread, cx| {
8558            thread
8559                .request_elicitation(
8560                    acp::CreateElicitationRequest::new(
8561                        acp::ElicitationFormMode::new(
8562                            acp::ElicitationSessionScope::new(session_id),
8563                            acp::ElicitationSchema::new().string("name", true),
8564                        ),
8565                        "Provide a name",
8566                    ),
8567                    cx,
8568                )
8569                .unwrap()
8570        });
8571
8572        let elicitation_id = thread.read_with(cx, |thread, _| {
8573            let (elicitation_id, _) = only_thread_elicitation(thread);
8574            elicitation_id
8575        });
8576
8577        thread.update(cx, |thread, cx| {
8578            thread.respond_to_elicitation(
8579                &elicitation_id,
8580                acp::CreateElicitationResponse::new(acp::ElicitationAction::Decline),
8581                cx,
8582            );
8583            thread.cancel(cx).detach();
8584        });
8585
8586        assert_eq!(response_task.await.action, acp::ElicitationAction::Decline);
8587        thread.read_with(cx, |thread, _| {
8588            let Some((_, elicitation)) = thread.elicitation(&elicitation_id) else {
8589                panic!("missing elicitation entry");
8590            };
8591            assert!(matches!(elicitation.status, ElicitationStatus::Declined));
8592        });
8593    }
8594
8595    #[gpui::test]
8596    async fn test_session_elicitation_ignores_duplicate_response(cx: &mut TestAppContext) {
8597        init_test(cx);
8598        enable_acp_beta(cx);
8599        let thread = new_test_thread(cx).await;
8600        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
8601
8602        let response_task = thread.update(cx, |thread, cx| {
8603            thread
8604                .request_elicitation(
8605                    acp::CreateElicitationRequest::new(
8606                        acp::ElicitationFormMode::new(
8607                            acp::ElicitationSessionScope::new(session_id),
8608                            acp::ElicitationSchema::new().string("name", true),
8609                        ),
8610                        "Provide a name",
8611                    ),
8612                    cx,
8613                )
8614                .unwrap()
8615        });
8616
8617        let elicitation_id = thread.read_with(cx, |thread, _| {
8618            let (elicitation_id, _) = only_thread_elicitation(thread);
8619            elicitation_id
8620        });
8621
8622        thread.update(cx, |thread, cx| {
8623            thread.respond_to_elicitation(
8624                &elicitation_id,
8625                acp::CreateElicitationResponse::new(acp::ElicitationAction::Decline),
8626                cx,
8627            );
8628            thread.respond_to_elicitation(
8629                &elicitation_id,
8630                acp::CreateElicitationResponse::new(acp::ElicitationAction::Accept(
8631                    acp::ElicitationAcceptAction::new(),
8632                )),
8633                cx,
8634            );
8635        });
8636
8637        assert_eq!(response_task.await.action, acp::ElicitationAction::Decline);
8638        thread.read_with(cx, |thread, _| {
8639            let Some((_, elicitation)) = thread.elicitation(&elicitation_id) else {
8640                panic!("missing elicitation entry");
8641            };
8642            assert!(matches!(elicitation.status, ElicitationStatus::Declined));
8643        });
8644    }
8645
8646    #[gpui::test]
8647    async fn test_url_elicitation_rejects_invalid_url(cx: &mut TestAppContext) {
8648        init_test(cx);
8649        enable_acp_beta(cx);
8650        let thread = new_test_thread(cx).await;
8651        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
8652
8653        let result = thread.update(cx, |thread, cx| {
8654            thread.request_elicitation(
8655                acp::CreateElicitationRequest::new(
8656                    acp::ElicitationUrlMode::new(
8657                        acp::ElicitationSessionScope::new(session_id),
8658                        "url-1",
8659                        "not a url",
8660                    ),
8661                    "Complete this in the browser",
8662                ),
8663                cx,
8664            )
8665        });
8666
8667        assert!(result.is_err());
8668        thread.read_with(cx, |thread, _| assert!(thread.entries().is_empty()));
8669    }
8670
8671    async fn run_until_first_tool_call(
8672        thread: &Entity<AcpThread>,
8673        cx: &mut TestAppContext,
8674    ) -> usize {
8675        let (mut tx, mut rx) = mpsc::channel::<usize>(1);
8676
8677        let subscription = cx.update(|cx| {
8678            cx.subscribe(thread, move |thread, _, cx| {
8679                for (ix, entry) in thread.read(cx).entries.iter().enumerate() {
8680                    if matches!(entry, AgentThreadEntry::ToolCall(_)) {
8681                        return tx.try_send(ix).unwrap();
8682                    }
8683                }
8684            })
8685        });
8686
8687        select! {
8688            _ = futures::FutureExt::fuse(cx.background_executor.timer(Duration::from_secs(10))) => {
8689                panic!("Timeout waiting for tool call")
8690            }
8691            ix = rx.next().fuse() => {
8692                drop(subscription);
8693                ix.unwrap()
8694            }
8695        }
8696    }
8697
8698    #[derive(Clone, Default)]
8699    struct FakeAgentConnection {
8700        auth_methods: Vec<acp::AuthMethod>,
8701        supports_truncate: bool,
8702        sessions: Arc<parking_lot::Mutex<HashMap<acp::SessionId, WeakEntity<AcpThread>>>>,
8703        set_title_calls: Rc<RefCell<Vec<SharedString>>>,
8704        on_user_message: Option<
8705            Rc<
8706                dyn Fn(
8707                        acp::PromptRequest,
8708                        WeakEntity<AcpThread>,
8709                        AsyncApp,
8710                    ) -> LocalBoxFuture<'static, Result<acp::PromptResponse>>
8711                    + 'static,
8712            >,
8713        >,
8714    }
8715
8716    impl FakeAgentConnection {
8717        fn new() -> Self {
8718            Self {
8719                auth_methods: Vec::new(),
8720                supports_truncate: true,
8721                on_user_message: None,
8722                sessions: Arc::default(),
8723                set_title_calls: Default::default(),
8724            }
8725        }
8726
8727        fn without_truncate_support(mut self) -> Self {
8728            self.supports_truncate = false;
8729            self
8730        }
8731
8732        #[expect(unused)]
8733        fn with_auth_methods(mut self, auth_methods: Vec<acp::AuthMethod>) -> Self {
8734            self.auth_methods = auth_methods;
8735            self
8736        }
8737
8738        fn on_user_message(
8739            mut self,
8740            handler: impl Fn(
8741                acp::PromptRequest,
8742                WeakEntity<AcpThread>,
8743                AsyncApp,
8744            ) -> LocalBoxFuture<'static, Result<acp::PromptResponse>>
8745            + 'static,
8746        ) -> Self {
8747            self.on_user_message.replace(Rc::new(handler));
8748            self
8749        }
8750    }
8751
8752    impl AgentConnection for FakeAgentConnection {
8753        fn agent_id(&self) -> AgentId {
8754            AgentId::new("fake")
8755        }
8756
8757        fn telemetry_id(&self) -> SharedString {
8758            "fake".into()
8759        }
8760
8761        fn auth_methods(&self) -> &[acp::AuthMethod] {
8762            &self.auth_methods
8763        }
8764
8765        fn new_session(
8766            self: Rc<Self>,
8767            project: Entity<Project>,
8768            work_dirs: PathList,
8769            cx: &mut App,
8770        ) -> Task<gpui::Result<Entity<AcpThread>>> {
8771            let session_id = acp::SessionId::new(
8772                rand::rng()
8773                    .sample_iter(&distr::Alphanumeric)
8774                    .take(7)
8775                    .map(char::from)
8776                    .collect::<String>(),
8777            );
8778            let action_log = cx.new(|_| ActionLog::new(project.clone()));
8779            let thread = cx.new(|cx| {
8780                AcpThread::new(
8781                    None,
8782                    None,
8783                    Some(work_dirs),
8784                    self.clone(),
8785                    project,
8786                    action_log,
8787                    session_id.clone(),
8788                    watch::Receiver::constant(
8789                        acp::PromptCapabilities::new()
8790                            .image(true)
8791                            .audio(true)
8792                            .embedded_context(true),
8793                    ),
8794                    cx,
8795                )
8796            });
8797            self.sessions.lock().insert(session_id, thread.downgrade());
8798            Task::ready(Ok(thread))
8799        }
8800
8801        fn authenticate(&self, method: acp::AuthMethodId, _cx: &mut App) -> Task<gpui::Result<()>> {
8802            if self.auth_methods().iter().any(|m| m.id() == &method) {
8803                Task::ready(Ok(()))
8804            } else {
8805                Task::ready(Err(anyhow!("Invalid Auth Method")))
8806            }
8807        }
8808
8809        fn prompt(
8810            &self,
8811            params: acp::PromptRequest,
8812            cx: &mut App,
8813        ) -> Task<gpui::Result<acp::PromptResponse>> {
8814            let sessions = self.sessions.lock();
8815            let thread = sessions.get(&params.session_id).unwrap();
8816            if let Some(handler) = &self.on_user_message {
8817                let handler = handler.clone();
8818                let thread = thread.clone();
8819                cx.spawn(async move |cx| handler(params, thread, cx.clone()).await)
8820            } else {
8821                Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)))
8822            }
8823        }
8824
8825        fn client_user_message_ids(
8826            &self,
8827            _cx: &App,
8828        ) -> Option<Rc<dyn AgentSessionClientUserMessageIds>> {
8829            self.supports_truncate.then(|| {
8830                Rc::new(FakeAgentSessionClientUserMessageIds {
8831                    connection: self.clone(),
8832                }) as Rc<dyn AgentSessionClientUserMessageIds>
8833            })
8834        }
8835
8836        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {}
8837
8838        fn truncate(
8839            &self,
8840            session_id: &acp::SessionId,
8841            _cx: &App,
8842        ) -> Option<Rc<dyn AgentSessionTruncate>> {
8843            self.supports_truncate.then(|| {
8844                Rc::new(FakeAgentSessionEditor {
8845                    _session_id: session_id.clone(),
8846                }) as Rc<dyn AgentSessionTruncate>
8847            })
8848        }
8849
8850        fn set_title(
8851            &self,
8852            _session_id: &acp::SessionId,
8853            _cx: &App,
8854        ) -> Option<Rc<dyn AgentSessionSetTitle>> {
8855            Some(Rc::new(FakeAgentSessionSetTitle {
8856                calls: self.set_title_calls.clone(),
8857            }))
8858        }
8859
8860        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
8861            self
8862        }
8863    }
8864
8865    struct FakeAgentSessionSetTitle {
8866        calls: Rc<RefCell<Vec<SharedString>>>,
8867    }
8868
8869    impl AgentSessionSetTitle for FakeAgentSessionSetTitle {
8870        fn run(&self, title: SharedString, _cx: &mut App) -> Task<Result<()>> {
8871            self.calls.borrow_mut().push(title);
8872            Task::ready(Ok(()))
8873        }
8874    }
8875
8876    struct FakeAgentSessionEditor {
8877        _session_id: acp::SessionId,
8878    }
8879
8880    impl AgentSessionTruncate for FakeAgentSessionEditor {
8881        fn run(
8882            &self,
8883            _client_user_message_id: ClientUserMessageId,
8884            _cx: &mut App,
8885        ) -> Task<Result<()>> {
8886            Task::ready(Ok(()))
8887        }
8888    }
8889
8890    struct FakeAgentSessionClientUserMessageIds {
8891        connection: FakeAgentConnection,
8892    }
8893
8894    impl AgentSessionClientUserMessageIds for FakeAgentSessionClientUserMessageIds {
8895        fn prompt(
8896            &self,
8897            _client_user_message_id: ClientUserMessageId,
8898            params: acp::PromptRequest,
8899            cx: &mut App,
8900        ) -> Task<Result<acp::PromptResponse>> {
8901            self.connection.prompt(params, cx)
8902        }
8903    }
8904
8905    #[gpui::test]
8906    async fn test_tool_call_not_found_creates_failed_entry(cx: &mut TestAppContext) {
8907        init_test(cx);
8908
8909        let fs = FakeFs::new(cx.executor());
8910        let project = Project::test(fs, [], cx).await;
8911        let connection = Rc::new(FakeAgentConnection::new());
8912        let thread = cx
8913            .update(|cx| {
8914                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
8915            })
8916            .await
8917            .unwrap();
8918
8919        // Try to update a tool call that doesn't exist
8920        let nonexistent_id = acp::ToolCallId::new("nonexistent-tool-call");
8921        thread.update(cx, |thread, cx| {
8922            let result = thread.handle_session_update(
8923                acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
8924                    nonexistent_id.clone(),
8925                    acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
8926                )),
8927                cx,
8928            );
8929
8930            // The update should succeed (not return an error)
8931            assert!(result.is_ok());
8932
8933            // There should now be exactly one entry in the thread
8934            assert_eq!(thread.entries.len(), 1);
8935
8936            // The entry should be a failed tool call
8937            if let AgentThreadEntry::ToolCall(tool_call) = &thread.entries[0] {
8938                assert_eq!(tool_call.id, nonexistent_id);
8939                assert!(matches!(tool_call.status, ToolCallStatus::Failed));
8940                assert_eq!(tool_call.kind, acp::ToolKind::Fetch);
8941
8942                // Check that the content contains the error message
8943                assert_eq!(tool_call.content.len(), 1);
8944                if let ToolCallContent::ContentBlock(content_block) = &tool_call.content[0] {
8945                    match content_block {
8946                        ContentBlock::Markdown { markdown } => {
8947                            let markdown_text = markdown.read(cx).source();
8948                            assert!(markdown_text.contains("Tool call not found"));
8949                        }
8950                        ContentBlock::Empty => panic!("Expected markdown content, got empty"),
8951                        ContentBlock::ResourceLink { .. } => {
8952                            panic!("Expected markdown content, got resource link")
8953                        }
8954                        ContentBlock::EmbeddedResource { .. } => {
8955                            panic!("Expected markdown content, got embedded resource")
8956                        }
8957                        ContentBlock::Image { .. } => {
8958                            panic!("Expected markdown content, got image")
8959                        }
8960                    }
8961                } else {
8962                    panic!("Expected ContentBlock, got: {:?}", tool_call.content[0]);
8963                }
8964            } else {
8965                panic!("Expected ToolCall entry, got: {:?}", thread.entries[0]);
8966            }
8967        });
8968    }
8969
8970    /// Tests that restoring a checkpoint properly cleans up terminals that were
8971    /// created after that checkpoint, and cancels any in-progress generation.
8972    ///
8973    /// Reproduces issue #35142: When a checkpoint is restored, any terminal processes
8974    /// that were started after that checkpoint should be terminated, and any in-progress
8975    /// AI generation should be canceled.
8976    #[gpui::test]
8977    async fn test_restore_checkpoint_kills_terminal(cx: &mut TestAppContext) {
8978        init_test(cx);
8979
8980        let fs = FakeFs::new(cx.executor());
8981        let project = Project::test(fs, [], cx).await;
8982        let connection = Rc::new(FakeAgentConnection::new());
8983        let thread = cx
8984            .update(|cx| {
8985                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
8986            })
8987            .await
8988            .unwrap();
8989
8990        // Send first user message to create a checkpoint
8991        cx.update(|cx| {
8992            thread.update(cx, |thread, cx| {
8993                thread.send(vec!["first message".into()], cx)
8994            })
8995        })
8996        .await
8997        .unwrap();
8998
8999        // Send second message (creates another checkpoint) - we'll restore to this one
9000        cx.update(|cx| {
9001            thread.update(cx, |thread, cx| {
9002                thread.send(vec!["second message".into()], cx)
9003            })
9004        })
9005        .await
9006        .unwrap();
9007
9008        // Create 2 terminals BEFORE the checkpoint that have completed running
9009        let terminal_id_1 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
9010        let mock_terminal_1 = cx.new(|cx| {
9011            let builder = ::terminal::TerminalBuilder::new_display_only(
9012                ::terminal::terminal_settings::CursorShape::default(),
9013                ::terminal::terminal_settings::AlternateScroll::On,
9014                None,
9015                0,
9016                cx.background_executor(),
9017                PathStyle::local(),
9018            );
9019            builder.subscribe(cx)
9020        });
9021
9022        thread.update(cx, |thread, cx| {
9023            thread.on_terminal_provider_event(
9024                TerminalProviderEvent::Created {
9025                    terminal_id: terminal_id_1.clone(),
9026                    label: "echo 'first'".to_string(),
9027                    cwd: Some(PathBuf::from("/test")),
9028                    output_byte_limit: None,
9029                    terminal: mock_terminal_1.clone(),
9030                },
9031                cx,
9032            );
9033        });
9034
9035        thread.update(cx, |thread, cx| {
9036            thread.on_terminal_provider_event(
9037                TerminalProviderEvent::Output {
9038                    terminal_id: terminal_id_1.clone(),
9039                    data: b"first\n".to_vec(),
9040                },
9041                cx,
9042            );
9043        });
9044
9045        thread.update(cx, |thread, cx| {
9046            thread.on_terminal_provider_event(
9047                TerminalProviderEvent::Exit {
9048                    terminal_id: terminal_id_1.clone(),
9049                    status: acp::TerminalExitStatus::new().exit_code(0),
9050                },
9051                cx,
9052            );
9053        });
9054
9055        let terminal_id_2 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
9056        let mock_terminal_2 = cx.new(|cx| {
9057            let builder = ::terminal::TerminalBuilder::new_display_only(
9058                ::terminal::terminal_settings::CursorShape::default(),
9059                ::terminal::terminal_settings::AlternateScroll::On,
9060                None,
9061                0,
9062                cx.background_executor(),
9063                PathStyle::local(),
9064            );
9065            builder.subscribe(cx)
9066        });
9067
9068        thread.update(cx, |thread, cx| {
9069            thread.on_terminal_provider_event(
9070                TerminalProviderEvent::Created {
9071                    terminal_id: terminal_id_2.clone(),
9072                    label: "echo 'second'".to_string(),
9073                    cwd: Some(PathBuf::from("/test")),
9074                    output_byte_limit: None,
9075                    terminal: mock_terminal_2.clone(),
9076                },
9077                cx,
9078            );
9079        });
9080
9081        thread.update(cx, |thread, cx| {
9082            thread.on_terminal_provider_event(
9083                TerminalProviderEvent::Output {
9084                    terminal_id: terminal_id_2.clone(),
9085                    data: b"second\n".to_vec(),
9086                },
9087                cx,
9088            );
9089        });
9090
9091        thread.update(cx, |thread, cx| {
9092            thread.on_terminal_provider_event(
9093                TerminalProviderEvent::Exit {
9094                    terminal_id: terminal_id_2.clone(),
9095                    status: acp::TerminalExitStatus::new().exit_code(0),
9096                },
9097                cx,
9098            );
9099        });
9100
9101        // Get the second message ID to restore to
9102        let second_message_id = thread.read_with(cx, |thread, _| {
9103            // At this point we have:
9104            // - Index 0: First user message (with checkpoint)
9105            // - Index 1: Second user message (with checkpoint)
9106            // No assistant responses because FakeAgentConnection just returns EndTurn
9107            let AgentThreadEntry::UserMessage(message) = &thread.entries[1] else {
9108                panic!("expected user message at index 1");
9109            };
9110            message.client_id.clone().unwrap()
9111        });
9112
9113        // Create a terminal AFTER the checkpoint we'll restore to.
9114        // This simulates the AI agent starting a long-running terminal command.
9115        let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
9116        let mock_terminal = cx.new(|cx| {
9117            let builder = ::terminal::TerminalBuilder::new_display_only(
9118                ::terminal::terminal_settings::CursorShape::default(),
9119                ::terminal::terminal_settings::AlternateScroll::On,
9120                None,
9121                0,
9122                cx.background_executor(),
9123                PathStyle::local(),
9124            );
9125            builder.subscribe(cx)
9126        });
9127
9128        // Register the terminal as created
9129        thread.update(cx, |thread, cx| {
9130            thread.on_terminal_provider_event(
9131                TerminalProviderEvent::Created {
9132                    terminal_id: terminal_id.clone(),
9133                    label: "sleep 1000".to_string(),
9134                    cwd: Some(PathBuf::from("/test")),
9135                    output_byte_limit: None,
9136                    terminal: mock_terminal.clone(),
9137                },
9138                cx,
9139            );
9140        });
9141
9142        // Simulate the terminal producing output (still running)
9143        thread.update(cx, |thread, cx| {
9144            thread.on_terminal_provider_event(
9145                TerminalProviderEvent::Output {
9146                    terminal_id: terminal_id.clone(),
9147                    data: b"terminal is running...\n".to_vec(),
9148                },
9149                cx,
9150            );
9151        });
9152
9153        // Create a tool call entry that references this terminal
9154        // This represents the agent requesting a terminal command
9155        thread.update(cx, |thread, cx| {
9156            thread
9157                .handle_session_update(
9158                    acp::SessionUpdate::ToolCall(
9159                        acp::ToolCall::new("terminal-tool-1", "Running command")
9160                            .kind(acp::ToolKind::Execute)
9161                            .status(acp::ToolCallStatus::InProgress)
9162                            .content(vec![acp::ToolCallContent::Terminal(acp::Terminal::new(
9163                                terminal_id.clone(),
9164                            ))])
9165                            .raw_input(serde_json::json!({"command": "sleep 1000", "cd": "/test"})),
9166                    ),
9167                    cx,
9168                )
9169                .unwrap();
9170        });
9171
9172        // Verify terminal exists and is in the thread
9173        let terminal_exists_before =
9174            thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id));
9175        assert!(
9176            terminal_exists_before,
9177            "Terminal should exist before checkpoint restore"
9178        );
9179
9180        // Verify the terminal's underlying task is still running (not completed)
9181        let terminal_running_before = thread.read_with(cx, |thread, _cx| {
9182            let terminal_entity = thread.terminals.get(&terminal_id).unwrap();
9183            terminal_entity.read_with(cx, |term, _cx| {
9184                term.output().is_none() // output is None means it's still running
9185            })
9186        });
9187        assert!(
9188            terminal_running_before,
9189            "Terminal should be running before checkpoint restore"
9190        );
9191
9192        // Verify we have the expected entries before restore
9193        let entry_count_before = thread.read_with(cx, |thread, _| thread.entries.len());
9194        assert!(
9195            entry_count_before > 1,
9196            "Should have multiple entries before restore"
9197        );
9198
9199        // Restore the checkpoint to the second message.
9200        // This should:
9201        // 1. Cancel any in-progress generation (via the cancel() call)
9202        // 2. Remove the terminal that was created after that point
9203        thread
9204            .update(cx, |thread, cx| {
9205                thread.restore_checkpoint(second_message_id, cx)
9206            })
9207            .await
9208            .unwrap();
9209
9210        // Verify that no send_task is in progress after restore
9211        // (cancel() clears the send_task)
9212        let has_send_task_after = thread.read_with(cx, |thread, _| thread.running_turn.is_some());
9213        assert!(
9214            !has_send_task_after,
9215            "Should not have a send_task after restore (cancel should have cleared it)"
9216        );
9217
9218        // Verify the entries were truncated (restoring to index 1 truncates at 1, keeping only index 0)
9219        let entry_count = thread.read_with(cx, |thread, _| thread.entries.len());
9220        assert_eq!(
9221            entry_count, 1,
9222            "Should have 1 entry after restore (only the first user message)"
9223        );
9224
9225        // Verify the 2 completed terminals from before the checkpoint still exist
9226        let terminal_1_exists = thread.read_with(cx, |thread, _| {
9227            thread.terminals.contains_key(&terminal_id_1)
9228        });
9229        assert!(
9230            terminal_1_exists,
9231            "Terminal 1 (from before checkpoint) should still exist"
9232        );
9233
9234        let terminal_2_exists = thread.read_with(cx, |thread, _| {
9235            thread.terminals.contains_key(&terminal_id_2)
9236        });
9237        assert!(
9238            terminal_2_exists,
9239            "Terminal 2 (from before checkpoint) should still exist"
9240        );
9241
9242        // Verify they're still in completed state
9243        let terminal_1_completed = thread.read_with(cx, |thread, _cx| {
9244            let terminal_entity = thread.terminals.get(&terminal_id_1).unwrap();
9245            terminal_entity.read_with(cx, |term, _cx| term.output().is_some())
9246        });
9247        assert!(terminal_1_completed, "Terminal 1 should still be completed");
9248
9249        let terminal_2_completed = thread.read_with(cx, |thread, _cx| {
9250            let terminal_entity = thread.terminals.get(&terminal_id_2).unwrap();
9251            terminal_entity.read_with(cx, |term, _cx| term.output().is_some())
9252        });
9253        assert!(terminal_2_completed, "Terminal 2 should still be completed");
9254
9255        // Verify the running terminal (created after checkpoint) was removed
9256        let terminal_3_exists =
9257            thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id));
9258        assert!(
9259            !terminal_3_exists,
9260            "Terminal 3 (created after checkpoint) should have been removed"
9261        );
9262
9263        // Verify total count is 2 (the two from before the checkpoint)
9264        let terminal_count = thread.read_with(cx, |thread, _| thread.terminals.len());
9265        assert_eq!(
9266            terminal_count, 2,
9267            "Should have exactly 2 terminals (the completed ones from before checkpoint)"
9268        );
9269    }
9270
9271    /// Tests that update_last_checkpoint correctly updates the original message's checkpoint
9272    /// even when a new user message is added while the async checkpoint comparison is in progress.
9273    ///
9274    /// This is a regression test for a bug where update_last_checkpoint would fail with
9275    /// "no checkpoint" if a new user message (without a checkpoint) was added between when
9276    /// update_last_checkpoint started and when its async closure ran.
9277    #[gpui::test]
9278    async fn test_update_last_checkpoint_with_new_message_added(cx: &mut TestAppContext) {
9279        init_test(cx);
9280
9281        let fs = FakeFs::new(cx.executor());
9282        fs.insert_tree(path!("/test"), json!({".git": {}, "file.txt": "content"}))
9283            .await;
9284        let project = Project::test(fs.clone(), [Path::new(path!("/test"))], cx).await;
9285
9286        let handler_done = Arc::new(AtomicBool::new(false));
9287        let handler_done_clone = handler_done.clone();
9288        let connection = Rc::new(FakeAgentConnection::new().on_user_message(
9289            move |_, _thread, _cx| {
9290                handler_done_clone.store(true, SeqCst);
9291                async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) }.boxed_local()
9292            },
9293        ));
9294
9295        let thread = cx
9296            .update(|cx| {
9297                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
9298            })
9299            .await
9300            .unwrap();
9301
9302        let send_future = thread.update(cx, |thread, cx| thread.send_raw("First message", cx));
9303        let send_task = cx.background_executor.spawn(send_future);
9304
9305        // Tick until handler completes, then a few more to let update_last_checkpoint start
9306        while !handler_done.load(SeqCst) {
9307            cx.executor().tick();
9308        }
9309        for _ in 0..5 {
9310            cx.executor().tick();
9311        }
9312
9313        thread.update(cx, |thread, cx| {
9314            thread.push_entry(
9315                AgentThreadEntry::UserMessage(UserMessage {
9316                    protocol_id: None,
9317                    client_id: Some(ClientUserMessageId::new()),
9318                    is_optimistic: true,
9319                    content: ContentBlock::Empty,
9320                    chunks: vec!["Injected message (no checkpoint)".into()],
9321                    checkpoint: None,
9322                    indented: false,
9323                }),
9324                cx,
9325            );
9326        });
9327
9328        cx.run_until_parked();
9329        let result = send_task.await;
9330
9331        assert!(
9332            result.is_ok(),
9333            "send should succeed even when new message added during update_last_checkpoint: {:?}",
9334            result.err()
9335        );
9336    }
9337
9338    /// This is a regression test for a bug where update_last_checkpoint would
9339    /// swallow a checkpoint comparison error and hide an already-visible
9340    /// "Restore checkpoint" button without logging anything.
9341    #[gpui::test]
9342    async fn test_update_last_checkpoint_compare_error_keeps_checkpoint_visible(
9343        cx: &mut TestAppContext,
9344    ) {
9345        init_test(cx);
9346
9347        let fs = FakeFs::new(cx.executor());
9348        fs.insert_tree(path!("/test"), json!({".git": {}, "file.txt": "content"}))
9349            .await;
9350        let project = Project::test(fs.clone(), [Path::new(path!("/test"))], cx).await;
9351
9352        // The handler waits for this signal so the repository can be swapped
9353        // out while the turn is still running.
9354        let (complete_tx, complete_rx) = futures::channel::oneshot::channel::<()>();
9355        let complete_rx = RefCell::new(Some(complete_rx));
9356        let connection = Rc::new(FakeAgentConnection::new().on_user_message(
9357            move |_, _thread, _cx| {
9358                let complete_rx = complete_rx.borrow_mut().take();
9359                async move {
9360                    if let Some(rx) = complete_rx {
9361                        rx.await.ok();
9362                    }
9363                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
9364                }
9365                .boxed_local()
9366            },
9367        ));
9368
9369        let thread = cx
9370            .update(|cx| {
9371                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
9372            })
9373            .await
9374            .unwrap();
9375
9376        let send_future = thread.update(cx, |thread, cx| thread.send_raw("message", cx));
9377        let send_task = cx.background_executor.spawn(send_future);
9378        cx.run_until_parked();
9379
9380        // Show the checkpoint, as update_last_checkpoint_if_changed does when
9381        // files change during the turn.
9382        thread.update(cx, |thread, _| {
9383            let (_, message) = thread.last_user_message().unwrap();
9384            message.checkpoint.as_mut().unwrap().show = true;
9385        });
9386
9387        // Recreate `.git` so the git store reopens the repository. The fresh
9388        // fake repository doesn't contain the checkpoint recorded at send
9389        // time, so the end-of-turn comparison fails.
9390        fs.remove_dir(
9391            Path::new(path!("/test/.git")),
9392            RemoveOptions {
9393                recursive: true,
9394                ignore_if_not_exists: false,
9395            },
9396        )
9397        .await
9398        .unwrap();
9399        cx.run_until_parked();
9400        fs.create_dir(Path::new(path!("/test/.git"))).await.unwrap();
9401        cx.run_until_parked();
9402
9403        complete_tx.send(()).unwrap();
9404        send_task.await.unwrap();
9405        cx.run_until_parked();
9406
9407        thread.update(cx, |thread, _| {
9408            let (_, message) = thread.last_user_message().unwrap();
9409            assert!(
9410                message.checkpoint.as_ref().unwrap().show,
9411                "a checkpoint comparison failure must not hide the restore checkpoint button"
9412            );
9413        });
9414    }
9415
9416    /// Tests that when a follow-up message is sent during generation,
9417    /// the first turn completing does NOT clear `running_turn` because
9418    /// it now belongs to the second turn.
9419    #[gpui::test]
9420    async fn test_follow_up_message_during_generation_does_not_clear_turn(cx: &mut TestAppContext) {
9421        init_test(cx);
9422
9423        let fs = FakeFs::new(cx.executor());
9424        let project = Project::test(fs, [], cx).await;
9425
9426        // First handler waits for this signal before completing
9427        let (first_complete_tx, first_complete_rx) = futures::channel::oneshot::channel::<()>();
9428        let first_complete_rx = RefCell::new(Some(first_complete_rx));
9429
9430        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
9431            move |params, _thread, _cx| {
9432                let first_complete_rx = first_complete_rx.borrow_mut().take();
9433                let is_first = params
9434                    .prompt
9435                    .iter()
9436                    .any(|c| matches!(c, acp::ContentBlock::Text(t) if t.text.contains("first")));
9437
9438                async move {
9439                    if is_first {
9440                        // First handler waits until signaled
9441                        if let Some(rx) = first_complete_rx {
9442                            rx.await.ok();
9443                        }
9444                    }
9445                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
9446                }
9447                .boxed_local()
9448            }
9449        }));
9450
9451        let thread = cx
9452            .update(|cx| {
9453                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
9454            })
9455            .await
9456            .unwrap();
9457
9458        // Send first message (turn_id=1) - handler will block
9459        let first_request = thread.update(cx, |thread, cx| thread.send_raw("first", cx));
9460        assert_eq!(thread.read_with(cx, |t, _| t.turn_id), 1);
9461
9462        // Send second message (turn_id=2) while first is still blocked
9463        // This calls cancel() which takes turn 1's running_turn and sets turn 2's
9464        let second_request = thread.update(cx, |thread, cx| thread.send_raw("second", cx));
9465        assert_eq!(thread.read_with(cx, |t, _| t.turn_id), 2);
9466
9467        let running_turn_after_second_send =
9468            thread.read_with(cx, |thread, _| thread.running_turn.as_ref().map(|t| t.id));
9469        assert_eq!(
9470            running_turn_after_second_send,
9471            Some(2),
9472            "running_turn should be set to turn 2 after sending second message"
9473        );
9474
9475        // Now signal first handler to complete
9476        first_complete_tx.send(()).ok();
9477
9478        // First request completes - should NOT clear running_turn
9479        // because running_turn now belongs to turn 2
9480        first_request.await.unwrap();
9481
9482        let running_turn_after_first =
9483            thread.read_with(cx, |thread, _| thread.running_turn.as_ref().map(|t| t.id));
9484        assert_eq!(
9485            running_turn_after_first,
9486            Some(2),
9487            "first turn completing should not clear running_turn (belongs to turn 2)"
9488        );
9489
9490        // Second request completes - SHOULD clear running_turn
9491        second_request.await.unwrap();
9492
9493        let running_turn_after_second =
9494            thread.read_with(cx, |thread, _| thread.running_turn.is_some());
9495        assert!(
9496            !running_turn_after_second,
9497            "second turn completing should clear running_turn"
9498        );
9499    }
9500
9501    #[gpui::test]
9502    async fn test_stale_cancelled_response_does_not_cancel_current_compaction(
9503        cx: &mut TestAppContext,
9504    ) {
9505        init_test(cx);
9506
9507        let fs = FakeFs::new(cx.executor());
9508        let project = Project::test(fs, [], cx).await;
9509
9510        let (first_complete_tx, first_complete_rx) = futures::channel::oneshot::channel::<()>();
9511        let first_complete_rx = RefCell::new(Some(first_complete_rx));
9512        let compaction_id = ContextCompactionId("test-compaction".into());
9513
9514        let connection = Rc::new(FakeAgentConnection::new().on_user_message({
9515            let compaction_id = compaction_id.clone();
9516            move |params, thread, mut cx| {
9517                let first_complete_rx = first_complete_rx.borrow_mut().take();
9518                let is_first = params.prompt.iter().any(|content| {
9519                    matches!(content, acp::ContentBlock::Text(text) if text.text.contains("first"))
9520                });
9521                let compaction_id = compaction_id.clone();
9522
9523                async move {
9524                    if is_first {
9525                        if let Some(rx) = first_complete_rx {
9526                            rx.await
9527                                .expect("first completion sender should still be alive");
9528                        }
9529
9530                        thread.update(&mut cx, |thread, cx| {
9531                            thread.push_context_compaction(
9532                                ContextCompaction {
9533                                    id: compaction_id,
9534                                    status: ContextCompactionStatus::InProgress,
9535                                    summary: None,
9536                                },
9537                                cx,
9538                            );
9539                        })?;
9540
9541                        Ok(acp::PromptResponse::new(acp::StopReason::Cancelled))
9542                    } else {
9543                        Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
9544                    }
9545                }
9546                .boxed_local()
9547            }
9548        }));
9549
9550        let thread = cx
9551            .update(|cx| {
9552                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
9553            })
9554            .await
9555            .unwrap();
9556
9557        let first_request = thread.update(cx, |thread, cx| thread.send_raw("first", cx));
9558        assert_eq!(thread.read_with(cx, |thread, _| thread.turn_id), 1);
9559
9560        let second_request = thread.update(cx, |thread, cx| thread.send_raw("second", cx));
9561        assert_eq!(thread.read_with(cx, |thread, _| thread.turn_id), 2);
9562
9563        first_complete_tx
9564            .send(())
9565            .expect("first completion receiver should still be alive");
9566
9567        let response = first_request
9568            .await
9569            .expect("first request should complete")
9570            .expect("first request should have response");
9571        assert_eq!(response.stop_reason, acp::StopReason::Cancelled);
9572
9573        thread.read_with(cx, |thread, _| {
9574            let compaction = thread
9575                .entries
9576                .iter()
9577                .find_map(|entry| {
9578                    let AgentThreadEntry::ContextCompaction(compaction) = entry else {
9579                        return None;
9580                    };
9581                    (compaction.id == compaction_id).then_some(compaction)
9582                })
9583                .expect("compaction entry should exist");
9584
9585            assert_eq!(
9586                compaction.status,
9587                ContextCompactionStatus::InProgress,
9588                "a stale cancelled response from an older turn should not cancel current compaction"
9589            );
9590        });
9591
9592        second_request
9593            .await
9594            .expect("second request should complete");
9595    }
9596
9597    #[gpui::test]
9598    async fn test_send_omits_message_id_without_client_user_message_id_support(
9599        cx: &mut TestAppContext,
9600    ) {
9601        init_test(cx);
9602
9603        let fs = FakeFs::new(cx.executor());
9604        let project = Project::test(fs, [], cx).await;
9605
9606        let connection = Rc::new(FakeAgentConnection::new().without_truncate_support());
9607        let thread = cx
9608            .update(|cx| {
9609                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
9610            })
9611            .await
9612            .unwrap();
9613
9614        let response = thread
9615            .update(cx, |thread, cx| thread.send_raw("test message", cx))
9616            .await;
9617
9618        assert!(response.is_ok(), "send should not fail: {response:?}");
9619        thread.read_with(cx, |thread, _| {
9620            let AgentThreadEntry::UserMessage(message) = &thread.entries[0] else {
9621                panic!("expected first entry to be a user message")
9622            };
9623            assert_eq!(message.protocol_id, None);
9624            assert_eq!(message.client_id, None);
9625            assert!(message.is_optimistic);
9626        });
9627    }
9628
9629    #[gpui::test]
9630    async fn test_send_returns_cancelled_response_and_marks_tools_as_cancelled(
9631        cx: &mut TestAppContext,
9632    ) {
9633        init_test(cx);
9634
9635        let fs = FakeFs::new(cx.executor());
9636        let project = Project::test(fs, [], cx).await;
9637
9638        let connection = Rc::new(FakeAgentConnection::new().on_user_message(
9639            move |_params, thread, mut cx| {
9640                async move {
9641                    thread
9642                        .update(&mut cx, |thread, cx| {
9643                            thread.handle_session_update(
9644                                acp::SessionUpdate::ToolCall(
9645                                    acp::ToolCall::new(
9646                                        acp::ToolCallId::new("test-tool"),
9647                                        "Test Tool",
9648                                    )
9649                                    .kind(acp::ToolKind::Fetch)
9650                                    .status(acp::ToolCallStatus::InProgress),
9651                                ),
9652                                cx,
9653                            )
9654                        })
9655                        .unwrap()
9656                        .unwrap();
9657
9658                    Ok(acp::PromptResponse::new(acp::StopReason::Cancelled))
9659                }
9660                .boxed_local()
9661            },
9662        ));
9663
9664        let thread = cx
9665            .update(|cx| {
9666                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
9667            })
9668            .await
9669            .unwrap();
9670
9671        let response = thread
9672            .update(cx, |thread, cx| thread.send_raw("test message", cx))
9673            .await;
9674
9675        let response = response
9676            .expect("send should succeed")
9677            .expect("should have response");
9678        assert_eq!(
9679            response.stop_reason,
9680            acp::StopReason::Cancelled,
9681            "response should have Cancelled stop_reason"
9682        );
9683
9684        thread.read_with(cx, |thread, _| {
9685            let tool_entry = thread
9686                .entries
9687                .iter()
9688                .find_map(|e| {
9689                    if let AgentThreadEntry::ToolCall(call) = e {
9690                        Some(call)
9691                    } else {
9692                        None
9693                    }
9694                })
9695                .expect("should have tool call entry");
9696
9697            assert!(
9698                matches!(tool_entry.status, ToolCallStatus::Canceled),
9699                "tool should be marked as Canceled when response is Cancelled, got {:?}",
9700                tool_entry.status
9701            );
9702        });
9703    }
9704
9705    #[gpui::test]
9706    async fn test_provisional_title_replaced_by_real_title(cx: &mut TestAppContext) {
9707        init_test(cx);
9708
9709        let fs = FakeFs::new(cx.executor());
9710        let project = Project::test(fs, [], cx).await;
9711        let connection = Rc::new(FakeAgentConnection::new());
9712        let set_title_calls = connection.set_title_calls.clone();
9713
9714        let thread = cx
9715            .update(|cx| {
9716                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
9717            })
9718            .await
9719            .unwrap();
9720
9721        // Initial title is the default.
9722        thread.read_with(cx, |thread, _| {
9723            assert_eq!(thread.title(), None);
9724        });
9725
9726        // Setting a provisional title updates the display title.
9727        thread.update(cx, |thread, cx| {
9728            thread.set_provisional_title("Hello, can you help…".into(), cx);
9729        });
9730        thread.read_with(cx, |thread, _| {
9731            assert_eq!(
9732                thread.title().as_ref().map(|s| s.as_str()),
9733                Some("Hello, can you help…")
9734            );
9735        });
9736
9737        // The provisional title should NOT have propagated to the connection.
9738        assert_eq!(
9739            set_title_calls.borrow().len(),
9740            0,
9741            "provisional title should not propagate to the connection"
9742        );
9743
9744        // When the real title arrives via set_title, it replaces the
9745        // provisional title and propagates to the connection.
9746        let task = thread.update(cx, |thread, cx| {
9747            thread.set_title("Helping with Rust question".into(), cx)
9748        });
9749        task.await.expect("set_title should succeed");
9750        thread.read_with(cx, |thread, _| {
9751            assert_eq!(
9752                thread.title().as_ref().map(|s| s.as_str()),
9753                Some("Helping with Rust question")
9754            );
9755        });
9756        assert_eq!(
9757            set_title_calls.borrow().as_slice(),
9758            &[SharedString::from("Helping with Rust question")],
9759            "real title should propagate to the connection"
9760        );
9761    }
9762
9763    #[gpui::test]
9764    async fn test_session_info_update_replaces_provisional_title_and_emits_event(
9765        cx: &mut TestAppContext,
9766    ) {
9767        init_test(cx);
9768
9769        let fs = FakeFs::new(cx.executor());
9770        let project = Project::test(fs, [], cx).await;
9771        let connection = Rc::new(FakeAgentConnection::new());
9772
9773        let thread = cx
9774            .update(|cx| {
9775                connection.clone().new_session(
9776                    project,
9777                    PathList::new(&[Path::new(path!("/test"))]),
9778                    cx,
9779                )
9780            })
9781            .await
9782            .unwrap();
9783
9784        let title_updated_events = Rc::new(RefCell::new(0usize));
9785        let title_updated_events_for_subscription = title_updated_events.clone();
9786        thread.update(cx, |_thread, cx| {
9787            cx.subscribe(
9788                &thread,
9789                move |_thread, _event_thread, event: &AcpThreadEvent, _cx| {
9790                    if matches!(event, AcpThreadEvent::TitleUpdated) {
9791                        *title_updated_events_for_subscription.borrow_mut() += 1;
9792                    }
9793                },
9794            )
9795            .detach();
9796        });
9797
9798        thread.update(cx, |thread, cx| {
9799            thread.set_provisional_title("Hello, can you help…".into(), cx);
9800        });
9801        assert_eq!(
9802            *title_updated_events.borrow(),
9803            1,
9804            "setting a provisional title should emit TitleUpdated"
9805        );
9806
9807        let result = thread.update(cx, |thread, cx| {
9808            thread.handle_session_update(
9809                acp::SessionUpdate::SessionInfoUpdate(
9810                    acp::SessionInfoUpdate::new().title("Helping with Rust question"),
9811                ),
9812                cx,
9813            )
9814        });
9815        result.expect("session info update should succeed");
9816
9817        thread.read_with(cx, |thread, _| {
9818            assert_eq!(
9819                thread.title().as_ref().map(|s| s.as_str()),
9820                Some("Helping with Rust question")
9821            );
9822            assert!(
9823                !thread.has_provisional_title(),
9824                "session info title update should clear provisional title"
9825            );
9826        });
9827
9828        assert_eq!(
9829            *title_updated_events.borrow(),
9830            2,
9831            "session info title update should emit TitleUpdated"
9832        );
9833        assert!(
9834            connection.set_title_calls.borrow().is_empty(),
9835            "session info title update should not propagate back to the connection"
9836        );
9837    }
9838
9839    #[gpui::test]
9840    async fn test_usage_update_populates_token_usage_and_cost(cx: &mut TestAppContext) {
9841        init_test(cx);
9842
9843        let fs = FakeFs::new(cx.executor());
9844        let project = Project::test(fs, [], cx).await;
9845        let connection = Rc::new(FakeAgentConnection::new());
9846        let thread = cx
9847            .update(|cx| {
9848                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
9849            })
9850            .await
9851            .unwrap();
9852
9853        thread.update(cx, |thread, cx| {
9854            thread
9855                .handle_session_update(
9856                    acp::SessionUpdate::UsageUpdate(
9857                        acp::UsageUpdate::new(5000, 10000).cost(acp::Cost::new(0.42, "USD")),
9858                    ),
9859                    cx,
9860                )
9861                .unwrap();
9862        });
9863
9864        thread.read_with(cx, |thread, _| {
9865            let usage = thread.token_usage().expect("token_usage should be set");
9866            assert_eq!(usage.max_tokens, 10000);
9867            assert_eq!(usage.used_tokens, 5000);
9868
9869            let cost = thread.cost().expect("cost should be set");
9870            assert!((cost.amount - 0.42).abs() < f64::EPSILON);
9871            assert_eq!(cost.currency.as_ref(), "USD");
9872        });
9873    }
9874
9875    #[gpui::test]
9876    async fn test_context_compaction_preserves_token_usage(cx: &mut TestAppContext) {
9877        init_test(cx);
9878
9879        let fs = FakeFs::new(cx.executor());
9880        let project = Project::test(fs, [], cx).await;
9881        let connection = Rc::new(FakeAgentConnection::new());
9882        let thread = cx
9883            .update(|cx| {
9884                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
9885            })
9886            .await
9887            .unwrap();
9888
9889        thread.update(cx, |thread, cx| {
9890            thread
9891                .handle_session_update(
9892                    acp::SessionUpdate::UsageUpdate(
9893                        acp::UsageUpdate::new(5000, 10000).cost(acp::Cost::new(0.42, "USD")),
9894                    ),
9895                    cx,
9896                )
9897                .unwrap();
9898
9899            thread.push_context_compaction(
9900                ContextCompaction {
9901                    id: ContextCompactionId("compaction-1".into()),
9902                    status: ContextCompactionStatus::InProgress,
9903                    summary: None,
9904                },
9905                cx,
9906            );
9907        });
9908
9909        thread.read_with(cx, |thread, _| {
9910            let usage = thread
9911                .token_usage()
9912                .expect("context compaction should not clear token usage on its own");
9913            assert_eq!(usage.used_tokens, 5000);
9914            assert_eq!(usage.max_tokens, 10000);
9915
9916            let cost = thread
9917                .cost()
9918                .expect("context compaction should not clear cost on its own");
9919            assert!((cost.amount - 0.42).abs() < f64::EPSILON);
9920        });
9921
9922        thread.update(cx, |thread, cx| {
9923            thread
9924                .handle_session_update(
9925                    acp::SessionUpdate::UsageUpdate(acp::UsageUpdate::new(1000, 10000)),
9926                    cx,
9927                )
9928                .unwrap();
9929        });
9930
9931        thread.read_with(cx, |thread, _| {
9932            let usage = thread
9933                .token_usage()
9934                .expect("token_usage should be restored by the next usage update");
9935            assert_eq!(usage.used_tokens, 1000);
9936            assert_eq!(usage.max_tokens, 10000);
9937        });
9938    }
9939
9940    #[gpui::test]
9941    async fn test_usage_update_without_cost_preserves_existing_cost(cx: &mut TestAppContext) {
9942        init_test(cx);
9943
9944        let fs = FakeFs::new(cx.executor());
9945        let project = Project::test(fs, [], cx).await;
9946        let connection = Rc::new(FakeAgentConnection::new());
9947        let thread = cx
9948            .update(|cx| {
9949                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
9950            })
9951            .await
9952            .unwrap();
9953
9954        thread.update(cx, |thread, cx| {
9955            thread
9956                .handle_session_update(
9957                    acp::SessionUpdate::UsageUpdate(
9958                        acp::UsageUpdate::new(1000, 10000).cost(acp::Cost::new(0.10, "USD")),
9959                    ),
9960                    cx,
9961                )
9962                .unwrap();
9963
9964            thread
9965                .handle_session_update(
9966                    acp::SessionUpdate::UsageUpdate(acp::UsageUpdate::new(2000, 10000)),
9967                    cx,
9968                )
9969                .unwrap();
9970        });
9971
9972        thread.read_with(cx, |thread, _| {
9973            let usage = thread.token_usage().expect("token_usage should be set");
9974            assert_eq!(usage.used_tokens, 2000);
9975
9976            let cost = thread.cost().expect("cost should be preserved");
9977            assert!((cost.amount - 0.10).abs() < f64::EPSILON);
9978        });
9979    }
9980
9981    #[gpui::test]
9982    async fn test_response_usage_does_not_clobber_session_usage(cx: &mut TestAppContext) {
9983        init_test(cx);
9984
9985        let fs = FakeFs::new(cx.executor());
9986        let project = Project::test(fs, [], cx).await;
9987        let connection = Rc::new(FakeAgentConnection::new().on_user_message(
9988            move |_, thread, mut cx| {
9989                async move {
9990                    thread.update(&mut cx, |thread, cx| {
9991                        thread
9992                            .handle_session_update(
9993                                acp::SessionUpdate::UsageUpdate(
9994                                    acp::UsageUpdate::new(3000, 10000)
9995                                        .cost(acp::Cost::new(0.05, "EUR")),
9996                                ),
9997                                cx,
9998                            )
9999                            .unwrap();
10000                    })?;
10001                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)
10002                        .usage(acp::Usage::new(500, 200, 300)))
10003                }
10004                .boxed_local()
10005            },
10006        ));
10007
10008        let thread = cx
10009            .update(|cx| {
10010                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
10011            })
10012            .await
10013            .unwrap();
10014
10015        thread
10016            .update(cx, |thread, cx| thread.send_raw("hello", cx))
10017            .await
10018            .unwrap();
10019
10020        thread.read_with(cx, |thread, _| {
10021            let usage = thread.token_usage().expect("token_usage should be set");
10022            assert_eq!(usage.max_tokens, 10000, "max_tokens from UsageUpdate");
10023            assert_eq!(usage.used_tokens, 3000, "used_tokens from UsageUpdate");
10024            assert_eq!(usage.input_tokens, 200, "input_tokens from response usage");
10025            assert_eq!(
10026                usage.output_tokens, 300,
10027                "output_tokens from response usage"
10028            );
10029
10030            let cost = thread.cost().expect("cost should be set");
10031            assert!((cost.amount - 0.05).abs() < f64::EPSILON);
10032            assert_eq!(cost.currency.as_ref(), "EUR");
10033        });
10034    }
10035
10036    #[gpui::test]
10037    async fn test_clearing_token_usage_also_clears_cost(cx: &mut TestAppContext) {
10038        init_test(cx);
10039
10040        let fs = FakeFs::new(cx.executor());
10041        let project = Project::test(fs, [], cx).await;
10042        let connection = Rc::new(FakeAgentConnection::new());
10043        let thread = cx
10044            .update(|cx| {
10045                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
10046            })
10047            .await
10048            .unwrap();
10049
10050        thread.update(cx, |thread, cx| {
10051            thread
10052                .handle_session_update(
10053                    acp::SessionUpdate::UsageUpdate(
10054                        acp::UsageUpdate::new(1000, 10000).cost(acp::Cost::new(0.25, "USD")),
10055                    ),
10056                    cx,
10057                )
10058                .unwrap();
10059
10060            assert!(thread.token_usage().is_some());
10061            assert!(thread.cost().is_some());
10062
10063            thread.update_token_usage(None, cx);
10064
10065            assert!(thread.token_usage().is_none());
10066            assert!(
10067                thread.cost().is_none(),
10068                "cost should be cleared when token usage is cleared"
10069            );
10070        });
10071    }
10072
10073    /// Regression test: if the inner send_task is cancelled before it can
10074    /// fire `tx.send(...)` (e.g. because the underlying future was dropped),
10075    /// the outer task observes `rx.await` returning `Err(Cancelled)` and
10076    /// must still clear `running_turn` so the panel transitions out of
10077    /// `Generating`. Without this, the agent thread is wedged in the
10078    /// loading state until Zed restarts.
10079    #[gpui::test]
10080    async fn test_running_turn_cleared_when_send_task_dropped(cx: &mut TestAppContext) {
10081        init_test(cx);
10082
10083        let fs = FakeFs::new(cx.executor());
10084        let project = Project::test(fs, [], cx).await;
10085
10086        // Handler hangs forever so the spawn at run_turn is parked inside
10087        // `f(this, cx).await` with `tx` still alive but unsent.
10088        let connection = Rc::new(FakeAgentConnection::new().on_user_message(
10089            |_params, _thread, _cx| {
10090                async move { futures::future::pending::<Result<acp::PromptResponse>>().await }
10091                    .boxed_local()
10092            },
10093        ));
10094
10095        let thread = cx
10096            .update(|cx| {
10097                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
10098            })
10099            .await
10100            .unwrap();
10101
10102        let request = thread.update(cx, |thread, cx| thread.send_raw("hello", cx));
10103        cx.run_until_parked();
10104
10105        assert_eq!(
10106            thread.read_with(cx, |t, _| t.status()),
10107            ThreadStatus::Generating,
10108            "thread should be generating while the handler is parked"
10109        );
10110
10111        // Replace the in-flight send_task with a no-op. Dropping the original
10112        // Task cancels its inner future, which drops `tx` without ever calling
10113        // `tx.send(...)`. This mirrors the production scenario where the
10114        // send_task future is cancelled before completion.
10115        thread.update(cx, |thread, _| {
10116            thread.running_turn.as_mut().unwrap().send_task = Task::ready(());
10117        });
10118
10119        let result = request.await;
10120        assert!(
10121            matches!(result, Ok(None)),
10122            "outer task should resolve to Ok(None) on dropped tx, got {result:?}"
10123        );
10124
10125        assert_eq!(
10126            thread.read_with(cx, |t, _| t.status()),
10127            ThreadStatus::Idle,
10128            "running_turn must be cleared even when tx was dropped without send"
10129        );
10130    }
10131
10132    /// OMEGA-DELTA-0045. A host-authored note lands in the transcript, once.
10133    ///
10134    /// The source check in `crates/omega_deltas` reads the shape; this runs it.
10135    /// Both halves matter: the shape check would pass an implementation that
10136    /// keyed on the id and appended anyway, and a behavioural test alone would
10137    /// pass an implementation nothing renders.
10138    #[gpui::test]
10139    async fn test_system_note_is_appended_once_per_note_id(cx: &mut TestAppContext) {
10140        init_test(cx);
10141
10142        let fs = FakeFs::new(cx.executor());
10143        let project = Project::test(fs, [], cx).await;
10144        let connection = Rc::new(FakeAgentConnection::new());
10145        let thread = cx
10146            .update(|cx| {
10147                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
10148            })
10149            .await
10150            .unwrap();
10151
10152        let note = |id: &str, text: &str| SystemNote {
10153            id: SystemNoteId(id.into()),
10154            text: text.into(),
10155        };
10156
10157        thread.update(cx, |thread, cx| {
10158            assert!(
10159                thread.push_system_note(
10160                    note("handoff.provider.1", "Provider handoff: codex-local to claude-local"),
10161                    cx,
10162                ),
10163                "the first append of a note id must land"
10164            );
10165            assert!(
10166                !thread.push_system_note(
10167                    note("handoff.provider.1", "Provider handoff: rewritten"),
10168                    cx,
10169                ),
10170                "a retry of the same note id must not append again, and must \
10171                 not rewrite what the owner has already read"
10172            );
10173            assert!(
10174                thread.push_system_note(
10175                    note("handoff.provider.2", "Provider handoff: claude-local to codex-local"),
10176                    cx,
10177                ),
10178                "a different note id is a different disclosure"
10179            );
10180        });
10181
10182        thread.read_with(cx, |thread, cx| {
10183            let notes: Vec<&SystemNote> = thread
10184                .entries()
10185                .iter()
10186                .filter_map(|entry| match entry {
10187                    AgentThreadEntry::SystemNote(note) => Some(note),
10188                    _ => None,
10189                })
10190                .collect();
10191            assert_eq!(notes.len(), 2, "one entry per distinct note id");
10192            assert_eq!(
10193                notes[0].text.as_ref(),
10194                "Provider handoff: codex-local to claude-local",
10195                "the retry must not have rewritten the first disclosure"
10196            );
10197
10198            // The transcript the owner copies out carries it too. A note that
10199            // is on screen and absent from the export would let the silence
10200            // rc11 shipped come back the moment anyone quotes a thread.
10201            let markdown = thread.to_markdown(cx);
10202            assert!(
10203                markdown.contains("Provider handoff: codex-local to claude-local"),
10204                "the exported transcript must carry the disclosure: {markdown}"
10205            );
10206        });
10207    }
10208}
10209
Served at tenant.openagents/omega Member data and write actions are omitted.