mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat(acp): replace raw config and secret methods (#9000)
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
This commit is contained in:
@@ -173,69 +173,85 @@ pub struct GetSessionExtensionsResponse {
|
||||
pub extensions: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Read a single non-secret config value.
|
||||
/// Read allowlisted user preferences. Empty `keys` means all supported preferences.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/config/read", response = ReadConfigResponse)]
|
||||
#[request(method = "_goose/preferences/read", response = PreferencesReadResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadConfigRequest {
|
||||
pub key: String,
|
||||
pub struct PreferencesReadRequest {
|
||||
#[serde(default)]
|
||||
pub keys: Vec<PreferenceKey>,
|
||||
}
|
||||
|
||||
/// Config read response.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
|
||||
/// Save allowlisted user preferences.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/preferences/save", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadConfigResponse {
|
||||
pub struct PreferencesSaveRequest {
|
||||
#[serde(default)]
|
||||
pub values: Vec<PreferenceValue>,
|
||||
}
|
||||
|
||||
/// Remove allowlisted user preferences.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/preferences/remove", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PreferencesRemoveRequest {
|
||||
#[serde(default)]
|
||||
pub keys: Vec<PreferenceKey>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PreferenceKey {
|
||||
#[default]
|
||||
AutoCompactThreshold,
|
||||
VoiceAutoSubmitPhrases,
|
||||
VoiceDictationProvider,
|
||||
VoiceDictationPreferredMic,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PreferenceValue {
|
||||
pub key: PreferenceKey,
|
||||
#[serde(default)]
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Upsert a single non-secret config value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/config/upsert", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpsertConfigRequest {
|
||||
pub key: String,
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Remove a single non-secret config value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/config/remove", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveConfigRequest {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// Check whether a secret exists. Never returns the actual value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/secret/check", response = CheckSecretResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CheckSecretRequest {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// Secret check response.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CheckSecretResponse {
|
||||
pub exists: bool,
|
||||
pub struct PreferencesReadResponse {
|
||||
pub values: Vec<PreferenceValue>,
|
||||
}
|
||||
|
||||
/// Set a secret value (write-only).
|
||||
/// Read Goose default provider and model configuration.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/secret/upsert", response = EmptyResponse)]
|
||||
#[request(method = "_goose/defaults/read", response = DefaultsReadResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpsertSecretRequest {
|
||||
pub key: String,
|
||||
pub value: serde_json::Value,
|
||||
pub struct DefaultsReadRequest {}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DefaultsReadResponse {
|
||||
pub provider_id: Option<String>,
|
||||
pub model_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Remove a secret.
|
||||
/// Set a dictation provider secret value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/secret/remove", response = EmptyResponse)]
|
||||
#[request(method = "_goose/dictation/secret/save", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveSecretRequest {
|
||||
pub key: String,
|
||||
pub struct DictationSecretSaveRequest {
|
||||
pub provider: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Remove a dictation provider secret value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/dictation/secret/delete", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DictationSecretDeleteRequest {
|
||||
pub provider: String,
|
||||
}
|
||||
|
||||
/// Update the project association for a session.
|
||||
|
||||
+20
-20
@@ -121,34 +121,24 @@
|
||||
"responseType": "ProviderConfigChangeResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/config/read",
|
||||
"requestType": "ReadConfigRequest",
|
||||
"responseType": "ReadConfigResponse"
|
||||
"method": "_goose/preferences/read",
|
||||
"requestType": "PreferencesReadRequest",
|
||||
"responseType": "PreferencesReadResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/config/upsert",
|
||||
"requestType": "UpsertConfigRequest",
|
||||
"method": "_goose/preferences/save",
|
||||
"requestType": "PreferencesSaveRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/config/remove",
|
||||
"requestType": "RemoveConfigRequest",
|
||||
"method": "_goose/preferences/remove",
|
||||
"requestType": "PreferencesRemoveRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/secret/check",
|
||||
"requestType": "CheckSecretRequest",
|
||||
"responseType": "CheckSecretResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/secret/upsert",
|
||||
"requestType": "UpsertSecretRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/secret/remove",
|
||||
"requestType": "RemoveSecretRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
"method": "_goose/defaults/read",
|
||||
"requestType": "DefaultsReadRequest",
|
||||
"responseType": "DefaultsReadResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/session/export",
|
||||
@@ -220,6 +210,16 @@
|
||||
"requestType": "DictationConfigRequest",
|
||||
"responseType": "DictationConfigResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/dictation/secret/save",
|
||||
"requestType": "DictationSecretSaveRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/dictation/secret/delete",
|
||||
"requestType": "DictationSecretDeleteRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/dictation/models/list",
|
||||
"requestType": "DictationModelsListRequest",
|
||||
|
||||
+153
-125
@@ -1246,118 +1246,114 @@
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/providers/config/delete"
|
||||
},
|
||||
"ReadConfigRequest": {
|
||||
"PreferencesReadRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
"keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PreferenceKey"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"description": "Read allowlisted user preferences. Empty `keys` means all supported preferences.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/preferences/read"
|
||||
},
|
||||
"PreferenceKey": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"autoCompactThreshold",
|
||||
"voiceAutoSubmitPhrases",
|
||||
"voiceDictationProvider",
|
||||
"voiceDictationPreferredMic"
|
||||
]
|
||||
},
|
||||
"PreferencesReadResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PreferenceValue"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
"values"
|
||||
],
|
||||
"description": "Read a single non-secret config value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/read"
|
||||
"x-method": "_goose/preferences/read"
|
||||
},
|
||||
"ReadConfigResponse": {
|
||||
"PreferenceValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"$ref": "#/$defs/PreferenceKey"
|
||||
},
|
||||
"value": {
|
||||
"default": null
|
||||
}
|
||||
},
|
||||
"description": "Config read response.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/read"
|
||||
"required": [
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"UpsertConfigRequest": {
|
||||
"PreferencesSaveRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PreferenceValue"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"description": "Save allowlisted user preferences.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/preferences/save"
|
||||
},
|
||||
"PreferencesRemoveRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PreferenceKey"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"description": "Remove allowlisted user preferences.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/preferences/remove"
|
||||
},
|
||||
"DefaultsReadRequest": {
|
||||
"type": "object",
|
||||
"description": "Read Goose default provider and model configuration.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/defaults/read"
|
||||
},
|
||||
"DefaultsReadResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"providerId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"value": {}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"description": "Upsert a single non-secret config value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/upsert"
|
||||
},
|
||||
"RemoveConfigRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
"modelId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"description": "Remove a single non-secret config value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/remove"
|
||||
},
|
||||
"CheckSecretRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"description": "Check whether a secret exists. Never returns the actual value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/check"
|
||||
},
|
||||
"CheckSecretResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exists": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"exists"
|
||||
],
|
||||
"description": "Secret check response.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/check"
|
||||
},
|
||||
"UpsertSecretRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"description": "Set a secret value (write-only).",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/upsert"
|
||||
},
|
||||
"RemoveSecretRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"description": "Remove a secret.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/remove"
|
||||
"x-method": "_goose/defaults/read"
|
||||
},
|
||||
"ExportSessionRequest": {
|
||||
"type": "object",
|
||||
@@ -1920,6 +1916,38 @@
|
||||
"description"
|
||||
]
|
||||
},
|
||||
"DictationSecretSaveRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"value"
|
||||
],
|
||||
"description": "Set a dictation provider secret value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/dictation/secret/save"
|
||||
},
|
||||
"DictationSecretDeleteRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider"
|
||||
],
|
||||
"description": "Remove a dictation provider secret value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/dictation/secret/delete"
|
||||
},
|
||||
"DictationModelsListRequest": {
|
||||
"type": "object",
|
||||
"description": "List available local Whisper models with their download status.",
|
||||
@@ -2330,56 +2358,38 @@
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/ReadConfigRequest"
|
||||
"$ref": "#/$defs/PreferencesReadRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/config/read",
|
||||
"title": "ReadConfigRequest"
|
||||
"description": "Params for _goose/preferences/read",
|
||||
"title": "PreferencesReadRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/UpsertConfigRequest"
|
||||
"$ref": "#/$defs/PreferencesSaveRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/config/upsert",
|
||||
"title": "UpsertConfigRequest"
|
||||
"description": "Params for _goose/preferences/save",
|
||||
"title": "PreferencesSaveRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/RemoveConfigRequest"
|
||||
"$ref": "#/$defs/PreferencesRemoveRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/config/remove",
|
||||
"title": "RemoveConfigRequest"
|
||||
"description": "Params for _goose/preferences/remove",
|
||||
"title": "PreferencesRemoveRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CheckSecretRequest"
|
||||
"$ref": "#/$defs/DefaultsReadRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/secret/check",
|
||||
"title": "CheckSecretRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/UpsertSecretRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/secret/upsert",
|
||||
"title": "UpsertSecretRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/RemoveSecretRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/secret/remove",
|
||||
"title": "RemoveSecretRequest"
|
||||
"description": "Params for _goose/defaults/read",
|
||||
"title": "DefaultsReadRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
@@ -2507,6 +2517,24 @@
|
||||
"description": "Params for _goose/dictation/config",
|
||||
"title": "DictationConfigRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/DictationSecretSaveRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/dictation/secret/save",
|
||||
"title": "DictationSecretSaveRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/DictationSecretDeleteRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/dictation/secret/delete",
|
||||
"title": "DictationSecretDeleteRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2730,18 +2758,18 @@
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/ReadConfigResponse"
|
||||
"$ref": "#/$defs/PreferencesReadResponse"
|
||||
}
|
||||
],
|
||||
"title": "ReadConfigResponse"
|
||||
"title": "PreferencesReadResponse"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CheckSecretResponse"
|
||||
"$ref": "#/$defs/DefaultsReadResponse"
|
||||
}
|
||||
],
|
||||
"title": "CheckSecretResponse"
|
||||
"title": "DefaultsReadResponse"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
|
||||
@@ -74,7 +74,6 @@ mod dispatch;
|
||||
mod extensions;
|
||||
mod providers;
|
||||
mod resources;
|
||||
mod secrets;
|
||||
mod sessions;
|
||||
mod sources;
|
||||
mod tools;
|
||||
|
||||
@@ -1,36 +1,173 @@
|
||||
use super::*;
|
||||
|
||||
impl GooseAcpAgent {
|
||||
pub(super) async fn on_read_config(
|
||||
pub(super) async fn on_preferences_read(
|
||||
&self,
|
||||
req: ReadConfigRequest,
|
||||
) -> Result<ReadConfigResponse, sacp::Error> {
|
||||
req: PreferencesReadRequest,
|
||||
) -> Result<PreferencesReadResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
let response = match config.get_param::<serde_json::Value>(&req.key) {
|
||||
Ok(value) => ReadConfigResponse { value },
|
||||
Err(crate::config::ConfigError::NotFound(_)) => ReadConfigResponse {
|
||||
value: serde_json::Value::Null,
|
||||
},
|
||||
Err(e) => return Err(sacp::Error::internal_error().data(e.to_string())),
|
||||
let keys = if req.keys.is_empty() {
|
||||
PREFERENCE_DEFS.iter().map(|def| def.key).collect()
|
||||
} else {
|
||||
req.keys
|
||||
};
|
||||
Ok(response)
|
||||
let mut values = Vec::with_capacity(keys.len());
|
||||
|
||||
for key in keys {
|
||||
let def = preference_def(key)?;
|
||||
let value = match config.get_param::<serde_json::Value>(def.config_key) {
|
||||
Ok(value) => value,
|
||||
Err(crate::config::ConfigError::NotFound(_)) => serde_json::Value::Null,
|
||||
Err(e) => return Err(sacp::Error::internal_error().data(e.to_string())),
|
||||
};
|
||||
values.push(PreferenceValue { key, value });
|
||||
}
|
||||
|
||||
Ok(PreferencesReadResponse { values })
|
||||
}
|
||||
|
||||
pub(super) async fn on_upsert_config(
|
||||
pub(super) async fn on_preferences_save(
|
||||
&self,
|
||||
req: UpsertConfigRequest,
|
||||
req: PreferencesSaveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
config.set_param(&req.key, &req.value).internal_err()?;
|
||||
let mut updates = Vec::with_capacity(req.values.len());
|
||||
|
||||
for preference in &req.values {
|
||||
let def = preference_def(preference.key)?;
|
||||
(def.validate)(&preference.value)?;
|
||||
updates.push((def.config_key.to_string(), preference.value.clone()));
|
||||
}
|
||||
|
||||
config.set_param_values(&updates).internal_err()?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
pub(super) async fn on_remove_config(
|
||||
pub(super) async fn on_preferences_remove(
|
||||
&self,
|
||||
req: RemoveConfigRequest,
|
||||
req: PreferencesRemoveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
config.delete(&req.key).internal_err()?;
|
||||
for key in req.keys {
|
||||
let def = preference_def(key)?;
|
||||
config.delete(def.config_key).internal_err()?;
|
||||
}
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
pub(super) async fn on_defaults_read(
|
||||
&self,
|
||||
_req: DefaultsReadRequest,
|
||||
) -> Result<DefaultsReadResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
Ok(DefaultsReadResponse {
|
||||
provider_id: optional_config_string(&config, "GOOSE_PROVIDER")?,
|
||||
model_id: optional_config_string(&config, "GOOSE_MODEL")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct PreferenceDef {
|
||||
key: PreferenceKey,
|
||||
config_key: &'static str,
|
||||
validate: fn(&serde_json::Value) -> Result<(), sacp::Error>,
|
||||
}
|
||||
|
||||
const PREFERENCE_DEFS: &[PreferenceDef] = &[
|
||||
PreferenceDef {
|
||||
key: PreferenceKey::AutoCompactThreshold,
|
||||
config_key: "GOOSE_AUTO_COMPACT_THRESHOLD",
|
||||
validate: validate_auto_compact_threshold,
|
||||
},
|
||||
PreferenceDef {
|
||||
key: PreferenceKey::VoiceAutoSubmitPhrases,
|
||||
config_key: "VOICE_AUTO_SUBMIT_PHRASES",
|
||||
validate: validate_voice_auto_submit_phrases,
|
||||
},
|
||||
PreferenceDef {
|
||||
key: PreferenceKey::VoiceDictationProvider,
|
||||
config_key: "VOICE_DICTATION_PROVIDER",
|
||||
validate: validate_voice_dictation_provider,
|
||||
},
|
||||
PreferenceDef {
|
||||
key: PreferenceKey::VoiceDictationPreferredMic,
|
||||
config_key: "VOICE_DICTATION_PREFERRED_MIC",
|
||||
validate: validate_voice_dictation_preferred_mic,
|
||||
},
|
||||
];
|
||||
|
||||
fn preference_def(key: PreferenceKey) -> Result<&'static PreferenceDef, sacp::Error> {
|
||||
PREFERENCE_DEFS
|
||||
.iter()
|
||||
.find(|def| def.key == key)
|
||||
.ok_or_else(|| {
|
||||
sacp::Error::internal_error().data(format!("Missing preference definition for {key:?}"))
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_auto_compact_threshold(value: &serde_json::Value) -> Result<(), sacp::Error> {
|
||||
let Some(value) = value.as_f64() else {
|
||||
return Err(sacp::Error::invalid_params().data("autoCompactThreshold must be a number"));
|
||||
};
|
||||
if !value.is_finite() || value <= 0.0 || value > 1.0 {
|
||||
return Err(sacp::Error::invalid_params()
|
||||
.data("autoCompactThreshold must be greater than 0 and at most 1"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_voice_auto_submit_phrases(value: &serde_json::Value) -> Result<(), sacp::Error> {
|
||||
if !value.is_string() {
|
||||
return Err(sacp::Error::invalid_params().data("voiceAutoSubmitPhrases must be a string"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_voice_dictation_provider(value: &serde_json::Value) -> Result<(), sacp::Error> {
|
||||
let Some(value) = value.as_str() else {
|
||||
return Err(sacp::Error::invalid_params().data("voiceDictationProvider must be a string"));
|
||||
};
|
||||
if !is_supported_voice_dictation_provider(value) {
|
||||
return Err(sacp::Error::invalid_params().data("voiceDictationProvider is not supported"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_voice_dictation_preferred_mic(value: &serde_json::Value) -> Result<(), sacp::Error> {
|
||||
let Some(value) = value.as_str() else {
|
||||
return Err(
|
||||
sacp::Error::invalid_params().data("voiceDictationPreferredMic must be a string")
|
||||
);
|
||||
};
|
||||
if value.is_empty() {
|
||||
return Err(
|
||||
sacp::Error::invalid_params().data("voiceDictationPreferredMic must be non-empty")
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_supported_voice_dictation_provider(value: &str) -> bool {
|
||||
matches!(value, "openai" | "groq" | "elevenlabs" | "__disabled__") || {
|
||||
#[cfg(feature = "local-inference")]
|
||||
{
|
||||
value == "local"
|
||||
}
|
||||
#[cfg(not(feature = "local-inference"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_config_string(config: &Config, key: &str) -> Result<Option<String>, sacp::Error> {
|
||||
match config.get_param::<String>(key) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(crate::config::ConfigError::NotFound(_)) => Ok(None),
|
||||
Err(e) => Err(sacp::Error::internal_error().data(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,52 +200,36 @@ impl GooseAcpAgent {
|
||||
self.on_delete_provider_config(req).await
|
||||
}
|
||||
|
||||
#[custom_method(ReadConfigRequest)]
|
||||
async fn dispatch_read_config(
|
||||
#[custom_method(PreferencesReadRequest)]
|
||||
async fn dispatch_preferences_read(
|
||||
&self,
|
||||
req: ReadConfigRequest,
|
||||
) -> Result<ReadConfigResponse, sacp::Error> {
|
||||
self.on_read_config(req).await
|
||||
req: PreferencesReadRequest,
|
||||
) -> Result<PreferencesReadResponse, sacp::Error> {
|
||||
self.on_preferences_read(req).await
|
||||
}
|
||||
|
||||
#[custom_method(UpsertConfigRequest)]
|
||||
async fn dispatch_upsert_config(
|
||||
#[custom_method(PreferencesSaveRequest)]
|
||||
async fn dispatch_preferences_save(
|
||||
&self,
|
||||
req: UpsertConfigRequest,
|
||||
req: PreferencesSaveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_upsert_config(req).await
|
||||
self.on_preferences_save(req).await
|
||||
}
|
||||
|
||||
#[custom_method(RemoveConfigRequest)]
|
||||
async fn dispatch_remove_config(
|
||||
#[custom_method(PreferencesRemoveRequest)]
|
||||
async fn dispatch_preferences_remove(
|
||||
&self,
|
||||
req: RemoveConfigRequest,
|
||||
req: PreferencesRemoveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_remove_config(req).await
|
||||
self.on_preferences_remove(req).await
|
||||
}
|
||||
|
||||
#[custom_method(CheckSecretRequest)]
|
||||
async fn dispatch_check_secret(
|
||||
#[custom_method(DefaultsReadRequest)]
|
||||
async fn dispatch_defaults_read(
|
||||
&self,
|
||||
req: CheckSecretRequest,
|
||||
) -> Result<CheckSecretResponse, sacp::Error> {
|
||||
self.on_check_secret(req).await
|
||||
}
|
||||
|
||||
#[custom_method(UpsertSecretRequest)]
|
||||
async fn dispatch_upsert_secret(
|
||||
&self,
|
||||
req: UpsertSecretRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_upsert_secret(req).await
|
||||
}
|
||||
|
||||
#[custom_method(RemoveSecretRequest)]
|
||||
async fn dispatch_remove_secret(
|
||||
&self,
|
||||
req: RemoveSecretRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_remove_secret(req).await
|
||||
req: DefaultsReadRequest,
|
||||
) -> Result<DefaultsReadResponse, sacp::Error> {
|
||||
self.on_defaults_read(req).await
|
||||
}
|
||||
|
||||
#[custom_method(ExportSessionRequest)]
|
||||
@@ -360,6 +344,22 @@ impl GooseAcpAgent {
|
||||
self.on_dictation_config(_req).await
|
||||
}
|
||||
|
||||
#[custom_method(DictationSecretSaveRequest)]
|
||||
async fn dispatch_dictation_secret_save(
|
||||
&self,
|
||||
req: DictationSecretSaveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_dictation_secret_save(req).await
|
||||
}
|
||||
|
||||
#[custom_method(DictationSecretDeleteRequest)]
|
||||
async fn dispatch_dictation_secret_delete(
|
||||
&self,
|
||||
req: DictationSecretDeleteRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_dictation_secret_delete(req).await
|
||||
}
|
||||
|
||||
#[custom_method(DictationModelsListRequest)]
|
||||
async fn dispatch_dictation_models_list(
|
||||
&self,
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::*;
|
||||
#[cfg(feature = "local-inference")]
|
||||
use crate::dictation::providers::transcribe_local;
|
||||
use crate::dictation::providers::{
|
||||
all_providers, is_configured, transcribe_with_provider, DictationProvider,
|
||||
all_providers, get_provider_def, is_configured, transcribe_with_provider, DictationProvider,
|
||||
};
|
||||
#[cfg(feature = "local-inference")]
|
||||
use crate::dictation::whisper;
|
||||
@@ -125,6 +125,30 @@ impl GooseAcpAgent {
|
||||
Ok(DictationConfigResponse { providers })
|
||||
}
|
||||
|
||||
pub(super) async fn on_dictation_secret_save(
|
||||
&self,
|
||||
req: DictationSecretSaveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let provider = parse_dictation_provider(&req.provider)?;
|
||||
let key = dictation_secret_config_key(provider)?;
|
||||
let config = self.config()?;
|
||||
config.set_secret(key, &req.value).internal_err()?;
|
||||
Config::global().invalidate_secrets_cache();
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
pub(super) async fn on_dictation_secret_delete(
|
||||
&self,
|
||||
req: DictationSecretDeleteRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let provider = parse_dictation_provider(&req.provider)?;
|
||||
let key = dictation_secret_config_key(provider)?;
|
||||
let config = self.config()?;
|
||||
config.delete_secret(key).internal_err()?;
|
||||
Config::global().invalidate_secrets_cache();
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
pub(super) async fn on_dictation_models_list(
|
||||
&self,
|
||||
_req: DictationModelsListRequest,
|
||||
@@ -321,6 +345,29 @@ impl GooseAcpAgent {
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_dictation_provider(provider: &str) -> Result<DictationProvider, sacp::Error> {
|
||||
serde_json::from_value(serde_json::Value::String(provider.to_string()))
|
||||
.map_err(|_| sacp::Error::invalid_params().data(format!("Unknown provider: {provider}")))
|
||||
}
|
||||
|
||||
fn dictation_secret_config_key(provider: DictationProvider) -> Result<&'static str, sacp::Error> {
|
||||
let def = get_provider_def(provider);
|
||||
if def.uses_provider_config {
|
||||
return Err(sacp::Error::invalid_params().data(
|
||||
"Dictation provider uses the main provider configuration. Configure its credentials in provider settings instead.",
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "local-inference")]
|
||||
if provider == DictationProvider::Local {
|
||||
return Err(sacp::Error::invalid_params()
|
||||
.data("Dictation provider does not use an API key or secret."));
|
||||
}
|
||||
|
||||
Ok(def.config_key)
|
||||
}
|
||||
|
||||
fn dictation_model_config_key(provider: DictationProvider) -> Option<String> {
|
||||
match provider {
|
||||
DictationProvider::OpenAI => Some(OPENAI_TRANSCRIPTION_MODEL_CONFIG_KEY.to_string()),
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
impl GooseAcpAgent {
|
||||
pub(super) async fn on_check_secret(
|
||||
&self,
|
||||
req: CheckSecretRequest,
|
||||
) -> Result<CheckSecretResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
let exists = config.get_secret::<serde_json::Value>(&req.key).is_ok();
|
||||
Ok(CheckSecretResponse { exists })
|
||||
}
|
||||
|
||||
pub(super) async fn on_upsert_secret(
|
||||
&self,
|
||||
req: UpsertSecretRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
config.set_secret(&req.key, &req.value).internal_err()?;
|
||||
Config::global().invalidate_secrets_cache();
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
pub(super) async fn on_remove_secret(
|
||||
&self,
|
||||
req: RemoveSecretRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
config.delete_secret(&req.key).internal_err()?;
|
||||
Config::global().invalidate_secrets_cache();
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
}
|
||||
@@ -719,6 +719,20 @@ impl Config {
|
||||
self.save_values(&values)
|
||||
}
|
||||
|
||||
/// Set multiple configuration values in the config file with one read and one write.
|
||||
pub fn set_param_values(&self, updates: &[(String, Value)]) -> Result<(), ConfigError> {
|
||||
if updates.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _guard = self.guard.lock().unwrap();
|
||||
let mut values = self.load_write_config()?;
|
||||
for (key, value) in updates {
|
||||
values.insert(serde_yaml::to_value(key)?, serde_yaml::to_value(value)?);
|
||||
}
|
||||
self.save_values(&values)
|
||||
}
|
||||
|
||||
/// Delete a configuration value in the config file.
|
||||
///
|
||||
/// This will immediately write the value to the config file. The value
|
||||
|
||||
@@ -69,7 +69,7 @@ fn mock_provider_factory() -> AcpProviderFactory {
|
||||
|
||||
#[test]
|
||||
fn test_custom_get_tools() {
|
||||
run_test(async {
|
||||
run_test(async move {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let mut conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;
|
||||
|
||||
@@ -92,7 +92,7 @@ fn test_custom_get_tools() {
|
||||
|
||||
#[test]
|
||||
fn test_custom_get_extensions() {
|
||||
run_test(async {
|
||||
run_test(async move {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;
|
||||
|
||||
@@ -140,53 +140,289 @@ fn test_custom_provider_inventory_includes_metadata() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_config_crud() {
|
||||
fn test_custom_preferences_read_save_remove() {
|
||||
run_test(async {
|
||||
let data_root = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
data_root
|
||||
.path()
|
||||
.join(goose::config::base::CONFIG_YAML_NAME),
|
||||
"GOOSE_MODEL: gpt-4o\nGOOSE_PROVIDER: openai\nGOOSE_AUTO_COMPACT_THRESHOLD: 0.7\nVOICE_AUTO_SUBMIT_PHRASES: send it\n",
|
||||
)
|
||||
.unwrap();
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let config = TestConnectionConfig {
|
||||
data_root: data_root.path().to_path_buf(),
|
||||
..Default::default()
|
||||
};
|
||||
let conn = AcpServerConnection::new(config, openai).await;
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/read",
|
||||
serde_json::json!({
|
||||
"keys": [
|
||||
"autoCompactThreshold",
|
||||
"voiceAutoSubmitPhrases",
|
||||
"voiceDictationPreferredMic"
|
||||
],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("preferences read should succeed");
|
||||
assert_eq!(
|
||||
response.get("values"),
|
||||
Some(&serde_json::json!([
|
||||
{ "key": "autoCompactThreshold", "value": 0.7 },
|
||||
{ "key": "voiceAutoSubmitPhrases", "value": "send it" },
|
||||
{ "key": "voiceDictationPreferredMic", "value": null },
|
||||
]))
|
||||
);
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/save",
|
||||
serde_json::json!({
|
||||
"values": [
|
||||
{ "key": "voiceDictationProvider", "value": "__disabled__" },
|
||||
{ "key": "voiceDictationPreferredMic", "value": "mic-1" }
|
||||
],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("preferences save should succeed");
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/remove",
|
||||
serde_json::json!({
|
||||
"keys": ["voiceDictationProvider"],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("preferences remove should succeed");
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/read",
|
||||
serde_json::json!({
|
||||
"keys": ["voiceDictationProvider", "voiceDictationPreferredMic"],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("preferences read after remove should succeed");
|
||||
assert_eq!(
|
||||
response.get("values"),
|
||||
Some(&serde_json::json!([
|
||||
{ "key": "voiceDictationProvider", "value": null },
|
||||
{ "key": "voiceDictationPreferredMic", "value": "mic-1" },
|
||||
]))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_preferences_save_rejects_invalid_values() {
|
||||
run_test(async {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;
|
||||
|
||||
let invalid_payloads = [
|
||||
serde_json::json!({
|
||||
"values": [{ "key": "autoCompactThreshold", "value": 0 }],
|
||||
}),
|
||||
serde_json::json!({
|
||||
"values": [{ "key": "autoCompactThreshold", "value": 1.1 }],
|
||||
}),
|
||||
serde_json::json!({
|
||||
"values": [{ "key": "voiceAutoSubmitPhrases", "value": ["send"] }],
|
||||
}),
|
||||
serde_json::json!({
|
||||
"values": [{ "key": "voiceDictationProvider", "value": "bogus" }],
|
||||
}),
|
||||
serde_json::json!({
|
||||
"values": [{ "key": "voiceDictationPreferredMic", "value": "" }],
|
||||
}),
|
||||
];
|
||||
|
||||
for payload in invalid_payloads {
|
||||
let result = send_custom(conn.cx(), "_goose/preferences/save", payload).await;
|
||||
assert!(result.is_err(), "expected invalid params error");
|
||||
}
|
||||
|
||||
let result = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/save",
|
||||
serde_json::json!({
|
||||
"values": [
|
||||
{ "key": "voiceDictationPreferredMic", "value": "mic-1" },
|
||||
{ "key": "voiceDictationProvider", "value": "bogus" }
|
||||
],
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err(), "expected invalid params error");
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/preferences/read",
|
||||
serde_json::json!({
|
||||
"keys": ["voiceDictationPreferredMic"],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("preferences read should succeed");
|
||||
assert_eq!(
|
||||
response.get("values"),
|
||||
Some(&serde_json::json!([
|
||||
{ "key": "voiceDictationPreferredMic", "value": null },
|
||||
]))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_defaults_read() {
|
||||
run_test(async {
|
||||
let data_root = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
data_root.path().join(goose::config::base::CONFIG_YAML_NAME),
|
||||
"GOOSE_MODEL: claude-3-5-haiku-latest\nGOOSE_PROVIDER: anthropic\n",
|
||||
)
|
||||
.unwrap();
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let config = TestConnectionConfig {
|
||||
data_root: data_root.path().to_path_buf(),
|
||||
..Default::default()
|
||||
};
|
||||
let conn = AcpServerConnection::new(config, openai).await;
|
||||
|
||||
let response = send_custom(conn.cx(), "_goose/defaults/read", serde_json::json!({}))
|
||||
.await
|
||||
.expect("defaults read should succeed");
|
||||
assert_eq!(
|
||||
response,
|
||||
serde_json::json!({
|
||||
"providerId": "anthropic",
|
||||
"modelId": "claude-3-5-haiku-latest",
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_dictation_secret_save_delete() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let root_path = root.path().to_string_lossy().to_string();
|
||||
let _env = env_lock::lock_env([
|
||||
("GOOSE_PATH_ROOT", Some(root_path.as_str())),
|
||||
("GOOSE_DISABLE_KEYRING", Some("1")),
|
||||
("GROQ_API_KEY", None::<&str>),
|
||||
]);
|
||||
let config_dir = goose::config::paths::Paths::config_dir();
|
||||
std::fs::create_dir_all(&config_dir).unwrap();
|
||||
std::fs::write(
|
||||
config_dir.join(goose::config::base::CONFIG_YAML_NAME),
|
||||
"GOOSE_MODEL: gpt-4o\nGOOSE_PROVIDER: openai\nGOOSE_DISABLE_KEYRING: true\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
run_test(async move {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let config = TestConnectionConfig {
|
||||
data_root: config_dir.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let conn = AcpServerConnection::new(config, openai).await;
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/dictation/secret/save",
|
||||
serde_json::json!({
|
||||
"provider": "groq",
|
||||
"value": "groq-key",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("dictation secret save should succeed");
|
||||
|
||||
let config = send_custom(conn.cx(), "_goose/dictation/config", serde_json::json!({}))
|
||||
.await
|
||||
.expect("dictation config should succeed");
|
||||
assert_eq!(
|
||||
config
|
||||
.pointer("/providers/groq/configured")
|
||||
.and_then(|value| value.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
|
||||
let provider_config_result = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/dictation/secret/save",
|
||||
serde_json::json!({
|
||||
"provider": "openai",
|
||||
"value": "openai-key",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
provider_config_result.is_err(),
|
||||
"provider-config dictation providers should be rejected"
|
||||
);
|
||||
|
||||
let unknown_result = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/dictation/secret/save",
|
||||
serde_json::json!({
|
||||
"provider": "unknown",
|
||||
"value": "key",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
unknown_result.is_err(),
|
||||
"unknown provider should be rejected"
|
||||
);
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/dictation/secret/delete",
|
||||
serde_json::json!({
|
||||
"provider": "groq",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("dictation secret delete should succeed");
|
||||
|
||||
let config = send_custom(conn.cx(), "_goose/dictation/config", serde_json::json!({}))
|
||||
.await
|
||||
.expect("dictation config should succeed");
|
||||
assert_eq!(
|
||||
config
|
||||
.pointer("/providers/groq/configured")
|
||||
.and_then(|value| value.as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_config_and_secret_methods_are_removed() {
|
||||
run_test(async {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;
|
||||
|
||||
for method in [
|
||||
"_goose/config/read",
|
||||
"_goose/config/upsert",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
"value": "anthropic",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config upsert should succeed");
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/config/read",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config read should succeed");
|
||||
assert_eq!(response.get("value"), Some(&serde_json::json!("anthropic")));
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/config/remove",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config remove should succeed");
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/config/read",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config read after remove should succeed");
|
||||
assert_eq!(response.get("value"), Some(&serde_json::Value::Null));
|
||||
"_goose/secret/check",
|
||||
"_goose/secret/upsert",
|
||||
"_goose/secret/remove",
|
||||
] {
|
||||
let result = send_custom(conn.cx(), method, serde_json::json!({})).await;
|
||||
assert!(result.is_err(), "{method} should be removed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ fn acp_secret_mutations_and_inventory_refresh_invalidate_global_secret_cache() {
|
||||
("GOOSE_PATH_ROOT", Some(root_path.as_str())),
|
||||
("GOOSE_DISABLE_KEYRING", Some("1")),
|
||||
("ANTHROPIC_API_KEY", None),
|
||||
("GROQ_API_KEY", None),
|
||||
("OPENAI_API_KEY", None),
|
||||
("XAI_API_KEY", None),
|
||||
("XAI_HOST", None),
|
||||
@@ -87,12 +88,12 @@ fn acp_secret_mutations_and_inventory_refresh_invalidate_global_secret_cache() {
|
||||
let config_dir = Paths::config_dir();
|
||||
let data_dir = Paths::data_dir();
|
||||
write_config(&config_dir);
|
||||
write_secrets(&config_dir, "OPENAI_API_KEY: stale-key\n");
|
||||
write_secrets(&config_dir, "GROQ_API_KEY: stale-key\n");
|
||||
|
||||
run_test(async move {
|
||||
assert_eq!(
|
||||
Config::global()
|
||||
.get_secret::<String>("OPENAI_API_KEY")
|
||||
.get_secret::<String>("GROQ_API_KEY")
|
||||
.unwrap(),
|
||||
"stale-key"
|
||||
);
|
||||
@@ -109,43 +110,43 @@ fn acp_secret_mutations_and_inventory_refresh_invalidate_global_secret_cache() {
|
||||
};
|
||||
let conn = AcpServerConnection::new(config, openai).await;
|
||||
|
||||
write_secrets(&config_dir, "OPENAI_API_KEY: fresh-key\n");
|
||||
write_secrets(&config_dir, "GROQ_API_KEY: fresh-key\n");
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/secret/upsert",
|
||||
"_goose/dictation/secret/save",
|
||||
serde_json::json!({
|
||||
"key": "OPENAI_API_KEY",
|
||||
"provider": "groq",
|
||||
"value": "fresh-key",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("secret upsert should succeed");
|
||||
.expect("dictation secret save should succeed");
|
||||
|
||||
assert_eq!(
|
||||
Config::global()
|
||||
.get_secret::<String>("OPENAI_API_KEY")
|
||||
.get_secret::<String>("GROQ_API_KEY")
|
||||
.unwrap(),
|
||||
"fresh-key",
|
||||
"ACP secret upsert should invalidate the global secrets cache"
|
||||
"ACP dictation secret save should invalidate the global secrets cache"
|
||||
);
|
||||
|
||||
write_secrets(&config_dir, "{}\n");
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/secret/remove",
|
||||
"_goose/dictation/secret/delete",
|
||||
serde_json::json!({
|
||||
"key": "OPENAI_API_KEY",
|
||||
"provider": "groq",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("secret remove should succeed");
|
||||
.expect("dictation secret delete should succeed");
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
Config::global().get_secret::<String>("OPENAI_API_KEY"),
|
||||
Config::global().get_secret::<String>("GROQ_API_KEY"),
|
||||
Err(ConfigError::NotFound(_))
|
||||
),
|
||||
"ACP secret remove should invalidate the global secrets cache"
|
||||
"ACP dictation secret delete should invalidate the global secrets cache"
|
||||
);
|
||||
|
||||
let save_provider_config = send_custom(
|
||||
|
||||
@@ -169,6 +169,14 @@ The skills → sources migration in [#8675](https://github.com/block/goose/pull/
|
||||
|
||||
For a minimal frontend `api/` wrapper using the typed shape, see `ui/goose2/src/features/providers/api/inventory.ts` — ~30 lines, typed SDK calls, thin adapter. For a fully worked end-to-end feature including OS-keychain handling and progress streaming, see the voice dictation feature ([#8609](https://github.com/block/goose/pull/8609)) and `ui/goose2/src/shared/api/dictation.ts`.
|
||||
|
||||
### Typed ACP config contracts
|
||||
|
||||
Build goose2 config flows around typed ACP methods whose contract matches the domain: provider config, preferences, defaults, dictation secrets, or extension config.
|
||||
|
||||
Keep raw Goose storage keys and secret-handling decisions behind backend-owned ACP methods. This lets Goose validate inputs, apply provider metadata, invalidate caches, refresh dependent state, and keep generated SDK types aligned with supported behavior.
|
||||
|
||||
For reference, define contracts in `crates/goose-sdk/src/custom_requests.rs`, implement them in `crates/goose/src/acp/server/`, regenerate `ui/sdk/src/generated/`, then call the generated `client.goose.*` methods from a feature or shared `api/` wrapper.
|
||||
|
||||
### When `invoke()` is still appropriate
|
||||
|
||||
Tauri commands (`invoke()` from `@tauri-apps/api/core`) are reserved for things that genuinely belong to the desktop shell, not to `goose` core. Provider config or secret mutations that affect the Goose runtime must flow through React → SDK → ACP → goose core so core can validate provider metadata, invalidate secret caches, refresh inventory, and apply provider changes consistently. In practice, Tauri is limited to:
|
||||
|
||||
@@ -2,7 +2,6 @@ import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
AUTO_COMPACT_PREFERENCES_EVENT,
|
||||
AUTO_COMPACT_THRESHOLD_CONFIG_KEY,
|
||||
DEFAULT_AUTO_COMPACT_THRESHOLD,
|
||||
} from "../../lib/autoCompact";
|
||||
|
||||
@@ -26,8 +25,10 @@ describe("useAutoCompactPreferences", () => {
|
||||
it("hydrates from the stored threshold value", async () => {
|
||||
mockGetClient.mockResolvedValue({
|
||||
goose: {
|
||||
GooseConfigRead: vi.fn().mockResolvedValue({ value: 0.65 }),
|
||||
GooseConfigUpsert: vi.fn().mockResolvedValue({}),
|
||||
GoosePreferencesRead: vi.fn().mockResolvedValue({
|
||||
values: [{ key: "autoCompactThreshold", value: 0.65 }],
|
||||
}),
|
||||
GoosePreferencesSave: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -42,13 +43,17 @@ describe("useAutoCompactPreferences", () => {
|
||||
const upsert = vi.fn().mockResolvedValue({});
|
||||
const read = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ value: null })
|
||||
.mockResolvedValue({ value: 0.9 });
|
||||
.mockResolvedValueOnce({
|
||||
values: [{ key: "autoCompactThreshold", value: null }],
|
||||
})
|
||||
.mockResolvedValue({
|
||||
values: [{ key: "autoCompactThreshold", value: 0.9 }],
|
||||
});
|
||||
|
||||
mockGetClient.mockResolvedValue({
|
||||
goose: {
|
||||
GooseConfigRead: read,
|
||||
GooseConfigUpsert: upsert,
|
||||
GoosePreferencesRead: read,
|
||||
GoosePreferencesSave: upsert,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -64,8 +69,7 @@ describe("useAutoCompactPreferences", () => {
|
||||
});
|
||||
|
||||
expect(upsert).toHaveBeenCalledWith({
|
||||
key: AUTO_COMPACT_THRESHOLD_CONFIG_KEY,
|
||||
value: 0.9,
|
||||
values: [{ key: "autoCompactThreshold", value: 0.9 }],
|
||||
});
|
||||
expect(eventListener).toHaveBeenCalledTimes(1);
|
||||
expect(result.current.autoCompactThreshold).toBe(0.9);
|
||||
@@ -73,13 +77,17 @@ describe("useAutoCompactPreferences", () => {
|
||||
window.removeEventListener(AUTO_COMPACT_PREFERENCES_EVENT, eventListener);
|
||||
});
|
||||
|
||||
it("marks the preferences hydrated even when the initial read fails", async () => {
|
||||
it("does not mark preferences hydrated when the initial read fails", async () => {
|
||||
mockGetClient.mockRejectedValue(new Error("ACP not ready"));
|
||||
|
||||
const { result } = renderHook(() => useAutoCompactPreferences());
|
||||
|
||||
await waitFor(() => expect(result.current.isHydrated).toBe(true));
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.isHydrated).toBe(false);
|
||||
expect(result.current.autoCompactThreshold).toBe(
|
||||
DEFAULT_AUTO_COMPACT_THRESHOLD,
|
||||
);
|
||||
@@ -90,12 +98,14 @@ describe("useAutoCompactPreferences", () => {
|
||||
const read = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("ACP not ready"))
|
||||
.mockResolvedValueOnce({ value: 0.65 });
|
||||
.mockResolvedValueOnce({
|
||||
values: [{ key: "autoCompactThreshold", value: 0.65 }],
|
||||
});
|
||||
|
||||
mockGetClient.mockResolvedValue({
|
||||
goose: {
|
||||
GooseConfigRead: read,
|
||||
GooseConfigUpsert: vi.fn().mockResolvedValue({}),
|
||||
GoosePreferencesRead: read,
|
||||
GoosePreferencesSave: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -106,7 +116,7 @@ describe("useAutoCompactPreferences", () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.isHydrated).toBe(true);
|
||||
expect(result.current.isHydrated).toBe(false);
|
||||
expect(result.current.autoCompactThreshold).toBe(
|
||||
DEFAULT_AUTO_COMPACT_THRESHOLD,
|
||||
);
|
||||
@@ -116,6 +126,47 @@ describe("useAutoCompactPreferences", () => {
|
||||
});
|
||||
|
||||
expect(result.current.autoCompactThreshold).toBe(0.65);
|
||||
expect(result.current.isHydrated).toBe(true);
|
||||
expect(read).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("backs off repeated hydration retries", async () => {
|
||||
vi.useFakeTimers();
|
||||
const read = vi.fn().mockRejectedValue(new Error("ACP not ready"));
|
||||
|
||||
mockGetClient.mockResolvedValue({
|
||||
goose: {
|
||||
GoosePreferencesRead: read,
|
||||
GoosePreferencesSave: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
});
|
||||
|
||||
renderHook(() => useAutoCompactPreferences());
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(read).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(999);
|
||||
});
|
||||
expect(read).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
});
|
||||
expect(read).toHaveBeenCalledTimes(2);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1999);
|
||||
});
|
||||
expect(read).toHaveBeenCalledTimes(2);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
});
|
||||
expect(read).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ const mockAcpPrepareSession = vi.fn();
|
||||
const mockAcpSetModel = vi.fn();
|
||||
const mockSetSelectedProvider = vi.fn();
|
||||
const mockResolveSessionCwd = vi.fn();
|
||||
const mockGooseConfigRead = vi.fn();
|
||||
const mockGooseDefaultsRead = vi.fn();
|
||||
const mockUseProviderInventory = vi.fn();
|
||||
const mockPickerState = {
|
||||
pickerAgents: [{ id: "goose", label: "Goose" }],
|
||||
@@ -31,7 +31,7 @@ vi.mock("@/shared/api/acp", () => ({
|
||||
vi.mock("@/shared/api/acpConnection", () => ({
|
||||
getClient: async () => ({
|
||||
goose: {
|
||||
GooseConfigRead: (...args: unknown[]) => mockGooseConfigRead(...args),
|
||||
GooseDefaultsRead: (...args: unknown[]) => mockGooseDefaultsRead(...args),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
@@ -116,7 +116,10 @@ describe("useChatSessionController", () => {
|
||||
mockAcpPrepareSession.mockResolvedValue(undefined);
|
||||
mockAcpSetModel.mockResolvedValue(undefined);
|
||||
mockResolveSessionCwd.mockResolvedValue("/tmp/project");
|
||||
mockGooseConfigRead.mockResolvedValue({ value: null });
|
||||
mockGooseDefaultsRead.mockResolvedValue({
|
||||
providerId: null,
|
||||
modelId: null,
|
||||
});
|
||||
mockUseProviderInventory.mockReturnValue({
|
||||
getEntry: () => undefined,
|
||||
});
|
||||
@@ -283,17 +286,10 @@ describe("useChatSessionController", () => {
|
||||
|
||||
it("falls back to the configured goose default model when no explicit model is stored", async () => {
|
||||
useAgentStore.setState({ selectedProvider: "goose" });
|
||||
mockGooseConfigRead.mockImplementation(
|
||||
async ({ key }: { key: string }): Promise<{ value: string | null }> => {
|
||||
if (key === "GOOSE_PROVIDER") {
|
||||
return { value: "databricks" };
|
||||
}
|
||||
if (key === "GOOSE_MODEL") {
|
||||
return { value: "goose-claude-4-6-opus" };
|
||||
}
|
||||
return { value: null };
|
||||
},
|
||||
);
|
||||
mockGooseDefaultsRead.mockResolvedValue({
|
||||
providerId: "databricks",
|
||||
modelId: "goose-claude-4-6-opus",
|
||||
});
|
||||
mockPickerState.availableModels = [
|
||||
{
|
||||
id: "goose-claude-4-6-opus",
|
||||
|
||||
@@ -26,7 +26,10 @@ describe("useResolvedAgentModelPicker", () => {
|
||||
|
||||
mockGetClient.mockResolvedValue({
|
||||
goose: {
|
||||
GooseConfigRead: vi.fn().mockResolvedValue({ value: null }),
|
||||
GooseDefaultsRead: vi.fn().mockResolvedValue({
|
||||
providerId: null,
|
||||
modelId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -27,17 +27,20 @@ describe("useVoiceInputPreferences", () => {
|
||||
|
||||
mockGetClient.mockResolvedValue({
|
||||
goose: {
|
||||
GooseConfigRead: vi.fn().mockImplementation(({ key }) => {
|
||||
if (key === "VOICE_DICTATION_PROVIDER") {
|
||||
if (shouldFailProviderRead) {
|
||||
return Promise.reject(new Error("temporary acp failure"));
|
||||
}
|
||||
return Promise.resolve({ value: "groq" });
|
||||
GoosePreferencesRead: vi.fn().mockImplementation(() => {
|
||||
if (shouldFailProviderRead) {
|
||||
return Promise.reject(new Error("temporary acp failure"));
|
||||
}
|
||||
return Promise.resolve({ value: null });
|
||||
return Promise.resolve({
|
||||
values: [
|
||||
{ key: "voiceAutoSubmitPhrases", value: null },
|
||||
{ key: "voiceDictationProvider", value: "groq" },
|
||||
{ key: "voiceDictationPreferredMic", value: null },
|
||||
],
|
||||
});
|
||||
}),
|
||||
GooseConfigUpsert: vi.fn().mockResolvedValue({}),
|
||||
GooseConfigRemove: vi.fn().mockResolvedValue({}),
|
||||
GoosePreferencesSave: vi.fn().mockResolvedValue({}),
|
||||
GoosePreferencesRemove: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -61,21 +64,27 @@ describe("useVoiceInputPreferences", () => {
|
||||
|
||||
it("broadcasts preference changes only after config persistence settles", async () => {
|
||||
const upsert = vi.fn();
|
||||
const providerRead = deferred<{ value?: unknown }>();
|
||||
const providerRead = deferred<{
|
||||
values: Array<{ key: string; value: unknown }>;
|
||||
}>();
|
||||
const pendingWrite = deferred<void>();
|
||||
|
||||
mockGetClient.mockResolvedValue({
|
||||
goose: {
|
||||
GooseConfigRead: vi
|
||||
GoosePreferencesRead: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ value: null })
|
||||
.mockResolvedValueOnce({ value: null })
|
||||
.mockResolvedValueOnce({ value: null })
|
||||
.mockResolvedValueOnce({
|
||||
values: [
|
||||
{ key: "voiceAutoSubmitPhrases", value: null },
|
||||
{ key: "voiceDictationProvider", value: null },
|
||||
{ key: "voiceDictationPreferredMic", value: null },
|
||||
],
|
||||
})
|
||||
.mockImplementation(() => providerRead.promise),
|
||||
GooseConfigUpsert: upsert.mockImplementation(
|
||||
GoosePreferencesSave: upsert.mockImplementation(
|
||||
() => pendingWrite.promise,
|
||||
),
|
||||
GooseConfigRemove: vi.fn().mockResolvedValue({}),
|
||||
GoosePreferencesRemove: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -100,7 +109,114 @@ describe("useVoiceInputPreferences", () => {
|
||||
|
||||
await waitFor(() => expect(eventListener).toHaveBeenCalledTimes(1));
|
||||
|
||||
providerRead.resolve({ value: "openai" });
|
||||
providerRead.resolve({
|
||||
values: [
|
||||
{ key: "voiceAutoSubmitPhrases", value: null },
|
||||
{ key: "voiceDictationProvider", value: "openai" },
|
||||
{ key: "voiceDictationPreferredMic", value: null },
|
||||
],
|
||||
});
|
||||
window.removeEventListener("goose:voice-input-preferences", eventListener);
|
||||
});
|
||||
|
||||
it("does not broadcast failed preference writes and re-syncs stored state", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const save = vi.fn().mockRejectedValue(new Error("write failed"));
|
||||
const read = vi.fn().mockResolvedValue({
|
||||
values: [
|
||||
{ key: "voiceAutoSubmitPhrases", value: null },
|
||||
{ key: "voiceDictationProvider", value: "groq" },
|
||||
{ key: "voiceDictationPreferredMic", value: null },
|
||||
],
|
||||
});
|
||||
|
||||
mockGetClient.mockResolvedValue({
|
||||
goose: {
|
||||
GoosePreferencesRead: read,
|
||||
GoosePreferencesSave: save,
|
||||
GoosePreferencesRemove: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
});
|
||||
|
||||
const eventListener = vi.fn();
|
||||
window.addEventListener("goose:voice-input-preferences", eventListener);
|
||||
|
||||
const { result } = renderHook(() => useVoiceInputPreferences());
|
||||
await waitFor(() => expect(result.current.isHydrated).toBe(true));
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedProvider("openai");
|
||||
});
|
||||
|
||||
expect(result.current.selectedProvider).toBe("openai");
|
||||
await waitFor(() => expect(read).toHaveBeenCalledTimes(2));
|
||||
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
expect(result.current.selectedProvider).toBe("groq");
|
||||
|
||||
window.removeEventListener("goose:voice-input-preferences", eventListener);
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
it("stores the disabled sentinel when provider is set to null", async () => {
|
||||
const save = vi.fn().mockResolvedValue({});
|
||||
|
||||
mockGetClient.mockResolvedValue({
|
||||
goose: {
|
||||
GoosePreferencesRead: vi.fn().mockResolvedValue({
|
||||
values: [
|
||||
{ key: "voiceAutoSubmitPhrases", value: null },
|
||||
{ key: "voiceDictationProvider", value: "groq" },
|
||||
{ key: "voiceDictationPreferredMic", value: null },
|
||||
],
|
||||
}),
|
||||
GoosePreferencesSave: save,
|
||||
GoosePreferencesRemove: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVoiceInputPreferences());
|
||||
await waitFor(() => expect(result.current.isHydrated).toBe(true));
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedProvider(null);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(save).toHaveBeenCalledWith({
|
||||
values: [{ key: "voiceDictationProvider", value: "__disabled__" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("clearing selected provider removes only the provider preference", async () => {
|
||||
const remove = vi.fn().mockResolvedValue({});
|
||||
|
||||
mockGetClient.mockResolvedValue({
|
||||
goose: {
|
||||
GoosePreferencesRead: vi.fn().mockResolvedValue({
|
||||
values: [
|
||||
{ key: "voiceAutoSubmitPhrases", value: "submit" },
|
||||
{ key: "voiceDictationProvider", value: "groq" },
|
||||
{ key: "voiceDictationPreferredMic", value: "mic-1" },
|
||||
],
|
||||
}),
|
||||
GoosePreferencesSave: vi.fn().mockResolvedValue({}),
|
||||
GoosePreferencesRemove: remove,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVoiceInputPreferences());
|
||||
await waitFor(() => expect(result.current.isHydrated).toBe(true));
|
||||
|
||||
act(() => {
|
||||
result.current.clearSelectedProvider();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(remove).toHaveBeenCalledWith({
|
||||
keys: ["voiceDictationProvider"],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { getClient } from "@/shared/api/acpConnection";
|
||||
import {
|
||||
AUTO_COMPACT_PREFERENCES_EVENT,
|
||||
AUTO_COMPACT_THRESHOLD_CONFIG_KEY,
|
||||
DEFAULT_AUTO_COMPACT_THRESHOLD,
|
||||
normalizeAutoCompactThreshold,
|
||||
} from "../lib/autoCompact";
|
||||
|
||||
const AUTO_COMPACT_RETRY_DELAY_MS = 1000;
|
||||
const AUTO_COMPACT_INITIAL_RETRY_DELAY_MS = 1000;
|
||||
const AUTO_COMPACT_MAX_RETRY_DELAY_MS = 30000;
|
||||
const AUTO_COMPACT_THRESHOLD_PREFERENCE_KEY = "autoCompactThreshold";
|
||||
|
||||
type ConfigReadResult =
|
||||
| {
|
||||
@@ -18,22 +19,29 @@ type ConfigReadResult =
|
||||
ok: false;
|
||||
};
|
||||
|
||||
async function readConfigValue(key: string): Promise<ConfigReadResult> {
|
||||
async function readAutoCompactThreshold(): Promise<ConfigReadResult> {
|
||||
try {
|
||||
const client = await getClient();
|
||||
const response = await client.goose.GooseConfigRead({ key });
|
||||
const response = await client.goose.GoosePreferencesRead({
|
||||
keys: [AUTO_COMPACT_THRESHOLD_PREFERENCE_KEY],
|
||||
});
|
||||
const preference = response.values.find(
|
||||
(value) => value.key === AUTO_COMPACT_THRESHOLD_PREFERENCE_KEY,
|
||||
);
|
||||
return {
|
||||
ok: true,
|
||||
value: response.value ?? null,
|
||||
value: preference?.value ?? null,
|
||||
};
|
||||
} catch {
|
||||
return { ok: false };
|
||||
}
|
||||
}
|
||||
|
||||
async function writeConfigValue(key: string, value: number): Promise<void> {
|
||||
async function writeAutoCompactThreshold(value: number): Promise<void> {
|
||||
const client = await getClient();
|
||||
await client.goose.GooseConfigUpsert({ key, value });
|
||||
await client.goose.GoosePreferencesSave({
|
||||
values: [{ key: AUTO_COMPACT_THRESHOLD_PREFERENCE_KEY, value }],
|
||||
});
|
||||
}
|
||||
|
||||
export function useAutoCompactPreferences() {
|
||||
@@ -41,31 +49,10 @@ export function useAutoCompactPreferences() {
|
||||
DEFAULT_AUTO_COMPACT_THRESHOLD,
|
||||
);
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
const [syncVersion, setSyncVersion] = useState(0);
|
||||
const retryDelayMsRef = useRef(AUTO_COMPACT_INITIAL_RETRY_DELAY_MS);
|
||||
|
||||
const requestSyncFromConfig = useCallback(() => {
|
||||
setSyncVersion((current) => current + 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
requestSyncFromConfig();
|
||||
};
|
||||
window.addEventListener(
|
||||
AUTO_COMPACT_PREFERENCES_EVENT,
|
||||
handler as EventListener,
|
||||
);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
AUTO_COMPACT_PREFERENCES_EVENT,
|
||||
handler as EventListener,
|
||||
);
|
||||
};
|
||||
}, [requestSyncFromConfig]);
|
||||
|
||||
const syncFromConfig = useCallback(async (_syncVersion: number) => {
|
||||
void _syncVersion;
|
||||
const result = await readConfigValue(AUTO_COMPACT_THRESHOLD_CONFIG_KEY);
|
||||
const syncFromConfig = useCallback(async () => {
|
||||
const result = await readAutoCompactThreshold();
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
@@ -73,8 +60,16 @@ export function useAutoCompactPreferences() {
|
||||
let cancelled = false;
|
||||
let retryTimer: number | null = null;
|
||||
|
||||
const clearRetryTimer = () => {
|
||||
if (retryTimer !== null) {
|
||||
window.clearTimeout(retryTimer);
|
||||
retryTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const applyConfig = async () => {
|
||||
const result = await syncFromConfig(syncVersion);
|
||||
clearRetryTimer();
|
||||
const result = await syncFromConfig();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
@@ -83,24 +78,40 @@ export function useAutoCompactPreferences() {
|
||||
setAutoCompactThresholdState(
|
||||
normalizeAutoCompactThreshold(result.value),
|
||||
);
|
||||
setIsHydrated(true);
|
||||
retryDelayMsRef.current = AUTO_COMPACT_INITIAL_RETRY_DELAY_MS;
|
||||
} else {
|
||||
retryTimer = window.setTimeout(
|
||||
requestSyncFromConfig,
|
||||
AUTO_COMPACT_RETRY_DELAY_MS,
|
||||
const delayMs = retryDelayMsRef.current;
|
||||
retryDelayMsRef.current = Math.min(
|
||||
delayMs * 2,
|
||||
AUTO_COMPACT_MAX_RETRY_DELAY_MS,
|
||||
);
|
||||
retryTimer = window.setTimeout(() => {
|
||||
void applyConfig();
|
||||
}, delayMs);
|
||||
}
|
||||
setIsHydrated(true);
|
||||
};
|
||||
|
||||
const handler = () => {
|
||||
retryDelayMsRef.current = AUTO_COMPACT_INITIAL_RETRY_DELAY_MS;
|
||||
void applyConfig();
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
AUTO_COMPACT_PREFERENCES_EVENT,
|
||||
handler as EventListener,
|
||||
);
|
||||
void applyConfig();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (retryTimer !== null) {
|
||||
window.clearTimeout(retryTimer);
|
||||
}
|
||||
clearRetryTimer();
|
||||
window.removeEventListener(
|
||||
AUTO_COMPACT_PREFERENCES_EVENT,
|
||||
handler as EventListener,
|
||||
);
|
||||
};
|
||||
}, [requestSyncFromConfig, syncFromConfig, syncVersion]);
|
||||
}, [syncFromConfig]);
|
||||
|
||||
const dispatchPreferencesEvent = useCallback(() => {
|
||||
window.dispatchEvent(new Event(AUTO_COMPACT_PREFERENCES_EVENT));
|
||||
@@ -109,7 +120,7 @@ export function useAutoCompactPreferences() {
|
||||
const setAutoCompactThreshold = useCallback(
|
||||
async (value: number) => {
|
||||
const normalized = normalizeAutoCompactThreshold(value);
|
||||
await writeConfigValue(AUTO_COMPACT_THRESHOLD_CONFIG_KEY, normalized);
|
||||
await writeAutoCompactThreshold(normalized);
|
||||
setAutoCompactThresholdState(normalized);
|
||||
setIsHydrated(true);
|
||||
dispatchPreferencesEvent();
|
||||
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
setStoredModelPreference,
|
||||
} from "../lib/modelPreferences";
|
||||
|
||||
const GOOSE_PROVIDER_CONFIG_KEY = "GOOSE_PROVIDER";
|
||||
const GOOSE_MODEL_CONFIG_KEY = "GOOSE_MODEL";
|
||||
const MODEL_ALIAS_IDS = new Set(["current", "default"]);
|
||||
|
||||
export type PreferredModelSelection = {
|
||||
@@ -134,23 +132,14 @@ export function useResolvedAgentModelPicker({
|
||||
const loadGooseDefaultSelection = async () => {
|
||||
try {
|
||||
const client = await getClient();
|
||||
const [providerResponse, modelResponse] = await Promise.all([
|
||||
client.goose.GooseConfigRead({ key: GOOSE_PROVIDER_CONFIG_KEY }),
|
||||
client.goose.GooseConfigRead({ key: GOOSE_MODEL_CONFIG_KEY }),
|
||||
]);
|
||||
const defaults = await client.goose.GooseDefaultsRead({});
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providerId =
|
||||
typeof providerResponse.value === "string"
|
||||
? providerResponse.value
|
||||
: undefined;
|
||||
const modelId =
|
||||
typeof modelResponse.value === "string"
|
||||
? modelResponse.value
|
||||
: undefined;
|
||||
const providerId = defaults.providerId ?? undefined;
|
||||
const modelId = defaults.modelId ?? undefined;
|
||||
|
||||
if (!modelId) {
|
||||
setGooseDefaultSelection(null);
|
||||
|
||||
@@ -3,47 +3,64 @@ import { getClient } from "@/shared/api/acpConnection";
|
||||
import {
|
||||
DEFAULT_AUTO_SUBMIT_PHRASES_RAW,
|
||||
DISABLED_DICTATION_PROVIDER_CONFIG_VALUE,
|
||||
VOICE_AUTO_SUBMIT_PHRASES_CONFIG_KEY,
|
||||
VOICE_DICTATION_PREFERRED_MIC_CONFIG_KEY,
|
||||
VOICE_DICTATION_PROVIDER_CONFIG_KEY,
|
||||
normalizeDictationProvider,
|
||||
parseAutoSubmitPhrases,
|
||||
} from "../lib/voiceInput";
|
||||
import type { DictationProvider } from "@/shared/types/dictation";
|
||||
|
||||
const VOICE_INPUT_PREFERENCES_EVENT = "goose:voice-input-preferences";
|
||||
const VOICE_AUTO_SUBMIT_PHRASES_PREFERENCE_KEY = "voiceAutoSubmitPhrases";
|
||||
const VOICE_DICTATION_PROVIDER_PREFERENCE_KEY = "voiceDictationProvider";
|
||||
const VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY =
|
||||
"voiceDictationPreferredMic";
|
||||
type VoicePreferenceKey =
|
||||
| typeof VOICE_AUTO_SUBMIT_PHRASES_PREFERENCE_KEY
|
||||
| typeof VOICE_DICTATION_PROVIDER_PREFERENCE_KEY
|
||||
| typeof VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY;
|
||||
|
||||
type ConfigReadResult = { ok: true; value: string | null } | { ok: false };
|
||||
|
||||
async function readConfigString(key: string): Promise<ConfigReadResult> {
|
||||
async function readPreferenceStrings(
|
||||
keys: VoicePreferenceKey[],
|
||||
): Promise<Record<VoicePreferenceKey, ConfigReadResult>> {
|
||||
const unavailable = Object.fromEntries(
|
||||
keys.map((key) => [key, { ok: false }]),
|
||||
) as Record<VoicePreferenceKey, ConfigReadResult>;
|
||||
|
||||
try {
|
||||
const client = await getClient();
|
||||
const response = await client.goose.GooseConfigRead({ key });
|
||||
return {
|
||||
ok: true,
|
||||
value: typeof response.value === "string" ? response.value : null,
|
||||
};
|
||||
const response = await client.goose.GoosePreferencesRead({ keys });
|
||||
const values = new Map(
|
||||
response.values.map((entry) => [entry.key, entry.value]),
|
||||
);
|
||||
return Object.fromEntries(
|
||||
keys.map((key) => {
|
||||
const value = values.get(key);
|
||||
return [
|
||||
key,
|
||||
{
|
||||
ok: true,
|
||||
value: typeof value === "string" ? value : null,
|
||||
},
|
||||
];
|
||||
}),
|
||||
) as Record<VoicePreferenceKey, ConfigReadResult>;
|
||||
} catch {
|
||||
return { ok: false };
|
||||
return unavailable;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeConfigString(key: string, value: string): Promise<void> {
|
||||
try {
|
||||
const client = await getClient();
|
||||
await client.goose.GooseConfigUpsert({ key, value });
|
||||
} catch {
|
||||
// goose config may be unavailable
|
||||
}
|
||||
async function writePreferenceString(
|
||||
key: VoicePreferenceKey,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
const client = await getClient();
|
||||
await client.goose.GoosePreferencesSave({ values: [{ key, value }] });
|
||||
}
|
||||
|
||||
async function removeConfigKey(key: string): Promise<void> {
|
||||
try {
|
||||
const client = await getClient();
|
||||
await client.goose.GooseConfigRemove({ key });
|
||||
} catch {
|
||||
// goose config may be unavailable
|
||||
}
|
||||
async function removePreferenceKey(key: VoicePreferenceKey): Promise<void> {
|
||||
const client = await getClient();
|
||||
await client.goose.GoosePreferencesRemove({ keys: [key] });
|
||||
}
|
||||
|
||||
export function useVoiceInputPreferences() {
|
||||
@@ -65,11 +82,14 @@ export function useVoiceInputPreferences() {
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
|
||||
const syncFromConfig = useCallback(async () => {
|
||||
const [phrasesResult, providerResult, micResult] = await Promise.all([
|
||||
readConfigString(VOICE_AUTO_SUBMIT_PHRASES_CONFIG_KEY),
|
||||
readConfigString(VOICE_DICTATION_PROVIDER_CONFIG_KEY),
|
||||
readConfigString(VOICE_DICTATION_PREFERRED_MIC_CONFIG_KEY),
|
||||
const results = await readPreferenceStrings([
|
||||
VOICE_AUTO_SUBMIT_PHRASES_PREFERENCE_KEY,
|
||||
VOICE_DICTATION_PROVIDER_PREFERENCE_KEY,
|
||||
VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY,
|
||||
]);
|
||||
const phrasesResult = results[VOICE_AUTO_SUBMIT_PHRASES_PREFERENCE_KEY];
|
||||
const providerResult = results[VOICE_DICTATION_PROVIDER_PREFERENCE_KEY];
|
||||
const micResult = results[VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY];
|
||||
|
||||
if (phrasesResult.ok) {
|
||||
setRawAutoSubmitPhrasesState(
|
||||
@@ -99,7 +119,9 @@ export function useVoiceInputPreferences() {
|
||||
// through to the default cleanly.
|
||||
setSelectedProviderState(null);
|
||||
setHasStoredProviderPreferenceState(false);
|
||||
void removeConfigKey(VOICE_DICTATION_PROVIDER_CONFIG_KEY);
|
||||
void removePreferenceKey(VOICE_DICTATION_PROVIDER_PREFERENCE_KEY).catch(
|
||||
() => undefined,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setSelectedProviderState(null);
|
||||
@@ -135,18 +157,23 @@ export function useVoiceInputPreferences() {
|
||||
|
||||
const persistAndBroadcast = useCallback(
|
||||
(operation: Promise<void>) => {
|
||||
void operation.finally(() => {
|
||||
dispatchPreferencesEvent();
|
||||
});
|
||||
void operation
|
||||
.then(() => {
|
||||
dispatchPreferencesEvent();
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.warn("Failed to persist voice input preferences", error);
|
||||
void syncFromConfig();
|
||||
});
|
||||
},
|
||||
[dispatchPreferencesEvent],
|
||||
[dispatchPreferencesEvent, syncFromConfig],
|
||||
);
|
||||
|
||||
const setRawAutoSubmitPhrases = useCallback(
|
||||
(value: string) => {
|
||||
setRawAutoSubmitPhrasesState(value);
|
||||
persistAndBroadcast(
|
||||
writeConfigString(VOICE_AUTO_SUBMIT_PHRASES_CONFIG_KEY, value),
|
||||
writePreferenceString(VOICE_AUTO_SUBMIT_PHRASES_PREFERENCE_KEY, value),
|
||||
);
|
||||
},
|
||||
[persistAndBroadcast],
|
||||
@@ -157,8 +184,8 @@ export function useVoiceInputPreferences() {
|
||||
setSelectedProviderState(value);
|
||||
setHasStoredProviderPreferenceState(true);
|
||||
persistAndBroadcast(
|
||||
writeConfigString(
|
||||
VOICE_DICTATION_PROVIDER_CONFIG_KEY,
|
||||
writePreferenceString(
|
||||
VOICE_DICTATION_PROVIDER_PREFERENCE_KEY,
|
||||
value ?? DISABLED_DICTATION_PROVIDER_CONFIG_VALUE,
|
||||
),
|
||||
);
|
||||
@@ -172,7 +199,9 @@ export function useVoiceInputPreferences() {
|
||||
const clearSelectedProvider = useCallback(() => {
|
||||
setSelectedProviderState(null);
|
||||
setHasStoredProviderPreferenceState(false);
|
||||
persistAndBroadcast(removeConfigKey(VOICE_DICTATION_PROVIDER_CONFIG_KEY));
|
||||
persistAndBroadcast(
|
||||
removePreferenceKey(VOICE_DICTATION_PROVIDER_PREFERENCE_KEY),
|
||||
);
|
||||
}, [persistAndBroadcast]);
|
||||
|
||||
const setPreferredMicrophoneId = useCallback(
|
||||
@@ -180,11 +209,14 @@ export function useVoiceInputPreferences() {
|
||||
setPreferredMicrophoneIdState(value);
|
||||
if (value) {
|
||||
persistAndBroadcast(
|
||||
writeConfigString(VOICE_DICTATION_PREFERRED_MIC_CONFIG_KEY, value),
|
||||
writePreferenceString(
|
||||
VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY,
|
||||
value,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
persistAndBroadcast(
|
||||
removeConfigKey(VOICE_DICTATION_PREFERRED_MIC_CONFIG_KEY),
|
||||
removePreferenceKey(VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,8 +3,7 @@ import type {
|
||||
DictationProviderStatus,
|
||||
} from "@/shared/types/dictation";
|
||||
|
||||
// goose config keys — stored in the user's goose config.yaml via the
|
||||
// _goose/config/{read,upsert,remove} ACP methods, not localStorage.
|
||||
// Stored in the user's goose config.yaml via typed ACP preference methods, not localStorage.
|
||||
export const VOICE_AUTO_SUBMIT_PHRASES_CONFIG_KEY = "VOICE_AUTO_SUBMIT_PHRASES";
|
||||
export const VOICE_DICTATION_PROVIDER_CONFIG_KEY = "VOICE_DICTATION_PROVIDER";
|
||||
export const VOICE_DICTATION_PREFERRED_MIC_CONFIG_KEY =
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
type AppRendererProps,
|
||||
type McpUiHostContext,
|
||||
} from "@mcp-ui/client";
|
||||
import type { GooseToolCallResponse } from "@aaif/goose-sdk";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import packageJson from "../../../../package.json";
|
||||
@@ -31,18 +30,13 @@ interface McpAppViewProps {
|
||||
|
||||
const DEFAULT_APP_HEIGHT = 240;
|
||||
// Goose2 currently only implements inline display mode.
|
||||
type HostContextDisplayMode = NonNullable<
|
||||
const GOOSE2_DISPLAY_MODE = "inline" satisfies NonNullable<
|
||||
McpUiHostContext["displayMode"]
|
||||
>;
|
||||
const AVAILABLE_DISPLAY_MODES: NonNullable<
|
||||
McpUiHostContext["availableDisplayModes"]
|
||||
>[number];
|
||||
type AvailableDisplayMode = Extract<HostContextDisplayMode, "inline">;
|
||||
const AVAILABLE_DISPLAY_MODES = [
|
||||
"inline",
|
||||
] satisfies readonly AvailableDisplayMode[];
|
||||
> = [GOOSE2_DISPLAY_MODE];
|
||||
const GOOSE2_USER_AGENT = `${packageJson.name}/${packageJson.version}`;
|
||||
const GOOSE2_HOST_INFO = {
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
} satisfies NonNullable<AppRendererProps["hostInfo"]>;
|
||||
const DESKTOP_SAFE_AREA_INSETS = {
|
||||
top: 0,
|
||||
right: 0,
|
||||
@@ -59,6 +53,12 @@ type CallToolResult = Awaited<
|
||||
type ReadResourceResult = Awaited<
|
||||
ReturnType<NonNullable<AppRendererProps["onReadResource"]>>
|
||||
>;
|
||||
type GooseToolCallResponse = {
|
||||
content?: unknown[];
|
||||
structuredContent?: unknown;
|
||||
isError: boolean;
|
||||
_meta?: unknown;
|
||||
};
|
||||
type HostContextToolInfo = NonNullable<McpUiHostContext["toolInfo"]>;
|
||||
type HostContextTool = HostContextToolInfo["tool"];
|
||||
|
||||
@@ -272,7 +272,7 @@ export function McpAppView({
|
||||
const hostContext = useMemo<McpUiHostContext>(
|
||||
() => ({
|
||||
theme: resolvedTheme,
|
||||
displayMode: "inline",
|
||||
displayMode: GOOSE2_DISPLAY_MODE,
|
||||
availableDisplayModes: [...AVAILABLE_DISPLAY_MODES],
|
||||
containerDimensions:
|
||||
containerWidth !== null
|
||||
@@ -426,7 +426,6 @@ export function McpAppView({
|
||||
toolResourceUri={renderableDocument.resourceUri}
|
||||
html={renderableDocument.html}
|
||||
sandbox={sandbox}
|
||||
hostInfo={GOOSE2_HOST_INFO}
|
||||
toolInput={currentToolInput}
|
||||
toolResult={currentToolResult}
|
||||
hostContext={hostContext}
|
||||
|
||||
@@ -260,7 +260,7 @@ describe("McpAppView nested tool calls", () => {
|
||||
expect(borderlessChrome?.className).not.toContain("overflow-hidden");
|
||||
});
|
||||
|
||||
it("passes Goose2 package identity as host info", async () => {
|
||||
it("passes Goose2 package identity in host context", async () => {
|
||||
render(
|
||||
<McpAppView
|
||||
payload={createPayload()}
|
||||
@@ -272,10 +272,9 @@ describe("McpAppView nested tool calls", () => {
|
||||
expect(screen.getByTestId("mock-app-renderer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(getLatestAppRendererProps().hostInfo).toEqual({
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
});
|
||||
expect(getLatestAppRendererProps().hostContext?.userAgent).toBe(
|
||||
`${packageJson.name}/${packageJson.version}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not install a fallback handler for non-standard app requests", async () => {
|
||||
|
||||
@@ -79,6 +79,12 @@ describe("useCredentials", () => {
|
||||
});
|
||||
|
||||
it("saves secret fields through the credential API and syncs inventory without requiring restart", async () => {
|
||||
const voiceConfigListener = vi.fn();
|
||||
window.addEventListener(
|
||||
"goose:voice-dictation-config",
|
||||
voiceConfigListener,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useCredentials());
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
@@ -110,8 +116,14 @@ describe("useCredentials", () => {
|
||||
initialRefresh: saveResponse.refresh,
|
||||
}),
|
||||
);
|
||||
expect(voiceConfigListener).toHaveBeenCalledTimes(1);
|
||||
expect(result.current).not.toHaveProperty("needsRestart");
|
||||
expect(result.current).not.toHaveProperty("restart");
|
||||
|
||||
window.removeEventListener(
|
||||
"goose:voice-dictation-config",
|
||||
voiceConfigListener,
|
||||
);
|
||||
});
|
||||
|
||||
it("records refresh failure as a provider warning without rejecting the save", async () => {
|
||||
@@ -140,6 +152,12 @@ describe("useCredentials", () => {
|
||||
});
|
||||
|
||||
it("suppresses stale refresh errors after deleting provider config", async () => {
|
||||
const voiceConfigListener = vi.fn();
|
||||
window.addEventListener(
|
||||
"goose:voice-dictation-config",
|
||||
voiceConfigListener,
|
||||
);
|
||||
|
||||
mocks.syncProviderInventory.mockResolvedValueOnce({
|
||||
entries: [
|
||||
{
|
||||
@@ -162,7 +180,13 @@ describe("useCredentials", () => {
|
||||
await waitFor(() =>
|
||||
expect(result.current.syncingProviderIds.has("anthropic")).toBe(false),
|
||||
);
|
||||
expect(voiceConfigListener).toHaveBeenCalledTimes(1);
|
||||
expect(result.current.inventoryWarnings.has("anthropic")).toBe(false);
|
||||
|
||||
window.removeEventListener(
|
||||
"goose:voice-dictation-config",
|
||||
voiceConfigListener,
|
||||
);
|
||||
});
|
||||
|
||||
it("invalidates native OAuth secrets before refreshing provider status", async () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type ProviderStatus,
|
||||
checkAllProviderStatus,
|
||||
} from "@/features/providers/api/credentials";
|
||||
import { notifyVoiceDictationConfigChanged } from "@/features/chat/lib/voiceInput";
|
||||
import {
|
||||
syncProviderInventory,
|
||||
type SyncProviderInventoryResult,
|
||||
@@ -208,6 +209,7 @@ export function useCredentials(): UseCredentialsReturn {
|
||||
fields.map(({ key, value }) => ({ key, value })),
|
||||
);
|
||||
updateProviderStatus(result.status);
|
||||
notifyVoiceDictationConfigChanged();
|
||||
startInventorySync(providerId, result.refresh);
|
||||
} finally {
|
||||
setProviderSaving(providerId, false);
|
||||
@@ -222,6 +224,7 @@ export function useCredentials(): UseCredentialsReturn {
|
||||
try {
|
||||
const result = await deleteProviderConfig(providerId);
|
||||
updateProviderStatus(result.status);
|
||||
notifyVoiceDictationConfigChanged();
|
||||
startInventorySync(providerId, result.refresh);
|
||||
} finally {
|
||||
setProviderSaving(providerId, false);
|
||||
@@ -241,6 +244,7 @@ export function useCredentials(): UseCredentialsReturn {
|
||||
setProviderInventoryWarning(providerId, errorMessage(error));
|
||||
}
|
||||
await refreshStatuses();
|
||||
notifyVoiceDictationConfigChanged();
|
||||
startInventorySync(providerId, initialRefresh);
|
||||
},
|
||||
[refreshStatuses, setProviderInventoryWarning, startInventorySync],
|
||||
|
||||
@@ -58,6 +58,10 @@ export function SettingsModal({
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveSection(initialSection);
|
||||
}, [initialSection]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getDefaultDictationProvider,
|
||||
} from "@/features/chat/lib/voiceInput";
|
||||
import { useVoiceInputPreferences } from "@/features/chat/hooks/useVoiceInputPreferences";
|
||||
import { requestOpenSettings } from "@/features/settings/lib/settingsEvents";
|
||||
import type {
|
||||
DictationProvider,
|
||||
DictationProviderStatus,
|
||||
@@ -145,11 +146,7 @@ export function VoiceInputSettings() {
|
||||
|
||||
setError(null);
|
||||
try {
|
||||
await saveDictationProviderSecret(
|
||||
selectedProvider,
|
||||
apiKeyInput,
|
||||
selectedStatus?.configKey ?? undefined,
|
||||
);
|
||||
await saveDictationProviderSecret(selectedProvider, apiKeyInput);
|
||||
setApiKeyInput("");
|
||||
setIsEditingApiKey(false);
|
||||
await refreshConfig();
|
||||
@@ -161,7 +158,7 @@ export function VoiceInputSettings() {
|
||||
: t("general.voiceInput.saveError"),
|
||||
);
|
||||
}
|
||||
}, [apiKeyInput, refreshConfig, selectedProvider, selectedStatus, t]);
|
||||
}, [apiKeyInput, refreshConfig, selectedProvider, t]);
|
||||
|
||||
const removeApiKey = useCallback(async () => {
|
||||
if (!selectedProvider) {
|
||||
@@ -170,10 +167,7 @@ export function VoiceInputSettings() {
|
||||
|
||||
setError(null);
|
||||
try {
|
||||
await deleteDictationProviderSecret(
|
||||
selectedProvider,
|
||||
selectedStatus?.configKey ?? undefined,
|
||||
);
|
||||
await deleteDictationProviderSecret(selectedProvider);
|
||||
setApiKeyInput("");
|
||||
setIsEditingApiKey(false);
|
||||
await refreshConfig();
|
||||
@@ -185,7 +179,7 @@ export function VoiceInputSettings() {
|
||||
: t("general.voiceInput.deleteError"),
|
||||
);
|
||||
}
|
||||
}, [refreshConfig, selectedProvider, selectedStatus, t]);
|
||||
}, [refreshConfig, selectedProvider, t]);
|
||||
|
||||
const handleModelChange = useCallback(
|
||||
async (modelId: string) => {
|
||||
@@ -333,6 +327,27 @@ export function VoiceInputSettings() {
|
||||
|
||||
{selectedStatus ? (
|
||||
<>
|
||||
{selectedStatus.usesProviderConfig ? (
|
||||
<div className="space-y-3 rounded-lg border border-border px-3 py-3">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
{t("general.voiceInput.providerConfigLabel")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{t("general.voiceInput.providerConfigDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline-flat"
|
||||
onClick={() => requestOpenSettings("providers")}
|
||||
>
|
||||
{t("general.voiceInput.openProviders")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!selectedStatus.usesProviderConfig &&
|
||||
selectedProvider !== "local" ? (
|
||||
<div className="space-y-3 rounded-lg border border-border px-3 py-3">
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { VoiceInputSettings } from "../VoiceInputSettings";
|
||||
|
||||
const mockGetDictationConfig = vi.fn();
|
||||
const mockUseVoiceInputPreferences = vi.fn();
|
||||
|
||||
vi.mock("@/shared/api/dictation", () => ({
|
||||
getDictationConfig: () => mockGetDictationConfig(),
|
||||
saveDictationModelSelection: vi.fn(),
|
||||
saveDictationProviderSecret: vi.fn(),
|
||||
deleteDictationProviderSecret: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/chat/hooks/useVoiceInputPreferences", () => ({
|
||||
useVoiceInputPreferences: () => mockUseVoiceInputPreferences(),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/ui/ai-elements/mic-selector", () => ({
|
||||
useAudioDevices: () => ({
|
||||
devices: [],
|
||||
error: null,
|
||||
hasPermission: false,
|
||||
loadDevices: vi.fn(),
|
||||
loading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../LocalWhisperModels", () => ({
|
||||
LocalWhisperModels: () => <div />,
|
||||
}));
|
||||
|
||||
describe("VoiceInputSettings", () => {
|
||||
beforeEach(() => {
|
||||
mockGetDictationConfig.mockReset();
|
||||
mockUseVoiceInputPreferences.mockReset();
|
||||
mockUseVoiceInputPreferences.mockReturnValue({
|
||||
clearSelectedProvider: vi.fn(),
|
||||
hasStoredProviderPreference: true,
|
||||
isHydrated: true,
|
||||
preferredMicrophoneId: null,
|
||||
rawAutoSubmitPhrases: "submit",
|
||||
selectedProvider: "openai",
|
||||
setPreferredMicrophoneId: vi.fn(),
|
||||
setRawAutoSubmitPhrases: vi.fn(),
|
||||
setSelectedProvider: vi.fn(),
|
||||
});
|
||||
mockGetDictationConfig.mockResolvedValue({
|
||||
openai: {
|
||||
configured: false,
|
||||
description: "Uses OpenAI Whisper API for high-quality transcription.",
|
||||
usesProviderConfig: true,
|
||||
settingsPath: "Settings > Models",
|
||||
configKey: null,
|
||||
modelConfigKey: "OPENAI_TRANSCRIPTION_MODEL",
|
||||
defaultModel: "whisper-1",
|
||||
selectedModel: null,
|
||||
availableModels: [
|
||||
{
|
||||
id: "whisper-1",
|
||||
label: "Whisper-1",
|
||||
description: "OpenAI's hosted Whisper transcription model.",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("points OpenAI Whisper setup to provider settings", async () => {
|
||||
const user = userEvent.setup();
|
||||
const openSettingsListener = vi.fn();
|
||||
window.addEventListener("goose:open-settings", openSettingsListener);
|
||||
|
||||
render(<VoiceInputSettings />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("Provider credentials")).toBeInTheDocument(),
|
||||
);
|
||||
expect(
|
||||
screen.getByText(
|
||||
"This transcription provider uses the credentials from its model provider setup.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Add API key" }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Open Providers" }));
|
||||
|
||||
expect(openSettingsListener).toHaveBeenCalledTimes(1);
|
||||
expect(openSettingsListener.mock.calls[0][0]).toMatchObject({
|
||||
detail: { section: "providers" },
|
||||
});
|
||||
|
||||
window.removeEventListener("goose:open-settings", openSettingsListener);
|
||||
});
|
||||
});
|
||||
@@ -1,161 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import {
|
||||
cancelDictationLocalModelDownload,
|
||||
deleteDictationLocalModel,
|
||||
deleteDictationProviderSecret,
|
||||
downloadDictationLocalModel,
|
||||
getDictationConfig,
|
||||
getDictationLocalModelDownloadProgress,
|
||||
listDictationLocalModels,
|
||||
saveDictationModelSelection,
|
||||
saveDictationProviderSecret,
|
||||
transcribeDictation,
|
||||
} from "../dictation";
|
||||
import { getClient } from "../acpConnection";
|
||||
|
||||
vi.mock("../acpConnection", () => ({
|
||||
getClient: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("dictation SDK wiring", () => {
|
||||
let client: { goose: Record<string, ReturnType<typeof vi.fn>> };
|
||||
beforeEach(() => {
|
||||
client = {
|
||||
goose: {
|
||||
GooseDictationConfig: vi.fn().mockResolvedValue({
|
||||
providers: {
|
||||
openai: {
|
||||
configured: true,
|
||||
description: "OpenAI transcription",
|
||||
usesProviderConfig: true,
|
||||
availableModels: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
GooseDictationTranscribe: vi.fn().mockResolvedValue({ text: "hello" }),
|
||||
},
|
||||
};
|
||||
vi.mocked(getClient).mockResolvedValue(
|
||||
client as unknown as Awaited<ReturnType<typeof getClient>>,
|
||||
);
|
||||
});
|
||||
|
||||
it("getDictationConfig calls GooseDictationConfig and returns providers map", async () => {
|
||||
const result = await getDictationConfig();
|
||||
expect(client.goose.GooseDictationConfig).toHaveBeenCalledWith({});
|
||||
expect(result.openai.configured).toBe(true);
|
||||
});
|
||||
|
||||
it("transcribeDictation forwards audio + mimeType + provider", async () => {
|
||||
const result = await transcribeDictation({
|
||||
audio: "base64==",
|
||||
mimeType: "audio/webm",
|
||||
provider: "openai",
|
||||
});
|
||||
expect(client.goose.GooseDictationTranscribe).toHaveBeenCalledWith({
|
||||
audio: "base64==",
|
||||
mimeType: "audio/webm",
|
||||
provider: "openai",
|
||||
});
|
||||
expect(result.text).toBe("hello");
|
||||
});
|
||||
|
||||
it("saveDictationModelSelection calls GooseDictationModelSelect", async () => {
|
||||
client.goose.GooseDictationModelSelect = vi.fn().mockResolvedValue({});
|
||||
await saveDictationModelSelection("local", "tiny");
|
||||
expect(client.goose.GooseDictationModelSelect).toHaveBeenCalledWith({
|
||||
provider: "local",
|
||||
modelId: "tiny",
|
||||
});
|
||||
});
|
||||
|
||||
it("saveDictationProviderSecret calls GooseSecretUpsert", async () => {
|
||||
client.goose.GooseSecretUpsert = vi.fn().mockResolvedValue({});
|
||||
await saveDictationProviderSecret("groq", "gsk-test", "GROQ_API_KEY");
|
||||
expect(client.goose.GooseSecretUpsert).toHaveBeenCalledWith({
|
||||
key: "GROQ_API_KEY",
|
||||
value: "gsk-test",
|
||||
});
|
||||
});
|
||||
|
||||
it("deleteDictationProviderSecret calls GooseSecretRemove", async () => {
|
||||
client.goose.GooseSecretRemove = vi.fn().mockResolvedValue({});
|
||||
await deleteDictationProviderSecret("groq", "GROQ_API_KEY");
|
||||
expect(client.goose.GooseSecretRemove).toHaveBeenCalledWith({
|
||||
key: "GROQ_API_KEY",
|
||||
});
|
||||
});
|
||||
|
||||
it("listDictationLocalModels returns the models array", async () => {
|
||||
client.goose.GooseDictationModelsList = vi.fn().mockResolvedValue({
|
||||
models: [
|
||||
{
|
||||
id: "tiny",
|
||||
description: "Tiny",
|
||||
sizeMb: 75,
|
||||
downloaded: true,
|
||||
downloadInProgress: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await listDictationLocalModels();
|
||||
expect(client.goose.GooseDictationModelsList).toHaveBeenCalledWith({});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe("tiny");
|
||||
});
|
||||
|
||||
it("downloadDictationLocalModel forwards modelId", async () => {
|
||||
client.goose.GooseDictationModelsDownload = vi.fn().mockResolvedValue({});
|
||||
await downloadDictationLocalModel("tiny");
|
||||
expect(client.goose.GooseDictationModelsDownload).toHaveBeenCalledWith({
|
||||
modelId: "tiny",
|
||||
});
|
||||
});
|
||||
|
||||
it("getDictationLocalModelDownloadProgress returns progress or null", async () => {
|
||||
client.goose.GooseDictationModelsDownloadProgress = vi
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
progress: {
|
||||
bytesDownloaded: 100,
|
||||
totalBytes: 1000,
|
||||
progressPercent: 10,
|
||||
status: "downloading",
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
const result = await getDictationLocalModelDownloadProgress("tiny");
|
||||
expect(result?.bytesDownloaded).toBe(100);
|
||||
expect(
|
||||
client.goose.GooseDictationModelsDownloadProgress,
|
||||
).toHaveBeenCalledWith({
|
||||
modelId: "tiny",
|
||||
});
|
||||
});
|
||||
|
||||
it("getDictationLocalModelDownloadProgress returns null when no download", async () => {
|
||||
client.goose.GooseDictationModelsDownloadProgress = vi
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
progress: undefined,
|
||||
});
|
||||
const result = await getDictationLocalModelDownloadProgress("tiny");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("cancelDictationLocalModelDownload forwards modelId", async () => {
|
||||
client.goose.GooseDictationModelsCancel = vi.fn().mockResolvedValue({});
|
||||
await cancelDictationLocalModelDownload("tiny");
|
||||
expect(client.goose.GooseDictationModelsCancel).toHaveBeenCalledWith({
|
||||
modelId: "tiny",
|
||||
});
|
||||
});
|
||||
|
||||
it("deleteDictationLocalModel forwards modelId", async () => {
|
||||
client.goose.GooseDictationModelsDelete = vi.fn().mockResolvedValue({});
|
||||
await deleteDictationLocalModel("tiny");
|
||||
expect(client.goose.GooseDictationModelsDelete).toHaveBeenCalledWith({
|
||||
modelId: "tiny",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -40,26 +40,18 @@ export async function saveDictationModelSelection(
|
||||
}
|
||||
|
||||
export async function saveDictationProviderSecret(
|
||||
_provider: DictationProvider,
|
||||
provider: DictationProvider,
|
||||
value: string,
|
||||
configKey?: string,
|
||||
): Promise<void> {
|
||||
if (!configKey) {
|
||||
throw new Error("No config key for this provider");
|
||||
}
|
||||
const client = await getClient();
|
||||
await client.goose.GooseSecretUpsert({ key: configKey, value });
|
||||
await client.goose.GooseDictationSecretSave({ provider, value });
|
||||
}
|
||||
|
||||
export async function deleteDictationProviderSecret(
|
||||
_provider: DictationProvider,
|
||||
configKey?: string,
|
||||
provider: DictationProvider,
|
||||
): Promise<void> {
|
||||
if (!configKey) {
|
||||
throw new Error("Cannot delete secrets for this provider");
|
||||
}
|
||||
const client = await getClient();
|
||||
await client.goose.GooseSecretRemove({ key: configKey });
|
||||
await client.goose.GooseDictationSecretDelete({ provider });
|
||||
}
|
||||
|
||||
export async function listDictationLocalModels(): Promise<
|
||||
|
||||
@@ -176,6 +176,9 @@
|
||||
"addApiKey": "Add API key",
|
||||
"updateApiKey": "Update API key",
|
||||
"removeApiKey": "Remove API key",
|
||||
"providerConfigLabel": "Provider credentials",
|
||||
"providerConfigDescription": "This transcription provider uses the credentials from its model provider setup.",
|
||||
"openProviders": "Open Providers",
|
||||
"localModelLabel": "Local Whisper Model",
|
||||
"localModelDescription": "Download a Whisper model to run transcription locally. Selecting a model sets it as your active local transcription model.",
|
||||
"noLocalModels": "No local Whisper models available.",
|
||||
|
||||
@@ -176,6 +176,9 @@
|
||||
"addApiKey": "Agregar clave API",
|
||||
"updateApiKey": "Actualizar clave API",
|
||||
"removeApiKey": "Eliminar clave API",
|
||||
"providerConfigLabel": "Credenciales del proveedor",
|
||||
"providerConfigDescription": "Este proveedor de transcripción usa las credenciales de la configuración de su proveedor de modelos.",
|
||||
"openProviders": "Abrir proveedores",
|
||||
"localModelLabel": "Modelo Whisper local",
|
||||
"localModelDescription": "Descarga un modelo Whisper para transcribir localmente. Seleccionar un modelo lo establece como tu modelo de transcripción local activo.",
|
||||
"noLocalModels": "No hay modelos Whisper locales disponibles.",
|
||||
|
||||
@@ -11,8 +11,6 @@ import type {
|
||||
AddConfigExtensionRequest,
|
||||
AddExtensionRequest,
|
||||
ArchiveSessionRequest,
|
||||
CheckSecretRequest,
|
||||
CheckSecretResponse,
|
||||
CreateSourceRequest,
|
||||
CreateSourceResponse,
|
||||
CustomProviderCreateRequest,
|
||||
@@ -23,6 +21,8 @@ import type {
|
||||
CustomProviderReadResponse,
|
||||
CustomProviderUpdateRequest,
|
||||
CustomProviderUpdateResponse,
|
||||
DefaultsReadRequest,
|
||||
DefaultsReadResponse,
|
||||
DeleteSessionRequest,
|
||||
DeleteSourceRequest,
|
||||
DictationConfigRequest,
|
||||
@@ -35,6 +35,8 @@ import type {
|
||||
DictationModelSelectRequest,
|
||||
DictationModelsListRequest,
|
||||
DictationModelsListResponse,
|
||||
DictationSecretDeleteRequest,
|
||||
DictationSecretSaveRequest,
|
||||
DictationTranscribeRequest,
|
||||
DictationTranscribeResponse,
|
||||
ExportSessionRequest,
|
||||
@@ -57,6 +59,10 @@ import type {
|
||||
ListProvidersResponse,
|
||||
ListSourcesRequest,
|
||||
ListSourcesResponse,
|
||||
PreferencesReadRequest,
|
||||
PreferencesReadResponse,
|
||||
PreferencesRemoveRequest,
|
||||
PreferencesSaveRequest,
|
||||
ProviderCatalogListRequest,
|
||||
ProviderCatalogListResponse,
|
||||
ProviderCatalogTemplateRequest,
|
||||
@@ -68,16 +74,12 @@ import type {
|
||||
ProviderConfigSaveRequest,
|
||||
ProviderConfigStatusRequest,
|
||||
ProviderConfigStatusResponse,
|
||||
ReadConfigRequest,
|
||||
ReadConfigResponse,
|
||||
ReadResourceRequest,
|
||||
ReadResourceResponse,
|
||||
RefreshProviderInventoryRequest,
|
||||
RefreshProviderInventoryResponse,
|
||||
RemoveConfigExtensionRequest,
|
||||
RemoveConfigRequest,
|
||||
RemoveExtensionRequest,
|
||||
RemoveSecretRequest,
|
||||
RenameSessionRequest,
|
||||
ToggleConfigExtensionRequest,
|
||||
UnarchiveSessionRequest,
|
||||
@@ -85,16 +87,14 @@ import type {
|
||||
UpdateSourceRequest,
|
||||
UpdateSourceResponse,
|
||||
UpdateWorkingDirRequest,
|
||||
UpsertConfigRequest,
|
||||
UpsertSecretRequest,
|
||||
} from './types.gen.js';
|
||||
import {
|
||||
zCheckSecretResponse,
|
||||
zCreateSourceResponse,
|
||||
zCustomProviderCreateResponse,
|
||||
zCustomProviderDeleteResponse,
|
||||
zCustomProviderReadResponse,
|
||||
zCustomProviderUpdateResponse,
|
||||
zDefaultsReadResponse,
|
||||
zDictationConfigResponse,
|
||||
zDictationModelDownloadProgressResponse,
|
||||
zDictationModelsListResponse,
|
||||
@@ -109,12 +109,12 @@ import {
|
||||
zImportSourcesResponse,
|
||||
zListProvidersResponse,
|
||||
zListSourcesResponse,
|
||||
zPreferencesReadResponse,
|
||||
zProviderCatalogListResponse,
|
||||
zProviderCatalogTemplateResponse,
|
||||
zProviderConfigChangeResponse,
|
||||
zProviderConfigReadResponse,
|
||||
zProviderConfigStatusResponse,
|
||||
zReadConfigResponse,
|
||||
zReadResourceResponse,
|
||||
zRefreshProviderInventoryResponse,
|
||||
zUpdateSourceResponse,
|
||||
@@ -327,34 +327,28 @@ export class GooseExtClient {
|
||||
) as ProviderConfigChangeResponse;
|
||||
}
|
||||
|
||||
async GooseConfigRead(
|
||||
params: ReadConfigRequest,
|
||||
): Promise<ReadConfigResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/config/read", params);
|
||||
return zReadConfigResponse.parse(raw) as ReadConfigResponse;
|
||||
async GoosePreferencesRead(
|
||||
params: PreferencesReadRequest,
|
||||
): Promise<PreferencesReadResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/preferences/read", params);
|
||||
return zPreferencesReadResponse.parse(raw) as PreferencesReadResponse;
|
||||
}
|
||||
|
||||
async GooseConfigUpsert(params: UpsertConfigRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/config/upsert", params);
|
||||
async GoosePreferencesSave(params: PreferencesSaveRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/preferences/save", params);
|
||||
}
|
||||
|
||||
async GooseConfigRemove(params: RemoveConfigRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/config/remove", params);
|
||||
async GoosePreferencesRemove(
|
||||
params: PreferencesRemoveRequest,
|
||||
): Promise<void> {
|
||||
await this.conn.extMethod("_goose/preferences/remove", params);
|
||||
}
|
||||
|
||||
async GooseSecretCheck(
|
||||
params: CheckSecretRequest,
|
||||
): Promise<CheckSecretResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/secret/check", params);
|
||||
return zCheckSecretResponse.parse(raw) as CheckSecretResponse;
|
||||
}
|
||||
|
||||
async GooseSecretUpsert(params: UpsertSecretRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/secret/upsert", params);
|
||||
}
|
||||
|
||||
async GooseSecretRemove(params: RemoveSecretRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/secret/remove", params);
|
||||
async GooseDefaultsRead(
|
||||
params: DefaultsReadRequest,
|
||||
): Promise<DefaultsReadResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/defaults/read", params);
|
||||
return zDefaultsReadResponse.parse(raw) as DefaultsReadResponse;
|
||||
}
|
||||
|
||||
async GooseSessionExport(
|
||||
@@ -447,6 +441,18 @@ export class GooseExtClient {
|
||||
return zDictationConfigResponse.parse(raw) as DictationConfigResponse;
|
||||
}
|
||||
|
||||
async GooseDictationSecretSave(
|
||||
params: DictationSecretSaveRequest,
|
||||
): Promise<void> {
|
||||
await this.conn.extMethod("_goose/dictation/secret/save", params);
|
||||
}
|
||||
|
||||
async GooseDictationSecretDelete(
|
||||
params: DictationSecretDeleteRequest,
|
||||
): Promise<void> {
|
||||
await this.conn.extMethod("_goose/dictation/secret/delete", params);
|
||||
}
|
||||
|
||||
async GooseDictationModelsList(
|
||||
params: DictationModelsListRequest,
|
||||
): Promise<DictationModelsListResponse> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type { AddConfigExtensionRequest, AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, CreateSourceRequest, CreateSourceResponse, CustomProviderConfigDto, CustomProviderCreateRequest, CustomProviderCreateResponse, CustomProviderDeleteRequest, CustomProviderDeleteResponse, CustomProviderReadRequest, CustomProviderReadResponse, CustomProviderUpdateRequest, CustomProviderUpdateResponse, 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, GooseToolCallRequest, GooseToolCallResponse, ImportSessionRequest, ImportSessionResponse, ImportSourcesRequest, ImportSourcesResponse, ListProvidersRequest, ListProvidersResponse, ListSourcesRequest, ListSourcesResponse, ProviderCatalogEntryDto, ProviderCatalogListRequest, ProviderCatalogListResponse, ProviderCatalogTemplateRequest, ProviderCatalogTemplateResponse, ProviderConfigChangeResponse, ProviderConfigDeleteRequest, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest, ProviderConfigReadResponse, ProviderConfigSaveRequest, ProviderConfigStatusDto, ProviderConfigStatusRequest, ProviderConfigStatusResponse, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderTemplateCapabilitiesDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RefreshProviderInventoryRequest, RefreshProviderInventoryResponse, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, RenameSessionRequest, SourceEntry, SourceType, ToggleConfigExtensionRequest, UnarchiveSessionRequest, UpdateSessionProjectRequest, UpdateSourceRequest, UpdateSourceResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js';
|
||||
export type { AddConfigExtensionRequest, AddExtensionRequest, ArchiveSessionRequest, CreateSourceRequest, CreateSourceResponse, CustomProviderConfigDto, CustomProviderCreateRequest, CustomProviderCreateResponse, CustomProviderDeleteRequest, CustomProviderDeleteResponse, CustomProviderReadRequest, CustomProviderReadResponse, CustomProviderUpdateRequest, CustomProviderUpdateResponse, DefaultsReadRequest, DefaultsReadResponse, DeleteSessionRequest, DeleteSourceRequest, DictationConfigRequest, DictationConfigResponse, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest, DictationModelDeleteRequest, DictationModelDownloadProgressRequest, DictationModelDownloadProgressResponse, DictationModelDownloadRequest, DictationModelOption, DictationModelSelectRequest, DictationModelsListRequest, DictationModelsListResponse, DictationProviderStatusEntry, DictationSecretDeleteRequest, DictationSecretSaveRequest, DictationTranscribeRequest, DictationTranscribeResponse, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExportSourceRequest, ExportSourceResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, GooseToolCallRequest, GooseToolCallResponse, ImportSessionRequest, ImportSessionResponse, ImportSourcesRequest, ImportSourcesResponse, ListProvidersRequest, ListProvidersResponse, ListSourcesRequest, ListSourcesResponse, PreferenceKey, PreferencesReadRequest, PreferencesReadResponse, PreferencesRemoveRequest, PreferencesSaveRequest, PreferenceValue, ProviderCatalogEntryDto, ProviderCatalogListRequest, ProviderCatalogListResponse, ProviderCatalogTemplateRequest, ProviderCatalogTemplateResponse, ProviderConfigChangeResponse, ProviderConfigDeleteRequest, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest, ProviderConfigReadResponse, ProviderConfigSaveRequest, ProviderConfigStatusDto, ProviderConfigStatusRequest, ProviderConfigStatusResponse, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderTemplateCapabilitiesDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest, ReadResourceResponse, RefreshProviderInventoryRequest, RefreshProviderInventoryResponse, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest, RemoveExtensionRequest, RenameSessionRequest, SourceEntry, SourceType, ToggleConfigExtensionRequest, UnarchiveSessionRequest, UpdateSessionProjectRequest, UpdateSourceRequest, UpdateSourceResponse, UpdateWorkingDirRequest } from './types.gen.js';
|
||||
|
||||
export const GOOSE_EXT_METHODS = [
|
||||
{
|
||||
@@ -124,34 +124,24 @@ export const GOOSE_EXT_METHODS = [
|
||||
responseType: "ProviderConfigChangeResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/config/read",
|
||||
requestType: "ReadConfigRequest",
|
||||
responseType: "ReadConfigResponse",
|
||||
method: "_goose/preferences/read",
|
||||
requestType: "PreferencesReadRequest",
|
||||
responseType: "PreferencesReadResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/config/upsert",
|
||||
requestType: "UpsertConfigRequest",
|
||||
method: "_goose/preferences/save",
|
||||
requestType: "PreferencesSaveRequest",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/config/remove",
|
||||
requestType: "RemoveConfigRequest",
|
||||
method: "_goose/preferences/remove",
|
||||
requestType: "PreferencesRemoveRequest",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/secret/check",
|
||||
requestType: "CheckSecretRequest",
|
||||
responseType: "CheckSecretResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/secret/upsert",
|
||||
requestType: "UpsertSecretRequest",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/secret/remove",
|
||||
requestType: "RemoveSecretRequest",
|
||||
responseType: "EmptyResponse",
|
||||
method: "_goose/defaults/read",
|
||||
requestType: "DefaultsReadRequest",
|
||||
responseType: "DefaultsReadResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/session/export",
|
||||
@@ -223,6 +213,16 @@ export const GOOSE_EXT_METHODS = [
|
||||
requestType: "DictationConfigRequest",
|
||||
responseType: "DictationConfigResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/dictation/secret/save",
|
||||
requestType: "DictationSecretSaveRequest",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/dictation/secret/delete",
|
||||
requestType: "DictationSecretDeleteRequest",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/dictation/models/list",
|
||||
requestType: "DictationModelsListRequest",
|
||||
|
||||
@@ -520,61 +520,47 @@ export type ProviderConfigDeleteRequest = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Read a single non-secret config value.
|
||||
* Read allowlisted user preferences. Empty `keys` means all supported preferences.
|
||||
*/
|
||||
export type ReadConfigRequest = {
|
||||
key: string;
|
||||
export type PreferencesReadRequest = {
|
||||
keys?: Array<PreferenceKey>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Config read response.
|
||||
*/
|
||||
export type ReadConfigResponse = {
|
||||
export type PreferenceKey = 'autoCompactThreshold' | 'voiceAutoSubmitPhrases' | 'voiceDictationProvider' | 'voiceDictationPreferredMic';
|
||||
|
||||
export type PreferencesReadResponse = {
|
||||
values: Array<PreferenceValue>;
|
||||
};
|
||||
|
||||
export type PreferenceValue = {
|
||||
key: PreferenceKey;
|
||||
value?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Upsert a single non-secret config value.
|
||||
* Save allowlisted user preferences.
|
||||
*/
|
||||
export type UpsertConfigRequest = {
|
||||
key: string;
|
||||
value: unknown;
|
||||
export type PreferencesSaveRequest = {
|
||||
values?: Array<PreferenceValue>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a single non-secret config value.
|
||||
* Remove allowlisted user preferences.
|
||||
*/
|
||||
export type RemoveConfigRequest = {
|
||||
key: string;
|
||||
export type PreferencesRemoveRequest = {
|
||||
keys?: Array<PreferenceKey>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether a secret exists. Never returns the actual value.
|
||||
* Read Goose default provider and model configuration.
|
||||
*/
|
||||
export type CheckSecretRequest = {
|
||||
key: string;
|
||||
export type DefaultsReadRequest = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Secret check response.
|
||||
*/
|
||||
export type CheckSecretResponse = {
|
||||
exists: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a secret value (write-only).
|
||||
*/
|
||||
export type UpsertSecretRequest = {
|
||||
key: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a secret.
|
||||
*/
|
||||
export type RemoveSecretRequest = {
|
||||
key: string;
|
||||
export type DefaultsReadResponse = {
|
||||
providerId?: string | null;
|
||||
modelId?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -818,6 +804,21 @@ export type DictationModelOption = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a dictation provider secret value.
|
||||
*/
|
||||
export type DictationSecretSaveRequest = {
|
||||
provider: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a dictation provider secret value.
|
||||
*/
|
||||
export type DictationSecretDeleteRequest = {
|
||||
provider: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* List available local Whisper models with their download status.
|
||||
*/
|
||||
@@ -895,14 +896,14 @@ export type DictationModelSelectRequest = {
|
||||
export type ExtRequest = {
|
||||
id: string;
|
||||
method: string;
|
||||
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | GooseToolCallRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | AddConfigExtensionRequest | RemoveConfigExtensionRequest | ToggleConfigExtensionRequest | GetSessionExtensionsRequest | ListProvidersRequest | ProviderCatalogListRequest | ProviderCatalogTemplateRequest | CustomProviderCreateRequest | CustomProviderReadRequest | CustomProviderUpdateRequest | CustomProviderDeleteRequest | RefreshProviderInventoryRequest | ProviderConfigReadRequest | ProviderConfigStatusRequest | ProviderConfigSaveRequest | ProviderConfigDeleteRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | UpdateSessionProjectRequest | RenameSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | CreateSourceRequest | ListSourcesRequest | UpdateSourceRequest | DeleteSourceRequest | ExportSourceRequest | ImportSourcesRequest | DictationTranscribeRequest | DictationConfigRequest | DictationModelsListRequest | DictationModelDownloadRequest | DictationModelDownloadProgressRequest | DictationModelCancelRequest | DictationModelDeleteRequest | DictationModelSelectRequest | {
|
||||
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | GooseToolCallRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | AddConfigExtensionRequest | RemoveConfigExtensionRequest | ToggleConfigExtensionRequest | GetSessionExtensionsRequest | ListProvidersRequest | ProviderCatalogListRequest | ProviderCatalogTemplateRequest | CustomProviderCreateRequest | CustomProviderReadRequest | CustomProviderUpdateRequest | CustomProviderDeleteRequest | RefreshProviderInventoryRequest | ProviderConfigReadRequest | ProviderConfigStatusRequest | ProviderConfigSaveRequest | ProviderConfigDeleteRequest | PreferencesReadRequest | PreferencesSaveRequest | PreferencesRemoveRequest | DefaultsReadRequest | ExportSessionRequest | ImportSessionRequest | UpdateSessionProjectRequest | RenameSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | CreateSourceRequest | ListSourcesRequest | UpdateSourceRequest | DeleteSourceRequest | ExportSourceRequest | ImportSourcesRequest | DictationTranscribeRequest | DictationConfigRequest | DictationSecretSaveRequest | DictationSecretDeleteRequest | DictationModelsListRequest | DictationModelDownloadRequest | DictationModelDownloadProgressRequest | DictationModelCancelRequest | DictationModelDeleteRequest | DictationModelSelectRequest | {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type ExtResponse = {
|
||||
id: string;
|
||||
result?: EmptyResponse | GetToolsResponse | GooseToolCallResponse | ReadResourceResponse | GetExtensionsResponse | GetSessionExtensionsResponse | ListProvidersResponse | ProviderCatalogListResponse | ProviderCatalogTemplateResponse | CustomProviderCreateResponse | CustomProviderReadResponse | CustomProviderUpdateResponse | CustomProviderDeleteResponse | RefreshProviderInventoryResponse | ProviderConfigReadResponse | ProviderConfigStatusResponse | ProviderConfigChangeResponse | ReadConfigResponse | CheckSecretResponse | ExportSessionResponse | ImportSessionResponse | CreateSourceResponse | ListSourcesResponse | UpdateSourceResponse | ExportSourceResponse | ImportSourcesResponse | DictationTranscribeResponse | DictationConfigResponse | DictationModelsListResponse | DictationModelDownloadProgressResponse | unknown;
|
||||
result?: EmptyResponse | GetToolsResponse | GooseToolCallResponse | ReadResourceResponse | GetExtensionsResponse | GetSessionExtensionsResponse | ListProvidersResponse | ProviderCatalogListResponse | ProviderCatalogTemplateResponse | CustomProviderCreateResponse | CustomProviderReadResponse | CustomProviderUpdateResponse | CustomProviderDeleteResponse | RefreshProviderInventoryResponse | ProviderConfigReadResponse | ProviderConfigStatusResponse | ProviderConfigChangeResponse | PreferencesReadResponse | DefaultsReadResponse | ExportSessionResponse | ImportSessionResponse | CreateSourceResponse | ListSourcesResponse | UpdateSourceResponse | ExportSourceResponse | ImportSourcesResponse | DictationTranscribeResponse | DictationConfigResponse | DictationModelsListResponse | DictationModelDownloadProgressResponse | unknown;
|
||||
} | {
|
||||
error: {
|
||||
code: number;
|
||||
|
||||
@@ -486,62 +486,57 @@ export const zProviderConfigDeleteRequest = z.object({
|
||||
providerId: z.string()
|
||||
});
|
||||
|
||||
/**
|
||||
* Read a single non-secret config value.
|
||||
*/
|
||||
export const zReadConfigRequest = z.object({
|
||||
key: z.string()
|
||||
});
|
||||
export const zPreferenceKey = z.enum([
|
||||
'autoCompactThreshold',
|
||||
'voiceAutoSubmitPhrases',
|
||||
'voiceDictationProvider',
|
||||
'voiceDictationPreferredMic'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Config read response.
|
||||
* Read allowlisted user preferences. Empty `keys` means all supported preferences.
|
||||
*/
|
||||
export const zReadConfigResponse = z.object({
|
||||
export const zPreferencesReadRequest = z.object({
|
||||
keys: z.array(zPreferenceKey).optional().default([])
|
||||
});
|
||||
|
||||
export const zPreferenceValue = z.object({
|
||||
key: zPreferenceKey,
|
||||
value: z.unknown().optional().default(null)
|
||||
});
|
||||
|
||||
/**
|
||||
* Upsert a single non-secret config value.
|
||||
*/
|
||||
export const zUpsertConfigRequest = z.object({
|
||||
key: z.string(),
|
||||
value: z.unknown()
|
||||
export const zPreferencesReadResponse = z.object({
|
||||
values: z.array(zPreferenceValue)
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove a single non-secret config value.
|
||||
* Save allowlisted user preferences.
|
||||
*/
|
||||
export const zRemoveConfigRequest = z.object({
|
||||
key: z.string()
|
||||
export const zPreferencesSaveRequest = z.object({
|
||||
values: z.array(zPreferenceValue).optional().default([])
|
||||
});
|
||||
|
||||
/**
|
||||
* Check whether a secret exists. Never returns the actual value.
|
||||
* Remove allowlisted user preferences.
|
||||
*/
|
||||
export const zCheckSecretRequest = z.object({
|
||||
key: z.string()
|
||||
export const zPreferencesRemoveRequest = z.object({
|
||||
keys: z.array(zPreferenceKey).optional().default([])
|
||||
});
|
||||
|
||||
/**
|
||||
* Secret check response.
|
||||
* Read Goose default provider and model configuration.
|
||||
*/
|
||||
export const zCheckSecretResponse = z.object({
|
||||
exists: z.boolean()
|
||||
});
|
||||
export const zDefaultsReadRequest = z.record(z.unknown());
|
||||
|
||||
/**
|
||||
* Set a secret value (write-only).
|
||||
*/
|
||||
export const zUpsertSecretRequest = z.object({
|
||||
key: z.string(),
|
||||
value: z.unknown()
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove a secret.
|
||||
*/
|
||||
export const zRemoveSecretRequest = z.object({
|
||||
key: z.string()
|
||||
export const zDefaultsReadResponse = z.object({
|
||||
providerId: z.union([
|
||||
z.string(),
|
||||
z.null()
|
||||
]).optional(),
|
||||
modelId: z.union([
|
||||
z.string(),
|
||||
z.null()
|
||||
]).optional()
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -802,6 +797,21 @@ export const zDictationConfigResponse = z.object({
|
||||
providers: z.record(zDictationProviderStatusEntry)
|
||||
});
|
||||
|
||||
/**
|
||||
* Set a dictation provider secret value.
|
||||
*/
|
||||
export const zDictationSecretSaveRequest = z.object({
|
||||
provider: z.string(),
|
||||
value: z.string()
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove a dictation provider secret value.
|
||||
*/
|
||||
export const zDictationSecretDeleteRequest = z.object({
|
||||
provider: z.string()
|
||||
});
|
||||
|
||||
/**
|
||||
* List available local Whisper models with their download status.
|
||||
*/
|
||||
@@ -903,12 +913,10 @@ export const zExtRequest = z.object({
|
||||
zProviderConfigStatusRequest,
|
||||
zProviderConfigSaveRequest,
|
||||
zProviderConfigDeleteRequest,
|
||||
zReadConfigRequest,
|
||||
zUpsertConfigRequest,
|
||||
zRemoveConfigRequest,
|
||||
zCheckSecretRequest,
|
||||
zUpsertSecretRequest,
|
||||
zRemoveSecretRequest,
|
||||
zPreferencesReadRequest,
|
||||
zPreferencesSaveRequest,
|
||||
zPreferencesRemoveRequest,
|
||||
zDefaultsReadRequest,
|
||||
zExportSessionRequest,
|
||||
zImportSessionRequest,
|
||||
zUpdateSessionProjectRequest,
|
||||
@@ -923,6 +931,8 @@ export const zExtRequest = z.object({
|
||||
zImportSourcesRequest,
|
||||
zDictationTranscribeRequest,
|
||||
zDictationConfigRequest,
|
||||
zDictationSecretSaveRequest,
|
||||
zDictationSecretDeleteRequest,
|
||||
zDictationModelsListRequest,
|
||||
zDictationModelDownloadRequest,
|
||||
zDictationModelDownloadProgressRequest,
|
||||
@@ -959,8 +969,8 @@ export const zExtResponse = z.union([
|
||||
zProviderConfigReadResponse,
|
||||
zProviderConfigStatusResponse,
|
||||
zProviderConfigChangeResponse,
|
||||
zReadConfigResponse,
|
||||
zCheckSecretResponse,
|
||||
zPreferencesReadResponse,
|
||||
zDefaultsReadResponse,
|
||||
zExportSessionResponse,
|
||||
zImportSessionResponse,
|
||||
zCreateSourceResponse,
|
||||
|
||||
@@ -359,9 +359,9 @@ export default function ConfigureScreen({
|
||||
|
||||
if (initialIntent === "model") {
|
||||
try {
|
||||
const cfg = await client.goose.GooseConfigRead({ key: "GOOSE_PROVIDER" });
|
||||
const cfg = await client.goose.GooseDefaultsRead({});
|
||||
if (cancelled) return;
|
||||
const current = sorted.find((p) => p.providerId === cfg.value);
|
||||
const current = sorted.find((p) => p.providerId === cfg.providerId);
|
||||
if (current) {
|
||||
setSelectedProvider(current);
|
||||
setPendingConfigValues({});
|
||||
@@ -395,19 +395,13 @@ export default function ConfigureScreen({
|
||||
) => {
|
||||
setPhase("saving");
|
||||
try {
|
||||
for (const [key, value] of Object.entries(configValues)) {
|
||||
const configKey = provider.configKeys.find((k) => k.name === key);
|
||||
if (configKey?.secret) {
|
||||
await client.goose.GooseSecretUpsert({ key, value });
|
||||
} else {
|
||||
await client.goose.GooseConfigUpsert({ key, value });
|
||||
}
|
||||
}
|
||||
await client.goose.GooseConfigUpsert({
|
||||
key: "GOOSE_PROVIDER",
|
||||
value: provider.providerId,
|
||||
await client.goose.GooseProvidersConfigSave({
|
||||
providerId: provider.providerId,
|
||||
fields: Object.entries(configValues).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
})),
|
||||
});
|
||||
await client.goose.GooseConfigUpsert({ key: "GOOSE_MODEL", value: model });
|
||||
await client.setSessionConfigOption({
|
||||
sessionId,
|
||||
configId: "provider",
|
||||
|
||||
@@ -32,10 +32,6 @@ function deriveNameFromValue(addType: AddType, value: string): string {
|
||||
try { return new URL(value.trim()).hostname; } catch { return value.trim(); }
|
||||
}
|
||||
|
||||
function keyFromName(name: string): string {
|
||||
return name.replace(/[^A-Za-z0-9_-]/g, "_").toLowerCase();
|
||||
}
|
||||
|
||||
function buildConfig(addType: AddType, value: string, name: string, description: string): ExtEntry {
|
||||
if (addType === "stdio") {
|
||||
const parts = value.trim().split(/\s+/);
|
||||
@@ -125,15 +121,12 @@ export default function ExtensionsManager({
|
||||
|
||||
const saveNewExtension = useCallback((description: string) => {
|
||||
const config = buildConfig(addType, addValue, addName, description);
|
||||
const key = keyFromName(config.name);
|
||||
withSaving(async () => {
|
||||
let extMap: Record<string, unknown> = {};
|
||||
try {
|
||||
const raw = await client.goose.GooseConfigRead({key: "extensions"});
|
||||
if (raw.value && typeof raw.value === "object") extMap = raw.value as Record<string, unknown>;
|
||||
} catch { }
|
||||
extMap[key] = config;
|
||||
await client.goose.GooseConfigUpsert({key: "extensions", value: extMap as any});
|
||||
await client.goose.GooseConfigExtensionsAdd({
|
||||
name: config.name,
|
||||
extensionConfig: config as any,
|
||||
enabled: true,
|
||||
});
|
||||
await client.goose.GooseExtensionsAdd({sessionId, config: config as any});
|
||||
});
|
||||
}, [addType, addValue, addName, client, sessionId, withSaving]);
|
||||
|
||||
@@ -559,21 +559,12 @@ export default function Onboarding({
|
||||
async (provider: ProviderInventoryEntryDto, values: Record<string, string>) => {
|
||||
setPhase("saving");
|
||||
try {
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
const configKey = provider.configKeys.find((k) => k.name === key);
|
||||
if (configKey?.secret) {
|
||||
await client.goose.GooseSecretUpsert({ key, value });
|
||||
} else {
|
||||
await client.goose.GooseConfigUpsert({ key, value });
|
||||
}
|
||||
}
|
||||
await client.goose.GooseConfigUpsert({
|
||||
key: "GOOSE_PROVIDER",
|
||||
value: provider.providerId,
|
||||
});
|
||||
await client.goose.GooseConfigUpsert({
|
||||
key: "GOOSE_MODEL",
|
||||
value: provider.defaultModel,
|
||||
await client.goose.GooseProvidersConfigSave({
|
||||
providerId: provider.providerId,
|
||||
fields: Object.entries(values).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
})),
|
||||
});
|
||||
setPhase("success");
|
||||
setTimeout(onComplete, 1000);
|
||||
|
||||
+4
-4
@@ -785,11 +785,11 @@ function App({
|
||||
setStatus("checking provider…");
|
||||
let hasProvider = false;
|
||||
try {
|
||||
const resp = await client.goose.GooseConfigRead({
|
||||
key: "GOOSE_PROVIDER",
|
||||
});
|
||||
const resp = await client.goose.GooseDefaultsRead({});
|
||||
hasProvider =
|
||||
resp.value != null && resp.value !== "" && resp.value !== "null";
|
||||
resp.providerId != null &&
|
||||
resp.providerId !== "" &&
|
||||
resp.providerId !== "null";
|
||||
} catch {
|
||||
hasProvider = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user