@wrongstack/desktop 0.282.0 → 0.282.2
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.
|
@@ -3,11 +3,43 @@ import { randomUUID } from "crypto";
|
|
|
3
3
|
import { EventEmitter } from "events";
|
|
4
4
|
import WebSocket from "ws";
|
|
5
5
|
var MAX_MESSAGES = 300;
|
|
6
|
+
var RECONNECT_CONFIG = {
|
|
7
|
+
/** Maximum number of reconnection attempts (0 = disabled) */
|
|
8
|
+
maxAttempts: 5,
|
|
9
|
+
/** Initial delay in ms before first reconnection */
|
|
10
|
+
initialDelayMs: 1e3,
|
|
11
|
+
/** Maximum delay in ms between reconnection attempts */
|
|
12
|
+
maxDelayMs: 3e4,
|
|
13
|
+
/** Multiplier for exponential backoff */
|
|
14
|
+
backoffMultiplier: 2,
|
|
15
|
+
/** Jitter factor (0-1) to add randomness to delays */
|
|
16
|
+
jitterFactor: 0.1
|
|
17
|
+
};
|
|
6
18
|
var DesktopAgentBridge = class extends EventEmitter {
|
|
7
19
|
conversations = /* @__PURE__ */ new Map();
|
|
8
20
|
snapshot(runtimeId) {
|
|
9
21
|
return publicConversation(this.getOrCreate(runtimeId));
|
|
10
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Get reconnection status for a runtime.
|
|
25
|
+
*/
|
|
26
|
+
getReconnectStatus(runtimeId) {
|
|
27
|
+
const conversation = this.conversations.get(runtimeId);
|
|
28
|
+
if (!conversation) return null;
|
|
29
|
+
return {
|
|
30
|
+
attempt: conversation.reconnectAttempt,
|
|
31
|
+
maxAttempts: RECONNECT_CONFIG.maxAttempts
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Force reconnection for a runtime (resets reconnection state).
|
|
36
|
+
*/
|
|
37
|
+
forceReconnect(runtimeId, wsUrl) {
|
|
38
|
+
const conversation = this.getOrCreate(runtimeId);
|
|
39
|
+
this.cancelReconnect(conversation);
|
|
40
|
+
conversation.reconnectAttempt = 0;
|
|
41
|
+
void this.ensureConnected(runtimeId, wsUrl);
|
|
42
|
+
}
|
|
11
43
|
async ensureConnected(runtimeId, wsUrl) {
|
|
12
44
|
const conversation = this.getOrCreate(runtimeId);
|
|
13
45
|
if (conversation.ws?.readyState === WebSocket.OPEN) return publicConversation(conversation);
|
|
@@ -15,23 +47,49 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
15
47
|
await conversation.connectPromise;
|
|
16
48
|
return publicConversation(conversation);
|
|
17
49
|
}
|
|
50
|
+
if (conversation.reconnectTimer) {
|
|
51
|
+
return publicConversation(conversation);
|
|
52
|
+
}
|
|
53
|
+
conversation.reconnectUrl = wsUrl;
|
|
54
|
+
conversation.reconnectAttempt = 0;
|
|
55
|
+
await this.connect(runtimeId, wsUrl);
|
|
56
|
+
return publicConversation(conversation);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Internal connect method that actually establishes the WebSocket.
|
|
60
|
+
*/
|
|
61
|
+
async connect(runtimeId, wsUrl) {
|
|
62
|
+
const conversation = this.getOrCreate(runtimeId);
|
|
63
|
+
this.cancelReconnect(conversation);
|
|
18
64
|
conversation.status = "connecting";
|
|
19
65
|
conversation.error = void 0;
|
|
20
66
|
this.emitChanged(conversation);
|
|
21
|
-
|
|
67
|
+
this.emitReconnectEvent(conversation, "connecting");
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
22
69
|
const ws = new WebSocket(wsUrl);
|
|
23
70
|
conversation.ws = ws;
|
|
71
|
+
const timeout = setTimeout(() => {
|
|
72
|
+
if (conversation.ws === ws) {
|
|
73
|
+
ws.close();
|
|
74
|
+
reject(new Error("Connection timeout"));
|
|
75
|
+
}
|
|
76
|
+
}, 1e4);
|
|
24
77
|
ws.once("open", () => {
|
|
78
|
+
clearTimeout(timeout);
|
|
25
79
|
conversation.status = "connected";
|
|
26
80
|
conversation.error = void 0;
|
|
27
81
|
conversation.connectPromise = null;
|
|
82
|
+
conversation.reconnectAttempt = 0;
|
|
83
|
+
conversation.reconnectUrl = null;
|
|
28
84
|
this.emitChanged(conversation);
|
|
85
|
+
this.emitReconnectEvent(conversation, "connected");
|
|
29
86
|
resolve();
|
|
30
87
|
});
|
|
31
88
|
ws.on("message", (data) => {
|
|
32
89
|
this.handleServerMessage(conversation, data.toString());
|
|
33
90
|
});
|
|
34
91
|
ws.once("error", (err) => {
|
|
92
|
+
clearTimeout(timeout);
|
|
35
93
|
conversation.status = "error";
|
|
36
94
|
conversation.error = err instanceof Error ? err.message : String(err);
|
|
37
95
|
conversation.connectPromise = null;
|
|
@@ -39,9 +97,12 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
39
97
|
role: "system",
|
|
40
98
|
text: `Connection error: ${conversation.error}`
|
|
41
99
|
});
|
|
100
|
+
this.emitChanged(conversation);
|
|
101
|
+
this.emitReconnectEvent(conversation, "error");
|
|
42
102
|
reject(err);
|
|
43
103
|
});
|
|
44
104
|
ws.once("close", () => {
|
|
105
|
+
clearTimeout(timeout);
|
|
45
106
|
if (conversation.ws === ws) conversation.ws = null;
|
|
46
107
|
conversation.connectPromise = null;
|
|
47
108
|
if (conversation.status !== "error") {
|
|
@@ -49,49 +110,103 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
49
110
|
}
|
|
50
111
|
conversation.activeAssistantMessageId = null;
|
|
51
112
|
this.emitChanged(conversation);
|
|
113
|
+
if (conversation.reconnectUrl && conversation.status === "disconnected") {
|
|
114
|
+
this.scheduleReconnect(conversation);
|
|
115
|
+
}
|
|
52
116
|
});
|
|
53
117
|
});
|
|
54
|
-
|
|
55
|
-
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Schedule a reconnection attempt with exponential backoff.
|
|
121
|
+
*/
|
|
122
|
+
scheduleReconnect(conversation) {
|
|
123
|
+
if (RECONNECT_CONFIG.maxAttempts === 0) return;
|
|
124
|
+
if (conversation.reconnectAttempt >= RECONNECT_CONFIG.maxAttempts) {
|
|
125
|
+
this.emitReconnectEvent(conversation, "exhausted");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const baseDelay = Math.min(
|
|
129
|
+
RECONNECT_CONFIG.initialDelayMs * Math.pow(RECONNECT_CONFIG.backoffMultiplier, conversation.reconnectAttempt),
|
|
130
|
+
RECONNECT_CONFIG.maxDelayMs
|
|
131
|
+
);
|
|
132
|
+
const jitter = baseDelay * RECONNECT_CONFIG.jitterFactor * Math.random();
|
|
133
|
+
const delay = Math.floor(baseDelay + jitter);
|
|
134
|
+
conversation.reconnectAttempt++;
|
|
135
|
+
this.emitReconnectEvent(conversation, "scheduled", { delay, attempt: conversation.reconnectAttempt });
|
|
136
|
+
conversation.reconnectTimer = setTimeout(() => {
|
|
137
|
+
conversation.reconnectTimer = null;
|
|
138
|
+
if (!conversation.reconnectUrl) return;
|
|
139
|
+
if (conversation.ws?.readyState !== WebSocket.OPEN) {
|
|
140
|
+
void this.connect(conversation.runtimeId, conversation.reconnectUrl);
|
|
141
|
+
}
|
|
142
|
+
}, delay);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Cancel pending reconnection.
|
|
146
|
+
*/
|
|
147
|
+
cancelReconnect(conversation) {
|
|
148
|
+
if (conversation.reconnectTimer) {
|
|
149
|
+
clearTimeout(conversation.reconnectTimer);
|
|
150
|
+
conversation.reconnectTimer = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Emit reconnection event for UI feedback.
|
|
155
|
+
*/
|
|
156
|
+
emitReconnectEvent(conversation, status, data) {
|
|
157
|
+
this.emit("reconnect", {
|
|
158
|
+
runtimeId: conversation.runtimeId,
|
|
159
|
+
status,
|
|
160
|
+
attempt: conversation.reconnectAttempt,
|
|
161
|
+
maxAttempts: RECONNECT_CONFIG.maxAttempts,
|
|
162
|
+
...data
|
|
163
|
+
});
|
|
56
164
|
}
|
|
57
165
|
async sendMessage(runtimeId, wsUrl, content) {
|
|
58
166
|
const trimmed = content.trim();
|
|
59
167
|
if (!trimmed) return this.snapshot(runtimeId);
|
|
60
|
-
await this.ensureConnected(runtimeId, wsUrl);
|
|
61
168
|
const conversation = this.getOrCreate(runtimeId);
|
|
62
|
-
|
|
169
|
+
conversation.reconnectAttempt = 0;
|
|
170
|
+
await this.ensureConnected(runtimeId, wsUrl);
|
|
171
|
+
const conv = this.getOrCreate(runtimeId);
|
|
172
|
+
this.appendMessage(conv, {
|
|
63
173
|
id: `user_${randomUUID()}`,
|
|
64
174
|
role: "user",
|
|
65
175
|
text: trimmed
|
|
66
176
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.emitChanged(
|
|
70
|
-
this.send(
|
|
177
|
+
conv.status = "running";
|
|
178
|
+
conv.activeAssistantMessageId = null;
|
|
179
|
+
this.emitChanged(conv);
|
|
180
|
+
this.send(conv, {
|
|
71
181
|
type: "user_message",
|
|
72
182
|
payload: {
|
|
73
183
|
id: `msg_${Date.now()}_${randomUUID().slice(0, 8)}`,
|
|
74
184
|
content: trimmed,
|
|
75
185
|
timestamp: Date.now(),
|
|
76
|
-
...
|
|
186
|
+
...conv.sessionId ? { sessionId: conv.sessionId } : {}
|
|
77
187
|
}
|
|
78
188
|
});
|
|
79
|
-
return publicConversation(
|
|
189
|
+
return publicConversation(conv);
|
|
80
190
|
}
|
|
81
191
|
async abort(runtimeId, wsUrl) {
|
|
82
|
-
await this.ensureConnected(runtimeId, wsUrl);
|
|
83
192
|
const conversation = this.getOrCreate(runtimeId);
|
|
84
|
-
|
|
193
|
+
conversation.reconnectAttempt = 0;
|
|
194
|
+
await this.ensureConnected(runtimeId, wsUrl);
|
|
195
|
+
const conv = this.getOrCreate(runtimeId);
|
|
196
|
+
this.send(conv, {
|
|
85
197
|
type: "abort",
|
|
86
|
-
payload:
|
|
198
|
+
payload: conv.sessionId ? { sessionId: conv.sessionId } : {}
|
|
87
199
|
});
|
|
88
|
-
|
|
89
|
-
this.appendMessage(
|
|
90
|
-
return publicConversation(
|
|
200
|
+
conv.status = "connected";
|
|
201
|
+
this.appendMessage(conv, { role: "system", text: "Abort requested." });
|
|
202
|
+
return publicConversation(conv);
|
|
91
203
|
}
|
|
92
204
|
close(runtimeId) {
|
|
93
205
|
const conversation = this.conversations.get(runtimeId);
|
|
94
206
|
if (!conversation) return;
|
|
207
|
+
this.cancelReconnect(conversation);
|
|
208
|
+
conversation.reconnectAttempt = 0;
|
|
209
|
+
conversation.reconnectUrl = null;
|
|
95
210
|
conversation.ws?.close();
|
|
96
211
|
conversation.ws = null;
|
|
97
212
|
conversation.connectPromise = null;
|
|
@@ -211,7 +326,10 @@ var DesktopAgentBridge = class extends EventEmitter {
|
|
|
211
326
|
messages: [],
|
|
212
327
|
ws: null,
|
|
213
328
|
connectPromise: null,
|
|
214
|
-
activeAssistantMessageId: null
|
|
329
|
+
activeAssistantMessageId: null,
|
|
330
|
+
reconnectAttempt: 0,
|
|
331
|
+
reconnectTimer: null,
|
|
332
|
+
reconnectUrl: null
|
|
215
333
|
};
|
|
216
334
|
this.conversations.set(runtimeId, conversation);
|
|
217
335
|
return conversation;
|
|
@@ -236,4 +354,4 @@ function stringValue(value) {
|
|
|
236
354
|
export {
|
|
237
355
|
DesktopAgentBridge
|
|
238
356
|
};
|
|
239
|
-
//# sourceMappingURL=chunk-
|
|
357
|
+
//# sourceMappingURL=chunk-AMPSW3AD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/main/agent-bridge.ts"],"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 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}\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\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 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 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.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.reconnectUrl = null;\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 // Check if reconnection is enabled and within limits\n if (RECONNECT_CONFIG.maxAttempts === 0) return;\n if (conversation.reconnectAttempt >= RECONNECT_CONFIG.maxAttempts) {\n this.emitReconnectEvent(conversation, 'exhausted');\n return;\n }\n\n // Calculate delay with exponential backoff and jitter\n const baseDelay = Math.min(\n RECONNECT_CONFIG.initialDelayMs *\n Math.pow(RECONNECT_CONFIG.backoffMultiplier, conversation.reconnectAttempt),\n RECONNECT_CONFIG.maxDelayMs,\n );\n \n // Add jitter\n const jitter = baseDelay * RECONNECT_CONFIG.jitterFactor * Math.random();\n const delay = Math.floor(baseDelay + jitter);\n\n conversation.reconnectAttempt++;\n this.emitReconnectEvent(conversation, 'scheduled', { delay, attempt: conversation.reconnectAttempt });\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 }, delay);\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 \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 \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 \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 let message: ServerMessage;\n try {\n message = JSON.parse(raw) as ServerMessage;\n } catch {\n return;\n }\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 };\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"],"mappings":"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","names":[]}
|