diff --git a/main/src/chat/index.ts b/main/src/chat/index.ts new file mode 100644 index 00000000..909829c1 --- /dev/null +++ b/main/src/chat/index.ts @@ -0,0 +1,7 @@ +// Export all chat-related functionality +export * from './types' +export * from './providers' +export * from './storage' +export * from './mcp-tools' +export * from './streaming' +export * from './stream-utils' diff --git a/main/src/chat/mcp-tools.ts b/main/src/chat/mcp-tools.ts new file mode 100644 index 00000000..e0426084 --- /dev/null +++ b/main/src/chat/mcp-tools.ts @@ -0,0 +1,280 @@ +import { + experimental_createMCPClient as createMCPClient, + type experimental_MCPClient as MCPClient, + type experimental_MCPClientConfig as MCPClientConfig, +} from 'ai' +import type { ToolSet } from 'ai' +import { Experimental_StdioMCPTransport as StdioMCPTransport } from 'ai/mcp-stdio' +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' +import { createClient } from '@api/client' +import { getApiV1BetaWorkloads } from '@api/sdk.gen' +import type { CoreWorkload } from '@api/types.gen' +import { getHeaders } from '../headers' +import { getToolhivePort } from '../toolhive-manager' +import log from '../logger' +import type { McpToolInfo } from './types' +import { getEnabledMcpTools } from './storage' + +// Interface for MCP tool definition from client +interface McpToolDefinition { + description?: string + inputSchema?: { + properties?: Record + } +} + +// Type guard to check if an object is a valid MCP tool definition +function isMcpToolDefinition(obj: unknown): obj is McpToolDefinition { + if (!obj || typeof obj !== 'object') return false + + const tool = obj as Record + + // Description should be string if present + if ('description' in tool && typeof tool.description !== 'string') + return false + + // InputSchema should be object if present + if ('inputSchema' in tool) { + if (!tool.inputSchema || typeof tool.inputSchema !== 'object') return false + + const inputSchema = tool.inputSchema as Record + if ( + 'properties' in inputSchema && + inputSchema.properties !== null && + typeof inputSchema.properties !== 'object' + ) { + return false + } + } + + return true +} + +// Create transport configuration based on workload type +function createTransport( + workload: CoreWorkload, + serverName: string, + port: number +): MCPClientConfig { + const transportConfigs = { + stdio: () => ({ + name: serverName, + transport: new StdioMCPTransport({ + command: 'node', + args: [], + }), + }), + 'streamable-http': () => { + const url = new URL(workload.url || `http://localhost:${port}/mcp`) + return { + name: serverName, + transport: new StreamableHTTPClientTransport(url), + } + }, + sse: () => ({ + name: serverName, + transport: { + type: 'sse' as const, + url: `${workload.url || `http://localhost:${port}/sse#${serverName}`}`, + }, + }), + default: () => ({ + name: serverName, + transport: { + type: 'sse' as const, + url: `${workload.url || `http://localhost:${port}/sse#${serverName}`}`, + }, + }), + } + + // Check if transport_type is stdio but URL suggests SSE + let transportType = workload.transport_type as keyof typeof transportConfigs + + if (transportType === 'stdio' && workload.url) { + // If URL contains /sse or #, use SSE transport instead + if (workload.url.includes('/sse') || workload.url.includes('#')) { + // Override stdio to SSE based on URL pattern + transportType = 'sse' + } + } + + const configBuilder = + transportConfigs[transportType] || transportConfigs.default + return configBuilder() +} + +// Get MCP server tools information +export async function getMcpServerTools(serverName?: string): Promise< + | McpToolInfo[] + | { + serverName: string + serverPackage?: string + tools: Array<{ + name: string + description?: string + parameters?: Record + enabled: boolean + }> + isRunning: boolean + } + | null +> { + try { + const port = getToolhivePort() + const client = createClient({ + baseUrl: `http://localhost:${port}`, + headers: getHeaders(), + }) + + const { data } = await getApiV1BetaWorkloads({ + client, + }) + const workloads = data?.workloads + + // If serverName is provided, return server-specific format + if (serverName) { + // Get server tools for specific server + + const workload = (workloads || []).find( + (w) => w.name === serverName && w.tool_type === 'mcp' + ) + + if (!workload) { + return null + } + + // Get enabled tools for this server + const enabledTools = getEnabledMcpTools() + const enabledToolNames = enabledTools[serverName] || [] + + // If workload.tools is empty, try to discover tools by connecting to the server + let discoveredTools: string[] = workload.tools || [] + const serverMcpTools: Record = {} + + if (discoveredTools.length === 0 && workload.status === 'running') { + try { + // Try to create an MCP client and discover tools + const config = createTransport(workload, serverName, port!) + if (config) { + const mcpClient = await createMCPClient(config) + const rawTools = await mcpClient.tools() + + // Filter and validate tools using type guard + for (const [toolName, toolDef] of Object.entries(rawTools)) { + if (isMcpToolDefinition(toolDef)) { + serverMcpTools[toolName] = toolDef + } + } + + discoveredTools = Object.keys(serverMcpTools) + await mcpClient.close() + } + } catch (error) { + log.error(`Failed to discover tools for ${serverName}:`, error) + } + } + + const result = { + serverName: workload.name!, + serverPackage: workload.package, + tools: discoveredTools.map((toolName) => { + const toolDef = serverMcpTools[toolName] + return { + name: toolName, + description: toolDef?.description || '', + parameters: toolDef?.inputSchema?.properties || {}, + enabled: enabledToolNames.includes(toolName), + } + }), + isRunning: workload.status === 'running', + } + + return result + } + + // Otherwise return the original format for backward compatibility + const mcpTools = (workloads || []) + .filter( + (workload) => + workload.name && workload.tools && workload.tool_type === 'mcp' + ) + .flatMap((workload) => + workload.tools!.map((toolName) => ({ + name: `mcp_${workload.name}_${toolName}`, + description: '', + inputSchema: {}, + serverName: workload.name!, + })) + ) + + return mcpTools + } catch (error) { + log.error('Failed to get MCP server tools:', error) + return serverName ? null : [] + } +} + +// Create MCP tools for AI SDK +export async function createMcpTools(): Promise<{ + tools: ToolSet + clients: MCPClient[] +}> { + const mcpTools: ToolSet = {} + const mcpClients: MCPClient[] = [] + + try { + const port = getToolhivePort() + const client = createClient({ + baseUrl: `http://localhost:${port}`, + headers: getHeaders(), + }) + + const { data } = await getApiV1BetaWorkloads({ + client, + }) + const workloads = data?.workloads + + // Get enabled tools from storage + const enabledTools = getEnabledMcpTools() + + if (Object.keys(enabledTools).length === 0) { + return { tools: mcpTools, clients: mcpClients } + } + + // Create MCP clients for each server with enabled tools + for (const [serverName, toolNames] of Object.entries(enabledTools)) { + if (toolNames.length === 0) continue + + const workload = workloads?.find((w) => w.name === serverName) + if (!workload || workload.tool_type !== 'mcp') continue + + try { + const config = createTransport(workload, serverName, port!) + + const mcpClient = await createMCPClient(config) + + mcpClients.push(mcpClient) + + // Get all tools from the MCP server using schema discovery + const serverMcpTools = await mcpClient.tools() + + // Add only the enabled tools from this server + for (const toolName of toolNames) { + if (serverMcpTools[toolName]) { + mcpTools[toolName] = serverMcpTools[toolName] + } + } + + // MCP client created successfully + } catch (error) { + log.error(`Failed to create MCP client for ${serverName}:`, error) + } + } + + // MCP tools created + } catch (error) { + log.error('Failed to create MCP tools:', error) + } + + return { tools: mcpTools, clients: mcpClients } +} diff --git a/main/src/chat/providers.ts b/main/src/chat/providers.ts new file mode 100644 index 00000000..ae5e2247 --- /dev/null +++ b/main/src/chat/providers.ts @@ -0,0 +1,331 @@ +import { createOpenAI } from '@ai-sdk/openai' +import { createAnthropic } from '@ai-sdk/anthropic' +import { createGoogleGenerativeAI } from '@ai-sdk/google' +import { createXai } from '@ai-sdk/xai' +import { createOpenRouter } from '@openrouter/ai-sdk-provider' +import type { LanguageModel } from 'ai' +import log from '../logger' + +// OpenRouter API interfaces +interface OpenRouterModel { + id: string + name: string + description?: string + context_length: number + pricing: { + prompt: string + completion: string + } + top_provider: { + context_length: number + max_completion_tokens?: number + } + architecture?: { + modality?: string + tokenizer?: string + instruct_type?: string + } + supported_parameters?: string[] +} + +interface OpenRouterModelsResponse { + data: OpenRouterModel[] +} + +// Provider configuration for IPC (serializable) +interface ChatProviderInfo { + id: string + name: string + models: string[] +} + +// Internal provider configuration with functions +interface ChatProvider extends ChatProviderInfo { + createModel: (modelId: string, apiKey: string) => LanguageModel +} + +// Serializable provider info for the renderer +export const CHAT_PROVIDER_INFO: ChatProviderInfo[] = [ + { + id: 'openai', + name: 'OpenAI', + models: [ + // GPT series + 'gpt-5', + 'gpt-4o', + 'gpt-4o-mini', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + 'gpt-5-reasoning', + 'gpt-5-mini', + 'gpt-5-nano', + 'gpt-oss-20b', + 'gpt-oss-120b', + 'gpt-imagegen', + // O-series reasoning models + 'o3', + 'o3-mini', + 'o3-pro', + 'o4-mini', + ], + }, + { + id: 'anthropic', + name: 'Anthropic', + models: [ + // Claude 4 models (newest) + 'claude-opus-4-20250514', + 'claude-sonnet-4-20250514', + + // Claude 3.7 models + 'claude-3-7-sonnet-20250219', + + // Claude 3.5 models (current generation) + 'claude-3-5-sonnet-latest', + 'claude-3-5-sonnet-20241022', + 'claude-3-5-sonnet-20240620', + 'claude-3-5-haiku-latest', + 'claude-3-5-haiku-20241022', + + // Claude 3 models (previous generation) + 'claude-3-opus-latest', + 'claude-3-opus-20240229', + 'claude-3-sonnet-20240229', + 'claude-3-haiku-20240307', + ], + }, + { + id: 'google', + name: 'Google', + models: [ + 'gemini-2.5-pro', + 'gemini-2.5-flash', + 'gemini-2.5-flash-lite', + 'gemini-2.0-flash', + 'gemini-2.0-flash-lite', + 'gemini-2.5-flash-thinking', + 'gemini-2.5-flash-lite-thinking', + 'gemini-imagen-4', + 'gemini-imagen-4-ultra', + ], + }, + { + id: 'xai', + name: 'xAI', + models: ['grok-4', 'grok-3', 'grok-3-mini'], + }, + { + id: 'openrouter', + name: 'OpenRouter', + models: [ + // This will be dynamically populated from the API including all providers + // Fallback models for when API is unavailable: + + // OpenAI models via OpenRouter + 'openai/gpt-5-chat', + 'openai/gpt-5-mini', + 'openai/gpt-4o', + 'openai/gpt-4o-mini', + 'openai/gpt-4.1', + 'openai/gpt-4.1-mini', + 'openai/gpt-4.1-nano', + 'openai/o3', + 'openai/o3-mini', + 'openai/o3-pro', + 'openai/o4-mini', + + // Anthropic models via OpenRouter + 'anthropic/claude-3.5-sonnet:beta', + 'anthropic/claude-3-5-sonnet-20241022', + 'anthropic/claude-3-5-haiku-20241022', + 'anthropic/claude-3-opus-20240229', + 'anthropic/claude-3-sonnet-20240229', + 'anthropic/claude-3-haiku-20240307', + + // Google models via OpenRouter + 'google/gemini-2.5-pro', + 'google/gemini-2.5-flash', + 'google/gemini-2.5-flash-lite', + 'google/gemini-2.0-flash', + 'google/gemini-2.0-flash-lite', + 'google/gemini-2.5-flash-thinking', + 'google/gemini-2.5-flash-lite-thinking', + + // xAI models via OpenRouter + 'xai/grok-4', + 'xai/grok-3', + 'xai/grok-3-mini', + + // Meta (Llama) models + 'meta-llama/llama-3.3-70b-instruct', + 'meta-llama/llama-4-scout', + 'meta-llama/llama-4-maverick', + + // DeepSeek models + 'deepseek/deepseek-r1-llama-distilled', + 'deepseek/deepseek-v3-fireworks', + 'deepseek/deepseek-v3-0324', + 'deepseek/deepseek-r1-openrouter', + 'deepseek/deepseek-r1-0528', + 'deepseek/deepseek-r1-qwen-distilled', + + // Alibaba (Qwen) models + 'qwen/qwen-2.5-32b-instruct', + 'qwen/qwen3-32b', + 'qwen/qwen3-235b-thinking', + 'qwen/qwen3-235b', + 'qwen/qwen3-coder', + + // Moonshot AI (Kimi) models + 'moonshot/kimi-k2', + + // Zhipu AI (GLM) models + 'zhipuai/glm-4.5', + 'zhipuai/glm-4.5-thinking', + ], + }, +] + +// Internal provider configurations with model creation functions +export const CHAT_PROVIDERS: ChatProvider[] = [ + { + id: 'openai', + name: 'OpenAI', + models: CHAT_PROVIDER_INFO.find((p) => p.id === 'openai')?.models || [], + createModel: (modelId: string, apiKey: string) => { + const openai = createOpenAI({ apiKey }) + return openai(modelId) + }, + }, + { + id: 'anthropic', + name: 'Anthropic', + models: CHAT_PROVIDER_INFO.find((p) => p.id === 'anthropic')?.models || [], + createModel: (modelId: string, apiKey: string) => { + const anthropic = createAnthropic({ apiKey }) + return anthropic(modelId) + }, + }, + { + id: 'google', + name: 'Google', + models: CHAT_PROVIDER_INFO.find((p) => p.id === 'google')?.models || [], + createModel: (modelId: string, apiKey: string) => { + const google = createGoogleGenerativeAI({ apiKey }) + return google(modelId) + }, + }, + { + id: 'xai', + name: 'xAI', + models: CHAT_PROVIDER_INFO.find((p) => p.id === 'xai')?.models || [], + createModel: (modelId: string, apiKey: string) => { + const xai = createXai({ apiKey }) + return xai(modelId) + }, + }, + { + id: 'openrouter', + name: 'OpenRouter', + models: CHAT_PROVIDER_INFO.find((p) => p.id === 'openrouter')?.models || [], + createModel: (modelId: string, apiKey: string) => { + // Validate API key format + if (!apiKey || !apiKey.startsWith('sk-or-v1-')) { + throw new Error( + 'OpenRouter API key must start with "sk-or-v1-". Please check your API key format.' + ) + } + + const openrouter = createOpenRouter({ apiKey }) + return openrouter(modelId) + }, + }, +] + +// Fetch available models from OpenRouter API +export async function fetchOpenRouterModels(): Promise { + try { + const response = await fetch('https://openrouter.ai/api/v1/models', { + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + log.error('Failed to fetch OpenRouter models:', response.statusText) + // Return fallback models if API fails + return CHAT_PROVIDER_INFO.find((p) => p.id === 'openrouter')?.models || [] + } + + const data = (await response.json()) as OpenRouterModelsResponse + + // Filter and sort models by popularity/relevance + const models = data.data + .filter((model) => { + // Filter out models that are likely not suitable for chat + const modelId = model.id.toLowerCase() + const isNotChatModel = + modelId.includes('embedding') || + modelId.includes('whisper') || + modelId.includes('tts') || + modelId.includes('dall-e') || + modelId.includes('moderation') + + if (isNotChatModel) return false + + // Only include models that support tools/function calling + const supportsTools = + model.supported_parameters?.includes('tools') || + model.supported_parameters?.includes('functions') || + model.supported_parameters?.includes('function_call') + + return supportsTools + }) + .sort((a, b) => { + // Get fallback models for priority sorting + const fallbackModels = + CHAT_PROVIDER_INFO.find((p) => p.id === 'openrouter')?.models || [] + + const aIndex = fallbackModels.indexOf(a.id) + const bIndex = fallbackModels.indexOf(b.id) + + // If both models are in fallback list, maintain their original order + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex + } + + // If only one model is in fallback list, prioritize it + if (aIndex !== -1) return -1 + if (bIndex !== -1) return 1 + + // For models not in fallback list, sort alphabetically by name + return a.name.localeCompare(b.name) + }) + .map((model) => model.id) + + return models + } catch (error) { + log.error('Error fetching OpenRouter models:', error) + // Return fallback models if API fails + return CHAT_PROVIDER_INFO.find((p) => p.id === 'openrouter')?.models || [] + } +} + +// Discover tool-supported models programmatically +export function discoverToolSupportedModels(): { + providers: Array<{ + id: string + name: string + models: string[] + }> +} { + // Return the models that we know support tools based on our provider configurations + return { + providers: CHAT_PROVIDER_INFO.map((provider) => ({ + id: provider.id, + name: provider.name, + models: provider.models, + })), + } +} diff --git a/main/src/chat/storage.ts b/main/src/chat/storage.ts new file mode 100644 index 00000000..7c66a1f4 --- /dev/null +++ b/main/src/chat/storage.ts @@ -0,0 +1,218 @@ +import Store from 'electron-store' +import log from '../logger' + +// Type guard functions +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every((item) => typeof item === 'string') +} + +function isProvidersRecord( + value: unknown +): value is Record { + if (!isRecord(value)) return false + return Object.values(value).every( + (item) => + isRecord(item) && + typeof item.apiKey === 'string' && + isStringArray(item.enabledTools) + ) +} + +function isToolsRecord(value: unknown): value is Record { + if (!isRecord(value)) return false + return Object.values(value).every((item) => isStringArray(item)) +} + +function isSelectedModel( + value: unknown +): value is { provider: string; model: string } { + return ( + isRecord(value) && + typeof value.provider === 'string' && + typeof value.model === 'string' + ) +} + +// Create a secure store for chat settings (API keys and model selection) +const chatStore = new Store({ + name: 'chat-settings', + encryptionKey: 'toolhive-chat-encryption-key', // Basic encryption for API keys + defaults: { + providers: {} as Record< + string, + { + apiKey: string + enabledTools: string[] + } + >, + selectedModel: { + provider: '', + model: '', + }, + // Individual tool enablement per server (single source of truth) + enabledMcpTools: {} as Record, // serverName -> [toolName1, toolName2] + }, +}) + +// Chat settings interface +interface ChatSettings { + apiKey: string + enabledTools: string[] +} + +// Get chat settings for a provider +export function getChatSettings(providerId: string): ChatSettings { + try { + const providers = chatStore.get('providers') + if (isProvidersRecord(providers)) { + return providers[providerId] || { apiKey: '', enabledTools: [] } + } + return { apiKey: '', enabledTools: [] } + } catch (error) { + log.error('Failed to get chat settings:', error) + return { apiKey: '', enabledTools: [] } + } +} + +// Save chat settings for a provider +export function saveChatSettings( + providerId: string, + settings: ChatSettings +): { success: boolean; error?: string } { + try { + const providers = chatStore.get('providers') + const typedProviders = isProvidersRecord(providers) ? providers : {} + typedProviders[providerId] = settings + chatStore.set('providers', typedProviders) + return { success: true } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } + } +} + +// Clear chat settings for a provider +export function clearChatSettings(providerId?: string): { + success: boolean + error?: string +} { + try { + if (providerId) { + const providers = chatStore.get('providers') + const typedProviders = isProvidersRecord(providers) ? providers : {} + delete typedProviders[providerId] + chatStore.set('providers', typedProviders) + } else { + // Clear all providers if no specific provider is given + chatStore.set('providers', {}) + } + return { success: true } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } + } +} + +// Get selected model +export function getSelectedModel(): { provider: string; model: string } { + try { + const selectedModel = chatStore.get('selectedModel') + if (isSelectedModel(selectedModel)) { + return selectedModel + } + return { provider: '', model: '' } + } catch (error) { + log.error('Failed to get selected model:', error) + return { provider: '', model: '' } + } +} + +// Save selected model +export function saveSelectedModel( + provider: string, + model: string +): { success: boolean; error?: string } { + try { + chatStore.set('selectedModel', { provider, model }) + return { success: true } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } + } +} + +// Get enabled MCP tools for a specific server +// function getEnabledMcpToolsForServer(serverName: string): string[] { +// try { +// const enabledMcpTools = chatStore.get('enabledMcpTools') +// if (isToolsRecord(enabledMcpTools)) { +// return enabledMcpTools[serverName] || [] +// } +// return [] +// } catch (error) { +// log.error('Failed to get enabled MCP tools:', error) +// return [] +// } +// } + +// Save enabled MCP tools for a server +export function saveEnabledMcpTools( + serverName: string, + toolNames: string[] +): { success: boolean; error?: string } { + try { + const enabledMcpTools = chatStore.get('enabledMcpTools') + const typedTools = isToolsRecord(enabledMcpTools) ? enabledMcpTools : {} + // Store tools with their vanilla names + typedTools[serverName] = toolNames + chatStore.set('enabledMcpTools', typedTools) + return { success: true } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } + } +} + +// Get all enabled MCP tools (global) +export function getEnabledMcpTools(): Record { + try { + const enabledMcpTools = chatStore.get('enabledMcpTools') + if (isToolsRecord(enabledMcpTools)) { + return enabledMcpTools + } + return {} + } catch (error) { + log.error('Failed to get all enabled MCP tools:', error) + return {} + } +} + +// Get enabled MCP servers from tools (get servers that have enabled tools) +export function getEnabledMcpServersFromTools(): string[] { + try { + const allEnabledTools = getEnabledMcpTools() + const enabledServerNames = Object.keys(allEnabledTools).filter( + (serverName) => { + const tools = allEnabledTools[serverName] + return tools && tools.length > 0 + } + ) + // Return server IDs in the format expected by the UI + return enabledServerNames.map((serverName) => `mcp_${serverName}`) + } catch (error) { + log.error('Failed to get enabled MCP servers from tools:', error) + return [] + } +} diff --git a/main/src/chat/stream-utils.ts b/main/src/chat/stream-utils.ts new file mode 100644 index 00000000..9c0fc356 --- /dev/null +++ b/main/src/chat/stream-utils.ts @@ -0,0 +1,63 @@ +import type { WebContents } from 'electron' +import log from '../logger' + +/** + * Send an async iterable stream over IPC as real-time events + */ +function sendAsyncIterable( + sender: WebContents, + channel: string, + streamId: string, + iterable: AsyncIterable, + onComplete?: () => void | Promise +) { + ;(async () => { + try { + for await (const item of iterable) { + sender.send(`${channel}:chunk`, { streamId, chunk: item }) + } + + sender.send(`${channel}:end`, { streamId }) + + // Call cleanup callback after successful completion + if (onComplete) { + await onComplete() + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error' + sender.send(`${channel}:error`, { streamId, error: errorMessage }) + log.error(`[STREAM] Stream ${streamId} failed:`, error) + + // Call cleanup callback even on error + if (onComplete) { + try { + await onComplete() + } catch (cleanupError) { + log.error( + `[STREAM] Error during cleanup for stream ${streamId}:`, + cleanupError + ) + } + } + } + })() +} + +/** + * Convert UI message stream to real-time IPC events + */ +export function streamUIMessagesOverIPC( + sender: WebContents, + streamId: string, + uiMessageStream: AsyncIterable, + onComplete?: () => void | Promise +) { + sendAsyncIterable( + sender, + 'chat:stream', + streamId, + uiMessageStream, + onComplete + ) +} diff --git a/main/src/chat/streaming.ts b/main/src/chat/streaming.ts new file mode 100644 index 00000000..f1d327f3 --- /dev/null +++ b/main/src/chat/streaming.ts @@ -0,0 +1,160 @@ +import { streamText, stepCountIs } from 'ai' +import log from '../logger' +import { CHAT_PROVIDERS } from './providers' +import { createMcpTools } from './mcp-tools' +import { streamUIMessagesOverIPC } from './stream-utils' +import type { ChatRequest } from './types' +/** + * Handle chat streaming request using real-time IPC events + */ +export async function handleChatStreamRealtime( + request: ChatRequest, + streamId: string, + sender: Electron.WebContents +): Promise { + try { + // Validate provider + const provider = CHAT_PROVIDERS.find((p) => p.id === request.provider) + if (!provider) { + throw new Error(`Unknown provider: ${request.provider}`) + } + + // Create AI model + const model = provider.createModel(request.model, request.apiKey) + + // Convert messages to AI SDK CoreMessage format + const messages = request.messages + .map((msg) => ({ + role: msg.role as 'user' | 'assistant', + content: msg.parts + .filter( + (part) => part.type === 'text' && part.text && part.text.trim() + ) + .map((part) => part.text!.trim()) + .join('\n'), + })) + .filter((msg) => msg.content.trim().length > 0) // Filter out messages with empty content + + // Get MCP tools if enabled + const { tools: mcpTools, clients: mcpClients } = await createMcpTools() + + try { + // Use AI SDK's streamText - this is the recommended approach + const result = streamText({ + model, + messages, + tools: Object.keys(mcpTools).length > 0 ? mcpTools : undefined, + toolChoice: Object.keys(mcpTools).length > 0 ? 'auto' : undefined, + stopWhen: stepCountIs(10), // Stop after 10 steps + system: `You are a helpful assistant with access to MCP (Model Context Protocol) servers from ToolHive. + +You have access to various specialized tools from enabled MCP servers. Each tool is prefixed with the server name (e.g., github-stats-mcp_get_repository_info). + +🚨 CRITICAL INSTRUCTION: After calling ANY tool, you MUST immediately follow up with a text response that processes and interprets the tool results. NEVER just call a tool and stop talking. + +MANDATORY WORKFLOW: +1. Call the appropriate tool(s) to get data +2. IMMEDIATELY after the tool returns data, write a comprehensive text response +3. Parse and analyze the tool results in your text response +4. Extract key information and insights +5. Format everything in beautiful markdown +6. Provide a complete answer to the user's question + +⚠️ IMPORTANT: You must ALWAYS provide a text response after tool calls. Tool calls alone are not sufficient - users need you to interpret and explain the results. + +🔄 CONTINUATION RULE: Even if you've called tools, you MUST continue the conversation with a detailed analysis. Do not end your response after tool execution - always provide interpretation, insights, and a complete answer. + +FORMATTING REQUIREMENTS: +- Always use **Markdown syntax** for all responses +- Use proper headings (# ## ###), lists (- or 1.), tables, code blocks, etc. +- Present tool results in well-structured, readable format +- Extract meaningful insights from data +- NEVER show raw JSON or unformatted technical data +- NEVER just say "here's the result" - always interpret and format it + +MARKDOWN FORMATTING EXAMPLES: + +For GitHub repository data: +\`\`\`markdown +# 📦 Repository: owner/repo-name + +## 🚀 Latest Release: v1.2.3 +- **Published:** March 15, 2024 +- **Author:** @username +- **Downloads:** 1,234 total + +## 📊 Repository Stats +| Metric | Value | +|--------|--------| +| ⭐ Stars | 1,234 | +| 🍴 Forks | 89 | +| 📝 Issues | 23 open | + +## 💾 Download Options +- [Windows Setup](url) - 45 downloads +- [macOS DMG](url) - 234 downloads +- [Linux AppImage](url) - 123 downloads + +## 📈 Recent Activity +The repository shows active development with regular commits and community engagement. +\`\`\` + +Remember: Always interpret and format tool results beautifully. Never show raw data!`, + }) + + // Create UI message stream with metadata + const startTime = Date.now() + const uiMessageStream = result.toUIMessageStream({ + messageMetadata: ({ part }) => { + // Send metadata when streaming starts + if (part.type === 'start') { + return { + createdAt: Date.now(), + model: request.model, + } + } + + // Send additional metadata when streaming completes + if (part.type === 'finish') { + const endTime = Date.now() + return { + totalUsage: part.totalUsage, + responseTime: endTime - startTime, + finishReason: part.finishReason, + } + } + }, + }) + + // Stream UI messages over IPC in real-time with cleanup callback + streamUIMessagesOverIPC(sender, streamId, uiMessageStream, async () => { + // Clean up MCP clients after stream completes + + for (const client of mcpClients) { + try { + await client.close() + } catch (error) { + log.error('[CHAT] Error closing MCP client:', error) + } + } + }) + } catch (error) { + // Clean up MCP clients on error as well + + for (const client of mcpClients) { + try { + await client.close() + } catch (cleanupError) { + log.error( + '[CHAT] Error closing MCP client during error cleanup:', + cleanupError + ) + } + } + throw error + } + } catch (error) { + log.error('[CHAT] Chat stream error:', error) + throw error + } +} diff --git a/main/src/chat/types.ts b/main/src/chat/types.ts new file mode 100644 index 00000000..4814f358 --- /dev/null +++ b/main/src/chat/types.ts @@ -0,0 +1,23 @@ +// Chat request interface +export interface ChatRequest { + messages: Array<{ + id: string + role: 'user' | 'assistant' + parts: Array<{ + type: string + text?: string + }> + }> + provider: string + model: string + apiKey: string + enabledTools?: string[] +} + +// MCP Tool information interface +export interface McpToolInfo { + name: string + description: string + inputSchema: Record + serverName: string +} diff --git a/main/src/main.ts b/main/src/main.ts index 7bf36bb5..5223359e 100644 --- a/main/src/main.ts +++ b/main/src/main.ts @@ -45,6 +45,21 @@ import { getAllFeatureFlags, type FeatureFlagKey, } from './feature-flags' +import { + CHAT_PROVIDER_INFO, + getChatSettings, + saveChatSettings, + clearChatSettings, + getSelectedModel, + saveSelectedModel, + getMcpServerTools, + getEnabledMcpTools, + getEnabledMcpServersFromTools, + saveEnabledMcpTools, + discoverToolSupportedModels, + fetchOpenRouterModels, + type ChatRequest, +} from './chat' let tray: Tray | null = null let isQuitting = false @@ -728,3 +743,92 @@ ipcMain.handle('feature-flags:disable', (_event, key: FeatureFlagKey): void => { ipcMain.handle('feature-flags:get-all', (): Record => { return getAllFeatureFlags() }) + +// ──────────────────────────────────────────────────────────────────────────── +// Chat IPC handlers +// ──────────────────────────────────────────────────────────────────────────── + +ipcMain.handle('chat:get-providers', async () => { + // Create a copy of the provider info to avoid modifying the original + const providers = [...CHAT_PROVIDER_INFO] + + // For OpenRouter, fetch the latest models dynamically only if API key is available + const openRouterIndex = providers.findIndex((p) => p.id === 'openrouter') + if (openRouterIndex !== -1) { + try { + const openRouterSettings = getChatSettings('openrouter') + + // Only fetch models if user has provided an API key + if ( + openRouterSettings.apiKey && + openRouterSettings.apiKey.trim() !== '' + ) { + const openRouterModels = await fetchOpenRouterModels() + const originalProvider = providers[openRouterIndex] + if (originalProvider) { + providers[openRouterIndex] = { + id: originalProvider.id, + name: originalProvider.name, + models: openRouterModels, + } + } + } + // If no API key, keep the original hardcoded models as fallback + } catch (error) { + log.error('Failed to fetch OpenRouter models, using fallback:', error) + // Keep the original hardcoded models as fallback + } + } + + return providers +}) + +// Chat streaming endpoint - uses real-time IPC events +ipcMain.handle('chat:stream', async (event, request: ChatRequest) => { + const streamId = `stream-${Date.now()}-${Math.random().toString(36).substring(2, 9)}` + const { handleChatStreamRealtime } = await import('./chat') + + // Start streaming (non-blocking) + handleChatStreamRealtime(request, streamId, event.sender) + + // Return the stream ID immediately + return { streamId } +}) + +// Chat settings store handlers +ipcMain.handle('chat:get-settings', (_, providerId: string) => + getChatSettings(providerId) +) +ipcMain.handle( + 'chat:save-settings', + ( + _, + providerId: string, + settings: { apiKey: string; enabledTools: string[] } + ) => saveChatSettings(providerId, settings) +) +ipcMain.handle('chat:clear-settings', (_, providerId?: string) => + clearChatSettings(providerId) +) +ipcMain.handle('chat:discover-models', () => discoverToolSupportedModels()) + +// Model selection persistence handlers +ipcMain.handle('chat:get-selected-model', () => getSelectedModel()) +ipcMain.handle( + 'chat:save-selected-model', + (_, provider: string, model: string) => saveSelectedModel(provider, model) +) + +// MCP tools management handlers (single source of truth) +ipcMain.handle('chat:get-mcp-server-tools', (_, serverName?: string) => + getMcpServerTools(serverName) +) +ipcMain.handle('chat:get-enabled-mcp-tools', () => getEnabledMcpTools()) +ipcMain.handle('chat:get-enabled-mcp-servers-from-tools', () => + getEnabledMcpServersFromTools() +) +ipcMain.handle( + 'chat:save-enabled-mcp-tools', + (_, serverName: string, enabledTools: string[]) => + saveEnabledMcpTools(serverName, enabledTools) +) diff --git a/package.json b/package.json index 81ee835a..f18b8865 100644 --- a/package.json +++ b/package.json @@ -90,10 +90,19 @@ "vitest-fail-on-console": "^0.9.0" }, "dependencies": { + "@ai-sdk/anthropic": "^2.0.1", + "@ai-sdk/google": "^2.0.4", + "@ai-sdk/openai": "^2.0.10", + "@ai-sdk/provider": "^2.0.0", + "@ai-sdk/react": "^2.0.13", + "@ai-sdk/xai": "^2.0.5", "@fontsource-variable/inter": "^5.2.6", "@fontsource/atkinson-hyperlegible": "^5.2.6", "@fontsource/space-mono": "^5.2.7", + "@modelcontextprotocol/sdk": "^1.17.2", + "@openrouter/ai-sdk-provider": "^1.1.2", "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", @@ -114,9 +123,11 @@ "@tanstack/react-query-devtools": "^5.80.5", "@tanstack/react-router": "^1.120.11", "@tanstack/react-router-devtools": "^1.120.11", + "ai": "^5.0.10", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "date-fns": "^4.1.0", "electron-log": "^5.4.1", "electron-squirrel-startup": "^1.0.1", "electron-store": "^10.1.0", @@ -124,7 +135,9 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "7.62.0", + "react-markdown": "^10.1.0", "regexp.escape": "^2.0.1", + "remark-gfm": "^4.0.1", "sonner": "^2.0.3", "tailwind-merge": "^3.3.0", "tailwindcss": "^4.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index caed2cd1..d43689a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,24 @@ importers: .: dependencies: + '@ai-sdk/anthropic': + specifier: ^2.0.1 + version: 2.0.1(zod@4.0.17) + '@ai-sdk/google': + specifier: ^2.0.4 + version: 2.0.4(zod@4.0.17) + '@ai-sdk/openai': + specifier: ^2.0.10 + version: 2.0.10(zod@4.0.17) + '@ai-sdk/provider': + specifier: ^2.0.0 + version: 2.0.0 + '@ai-sdk/react': + specifier: ^2.0.13 + version: 2.0.13(react@19.1.1)(zod@4.0.17) + '@ai-sdk/xai': + specifier: ^2.0.5 + version: 2.0.5(zod@4.0.17) '@fontsource-variable/inter': specifier: ^5.2.6 version: 5.2.6 @@ -17,9 +35,18 @@ importers: '@fontsource/space-mono': specifier: ^5.2.7 version: 5.2.8 + '@modelcontextprotocol/sdk': + specifier: ^1.17.2 + version: 1.17.2 + '@openrouter/ai-sdk-provider': + specifier: ^1.1.2 + version: 1.1.2(ai@5.0.10(zod@4.0.17))(zod@4.0.17) '@radix-ui/react-checkbox': specifier: ^1.3.2 version: 1.3.2(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-collapsible': + specifier: ^1.1.12 + version: 1.1.12(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-dialog': specifier: ^1.1.14 version: 1.1.14(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -80,6 +107,9 @@ importers: '@tanstack/react-router-devtools': specifier: ^1.120.11 version: 1.131.10(@tanstack/react-router@1.131.10(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.131.7)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.7)(tiny-invariant@1.3.3) + ai: + specifier: ^5.0.10 + version: 5.0.10(zod@4.0.17) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -89,6 +119,9 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + date-fns: + specifier: ^4.1.0 + version: 4.1.0 electron-log: specifier: ^5.4.1 version: 5.4.2 @@ -110,9 +143,15 @@ importers: react-hook-form: specifier: 7.62.0 version: 7.62.0(react@19.1.1) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.1.10)(react@19.1.1) regexp.escape: specifier: ^2.0.1 version: 2.0.1 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 sonner: specifier: ^2.0.3 version: 2.0.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -218,7 +257,7 @@ importers: version: 4.0.1(vite@7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1)) '@vitest/coverage-istanbul': specifier: ^3.1.4 - version: 3.2.4(vitest@3.2.4(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1)) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) @@ -290,16 +329,84 @@ importers: version: 7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1) vitest: specifier: ^3.1.4 - version: 3.2.4(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1) vitest-fail-on-console: specifier: ^0.9.0 - version: 0.9.0(vite@7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1)) + version: 0.9.0(vite@7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1)) packages: '@adobe/css-tools@4.4.3': resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} + '@ai-sdk/anthropic@2.0.1': + resolution: {integrity: sha512-HtNbpNV9qXQosHu00+CBMEcdTerwZY+kpVMNak0xP/P5TF6XkPf7IyizhLuc7y5zcXMjZCMA7jDGkcEdZCEdkw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/gateway@1.0.4': + resolution: {integrity: sha512-1roLdgMbFU3Nr4MC97/te7w6OqxsWBkDUkpbCcvxF3jz/ku91WVaJldn/PKU8feMKNyI5W9wnqhbjb1BqbExOQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/gateway@1.0.6': + resolution: {integrity: sha512-JuSj1MtTr4vw2VBBth4wlbciQnQIV0o1YV9qGLFA+r85nR5H+cJp3jaYE0nprqfzC9rYG8w9c6XGHB3SDKgcgA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/google@2.0.4': + resolution: {integrity: sha512-d8CF3uzabVqxPwcuXLVv5OIq55bM5oKKNNMQacYQMEv3I9W6EYYYeaM9Buo+/yi1IdKsRIPsa9LQO/H9S9x8yQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/openai-compatible@1.0.5': + resolution: {integrity: sha512-4eXN6m1x6gs+PJEqjCi2G5RKAgydCkH1mMqDngc3Cz11lojehGdthQfr696jCT/qQY1UZDOu2PVFWcYPgTFV2A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/openai@2.0.10': + resolution: {integrity: sha512-vnB6Jk2Qb245fajaWjG3q6N0QQy/uej7kZ0QR9xxq09x++3Tx/UPOcgAKMhFFA2fnuGpkFSRKoiDCyp/E3RARQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/provider-utils@3.0.1': + resolution: {integrity: sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/provider-utils@3.0.3': + resolution: {integrity: sha512-kAxIw1nYmFW1g5TvE54ZB3eNtgZna0RnLjPUp1ltz1+t9xkXJIuDT4atrwfau9IbS0BOef38wqrI8CjFfQrxhw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/provider@2.0.0': + resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} + engines: {node: '>=18'} + + '@ai-sdk/react@2.0.13': + resolution: {integrity: sha512-jlxIwmneDv1LZpNuWI2L3gn8Frb+os11JNZqwhlhRLHj4rwodg+53VLFsh65VNDbhGm/TU8g+Q165apG1yREIQ==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.25.76 || ^4 + peerDependenciesMeta: + zod: + optional: true + + '@ai-sdk/xai@2.0.5': + resolution: {integrity: sha512-YqhReQdh5dbCrNpg4xKVVaZe3LlUZ+c0n/RhgjC2X7AfKfJlGR+yfHsT9LJsbu3PhSzOQyGWeAvGqrCJSMetqQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1099,6 +1206,10 @@ packages: resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} engines: {node: '>= 10.0.0'} + '@modelcontextprotocol/sdk@1.17.2': + resolution: {integrity: sha512-EFLRNXR/ixpXQWu6/3Cu30ndDFIFNaqUXcTqsGebujeMan9FzhAaFFswLRiFj61rgygDRr8WO1N+UijjgRxX9g==} + engines: {node: '>=18'} + '@mswjs/interceptors@0.39.6': resolution: {integrity: sha512-bndDP83naYYkfayr/qhBHMhk0YGwS1iv6vaEGcr0SQbO0IZtbOPqjKjds/WcG+bJA+1T5vCx6kprKOzn5Bg+Vw==} engines: {node: '>=18'} @@ -1200,6 +1311,13 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@openrouter/ai-sdk-provider@1.1.2': + resolution: {integrity: sha512-cfiKVpNygGFaJojBHFvtTf7UiF458Xh9yPcTg4FXF7bGYN5V33Rxx9dXNE12fjv6lHeC5C7jwQHDrzUIFol1iQ==} + engines: {node: '>=18'} + peerDependencies: + ai: ^5.0.0 + zod: ^3.24.1 || ^v4 + '@opentelemetry/api-logs@0.57.2': resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} engines: {node: '>=14'} @@ -1503,6 +1621,9 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-arrow@1.1.7': resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} peerDependencies: @@ -1529,6 +1650,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -1730,6 +1864,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: @@ -2173,6 +2320,9 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@swc/core-darwin-arm64@1.13.3': resolution: {integrity: sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==} engines: {node: '>=10'} @@ -2505,18 +2655,27 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/electron-squirrel-startup@1.0.2': resolution: {integrity: sha512-AzxnvBzNh8K/0SmxMmZtpJf1/IWoGXLP+pQDuUaVkPyotI8ryvAtBSqgxR/qOSvxWHYWrxkeNsJ+Ca5xOuUxJQ==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -2526,6 +2685,12 @@ packages: '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} @@ -2564,6 +2729,12 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/unzipper@0.10.11': resolution: {integrity: sha512-D25im2zjyMCcgL9ag6N46+wbtJBnXIr7SI4zHf9eJD2Dw2tEB5e+p5MYkrxKIVRscs5QV0EhtU9rgXSPx90oJg==} @@ -2629,6 +2800,9 @@ packages: resolution: {integrity: sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react-swc@4.0.1': resolution: {integrity: sha512-NQhPjysi5duItyrMd5JWZFf2vNOuSMyw+EoZyTBDzk+DkfYD8WNrsUs09sELV2cr1P15nufsN25hsUBt4CKF9Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2676,6 +2850,10 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -2716,6 +2894,18 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ai@5.0.10: + resolution: {integrity: sha512-oPvaifsnHZzT3I07qI9jgWDOGpXDAFSXJ54rgpeHSq6qKQlQ3vwaCgQz861wb+5iJ/kk+B/qm3i5Yfghc/+XSw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + ai@5.0.13: + resolution: {integrity: sha512-b/UBzDXilDtsyUlf0D3/R1oyScnoMyWKKO4BHeUJD1UQ04V1rNPu27ZD7v9Em3WqvMOkWFdPYA/kCwMcF1G93A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -2845,6 +3035,9 @@ packages: babel-dead-code-elimination@1.0.10: resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2867,6 +3060,10 @@ packages: bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + boolean@3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -2910,6 +3107,10 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + c12@2.0.1: resolution: {integrity: sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==} peerDependencies: @@ -2956,6 +3157,9 @@ packages: caniuse-lite@1.0.30001735: resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.2.0: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} engines: {node: '>=12'} @@ -2968,6 +3172,18 @@ packages: resolution: {integrity: sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -3081,6 +3297,9 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -3123,12 +3342,24 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -3136,6 +3367,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -3180,6 +3415,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debounce-fn@6.0.0: resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==} engines: {node: '>=18'} @@ -3204,6 +3442,9 @@ packages: decimal.js@10.5.0: resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -3245,6 +3486,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} @@ -3265,6 +3510,9 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -3311,6 +3559,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-installer-common@0.10.4: resolution: {integrity: sha512-8gMNPXfAqUE5CfXg8RL0vXpLE9HAaPkgLXVoHE3BMUzogMWenf4LmwQ27BdCUrEhkjrKl+igs2IHJibclR3z3Q==} engines: {node: '>= 10.0.0'} @@ -3370,6 +3621,10 @@ packages: encode-utf8@1.0.3: resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} @@ -3441,6 +3696,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -3449,6 +3707,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-plugin-react-hooks@5.2.0: resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} engines: {node: '>=10'} @@ -3503,6 +3765,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -3510,9 +3775,21 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + eventsource-parser@3.0.3: + resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} + engines: {node: '>=20.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@1.0.0: resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} engines: {node: '>=6'} @@ -3524,6 +3801,19 @@ packages: exponential-backoff@3.1.2: resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -3578,6 +3868,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + find-up@2.1.0: resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} engines: {node: '>=4'} @@ -3616,9 +3910,17 @@ packages: forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -3846,6 +4148,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} @@ -3859,9 +4167,16 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} @@ -3948,6 +4263,9 @@ packages: resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} engines: {node: '>=10'} + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -3960,6 +4278,16 @@ packages: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -4002,6 +4330,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4035,6 +4366,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -4072,9 +4406,16 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} @@ -4213,6 +4554,9 @@ packages: json-schema-typed@8.0.1: resolution: {integrity: sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -4371,6 +4715,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loupe@3.1.4: resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} @@ -4426,6 +4773,9 @@ packages: resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} engines: {node: '>=6'} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + matcher@3.0.0: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} @@ -4434,14 +4784,151 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + mem@4.3.0: resolution: {integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==} engines: {node: '>=6'} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -4450,10 +4937,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -4603,6 +5098,10 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -4666,6 +5165,10 @@ packages: engines: {node: ^14.16.0 || >=16.10.0} hasBin: true + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -4681,6 +5184,10 @@ packages: ohash@1.1.6: resolution: {integrity: sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -4768,6 +5275,9 @@ packages: parse-color@1.0.0: resolution: {integrity: sha512-fuDHYgFHJGbpGMgw9skY/bj3HL/Jrn4l/5rSspy00DoT4RyLnDcRvPxdZ+r6OFwIsgAuhDh4I09tAId4mI12bw==} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@2.2.0: resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==} engines: {node: '>=0.10.0'} @@ -4775,6 +5285,10 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -4805,6 +5319,10 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + path-type@2.0.0: resolution: {integrity: sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==} engines: {node: '>=4'} @@ -4864,6 +5382,10 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + pkce-challenge@5.0.0: + resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -5033,6 +5555,13 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -5046,6 +5575,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -5059,6 +5592,14 @@ packages: random-path@0.1.2: resolution: {integrity: sha512-4jY0yoEaQ5v9StCl5kZbNIQlg1QheIDBrdkDn53EynpPb9FgO6//p3X/tgMnrC45XN6QZCzU1Xz/+pSSsJBpRw==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -5076,6 +5617,12 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -5161,6 +5708,18 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} @@ -5244,6 +5803,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -5298,6 +5861,10 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + serialize-error@7.0.1: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} @@ -5312,6 +5879,10 @@ packages: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -5324,6 +5895,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.33.5: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -5424,6 +5998,9 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -5446,6 +6023,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -5498,6 +6079,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -5536,6 +6120,12 @@ packages: stubborn-fs@1.2.5: resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} + style-to-js@1.1.17: + resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} + + style-to-object@1.0.9: + resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} + sudo-prompt@9.2.1: resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -5552,6 +6142,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swr@2.3.6: + resolution: {integrity: sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -5581,6 +6176,10 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + tiny-each-async@2.0.3: resolution: {integrity: sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA==} @@ -5637,6 +6236,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -5652,10 +6255,16 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + trim-repeated@1.0.0: resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} engines: {node: '>=0.10.0'} + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -5707,6 +6316,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -5754,6 +6367,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unique-filename@2.0.1: resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -5762,6 +6378,21 @@ packages: resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} @@ -5781,7 +6412,11 @@ packages: resolution: {integrity: sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==} engines: {node: '>= 0.4.0'} - unplugin@1.0.1: + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin@1.0.1: resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} unplugin@2.3.5: @@ -5844,6 +6479,16 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -6102,6 +6747,11 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + zod-validation-error@3.5.3: resolution: {integrity: sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==} engines: {node: '>=18.0.0'} @@ -6117,10 +6767,86 @@ packages: zod@4.0.17: resolution: {integrity: sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@adobe/css-tools@4.4.3': {} + '@ai-sdk/anthropic@2.0.1(zod@4.0.17)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.1(zod@4.0.17) + zod: 4.0.17 + + '@ai-sdk/gateway@1.0.4(zod@4.0.17)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.1(zod@4.0.17) + zod: 4.0.17 + + '@ai-sdk/gateway@1.0.6(zod@4.0.17)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.3(zod@4.0.17) + zod: 4.0.17 + + '@ai-sdk/google@2.0.4(zod@4.0.17)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.1(zod@4.0.17) + zod: 4.0.17 + + '@ai-sdk/openai-compatible@1.0.5(zod@4.0.17)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.1(zod@4.0.17) + zod: 4.0.17 + + '@ai-sdk/openai@2.0.10(zod@4.0.17)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.1(zod@4.0.17) + zod: 4.0.17 + + '@ai-sdk/provider-utils@3.0.1(zod@4.0.17)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.3 + zod: 4.0.17 + zod-to-json-schema: 3.24.6(zod@4.0.17) + + '@ai-sdk/provider-utils@3.0.3(zod@4.0.17)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.3 + zod: 4.0.17 + zod-to-json-schema: 3.24.6(zod@4.0.17) + + '@ai-sdk/provider@2.0.0': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@2.0.13(react@19.1.1)(zod@4.0.17)': + dependencies: + '@ai-sdk/provider-utils': 3.0.3(zod@4.0.17) + ai: 5.0.13(zod@4.0.17) + react: 19.1.1 + swr: 2.3.6(react@19.1.1) + throttleit: 2.1.0 + optionalDependencies: + zod: 4.0.17 + + '@ai-sdk/xai@2.0.5(zod@4.0.17)': + dependencies: + '@ai-sdk/openai-compatible': 1.0.5(zod@4.0.17) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.1(zod@4.0.17) + zod: 4.0.17 + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -7159,6 +7885,23 @@ snapshots: - supports-color optional: true + '@modelcontextprotocol/sdk@1.17.2': + dependencies: + ajv: 6.12.6 + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.3 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.0 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@mswjs/interceptors@0.39.6': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -7282,6 +8025,11 @@ snapshots: '@open-draft/until@2.1.0': {} + '@openrouter/ai-sdk-provider@1.1.2(ai@5.0.10(zod@4.0.17))(zod@4.0.17)': + dependencies: + ai: 5.0.10(zod@4.0.17) + zod: 4.0.17 + '@opentelemetry/api-logs@0.57.2': dependencies: '@opentelemetry/api': 1.9.0 @@ -7601,6 +8349,8 @@ snapshots: '@radix-ui/primitive@1.1.2': {} + '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -7626,6 +8376,22 @@ snapshots: '@types/react': 19.1.10 '@types/react-dom': 19.1.7(@types/react@19.1.10) + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.10)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.10 + '@types/react-dom': 19.1.7(@types/react@19.1.10) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.10)(react@19.1.1) @@ -7848,6 +8614,16 @@ snapshots: '@types/react': 19.1.10 '@types/react-dom': 19.1.7(@types/react@19.1.10) + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.10)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.10 + '@types/react-dom': 19.1.7(@types/react@19.1.10) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@19.1.10)(react@19.1.1) @@ -8293,6 +9069,8 @@ snapshots: '@sindresorhus/is@4.6.0': {} + '@standard-schema/spec@1.0.0': {} + '@swc/core-darwin-arm64@1.13.3': optional: true @@ -8619,10 +9397,18 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} '@types/electron-squirrel-startup@1.0.2': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} '@types/fs-extra@9.0.13': @@ -8630,6 +9416,10 @@ snapshots: '@types/node': 22.17.2 optional: true + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/http-cache-semantics@4.0.4': {} '@types/json-schema@7.0.15': {} @@ -8638,6 +9428,12 @@ snapshots: dependencies: '@types/node': 22.17.2 + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/mysql@2.15.26': dependencies: '@types/node': 22.17.2 @@ -8680,6 +9476,10 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/unzipper@0.10.11': dependencies: '@types/node': 22.17.2 @@ -8782,6 +9582,8 @@ snapshots: '@typescript-eslint/types': 8.39.1 eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} + '@vitejs/plugin-react-swc@4.0.1(vite@7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.32 @@ -8790,7 +9592,7 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1))': + '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@istanbuljs/schema': 0.1.3 debug: 4.4.1 @@ -8802,7 +9604,7 @@ snapshots: magicast: 0.3.5 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -8853,6 +9655,11 @@ snapshots: abbrev@1.1.1: {} + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -8886,6 +9693,22 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ai@5.0.10(zod@4.0.17): + dependencies: + '@ai-sdk/gateway': 1.0.4(zod@4.0.17) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.1(zod@4.0.17) + '@opentelemetry/api': 1.9.0 + zod: 4.0.17 + + ai@5.0.13(zod@4.0.17): + dependencies: + '@ai-sdk/gateway': 1.0.6(zod@4.0.17) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.3(zod@4.0.17) + '@opentelemetry/api': 1.9.0 + zod: 4.0.17 + ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -9024,6 +9847,8 @@ snapshots: transitivePeerDependencies: - supports-color + bail@2.0.2: {} + balanced-match@1.0.2: {} base32-encode@1.2.0: @@ -9045,6 +9870,20 @@ snapshots: bluebird@3.7.2: {} + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.1 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolean@3.2.0: optional: true @@ -9095,6 +9934,8 @@ snapshots: dependencies: run-applescript: 7.0.0 + bytes@3.1.2: {} + c12@2.0.1(magicast@0.3.5): dependencies: chokidar: 4.0.3 @@ -9172,6 +10013,8 @@ snapshots: caniuse-lite@1.0.30001735: {} + ccount@2.0.1: {} + chai@5.2.0: dependencies: assertion-error: 2.0.1 @@ -9187,6 +10030,14 @@ snapshots: chalk@5.5.0: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + check-error@2.1.1: {} chokidar@3.6.0: @@ -9305,6 +10156,8 @@ snapshots: colorette@2.0.20: {} + comma-separated-tokens@2.0.3: {} + commander@11.1.0: {} commander@12.1.0: {} @@ -9337,14 +10190,27 @@ snapshots: consola@3.4.2: {} + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} cookie-es@1.2.2: {} + cookie-signature@1.2.2: {} + cookie@0.7.2: {} core-util-is@1.0.3: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + create-require@1.1.1: {} cross-dirname@0.1.0: {} @@ -9397,6 +10263,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + date-fns@4.1.0: {} + debounce-fn@6.0.0: dependencies: mimic-function: 5.0.1 @@ -9411,6 +10279,10 @@ snapshots: decimal.js@10.5.0: {} + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -9448,6 +10320,8 @@ snapshots: defu@6.1.4: {} + depd@2.0.0: {} + deprecation@2.3.1: {} dequal@2.0.3: {} @@ -9461,6 +10335,10 @@ snapshots: detect-node@2.1.0: optional: true + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff@4.0.2: {} diff@5.1.0: {} @@ -9503,6 +10381,8 @@ snapshots: eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + electron-installer-common@0.10.4: dependencies: '@electron/asar': 3.4.1 @@ -9605,6 +10485,8 @@ snapshots: encode-utf8@1.0.3: optional: true + encodeurl@2.0.0: {} + encoding@0.1.13: dependencies: iconv-lite: 0.6.3 @@ -9746,10 +10628,14 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-plugin-react-hooks@5.2.0(eslint@9.33.0(jiti@2.5.1)): dependencies: eslint: 9.33.0(jiti@2.5.1) @@ -9827,14 +10713,24 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 esutils@2.0.3: {} + etag@1.8.1: {} + eventemitter3@5.0.1: {} + eventsource-parser@3.0.3: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.3 + execa@1.0.0: dependencies: cross-spawn: 6.0.6 @@ -9849,6 +10745,44 @@ snapshots: exponential-backoff@3.1.2: {} + express-rate-limit@7.5.1(express@5.1.0): + dependencies: + express: 5.1.0 + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + extract-zip@2.0.1: dependencies: debug: 4.4.1 @@ -9907,6 +10841,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@2.1.0: dependencies: locate-path: 2.0.0 @@ -9950,8 +10895,12 @@ snapshots: forwarded-parse@2.1.2: {} + forwarded@0.2.0: {} + fraction.js@4.3.7: {} + fresh@2.0.0: {} + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -10238,6 +11187,30 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.17 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + headers-polyfill@4.0.3: {} hosted-git-info@2.8.9: {} @@ -10248,8 +11221,18 @@ snapshots: html-escaper@2.0.2: {} + html-url-attributes@3.0.1: {} + http-cache-semantics@4.2.0: {} + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + http-proxy-agent@5.0.0: dependencies: '@tootallnate/once': 2.0.0 @@ -10339,6 +11322,8 @@ snapshots: ini@2.0.0: {} + inline-style-parser@0.2.4: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -10349,6 +11334,15 @@ snapshots: ip-address@10.0.1: {} + ipaddr.js@1.9.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -10397,6 +11391,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -10424,6 +11420,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -10457,8 +11455,12 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + is-property@1.0.2: optional: true @@ -10607,6 +11609,8 @@ snapshots: json-schema-typed@8.0.1: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-safe@5.0.1: @@ -10785,6 +11789,8 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 + longest-streak@3.1.0: {} + loupe@3.1.4: {} lowercase-keys@2.0.0: {} @@ -10854,6 +11860,8 @@ snapshots: dependencies: p-defer: 1.0.0 + markdown-table@3.0.4: {} + matcher@3.0.0: dependencies: escape-string-regexp: 4.0.0 @@ -10861,14 +11869,362 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + media-typer@1.1.0: {} + mem@4.3.0: dependencies: map-age-cleaner: 0.1.3 mimic-fn: 2.1.0 p-is-promise: 2.1.0 + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.1 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -10876,10 +12232,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + mimic-fn@2.1.0: {} mimic-function@5.0.1: {} @@ -11020,6 +12382,8 @@ snapshots: negotiator@0.6.4: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} nice-try@1.0.5: {} @@ -11076,6 +12440,8 @@ snapshots: tinyexec: 0.3.2 ufo: 1.6.1 + object-assign@4.1.1: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -11091,6 +12457,10 @@ snapshots: ohash@1.1.6: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -11208,6 +12578,16 @@ snapshots: color-convert: 0.5.3 optional: true + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@2.2.0: dependencies: error-ex: 1.3.2 @@ -11216,6 +12596,8 @@ snapshots: dependencies: entities: 6.0.0 + parseurl@1.3.3: {} + path-exists@3.0.0: {} path-exists@4.0.0: {} @@ -11235,6 +12617,8 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.2.0: {} + path-type@2.0.0: dependencies: pify: 2.3.0 @@ -11275,6 +12659,8 @@ snapshots: pify@2.3.0: {} + pkce-challenge@5.0.0: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -11358,6 +12744,13 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + property-information@7.1.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} psl@1.15.0: @@ -11371,6 +12764,10 @@ snapshots: punycode@2.3.1: {} + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -11383,6 +12780,15 @@ snapshots: murmur-32: 0.2.0 optional: true + range-parser@1.2.1: {} + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + rc9@2.1.2: dependencies: defu: 6.1.4 @@ -11399,6 +12805,24 @@ snapshots: react-is@17.0.2: {} + react-markdown@10.1.0(@types/react@19.1.10)(react@19.1.1): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.1.10 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 19.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-remove-scroll-bar@2.3.8(@types/react@19.1.10)(react@19.1.1): dependencies: react: 19.1.1 @@ -11513,6 +12937,40 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + repeat-string@1.6.1: optional: true @@ -11616,6 +13074,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.46.2 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + rrweb-cssom@0.8.0: {} run-applescript@7.0.0: {} @@ -11664,6 +13132,22 @@ snapshots: semver@7.7.2: {} + send@1.2.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serialize-error@7.0.1: dependencies: type-fest: 0.13.1 @@ -11675,6 +13159,15 @@ snapshots: seroval@1.3.2: {} + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -11697,6 +13190,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setprototypeof@1.2.0: {} + sharp@0.33.5: dependencies: color: 4.2.3 @@ -11824,6 +13319,8 @@ snapshots: source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -11847,6 +13344,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + statuses@2.0.2: {} std-env@3.9.0: {} @@ -11912,6 +13411,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -11942,6 +13446,14 @@ snapshots: stubborn-fs@1.2.5: {} + style-to-js@1.1.17: + dependencies: + style-to-object: 1.0.9 + + style-to-object@1.0.9: + dependencies: + inline-style-parser: 0.2.4 + sudo-prompt@9.2.1: {} sumchecker@3.0.1: @@ -11956,6 +13468,12 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swr@2.3.6(react@19.1.1): + dependencies: + dequal: 2.0.3 + react: 19.1.1 + use-sync-external-store: 1.5.0(react@19.1.1) + symbol-tree@3.2.4: {} tailwind-merge@3.3.1: {} @@ -11994,6 +13512,8 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 + throttleit@2.1.0: {} + tiny-each-async@2.0.3: optional: true @@ -12042,6 +13562,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tough-cookie@4.1.4: dependencies: psl: 1.15.0 @@ -12059,10 +13581,14 @@ snapshots: dependencies: punycode: 2.3.1 + trim-lines@3.0.1: {} + trim-repeated@1.0.0: dependencies: escape-string-regexp: 1.0.5 + trough@2.2.0: {} + ts-api-utils@2.1.0(typescript@5.9.2): dependencies: typescript: 5.9.2 @@ -12111,6 +13637,12 @@ snapshots: type-fest@4.41.0: {} + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -12173,6 +13705,16 @@ snapshots: undici-types@6.21.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + unique-filename@2.0.1: dependencies: unique-slug: 3.0.0 @@ -12181,6 +13723,29 @@ snapshots: dependencies: imurmurhash: 0.1.4 + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + universal-user-agent@6.0.1: {} universalify@0.1.2: {} @@ -12192,6 +13757,8 @@ snapshots: unorm@1.6.0: optional: true + unpipe@1.0.0: {} + unplugin@1.0.1: dependencies: acorn: 8.15.0 @@ -12272,6 +13839,18 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vary@1.1.2: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-node@3.2.4(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -12309,13 +13888,13 @@ snapshots: tsx: 4.20.3 yaml: 2.8.1 - vitest-fail-on-console@0.9.0(vite@7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1)): + vitest-fail-on-console@0.9.0(vite@7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1)): dependencies: chalk: 5.5.0 vite: 7.1.2(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1) - vitest: 3.2.4(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1) - vitest@3.2.4(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -12341,6 +13920,7 @@ snapshots: vite-node: 3.2.4(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: + '@types/debug': 4.1.12 '@types/node': 22.17.2 jsdom: 26.1.0 transitivePeerDependencies: @@ -12536,6 +14116,14 @@ snapshots: yoctocolors-cjs@2.1.2: {} + zod-to-json-schema@3.24.6(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-to-json-schema@3.24.6(zod@4.0.17): + dependencies: + zod: 4.0.17 + zod-validation-error@3.5.3(zod@3.25.76): dependencies: zod: 3.25.76 @@ -12545,3 +14133,5 @@ snapshots: zod@3.25.76: {} zod@4.0.17: {} + + zwitch@2.0.4: {} diff --git a/preload/src/preload.ts b/preload/src/preload.ts index e643d18d..31952163 100644 --- a/preload/src/preload.ts +++ b/preload/src/preload.ts @@ -116,6 +116,56 @@ contextBridge.exposeInMainWorld('electronAPI', { disable: (key: string) => ipcRenderer.invoke('feature-flags:disable', key), getAll: () => ipcRenderer.invoke('feature-flags:get-all'), }, + + // Chat functionality + chat: { + getProviders: () => ipcRenderer.invoke('chat:get-providers'), + stream: (request: { + messages: Array<{ + id: string + role: string + parts: Array<{ type: string; text: string }> + }> + provider: string + model: string + apiKey: string + enabledTools?: string[] + }) => + ipcRenderer.invoke('chat:stream', request) as Promise<{ + streamId: string + }>, + getSettings: (providerId: string) => + ipcRenderer.invoke('chat:get-settings', providerId), + saveSettings: ( + providerId: string, + settings: { apiKey: string; enabledTools: string[] } + ) => ipcRenderer.invoke('chat:save-settings', providerId, settings), + clearSettings: (providerId?: string) => + ipcRenderer.invoke('chat:clear-settings', providerId), + discoverModels: () => ipcRenderer.invoke('chat:discover-models'), + getSelectedModel: () => ipcRenderer.invoke('chat:get-selected-model'), + saveSelectedModel: (provider: string, model: string) => + ipcRenderer.invoke('chat:save-selected-model', provider, model), + getMcpServerTools: (serverName: string) => + ipcRenderer.invoke('chat:get-mcp-server-tools', serverName), + getEnabledMcpTools: () => ipcRenderer.invoke('chat:get-enabled-mcp-tools'), + getEnabledMcpServersFromTools: () => + ipcRenderer.invoke('chat:get-enabled-mcp-servers-from-tools'), + saveEnabledMcpTools: (serverName: string, enabledTools: string[]) => + ipcRenderer.invoke( + 'chat:save-enabled-mcp-tools', + serverName, + enabledTools + ), + }, + + // IPC event listeners for streaming + on: (channel: string, listener: (...args: unknown[]) => void) => { + ipcRenderer.on(channel, (_, ...args) => listener(...args)) + }, + removeListener: (channel: string, listener: (...args: unknown[]) => void) => { + ipcRenderer.removeListener(channel, listener) + }, }) export interface ElectronAPI { @@ -185,4 +235,73 @@ export interface ElectronAPI { // File/folder pickers selectFile: () => Promise selectFolder: () => Promise + // chat + chat: { + getProviders: () => Promise< + Array<{ id: string; name: string; models: string[] }> + > + stream: (request: { + messages: Array<{ + id: string + role: string + parts: Array<{ type: string; text: string }> + }> + provider: string + model: string + apiKey: string + enabledTools?: string[] + }) => Promise<{ streamId: string }> + getSettings: ( + providerId: string + ) => Promise<{ apiKey: string; enabledTools: string[] }> + saveSettings: ( + providerId: string, + settings: { apiKey: string; enabledTools: string[] } + ) => Promise<{ success: boolean; error?: string }> + clearSettings: ( + providerId?: string + ) => Promise<{ success: boolean; error?: string }> + discoverModels: () => Promise<{ + providers: Array<{ + id: string + name: string + models: Array<{ + id: string + supportsTools: boolean + category?: string + experimental?: boolean + }> + }> + discoveredAt: string + }> + getSelectedModel: () => Promise<{ provider: string; model: string }> + saveSelectedModel: ( + provider: string, + model: string + ) => Promise<{ success: boolean; error?: string }> + getMcpServerTools: (serverName: string) => Promise<{ + serverName: string + serverPackage?: string + tools: Array<{ + name: string + description?: string + parameters?: Record + enabled: boolean + }> + isRunning: boolean + } | null> + getEnabledMcpTools: () => Promise> + getEnabledMcpServersFromTools: () => Promise + saveEnabledMcpTools: ( + serverName: string, + enabledTools: string[] + ) => Promise<{ success: boolean; error?: string }> + } + + // IPC event listeners for streaming + on: (channel: string, listener: (...args: unknown[]) => void) => void + removeListener: ( + channel: string, + listener: (...args: unknown[]) => void + ) => void } diff --git a/renderer/src/common/components/layout/top-nav/index.tsx b/renderer/src/common/components/layout/top-nav/index.tsx index cf574185..3614314d 100644 --- a/renderer/src/common/components/layout/top-nav/index.tsx +++ b/renderer/src/common/components/layout/top-nav/index.tsx @@ -15,8 +15,11 @@ import { Separator } from '../../ui/separator' import { useConfirmQuit } from '@/common/hooks/use-confirm-quit' import { QuitConfirmationListener } from './quit-confirmation-listener' import { SettingsIcon } from 'lucide-react' +import { useFeatureFlag } from '@/common/hooks/use-feature-flag' function TopNavLinks() { + const isPlaygroundEnabled = useFeatureFlag('playground') + return ( @@ -97,6 +100,35 @@ function TopNavLinks() { Clients + {isPlaygroundEnabled && ( + + + + Playground + + + + )} +} + +function CollapsibleTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CollapsibleContent({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/renderer/src/common/hooks/use-feature-flag.ts b/renderer/src/common/hooks/use-feature-flag.ts new file mode 100644 index 00000000..59cb9ea6 --- /dev/null +++ b/renderer/src/common/hooks/use-feature-flag.ts @@ -0,0 +1,20 @@ +import { useQuery } from '@tanstack/react-query' +import type { FeatureFlagKey } from '../lib/feature-flags' + +export function useFeatureFlag(flagKey: FeatureFlagKey): boolean { + const { data } = useQuery({ + queryKey: ['featureFlag', flagKey], + queryFn: async () => { + try { + return await window.electronAPI.featureFlags.get(flagKey) + } catch (error) { + console.error(`Failed to get feature flag ${flagKey}:`, error) + return false + } + }, + staleTime: 5 * 60 * 1000, // 5 minutes + refetchOnWindowFocus: false, + }) + + return data ?? false +} diff --git a/renderer/src/common/lib/feature-flags.ts b/renderer/src/common/lib/feature-flags.ts index aae85212..09731c6d 100644 --- a/renderer/src/common/lib/feature-flags.ts +++ b/renderer/src/common/lib/feature-flags.ts @@ -1,6 +1,8 @@ import { featureFlagKeys } from '../../../../utils/feature-flags' +import { queryClient } from './query-client' -type FeatureFlagKey = (typeof featureFlagKeys)[keyof typeof featureFlagKeys] +export type FeatureFlagKey = + (typeof featureFlagKeys)[keyof typeof featureFlagKeys] const getFeatureFlag = (key: FeatureFlagKey) => async (): Promise => { try { @@ -16,6 +18,9 @@ const getCreateFeatureFlag = (key: FeatureFlagKey) => async (): Promise => { try { await window.electronAPI.featureFlags.enable(key) + // Update React Query cache + queryClient.setQueryData(['featureFlag', key], true) + queryClient.invalidateQueries({ queryKey: ['featureFlag', key] }) } catch (error) { console.error(`Failed to enable feature flag ${key}:`, error) } @@ -25,6 +30,9 @@ const getDeleteFeatureFlag = (key: FeatureFlagKey) => async (): Promise => { try { await window.electronAPI.featureFlags.disable(key) + // Update React Query cache + queryClient.setQueryData(['featureFlag', key], false) + queryClient.invalidateQueries({ queryKey: ['featureFlag', key] }) } catch (error) { console.error(`Failed to disable feature flag ${key}:`, error) } diff --git a/renderer/src/common/lib/query-client.ts b/renderer/src/common/lib/query-client.ts new file mode 100644 index 00000000..2252401d --- /dev/null +++ b/renderer/src/common/lib/query-client.ts @@ -0,0 +1,3 @@ +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({}) diff --git a/renderer/src/features/chat/components/chat-input.tsx b/renderer/src/features/chat/components/chat-input.tsx new file mode 100644 index 00000000..2449766f --- /dev/null +++ b/renderer/src/features/chat/components/chat-input.tsx @@ -0,0 +1,107 @@ +import { useState, type KeyboardEvent } from 'react' +import { Button } from '@/common/components/ui/button' +import { Input } from '@/common/components/ui/input' +import { Send, Square } from 'lucide-react' + +interface ChatInputProps { + onSendMessage: (message: string) => void + onStopGeneration: () => void + isLoading: boolean + disabled: boolean + placeholder?: string + selectedModel?: string +} + +export function ChatInput({ + onSendMessage, + onStopGeneration, + isLoading, + disabled, + placeholder = 'Type your message...', + selectedModel, +}: ChatInputProps) { + const [input, setInput] = useState('') + + const handleSend = () => { + if (input.trim() && !isLoading && !disabled) { + onSendMessage(input.trim()) + setInput('') + } + } + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + handleSend() + } + } + + const handleStop = () => { + onStopGeneration() + } + + const getPlaceholder = () => { + if (disabled) return 'Select an AI model to get started' + if (selectedModel) { + const modelName = selectedModel.includes('claude') + ? 'Claude' + : selectedModel.includes('gpt') + ? 'ChatGPT' + : selectedModel.includes('gemini') + ? 'Gemini' + : selectedModel.includes('grok') + ? 'Grok' + : 'AI' + return `Message ${modelName}...` + } + return placeholder + } + + return ( +
+
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={getPlaceholder()} + disabled={disabled} + className="placeholder:text-muted-foreground min-h-[60px] resize-none + border-0 bg-transparent px-4 py-4 pr-12 text-base + focus-visible:ring-0 focus-visible:ring-offset-0" + /> + + {isLoading ? ( + + ) : ( + + )} +
+ + {/* Helper text */} +
+ {disabled + ? 'Select an AI model to start chatting' + : 'Press Enter to send'} +
+
+ ) +} diff --git a/renderer/src/features/chat/components/chat-interface.tsx b/renderer/src/features/chat/components/chat-interface.tsx new file mode 100644 index 00000000..f8579442 --- /dev/null +++ b/renderer/src/features/chat/components/chat-interface.tsx @@ -0,0 +1,214 @@ +import { useRef, useEffect, useState, useCallback } from 'react' +import { Button } from '@/common/components/ui/button' + +import { Trash2, MessageSquare } from 'lucide-react' +import { ChatMessage } from './chat-message' +import { ChatInput } from './chat-input' +import { DialogApiKeys } from './dialog-api-keys' +import { McpServerSelector } from './mcp-server-selector' +import { ModelSelector } from './model-selector' +import { ErrorAlert } from './error-alert' + +import { useChatStreaming } from '../hooks/use-chat-streaming' + +export function ChatInterface() { + const { + messages, + isLoading, + error, + settings, + sendMessage, + clearMessages, + updateSettings, + cancelRequest, + loadPersistedSettings, + } = useChatStreaming() + + const messagesEndRef = useRef(null) + const [isSettingsOpen, setIsSettingsOpen] = useState(false) + + const handleApiKeysSaved = useCallback(() => { + // Dispatch event to notify that API keys have changed + window.dispatchEvent(new CustomEvent('api-keys-changed')) + }, []) + + const handleProviderChange = useCallback( + (providerId: string) => { + loadPersistedSettings(providerId, true) // Preserve enabled tools when changing provider + }, + [loadPersistedSettings] + ) + + // Simple smooth scroll to bottom - only when messages change + useEffect(() => { + if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [messages]) + + const hasProviderAndModel = settings.provider && settings.model + const hasMessages = messages.length > 0 + + return ( +
+ {/* Model Selection Bar */} +
+
+
+ setIsSettingsOpen(true)} + onProviderChange={handleProviderChange} + /> + {hasMessages && ( + + )} +
+
+
+ + {/* Messages Area */} +
+ {hasMessages ? ( +
+
+
+ {messages.map((message, index) => ( +
+ +
+ ))} + {isLoading && ( +
+
+ +
+
+
+
+
+
+
+
+ + Thinking... + +
+
+
+ )} +
+
+
+
+ ) : ( +
+
+ {settings.provider && settings.model ? ( +
+ How can I help you today? +
+ ) : ( + <> +
+ Welcome to Chat Playground +
+

+ Select an AI model above to get started +

+ + )} +
+
+ )} +
+ + {/* Error Display */} + + + {/* Input Area */} +
+
+ {/* MCP Tools Selection */} + {hasProviderAndModel && ( +
+ + updateSettings({ ...settings, enabledTools: tools }) + } + /> +
+ )} + + {/* Response Timer */} + {isLoading && ( +
+
+ Generating response... +
+
+ )} + + {/* Chat Input */} + +
+
+ + {/* API Keys Modal */} + +
+ ) +} diff --git a/renderer/src/features/chat/components/chat-message.tsx b/renderer/src/features/chat/components/chat-message.tsx new file mode 100644 index 00000000..30f5a4ab --- /dev/null +++ b/renderer/src/features/chat/components/chat-message.tsx @@ -0,0 +1,586 @@ +import { formatDistanceToNow } from 'date-fns' +import { + User, + Bot, + Wrench, + CheckCircle, + AlertCircle, + ChevronDown, + ChevronRight, + Brain, + Zap, +} from 'lucide-react' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { TokenUsage } from './token-usage' +import { NoContentMessage } from './no-content-message' +import { useState } from 'react' +import type { ChatUIMessage } from '../types' + +interface ChatMessageProps { + message: ChatUIMessage +} + +// Helper function to render reasoning steps +function ReasoningComponent({ part }: { part: ChatUIMessage['parts'][0] }) { + const [isOpen, setIsOpen] = useState(false) + + if (part.type !== 'reasoning') return null + + return ( +
+ {/* Reasoning Header */} +
+ + + AI Reasoning + +
+ + {/* Reasoning Content Toggle */} +
+ + + {isOpen && ( +
+
+ + {'text' in part + ? part.text || 'No reasoning content' + : 'No reasoning content'} + +
+
+ )} +
+
+ ) +} + +// Note: Steps are not a standard AI SDK part type, so we don't need a separate component + +// Helper function to render step start boundaries (AI SDK feature) +function StepStartComponent({ + part, + index, +}: { + part: ChatUIMessage['parts'][0] + index: number +}) { + if (part.type !== 'step-start') return null + + // Show step boundaries as horizontal lines (skip first step) + return index > 0 ? ( +
+
+
+
+ + Step Boundary +
+
+
+ ) : null +} + +// Helper function to render tool calls with comprehensive information +function ToolCallComponent({ part }: { part: ChatUIMessage['parts'][0] }) { + const [isInputOpen, setIsInputOpen] = useState(false) + const [isOutputOpen, setIsOutputOpen] = useState(false) + const [isDetailsOpen, setIsDetailsOpen] = useState(false) + + // Handle AI SDK tool call parts (type starts with 'tool-' or is 'dynamic-tool') + if (!part.type.startsWith('tool-') && part.type !== 'dynamic-tool') + return null + + // Extract tool name from the type or use toolName property for dynamic tools + const toolName = + part.type === 'dynamic-tool' + ? 'toolName' in part + ? String(part.toolName) + : 'Unknown Tool' + : part.type.replace('tool-', '') + + const toolCallId = 'toolCallId' in part ? part.toolCallId : 'unknown' + const hasState = 'state' in part + const state = hasState ? part.state : null + + return ( +
+ {/* Tool Header */} +
+ + + Tool: {toolName} + + + {/* State indicators */} + {state === 'output-available' && ( + + )} + {state === 'output-error' && ( + + )} + {state === 'input-streaming' && ( +
+
+ Streaming... +
+ )} + + {/* Tool Call ID Badge */} + + ID: {toolCallId.slice(-8)} + +
+ + {/* Tool Details Toggle */} +
+ + + {isDetailsOpen && ( +
+
+ Tool Name: {toolName} +
+
+ Call ID:{' '} + {toolCallId} +
+
+ Type: {part.type} +
+ {hasState && ( +
+ State:{' '} + {state} +
+ )} + {'providerExecuted' in part && ( +
+ Provider Executed:{' '} + {part.providerExecuted ? 'Yes' : 'No'} +
+ )} +
+ )} +
+ + {/* Input Parameters */} + {'input' in part && part.input !== undefined && ( +
+ + {isInputOpen && ( +
+
+                {JSON.stringify(part.input, null, 2)}
+              
+
+ )} +
+ )} + + {/* Output Results */} + {'output' in part && part.output !== undefined && ( +
+ + {isOutputOpen && ( +
+
+                {JSON.stringify(part.output, null, 2)}
+              
+
+ )} +
+ )} + + {/* Error Display */} + {state === 'output-error' && ( +
+
+ + Tool Execution Error +
+
+ {'errorText' in part ? part.errorText : 'Tool execution failed'} +
+
+ )} + + {/* Status Summary */} +
+
+ + Status:{' '} + {state || 'Unknown'} + + {'input' in part && + part.input !== undefined && + 'output' in part && + part.output !== undefined && ( + + ✓ Completed + + )} + {state === 'input-streaming' && ( + + ⏳ Processing... + + )} + {state === 'output-error' && ( + ✗ Failed + )} +
+
+
+ ) +} + +export function ChatMessage({ message }: ChatMessageProps) { + const isUser = message.role === 'user' + + if (isUser) { + // User message with bubble styling + return ( +
+
+ {/* Message Content */} +
+
+
+ ( +

{children}

+ ), + strong: ({ children }) => ( + {children} + ), + em: ({ children }) => ( + {children} + ), + code: ({ children }) => ( + + {children} + + ), + }} + > + {message.parts.find((p) => p.type === 'text' && 'text' in p) + ?.text || ''} +
+
+
+ + {/* Timestamp for user messages */} +
+ {formatDistanceToNow( + message.metadata?.createdAt + ? new Date(message.metadata.createdAt) + : new Date(), + { addSuffix: true } + )} +
+
+ + {/* User avatar */} +
+ +
+
+
+ ) + } + + // Assistant message with original styling + return ( +
+ {/* Bot avatar */} +
+ +
+ + {/* Message Content */} +
+ {/* Render message content - simplified for streaming */} +
+ {/* Render all message parts in order */} + {message.parts.map((part, index) => { + switch (part.type) { + case 'step-start': + return ( + + ) + + case 'reasoning': + return ( + + ) + + case 'dynamic-tool': + return ( + + ) + + default: + // Handle all tool-* parts + if (part.type.startsWith('tool-')) { + return + } + return null + } + })} + + {/* Render text content after tools */} + {(() => { + // Combine all text content from text parts + interface TextPart { + type: 'text' + text: string + } + + const allTextContent = message.parts + .filter((p): p is TextPart => p.type === 'text' && 'text' in p) + .map((p) => p.text || '') + .join('') + + if (allTextContent.trim()) { + return ( +
+ ( +

+ {children} +

+ ), + h2: ({ children }) => ( +

+ {children} +

+ ), + h3: ({ children }) => ( +

+ {children} +

+ ), + // List styles - balanced + ul: ({ children }) => ( +
    + {children} +
+ ), + ol: ({ children }) => ( +
    + {children} +
+ ), + li: ({ children }) => ( +
  • + {children} +
  • + ), + // Table styles + table: ({ children }) => ( +
    + + {children} +
    +
    + ), + th: ({ children }) => ( + + {children} + + ), + td: ({ children }) => ( + + {children} + + ), + // Code styles - balanced + code: ({ className, children }) => { + const isInline = !className + if (isInline) { + return ( + + {children} + + ) + } + return ( + + {children} + + ) + }, + pre: ({ children }) => ( +
    +                          {children}
    +                        
    + ), + // Link styles + a: ({ href, children }) => ( + + {children} + + ), + // Text styles - balanced + p: ({ children }) => ( +

    + {children} +

    + ), + strong: ({ children }) => ( + + {children} + + ), + em: ({ children }) => ( + + {children} + + ), + blockquote: ({ children }) => ( +
    + {children} +
    + ), + }} + > + {allTextContent} +
    +
    + ) + } + return null + })()} + + {/* Show message if no content and stream is finished */} + +
    + + {/* Timestamp and Token Usage */} +
    +
    + {formatDistanceToNow( + message.metadata?.createdAt + ? new Date(message.metadata.createdAt) + : new Date(), + { addSuffix: true } + )} + {message.metadata?.model && ( + + • {message.metadata.model} + + )} +
    + + {/* Show token usage for assistant messages */} + {message.role === 'assistant' && message.metadata?.totalUsage && ( + + )} +
    +
    +
    + ) +} diff --git a/renderer/src/features/chat/components/dialog-api-keys.tsx b/renderer/src/features/chat/components/dialog-api-keys.tsx new file mode 100644 index 00000000..2d5797ce --- /dev/null +++ b/renderer/src/features/chat/components/dialog-api-keys.tsx @@ -0,0 +1,310 @@ +import { useState, useEffect } from 'react' +import { Button } from '@/common/components/ui/button' +import { Input } from '@/common/components/ui/input' +import { Label } from '@/common/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/common/components/ui/dialog' +import { Badge } from '@/common/components/ui/badge' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/common/components/ui/collapsible' +import { + Eye, + EyeOff, + Key, + Check, + AlertCircle, + Trash2, + ChevronDown, + ChevronRight, +} from 'lucide-react' +import { getProviderIcon } from './provider-icons' +import type { ChatProvider } from '../types' + +interface DialogApiKeysProps { + isOpen: boolean + onOpenChange: (open: boolean) => void + onSaved?: () => void +} + +interface ProviderApiKey { + provider: ChatProvider + apiKey: string + hasKey: boolean +} + +export function DialogApiKeys({ + isOpen, + onOpenChange, + onSaved, +}: DialogApiKeysProps) { + const [providerKeys, setProviderKeys] = useState([]) + const [showApiKeys, setShowApiKeys] = useState>({}) + const [expandedProviders, setExpandedProviders] = useState< + Record + >({}) + const [saving, setSaving] = useState(false) + + useEffect(() => { + if (isOpen) { + loadProvidersAndKeys() + } + }, [isOpen]) + + const loadProvidersAndKeys = async () => { + try { + const allProviders = await window.electronAPI.chat.getProviders() + + // Load existing API keys for each provider + const keysData = await Promise.all( + allProviders.map(async (provider) => { + try { + const settings = await window.electronAPI.chat.getSettings( + provider.id + ) + return { + provider, + apiKey: settings.apiKey || '', + hasKey: Boolean(settings.apiKey), + } + } catch { + return { + provider, + apiKey: '', + hasKey: false, + } + } + }) + ) + + setProviderKeys(keysData) + + // Start with all providers collapsed + const expandedState: Record = {} + keysData.forEach((pk) => { + expandedState[pk.provider.id] = false + }) + setExpandedProviders(expandedState) + } catch (error) { + console.error('Failed to load providers and keys:', error) + } + } + + const handleApiKeyChange = (providerId: string, apiKey: string) => { + setProviderKeys((prev) => + prev.map((pk) => + pk.provider.id === providerId + ? { ...pk, apiKey, hasKey: Boolean(apiKey) } + : pk + ) + ) + } + + const toggleShowApiKey = (providerId: string) => { + setShowApiKeys((prev) => ({ + ...prev, + [providerId]: !prev[providerId], + })) + } + + const handleRemoveApiKey = (providerId: string) => { + setProviderKeys((prev) => + prev.map((pk) => + pk.provider.id === providerId + ? { ...pk, apiKey: '', hasKey: false } + : pk + ) + ) + } + + const toggleProviderExpanded = (providerId: string) => { + setExpandedProviders((prev) => ({ + ...prev, + [providerId]: !prev[providerId], + })) + } + + const handleSave = async () => { + setSaving(true) + try { + // Save or clear API keys for each provider + await Promise.all( + providerKeys.map(async (pk) => { + if (pk.apiKey.trim()) { + // Save API key + await window.electronAPI.chat.saveSettings(pk.provider.id, { + apiKey: pk.apiKey.trim(), + enabledTools: [], // Keep existing enabled tools or default to empty + }) + } else { + // Clear/remove API key + await window.electronAPI.chat.clearSettings(pk.provider.id) + } + }) + ) + + onSaved?.() + onOpenChange(false) + } catch (error) { + console.error('Failed to save API keys:', error) + } finally { + setSaving(false) + } + } + + return ( + + + + + + Manage API Keys + + + Configure your API keys for different AI providers. Only providers + with API keys will be available for model selection. + + + +
    + {providerKeys.map((pk) => ( + toggleProviderExpanded(pk.provider.id)} + className="border-border overflow-hidden rounded-lg border" + > + + + + + +
    +
    +
    + +
    +
    + + handleApiKeyChange(pk.provider.id, e.target.value) + } + placeholder={`Enter your ${pk.provider.name} API key`} + className="pr-10" + /> + +
    + {pk.hasKey && ( + + )} +
    +
    + + {/* Show sample models */} +
    + Available models:{' '} + {pk.provider.models.slice(0, 3).join(', ')} + {pk.provider.models.length > 3 && + ` +${pk.provider.models.length - 3} more`} +
    +
    +
    +
    +
    + ))} +
    + + + + + +
    +
    + ) +} diff --git a/renderer/src/features/chat/components/error-alert.tsx b/renderer/src/features/chat/components/error-alert.tsx new file mode 100644 index 00000000..71e1d0e3 --- /dev/null +++ b/renderer/src/features/chat/components/error-alert.tsx @@ -0,0 +1,45 @@ +import { useState, useEffect } from 'react' +import { Button } from '@/common/components/ui/button' +import { Alert, AlertDescription } from '@/common/components/ui/alert' +import { X, AlertTriangle } from 'lucide-react' + +interface ErrorAlertProps { + error: string | null + className?: string +} + +export function ErrorAlert({ error, className = '' }: ErrorAlertProps) { + const [isDismissed, setIsDismissed] = useState(false) + + // Reset dismissed state when a new error occurs + useEffect(() => { + if (error) { + setIsDismissed(false) + } + }, [error]) + + if (!error || isDismissed) { + return null + } + + return ( +
    + + + {error} + + +
    + ) +} diff --git a/renderer/src/features/chat/components/mcp-server-badge.tsx b/renderer/src/features/chat/components/mcp-server-badge.tsx new file mode 100644 index 00000000..741398bb --- /dev/null +++ b/renderer/src/features/chat/components/mcp-server-badge.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { Badge } from '@/common/components/ui/badge' +import { Settings } from 'lucide-react' +import { McpToolsModal } from './mcp-tools-modal' + +interface McpServerBadgeProps { + serverName: string + onToolsChange: () => void +} + +export function McpServerBadge({ + serverName, + onToolsChange, +}: McpServerBadgeProps) { + const [modalOpen, setModalOpen] = useState(false) + + // Fetch enabled tools for this specific server + const { data: enabledMcpTools } = useQuery({ + queryKey: ['enabled-mcp-tools'], + queryFn: () => window.electronAPI.chat.getEnabledMcpTools(), + refetchInterval: 2000, // Refresh every 2 seconds to keep counts updated + }) + + // Fetch server tools data to get total count + const { data: serverTools } = useQuery({ + queryKey: ['mcp-server-tools', serverName], + queryFn: () => window.electronAPI.chat.getMcpServerTools(serverName), + refetchInterval: 5000, // Refresh every 5 seconds + staleTime: 0, + refetchOnMount: true, + }) + + // Helper function to get enabled tools count for this server + const getEnabledToolsCount = (): number => { + if (!enabledMcpTools) return 0 + const serverToolsList = enabledMcpTools[serverName] || [] + return serverToolsList.length + } + + // Helper function to format the badge counter + const formatBadgeCounter = (): string => { + const enabledCount = getEnabledToolsCount() + const totalCount = serverTools?.tools?.length || 0 + + if (totalCount === 0) { + return '(0)' + } + + // If all tools are enabled, just show the count + if (enabledCount === totalCount) { + return `(${enabledCount})` + } + + // If not all tools are enabled, show enabled/total + return `(${enabledCount}/${totalCount})` + } + + const handleBadgeClick = () => { + setModalOpen(true) + } + + const handleToolsChange = () => { + onToolsChange() + } + + return ( + <> + + {serverName} + {formatBadgeCounter()} + + + + {/* MCP Tools Modal */} + + + ) +} diff --git a/renderer/src/features/chat/components/mcp-server-selector.tsx b/renderer/src/features/chat/components/mcp-server-selector.tsx new file mode 100644 index 00000000..b12fbbfd --- /dev/null +++ b/renderer/src/features/chat/components/mcp-server-selector.tsx @@ -0,0 +1,221 @@ +import { useState, useEffect } from 'react' +import { useQuery } from '@tanstack/react-query' +import { getApiV1BetaWorkloadsOptions } from '@api/@tanstack/react-query.gen' +import type { CoreWorkload } from '@api/types.gen' +import { Button } from '@/common/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuCheckboxItem, + DropdownMenuTrigger, + DropdownMenuSeparator, + DropdownMenuLabel, +} from '@/common/components/ui/dropdown-menu' +import { ChevronDown } from 'lucide-react' +import { McpServerBadge } from './mcp-server-badge' + +interface McpServerSelectorProps { + enabledTools: string[] + onEnabledToolsChange: (tools: string[]) => void +} + +interface McpServer { + id: string + name: string + status: 'running' | 'stopped' + package?: string +} + +export function McpServerSelector({ + enabledTools, + onEnabledToolsChange, +}: McpServerSelectorProps) { + const [isOpen, setIsOpen] = useState(false) + + const { data: workloadsData } = useQuery({ + ...getApiV1BetaWorkloadsOptions({ query: { all: true } }), + refetchInterval: 5000, // Refresh every 5 seconds to keep status updated + }) + + // Process workloads data to get running MCP servers + const mcpServers: McpServer[] = (workloadsData?.workloads || []) + .filter((w: CoreWorkload) => w.status === 'running' && w.url) + .map((w: CoreWorkload) => ({ + id: `mcp_${w.name}`, + name: w.name || 'Unknown', + status: w.status as 'running' | 'stopped', + package: w.package, + })) + + const enabledMcpServers = mcpServers.filter((server) => + enabledTools.includes(server.id) + ) + + const handleToggleTool = async (toolId: string) => { + // Extract server name from toolId (remove 'mcp_' prefix) + const serverName = toolId.replace('mcp_', '') + + if (enabledTools.includes(toolId)) { + // Disable all tools for this server + try { + await window.electronAPI.chat.saveEnabledMcpTools(serverName, []) + // Refresh the enabled tools list from the single source of truth + const newEnabledServers = + await window.electronAPI.chat.getEnabledMcpServersFromTools() + onEnabledToolsChange(newEnabledServers) + } catch (error) { + console.error('Failed to disable server tools:', error) + } + } else { + // Enable all tools for this server by default + try { + // First get the server's available tools + const serverTools = + await window.electronAPI.chat.getMcpServerTools(serverName) + + if (serverTools?.tools && serverTools.tools.length > 0) { + const allToolNames = serverTools.tools.map((tool) => tool.name) + await window.electronAPI.chat.saveEnabledMcpTools( + serverName, + allToolNames + ) + // Refresh the enabled tools list from the single source of truth + const newEnabledServers = + await window.electronAPI.chat.getEnabledMcpServersFromTools() + + onEnabledToolsChange(newEnabledServers) + } + } catch (error) { + console.error('Failed to enable server tools:', error) + } + } + } + + // Effect to sync server list with backend state when workloads change + useEffect(() => { + const syncServerList = async () => { + try { + const backendEnabledServers = + await window.electronAPI.chat.getEnabledMcpServersFromTools() + // If the current enabledTools doesn't match the backend, update it + if ( + JSON.stringify(enabledTools.sort()) !== + JSON.stringify(backendEnabledServers.sort()) + ) { + onEnabledToolsChange(backendEnabledServers) + } + } catch (error) { + console.error('Failed to sync server list with backend:', error) + } + } + + // Only sync if we have workload data + if (workloadsData?.workloads) { + syncServerList() + } + }, [workloadsData?.workloads, enabledTools, onEnabledToolsChange]) // Include dependencies + + const handleToolsChange = async () => { + // Individual tool changes are already saved by the modal, + // now refresh the server list from the single source of truth + try { + // Refresh the enabled servers list from individual tool states + const newEnabledServers = + await window.electronAPI.chat.getEnabledMcpServersFromTools() + onEnabledToolsChange(newEnabledServers) + } catch (error) { + console.error('Failed to refresh enabled servers:', error) + } + } + + return ( +
    + {/* Header with dropdown */} +
    +
    + MCP Server selected ({enabledMcpServers.length}) +
    + + + + + + Available MCP Servers + + + {mcpServers.length === 0 ? ( +
    + No MCP servers running +
    + ) : ( + mcpServers.map((server) => ( + handleToggleTool(server.id)} + className="flex items-center gap-3 py-3" + > +
    + {server.name} + {server.package && ( + + {server.package} + + )} +
    +
    +
    + + Running + +
    + + )) + )} + + {mcpServers.length > 0 && ( + <> + +
    + +
    + + )} + + +
    + + {/* Selected tools as badges */} + {enabledMcpServers.length > 0 && ( +
    + {enabledMcpServers.map((server) => ( + + ))} +
    + )} +
    + ) +} diff --git a/renderer/src/features/chat/components/mcp-tools-modal.tsx b/renderer/src/features/chat/components/mcp-tools-modal.tsx new file mode 100644 index 00000000..0faf14e1 --- /dev/null +++ b/renderer/src/features/chat/components/mcp-tools-modal.tsx @@ -0,0 +1,347 @@ +import { useState, useEffect } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/common/components/ui/dialog' +import { Button } from '@/common/components/ui/button' +import { Badge } from '@/common/components/ui/badge' +import { Switch } from '@/common/components/ui/switch' +import { Input } from '@/common/components/ui/input' + +import { Search, Wrench, Package, AlertCircle } from 'lucide-react' +import { cn } from '@/common/lib/utils' + +interface McpToolInfo { + name: string + description?: string + parameters?: Record + enabled: boolean +} + +interface McpServerToolsResponse { + serverName: string + serverPackage?: string + tools: McpToolInfo[] + isRunning: boolean +} + +interface McpToolsModalProps { + open: boolean + onOpenChange: (open: boolean) => void + serverName: string + onToolsChange?: (serverName: string, enabledTools: string[]) => void +} + +export function McpToolsModal({ + open, + onOpenChange, + serverName, + onToolsChange, +}: McpToolsModalProps) { + const [searchQuery, setSearchQuery] = useState('') + const [localEnabledTools, setLocalEnabledTools] = useState([]) + const queryClient = useQueryClient() + + // Fetch tools for the specific server + const { + data: serverTools, + isLoading, + error, + } = useQuery({ + queryKey: ['mcp-server-tools', serverName], + queryFn: async (): Promise => { + return window.electronAPI.chat.getMcpServerTools(serverName) + }, + enabled: open && !!serverName, + refetchOnWindowFocus: false, + staleTime: 0, // Always consider data stale to ensure fresh fetches + refetchOnMount: true, // Always refetch when component mounts + }) + + // Initialize local state when data loads + useEffect(() => { + if (serverTools?.tools) { + const enabledTools = serverTools.tools + .filter((tool) => tool.enabled) + .map((tool) => tool.name) + + setLocalEnabledTools(enabledTools) + } + }, [serverTools, serverName]) + + // Save tools mutation + const saveToolsMutation = useMutation({ + mutationFn: async (enabledTools: string[]) => { + return window.electronAPI.chat.saveEnabledMcpTools( + serverName, + enabledTools + ) + }, + onSuccess: (result) => { + if (result.success) { + // Invalidate the specific server's tools query + queryClient.invalidateQueries({ + queryKey: ['mcp-server-tools', serverName], + }) + // Also invalidate all mcp-server-tools queries to ensure consistency + queryClient.invalidateQueries({ queryKey: ['mcp-server-tools'] }) + // Invalidate the global enabled MCP tools query + queryClient.invalidateQueries({ queryKey: ['enabled-mcp-tools'] }) + onToolsChange?.(serverName, localEnabledTools) + onOpenChange(false) + } + }, + }) + + const handleToolToggle = (toolName: string) => { + setLocalEnabledTools((prev) => + prev.includes(toolName) + ? prev.filter((name) => name !== toolName) + : [...prev, toolName] + ) + } + + const handleSave = () => { + saveToolsMutation.mutate(localEnabledTools) + } + + const handleCancel = () => { + // Reset to original state + if (serverTools?.tools) { + const enabledTools = serverTools.tools + .filter((tool) => tool.enabled) + .map((tool) => tool.name) + setLocalEnabledTools(enabledTools) + } + onOpenChange(false) + } + + const filteredTools = + serverTools?.tools.filter( + (tool) => + tool.name.toLowerCase().includes(searchQuery.toLowerCase()) || + tool.description?.toLowerCase().includes(searchQuery.toLowerCase()) + ) || [] + + const enabledCount = localEnabledTools.length + const totalCount = serverTools?.tools.length || 0 + + return ( + + + + + + MCP Tools - {serverName} + + {serverTools?.serverPackage && ( +
    + + {serverTools.serverPackage} +
    + )} + + Manage individual tools for this MCP server. Enable or disable + specific tools to control what's available in the chat. + +
    + +
    + {/* Status and Search */} +
    +
    +
    + + {serverTools?.isRunning ? 'Running' : 'Stopped'} + + {totalCount > 0 && ( + + {enabledCount}/{totalCount} tools enabled + + )} +
    +
    + +
    + + setSearchQuery(e.target.value)} + className="pl-9" + /> +
    +
    + + {/* Enable/Disable All Controls */} + {serverTools?.isRunning && totalCount > 0 && ( +
    +
    + + Quick Actions + + + {enabledCount}/{totalCount} enabled + +
    +
    + + +
    +
    + )} + + {/* Tools List */} +
    0 + ? 'rounded-b-md border-t-0' + : 'rounded-md' + )} + > +
    + {isLoading ? ( +
    +
    + Loading tools... +
    +
    + ) : error ? ( +
    +
    + + Failed to load tools +
    +
    + ) : !serverTools?.isRunning ? ( +
    +
    + Server is not running. Start the server to see available + tools. +
    +
    + ) : filteredTools.length === 0 ? ( +
    +
    + {searchQuery + ? 'No tools match your search.' + : 'No tools available.'} +
    +
    + ) : ( +
    + {filteredTools.map((tool) => ( +
    +
    +
    + +

    {tool.name}

    +
    + + {/* Tool Description */} +
    + {tool.description ? ( +

    + {tool.description} +

    + ) : ( +

    + No description available +

    + )} +
    + + {/* Tool Parameters */} + {tool.parameters && + Object.keys(tool.parameters).length > 0 && ( +
    + + {Object.keys(tool.parameters).length} parameter + {Object.keys(tool.parameters).length !== 1 + ? 's' + : ''} + +
    + )} +
    + handleToolToggle(tool.name)} + className="mt-1 shrink-0" + /> +
    + ))} +
    + )} +
    +
    +
    + + + + + +
    +
    + ) +} diff --git a/renderer/src/features/chat/components/model-selector.tsx b/renderer/src/features/chat/components/model-selector.tsx new file mode 100644 index 00000000..fa542245 --- /dev/null +++ b/renderer/src/features/chat/components/model-selector.tsx @@ -0,0 +1,274 @@ +import { useState } from 'react' +import { Bot, Key, Settings, ChevronDown, Check, Search } from 'lucide-react' +import { Button } from '@/common/components/ui/button' +import { Input } from '@/common/components/ui/input' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '@/common/components/ui/dropdown-menu' +import { Badge } from '@/common/components/ui/badge' +import { useAvailableModels } from '../hooks/use-available-models' +import { getProviderIcon } from './provider-icons' +import type { ChatSettings } from '../types' + +interface ModelSelectorProps { + settings: ChatSettings + onSettingsChange: (settings: ChatSettings) => void + onOpenSettings: () => void + onProviderChange?: (providerId: string) => void +} + +export function ModelSelector({ + settings, + onSettingsChange, + onOpenSettings, + onProviderChange, +}: ModelSelectorProps) { + const { providersWithApiKeys, isLoading } = useAvailableModels() + const [searchQueries, setSearchQueries] = useState>({}) + + if (isLoading) { + return ( + + ) + } + + if (providersWithApiKeys.length === 0) { + return ( + + ) + } + + const selectedProvider = providersWithApiKeys.find( + (p) => p.id === settings.provider + ) + + const handleModelSelect = (providerId: string, modelId: string) => { + // If provider changed, load its API key + if (providerId !== settings.provider && onProviderChange) { + onProviderChange(providerId) + } + + onSettingsChange({ + ...settings, + provider: providerId, + model: modelId, + }) + } + + // Filter models based on search query for each provider + const getFilteredModels = (provider: { id: string; models: string[] }) => { + const query = searchQueries[provider.id]?.toLowerCase() || '' + if (!query) return provider.models + + return provider.models.filter((model: string) => + model.toLowerCase().includes(query) + ) + } + + // Format model name for better display + const formatModelName = (model: string, providerId: string): string => { + if (providerId !== 'openrouter') return model + + // For OpenRouter models, extract provider and model name + const parts = model.split('/') + if (parts.length >= 2) { + const provider = parts[0] + const modelName = parts.slice(1).join('/') + + // Capitalize provider name + const providerName = + provider + ?.split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') || provider + + // Format model name + const formattedModelName = modelName + .replace(/-/g, ' ') + .replace(/\b\w/g, (l) => l.toUpperCase()) + + return `${formattedModelName} (${providerName})` + } + + // Fallback for models without provider prefix + return model.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()) + } + + return ( + + + + + + + + + AI Models + + + + {providersWithApiKeys.map((provider) => { + const filteredModels = getFilteredModels(provider) + const hasSearch = provider.models.length > 10 + + return ( + + +
    +
    + {getProviderIcon(provider.id)} + {provider.name} +
    + + {searchQueries[provider.id] + ? filteredModels.length + : provider.models.length} + +
    +
    + +
    + + {provider.name} Models + + + + {/* Search input for providers with many models */} + {hasSearch && ( +
    +
    + + + setSearchQueries((prev) => ({ + ...prev, + [provider.id]: e.target.value, + })) + } + className="h-8 pl-8" + onClick={(e) => e.stopPropagation()} + /> +
    +
    + )} +
    + + {/* Models list with scroll */} +
    + {filteredModels.length > 0 ? ( + filteredModels.map((model) => { + const isSelected = + settings.provider === provider.id && + settings.model === model + + return ( + handleModelSelect(provider.id, model)} + className="flex cursor-pointer items-center + justify-between" + > +
    + {isSelected && } +
    + + {provider.id === 'openrouter' + ? formatModelName(model, provider.id) + : model} + + {provider.id === 'openrouter' && ( + + {model} + + )} +
    +
    +
    + {(model.includes('gpt-5') || + model.includes('claude-4') || + model.includes('gemini-2.5')) && ( + + Latest + + )} + {(model.includes('o1') || + model.includes('o3') || + model.includes('reasoning') || + model.includes('thinking')) && ( + + Reasoning + + )} +
    +
    + ) + }) + ) : ( +
    + No models found matching "{searchQueries[provider.id]}" +
    + )} +
    + + {/* Model count info for providers with search */} + {hasSearch && ( +
    +

    + {searchQueries[provider.id] + ? `${filteredModels.length} of ${provider.models.length} models` + : `${provider.models.length} models available`} +

    +
    + )} +
    +
    + ) + })} + + + + + Manage API Keys + +
    +
    + ) +} diff --git a/renderer/src/features/chat/components/no-content-message.tsx b/renderer/src/features/chat/components/no-content-message.tsx new file mode 100644 index 00000000..b071ec4e --- /dev/null +++ b/renderer/src/features/chat/components/no-content-message.tsx @@ -0,0 +1,42 @@ +import type { ChatUIMessage } from '../types' + +interface NoContentMessageProps { + message: ChatUIMessage +} +// Check for any text content +interface TextPart { + type: 'text' + text: string +} + +export function NoContentMessage({ message }: NoContentMessageProps) { + const hasTextContent = message.parts.some( + (p): p is TextPart => + p.type === 'text' && 'text' in p && !!(p as TextPart).text.trim() + ) + + const hasActivity = + message.parts.filter( + (p) => + p.type.startsWith('tool-') || + p.type === 'dynamic-tool' || + ['reasoning', 'step-start'].includes(p.type) || + ('state' in p && + p.state && + ['input-streaming', 'input-available'].includes(p.state)) + ).length > 0 + + // Check if the message is finished (has token usage metadata) + const isFinished = !!message.metadata?.totalUsage + + // Only show "No response content" if the message is finished and has no content/activity + if (isFinished && !hasTextContent && !hasActivity) { + return ( +
    + No response content +
    + ) + } + + return null +} diff --git a/renderer/src/features/chat/components/provider-icons.tsx b/renderer/src/features/chat/components/provider-icons.tsx new file mode 100644 index 00000000..5caf4cef --- /dev/null +++ b/renderer/src/features/chat/components/provider-icons.tsx @@ -0,0 +1,82 @@ +import React from 'react' + +// Provider SVG icons for the UI +const PROVIDER_ICONS: Record = { + openai: ( + + + + ), + + anthropic: ( + + Anthropic + + + ), + + google: ( + + Gemini + + + ), + + xai: ( + + + + + + ), + + openrouter: ( + + + + + + + + + ), +} + +// Get provider icon as React component +export function getProviderIcon(providerId: string): React.ReactElement | null { + return PROVIDER_ICONS[providerId] || PROVIDER_ICONS['openrouter'] || null +} diff --git a/renderer/src/features/chat/components/token-usage.tsx b/renderer/src/features/chat/components/token-usage.tsx new file mode 100644 index 00000000..939ae667 --- /dev/null +++ b/renderer/src/features/chat/components/token-usage.tsx @@ -0,0 +1,101 @@ +import { Zap, ArrowRight, Hash } from 'lucide-react' +import type { LanguageModelV2Usage } from '@ai-sdk/provider' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/common/components/ui/tooltip' + +interface TokenUsageProps { + usage: LanguageModelV2Usage + responseTime?: number +} + +export function TokenUsage({ usage, responseTime }: TokenUsageProps) { + const formatTime = (ms: number) => { + if (ms < 1000) return `${ms}ms` + return `${(ms / 1000).toFixed(1)}s` + } + + return ( + +
    + + +
    + + {usage.inputTokens || 0} + + {usage.outputTokens || 0} + + = {usage.totalTokens || 0} + +
    +
    + +
    +
    Token Usage Breakdown
    +
    +
    + Input tokens:{' '} + {(usage.inputTokens || 0).toLocaleString()} (your message + + context) +
    +
    + Output tokens:{' '} + {(usage.outputTokens || 0).toLocaleString()} (AI response) +
    + {usage.reasoningTokens && usage.reasoningTokens > 0 && ( +
    + Reasoning tokens:{' '} + {usage.reasoningTokens.toLocaleString()} (internal + reasoning) +
    + )} + {usage.cachedInputTokens && usage.cachedInputTokens > 0 && ( +
    + Cached input tokens:{' '} + {usage.cachedInputTokens.toLocaleString()} (cached from + previous requests) +
    + )} +
    + Total tokens:{' '} + {(usage.totalTokens || 0).toLocaleString()} +
    +
    +
    + Tokens are units of text that AI models process. More tokens = + higher cost. +
    +
    +
    +
    + + {responseTime && ( + <> + + + +
    + + {formatTime(responseTime)} +
    +
    + +
    +
    Response Time
    +
    + Time taken to generate the complete response:{' '} + {formatTime(responseTime)} +
    +
    +
    +
    + + )} +
    +
    + ) +} diff --git a/renderer/src/features/chat/hooks/use-available-models.ts b/renderer/src/features/chat/hooks/use-available-models.ts new file mode 100644 index 00000000..615e66c7 --- /dev/null +++ b/renderer/src/features/chat/hooks/use-available-models.ts @@ -0,0 +1,75 @@ +import { useState, useEffect } from 'react' +import type { ChatProvider } from '../types' + +interface AvailableProvider extends ChatProvider { + hasApiKey: boolean +} + +export function useAvailableModels() { + const [availableProviders, setAvailableProviders] = useState< + AvailableProvider[] + >([]) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + async function fetchAvailableModels() { + try { + setIsLoading(true) + + // Get all providers + const providers: ChatProvider[] = + await window.electronAPI.chat.getProviders() + + // Check which providers have API keys + const providersWithApiKeys = await Promise.all( + providers.map(async (provider) => { + try { + const settings = await window.electronAPI.chat.getSettings( + provider.id + ) + return { + ...provider, + hasApiKey: Boolean(settings.apiKey), + } + } catch { + return { + ...provider, + hasApiKey: false, + } + } + }) + ) + + setAvailableProviders(providersWithApiKeys) + } catch (error) { + console.error('Failed to fetch available models:', error) + setAvailableProviders([]) + } finally { + setIsLoading(false) + } + } + + fetchAvailableModels() + + // Listen for API key changes + const handleApiKeysChanged = () => { + fetchAvailableModels() + } + + window.addEventListener('api-keys-changed', handleApiKeysChanged) + return () => { + window.removeEventListener('api-keys-changed', handleApiKeysChanged) + } + }, []) + + // Filter to only providers with API keys + const providersWithApiKeys = availableProviders.filter( + (provider) => provider.hasApiKey + ) + + return { + availableProviders, + providersWithApiKeys, + isLoading, + } +} diff --git a/renderer/src/features/chat/hooks/use-chat-streaming.ts b/renderer/src/features/chat/hooks/use-chat-streaming.ts new file mode 100644 index 00000000..792db9db --- /dev/null +++ b/renderer/src/features/chat/hooks/use-chat-streaming.ts @@ -0,0 +1,192 @@ +import { useState, useCallback, useEffect, useMemo } from 'react' +import { useChat } from '@ai-sdk/react' +import type { ChatUIMessage, ChatSettings } from '../types' +import { ElectronIPCChatTransport } from '../transport/electron-ipc-chat-transport' + +export function useChatStreaming() { + // Create custom IPC transport that always gets current settings from IPC store + const ipcTransport = useMemo( + () => + new ElectronIPCChatTransport({ + getSettings: async () => { + try { + // Always get the latest selected model and settings from IPC store + const model = await window.electronAPI.chat.getSelectedModel() + if (!model.provider || !model.model) { + return { provider: '', model: '', apiKey: '', enabledTools: [] } + } + + const providerSettings = await window.electronAPI.chat.getSettings( + model.provider + ) + return { + provider: model.provider, + model: model.model, + apiKey: providerSettings.apiKey || '', + enabledTools: providerSettings.enabledTools || [], + } + } catch (error) { + console.error('Failed to get settings from IPC store:', error) + return { provider: '', model: '', apiKey: '', enabledTools: [] } + } + }, + }), + [] + ) + + // Use official AI SDK useChat with our custom transport and typed messages + const { messages, sendMessage, status, error, stop, setMessages } = + useChat({ + transport: ipcTransport, + }) + + // Convert status to our isLoading format + const isLoading = status === 'submitted' || status === 'streaming' + + // Get current settings from IPC store (for UI display) + const [settings, setSettings] = useState({ + provider: '', + model: '', + apiKey: '', + enabledTools: [], + }) + + const refreshSettings = useCallback(async () => { + try { + const model = await window.electronAPI.chat.getSelectedModel() + if (model.provider && model.model) { + const providerSettings = await window.electronAPI.chat.getSettings( + model.provider + ) + setSettings({ + provider: model.provider, + model: model.model, + apiKey: providerSettings.apiKey || '', + enabledTools: providerSettings.enabledTools || [], + }) + } else { + setSettings({ + provider: '', + model: '', + apiKey: '', + enabledTools: [], + }) + } + } catch (error) { + console.error('Failed to refresh settings:', error) + setSettings({ + provider: '', + model: '', + apiKey: '', + enabledTools: [], + }) + } + }, []) + + const updateSettings = useCallback( + async (newSettings: ChatSettings) => { + try { + // Save to IPC store first + await window.electronAPI.chat.saveSelectedModel( + newSettings.provider, + newSettings.model + ) + await window.electronAPI.chat.saveSettings(newSettings.provider, { + apiKey: newSettings.apiKey, + enabledTools: newSettings.enabledTools || [], + }) + // Then refresh local state from IPC store + await refreshSettings() + } catch (err) { + console.error('Failed to save settings:', err) + } + }, + [refreshSettings] + ) + + const handleSendMessage = useCallback( + async (content: string) => { + // Validation is now handled in the transport layer using IPC store + await sendMessage({ text: content }) + }, + [sendMessage] + ) + + const clearMessages = useCallback(() => { + setMessages([]) + }, [setMessages]) + + const loadPersistedSettings = useCallback( + async (providerId: string, preserveEnabledTools = false) => { + if (!providerId) return + + try { + const persistedSettings = + await window.electronAPI.chat.getSettings(providerId) + const currentModel = await window.electronAPI.chat.getSelectedModel() + + // Update the selected model if provider changed + if (currentModel.provider !== providerId) { + // Get the first available model for this provider + const providers = await window.electronAPI.chat.getProviders() + const provider = providers.find((p) => p.id === providerId) + if (provider && provider.models.length > 0) { + const firstModel = provider.models[0] + if (firstModel) { + await window.electronAPI.chat.saveSelectedModel( + providerId, + firstModel + ) + } + } + } + + // Save settings to IPC store if preserving tools + if ( + preserveEnabledTools && + settings.enabledTools && + settings.enabledTools.length > 0 + ) { + await window.electronAPI.chat.saveSettings(providerId, { + apiKey: persistedSettings.apiKey || '', + enabledTools: settings.enabledTools, + }) + } + + // Refresh settings from IPC store + await refreshSettings() + } catch (err) { + console.error('Failed to load persisted settings:', err) + } + }, + [refreshSettings, settings.enabledTools] + ) + + // Load persisted model selection on mount and listen for changes + useEffect(() => { + // Initial load + refreshSettings() + + // Listen for API key changes and refresh settings + const handleApiKeysChanged = () => { + refreshSettings() + } + + window.addEventListener('api-keys-changed', handleApiKeysChanged) + return () => { + window.removeEventListener('api-keys-changed', handleApiKeysChanged) + } + }, [refreshSettings]) + + return { + messages, + isLoading, + error: error?.message || null, + settings, + sendMessage: handleSendMessage, + clearMessages, + cancelRequest: stop, + updateSettings, + loadPersistedSettings, + } +} diff --git a/renderer/src/features/chat/transport/electron-ipc-chat-transport.ts b/renderer/src/features/chat/transport/electron-ipc-chat-transport.ts new file mode 100644 index 00000000..98035ded --- /dev/null +++ b/renderer/src/features/chat/transport/electron-ipc-chat-transport.ts @@ -0,0 +1,146 @@ +import type { ChatTransport, UIMessageChunk, ChatRequestOptions } from 'ai' +import type { ChatUIMessage } from '../types' + +interface ElectronIPCChatTransportConfig { + getSettings: () => Promise<{ + provider: string + model: string + apiKey: string + enabledTools?: string[] + }> +} + +/** + * Custom chat transport for Electron IPC that implements the AI SDK ChatTransport interface + */ +export class ElectronIPCChatTransport implements ChatTransport { + constructor(private config: ElectronIPCChatTransportConfig) {} + + async sendMessages( + options: { + trigger: 'submit-message' | 'regenerate-message' + chatId: string + messageId: string | undefined + messages: ChatUIMessage[] + abortSignal: AbortSignal | undefined + } & ChatRequestOptions + ): Promise> { + const settings = await this.config.getSettings() + + if ( + !settings.provider || + !settings.model || + !settings.apiKey || + !settings.apiKey.trim() + ) { + console.error('Transport validation failed:', settings) + throw new Error('Please configure your AI provider settings first') + } + + const backendRequest = { + messages: options.messages.map((msg) => ({ + id: msg.id, + role: msg.role, + parts: msg.parts.map((part) => ({ + type: part.type, + text: part.type === 'text' ? part.text : '', + })), + })), + provider: settings.provider, + model: settings.model, + apiKey: settings.apiKey, + enabledTools: settings.enabledTools || [], + } + + try { + // Start streaming and get stream ID + const response = await window.electronAPI.chat.stream(backendRequest) + const { streamId } = response as { streamId: string } + + // Create a readable stream that will be populated by IPC events + return new ReadableStream({ + start(controller) { + let isClosed = false + + // Listen for stream chunks + const handleChunk = (...args: unknown[]) => { + const data = args[0] as { streamId: string; chunk: UIMessageChunk } + if (data && data.streamId === streamId && !isClosed) { + try { + // Enqueue the UIMessageChunk directly + controller.enqueue(data.chunk) + } catch (error) { + console.warn( + 'Failed to enqueue chunk (stream likely closed):', + error + ) + isClosed = true + } + } + } + + // Listen for stream end + const handleEnd = (...args: unknown[]) => { + const data = args[0] as { streamId: string } + if (data && data.streamId === streamId && !isClosed) { + try { + controller.close() + isClosed = true + } catch (error) { + console.warn('Failed to close stream (already closed):', error) + } + // Clean up listeners + window.electronAPI.removeListener?.( + 'chat:stream:chunk', + handleChunk + ) + window.electronAPI.removeListener?.('chat:stream:end', handleEnd) + window.electronAPI.removeListener?.( + 'chat:stream:error', + handleError + ) + } + } + + // Listen for stream errors + const handleError = (...args: unknown[]) => { + const data = args[0] as { streamId: string; error: string } + if (data && data.streamId === streamId && !isClosed) { + try { + controller.error(new Error(data.error)) + isClosed = true + } catch (error) { + console.warn('Failed to error stream (already closed):', error) + } + // Clean up listeners + window.electronAPI.removeListener?.( + 'chat:stream:chunk', + handleChunk + ) + window.electronAPI.removeListener?.('chat:stream:end', handleEnd) + window.electronAPI.removeListener?.( + 'chat:stream:error', + handleError + ) + } + } + + // Set up IPC listeners + + window.electronAPI.on?.('chat:stream:chunk', handleChunk) + window.electronAPI.on?.('chat:stream:end', handleEnd) + window.electronAPI.on?.('chat:stream:error', handleError) + }, + }) + } catch (error) { + throw new Error( + `IPC communication failed: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } + } + + async reconnectToStream(): Promise | null> { + // For now, we don't support reconnection - return null + return null + } +} diff --git a/renderer/src/features/chat/types.ts b/renderer/src/features/chat/types.ts new file mode 100644 index 00000000..c0a495a6 --- /dev/null +++ b/renderer/src/features/chat/types.ts @@ -0,0 +1,27 @@ +import type { UIMessage } from 'ai' +import type { LanguageModelV2Usage } from '@ai-sdk/provider' + +// Define message metadata schema for type safety +interface MessageMetadata { + createdAt?: number + model?: string + totalUsage?: LanguageModelV2Usage + responseTime?: number + finishReason?: string +} + +// Create a typed UIMessage with our metadata +export type ChatUIMessage = UIMessage + +export interface ChatProvider { + id: string + name: string + models: string[] +} + +export interface ChatSettings { + provider: string + model: string + apiKey: string + enabledTools?: string[] +} diff --git a/renderer/src/renderer.tsx b/renderer/src/renderer.tsx index c926b99c..6b1c23fd 100644 --- a/renderer/src/renderer.tsx +++ b/renderer/src/renderer.tsx @@ -7,7 +7,7 @@ import { createRouter, } from '@tanstack/react-router' import { routeTree } from './route-tree.gen' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { QueryClientProvider } from '@tanstack/react-query' import { TooltipProvider } from '@radix-ui/react-tooltip' import * as Sentry from '@sentry/electron/renderer' import { ThemeProvider } from './common/components/theme/theme-provider' @@ -17,6 +17,7 @@ import log from 'electron-log/renderer' import './index.css' import { ConfirmProvider } from './common/contexts/confirm/provider' import { trackPageView } from './common/lib/analytics' +import { queryClient } from './common/lib/query-client' // Import feature flags to bind them to window for developer tools access import './common/lib/feature-flags' @@ -56,7 +57,6 @@ const memoryHistory = createMemoryHistory({ initialEntries: ['/'], }) -const queryClient = new QueryClient({}) const router = createRouter({ routeTree, context: { queryClient }, diff --git a/renderer/src/route-tree.gen.ts b/renderer/src/route-tree.gen.ts index fcc18c34..8f91daea 100644 --- a/renderer/src/route-tree.gen.ts +++ b/renderer/src/route-tree.gen.ts @@ -12,6 +12,7 @@ import { Route as rootRouteImport } from "./routes/__root" import { Route as ShutdownRouteImport } from "./routes/shutdown" import { Route as SettingsRouteImport } from "./routes/settings" import { Route as SecretsRouteImport } from "./routes/secrets" +import { Route as PlaygroundRouteImport } from "./routes/playground" import { Route as ClientsRouteImport } from "./routes/clients" import { Route as IndexRouteImport } from "./routes/index" import { Route as LogsServerNameRouteImport } from "./routes/logs.$serverName" @@ -33,6 +34,11 @@ const SecretsRoute = SecretsRouteImport.update({ path: "/secrets", getParentRoute: () => rootRouteImport, } as any) +const PlaygroundRoute = PlaygroundRouteImport.update({ + id: "/playground", + path: "/playground", + getParentRoute: () => rootRouteImport, +} as any) const ClientsRoute = ClientsRouteImport.update({ id: "/clients", path: "/clients", @@ -62,6 +68,7 @@ const registryRegistryNameRoute = registryRegistryNameRouteImport.update({ export interface FileRoutesByFullPath { "/": typeof IndexRoute "/clients": typeof ClientsRoute + "/playground": typeof PlaygroundRoute "/secrets": typeof SecretsRoute "/settings": typeof SettingsRoute "/shutdown": typeof ShutdownRoute @@ -72,6 +79,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { "/": typeof IndexRoute "/clients": typeof ClientsRoute + "/playground": typeof PlaygroundRoute "/secrets": typeof SecretsRoute "/settings": typeof SettingsRoute "/shutdown": typeof ShutdownRoute @@ -83,6 +91,7 @@ export interface FileRoutesById { __root__: typeof rootRouteImport "/": typeof IndexRoute "/clients": typeof ClientsRoute + "/playground": typeof PlaygroundRoute "/secrets": typeof SecretsRoute "/settings": typeof SettingsRoute "/shutdown": typeof ShutdownRoute @@ -95,6 +104,7 @@ export interface FileRouteTypes { fullPaths: | "/" | "/clients" + | "/playground" | "/secrets" | "/settings" | "/shutdown" @@ -105,6 +115,7 @@ export interface FileRouteTypes { to: | "/" | "/clients" + | "/playground" | "/secrets" | "/settings" | "/shutdown" @@ -115,6 +126,7 @@ export interface FileRouteTypes { | "__root__" | "/" | "/clients" + | "/playground" | "/secrets" | "/settings" | "/shutdown" @@ -126,6 +138,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute ClientsRoute: typeof ClientsRoute + PlaygroundRoute: typeof PlaygroundRoute SecretsRoute: typeof SecretsRoute SettingsRoute: typeof SettingsRoute ShutdownRoute: typeof ShutdownRoute @@ -157,6 +170,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof SecretsRouteImport parentRoute: typeof rootRouteImport } + "/playground": { + id: "/playground" + path: "/playground" + fullPath: "/playground" + preLoaderRoute: typeof PlaygroundRouteImport + parentRoute: typeof rootRouteImport + } "/clients": { id: "/clients" path: "/clients" @@ -198,6 +218,7 @@ declare module "@tanstack/react-router" { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ClientsRoute: ClientsRoute, + PlaygroundRoute: PlaygroundRoute, SecretsRoute: SecretsRoute, SettingsRoute: SettingsRoute, ShutdownRoute: ShutdownRoute, diff --git a/renderer/src/routes/playground.tsx b/renderer/src/routes/playground.tsx new file mode 100644 index 00000000..2052cf78 --- /dev/null +++ b/renderer/src/routes/playground.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/react-router' +import { ChatInterface } from '@/features/chat/components/chat-interface' +import { TitlePage } from '@/common/components/title-page' + +export const Route = createFileRoute('/playground')({ + component: Playground, +}) + +function Playground() { + return ( + <> + +

    + Test and interact with AI models using your MCP servers +

    +
    +
    + +
    + + ) +}