@wrongstack/desktop 0.309.0 → 0.310.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main/agent-bridge.js +1 -1
- package/dist/main/agent-bridge.js.map +1 -1
- package/dist/main/main.js +57 -37
- package/dist/main/main.js.map +3 -3
- package/package.json +8 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/main/agent-bridge.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Desktop Agent Bridge - WebSocket communication with runtimes.\n * Handles conversation management and WebSocket connections with automatic reconnection.\n */\nimport { randomUUID } from 'node:crypto';\nimport { EventEmitter } from 'node:events';\nimport {\n createSurfaceConnectionState,\n DEFAULT_SURFACE_CONNECTION_CONFIG,\n decodeProtocolFrame,\n markConnectionActivity,\n markConnectionConnecting,\n markConnectionOpen,\n planConnectionReconnect,\n resetConnection,\n type SurfaceConnectionState,\n stopConnection,\n} from '@wrongstack/webui-server/protocol';\nimport WebSocket from 'ws';\nimport type {\n DesktopConversationMessage,\n DesktopConversationSnapshot,\n DesktopConversationStatus,\n} from '../shared/types.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface ConversationInternal {\n runtimeId: string;\n status: DesktopConversationStatus;\n sessionId?: string | undefined;\n error?: string | undefined;\n messages: DesktopConversationMessage[];\n ws: WebSocket | null;\n connectPromise: Promise<void> | null;\n activeAssistantMessageId: string | null;\n /** Reconnection state */\n reconnectAttempt: number;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n reconnectUrl: string | null;\n connectionState: SurfaceConnectionState;\n // Per-reconnect message-handler bookkeeping. The anonymous\n // `ws.on('message', \u2026)` pattern pins the socket + its closure (which\n // captures `conversation`) for the socket's full lifetime \u2014 fine for\n // open sockets, fatal across reconnects, where each new connect() leaks\n // a stale message listener + closure onto the previous (now-orphaned)\n // WebSocket. RAM-leak audit 2026-08-11, HIGH.\n onMessage: ((data: WebSocket.RawData) => void) | null;\n socketGeneration: number;\n}\n\ninterface ServerMessage {\n type: string;\n payload?: Record<string, unknown> | undefined;\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nconst MAX_MESSAGES = 300;\n\n/** Reconnection configuration */\nconst RECONNECT_CONFIG = {\n /** Maximum number of reconnection attempts (0 = disabled) */\n maxAttempts: 5,\n /** Initial delay in ms before first reconnection */\n initialDelayMs: 1000,\n /** Maximum delay in ms between reconnection attempts */\n maxDelayMs: 30000,\n /** Multiplier for exponential backoff */\n backoffMultiplier: 2,\n /** Jitter factor (0-1) to add randomness to delays */\n jitterFactor: 0.1,\n};\n\nconst DESKTOP_CONNECTION_CONFIG = {\n ...DEFAULT_SURFACE_CONNECTION_CONFIG,\n maxReconnectAttempts: RECONNECT_CONFIG.maxAttempts,\n initialBackoffMs: RECONNECT_CONFIG.initialDelayMs,\n maxBackoffMs: RECONNECT_CONFIG.maxDelayMs,\n backoffMultiplier: RECONNECT_CONFIG.backoffMultiplier,\n jitterRatio: RECONNECT_CONFIG.jitterFactor,\n};\n\n// ============================================================================\n// Bridge Implementation\n// ============================================================================\n\nexport class DesktopAgentBridge extends EventEmitter {\n private readonly conversations = new Map<string, ConversationInternal>();\n\n snapshot(runtimeId: string): DesktopConversationSnapshot {\n return publicConversation(this.getOrCreate(runtimeId));\n }\n\n /**\n * Get reconnection status for a runtime.\n */\n getReconnectStatus(runtimeId: string): { attempt: number; maxAttempts: number } | null {\n const conversation = this.conversations.get(runtimeId);\n if (!conversation) return null;\n return {\n attempt: conversation.reconnectAttempt,\n maxAttempts: RECONNECT_CONFIG.maxAttempts,\n };\n }\n\n /**\n * Force reconnection for a runtime (resets reconnection state).\n */\n forceReconnect(runtimeId: string, wsUrl: string): void {\n const conversation = this.getOrCreate(runtimeId);\n this.cancelReconnect(conversation);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n void this.ensureConnected(runtimeId, wsUrl);\n }\n\n async ensureConnected(runtimeId: string, wsUrl: string): Promise<DesktopConversationSnapshot> {\n const conversation = this.getOrCreate(runtimeId);\n\n // If already connected, return immediately\n if (conversation.ws?.readyState === WebSocket.OPEN) return publicConversation(conversation);\n\n // If currently connecting, wait for it\n if (conversation.connectPromise) {\n await conversation.connectPromise;\n return publicConversation(conversation);\n }\n\n // A user action cannot continue while only a delayed reconnect exists:\n // sendMessage() would otherwise write to a closed socket.\n if (conversation.reconnectTimer) {\n this.cancelReconnect(conversation);\n await this.connect(runtimeId, wsUrl);\n return publicConversation(conversation);\n }\n\n // Start fresh connection\n conversation.reconnectUrl = wsUrl;\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n await this.connect(runtimeId, wsUrl);\n return publicConversation(conversation);\n }\n\n /**\n * Internal connect method that actually establishes the WebSocket.\n */\n private async connect(runtimeId: string, wsUrl: string): Promise<void> {\n const conversation = this.getOrCreate(runtimeId);\n\n // Cancel any pending reconnect timer\n this.cancelReconnect(conversation);\n\n conversation.status = 'connecting';\n conversation.connectionState = markConnectionConnecting(conversation.connectionState);\n conversation.error = undefined;\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'connecting');\n\n // Store the in-flight connect promise so concurrent ensureConnected()\n // calls dedup through the conversation.connectPromise read at L129-130.\n // Without this, the dedup branch is dead and two parallel calls each\n // construct a fresh WebSocket \u2014 the loser's close-handler guard\n // (if (conversation.ws === ws)) means the superseded socket is never\n // closed, leaking one OPEN socket per race until close().\n const promise = new Promise<void>((resolve, reject) => {\n const ws = new WebSocket(wsUrl);\n conversation.ws = ws;\n\n // Bump the generation so a message handler from this socket knows it\n // is the current one. Each connect() call gets a unique generation,\n // so a stale socket that fires a late 'message' after its successor\n // has taken over sees a mismatch and bails before touching state.\n // RAM-leak audit 2026-08-11, HIGH.\n const myGeneration = ++conversation.socketGeneration;\n\n // Named (not anonymous) so we can detach it in close() and in the\n // 'close' handler. An anonymous ws.on('message', \u2026) was never removed,\n // so the closure pinned ConversationInternal (and its messages[],\n // ws, sessionId, \u2026) until the WebSocket was GC'd \u2014 across a reconnect\n // storm this leaked every ConversationInternal that ever connected.\n // RAM-leak audit 2026-08-11, HIGH.\n const onMessage = (data: unknown): void => {\n if (conversation.socketGeneration !== myGeneration) return;\n this.handleServerMessage(conversation, String(data));\n };\n conversation.onMessage = onMessage;\n ws.on('message', onMessage);\n\n // Set connection timeout\n const timeout = setTimeout(() => {\n if (conversation.ws === ws) {\n ws.close();\n reject(new Error('Connection timeout'));\n }\n }, 10000);\n\n ws.once('open', () => {\n clearTimeout(timeout);\n conversation.status = 'connected';\n conversation.error = undefined;\n conversation.connectPromise = null;\n conversation.reconnectAttempt = 0;\n conversation.connectionState = markConnectionOpen(conversation.connectionState);\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'connected');\n resolve();\n });\n\n ws.once('error', (err) => {\n clearTimeout(timeout);\n conversation.status = 'error';\n conversation.error = err instanceof Error ? err.message : String(err);\n conversation.connectPromise = null;\n this.appendMessage(conversation, {\n role: 'system',\n text: `Connection error: ${conversation.error}`,\n });\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'error');\n reject(err);\n });\n\n ws.once('close', () => {\n clearTimeout(timeout);\n // Only the socket that currently owns the conversation invalidates\n // the generation \u2014 a superseded socket closing must not clobber its\n // successor's generation, or late messages from the successor would\n // be wrongly dropped. RAM-leak audit 2026-08-11, HIGH (chimera review).\n if (conversation.ws === ws) {\n conversation.socketGeneration++;\n conversation.ws = null;\n }\n // Detach our named message handler so the closure (which retains\n // conversation) is releasable even if the ws is kept alive by the\n // runtime or pending GC. RAM-leak audit 2026-08-11, HIGH.\n ws.off('message', onMessage);\n // Only clear the tracked handler if it's still ours \u2014 a newer\n // connect() may have already installed its own. Clearing\n // unconditionally would orphan the new handler's reference,\n // making it impossible for close(runtimeId) to detach it later.\n // RAM-leak audit 2026-08-11, HIGH (chimera review Medium #1).\n if (conversation.onMessage === onMessage) {\n conversation.onMessage = null;\n }\n conversation.connectPromise = null;\n\n if (conversation.status !== 'error') {\n conversation.status = 'disconnected';\n }\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n\n // Schedule reconnection if applicable\n if (conversation.reconnectUrl && conversation.status === 'disconnected') {\n this.scheduleReconnect(conversation);\n }\n });\n });\n conversation.connectPromise = promise;\n return promise;\n }\n\n /**\n * Schedule a reconnection attempt with exponential backoff.\n */\n private scheduleReconnect(conversation: ConversationInternal): void {\n const reconnect = planConnectionReconnect(\n conversation.connectionState,\n DESKTOP_CONNECTION_CONFIG,\n );\n conversation.connectionState = reconnect.state;\n conversation.reconnectAttempt = reconnect.state.reconnectAttempt;\n if (!reconnect.plan) {\n this.emitReconnectEvent(conversation, 'exhausted');\n return;\n }\n this.emitReconnectEvent(conversation, 'scheduled', {\n delay: reconnect.plan.delayMs,\n attempt: reconnect.plan.attempt,\n });\n\n conversation.reconnectTimer = setTimeout(() => {\n conversation.reconnectTimer = null;\n if (!conversation.reconnectUrl) return;\n\n // Check if still disconnected\n if (conversation.ws?.readyState !== WebSocket.OPEN) {\n void this.connect(conversation.runtimeId, conversation.reconnectUrl);\n }\n }, reconnect.plan.delayMs);\n }\n\n /**\n * Cancel pending reconnection.\n */\n private cancelReconnect(conversation: ConversationInternal): void {\n if (conversation.reconnectTimer) {\n clearTimeout(conversation.reconnectTimer);\n conversation.reconnectTimer = null;\n }\n }\n\n /**\n * Emit reconnection event for UI feedback.\n */\n private emitReconnectEvent(\n conversation: ConversationInternal,\n status: 'connecting' | 'connected' | 'error' | 'scheduled' | 'exhausted',\n data?: { delay?: number; attempt?: number },\n ): void {\n this.emit('reconnect', {\n runtimeId: conversation.runtimeId,\n status,\n attempt: conversation.reconnectAttempt,\n maxAttempts: RECONNECT_CONFIG.maxAttempts,\n ...data,\n });\n }\n\n async sendMessage(\n runtimeId: string,\n wsUrl: string,\n content: string,\n ): Promise<DesktopConversationSnapshot> {\n const trimmed = content.trim();\n if (!trimmed) return this.snapshot(runtimeId);\n\n // Reset reconnection state on manual action\n const conversation = this.getOrCreate(runtimeId);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n\n await this.ensureConnected(runtimeId, wsUrl);\n\n // Refresh conversation after connect\n const conv = this.getOrCreate(runtimeId);\n this.appendMessage(conv, {\n id: `user_${randomUUID()}`,\n role: 'user',\n text: trimmed,\n });\n conv.status = 'running';\n conv.activeAssistantMessageId = null;\n this.emitChanged(conv);\n this.send(conv, {\n type: 'user_message',\n payload: {\n id: `msg_${Date.now()}_${randomUUID().slice(0, 8)}`,\n content: trimmed,\n timestamp: Date.now(),\n ...(conv.sessionId ? { sessionId: conv.sessionId } : {}),\n },\n });\n return publicConversation(conv);\n }\n\n async abort(runtimeId: string, wsUrl: string): Promise<DesktopConversationSnapshot> {\n // Reset reconnection state on manual action\n const conversation = this.getOrCreate(runtimeId);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n\n await this.ensureConnected(runtimeId, wsUrl);\n const conv = this.getOrCreate(runtimeId);\n this.send(conv, {\n type: 'abort',\n payload: conv.sessionId ? { sessionId: conv.sessionId } : {},\n });\n conv.status = 'connected';\n this.appendMessage(conv, { role: 'system', text: 'Abort requested.' });\n return publicConversation(conv);\n }\n\n close(runtimeId: string): void {\n const conversation = this.conversations.get(runtimeId);\n if (!conversation) return;\n\n // Cancel reconnection and clear state\n this.cancelReconnect(conversation);\n conversation.reconnectAttempt = 0;\n conversation.reconnectUrl = null;\n conversation.connectionState = stopConnection(conversation.connectionState);\n\n // Detach the per-reconnect message listener BEFORE nulling the ws ref.\n // Without this, the ws.on('message', \u2026) closure pins ConversationInternal\n // (and its messages[]) until the socket is GC'd. RAM-leak audit 2026-08-11, HIGH.\n if (conversation.onMessage) {\n conversation.ws?.off('message', conversation.onMessage);\n conversation.onMessage = null;\n }\n // Invalidate this conversation's generation so any in-flight socket whose\n // 'close' event fires after deletion cannot resurrect the conversation ref.\n conversation.socketGeneration++;\n conversation.ws?.close();\n conversation.ws = null;\n conversation.connectPromise = null;\n conversation.status = 'disconnected';\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n\n // Remove from the map so closed conversations don't accumulate in memory.\n this.conversations.delete(runtimeId);\n }\n\n closeAll(): void {\n // Snapshot keys first \u2014 close() deletes from the map during iteration.\n for (const runtimeId of [...this.conversations.keys()]) {\n this.close(runtimeId);\n }\n }\n\n private handleServerMessage(conversation: ConversationInternal, raw: string): void {\n const decoded = decodeProtocolFrame(raw, 'server');\n if (!decoded.ok) return;\n conversation.connectionState = markConnectionActivity(conversation.connectionState);\n const message = decoded.message as ServerMessage;\n const payload = message.payload ?? {};\n switch (message.type) {\n case 'session.start': {\n const sessionId = stringValue(payload['sessionId']);\n if (sessionId) conversation.sessionId = sessionId;\n conversation.status = conversation.status === 'running' ? 'running' : 'connected';\n this.emitChanged(conversation);\n break;\n }\n case 'provider.text_delta': {\n conversation.status = 'running';\n this.appendAssistantDelta(conversation, stringValue(payload['text']) ?? '');\n break;\n }\n case 'tool.started': {\n conversation.status = 'running';\n const name = stringValue(payload['name']) ?? 'tool';\n this.appendMessage(conversation, { role: 'tool', text: `Started ${name}` });\n break;\n }\n case 'tool.executed': {\n const name = stringValue(payload['name']) ?? 'tool';\n const ok = payload['ok'] === true;\n this.appendMessage(conversation, {\n role: 'tool',\n text: `${name} ${ok ? 'completed' : 'failed'}`,\n });\n break;\n }\n case 'provider.error':\n case 'provider.stream_error':\n case 'error': {\n const text =\n stringValue(payload['message']) ??\n stringValue(payload['description']) ??\n `${message.type} received`;\n conversation.status = 'error';\n conversation.error = text;\n this.appendMessage(conversation, { role: 'system', text });\n break;\n }\n case 'run.result': {\n const finalText = stringValue(payload['finalText']);\n if (finalText && !this.lastAssistantHasText(conversation)) {\n this.appendMessage(conversation, { role: 'assistant', text: finalText });\n }\n conversation.status = payload['status'] === 'failed' ? 'error' : 'connected';\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n break;\n }\n }\n }\n\n private send(conversation: ConversationInternal, message: Record<string, unknown>): void {\n if (conversation.ws?.readyState !== WebSocket.OPEN) {\n throw new Error('Runtime socket is not connected');\n }\n conversation.ws.send(JSON.stringify(message));\n }\n\n private appendAssistantDelta(conversation: ConversationInternal, text: string): void {\n if (!text) return;\n let message = conversation.messages.find((m) => m.id === conversation.activeAssistantMessageId);\n if (!message) {\n message = {\n id: `assistant_${randomUUID()}`,\n role: 'assistant',\n text: '',\n timestamp: Date.now(),\n };\n conversation.activeAssistantMessageId = message.id;\n conversation.messages.push(message);\n }\n message.text += text;\n this.trimMessages(conversation);\n this.emitChanged(conversation);\n }\n\n private appendMessage(\n conversation: ConversationInternal,\n input: Partial<DesktopConversationMessage> & Pick<DesktopConversationMessage, 'role' | 'text'>,\n ): void {\n conversation.messages.push({\n id: input.id ?? `${input.role}_${randomUUID()}`,\n role: input.role,\n text: input.text,\n timestamp: input.timestamp ?? Date.now(),\n });\n this.trimMessages(conversation);\n this.emitChanged(conversation);\n }\n\n private lastAssistantHasText(conversation: ConversationInternal): boolean {\n const lastAssistant = [...conversation.messages].reverse().find((m) => m.role === 'assistant');\n return Boolean(lastAssistant?.text.trim());\n }\n\n private trimMessages(conversation: ConversationInternal): void {\n if (conversation.messages.length <= MAX_MESSAGES) return;\n conversation.messages.splice(0, conversation.messages.length - MAX_MESSAGES);\n }\n\n private getOrCreate(runtimeId: string): ConversationInternal {\n let conversation = this.conversations.get(runtimeId);\n if (conversation) return conversation;\n conversation = {\n runtimeId,\n status: 'disconnected',\n messages: [],\n ws: null,\n connectPromise: null,\n activeAssistantMessageId: null,\n reconnectAttempt: 0,\n reconnectTimer: null,\n reconnectUrl: null,\n connectionState: createSurfaceConnectionState(),\n onMessage: null,\n socketGeneration: 0,\n };\n this.conversations.set(runtimeId, conversation);\n return conversation;\n }\n\n private emitChanged(conversation: ConversationInternal): void {\n this.emit('changed', publicConversation(conversation));\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\nfunction publicConversation(conversation: ConversationInternal): DesktopConversationSnapshot {\n return {\n runtimeId: conversation.runtimeId,\n status: conversation.status,\n sessionId: conversation.sessionId,\n error: conversation.error,\n messages: conversation.messages.map((message) => ({ ...message })),\n };\n}\n\nfunction stringValue(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\n// ============================================================================\n// Reconnect Event Type (for external consumers)\n// ============================================================================\n\nexport interface DesktopBridgeReconnectEvent {\n runtimeId: string;\n status: 'connecting' | 'connected' | 'error' | 'scheduled' | 'exhausted';\n attempt: number;\n maxAttempts: number;\n delay?: number;\n}\n"],
|
|
4
|
+
"sourcesContent": ["/**\n * Desktop Agent Bridge - WebSocket communication with runtimes.\n * Handles conversation management and WebSocket connections with automatic reconnection.\n */\nimport { randomUUID } from 'node:crypto';\nimport { EventEmitter } from 'node:events';\nimport {\n createSurfaceConnectionState,\n DEFAULT_SURFACE_CONNECTION_CONFIG,\n decodeProtocolFrame,\n markConnectionActivity,\n markConnectionConnecting,\n markConnectionOpen,\n planConnectionReconnect,\n resetConnection,\n type SurfaceConnectionState,\n stopConnection,\n} from '@wrongstack/webui-protocol';\nimport WebSocket from 'ws';\nimport type {\n DesktopConversationMessage,\n DesktopConversationSnapshot,\n DesktopConversationStatus,\n} from '../shared/types.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface ConversationInternal {\n runtimeId: string;\n status: DesktopConversationStatus;\n sessionId?: string | undefined;\n error?: string | undefined;\n messages: DesktopConversationMessage[];\n ws: WebSocket | null;\n connectPromise: Promise<void> | null;\n activeAssistantMessageId: string | null;\n /** Reconnection state */\n reconnectAttempt: number;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n reconnectUrl: string | null;\n connectionState: SurfaceConnectionState;\n // Per-reconnect message-handler bookkeeping. The anonymous\n // `ws.on('message', \u2026)` pattern pins the socket + its closure (which\n // captures `conversation`) for the socket's full lifetime \u2014 fine for\n // open sockets, fatal across reconnects, where each new connect() leaks\n // a stale message listener + closure onto the previous (now-orphaned)\n // WebSocket. RAM-leak audit 2026-08-11, HIGH.\n onMessage: ((data: WebSocket.RawData) => void) | null;\n socketGeneration: number;\n}\n\ninterface ServerMessage {\n type: string;\n payload?: Record<string, unknown> | undefined;\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nconst MAX_MESSAGES = 300;\n\n/** Reconnection configuration */\nconst RECONNECT_CONFIG = {\n /** Maximum number of reconnection attempts (0 = disabled) */\n maxAttempts: 5,\n /** Initial delay in ms before first reconnection */\n initialDelayMs: 1000,\n /** Maximum delay in ms between reconnection attempts */\n maxDelayMs: 30000,\n /** Multiplier for exponential backoff */\n backoffMultiplier: 2,\n /** Jitter factor (0-1) to add randomness to delays */\n jitterFactor: 0.1,\n};\n\nconst DESKTOP_CONNECTION_CONFIG = {\n ...DEFAULT_SURFACE_CONNECTION_CONFIG,\n maxReconnectAttempts: RECONNECT_CONFIG.maxAttempts,\n initialBackoffMs: RECONNECT_CONFIG.initialDelayMs,\n maxBackoffMs: RECONNECT_CONFIG.maxDelayMs,\n backoffMultiplier: RECONNECT_CONFIG.backoffMultiplier,\n jitterRatio: RECONNECT_CONFIG.jitterFactor,\n};\n\n// ============================================================================\n// Bridge Implementation\n// ============================================================================\n\nexport class DesktopAgentBridge extends EventEmitter {\n private readonly conversations = new Map<string, ConversationInternal>();\n\n snapshot(runtimeId: string): DesktopConversationSnapshot {\n return publicConversation(this.getOrCreate(runtimeId));\n }\n\n /**\n * Get reconnection status for a runtime.\n */\n getReconnectStatus(runtimeId: string): { attempt: number; maxAttempts: number } | null {\n const conversation = this.conversations.get(runtimeId);\n if (!conversation) return null;\n return {\n attempt: conversation.reconnectAttempt,\n maxAttempts: RECONNECT_CONFIG.maxAttempts,\n };\n }\n\n /**\n * Force reconnection for a runtime (resets reconnection state).\n */\n forceReconnect(runtimeId: string, wsUrl: string): void {\n const conversation = this.getOrCreate(runtimeId);\n this.cancelReconnect(conversation);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n void this.ensureConnected(runtimeId, wsUrl);\n }\n\n async ensureConnected(runtimeId: string, wsUrl: string): Promise<DesktopConversationSnapshot> {\n const conversation = this.getOrCreate(runtimeId);\n\n // If already connected, return immediately\n if (conversation.ws?.readyState === WebSocket.OPEN) return publicConversation(conversation);\n\n // If currently connecting, wait for it\n if (conversation.connectPromise) {\n await conversation.connectPromise;\n return publicConversation(conversation);\n }\n\n // A user action cannot continue while only a delayed reconnect exists:\n // sendMessage() would otherwise write to a closed socket.\n if (conversation.reconnectTimer) {\n this.cancelReconnect(conversation);\n await this.connect(runtimeId, wsUrl);\n return publicConversation(conversation);\n }\n\n // Start fresh connection\n conversation.reconnectUrl = wsUrl;\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n await this.connect(runtimeId, wsUrl);\n return publicConversation(conversation);\n }\n\n /**\n * Internal connect method that actually establishes the WebSocket.\n */\n private async connect(runtimeId: string, wsUrl: string): Promise<void> {\n const conversation = this.getOrCreate(runtimeId);\n\n // Cancel any pending reconnect timer\n this.cancelReconnect(conversation);\n\n conversation.status = 'connecting';\n conversation.connectionState = markConnectionConnecting(conversation.connectionState);\n conversation.error = undefined;\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'connecting');\n\n // Store the in-flight connect promise so concurrent ensureConnected()\n // calls dedup through the conversation.connectPromise read at L129-130.\n // Without this, the dedup branch is dead and two parallel calls each\n // construct a fresh WebSocket \u2014 the loser's close-handler guard\n // (if (conversation.ws === ws)) means the superseded socket is never\n // closed, leaking one OPEN socket per race until close().\n const promise = new Promise<void>((resolve, reject) => {\n const ws = new WebSocket(wsUrl);\n conversation.ws = ws;\n\n // Bump the generation so a message handler from this socket knows it\n // is the current one. Each connect() call gets a unique generation,\n // so a stale socket that fires a late 'message' after its successor\n // has taken over sees a mismatch and bails before touching state.\n // RAM-leak audit 2026-08-11, HIGH.\n const myGeneration = ++conversation.socketGeneration;\n\n // Named (not anonymous) so we can detach it in close() and in the\n // 'close' handler. An anonymous ws.on('message', \u2026) was never removed,\n // so the closure pinned ConversationInternal (and its messages[],\n // ws, sessionId, \u2026) until the WebSocket was GC'd \u2014 across a reconnect\n // storm this leaked every ConversationInternal that ever connected.\n // RAM-leak audit 2026-08-11, HIGH.\n const onMessage = (data: unknown): void => {\n if (conversation.socketGeneration !== myGeneration) return;\n this.handleServerMessage(conversation, String(data));\n };\n conversation.onMessage = onMessage;\n ws.on('message', onMessage);\n\n // Set connection timeout\n const timeout = setTimeout(() => {\n if (conversation.ws === ws) {\n ws.close();\n reject(new Error('Connection timeout'));\n }\n }, 10000);\n\n ws.once('open', () => {\n clearTimeout(timeout);\n conversation.status = 'connected';\n conversation.error = undefined;\n conversation.connectPromise = null;\n conversation.reconnectAttempt = 0;\n conversation.connectionState = markConnectionOpen(conversation.connectionState);\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'connected');\n resolve();\n });\n\n ws.once('error', (err) => {\n clearTimeout(timeout);\n conversation.status = 'error';\n conversation.error = err instanceof Error ? err.message : String(err);\n conversation.connectPromise = null;\n this.appendMessage(conversation, {\n role: 'system',\n text: `Connection error: ${conversation.error}`,\n });\n this.emitChanged(conversation);\n this.emitReconnectEvent(conversation, 'error');\n reject(err);\n });\n\n ws.once('close', () => {\n clearTimeout(timeout);\n // Only the socket that currently owns the conversation invalidates\n // the generation \u2014 a superseded socket closing must not clobber its\n // successor's generation, or late messages from the successor would\n // be wrongly dropped. RAM-leak audit 2026-08-11, HIGH (chimera review).\n if (conversation.ws === ws) {\n conversation.socketGeneration++;\n conversation.ws = null;\n }\n // Detach our named message handler so the closure (which retains\n // conversation) is releasable even if the ws is kept alive by the\n // runtime or pending GC. RAM-leak audit 2026-08-11, HIGH.\n ws.off('message', onMessage);\n // Only clear the tracked handler if it's still ours \u2014 a newer\n // connect() may have already installed its own. Clearing\n // unconditionally would orphan the new handler's reference,\n // making it impossible for close(runtimeId) to detach it later.\n // RAM-leak audit 2026-08-11, HIGH (chimera review Medium #1).\n if (conversation.onMessage === onMessage) {\n conversation.onMessage = null;\n }\n conversation.connectPromise = null;\n\n if (conversation.status !== 'error') {\n conversation.status = 'disconnected';\n }\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n\n // Schedule reconnection if applicable\n if (conversation.reconnectUrl && conversation.status === 'disconnected') {\n this.scheduleReconnect(conversation);\n }\n });\n });\n conversation.connectPromise = promise;\n return promise;\n }\n\n /**\n * Schedule a reconnection attempt with exponential backoff.\n */\n private scheduleReconnect(conversation: ConversationInternal): void {\n const reconnect = planConnectionReconnect(\n conversation.connectionState,\n DESKTOP_CONNECTION_CONFIG,\n );\n conversation.connectionState = reconnect.state;\n conversation.reconnectAttempt = reconnect.state.reconnectAttempt;\n if (!reconnect.plan) {\n this.emitReconnectEvent(conversation, 'exhausted');\n return;\n }\n this.emitReconnectEvent(conversation, 'scheduled', {\n delay: reconnect.plan.delayMs,\n attempt: reconnect.plan.attempt,\n });\n\n conversation.reconnectTimer = setTimeout(() => {\n conversation.reconnectTimer = null;\n if (!conversation.reconnectUrl) return;\n\n // Check if still disconnected\n if (conversation.ws?.readyState !== WebSocket.OPEN) {\n void this.connect(conversation.runtimeId, conversation.reconnectUrl);\n }\n }, reconnect.plan.delayMs);\n }\n\n /**\n * Cancel pending reconnection.\n */\n private cancelReconnect(conversation: ConversationInternal): void {\n if (conversation.reconnectTimer) {\n clearTimeout(conversation.reconnectTimer);\n conversation.reconnectTimer = null;\n }\n }\n\n /**\n * Emit reconnection event for UI feedback.\n */\n private emitReconnectEvent(\n conversation: ConversationInternal,\n status: 'connecting' | 'connected' | 'error' | 'scheduled' | 'exhausted',\n data?: { delay?: number; attempt?: number },\n ): void {\n this.emit('reconnect', {\n runtimeId: conversation.runtimeId,\n status,\n attempt: conversation.reconnectAttempt,\n maxAttempts: RECONNECT_CONFIG.maxAttempts,\n ...data,\n });\n }\n\n async sendMessage(\n runtimeId: string,\n wsUrl: string,\n content: string,\n ): Promise<DesktopConversationSnapshot> {\n const trimmed = content.trim();\n if (!trimmed) return this.snapshot(runtimeId);\n\n // Reset reconnection state on manual action\n const conversation = this.getOrCreate(runtimeId);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n\n await this.ensureConnected(runtimeId, wsUrl);\n\n // Refresh conversation after connect\n const conv = this.getOrCreate(runtimeId);\n this.appendMessage(conv, {\n id: `user_${randomUUID()}`,\n role: 'user',\n text: trimmed,\n });\n conv.status = 'running';\n conv.activeAssistantMessageId = null;\n this.emitChanged(conv);\n this.send(conv, {\n type: 'user_message',\n payload: {\n id: `msg_${Date.now()}_${randomUUID().slice(0, 8)}`,\n content: trimmed,\n timestamp: Date.now(),\n ...(conv.sessionId ? { sessionId: conv.sessionId } : {}),\n },\n });\n return publicConversation(conv);\n }\n\n async abort(runtimeId: string, wsUrl: string): Promise<DesktopConversationSnapshot> {\n // Reset reconnection state on manual action\n const conversation = this.getOrCreate(runtimeId);\n conversation.reconnectAttempt = 0;\n conversation.connectionState = resetConnection(conversation.connectionState);\n\n await this.ensureConnected(runtimeId, wsUrl);\n const conv = this.getOrCreate(runtimeId);\n this.send(conv, {\n type: 'abort',\n payload: conv.sessionId ? { sessionId: conv.sessionId } : {},\n });\n conv.status = 'connected';\n this.appendMessage(conv, { role: 'system', text: 'Abort requested.' });\n return publicConversation(conv);\n }\n\n close(runtimeId: string): void {\n const conversation = this.conversations.get(runtimeId);\n if (!conversation) return;\n\n // Cancel reconnection and clear state\n this.cancelReconnect(conversation);\n conversation.reconnectAttempt = 0;\n conversation.reconnectUrl = null;\n conversation.connectionState = stopConnection(conversation.connectionState);\n\n // Detach the per-reconnect message listener BEFORE nulling the ws ref.\n // Without this, the ws.on('message', \u2026) closure pins ConversationInternal\n // (and its messages[]) until the socket is GC'd. RAM-leak audit 2026-08-11, HIGH.\n if (conversation.onMessage) {\n conversation.ws?.off('message', conversation.onMessage);\n conversation.onMessage = null;\n }\n // Invalidate this conversation's generation so any in-flight socket whose\n // 'close' event fires after deletion cannot resurrect the conversation ref.\n conversation.socketGeneration++;\n conversation.ws?.close();\n conversation.ws = null;\n conversation.connectPromise = null;\n conversation.status = 'disconnected';\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n\n // Remove from the map so closed conversations don't accumulate in memory.\n this.conversations.delete(runtimeId);\n }\n\n closeAll(): void {\n // Snapshot keys first \u2014 close() deletes from the map during iteration.\n for (const runtimeId of [...this.conversations.keys()]) {\n this.close(runtimeId);\n }\n }\n\n private handleServerMessage(conversation: ConversationInternal, raw: string): void {\n const decoded = decodeProtocolFrame(raw, 'server');\n if (!decoded.ok) return;\n conversation.connectionState = markConnectionActivity(conversation.connectionState);\n const message = decoded.message as ServerMessage;\n const payload = message.payload ?? {};\n switch (message.type) {\n case 'session.start': {\n const sessionId = stringValue(payload['sessionId']);\n if (sessionId) conversation.sessionId = sessionId;\n conversation.status = conversation.status === 'running' ? 'running' : 'connected';\n this.emitChanged(conversation);\n break;\n }\n case 'provider.text_delta': {\n conversation.status = 'running';\n this.appendAssistantDelta(conversation, stringValue(payload['text']) ?? '');\n break;\n }\n case 'tool.started': {\n conversation.status = 'running';\n const name = stringValue(payload['name']) ?? 'tool';\n this.appendMessage(conversation, { role: 'tool', text: `Started ${name}` });\n break;\n }\n case 'tool.executed': {\n const name = stringValue(payload['name']) ?? 'tool';\n const ok = payload['ok'] === true;\n this.appendMessage(conversation, {\n role: 'tool',\n text: `${name} ${ok ? 'completed' : 'failed'}`,\n });\n break;\n }\n case 'provider.error':\n case 'provider.stream_error':\n case 'error': {\n const text =\n stringValue(payload['message']) ??\n stringValue(payload['description']) ??\n `${message.type} received`;\n conversation.status = 'error';\n conversation.error = text;\n this.appendMessage(conversation, { role: 'system', text });\n break;\n }\n case 'run.result': {\n const finalText = stringValue(payload['finalText']);\n if (finalText && !this.lastAssistantHasText(conversation)) {\n this.appendMessage(conversation, { role: 'assistant', text: finalText });\n }\n conversation.status = payload['status'] === 'failed' ? 'error' : 'connected';\n conversation.activeAssistantMessageId = null;\n this.emitChanged(conversation);\n break;\n }\n }\n }\n\n private send(conversation: ConversationInternal, message: Record<string, unknown>): void {\n if (conversation.ws?.readyState !== WebSocket.OPEN) {\n throw new Error('Runtime socket is not connected');\n }\n conversation.ws.send(JSON.stringify(message));\n }\n\n private appendAssistantDelta(conversation: ConversationInternal, text: string): void {\n if (!text) return;\n let message = conversation.messages.find((m) => m.id === conversation.activeAssistantMessageId);\n if (!message) {\n message = {\n id: `assistant_${randomUUID()}`,\n role: 'assistant',\n text: '',\n timestamp: Date.now(),\n };\n conversation.activeAssistantMessageId = message.id;\n conversation.messages.push(message);\n }\n message.text += text;\n this.trimMessages(conversation);\n this.emitChanged(conversation);\n }\n\n private appendMessage(\n conversation: ConversationInternal,\n input: Partial<DesktopConversationMessage> & Pick<DesktopConversationMessage, 'role' | 'text'>,\n ): void {\n conversation.messages.push({\n id: input.id ?? `${input.role}_${randomUUID()}`,\n role: input.role,\n text: input.text,\n timestamp: input.timestamp ?? Date.now(),\n });\n this.trimMessages(conversation);\n this.emitChanged(conversation);\n }\n\n private lastAssistantHasText(conversation: ConversationInternal): boolean {\n const lastAssistant = [...conversation.messages].reverse().find((m) => m.role === 'assistant');\n return Boolean(lastAssistant?.text.trim());\n }\n\n private trimMessages(conversation: ConversationInternal): void {\n if (conversation.messages.length <= MAX_MESSAGES) return;\n conversation.messages.splice(0, conversation.messages.length - MAX_MESSAGES);\n }\n\n private getOrCreate(runtimeId: string): ConversationInternal {\n let conversation = this.conversations.get(runtimeId);\n if (conversation) return conversation;\n conversation = {\n runtimeId,\n status: 'disconnected',\n messages: [],\n ws: null,\n connectPromise: null,\n activeAssistantMessageId: null,\n reconnectAttempt: 0,\n reconnectTimer: null,\n reconnectUrl: null,\n connectionState: createSurfaceConnectionState(),\n onMessage: null,\n socketGeneration: 0,\n };\n this.conversations.set(runtimeId, conversation);\n return conversation;\n }\n\n private emitChanged(conversation: ConversationInternal): void {\n this.emit('changed', publicConversation(conversation));\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\nfunction publicConversation(conversation: ConversationInternal): DesktopConversationSnapshot {\n return {\n runtimeId: conversation.runtimeId,\n status: conversation.status,\n sessionId: conversation.sessionId,\n error: conversation.error,\n messages: conversation.messages.map((message) => ({ ...message })),\n };\n}\n\nfunction stringValue(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\n// ============================================================================\n// Reconnect Event Type (for external consumers)\n// ============================================================================\n\nexport interface DesktopBridgeReconnectEvent {\n runtimeId: string;\n status: 'connecting' | 'connected' | 'error' | 'scheduled' | 'exhausted';\n attempt: number;\n maxAttempts: number;\n delay?: number;\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/main/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/main/main.ts
|
|
2
|
-
import * as
|
|
2
|
+
import * as path6 from "node:path";
|
|
3
3
|
import { installCrashShield, resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
|
|
4
4
|
import {
|
|
5
5
|
app as app2,
|
|
@@ -40,9 +40,9 @@ function handleFileOpen(filePath) {
|
|
|
40
40
|
}
|
|
41
41
|
var pendingOpenFilePath = null;
|
|
42
42
|
function drainPendingOpenFilePath() {
|
|
43
|
-
const
|
|
43
|
+
const path7 = pendingOpenFilePath;
|
|
44
44
|
pendingOpenFilePath = null;
|
|
45
|
-
return
|
|
45
|
+
return path7;
|
|
46
46
|
}
|
|
47
47
|
function firstOpenFileArg(argv) {
|
|
48
48
|
if (process.platform !== "darwin") return null;
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
planConnectionReconnect,
|
|
70
70
|
resetConnection,
|
|
71
71
|
stopConnection
|
|
72
|
-
} from "@wrongstack/webui-
|
|
72
|
+
} from "@wrongstack/webui-protocol";
|
|
73
73
|
import WebSocket from "ws";
|
|
74
74
|
var MAX_MESSAGES = 300;
|
|
75
75
|
var RECONNECT_CONFIG = {
|
|
@@ -147,7 +147,7 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
147
147
|
conversation.error = void 0;
|
|
148
148
|
this.emitChanged(conversation);
|
|
149
149
|
this.emitReconnectEvent(conversation, "connecting");
|
|
150
|
-
const promise = new Promise((
|
|
150
|
+
const promise = new Promise((resolve4, reject) => {
|
|
151
151
|
const ws = new WebSocket(wsUrl);
|
|
152
152
|
conversation.ws = ws;
|
|
153
153
|
const myGeneration = ++conversation.socketGeneration;
|
|
@@ -172,7 +172,7 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
172
172
|
conversation.connectionState = markConnectionOpen(conversation.connectionState);
|
|
173
173
|
this.emitChanged(conversation);
|
|
174
174
|
this.emitReconnectEvent(conversation, "connected");
|
|
175
|
-
|
|
175
|
+
resolve4();
|
|
176
176
|
});
|
|
177
177
|
ws.once("error", (err) => {
|
|
178
178
|
clearTimeout(timeout);
|
|
@@ -1445,14 +1445,14 @@ function hasChildExited(child) {
|
|
|
1445
1445
|
}
|
|
1446
1446
|
function waitForChildExit(child, timeoutMs) {
|
|
1447
1447
|
if (hasChildExited(child)) return Promise.resolve(true);
|
|
1448
|
-
return new Promise((
|
|
1448
|
+
return new Promise((resolve4) => {
|
|
1449
1449
|
let settled = false;
|
|
1450
1450
|
const finish = (exited) => {
|
|
1451
1451
|
if (settled) return;
|
|
1452
1452
|
settled = true;
|
|
1453
1453
|
clearTimeout(timer);
|
|
1454
1454
|
child.off("exit", onExit);
|
|
1455
|
-
|
|
1455
|
+
resolve4(exited);
|
|
1456
1456
|
};
|
|
1457
1457
|
const onExit = () => finish(true);
|
|
1458
1458
|
const timer = setTimeout(() => finish(hasChildExited(child)), timeoutMs);
|
|
@@ -1478,12 +1478,12 @@ async function terminateProcessTree(child) {
|
|
|
1478
1478
|
}
|
|
1479
1479
|
return;
|
|
1480
1480
|
}
|
|
1481
|
-
await new Promise((
|
|
1481
|
+
await new Promise((resolve4) => {
|
|
1482
1482
|
let settled = false;
|
|
1483
1483
|
const finish = () => {
|
|
1484
1484
|
if (settled) return;
|
|
1485
1485
|
settled = true;
|
|
1486
|
-
|
|
1486
|
+
resolve4();
|
|
1487
1487
|
};
|
|
1488
1488
|
const timer = setTimeout(finish, 3e3);
|
|
1489
1489
|
timer.unref?.();
|
|
@@ -1628,11 +1628,11 @@ async function findFreePort(startPort, exclude) {
|
|
|
1628
1628
|
throw new Error(`No free local port found near ${startPort}`);
|
|
1629
1629
|
}
|
|
1630
1630
|
function isPortFree(port) {
|
|
1631
|
-
return new Promise((
|
|
1631
|
+
return new Promise((resolve4) => {
|
|
1632
1632
|
const server = net.createServer();
|
|
1633
|
-
server.once("error", () =>
|
|
1633
|
+
server.once("error", () => resolve4(false));
|
|
1634
1634
|
server.once("listening", () => {
|
|
1635
|
-
server.close(() =>
|
|
1635
|
+
server.close(() => resolve4(true));
|
|
1636
1636
|
});
|
|
1637
1637
|
server.listen(port, "127.0.0.1");
|
|
1638
1638
|
});
|
|
@@ -1642,7 +1642,7 @@ function waitForHttpReady(baseUrl, token, timeoutMs) {
|
|
|
1642
1642
|
const url = new URL(baseUrl);
|
|
1643
1643
|
url.searchParams.set("token", token);
|
|
1644
1644
|
url.searchParams.set("shell", "desktop");
|
|
1645
|
-
return new Promise((
|
|
1645
|
+
return new Promise((resolve4, reject) => {
|
|
1646
1646
|
let probeTimer;
|
|
1647
1647
|
const cleanup = () => {
|
|
1648
1648
|
if (probeTimer) {
|
|
@@ -1667,7 +1667,7 @@ function waitForHttpReady(baseUrl, token, timeoutMs) {
|
|
|
1667
1667
|
if (!done) {
|
|
1668
1668
|
done = true;
|
|
1669
1669
|
cleanup();
|
|
1670
|
-
|
|
1670
|
+
resolve4();
|
|
1671
1671
|
}
|
|
1672
1672
|
return;
|
|
1673
1673
|
}
|
|
@@ -2185,14 +2185,14 @@ var DesktopWebuiController = class {
|
|
|
2185
2185
|
if (this.views.get(entry.runtimeId) !== entry || !entry.url) return Promise.resolve(false);
|
|
2186
2186
|
const requestId = `${entry.runtimeId}:${Date.now()}:${++this.commandSequence}`;
|
|
2187
2187
|
const outbound = { ...command, requestId };
|
|
2188
|
-
return new Promise((
|
|
2188
|
+
return new Promise((resolve4) => {
|
|
2189
2189
|
const fallbackTimer = setTimeout(() => {
|
|
2190
2190
|
if (!this.pendingAcks.has(requestId)) return;
|
|
2191
2191
|
if (this.views.get(entry.runtimeId) !== entry || entry.view.webContents.isDestroyed()) return;
|
|
2192
2192
|
void entry.view.webContents.executeJavaScript(buildWebuiCommandFallbackScript(outbound), true).catch(() => void 0);
|
|
2193
2193
|
}, WEBUI_COMMAND_FALLBACK_MS);
|
|
2194
2194
|
const timer = setTimeout(() => this.settleAck(requestId, false), WEBUI_COMMAND_ACK_TIMEOUT_MS);
|
|
2195
|
-
this.pendingAcks.set(requestId, { runtimeId: entry.runtimeId, timer, fallbackTimer, resolve:
|
|
2195
|
+
this.pendingAcks.set(requestId, { runtimeId: entry.runtimeId, timer, fallbackTimer, resolve: resolve4 });
|
|
2196
2196
|
try {
|
|
2197
2197
|
entry.view.webContents.send(IPC.webuiCommand, outbound);
|
|
2198
2198
|
if (this.activeRuntimeId === entry.runtimeId) entry.view.webContents.focus();
|
|
@@ -2772,10 +2772,24 @@ function createValidationLogger(prefix) {
|
|
|
2772
2772
|
}
|
|
2773
2773
|
|
|
2774
2774
|
// src/main/validation/schemas.ts
|
|
2775
|
+
import { statSync } from "node:fs";
|
|
2776
|
+
import * as path5 from "node:path";
|
|
2775
2777
|
import { z } from "zod";
|
|
2776
2778
|
var RUNTIME_ID_PATTERN = /^[a-zA-Z0-9._:-]{3,120}$/;
|
|
2777
2779
|
var runtimeIdSchema = z.string().regex(RUNTIME_ID_PATTERN, "Invalid runtime ID format");
|
|
2778
2780
|
var pathSchema = z.string().min(1).max(1e4);
|
|
2781
|
+
var projectRootSchema = pathSchema.refine((value) => !value.includes("\0"), {
|
|
2782
|
+
message: "path must not contain a NUL byte"
|
|
2783
|
+
}).refine(
|
|
2784
|
+
(value) => {
|
|
2785
|
+
try {
|
|
2786
|
+
return statSync(path5.resolve(value)).isDirectory();
|
|
2787
|
+
} catch {
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
},
|
|
2791
|
+
{ message: "path must be an existing directory" }
|
|
2792
|
+
);
|
|
2779
2793
|
var booleanSchema = z.boolean();
|
|
2780
2794
|
var numberSchema = z.number().int().finite();
|
|
2781
2795
|
var openProjectSchema = z.object({
|
|
@@ -2861,11 +2875,11 @@ function registerIpcHandlers(ctx) {
|
|
|
2861
2875
|
return ctx.openProjectSession(validated);
|
|
2862
2876
|
});
|
|
2863
2877
|
ipcMain.handle(IPC.openProject, async (_event, requestedRoot) => {
|
|
2864
|
-
const validated = validateOptional(
|
|
2878
|
+
const validated = validateOptional(projectRootSchema, requestedRoot);
|
|
2865
2879
|
return ctx.openProject(validated);
|
|
2866
2880
|
});
|
|
2867
2881
|
ipcMain.handle(IPC.registerProject, async (_event, requestedRoot) => {
|
|
2868
|
-
const validated = validateOptional(
|
|
2882
|
+
const validated = validateOptional(projectRootSchema, requestedRoot);
|
|
2869
2883
|
return ctx.registerProject(validated);
|
|
2870
2884
|
});
|
|
2871
2885
|
ipcMain.handle(IPC.unregisterProject, async (_event, root) => {
|
|
@@ -2898,7 +2912,10 @@ function registerIpcHandlers(ctx) {
|
|
|
2898
2912
|
validationLogger.log(`sendMessage (id): ${idResult.error}`);
|
|
2899
2913
|
return ctx.sendMessage("", "", "");
|
|
2900
2914
|
}
|
|
2901
|
-
const contentResult = validate(
|
|
2915
|
+
const contentResult = validate(
|
|
2916
|
+
pathSchema.transform(() => String(content ?? "")),
|
|
2917
|
+
content
|
|
2918
|
+
);
|
|
2902
2919
|
const safeContent = contentResult.success ? contentResult.data : String(content ?? "");
|
|
2903
2920
|
return ctx.sendMessage(
|
|
2904
2921
|
idResult.data,
|
|
@@ -2957,22 +2974,25 @@ function registerIpcHandlers(ctx) {
|
|
|
2957
2974
|
prefs: { ...entry.status.prefs ?? {}, ...sanitized }
|
|
2958
2975
|
});
|
|
2959
2976
|
});
|
|
2960
|
-
ipcMain.on(
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2977
|
+
ipcMain.on(
|
|
2978
|
+
IPC.webuiCommandAck,
|
|
2979
|
+
(event, requestId, handled, _message) => {
|
|
2980
|
+
const entry = ctx.findWebuiEntryBySenderId(event.sender.id);
|
|
2981
|
+
if (!entry) return;
|
|
2982
|
+
const result = validate(webuiCommandAckSchema, {
|
|
2983
|
+
requestId,
|
|
2984
|
+
handled,
|
|
2985
|
+
message: _message
|
|
2986
|
+
});
|
|
2987
|
+
if (!result.success) {
|
|
2988
|
+
validationLogger.log(`webuiCommandAck: ${result.error}`);
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
const pending = ctx.getPendingWebuiCommandAcks().get(result.data.requestId);
|
|
2992
|
+
if (!pending || pending.runtimeId !== entry.runtimeId) return;
|
|
2993
|
+
ctx.settlePendingWebuiCommandAck(result.data.requestId, result.data.handled);
|
|
2971
2994
|
}
|
|
2972
|
-
|
|
2973
|
-
if (!pending || pending.runtimeId !== entry.runtimeId) return;
|
|
2974
|
-
ctx.settlePendingWebuiCommandAck(result.data.requestId, result.data.handled);
|
|
2975
|
-
});
|
|
2995
|
+
);
|
|
2976
2996
|
ipcMain.on(IPC.setLocale, (_event, locale) => {
|
|
2977
2997
|
const result = validate(setLocaleSchema, { locale });
|
|
2978
2998
|
if (!result.success) {
|
|
@@ -3011,7 +3031,7 @@ if (process.platform === "win32") {
|
|
|
3011
3031
|
}
|
|
3012
3032
|
app2.setPath(
|
|
3013
3033
|
"userData",
|
|
3014
|
-
|
|
3034
|
+
path6.join(
|
|
3015
3035
|
resolveWstackPaths3({ projectRoot: process.cwd() }).configDir,
|
|
3016
3036
|
"desktop",
|
|
3017
3037
|
"electron-profile"
|
|
@@ -3065,7 +3085,7 @@ function revealInExplorer(root) {
|
|
|
3065
3085
|
if (!authorization.allowed) return;
|
|
3066
3086
|
return shell2.openPath(root).catch((err) => {
|
|
3067
3087
|
if (process.platform === "darwin") {
|
|
3068
|
-
void shell2.openPath(
|
|
3088
|
+
void shell2.openPath(path6.dirname(root)).catch(() => void 0);
|
|
3069
3089
|
}
|
|
3070
3090
|
console.error(
|
|
3071
3091
|
JSON.stringify({
|