Standalone mcp apps (#6458)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -355,11 +355,13 @@ derive_utoipa!(Icon as IconSchema);
|
|||||||
super::routes::config_management::get_pricing,
|
super::routes::config_management::get_pricing,
|
||||||
super::routes::agent::start_agent,
|
super::routes::agent::start_agent,
|
||||||
super::routes::agent::resume_agent,
|
super::routes::agent::resume_agent,
|
||||||
|
super::routes::agent::stop_agent,
|
||||||
super::routes::agent::restart_agent,
|
super::routes::agent::restart_agent,
|
||||||
super::routes::agent::update_working_dir,
|
super::routes::agent::update_working_dir,
|
||||||
super::routes::agent::get_tools,
|
super::routes::agent::get_tools,
|
||||||
super::routes::agent::read_resource,
|
super::routes::agent::read_resource,
|
||||||
super::routes::agent::call_tool,
|
super::routes::agent::call_tool,
|
||||||
|
super::routes::agent::list_apps,
|
||||||
super::routes::agent::update_from_session,
|
super::routes::agent::update_from_session,
|
||||||
super::routes::agent::agent_add_extension,
|
super::routes::agent::agent_add_extension,
|
||||||
super::routes::agent::agent_remove_extension,
|
super::routes::agent::agent_remove_extension,
|
||||||
@@ -533,8 +535,11 @@ derive_utoipa!(Icon as IconSchema);
|
|||||||
super::routes::agent::ReadResourceResponse,
|
super::routes::agent::ReadResourceResponse,
|
||||||
super::routes::agent::CallToolRequest,
|
super::routes::agent::CallToolRequest,
|
||||||
super::routes::agent::CallToolResponse,
|
super::routes::agent::CallToolResponse,
|
||||||
|
super::routes::agent::ListAppsRequest,
|
||||||
|
super::routes::agent::ListAppsResponse,
|
||||||
super::routes::agent::StartAgentRequest,
|
super::routes::agent::StartAgentRequest,
|
||||||
super::routes::agent::ResumeAgentRequest,
|
super::routes::agent::ResumeAgentRequest,
|
||||||
|
super::routes::agent::StopAgentRequest,
|
||||||
super::routes::agent::RestartAgentRequest,
|
super::routes::agent::RestartAgentRequest,
|
||||||
super::routes::agent::UpdateWorkingDirRequest,
|
super::routes::agent::UpdateWorkingDirRequest,
|
||||||
super::routes::agent::UpdateFromSessionRequest,
|
super::routes::agent::UpdateFromSessionRequest,
|
||||||
@@ -547,6 +552,8 @@ derive_utoipa!(Icon as IconSchema);
|
|||||||
super::tunnel::TunnelInfo,
|
super::tunnel::TunnelInfo,
|
||||||
super::tunnel::TunnelState,
|
super::tunnel::TunnelState,
|
||||||
super::routes::telemetry::TelemetryEventRequest,
|
super::routes::telemetry::TelemetryEventRequest,
|
||||||
|
goose::goose_apps::GooseApp,
|
||||||
|
goose::goose_apps::WindowProps,
|
||||||
goose::goose_apps::McpAppResource,
|
goose::goose_apps::McpAppResource,
|
||||||
goose::goose_apps::CspMetadata,
|
goose::goose_apps::CspMetadata,
|
||||||
goose::goose_apps::UiMetadata,
|
goose::goose_apps::UiMetadata,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use axum::{
|
|||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use goose::agents::ExtensionLoadResult;
|
use goose::agents::ExtensionLoadResult;
|
||||||
|
use goose::goose_apps::{fetch_mcp_apps, GooseApp, McpAppCache};
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use goose::agents::ExtensionConfig;
|
use goose::agents::ExtensionConfig;
|
||||||
@@ -35,7 +36,7 @@ use std::path::PathBuf;
|
|||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::error;
|
use tracing::{error, warn};
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::ToSchema)]
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
pub struct UpdateFromSessionRequest {
|
pub struct UpdateFromSessionRequest {
|
||||||
@@ -933,6 +934,87 @@ async fn call_tool(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::IntoParams, utoipa::ToSchema)]
|
||||||
|
pub struct ListAppsRequest {
|
||||||
|
session_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ListAppsResponse {
|
||||||
|
pub apps: Vec<GooseApp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/agent/list_apps",
|
||||||
|
params(
|
||||||
|
ListAppsRequest
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "List of apps retrieved successfully", body = ListAppsResponse),
|
||||||
|
(status = 401, description = "Unauthorized - Invalid or missing API key", body = ErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("api_key" = [])
|
||||||
|
),
|
||||||
|
tag = "Agent"
|
||||||
|
)]
|
||||||
|
async fn list_apps(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Query(params): Query<ListAppsRequest>,
|
||||||
|
) -> Result<Json<ListAppsResponse>, ErrorResponse> {
|
||||||
|
let cache = McpAppCache::new().ok();
|
||||||
|
|
||||||
|
let Some(session_id) = params.session_id else {
|
||||||
|
let apps = cache
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| c.list_apps().ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
return Ok(Json(ListAppsResponse { apps }));
|
||||||
|
};
|
||||||
|
|
||||||
|
let agent = state
|
||||||
|
.get_agent_for_route(session_id)
|
||||||
|
.await
|
||||||
|
.map_err(|status| ErrorResponse {
|
||||||
|
message: "Failed to get agent".to_string(),
|
||||||
|
status,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let apps = fetch_mcp_apps(&agent.extension_manager)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ErrorResponse {
|
||||||
|
message: format!("Failed to list apps: {}", e.message),
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Some(cache) = cache.as_ref() {
|
||||||
|
let active_extensions: std::collections::HashSet<String> = apps
|
||||||
|
.iter()
|
||||||
|
.filter_map(|app| app.mcp_server.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for extension_name in active_extensions {
|
||||||
|
if let Err(e) = cache.delete_extension_apps(&extension_name) {
|
||||||
|
warn!(
|
||||||
|
"Failed to clean cache for extension {}: {}",
|
||||||
|
extension_name, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for app in &apps {
|
||||||
|
if let Err(e) = cache.store_app(app) {
|
||||||
|
warn!("Failed to cache app {}: {}", app.resource.name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(ListAppsResponse { apps }))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes(state: Arc<AppState>) -> Router {
|
pub fn routes(state: Arc<AppState>) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/agent/start", post(start_agent))
|
.route("/agent/start", post(start_agent))
|
||||||
@@ -942,6 +1024,7 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
|||||||
.route("/agent/tools", get(get_tools))
|
.route("/agent/tools", get(get_tools))
|
||||||
.route("/agent/read_resource", post(read_resource))
|
.route("/agent/read_resource", post(read_resource))
|
||||||
.route("/agent/call_tool", post(call_tool))
|
.route("/agent/call_tool", post(call_tool))
|
||||||
|
.route("/agent/list_apps", get(list_apps))
|
||||||
.route("/agent/update_provider", post(update_agent_provider))
|
.route("/agent/update_provider", post(update_agent_provider))
|
||||||
.route("/agent/update_from_session", post(update_from_session))
|
.route("/agent/update_from_session", post(update_from_session))
|
||||||
.route("/agent/add_extension", post(agent_add_extension))
|
.route("/agent/add_extension", post(agent_add_extension))
|
||||||
|
|||||||
@@ -1,9 +1,203 @@
|
|||||||
//! goose Apps module
|
|
||||||
//!
|
|
||||||
//! This module contains types and utilities for working with goose Apps,
|
|
||||||
//! which are UI resources that can be rendered in an MCP server or native
|
|
||||||
//! goose apps, or something in between.
|
|
||||||
|
|
||||||
pub mod resource;
|
pub mod resource;
|
||||||
|
|
||||||
|
use crate::agents::ExtensionManager;
|
||||||
|
use crate::config::paths::Paths;
|
||||||
|
use rmcp::model::ErrorData;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use tracing::warn;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
pub use resource::{CspMetadata, McpAppResource, ResourceMetadata, UiMetadata};
|
pub use resource::{CspMetadata, McpAppResource, ResourceMetadata, UiMetadata};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WindowProps {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub resizable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Goose App combining MCP resource data with Goose-specific metadata
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GooseApp {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub resource: McpAppResource,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mcp_server: Option<String>,
|
||||||
|
#[serde(flatten, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub window_props: Option<WindowProps>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct McpAppCache {
|
||||||
|
cache_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl McpAppCache {
|
||||||
|
pub fn new() -> Result<Self, std::io::Error> {
|
||||||
|
let config_dir = Paths::config_dir();
|
||||||
|
let cache_dir = config_dir.join("mcp-apps-cache");
|
||||||
|
Ok(Self { cache_dir })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cache_key(extension_name: &str, resource_uri: &str) -> String {
|
||||||
|
let input = format!("{}::{}", extension_name, resource_uri);
|
||||||
|
let hash = Sha256::digest(input.as_bytes());
|
||||||
|
format!("{}_{:x}", extension_name, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_apps(&self) -> Result<Vec<GooseApp>, std::io::Error> {
|
||||||
|
let mut apps = Vec::new();
|
||||||
|
|
||||||
|
if !self.cache_dir.exists() {
|
||||||
|
return Ok(apps);
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in fs::read_dir(&self.cache_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.extension().and_then(|s| s.to_str()) == Some("json") {
|
||||||
|
match fs::read_to_string(&path) {
|
||||||
|
Ok(content) => match serde_json::from_str::<GooseApp>(&content) {
|
||||||
|
Ok(app) => apps.push(app),
|
||||||
|
Err(e) => warn!("Failed to parse cached app from {:?}: {}", path, e),
|
||||||
|
},
|
||||||
|
Err(e) => warn!("Failed to read cached app from {:?}: {}", path, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(apps)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_app(&self, app: &GooseApp) -> Result<(), std::io::Error> {
|
||||||
|
fs::create_dir_all(&self.cache_dir)?;
|
||||||
|
|
||||||
|
if let Some(ref extension_name) = app.mcp_server {
|
||||||
|
let cache_key = Self::cache_key(extension_name, &app.resource.uri);
|
||||||
|
let app_path = self.cache_dir.join(format!("{}.json", cache_key));
|
||||||
|
let json = serde_json::to_string_pretty(app).map_err(std::io::Error::other)?;
|
||||||
|
fs::write(app_path, json)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_app(&self, extension_name: &str, resource_uri: &str) -> Option<GooseApp> {
|
||||||
|
let cache_key = Self::cache_key(extension_name, resource_uri);
|
||||||
|
let app_path = self.cache_dir.join(format!("{}.json", cache_key));
|
||||||
|
|
||||||
|
if !app_path.exists() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::read_to_string(&app_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|content| serde_json::from_str::<GooseApp>(&content).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_extension_apps(&self, extension_name: &str) -> Result<usize, std::io::Error> {
|
||||||
|
let mut deleted_count = 0;
|
||||||
|
|
||||||
|
if !self.cache_dir.exists() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in fs::read_dir(&self.cache_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.extension().and_then(|s| s.to_str()) == Some("json") {
|
||||||
|
if let Ok(content) = fs::read_to_string(&path) {
|
||||||
|
if let Ok(app) = serde_json::from_str::<GooseApp>(&content) {
|
||||||
|
if app.mcp_server.as_deref() == Some(extension_name)
|
||||||
|
&& fs::remove_file(&path).is_ok()
|
||||||
|
{
|
||||||
|
deleted_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(deleted_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_mcp_apps(
|
||||||
|
extension_manager: &ExtensionManager,
|
||||||
|
) -> Result<Vec<GooseApp>, ErrorData> {
|
||||||
|
let mut apps = Vec::new();
|
||||||
|
|
||||||
|
let ui_resources = extension_manager.get_ui_resources().await?;
|
||||||
|
|
||||||
|
for (extension_name, resource) in ui_resources {
|
||||||
|
match extension_manager
|
||||||
|
.read_resource(&resource.uri, &extension_name, CancellationToken::default())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(read_result) => {
|
||||||
|
let mut html = String::new();
|
||||||
|
for content in read_result.contents {
|
||||||
|
if let rmcp::model::ResourceContents::TextResourceContents { text, .. } =
|
||||||
|
content
|
||||||
|
{
|
||||||
|
html = text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !html.is_empty() {
|
||||||
|
let mcp_resource = McpAppResource {
|
||||||
|
uri: resource.uri.clone(),
|
||||||
|
name: format_resource_name(resource.name.clone()),
|
||||||
|
description: resource.description.clone(),
|
||||||
|
mime_type: "text/html;profile=mcp-app".to_string(),
|
||||||
|
text: Some(html),
|
||||||
|
blob: None,
|
||||||
|
meta: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let app = GooseApp {
|
||||||
|
resource: mcp_resource,
|
||||||
|
mcp_server: Some(extension_name),
|
||||||
|
window_props: Some(WindowProps {
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
resizable: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
apps.push(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"Failed to read resource {} from {}: {}",
|
||||||
|
resource.uri, extension_name, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(apps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_resource_name(name: String) -> String {
|
||||||
|
name.replace('_', " ")
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|word| {
|
||||||
|
let mut chars = word.chars();
|
||||||
|
match chars.next() {
|
||||||
|
None => String::new(),
|
||||||
|
Some(first) => first.to_uppercase().chain(chars).collect(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|||||||
@@ -128,6 +128,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/agent/list_apps": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Agent"
|
||||||
|
],
|
||||||
|
"operationId": "list_apps",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "session_id",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of apps retrieved successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ListAppsResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - Invalid or missing API key",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/agent/read_resource": {
|
"/agent/read_resource": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -340,6 +396,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/agent/stop": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"super::routes::agent"
|
||||||
|
],
|
||||||
|
"operationId": "stop_agent",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/StopAgentRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Agent stopped successfully",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - invalid secret key"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Session not found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/agent/tools": {
|
"/agent/tools": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -3762,6 +3857,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GooseApp": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/McpAppResource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/WindowProps"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mcpServer": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A Goose App combining MCP resource data with Goose-specific metadata"
|
||||||
|
},
|
||||||
"Icon": {
|
"Icon": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -3855,6 +3975,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ListAppsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"session_id": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ListAppsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"apps"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"apps": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/GooseApp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ListRecipeResponse": {
|
"ListRecipeResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -5674,6 +5817,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"StopAgentRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"session_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SubRecipe": {
|
"SubRecipe": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -6279,6 +6433,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"WindowProps": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"resizable"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"height": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"resizable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import PermissionSettingsView from './components/settings/permission/PermissionS
|
|||||||
|
|
||||||
import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView';
|
import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView';
|
||||||
import RecipesView from './components/recipes/RecipesView';
|
import RecipesView from './components/recipes/RecipesView';
|
||||||
|
import AppsView from './components/apps/AppsView';
|
||||||
|
import StandaloneAppView from './components/apps/StandaloneAppView';
|
||||||
import { View, ViewOptions } from './utils/navigationUtils';
|
import { View, ViewOptions } from './utils/navigationUtils';
|
||||||
|
|
||||||
import { useNavigation } from './hooks/useNavigation';
|
import { useNavigation } from './hooks/useNavigation';
|
||||||
@@ -583,6 +585,7 @@ export function AppInner() {
|
|||||||
element={<WelcomeRoute onSelectProvider={() => setDidSelectProvider(true)} />}
|
element={<WelcomeRoute onSelectProvider={() => setDidSelectProvider(true)} />}
|
||||||
/>
|
/>
|
||||||
<Route path="configure-providers" element={<ConfigureProvidersRoute />} />
|
<Route path="configure-providers" element={<ConfigureProvidersRoute />} />
|
||||||
|
<Route path="standalone-app" element={<StandaloneAppView />} />
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
@@ -604,6 +607,7 @@ export function AppInner() {
|
|||||||
</ChatProvider>
|
</ChatProvider>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route path="apps" element={<AppsView />} />
|
||||||
<Route path="sessions" element={<SessionsRoute />} />
|
<Route path="sessions" element={<SessionsRoute />} />
|
||||||
<Route path="schedules" element={<SchedulesRoute />} />
|
<Route path="schedules" element={<SchedulesRoute />} />
|
||||||
<Route path="recipes" element={<RecipesRoute />} />
|
<Route path="recipes" element={<RecipesRoute />} />
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { Client, Options as Options2, TDataShape } from './client';
|
import type { Client, Options as Options2, TDataShape } from './client';
|
||||||
import { client } from './client.gen';
|
import { client } from './client.gen';
|
||||||
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPricingData, GetPricingResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPricingData, GetPricingResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
||||||
|
|
||||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +45,8 @@ export const callTool = <ThrowOnError extends boolean = false>(options: Options<
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const listApps = <ThrowOnError extends boolean = false>(options?: Options<ListAppsData, ThrowOnError>) => (options?.client ?? client).get<ListAppsResponses, ListAppsErrors, ThrowOnError>({ url: '/agent/list_apps', ...options });
|
||||||
|
|
||||||
export const readResource = <ThrowOnError extends boolean = false>(options: Options<ReadResourceData, ThrowOnError>) => (options.client ?? client).post<ReadResourceResponses, ReadResourceErrors, ThrowOnError>({
|
export const readResource = <ThrowOnError extends boolean = false>(options: Options<ReadResourceData, ThrowOnError>) => (options.client ?? client).post<ReadResourceResponses, ReadResourceErrors, ThrowOnError>({
|
||||||
url: '/agent/read_resource',
|
url: '/agent/read_resource',
|
||||||
...options,
|
...options,
|
||||||
@@ -90,6 +92,15 @@ export const startAgent = <ThrowOnError extends boolean = false>(options: Option
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const stopAgent = <ThrowOnError extends boolean = false>(options: Options<StopAgentData, ThrowOnError>) => (options.client ?? client).post<StopAgentResponses, StopAgentErrors, ThrowOnError>({
|
||||||
|
url: '/agent/stop',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const getTools = <ThrowOnError extends boolean = false>(options: Options<GetToolsData, ThrowOnError>) => (options.client ?? client).get<GetToolsResponses, GetToolsErrors, ThrowOnError>({ url: '/agent/tools', ...options });
|
export const getTools = <ThrowOnError extends boolean = false>(options: Options<GetToolsData, ThrowOnError>) => (options.client ?? client).get<GetToolsResponses, GetToolsErrors, ThrowOnError>({ url: '/agent/tools', ...options });
|
||||||
|
|
||||||
export const updateFromSession = <ThrowOnError extends boolean = false>(options: Options<UpdateFromSessionData, ThrowOnError>) => (options.client ?? client).post<UpdateFromSessionResponses, UpdateFromSessionErrors, ThrowOnError>({
|
export const updateFromSession = <ThrowOnError extends boolean = false>(options: Options<UpdateFromSessionData, ThrowOnError>) => (options.client ?? client).post<UpdateFromSessionResponses, UpdateFromSessionErrors, ThrowOnError>({
|
||||||
|
|||||||
@@ -364,6 +364,13 @@ export type GetToolsQuery = {
|
|||||||
session_id: string;
|
session_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Goose App combining MCP resource data with Goose-specific metadata
|
||||||
|
*/
|
||||||
|
export type GooseApp = McpAppResource & (WindowProps | null) & {
|
||||||
|
mcpServer?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type Icon = {
|
export type Icon = {
|
||||||
mimeType?: string;
|
mimeType?: string;
|
||||||
sizes?: Array<string>;
|
sizes?: Array<string>;
|
||||||
@@ -399,6 +406,14 @@ export type KillJobResponse = {
|
|||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ListAppsRequest = {
|
||||||
|
session_id?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListAppsResponse = {
|
||||||
|
apps: Array<GooseApp>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ListRecipeResponse = {
|
export type ListRecipeResponse = {
|
||||||
manifests: Array<RecipeManifest>;
|
manifests: Array<RecipeManifest>;
|
||||||
};
|
};
|
||||||
@@ -967,6 +982,10 @@ export type StartAgentRequest = {
|
|||||||
working_dir: string;
|
working_dir: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StopAgentRequest = {
|
||||||
|
session_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type SubRecipe = {
|
export type SubRecipe = {
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -1193,6 +1212,12 @@ export type UpsertPermissionsQuery = {
|
|||||||
tool_permissions: Array<ToolPermission>;
|
tool_permissions: Array<ToolPermission>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WindowProps = {
|
||||||
|
height: number;
|
||||||
|
resizable: boolean;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ConfirmToolActionData = {
|
export type ConfirmToolActionData = {
|
||||||
body: ConfirmToolActionRequest;
|
body: ConfirmToolActionRequest;
|
||||||
path?: never;
|
path?: never;
|
||||||
@@ -1284,6 +1309,37 @@ export type CallToolResponses = {
|
|||||||
|
|
||||||
export type CallToolResponse2 = CallToolResponses[keyof CallToolResponses];
|
export type CallToolResponse2 = CallToolResponses[keyof CallToolResponses];
|
||||||
|
|
||||||
|
export type ListAppsData = {
|
||||||
|
body?: never;
|
||||||
|
path?: never;
|
||||||
|
query?: {
|
||||||
|
session_id?: string | null;
|
||||||
|
};
|
||||||
|
url: '/agent/list_apps';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListAppsErrors = {
|
||||||
|
/**
|
||||||
|
* Unauthorized - Invalid or missing API key
|
||||||
|
*/
|
||||||
|
401: ErrorResponse;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: ErrorResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListAppsError = ListAppsErrors[keyof ListAppsErrors];
|
||||||
|
|
||||||
|
export type ListAppsResponses = {
|
||||||
|
/**
|
||||||
|
* List of apps retrieved successfully
|
||||||
|
*/
|
||||||
|
200: ListAppsResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListAppsResponse2 = ListAppsResponses[keyof ListAppsResponses];
|
||||||
|
|
||||||
export type ReadResourceData = {
|
export type ReadResourceData = {
|
||||||
body: ReadResourceRequest;
|
body: ReadResourceRequest;
|
||||||
path?: never;
|
path?: never;
|
||||||
@@ -1445,6 +1501,37 @@ export type StartAgentResponses = {
|
|||||||
|
|
||||||
export type StartAgentResponse = StartAgentResponses[keyof StartAgentResponses];
|
export type StartAgentResponse = StartAgentResponses[keyof StartAgentResponses];
|
||||||
|
|
||||||
|
export type StopAgentData = {
|
||||||
|
body: StopAgentRequest;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/agent/stop';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StopAgentErrors = {
|
||||||
|
/**
|
||||||
|
* Unauthorized - invalid secret key
|
||||||
|
*/
|
||||||
|
401: unknown;
|
||||||
|
/**
|
||||||
|
* Session not found
|
||||||
|
*/
|
||||||
|
404: unknown;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StopAgentResponses = {
|
||||||
|
/**
|
||||||
|
* Agent stopped successfully
|
||||||
|
*/
|
||||||
|
200: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StopAgentResponse = StopAgentResponses[keyof StopAgentResponses];
|
||||||
|
|
||||||
export type GetToolsData = {
|
export type GetToolsData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { FileText, Clock, Home, Puzzle, History } from 'lucide-react';
|
import { FileText, Clock, Home, Puzzle, History, AppWindow } from 'lucide-react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -16,6 +16,7 @@ import { ViewOptions, View } from '../../utils/navigationUtils';
|
|||||||
import { useChatContext } from '../../contexts/ChatContext';
|
import { useChatContext } from '../../contexts/ChatContext';
|
||||||
import { DEFAULT_CHAT_TITLE } from '../../contexts/ChatContext';
|
import { DEFAULT_CHAT_TITLE } from '../../contexts/ChatContext';
|
||||||
import EnvironmentBadge from './EnvironmentBadge';
|
import EnvironmentBadge from './EnvironmentBadge';
|
||||||
|
import { listApps } from '../../api';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
onSelectSession: (sessionId: string) => void;
|
onSelectSession: (sessionId: string) => void;
|
||||||
@@ -84,6 +85,13 @@ const menuItems: NavigationEntry[] = [
|
|||||||
icon: Puzzle,
|
icon: Puzzle,
|
||||||
tooltip: 'Manage your extensions',
|
tooltip: 'Manage your extensions',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
path: '/apps',
|
||||||
|
label: 'Apps',
|
||||||
|
icon: AppWindow,
|
||||||
|
tooltip: 'Browse and launch MCP apps',
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
@@ -100,6 +108,7 @@ const AppSidebar: React.FC<SidebarProps> = ({ currentPath }) => {
|
|||||||
const chatContext = useChatContext();
|
const chatContext = useChatContext();
|
||||||
const lastSessionIdRef = useRef<string | null>(null);
|
const lastSessionIdRef = useRef<string | null>(null);
|
||||||
const currentSessionId = currentPath === '/pair' ? searchParams.get('resumeSessionId') : null;
|
const currentSessionId = currentPath === '/pair' ? searchParams.get('resumeSessionId') : null;
|
||||||
|
const [hasApps, setHasApps] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentSessionId) {
|
if (currentSessionId) {
|
||||||
@@ -108,12 +117,19 @@ const AppSidebar: React.FC<SidebarProps> = ({ currentPath }) => {
|
|||||||
}, [currentSessionId]);
|
}, [currentSessionId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const checkApps = async () => {
|
||||||
// setIsVisible(true);
|
try {
|
||||||
}, 100);
|
const response = await listApps({
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
setHasApps((response.data?.apps || []).length > 0);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to check for apps:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
checkApps();
|
||||||
}, []);
|
}, [currentPath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentItem = menuItems.find(
|
const currentItem = menuItems.find(
|
||||||
@@ -179,10 +195,19 @@ const AppSidebar: React.FC<SidebarProps> = ({ currentPath }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const visibleMenuItems = menuItems.filter((entry) => {
|
||||||
|
if (entry.type === 'item' && entry.path === '/apps') {
|
||||||
|
return hasApps;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SidebarContent className="pt-16">
|
<SidebarContent className="pt-16">
|
||||||
<SidebarMenu>{menuItems.map((entry, index) => renderMenuItem(entry, index))}</SidebarMenu>
|
<SidebarMenu>
|
||||||
|
{visibleMenuItems.map((entry, index) => renderMenuItem(entry, index))}
|
||||||
|
</SidebarMenu>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter className="pb-2 flex items-start">
|
<SidebarFooter className="pb-2 flex items-start">
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ import { readResource, callTool } from '../../api';
|
|||||||
interface McpAppRendererProps {
|
interface McpAppRendererProps {
|
||||||
resourceUri: string;
|
resourceUri: string;
|
||||||
extensionName: string;
|
extensionName: string;
|
||||||
sessionId: string;
|
sessionId?: string | null;
|
||||||
toolInput?: ToolInput;
|
toolInput?: ToolInput;
|
||||||
toolInputPartial?: ToolInputPartial;
|
toolInputPartial?: ToolInputPartial;
|
||||||
toolResult?: ToolResult;
|
toolResult?: ToolResult;
|
||||||
toolCancelled?: ToolCancelled;
|
toolCancelled?: ToolCancelled;
|
||||||
append?: (text: string) => void;
|
append?: (text: string) => void;
|
||||||
|
fullscreen?: boolean;
|
||||||
|
cachedHtml?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResourceData {
|
interface ResourceData {
|
||||||
@@ -47,9 +49,11 @@ export default function McpAppRenderer({
|
|||||||
toolResult,
|
toolResult,
|
||||||
toolCancelled,
|
toolCancelled,
|
||||||
append,
|
append,
|
||||||
|
fullscreen = false,
|
||||||
|
cachedHtml,
|
||||||
}: McpAppRendererProps) {
|
}: McpAppRendererProps) {
|
||||||
const [resource, setResource] = useState<ResourceData>({
|
const [resource, setResource] = useState<ResourceData>({
|
||||||
html: null,
|
html: cachedHtml || null,
|
||||||
csp: null,
|
csp: null,
|
||||||
prefersBorder: true,
|
prefersBorder: true,
|
||||||
});
|
});
|
||||||
@@ -57,6 +61,10 @@ export default function McpAppRenderer({
|
|||||||
const [iframeHeight, setIframeHeight] = useState(DEFAULT_IFRAME_HEIGHT);
|
const [iframeHeight, setIframeHeight] = useState(DEFAULT_IFRAME_HEIGHT);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fetchResource = async () => {
|
const fetchResource = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await readResource({
|
const response = await readResource({
|
||||||
@@ -73,19 +81,25 @@ export default function McpAppRenderer({
|
|||||||
| { ui?: { csp?: CspMetadata; prefersBorder?: boolean } }
|
| { ui?: { csp?: CspMetadata; prefersBorder?: boolean } }
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
setResource({
|
if (content.text !== cachedHtml) {
|
||||||
html: content.text,
|
setResource({
|
||||||
csp: meta?.ui?.csp || null,
|
html: content.text,
|
||||||
prefersBorder: meta?.ui?.prefersBorder ?? true,
|
csp: meta?.ui?.csp || null,
|
||||||
});
|
prefersBorder: meta?.ui?.prefersBorder ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load resource');
|
if (!cachedHtml) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load resource');
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to fetch fresh resource, using cached version:', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchResource();
|
fetchResource();
|
||||||
}, [resourceUri, extensionName, sessionId]);
|
}, [resourceUri, extensionName, sessionId, cachedHtml]);
|
||||||
|
|
||||||
const handleMcpRequest = useCallback(
|
const handleMcpRequest = useCallback(
|
||||||
async (
|
async (
|
||||||
@@ -93,6 +107,12 @@ export default function McpAppRenderer({
|
|||||||
params: Record<string, unknown> = {},
|
params: Record<string, unknown> = {},
|
||||||
_id?: string | number
|
_id?: string | number
|
||||||
): Promise<unknown> => {
|
): Promise<unknown> => {
|
||||||
|
// Methods that require a session
|
||||||
|
const requiresSession = ['tools/call', 'resources/read'];
|
||||||
|
if (requiresSession.includes(method) && !sessionId) {
|
||||||
|
throw new Error('Session not initialized for MCP request');
|
||||||
|
}
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'ui/open-link': {
|
case 'ui/open-link': {
|
||||||
const { url } = params as McpMethodParams['ui/open-link'];
|
const { url } = params as McpMethodParams['ui/open-link'];
|
||||||
@@ -121,7 +141,7 @@ export default function McpAppRenderer({
|
|||||||
const fullToolName = `${extensionName}__${name}`;
|
const fullToolName = `${extensionName}__${name}`;
|
||||||
const response = await callTool({
|
const response = await callTool({
|
||||||
body: {
|
body: {
|
||||||
session_id: sessionId,
|
session_id: sessionId!,
|
||||||
name: fullToolName,
|
name: fullToolName,
|
||||||
arguments: args || {},
|
arguments: args || {},
|
||||||
},
|
},
|
||||||
@@ -139,7 +159,7 @@ export default function McpAppRenderer({
|
|||||||
const { uri } = params as McpMethodParams['resources/read'];
|
const { uri } = params as McpMethodParams['resources/read'];
|
||||||
const response = await readResource({
|
const response = await readResource({
|
||||||
body: {
|
body: {
|
||||||
session_id: sessionId,
|
session_id: sessionId!,
|
||||||
uri,
|
uri,
|
||||||
extension_name: extensionName,
|
extension_name: extensionName,
|
||||||
},
|
},
|
||||||
@@ -193,6 +213,33 @@ export default function McpAppRenderer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fullscreen) {
|
||||||
|
return proxyUrl ? (
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
src={proxyUrl}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
sandbox="allow-scripts allow-same-origin"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Play } from 'lucide-react';
|
||||||
|
import { GooseApp, listApps } from '../../api';
|
||||||
|
import { useChatContext } from '../../contexts/ChatContext';
|
||||||
|
|
||||||
|
const GridLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="grid gap-4 p-1"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AppsView() {
|
||||||
|
const [apps, setApps] = useState<GooseApp[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const chatContext = useChatContext();
|
||||||
|
const sessionId = chatContext?.chat.sessionId;
|
||||||
|
|
||||||
|
// Load cached apps immediately on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCachedApps = async () => {
|
||||||
|
try {
|
||||||
|
const response = await listApps({
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
const cachedApps = response.data?.apps || [];
|
||||||
|
setApps(cachedApps);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to load cached apps:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadCachedApps();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// When sessionId becomes available, fetch fresh apps and update cache
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
const refreshApps = async () => {
|
||||||
|
try {
|
||||||
|
const response = await listApps({
|
||||||
|
throwOnError: true,
|
||||||
|
query: { session_id: sessionId },
|
||||||
|
});
|
||||||
|
const freshApps = response.data?.apps || [];
|
||||||
|
setApps(freshApps);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to refresh apps:', err);
|
||||||
|
// Don't set error if we already have cached apps
|
||||||
|
if (apps.length === 0) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load apps');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
refreshApps();
|
||||||
|
// apps.length intentionally not in deps: we want to capture the initial apps.length to check
|
||||||
|
// "did we have cached apps when refresh started?" Adding it would cause infinite loop since setApps() changes apps.length
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [sessionId]);
|
||||||
|
|
||||||
|
const loadApps = useCallback(async () => {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await listApps({
|
||||||
|
throwOnError: true,
|
||||||
|
query: { session_id: sessionId },
|
||||||
|
});
|
||||||
|
const fetchedApps = response.data?.apps || [];
|
||||||
|
setApps(fetchedApps);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
// Only set error if we don't have apps to show
|
||||||
|
if (apps.length === 0) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load apps');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [sessionId, apps.length]);
|
||||||
|
|
||||||
|
const handleLaunchApp = async (app: GooseApp) => {
|
||||||
|
try {
|
||||||
|
await window.electron.launchApp(app);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to launch app:', err);
|
||||||
|
// App launch errors shouldn't hide the apps list, just log it
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only show error-only UI if we have no apps to display
|
||||||
|
if (error && apps.length === 0) {
|
||||||
|
return (
|
||||||
|
<MainPanelLayout>
|
||||||
|
<div className="flex flex-col items-center justify-center h-64 text-center">
|
||||||
|
<p className="text-red-500 mb-4">Error loading apps: {error}</p>
|
||||||
|
<Button onClick={loadApps}>Retry</Button>
|
||||||
|
</div>
|
||||||
|
</MainPanelLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainPanelLayout>
|
||||||
|
<div className="flex-1 flex flex-col min-h-0">
|
||||||
|
<div className="bg-background-default px-8 pb-8 pt-16">
|
||||||
|
<div className="flex flex-col page-transition">
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<h1 className="text-4xl font-light">Apps</h1>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-text-muted mb-4">
|
||||||
|
Applications from your MCP servers that can run in standalone windows.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto bg-background-subtle px-8 pb-8">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<p className="text-text-muted">Loading apps...</p>
|
||||||
|
</div>
|
||||||
|
) : apps.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-medium mb-2">No apps available</h3>
|
||||||
|
<p className="text-sm text-text-muted">
|
||||||
|
Install MCP servers that provide UI resources to see apps here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<GridLayout>
|
||||||
|
{apps.map((app) => (
|
||||||
|
<div
|
||||||
|
key={`${app.uri}-${app.mcpServer}`}
|
||||||
|
className="flex flex-col p-4 border border-border-muted rounded-lg bg-background-panel hover:border-border-default transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex-1 mb-4">
|
||||||
|
<h3 className="font-medium text-text-default mb-2">{app.name}</h3>
|
||||||
|
{app.description && (
|
||||||
|
<p className="text-sm text-text-muted mb-2">{app.description}</p>
|
||||||
|
)}
|
||||||
|
{app.mcpServer && (
|
||||||
|
<span className="inline-block px-2 py-1 text-xs bg-background-subtle text-text-muted rounded">
|
||||||
|
{app.mcpServer}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleLaunchApp(app)}
|
||||||
|
className="flex items-center gap-2 flex-1"
|
||||||
|
>
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
Launch
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</GridLayout>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MainPanelLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import McpAppRenderer from '../McpApps/McpAppRenderer';
|
||||||
|
import { startAgent, resumeAgent, listApps, stopAgent } from '../../api';
|
||||||
|
|
||||||
|
export default function StandaloneAppView() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||||
|
const [cachedHtml, setCachedHtml] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const resourceUri = searchParams.get('resourceUri');
|
||||||
|
const extensionName = searchParams.get('extensionName');
|
||||||
|
const appName = searchParams.get('appName');
|
||||||
|
const workingDir = searchParams.get('workingDir');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadCachedHtml() {
|
||||||
|
if (
|
||||||
|
!resourceUri ||
|
||||||
|
!extensionName ||
|
||||||
|
resourceUri === 'undefined' ||
|
||||||
|
extensionName === 'undefined'
|
||||||
|
) {
|
||||||
|
setError('Missing required parameters');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await listApps({
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const apps = response.data?.apps || [];
|
||||||
|
const cachedApp = apps.find(
|
||||||
|
(app) => app.uri === resourceUri && app.mcpServer === extensionName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedApp?.text) {
|
||||||
|
setCachedHtml(cachedApp.text);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to load cached HTML:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCachedHtml();
|
||||||
|
}, [resourceUri, extensionName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function initSession() {
|
||||||
|
if (!resourceUri || !extensionName || !workingDir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startResponse = await startAgent({
|
||||||
|
body: { working_dir: workingDir },
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sid = startResponse.data.id;
|
||||||
|
|
||||||
|
await resumeAgent({
|
||||||
|
body: {
|
||||||
|
session_id: sid,
|
||||||
|
load_model_and_extensions: true,
|
||||||
|
},
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSessionId(sid);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to initialize session:', err);
|
||||||
|
if (!cachedHtml) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to initialize session');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initSession();
|
||||||
|
}, [resourceUri, extensionName, workingDir, cachedHtml]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (appName) {
|
||||||
|
document.title = appName;
|
||||||
|
}
|
||||||
|
}, [appName]);
|
||||||
|
|
||||||
|
// Cleanup session when component unmounts
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (sessionId) {
|
||||||
|
stopAgent({
|
||||||
|
body: { session_id: sessionId },
|
||||||
|
throwOnError: false,
|
||||||
|
}).catch((err: unknown) => {
|
||||||
|
console.warn('Failed to stop agent on unmount:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [sessionId]);
|
||||||
|
|
||||||
|
if (error && !cachedHtml) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '16px',
|
||||||
|
padding: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 style={{ color: 'var(--text-error, #ef4444)' }}>Failed to Load App</h2>
|
||||||
|
<p style={{ color: 'var(--text-muted, #6b7280)' }}>{error}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && !cachedHtml) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p style={{ color: 'var(--text-muted, #6b7280)' }}>Initializing app...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedHtml || sessionId) {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100vw', height: '100vh', overflow: 'hidden' }}>
|
||||||
|
<McpAppRenderer
|
||||||
|
resourceUri={resourceUri!}
|
||||||
|
extensionName={extensionName!}
|
||||||
|
sessionId={sessionId || null}
|
||||||
|
fullscreen={true}
|
||||||
|
cachedHtml={cachedHtml || undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p style={{ color: 'var(--text-muted, #6b7280)' }}>Initializing app...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
TokenState,
|
TokenState,
|
||||||
updateFromSession,
|
updateFromSession,
|
||||||
updateSessionUserRecipeValues,
|
updateSessionUserRecipeValues,
|
||||||
|
listApps,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -274,6 +275,14 @@ export function useChatStream({
|
|||||||
accumulatedTotalTokens: loadedSession?.accumulated_total_tokens ?? 0,
|
accumulatedTotalTokens: loadedSession?.accumulated_total_tokens ?? 0,
|
||||||
});
|
});
|
||||||
setChatState(ChatState.Idle);
|
setChatState(ChatState.Idle);
|
||||||
|
|
||||||
|
listApps({
|
||||||
|
throwOnError: true,
|
||||||
|
query: { session_id: sessionId },
|
||||||
|
}).catch((err) => {
|
||||||
|
console.warn('Failed to populate apps cache:', err);
|
||||||
|
});
|
||||||
|
|
||||||
onSessionLoaded?.();
|
onSessionLoaded?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
import { UPDATES_ENABLED } from './updates';
|
import { UPDATES_ENABLED } from './updates';
|
||||||
import './utils/recipeHash';
|
import './utils/recipeHash';
|
||||||
import { Client, createClient, createConfig } from './api/client';
|
import { Client, createClient, createConfig } from './api/client';
|
||||||
|
import { GooseApp } from './api';
|
||||||
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
|
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
|
||||||
|
|
||||||
// Updater functions (moved here to keep updates.ts minimal for release replacement)
|
// Updater functions (moved here to keep updates.ts minimal for release replacement)
|
||||||
@@ -2437,6 +2438,59 @@ async function appMain() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('launch-app', async (event, gooseApp: GooseApp) => {
|
||||||
|
try {
|
||||||
|
const launchingWindow = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (!launchingWindow) {
|
||||||
|
throw new Error('Could not find launching window');
|
||||||
|
}
|
||||||
|
|
||||||
|
const launchingWindowId = launchingWindow.id;
|
||||||
|
const launchingClient = goosedClients.get(launchingWindowId);
|
||||||
|
if (!launchingClient) {
|
||||||
|
throw new Error('No client found for launching window');
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUrl = launchingWindow.webContents.getURL();
|
||||||
|
const baseUrl = new URL(currentUrl).origin;
|
||||||
|
|
||||||
|
const appWindow = new BrowserWindow({
|
||||||
|
title: gooseApp.name,
|
||||||
|
width: gooseApp.width ?? 800,
|
||||||
|
height: gooseApp.height ?? 600,
|
||||||
|
resizable: gooseApp.resizable ?? true,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
webSecurity: true,
|
||||||
|
partition: 'persist:goose',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
goosedClients.set(appWindow.id, launchingClient);
|
||||||
|
|
||||||
|
appWindow.on('close', () => {
|
||||||
|
goosedClients.delete(appWindow.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const workingDir = app.getPath('home');
|
||||||
|
const extensionName = gooseApp.mcpServer ?? '';
|
||||||
|
const standaloneUrl =
|
||||||
|
`${baseUrl}/#/standalone-app?` +
|
||||||
|
`resourceUri=${encodeURIComponent(gooseApp.uri)}` +
|
||||||
|
`&extensionName=${encodeURIComponent(extensionName)}` +
|
||||||
|
`&appName=${encodeURIComponent(gooseApp.name)}` +
|
||||||
|
`&workingDir=${encodeURIComponent(workingDir)}`;
|
||||||
|
|
||||||
|
await appWindow.loadURL(standaloneUrl);
|
||||||
|
appWindow.show();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to launch app:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Electron, { contextBridge, ipcRenderer, webUtils } from 'electron';
|
import Electron, { contextBridge, ipcRenderer, webUtils } from 'electron';
|
||||||
import { Recipe } from './recipe';
|
import { Recipe } from './recipe';
|
||||||
|
import { GooseApp } from './api';
|
||||||
|
|
||||||
interface NotificationData {
|
interface NotificationData {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -136,6 +137,7 @@ type ElectronAPI = {
|
|||||||
hasAcceptedRecipeBefore: (recipe: Recipe) => Promise<boolean>;
|
hasAcceptedRecipeBefore: (recipe: Recipe) => Promise<boolean>;
|
||||||
recordRecipeHash: (recipe: Recipe) => Promise<boolean>;
|
recordRecipeHash: (recipe: Recipe) => Promise<boolean>;
|
||||||
openDirectoryInExplorer: (directoryPath: string) => Promise<boolean>;
|
openDirectoryInExplorer: (directoryPath: string) => Promise<boolean>;
|
||||||
|
launchApp: (app: GooseApp) => Promise<void>;
|
||||||
addRecentDir: (dir: string) => Promise<boolean>;
|
addRecentDir: (dir: string) => Promise<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -275,6 +277,7 @@ const electronAPI: ElectronAPI = {
|
|||||||
recordRecipeHash: (recipe: Recipe) => ipcRenderer.invoke('record-recipe-hash', recipe),
|
recordRecipeHash: (recipe: Recipe) => ipcRenderer.invoke('record-recipe-hash', recipe),
|
||||||
openDirectoryInExplorer: (directoryPath: string) =>
|
openDirectoryInExplorer: (directoryPath: string) =>
|
||||||
ipcRenderer.invoke('open-directory-in-explorer', directoryPath),
|
ipcRenderer.invoke('open-directory-in-explorer', directoryPath),
|
||||||
|
launchApp: (app: GooseApp) => ipcRenderer.invoke('launch-app', app),
|
||||||
addRecentDir: (dir: string) => ipcRenderer.invoke('add-recent-dir', dir),
|
addRecentDir: (dir: string) => ipcRenderer.invoke('add-recent-dir', dir),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user