Improve AI provider model configuration
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
use galaxy_acp::{AcpAgentPreset, AcpLaunchConfig, CODEX_ACP_NPM_VERSION, OPENCODE_NPM_VERSION};
|
use galaxy_acp::{
|
||||||
|
resolve_known_acp_agent, AcpAgentPreset, AcpLaunchConfig, CODEX_ACP_NPM_VERSION,
|
||||||
|
OPENCODE_NPM_VERSION,
|
||||||
|
};
|
||||||
use sha2::{Digest as _, Sha256};
|
use sha2::{Digest as _, Sha256};
|
||||||
|
|
||||||
use crate::persistence::model::AcpConversationData;
|
use crate::persistence::model::AcpConversationData;
|
||||||
@@ -215,9 +218,11 @@ pub(crate) fn resolve_acp_launch(
|
|||||||
match agent_id.trim().to_ascii_lowercase().as_str() {
|
match agent_id.trim().to_ascii_lowercase().as_str() {
|
||||||
"codex" => AcpAgentPreset::Codex.resolve_launch_config(),
|
"codex" => AcpAgentPreset::Codex.resolve_launch_config(),
|
||||||
"opencode" => AcpAgentPreset::OpenCode.resolve_launch_config(),
|
"opencode" => AcpAgentPreset::OpenCode.resolve_launch_config(),
|
||||||
unknown => Err(format!(
|
_ => resolve_known_acp_agent(agent_id).map_err(|error| {
|
||||||
"Unknown ACP agent preset {unknown:?}; choose \"codex\" or \"opencode\", or configure a custom ACP executable"
|
format!(
|
||||||
)),
|
"{error} Configure a custom ACP executable if this client uses a different command."
|
||||||
|
)
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use super::*;
|
|||||||
fn unknown_builtin_agent_ids_are_rejected() {
|
fn unknown_builtin_agent_ids_are_rejected() {
|
||||||
let error = resolve_acp_launch("mystery-agent", "", &[]).unwrap_err();
|
let error = resolve_acp_launch("mystery-agent", "", &[]).unwrap_err();
|
||||||
|
|
||||||
assert!(error.contains("Unknown ACP agent preset"));
|
assert!(error.contains("Unknown ACP agent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+8
-5
@@ -509,7 +509,7 @@ fn default_computer_use_llms() -> AvailableLLMs {
|
|||||||
},
|
},
|
||||||
description: None,
|
description: None,
|
||||||
disable_reason: None,
|
disable_reason: None,
|
||||||
vision_supported: true,
|
vision_supported: false,
|
||||||
spec: None,
|
spec: None,
|
||||||
provider: LLMProvider::Unknown,
|
provider: LLMProvider::Unknown,
|
||||||
host_configs: HashMap::new(),
|
host_configs: HashMap::new(),
|
||||||
@@ -1164,7 +1164,7 @@ impl LLMPreferences {
|
|||||||
},
|
},
|
||||||
description: Some(provider_name.clone()),
|
description: Some(provider_name.clone()),
|
||||||
disable_reason: None,
|
disable_reason: None,
|
||||||
vision_supported: model.vision_supported,
|
vision_supported: model.effective_vision_supported(),
|
||||||
spec: None,
|
spec: None,
|
||||||
provider: LLMProvider::LiteLLM,
|
provider: LLMProvider::LiteLLM,
|
||||||
host_configs: HashMap::from([(
|
host_configs: HashMap::from([(
|
||||||
@@ -1670,13 +1670,14 @@ impl LLMPreferences {
|
|||||||
.map(|model| OpenAIModelConfig {
|
.map(|model| OpenAIModelConfig {
|
||||||
model_id: model.id,
|
model_id: model.id,
|
||||||
display_name: model.display_name,
|
display_name: model.display_name,
|
||||||
vision_supported: false,
|
vision_supported: true,
|
||||||
context_size: model.context_size.unwrap_or(128_000),
|
context_size: model.context_size.unwrap_or(128_000),
|
||||||
max_input_tokens: model.context_size,
|
max_input_tokens: model.context_size,
|
||||||
max_output_tokens: None,
|
max_output_tokens: None,
|
||||||
provider: None,
|
provider: None,
|
||||||
use_rig: true,
|
use_rig: true,
|
||||||
supports_system_messages: Some(true),
|
supports_system_messages: Some(true),
|
||||||
|
capability_overrides: std::collections::HashMap::new(),
|
||||||
reasoning_efforts: Vec::new(),
|
reasoning_efforts: Vec::new(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
@@ -2594,7 +2595,7 @@ async fn fetch_from_litellm_model_info(
|
|||||||
.and_then(|v| u32::try_from(v).ok());
|
.and_then(|v| u32::try_from(v).ok());
|
||||||
let context_size = max_input_tokens.unwrap_or(200_000);
|
let context_size = max_input_tokens.unwrap_or(200_000);
|
||||||
|
|
||||||
let vision_supported = model_info["supports_vision"].as_bool().unwrap_or(false);
|
let vision_supported = model_info["supports_vision"].as_bool().unwrap_or(true);
|
||||||
|
|
||||||
let display_name = model_name.replace(['-', '_'], " ");
|
let display_name = model_name.replace(['-', '_'], " ");
|
||||||
let display_name = display_name
|
let display_name = display_name
|
||||||
@@ -2651,6 +2652,7 @@ async fn fetch_from_litellm_model_info(
|
|||||||
} else {
|
} else {
|
||||||
model_info["supports_system_messages"].as_bool()
|
model_info["supports_system_messages"].as_bool()
|
||||||
},
|
},
|
||||||
|
capability_overrides: std::collections::HashMap::new(),
|
||||||
reasoning_efforts: Vec::new(),
|
reasoning_efforts: Vec::new(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
@@ -2769,7 +2771,7 @@ async fn fetch_from_openai_models(
|
|||||||
vision_supported: m["supports_vision"]
|
vision_supported: m["supports_vision"]
|
||||||
.as_bool()
|
.as_bool()
|
||||||
.or_else(|| m["vision_support"].as_bool())
|
.or_else(|| m["vision_support"].as_bool())
|
||||||
.unwrap_or(false),
|
.unwrap_or(true),
|
||||||
context_size,
|
context_size,
|
||||||
max_input_tokens,
|
max_input_tokens,
|
||||||
max_output_tokens,
|
max_output_tokens,
|
||||||
@@ -2780,6 +2782,7 @@ async fn fetch_from_openai_models(
|
|||||||
} else {
|
} else {
|
||||||
m["supports_system_messages"].as_bool()
|
m["supports_system_messages"].as_bool()
|
||||||
},
|
},
|
||||||
|
capability_overrides: std::collections::HashMap::new(),
|
||||||
reasoning_efforts: Vec::new(),
|
reasoning_efforts: Vec::new(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ fn openai_model(model_id: &str) -> OpenAIModelConfig {
|
|||||||
provider: None,
|
provider: None,
|
||||||
use_rig: false,
|
use_rig: false,
|
||||||
supports_system_messages: None,
|
supports_system_messages: None,
|
||||||
|
capability_overrides: std::collections::HashMap::new(),
|
||||||
reasoning_efforts: Vec::new(),
|
reasoning_efforts: Vec::new(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1656,7 +1656,7 @@ impl Element for EditorElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if size.x().is_infinite() {
|
if size.x().is_infinite() {
|
||||||
unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
|
size.set_x(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let top_section_height_lines = top_section_height_px / view_snapshot.line_height;
|
let top_section_height_lines = top_section_height_px / view_snapshot.line_height;
|
||||||
|
|||||||
+69
-3
@@ -893,6 +893,13 @@ pub struct OpenAIModelConfig {
|
|||||||
description = "Whether this endpoint accepts system-role messages. Set false for ChatGPT-backed LiteLLM models that reject them."
|
description = "Whether this endpoint accepts system-role messages. Set false for ChatGPT-backed LiteLLM models that reject them."
|
||||||
)]
|
)]
|
||||||
pub supports_system_messages: Option<bool>,
|
pub supports_system_messages: Option<bool>,
|
||||||
|
/// Per-model capability overrides. Missing entries mean Auto: use provider
|
||||||
|
/// metadata when available and allow the request path to determine support.
|
||||||
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||||
|
#[schemars(
|
||||||
|
description = "Optional per-capability overrides: auto, supported, or unsupported."
|
||||||
|
)]
|
||||||
|
pub capability_overrides: HashMap<String, ModelCapabilityOverride>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[schemars(
|
#[schemars(
|
||||||
description = "Reasoning effort modes supported by this model when using the ChatGPT subscription provider."
|
description = "Reasoning effort modes supported by this model when using the ChatGPT subscription provider."
|
||||||
@@ -906,11 +913,62 @@ pub struct OpenAIModelConfig {
|
|||||||
impl settings_value::SettingsValue for OpenAIModelConfig {}
|
impl settings_value::SettingsValue for OpenAIModelConfig {}
|
||||||
|
|
||||||
impl OpenAIModelConfig {
|
impl OpenAIModelConfig {
|
||||||
|
pub fn capability_override(&self, capability: &str) -> ModelCapabilityOverride {
|
||||||
|
self.capability_overrides
|
||||||
|
.get(capability)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn effective_vision_supported(&self) -> bool {
|
||||||
|
match self.capability_override("vision") {
|
||||||
|
ModelCapabilityOverride::Auto => self.vision_supported,
|
||||||
|
ModelCapabilityOverride::Supported => true,
|
||||||
|
ModelCapabilityOverride::Unsupported => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn supports_system_messages(&self) -> bool {
|
pub fn supports_system_messages(&self) -> bool {
|
||||||
|
if self.capability_override("system_messages") == ModelCapabilityOverride::Unsupported {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if self.model_id.starts_with("codex-gpt-") {
|
if self.model_id.starts_with("codex-gpt-") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.supports_system_messages.unwrap_or(true)
|
match self.capability_override("system_messages") {
|
||||||
|
ModelCapabilityOverride::Supported => true,
|
||||||
|
ModelCapabilityOverride::Auto => self.supports_system_messages.unwrap_or(true),
|
||||||
|
ModelCapabilityOverride::Unsupported => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ModelCapabilityOverride {
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
Supported,
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelCapabilityOverride {
|
||||||
|
pub fn next(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Auto => Self::Supported,
|
||||||
|
Self::Supported => Self::Unsupported,
|
||||||
|
Self::Unsupported => Self::Auto,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Auto => "Auto",
|
||||||
|
Self::Supported => "On",
|
||||||
|
Self::Unsupported => "Off",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,13 +1071,18 @@ fn default_chatgpt_models() -> Vec<OpenAIModelConfig> {
|
|||||||
|(model_id, display_name, reasoning_efforts)| OpenAIModelConfig {
|
|(model_id, display_name, reasoning_efforts)| OpenAIModelConfig {
|
||||||
model_id: model_id.to_string(),
|
model_id: model_id.to_string(),
|
||||||
display_name: display_name.to_string(),
|
display_name: display_name.to_string(),
|
||||||
vision_supported: false,
|
// ChatGPT's subscription backend accepts image input for its chat
|
||||||
|
// models, but it does not expose a public capability discovery
|
||||||
|
// endpoint. Keep this explicit catalog in sync with that contract
|
||||||
|
// so the model picker does not hide vision context.
|
||||||
|
vision_supported: true,
|
||||||
context_size: default_context_size(),
|
context_size: default_context_size(),
|
||||||
max_input_tokens: None,
|
max_input_tokens: None,
|
||||||
max_output_tokens: None,
|
max_output_tokens: None,
|
||||||
provider: Some("openai".to_string()),
|
provider: Some("openai".to_string()),
|
||||||
use_rig: true,
|
use_rig: true,
|
||||||
supports_system_messages: Some(true),
|
supports_system_messages: Some(true),
|
||||||
|
capability_overrides: HashMap::new(),
|
||||||
reasoning_efforts: reasoning_efforts.into_iter().map(str::to_string).collect(),
|
reasoning_efforts: reasoning_efforts.into_iter().map(str::to_string).collect(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
@@ -1055,13 +1118,16 @@ fn default_openai_providers() -> Vec<OpenAIProviderConfig> {
|
|||||||
models: vec![OpenAIModelConfig {
|
models: vec![OpenAIModelConfig {
|
||||||
model_id: INITIAL_RIG_MODEL_ID.to_string(),
|
model_id: INITIAL_RIG_MODEL_ID.to_string(),
|
||||||
display_name: "Codex GPT-5.6 SOL (xhigh)".to_string(),
|
display_name: "Codex GPT-5.6 SOL (xhigh)".to_string(),
|
||||||
vision_supported: false,
|
// Auto capability detection is optimistic for modern
|
||||||
|
// multimodal-compatible endpoints; users can override it per model.
|
||||||
|
vision_supported: true,
|
||||||
context_size: default_context_size(),
|
context_size: default_context_size(),
|
||||||
max_input_tokens: None,
|
max_input_tokens: None,
|
||||||
max_output_tokens: None,
|
max_output_tokens: None,
|
||||||
provider: Some("openai".to_string()),
|
provider: Some("openai".to_string()),
|
||||||
use_rig: true,
|
use_rig: true,
|
||||||
supports_system_messages: Some(false),
|
supports_system_messages: Some(false),
|
||||||
|
capability_overrides: HashMap::new(),
|
||||||
reasoning_efforts: Vec::new(),
|
reasoning_efforts: Vec::new(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
}],
|
}],
|
||||||
|
|||||||
@@ -707,13 +707,19 @@ impl AISettingsPageView {
|
|||||||
draft: &AcpProviderDraft,
|
draft: &AcpProviderDraft,
|
||||||
ctx: &mut ViewContext<Self>,
|
ctx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let Ok(config) = crate::ai::acp::AcpRuntimeModel::discovery_config_for_values(
|
let config = match crate::ai::acp::AcpRuntimeModel::discovery_config_for_values(
|
||||||
&draft.agent_id,
|
&draft.agent_id,
|
||||||
&draft.command,
|
&draft.command,
|
||||||
&draft.args,
|
&draft.args,
|
||||||
) else {
|
) {
|
||||||
log::warn!("Could not resolve ACP launch configuration for discovery");
|
Ok(config) => config,
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("Could not resolve ACP launch configuration for discovery: {error}");
|
||||||
|
self.provider_setup_modal_body.update(ctx, |body, ctx| {
|
||||||
|
body.finish_acp_discovery(Err(error), Vec::new(), ctx);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
self.start_acp_discovery(config, draft.agent_id.clone(), ctx);
|
self.start_acp_discovery(config, draft.agent_id.clone(), ctx);
|
||||||
}
|
}
|
||||||
@@ -739,7 +745,7 @@ impl AISettingsPageView {
|
|||||||
log::warn!("Could not start ACP discovery: {error}");
|
log::warn!("Could not start ACP discovery: {error}");
|
||||||
let error_text = error.to_string();
|
let error_text = error.to_string();
|
||||||
provider_setup_modal_body.update(ctx, |body, ctx| {
|
provider_setup_modal_body.update(ctx, |body, ctx| {
|
||||||
body.finish_acp_discovery(Err(error_text), ctx);
|
body.finish_acp_discovery(Err(error_text), Vec::new(), ctx);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -751,6 +757,10 @@ impl AISettingsPageView {
|
|||||||
match result {
|
match result {
|
||||||
Ok(options) => {
|
Ok(options) => {
|
||||||
let option_count = options.len();
|
let option_count = options.len();
|
||||||
|
let config_options =
|
||||||
|
crate::ai::acp::AcpRuntimeModel::normalize_config_options(
|
||||||
|
options.clone(),
|
||||||
|
);
|
||||||
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
crate::ai::acp::AcpRuntimeModel::upsert_agent_settings(
|
crate::ai::acp::AcpRuntimeModel::upsert_agent_settings(
|
||||||
@@ -774,7 +784,7 @@ impl AISettingsPageView {
|
|||||||
runtime.finish_discovery_success(option_count, ctx);
|
runtime.finish_discovery_success(option_count, ctx);
|
||||||
});
|
});
|
||||||
provider_setup_modal_body.update(ctx, |body, ctx| {
|
provider_setup_modal_body.update(ctx, |body, ctx| {
|
||||||
body.finish_acp_discovery(Ok(()), ctx);
|
body.finish_acp_discovery(Ok(()), config_options, ctx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -796,7 +806,7 @@ impl AISettingsPageView {
|
|||||||
runtime.finish_discovery_failure(error_text.clone(), ctx);
|
runtime.finish_discovery_failure(error_text.clone(), ctx);
|
||||||
});
|
});
|
||||||
provider_setup_modal_body.update(ctx, |body, ctx| {
|
provider_setup_modal_body.update(ctx, |body, ctx| {
|
||||||
body.finish_acp_discovery(Err(error_text), ctx);
|
body.finish_acp_discovery(Err(error_text), Vec::new(), ctx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1791,12 +1801,12 @@ impl AISettingsPageView {
|
|||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
.with_modal_style(UiComponentStyles {
|
.with_modal_style(UiComponentStyles {
|
||||||
width: Some(640.),
|
width: Some(900.),
|
||||||
height: Some(600.),
|
height: Some(700.),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.with_body_style(UiComponentStyles {
|
.with_body_style(UiComponentStyles {
|
||||||
height: Some(530.),
|
height: Some(630.),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.with_dismiss_on_click()
|
.with_dismiss_on_click()
|
||||||
@@ -2017,6 +2027,13 @@ impl AISettingsPageView {
|
|||||||
agent_id: settings.acp_agent_id.value().clone(),
|
agent_id: settings.acp_agent_id.value().clone(),
|
||||||
command: settings.acp_agent_command.value().clone(),
|
command: settings.acp_agent_command.value().clone(),
|
||||||
args: settings.acp_agent_args.value().clone(),
|
args: settings.acp_agent_args.value().clone(),
|
||||||
|
config_options: settings
|
||||||
|
.acp_agents
|
||||||
|
.value()
|
||||||
|
.iter()
|
||||||
|
.find(|agent| agent.id.eq_ignore_ascii_case(settings.acp_agent_id.value()))
|
||||||
|
.map(|agent| agent.config_options.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
};
|
};
|
||||||
let body = self
|
let body = self
|
||||||
.provider_setup_modal_state
|
.provider_setup_modal_state
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use galaxyui::elements::{
|
|||||||
MouseStateHandle, Padding, ParentElement, Radius, ScrollbarWidth, Text,
|
MouseStateHandle, Padding, ParentElement, Radius, ScrollbarWidth, Text,
|
||||||
};
|
};
|
||||||
use galaxyui::fonts::{Properties, Weight};
|
use galaxyui::fonts::{Properties, Weight};
|
||||||
|
use galaxyui::text_layout::ClipConfig;
|
||||||
use galaxyui::ui_components::button::ButtonVariant;
|
use galaxyui::ui_components::button::ButtonVariant;
|
||||||
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||||
use galaxyui::ui_components::switch::SwitchStateHandle;
|
use galaxyui::ui_components::switch::SwitchStateHandle;
|
||||||
@@ -21,17 +22,17 @@ use crate::editor::{
|
|||||||
};
|
};
|
||||||
use crate::modal::{Modal, ModalViewState};
|
use crate::modal::{Modal, ModalViewState};
|
||||||
use crate::settings::ai::{
|
use crate::settings::ai::{
|
||||||
BedrockAuthMethod, BedrockModelConfig, OpenAIModelConfig, OpenAIProviderConfig,
|
AcpConfigOptionSettings, BedrockAuthMethod, BedrockModelConfig, ModelCapabilityOverride,
|
||||||
OpenAIProviderKind,
|
OpenAIModelConfig, OpenAIProviderConfig, OpenAIProviderKind,
|
||||||
};
|
};
|
||||||
use crate::ui_components::icons::Icon;
|
use crate::ui_components::icons::Icon;
|
||||||
use crate::view_components::action_button::{
|
use crate::view_components::action_button::{
|
||||||
ActionButton, NakedTheme, PrimaryTheme, SecondaryTheme,
|
ActionButton, ButtonSize, NakedTheme, PrimaryTheme, SecondaryTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODAL_WIDTH: f32 = 640.;
|
const MODAL_WIDTH: f32 = 900.;
|
||||||
const MODAL_HEIGHT: f32 = 600.;
|
const MODAL_HEIGHT: f32 = 700.;
|
||||||
const BODY_HEIGHT: f32 = 530.;
|
const BODY_HEIGHT: f32 = 630.;
|
||||||
const INPUT_FONT_SIZE: f32 = 12.;
|
const INPUT_FONT_SIZE: f32 = 12.;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@@ -111,6 +112,7 @@ pub struct AcpProviderDraft {
|
|||||||
pub agent_id: String,
|
pub agent_id: String,
|
||||||
pub command: String,
|
pub command: String,
|
||||||
pub args: Vec<String>,
|
pub args: Vec<String>,
|
||||||
|
pub config_options: Vec<AcpConfigOptionSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
@@ -120,6 +122,45 @@ enum DiscoveryState {
|
|||||||
Failed(String),
|
Failed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum CapabilityKey {
|
||||||
|
Vision,
|
||||||
|
Files,
|
||||||
|
Audio,
|
||||||
|
Tools,
|
||||||
|
SystemMessages,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CapabilityKey {
|
||||||
|
const ALL: [Self; 5] = [
|
||||||
|
Self::Vision,
|
||||||
|
Self::Files,
|
||||||
|
Self::Audio,
|
||||||
|
Self::Tools,
|
||||||
|
Self::SystemMessages,
|
||||||
|
];
|
||||||
|
|
||||||
|
fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Vision => "Images",
|
||||||
|
Self::Files => "Files",
|
||||||
|
Self::Audio => "Audio",
|
||||||
|
Self::Tools => "Tools",
|
||||||
|
Self::SystemMessages => "System",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setting_key(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Vision => "vision",
|
||||||
|
Self::Files => "files",
|
||||||
|
Self::Audio => "audio",
|
||||||
|
Self::Tools => "tools",
|
||||||
|
Self::SystemMessages => "system_messages",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum ProviderSetupModalBodyEvent {
|
pub enum ProviderSetupModalBodyEvent {
|
||||||
Close,
|
Close,
|
||||||
RequestAcpDiscovery(AcpProviderDraft),
|
RequestAcpDiscovery(AcpProviderDraft),
|
||||||
@@ -138,12 +179,14 @@ pub enum ProviderSetupModalBodyAction {
|
|||||||
Back,
|
Back,
|
||||||
Cancel,
|
Cancel,
|
||||||
ToggleModel(usize),
|
ToggleModel(usize),
|
||||||
|
CycleModelCapability(usize, CapabilityKey),
|
||||||
ConnectChatGPT,
|
ConnectChatGPT,
|
||||||
OpenChatGPTDevicePage,
|
OpenChatGPTDevicePage,
|
||||||
CopyChatGPTDeviceCode,
|
CopyChatGPTDeviceCode,
|
||||||
SelectBedrockAuth(BedrockAuthMethod),
|
SelectBedrockAuth(BedrockAuthMethod),
|
||||||
ToggleBedrockCrossRegion,
|
ToggleBedrockCrossRegion,
|
||||||
ToggleBedrockAutoLogin,
|
ToggleBedrockAutoLogin,
|
||||||
|
SelectAcpAgent(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ProviderSetupModalState = ModalViewState<Modal<ProviderSetupModalBody>>;
|
pub type ProviderSetupModalState = ModalViewState<Modal<ProviderSetupModalBody>>;
|
||||||
@@ -162,6 +205,7 @@ pub struct ProviderSetupModalBody {
|
|||||||
draft_acp: AcpProviderDraft,
|
draft_acp: AcpProviderDraft,
|
||||||
discovery_state: DiscoveryState,
|
discovery_state: DiscoveryState,
|
||||||
provider_type_buttons: Vec<ViewHandle<ActionButton>>,
|
provider_type_buttons: Vec<ViewHandle<ActionButton>>,
|
||||||
|
acp_agent_buttons: Vec<ViewHandle<ActionButton>>,
|
||||||
name_editor: ViewHandle<EditorView>,
|
name_editor: ViewHandle<EditorView>,
|
||||||
base_url_editor: ViewHandle<EditorView>,
|
base_url_editor: ViewHandle<EditorView>,
|
||||||
api_key_editor: ViewHandle<EditorView>,
|
api_key_editor: ViewHandle<EditorView>,
|
||||||
@@ -175,10 +219,16 @@ pub struct ProviderSetupModalBody {
|
|||||||
acp_agent_id_editor: ViewHandle<EditorView>,
|
acp_agent_id_editor: ViewHandle<EditorView>,
|
||||||
acp_command_editor: ViewHandle<EditorView>,
|
acp_command_editor: ViewHandle<EditorView>,
|
||||||
acp_args_editor: ViewHandle<EditorView>,
|
acp_args_editor: ViewHandle<EditorView>,
|
||||||
|
chatgpt_connect_mouse_state: MouseStateHandle,
|
||||||
|
chatgpt_open_mouse_state: MouseStateHandle,
|
||||||
|
chatgpt_copy_mouse_state: MouseStateHandle,
|
||||||
bedrock_auth_buttons: Vec<ViewHandle<ActionButton>>,
|
bedrock_auth_buttons: Vec<ViewHandle<ActionButton>>,
|
||||||
bedrock_cross_region_toggle: SwitchStateHandle,
|
bedrock_cross_region_toggle: SwitchStateHandle,
|
||||||
bedrock_auto_login_toggle: SwitchStateHandle,
|
bedrock_auto_login_toggle: SwitchStateHandle,
|
||||||
model_switches: Vec<SwitchStateHandle>,
|
model_switches: Vec<SwitchStateHandle>,
|
||||||
|
model_capability_switches: Vec<[SwitchStateHandle; 2]>,
|
||||||
|
model_capability_buttons: Vec<Vec<ViewHandle<ActionButton>>>,
|
||||||
|
model_context_editors: Vec<ViewHandle<EditorView>>,
|
||||||
provider_type_scroll_state: ClippedScrollStateHandle,
|
provider_type_scroll_state: ClippedScrollStateHandle,
|
||||||
models_scroll_state: ClippedScrollStateHandle,
|
models_scroll_state: ClippedScrollStateHandle,
|
||||||
back_button: ViewHandle<ActionButton>,
|
back_button: ViewHandle<ActionButton>,
|
||||||
@@ -219,6 +269,34 @@ impl ProviderSetupModalBody {
|
|||||||
let acp_command_editor = Self::create_editor("Optional executable", 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 acp_args_editor = Self::create_editor(r#"["arg1", "arg2"]"#, false, ctx);
|
||||||
|
|
||||||
|
let mut acp_agent_buttons = galaxy_acp::known_acp_agents()
|
||||||
|
.iter()
|
||||||
|
.map(|agent| {
|
||||||
|
let id = agent.id.to_owned();
|
||||||
|
ctx.add_typed_action_view(move |_| {
|
||||||
|
ActionButton::new(agent.name, NakedTheme)
|
||||||
|
.with_full_width(true)
|
||||||
|
.on_click({
|
||||||
|
let id = id.clone();
|
||||||
|
move |ctx| {
|
||||||
|
ctx.dispatch_typed_action(
|
||||||
|
ProviderSetupModalBodyAction::SelectAcpAgent(id.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.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(ProviderSetupModalBodyAction::SelectAcpAgent(
|
||||||
|
"custom".to_owned(),
|
||||||
|
));
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
let bedrock_auth_buttons = [
|
let bedrock_auth_buttons = [
|
||||||
BedrockAuthMethod::Profile,
|
BedrockAuthMethod::Profile,
|
||||||
BedrockAuthMethod::Sso,
|
BedrockAuthMethod::Sso,
|
||||||
@@ -358,9 +436,11 @@ impl ProviderSetupModalBody {
|
|||||||
agent_id: "codex".to_string(),
|
agent_id: "codex".to_string(),
|
||||||
command: String::new(),
|
command: String::new(),
|
||||||
args: Vec::new(),
|
args: Vec::new(),
|
||||||
|
config_options: Vec::new(),
|
||||||
},
|
},
|
||||||
discovery_state: DiscoveryState::Idle,
|
discovery_state: DiscoveryState::Idle,
|
||||||
provider_type_buttons,
|
provider_type_buttons,
|
||||||
|
acp_agent_buttons,
|
||||||
name_editor,
|
name_editor,
|
||||||
base_url_editor,
|
base_url_editor,
|
||||||
api_key_editor,
|
api_key_editor,
|
||||||
@@ -374,10 +454,16 @@ impl ProviderSetupModalBody {
|
|||||||
acp_agent_id_editor,
|
acp_agent_id_editor,
|
||||||
acp_command_editor,
|
acp_command_editor,
|
||||||
acp_args_editor,
|
acp_args_editor,
|
||||||
|
chatgpt_connect_mouse_state: MouseStateHandle::default(),
|
||||||
|
chatgpt_open_mouse_state: MouseStateHandle::default(),
|
||||||
|
chatgpt_copy_mouse_state: MouseStateHandle::default(),
|
||||||
bedrock_auth_buttons,
|
bedrock_auth_buttons,
|
||||||
bedrock_cross_region_toggle: SwitchStateHandle::default(),
|
bedrock_cross_region_toggle: SwitchStateHandle::default(),
|
||||||
bedrock_auto_login_toggle: SwitchStateHandle::default(),
|
bedrock_auto_login_toggle: SwitchStateHandle::default(),
|
||||||
model_switches: Vec::new(),
|
model_switches: Vec::new(),
|
||||||
|
model_capability_switches: Vec::new(),
|
||||||
|
model_capability_buttons: Vec::new(),
|
||||||
|
model_context_editors: Vec::new(),
|
||||||
provider_type_scroll_state: ClippedScrollStateHandle::default(),
|
provider_type_scroll_state: ClippedScrollStateHandle::default(),
|
||||||
models_scroll_state: ClippedScrollStateHandle::default(),
|
models_scroll_state: ClippedScrollStateHandle::default(),
|
||||||
back_button,
|
back_button,
|
||||||
@@ -440,10 +526,12 @@ impl ProviderSetupModalBody {
|
|||||||
agent_id: "codex".to_string(),
|
agent_id: "codex".to_string(),
|
||||||
command: String::new(),
|
command: String::new(),
|
||||||
args: Vec::new(),
|
args: Vec::new(),
|
||||||
|
config_options: Vec::new(),
|
||||||
};
|
};
|
||||||
self.discovery_state = DiscoveryState::Idle;
|
self.discovery_state = DiscoveryState::Idle;
|
||||||
self.sync_editors(ctx);
|
self.sync_editors(ctx);
|
||||||
self.sync_provider_type_buttons(ctx);
|
self.sync_provider_type_buttons(ctx);
|
||||||
|
self.sync_acp_agent_buttons(ctx);
|
||||||
self.sync_bedrock_auth_buttons(ctx);
|
self.sync_bedrock_auth_buttons(ctx);
|
||||||
self.sync_model_switches(ctx);
|
self.sync_model_switches(ctx);
|
||||||
self.update_next_button(ctx);
|
self.update_next_button(ctx);
|
||||||
@@ -457,7 +545,9 @@ impl ProviderSetupModalBody {
|
|||||||
provider: OpenAIProviderConfig,
|
provider: OpenAIProviderConfig,
|
||||||
ctx: &mut ViewContext<Self>,
|
ctx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
self.step = ProviderSetupStep::Configure;
|
// Editing an existing provider is a local catalog operation. Do not
|
||||||
|
// send the user through credentials or model discovery again.
|
||||||
|
self.step = ProviderSetupStep::Models;
|
||||||
self.editing_index = Some(editing_index);
|
self.editing_index = Some(editing_index);
|
||||||
self.provider_type = match provider.kind {
|
self.provider_type = match provider.kind {
|
||||||
OpenAIProviderKind::ChatGPTSubscription => {
|
OpenAIProviderKind::ChatGPTSubscription => {
|
||||||
@@ -477,6 +567,7 @@ impl ProviderSetupModalBody {
|
|||||||
self.discovery_state = DiscoveryState::Idle;
|
self.discovery_state = DiscoveryState::Idle;
|
||||||
self.sync_editors(ctx);
|
self.sync_editors(ctx);
|
||||||
self.sync_provider_type_buttons(ctx);
|
self.sync_provider_type_buttons(ctx);
|
||||||
|
self.sync_acp_agent_buttons(ctx);
|
||||||
self.sync_bedrock_auth_buttons(ctx);
|
self.sync_bedrock_auth_buttons(ctx);
|
||||||
self.sync_model_switches(ctx);
|
self.sync_model_switches(ctx);
|
||||||
self.update_next_button(ctx);
|
self.update_next_button(ctx);
|
||||||
@@ -502,7 +593,11 @@ impl ProviderSetupModalBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn begin_edit_acp(&mut self, draft: AcpProviderDraft, ctx: &mut ViewContext<Self>) {
|
pub fn begin_edit_acp(&mut self, draft: AcpProviderDraft, ctx: &mut ViewContext<Self>) {
|
||||||
self.step = ProviderSetupStep::Configure;
|
self.step = if draft.config_options.is_empty() {
|
||||||
|
ProviderSetupStep::Configure
|
||||||
|
} else {
|
||||||
|
ProviderSetupStep::Models
|
||||||
|
};
|
||||||
self.editing_index = None;
|
self.editing_index = None;
|
||||||
self.provider_type = ProviderSetupProviderType::Acp;
|
self.provider_type = ProviderSetupProviderType::Acp;
|
||||||
self.draft_name = draft.name.clone();
|
self.draft_name = draft.name.clone();
|
||||||
@@ -521,10 +616,12 @@ impl ProviderSetupModalBody {
|
|||||||
pub fn finish_acp_discovery(
|
pub fn finish_acp_discovery(
|
||||||
&mut self,
|
&mut self,
|
||||||
result: Result<(), String>,
|
result: Result<(), String>,
|
||||||
|
config_options: Vec<AcpConfigOptionSettings>,
|
||||||
ctx: &mut ViewContext<Self>,
|
ctx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
self.draft_acp.config_options = config_options;
|
||||||
self.discovery_state = DiscoveryState::Idle;
|
self.discovery_state = DiscoveryState::Idle;
|
||||||
self.step = ProviderSetupStep::Models;
|
self.step = ProviderSetupStep::Models;
|
||||||
ctx.focus(&self.name_editor);
|
ctx.focus(&self.name_editor);
|
||||||
@@ -593,6 +690,23 @@ impl ProviderSetupModalBody {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sync_acp_agent_buttons(&self, ctx: &mut ViewContext<Self>) {
|
||||||
|
let selected = self.draft_acp.agent_id.trim();
|
||||||
|
for (agent, button) in galaxy_acp::known_acp_agents()
|
||||||
|
.iter()
|
||||||
|
.zip(self.acp_agent_buttons.iter())
|
||||||
|
{
|
||||||
|
button.update(ctx, |button, ctx| {
|
||||||
|
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>) {
|
fn sync_bedrock_auth_buttons(&self, ctx: &mut ViewContext<Self>) {
|
||||||
for (index, button) in self.bedrock_auth_buttons.iter().enumerate() {
|
for (index, button) in self.bedrock_auth_buttons.iter().enumerate() {
|
||||||
let method = match index {
|
let method = match index {
|
||||||
@@ -612,6 +726,68 @@ impl ProviderSetupModalBody {
|
|||||||
self.model_switches.push(SwitchStateHandle::default());
|
self.model_switches.push(SwitchStateHandle::default());
|
||||||
}
|
}
|
||||||
self.model_switches.truncate(self.draft_models.len());
|
self.model_switches.truncate(self.draft_models.len());
|
||||||
|
|
||||||
|
while self.model_capability_switches.len() < self.draft_models.len() {
|
||||||
|
self.model_capability_switches
|
||||||
|
.push([SwitchStateHandle::default(), SwitchStateHandle::default()]);
|
||||||
|
}
|
||||||
|
self.model_capability_switches
|
||||||
|
.truncate(self.draft_models.len());
|
||||||
|
|
||||||
|
while self.model_capability_buttons.len() < self.draft_models.len() {
|
||||||
|
let index = self.model_capability_buttons.len();
|
||||||
|
let buttons = CapabilityKey::ALL
|
||||||
|
.into_iter()
|
||||||
|
.map(|key| {
|
||||||
|
ctx.add_typed_action_view(move |_| {
|
||||||
|
ActionButton::new(format!("{}: Auto", key.label()), NakedTheme)
|
||||||
|
.with_size(ButtonSize::XSmall)
|
||||||
|
.on_click(move |ctx| {
|
||||||
|
ctx.dispatch_typed_action(
|
||||||
|
ProviderSetupModalBodyAction::CycleModelCapability(index, key),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
self.model_capability_buttons.push(buttons);
|
||||||
|
}
|
||||||
|
self.model_capability_buttons
|
||||||
|
.truncate(self.draft_models.len());
|
||||||
|
|
||||||
|
while self.model_context_editors.len() < self.draft_models.len() {
|
||||||
|
let index = self.model_context_editors.len();
|
||||||
|
let editor = Self::create_editor("Context window", false, ctx);
|
||||||
|
ctx.subscribe_to_view(&editor, move |me, editor, event, ctx| {
|
||||||
|
if matches!(event, EditorEvent::Edited(_)) {
|
||||||
|
if let Some(model) = me.draft_models.get_mut(index) {
|
||||||
|
if let Ok(context_size) = editor.as_ref(ctx).buffer_text(ctx).parse() {
|
||||||
|
model.context_size = context_size;
|
||||||
|
model.max_input_tokens = Some(context_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
me.update_next_button(ctx);
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.model_context_editors.push(editor);
|
||||||
|
}
|
||||||
|
self.model_context_editors.truncate(self.draft_models.len());
|
||||||
|
|
||||||
|
for (index, model) in self.draft_models.iter().enumerate() {
|
||||||
|
for (button, key) in self.model_capability_buttons[index]
|
||||||
|
.iter()
|
||||||
|
.zip(CapabilityKey::ALL)
|
||||||
|
{
|
||||||
|
let state = model.capability_override(key.setting_key());
|
||||||
|
button.update(ctx, |button, ctx| {
|
||||||
|
button.set_label(format!("{}: {}", key.label(), state.label()), ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.model_context_editors[index].update(ctx, |editor, ctx| {
|
||||||
|
editor.system_reset_buffer_text(&model.context_size.to_string(), ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_next_button(&self, ctx: &mut ViewContext<Self>) {
|
fn update_next_button(&self, ctx: &mut ViewContext<Self>) {
|
||||||
@@ -628,7 +804,11 @@ impl ProviderSetupModalBody {
|
|||||||
.is_none_or(|key| key.trim().is_empty())
|
.is_none_or(|key| key.trim().is_empty())
|
||||||
}
|
}
|
||||||
ProviderSetupProviderType::VertexAI => self.draft_project_id.trim().is_empty(),
|
ProviderSetupProviderType::VertexAI => self.draft_project_id.trim().is_empty(),
|
||||||
ProviderSetupProviderType::Acp => self.draft_acp.agent_id.trim().is_empty(),
|
ProviderSetupProviderType::Acp => {
|
||||||
|
self.draft_acp.agent_id.trim().is_empty()
|
||||||
|
|| (self.draft_acp.agent_id.eq_ignore_ascii_case("custom")
|
||||||
|
&& self.draft_acp.command.trim().is_empty())
|
||||||
|
}
|
||||||
ProviderSetupProviderType::ChatGPTSubscription
|
ProviderSetupProviderType::ChatGPTSubscription
|
||||||
| ProviderSetupProviderType::Bedrock => false,
|
| ProviderSetupProviderType::Bedrock => false,
|
||||||
};
|
};
|
||||||
@@ -957,6 +1137,14 @@ impl ProviderSetupModalBody {
|
|||||||
.soft_wrap(true)
|
.soft_wrap(true)
|
||||||
.finish(),
|
.finish(),
|
||||||
);
|
);
|
||||||
|
if let ChatGPTAuthState::Failed(error) = &state {
|
||||||
|
children.push(
|
||||||
|
Text::new(error.clone(), appearance.monospace_font_family(), 11.)
|
||||||
|
.with_color(appearance.theme().ui_error_color().into())
|
||||||
|
.soft_wrap(true)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if let ChatGPTAuthState::AwaitingDeviceCode {
|
if let ChatGPTAuthState::AwaitingDeviceCode {
|
||||||
verification_uri,
|
verification_uri,
|
||||||
@@ -985,7 +1173,10 @@ impl ProviderSetupModalBody {
|
|||||||
.with_child(
|
.with_child(
|
||||||
appearance
|
appearance
|
||||||
.ui_builder()
|
.ui_builder()
|
||||||
.button(ButtonVariant::Secondary, MouseStateHandle::default())
|
.button(
|
||||||
|
ButtonVariant::Secondary,
|
||||||
|
self.chatgpt_open_mouse_state.clone(),
|
||||||
|
)
|
||||||
.with_text_label("Open sign-in page".to_owned())
|
.with_text_label("Open sign-in page".to_owned())
|
||||||
.build()
|
.build()
|
||||||
.on_click(|ctx, _, _| {
|
.on_click(|ctx, _, _| {
|
||||||
@@ -998,7 +1189,10 @@ impl ProviderSetupModalBody {
|
|||||||
.with_child(
|
.with_child(
|
||||||
appearance
|
appearance
|
||||||
.ui_builder()
|
.ui_builder()
|
||||||
.button(ButtonVariant::Secondary, MouseStateHandle::default())
|
.button(
|
||||||
|
ButtonVariant::Secondary,
|
||||||
|
self.chatgpt_copy_mouse_state.clone(),
|
||||||
|
)
|
||||||
.with_text_label("Copy code".to_owned())
|
.with_text_label("Copy code".to_owned())
|
||||||
.build()
|
.build()
|
||||||
.on_click(|ctx, _, _| {
|
.on_click(|ctx, _, _| {
|
||||||
@@ -1029,7 +1223,10 @@ impl ProviderSetupModalBody {
|
|||||||
children.push(
|
children.push(
|
||||||
appearance
|
appearance
|
||||||
.ui_builder()
|
.ui_builder()
|
||||||
.button(ButtonVariant::Secondary, MouseStateHandle::default())
|
.button(
|
||||||
|
ButtonVariant::Secondary,
|
||||||
|
self.chatgpt_connect_mouse_state.clone(),
|
||||||
|
)
|
||||||
.with_text_label("Connect ChatGPT".to_owned())
|
.with_text_label("Connect ChatGPT".to_owned())
|
||||||
.build()
|
.build()
|
||||||
.on_click(|ctx, _, _| {
|
.on_click(|ctx, _, _| {
|
||||||
@@ -1216,14 +1413,21 @@ impl ProviderSetupModalBody {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
ProviderSetupProviderType::Acp => {
|
ProviderSetupProviderType::Acp => {
|
||||||
|
children.push(
|
||||||
|
Text::new("ACP client", appearance.ui_font_family(), INPUT_FONT_SIZE)
|
||||||
|
.with_color(appearance.theme().active_ui_text_color().into())
|
||||||
|
.with_style(Properties::default().weight(Weight::Bold))
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
children.extend(
|
||||||
|
self.acp_agent_buttons
|
||||||
|
.iter()
|
||||||
|
.map(|button| ChildView::new(button).finish()),
|
||||||
|
);
|
||||||
|
if self.draft_acp.agent_id.eq_ignore_ascii_case("custom") {
|
||||||
children.push(self.render_input(
|
children.push(self.render_input(
|
||||||
appearance,
|
appearance,
|
||||||
"Agent preset",
|
"Executable",
|
||||||
&self.acp_agent_id_editor,
|
|
||||||
));
|
|
||||||
children.push(self.render_input(
|
|
||||||
appearance,
|
|
||||||
"Custom executable (optional)",
|
|
||||||
&self.acp_command_editor,
|
&self.acp_command_editor,
|
||||||
));
|
));
|
||||||
children.push(self.render_input(
|
children.push(self.render_input(
|
||||||
@@ -1231,9 +1435,10 @@ impl ProviderSetupModalBody {
|
|||||||
"Arguments (JSON array)",
|
"Arguments (JSON array)",
|
||||||
&self.acp_args_editor,
|
&self.acp_args_editor,
|
||||||
));
|
));
|
||||||
|
}
|
||||||
children.push(
|
children.push(
|
||||||
Text::new(
|
Text::new(
|
||||||
"ACP agents own their model and authentication. Galaxy will discover the configured runtime before saving.",
|
"Known clients use their local executable. If the client is not installed, Galaxy will show a launch error. Choose Custom for another ACP-compatible command.",
|
||||||
appearance.ui_font_family(),
|
appearance.ui_font_family(),
|
||||||
INPUT_FONT_SIZE,
|
INPUT_FONT_SIZE,
|
||||||
)
|
)
|
||||||
@@ -1330,7 +1535,150 @@ impl ProviderSetupModalBody {
|
|||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
.with_width(MODAL_WIDTH - 56.)
|
.with_width(MODAL_WIDTH - 56.)
|
||||||
.with_max_height(320.)
|
.with_max_height(430.)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_model_table_header(&self, appearance: &Appearance) -> Box<dyn galaxyui::Element> {
|
||||||
|
let header = |label: &str| {
|
||||||
|
Text::new(label.to_owned(), appearance.ui_font_family(), 11.)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.with_style(Properties::default().weight(Weight::Semibold))
|
||||||
|
.finish()
|
||||||
|
};
|
||||||
|
Container::new(
|
||||||
|
Flex::row()
|
||||||
|
.with_spacing(12.)
|
||||||
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||||
|
.with_child(ConstrainedBox::new(header("Use")).with_width(52.).finish())
|
||||||
|
.with_child(
|
||||||
|
ConstrainedBox::new(header("Model"))
|
||||||
|
.with_width(250.)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
ConstrainedBox::new(header("Context"))
|
||||||
|
.with_width(140.)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
ConstrainedBox::new(header("Capabilities"))
|
||||||
|
.with_width(330.)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_padding(Padding::uniform(10.).with_vertical(9.))
|
||||||
|
.with_background(appearance.theme().surface_2())
|
||||||
|
.with_border(Border::bottom(1.).with_border_fill(appearance.theme().outline()))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_model_capabilities(
|
||||||
|
&self,
|
||||||
|
appearance: &Appearance,
|
||||||
|
index: usize,
|
||||||
|
) -> Box<dyn galaxyui::Element> {
|
||||||
|
let buttons = &self.model_capability_buttons[index];
|
||||||
|
let first_row = buttons[..3]
|
||||||
|
.iter()
|
||||||
|
.map(|button| ChildView::new(button).finish())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let second_row = buttons[3..]
|
||||||
|
.iter()
|
||||||
|
.map(|button| ChildView::new(button).finish())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Flex::column()
|
||||||
|
.with_spacing(6.)
|
||||||
|
.with_child(
|
||||||
|
Flex::row()
|
||||||
|
.with_spacing(8.)
|
||||||
|
.with_children(first_row)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Flex::row()
|
||||||
|
.with_spacing(8.)
|
||||||
|
.with_children(second_row)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_openai_model_row(
|
||||||
|
&self,
|
||||||
|
appearance: &Appearance,
|
||||||
|
index: usize,
|
||||||
|
model: &OpenAIModelConfig,
|
||||||
|
) -> Box<dyn galaxyui::Element> {
|
||||||
|
let model_info = Flex::column()
|
||||||
|
.with_spacing(4.)
|
||||||
|
.with_child(
|
||||||
|
Text::new_inline(model.display_name.clone(), appearance.ui_font_family(), 12.)
|
||||||
|
.with_color(appearance.theme().active_ui_text_color().into())
|
||||||
|
.with_clip(ClipConfig::end())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Text::new_inline(
|
||||||
|
model.model_id.clone(),
|
||||||
|
appearance.monospace_font_family(),
|
||||||
|
10.,
|
||||||
|
)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.with_clip(ClipConfig::end())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
let context_input = appearance
|
||||||
|
.ui_builder()
|
||||||
|
.text_input(self.model_context_editors[index].clone())
|
||||||
|
.with_style(UiComponentStyles {
|
||||||
|
padding: Some(Coords {
|
||||||
|
top: 8.,
|
||||||
|
bottom: 8.,
|
||||||
|
left: 8.,
|
||||||
|
right: 8.,
|
||||||
|
}),
|
||||||
|
background: Some(appearance.theme().surface_1().into()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
Container::new(
|
||||||
|
Flex::row()
|
||||||
|
.with_spacing(12.)
|
||||||
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||||
|
.with_child(
|
||||||
|
ConstrainedBox::new(
|
||||||
|
appearance
|
||||||
|
.ui_builder()
|
||||||
|
.switch(self.model_switches[index].clone())
|
||||||
|
.check(model.enabled)
|
||||||
|
.build()
|
||||||
|
.on_click(move |ctx, _, _| {
|
||||||
|
ctx.dispatch_typed_action(
|
||||||
|
ProviderSetupModalBodyAction::ToggleModel(index),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_width(52.)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_child(ConstrainedBox::new(model_info).with_width(250.).finish())
|
||||||
|
.with_child(ConstrainedBox::new(context_input).with_width(140.).finish())
|
||||||
|
.with_child(
|
||||||
|
ConstrainedBox::new(self.render_model_capabilities(appearance, index))
|
||||||
|
.with_width(330.)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_padding(Padding::uniform(12.).with_vertical(10.))
|
||||||
|
.with_border(Border::bottom(1.).with_border_fill(appearance.theme().outline()))
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1382,42 +1730,42 @@ impl ProviderSetupModalBody {
|
|||||||
.finish();
|
.finish();
|
||||||
}
|
}
|
||||||
if self.provider_type == ProviderSetupProviderType::Acp {
|
if self.provider_type == ProviderSetupProviderType::Acp {
|
||||||
return Flex::column()
|
let option_rows = self
|
||||||
.with_spacing(12.)
|
.draft_acp
|
||||||
.with_child(self.render_input(appearance, "Connection name", &self.name_editor))
|
.config_options
|
||||||
.with_child(
|
.iter()
|
||||||
Text::new(
|
.filter(|option| {
|
||||||
"ACP owns model selection. The configured agent runtime was checked before this step.",
|
matches!(
|
||||||
appearance.ui_font_family(),
|
option.category.as_deref(),
|
||||||
INPUT_FONT_SIZE,
|
Some("model") | Some("thought_level") | Some("mode")
|
||||||
)
|
)
|
||||||
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
})
|
||||||
.soft_wrap(true)
|
.map(|option| {
|
||||||
.finish(),
|
let values = option
|
||||||
)
|
.options
|
||||||
.finish();
|
.iter()
|
||||||
}
|
.map(|value| value.name.as_str())
|
||||||
let mut rows = Vec::with_capacity(self.draft_models.len());
|
.collect::<Vec<_>>()
|
||||||
for (index, model) in self.draft_models.iter().enumerate() {
|
.join(", ");
|
||||||
let modes = if model.reasoning_efforts.is_empty() {
|
Flex::column()
|
||||||
"Standard".to_string()
|
|
||||||
} else {
|
|
||||||
model.reasoning_efforts.join(", ")
|
|
||||||
};
|
|
||||||
let info = Flex::column()
|
|
||||||
.with_spacing(2.)
|
.with_spacing(2.)
|
||||||
.with_child(
|
.with_child(
|
||||||
Text::new(
|
Text::new(
|
||||||
model.display_name.clone(),
|
option.name.clone(),
|
||||||
appearance.ui_font_family(),
|
appearance.ui_font_family(),
|
||||||
INPUT_FONT_SIZE,
|
INPUT_FONT_SIZE,
|
||||||
)
|
)
|
||||||
.with_color(appearance.theme().active_ui_text_color().into())
|
.with_color(appearance.theme().active_ui_text_color().into())
|
||||||
|
.with_style(Properties::default().weight(Weight::Bold))
|
||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Text::new(
|
Text::new(
|
||||||
format!("{} · modes: {modes}", model.model_id),
|
if values.is_empty() {
|
||||||
|
option.current_value.to_string()
|
||||||
|
} else {
|
||||||
|
values
|
||||||
|
},
|
||||||
appearance.monospace_font_family(),
|
appearance.monospace_font_family(),
|
||||||
10.,
|
10.,
|
||||||
)
|
)
|
||||||
@@ -1425,37 +1773,53 @@ impl ProviderSetupModalBody {
|
|||||||
.soft_wrap(true)
|
.soft_wrap(true)
|
||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
.finish();
|
.finish()
|
||||||
rows.push(
|
|
||||||
Flex::row()
|
|
||||||
.with_spacing(10.)
|
|
||||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
||||||
.with_child(
|
|
||||||
appearance
|
|
||||||
.ui_builder()
|
|
||||||
.switch(self.model_switches[index].clone())
|
|
||||||
.check(model.enabled)
|
|
||||||
.build()
|
|
||||||
.on_click(move |ctx, _, _| {
|
|
||||||
ctx.dispatch_typed_action(
|
|
||||||
ProviderSetupModalBodyAction::ToggleModel(index),
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let catalog = if option_rows.is_empty() {
|
||||||
|
Text::new(
|
||||||
|
"No model or mode catalog has been discovered yet. Continue to test the ACP agent.",
|
||||||
|
appearance.ui_font_family(),
|
||||||
|
INPUT_FONT_SIZE,
|
||||||
|
)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.soft_wrap(true)
|
||||||
|
.finish()
|
||||||
|
} else {
|
||||||
|
self.render_model_table(appearance, option_rows, 10.)
|
||||||
|
};
|
||||||
|
return Flex::column()
|
||||||
|
.with_spacing(12.)
|
||||||
|
.with_child(self.render_input(appearance, "Connection name", &self.name_editor))
|
||||||
|
.with_child(
|
||||||
|
Text::new(
|
||||||
|
"ACP-discovered models and modes are exposed as selectable combinations in Galaxy's model picker.",
|
||||||
|
appearance.ui_font_family(),
|
||||||
|
INPUT_FONT_SIZE,
|
||||||
|
)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.soft_wrap(true)
|
||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
.with_child(info)
|
.with_child(catalog)
|
||||||
.finish(),
|
.finish();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
let mut rows = vec![self.render_model_table_header(appearance)];
|
||||||
|
rows.extend(
|
||||||
|
self.draft_models
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, model)| self.render_openai_model_row(appearance, index, model)),
|
||||||
|
);
|
||||||
|
|
||||||
let table = self.render_model_table(appearance, rows, 12.);
|
let table = self.render_model_table(appearance, rows, 0.);
|
||||||
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_spacing(12.)
|
.with_spacing(12.)
|
||||||
.with_child(self.render_input(appearance, "Connection name", &self.name_editor))
|
.with_child(self.render_input(appearance, "Connection name", &self.name_editor))
|
||||||
.with_child(
|
.with_child(
|
||||||
Text::new(
|
Text::new(
|
||||||
"Choose which models Galaxy should make available. Reasoning modes remain selectable from the model picker.",
|
"Enable the models Galaxy should offer. Context is the maximum input window. Capabilities use Auto by default and can be overridden per model.",
|
||||||
appearance.ui_font_family(),
|
appearance.ui_font_family(),
|
||||||
INPUT_FONT_SIZE,
|
INPUT_FONT_SIZE,
|
||||||
)
|
)
|
||||||
@@ -1631,6 +1995,17 @@ impl TypedActionView for ProviderSetupModalBody {
|
|||||||
ProviderSetupModalBodyAction::Cancel => {
|
ProviderSetupModalBodyAction::Cancel => {
|
||||||
ctx.emit(ProviderSetupModalBodyEvent::Close);
|
ctx.emit(ProviderSetupModalBodyEvent::Close);
|
||||||
}
|
}
|
||||||
|
ProviderSetupModalBodyAction::SelectAcpAgent(agent_id) => {
|
||||||
|
self.draft_acp.agent_id = agent_id.clone();
|
||||||
|
if !agent_id.eq_ignore_ascii_case("custom") {
|
||||||
|
self.draft_acp.command.clear();
|
||||||
|
self.draft_acp.args.clear();
|
||||||
|
}
|
||||||
|
self.sync_editors(ctx);
|
||||||
|
self.sync_acp_agent_buttons(ctx);
|
||||||
|
self.update_next_button(ctx);
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
ProviderSetupModalBodyAction::ToggleModel(index) => {
|
ProviderSetupModalBodyAction::ToggleModel(index) => {
|
||||||
if let Some(model) = self.draft_models.get_mut(*index) {
|
if let Some(model) = self.draft_models.get_mut(*index) {
|
||||||
model.enabled = !model.enabled;
|
model.enabled = !model.enabled;
|
||||||
@@ -1638,6 +2013,16 @@ impl TypedActionView for ProviderSetupModalBody {
|
|||||||
ctx.notify();
|
ctx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ProviderSetupModalBodyAction::CycleModelCapability(index, capability) => {
|
||||||
|
if let Some(model) = self.draft_models.get_mut(*index) {
|
||||||
|
let key = capability.setting_key().to_string();
|
||||||
|
let next = model.capability_override(&key).next();
|
||||||
|
model.capability_overrides.insert(key, next);
|
||||||
|
self.update_next_button(ctx);
|
||||||
|
self.sync_model_switches(ctx);
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
ProviderSetupModalBodyAction::ConnectChatGPT => {
|
ProviderSetupModalBodyAction::ConnectChatGPT => {
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
ChatGPTAuthModel::handle(ctx).update(ctx, |model, ctx| model.connect(ctx));
|
ChatGPTAuthModel::handle(ctx).update(ctx, |model, ctx| model.connect(ctx));
|
||||||
|
|||||||
+121
-27
@@ -10,12 +10,109 @@ use agent_client_protocol::AcpAgentConfig;
|
|||||||
|
|
||||||
use crate::{DenyByDefaultPermissionHandler, PermissionHandler};
|
use crate::{DenyByDefaultPermissionHandler, PermissionHandler};
|
||||||
|
|
||||||
/// Pinned version of the official Codex ACP adapter.
|
/// Version of the official Codex ACP adapter supported by the built-in setup.
|
||||||
pub const CODEX_ACP_NPM_VERSION: &str = "1.1.7";
|
pub const CODEX_ACP_NPM_VERSION: &str = "1.1.14";
|
||||||
|
|
||||||
/// Pinned version of OpenCode used by the built-in ACP launch preset.
|
/// Pinned version of OpenCode used by the built-in ACP launch preset.
|
||||||
pub const OPENCODE_NPM_VERSION: &str = "1.18.9";
|
pub const OPENCODE_NPM_VERSION: &str = "1.18.9";
|
||||||
|
|
||||||
|
/// A known ACP client that can be selected in Galaxy settings.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct AcpKnownAgent {
|
||||||
|
pub id: &'static str,
|
||||||
|
pub name: &'static str,
|
||||||
|
pub description: &'static str,
|
||||||
|
pub command: &'static str,
|
||||||
|
pub args: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Curated ACP Registry catalog. Launch commands are intentionally local-only:
|
||||||
|
/// Galaxy never installs or downloads an agent on the user's behalf.
|
||||||
|
pub const KNOWN_ACP_AGENTS: &[AcpKnownAgent] = &[
|
||||||
|
AcpKnownAgent {
|
||||||
|
id: "codex",
|
||||||
|
name: "Codex",
|
||||||
|
description: "OpenAI's coding assistant",
|
||||||
|
command: "codex",
|
||||||
|
args: &[],
|
||||||
|
},
|
||||||
|
AcpKnownAgent {
|
||||||
|
id: "opencode",
|
||||||
|
name: "OpenCode",
|
||||||
|
description: "Open source coding agent",
|
||||||
|
command: "opencode",
|
||||||
|
args: &["acp"],
|
||||||
|
},
|
||||||
|
AcpKnownAgent {
|
||||||
|
id: "claude-acp",
|
||||||
|
name: "Claude Agent",
|
||||||
|
description: "Anthropic's coding agent",
|
||||||
|
command: "claude-agent-acp",
|
||||||
|
args: &[],
|
||||||
|
},
|
||||||
|
AcpKnownAgent {
|
||||||
|
id: "gemini",
|
||||||
|
name: "Gemini CLI",
|
||||||
|
description: "Google's coding agent",
|
||||||
|
command: "gemini",
|
||||||
|
args: &["--acp"],
|
||||||
|
},
|
||||||
|
AcpKnownAgent {
|
||||||
|
id: "cline",
|
||||||
|
name: "Cline",
|
||||||
|
description: "Autonomous coding agent",
|
||||||
|
command: "cline",
|
||||||
|
args: &["--acp"],
|
||||||
|
},
|
||||||
|
AcpKnownAgent {
|
||||||
|
id: "cursor",
|
||||||
|
name: "Cursor",
|
||||||
|
description: "Cursor's coding agent",
|
||||||
|
command: "cursor-agent",
|
||||||
|
args: &["acp"],
|
||||||
|
},
|
||||||
|
AcpKnownAgent {
|
||||||
|
id: "github-copilot-cli",
|
||||||
|
name: "GitHub Copilot",
|
||||||
|
description: "GitHub's AI pair programmer",
|
||||||
|
command: "copilot",
|
||||||
|
args: &["--acp"],
|
||||||
|
},
|
||||||
|
AcpKnownAgent {
|
||||||
|
id: "goose",
|
||||||
|
name: "Goose",
|
||||||
|
description: "Block's open source AI agent",
|
||||||
|
command: "goose",
|
||||||
|
args: &["acp"],
|
||||||
|
},
|
||||||
|
AcpKnownAgent {
|
||||||
|
id: "auggie",
|
||||||
|
name: "Auggie CLI",
|
||||||
|
description: "Augment Code's coding agent",
|
||||||
|
command: "auggie",
|
||||||
|
args: &["--acp"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn known_acp_agents() -> &'static [AcpKnownAgent] {
|
||||||
|
KNOWN_ACP_AGENTS
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a registry-listed agent from the local PATH.
|
||||||
|
pub fn resolve_known_acp_agent(agent_id: &str) -> Result<AcpLaunchConfig, String> {
|
||||||
|
let agent = known_acp_agents()
|
||||||
|
.iter()
|
||||||
|
.find(|agent| agent.id.eq_ignore_ascii_case(agent_id.trim()))
|
||||||
|
.ok_or_else(|| format!("Unknown ACP agent: {agent_id:?}"))?;
|
||||||
|
let command = executable_on_path(agent.command).ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"{} is not installed or could not be found on PATH (expected `{}`). Install it or choose Custom.",
|
||||||
|
agent.name, agent.command
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(AcpLaunchConfig::new(command).args(agent.args.iter().copied()))
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_CANCELLATION_GRACE_PERIOD: Duration = Duration::from_secs(5);
|
const DEFAULT_CANCELLATION_GRACE_PERIOD: Duration = Duration::from_secs(5);
|
||||||
const DEFAULT_INITIALIZATION_TIMEOUT: Duration = Duration::from_secs(30);
|
const DEFAULT_INITIALIZATION_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
const DEFAULT_AUTHENTICATION_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
const DEFAULT_AUTHENTICATION_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
||||||
@@ -37,7 +134,7 @@ impl AcpAgentPreset {
|
|||||||
pub fn launch_config(self) -> AcpLaunchConfig {
|
pub fn launch_config(self) -> AcpLaunchConfig {
|
||||||
match self {
|
match self {
|
||||||
Self::Codex => AcpLaunchConfig::new("npx")
|
Self::Codex => AcpLaunchConfig::new("npx")
|
||||||
.args(vec![
|
.args([
|
||||||
"--yes".to_owned(),
|
"--yes".to_owned(),
|
||||||
format!("@agentclientprotocol/codex-acp@{CODEX_ACP_NPM_VERSION}"),
|
format!("@agentclientprotocol/codex-acp@{CODEX_ACP_NPM_VERSION}"),
|
||||||
])
|
])
|
||||||
@@ -61,9 +158,9 @@ impl AcpAgentPreset {
|
|||||||
|
|
||||||
/// Resolves the best available executable for this preset.
|
/// Resolves the best available executable for this preset.
|
||||||
///
|
///
|
||||||
/// OpenCode's native binary is preferred when installed. The Codex adapter
|
/// OpenCode's native binary is preferred when installed. Codex runs its ACP
|
||||||
/// uses `npx` when available and can run through Bun's Node compatibility
|
/// adapter through npx, while CODEX_PATH points at the user's installed
|
||||||
/// mode. OpenCode's npm wrapper requires Node during installation.
|
/// Codex CLI rather than downloading a second Codex installation.
|
||||||
pub fn resolve_launch_config(self) -> Result<AcpLaunchConfig, String> {
|
pub fn resolve_launch_config(self) -> Result<AcpLaunchConfig, String> {
|
||||||
self.resolve_launch_config_with(executable_on_path)
|
self.resolve_launch_config_with(executable_on_path)
|
||||||
}
|
}
|
||||||
@@ -74,33 +171,30 @@ impl AcpAgentPreset {
|
|||||||
) -> Result<AcpLaunchConfig, String> {
|
) -> Result<AcpLaunchConfig, String> {
|
||||||
match self {
|
match self {
|
||||||
Self::Codex => {
|
Self::Codex => {
|
||||||
let (command, args) = if let Some(command) = resolve("npx") {
|
let Some(codex) = resolve("codex") else {
|
||||||
(
|
|
||||||
command,
|
|
||||||
vec![
|
|
||||||
"--yes".to_owned(),
|
|
||||||
format!("@agentclientprotocol/codex-acp@{CODEX_ACP_NPM_VERSION}"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
} else if let Some(command) = resolve("bunx") {
|
|
||||||
(
|
|
||||||
command,
|
|
||||||
vec![
|
|
||||||
"--bun".to_owned(),
|
|
||||||
format!("@agentclientprotocol/codex-acp@{CODEX_ACP_NPM_VERSION}"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return Err(
|
return Err(
|
||||||
"Codex ACP requires npx or bunx; install Node.js/npm or Bun, or configure a custom ACP executable"
|
"Codex ACP requires the locally installed codex CLI; install Codex or configure a custom ACP executable"
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Ok(AcpLaunchConfig::new(command)
|
let launch = if let Some(adapter) = resolve("codex-acp") {
|
||||||
.args(args)
|
AcpLaunchConfig::new(adapter)
|
||||||
|
} else if let Some(npx) = resolve("npx") {
|
||||||
|
AcpLaunchConfig::new(npx).args([
|
||||||
|
"--yes".to_owned(),
|
||||||
|
format!("@agentclientprotocol/codex-acp@{CODEX_ACP_NPM_VERSION}"),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
return Err(
|
||||||
|
"Codex ACP requires either a local codex-acp executable or npx; install the ACP adapter, install Node.js/npm, or configure a custom ACP executable"
|
||||||
|
.to_owned(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Ok(launch
|
||||||
.preferred_auth_method("chat-gpt")
|
.preferred_auth_method("chat-gpt")
|
||||||
.env("DEFAULT_AUTH_REQUEST", r#"{"methodId":"chat-gpt"}"#)
|
.env("DEFAULT_AUTH_REQUEST", r#"{"methodId":"chat-gpt"}"#)
|
||||||
.env("INITIAL_AGENT_MODE", "read-only"))
|
.env("INITIAL_AGENT_MODE", "read-only")
|
||||||
|
.codex_path(codex))
|
||||||
}
|
}
|
||||||
Self::OpenCode => {
|
Self::OpenCode => {
|
||||||
if let Some(command) = resolve("opencode") {
|
if let Some(command) = resolve("opencode") {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::time::Duration;
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codex_preset_is_version_pinned() {
|
fn codex_preset_uses_the_adapter_with_npx() {
|
||||||
let launch = AcpAgentPreset::Codex.launch_config();
|
let launch = AcpAgentPreset::Codex.launch_config();
|
||||||
|
|
||||||
assert_eq!(launch.command, PathBuf::from("npx"));
|
assert_eq!(launch.command, PathBuf::from("npx"));
|
||||||
@@ -62,20 +62,28 @@ fn resolved_opencode_prefers_the_native_executable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolved_codex_falls_back_to_bun_compatibility_mode() {
|
fn resolved_codex_uses_npx_adapter_and_local_cli() {
|
||||||
let resolve = |command: &str| (command == "bunx").then(|| PathBuf::from("/opt/bin/bunx"));
|
let resolve = |command: &str| match command {
|
||||||
|
"npx" => Some(PathBuf::from("/opt/bin/npx")),
|
||||||
|
"codex" => Some(PathBuf::from("/opt/homebrew/bin/codex")),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
let codex = AcpAgentPreset::Codex
|
let codex = AcpAgentPreset::Codex
|
||||||
.resolve_launch_config_with(resolve)
|
.resolve_launch_config_with(resolve)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(codex.command, PathBuf::from("/opt/bin/bunx"));
|
assert_eq!(codex.command, PathBuf::from("/opt/bin/npx"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
codex.args,
|
codex.args,
|
||||||
vec![
|
vec![
|
||||||
"--bun".to_owned(),
|
"--yes".to_owned(),
|
||||||
format!("@agentclientprotocol/codex-acp@{CODEX_ACP_NPM_VERSION}")
|
format!("@agentclientprotocol/codex-acp@{CODEX_ACP_NPM_VERSION}")
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
codex.env.get("CODEX_PATH").map(String::as_str),
|
||||||
|
Some("/opt/homebrew/bin/codex")
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
codex.env.get("INITIAL_AGENT_MODE").map(String::as_str),
|
codex.env.get("INITIAL_AGENT_MODE").map(String::as_str),
|
||||||
Some("read-only")
|
Some("read-only")
|
||||||
@@ -89,13 +97,32 @@ fn resolved_codex_falls_back_to_bun_compatibility_mode() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolved_codex_falls_back_to_local_adapter_without_npx() {
|
||||||
|
let resolve = |command: &str| match command {
|
||||||
|
"codex" => Some(PathBuf::from("/opt/homebrew/bin/codex")),
|
||||||
|
"codex-acp" => Some(PathBuf::from("/opt/bin/codex-acp")),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let codex = AcpAgentPreset::Codex
|
||||||
|
.resolve_launch_config_with(resolve)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(codex.command, PathBuf::from("/opt/bin/codex-acp"));
|
||||||
|
assert!(codex.args.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
codex.env.get("CODEX_PATH").map(String::as_str),
|
||||||
|
Some("/opt/homebrew/bin/codex")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolved_presets_explain_missing_launchers() {
|
fn resolved_presets_explain_missing_launchers() {
|
||||||
let error = AcpAgentPreset::Codex
|
let error = AcpAgentPreset::Codex
|
||||||
.resolve_launch_config_with(|_| None)
|
.resolve_launch_config_with(|_| None)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(error.contains("requires npx or bunx"));
|
assert!(error.contains("requires the locally installed codex CLI"));
|
||||||
|
|
||||||
let opencode_error = AcpAgentPreset::OpenCode
|
let opencode_error = AcpAgentPreset::OpenCode
|
||||||
.resolve_launch_config_with(|command| {
|
.resolve_launch_config_with(|command| {
|
||||||
@@ -103,6 +130,13 @@ fn resolved_presets_explain_missing_launchers() {
|
|||||||
})
|
})
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(opencode_error.contains("requires the opencode executable or npx"));
|
assert!(opencode_error.contains("requires the opencode executable or npx"));
|
||||||
|
|
||||||
|
let codex_adapter_error = AcpAgentPreset::Codex
|
||||||
|
.resolve_launch_config_with(|command| {
|
||||||
|
(command == "codex").then(|| PathBuf::from("/opt/homebrew/bin/codex"))
|
||||||
|
})
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(codex_adapter_error.contains("requires either a local codex-acp executable or npx"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ pub use agent_runtime::{
|
|||||||
AcpAgentRuntime, AcpAgentRuntimeConfig, AcpRuntimeState, AcpRuntimeStateHandle,
|
AcpAgentRuntime, AcpAgentRuntimeConfig, AcpRuntimeState, AcpRuntimeStateHandle,
|
||||||
};
|
};
|
||||||
pub use config::{
|
pub use config::{
|
||||||
AcpAgentPreset, AcpLaunchConfig, AcpManagerConfig, CODEX_ACP_NPM_VERSION, OPENCODE_NPM_VERSION,
|
known_acp_agents, resolve_known_acp_agent, AcpAgentPreset, AcpKnownAgent, AcpLaunchConfig,
|
||||||
|
AcpManagerConfig, CODEX_ACP_NPM_VERSION, KNOWN_ACP_AGENTS, OPENCODE_NPM_VERSION,
|
||||||
};
|
};
|
||||||
pub use events::AcpEvent;
|
pub use events::AcpEvent;
|
||||||
pub use permissions::{
|
pub use permissions::{
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ impl AgentRuntime for ChatGPTSubscriptionRuntime {
|
|||||||
request,
|
request,
|
||||||
self.config.max_output_tokens,
|
self.config.max_output_tokens,
|
||||||
true,
|
true,
|
||||||
false,
|
true,
|
||||||
additional_params,
|
additional_params,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ fn build_completion_request(
|
|||||||
request,
|
request,
|
||||||
configured_max_output_tokens,
|
configured_max_output_tokens,
|
||||||
supports_system_messages,
|
supports_system_messages,
|
||||||
false,
|
true,
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"stream_options": { "include_usage": true }
|
"stream_options": { "include_usage": true }
|
||||||
})),
|
})),
|
||||||
|
|||||||
Reference in New Issue
Block a user