From 2311e8dbf73a8833497c4483b1e5dc77ec9e1e2b Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Wed, 7 Jan 2026 17:49:38 -0500 Subject: [PATCH 01/17] Standalone --- crates/goose-server/src/openapi.rs | 4 + crates/goose-server/src/routes/agent.rs | 50 +++++++ crates/goose/src/goose_apps/mod.rs | 89 +++++++++++++ ui/desktop/openapi.json | 124 ++++++++++++++++++ ui/desktop/src/App.tsx | 4 + ui/desktop/src/api/sdk.gen.ts | 4 +- ui/desktop/src/api/types.gen.ts | 53 ++++++++ .../components/GooseSidebar/AppSidebar.tsx | 9 +- ui/desktop/src/main.ts | 55 ++++++++ ui/desktop/src/preload.ts | 3 + 10 files changed, 393 insertions(+), 2 deletions(-) diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 04debe7dec2d..67d898cd43af 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -357,6 +357,7 @@ derive_utoipa!(Icon as IconSchema); super::routes::agent::get_tools, super::routes::agent::read_resource, super::routes::agent::call_tool, + super::routes::agent::list_apps, super::routes::agent::update_from_session, super::routes::agent::agent_add_extension, super::routes::agent::agent_remove_extension, @@ -527,6 +528,8 @@ derive_utoipa!(Icon as IconSchema); super::routes::agent::ReadResourceResponse, super::routes::agent::CallToolRequest, super::routes::agent::CallToolResponse, + super::routes::agent::ListAppsRequest, + super::routes::agent::ListAppsResponse, super::routes::agent::StartAgentRequest, super::routes::agent::ResumeAgentRequest, super::routes::agent::UpdateFromSessionRequest, @@ -536,6 +539,7 @@ derive_utoipa!(Icon as IconSchema); super::tunnel::TunnelInfo, super::tunnel::TunnelState, super::routes::telemetry::TelemetryEventRequest, + goose::goose_apps::GooseApp, goose::goose_apps::McpAppResource, goose::goose_apps::CspMetadata, goose::goose_apps::UiMetadata, diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 70b871fa7048..c8a49c223d92 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -11,6 +11,7 @@ use axum::{ Json, Router, }; use goose::config::PermissionManager; +use goose::goose_apps::{list_mcp_apps, GooseApp}; use base64::Engine; use goose::agents::ExtensionConfig; @@ -698,6 +699,54 @@ async fn call_tool( })) } +#[derive(Deserialize, utoipa::IntoParams, utoipa::ToSchema)] +pub struct ListAppsRequest { + session_id: String, +} + +#[derive(Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ListAppsResponse { + pub apps: Vec, +} + +#[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>, + Query(params): Query, +) -> Result, ErrorResponse> { + let agent = state + .get_agent_for_route(params.session_id) + .await + .map_err(|status| ErrorResponse { + message: "Failed to get agent".to_string(), + status, + })?; + let apps = list_mcp_apps(&agent.extension_manager) + .await + .map_err(|e| ErrorResponse { + message: format!("Failed to list apps: {}", e.message), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + Ok(Json(ListAppsResponse { apps })) +} + pub fn routes(state: Arc) -> Router { Router::new() .route("/agent/start", post(start_agent)) @@ -705,6 +754,7 @@ pub fn routes(state: Arc) -> Router { .route("/agent/tools", get(get_tools)) .route("/agent/read_resource", post(read_resource)) .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_from_session", post(update_from_session)) .route("/agent/add_extension", post(agent_add_extension)) diff --git a/crates/goose/src/goose_apps/mod.rs b/crates/goose/src/goose_apps/mod.rs index 4fdeb773f2b4..dde9f68fe9d3 100644 --- a/crates/goose/src/goose_apps/mod.rs +++ b/crates/goose/src/goose_apps/mod.rs @@ -6,4 +6,93 @@ pub mod resource; +use crate::agents::ExtensionManager; +use rmcp::model::ErrorData; +use serde::{Deserialize, Serialize}; +use tokio_util::sync::CancellationToken; +use tracing::warn; +use utoipa::ToSchema; + pub use resource::{CspMetadata, McpAppResource, ResourceMetadata, UiMetadata}; + +/// GooseApp represents an app that can be launched in a standalone window +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct GooseApp { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub resizable: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mcp_server: Option, + pub resource_uri: String, + pub html: String, +} + +/// List all MCP apps from loaded extensions +pub async fn list_mcp_apps( + extension_manager: &ExtensionManager, +) -> Result, 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) => { + // Extract HTML from the first text content + 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() { + apps.push(GooseApp { + name: format_resource_name(resource.name.clone()), + description: resource.description.clone(), + resource_uri: resource.uri.clone(), + html, + width: None, + height: None, + resizable: Some(true), + mcp_server: Some(extension_name), + }); + } + } + 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::>() + .join(" ") +} diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index d539388d16ed..e51b3c28691b 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -128,6 +128,61 @@ } } }, + "/agent/list_apps": { + "get": { + "tags": [ + "Agent" + ], + "operationId": "list_apps", + "parameters": [ + { + "name": "session_id", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "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": { "post": { "tags": [ @@ -3604,6 +3659,50 @@ } } }, + "GooseApp": { + "type": "object", + "description": "GooseApp represents an app that can be launched in a standalone window", + "required": [ + "name", + "resourceUri", + "html" + ], + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "height": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + }, + "html": { + "type": "string" + }, + "mcpServer": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string" + }, + "resizable": { + "type": "boolean", + "nullable": true + }, + "resourceUri": { + "type": "string" + }, + "width": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + } + } + }, "Icon": { "type": "object", "required": [ @@ -3697,6 +3796,31 @@ } } }, + "ListAppsRequest": { + "type": "object", + "required": [ + "session_id" + ], + "properties": { + "session_id": { + "type": "string" + } + } + }, + "ListAppsResponse": { + "type": "object", + "required": [ + "apps" + ], + "properties": { + "apps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GooseApp" + } + } + } + }, "ListRecipeResponse": { "type": "object", "required": [ diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index cffd8c8625ce..8f96547026f6 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -38,6 +38,8 @@ import PermissionSettingsView from './components/settings/permission/PermissionS import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView'; 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 { NoProviderOrModelError, useAgent } from './hooks/useAgent'; import { useNavigation } from './hooks/useNavigation'; @@ -612,6 +614,7 @@ export function AppInner() { element={ setDidSelectProvider(true)} />} /> } /> + } /> } /> + } /> } /> } /> } /> diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index c0a2449959af..bd8c31b16cf8 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; 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, 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, 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, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, 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, 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, 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, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -45,6 +45,8 @@ export const callTool = (options: Options< } }); +export const listApps = (options: Options) => (options.client ?? client).get({ url: '/agent/list_apps', ...options }); + export const readResource = (options: Options) => (options.client ?? client).post({ url: '/agent/read_resource', ...options, diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 4c8ff01c4362..2df08b5ea8a9 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -358,6 +358,20 @@ export type GetToolsQuery = { session_id: string; }; +/** + * GooseApp represents an app that can be launched in a standalone window + */ +export type GooseApp = { + description?: string | null; + height?: number | null; + html: string; + mcpServer?: string | null; + name: string; + resizable?: boolean | null; + resourceUri: string; + width?: number | null; +}; + export type Icon = { mimeType?: string; sizes?: Array; @@ -393,6 +407,14 @@ export type KillJobResponse = { message: string; }; +export type ListAppsRequest = { + session_id: string; +}; + +export type ListAppsResponse = { + apps: Array; +}; + export type ListRecipeResponse = { manifests: Array; }; @@ -1245,6 +1267,37 @@ export type CallToolResponses = { export type CallToolResponse2 = CallToolResponses[keyof CallToolResponses]; +export type ListAppsData = { + body?: never; + path?: never; + query: { + session_id: string; + }; + 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 = { body: ReadResourceRequest; path?: never; diff --git a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx index e99bfb0584d9..4ef0fe553d93 100644 --- a/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx +++ b/ui/desktop/src/components/GooseSidebar/AppSidebar.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { FileText, Clock, Home, Puzzle, History } from 'lucide-react'; +import { FileText, Clock, Home, Puzzle, History, AppWindow } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { SidebarContent, @@ -84,6 +84,13 @@ const menuItems: NavigationEntry[] = [ icon: Puzzle, tooltip: 'Manage your extensions', }, + { + type: 'item', + path: '/apps', + label: 'Apps', + icon: AppWindow, + tooltip: 'Browse and launch MCP apps', + }, { type: 'separator' }, { type: 'item', diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 367a47c9d2df..eed9de6b9f11 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -49,6 +49,7 @@ import { import { UPDATES_ENABLED } from './updates'; import './utils/recipeHash'; import { Client, createClient, createConfig } from './api/client'; +import { GooseApp } from './api'; import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'; // Updater functions (moved here to keep updates.ts minimal for release replacement) @@ -2362,6 +2363,60 @@ async function appMain() { 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', + }, + }); + + // Share the same client with the standalone window + goosedClients.set(appWindow.id, launchingClient); + + appWindow.on('close', () => { + goosedClients.delete(appWindow.id); + }); + + // Build standalone URL with app parameters (using hash router) + const workingDir = app.getPath('home'); + const standaloneUrl = + `${baseUrl}/#/standalone-app?` + + `resourceUri=${encodeURIComponent(gooseApp.resourceUri)}` + + `&extensionName=${encodeURIComponent(gooseApp.mcpServer || '')}` + + `&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 () => { diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index b012691da077..ecab92030fc8 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -1,5 +1,6 @@ import Electron, { contextBridge, ipcRenderer, webUtils } from 'electron'; import { Recipe } from './recipe'; +import { GooseApp } from './api'; interface NotificationData { title: string; @@ -134,6 +135,7 @@ type ElectronAPI = { hasAcceptedRecipeBefore: (recipe: Recipe) => Promise; recordRecipeHash: (recipe: Recipe) => Promise; openDirectoryInExplorer: (directoryPath: string) => Promise; + launchApp: (app: GooseApp) => Promise; }; type AppConfigAPI = { @@ -270,6 +272,7 @@ const electronAPI: ElectronAPI = { recordRecipeHash: (recipe: Recipe) => ipcRenderer.invoke('record-recipe-hash', recipe), openDirectoryInExplorer: (directoryPath: string) => ipcRenderer.invoke('open-directory-in-explorer', directoryPath), + launchApp: (app: GooseApp) => ipcRenderer.invoke('launch-app', app), }; const appConfigAPI: AppConfigAPI = { From d1d881cc3c2c3f7dfe0021c03f22a116d94e228b Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Wed, 7 Jan 2026 18:51:01 -0500 Subject: [PATCH 02/17] add --- ui/desktop/src/components/apps/AppsView.tsx | 137 ++++++++++++++++++ .../src/components/apps/StandaloneAppView.tsx | 108 ++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 ui/desktop/src/components/apps/AppsView.tsx create mode 100644 ui/desktop/src/components/apps/StandaloneAppView.tsx diff --git a/ui/desktop/src/components/apps/AppsView.tsx b/ui/desktop/src/components/apps/AppsView.tsx new file mode 100644 index 000000000000..5fd5fa816e3b --- /dev/null +++ b/ui/desktop/src/components/apps/AppsView.tsx @@ -0,0 +1,137 @@ +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 ( +
+ {children} +
+ ); +}; + +export default function AppsView() { + const [apps, setApps] = useState([]); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + const chatContext = useChatContext(); + const sessionId = chatContext?.chat.sessionId; + + const loadApps = useCallback(async () => { + if (!sessionId) return; + + try { + setLoading(true); + const response = await listApps({ + throwOnError: true, + query: { session_id: sessionId }, + }); + const apps = response.data?.apps || []; + setApps(apps); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load apps'); + } finally { + setLoading(false); + } + }, [sessionId]); + + useEffect(() => { + loadApps(); + }, [loadApps]); + + const handleLaunchApp = async (app: GooseApp) => { + try { + await window.electron.launchApp(app); + } catch (err) { + console.error('Failed to launch app:', err); + setError(err instanceof Error ? err.message : 'Failed to launch app'); + } + }; + + if (error) { + return ( + +
+

Error loading apps: {error}

+ +
+
+ ); + } + + return ( + +
+
+
+
+

Apps

+
+

+ Applications from your MCP servers that can run in standalone windows. +

+
+
+ +
+ {loading ? ( +
+

Loading apps...

+
+ ) : apps.length === 0 ? ( +
+
+

No apps available

+

+ Install MCP servers that provide UI resources to see apps here. +

+
+
+ ) : ( + + {apps.map((app, index) => ( +
+
+

{app.name}

+ {app.description && ( +

{app.description}

+ )} + {app.mcpServer && ( + + {app.mcpServer} + + )} +
+
+ +
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/ui/desktop/src/components/apps/StandaloneAppView.tsx b/ui/desktop/src/components/apps/StandaloneAppView.tsx new file mode 100644 index 000000000000..4e54f89e36be --- /dev/null +++ b/ui/desktop/src/components/apps/StandaloneAppView.tsx @@ -0,0 +1,108 @@ +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import McpAppRenderer from '../McpApps/McpAppRenderer'; +import { startAgent, resumeAgent } from '../../api'; + +export default function StandaloneAppView() { + const [searchParams] = useSearchParams(); + const [sessionId, setSessionId] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const resourceUri = searchParams.get('resourceUri'); + const extensionName = searchParams.get('extensionName'); + const appName = searchParams.get('appName'); + const workingDir = searchParams.get('workingDir'); + + console.log('[StandaloneAppView] Rendering with:', { resourceUri, extensionName, appName, workingDir, loading, error, sessionId }); + + useEffect(() => { + async function initSession() { + if (!resourceUri || !extensionName || !workingDir || resourceUri === 'undefined' || extensionName === 'undefined') { + setError('Missing required parameters'); + setLoading(false); + return; + } + + try { + // Create a new session for this standalone app + const startResponse = await startAgent({ + body: { working_dir: workingDir }, + throwOnError: true, + }); + + const sid = startResponse.data.id; + + // Load all configured extensions (including the MCP server for this app) + await resumeAgent({ + body: { + session_id: sid, + load_model_and_extensions: true, + }, + throwOnError: true, + }); + + setSessionId(sid); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to initialize session'); + } finally { + setLoading(false); + } + } + + initSession(); + }, [resourceUri, extensionName, workingDir]); + + // Update window title when app name is available + useEffect(() => { + if (appName) { + document.title = appName; + } + }, [appName]); + + if (error) { + return ( +
+

Failed to Load App

+

{error}

+
+ ); + } + + if (loading || !sessionId) { + return ( +
+

Initializing app...

+
+ ); + } + + return ( +
+ +
+ ); +} From 4378820ed9ad03f6203ba4fd205ae5d82367c61b Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Thu, 8 Jan 2026 14:07:21 -0500 Subject: [PATCH 03/17] Standalone apps --- .../src/components/McpApps/McpAppRenderer.tsx | 29 +++++++++++++++++++ .../components/McpApps/useSandboxBridge.ts | 3 ++ .../src/components/apps/StandaloneAppView.tsx | 19 ++++++++++-- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/ui/desktop/src/components/McpApps/McpAppRenderer.tsx b/ui/desktop/src/components/McpApps/McpAppRenderer.tsx index 35acaf116864..e6b87660faf9 100644 --- a/ui/desktop/src/components/McpApps/McpAppRenderer.tsx +++ b/ui/desktop/src/components/McpApps/McpAppRenderer.tsx @@ -30,6 +30,7 @@ interface McpAppRendererProps { toolResult?: ToolResult; toolCancelled?: ToolCancelled; append?: (text: string) => void; + fullscreen?: boolean; } export default function McpAppRenderer({ @@ -41,6 +42,7 @@ export default function McpAppRenderer({ toolResult, toolCancelled, append, + fullscreen = false, }: McpAppRendererProps) { const [resourceHtml, setResourceHtml] = useState(null); const [resourceCsp, setResourceCsp] = useState(null); @@ -190,6 +192,33 @@ export default function McpAppRenderer({ ); } + if (fullscreen) { + return proxyUrl ? ( +