commands to acp+ migration: extensions management (#8733)

This commit is contained in:
Alex Hancock
2026-04-22 15:23:02 -04:00
committed by GitHub
parent ee46794b18
commit e7e50320a1
13 changed files with 303 additions and 204 deletions
@@ -1,168 +0,0 @@
use serde_json::Value;
use tauri::State;
use crate::services::goose_config::GooseConfig;
fn yaml_to_json(yaml: serde_yaml::Value) -> Value {
match yaml {
serde_yaml::Value::Null => Value::Null,
serde_yaml::Value::Bool(b) => Value::Bool(b),
serde_yaml::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Number(i.into())
} else if let Some(u) = n.as_u64() {
Value::Number(u.into())
} else if let Some(f) = n.as_f64() {
serde_json::Number::from_f64(f)
.map(Value::Number)
.unwrap_or(Value::Null)
} else {
Value::Null
}
}
serde_yaml::Value::String(s) => Value::String(s),
serde_yaml::Value::Sequence(seq) => {
Value::Array(seq.into_iter().map(yaml_to_json).collect())
}
serde_yaml::Value::Mapping(map) => {
let obj = map
.into_iter()
.filter_map(|(k, v)| {
let key = match k {
serde_yaml::Value::String(s) => s,
other => serde_yaml::to_string(&other).ok()?.trim().to_string(),
};
Some((key, yaml_to_json(v)))
})
.collect();
Value::Object(obj)
}
serde_yaml::Value::Tagged(tagged) => yaml_to_json(tagged.value),
}
}
fn json_to_yaml(json: Value) -> serde_yaml::Value {
match json {
Value::Null => serde_yaml::Value::Null,
Value::Bool(b) => serde_yaml::Value::Bool(b),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
serde_yaml::Value::Number(i.into())
} else if let Some(u) = n.as_u64() {
serde_yaml::Value::Number(u.into())
} else if let Some(f) = n.as_f64() {
serde_yaml::Value::Number(f.into())
} else {
serde_yaml::Value::Null
}
}
Value::String(s) => serde_yaml::Value::String(s),
Value::Array(arr) => {
serde_yaml::Value::Sequence(arr.into_iter().map(json_to_yaml).collect())
}
Value::Object(obj) => {
let mut map = serde_yaml::Mapping::new();
for (k, v) in obj {
map.insert(serde_yaml::Value::String(k), json_to_yaml(v));
}
serde_yaml::Value::Mapping(map)
}
}
}
fn name_to_key(name: &str) -> String {
let mut result = String::with_capacity(name.len());
for c in name.chars() {
match c {
c if c.is_ascii_alphanumeric() || c == '_' || c == '-' => result.push(c),
c if c.is_whitespace() => continue,
_ => result.push('_'),
}
}
result.to_lowercase()
}
#[tauri::command]
pub fn list_extensions(config: State<'_, GooseConfig>) -> Result<Vec<Value>, String> {
let raw = config.get_extensions_raw();
let mut entries = Vec::with_capacity(raw.len());
for (k, v) in raw {
let key = match k {
serde_yaml::Value::String(s) => s,
_ => continue,
};
let mut json = yaml_to_json(v);
if let Value::Object(ref mut obj) = json {
if !obj.contains_key("type") {
continue;
}
obj.insert("config_key".to_string(), Value::String(key.clone()));
obj.entry("name".to_string())
.or_insert_with(|| Value::String(key));
obj.entry("enabled".to_string())
.or_insert(Value::Bool(false));
entries.push(json);
}
}
Ok(entries)
}
#[tauri::command]
pub fn add_extension(
name: String,
extension_config: Value,
enabled: bool,
config: State<'_, GooseConfig>,
) -> Result<(), String> {
let key = name_to_key(&name);
let mut raw = config.get_extensions_raw();
let mut entry = match extension_config {
Value::Object(obj) => obj,
_ => return Err("extension_config must be a JSON object".to_string()),
};
entry.insert("enabled".to_string(), Value::Bool(enabled));
entry.insert("name".to_string(), Value::String(name));
let yaml_value = json_to_yaml(Value::Object(entry));
raw.insert(serde_yaml::Value::String(key), yaml_value);
config.set_extensions_raw(raw)
}
#[tauri::command]
pub fn remove_extension(config_key: String, config: State<'_, GooseConfig>) -> Result<(), String> {
let mut raw = config.get_extensions_raw();
let yaml_key = serde_yaml::Value::String(config_key.clone());
if raw.remove(&yaml_key).is_none() {
return Err(format!("Extension '{}' not found", config_key));
}
config.set_extensions_raw(raw)
}
#[tauri::command]
pub fn toggle_extension(
config_key: String,
enabled: bool,
config: State<'_, GooseConfig>,
) -> Result<(), String> {
let mut raw = config.get_extensions_raw();
let yaml_key = serde_yaml::Value::String(config_key.clone());
if let Some(entry) = raw.get_mut(&yaml_key) {
if let serde_yaml::Value::Mapping(ref mut map) = entry {
map.insert(
serde_yaml::Value::String("enabled".to_string()),
serde_yaml::Value::Bool(enabled),
);
}
config.set_extensions_raw(raw)
} else {
Err(format!("Extension '{}' not found", config_key))
}
}
-1
View File
@@ -3,7 +3,6 @@ pub mod agent_setup;
pub mod agents;
pub mod credentials;
pub mod doctor;
pub mod extensions;
pub mod git;
pub mod git_changes;
pub mod model_setup;
-4
View File
@@ -56,10 +56,6 @@ pub fn run() {
commands::projects::restore_project,
commands::doctor::run_doctor,
commands::doctor::run_doctor_fix,
commands::extensions::list_extensions,
commands::extensions::add_extension,
commands::extensions::remove_extension,
commands::extensions::toggle_extension,
commands::git::get_git_state,
commands::git_changes::get_changed_files,
commands::git::git_switch_branch,
@@ -321,26 +321,6 @@ impl GooseConfig {
.collect())
}
pub fn get_extensions_raw(&self) -> serde_yaml::Mapping {
let config = self.read_config_map();
let key = serde_yaml::Value::String("extensions".to_string());
config
.get(&key)
.and_then(|v| v.as_mapping())
.cloned()
.unwrap_or_default()
}
pub fn set_extensions_raw(&self, extensions: serde_yaml::Mapping) -> Result<(), String> {
let _guard = self.guard.lock().unwrap();
let mut config = self.read_config_map();
config.insert(
serde_yaml::Value::String("extensions".to_string()),
serde_yaml::Value::Mapping(extensions),
);
self.write_config_map(&config)
}
pub fn delete_all_provider_fields(&self, provider_id: &str) -> Result<(), String> {
let def = find_provider_def(provider_id)
.ok_or_else(|| format!("Unknown provider '{provider_id}'"))?;
@@ -1,4 +1,4 @@
import { invoke } from "@tauri-apps/api/core";
import { getClient } from "@/shared/api/acpConnection";
import type { ExtensionConfig, ExtensionEntry } from "../types";
export function nameToKey(name: string): string {
@@ -9,7 +9,9 @@ export function nameToKey(name: string): string {
}
export async function listExtensions(): Promise<ExtensionEntry[]> {
return invoke("list_extensions");
const client = await getClient();
const response = await client.goose.GooseConfigExtensions({});
return response.extensions as ExtensionEntry[];
}
export async function addExtension(
@@ -17,7 +19,8 @@ export async function addExtension(
extensionConfig: ExtensionConfig,
enabled: boolean,
): Promise<void> {
return invoke("add_extension", {
const client = await getClient();
await client.goose.GooseConfigExtensionsAdd({
name,
extensionConfig,
enabled,
@@ -25,12 +28,14 @@ export async function addExtension(
}
export async function removeExtension(configKey: string): Promise<void> {
return invoke("remove_extension", { configKey });
const client = await getClient();
await client.goose.GooseConfigExtensionsRemove({ configKey });
}
export async function toggleExtension(
configKey: string,
enabled: boolean,
): Promise<void> {
return invoke("toggle_extension", { configKey, enabled });
const client = await getClient();
await client.goose.GooseConfigExtensionsToggle({ configKey, enabled });
}
+21
View File
@@ -8,6 +8,7 @@ export interface ExtMethodProvider {
}
import type {
AddConfigExtensionRequest,
AddExtensionRequest,
ArchiveSessionRequest,
CheckSecretRequest,
@@ -52,9 +53,11 @@ import type {
ReadResourceResponse,
RefreshProviderInventoryRequest,
RefreshProviderInventoryResponse,
RemoveConfigExtensionRequest,
RemoveConfigRequest,
RemoveExtensionRequest,
RemoveSecretRequest,
ToggleConfigExtensionRequest,
UnarchiveSessionRequest,
UpdateSessionProjectRequest,
UpdateSourceRequest,
@@ -123,6 +126,24 @@ export class GooseExtClient {
return zGetExtensionsResponse.parse(raw) as GetExtensionsResponse;
}
async GooseConfigExtensionsAdd(
params: AddConfigExtensionRequest,
): Promise<void> {
await this.conn.extMethod("_goose/config/extensions/add", params);
}
async GooseConfigExtensionsRemove(
params: RemoveConfigExtensionRequest,
): Promise<void> {
await this.conn.extMethod("_goose/config/extensions/remove", params);
}
async GooseConfigExtensionsToggle(
params: ToggleConfigExtensionRequest,
): Promise<void> {
await this.conn.extMethod("_goose/config/extensions/toggle", params);
}
async GooseSessionExtensions(
params: GetSessionExtensionsRequest,
): Promise<GetSessionExtensionsResponse> {
+16 -1
View File
@@ -1,6 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts
export type { AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, CreateSourceRequest, CreateSourceResponse, DeleteSessionRequest, DeleteSourceRequest, DictationConfigRequest, DictationConfigResponse, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest, DictationModelDeleteRequest, DictationModelDownloadProgressRequest, DictationModelDownloadProgressResponse, DictationModelDownloadRequest, DictationModelOption, DictationModelSelectRequest, DictationModelsListRequest, DictationModelsListResponse, DictationProviderStatusEntry, DictationTranscribeRequest, DictationTranscribeResponse, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExportSourceRequest, ExportSourceResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ImportSourcesRequest, ImportSourcesResponse, ListProvidersRequest, ListProvidersResponse, ListSourcesRequest, ListSourcesResponse, ProviderConfigKey, ProviderInventoryEntryDto, ProviderInventoryModelDto, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RefreshProviderInventoryRequest, RefreshProviderInventoryResponse, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, SourceEntry, SourceType, UnarchiveSessionRequest, UpdateSessionProjectRequest, UpdateSourceRequest, UpdateSourceResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js';
export type { AddConfigExtensionRequest, AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, CreateSourceRequest, CreateSourceResponse, DeleteSessionRequest, DeleteSourceRequest, DictationConfigRequest, DictationConfigResponse, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest, DictationModelDeleteRequest, DictationModelDownloadProgressRequest, DictationModelDownloadProgressResponse, DictationModelDownloadRequest, DictationModelOption, DictationModelSelectRequest, DictationModelsListRequest, DictationModelsListResponse, DictationProviderStatusEntry, DictationTranscribeRequest, DictationTranscribeResponse, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExportSourceRequest, ExportSourceResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ImportSourcesRequest, ImportSourcesResponse, ListProvidersRequest, ListProvidersResponse, ListSourcesRequest, ListSourcesResponse, ProviderConfigKey, ProviderInventoryEntryDto, ProviderInventoryModelDto, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RefreshProviderInventoryRequest, RefreshProviderInventoryResponse, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, SourceEntry, SourceType, ToggleConfigExtensionRequest, UnarchiveSessionRequest, UpdateSessionProjectRequest, UpdateSourceRequest, UpdateSourceResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js';
export const GOOSE_EXT_METHODS = [
{
@@ -38,6 +38,21 @@ export const GOOSE_EXT_METHODS = [
requestType: "GetExtensionsRequest",
responseType: "GetExtensionsResponse",
},
{
method: "_goose/config/extensions/add",
requestType: "AddConfigExtensionRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/config/extensions/remove",
requestType: "RemoveConfigExtensionRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/config/extensions/toggle",
requestType: "ToggleConfigExtensionRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/session/extensions",
requestType: "GetSessionExtensionsRequest",
+31 -2
View File
@@ -90,12 +90,41 @@ export type GetExtensionsRequest = {
*/
export type GetExtensionsResponse = {
/**
* Array of ExtensionEntry objects with `enabled` flag and config details.
* Array of ExtensionEntry objects with `enabled` flag, `configKey`, and flattened config details.
*/
extensions: Array<unknown>;
warnings: Array<string>;
};
/**
* Persist a new extension to the user's global goose config.
*/
export type AddConfigExtensionRequest = {
name: string;
/**
* Extension configuration. Must be a JSON object matching one of the
* `ExtensionConfig` variants (e.g. `stdio`, `streamable_http`, `builtin`).
* `name` and `enabled` are injected server-side.
*/
extensionConfig?: unknown;
enabled?: boolean;
};
/**
* Remove a persisted extension from the user's global goose config.
*/
export type RemoveConfigExtensionRequest = {
configKey: string;
};
/**
* Toggle the `enabled` flag for a persisted extension in the user's global goose config.
*/
export type ToggleConfigExtensionRequest = {
configKey: string;
enabled: boolean;
};
export type GetSessionExtensionsRequest = {
sessionId: string;
};
@@ -626,7 +655,7 @@ export type DictationModelSelectRequest = {
export type ExtRequest = {
id: string;
method: string;
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | GetSessionExtensionsRequest | ListProvidersRequest | RefreshProviderInventoryRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | UpdateSessionProjectRequest | ArchiveSessionRequest | UnarchiveSessionRequest | CreateSourceRequest | ListSourcesRequest | UpdateSourceRequest | DeleteSourceRequest | ExportSourceRequest | ImportSourcesRequest | DictationTranscribeRequest | DictationConfigRequest | DictationModelsListRequest | DictationModelDownloadRequest | DictationModelDownloadProgressRequest | DictationModelCancelRequest | DictationModelDeleteRequest | DictationModelSelectRequest | {
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | AddConfigExtensionRequest | RemoveConfigExtensionRequest | ToggleConfigExtensionRequest | GetSessionExtensionsRequest | ListProvidersRequest | RefreshProviderInventoryRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | UpdateSessionProjectRequest | ArchiveSessionRequest | UnarchiveSessionRequest | CreateSourceRequest | ListSourcesRequest | UpdateSourceRequest | DeleteSourceRequest | ExportSourceRequest | ImportSourcesRequest | DictationTranscribeRequest | DictationConfigRequest | DictationModelsListRequest | DictationModelDownloadRequest | DictationModelDownloadProgressRequest | DictationModelCancelRequest | DictationModelDeleteRequest | DictationModelSelectRequest | {
[key: string]: unknown;
} | null;
};
+27
View File
@@ -81,6 +81,30 @@ export const zGetExtensionsResponse = z.object({
warnings: z.array(z.string())
});
/**
* Persist a new extension to the user's global goose config.
*/
export const zAddConfigExtensionRequest = z.object({
name: z.string(),
extensionConfig: z.unknown().optional().default(null),
enabled: z.boolean().optional().default(false)
});
/**
* Remove a persisted extension from the user's global goose config.
*/
export const zRemoveConfigExtensionRequest = z.object({
configKey: z.string()
});
/**
* Toggle the `enabled` flag for a persisted extension in the user's global goose config.
*/
export const zToggleConfigExtensionRequest = z.object({
configKey: z.string(),
enabled: z.boolean()
});
export const zGetSessionExtensionsRequest = z.object({
sessionId: z.string()
});
@@ -594,6 +618,9 @@ export const zExtRequest = z.object({
zUpdateWorkingDirRequest,
zDeleteSessionRequest,
zGetExtensionsRequest,
zAddConfigExtensionRequest,
zRemoveConfigExtensionRequest,
zToggleConfigExtensionRequest,
zGetSessionExtensionsRequest,
zListProvidersRequest,
zRefreshProviderInventoryRequest,