@wrongstack/desktop 0.284.0 → 0.285.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/main.js CHANGED
@@ -1,10 +1,6 @@
1
- import {
2
- DesktopAgentBridge
3
- } from "./chunk-AMPSW3AD.js";
4
-
5
1
  // src/main/main.ts
6
- import * as path3 from "path";
7
- import * as fs3 from "fs/promises";
2
+ import * as path3 from "node:path";
3
+ import * as fs3 from "node:fs/promises";
8
4
  import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
9
5
  import {
10
6
  app,
@@ -16,6 +12,359 @@ import {
16
12
  WebContentsView
17
13
  } from "electron";
18
14
 
15
+ // src/main/agent-bridge.ts
16
+ import { randomUUID } from "node:crypto";
17
+ import { EventEmitter } from "node:events";
18
+ import WebSocket from "ws";
19
+ var MAX_MESSAGES = 300;
20
+ var RECONNECT_CONFIG = {
21
+ /** Maximum number of reconnection attempts (0 = disabled) */
22
+ maxAttempts: 5,
23
+ /** Initial delay in ms before first reconnection */
24
+ initialDelayMs: 1e3,
25
+ /** Maximum delay in ms between reconnection attempts */
26
+ maxDelayMs: 3e4,
27
+ /** Multiplier for exponential backoff */
28
+ backoffMultiplier: 2,
29
+ /** Jitter factor (0-1) to add randomness to delays */
30
+ jitterFactor: 0.1
31
+ };
32
+ var DesktopAgentBridge = class extends EventEmitter {
33
+ conversations = /* @__PURE__ */ new Map();
34
+ snapshot(runtimeId) {
35
+ return publicConversation(this.getOrCreate(runtimeId));
36
+ }
37
+ /**
38
+ * Get reconnection status for a runtime.
39
+ */
40
+ getReconnectStatus(runtimeId) {
41
+ const conversation = this.conversations.get(runtimeId);
42
+ if (!conversation) return null;
43
+ return {
44
+ attempt: conversation.reconnectAttempt,
45
+ maxAttempts: RECONNECT_CONFIG.maxAttempts
46
+ };
47
+ }
48
+ /**
49
+ * Force reconnection for a runtime (resets reconnection state).
50
+ */
51
+ forceReconnect(runtimeId, wsUrl) {
52
+ const conversation = this.getOrCreate(runtimeId);
53
+ this.cancelReconnect(conversation);
54
+ conversation.reconnectAttempt = 0;
55
+ void this.ensureConnected(runtimeId, wsUrl);
56
+ }
57
+ async ensureConnected(runtimeId, wsUrl) {
58
+ const conversation = this.getOrCreate(runtimeId);
59
+ if (conversation.ws?.readyState === WebSocket.OPEN) return publicConversation(conversation);
60
+ if (conversation.connectPromise) {
61
+ await conversation.connectPromise;
62
+ return publicConversation(conversation);
63
+ }
64
+ if (conversation.reconnectTimer) {
65
+ return publicConversation(conversation);
66
+ }
67
+ conversation.reconnectUrl = wsUrl;
68
+ conversation.reconnectAttempt = 0;
69
+ await this.connect(runtimeId, wsUrl);
70
+ return publicConversation(conversation);
71
+ }
72
+ /**
73
+ * Internal connect method that actually establishes the WebSocket.
74
+ */
75
+ async connect(runtimeId, wsUrl) {
76
+ const conversation = this.getOrCreate(runtimeId);
77
+ this.cancelReconnect(conversation);
78
+ conversation.status = "connecting";
79
+ conversation.error = void 0;
80
+ this.emitChanged(conversation);
81
+ this.emitReconnectEvent(conversation, "connecting");
82
+ return new Promise((resolve2, reject) => {
83
+ const ws = new WebSocket(wsUrl);
84
+ conversation.ws = ws;
85
+ const timeout = setTimeout(() => {
86
+ if (conversation.ws === ws) {
87
+ ws.close();
88
+ reject(new Error("Connection timeout"));
89
+ }
90
+ }, 1e4);
91
+ ws.once("open", () => {
92
+ clearTimeout(timeout);
93
+ conversation.status = "connected";
94
+ conversation.error = void 0;
95
+ conversation.connectPromise = null;
96
+ conversation.reconnectAttempt = 0;
97
+ conversation.reconnectUrl = null;
98
+ this.emitChanged(conversation);
99
+ this.emitReconnectEvent(conversation, "connected");
100
+ resolve2();
101
+ });
102
+ ws.on("message", (data) => {
103
+ this.handleServerMessage(conversation, data.toString());
104
+ });
105
+ ws.once("error", (err) => {
106
+ clearTimeout(timeout);
107
+ conversation.status = "error";
108
+ conversation.error = err instanceof Error ? err.message : String(err);
109
+ conversation.connectPromise = null;
110
+ this.appendMessage(conversation, {
111
+ role: "system",
112
+ text: `Connection error: ${conversation.error}`
113
+ });
114
+ this.emitChanged(conversation);
115
+ this.emitReconnectEvent(conversation, "error");
116
+ reject(err);
117
+ });
118
+ ws.once("close", () => {
119
+ clearTimeout(timeout);
120
+ if (conversation.ws === ws) conversation.ws = null;
121
+ conversation.connectPromise = null;
122
+ if (conversation.status !== "error") {
123
+ conversation.status = "disconnected";
124
+ }
125
+ conversation.activeAssistantMessageId = null;
126
+ this.emitChanged(conversation);
127
+ if (conversation.reconnectUrl && conversation.status === "disconnected") {
128
+ this.scheduleReconnect(conversation);
129
+ }
130
+ });
131
+ });
132
+ }
133
+ /**
134
+ * Schedule a reconnection attempt with exponential backoff.
135
+ */
136
+ scheduleReconnect(conversation) {
137
+ if (RECONNECT_CONFIG.maxAttempts === 0) return;
138
+ if (conversation.reconnectAttempt >= RECONNECT_CONFIG.maxAttempts) {
139
+ this.emitReconnectEvent(conversation, "exhausted");
140
+ return;
141
+ }
142
+ const baseDelay = Math.min(
143
+ RECONNECT_CONFIG.initialDelayMs * Math.pow(RECONNECT_CONFIG.backoffMultiplier, conversation.reconnectAttempt),
144
+ RECONNECT_CONFIG.maxDelayMs
145
+ );
146
+ const jitter = baseDelay * RECONNECT_CONFIG.jitterFactor * Math.random();
147
+ const delay = Math.floor(baseDelay + jitter);
148
+ conversation.reconnectAttempt++;
149
+ this.emitReconnectEvent(conversation, "scheduled", { delay, attempt: conversation.reconnectAttempt });
150
+ conversation.reconnectTimer = setTimeout(() => {
151
+ conversation.reconnectTimer = null;
152
+ if (!conversation.reconnectUrl) return;
153
+ if (conversation.ws?.readyState !== WebSocket.OPEN) {
154
+ void this.connect(conversation.runtimeId, conversation.reconnectUrl);
155
+ }
156
+ }, delay);
157
+ }
158
+ /**
159
+ * Cancel pending reconnection.
160
+ */
161
+ cancelReconnect(conversation) {
162
+ if (conversation.reconnectTimer) {
163
+ clearTimeout(conversation.reconnectTimer);
164
+ conversation.reconnectTimer = null;
165
+ }
166
+ }
167
+ /**
168
+ * Emit reconnection event for UI feedback.
169
+ */
170
+ emitReconnectEvent(conversation, status, data) {
171
+ this.emit("reconnect", {
172
+ runtimeId: conversation.runtimeId,
173
+ status,
174
+ attempt: conversation.reconnectAttempt,
175
+ maxAttempts: RECONNECT_CONFIG.maxAttempts,
176
+ ...data
177
+ });
178
+ }
179
+ async sendMessage(runtimeId, wsUrl, content) {
180
+ const trimmed = content.trim();
181
+ if (!trimmed) return this.snapshot(runtimeId);
182
+ const conversation = this.getOrCreate(runtimeId);
183
+ conversation.reconnectAttempt = 0;
184
+ await this.ensureConnected(runtimeId, wsUrl);
185
+ const conv = this.getOrCreate(runtimeId);
186
+ this.appendMessage(conv, {
187
+ id: `user_${randomUUID()}`,
188
+ role: "user",
189
+ text: trimmed
190
+ });
191
+ conv.status = "running";
192
+ conv.activeAssistantMessageId = null;
193
+ this.emitChanged(conv);
194
+ this.send(conv, {
195
+ type: "user_message",
196
+ payload: {
197
+ id: `msg_${Date.now()}_${randomUUID().slice(0, 8)}`,
198
+ content: trimmed,
199
+ timestamp: Date.now(),
200
+ ...conv.sessionId ? { sessionId: conv.sessionId } : {}
201
+ }
202
+ });
203
+ return publicConversation(conv);
204
+ }
205
+ async abort(runtimeId, wsUrl) {
206
+ const conversation = this.getOrCreate(runtimeId);
207
+ conversation.reconnectAttempt = 0;
208
+ await this.ensureConnected(runtimeId, wsUrl);
209
+ const conv = this.getOrCreate(runtimeId);
210
+ this.send(conv, {
211
+ type: "abort",
212
+ payload: conv.sessionId ? { sessionId: conv.sessionId } : {}
213
+ });
214
+ conv.status = "connected";
215
+ this.appendMessage(conv, { role: "system", text: "Abort requested." });
216
+ return publicConversation(conv);
217
+ }
218
+ close(runtimeId) {
219
+ const conversation = this.conversations.get(runtimeId);
220
+ if (!conversation) return;
221
+ this.cancelReconnect(conversation);
222
+ conversation.reconnectAttempt = 0;
223
+ conversation.reconnectUrl = null;
224
+ conversation.ws?.close();
225
+ conversation.ws = null;
226
+ conversation.connectPromise = null;
227
+ conversation.status = "disconnected";
228
+ conversation.activeAssistantMessageId = null;
229
+ this.emitChanged(conversation);
230
+ }
231
+ closeAll() {
232
+ for (const runtimeId of this.conversations.keys()) {
233
+ this.close(runtimeId);
234
+ }
235
+ }
236
+ handleServerMessage(conversation, raw) {
237
+ let message;
238
+ try {
239
+ message = JSON.parse(raw);
240
+ } catch {
241
+ return;
242
+ }
243
+ const payload = message.payload ?? {};
244
+ switch (message.type) {
245
+ case "session.start": {
246
+ const sessionId = stringValue(payload["sessionId"]);
247
+ if (sessionId) conversation.sessionId = sessionId;
248
+ conversation.status = conversation.status === "running" ? "running" : "connected";
249
+ this.emitChanged(conversation);
250
+ break;
251
+ }
252
+ case "provider.text_delta": {
253
+ conversation.status = "running";
254
+ this.appendAssistantDelta(conversation, stringValue(payload["text"]) ?? "");
255
+ break;
256
+ }
257
+ case "tool.started": {
258
+ conversation.status = "running";
259
+ const name = stringValue(payload["name"]) ?? "tool";
260
+ this.appendMessage(conversation, { role: "tool", text: `Started ${name}` });
261
+ break;
262
+ }
263
+ case "tool.executed": {
264
+ const name = stringValue(payload["name"]) ?? "tool";
265
+ const ok = payload["ok"] === true;
266
+ this.appendMessage(conversation, {
267
+ role: "tool",
268
+ text: `${name} ${ok ? "completed" : "failed"}`
269
+ });
270
+ break;
271
+ }
272
+ case "provider.error":
273
+ case "provider.stream_error":
274
+ case "error": {
275
+ const text = stringValue(payload["message"]) ?? stringValue(payload["description"]) ?? `${message.type} received`;
276
+ conversation.status = "error";
277
+ conversation.error = text;
278
+ this.appendMessage(conversation, { role: "system", text });
279
+ break;
280
+ }
281
+ case "run.result": {
282
+ const finalText = stringValue(payload["finalText"]);
283
+ if (finalText && !this.lastAssistantHasText(conversation)) {
284
+ this.appendMessage(conversation, { role: "assistant", text: finalText });
285
+ }
286
+ conversation.status = payload["status"] === "failed" ? "error" : "connected";
287
+ conversation.activeAssistantMessageId = null;
288
+ this.emitChanged(conversation);
289
+ break;
290
+ }
291
+ }
292
+ }
293
+ send(conversation, message) {
294
+ if (conversation.ws?.readyState !== WebSocket.OPEN) {
295
+ throw new Error("Runtime socket is not connected");
296
+ }
297
+ conversation.ws.send(JSON.stringify(message));
298
+ }
299
+ appendAssistantDelta(conversation, text) {
300
+ if (!text) return;
301
+ let message = conversation.messages.find((m) => m.id === conversation.activeAssistantMessageId);
302
+ if (!message) {
303
+ message = {
304
+ id: `assistant_${randomUUID()}`,
305
+ role: "assistant",
306
+ text: "",
307
+ timestamp: Date.now()
308
+ };
309
+ conversation.activeAssistantMessageId = message.id;
310
+ conversation.messages.push(message);
311
+ }
312
+ message.text += text;
313
+ this.trimMessages(conversation);
314
+ this.emitChanged(conversation);
315
+ }
316
+ appendMessage(conversation, input) {
317
+ conversation.messages.push({
318
+ id: input.id ?? `${input.role}_${randomUUID()}`,
319
+ role: input.role,
320
+ text: input.text,
321
+ timestamp: input.timestamp ?? Date.now()
322
+ });
323
+ this.trimMessages(conversation);
324
+ this.emitChanged(conversation);
325
+ }
326
+ lastAssistantHasText(conversation) {
327
+ const lastAssistant = [...conversation.messages].reverse().find((m) => m.role === "assistant");
328
+ return Boolean(lastAssistant?.text.trim());
329
+ }
330
+ trimMessages(conversation) {
331
+ if (conversation.messages.length <= MAX_MESSAGES) return;
332
+ conversation.messages.splice(0, conversation.messages.length - MAX_MESSAGES);
333
+ }
334
+ getOrCreate(runtimeId) {
335
+ let conversation = this.conversations.get(runtimeId);
336
+ if (conversation) return conversation;
337
+ conversation = {
338
+ runtimeId,
339
+ status: "disconnected",
340
+ messages: [],
341
+ ws: null,
342
+ connectPromise: null,
343
+ activeAssistantMessageId: null,
344
+ reconnectAttempt: 0,
345
+ reconnectTimer: null,
346
+ reconnectUrl: null
347
+ };
348
+ this.conversations.set(runtimeId, conversation);
349
+ return conversation;
350
+ }
351
+ emitChanged(conversation) {
352
+ this.emit("changed", publicConversation(conversation));
353
+ }
354
+ };
355
+ function publicConversation(conversation) {
356
+ return {
357
+ runtimeId: conversation.runtimeId,
358
+ status: conversation.status,
359
+ sessionId: conversation.sessionId,
360
+ error: conversation.error,
361
+ messages: conversation.messages.map((message) => ({ ...message }))
362
+ };
363
+ }
364
+ function stringValue(value) {
365
+ return typeof value === "string" ? value : void 0;
366
+ }
367
+
19
368
  // src/main/ipc.ts
20
369
  var IPC = {
21
370
  getState: "desktop:get-state",
@@ -352,8 +701,8 @@ function tMain(key) {
352
701
  }
353
702
 
354
703
  // src/main/desktop-config-io.ts
355
- import * as fs from "fs/promises";
356
- import * as path from "path";
704
+ import * as fs from "node:fs/promises";
705
+ import * as path from "node:path";
357
706
  import { DefaultSecretVault } from "@wrongstack/core";
358
707
  import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
359
708
  import { atomicWrite, wstackGlobalRoot } from "@wrongstack/core/utils";
@@ -400,24 +749,24 @@ async function writeUiLocale(code) {
400
749
  }
401
750
 
402
751
  // src/main/runtime-manager.ts
403
- import { spawn } from "child_process";
404
- import { randomBytes } from "crypto";
405
- import { EventEmitter } from "events";
406
- import * as fs2 from "fs/promises";
407
- import { existsSync } from "fs";
408
- import * as http from "http";
409
- import { createRequire } from "module";
410
- import * as net from "net";
411
- import * as os from "os";
412
- import * as path2 from "path";
413
- import { fileURLToPath } from "url";
752
+ import { spawn } from "node:child_process";
753
+ import { randomBytes } from "node:crypto";
754
+ import { EventEmitter as EventEmitter2 } from "node:events";
755
+ import * as fs2 from "node:fs/promises";
756
+ import { existsSync } from "node:fs";
757
+ import * as http from "node:http";
758
+ import { createRequire } from "node:module";
759
+ import * as net from "node:net";
760
+ import * as os from "node:os";
761
+ import * as path2 from "node:path";
762
+ import { fileURLToPath } from "node:url";
414
763
  import { atomicWrite as atomicWrite2, projectSlug, toErrorMessage, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
415
764
  var HTTP_PORT_START = 34560;
416
765
  var WS_PORT_START = 34660;
417
766
  var START_TIMEOUT_MS = 3e4;
418
767
  var MIN_WINDOW_WIDTH = 760;
419
768
  var MIN_WINDOW_HEIGHT = 520;
420
- var DesktopRuntimeManager = class extends EventEmitter {
769
+ var DesktopRuntimeManager = class extends EventEmitter2 {
421
770
  runtimes = /* @__PURE__ */ new Map();
422
771
  stateFile = path2.join(wstackGlobalRoot2(), "desktop.json");
423
772
  recentProjects = [];
@@ -1331,7 +1680,7 @@ function getSidebarWidth(windowWidth, collapsed) {
1331
1680
  import { Menu } from "electron";
1332
1681
 
1333
1682
  // src/main/menu/projects-menu.ts
1334
- var posix = (await import("path")).posix;
1683
+ var posix = (await import("node:path")).posix;
1335
1684
  function buildProjectsMenu(runtimes, actions, t) {
1336
1685
  const projectGroups = groupProjectRuntimesForMenu(runtimes);
1337
1686
  const menu = [
@@ -2454,4 +2803,4 @@ app.on("activate", () => {
2454
2803
  mainWindow.show();
2455
2804
  shellView?.webContents.focus();
2456
2805
  });
2457
- //# sourceMappingURL=main.js.map
2806
+ //# sourceMappingURL=main.js.map