mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): show all ACP runtimes with install status and install buttons
The "Preferred runtime" dropdown in Agents > Add Persona and the Doctor panel only showed runtimes with a resolved ACP adapter binary, silently hiding Claude Code and Codex when their adapters weren't installed. Add three-state availability detection (Available / AdapterMissing / NotInstalled) to distinguish "CLI present but adapter missing" from "nothing installed." Expose a full provider catalog via `discover_all_acp_providers` and an `install_acp_runtime` command that runs server-defined install scripts in a login shell with a 5-minute timeout. Doctor panel now shows all four known runtimes with status badges and Install buttons. PersonaDialog shows all runtimes with status labels so users can store a preference for later. CreateAgentDialog shows a hint when additional runtimes are available to install.
This commit is contained in:
@@ -3,9 +3,9 @@ use tauri::{AppHandle, State};
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
managed_agents::{
|
||||
command_availability, discover_local_acp_providers, AcpProviderInfo,
|
||||
DiscoverManagedAgentPrereqsRequest, ManagedAgentPrereqsInfo, RelayAgentInfo,
|
||||
DEFAULT_ACP_COMMAND, DEFAULT_MCP_COMMAND,
|
||||
command_availability, discover_local_acp_providers, AcpProviderCatalogEntry, AcpProviderInfo,
|
||||
DiscoverManagedAgentPrereqsRequest, InstallRuntimeResult, InstallStepResult,
|
||||
ManagedAgentPrereqsInfo, RelayAgentInfo, DEFAULT_ACP_COMMAND, DEFAULT_MCP_COMMAND,
|
||||
},
|
||||
nostr_convert,
|
||||
relay::query_relay,
|
||||
@@ -16,6 +16,173 @@ pub fn discover_acp_providers() -> Vec<AcpProviderInfo> {
|
||||
discover_local_acp_providers()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn discover_all_acp_providers() -> Vec<AcpProviderCatalogEntry> {
|
||||
crate::managed_agents::discover_all_acp_providers()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_acp_runtime(provider_id: String) -> Result<InstallRuntimeResult, String> {
|
||||
tokio::task::spawn_blocking(move || install_acp_runtime_blocking(&provider_id))
|
||||
.await
|
||||
.map_err(|e| format!("install task panicked: {e}"))?
|
||||
}
|
||||
|
||||
fn install_acp_runtime_blocking(provider_id: &str) -> Result<InstallRuntimeResult, String> {
|
||||
let provider = crate::managed_agents::known_acp_provider(provider_id)
|
||||
.ok_or_else(|| format!("unknown provider: {provider_id}"))?;
|
||||
|
||||
let mut steps = Vec::new();
|
||||
|
||||
// Phase 1: Install CLI if missing and commands are available.
|
||||
if let Some(cli) = provider.underlying_cli {
|
||||
if crate::managed_agents::resolve_command(cli, None).is_none() {
|
||||
for cmd in provider.cli_install_commands {
|
||||
let result = run_install_command("cli", cmd);
|
||||
let success = result.success;
|
||||
steps.push(result);
|
||||
if !success {
|
||||
return Ok(InstallRuntimeResult {
|
||||
success: false,
|
||||
steps,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Install adapter if missing and commands are available.
|
||||
let adapter_found = provider
|
||||
.commands
|
||||
.iter()
|
||||
.any(|cmd| crate::managed_agents::resolve_command(cmd, None).is_some());
|
||||
if !adapter_found {
|
||||
for cmd in provider.adapter_install_commands {
|
||||
let result = run_install_command("adapter", cmd);
|
||||
let success = result.success;
|
||||
steps.push(result);
|
||||
if !success {
|
||||
return Ok(InstallRuntimeResult {
|
||||
success: false,
|
||||
steps,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the resolve cache so the next discovery picks up new binaries.
|
||||
crate::managed_agents::clear_resolve_cache();
|
||||
|
||||
Ok(InstallRuntimeResult {
|
||||
success: true,
|
||||
steps,
|
||||
})
|
||||
}
|
||||
|
||||
fn run_install_command(step: &str, command: &str) -> InstallStepResult {
|
||||
let shell_path = crate::managed_agents::login_shell_path();
|
||||
let shell = if std::path::Path::new("/bin/zsh").exists() {
|
||||
"/bin/zsh"
|
||||
} else {
|
||||
"/bin/bash"
|
||||
};
|
||||
|
||||
let mut cmd = std::process::Command::new(shell);
|
||||
cmd.args(["-l", "-c", command]);
|
||||
|
||||
if let Some(ref path) = shell_path {
|
||||
cmd.env("PATH", path);
|
||||
}
|
||||
|
||||
let mut child = match cmd
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => child,
|
||||
Err(e) => {
|
||||
return InstallStepResult {
|
||||
step: step.to_string(),
|
||||
command: command.to_string(),
|
||||
success: false,
|
||||
stdout: String::new(),
|
||||
stderr: format!("failed to spawn shell: {e}"),
|
||||
exit_code: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 5-minute timeout for install commands.
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.map(|mut s| {
|
||||
let mut buf = String::new();
|
||||
let _ = std::io::Read::read_to_string(&mut s, &mut buf);
|
||||
buf
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let stderr_raw = child
|
||||
.stderr
|
||||
.take()
|
||||
.map(|mut s| {
|
||||
let mut buf = String::new();
|
||||
let _ = std::io::Read::read_to_string(&mut s, &mut buf);
|
||||
buf
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let stderr = truncate_output(stderr_raw);
|
||||
return InstallStepResult {
|
||||
step: step.to_string(),
|
||||
command: command.to_string(),
|
||||
success: status.success(),
|
||||
stdout: truncate_output(stdout),
|
||||
stderr,
|
||||
exit_code: status.code(),
|
||||
};
|
||||
}
|
||||
Ok(None) => {
|
||||
if std::time::Instant::now() >= deadline {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return InstallStepResult {
|
||||
step: step.to_string(),
|
||||
command: command.to_string(),
|
||||
success: false,
|
||||
stdout: String::new(),
|
||||
stderr: "install command timed out after 5 minutes".to_string(),
|
||||
exit_code: None,
|
||||
};
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
}
|
||||
Err(e) => {
|
||||
return InstallStepResult {
|
||||
step: step.to_string(),
|
||||
command: command.to_string(),
|
||||
success: false,
|
||||
stdout: String::new(),
|
||||
stderr: format!("failed to check process status: {e}"),
|
||||
exit_code: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cap output at 2 KB to avoid flooding the UI with large error dumps.
|
||||
fn truncate_output(s: String) -> String {
|
||||
if s.len() > 2048 {
|
||||
format!("{}... (truncated)", &s[..2048])
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn discover_managed_agent_prereqs(
|
||||
input: DiscoverManagedAgentPrereqsRequest,
|
||||
|
||||
@@ -466,6 +466,8 @@ pub fn run() {
|
||||
get_relay_http_url,
|
||||
get_media_proxy_port,
|
||||
discover_acp_providers,
|
||||
discover_all_acp_providers,
|
||||
install_acp_runtime,
|
||||
discover_managed_agent_prereqs,
|
||||
sign_event,
|
||||
decrypt_observer_event,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::process::Command;
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::managed_agents::{AcpProviderInfo, CommandAvailabilityInfo};
|
||||
use crate::managed_agents::{AcpAvailabilityStatus, AcpProviderCatalogEntry, AcpProviderInfo, CommandAvailabilityInfo};
|
||||
|
||||
pub(crate) struct KnownAcpProvider {
|
||||
pub id: &'static str,
|
||||
@@ -15,6 +15,16 @@ pub(crate) struct KnownAcpProvider {
|
||||
pub mcp_command: Option<&'static str>,
|
||||
/// Whether to enable MCP hook tools (`_Stop`, `_PostCompact`) for this agent.
|
||||
pub mcp_hooks: bool,
|
||||
/// CLI binary that indicates partial install (e.g. `"claude"` when `claude-agent-acp` is missing).
|
||||
pub underlying_cli: Option<&'static str>,
|
||||
/// Shell commands to install the runtime CLI itself (run sequentially).
|
||||
pub cli_install_commands: &'static [&'static str],
|
||||
/// Shell commands to install the ACP adapter (run sequentially, after CLI).
|
||||
pub adapter_install_commands: &'static [&'static str],
|
||||
/// Link to docs/repo for manual instructions.
|
||||
pub install_instructions_url: &'static str,
|
||||
/// Human-readable guidance for the UI.
|
||||
pub install_hint: &'static str,
|
||||
}
|
||||
|
||||
const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png";
|
||||
@@ -54,6 +64,11 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[
|
||||
avatar_url: GOOSE_AVATAR_URL,
|
||||
mcp_command: None,
|
||||
mcp_hooks: false,
|
||||
underlying_cli: None,
|
||||
cli_install_commands: &[],
|
||||
adapter_install_commands: &["curl -fsSL https://github.com/block-open-source/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash"],
|
||||
install_instructions_url: "https://block.github.io/goose/",
|
||||
install_hint: "Install Goose via the official install script.",
|
||||
},
|
||||
KnownAcpProvider {
|
||||
id: "claude",
|
||||
@@ -63,6 +78,11 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[
|
||||
avatar_url: CLAUDE_CODE_AVATAR_URL,
|
||||
mcp_command: None,
|
||||
mcp_hooks: false,
|
||||
underlying_cli: Some("claude"),
|
||||
cli_install_commands: &["npm install -g @anthropic-ai/claude-code"],
|
||||
adapter_install_commands: &["npm install -g @anthropic-ai/claude-agent-acp"],
|
||||
install_instructions_url: "https://www.npmjs.com/package/@anthropic-ai/claude-agent-acp",
|
||||
install_hint: "Install the Claude Code ACP adapter via npm.",
|
||||
},
|
||||
KnownAcpProvider {
|
||||
id: "codex",
|
||||
@@ -72,6 +92,11 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[
|
||||
avatar_url: CODEX_AVATAR_URL,
|
||||
mcp_command: None,
|
||||
mcp_hooks: false,
|
||||
underlying_cli: Some("codex"),
|
||||
cli_install_commands: &[],
|
||||
adapter_install_commands: &[],
|
||||
install_instructions_url: "https://github.com/openai/codex",
|
||||
install_hint: "The codex-acp adapter must be built from source. See the GitHub repo.",
|
||||
},
|
||||
KnownAcpProvider {
|
||||
id: "sprout-agent",
|
||||
@@ -81,6 +106,11 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[
|
||||
avatar_url: SPROUT_AGENT_AVATAR_URL,
|
||||
mcp_command: Some("sprout-dev-mcp"),
|
||||
mcp_hooks: true,
|
||||
underlying_cli: None,
|
||||
cli_install_commands: &[],
|
||||
adapter_install_commands: &[],
|
||||
install_instructions_url: "https://github.com/block/sprout",
|
||||
install_hint: "Ships with the Sprout desktop app.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -219,15 +249,18 @@ fn resolve_workspace_command(command: &str, app: Option<&AppHandle>) -> Option<P
|
||||
.find(|candidate| candidate.exists())
|
||||
}
|
||||
|
||||
fn resolve_cache() -> &'static std::sync::Mutex<std::collections::HashMap<String, Option<PathBuf>>> {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
static CACHE: OnceLock<Mutex<HashMap<String, Option<PathBuf>>>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// Resolve a command to an absolute path, caching results for the app lifetime.
|
||||
/// The cache eliminates redundant login-shell spawns when multiple agents share
|
||||
/// the same binaries (e.g. `npx`, `uvx`).
|
||||
pub fn resolve_command(command: &str, app: Option<&AppHandle>) -> Option<PathBuf> {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
static CACHE: OnceLock<Mutex<HashMap<String, Option<PathBuf>>>> = OnceLock::new();
|
||||
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
let cache = resolve_cache();
|
||||
|
||||
// Fast path: return cached result without allocating a key.
|
||||
if let Ok(guard) = cache.lock() {
|
||||
@@ -248,6 +281,12 @@ pub fn resolve_command(command: &str, app: Option<&AppHandle>) -> Option<PathBuf
|
||||
result
|
||||
}
|
||||
|
||||
/// Clear the resolve_command cache so that newly-installed binaries are detected.
|
||||
pub fn clear_resolve_cache() {
|
||||
let mut guard = resolve_cache().lock().unwrap_or_else(|e| e.into_inner());
|
||||
guard.clear();
|
||||
}
|
||||
|
||||
fn resolve_command_uncached(command: &str, app: Option<&AppHandle>) -> Option<PathBuf> {
|
||||
if let Some(path) = resolve_workspace_command(command, app) {
|
||||
return Some(path);
|
||||
@@ -369,6 +408,63 @@ pub fn discover_local_acp_providers() -> Vec<AcpProviderInfo> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn discover_all_acp_providers() -> Vec<AcpProviderCatalogEntry> {
|
||||
KNOWN_ACP_PROVIDERS
|
||||
.iter()
|
||||
.map(|provider| {
|
||||
// Try to find the ACP adapter binary.
|
||||
let adapter_result = provider
|
||||
.commands
|
||||
.iter()
|
||||
.find_map(|command| find_command(command).map(|path| (*command, path)));
|
||||
|
||||
let (availability, command, binary_path) = if let Some((cmd, path)) = adapter_result {
|
||||
(
|
||||
AcpAvailabilityStatus::Available,
|
||||
Some(cmd.to_string()),
|
||||
Some(path.display().to_string()),
|
||||
)
|
||||
} else if let Some(cli) = provider.underlying_cli {
|
||||
if find_command(cli).is_some() {
|
||||
(AcpAvailabilityStatus::AdapterMissing, None, None)
|
||||
} else {
|
||||
(AcpAvailabilityStatus::NotInstalled, None, None)
|
||||
}
|
||||
} else {
|
||||
(AcpAvailabilityStatus::NotInstalled, None, None)
|
||||
};
|
||||
|
||||
let underlying_cli_path = provider
|
||||
.underlying_cli
|
||||
.and_then(|cli| find_command(cli))
|
||||
.map(|p| p.display().to_string());
|
||||
|
||||
let default_args = command
|
||||
.as_deref()
|
||||
.map(|cmd| normalize_agent_args(cmd, Vec::new()))
|
||||
.unwrap_or_default();
|
||||
|
||||
let can_auto_install = !provider.cli_install_commands.is_empty()
|
||||
|| !provider.adapter_install_commands.is_empty();
|
||||
|
||||
AcpProviderCatalogEntry {
|
||||
id: provider.id.to_string(),
|
||||
label: provider.label.to_string(),
|
||||
avatar_url: provider.avatar_url.to_string(),
|
||||
availability,
|
||||
command,
|
||||
binary_path,
|
||||
default_args,
|
||||
mcp_command: provider.mcp_command.map(str::to_string),
|
||||
install_hint: provider.install_hint.to_string(),
|
||||
install_instructions_url: provider.install_instructions_url.to_string(),
|
||||
can_auto_install,
|
||||
underlying_cli_path,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn managed_agent_avatar_url(command: &str) -> Option<String> {
|
||||
let provider = known_acp_provider(command)?;
|
||||
Some(provider.avatar_url.to_string())
|
||||
|
||||
@@ -286,6 +286,49 @@ pub struct AcpProviderInfo {
|
||||
pub mcp_command: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AcpAvailabilityStatus {
|
||||
Available,
|
||||
AdapterMissing,
|
||||
NotInstalled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AcpProviderCatalogEntry {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub avatar_url: String,
|
||||
pub availability: AcpAvailabilityStatus,
|
||||
pub command: Option<String>,
|
||||
pub binary_path: Option<String>,
|
||||
pub default_args: Vec<String>,
|
||||
pub mcp_command: Option<String>,
|
||||
pub install_hint: String,
|
||||
pub install_instructions_url: String,
|
||||
/// true when at least one automated install step is available
|
||||
pub can_auto_install: bool,
|
||||
pub underlying_cli_path: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of a single install step (CLI or adapter).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct InstallStepResult {
|
||||
pub step: String,
|
||||
pub command: String,
|
||||
pub success: bool,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub exit_code: Option<i32>,
|
||||
}
|
||||
|
||||
/// Aggregate result of installing a runtime (may include CLI + adapter steps).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct InstallRuntimeResult {
|
||||
pub success: bool,
|
||||
pub steps: Vec<InstallStepResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CommandAvailabilityInfo {
|
||||
pub command: String,
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
createManagedAgent,
|
||||
deleteManagedAgent,
|
||||
discoverAcpProviders,
|
||||
discoverAllAcpProviders,
|
||||
discoverBackendProviders,
|
||||
discoverManagedAgentPrereqs,
|
||||
getManagedAgentLog,
|
||||
installAcpRuntime,
|
||||
listManagedAgents,
|
||||
listRelayAgents,
|
||||
startManagedAgent,
|
||||
@@ -72,6 +74,7 @@ export const managedAgentsQueryKey = ["managed-agents"] as const;
|
||||
export const personasQueryKey = ["personas"] as const;
|
||||
export const teamsQueryKey = ["teams"] as const;
|
||||
export const acpProvidersQueryKey = ["acp-providers"] as const;
|
||||
export const allAcpProvidersQueryKey = ["all-acp-providers"] as const;
|
||||
export const managedAgentPrereqsQueryKey = ["managed-agent-prereqs"] as const;
|
||||
export const backendProvidersQueryKey = ["backend-providers"] as const;
|
||||
|
||||
@@ -105,6 +108,27 @@ export function useAcpProvidersQuery() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useAllAcpProvidersQuery() {
|
||||
return useQuery({
|
||||
queryKey: allAcpProvidersQueryKey,
|
||||
queryFn: discoverAllAcpProviders,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstallAcpRuntimeMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (providerId: string) => installAcpRuntime(providerId),
|
||||
onSettled: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: acpProvidersQueryKey });
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: allAcpProvidersQueryKey,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useBackendProvidersQuery() {
|
||||
return useQuery({
|
||||
queryKey: backendProvidersQueryKey,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AcpProvider, ManagedAgentPrereqs } from "@/shared/api/types";
|
||||
import { useAllAcpProvidersQuery } from "@/features/agents/hooks";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
@@ -48,6 +49,11 @@ export function CreateAgentRuntimeProviderField({
|
||||
selectedProviderId: string;
|
||||
onProviderChange: (value: string) => void;
|
||||
}) {
|
||||
const allProvidersQuery = useAllAcpProvidersQuery();
|
||||
const unavailableCount = (allProvidersQuery.data ?? []).filter(
|
||||
(p) => p.availability !== "available",
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium" htmlFor="agent-provider">
|
||||
@@ -86,6 +92,14 @@ export function CreateAgentRuntimeProviderField({
|
||||
command in Advanced setup.
|
||||
</p>
|
||||
)}
|
||||
{unavailableCount > 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{unavailableCount} additional{" "}
|
||||
{unavailableCount === 1 ? "runtime" : "runtimes"} available to
|
||||
install. Visit Settings > Doctor to set{" "}
|
||||
{unavailableCount === 1 ? "it" : "them"} up.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import { RefreshCw, Upload } from "lucide-react";
|
||||
|
||||
import type {
|
||||
AcpProvider,
|
||||
AcpProviderCatalogEntry,
|
||||
CreatePersonaInput,
|
||||
UpdatePersonaInput,
|
||||
} from "@/shared/api/types";
|
||||
@@ -35,7 +35,7 @@ type PersonaDialogProps = {
|
||||
error: Error | null;
|
||||
isPending: boolean;
|
||||
isImportPending?: boolean;
|
||||
providers: AcpProvider[];
|
||||
providers: AcpProviderCatalogEntry[];
|
||||
providersLoading?: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubmit: (input: CreatePersonaInput | UpdatePersonaInput) => Promise<void>;
|
||||
@@ -354,13 +354,32 @@ export function PersonaDialog({
|
||||
{providers.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.label}
|
||||
{p.availability === "adapter_missing"
|
||||
? " (adapter missing)"
|
||||
: p.availability === "not_installed"
|
||||
? " (not installed)"
|
||||
: ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Optional. When deploying this persona, the selected runtime will
|
||||
be pre-selected. Falls back to the default if unavailable.
|
||||
be pre-selected. Unavailable runtimes can be installed from
|
||||
Settings > Doctor.
|
||||
</p>
|
||||
{(() => {
|
||||
const selected = providers.find((p) => p.id === provider);
|
||||
if (!selected || selected.availability === "available")
|
||||
return null;
|
||||
return (
|
||||
<p className="text-xs text-warning">
|
||||
{selected.availability === "adapter_missing"
|
||||
? `${selected.label} CLI is installed but the ACP adapter is missing.`
|
||||
: `${selected.label} is not installed.`}{" "}
|
||||
Visit Settings > Doctor to set it up.
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
personasQueryKey,
|
||||
useAcpProvidersQuery,
|
||||
useAllAcpProvidersQuery,
|
||||
useCreatePersonaMutation,
|
||||
useDeletePersonaMutation,
|
||||
useExportPersonaJsonMutation,
|
||||
@@ -36,7 +36,7 @@ type PersonaFeedbackSurface = "catalog" | "library";
|
||||
export function usePersonaActions() {
|
||||
const queryClient = useQueryClient();
|
||||
const personasQuery = usePersonasQuery();
|
||||
const acpProvidersQuery = useAcpProvidersQuery();
|
||||
const acpProvidersQuery = useAllAcpProvidersQuery();
|
||||
const createPersonaMutation = useCreatePersonaMutation();
|
||||
const updatePersonaMutation = useUpdatePersonaMutation();
|
||||
const deletePersonaMutation = useDeletePersonaMutation();
|
||||
|
||||
@@ -1,73 +1,230 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
Download,
|
||||
ExternalLink,
|
||||
RefreshCw,
|
||||
Stethoscope,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useAcpProvidersQuery } from "@/features/agents/hooks";
|
||||
import {
|
||||
useAllAcpProvidersQuery,
|
||||
useInstallAcpRuntimeMutation,
|
||||
} from "@/features/agents/hooks";
|
||||
import { describeResolvedCommand } from "@/features/agents/ui/agentUi";
|
||||
import type { AcpProviderCatalogEntry } from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
|
||||
function StatusIcon({ available }: { available: boolean }) {
|
||||
return available ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-status-added" />
|
||||
) : (
|
||||
<AlertTriangle className="h-4 w-4 text-warning" />
|
||||
function StatusIcon({
|
||||
availability,
|
||||
}: {
|
||||
availability: AcpProviderCatalogEntry["availability"];
|
||||
}) {
|
||||
switch (availability) {
|
||||
case "available":
|
||||
return <CheckCircle2 className="h-4 w-4 text-status-added" />;
|
||||
case "adapter_missing":
|
||||
return <AlertTriangle className="h-4 w-4 text-warning" />;
|
||||
case "not_installed":
|
||||
return <Circle className="h-4 w-4 text-muted-foreground/50" />;
|
||||
}
|
||||
}
|
||||
|
||||
function InstallActions({
|
||||
isInstalling,
|
||||
onInstall,
|
||||
provider,
|
||||
}: {
|
||||
isInstalling: boolean;
|
||||
onInstall: () => void;
|
||||
provider: AcpProviderCatalogEntry;
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
{provider.canAutoInstall ? (
|
||||
<Button
|
||||
disabled={isInstalling}
|
||||
onClick={onInstall}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{isInstalling ? (
|
||||
<RefreshCw className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{isInstalling ? "Installing..." : "Install"}
|
||||
</Button>
|
||||
) : null}
|
||||
<button
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
||||
onClick={() => window.open(provider.installInstructionsUrl, "_blank")}
|
||||
type="button"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
View instructions
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderRow({
|
||||
command,
|
||||
defaultArgs,
|
||||
label,
|
||||
providerId,
|
||||
resolvedPath,
|
||||
installError,
|
||||
installSuccess,
|
||||
isInstalling,
|
||||
onInstall,
|
||||
provider,
|
||||
}: {
|
||||
command: string;
|
||||
defaultArgs: string[];
|
||||
label: string;
|
||||
providerId: string;
|
||||
resolvedPath: string;
|
||||
installError: string | null;
|
||||
installSuccess: boolean;
|
||||
isInstalling: boolean;
|
||||
onInstall: () => void;
|
||||
provider: AcpProviderCatalogEntry;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-start gap-3 rounded-xl border border-border/70 bg-background/80 px-4 py-3"
|
||||
data-testid={`doctor-provider-${providerId}`}
|
||||
className={cn(
|
||||
"flex items-start gap-3 rounded-xl border px-4 py-3",
|
||||
provider.availability === "available"
|
||||
? "border-border/70 bg-background/80"
|
||||
: provider.availability === "adapter_missing"
|
||||
? "border-amber-500/30 bg-amber-500/5"
|
||||
: "border-border/50 bg-muted/30",
|
||||
)}
|
||||
data-testid={`doctor-provider-${provider.id}`}
|
||||
>
|
||||
<div className="mt-0.5 shrink-0">
|
||||
<StatusIcon available={true} />
|
||||
<StatusIcon availability={provider.availability} />
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-sm font-semibold tracking-tight">{label}</p>
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 text-[11px]">
|
||||
{command}
|
||||
</code>
|
||||
<p className="text-sm font-semibold tracking-tight">
|
||||
{provider.label}
|
||||
</p>
|
||||
{provider.command ? (
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 text-[11px]">
|
||||
{provider.command}
|
||||
</code>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Available via {describeResolvedCommand(command, resolvedPath)}.
|
||||
</p>
|
||||
{defaultArgs.length > 0 ? (
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
Default args:{" "}
|
||||
<code className="font-mono">{defaultArgs.join(", ")}</code>
|
||||
|
||||
{provider.availability === "available" &&
|
||||
provider.command &&
|
||||
provider.binaryPath ? (
|
||||
<>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Available via{" "}
|
||||
{describeResolvedCommand(provider.command, provider.binaryPath)}.
|
||||
</p>
|
||||
{provider.defaultArgs.length > 0 ? (
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
Default args:{" "}
|
||||
<code className="font-mono">
|
||||
{provider.defaultArgs.join(", ")}
|
||||
</code>
|
||||
</p>
|
||||
) : null}
|
||||
<p className="mt-1 break-all font-mono text-[11px] text-muted-foreground/80">
|
||||
{provider.binaryPath}
|
||||
</p>
|
||||
</>
|
||||
) : provider.availability === "adapter_missing" ? (
|
||||
<>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
CLI detected at{" "}
|
||||
<code className="rounded bg-muted px-1 py-0.5 text-[11px]">
|
||||
{provider.underlyingCliPath ?? "unknown path"}
|
||||
</code>{" "}
|
||||
but ACP adapter not found.
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{provider.installHint}
|
||||
</p>
|
||||
<InstallActions
|
||||
isInstalling={isInstalling}
|
||||
onInstall={onInstall}
|
||||
provider={provider}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Not installed</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{provider.installHint}
|
||||
</p>
|
||||
<InstallActions
|
||||
isInstalling={isInstalling}
|
||||
onInstall={onInstall}
|
||||
provider={provider}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{installSuccess ? (
|
||||
<p className="mt-2 rounded-lg border border-green-500/30 bg-green-500/10 px-3 py-1.5 text-xs text-green-700 dark:text-green-400">
|
||||
Installed successfully. Re-run Doctor to verify.
|
||||
</p>
|
||||
) : null}
|
||||
{installError ? (
|
||||
<p className="mt-2 rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-1.5 text-xs text-destructive">
|
||||
{installError}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="mt-1 break-all font-mono text-[11px] text-muted-foreground/80">
|
||||
{resolvedPath}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DoctorSettingsPanel() {
|
||||
const providersQuery = useAcpProvidersQuery();
|
||||
const providersQuery = useAllAcpProvidersQuery();
|
||||
const providers = providersQuery.data ?? [];
|
||||
const isRefreshing = providersQuery.isFetching;
|
||||
const installMutation = useInstallAcpRuntimeMutation();
|
||||
const [installResults, setInstallResults] = React.useState<
|
||||
Record<string, { success: boolean; error: string | null }>
|
||||
>({});
|
||||
|
||||
function handleInstall(providerId: string) {
|
||||
setInstallResults((prev) => ({
|
||||
...prev,
|
||||
[providerId]: { success: false, error: null },
|
||||
}));
|
||||
installMutation.mutate(providerId, {
|
||||
onSuccess: (result) => {
|
||||
if (result.success) {
|
||||
setInstallResults((prev) => ({
|
||||
...prev,
|
||||
[providerId]: { success: true, error: null },
|
||||
}));
|
||||
} else {
|
||||
const lastStep = result.steps[result.steps.length - 1];
|
||||
setInstallResults((prev) => ({
|
||||
...prev,
|
||||
[providerId]: {
|
||||
success: false,
|
||||
error: lastStep
|
||||
? `Step "${lastStep.step}" failed: ${lastStep.stderr || "unknown error"}`
|
||||
: "Install failed with no output.",
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setInstallResults((prev) => ({
|
||||
...prev,
|
||||
[providerId]: {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Install failed.",
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-5" data-testid="settings-doctor">
|
||||
@@ -86,6 +243,7 @@ export function DoctorSettingsPanel() {
|
||||
className="shrink-0"
|
||||
disabled={isRefreshing}
|
||||
onClick={() => {
|
||||
setInstallResults({});
|
||||
void providersQuery.refetch();
|
||||
}}
|
||||
size="sm"
|
||||
@@ -103,29 +261,31 @@ export function DoctorSettingsPanel() {
|
||||
<div className="rounded-xl border border-border/70 bg-muted/20 p-4">
|
||||
<h3 className="text-sm font-semibold tracking-tight">ACP runtimes</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Installed runtimes that the desktop app can offer in Create agent.
|
||||
Known runtimes and their installation status.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 space-y-2">
|
||||
{providersQuery.isLoading ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Looking for installed ACP runtimes...
|
||||
Looking for ACP runtimes...
|
||||
</p>
|
||||
) : providers.length > 0 ? (
|
||||
providers.map((provider) => (
|
||||
<ProviderRow
|
||||
command={provider.command}
|
||||
defaultArgs={provider.defaultArgs}
|
||||
installError={installResults[provider.id]?.error ?? null}
|
||||
installSuccess={installResults[provider.id]?.success ?? false}
|
||||
isInstalling={
|
||||
installMutation.isPending &&
|
||||
installMutation.variables === provider.id
|
||||
}
|
||||
key={provider.id}
|
||||
label={provider.label}
|
||||
providerId={provider.id}
|
||||
resolvedPath={provider.binaryPath}
|
||||
onInstall={() => handleInstall(provider.id)}
|
||||
provider={provider}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-warning">
|
||||
No known ACP runtime was detected on your PATH yet. You can
|
||||
still use a custom command in Create agent.
|
||||
No known ACP runtimes found.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -39,8 +39,11 @@ import type {
|
||||
CreateManagedAgentInput,
|
||||
AgentModelsResponse,
|
||||
UpdateManagedAgentInput,
|
||||
AcpAvailabilityStatus,
|
||||
AcpProvider,
|
||||
AcpProviderCatalogEntry,
|
||||
CommandAvailability,
|
||||
InstallRuntimeResult,
|
||||
OpenDmInput,
|
||||
} from "@/shared/api/types";
|
||||
|
||||
@@ -254,6 +257,35 @@ type RawAcpProvider = {
|
||||
mcp_command: string | null;
|
||||
};
|
||||
|
||||
type RawAcpProviderCatalogEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
avatar_url: string;
|
||||
availability: AcpAvailabilityStatus;
|
||||
command: string | null;
|
||||
binary_path: string | null;
|
||||
default_args: string[];
|
||||
mcp_command: string | null;
|
||||
install_hint: string;
|
||||
install_instructions_url: string;
|
||||
can_auto_install: boolean;
|
||||
underlying_cli_path: string | null;
|
||||
};
|
||||
|
||||
type RawInstallStepResult = {
|
||||
step: string;
|
||||
command: string;
|
||||
success: boolean;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exit_code: number | null;
|
||||
};
|
||||
|
||||
type RawInstallRuntimeResult = {
|
||||
success: boolean;
|
||||
steps: RawInstallStepResult[];
|
||||
};
|
||||
|
||||
type RawCommandAvailability = {
|
||||
command: string;
|
||||
resolved_path: string | null;
|
||||
@@ -861,6 +893,41 @@ function fromRawAcpProvider(provider: RawAcpProvider): AcpProvider {
|
||||
};
|
||||
}
|
||||
|
||||
function fromRawAcpProviderCatalogEntry(
|
||||
entry: RawAcpProviderCatalogEntry,
|
||||
): AcpProviderCatalogEntry {
|
||||
return {
|
||||
id: entry.id,
|
||||
label: entry.label,
|
||||
avatarUrl: entry.avatar_url,
|
||||
availability: entry.availability,
|
||||
command: entry.command,
|
||||
binaryPath: entry.binary_path,
|
||||
defaultArgs: entry.default_args,
|
||||
mcpCommand: entry.mcp_command,
|
||||
installHint: entry.install_hint,
|
||||
installInstructionsUrl: entry.install_instructions_url,
|
||||
canAutoInstall: entry.can_auto_install,
|
||||
underlyingCliPath: entry.underlying_cli_path,
|
||||
};
|
||||
}
|
||||
|
||||
function fromRawInstallRuntimeResult(
|
||||
raw: RawInstallRuntimeResult,
|
||||
): InstallRuntimeResult {
|
||||
return {
|
||||
success: raw.success,
|
||||
steps: raw.steps.map((step) => ({
|
||||
step: step.step,
|
||||
command: step.command,
|
||||
success: step.success,
|
||||
stdout: step.stdout,
|
||||
stderr: step.stderr,
|
||||
exitCode: step.exit_code,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function fromRawCommandAvailability(
|
||||
command: RawCommandAvailability,
|
||||
): CommandAvailability {
|
||||
@@ -1019,6 +1086,26 @@ export async function discoverAcpProviders(): Promise<AcpProvider[]> {
|
||||
);
|
||||
}
|
||||
|
||||
export async function discoverAllAcpProviders(): Promise<
|
||||
AcpProviderCatalogEntry[]
|
||||
> {
|
||||
return (
|
||||
await invokeTauri<RawAcpProviderCatalogEntry[]>(
|
||||
"discover_all_acp_providers",
|
||||
)
|
||||
).map(fromRawAcpProviderCatalogEntry);
|
||||
}
|
||||
|
||||
export async function installAcpRuntime(
|
||||
providerId: string,
|
||||
): Promise<InstallRuntimeResult> {
|
||||
const raw = await invokeTauri<RawInstallRuntimeResult>(
|
||||
"install_acp_runtime",
|
||||
{ providerId },
|
||||
);
|
||||
return fromRawInstallRuntimeResult(raw);
|
||||
}
|
||||
|
||||
export async function discoverManagedAgentPrereqs(input: {
|
||||
acpCommand?: string;
|
||||
mcpCommand?: string;
|
||||
|
||||
@@ -376,6 +376,40 @@ export type AcpProvider = {
|
||||
mcpCommand: string | null;
|
||||
};
|
||||
|
||||
export type AcpAvailabilityStatus =
|
||||
| "available"
|
||||
| "adapter_missing"
|
||||
| "not_installed";
|
||||
|
||||
export type AcpProviderCatalogEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
avatarUrl: string;
|
||||
availability: AcpAvailabilityStatus;
|
||||
command: string | null;
|
||||
binaryPath: string | null;
|
||||
defaultArgs: string[];
|
||||
mcpCommand: string | null;
|
||||
installHint: string;
|
||||
installInstructionsUrl: string;
|
||||
canAutoInstall: boolean;
|
||||
underlyingCliPath: string | null;
|
||||
};
|
||||
|
||||
export type InstallStepResult = {
|
||||
step: string;
|
||||
command: string;
|
||||
success: boolean;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number | null;
|
||||
};
|
||||
|
||||
export type InstallRuntimeResult = {
|
||||
success: boolean;
|
||||
steps: InstallStepResult[];
|
||||
};
|
||||
|
||||
export type CommandAvailability = {
|
||||
command: string;
|
||||
resolvedPath: string | null;
|
||||
|
||||
@@ -327,6 +327,33 @@ type RawAcpProvider = {
|
||||
mcp_command: string | null;
|
||||
};
|
||||
|
||||
type RawAcpProviderCatalogEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
avatar_url: string;
|
||||
availability: "available" | "adapter_missing" | "not_installed";
|
||||
command: string | null;
|
||||
binary_path: string | null;
|
||||
default_args: string[];
|
||||
mcp_command: string | null;
|
||||
install_hint: string;
|
||||
install_instructions_url: string;
|
||||
can_auto_install: boolean;
|
||||
underlying_cli_path: string | null;
|
||||
};
|
||||
|
||||
type RawInstallRuntimeResult = {
|
||||
success: boolean;
|
||||
steps: Array<{
|
||||
step: string;
|
||||
command: string;
|
||||
success: boolean;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exit_code: number | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
type RawCommandAvailability = {
|
||||
command: string;
|
||||
resolved_path: string | null;
|
||||
@@ -3501,6 +3528,89 @@ async function handleDiscoverAcpProviders(
|
||||
];
|
||||
}
|
||||
|
||||
async function handleDiscoverAllAcpProviders(
|
||||
_config: E2eConfig | undefined,
|
||||
): Promise<RawAcpProviderCatalogEntry[]> {
|
||||
return [
|
||||
{
|
||||
id: "goose",
|
||||
label: "Goose",
|
||||
avatar_url: "",
|
||||
availability: "available",
|
||||
command: "goose",
|
||||
binary_path: "/usr/local/bin/goose",
|
||||
default_args: ["acp"],
|
||||
mcp_command: null,
|
||||
install_hint: "Install Goose via the official install script.",
|
||||
install_instructions_url: "https://block.github.io/goose/",
|
||||
can_auto_install: true,
|
||||
underlying_cli_path: null,
|
||||
},
|
||||
{
|
||||
id: "claude",
|
||||
label: "Claude Code",
|
||||
avatar_url: "",
|
||||
availability: "adapter_missing",
|
||||
command: null,
|
||||
binary_path: null,
|
||||
default_args: [],
|
||||
mcp_command: null,
|
||||
install_hint: "Install the Claude Code ACP adapter via npm.",
|
||||
install_instructions_url:
|
||||
"https://www.npmjs.com/package/@anthropic-ai/claude-agent-acp",
|
||||
can_auto_install: true,
|
||||
underlying_cli_path: "/usr/local/bin/claude",
|
||||
},
|
||||
{
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
avatar_url: "",
|
||||
availability: "not_installed",
|
||||
command: null,
|
||||
binary_path: null,
|
||||
default_args: [],
|
||||
mcp_command: null,
|
||||
install_hint:
|
||||
"The codex-acp adapter must be built from source. See the GitHub repo.",
|
||||
install_instructions_url: "https://github.com/openai/codex",
|
||||
can_auto_install: false,
|
||||
underlying_cli_path: null,
|
||||
},
|
||||
{
|
||||
id: "sprout-agent",
|
||||
label: "Sprout Agent",
|
||||
avatar_url: "",
|
||||
availability: "available",
|
||||
command: "sprout-agent",
|
||||
binary_path: "/usr/local/bin/sprout-agent",
|
||||
default_args: [],
|
||||
mcp_command: "sprout-dev-mcp",
|
||||
install_hint: "Ships with the Sprout desktop app.",
|
||||
install_instructions_url: "https://github.com/block/sprout",
|
||||
can_auto_install: false,
|
||||
underlying_cli_path: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async function handleInstallAcpRuntime(args: {
|
||||
providerId?: string;
|
||||
}): Promise<RawInstallRuntimeResult> {
|
||||
return {
|
||||
success: true,
|
||||
steps: [
|
||||
{
|
||||
step: "adapter",
|
||||
command: `mock install ${args.providerId ?? "unknown"}`,
|
||||
success: true,
|
||||
stdout: "mock: installed successfully",
|
||||
stderr: "",
|
||||
exit_code: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function handleDiscoverManagedAgentPrereqs(
|
||||
args: {
|
||||
input?: {
|
||||
@@ -4673,6 +4783,10 @@ export function maybeInstallE2eTauriMocks() {
|
||||
return getRelayHttpUrl(activeConfig);
|
||||
case "discover_acp_providers":
|
||||
return handleDiscoverAcpProviders(activeConfig);
|
||||
case "discover_all_acp_providers":
|
||||
return handleDiscoverAllAcpProviders(activeConfig);
|
||||
case "install_acp_runtime":
|
||||
return handleInstallAcpRuntime(payload as { providerId?: string });
|
||||
case "discover_backend_providers":
|
||||
return [];
|
||||
case "probe_backend_provider":
|
||||
|
||||
Reference in New Issue
Block a user