import type { ApiServicePayload, AutomationsPayload, AutomationUpdatePayload, ChannelConfigurePayload, ChannelConnectPayload, ChannelValidationPayload, ChatSummary, CliAppsPayload, FilePreviewPayload, ImageGenerationSettingsUpdate, McpPresetsPayload, MarketplaceProvider, NanobotFeaturesPayload, ModelConfigurationCreate, ModelConfigurationUpdate, NetworkSafetySettingsUpdate, PairingPayload, ProviderCreationUpdate, ProviderModelsPayload, ProviderOAuthCompletionResult, ProviderOAuthLoginResult, ProviderSettingsUpdate, SessionDeleteResult, SessionAutomationsPayload, SettingsPayload, SettingsUpdate, SidebarStatePayload, SkillDetail, SkillActionPayload, SkillInstallPayload, SkillsPayload, SkillsSearchPayload, SkillsTrendsPayload, SkillsTrendingPayload, SlashCommand, SlashCommandLifecycle, TranscriptionSettingsUpdate, WebSearchSettingsUpdate, WorkspacesPayload, WebuiThreadPersistedPayload, WorkspaceScopePayload, } from "./types"; import { fetchWithTimeout } from "./http"; const API_READ_TIMEOUT_MS = 20_000; const API_MUTATION_TIMEOUT_MS = 20_000; const PACKAGE_MUTATION_TIMEOUT_MS = 150_000; const SLASH_COMMAND_LIFECYCLES = new Set([ "side_channel", "finalize_active_turn", "stop_active_turn", "agent_turn", "agent_turn_with_args", ]); function isSlashCommandLifecycle(value: unknown): value is SlashCommandLifecycle { return ( typeof value === "string" && SLASH_COMMAND_LIFECYCLES.has(value as SlashCommandLifecycle) ); } export class ApiError extends Error { status: number; constructor(status: number, message: string) { super(message); this.status = status; this.name = "ApiError"; } } export interface WebUIMutationTransport { requestMutation( action: string, payload?: Record, timeoutMs?: number, ): Promise; } async function request( url: string, token: string, init?: RequestInit, timeoutMs: number = 0, ): Promise { const res = await fetchWithTimeout( url, { ...(init ?? {}), headers: { ...(init?.headers ?? {}), Authorization: `Bearer ${token}`, }, credentials: "same-origin", }, timeoutMs, ); if (!res.ok) { const text = typeof res.text === "function" ? (await res.text()).trim() : ""; throw new ApiError(res.status, text || `HTTP ${res.status}`); } const contentType = res.headers?.get?.("content-type") ?? ""; if (contentType && !contentType.toLowerCase().includes("application/json")) { const text = typeof res.text === "function" ? await res.text() : ""; const isHtml = text.trimStart().toLowerCase().startsWith("( transport: WebUIMutationTransport, action: string, payload: Record = {}, timeoutMs: number = API_MUTATION_TIMEOUT_MS, ): Promise { try { return await transport.requestMutation(action, payload, timeoutMs); } catch (reason) { const status = ( typeof reason === "object" && reason !== null && "status" in reason && typeof reason.status === "number" ) ? reason.status : 500; const message = reason instanceof Error ? reason.message : "WebUI mutation failed"; throw new ApiError(status, message); } } function compactMcpValues(values: Record): Record { const payload: Record = {}; Object.entries(values).forEach(([key, value]) => { if (value === null || value === undefined) return; if (typeof value === "string") { const trimmed = value.trim(); if (trimmed) payload[key] = trimmed; return; } payload[key] = value; }); return payload; } function splitKey(key: string): { channel: string; chatId: string } { const idx = key.indexOf(":"); if (idx === -1) return { channel: "", chatId: key }; return { channel: key.slice(0, idx), chatId: key.slice(idx + 1) }; } export async function listSessions( token: string, base: string = "", ): Promise { type Row = { key: string; created_at: string | null; updated_at: string | null; title?: string; preview?: string; model_preset?: string | null; run_started_at?: number | null; workspace_scope?: WorkspaceScopePayload | null; }; const body = await request<{ sessions: Row[] }>( `${base}/api/sessions`, token, undefined, API_READ_TIMEOUT_MS, ); return body.sessions.map((s) => ({ key: s.key, ...splitKey(s.key), createdAt: s.created_at, updatedAt: s.updated_at, title: s.title ?? "", preview: s.preview ?? "", modelPreset: s.model_preset ?? null, runStartedAt: s.run_started_at ?? null, workspaceScope: s.workspace_scope ?? null, })); } /** Disk-backed WebUI display thread snapshot (separate from agent session). */ export interface FetchWebuiThreadOptions { limit?: number; direction?: "latest"; before?: string | null; signal?: AbortSignal; } export async function fetchWebuiThread( token: string, key: string, optionsOrBase?: FetchWebuiThreadOptions | string, base: string = "", ): Promise { const options = typeof optionsOrBase === "string" ? undefined : optionsOrBase; const resolvedBase = typeof optionsOrBase === "string" ? optionsOrBase : base; const params = new URLSearchParams(); if (options?.limit !== undefined) params.set("limit", String(options.limit)); if (options?.direction) params.set("direction", options.direction); if (options?.before) params.set("before", options.before); const query = params.toString(); const suffix = query ? `?${query}` : ""; const url = `${resolvedBase}/api/sessions/${encodeURIComponent(key)}/webui-thread${suffix}`; const res = await fetchWithTimeout(url, { headers: { Authorization: `Bearer ${token}` }, credentials: "same-origin", cache: "no-store", signal: options?.signal, }); if (res.status === 404) return null; if (!res.ok) throw new ApiError(res.status, `HTTP ${res.status}`); return (await res.json()) as WebuiThreadPersistedPayload; } export async function fetchFilePreview( token: string, key: string, path: string, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("path", path); return request( `${base}/api/sessions/${encodeURIComponent(key)}/file-preview?${query}`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchFilePreviewAvailability( token: string, key: string, path: string, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("path", path); query.set("probe", "1"); const payload = await request<{ available?: boolean }>( `${base}/api/sessions/${encodeURIComponent(key)}/file-preview?${query}`, token, undefined, API_READ_TIMEOUT_MS, ); return payload.available !== false; } export async function fetchSessionAutomations( token: string, key: string, base: string = "", ): Promise { return request( `${base}/api/sessions/${encodeURIComponent(key)}/automations`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchAutomations( token: string, base: string = "", ): Promise { return request( `${base}/api/webui/automations`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function runAutomationAction( transport: WebUIMutationTransport, action: "enable" | "disable" | "delete" | "run", id: string, ): Promise { return mutation(transport, `automation.${action}`, { id }); } export async function updateAutomation( transport: WebUIMutationTransport, id: string, values: AutomationUpdatePayload, ): Promise { return mutation(transport, "automation.update", { id, values }); } export async function fetchSkills( token: string, base: string = "", ): Promise { return request( `${base}/api/webui/skills`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchSkillDetail( token: string, name: string, base: string = "", ): Promise { return request( `${base}/api/webui/skills/${encodeURIComponent(name)}`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function updateSkillEnabled( transport: WebUIMutationTransport, name: string, enabled: boolean, ): Promise { return mutation(transport, "skill.update", { name, enabled }); } export async function deleteSkill( transport: WebUIMutationTransport, name: string, ): Promise { return mutation(transport, "skill.delete", { name }); } export async function searchMarketplaceSkills( token: string, query: string, provider: MarketplaceProvider = "all", base: string = "", ): Promise { const params = new URLSearchParams({ q: query, provider }); return request( `${base}/api/webui/skills/search?${params}`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchTrendingMarketplaceSkills( token: string, provider: MarketplaceProvider = "all", base: string = "", ): Promise { const params = new URLSearchParams({ provider }); return request( `${base}/api/webui/skills/trending?${params}`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchMarketplaceSkillTrends( token: string, skillIds: string[], base: string = "", ): Promise { const params = new URLSearchParams(); skillIds.forEach((id) => params.append("id", id)); return request( `${base}/api/webui/skills/trends?${params}`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function installMarketplaceSkill( transport: WebUIMutationTransport, provider: Exclude, source: string, skill: string, version: string = "", ): Promise { return mutation( transport, "skill.install", { provider, source, skill, ...(version ? { version } : {}) }, PACKAGE_MUTATION_TIMEOUT_MS, ); } export async function deleteSession( transport: WebUIMutationTransport, key: string, optionsOrBase?: { deleteAutomations?: boolean } | string, ): Promise { const options = typeof optionsOrBase === "string" ? undefined : optionsOrBase; return mutation( transport, "session.delete", { key, ...(options?.deleteAutomations ? { delete_automations: true } : {}), }, ); } export async function fetchSettings( token: string, base: string = "", ): Promise { return request( `${base}/api/settings`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchSettingsUsage( token: string, base: string = "", ): Promise> { return request>( `${base}/api/settings/usage`, token, undefined, API_READ_TIMEOUT_MS, ); } export interface VersionCheckResult { updateAvailable: { currentVersion: string; latestVersion: string; pypiUrl?: string; } | null; } export async function checkVersion( token: string, base: string = "", ): Promise { return request( `${base}/api/settings/version-check`, token, undefined, 10_000, ); } export async function fetchWorkspaces( token: string, base: string = "", ): Promise { return request( `${base}/api/workspaces`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchCliApps( token: string, base: string = "", ): Promise { return request( `${base}/api/settings/cli-apps`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchInstalledCliApps( token: string, base: string = "", ): Promise { return request( `${base}/api/settings/cli-apps?installed_only=1`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchNanobotFeatures( token: string, base: string = "", ): Promise { return request( `${base}/api/settings/nanobot-features`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchApiService(token: string, base: string = ""): Promise { return request(`${base}/api/settings/api-service`, token); } export async function startApiService( transport: WebUIMutationTransport, values: { host: string; port: number; timeout: number; apiKey?: string }, ): Promise { return mutation( transport, "settings.api_service.start", { host: values.host, port: values.port, timeout: values.timeout, ...(values.apiKey !== undefined ? { api_key: values.apiKey } : {}), }, PACKAGE_MUTATION_TIMEOUT_MS, ); } export async function stopApiService( transport: WebUIMutationTransport, ): Promise { return mutation(transport, "settings.api_service.stop"); } export async function enableNanobotFeature( transport: WebUIMutationTransport, name: string, options: { instanceId?: string } = {}, ): Promise { return mutation( transport, "settings.feature.enable", { name, ...(options.instanceId ? { instance_id: options.instanceId } : {}) }, PACKAGE_MUTATION_TIMEOUT_MS, ); } export async function disableNanobotFeature( transport: WebUIMutationTransport, name: string, options: { instanceId?: string } = {}, ): Promise { return mutation( transport, "settings.feature.disable", { name, ...(options.instanceId ? { instance_id: options.instanceId } : {}) }, ); } export async function fetchPairingRequests( token: string, base: string = "", ): Promise { return request( `${base}/api/settings/pairing`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function runPairingAction( transport: WebUIMutationTransport, action: "approve" | "deny", code: string, ): Promise { return mutation(transport, `settings.pairing.${action}`, { code }); } export async function startChannelConnect( transport: WebUIMutationTransport, channel: string, options: { domain?: string; instanceId?: string; mode?: "replace" | "create"; force?: boolean; } = {}, ): Promise { return mutation( transport, "settings.channel.connect.start", { channel, ...(options.domain ? { domain: options.domain } : {}), ...(options.instanceId ? { instance_id: options.instanceId } : {}), ...(options.mode ? { mode: options.mode } : {}), ...(options.force ? { force: true } : {}), }, PACKAGE_MUTATION_TIMEOUT_MS, ); } export async function pollChannelConnect( transport: WebUIMutationTransport, channel: string, sessionId: string, params: Readonly> = {}, ): Promise { const values = Object.fromEntries( Object.entries(params).filter(([key]) => key !== "session_id"), ); return mutation( transport, "settings.channel.connect.poll", { channel, session_id: sessionId, ...values }, PACKAGE_MUTATION_TIMEOUT_MS, ); } export async function cancelChannelConnect( transport: WebUIMutationTransport, channel: string, sessionId: string, ): Promise { return mutation( transport, "settings.channel.connect.cancel", { channel, session_id: sessionId }, ); } export async function configureChannel( transport: WebUIMutationTransport, name: string, values: Record, options: { enable?: boolean; instanceId?: string } = {}, ): Promise { return mutation( transport, "settings.channel.configure", { name, values, ...(options.enable !== undefined ? { enable: options.enable } : {}), ...(options.instanceId ? { instance_id: options.instanceId } : {}), }, PACKAGE_MUTATION_TIMEOUT_MS, ); } export async function validateChannel( transport: WebUIMutationTransport, name: string, values: Record = {}, options: { instanceId?: string } = {}, ): Promise { return mutation( transport, "settings.channel.validate", { name, values, ...(options.instanceId ? { instance_id: options.instanceId } : {}) }, ); } export async function runCliAppAction( transport: WebUIMutationTransport, action: "install" | "update" | "uninstall" | "test", name: string, ): Promise { return mutation( transport, `settings.cli_app.${action}`, { name }, action === "install" || action === "update" ? PACKAGE_MUTATION_TIMEOUT_MS : API_MUTATION_TIMEOUT_MS, ); } export async function fetchMcpPresets( token: string, base: string = "", ): Promise { return request( `${base}/api/settings/mcp-presets`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function fetchProviderModels( token: string, provider: string, base: string = "", ): Promise { const query = new URLSearchParams(); query.set("provider", provider); return request( `${base}/api/settings/provider-models?${query}`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function runMcpPresetAction( transport: WebUIMutationTransport, action: "enable" | "remove" | "test", name: string, values: Record = {}, ): Promise { return mutation( transport, `settings.mcp.${action}`, { name, ...compactMcpValues(values) }, ); } export async function saveCustomMcpServer( transport: WebUIMutationTransport, values: Record, ): Promise { return mutation( transport, "settings.mcp.custom", compactMcpValues(values), ); } export async function importMcpConfig( transport: WebUIMutationTransport, config: string, ): Promise { return mutation(transport, "settings.mcp.import", { config }); } export async function updateMcpServerTools( transport: WebUIMutationTransport, name: string, enabledTools: string[], ): Promise { return mutation( transport, "settings.mcp.tools", { name, enabled_tools: enabledTools }, ); } export async function listSlashCommands( token: string, base: string = "", ): Promise { type Row = { command: string; title: string; description: string; icon: string; arg_hint?: string; lifecycle?: unknown; accepts_args?: unknown; }; const body = await request<{ commands: Row[] }>( `${base}/api/commands`, token, undefined, API_READ_TIMEOUT_MS, ); return body.commands .flatMap((command) => { if (!isSlashCommandLifecycle(command.lifecycle)) return []; return [{ command: command.command, title: command.title, description: command.description, icon: command.icon, argHint: command.arg_hint ?? "", lifecycle: command.lifecycle, acceptsArgs: command.accepts_args === true, }]; }); } export async function fetchSidebarState( token: string, base: string = "", ): Promise { return request( `${base}/api/webui/sidebar-state`, token, undefined, API_READ_TIMEOUT_MS, ); } export async function updateSidebarState( transport: WebUIMutationTransport, state: SidebarStatePayload, ): Promise { return mutation(transport, "sidebar.update", { state }); } export async function updateSettings( transport: WebUIMutationTransport, update: SettingsUpdate, ): Promise { const payload: Record = {}; if (update.modelPreset !== undefined) { payload.model_preset = update.modelPreset ?? "default"; } if (update.model !== undefined) payload.model = update.model; if (update.provider !== undefined) payload.provider = update.provider; if (update.contextWindowTokens !== undefined) { payload.context_window_tokens = update.contextWindowTokens; } if (update.timezone !== undefined) payload.timezone = update.timezone; if (update.toolHintMaxLength !== undefined) { payload.tool_hint_max_length = update.toolHintMaxLength; } return mutation(transport, "settings.agent.update", payload); } function modelGenerationSettingsPayload( configuration: Pick< ModelConfigurationCreate, "maxTokens" | "contextWindowTokens" | "temperature" | "reasoningEffort" >, ): Record { const payload: Record = {}; if (configuration.maxTokens !== undefined) { payload.max_tokens = configuration.maxTokens; } if (configuration.contextWindowTokens !== undefined) { payload.context_window_tokens = configuration.contextWindowTokens; } if (configuration.temperature !== undefined) { payload.temperature = configuration.temperature; } if (configuration.reasoningEffort !== undefined) { payload.reasoning_effort = configuration.reasoningEffort ?? ""; } return payload; } export async function createModelConfiguration( transport: WebUIMutationTransport, configuration: ModelConfigurationCreate, ): Promise { return mutation( transport, "settings.model_configuration.create", { ...(configuration.name !== undefined ? { name: configuration.name } : {}), label: configuration.label, provider: configuration.provider, model: configuration.model, ...modelGenerationSettingsPayload(configuration), }, ); } export async function updateModelConfiguration( transport: WebUIMutationTransport, configuration: ModelConfigurationUpdate, ): Promise { return mutation( transport, "settings.model_configuration.update", { name: configuration.name, ...(configuration.label !== undefined ? { label: configuration.label } : {}), ...(configuration.provider !== undefined ? { provider: configuration.provider } : {}), ...(configuration.model !== undefined ? { model: configuration.model } : {}), ...modelGenerationSettingsPayload(configuration), }, ); } export async function deleteModelConfiguration( transport: WebUIMutationTransport, name: string, ): Promise { return mutation( transport, "settings.model_configuration.delete", { name }, ); } export async function migrateModelConfigurations( transport: WebUIMutationTransport, ): Promise { return mutation(transport, "settings.model_configuration.migrate"); } export async function updateModelCallOrder( transport: WebUIMutationTransport, order: string[], ): Promise { return mutation(transport, "settings.model_call_order.update", { order }); } export async function updateProviderSettings( transport: WebUIMutationTransport, update: ProviderSettingsUpdate, ): Promise { return mutation(transport, "settings.provider.update", { ...update }); } export async function createProviderSettings( transport: WebUIMutationTransport, update: ProviderCreationUpdate, ): Promise { return mutation(transport, "settings.provider.create", { ...update }); } export async function loginProviderOAuth( transport: WebUIMutationTransport, provider: string, remoteBrowserAccess: boolean = false, ): Promise { return mutation( transport, "settings.provider.oauth_login", { provider, ...(remoteBrowserAccess ? { remote_browser: true } : {}) }, ); } export async function completeProviderOAuth( transport: WebUIMutationTransport, provider: string, flowId: string, authorizationResponse?: string, ): Promise { return mutation( transport, "settings.provider.oauth_complete", { provider, flow_id: flowId, ...(authorizationResponse ? { authorization_response: authorizationResponse } : {}), }, ); } export async function logoutProviderOAuth( transport: WebUIMutationTransport, provider: string, ): Promise { return mutation(transport, "settings.provider.oauth_logout", { provider }); } export async function updateWebSearchSettings( transport: WebUIMutationTransport, update: WebSearchSettingsUpdate, ): Promise { return mutation( transport, "settings.web_search.update", { provider: update.provider, ...(update.apiKey !== undefined ? { api_key: update.apiKey } : {}), ...(update.baseUrl !== undefined ? { base_url: update.baseUrl } : {}), ...(update.maxResults !== undefined ? { max_results: update.maxResults } : {}), ...(update.timeout !== undefined ? { timeout: update.timeout } : {}), ...(update.useJinaReader !== undefined ? { use_jina_reader: update.useJinaReader } : {}), }, ); } export async function updateNetworkSafetySettings( transport: WebUIMutationTransport, update: NetworkSafetySettingsUpdate, ): Promise { return mutation( transport, "settings.network_safety.update", { webui_allow_local_service_access: update.webuiAllowLocalServiceAccess, webui_default_access_mode: update.webuiDefaultAccessMode, }, ); } export async function updateImageGenerationSettings( transport: WebUIMutationTransport, update: ImageGenerationSettingsUpdate, ): Promise { return mutation( transport, "settings.image_generation.update", { enabled: update.enabled, provider: update.provider, model: update.model, default_aspect_ratio: update.defaultAspectRatio, default_image_size: update.defaultImageSize, max_images_per_turn: update.maxImagesPerTurn, }, ); } export async function updateTranscriptionSettings( transport: WebUIMutationTransport, update: TranscriptionSettingsUpdate, ): Promise { return mutation( transport, "settings.transcription.update", { enabled: update.enabled, provider: update.provider, model: update.model, language: update.language, max_duration_sec: update.maxDurationSec, max_upload_mb: update.maxUploadMb, }, ); }