feat: onboarding UX for the TUI (#8513)

This commit is contained in:
Alex Hancock
2026-04-14 10:17:01 -04:00
committed by GitHub
parent 2f018285a4
commit 17a404d4f9
26 changed files with 2325 additions and 423 deletions
+5
View File
@@ -45,6 +45,11 @@
"requestType": "ListProvidersRequest",
"responseType": "ListProvidersResponse"
},
{
"method": "_goose/providers/details",
"requestType": "GetProviderDetailsRequest",
"responseType": "GetProviderDetailsResponse"
},
{
"method": "_goose/config/read",
"requestType": "ReadConfigRequest",
+123
View File
@@ -261,6 +261,112 @@
"label"
]
},
"GetProviderDetailsRequest": {
"type": "object",
"description": "List providers with full metadata (config keys, setup steps, etc.).",
"x-side": "agent",
"x-method": "_goose/providers/details"
},
"GetProviderDetailsResponse": {
"type": "object",
"properties": {
"providers": {
"type": "array",
"items": {
"$ref": "#/$defs/ProviderDetailEntry"
}
}
},
"required": [
"providers"
],
"description": "Provider details response.",
"x-side": "agent",
"x-method": "_goose/providers/details"
},
"ProviderDetailEntry": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"displayName": {
"type": "string"
},
"description": {
"type": "string"
},
"defaultModel": {
"type": "string"
},
"isConfigured": {
"type": "boolean"
},
"providerType": {
"type": "string"
},
"configKeys": {
"type": "array",
"items": {
"$ref": "#/$defs/ProviderConfigKey"
}
},
"setupSteps": {
"type": "array",
"items": {
"type": "string"
},
"default": []
}
},
"required": [
"name",
"displayName",
"description",
"defaultModel",
"isConfigured",
"providerType",
"configKeys"
]
},
"ProviderConfigKey": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"required": {
"type": "boolean"
},
"secret": {
"type": "boolean"
},
"default": {
"type": [
"string",
"null"
],
"default": null
},
"oauthFlow": {
"type": "boolean",
"default": false
},
"deviceCodeFlow": {
"type": "boolean",
"default": false
},
"primary": {
"type": "boolean",
"default": false
}
},
"required": [
"name",
"required",
"secret"
]
},
"ReadConfigRequest": {
"type": "object",
"properties": {
@@ -568,6 +674,15 @@
"description": "Params for _goose/providers/list",
"title": "ListProvidersRequest"
},
{
"allOf": [
{
"$ref": "#/$defs/GetProviderDetailsRequest"
}
],
"description": "Params for _goose/providers/details",
"title": "GetProviderDetailsRequest"
},
{
"allOf": [
{
@@ -736,6 +851,14 @@
],
"title": "ListProvidersResponse"
},
{
"allOf": [
{
"$ref": "#/$defs/GetProviderDetailsResponse"
}
],
"title": "GetProviderDetailsResponse"
},
{
"allOf": [
{
+52
View File
@@ -2323,6 +2323,58 @@ impl GooseAcpAgent {
})
}
#[custom_method(GetProviderDetailsRequest)]
async fn on_get_provider_details(
&self,
_req: GetProviderDetailsRequest,
) -> Result<GetProviderDetailsResponse, sacp::Error> {
let config = self.load_config().ok();
let all = goose::providers::providers().await;
let entries = all
.into_iter()
.map(|(metadata, provider_type)| {
let is_configured = config
.as_ref()
.map(|c| {
metadata.config_keys.iter().all(|k| {
if !k.required {
return true;
}
if k.secret {
c.get_secret::<String>(&k.name).is_ok()
} else {
c.get_param::<String>(&k.name).is_ok()
}
})
})
.unwrap_or(false);
ProviderDetailEntry {
name: metadata.name.clone(),
display_name: metadata.display_name.clone(),
description: metadata.description.clone(),
default_model: metadata.default_model.clone(),
is_configured,
provider_type: format!("{:?}", provider_type),
config_keys: metadata
.config_keys
.iter()
.map(|k| ProviderConfigKey {
name: k.name.clone(),
required: k.required,
secret: k.secret,
default: k.default.clone(),
oauth_flow: k.oauth_flow,
device_code_flow: k.device_code_flow,
primary: k.primary,
})
.collect(),
setup_steps: metadata.setup_steps.clone(),
}
})
.collect();
Ok(GetProviderDetailsResponse { providers: entries })
}
#[custom_method(ReadConfigRequest)]
async fn on_read_config(
&self,
+41
View File
@@ -254,6 +254,47 @@ pub struct ImportSessionResponse {
pub message_count: u64,
}
/// List providers with full metadata (config keys, setup steps, etc.).
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/providers/details", response = GetProviderDetailsResponse)]
pub struct GetProviderDetailsRequest {}
/// Provider details response.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct GetProviderDetailsResponse {
pub providers: Vec<ProviderDetailEntry>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderDetailEntry {
pub name: String,
pub display_name: String,
pub description: String,
pub default_model: String,
pub is_configured: bool,
pub provider_type: String,
pub config_keys: Vec<ProviderConfigKey>,
#[serde(default)]
pub setup_steps: Vec<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderConfigKey {
pub name: String,
pub required: bool,
pub secret: bool,
#[serde(default)]
pub default: Option<String>,
#[serde(default)]
pub oauth_flow: bool,
#[serde(default)]
pub device_code_flow: bool,
#[serde(default)]
pub primary: bool,
}
/// Empty success response for operations that return no data.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct EmptyResponse {}