feat - updating icons and glyphs, added claude code subscription integration
This commit is contained in:
@@ -163,6 +163,7 @@ pub enum AISubpage {
|
||||
ProviderLiteLLM,
|
||||
/// ChatGPT subscription provider settings.
|
||||
ProviderChatGPTSubscription,
|
||||
ProviderAnthropic,
|
||||
/// AWS Bedrock provider settings.
|
||||
ProviderBedrock,
|
||||
/// ACP provider settings.
|
||||
@@ -185,6 +186,7 @@ impl AISubpage {
|
||||
SettingsSection::ProviderOpenAI => Some(Self::ProviderOpenAI),
|
||||
SettingsSection::ProviderLiteLLM => Some(Self::ProviderLiteLLM),
|
||||
SettingsSection::ProviderChatGPTSubscription => Some(Self::ProviderChatGPTSubscription),
|
||||
SettingsSection::ProviderAnthropic => Some(Self::ProviderAnthropic),
|
||||
SettingsSection::ProviderBedrock => Some(Self::ProviderBedrock),
|
||||
SettingsSection::ProviderACP => Some(Self::ProviderACP),
|
||||
// AgentMCPServers renders the standalone MCPServers page, not an AI subpage.
|
||||
@@ -676,6 +678,83 @@ impl AISettingsPageView {
|
||||
&mut self,
|
||||
draft: &AcpProviderDraft,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let generation = self
|
||||
.provider_setup_body
|
||||
.read(ctx, |body, _| body.acp_discovery_generation());
|
||||
#[cfg(feature = "local_fs")]
|
||||
if draft.agent_id.eq_ignore_ascii_case("claude-acp") && draft.command.trim().is_empty() {
|
||||
let (progress, updates) = async_channel::bounded(8);
|
||||
self.watch_claude_setup_progress(updates, generation, ctx);
|
||||
let draft = draft.clone();
|
||||
let registration = self.provider_setup_body.update(ctx, |body, ctx| {
|
||||
body.set_acp_setup_status("Checking Claude Code installation…".to_owned(), ctx);
|
||||
body.prepare_acp_setup()
|
||||
});
|
||||
let _ = ctx.spawn(
|
||||
futures::future::Abortable::new(
|
||||
async move { crate::ai::acp::ensure_claude_code_installed(progress).await },
|
||||
registration,
|
||||
),
|
||||
move |me, result, ctx| {
|
||||
if !me
|
||||
.provider_setup_body
|
||||
.read(ctx, |body, _| body.acp_discovery_is_current(generation))
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Ok(result) = result else {
|
||||
return;
|
||||
};
|
||||
match result {
|
||||
Ok(()) => me.resolve_and_start_acp_discovery(&draft, generation, ctx),
|
||||
Err(error) => {
|
||||
log::warn!("Claude Code setup failed: {error}");
|
||||
me.provider_setup_body.update(ctx, |body, ctx| {
|
||||
body.finish_acp_discovery(Err(error), Vec::new(), ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.resolve_and_start_acp_discovery(draft, generation, ctx);
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn watch_claude_setup_progress(
|
||||
&mut self,
|
||||
updates: async_channel::Receiver<&'static str>,
|
||||
generation: u64,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let remaining_updates = updates.clone();
|
||||
let _ = ctx.spawn(
|
||||
async move { updates.recv().await },
|
||||
move |me, update, ctx| {
|
||||
if !me
|
||||
.provider_setup_body
|
||||
.read(ctx, |body, _| body.acp_discovery_is_current(generation))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Ok(update) = update {
|
||||
me.provider_setup_body.update(ctx, |body, ctx| {
|
||||
body.set_acp_setup_status(update.to_owned(), ctx);
|
||||
});
|
||||
me.watch_claude_setup_progress(remaining_updates, generation, ctx);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn resolve_and_start_acp_discovery(
|
||||
&mut self,
|
||||
draft: &AcpProviderDraft,
|
||||
generation: u64,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let config = match crate::ai::acp::AcpRuntimeModel::discovery_config_for_values(
|
||||
&draft.agent_id,
|
||||
@@ -691,7 +770,7 @@ impl AISettingsPageView {
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.start_acp_discovery(config, draft.agent_id.clone(), ctx);
|
||||
self.start_acp_discovery(config, draft.agent_id.clone(), generation, ctx);
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
@@ -699,6 +778,7 @@ impl AISettingsPageView {
|
||||
&mut self,
|
||||
config: galaxy_acp::AcpManagerConfig,
|
||||
agent_id: String,
|
||||
generation: u64,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let provider_setup_body = self.provider_setup_body.clone();
|
||||
@@ -724,6 +804,11 @@ impl AISettingsPageView {
|
||||
let _ = ctx.spawn(
|
||||
async move { manager.discover_config_options(cwd, Vec::new()).await },
|
||||
move |_me, result, ctx| {
|
||||
if !provider_setup_body
|
||||
.read(ctx, |body, _| body.acp_discovery_is_current(generation))
|
||||
{
|
||||
return;
|
||||
}
|
||||
match result {
|
||||
Ok(options) => {
|
||||
let option_count = options.len();
|
||||
@@ -1710,19 +1795,64 @@ impl AISettingsPageView {
|
||||
mut provider: OpenAIProviderConfig,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
let new_reference =
|
||||
match crate::ai::claude_subscription::store_credential(&mut provider, ctx) {
|
||||
Ok(reference) => reference,
|
||||
Err(error) => {
|
||||
self.provider_setup_body
|
||||
.update(ctx, |body, ctx| body.show_connection_error(error, ctx));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let old_reference = editing_index.and_then(|index| {
|
||||
AISettings::as_ref(ctx)
|
||||
.openai_providers
|
||||
.value()
|
||||
.get(index)
|
||||
.and_then(|provider| provider.api_key.clone())
|
||||
});
|
||||
let saved = AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
let mut providers = settings.openai_providers.value().clone();
|
||||
match editing_index {
|
||||
Some(index) => {
|
||||
if let Some(existing) = providers.get_mut(index) {
|
||||
provider.enabled = existing.enabled;
|
||||
*existing = provider.clone();
|
||||
}
|
||||
let Some(existing) = providers.get_mut(index) else {
|
||||
return false;
|
||||
};
|
||||
provider.enabled = existing.enabled;
|
||||
*existing = provider.clone();
|
||||
}
|
||||
None => providers.push(provider.clone()),
|
||||
}
|
||||
report_if_error!(settings.openai_providers.set_value(providers, ctx));
|
||||
match settings.openai_providers.set_value(providers, ctx) {
|
||||
Ok(_) => true,
|
||||
Err(error) => {
|
||||
report_error!(error);
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
if !saved {
|
||||
if let Some(reference) = new_reference {
|
||||
crate::ai::claude_subscription::remove_credential(&reference, ctx);
|
||||
}
|
||||
self.provider_setup_body.update(ctx, |body, ctx| {
|
||||
body.show_connection_error(
|
||||
"Could not save the provider connection.".to_string(),
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if let Some(reference) = old_reference {
|
||||
if !AISettings::as_ref(ctx)
|
||||
.openai_providers
|
||||
.value()
|
||||
.iter()
|
||||
.any(|provider| provider.api_key.as_deref() == Some(reference.as_str()))
|
||||
{
|
||||
crate::ai::claude_subscription::remove_credential(&reference, ctx);
|
||||
}
|
||||
}
|
||||
self.clear_inline_provider_setup(ctx);
|
||||
}
|
||||
|
||||
@@ -1929,6 +2059,12 @@ impl AISettingsPageView {
|
||||
ProviderSetupProviderType::ChatGPTSubscription,
|
||||
)));
|
||||
}
|
||||
Some(AISubpage::ProviderAnthropic) => {
|
||||
widgets.push(Box::new(ProviderSettingsWidget::new(
|
||||
ctx,
|
||||
ProviderSetupProviderType::Anthropic,
|
||||
)));
|
||||
}
|
||||
Some(AISubpage::ProviderBedrock) => {
|
||||
widgets.push(Box::new(ProviderSettingsWidget::new(
|
||||
ctx,
|
||||
@@ -3297,8 +3433,22 @@ impl TypedActionView for AISettingsPageView {
|
||||
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
let mut providers = settings.openai_providers.value().clone();
|
||||
if *provider_index < providers.len() {
|
||||
providers.remove(*provider_index);
|
||||
report_if_error!(settings.openai_providers.set_value(providers, ctx));
|
||||
let removed = providers.remove(*provider_index);
|
||||
let remaining = providers.clone();
|
||||
match settings.openai_providers.set_value(providers, ctx) {
|
||||
Ok(_) => {
|
||||
if let Some(reference) = removed.api_key {
|
||||
if !remaining.iter().any(|provider| {
|
||||
provider.api_key.as_deref() == Some(reference.as_str())
|
||||
}) {
|
||||
crate::ai::claude_subscription::remove_credential(
|
||||
&reference, ctx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => report_error!(error),
|
||||
}
|
||||
}
|
||||
});
|
||||
self.rebuild_active_subpage(ctx);
|
||||
@@ -3337,7 +3487,9 @@ impl TypedActionView for AISettingsPageView {
|
||||
let mut providers = settings.acp_providers.value().clone();
|
||||
if providers.is_empty() && provider_id == "legacy" {
|
||||
report_if_error!(settings.acp_enabled.set_value(false, ctx));
|
||||
report_if_error!(settings.acp_agent_id.set_value("codex".to_string(), ctx));
|
||||
report_if_error!(settings
|
||||
.acp_agent_id
|
||||
.set_value("claude-acp".to_string(), ctx));
|
||||
report_if_error!(settings.acp_agent_command.set_value(String::new(), ctx));
|
||||
report_if_error!(settings.acp_agent_args.set_value(Vec::new(), ctx));
|
||||
report_if_error!(settings
|
||||
@@ -3366,7 +3518,9 @@ impl TypedActionView for AISettingsPageView {
|
||||
.set_value(first_provider.name.clone(), ctx));
|
||||
} else {
|
||||
report_if_error!(settings.acp_enabled.set_value(false, ctx));
|
||||
report_if_error!(settings.acp_agent_id.set_value("codex".to_string(), ctx));
|
||||
report_if_error!(settings
|
||||
.acp_agent_id
|
||||
.set_value("claude-acp".to_string(), ctx));
|
||||
report_if_error!(settings.acp_agent_command.set_value(String::new(), ctx));
|
||||
report_if_error!(settings.acp_agent_args.set_value(Vec::new(), ctx));
|
||||
report_if_error!(settings
|
||||
@@ -6372,7 +6526,7 @@ impl SettingsWidget for ModelsOverviewWidget {
|
||||
type View = AISettingsPageView;
|
||||
|
||||
fn search_terms(&self) -> &str {
|
||||
"models providers runtimes rig litellm openai compatible ollama lm studio bedrock acp agent client protocol"
|
||||
"models providers runtimes rig litellm openai compatible ollama lm studio bedrock acp agent client protocol anthropic claude subscription token"
|
||||
}
|
||||
|
||||
fn render(
|
||||
@@ -6390,6 +6544,12 @@ impl SettingsWidget for ModelsOverviewWidget {
|
||||
ModelProviderSection::for_provider(provider) == ModelProviderSection::OpenAI
|
||||
})
|
||||
.count();
|
||||
let anthropic_count = settings
|
||||
.openai_providers
|
||||
.value()
|
||||
.iter()
|
||||
.filter(|provider| provider.kind == OpenAIProviderKind::Anthropic)
|
||||
.count();
|
||||
let litellm_count = settings
|
||||
.openai_providers
|
||||
.value()
|
||||
@@ -6420,13 +6580,13 @@ impl SettingsWidget for ModelsOverviewWidget {
|
||||
.with_spacing(8.)
|
||||
.with_child(build_sub_header(appearance, "Models", None).finish())
|
||||
.with_child(render_ai_setting_description(
|
||||
"Configure Galaxy's direct model providers and agent runtimes in one place. OpenAI, LiteLLM, ChatGPT subscription, and Bedrock connections are configured here. ACP coding agents are shown read-only because the agent runtime controls their model, login, session, and tool loop.",
|
||||
"Configure Galaxy's direct model providers and agent runtimes in one place. OpenAI, LiteLLM, ChatGPT subscription, Anthropic / Claude subscription, and Bedrock connections are configured here. ACP coding agents are shown read-only because the agent runtime controls their model, login, session, and tool loop.",
|
||||
true,
|
||||
app,
|
||||
))
|
||||
.with_child(render_ai_setting_description(
|
||||
format!(
|
||||
"{openai_count} OpenAI provider(s); {litellm_count} LiteLLM provider(s); {chatgpt_count} ChatGPT subscription provider(s); {endpoint_model_count} direct model(s); {bedrock_model_count} Bedrock model(s); {agent_runtime_count} enabled ACP runtime(s)."
|
||||
"{openai_count} OpenAI provider(s); {litellm_count} LiteLLM provider(s); {chatgpt_count} ChatGPT subscription provider(s); {anthropic_count} Anthropic / Claude subscription provider(s); {endpoint_model_count} direct model(s); {bedrock_model_count} Bedrock model(s); {agent_runtime_count} enabled ACP runtime(s)."
|
||||
),
|
||||
true,
|
||||
app,
|
||||
@@ -6437,6 +6597,7 @@ impl SettingsWidget for ModelsOverviewWidget {
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ModelProviderSection {
|
||||
Anthropic,
|
||||
OpenAI,
|
||||
LiteLLM,
|
||||
ChatGPTSubscription,
|
||||
@@ -6445,8 +6606,8 @@ enum ModelProviderSection {
|
||||
impl ModelProviderSection {
|
||||
fn for_provider(provider: &OpenAIProviderConfig) -> Self {
|
||||
match provider.kind {
|
||||
OpenAIProviderKind::Anthropic => Self::Anthropic,
|
||||
OpenAIProviderKind::OpenAI
|
||||
| OpenAIProviderKind::Anthropic
|
||||
| OpenAIProviderKind::Gemini
|
||||
| OpenAIProviderKind::VertexAI => Self::OpenAI,
|
||||
OpenAIProviderKind::LiteLLM => Self::LiteLLM,
|
||||
@@ -6472,6 +6633,7 @@ struct ProviderSettingsWidget {
|
||||
bedrock_enabled_toggle: SwitchStateHandle,
|
||||
bedrock_model_switches: RefCell<HashMap<String, SwitchStateHandle>>,
|
||||
bedrock_model_table_state: TableStateHandle,
|
||||
add_anthropic_provider_button: ViewHandle<ActionButton>,
|
||||
add_openai_provider_button: ViewHandle<ActionButton>,
|
||||
add_litellm_provider_button: ViewHandle<ActionButton>,
|
||||
add_chatgpt_provider_button: ViewHandle<ActionButton>,
|
||||
@@ -6529,6 +6691,15 @@ impl ProviderSettingsWidget {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let add_anthropic_provider_button = ctx.add_typed_action_view(|_| {
|
||||
ActionButton::new("Add provider", SecondaryTheme)
|
||||
.with_icon(Icon::Plus)
|
||||
.on_click(|ctx| {
|
||||
ctx.dispatch_typed_action(AISettingsPageAction::AddOpenAIProvider(
|
||||
ProviderSetupProviderType::Anthropic,
|
||||
));
|
||||
})
|
||||
});
|
||||
let add_openai_provider_button = ctx.add_typed_action_view(|_| {
|
||||
ActionButton::new("Add provider", SecondaryTheme)
|
||||
.with_icon(Icon::Plus)
|
||||
@@ -6587,6 +6758,7 @@ impl ProviderSettingsWidget {
|
||||
bedrock_enabled_toggle: SwitchStateHandle::default(),
|
||||
bedrock_model_switches: RefCell::new(HashMap::new()),
|
||||
bedrock_model_table_state: TableStateHandle::new(0, |_, _| Vec::new()),
|
||||
add_anthropic_provider_button,
|
||||
add_openai_provider_button,
|
||||
add_litellm_provider_button,
|
||||
add_chatgpt_provider_button,
|
||||
@@ -6620,7 +6792,17 @@ impl ProviderSettingsWidget {
|
||||
OpenAIProviderKind::OpenAI => "OpenAI",
|
||||
OpenAIProviderKind::LiteLLM => "LiteLLM",
|
||||
OpenAIProviderKind::ChatGPTSubscription => "ChatGPT subscription",
|
||||
OpenAIProviderKind::Anthropic => "Anthropic",
|
||||
OpenAIProviderKind::Anthropic => {
|
||||
if provider
|
||||
.api_key
|
||||
.as_deref()
|
||||
.is_some_and(crate::ai::claude_subscription::is_subscription_credential)
|
||||
{
|
||||
"Claude subscription (unsupported)"
|
||||
} else {
|
||||
"Anthropic API"
|
||||
}
|
||||
}
|
||||
OpenAIProviderKind::Gemini => "Google Gemini",
|
||||
OpenAIProviderKind::VertexAI => "Google Vertex AI",
|
||||
}
|
||||
@@ -7212,8 +7394,8 @@ impl ProviderSettingsWidget {
|
||||
ProviderSetupProviderType::ChatGPTSubscription => {
|
||||
Some(ModelProviderSection::ChatGPTSubscription)
|
||||
}
|
||||
ProviderSetupProviderType::Anthropic
|
||||
| ProviderSetupProviderType::Gemini
|
||||
ProviderSetupProviderType::Anthropic => Some(ModelProviderSection::Anthropic),
|
||||
ProviderSetupProviderType::Gemini
|
||||
| ProviderSetupProviderType::VertexAI
|
||||
| ProviderSetupProviderType::Bedrock
|
||||
| ProviderSetupProviderType::Acp => None,
|
||||
@@ -7226,8 +7408,8 @@ impl ProviderSettingsWidget {
|
||||
ProviderSetupProviderType::LiteLLM => "LiteLLM",
|
||||
ProviderSetupProviderType::ChatGPTSubscription => "ChatGPT Subscription",
|
||||
ProviderSetupProviderType::Bedrock => "Bedrock",
|
||||
ProviderSetupProviderType::Acp => "ACP",
|
||||
ProviderSetupProviderType::Anthropic => "Anthropic",
|
||||
ProviderSetupProviderType::Acp => "Claude Code",
|
||||
ProviderSetupProviderType::Anthropic => "Anthropic / Claude subscription",
|
||||
ProviderSetupProviderType::Gemini => "Google Gemini",
|
||||
ProviderSetupProviderType::VertexAI => "Google Vertex AI",
|
||||
}
|
||||
@@ -7248,9 +7430,9 @@ impl ProviderSettingsWidget {
|
||||
"Use AWS Bedrock credentials and discovered Bedrock foundation models."
|
||||
}
|
||||
ProviderSetupProviderType::Acp => {
|
||||
"Add an ACP agent connection here. After it is added, Settings shows it read-only because the agent controls its model, login, session, and tool loop."
|
||||
"Connect Claude Code with your Claude account. Setup downloads missing components and opens sign-in in your browser."
|
||||
}
|
||||
ProviderSetupProviderType::Anthropic => "Connect directly to Anthropic.",
|
||||
ProviderSetupProviderType::Anthropic => crate::ai::claude_subscription::SUBSCRIPTION_WARNING,
|
||||
ProviderSetupProviderType::Gemini => "Connect directly to Google Gemini.",
|
||||
ProviderSetupProviderType::VertexAI => "Connect directly to Google Vertex AI.",
|
||||
}
|
||||
@@ -7264,7 +7446,7 @@ impl ProviderSettingsWidget {
|
||||
"No ChatGPT subscription provider configured."
|
||||
}
|
||||
ProviderSetupProviderType::Bedrock => "No Bedrock provider configured.",
|
||||
ProviderSetupProviderType::Acp => "No ACP agent connection configured.",
|
||||
ProviderSetupProviderType::Acp => "No Claude Code connection configured.",
|
||||
ProviderSetupProviderType::Anthropic => "No Anthropic provider configured.",
|
||||
ProviderSetupProviderType::Gemini => "No Google Gemini provider configured.",
|
||||
ProviderSetupProviderType::VertexAI => "No Google Vertex AI provider configured.",
|
||||
@@ -7283,9 +7465,8 @@ impl ProviderSettingsWidget {
|
||||
}
|
||||
ProviderSetupProviderType::Bedrock => Some(&self.bedrock_add_button),
|
||||
ProviderSetupProviderType::Acp => Some(&self.acp_add_button),
|
||||
ProviderSetupProviderType::Anthropic
|
||||
| ProviderSetupProviderType::Gemini
|
||||
| ProviderSetupProviderType::VertexAI => None,
|
||||
ProviderSetupProviderType::Anthropic => Some(&self.add_anthropic_provider_button),
|
||||
ProviderSetupProviderType::Gemini | ProviderSetupProviderType::VertexAI => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7369,7 +7550,8 @@ impl SettingsWidget for ProviderSettingsWidget {
|
||||
match self.provider_type {
|
||||
ProviderSetupProviderType::OpenAI
|
||||
| ProviderSetupProviderType::LiteLLM
|
||||
| ProviderSetupProviderType::ChatGPTSubscription => {
|
||||
| ProviderSetupProviderType::ChatGPTSubscription
|
||||
| ProviderSetupProviderType::Anthropic => {
|
||||
if is_setup_visible {
|
||||
column.add_child(Self::render_inline_setup(appearance, view));
|
||||
}
|
||||
@@ -7462,22 +7644,20 @@ impl SettingsWidget for ProviderSettingsWidget {
|
||||
None
|
||||
};
|
||||
let empty_message = if acp_supported {
|
||||
"No ACP agent connection configured."
|
||||
"No Claude Code connection configured."
|
||||
} else {
|
||||
"ACP providers are not available on this platform."
|
||||
};
|
||||
column.add_child(Self::render_provider_section(
|
||||
"ACP",
|
||||
"Add an ACP agent connection here. After it is added, Settings shows it read-only because the agent controls its model, login, session, and tool loop.",
|
||||
"Claude Code",
|
||||
"Connect Claude Code with your Claude account. Setup downloads missing components and opens sign-in in your browser.",
|
||||
acp_add_button,
|
||||
acp_cards,
|
||||
empty_message,
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
ProviderSetupProviderType::Anthropic
|
||||
| ProviderSetupProviderType::Gemini
|
||||
| ProviderSetupProviderType::VertexAI => {}
|
||||
ProviderSetupProviderType::Gemini | ProviderSetupProviderType::VertexAI => {}
|
||||
}
|
||||
|
||||
column.finish()
|
||||
|
||||
@@ -250,6 +250,7 @@ pub enum SettingsSection {
|
||||
ProviderOpenAI,
|
||||
ProviderLiteLLM,
|
||||
ProviderChatGPTSubscription,
|
||||
ProviderAnthropic,
|
||||
ProviderBedrock,
|
||||
ProviderACP,
|
||||
/// Internal backing-page identifier for CodeSettingsPageView. Multiple subpages
|
||||
@@ -282,6 +283,7 @@ impl Display for SettingsSection {
|
||||
SettingsSection::ProviderOpenAI => write!(f, "OpenAI"),
|
||||
SettingsSection::ProviderLiteLLM => write!(f, "LiteLLM"),
|
||||
SettingsSection::ProviderChatGPTSubscription => write!(f, "ChatGPT Subscription"),
|
||||
SettingsSection::ProviderAnthropic => write!(f, "Anthropic / Claude subscription"),
|
||||
SettingsSection::ProviderBedrock => write!(f, "Bedrock"),
|
||||
SettingsSection::ProviderACP => write!(f, "ACP"),
|
||||
SettingsSection::Wormhole => write!(f, "Wormhole"),
|
||||
@@ -318,6 +320,7 @@ impl SettingsSection {
|
||||
Self::ProviderOpenAI
|
||||
| Self::ProviderLiteLLM
|
||||
| Self::ProviderChatGPTSubscription
|
||||
| Self::ProviderAnthropic
|
||||
| Self::ProviderBedrock
|
||||
| Self::ProviderACP
|
||||
)
|
||||
@@ -335,6 +338,7 @@ impl SettingsSection {
|
||||
| Self::ProviderOpenAI
|
||||
| Self::ProviderLiteLLM
|
||||
| Self::ProviderChatGPTSubscription
|
||||
| Self::ProviderAnthropic
|
||||
| Self::ProviderBedrock
|
||||
| Self::ProviderACP
|
||||
)
|
||||
@@ -376,6 +380,7 @@ impl SettingsSection {
|
||||
Self::ProviderOpenAI,
|
||||
Self::ProviderLiteLLM,
|
||||
Self::ProviderChatGPTSubscription,
|
||||
Self::ProviderAnthropic,
|
||||
Self::ProviderBedrock,
|
||||
Self::ProviderACP,
|
||||
]
|
||||
@@ -413,6 +418,9 @@ impl FromStr for SettingsSection {
|
||||
"OpenAI / LiteLLM" | "LiteLLM" => Ok(Self::ProviderLiteLLM),
|
||||
"OpenAI" => Ok(Self::ProviderOpenAI),
|
||||
"ChatGPT Subscription" | "ChatGPTSubscription" => Ok(Self::ProviderChatGPTSubscription),
|
||||
"Anthropic" | "Claude Subscription" | "Anthropic / Claude subscription" => {
|
||||
Ok(Self::ProviderAnthropic)
|
||||
}
|
||||
"Agent Client Protocol" | "ACP" => Ok(Self::ProviderACP),
|
||||
"Indexing and projects" | "CodeIndexing" => Ok(Self::CodeIndexing),
|
||||
"Editor and Code Review" | "EditorAndCodeReview" => Ok(Self::EditorAndCodeReview),
|
||||
|
||||
@@ -41,6 +41,7 @@ fn provider_subpages_are_classified_and_map_to_ai_backing_page() {
|
||||
SettingsSection::ProviderOpenAI,
|
||||
SettingsSection::ProviderLiteLLM,
|
||||
SettingsSection::ProviderChatGPTSubscription,
|
||||
SettingsSection::ProviderAnthropic,
|
||||
SettingsSection::ProviderBedrock,
|
||||
SettingsSection::ProviderACP,
|
||||
]
|
||||
@@ -116,6 +117,10 @@ fn current_settings_display_names_round_trip() {
|
||||
SettingsSection::ProviderChatGPTSubscription,
|
||||
"ChatGPT Subscription",
|
||||
),
|
||||
(
|
||||
SettingsSection::ProviderAnthropic,
|
||||
"Anthropic / Claude subscription",
|
||||
),
|
||||
(SettingsSection::ProviderBedrock, "Bedrock"),
|
||||
(SettingsSection::ProviderACP, "ACP"),
|
||||
(SettingsSection::CodeIndexing, "Indexing and projects"),
|
||||
|
||||
@@ -67,7 +67,7 @@ use crate::ai::harness_display;
|
||||
use crate::ai::llms::{merge_discovered_provider_models, replace_litellm_catalog, LLMPreferences};
|
||||
use crate::appearance::Appearance;
|
||||
use crate::editor::{
|
||||
EditorView, Event as EditorEvent, SingleLineEditorOptions, TextColors, TextOptions,
|
||||
EditOrigin, EditorView, Event as EditorEvent, SingleLineEditorOptions, TextColors, TextOptions,
|
||||
};
|
||||
use crate::settings::ai::{
|
||||
AcpConfigOptionSettings, BedrockAuthMethod, BedrockModelConfig, ModelCapabilityOverride,
|
||||
@@ -118,6 +118,11 @@ const PROVIDER_TYPE_OPTIONS: &[(ProviderSetupProviderType, &str, &str)] = &[
|
||||
"ChatGPT subscription",
|
||||
"Use your ChatGPT Plus or Pro subscription with native OAuth.",
|
||||
),
|
||||
(
|
||||
ProviderSetupProviderType::Anthropic,
|
||||
"Anthropic / Claude subscription",
|
||||
"Use an Anthropic API key or a Claude Code authorization token with Galaxy’s own tools.",
|
||||
),
|
||||
(
|
||||
ProviderSetupProviderType::Bedrock,
|
||||
"AWS Bedrock",
|
||||
@@ -215,6 +220,9 @@ pub enum ProviderSetupViewAction {
|
||||
ToggleModel(usize),
|
||||
CycleModelCapability(usize, CapabilityKey),
|
||||
ConnectChatGPT,
|
||||
ConnectClaude,
|
||||
SignOutClaude,
|
||||
CancelClaudeSignIn,
|
||||
SelectBedrockAuth(BedrockAuthMethod),
|
||||
SelectBedrockProfile(String),
|
||||
ToggleBedrockCrossRegion,
|
||||
@@ -236,6 +244,9 @@ pub struct ProviderSetupView {
|
||||
draft_bedrock: BedrockProviderDraft,
|
||||
draft_acp: AcpProviderDraft,
|
||||
discovery_state: DiscoveryState,
|
||||
acp_setup_status: String,
|
||||
acp_discovery_generation: u64,
|
||||
acp_setup_abort: Option<futures::future::AbortHandle>,
|
||||
provider_type_buttons: Vec<ViewHandle<ActionButton>>,
|
||||
acp_agent_buttons: Vec<ViewHandle<ActionButton>>,
|
||||
name_editor: ViewHandle<EditorView>,
|
||||
@@ -253,6 +264,11 @@ pub struct ProviderSetupView {
|
||||
acp_command_editor: ViewHandle<EditorView>,
|
||||
acp_args_editor: ViewHandle<EditorView>,
|
||||
chatgpt_connect_mouse_state: MouseStateHandle,
|
||||
claude_connect_mouse_state: MouseStateHandle,
|
||||
claude_signin_abort: Option<futures::future::AbortHandle>,
|
||||
claude_signin_generation: u64,
|
||||
claude_signin_status: String,
|
||||
claude_credentials_available: bool,
|
||||
bedrock_auth_buttons: Vec<ViewHandle<ActionButton>>,
|
||||
bedrock_cross_region_toggle: SwitchStateHandle,
|
||||
bedrock_auto_login_toggle: SwitchStateHandle,
|
||||
@@ -300,11 +316,11 @@ impl ProviderSetupView {
|
||||
let bedrock_refresh_command_editor = Self::create_editor("aws sso login", false, ctx);
|
||||
let bedrock_access_key_editor = Self::create_editor("AKIA...", false, ctx);
|
||||
let bedrock_secret_key_editor = Self::create_editor("Secret access key", true, ctx);
|
||||
let acp_agent_id_editor = Self::create_editor("codex", false, ctx);
|
||||
let acp_agent_id_editor = Self::create_editor("claude-acp", false, ctx);
|
||||
let acp_command_editor = Self::create_editor("Optional executable", false, ctx);
|
||||
let acp_args_editor = Self::create_editor(r#"["arg1", "arg2"]"#, false, ctx);
|
||||
|
||||
let mut acp_agent_buttons = galaxy_acp::known_acp_agents()
|
||||
let acp_agent_buttons = galaxy_acp::known_acp_agents()
|
||||
.iter()
|
||||
.map(|agent| {
|
||||
let id = agent.id.to_owned();
|
||||
@@ -322,15 +338,6 @@ impl ProviderSetupView {
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
acp_agent_buttons.push(ctx.add_typed_action_view(|_| {
|
||||
ActionButton::new("Custom", NakedTheme)
|
||||
.with_full_width(true)
|
||||
.on_click(|ctx| {
|
||||
ctx.dispatch_typed_action(ProviderSetupViewAction::SelectAcpAgent(
|
||||
"custom".to_owned(),
|
||||
));
|
||||
})
|
||||
}));
|
||||
|
||||
let bedrock_auth_buttons = [
|
||||
BedrockAuthMethod::Profile,
|
||||
@@ -362,9 +369,15 @@ impl ProviderSetupView {
|
||||
}
|
||||
});
|
||||
ctx.subscribe_to_view(&api_key_editor, |me, editor, event, ctx| {
|
||||
if matches!(event, EditorEvent::Edited(_)) {
|
||||
if matches!(event, EditorEvent::Edited(origin) if *origin != EditOrigin::SystemEdit) {
|
||||
let value = editor.as_ref(ctx).buffer_text(ctx);
|
||||
me.cancel_claude_signin();
|
||||
me.draft_api_key = (!value.trim().is_empty()).then_some(value);
|
||||
me.claude_credentials_available = false;
|
||||
if me.provider_type == ProviderSetupProviderType::Anthropic {
|
||||
me.draft_models.clear();
|
||||
}
|
||||
me.update_next_button(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
});
|
||||
@@ -465,12 +478,15 @@ impl ProviderSetupView {
|
||||
},
|
||||
draft_acp: AcpProviderDraft {
|
||||
name: String::new(),
|
||||
agent_id: "codex".to_string(),
|
||||
agent_id: "claude-acp".to_string(),
|
||||
command: String::new(),
|
||||
args: Vec::new(),
|
||||
config_options: Vec::new(),
|
||||
},
|
||||
discovery_state: DiscoveryState::Idle,
|
||||
acp_setup_status: String::new(),
|
||||
acp_discovery_generation: 0,
|
||||
acp_setup_abort: None,
|
||||
provider_type_buttons,
|
||||
acp_agent_buttons,
|
||||
name_editor,
|
||||
@@ -488,6 +504,11 @@ impl ProviderSetupView {
|
||||
acp_command_editor,
|
||||
acp_args_editor,
|
||||
chatgpt_connect_mouse_state: MouseStateHandle::default(),
|
||||
claude_connect_mouse_state: MouseStateHandle::default(),
|
||||
claude_signin_abort: None,
|
||||
claude_signin_generation: 0,
|
||||
claude_signin_status: String::new(),
|
||||
claude_credentials_available: false,
|
||||
bedrock_auth_buttons,
|
||||
bedrock_cross_region_toggle: SwitchStateHandle::default(),
|
||||
bedrock_auto_login_toggle: SwitchStateHandle::default(),
|
||||
@@ -542,7 +563,7 @@ impl ProviderSetupView {
|
||||
ProviderSetupProviderType::Gemini => "Google Gemini",
|
||||
ProviderSetupProviderType::VertexAI => "Google Vertex AI",
|
||||
ProviderSetupProviderType::Bedrock => "AWS Bedrock",
|
||||
ProviderSetupProviderType::Acp => "ACP agent runtime",
|
||||
ProviderSetupProviderType::Acp => "Claude Code",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,6 +585,8 @@ impl ProviderSetupView {
|
||||
provider_type: ProviderSetupProviderType,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.cancel_acp_setup();
|
||||
self.cancel_claude_signin();
|
||||
self.step = ProviderSetupStep::Configure;
|
||||
self.editing_index = None;
|
||||
self.provider_type = provider_type;
|
||||
@@ -571,6 +594,7 @@ impl ProviderSetupView {
|
||||
self.draft_name = Self::default_name(provider_type).to_string();
|
||||
self.draft_base_url = Self::default_base_url(provider_type).to_string();
|
||||
self.draft_api_key = None;
|
||||
self.claude_credentials_available = false;
|
||||
self.draft_project_id.clear();
|
||||
self.draft_location = "global".to_string();
|
||||
self.draft_models.clear();
|
||||
@@ -588,7 +612,7 @@ impl ProviderSetupView {
|
||||
};
|
||||
self.draft_acp = AcpProviderDraft {
|
||||
name: String::new(),
|
||||
agent_id: "codex".to_string(),
|
||||
agent_id: "claude-acp".to_string(),
|
||||
command: String::new(),
|
||||
args: Vec::new(),
|
||||
config_options: Vec::new(),
|
||||
@@ -613,6 +637,7 @@ impl ProviderSetupView {
|
||||
provider: OpenAIProviderConfig,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.cancel_claude_signin();
|
||||
// Editing an existing provider is a local catalog operation. Do not
|
||||
// send the user through credentials or model discovery again.
|
||||
self.step = ProviderSetupStep::Models;
|
||||
@@ -631,6 +656,14 @@ impl ProviderSetupView {
|
||||
self.draft_name = provider.name;
|
||||
self.draft_base_url = provider.base_url;
|
||||
self.draft_api_key = provider.api_key;
|
||||
{
|
||||
use galaxyui_extras::secure_storage::AppContextExt as _;
|
||||
self.claude_credentials_available =
|
||||
self.draft_api_key.as_deref().is_some_and(|reference| {
|
||||
crate::ai::claude_subscription::is_stored_credential(reference)
|
||||
&& ctx.secure_storage().read_value(reference).is_ok()
|
||||
});
|
||||
}
|
||||
self.draft_project_id = provider.project_id.unwrap_or_default();
|
||||
self.draft_location = provider.location.unwrap_or_else(|| "global".to_string());
|
||||
self.draft_models = provider.models;
|
||||
@@ -667,6 +700,7 @@ impl ProviderSetupView {
|
||||
}
|
||||
|
||||
pub fn begin_edit_acp(&mut self, draft: AcpProviderDraft, ctx: &mut ViewContext<Self>) {
|
||||
self.cancel_acp_setup();
|
||||
self.step = if draft.config_options.is_empty() {
|
||||
ProviderSetupStep::Configure
|
||||
} else {
|
||||
@@ -688,6 +722,43 @@ impl ProviderSetupView {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub fn acp_discovery_generation(&self) -> u64 {
|
||||
self.acp_discovery_generation
|
||||
}
|
||||
|
||||
pub fn acp_discovery_is_current(&self, generation: u64) -> bool {
|
||||
self.provider_type == ProviderSetupProviderType::Acp
|
||||
&& self.step == ProviderSetupStep::Discover
|
||||
&& matches!(self.discovery_state, DiscoveryState::Loading)
|
||||
&& self.acp_discovery_generation == generation
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub fn prepare_acp_setup(&mut self) -> futures::future::AbortRegistration {
|
||||
self.cancel_acp_setup();
|
||||
let (handle, registration) = futures::future::AbortHandle::new_pair();
|
||||
self.acp_setup_abort = Some(handle);
|
||||
registration
|
||||
}
|
||||
|
||||
fn cancel_acp_setup(&mut self) {
|
||||
if let Some(handle) = self.acp_setup_abort.take() {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_acp_setup_status(&mut self, status: String, ctx: &mut ViewContext<Self>) {
|
||||
self.acp_setup_status = status;
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub fn show_connection_error(&mut self, error: String, ctx: &mut ViewContext<Self>) {
|
||||
self.discovery_state = DiscoveryState::Failed(error);
|
||||
self.step = ProviderSetupStep::Discover;
|
||||
self.update_next_button(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub fn finish_acp_discovery(
|
||||
&mut self,
|
||||
result: Result<(), String>,
|
||||
@@ -754,7 +825,13 @@ impl ProviderSetupView {
|
||||
editor.system_reset_buffer_text(&self.draft_base_url, ctx);
|
||||
});
|
||||
self.api_key_editor.update(ctx, |editor, ctx| {
|
||||
editor.system_reset_buffer_text(self.draft_api_key.as_deref().unwrap_or_default(), ctx);
|
||||
let credential = self.draft_api_key.as_deref().unwrap_or_default();
|
||||
let text = if crate::ai::claude_subscription::is_stored_credential(credential) {
|
||||
""
|
||||
} else {
|
||||
credential
|
||||
};
|
||||
editor.system_reset_buffer_text(text, ctx);
|
||||
});
|
||||
self.project_id_editor.update(ctx, |editor, ctx| {
|
||||
editor.system_reset_buffer_text(&self.draft_project_id, ctx);
|
||||
@@ -834,11 +911,6 @@ impl ProviderSetupView {
|
||||
button.set_active(agent.id.eq_ignore_ascii_case(selected), ctx);
|
||||
});
|
||||
}
|
||||
if let Some(button) = self.acp_agent_buttons.last() {
|
||||
button.update(ctx, |button, ctx| {
|
||||
button.set_active(selected.eq_ignore_ascii_case("custom"), ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_bedrock_auth_buttons(&self, ctx: &mut ViewContext<Self>) {
|
||||
@@ -948,7 +1020,9 @@ impl ProviderSetupView {
|
||||
| ProviderSetupProviderType::VertexAI => (
|
||||
"Save",
|
||||
self.draft_name.trim().is_empty()
|
||||
|| !self.draft_models.iter().any(|model| model.enabled),
|
||||
|| !self.draft_models.iter().any(|model| model.enabled)
|
||||
|| (self.provider_type == ProviderSetupProviderType::Anthropic
|
||||
&& !self.is_configure_valid()),
|
||||
),
|
||||
ProviderSetupProviderType::Bedrock => (
|
||||
"Save",
|
||||
@@ -1057,8 +1131,11 @@ impl ProviderSetupView {
|
||||
}
|
||||
|
||||
fn begin_discovery(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.cancel_acp_setup();
|
||||
self.step = ProviderSetupStep::Discover;
|
||||
self.discovery_state = DiscoveryState::Loading;
|
||||
self.acp_setup_status.clear();
|
||||
self.acp_discovery_generation = self.acp_discovery_generation.wrapping_add(1);
|
||||
self.update_next_button(ctx);
|
||||
ctx.notify();
|
||||
|
||||
@@ -1136,26 +1213,38 @@ impl ProviderSetupView {
|
||||
let provider = self.draft_provider();
|
||||
let existing_models = self.draft_models.clone();
|
||||
let authoritative_catalog = provider.kind == OpenAIProviderKind::LiteLLM;
|
||||
let generation = self.acp_discovery_generation;
|
||||
let requested_kind = self.provider_type;
|
||||
let requested_credential = self.draft_api_key.clone();
|
||||
ctx.spawn(
|
||||
async move { LLMPreferences::discover_openai_provider_models(provider).await },
|
||||
move |me, result, ctx| match result {
|
||||
Ok(models) => {
|
||||
me.draft_models = if authoritative_catalog {
|
||||
replace_litellm_catalog(&existing_models, models)
|
||||
} else {
|
||||
merge_discovered_provider_models(&existing_models, models)
|
||||
};
|
||||
me.discovery_state = DiscoveryState::Idle;
|
||||
me.step = ProviderSetupStep::Models;
|
||||
me.sync_model_switches(ctx);
|
||||
me.update_next_button(ctx);
|
||||
ctx.focus(&me.name_editor);
|
||||
ctx.notify();
|
||||
move |me, result, ctx| {
|
||||
if generation != me.acp_discovery_generation
|
||||
|| requested_kind != me.provider_type
|
||||
|| requested_credential != me.draft_api_key
|
||||
|| me.step != ProviderSetupStep::Discover
|
||||
{
|
||||
return;
|
||||
}
|
||||
Err(error) => {
|
||||
me.discovery_state = DiscoveryState::Failed(error);
|
||||
me.update_next_button(ctx);
|
||||
ctx.notify();
|
||||
match result {
|
||||
Ok(models) => {
|
||||
me.draft_models = if authoritative_catalog {
|
||||
replace_litellm_catalog(&existing_models, models)
|
||||
} else {
|
||||
merge_discovered_provider_models(&existing_models, models)
|
||||
};
|
||||
me.discovery_state = DiscoveryState::Idle;
|
||||
me.step = ProviderSetupStep::Models;
|
||||
me.sync_model_switches(ctx);
|
||||
me.update_next_button(ctx);
|
||||
ctx.focus(&me.name_editor);
|
||||
ctx.notify();
|
||||
}
|
||||
Err(error) => {
|
||||
me.discovery_state = DiscoveryState::Failed(error);
|
||||
me.update_next_button(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1313,6 +1402,138 @@ impl ProviderSetupView {
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn cancel_claude_signin(&mut self) {
|
||||
self.claude_signin_generation = self.claude_signin_generation.wrapping_add(1);
|
||||
if let Some(abort) = self.claude_signin_abort.take() {
|
||||
abort.abort();
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
if let Some(reference) = self.draft_api_key.as_deref() {
|
||||
crate::ai::claude_oauth::discard_draft(reference);
|
||||
}
|
||||
self.claude_signin_status.clear();
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn connect_claude(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
if self.claude_signin_abort.is_some() {
|
||||
return;
|
||||
}
|
||||
self.cancel_claude_signin();
|
||||
let login = match crate::ai::claude_oauth::BrowserLogin::start() {
|
||||
Ok(login) => login,
|
||||
Err(error) => {
|
||||
self.claude_signin_status = error;
|
||||
ctx.notify();
|
||||
return;
|
||||
}
|
||||
};
|
||||
let generation = self.claude_signin_generation;
|
||||
let (abort, registration) = futures::future::AbortHandle::new_pair();
|
||||
self.claude_signin_abort = Some(abort);
|
||||
self.claude_signin_status = "Waiting for Claude sign-in in your browser…".to_string();
|
||||
self.update_next_button(ctx);
|
||||
ctx.open_url(login.url.as_str());
|
||||
ctx.notify();
|
||||
ctx.spawn(
|
||||
futures::future::Abortable::new(login.finish(), registration),
|
||||
move |me, result, ctx| {
|
||||
if generation != me.claude_signin_generation
|
||||
|| me.provider_type != ProviderSetupProviderType::Anthropic
|
||||
{
|
||||
return;
|
||||
}
|
||||
me.claude_signin_abort = None;
|
||||
match result {
|
||||
Ok(Ok(credentials)) => {
|
||||
let reference = format!(
|
||||
"{}{}",
|
||||
crate::ai::claude_subscription::OAUTH_REFERENCE_PREFIX,
|
||||
uuid::Uuid::new_v4()
|
||||
);
|
||||
match crate::ai::claude_oauth::save_credentials(
|
||||
&reference,
|
||||
&credentials,
|
||||
ctx,
|
||||
) {
|
||||
Ok(()) => {
|
||||
me.draft_api_key = Some(reference);
|
||||
me.claude_credentials_available = true;
|
||||
me.draft_models.clear();
|
||||
me.claude_signin_status =
|
||||
"Claude subscription connected.".to_string();
|
||||
me.sync_editors(ctx);
|
||||
}
|
||||
Err(error) => me.claude_signin_status = error,
|
||||
}
|
||||
}
|
||||
Ok(Err(error)) => me.claude_signin_status = error,
|
||||
Err(_) => me.claude_signin_status = "Claude sign-in cancelled.".to_string(),
|
||||
}
|
||||
me.update_next_button(ctx);
|
||||
ctx.notify();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn render_claude_auth(&self, appearance: &Appearance) -> Box<dyn galaxyui::Element> {
|
||||
let connected = self.claude_credentials_available;
|
||||
let description = if !self.claude_signin_status.is_empty() {
|
||||
self.claude_signin_status.as_str()
|
||||
} else if connected {
|
||||
"Claude subscription credentials saved on this device."
|
||||
} else {
|
||||
"Sign in with your Claude subscription in your browser."
|
||||
};
|
||||
let mut children = vec![
|
||||
Self::render_label(appearance, "Claude subscription authorization"),
|
||||
Text::new(
|
||||
description.to_string(),
|
||||
appearance.ui_font_family(),
|
||||
INPUT_FONT_SIZE,
|
||||
)
|
||||
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||
.soft_wrap(true)
|
||||
.finish(),
|
||||
];
|
||||
{
|
||||
let pending = self.claude_signin_abort.is_some();
|
||||
children.push(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.button(
|
||||
ButtonVariant::Secondary,
|
||||
self.claude_connect_mouse_state.clone(),
|
||||
)
|
||||
.with_text_label(
|
||||
if pending {
|
||||
"Cancel sign-in"
|
||||
} else if connected {
|
||||
"Sign out"
|
||||
} else {
|
||||
"Sign in with Claude"
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(if pending {
|
||||
ProviderSetupViewAction::CancelClaudeSignIn
|
||||
} else if connected {
|
||||
ProviderSetupViewAction::SignOutClaude
|
||||
} else {
|
||||
ProviderSetupViewAction::ConnectClaude
|
||||
});
|
||||
})
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
Flex::column()
|
||||
.with_spacing(10.)
|
||||
.with_children(children)
|
||||
.finish()
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn render_chatgpt_auth(
|
||||
&self,
|
||||
@@ -1438,10 +1659,21 @@ impl ProviderSetupView {
|
||||
);
|
||||
}
|
||||
ProviderSetupProviderType::Anthropic => {
|
||||
children.push(self.render_input(appearance, "API key", &self.api_key_editor));
|
||||
children.push(self.render_claude_auth(appearance));
|
||||
if !self
|
||||
.draft_api_key
|
||||
.as_deref()
|
||||
.is_some_and(crate::ai::claude_subscription::is_stored_credential)
|
||||
{
|
||||
children.push(self.render_input(
|
||||
appearance,
|
||||
"API key or setup token (optional alternative)",
|
||||
&self.api_key_editor,
|
||||
));
|
||||
}
|
||||
children.push(
|
||||
Text::new(
|
||||
"The key is stored locally and is never synced to the cloud. Models will be discovered from Anthropic after the connection test.",
|
||||
"Sign in with Claude to use your subscription. OAuth credentials are saved in Galaxy’s secure credential store and refreshed automatically. Alternatively, paste an API key or a token from `claude setup-token`; pasted setup tokens must be replaced manually if they expire. API keys use Anthropic API billing.",
|
||||
appearance.ui_font_family(),
|
||||
INPUT_FONT_SIZE,
|
||||
)
|
||||
@@ -1449,6 +1681,16 @@ impl ProviderSetupView {
|
||||
.soft_wrap(true)
|
||||
.finish(),
|
||||
);
|
||||
children.push(
|
||||
Text::new(
|
||||
crate::ai::claude_subscription::SUBSCRIPTION_WARNING,
|
||||
appearance.ui_font_family(),
|
||||
INPUT_FONT_SIZE,
|
||||
)
|
||||
.with_color(appearance.theme().accent().into())
|
||||
.soft_wrap(true)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
ProviderSetupProviderType::Gemini => {
|
||||
children.push(self.render_input(appearance, "API key", &self.api_key_editor));
|
||||
@@ -1576,7 +1818,7 @@ impl ProviderSetupView {
|
||||
}
|
||||
children.push(
|
||||
Text::new(
|
||||
"Known NPM-backed clients use npx with a pinned package version. If an ACP client needs another toolchain, choose Custom and configure the executable after installing it yourself.",
|
||||
"Test downloads Claude Code and its ACP adapter when missing. Sign-in opens through Claude Code in your browser; Galaxy does not store your Claude credentials.",
|
||||
appearance.ui_font_family(),
|
||||
INPUT_FONT_SIZE,
|
||||
)
|
||||
@@ -1596,7 +1838,13 @@ impl ProviderSetupView {
|
||||
fn render_discovery(&self, appearance: &Appearance) -> Box<dyn galaxyui::Element> {
|
||||
let (message, color) = match &self.discovery_state {
|
||||
DiscoveryState::Idle | DiscoveryState::Loading => (
|
||||
"Testing the connection and discovering available models...".to_string(),
|
||||
if self.provider_type == ProviderSetupProviderType::Acp
|
||||
&& !self.acp_setup_status.is_empty()
|
||||
{
|
||||
self.acp_setup_status.clone()
|
||||
} else {
|
||||
"Testing the connection and discovering available models...".to_string()
|
||||
},
|
||||
appearance.theme().active_ui_text_color().into_solid(),
|
||||
),
|
||||
DiscoveryState::Failed(error) => (error.clone(), appearance.theme().ui_error_color()),
|
||||
@@ -2012,6 +2260,9 @@ impl ProviderSetupView {
|
||||
}
|
||||
|
||||
fn is_configure_valid(&self) -> bool {
|
||||
if self.claude_signin_abort.is_some() {
|
||||
return false;
|
||||
}
|
||||
match self.provider_type {
|
||||
ProviderSetupProviderType::OpenAI => {
|
||||
!self.draft_base_url.trim().is_empty()
|
||||
@@ -2021,7 +2272,16 @@ impl ProviderSetupView {
|
||||
.is_some_and(|key| !key.trim().is_empty())
|
||||
}
|
||||
ProviderSetupProviderType::LiteLLM => !self.draft_base_url.trim().is_empty(),
|
||||
ProviderSetupProviderType::Anthropic | ProviderSetupProviderType::Gemini => self
|
||||
ProviderSetupProviderType::Anthropic => {
|
||||
self.draft_api_key.as_deref().is_some_and(|key| {
|
||||
if crate::ai::claude_subscription::is_stored_credential(key) {
|
||||
self.claude_credentials_available
|
||||
} else {
|
||||
!key.trim().is_empty()
|
||||
}
|
||||
})
|
||||
}
|
||||
ProviderSetupProviderType::Gemini => self
|
||||
.draft_api_key
|
||||
.as_deref()
|
||||
.is_some_and(|key| !key.trim().is_empty()),
|
||||
@@ -2202,6 +2462,9 @@ impl TypedActionView for ProviderSetupView {
|
||||
match action {
|
||||
ProviderSetupViewAction::SelectProvider(kind) => {
|
||||
if self.provider_type != *kind {
|
||||
self.cancel_claude_signin();
|
||||
self.draft_api_key = None;
|
||||
self.claude_credentials_available = false;
|
||||
self.draft_models.clear();
|
||||
self.discovery_state = DiscoveryState::Idle;
|
||||
}
|
||||
@@ -2244,6 +2507,8 @@ impl TypedActionView for ProviderSetupView {
|
||||
| ProviderSetupProviderType::VertexAI => {
|
||||
if self.draft_name.trim().is_empty()
|
||||
|| !self.draft_models.iter().any(|model| model.enabled)
|
||||
|| (self.provider_type == ProviderSetupProviderType::Anthropic
|
||||
&& !self.is_configure_valid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -2282,6 +2547,7 @@ impl TypedActionView for ProviderSetupView {
|
||||
ctx.notify();
|
||||
}
|
||||
ProviderSetupStep::Discover => {
|
||||
self.cancel_acp_setup();
|
||||
self.step = ProviderSetupStep::Configure;
|
||||
self.discovery_state = DiscoveryState::Idle;
|
||||
self.update_next_button(ctx);
|
||||
@@ -2294,6 +2560,9 @@ impl TypedActionView for ProviderSetupView {
|
||||
}
|
||||
},
|
||||
ProviderSetupViewAction::Cancel => {
|
||||
self.cancel_acp_setup();
|
||||
self.cancel_claude_signin();
|
||||
self.discovery_state = DiscoveryState::Idle;
|
||||
ctx.emit(ProviderSetupViewEvent::Close);
|
||||
}
|
||||
ProviderSetupViewAction::SelectAcpAgent(agent_id) => {
|
||||
@@ -2338,6 +2607,28 @@ impl TypedActionView for ProviderSetupView {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
ChatGPTAuthModel::handle(ctx).update(ctx, |model, ctx| model.connect(ctx));
|
||||
}
|
||||
ProviderSetupViewAction::ConnectClaude => {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
self.connect_claude(ctx);
|
||||
}
|
||||
ProviderSetupViewAction::SignOutClaude => {
|
||||
if let Some(reference) = self.draft_api_key.take() {
|
||||
crate::ai::claude_subscription::remove_credential(&reference, ctx);
|
||||
}
|
||||
self.cancel_claude_signin();
|
||||
self.claude_credentials_available = false;
|
||||
self.claude_signin_status = "Signed out of Claude on this device.".to_string();
|
||||
self.draft_models.clear();
|
||||
self.discovery_state = DiscoveryState::Idle;
|
||||
self.update_next_button(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
ProviderSetupViewAction::CancelClaudeSignIn => {
|
||||
self.cancel_claude_signin();
|
||||
self.claude_signin_status = "Claude sign-in cancelled.".to_string();
|
||||
self.update_next_button(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
ProviderSetupViewAction::SelectBedrockProfile(profile) => {
|
||||
if self.draft_bedrock.profile != *profile {
|
||||
self.draft_bedrock.profile = profile.clone();
|
||||
@@ -2394,6 +2685,13 @@ fn provider_type_label(kind: ProviderSetupProviderType) -> &'static str {
|
||||
ProviderSetupProviderType::Gemini => "Google Gemini",
|
||||
ProviderSetupProviderType::VertexAI => "Google Vertex AI",
|
||||
ProviderSetupProviderType::Bedrock => "AWS Bedrock",
|
||||
ProviderSetupProviderType::Acp => "ACP agent runtime",
|
||||
ProviderSetupProviderType::Acp => "Claude Code",
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProviderSetupView {
|
||||
fn drop(&mut self) {
|
||||
self.cancel_claude_signin();
|
||||
self.cancel_acp_setup();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user