@wrongstack/desktop 0.297.0 → 0.298.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.
@@ -239,9 +239,10 @@ var DesktopAgentBridge = class extends EventEmitter {
239
239
  conversation.status = "disconnected";
240
240
  conversation.activeAssistantMessageId = null;
241
241
  this.emitChanged(conversation);
242
+ this.conversations.delete(runtimeId);
242
243
  }
243
244
  closeAll() {
244
- for (const runtimeId of this.conversations.keys()) {
245
+ for (const runtimeId of [...this.conversations.keys()]) {
245
246
  this.close(runtimeId);
246
247
  }
247
248
  }
@@ -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}\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 // If reconnecting in background, return current state\n if (conversation.reconnectTimer) {\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 return new Promise((resolve, reject) => {\n const ws = new WebSocket(wsUrl);\n conversation.ws = ws;\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.on('message', (data) => {\n this.handleServerMessage(conversation, data.toString());\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 if (conversation.ws === ws) conversation.ws = null;\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 }\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 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\n closeAll(): void {\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 };\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
- "mappings": "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",
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}\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 // If reconnecting in background, return current state\n if (conversation.reconnectTimer) {\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 return new Promise((resolve, reject) => {\n const ws = new WebSocket(wsUrl);\n conversation.ws = ws;\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.on('message', (data) => {\n this.handleServerMessage(conversation, data.toString());\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 if (conversation.ws === ws) conversation.ws = null;\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 }\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 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 };\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
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/main/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/main/main.ts
2
2
  import * as path5 from "node:path";
3
- import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
3
+ import { installCrashShield, resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
4
4
  import {
5
5
  app as app2,
6
6
  BaseWindow,
@@ -297,9 +297,10 @@ var DesktopAgentBridge = class extends EventEmitter {
297
297
  conversation.status = "disconnected";
298
298
  conversation.activeAssistantMessageId = null;
299
299
  this.emitChanged(conversation);
300
+ this.conversations.delete(runtimeId);
300
301
  }
301
302
  closeAll() {
302
- for (const runtimeId of this.conversations.keys()) {
303
+ for (const runtimeId of [...this.conversations.keys()]) {
303
304
  this.close(runtimeId);
304
305
  }
305
306
  }
@@ -1172,8 +1173,6 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1172
1173
  String(wsPort),
1173
1174
  "--dist-dir",
1174
1175
  distDir,
1175
- "--token",
1176
- token,
1177
1176
  "--require-token"
1178
1177
  ],
1179
1178
  {
@@ -1182,7 +1181,16 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1182
1181
  ...buildChildEnv(),
1183
1182
  ELECTRON_RUN_AS_NODE: "1",
1184
1183
  WEBUI_STRICT_PORT: "1",
1185
- WRONGSTACK_DESKTOP: "1"
1184
+ WRONGSTACK_DESKTOP: "1",
1185
+ // Passed by environment, not argv. A process command line is
1186
+ // world-readable on every platform this ships to — `ps -ef` on
1187
+ // POSIX, Task Manager's command-line column or a plain WMI query on
1188
+ // Windows — so `--token <secret>` handed the WebUI access token to
1189
+ // any local process that cared to look. The token grants full agent
1190
+ // control, so that is a credential disclosure, not a nuisance.
1191
+ // entry.ts already reads WEBUI_TOKEN; argv only took precedence
1192
+ // over it (WS-087).
1193
+ WEBUI_TOKEN: token
1186
1194
  },
1187
1195
  stdio: ["ignore", "pipe", "pipe"],
1188
1196
  windowsHide: true
@@ -1972,7 +1980,13 @@ var DesktopWebuiController = class {
1972
1980
  preload: webuiPreloadPath(),
1973
1981
  contextIsolation: true,
1974
1982
  nodeIntegration: false,
1975
- sandbox: false
1983
+ // This is the view that renders agent output, tool results, file
1984
+ // contents, and fetched pages — i.e. the most attacker-influenceable
1985
+ // surface in the app, and the one that most needs process-level
1986
+ // containment rather than only bridge-level. webui-preload.ts imports
1987
+ // just electron's contextBridge/ipcRenderer and a constants map, so it
1988
+ // is already within the sandboxed preload subset (WS-093).
1989
+ sandbox: true
1976
1990
  }
1977
1991
  });
1978
1992
  const entry = {
@@ -3201,13 +3215,24 @@ async function boot() {
3201
3215
  preload: preloadPath(),
3202
3216
  contextIsolation: true,
3203
3217
  nodeIntegration: false,
3204
- sandbox: false
3218
+ // OS-level renderer sandbox. contextIsolation and nodeIntegration:false
3219
+ // already bound what renderer JS can reach through the bridge; the
3220
+ // sandbox is what contains the renderer PROCESS if it is compromised
3221
+ // through the content it renders. This preload only imports electron's
3222
+ // contextBridge/ipcRenderer plus a constants map, so it runs unchanged
3223
+ // under the sandboxed preload subset (WS-093).
3224
+ sandbox: true
3205
3225
  }
3206
3226
  });
3207
3227
  shellView.webContents.setWindowOpenHandler(({ url }) => {
3208
3228
  safeOpenExternal(url);
3209
3229
  return { action: "deny" };
3210
3230
  });
3231
+ shellView.webContents.on("will-navigate", (event, url) => {
3232
+ if (url === shellUrl) return;
3233
+ event.preventDefault();
3234
+ safeOpenExternal(url);
3235
+ });
3211
3236
  registerIpcHandlers(buildIpcHandlerContext());
3212
3237
  await shellView.webContents.loadURL(shellUrl);
3213
3238
  const prevState = windowStateController.validated(manager.getWindowState());
@@ -3289,7 +3314,12 @@ async function boot() {
3289
3314
  mainWindow.show();
3290
3315
  shellView.webContents.focus();
3291
3316
  }
3292
- app2.whenReady().then(boot);
3317
+ installCrashShield();
3318
+ app2.whenReady().then(boot, (error) => {
3319
+ const detail = error instanceof Error ? error.stack ?? error.message : String(error);
3320
+ dialog.showErrorBox("WrongStack failed to start", detail);
3321
+ app2.exit(1);
3322
+ });
3293
3323
  app2.on("window-all-closed", () => {
3294
3324
  if (process.platform !== "darwin") {
3295
3325
  app2.quit();
@@ -3302,6 +3332,7 @@ app2.on("before-quit", () => {
3302
3332
  }
3303
3333
  bridge.closeAll();
3304
3334
  webuiController.disposeAll();
3335
+ void manager.closeAll({ persistWorkspace: false });
3305
3336
  });
3306
3337
  app2.on("activate", () => {
3307
3338
  if (!mainWindow) return;