From eae9b29c4edafeea8d2768290e48464ae15dd561 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Wed, 4 Feb 2026 09:44:47 -0500 Subject: [PATCH 1/2] feat(mcp-apps): add Permission Policy support for sandbox iframes Implements _meta.ui.permissions support per MCP Apps spec Section 2. This allows MCP Apps to request browser capabilities like camera, microphone, geolocation, and clipboard-write access. Changes: - Add PermissionsMetadata struct in goose_apps/resource.rs - Pass permissions through sandbox bridge to proxy iframe - Build iframe 'allow' attribute from requested permissions - Update OpenAPI schema and generated TypeScript types This completes Section 2 (UI Resource Format) of the MCP Apps compliance checklist. --- crates/goose-server/src/openapi.rs | 1 + .../src/routes/templates/mcp_app_proxy.html | 18 ++++- crates/goose/src/goose_apps/mod.rs | 4 +- crates/goose/src/goose_apps/resource.rs | 24 ++++++ ui/desktop/openapi.json | 34 ++++++++ ui/desktop/src/api/client/client.gen.ts | 81 ++++++++----------- ui/desktop/src/api/client/types.gen.ts | 52 +++--------- ui/desktop/src/api/client/utils.gen.ts | 28 ++----- ui/desktop/src/api/core/auth.gen.ts | 3 +- ui/desktop/src/api/core/bodySerializer.gen.ts | 28 ++----- ui/desktop/src/api/core/params.gen.ts | 13 +-- ui/desktop/src/api/core/pathSerializer.gen.ts | 18 +---- .../src/api/core/queryKeySerializer.gen.ts | 31 ++----- .../src/api/core/serverSentEvents.gen.ts | 39 ++------- ui/desktop/src/api/core/types.gen.ts | 22 +---- ui/desktop/src/api/core/utils.gen.ts | 5 +- ui/desktop/src/api/index.ts | 2 +- ui/desktop/src/api/types.gen.ts | 25 ++++++ .../src/components/McpApps/McpAppRenderer.tsx | 7 +- ui/desktop/src/components/McpApps/types.ts | 2 +- .../components/McpApps/useSandboxBridge.ts | 6 +- 21 files changed, 198 insertions(+), 245 deletions(-) diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 597fb121249a..61917905c719 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -576,6 +576,7 @@ derive_utoipa!(Icon as IconSchema); goose::goose_apps::WindowProps, goose::goose_apps::McpAppResource, goose::goose_apps::CspMetadata, + goose::goose_apps::PermissionsMetadata, goose::goose_apps::UiMetadata, goose::goose_apps::ResourceMetadata, super::routes::dictation::TranscribeRequest, diff --git a/crates/goose-server/src/routes/templates/mcp_app_proxy.html b/crates/goose-server/src/routes/templates/mcp_app_proxy.html index 805ec0265963..07d91505c103 100644 --- a/crates/goose-server/src/routes/templates/mcp_app_proxy.html +++ b/crates/goose-server/src/routes/templates/mcp_app_proxy.html @@ -33,7 +33,7 @@ let guestIframe = null; - function createGuestIframe(html) { + function createGuestIframe(html, permissions) { if (guestIframe) { guestIframe.remove(); } @@ -46,6 +46,17 @@ // allow-forms: needed if the app has forms guestIframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms'); + // Build Permission Policy allow attribute from requested permissions + // These control access to sensitive browser APIs like camera, microphone, etc. + var allowList = []; + if (permissions && permissions.camera) allowList.push('camera'); + if (permissions && permissions.microphone) allowList.push('microphone'); + if (permissions && permissions.geolocation) allowList.push('geolocation'); + if (permissions && permissions.clipboardWrite) allowList.push('clipboard-write'); + if (allowList.length > 0) { + guestIframe.setAttribute('allow', allowList.join('; ')); + } + guestIframe.srcdoc = html; guestIframe.style.cssText = 'width:100%; height:100%; border:none;'; @@ -73,8 +84,9 @@ if (method === 'ui/notifications/sandbox-resource-ready') { var params = data.params || {}; var html = params.html || ''; + var permissions = params.permissions || null; - createGuestIframe(html); + createGuestIframe(html, permissions); return; } @@ -132,4 +144,4 @@ })(); - \ No newline at end of file + diff --git a/crates/goose/src/goose_apps/mod.rs b/crates/goose/src/goose_apps/mod.rs index 327b0e1bf93e..11c0d8450ecf 100644 --- a/crates/goose/src/goose_apps/mod.rs +++ b/crates/goose/src/goose_apps/mod.rs @@ -4,4 +4,6 @@ pub mod resource; pub use app::{fetch_mcp_apps, GooseApp, WindowProps}; pub use cache::McpAppCache; -pub use resource::{CspMetadata, McpAppResource, ResourceMetadata, UiMetadata}; +pub use resource::{ + CspMetadata, McpAppResource, PermissionsMetadata, ResourceMetadata, UiMetadata, +}; diff --git a/crates/goose/src/goose_apps/resource.rs b/crates/goose/src/goose_apps/resource.rs index a6ea769c7462..50d45a9bf73c 100644 --- a/crates/goose/src/goose_apps/resource.rs +++ b/crates/goose/src/goose_apps/resource.rs @@ -14,6 +14,26 @@ pub struct CspMetadata { pub resource_domains: Option>, } +/// Sandbox permissions for MCP Apps +/// Specifies which browser capabilities the UI needs access to. +/// Maps to the iframe Permission Policy `allow` attribute. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq, Default)] +#[serde(rename_all = "camelCase")] +pub struct PermissionsMetadata { + /// Request camera access (maps to Permission Policy `camera` feature) + #[serde(skip_serializing_if = "Option::is_none")] + pub camera: Option, + /// Request microphone access (maps to Permission Policy `microphone` feature) + #[serde(skip_serializing_if = "Option::is_none")] + pub microphone: Option, + /// Request geolocation access (maps to Permission Policy `geolocation` feature) + #[serde(skip_serializing_if = "Option::is_none")] + pub geolocation: Option, + /// Request clipboard write access (maps to Permission Policy `clipboard-write` feature) + #[serde(skip_serializing_if = "Option::is_none")] + pub clipboard_write: Option, +} + /// UI-specific metadata for MCP resources #[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)] #[serde(rename_all = "camelCase")] @@ -21,6 +41,9 @@ pub struct UiMetadata { /// Content Security Policy configuration #[serde(skip_serializing_if = "Option::is_none")] pub csp: Option, + /// Sandbox permissions requested by the UI + #[serde(skip_serializing_if = "Option::is_none")] + pub permissions: Option, /// Preferred domain for the app (used for CORS) #[serde(skip_serializing_if = "Option::is_none")] pub domain: Option, @@ -87,6 +110,7 @@ impl McpAppResource { meta: Some(ResourceMetadata { ui: Some(UiMetadata { csp: Some(csp), + permissions: None, domain: None, prefers_border: None, }), diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index c77adf765994..bdc55f928559 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -5249,6 +5249,32 @@ "never_allow" ] }, + "PermissionsMetadata": { + "type": "object", + "description": "Sandbox permissions for MCP Apps\nSpecifies which browser capabilities the UI needs access to.\nMaps to the iframe Permission Policy `allow` attribute.", + "properties": { + "camera": { + "type": "boolean", + "description": "Request camera access (maps to Permission Policy `camera` feature)", + "nullable": true + }, + "clipboardWrite": { + "type": "boolean", + "description": "Request clipboard write access (maps to Permission Policy `clipboard-write` feature)", + "nullable": true + }, + "geolocation": { + "type": "boolean", + "description": "Request geolocation access (maps to Permission Policy `geolocation` feature)", + "nullable": true + }, + "microphone": { + "type": "boolean", + "description": "Request microphone access (maps to Permission Policy `microphone` feature)", + "nullable": true + } + } + }, "PricingData": { "type": "object", "required": [ @@ -7021,6 +7047,14 @@ "description": "Preferred domain for the app (used for CORS)", "nullable": true }, + "permissions": { + "allOf": [ + { + "$ref": "#/components/schemas/PermissionsMetadata" + } + ], + "nullable": true + }, "prefersBorder": { "type": "boolean", "description": "Whether the app prefers to have a border around it", diff --git a/ui/desktop/src/api/client/client.gen.ts b/ui/desktop/src/api/client/client.gen.ts index bf75e621b5d3..d2e55a14497d 100644 --- a/ui/desktop/src/api/client/client.gen.ts +++ b/ui/desktop/src/api/client/client.gen.ts @@ -3,12 +3,7 @@ import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; import { getValidRequestBody } from '../core/utils.gen'; -import type { - Client, - Config, - RequestOptions, - ResolvedRequestOptions, -} from './types.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; import { buildUrl, createConfig, @@ -34,12 +29,7 @@ export const createClient = (config: Config = {}): Client => { return getConfig(); }; - const interceptors = createInterceptors< - Request, - Response, - unknown, - ResolvedRequestOptions - >(); + const interceptors = createInterceptors(); const beforeRequest = async (options: RequestOptions) => { const opts = { @@ -105,12 +95,7 @@ export const createClient = (config: Config = {}): Client => { for (const fn of interceptors.error.fns) { if (fn) { - finalError = (await fn( - error, - undefined as any, - request, - opts, - )) as unknown; + finalError = (await fn(error, undefined as any, request, opts)) as unknown; } } @@ -147,10 +132,7 @@ export const createClient = (config: Config = {}): Client => { ? getParseAs(response.headers.get('Content-Type')) : opts.parseAs) ?? 'json'; - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { + if (response.status === 204 || response.headers.get('Content-Length') === '0') { let emptyData: any; switch (parseAs) { case 'arrayBuffer': @@ -182,10 +164,16 @@ export const createClient = (config: Config = {}): Client => { case 'arrayBuffer': case 'blob': case 'formData': - case 'json': case 'text': data = await response[parseAs](); break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } case 'stream': return opts.responseStyle === 'data' ? response.body @@ -246,34 +234,29 @@ export const createClient = (config: Config = {}): Client => { }; }; - const makeMethodFn = - (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); - const makeSseFn = - (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); } - return request; - }, - serializedBody: getValidRequestBody(opts) as - | BodyInit - | null - | undefined, - url, - }); - }; + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; return { buildUrl, diff --git a/ui/desktop/src/api/client/types.gen.ts b/ui/desktop/src/api/client/types.gen.ts index b4a499cc032e..cb6d0d54a0ad 100644 --- a/ui/desktop/src/api/client/types.gen.ts +++ b/ui/desktop/src/api/client/types.gen.ts @@ -5,17 +5,13 @@ import type { ServerSentEventsOptions, ServerSentEventsResult, } from '../core/serverSentEvents.gen'; -import type { - Client as CoreClient, - Config as CoreConfig, -} from '../core/types.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; import type { Middleware } from './utils.gen'; export type ResponseStyle = 'data' | 'fields'; export interface Config - extends Omit, - CoreConfig { + extends Omit, CoreConfig { /** * Base URL for all requests made by this client. */ @@ -42,14 +38,7 @@ export interface Config * * @default 'auto' */ - parseAs?: - | 'arrayBuffer' - | 'auto' - | 'blob' - | 'formData' - | 'json' - | 'stream' - | 'text'; + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; /** * Should we return only data or multiple fields (data, error, response, etc.)? * @@ -69,7 +58,9 @@ export interface RequestOptions< TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, -> extends Config<{ +> + extends + Config<{ responseStyle: TResponseStyle; throwOnError: ThrowOnError; }>, @@ -116,32 +107,22 @@ export type RequestResult< ? TData[keyof TData] : TData : { - data: TData extends Record - ? TData[keyof TData] - : TData; + data: TData extends Record ? TData[keyof TData] : TData; request: Request; response: Response; } > : Promise< TResponseStyle extends 'data' - ? - | (TData extends Record - ? TData[keyof TData] - : TData) - | undefined + ? (TData extends Record ? TData[keyof TData] : TData) | undefined : ( | { - data: TData extends Record - ? TData[keyof TData] - : TData; + data: TData extends Record ? TData[keyof TData] : TData; error: undefined; } | { data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; + error: TError extends Record ? TError[keyof TError] : TError; } ) & { request: Request; @@ -180,10 +161,7 @@ type RequestFn = < TResponseStyle extends ResponseStyle = 'fields', >( options: Omit, 'method'> & - Pick< - Required>, - 'method' - >, + Pick>, 'method'>, ) => RequestResult; type BuildUrlFn = < @@ -197,13 +175,7 @@ type BuildUrlFn = < options: TData & Options, ) => string; -export type Client = CoreClient< - RequestFn, - Config, - MethodFn, - BuildUrlFn, - SseFn -> & { +export type Client = CoreClient & { interceptors: Middleware; }; diff --git a/ui/desktop/src/api/client/utils.gen.ts b/ui/desktop/src/api/client/utils.gen.ts index 4c48a9ee1152..b4bd2435ce0b 100644 --- a/ui/desktop/src/api/client/utils.gen.ts +++ b/ui/desktop/src/api/client/utils.gen.ts @@ -65,9 +65,7 @@ export const createQuerySerializer = ({ /** * Infers parseAs value from provided Content-Type header. */ -export const getParseAs = ( - contentType: string | null, -): Exclude => { +export const getParseAs = (contentType: string | null): Exclude => { if (!contentType) { // If no Content-Type header is provided, the best we can do is return the raw response body, // which is effectively the same as the 'stream' option. @@ -80,10 +78,7 @@ export const getParseAs = ( return; } - if ( - cleanContent.startsWith('application/json') || - cleanContent.endsWith('+json') - ) { + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { return 'json'; } @@ -92,9 +87,7 @@ export const getParseAs = ( } if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => - cleanContent.startsWith(type), - ) + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) ) { return 'blob'; } @@ -201,10 +194,7 @@ export const mergeHeaders = ( continue; } - const iterator = - header instanceof Headers - ? headersEntries(header) - : Object.entries(header); + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -233,10 +223,7 @@ type ErrInterceptor = ( options: Options, ) => Err | Promise; -type ReqInterceptor = ( - request: Req, - options: Options, -) => Req | Promise; +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; type ResInterceptor = ( response: Res, @@ -270,10 +257,7 @@ class Interceptors { return this.fns.indexOf(id); } - update( - id: number | Interceptor, - fn: Interceptor, - ): number | Interceptor | false { + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { const index = this.getInterceptorIndex(id); if (this.fns[index]) { this.fns[index] = fn; diff --git a/ui/desktop/src/api/core/auth.gen.ts b/ui/desktop/src/api/core/auth.gen.ts index f8a73266f934..3ebf9947883f 100644 --- a/ui/desktop/src/api/core/auth.gen.ts +++ b/ui/desktop/src/api/core/auth.gen.ts @@ -23,8 +23,7 @@ export const getAuthToken = async ( auth: Auth, callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, ): Promise => { - const token = - typeof callback === 'function' ? await callback(auth) : callback; + const token = typeof callback === 'function' ? await callback(auth) : callback; if (!token) { return; diff --git a/ui/desktop/src/api/core/bodySerializer.gen.ts b/ui/desktop/src/api/core/bodySerializer.gen.ts index 552b50f7c8d2..7a837a2b9486 100644 --- a/ui/desktop/src/api/core/bodySerializer.gen.ts +++ b/ui/desktop/src/api/core/bodySerializer.gen.ts @@ -1,10 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { - ArrayStyle, - ObjectStyle, - SerializerOptions, -} from './pathSerializer.gen'; +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; @@ -24,11 +20,7 @@ export type QuerySerializerOptions = QuerySerializerOptionsObject & { parameters?: Record; }; -const serializeFormDataPair = ( - data: FormData, - key: string, - value: unknown, -): void => { +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); } else if (value instanceof Date) { @@ -38,11 +30,7 @@ const serializeFormDataPair = ( } }; -const serializeUrlSearchParamsPair = ( - data: URLSearchParams, - key: string, - value: unknown, -): void => { +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { if (typeof value === 'string') { data.append(key, value); } else { @@ -52,7 +40,7 @@ const serializeUrlSearchParamsPair = ( export const formDataBodySerializer = { bodySerializer: | Array>>( - body: T, + body: T ): FormData => { const data = new FormData(); @@ -73,15 +61,11 @@ export const formDataBodySerializer = { export const jsonBodySerializer = { bodySerializer: (body: T): string => - JSON.stringify(body, (_key, value) => - typeof value === 'bigint' ? value.toString() : value, - ), + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), }; export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): string => { + bodySerializer: | Array>>(body: T): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { diff --git a/ui/desktop/src/api/core/params.gen.ts b/ui/desktop/src/api/core/params.gen.ts index 602715c46cc9..6099cab1b428 100644 --- a/ui/desktop/src/api/core/params.gen.ts +++ b/ui/desktop/src/api/core/params.gen.ts @@ -102,10 +102,7 @@ const stripEmptySlots = (params: Params) => { } }; -export const buildClientParams = ( - args: ReadonlyArray, - fields: FieldsConfig, -) => { +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { const params: Params = { body: {}, headers: {}, @@ -148,15 +145,11 @@ export const buildClientParams = ( params[field.map] = value; } } else { - const extra = extraPrefixes.find(([prefix]) => - key.startsWith(prefix), - ); + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); if (extra) { const [prefix, slot] = extra; - (params[slot] as Record)[ - key.slice(prefix.length) - ] = value; + (params[slot] as Record)[key.slice(prefix.length)] = value; } else if ('allowExtra' in config && config.allowExtra) { for (const [slot, allowed] of Object.entries(config.allowExtra)) { if (allowed) { diff --git a/ui/desktop/src/api/core/pathSerializer.gen.ts b/ui/desktop/src/api/core/pathSerializer.gen.ts index 8d9993104743..994b2848c63f 100644 --- a/ui/desktop/src/api/core/pathSerializer.gen.ts +++ b/ui/desktop/src/api/core/pathSerializer.gen.ts @@ -1,8 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -interface SerializeOptions - extends SerializePrimitiveOptions, - SerializerOptions {} +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} interface SerializePrimitiveOptions { allowReserved?: boolean; @@ -105,9 +103,7 @@ export const serializeArrayParam = ({ }); }) .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; }; export const serializePrimitiveParam = ({ @@ -146,11 +142,7 @@ export const serializeObjectParam = ({ if (style !== 'deepObject' && !explode) { let values: string[] = []; Object.entries(value).forEach(([key, v]) => { - values = [ - ...values, - key, - allowReserved ? (v as string) : encodeURIComponent(v as string), - ]; + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; }); const joinedValues = values.join(','); switch (style) { @@ -175,7 +167,5 @@ export const serializeObjectParam = ({ }), ) .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; }; diff --git a/ui/desktop/src/api/core/queryKeySerializer.gen.ts b/ui/desktop/src/api/core/queryKeySerializer.gen.ts index d3bb68396e96..5000df606f37 100644 --- a/ui/desktop/src/api/core/queryKeySerializer.gen.ts +++ b/ui/desktop/src/api/core/queryKeySerializer.gen.ts @@ -15,11 +15,7 @@ export type JsonValue = * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. */ export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if ( - value === undefined || - typeof value === 'function' || - typeof value === 'symbol' - ) { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { return undefined; } if (typeof value === 'bigint') { @@ -61,9 +57,7 @@ const isPlainObject = (value: unknown): value is Record => { * Turns URLSearchParams into a sorted JSON object for deterministic keys. */ const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => - a.localeCompare(b), - ); + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); const result: Record = {}; for (const [key, value] of entries) { @@ -86,26 +80,16 @@ const serializeSearchParams = (params: URLSearchParams): JsonValue => { /** * Normalizes any accepted value into a JSON-friendly shape for query keys. */ -export const serializeQueryKeyValue = ( - value: unknown, -): JsonValue | undefined => { +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { if (value === null) { return null; } - if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return value; } - if ( - value === undefined || - typeof value === 'function' || - typeof value === 'symbol' - ) { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { return undefined; } @@ -121,10 +105,7 @@ export const serializeQueryKeyValue = ( return stringifyToJsonValue(value); } - if ( - typeof URLSearchParams !== 'undefined' && - value instanceof URLSearchParams - ) { + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { return serializeSearchParams(value); } diff --git a/ui/desktop/src/api/core/serverSentEvents.gen.ts b/ui/desktop/src/api/core/serverSentEvents.gen.ts index 343d25af8052..6aa6cf02a4f4 100644 --- a/ui/desktop/src/api/core/serverSentEvents.gen.ts +++ b/ui/desktop/src/api/core/serverSentEvents.gen.ts @@ -2,10 +2,7 @@ import type { Config } from './types.gen'; -export type ServerSentEventsOptions = Omit< - RequestInit, - 'method' -> & +export type ServerSentEventsOptions = Omit & Pick & { /** * Fetch API implementation. You can use this option to provide a custom @@ -74,11 +71,7 @@ export interface StreamEvent { retry?: number; } -export type ServerSentEventsResult< - TData = unknown, - TReturn = void, - TNext = unknown, -> = { +export type ServerSentEventsResult = { stream: AsyncGenerator< TData extends Record ? TData[keyof TData] : TData, TReturn, @@ -101,9 +94,7 @@ export const createSseClient = ({ }: ServerSentEventsOptions): ServerSentEventsResult => { let lastEventId: string | undefined; - const sleep = - sseSleepFn ?? - ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); const createStream = async function* () { let retryDelay: number = sseDefaultRetryDelay ?? 3000; @@ -141,16 +132,11 @@ export const createSseClient = ({ const _fetch = options.fetch ?? globalThis.fetch; const response = await _fetch(request); - if (!response.ok) - throw new Error( - `SSE failed: ${response.status} ${response.statusText}`, - ); + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); if (!response.body) throw new Error('No body in SSE response'); - const reader = response.body - .pipeThrough(new TextDecoderStream()) - .getReader(); + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ''; @@ -188,10 +174,7 @@ export const createSseClient = ({ } else if (line.startsWith('id:')) { lastEventId = line.replace(/^id:\s*/, ''); } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt( - line.replace(/^retry:\s*/, ''), - 10, - ); + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); if (!Number.isNaN(parsed)) { retryDelay = parsed; } @@ -243,18 +226,12 @@ export const createSseClient = ({ // connection failed or aborted; retry after delay onSseError?.(error); - if ( - sseMaxRetryAttempts !== undefined && - attempt >= sseMaxRetryAttempts - ) { + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { break; // stop after firing error } // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min( - retryDelay * 2 ** (attempt - 1), - sseMaxRetryDelay ?? 30000, - ); + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); await sleep(backoff); } } diff --git a/ui/desktop/src/api/core/types.gen.ts b/ui/desktop/src/api/core/types.gen.ts index 643c070c9d29..97463257e43e 100644 --- a/ui/desktop/src/api/core/types.gen.ts +++ b/ui/desktop/src/api/core/types.gen.ts @@ -1,11 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Auth, AuthToken } from './auth.gen'; -import type { - BodySerializer, - QuerySerializer, - QuerySerializerOptions, -} from './bodySerializer.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; export type HttpMethod = | 'connect' @@ -34,9 +30,7 @@ export type Client< setConfig: (config: Config) => Config; } & { [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] - ? { sse?: never } - : { sse: { [K in HttpMethod]: SseFn } }); +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -59,13 +53,7 @@ export interface Config { | RequestInit['headers'] | Record< string, - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined - | unknown + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown >; /** * The request method. @@ -112,7 +100,5 @@ type IsExactlyNeverOrNeverUndefined = [T] extends [never] : false; export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true - ? never - : K]: T[K]; + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; }; diff --git a/ui/desktop/src/api/core/utils.gen.ts b/ui/desktop/src/api/core/utils.gen.ts index 0b5389d08996..e7ddbe354117 100644 --- a/ui/desktop/src/api/core/utils.gen.ts +++ b/ui/desktop/src/api/core/utils.gen.ts @@ -44,10 +44,7 @@ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { } if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); continue; } diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index 0ceb26947862..feba78d3e4be 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, cancelDownload, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteModel, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getPricing, getPrompt, getPrompts, getProviderModels, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, sendTelemetryEvent, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateSchedule, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadModelData, DownloadModelErrors, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetPricingData, GetPricingResponse, GetPricingResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelConfig, ModelInfo, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, PermissionLevel, PricingData, PricingQuery, PricingResponse, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionDisplayInfo, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; +export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadModelData, DownloadModelErrors, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetPricingData, GetPricingResponse, GetPricingResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelConfig, ModelInfo, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, PermissionLevel, PermissionsMetadata, PricingData, PricingQuery, PricingResponse, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionDisplayInfo, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index d40b90a0dd78..e8c7fcd683cf 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -667,6 +667,30 @@ export type ParseRecipeResponse = { */ export type PermissionLevel = 'always_allow' | 'ask_before' | 'never_allow'; +/** + * Sandbox permissions for MCP Apps + * Specifies which browser capabilities the UI needs access to. + * Maps to the iframe Permission Policy `allow` attribute. + */ +export type PermissionsMetadata = { + /** + * Request camera access (maps to Permission Policy `camera` feature) + */ + camera?: boolean | null; + /** + * Request clipboard write access (maps to Permission Policy `clipboard-write` feature) + */ + clipboardWrite?: boolean | null; + /** + * Request geolocation access (maps to Permission Policy `geolocation` feature) + */ + geolocation?: boolean | null; + /** + * Request microphone access (maps to Permission Policy `microphone` feature) + */ + microphone?: boolean | null; +}; + export type PricingData = { context_length?: number | null; currency: string; @@ -1273,6 +1297,7 @@ export type UiMetadata = { * Preferred domain for the app (used for CORS) */ domain?: string | null; + permissions?: PermissionsMetadata | null; /** * Whether the app prefers to have a border around it */ diff --git a/ui/desktop/src/components/McpApps/McpAppRenderer.tsx b/ui/desktop/src/components/McpApps/McpAppRenderer.tsx index 5607895b2c88..1c9d8fab6ccc 100644 --- a/ui/desktop/src/components/McpApps/McpAppRenderer.tsx +++ b/ui/desktop/src/components/McpApps/McpAppRenderer.tsx @@ -15,6 +15,7 @@ import { ToolResult, ToolCancelled, CspMetadata, + PermissionsMetadata, McpMethodParams, McpMethodResponse, } from './types'; @@ -40,6 +41,7 @@ interface McpAppRendererProps { interface ResourceData { html: string | null; csp: CspMetadata | null; + permissions: PermissionsMetadata | null; prefersBorder: boolean; } @@ -58,6 +60,7 @@ export default function McpAppRenderer({ const [resource, setResource] = useState({ html: cachedHtml || null, csp: null, + permissions: null, prefersBorder: true, }); const [error, setError] = useState(null); @@ -82,13 +85,14 @@ export default function McpAppRenderer({ if (response.data) { const content = response.data; const meta = content._meta as - | { ui?: { csp?: CspMetadata; prefersBorder?: boolean } } + | { ui?: { csp?: CspMetadata; permissions?: PermissionsMetadata; prefersBorder?: boolean } } | undefined; if (content.text !== cachedHtml) { setResource({ html: content.text, csp: meta?.ui?.csp || null, + permissions: meta?.ui?.permissions || null, prefersBorder: meta?.ui?.prefersBorder ?? true, }); } @@ -241,6 +245,7 @@ export default function McpAppRenderer({ const { iframeRef, proxyUrl } = useSandboxBridge({ resourceHtml: resource.html || '', resourceCsp: resource.csp, + resourcePermissions: resource.permissions, resourceUri, toolInput, toolInputPartial, diff --git a/ui/desktop/src/components/McpApps/types.ts b/ui/desktop/src/components/McpApps/types.ts index c24c7ae80f85..3e70f48e772a 100644 --- a/ui/desktop/src/components/McpApps/types.ts +++ b/ui/desktop/src/components/McpApps/types.ts @@ -1,4 +1,4 @@ -export type { CspMetadata, CallToolResponse as ToolResult } from '../../api/types.gen'; +export type { CspMetadata, PermissionsMetadata, CallToolResponse as ToolResult } from '../../api/types.gen'; export type ContentBlock = | { type: 'text'; text: string } diff --git a/ui/desktop/src/components/McpApps/useSandboxBridge.ts b/ui/desktop/src/components/McpApps/useSandboxBridge.ts index f519f50bbe19..2089cacf9d9d 100644 --- a/ui/desktop/src/components/McpApps/useSandboxBridge.ts +++ b/ui/desktop/src/components/McpApps/useSandboxBridge.ts @@ -9,6 +9,7 @@ import type { ToolCancelled, HostContext, CspMetadata, + PermissionsMetadata, } from './types'; import { fetchMcpAppProxyUrl } from './utils'; import { useTheme } from '../../contexts/ThemeContext'; @@ -18,6 +19,7 @@ import { errorMessage } from '../../utils/conversionUtils'; interface SandboxBridgeOptions { resourceHtml: string; resourceCsp: CspMetadata | null; + resourcePermissions: PermissionsMetadata | null; resourceUri: string; toolInput?: ToolInput; toolInputPartial?: ToolInputPartial; @@ -40,6 +42,7 @@ export function useSandboxBridge(options: SandboxBridgeOptions): SandboxBridgeRe const { resourceHtml, resourceCsp, + resourcePermissions, resourceUri, toolInput, toolInputPartial, @@ -80,7 +83,7 @@ export function useSandboxBridge(options: SandboxBridgeOptions): SandboxBridgeRe sendToSandbox({ jsonrpc: '2.0', method: 'ui/notifications/sandbox-resource-ready', - params: { html: resourceHtml, csp: resourceCsp }, + params: { html: resourceHtml, csp: resourceCsp, permissions: resourcePermissions }, }); break; @@ -181,6 +184,7 @@ export function useSandboxBridge(options: SandboxBridgeOptions): SandboxBridgeRe [ resourceHtml, resourceCsp, + resourcePermissions, resolvedTheme, sendToSandbox, onMcpRequest, From 3b7d70b437911cd7a7a9e1ea05ccf53c2cd2c8b5 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Wed, 4 Feb 2026 11:39:53 -0500 Subject: [PATCH 2/2] refactor: make PermissionsMetadata fields non-optional with defaults Address PR feedback to simplify generated types by: - Changing PermissionsMetadata fields from Option to bool with defaults - Changing UiMetadata.permissions from Option to PermissionsMetadata - Regenerating OpenAPI types This makes the client code cleaner and easier to read. --- crates/goose/src/goose_apps/resource.rs | 26 +++--- ui/desktop/openapi.json | 19 ++--- ui/desktop/src/api/client/client.gen.ts | 81 +++++++++++-------- ui/desktop/src/api/client/types.gen.ts | 52 +++++++++--- ui/desktop/src/api/client/utils.gen.ts | 28 +++++-- ui/desktop/src/api/core/auth.gen.ts | 3 +- ui/desktop/src/api/core/bodySerializer.gen.ts | 28 +++++-- ui/desktop/src/api/core/params.gen.ts | 13 ++- ui/desktop/src/api/core/pathSerializer.gen.ts | 18 ++++- .../src/api/core/queryKeySerializer.gen.ts | 31 +++++-- .../src/api/core/serverSentEvents.gen.ts | 39 +++++++-- ui/desktop/src/api/core/types.gen.ts | 22 ++++- ui/desktop/src/api/core/utils.gen.ts | 5 +- ui/desktop/src/api/types.gen.ts | 10 +-- 14 files changed, 262 insertions(+), 113 deletions(-) diff --git a/crates/goose/src/goose_apps/resource.rs b/crates/goose/src/goose_apps/resource.rs index 50d45a9bf73c..6afbdcc4b171 100644 --- a/crates/goose/src/goose_apps/resource.rs +++ b/crates/goose/src/goose_apps/resource.rs @@ -21,17 +21,21 @@ pub struct CspMetadata { #[serde(rename_all = "camelCase")] pub struct PermissionsMetadata { /// Request camera access (maps to Permission Policy `camera` feature) - #[serde(skip_serializing_if = "Option::is_none")] - pub camera: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub camera: bool, /// Request microphone access (maps to Permission Policy `microphone` feature) - #[serde(skip_serializing_if = "Option::is_none")] - pub microphone: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub microphone: bool, /// Request geolocation access (maps to Permission Policy `geolocation` feature) - #[serde(skip_serializing_if = "Option::is_none")] - pub geolocation: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub geolocation: bool, /// Request clipboard write access (maps to Permission Policy `clipboard-write` feature) - #[serde(skip_serializing_if = "Option::is_none")] - pub clipboard_write: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub clipboard_write: bool, +} + +fn is_default_permissions(p: &PermissionsMetadata) -> bool { + *p == PermissionsMetadata::default() } /// UI-specific metadata for MCP resources @@ -42,8 +46,8 @@ pub struct UiMetadata { #[serde(skip_serializing_if = "Option::is_none")] pub csp: Option, /// Sandbox permissions requested by the UI - #[serde(skip_serializing_if = "Option::is_none")] - pub permissions: Option, + #[serde(default, skip_serializing_if = "is_default_permissions")] + pub permissions: PermissionsMetadata, /// Preferred domain for the app (used for CORS) #[serde(skip_serializing_if = "Option::is_none")] pub domain: Option, @@ -110,7 +114,7 @@ impl McpAppResource { meta: Some(ResourceMetadata { ui: Some(UiMetadata { csp: Some(csp), - permissions: None, + permissions: PermissionsMetadata::default(), domain: None, prefers_border: None, }), diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index bdc55f928559..6f0473f98ff2 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -5255,23 +5255,19 @@ "properties": { "camera": { "type": "boolean", - "description": "Request camera access (maps to Permission Policy `camera` feature)", - "nullable": true + "description": "Request camera access (maps to Permission Policy `camera` feature)" }, "clipboardWrite": { "type": "boolean", - "description": "Request clipboard write access (maps to Permission Policy `clipboard-write` feature)", - "nullable": true + "description": "Request clipboard write access (maps to Permission Policy `clipboard-write` feature)" }, "geolocation": { "type": "boolean", - "description": "Request geolocation access (maps to Permission Policy `geolocation` feature)", - "nullable": true + "description": "Request geolocation access (maps to Permission Policy `geolocation` feature)" }, "microphone": { "type": "boolean", - "description": "Request microphone access (maps to Permission Policy `microphone` feature)", - "nullable": true + "description": "Request microphone access (maps to Permission Policy `microphone` feature)" } } }, @@ -7048,12 +7044,7 @@ "nullable": true }, "permissions": { - "allOf": [ - { - "$ref": "#/components/schemas/PermissionsMetadata" - } - ], - "nullable": true + "$ref": "#/components/schemas/PermissionsMetadata" }, "prefersBorder": { "type": "boolean", diff --git a/ui/desktop/src/api/client/client.gen.ts b/ui/desktop/src/api/client/client.gen.ts index d2e55a14497d..bf75e621b5d3 100644 --- a/ui/desktop/src/api/client/client.gen.ts +++ b/ui/desktop/src/api/client/client.gen.ts @@ -3,7 +3,12 @@ import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; import { getValidRequestBody } from '../core/utils.gen'; -import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; import { buildUrl, createConfig, @@ -29,7 +34,12 @@ export const createClient = (config: Config = {}): Client => { return getConfig(); }; - const interceptors = createInterceptors(); + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); const beforeRequest = async (options: RequestOptions) => { const opts = { @@ -95,7 +105,12 @@ export const createClient = (config: Config = {}): Client => { for (const fn of interceptors.error.fns) { if (fn) { - finalError = (await fn(error, undefined as any, request, opts)) as unknown; + finalError = (await fn( + error, + undefined as any, + request, + opts, + )) as unknown; } } @@ -132,7 +147,10 @@ export const createClient = (config: Config = {}): Client => { ? getParseAs(response.headers.get('Content-Type')) : opts.parseAs) ?? 'json'; - if (response.status === 204 || response.headers.get('Content-Length') === '0') { + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { let emptyData: any; switch (parseAs) { case 'arrayBuffer': @@ -164,16 +182,10 @@ export const createClient = (config: Config = {}): Client => { case 'arrayBuffer': case 'blob': case 'formData': + case 'json': case 'text': data = await response[parseAs](); break; - case 'json': { - // Some servers return 200 with no Content-Length and empty body. - // response.json() would throw; read as text and parse if non-empty. - const text = await response.text(); - data = text ? JSON.parse(text) : {}; - break; - } case 'stream': return opts.responseStyle === 'data' ? response.body @@ -234,29 +246,34 @@ export const createClient = (config: Config = {}): Client => { }; }; - const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); - const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } } - } - return request; - }, - serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, - url, - }); - }; + return request; + }, + serializedBody: getValidRequestBody(opts) as + | BodyInit + | null + | undefined, + url, + }); + }; return { buildUrl, diff --git a/ui/desktop/src/api/client/types.gen.ts b/ui/desktop/src/api/client/types.gen.ts index cb6d0d54a0ad..b4a499cc032e 100644 --- a/ui/desktop/src/api/client/types.gen.ts +++ b/ui/desktop/src/api/client/types.gen.ts @@ -5,13 +5,17 @@ import type { ServerSentEventsOptions, ServerSentEventsResult, } from '../core/serverSentEvents.gen'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; import type { Middleware } from './utils.gen'; export type ResponseStyle = 'data' | 'fields'; export interface Config - extends Omit, CoreConfig { + extends Omit, + CoreConfig { /** * Base URL for all requests made by this client. */ @@ -38,7 +42,14 @@ export interface Config * * @default 'auto' */ - parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; /** * Should we return only data or multiple fields (data, error, response, etc.)? * @@ -58,9 +69,7 @@ export interface RequestOptions< TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, -> - extends - Config<{ +> extends Config<{ responseStyle: TResponseStyle; throwOnError: ThrowOnError; }>, @@ -107,22 +116,32 @@ export type RequestResult< ? TData[keyof TData] : TData : { - data: TData extends Record ? TData[keyof TData] : TData; + data: TData extends Record + ? TData[keyof TData] + : TData; request: Request; response: Response; } > : Promise< TResponseStyle extends 'data' - ? (TData extends Record ? TData[keyof TData] : TData) | undefined + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined : ( | { - data: TData extends Record ? TData[keyof TData] : TData; + data: TData extends Record + ? TData[keyof TData] + : TData; error: undefined; } | { data: undefined; - error: TError extends Record ? TError[keyof TError] : TError; + error: TError extends Record + ? TError[keyof TError] + : TError; } ) & { request: Request; @@ -161,7 +180,10 @@ type RequestFn = < TResponseStyle extends ResponseStyle = 'fields', >( options: Omit, 'method'> & - Pick>, 'method'>, + Pick< + Required>, + 'method' + >, ) => RequestResult; type BuildUrlFn = < @@ -175,7 +197,13 @@ type BuildUrlFn = < options: TData & Options, ) => string; -export type Client = CoreClient & { +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { interceptors: Middleware; }; diff --git a/ui/desktop/src/api/client/utils.gen.ts b/ui/desktop/src/api/client/utils.gen.ts index b4bd2435ce0b..4c48a9ee1152 100644 --- a/ui/desktop/src/api/client/utils.gen.ts +++ b/ui/desktop/src/api/client/utils.gen.ts @@ -65,7 +65,9 @@ export const createQuerySerializer = ({ /** * Infers parseAs value from provided Content-Type header. */ -export const getParseAs = (contentType: string | null): Exclude => { +export const getParseAs = ( + contentType: string | null, +): Exclude => { if (!contentType) { // If no Content-Type header is provided, the best we can do is return the raw response body, // which is effectively the same as the 'stream' option. @@ -78,7 +80,10 @@ export const getParseAs = (contentType: string | null): Exclude cleanContent.startsWith(type)) + ['application/', 'audio/', 'image/', 'video/'].some((type) => + cleanContent.startsWith(type), + ) ) { return 'blob'; } @@ -194,7 +201,10 @@ export const mergeHeaders = ( continue; } - const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + const iterator = + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -223,7 +233,10 @@ type ErrInterceptor = ( options: Options, ) => Err | Promise; -type ReqInterceptor = (request: Req, options: Options) => Req | Promise; +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; type ResInterceptor = ( response: Res, @@ -257,7 +270,10 @@ class Interceptors { return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); if (this.fns[index]) { this.fns[index] = fn; diff --git a/ui/desktop/src/api/core/auth.gen.ts b/ui/desktop/src/api/core/auth.gen.ts index 3ebf9947883f..f8a73266f934 100644 --- a/ui/desktop/src/api/core/auth.gen.ts +++ b/ui/desktop/src/api/core/auth.gen.ts @@ -23,7 +23,8 @@ export const getAuthToken = async ( auth: Auth, callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, ): Promise => { - const token = typeof callback === 'function' ? await callback(auth) : callback; + const token = + typeof callback === 'function' ? await callback(auth) : callback; if (!token) { return; diff --git a/ui/desktop/src/api/core/bodySerializer.gen.ts b/ui/desktop/src/api/core/bodySerializer.gen.ts index 7a837a2b9486..552b50f7c8d2 100644 --- a/ui/desktop/src/api/core/bodySerializer.gen.ts +++ b/ui/desktop/src/api/core/bodySerializer.gen.ts @@ -1,6 +1,10 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; @@ -20,7 +24,11 @@ export type QuerySerializerOptions = QuerySerializerOptionsObject & { parameters?: Record; }; -const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); } else if (value instanceof Date) { @@ -30,7 +38,11 @@ const serializeFormDataPair = (data: FormData, key: string, value: unknown): voi } }; -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { if (typeof value === 'string') { data.append(key, value); } else { @@ -40,7 +52,7 @@ const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: export const formDataBodySerializer = { bodySerializer: | Array>>( - body: T + body: T, ): FormData => { const data = new FormData(); @@ -61,11 +73,15 @@ export const formDataBodySerializer = { export const jsonBodySerializer = { bodySerializer: (body: T): string => - JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), }; export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>(body: T): string => { + bodySerializer: | Array>>( + body: T, + ): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { diff --git a/ui/desktop/src/api/core/params.gen.ts b/ui/desktop/src/api/core/params.gen.ts index 6099cab1b428..602715c46cc9 100644 --- a/ui/desktop/src/api/core/params.gen.ts +++ b/ui/desktop/src/api/core/params.gen.ts @@ -102,7 +102,10 @@ const stripEmptySlots = (params: Params) => { } }; -export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { const params: Params = { body: {}, headers: {}, @@ -145,11 +148,15 @@ export const buildClientParams = (args: ReadonlyArray, fields: FieldsCo params[field.map] = value; } } else { - const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); if (extra) { const [prefix, slot] = extra; - (params[slot] as Record)[key.slice(prefix.length)] = value; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; } else if ('allowExtra' in config && config.allowExtra) { for (const [slot, allowed] of Object.entries(config.allowExtra)) { if (allowed) { diff --git a/ui/desktop/src/api/core/pathSerializer.gen.ts b/ui/desktop/src/api/core/pathSerializer.gen.ts index 994b2848c63f..8d9993104743 100644 --- a/ui/desktop/src/api/core/pathSerializer.gen.ts +++ b/ui/desktop/src/api/core/pathSerializer.gen.ts @@ -1,6 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts -interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} interface SerializePrimitiveOptions { allowReserved?: boolean; @@ -103,7 +105,9 @@ export const serializeArrayParam = ({ }); }) .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; }; export const serializePrimitiveParam = ({ @@ -142,7 +146,11 @@ export const serializeObjectParam = ({ if (style !== 'deepObject' && !explode) { let values: string[] = []; Object.entries(value).forEach(([key, v]) => { - values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; }); const joinedValues = values.join(','); switch (style) { @@ -167,5 +175,7 @@ export const serializeObjectParam = ({ }), ) .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; }; diff --git a/ui/desktop/src/api/core/queryKeySerializer.gen.ts b/ui/desktop/src/api/core/queryKeySerializer.gen.ts index 5000df606f37..d3bb68396e96 100644 --- a/ui/desktop/src/api/core/queryKeySerializer.gen.ts +++ b/ui/desktop/src/api/core/queryKeySerializer.gen.ts @@ -15,7 +15,11 @@ export type JsonValue = * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. */ export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { return undefined; } if (typeof value === 'bigint') { @@ -57,7 +61,9 @@ const isPlainObject = (value: unknown): value is Record => { * Turns URLSearchParams into a sorted JSON object for deterministic keys. */ const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); const result: Record = {}; for (const [key, value] of entries) { @@ -80,16 +86,26 @@ const serializeSearchParams = (params: URLSearchParams): JsonValue => { /** * Normalizes any accepted value into a JSON-friendly shape for query keys. */ -export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { if (value === null) { return null; } - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { return value; } - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { return undefined; } @@ -105,7 +121,10 @@ export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => return stringifyToJsonValue(value); } - if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { return serializeSearchParams(value); } diff --git a/ui/desktop/src/api/core/serverSentEvents.gen.ts b/ui/desktop/src/api/core/serverSentEvents.gen.ts index 6aa6cf02a4f4..343d25af8052 100644 --- a/ui/desktop/src/api/core/serverSentEvents.gen.ts +++ b/ui/desktop/src/api/core/serverSentEvents.gen.ts @@ -2,7 +2,10 @@ import type { Config } from './types.gen'; -export type ServerSentEventsOptions = Omit & +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & Pick & { /** * Fetch API implementation. You can use this option to provide a custom @@ -71,7 +74,11 @@ export interface StreamEvent { retry?: number; } -export type ServerSentEventsResult = { +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { stream: AsyncGenerator< TData extends Record ? TData[keyof TData] : TData, TReturn, @@ -94,7 +101,9 @@ export const createSseClient = ({ }: ServerSentEventsOptions): ServerSentEventsResult => { let lastEventId: string | undefined; - const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); const createStream = async function* () { let retryDelay: number = sseDefaultRetryDelay ?? 3000; @@ -132,11 +141,16 @@ export const createSseClient = ({ const _fetch = options.fetch ?? globalThis.fetch; const response = await _fetch(request); - if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); if (!response.body) throw new Error('No body in SSE response'); - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); let buffer = ''; @@ -174,7 +188,10 @@ export const createSseClient = ({ } else if (line.startsWith('id:')) { lastEventId = line.replace(/^id:\s*/, ''); } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); if (!Number.isNaN(parsed)) { retryDelay = parsed; } @@ -226,12 +243,18 @@ export const createSseClient = ({ // connection failed or aborted; retry after delay onSseError?.(error); - if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { break; // stop after firing error } // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); await sleep(backoff); } } diff --git a/ui/desktop/src/api/core/types.gen.ts b/ui/desktop/src/api/core/types.gen.ts index 97463257e43e..643c070c9d29 100644 --- a/ui/desktop/src/api/core/types.gen.ts +++ b/ui/desktop/src/api/core/types.gen.ts @@ -1,7 +1,11 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Auth, AuthToken } from './auth.gen'; -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; export type HttpMethod = | 'connect' @@ -30,7 +34,9 @@ export type Client< setConfig: (config: Config) => Config; } & { [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -53,7 +59,13 @@ export interface Config { | RequestInit['headers'] | Record< string, - string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown >; /** * The request method. @@ -100,5 +112,7 @@ type IsExactlyNeverOrNeverUndefined = [T] extends [never] : false; export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; }; diff --git a/ui/desktop/src/api/core/utils.gen.ts b/ui/desktop/src/api/core/utils.gen.ts index e7ddbe354117..0b5389d08996 100644 --- a/ui/desktop/src/api/core/utils.gen.ts +++ b/ui/desktop/src/api/core/utils.gen.ts @@ -44,7 +44,10 @@ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { } if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); continue; } diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index e8c7fcd683cf..eca80ef67000 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -676,19 +676,19 @@ export type PermissionsMetadata = { /** * Request camera access (maps to Permission Policy `camera` feature) */ - camera?: boolean | null; + camera?: boolean; /** * Request clipboard write access (maps to Permission Policy `clipboard-write` feature) */ - clipboardWrite?: boolean | null; + clipboardWrite?: boolean; /** * Request geolocation access (maps to Permission Policy `geolocation` feature) */ - geolocation?: boolean | null; + geolocation?: boolean; /** * Request microphone access (maps to Permission Policy `microphone` feature) */ - microphone?: boolean | null; + microphone?: boolean; }; export type PricingData = { @@ -1297,7 +1297,7 @@ export type UiMetadata = { * Preferred domain for the app (used for CORS) */ domain?: string | null; - permissions?: PermissionsMetadata | null; + permissions?: PermissionsMetadata; /** * Whether the app prefers to have a border around it */