Add support for Claude Code auth (#37103)

Co-authored-by: Antonio Scandurra <me@as-cii.com>

Closes #ISSUE

Release Notes:

- N/A

Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
Conrad Irwin
2025-08-28 12:59:31 -06:00
committed by GitHub
co-authored by Antonio Scandurra
parent 909d7215c0
commit 69933d5b81
3 changed files with 136 additions and 6 deletions
+14 -1
View File
@@ -2,6 +2,7 @@ use crate::{AgentServerCommand, AgentServerSettings};
use acp_thread::AgentConnection;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
use project::Project;
use std::{path::Path, rc::Rc};
use ui::IconName;
@@ -49,10 +50,22 @@ impl crate::AgentServer for CustomAgentServer {
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let server_name = self.name();
let command = self.command.clone();
let mut command = self.command.clone();
let root_dir = root_dir.to_path_buf();
// TODO: Remove this once we have Claude properly
cx.spawn(async move |mut cx| {
if let Some(api_key) = cx
.update(AnthropicLanguageModelProvider::api_key)?
.await
.ok()
{
command
.env
.get_or_insert_default()
.insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
}
crate::acp::connect(server_name, command, &root_dir, &mut cx).await
})
}
+121 -4
View File
@@ -9,7 +9,7 @@ use agent_client_protocol::{self as acp, PromptCapabilities};
use agent_servers::{AgentServer, ClaudeCode};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore};
use anyhow::bail;
use anyhow::{Result, anyhow, bail};
use audio::{Audio, Sound};
use buffer_diff::BufferDiff;
use client::zed_urls;
@@ -18,6 +18,7 @@ use editor::scroll::Autoscroll;
use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects};
use file_icons::FileIcons;
use fs::Fs;
use futures::FutureExt as _;
use gpui::{
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
@@ -39,6 +40,8 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use std::{collections::BTreeMap, rc::Rc, time::Duration};
use task::SpawnInTerminal;
use terminal_view::terminal_panel::TerminalPanel;
use text::Anchor;
use theme::ThemeSettings;
use ui::{
@@ -93,6 +96,10 @@ impl ThreadError {
error.downcast_ref::<language_model::ModelRequestLimitReachedError>()
{
Self::ModelRequestLimitReached(error.plan)
} else if let Some(acp_error) = error.downcast_ref::<acp::Error>()
&& acp_error.code == acp::ErrorCode::AUTH_REQUIRED.code
{
Self::AuthenticationRequired(acp_error.message.clone().into())
} else {
let string = error.to_string();
// TODO: we should have Gemini return better errors here.
@@ -898,7 +905,7 @@ impl AcpThreadView {
fn send_impl(
&mut self,
contents: Task<anyhow::Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>,
contents: Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1234,6 +1241,31 @@ impl AcpThreadView {
});
return;
}
} else if method.0.as_ref() == "anthropic-api-key" {
let registry = LanguageModelRegistry::global(cx);
let provider = registry
.read(cx)
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
.unwrap();
if !provider.is_authenticated(cx) {
let this = cx.weak_entity();
let agent = self.agent.clone();
let connection = connection.clone();
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired {
description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
},
agent,
connection,
window,
cx,
);
});
return;
}
} else if method.0.as_ref() == "vertex-ai"
&& std::env::var("GOOGLE_API_KEY").is_err()
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
@@ -1265,7 +1297,15 @@ impl AcpThreadView {
self.thread_error.take();
configuration_view.take();
pending_auth_method.replace(method.clone());
let authenticate = connection.authenticate(method, cx);
let authenticate = if method.0.as_ref() == "claude-login" {
if let Some(workspace) = self.workspace.upgrade() {
Self::spawn_claude_login(&workspace, window, cx)
} else {
Task::ready(Ok(()))
}
} else {
connection.authenticate(method, cx)
};
cx.notify();
self.auth_task =
Some(cx.spawn_in(window, {
@@ -1289,6 +1329,13 @@ impl AcpThreadView {
this.update_in(cx, |this, window, cx| {
if let Err(err) = result {
if let ThreadState::Unauthenticated {
pending_auth_method,
..
} = &mut this.thread_state
{
pending_auth_method.take();
}
this.handle_thread_error(err, cx);
} else {
this.thread_state = Self::initial_state(
@@ -1307,6 +1354,76 @@ impl AcpThreadView {
}));
}
fn spawn_claude_login(
workspace: &Entity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>> {
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return Task::ready(Ok(()));
};
let project = workspace.read(cx).project().read(cx);
let cwd = project.first_project_directory(cx);
let shell = project.terminal_settings(&cwd, cx).shell.clone();
let terminal = terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.spawn_task(
&SpawnInTerminal {
id: task::TaskId("claude-login".into()),
full_label: "claude /login".to_owned(),
label: "claude /login".to_owned(),
command: Some("claude".to_owned()),
args: vec!["/login".to_owned()],
command_label: "claude /login".to_owned(),
cwd,
use_new_terminal: true,
allow_concurrent_runs: true,
hide: task::HideStrategy::Always,
shell,
..Default::default()
},
window,
cx,
)
});
cx.spawn(async move |cx| {
let terminal = terminal.await?;
let mut exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.fuse();
let logged_in = cx
.spawn({
let terminal = terminal.clone();
async move |cx| {
loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
let content =
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
if content.contains("Login successful") {
return anyhow::Ok(());
}
}
}
})
.fuse();
futures::pin_mut!(logged_in);
futures::select_biased! {
result = logged_in => {
if let Err(e) = result {
log::error!("{e}");
return Err(anyhow!("exited before logging in"));
}
}
_ = exit_status => {
return Err(anyhow!("exited before logging in"));
}
}
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
Ok(())
})
}
fn authorize_tool_call(
&mut self,
tool_call_id: acp::ToolCallId,
@@ -4024,7 +4141,7 @@ impl AcpThreadView {
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<anyhow::Result<()>> {
) -> Task<Result<()>> {
let markdown_language_task = workspace
.read(cx)
.app_state()
+1 -1
View File
@@ -485,7 +485,7 @@ impl TerminalPanel {
.detach_and_log_err(cx);
}
fn spawn_task(
pub fn spawn_task(
&mut self,
task: &SpawnInTerminal,
window: &mut Window,