claude.rs

   1mod mcp_server;
   2pub mod tools;
   3
   4use collections::HashMap;
   5use context_server::listener::McpServerTool;
   6use project::Project;
   7use settings::SettingsStore;
   8use smol::process::Child;
   9use std::any::Any;
  10use std::cell::RefCell;
  11use std::fmt::Display;
  12use std::path::Path;
  13use std::rc::Rc;
  14use uuid::Uuid;
  15
  16use agent_client_protocol as acp;
  17use anyhow::{Result, anyhow};
  18use futures::channel::oneshot;
  19use futures::{AsyncBufReadExt, AsyncWriteExt};
  20use futures::{
  21    AsyncRead, AsyncWrite, FutureExt, StreamExt,
  22    channel::mpsc::{self, UnboundedReceiver, UnboundedSender},
  23    io::BufReader,
  24    select_biased,
  25};
  26use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
  27use serde::{Deserialize, Serialize};
  28use util::{ResultExt, debug_panic};
  29
  30use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
  31use crate::claude::tools::ClaudeTool;
  32use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
  33use acp_thread::{AcpThread, AgentConnection};
  34
  35#[derive(Clone)]
  36pub struct ClaudeCode;
  37
  38impl AgentServer for ClaudeCode {
  39    fn name(&self) -> &'static str {
  40        "Claude Code"
  41    }
  42
  43    fn empty_state_headline(&self) -> &'static str {
  44        self.name()
  45    }
  46
  47    fn empty_state_message(&self) -> &'static str {
  48        "How can I help you today?"
  49    }
  50
  51    fn logo(&self) -> ui::IconName {
  52        ui::IconName::AiClaude
  53    }
  54
  55    fn connect(
  56        &self,
  57        _root_dir: &Path,
  58        _project: &Entity<Project>,
  59        _cx: &mut App,
  60    ) -> Task<Result<Rc<dyn AgentConnection>>> {
  61        let connection = ClaudeAgentConnection {
  62            sessions: Default::default(),
  63        };
  64
  65        Task::ready(Ok(Rc::new(connection) as _))
  66    }
  67}
  68
  69struct ClaudeAgentConnection {
  70    sessions: Rc<RefCell<HashMap<acp::SessionId, ClaudeAgentSession>>>,
  71}
  72
  73impl AgentConnection for ClaudeAgentConnection {
  74    fn new_thread(
  75        self: Rc<Self>,
  76        project: Entity<Project>,
  77        cwd: &Path,
  78        cx: &mut App,
  79    ) -> Task<Result<Entity<AcpThread>>> {
  80        let cwd = cwd.to_owned();
  81        cx.spawn(async move |cx| {
  82            let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
  83            let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?;
  84
  85            let mut mcp_servers = HashMap::default();
  86            mcp_servers.insert(
  87                mcp_server::SERVER_NAME.to_string(),
  88                permission_mcp_server.server_config()?,
  89            );
  90            let mcp_config = McpConfig { mcp_servers };
  91
  92            let mcp_config_file = tempfile::NamedTempFile::new()?;
  93            let (mcp_config_file, mcp_config_path) = mcp_config_file.into_parts();
  94
  95            let mut mcp_config_file = smol::fs::File::from(mcp_config_file);
  96            mcp_config_file
  97                .write_all(serde_json::to_string(&mcp_config)?.as_bytes())
  98                .await?;
  99            mcp_config_file.flush().await?;
 100
 101            let settings = cx.read_global(|settings: &SettingsStore, _| {
 102                settings.get::<AllAgentServersSettings>(None).claude.clone()
 103            })?;
 104
 105            let Some(command) = AgentServerCommand::resolve(
 106                "claude",
 107                &[],
 108                Some(&util::paths::home_dir().join(".claude/local/claude")),
 109                settings,
 110                &project,
 111                cx,
 112            )
 113            .await
 114            else {
 115                anyhow::bail!("Failed to find claude binary");
 116            };
 117
 118            let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded();
 119            let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
 120
 121            let session_id = acp::SessionId(Uuid::new_v4().to_string().into());
 122
 123            log::trace!("Starting session with id: {}", session_id);
 124
 125            let mut child = spawn_claude(
 126                &command,
 127                ClaudeSessionMode::Start,
 128                session_id.clone(),
 129                &mcp_config_path,
 130                &cwd,
 131            )?;
 132
 133            let stdin = child.stdin.take().unwrap();
 134            let stdout = child.stdout.take().unwrap();
 135
 136            let pid = child.id();
 137            log::trace!("Spawned (pid: {})", pid);
 138
 139            cx.background_spawn(async move {
 140                let mut outgoing_rx = Some(outgoing_rx);
 141
 142                ClaudeAgentSession::handle_io(
 143                    outgoing_rx.take().unwrap(),
 144                    incoming_message_tx.clone(),
 145                    stdin,
 146                    stdout,
 147                )
 148                .await?;
 149
 150                log::trace!("Stopped (pid: {})", pid);
 151
 152                drop(mcp_config_path);
 153                anyhow::Ok(())
 154            })
 155            .detach();
 156
 157            let turn_state = Rc::new(RefCell::new(TurnState::None));
 158
 159            let handler_task = cx.spawn({
 160                let turn_state = turn_state.clone();
 161                let mut thread_rx = thread_rx.clone();
 162                async move |cx| {
 163                    while let Some(message) = incoming_message_rx.next().await {
 164                        ClaudeAgentSession::handle_message(
 165                            thread_rx.clone(),
 166                            message,
 167                            turn_state.clone(),
 168                            cx,
 169                        )
 170                        .await
 171                    }
 172
 173                    if let Some(status) = child.status().await.log_err() {
 174                        if let Some(thread) = thread_rx.recv().await.ok() {
 175                            thread
 176                                .update(cx, |thread, cx| {
 177                                    thread.emit_server_exited(status, cx);
 178                                })
 179                                .ok();
 180                        }
 181                    }
 182                }
 183            });
 184
 185            let thread = cx.new(|cx| {
 186                AcpThread::new("Claude Code", self.clone(), project, session_id.clone(), cx)
 187            })?;
 188
 189            thread_tx.send(thread.downgrade())?;
 190
 191            let session = ClaudeAgentSession {
 192                outgoing_tx,
 193                turn_state,
 194                _handler_task: handler_task,
 195                _mcp_server: Some(permission_mcp_server),
 196            };
 197
 198            self.sessions.borrow_mut().insert(session_id, session);
 199
 200            Ok(thread)
 201        })
 202    }
 203
 204    fn auth_methods(&self) -> &[acp::AuthMethod] {
 205        &[]
 206    }
 207
 208    fn authenticate(&self, _: acp::AuthMethodId, _cx: &mut App) -> Task<Result<()>> {
 209        Task::ready(Err(anyhow!("Authentication not supported")))
 210    }
 211
 212    fn prompt(
 213        &self,
 214        _id: Option<acp_thread::UserMessageId>,
 215        params: acp::PromptRequest,
 216        cx: &mut App,
 217    ) -> Task<Result<acp::PromptResponse>> {
 218        let sessions = self.sessions.borrow();
 219        let Some(session) = sessions.get(&params.session_id) else {
 220            return Task::ready(Err(anyhow!(
 221                "Attempted to send message to nonexistent session {}",
 222                params.session_id
 223            )));
 224        };
 225
 226        let (end_tx, end_rx) = oneshot::channel();
 227        session.turn_state.replace(TurnState::InProgress { end_tx });
 228
 229        let mut content = String::new();
 230        for chunk in params.prompt {
 231            match chunk {
 232                acp::ContentBlock::Text(text_content) => {
 233                    content.push_str(&text_content.text);
 234                }
 235                acp::ContentBlock::ResourceLink(resource_link) => {
 236                    content.push_str(&format!("@{}", resource_link.uri));
 237                }
 238                acp::ContentBlock::Audio(_)
 239                | acp::ContentBlock::Image(_)
 240                | acp::ContentBlock::Resource(_) => {
 241                    // TODO
 242                }
 243            }
 244        }
 245
 246        if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User {
 247            message: Message {
 248                role: Role::User,
 249                content: Content::UntaggedText(content),
 250                id: None,
 251                model: None,
 252                stop_reason: None,
 253                stop_sequence: None,
 254                usage: None,
 255            },
 256            session_id: Some(params.session_id.to_string()),
 257        }) {
 258            return Task::ready(Err(anyhow!(err)));
 259        }
 260
 261        cx.foreground_executor().spawn(async move { end_rx.await? })
 262    }
 263
 264    fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
 265        let sessions = self.sessions.borrow();
 266        let Some(session) = sessions.get(&session_id) else {
 267            log::warn!("Attempted to cancel nonexistent session {}", session_id);
 268            return;
 269        };
 270
 271        let request_id = new_request_id();
 272
 273        let turn_state = session.turn_state.take();
 274        let TurnState::InProgress { end_tx } = turn_state else {
 275            // Already cancelled or idle, put it back
 276            session.turn_state.replace(turn_state);
 277            return;
 278        };
 279
 280        session.turn_state.replace(TurnState::CancelRequested {
 281            end_tx,
 282            request_id: request_id.clone(),
 283        });
 284
 285        session
 286            .outgoing_tx
 287            .unbounded_send(SdkMessage::ControlRequest {
 288                request_id,
 289                request: ControlRequest::Interrupt,
 290            })
 291            .log_err();
 292    }
 293
 294    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 295        self
 296    }
 297}
 298
 299#[derive(Clone, Copy)]
 300enum ClaudeSessionMode {
 301    Start,
 302    #[expect(dead_code)]
 303    Resume,
 304}
 305
 306fn spawn_claude(
 307    command: &AgentServerCommand,
 308    mode: ClaudeSessionMode,
 309    session_id: acp::SessionId,
 310    mcp_config_path: &Path,
 311    root_dir: &Path,
 312) -> Result<Child> {
 313    let child = util::command::new_smol_command(&command.path)
 314        .args([
 315            "--input-format",
 316            "stream-json",
 317            "--output-format",
 318            "stream-json",
 319            "--print",
 320            "--verbose",
 321            "--mcp-config",
 322            mcp_config_path.to_string_lossy().as_ref(),
 323            "--permission-prompt-tool",
 324            &format!(
 325                "mcp__{}__{}",
 326                mcp_server::SERVER_NAME,
 327                mcp_server::PermissionTool::NAME,
 328            ),
 329            "--allowedTools",
 330            &format!(
 331                "mcp__{}__{},mcp__{}__{}",
 332                mcp_server::SERVER_NAME,
 333                mcp_server::EditTool::NAME,
 334                mcp_server::SERVER_NAME,
 335                mcp_server::ReadTool::NAME
 336            ),
 337            "--disallowedTools",
 338            "Read,Edit",
 339        ])
 340        .args(match mode {
 341            ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()],
 342            ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()],
 343        })
 344        .args(command.args.iter().map(|arg| arg.as_str()))
 345        .current_dir(root_dir)
 346        .stdin(std::process::Stdio::piped())
 347        .stdout(std::process::Stdio::piped())
 348        .stderr(std::process::Stdio::inherit())
 349        .kill_on_drop(true)
 350        .spawn()?;
 351
 352    Ok(child)
 353}
 354
 355struct ClaudeAgentSession {
 356    outgoing_tx: UnboundedSender<SdkMessage>,
 357    turn_state: Rc<RefCell<TurnState>>,
 358    _mcp_server: Option<ClaudeZedMcpServer>,
 359    _handler_task: Task<()>,
 360}
 361
 362#[derive(Debug, Default)]
 363enum TurnState {
 364    #[default]
 365    None,
 366    InProgress {
 367        end_tx: oneshot::Sender<Result<acp::PromptResponse>>,
 368    },
 369    CancelRequested {
 370        end_tx: oneshot::Sender<Result<acp::PromptResponse>>,
 371        request_id: String,
 372    },
 373    CancelConfirmed {
 374        end_tx: oneshot::Sender<Result<acp::PromptResponse>>,
 375    },
 376}
 377
 378impl TurnState {
 379    fn is_cancelled(&self) -> bool {
 380        matches!(self, TurnState::CancelConfirmed { .. })
 381    }
 382
 383    fn end_tx(self) -> Option<oneshot::Sender<Result<acp::PromptResponse>>> {
 384        match self {
 385            TurnState::None => None,
 386            TurnState::InProgress { end_tx, .. } => Some(end_tx),
 387            TurnState::CancelRequested { end_tx, .. } => Some(end_tx),
 388            TurnState::CancelConfirmed { end_tx } => Some(end_tx),
 389        }
 390    }
 391
 392    fn confirm_cancellation(self, id: &str) -> Self {
 393        match self {
 394            TurnState::CancelRequested { request_id, end_tx } if request_id == id => {
 395                TurnState::CancelConfirmed { end_tx }
 396            }
 397            _ => self,
 398        }
 399    }
 400}
 401
 402impl ClaudeAgentSession {
 403    async fn handle_message(
 404        mut thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
 405        message: SdkMessage,
 406        turn_state: Rc<RefCell<TurnState>>,
 407        cx: &mut AsyncApp,
 408    ) {
 409        match message {
 410            // we should only be sending these out, they don't need to be in the thread
 411            SdkMessage::ControlRequest { .. } => {}
 412            SdkMessage::User {
 413                message,
 414                session_id: _,
 415            } => {
 416                let Some(thread) = thread_rx
 417                    .recv()
 418                    .await
 419                    .log_err()
 420                    .and_then(|entity| entity.upgrade())
 421                else {
 422                    log::error!("Received an SDK message but thread is gone");
 423                    return;
 424                };
 425
 426                for chunk in message.content.chunks() {
 427                    match chunk {
 428                        ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
 429                            if !turn_state.borrow().is_cancelled() {
 430                                thread
 431                                    .update(cx, |thread, cx| {
 432                                        thread.push_user_content_block(None, text.into(), cx)
 433                                    })
 434                                    .log_err();
 435                            }
 436                        }
 437                        ContentChunk::ToolResult {
 438                            content,
 439                            tool_use_id,
 440                        } => {
 441                            let content = content.to_string();
 442                            thread
 443                                .update(cx, |thread, cx| {
 444                                    thread.update_tool_call(
 445                                        acp::ToolCallUpdate {
 446                                            id: acp::ToolCallId(tool_use_id.into()),
 447                                            fields: acp::ToolCallUpdateFields {
 448                                                status: if turn_state.borrow().is_cancelled() {
 449                                                    // Do not set to completed if turn was cancelled
 450                                                    None
 451                                                } else {
 452                                                    Some(acp::ToolCallStatus::Completed)
 453                                                },
 454                                                content: (!content.is_empty())
 455                                                    .then(|| vec![content.into()]),
 456                                                ..Default::default()
 457                                            },
 458                                        },
 459                                        cx,
 460                                    )
 461                                })
 462                                .log_err();
 463                        }
 464                        ContentChunk::Thinking { .. }
 465                        | ContentChunk::RedactedThinking
 466                        | ContentChunk::ToolUse { .. } => {
 467                            debug_panic!(
 468                                "Should not get {:?} with role: assistant. should we handle this?",
 469                                chunk
 470                            );
 471                        }
 472
 473                        ContentChunk::Image
 474                        | ContentChunk::Document
 475                        | ContentChunk::WebSearchToolResult => {
 476                            thread
 477                                .update(cx, |thread, cx| {
 478                                    thread.push_assistant_content_block(
 479                                        format!("Unsupported content: {:?}", chunk).into(),
 480                                        false,
 481                                        cx,
 482                                    )
 483                                })
 484                                .log_err();
 485                        }
 486                    }
 487                }
 488            }
 489            SdkMessage::Assistant {
 490                message,
 491                session_id: _,
 492            } => {
 493                let Some(thread) = thread_rx
 494                    .recv()
 495                    .await
 496                    .log_err()
 497                    .and_then(|entity| entity.upgrade())
 498                else {
 499                    log::error!("Received an SDK message but thread is gone");
 500                    return;
 501                };
 502
 503                for chunk in message.content.chunks() {
 504                    match chunk {
 505                        ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
 506                            thread
 507                                .update(cx, |thread, cx| {
 508                                    thread.push_assistant_content_block(text.into(), false, cx)
 509                                })
 510                                .log_err();
 511                        }
 512                        ContentChunk::Thinking { thinking } => {
 513                            thread
 514                                .update(cx, |thread, cx| {
 515                                    thread.push_assistant_content_block(thinking.into(), true, cx)
 516                                })
 517                                .log_err();
 518                        }
 519                        ContentChunk::RedactedThinking => {
 520                            thread
 521                                .update(cx, |thread, cx| {
 522                                    thread.push_assistant_content_block(
 523                                        "[REDACTED]".into(),
 524                                        true,
 525                                        cx,
 526                                    )
 527                                })
 528                                .log_err();
 529                        }
 530                        ContentChunk::ToolUse { id, name, input } => {
 531                            let claude_tool = ClaudeTool::infer(&name, input);
 532
 533                            thread
 534                                .update(cx, |thread, cx| {
 535                                    if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
 536                                        thread.update_plan(
 537                                            acp::Plan {
 538                                                entries: params
 539                                                    .todos
 540                                                    .into_iter()
 541                                                    .map(Into::into)
 542                                                    .collect(),
 543                                            },
 544                                            cx,
 545                                        )
 546                                    } else {
 547                                        thread.upsert_tool_call(
 548                                            claude_tool.as_acp(acp::ToolCallId(id.into())),
 549                                            cx,
 550                                        );
 551                                    }
 552                                })
 553                                .log_err();
 554                        }
 555                        ContentChunk::ToolResult { .. } | ContentChunk::WebSearchToolResult => {
 556                            debug_panic!(
 557                                "Should not get tool results with role: assistant. should we handle this?"
 558                            );
 559                        }
 560                        ContentChunk::Image | ContentChunk::Document => {
 561                            thread
 562                                .update(cx, |thread, cx| {
 563                                    thread.push_assistant_content_block(
 564                                        format!("Unsupported content: {:?}", chunk).into(),
 565                                        false,
 566                                        cx,
 567                                    )
 568                                })
 569                                .log_err();
 570                        }
 571                    }
 572                }
 573            }
 574            SdkMessage::Result {
 575                is_error,
 576                subtype,
 577                result,
 578                ..
 579            } => {
 580                let turn_state = turn_state.take();
 581                let was_cancelled = turn_state.is_cancelled();
 582                let Some(end_turn_tx) = turn_state.end_tx() else {
 583                    debug_panic!("Received `SdkMessage::Result` but there wasn't an active turn");
 584                    return;
 585                };
 586
 587                if is_error || (!was_cancelled && subtype == ResultErrorType::ErrorDuringExecution)
 588                {
 589                    end_turn_tx
 590                        .send(Err(anyhow!(
 591                            "Error: {}",
 592                            result.unwrap_or_else(|| subtype.to_string())
 593                        )))
 594                        .ok();
 595                } else {
 596                    let stop_reason = match subtype {
 597                        ResultErrorType::Success => acp::StopReason::EndTurn,
 598                        ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests,
 599                        ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled,
 600                    };
 601                    end_turn_tx
 602                        .send(Ok(acp::PromptResponse { stop_reason }))
 603                        .ok();
 604                }
 605            }
 606            SdkMessage::ControlResponse { response } => {
 607                if matches!(response.subtype, ResultErrorType::Success) {
 608                    let new_state = turn_state.take().confirm_cancellation(&response.request_id);
 609                    turn_state.replace(new_state);
 610                } else {
 611                    log::error!("Control response error: {:?}", response);
 612                }
 613            }
 614            SdkMessage::System { .. } => {}
 615        }
 616    }
 617
 618    async fn handle_io(
 619        mut outgoing_rx: UnboundedReceiver<SdkMessage>,
 620        incoming_tx: UnboundedSender<SdkMessage>,
 621        mut outgoing_bytes: impl Unpin + AsyncWrite,
 622        incoming_bytes: impl Unpin + AsyncRead,
 623    ) -> Result<UnboundedReceiver<SdkMessage>> {
 624        let mut output_reader = BufReader::new(incoming_bytes);
 625        let mut outgoing_line = Vec::new();
 626        let mut incoming_line = String::new();
 627        loop {
 628            select_biased! {
 629                message = outgoing_rx.next() => {
 630                    if let Some(message) = message {
 631                        outgoing_line.clear();
 632                        serde_json::to_writer(&mut outgoing_line, &message)?;
 633                        log::trace!("send: {}", String::from_utf8_lossy(&outgoing_line));
 634                        outgoing_line.push(b'\n');
 635                        outgoing_bytes.write_all(&outgoing_line).await.ok();
 636                    } else {
 637                        break;
 638                    }
 639                }
 640                bytes_read = output_reader.read_line(&mut incoming_line).fuse() => {
 641                    if bytes_read? == 0 {
 642                        break
 643                    }
 644                    log::trace!("recv: {}", &incoming_line);
 645                    match serde_json::from_str::<SdkMessage>(&incoming_line) {
 646                        Ok(message) => {
 647                            incoming_tx.unbounded_send(message).log_err();
 648                        }
 649                        Err(error) => {
 650                            log::error!("failed to parse incoming message: {error}. Raw: {incoming_line}");
 651                        }
 652                    }
 653                    incoming_line.clear();
 654                }
 655            }
 656        }
 657
 658        Ok(outgoing_rx)
 659    }
 660}
 661
 662#[derive(Debug, Clone, Serialize, Deserialize)]
 663struct Message {
 664    role: Role,
 665    content: Content,
 666    #[serde(skip_serializing_if = "Option::is_none")]
 667    id: Option<String>,
 668    #[serde(skip_serializing_if = "Option::is_none")]
 669    model: Option<String>,
 670    #[serde(skip_serializing_if = "Option::is_none")]
 671    stop_reason: Option<String>,
 672    #[serde(skip_serializing_if = "Option::is_none")]
 673    stop_sequence: Option<String>,
 674    #[serde(skip_serializing_if = "Option::is_none")]
 675    usage: Option<Usage>,
 676}
 677
 678#[derive(Debug, Clone, Serialize, Deserialize)]
 679#[serde(untagged)]
 680enum Content {
 681    UntaggedText(String),
 682    Chunks(Vec<ContentChunk>),
 683}
 684
 685impl Content {
 686    pub fn chunks(self) -> impl Iterator<Item = ContentChunk> {
 687        match self {
 688            Self::Chunks(chunks) => chunks.into_iter(),
 689            Self::UntaggedText(text) => vec![ContentChunk::Text { text: text.clone() }].into_iter(),
 690        }
 691    }
 692}
 693
 694impl Display for Content {
 695    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 696        match self {
 697            Content::UntaggedText(txt) => write!(f, "{}", txt),
 698            Content::Chunks(chunks) => {
 699                for chunk in chunks {
 700                    write!(f, "{}", chunk)?;
 701                }
 702                Ok(())
 703            }
 704        }
 705    }
 706}
 707
 708#[derive(Debug, Clone, Serialize, Deserialize)]
 709#[serde(tag = "type", rename_all = "snake_case")]
 710enum ContentChunk {
 711    Text {
 712        text: String,
 713    },
 714    ToolUse {
 715        id: String,
 716        name: String,
 717        input: serde_json::Value,
 718    },
 719    ToolResult {
 720        content: Content,
 721        tool_use_id: String,
 722    },
 723    Thinking {
 724        thinking: String,
 725    },
 726    RedactedThinking,
 727    // TODO
 728    Image,
 729    Document,
 730    WebSearchToolResult,
 731    #[serde(untagged)]
 732    UntaggedText(String),
 733}
 734
 735impl Display for ContentChunk {
 736    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 737        match self {
 738            ContentChunk::Text { text } => write!(f, "{}", text),
 739            ContentChunk::Thinking { thinking } => write!(f, "Thinking: {}", thinking),
 740            ContentChunk::RedactedThinking => write!(f, "Thinking: [REDACTED]"),
 741            ContentChunk::UntaggedText(text) => write!(f, "{}", text),
 742            ContentChunk::ToolResult { content, .. } => write!(f, "{}", content),
 743            ContentChunk::Image
 744            | ContentChunk::Document
 745            | ContentChunk::ToolUse { .. }
 746            | ContentChunk::WebSearchToolResult => {
 747                write!(f, "\n{:?}\n", &self)
 748            }
 749        }
 750    }
 751}
 752
 753#[derive(Debug, Clone, Serialize, Deserialize)]
 754struct Usage {
 755    input_tokens: u32,
 756    cache_creation_input_tokens: u32,
 757    cache_read_input_tokens: u32,
 758    output_tokens: u32,
 759    service_tier: String,
 760}
 761
 762#[derive(Debug, Clone, Serialize, Deserialize)]
 763#[serde(rename_all = "snake_case")]
 764enum Role {
 765    System,
 766    Assistant,
 767    User,
 768}
 769
 770#[derive(Debug, Clone, Serialize, Deserialize)]
 771struct MessageParam {
 772    role: Role,
 773    content: String,
 774}
 775
 776#[derive(Debug, Clone, Serialize, Deserialize)]
 777#[serde(tag = "type", rename_all = "snake_case")]
 778enum SdkMessage {
 779    // An assistant message
 780    Assistant {
 781        message: Message, // from Anthropic SDK
 782        #[serde(skip_serializing_if = "Option::is_none")]
 783        session_id: Option<String>,
 784    },
 785    // A user message
 786    User {
 787        message: Message, // from Anthropic SDK
 788        #[serde(skip_serializing_if = "Option::is_none")]
 789        session_id: Option<String>,
 790    },
 791    // Emitted as the last message in a conversation
 792    Result {
 793        subtype: ResultErrorType,
 794        duration_ms: f64,
 795        duration_api_ms: f64,
 796        is_error: bool,
 797        num_turns: i32,
 798        #[serde(skip_serializing_if = "Option::is_none")]
 799        result: Option<String>,
 800        session_id: String,
 801        total_cost_usd: f64,
 802    },
 803    // Emitted as the first message at the start of a conversation
 804    System {
 805        cwd: String,
 806        session_id: String,
 807        tools: Vec<String>,
 808        model: String,
 809        mcp_servers: Vec<McpServer>,
 810        #[serde(rename = "apiKeySource")]
 811        api_key_source: String,
 812        #[serde(rename = "permissionMode")]
 813        permission_mode: PermissionMode,
 814    },
 815    /// Messages used to control the conversation, outside of chat messages to the model
 816    ControlRequest {
 817        request_id: String,
 818        request: ControlRequest,
 819    },
 820    /// Response to a control request
 821    ControlResponse { response: ControlResponse },
 822}
 823
 824#[derive(Debug, Clone, Serialize, Deserialize)]
 825#[serde(tag = "subtype", rename_all = "snake_case")]
 826enum ControlRequest {
 827    /// Cancel the current conversation
 828    Interrupt,
 829}
 830
 831#[derive(Debug, Clone, Serialize, Deserialize)]
 832struct ControlResponse {
 833    request_id: String,
 834    subtype: ResultErrorType,
 835}
 836
 837#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
 838#[serde(rename_all = "snake_case")]
 839enum ResultErrorType {
 840    Success,
 841    ErrorMaxTurns,
 842    ErrorDuringExecution,
 843}
 844
 845impl Display for ResultErrorType {
 846    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 847        match self {
 848            ResultErrorType::Success => write!(f, "success"),
 849            ResultErrorType::ErrorMaxTurns => write!(f, "error_max_turns"),
 850            ResultErrorType::ErrorDuringExecution => write!(f, "error_during_execution"),
 851        }
 852    }
 853}
 854
 855fn new_request_id() -> String {
 856    use rand::Rng;
 857    // In the Claude Code TS SDK they just generate a random 12 character string,
 858    // `Math.random().toString(36).substring(2, 15)`
 859    rand::thread_rng()
 860        .sample_iter(&rand::distributions::Alphanumeric)
 861        .take(12)
 862        .map(char::from)
 863        .collect()
 864}
 865
 866#[derive(Debug, Clone, Serialize, Deserialize)]
 867struct McpServer {
 868    name: String,
 869    status: String,
 870}
 871
 872#[derive(Debug, Clone, Serialize, Deserialize)]
 873#[serde(rename_all = "camelCase")]
 874enum PermissionMode {
 875    Default,
 876    AcceptEdits,
 877    BypassPermissions,
 878    Plan,
 879}
 880
 881#[cfg(test)]
 882pub(crate) mod tests {
 883    use super::*;
 884    use crate::e2e_tests;
 885    use gpui::TestAppContext;
 886    use serde_json::json;
 887
 888    crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow");
 889
 890    pub fn local_command() -> AgentServerCommand {
 891        AgentServerCommand {
 892            path: "claude".into(),
 893            args: vec![],
 894            env: None,
 895        }
 896    }
 897
 898    #[gpui::test]
 899    #[cfg_attr(not(feature = "e2e"), ignore)]
 900    async fn test_todo_plan(cx: &mut TestAppContext) {
 901        let fs = e2e_tests::init_test(cx).await;
 902        let project = Project::test(fs, [], cx).await;
 903        let thread =
 904            e2e_tests::new_test_thread(ClaudeCode, project.clone(), "/private/tmp", cx).await;
 905
 906        thread
 907            .update(cx, |thread, cx| {
 908                thread.send_raw(
 909                    "Create a todo plan for initializing a new React app. I'll follow it myself, do not execute on it.",
 910                    cx,
 911                )
 912            })
 913            .await
 914            .unwrap();
 915
 916        let mut entries_len = 0;
 917
 918        thread.read_with(cx, |thread, _| {
 919            entries_len = thread.plan().entries.len();
 920            assert!(thread.plan().entries.len() > 0, "Empty plan");
 921        });
 922
 923        thread
 924            .update(cx, |thread, cx| {
 925                thread.send_raw(
 926                    "Mark the first entry status as in progress without acting on it.",
 927                    cx,
 928                )
 929            })
 930            .await
 931            .unwrap();
 932
 933        thread.read_with(cx, |thread, _| {
 934            assert!(matches!(
 935                thread.plan().entries[0].status,
 936                acp::PlanEntryStatus::InProgress
 937            ));
 938            assert_eq!(thread.plan().entries.len(), entries_len);
 939        });
 940
 941        thread
 942            .update(cx, |thread, cx| {
 943                thread.send_raw(
 944                    "Now mark the first entry as completed without acting on it.",
 945                    cx,
 946                )
 947            })
 948            .await
 949            .unwrap();
 950
 951        thread.read_with(cx, |thread, _| {
 952            assert!(matches!(
 953                thread.plan().entries[0].status,
 954                acp::PlanEntryStatus::Completed
 955            ));
 956            assert_eq!(thread.plan().entries.len(), entries_len);
 957        });
 958    }
 959
 960    #[test]
 961    fn test_deserialize_content_untagged_text() {
 962        let json = json!("Hello, world!");
 963        let content: Content = serde_json::from_value(json).unwrap();
 964        match content {
 965            Content::UntaggedText(text) => assert_eq!(text, "Hello, world!"),
 966            _ => panic!("Expected UntaggedText variant"),
 967        }
 968    }
 969
 970    #[test]
 971    fn test_deserialize_content_chunks() {
 972        let json = json!([
 973            {
 974                "type": "text",
 975                "text": "Hello"
 976            },
 977            {
 978                "type": "tool_use",
 979                "id": "tool_123",
 980                "name": "calculator",
 981                "input": {"operation": "add", "a": 1, "b": 2}
 982            }
 983        ]);
 984        let content: Content = serde_json::from_value(json).unwrap();
 985        match content {
 986            Content::Chunks(chunks) => {
 987                assert_eq!(chunks.len(), 2);
 988                match &chunks[0] {
 989                    ContentChunk::Text { text } => assert_eq!(text, "Hello"),
 990                    _ => panic!("Expected Text chunk"),
 991                }
 992                match &chunks[1] {
 993                    ContentChunk::ToolUse { id, name, input } => {
 994                        assert_eq!(id, "tool_123");
 995                        assert_eq!(name, "calculator");
 996                        assert_eq!(input["operation"], "add");
 997                        assert_eq!(input["a"], 1);
 998                        assert_eq!(input["b"], 2);
 999                    }
1000                    _ => panic!("Expected ToolUse chunk"),
1001                }
1002            }
1003            _ => panic!("Expected Chunks variant"),
1004        }
1005    }
1006
1007    #[test]
1008    fn test_deserialize_tool_result_untagged_text() {
1009        let json = json!({
1010            "type": "tool_result",
1011            "content": "Result content",
1012            "tool_use_id": "tool_456"
1013        });
1014        let chunk: ContentChunk = serde_json::from_value(json).unwrap();
1015        match chunk {
1016            ContentChunk::ToolResult {
1017                content,
1018                tool_use_id,
1019            } => {
1020                match content {
1021                    Content::UntaggedText(text) => assert_eq!(text, "Result content"),
1022                    _ => panic!("Expected UntaggedText content"),
1023                }
1024                assert_eq!(tool_use_id, "tool_456");
1025            }
1026            _ => panic!("Expected ToolResult variant"),
1027        }
1028    }
1029
1030    #[test]
1031    fn test_deserialize_tool_result_chunks() {
1032        let json = json!({
1033            "type": "tool_result",
1034            "content": [
1035                {
1036                    "type": "text",
1037                    "text": "Processing complete"
1038                },
1039                {
1040                    "type": "text",
1041                    "text": "Result: 42"
1042                }
1043            ],
1044            "tool_use_id": "tool_789"
1045        });
1046        let chunk: ContentChunk = serde_json::from_value(json).unwrap();
1047        match chunk {
1048            ContentChunk::ToolResult {
1049                content,
1050                tool_use_id,
1051            } => {
1052                match content {
1053                    Content::Chunks(chunks) => {
1054                        assert_eq!(chunks.len(), 2);
1055                        match &chunks[0] {
1056                            ContentChunk::Text { text } => assert_eq!(text, "Processing complete"),
1057                            _ => panic!("Expected Text chunk"),
1058                        }
1059                        match &chunks[1] {
1060                            ContentChunk::Text { text } => assert_eq!(text, "Result: 42"),
1061                            _ => panic!("Expected Text chunk"),
1062                        }
1063                    }
1064                    _ => panic!("Expected Chunks content"),
1065                }
1066                assert_eq!(tool_use_id, "tool_789");
1067            }
1068            _ => panic!("Expected ToolResult variant"),
1069        }
1070    }
1071}