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