@wrongstack/desktop 0.293.0 → 0.296.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.
package/dist/main/main.js CHANGED
@@ -1,16 +1,13 @@
1
1
  // src/main/main.ts
2
- import * as path4 from "node:path";
3
- import * as fs3 from "node:fs/promises";
4
- import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
2
+ import * as path5 from "node:path";
3
+ import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
5
4
  import {
6
5
  app as app2,
7
6
  BaseWindow,
8
7
  dialog,
9
- ipcMain as ipcMain2,
10
- nativeImage,
11
8
  screen,
12
- shell,
13
- WebContentsView
9
+ shell as shell2,
10
+ WebContentsView as WebContentsView2
14
11
  } from "electron";
15
12
 
16
13
  // src/main/macos-platform.ts
@@ -43,9 +40,9 @@ function handleFileOpen(filePath) {
43
40
  }
44
41
  var pendingOpenFilePath = null;
45
42
  function drainPendingOpenFilePath() {
46
- const path5 = pendingOpenFilePath;
43
+ const path6 = pendingOpenFilePath;
47
44
  pendingOpenFilePath = null;
48
- return path5;
45
+ return path6;
49
46
  }
50
47
  function firstOpenFileArg(argv) {
51
48
  if (process.platform !== "darwin") return null;
@@ -62,6 +59,17 @@ function firstOpenFileArg(argv) {
62
59
  // src/main/agent-bridge.ts
63
60
  import { randomUUID } from "node:crypto";
64
61
  import { EventEmitter } from "node:events";
62
+ import {
63
+ createSurfaceConnectionState,
64
+ DEFAULT_SURFACE_CONNECTION_CONFIG,
65
+ decodeProtocolFrame,
66
+ markConnectionActivity,
67
+ markConnectionConnecting,
68
+ markConnectionOpen,
69
+ planConnectionReconnect,
70
+ resetConnection,
71
+ stopConnection
72
+ } from "@wrongstack/webui-server/protocol";
65
73
  import WebSocket from "ws";
66
74
  var MAX_MESSAGES = 300;
67
75
  var RECONNECT_CONFIG = {
@@ -76,6 +84,14 @@ var RECONNECT_CONFIG = {
76
84
  /** Jitter factor (0-1) to add randomness to delays */
77
85
  jitterFactor: 0.1
78
86
  };
87
+ var DESKTOP_CONNECTION_CONFIG = {
88
+ ...DEFAULT_SURFACE_CONNECTION_CONFIG,
89
+ maxReconnectAttempts: RECONNECT_CONFIG.maxAttempts,
90
+ initialBackoffMs: RECONNECT_CONFIG.initialDelayMs,
91
+ maxBackoffMs: RECONNECT_CONFIG.maxDelayMs,
92
+ backoffMultiplier: RECONNECT_CONFIG.backoffMultiplier,
93
+ jitterRatio: RECONNECT_CONFIG.jitterFactor
94
+ };
79
95
  var DesktopAgentBridge = class extends EventEmitter {
80
96
  conversations = /* @__PURE__ */ new Map();
81
97
  snapshot(runtimeId) {
@@ -99,6 +115,7 @@ var DesktopAgentBridge = class extends EventEmitter {
99
115
  const conversation = this.getOrCreate(runtimeId);
100
116
  this.cancelReconnect(conversation);
101
117
  conversation.reconnectAttempt = 0;
118
+ conversation.connectionState = resetConnection(conversation.connectionState);
102
119
  void this.ensureConnected(runtimeId, wsUrl);
103
120
  }
104
121
  async ensureConnected(runtimeId, wsUrl) {
@@ -113,6 +130,7 @@ var DesktopAgentBridge = class extends EventEmitter {
113
130
  }
114
131
  conversation.reconnectUrl = wsUrl;
115
132
  conversation.reconnectAttempt = 0;
133
+ conversation.connectionState = resetConnection(conversation.connectionState);
116
134
  await this.connect(runtimeId, wsUrl);
117
135
  return publicConversation(conversation);
118
136
  }
@@ -123,10 +141,11 @@ var DesktopAgentBridge = class extends EventEmitter {
123
141
  const conversation = this.getOrCreate(runtimeId);
124
142
  this.cancelReconnect(conversation);
125
143
  conversation.status = "connecting";
144
+ conversation.connectionState = markConnectionConnecting(conversation.connectionState);
126
145
  conversation.error = void 0;
127
146
  this.emitChanged(conversation);
128
147
  this.emitReconnectEvent(conversation, "connecting");
129
- return new Promise((resolve2, reject) => {
148
+ return new Promise((resolve3, reject) => {
130
149
  const ws = new WebSocket(wsUrl);
131
150
  conversation.ws = ws;
132
151
  const timeout = setTimeout(() => {
@@ -141,10 +160,10 @@ var DesktopAgentBridge = class extends EventEmitter {
141
160
  conversation.error = void 0;
142
161
  conversation.connectPromise = null;
143
162
  conversation.reconnectAttempt = 0;
144
- conversation.reconnectUrl = null;
163
+ conversation.connectionState = markConnectionOpen(conversation.connectionState);
145
164
  this.emitChanged(conversation);
146
165
  this.emitReconnectEvent(conversation, "connected");
147
- resolve2();
166
+ resolve3();
148
167
  });
149
168
  ws.on("message", (data) => {
150
169
  this.handleServerMessage(conversation, data.toString());
@@ -181,26 +200,27 @@ var DesktopAgentBridge = class extends EventEmitter {
181
200
  * Schedule a reconnection attempt with exponential backoff.
182
201
  */
183
202
  scheduleReconnect(conversation) {
184
- if (RECONNECT_CONFIG.maxAttempts === 0) return;
185
- if (conversation.reconnectAttempt >= RECONNECT_CONFIG.maxAttempts) {
203
+ const reconnect = planConnectionReconnect(
204
+ conversation.connectionState,
205
+ DESKTOP_CONNECTION_CONFIG
206
+ );
207
+ conversation.connectionState = reconnect.state;
208
+ conversation.reconnectAttempt = reconnect.state.reconnectAttempt;
209
+ if (!reconnect.plan) {
186
210
  this.emitReconnectEvent(conversation, "exhausted");
187
211
  return;
188
212
  }
189
- const baseDelay = Math.min(
190
- RECONNECT_CONFIG.initialDelayMs * RECONNECT_CONFIG.backoffMultiplier ** conversation.reconnectAttempt,
191
- RECONNECT_CONFIG.maxDelayMs
192
- );
193
- const jitter = baseDelay * RECONNECT_CONFIG.jitterFactor * Math.random();
194
- const delay = Math.floor(baseDelay + jitter);
195
- conversation.reconnectAttempt++;
196
- this.emitReconnectEvent(conversation, "scheduled", { delay, attempt: conversation.reconnectAttempt });
213
+ this.emitReconnectEvent(conversation, "scheduled", {
214
+ delay: reconnect.plan.delayMs,
215
+ attempt: reconnect.plan.attempt
216
+ });
197
217
  conversation.reconnectTimer = setTimeout(() => {
198
218
  conversation.reconnectTimer = null;
199
219
  if (!conversation.reconnectUrl) return;
200
220
  if (conversation.ws?.readyState !== WebSocket.OPEN) {
201
221
  void this.connect(conversation.runtimeId, conversation.reconnectUrl);
202
222
  }
203
- }, delay);
223
+ }, reconnect.plan.delayMs);
204
224
  }
205
225
  /**
206
226
  * Cancel pending reconnection.
@@ -228,6 +248,7 @@ var DesktopAgentBridge = class extends EventEmitter {
228
248
  if (!trimmed) return this.snapshot(runtimeId);
229
249
  const conversation = this.getOrCreate(runtimeId);
230
250
  conversation.reconnectAttempt = 0;
251
+ conversation.connectionState = resetConnection(conversation.connectionState);
231
252
  await this.ensureConnected(runtimeId, wsUrl);
232
253
  const conv = this.getOrCreate(runtimeId);
233
254
  this.appendMessage(conv, {
@@ -252,6 +273,7 @@ var DesktopAgentBridge = class extends EventEmitter {
252
273
  async abort(runtimeId, wsUrl) {
253
274
  const conversation = this.getOrCreate(runtimeId);
254
275
  conversation.reconnectAttempt = 0;
276
+ conversation.connectionState = resetConnection(conversation.connectionState);
255
277
  await this.ensureConnected(runtimeId, wsUrl);
256
278
  const conv = this.getOrCreate(runtimeId);
257
279
  this.send(conv, {
@@ -268,6 +290,7 @@ var DesktopAgentBridge = class extends EventEmitter {
268
290
  this.cancelReconnect(conversation);
269
291
  conversation.reconnectAttempt = 0;
270
292
  conversation.reconnectUrl = null;
293
+ conversation.connectionState = stopConnection(conversation.connectionState);
271
294
  conversation.ws?.close();
272
295
  conversation.ws = null;
273
296
  conversation.connectPromise = null;
@@ -281,12 +304,10 @@ var DesktopAgentBridge = class extends EventEmitter {
281
304
  }
282
305
  }
283
306
  handleServerMessage(conversation, raw) {
284
- let message;
285
- try {
286
- message = JSON.parse(raw);
287
- } catch {
288
- return;
289
- }
307
+ const decoded = decodeProtocolFrame(raw, "server");
308
+ if (!decoded.ok) return;
309
+ conversation.connectionState = markConnectionActivity(conversation.connectionState);
310
+ const message = decoded.message;
290
311
  const payload = message.payload ?? {};
291
312
  switch (message.type) {
292
313
  case "session.start": {
@@ -390,7 +411,8 @@ var DesktopAgentBridge = class extends EventEmitter {
390
411
  activeAssistantMessageId: null,
391
412
  reconnectAttempt: 0,
392
413
  reconnectTimer: null,
393
- reconnectUrl: null
414
+ reconnectUrl: null,
415
+ connectionState: createSurfaceConnectionState()
394
416
  };
395
417
  this.conversations.set(runtimeId, conversation);
396
418
  return conversation;
@@ -412,6 +434,73 @@ function stringValue(value) {
412
434
  return typeof value === "string" ? value : void 0;
413
435
  }
414
436
 
437
+ // src/main/desktop-privileged-actions.ts
438
+ import { randomUUID as randomUUID2 } from "node:crypto";
439
+ import {
440
+ createCompatibilityTrustBoundary,
441
+ isTrustDecisionAllowed
442
+ } from "@wrongstack/core/security";
443
+ var desktopCompatibilityTrustBoundary = createCompatibilityTrustBoundary({
444
+ policyId: "desktop-trusted-host-compat-v1"
445
+ });
446
+ async function authorizeDesktopAction(boundary, action, logger) {
447
+ const request = {
448
+ version: 1,
449
+ requestId: randomUUID2(),
450
+ actor: { kind: "user", id: "desktop-user" },
451
+ surface: "desktop",
452
+ capability: action.capability,
453
+ subject: action.subject,
454
+ risk: action.risk,
455
+ scope: action.cwd ? { cwd: action.cwd } : {},
456
+ authContext: { method: "local-process", principalId: "desktop-user" },
457
+ ...action.metadata ? { metadata: action.metadata } : {}
458
+ };
459
+ const decision = await boundary.evaluate(request);
460
+ const auditEntry = {
461
+ event: "desktop.trust_boundary.decision",
462
+ requestId: request.requestId,
463
+ capability: request.capability,
464
+ decision: decision.kind,
465
+ policyId: decision.policyId
466
+ };
467
+ if (logger) {
468
+ logger.info("Trust boundary decision", auditEntry);
469
+ } else {
470
+ console.warn(JSON.stringify({ level: "info", ...auditEntry, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
471
+ }
472
+ return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason };
473
+ }
474
+ function authorizeDesktopRuntimeStart(boundary, cwd, runtimeKind) {
475
+ return authorizeDesktopAction(boundary, {
476
+ capability: "process.spawn",
477
+ subject: {
478
+ kind: "command",
479
+ id: "wrongstack-webui-runtime",
480
+ attributes: { runtimeKind }
481
+ },
482
+ risk: "high",
483
+ cwd,
484
+ metadata: { operation: "open-project" }
485
+ });
486
+ }
487
+ function authorizeDesktopRuntimeStop(boundary, runtime) {
488
+ return authorizeDesktopAction(boundary, {
489
+ capability: "process.terminate",
490
+ subject: {
491
+ kind: "process",
492
+ id: runtime.id,
493
+ attributes: {
494
+ ...runtime.pid !== void 0 ? { pid: runtime.pid } : {},
495
+ runtimeId: runtime.id
496
+ }
497
+ },
498
+ risk: "high",
499
+ cwd: runtime.root,
500
+ metadata: { operation: "close-runtime" }
501
+ });
502
+ }
503
+
415
504
  // src/main/ipc.ts
416
505
  var IPC = {
417
506
  getState: "desktop:get-state",
@@ -754,36 +843,53 @@ function tMain(key) {
754
843
  // src/main/desktop-config-io.ts
755
844
  import * as fs from "node:fs/promises";
756
845
  import * as path from "node:path";
757
- import { DefaultSecretVault } from "@wrongstack/core";
758
- import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
846
+ import { decryptConfigSecrets, DefaultSecretVault, encryptConfigSecrets } from "@wrongstack/core/security";
759
847
  import { atomicWrite, wstackGlobalRoot } from "@wrongstack/core/utils";
760
- var globalConfigPath = path.join(wstackGlobalRoot(), "config.json");
848
+ var globalRoot = wstackGlobalRoot();
849
+ var bootstrapConfigPath = path.join(globalRoot, "config.json");
850
+ var defaultProfileConfigPath = path.join(globalRoot, "profiles", "default", "config.json");
761
851
  var vault = new DefaultSecretVault({
762
- keyFile: path.join(wstackGlobalRoot(), ".key")
852
+ keyFile: path.join(globalRoot, ".key")
763
853
  });
854
+ function safeProfileName(value) {
855
+ if (typeof value !== "string" || !value.trim()) return "default";
856
+ return value.replace(/[/\\:]/g, "_").replace(/\.\./g, "_") || "default";
857
+ }
858
+ async function resolveActiveProfileConfigPath() {
859
+ try {
860
+ const raw = await fs.readFile(bootstrapConfigPath, "utf8");
861
+ const parsed = JSON.parse(raw);
862
+ return path.join(globalRoot, "profiles", safeProfileName(parsed.activeProfile), "config.json");
863
+ } catch {
864
+ return defaultProfileConfigPath;
865
+ }
866
+ }
764
867
  async function readUiLocale() {
868
+ const profileConfigPath = await resolveActiveProfileConfigPath();
765
869
  let raw;
766
870
  try {
767
- raw = await fs.readFile(globalConfigPath, "utf8");
871
+ raw = await fs.readFile(profileConfigPath, "utf8");
768
872
  } catch {
769
873
  return void 0;
770
874
  }
771
875
  try {
772
- const decrypted = decryptConfigSecrets(
773
- JSON.parse(raw),
774
- vault
775
- );
876
+ const decrypted = decryptConfigSecrets(JSON.parse(raw), vault);
776
877
  const value = decrypted.uiLocale;
777
878
  return typeof value === "string" && value ? value : void 0;
778
879
  } catch {
779
880
  return void 0;
780
881
  }
781
882
  }
782
- var desktopConfigPaths = { globalConfigPath, vault };
883
+ var desktopConfigPaths = {
884
+ bootstrapConfigPath,
885
+ profileConfigPath: defaultProfileConfigPath,
886
+ vault
887
+ };
783
888
  async function writeUiLocale(code) {
889
+ const profileConfigPath = await resolveActiveProfileConfigPath();
784
890
  let raw;
785
891
  try {
786
- raw = await fs.readFile(globalConfigPath, "utf8");
892
+ raw = await fs.readFile(profileConfigPath, "utf8");
787
893
  } catch {
788
894
  raw = "{}";
789
895
  }
@@ -796,30 +902,90 @@ async function writeUiLocale(code) {
796
902
  const decrypted = decryptConfigSecrets(parsed, vault);
797
903
  decrypted.uiLocale = code;
798
904
  const encrypted = encryptConfigSecrets(decrypted, vault);
799
- await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
905
+ await fs.mkdir(path.dirname(profileConfigPath), { recursive: true });
906
+ await atomicWrite(profileConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
800
907
  }
801
908
 
802
909
  // src/main/runtime-manager.ts
803
910
  import { spawn } from "node:child_process";
804
911
  import { randomBytes } from "node:crypto";
805
912
  import { EventEmitter as EventEmitter2 } from "node:events";
806
- import { existsSync } from "node:fs";
807
913
  import * as fs2 from "node:fs/promises";
808
914
  import * as http from "node:http";
809
- import { createRequire } from "node:module";
810
915
  import * as net from "node:net";
811
916
  import * as os from "node:os";
917
+ import * as path3 from "node:path";
918
+ import {
919
+ atomicWrite as atomicWrite2,
920
+ buildChildEnv,
921
+ projectSlug,
922
+ resolveWstackPaths as resolveWstackPaths2,
923
+ toErrorMessage,
924
+ wstackGlobalRoot as wstackGlobalRoot2
925
+ } from "@wrongstack/core/utils";
926
+
927
+ // src/main/runtime-manager-paths.ts
928
+ import { existsSync } from "node:fs";
929
+ import { createRequire } from "node:module";
812
930
  import * as path2 from "node:path";
813
931
  import { fileURLToPath } from "node:url";
814
- import { atomicWrite as atomicWrite2, projectSlug, toErrorMessage, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
932
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
933
+ function resolveWebUiEntry() {
934
+ if (process.env["WRONGSTACK_WEBUI_ENTRY"]) {
935
+ return path2.resolve(process.env["WRONGSTACK_WEBUI_ENTRY"]);
936
+ }
937
+ const require2 = createRequire(import.meta.url);
938
+ try {
939
+ const serverPkgPath = require2.resolve("@wrongstack/webui-server/package.json");
940
+ const candidate = path2.join(path2.dirname(serverPkgPath), "dist", "server", "entry.js");
941
+ if (existsSync(candidate)) return candidate;
942
+ } catch {
943
+ }
944
+ const serverIndex = require2.resolve("@wrongstack/webui-server");
945
+ return path2.join(path2.dirname(serverIndex), "server", "entry.js");
946
+ }
947
+ function resolveWebUiDistDir() {
948
+ if (process.env["WRONGSTACK_WEBUI_DIST"]) {
949
+ return path2.resolve(process.env["WRONGSTACK_WEBUI_DIST"]);
950
+ }
951
+ const require2 = createRequire(import.meta.url);
952
+ const serverEntry = require2.resolve("@wrongstack/webui");
953
+ const candidate = path2.dirname(serverEntry);
954
+ if (existsSync(candidate)) return candidate;
955
+ throw new Error(
956
+ `WebUI frontend assets not found at ${candidate}. Build @wrongstack/webui or set WRONGSTACK_WEBUI_DIST.`
957
+ );
958
+ }
959
+ function rendererIndexPath() {
960
+ return new URL("../renderer/index.html", import.meta.url).href;
961
+ }
962
+ function preloadPath() {
963
+ return fileURLToPath(new URL("../preload/preload.cjs", import.meta.url));
964
+ }
965
+ function webuiPreloadPath() {
966
+ return fileURLToPath(new URL("../preload/webui-preload.cjs", import.meta.url));
967
+ }
968
+ function desktopSettingsWorkspaceRoot() {
969
+ return path2.join(resolveWstackPaths({ projectRoot: process.cwd() }).configDir, "settings");
970
+ }
971
+
972
+ // src/main/runtime-manager.ts
815
973
  var HTTP_PORT_START = 34560;
816
974
  var WS_PORT_START = 34660;
817
975
  var START_TIMEOUT_MS = 3e4;
818
976
  var MIN_WINDOW_WIDTH = 760;
819
977
  var MIN_WINDOW_HEIGHT = 520;
820
978
  var DesktopRuntimeManager = class extends EventEmitter2 {
979
+ constructor(trustBoundary = desktopCompatibilityTrustBoundary) {
980
+ super();
981
+ this.trustBoundary = trustBoundary;
982
+ }
983
+ trustBoundary;
821
984
  runtimes = /* @__PURE__ */ new Map();
822
- stateFile = path2.join(wstackGlobalRoot2(), "desktop.json");
985
+ stateFile = path3.join(
986
+ resolveWstackPaths2({ projectRoot: process.cwd() }).configDir,
987
+ "desktop.json"
988
+ );
823
989
  recentProjects = [];
824
990
  registeredProjects = [];
825
991
  restoreProjectSessions = [];
@@ -924,12 +1090,16 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
924
1090
  return url.toString();
925
1091
  }
926
1092
  async openProject(projectRoot, options = {}) {
927
- const resolved = path2.resolve(projectRoot);
1093
+ const resolved = path3.resolve(projectRoot);
928
1094
  const stat3 = await fs2.stat(resolved).catch(() => null);
929
1095
  if (!stat3?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
930
1096
  const kind = options.kind ?? "project";
931
1097
  const touchRecent = options.touchRecent ?? kind === "project";
932
1098
  const forceNew = options.forceNew === true;
1099
+ const authorization = await authorizeDesktopRuntimeStart(this.trustBoundary, resolved, kind);
1100
+ if (!authorization.allowed) {
1101
+ throw new Error(`Desktop runtime start denied: ${authorization.reason}`);
1102
+ }
933
1103
  if (!forceNew) {
934
1104
  const existing = Array.from(this.runtimes.values()).find(
935
1105
  (runtime2) => samePath(runtime2.root, resolved) && runtime2.kind === kind && (runtime2.status === "starting" || runtime2.status === "running")
@@ -1009,7 +1179,7 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1009
1179
  {
1010
1180
  cwd: resolved,
1011
1181
  env: {
1012
- ...process.env,
1182
+ ...buildChildEnv(),
1013
1183
  ELECTRON_RUN_AS_NODE: "1",
1014
1184
  WEBUI_STRICT_PORT: "1",
1015
1185
  WRONGSTACK_DESKTOP: "1"
@@ -1088,6 +1258,13 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1088
1258
  this.emitChanged();
1089
1259
  }
1090
1260
  async closeRuntime(id) {
1261
+ const runtime = this.runtimes.get(id);
1262
+ if (runtime) {
1263
+ const authorization = await authorizeDesktopRuntimeStop(this.trustBoundary, runtime);
1264
+ if (!authorization.allowed) {
1265
+ throw new Error(`Desktop runtime stop denied: ${authorization.reason}`);
1266
+ }
1267
+ }
1091
1268
  await this.closeRuntimeInternal(id, { persistWorkspace: true });
1092
1269
  }
1093
1270
  async closeAll(options = {}) {
@@ -1099,12 +1276,12 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1099
1276
  );
1100
1277
  }
1101
1278
  async registerProject(projectRoot) {
1102
- const resolved = path2.resolve(projectRoot);
1279
+ const resolved = path3.resolve(projectRoot);
1103
1280
  const stat3 = await fs2.stat(resolved).catch(() => null);
1104
1281
  if (!stat3?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
1105
1282
  const now = (/* @__PURE__ */ new Date()).toISOString();
1106
1283
  const entry = {
1107
- name: path2.basename(resolved) || resolved,
1284
+ name: path3.basename(resolved) || resolved,
1108
1285
  root: resolved,
1109
1286
  slug: projectSlug(resolved),
1110
1287
  lastSeen: now,
@@ -1114,7 +1291,7 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1114
1291
  this.emitChanged();
1115
1292
  }
1116
1293
  async unregisterProject(projectRoot) {
1117
- const resolved = path2.resolve(projectRoot);
1294
+ const resolved = path3.resolve(projectRoot);
1118
1295
  this.registeredProjects = await removeGlobalProjectManifest(resolved);
1119
1296
  await this.saveDesktopState();
1120
1297
  this.emitChanged();
@@ -1141,10 +1318,10 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1141
1318
  this.emitChanged();
1142
1319
  }
1143
1320
  async touchProject(projectRoot) {
1144
- const resolved = path2.resolve(projectRoot);
1321
+ const resolved = path3.resolve(projectRoot);
1145
1322
  const now = (/* @__PURE__ */ new Date()).toISOString();
1146
1323
  const entry = {
1147
- name: path2.basename(resolved) || resolved,
1324
+ name: path3.basename(resolved) || resolved,
1148
1325
  root: resolved,
1149
1326
  slug: projectSlug(resolved),
1150
1327
  lastSeen: now,
@@ -1177,7 +1354,7 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1177
1354
  openProjects,
1178
1355
  openProjectSessions,
1179
1356
  activeRuntimeId: normalizeRuntimeId(parsed.activeRuntimeId) ?? null,
1180
- activeProjectRoot: typeof parsed.activeProjectRoot === "string" && parsed.activeProjectRoot.trim() ? path2.resolve(parsed.activeProjectRoot) : null,
1357
+ activeProjectRoot: typeof parsed.activeProjectRoot === "string" && parsed.activeProjectRoot.trim() ? path3.resolve(parsed.activeProjectRoot) : null,
1181
1358
  window: normalizeWindowState(parsed.window)
1182
1359
  };
1183
1360
  } catch {
@@ -1192,7 +1369,7 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1192
1369
  }
1193
1370
  }
1194
1371
  async saveDesktopState() {
1195
- await fs2.mkdir(path2.dirname(this.stateFile), { recursive: true });
1372
+ await fs2.mkdir(path3.dirname(this.stateFile), { recursive: true });
1196
1373
  const liveProjectSessions = Array.from(this.runtimes.values()).filter((runtime) => runtime.status !== "stopped" && runtime.kind === "project").map((runtime) => runtimeToSessionState(runtime));
1197
1374
  const openProjectSessions = liveProjectSessions.length === 0 && !this.workspaceRestoreCompleted ? [...this.restoreProjectSessions] : liveProjectSessions;
1198
1375
  const openProjects = openProjectSessions.map((session) => session.root);
@@ -1238,14 +1415,14 @@ function hasChildExited(child) {
1238
1415
  }
1239
1416
  function waitForChildExit(child, timeoutMs) {
1240
1417
  if (hasChildExited(child)) return Promise.resolve(true);
1241
- return new Promise((resolve2) => {
1418
+ return new Promise((resolve3) => {
1242
1419
  let settled = false;
1243
1420
  const finish = (exited) => {
1244
1421
  if (settled) return;
1245
1422
  settled = true;
1246
1423
  clearTimeout(timer);
1247
1424
  child.off("exit", onExit);
1248
- resolve2(exited);
1425
+ resolve3(exited);
1249
1426
  };
1250
1427
  const onExit = () => finish(true);
1251
1428
  const timer = setTimeout(() => finish(hasChildExited(child)), timeoutMs);
@@ -1271,12 +1448,12 @@ async function terminateProcessTree(child) {
1271
1448
  }
1272
1449
  return;
1273
1450
  }
1274
- await new Promise((resolve2) => {
1451
+ await new Promise((resolve3) => {
1275
1452
  let settled = false;
1276
1453
  const finish = () => {
1277
1454
  if (settled) return;
1278
1455
  settled = true;
1279
- resolve2();
1456
+ resolve3();
1280
1457
  };
1281
1458
  const timer = setTimeout(finish, 3e3);
1282
1459
  timer.unref?.();
@@ -1334,7 +1511,7 @@ function normalizePathList(value) {
1334
1511
  const roots = [];
1335
1512
  for (const item of value) {
1336
1513
  if (typeof item !== "string" || !item.trim()) continue;
1337
- const resolved = path2.resolve(item);
1514
+ const resolved = path3.resolve(item);
1338
1515
  roots.push(resolved);
1339
1516
  }
1340
1517
  return roots.slice(0, 12);
@@ -1349,7 +1526,7 @@ function normalizeSessionStateList(value, fallbackRoots) {
1349
1526
  const candidate = item;
1350
1527
  if (typeof candidate.root !== "string" || !candidate.root.trim()) continue;
1351
1528
  const session = {
1352
- root: path2.resolve(candidate.root)
1529
+ root: path3.resolve(candidate.root)
1353
1530
  };
1354
1531
  const runtimeId = normalizeRuntimeId(candidate.runtimeId);
1355
1532
  if (runtimeId) session.runtimeId = runtimeId;
@@ -1402,7 +1579,7 @@ function usedPorts(runtimes) {
1402
1579
  return ports;
1403
1580
  }
1404
1581
  function nextRuntimeName(runtimes, root, kind) {
1405
- const baseName = path2.basename(root) || root;
1582
+ const baseName = path3.basename(root) || root;
1406
1583
  if (kind !== "project") return baseName;
1407
1584
  const liveSameRoot = Array.from(runtimes.values()).filter(
1408
1585
  (runtime) => runtime.kind === "project" && samePath(runtime.root, root) && runtime.status !== "stopped"
@@ -1410,7 +1587,7 @@ function nextRuntimeName(runtimes, root, kind) {
1410
1587
  return liveSameRoot === 0 ? baseName : `${baseName} #${liveSameRoot + 1}`;
1411
1588
  }
1412
1589
  function pathKey(value) {
1413
- const resolved = path2.resolve(value);
1590
+ const resolved = path3.resolve(value);
1414
1591
  return os.platform() === "win32" ? resolved.toLowerCase() : resolved;
1415
1592
  }
1416
1593
  async function findFreePort(startPort, exclude) {
@@ -1421,11 +1598,11 @@ async function findFreePort(startPort, exclude) {
1421
1598
  throw new Error(`No free local port found near ${startPort}`);
1422
1599
  }
1423
1600
  function isPortFree(port) {
1424
- return new Promise((resolve2) => {
1601
+ return new Promise((resolve3) => {
1425
1602
  const server = net.createServer();
1426
- server.once("error", () => resolve2(false));
1603
+ server.once("error", () => resolve3(false));
1427
1604
  server.once("listening", () => {
1428
- server.close(() => resolve2(true));
1605
+ server.close(() => resolve3(true));
1429
1606
  });
1430
1607
  server.listen(port, "127.0.0.1");
1431
1608
  });
@@ -1435,12 +1612,12 @@ function waitForHttpReady(baseUrl, token, timeoutMs) {
1435
1612
  const url = new URL(baseUrl);
1436
1613
  url.searchParams.set("token", token);
1437
1614
  url.searchParams.set("shell", "desktop");
1438
- return new Promise((resolve2, reject) => {
1615
+ return new Promise((resolve3, reject) => {
1439
1616
  const probe = () => {
1440
1617
  const req = http.get(url, (res) => {
1441
1618
  res.resume();
1442
1619
  if (res.statusCode && res.statusCode >= 200 && res.statusCode < 500) {
1443
- resolve2();
1620
+ resolve3();
1444
1621
  return;
1445
1622
  }
1446
1623
  retry();
@@ -1461,34 +1638,8 @@ function waitForHttpReady(baseUrl, token, timeoutMs) {
1461
1638
  probe();
1462
1639
  });
1463
1640
  }
1464
- function resolveWebUiEntry() {
1465
- if (process.env["WRONGSTACK_WEBUI_ENTRY"]) {
1466
- return path2.resolve(process.env["WRONGSTACK_WEBUI_ENTRY"]);
1467
- }
1468
- const require2 = createRequire(import.meta.url);
1469
- try {
1470
- const serverPkgPath = require2.resolve("@wrongstack/webui-server/package.json");
1471
- const candidate = path2.join(path2.dirname(serverPkgPath), "dist", "server", "entry.js");
1472
- if (existsSync(candidate)) return candidate;
1473
- } catch {
1474
- }
1475
- const serverIndex = require2.resolve("@wrongstack/webui-server");
1476
- return path2.join(path2.dirname(serverIndex), "server", "entry.js");
1477
- }
1478
- function resolveWebUiDistDir() {
1479
- if (process.env["WRONGSTACK_WEBUI_DIST"]) {
1480
- return path2.resolve(process.env["WRONGSTACK_WEBUI_DIST"]);
1481
- }
1482
- const require2 = createRequire(import.meta.url);
1483
- const serverEntry = require2.resolve("@wrongstack/webui");
1484
- const candidate = path2.dirname(serverEntry);
1485
- if (existsSync(candidate)) return candidate;
1486
- throw new Error(
1487
- `WebUI frontend assets not found at ${candidate}. Build @wrongstack/webui or set WRONGSTACK_WEBUI_DIST.`
1488
- );
1489
- }
1490
1641
  async function readGlobalProjectManifest() {
1491
- const manifestFile = path2.join(wstackGlobalRoot2(), "projects.json");
1642
+ const manifestFile = path3.join(wstackGlobalRoot2(), "projects.json");
1492
1643
  try {
1493
1644
  const raw = await fs2.readFile(manifestFile, "utf8");
1494
1645
  return normalizeProjectManifest(JSON.parse(raw));
@@ -1523,8 +1674,8 @@ function normalizeProjectEntry(value) {
1523
1674
  if (!value || typeof value !== "object") return null;
1524
1675
  const candidate = value;
1525
1676
  if (typeof candidate.root !== "string" || !candidate.root.trim()) return null;
1526
- const root = path2.resolve(candidate.root);
1527
- const name = typeof candidate.name === "string" && candidate.name.trim() ? candidate.name.trim() : path2.basename(root) || root;
1677
+ const root = path3.resolve(candidate.root);
1678
+ const name = typeof candidate.name === "string" && candidate.name.trim() ? candidate.name.trim() : path3.basename(root) || root;
1528
1679
  const entry = {
1529
1680
  name,
1530
1681
  root,
@@ -1537,12 +1688,12 @@ function normalizeProjectEntry(value) {
1537
1688
  entry.createdAt = candidate.createdAt.trim();
1538
1689
  }
1539
1690
  if (typeof candidate.lastWorkingDir === "string" && candidate.lastWorkingDir.trim()) {
1540
- entry.lastWorkingDir = path2.resolve(candidate.lastWorkingDir);
1691
+ entry.lastWorkingDir = path3.resolve(candidate.lastWorkingDir);
1541
1692
  }
1542
1693
  return entry;
1543
1694
  }
1544
1695
  async function touchGlobalProjectManifest(entry) {
1545
- const manifestFile = path2.join(wstackGlobalRoot2(), "projects.json");
1696
+ const manifestFile = path3.join(wstackGlobalRoot2(), "projects.json");
1546
1697
  const projects = await readGlobalProjectManifest();
1547
1698
  const existing = projects.find((p) => samePath(p.root, entry.root));
1548
1699
  if (existing) {
@@ -1556,7 +1707,7 @@ async function touchGlobalProjectManifest(entry) {
1556
1707
  const sorted = projects.sort(
1557
1708
  (a, b) => (b.lastSeen ?? b.createdAt ?? "").localeCompare(a.lastSeen ?? a.createdAt ?? "")
1558
1709
  ).slice(0, 80);
1559
- await fs2.mkdir(path2.dirname(manifestFile), { recursive: true });
1710
+ await fs2.mkdir(path3.dirname(manifestFile), { recursive: true });
1560
1711
  await atomicWrite2(manifestFile, `${JSON.stringify({ projects: sorted }, null, 2)}
1561
1712
  `, {
1562
1713
  mode: 384
@@ -1564,37 +1715,41 @@ async function touchGlobalProjectManifest(entry) {
1564
1715
  return sorted;
1565
1716
  }
1566
1717
  async function removeGlobalProjectManifest(projectRoot) {
1567
- const manifestFile = path2.join(wstackGlobalRoot2(), "projects.json");
1568
- const resolved = path2.resolve(projectRoot);
1718
+ const manifestFile = path3.join(wstackGlobalRoot2(), "projects.json");
1719
+ const resolved = path3.resolve(projectRoot);
1569
1720
  const projects = (await readGlobalProjectManifest()).filter(
1570
1721
  (project) => !samePath(project.root, resolved)
1571
1722
  );
1572
- await fs2.mkdir(path2.dirname(manifestFile), { recursive: true });
1723
+ await fs2.mkdir(path3.dirname(manifestFile), { recursive: true });
1573
1724
  await atomicWrite2(manifestFile, `${JSON.stringify({ projects }, null, 2)}
1574
1725
  `, { mode: 384 });
1575
1726
  return projects;
1576
1727
  }
1577
1728
  function samePath(left, right) {
1578
- const a = path2.resolve(left);
1579
- const b = path2.resolve(right);
1729
+ const a = path3.resolve(left);
1730
+ const b = path3.resolve(right);
1580
1731
  return os.platform() === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
1581
1732
  }
1582
- function rendererIndexPath() {
1583
- return fileURLToPath(new URL("../renderer/index.html", import.meta.url));
1584
- }
1585
- function preloadPath() {
1586
- return fileURLToPath(new URL("../preload/preload.cjs", import.meta.url));
1587
- }
1588
- function webuiPreloadPath() {
1589
- return fileURLToPath(new URL("../preload/webui-preload.cjs", import.meta.url));
1590
- }
1591
- function desktopSettingsWorkspaceRoot() {
1592
- return path2.join(wstackGlobalRoot2(), "settings");
1593
- }
1594
1733
 
1595
1734
  // src/main/main.ts
1596
1735
  import { watchProviderConfig } from "@wrongstack/core/storage";
1597
1736
 
1737
+ // src/main/webui/controller.ts
1738
+ import { shell, WebContentsView } from "electron";
1739
+
1740
+ // src/main/state/constants.ts
1741
+ var OPEN_EXTERNAL_ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
1742
+ var SIDEBAR_WIDTH_WIDE = 292;
1743
+ var SIDEBAR_WIDTH_MEDIUM = 276;
1744
+ var SIDEBAR_WIDTH_NARROW = 252;
1745
+ var SIDEBAR_WIDTH_COLLAPSED = 56;
1746
+ var MIN_WINDOW_WIDTH2 = 760;
1747
+ var MIN_WINDOW_HEIGHT2 = 520;
1748
+ var MAX_PENDING_WEBUI_COMMANDS = 50;
1749
+ var MAX_PENDING_FLUSH_ATTEMPTS = 80;
1750
+ var WEBUI_COMMAND_FALLBACK_MS = 350;
1751
+ var WEBUI_COMMAND_ACK_TIMEOUT_MS = 2e3;
1752
+
1598
1753
  // src/main/webui-command-bridge.ts
1599
1754
  var DESKTOP_WEBUI_ACTIONS = /* @__PURE__ */ new Set([
1600
1755
  "new-session",
@@ -1746,18 +1901,430 @@ function isRecord(value) {
1746
1901
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1747
1902
  }
1748
1903
 
1749
- // src/main/state/constants.ts
1750
- var OPEN_EXTERNAL_ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
1751
- var SIDEBAR_WIDTH_WIDE = 292;
1752
- var SIDEBAR_WIDTH_MEDIUM = 276;
1753
- var SIDEBAR_WIDTH_NARROW = 252;
1754
- var SIDEBAR_WIDTH_COLLAPSED = 56;
1755
- var MIN_WINDOW_WIDTH2 = 760;
1756
- var MIN_WINDOW_HEIGHT2 = 520;
1757
- var MAX_PENDING_WEBUI_COMMANDS = 50;
1758
- var MAX_PENDING_FLUSH_ATTEMPTS = 80;
1759
- var WEBUI_COMMAND_FALLBACK_MS = 350;
1760
- var WEBUI_COMMAND_ACK_TIMEOUT_MS = 2e3;
1904
+ // src/main/webui/navigation.ts
1905
+ function allowedExternalProtocol(target) {
1906
+ try {
1907
+ const protocol = new URL(target).protocol;
1908
+ return OPEN_EXTERNAL_ALLOWED_PROTOCOLS.has(protocol) ? protocol : void 0;
1909
+ } catch {
1910
+ return void 0;
1911
+ }
1912
+ }
1913
+ function sameOrigin(candidate, base) {
1914
+ if (!base) return false;
1915
+ try {
1916
+ return new URL(candidate).origin === new URL(base).origin;
1917
+ } catch {
1918
+ return false;
1919
+ }
1920
+ }
1921
+
1922
+ // src/main/webui/controller.ts
1923
+ var DesktopWebuiController = class {
1924
+ constructor(ctx) {
1925
+ this.ctx = ctx;
1926
+ }
1927
+ ctx;
1928
+ views = /* @__PURE__ */ new Map();
1929
+ pendingAcks = /* @__PURE__ */ new Map();
1930
+ activeRuntimeId = null;
1931
+ status = { runtimeId: null, status: "idle" };
1932
+ commandSequence = 0;
1933
+ openExternal(target) {
1934
+ const protocol = allowedExternalProtocol(target);
1935
+ if (!protocol) return;
1936
+ void authorizeDesktopAction(this.ctx.trustBoundary, {
1937
+ capability: "url.open-external",
1938
+ subject: { kind: "url", id: target, attributes: { protocol } },
1939
+ risk: "elevated",
1940
+ metadata: { operation: "webui-navigation" }
1941
+ }).then(
1942
+ (decision) => decision.allowed ? void shell.openExternal(target) : void 0
1943
+ ).catch(() => void 0);
1944
+ }
1945
+ publishStatus(next) {
1946
+ this.status = next;
1947
+ const shellView2 = this.ctx.getShellView();
1948
+ if (!shellView2 || shellView2.webContents.isDestroyed()) return;
1949
+ shellView2.webContents.send(IPC.webuiStatusChanged, next);
1950
+ }
1951
+ setEntryStatus(entry, next) {
1952
+ const previousPrefs = entry.status.prefs;
1953
+ entry.status = {
1954
+ ...next,
1955
+ ...next.prefs === void 0 && previousPrefs !== void 0 ? { prefs: previousPrefs } : {},
1956
+ // `pendingCommands` is always derived from the entry's own array (source of truth).
1957
+ // Any `next.pendingCommands` value is intentionally overwritten.
1958
+ pendingCommands: entry.pendingCommands.length
1959
+ };
1960
+ if (this.activeRuntimeId === entry.runtimeId) {
1961
+ this.publishStatus(entry.status);
1962
+ this.ctx.onPrefsChanged?.(previousPrefs, entry.status.prefs);
1963
+ }
1964
+ }
1965
+ ensure(runtimeId) {
1966
+ const mainWindow2 = this.ctx.getMainWindow();
1967
+ if (!mainWindow2) return null;
1968
+ const existing = this.views.get(runtimeId);
1969
+ if (existing) return existing;
1970
+ const view = new WebContentsView({
1971
+ webPreferences: {
1972
+ preload: webuiPreloadPath(),
1973
+ contextIsolation: true,
1974
+ nodeIntegration: false,
1975
+ sandbox: false
1976
+ }
1977
+ });
1978
+ const entry = {
1979
+ runtimeId,
1980
+ view,
1981
+ url: null,
1982
+ status: { runtimeId, status: "idle" },
1983
+ bridgeReady: false,
1984
+ attached: false,
1985
+ pendingCommands: [],
1986
+ pendingFlushTimer: null,
1987
+ pendingFlushAttempts: 0
1988
+ };
1989
+ view.webContents.setWindowOpenHandler(({ url }) => {
1990
+ this.openExternal(url);
1991
+ return { action: "deny" };
1992
+ });
1993
+ view.webContents.on("will-navigate", (event, url) => {
1994
+ if (sameOrigin(url, entry.url)) return;
1995
+ event.preventDefault();
1996
+ this.openExternal(url);
1997
+ });
1998
+ view.webContents.on("did-start-loading", () => {
1999
+ if (this.views.get(runtimeId) !== entry) return;
2000
+ entry.bridgeReady = false;
2001
+ this.setEntryStatus(entry, { runtimeId, status: "loading" });
2002
+ });
2003
+ view.webContents.on("did-finish-load", () => {
2004
+ if (this.views.get(runtimeId) !== entry) return;
2005
+ this.scheduleFlush(entry);
2006
+ try {
2007
+ entry.view.webContents.send(IPC.webuiLocaleChanged, this.ctx.getLocale());
2008
+ } catch {
2009
+ }
2010
+ });
2011
+ view.webContents.on("did-fail-load", (_event, errorCode, errorDescription) => {
2012
+ if (this.views.get(runtimeId) !== entry || errorCode === -3) return;
2013
+ this.setEntryStatus(entry, { runtimeId, status: "error", error: errorDescription });
2014
+ });
2015
+ view.webContents.on("render-process-gone", (_event, details) => {
2016
+ if (this.views.get(runtimeId) !== entry) return;
2017
+ this.setEntryStatus(entry, {
2018
+ runtimeId,
2019
+ status: "error",
2020
+ error: `WebUI renderer exited: ${details.reason}`
2021
+ });
2022
+ });
2023
+ this.views.set(runtimeId, entry);
2024
+ return entry;
2025
+ }
2026
+ attach(entry) {
2027
+ const mainWindow2 = this.ctx.getMainWindow();
2028
+ if (!mainWindow2 || entry.attached) return;
2029
+ mainWindow2.contentView.addChildView(entry.view);
2030
+ entry.attached = true;
2031
+ }
2032
+ dispose(entry) {
2033
+ this.views.delete(entry.runtimeId);
2034
+ entry.pendingCommands.length = 0;
2035
+ this.settleRuntimeAcks(entry.runtimeId, false);
2036
+ if (entry.pendingFlushTimer) clearTimeout(entry.pendingFlushTimer);
2037
+ entry.pendingFlushTimer = null;
2038
+ const mainWindow2 = this.ctx.getMainWindow();
2039
+ if (mainWindow2 && entry.attached) mainWindow2.contentView.removeChildView(entry.view);
2040
+ entry.attached = false;
2041
+ if (!entry.view.webContents.isDestroyed()) entry.view.webContents.close();
2042
+ if (this.activeRuntimeId === entry.runtimeId) this.activeRuntimeId = null;
2043
+ }
2044
+ disposeAll() {
2045
+ for (const entry of [...this.views.values()]) this.dispose(entry);
2046
+ }
2047
+ findBySenderId(senderId) {
2048
+ return [...this.views.values()].find((entry) => entry.view.webContents.id === senderId);
2049
+ }
2050
+ syncActive() {
2051
+ if (!this.ctx.getMainWindow()) return;
2052
+ const snapshot = this.ctx.manager.snapshot();
2053
+ const live = new Set(snapshot.runtimes.filter((r) => r.status === "running").map((r) => r.id));
2054
+ for (const [id, entry2] of this.views) if (!live.has(id)) this.dispose(entry2);
2055
+ const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
2056
+ if (active?.status !== "running") {
2057
+ this.activeRuntimeId = active?.id ?? null;
2058
+ this.publishStatus({ runtimeId: active?.id ?? null, status: "idle" });
2059
+ this.ctx.layoutViews();
2060
+ return;
2061
+ }
2062
+ const url = this.ctx.manager.getRuntimeUrlWithToken(active.id);
2063
+ if (!url) {
2064
+ this.activeRuntimeId = active.id;
2065
+ this.publishStatus({ runtimeId: active.id, status: "idle" });
2066
+ this.ctx.layoutViews();
2067
+ return;
2068
+ }
2069
+ const entry = this.ensure(active.id);
2070
+ if (!entry) return;
2071
+ this.activeRuntimeId = active.id;
2072
+ this.attach(entry);
2073
+ this.ctx.layoutViews();
2074
+ this.publishStatus(entry.status);
2075
+ if (entry.url === url) return;
2076
+ entry.url = url;
2077
+ entry.bridgeReady = false;
2078
+ this.setEntryStatus(entry, { runtimeId: active.id, status: "loading" });
2079
+ void entry.view.webContents.loadURL(url).catch((error) => {
2080
+ this.setEntryStatus(entry, {
2081
+ runtimeId: active.id,
2082
+ status: "error",
2083
+ error: error instanceof Error ? error.message : String(error)
2084
+ });
2085
+ });
2086
+ }
2087
+ broadcastLocale(locale) {
2088
+ for (const entry of this.views.values()) {
2089
+ if (!entry.view.webContents.isDestroyed())
2090
+ entry.view.webContents.send(IPC.webuiLocaleChanged, locale);
2091
+ }
2092
+ }
2093
+ activeEntry() {
2094
+ const id = this.ctx.manager.snapshot().activeRuntimeId;
2095
+ return id ? this.views.get(id) : void 0;
2096
+ }
2097
+ async dispatch(commandInput) {
2098
+ const command = normalizeDesktopWebuiCommand(commandInput);
2099
+ if (!command) return false;
2100
+ const entry = this.activeEntry();
2101
+ if (!entry?.url) return false;
2102
+ if (entry.status.status !== "ready" || !entry.bridgeReady) {
2103
+ if (!entry.view.webContents.isLoading() && entry.status.status !== "error")
2104
+ return this.dispatchNow(entry, command);
2105
+ this.queue(entry, command);
2106
+ this.scheduleFlush(entry);
2107
+ return true;
2108
+ }
2109
+ return this.dispatchNow(entry, command);
2110
+ }
2111
+ async reload() {
2112
+ const entry = this.activeEntry();
2113
+ if (!entry?.url) return false;
2114
+ entry.bridgeReady = false;
2115
+ this.setEntryStatus(entry, { runtimeId: entry.runtimeId, status: "loading" });
2116
+ return entry.view.webContents.loadURL(entry.url).then(() => true).catch((error) => {
2117
+ this.setEntryStatus(entry, {
2118
+ runtimeId: entry.runtimeId,
2119
+ status: "error",
2120
+ error: error instanceof Error ? error.message : String(error)
2121
+ });
2122
+ return false;
2123
+ });
2124
+ }
2125
+ queue(entry, command) {
2126
+ entry.pendingCommands.push(command);
2127
+ if (entry.pendingCommands.length > MAX_PENDING_WEBUI_COMMANDS)
2128
+ entry.pendingCommands.splice(0, entry.pendingCommands.length - MAX_PENDING_WEBUI_COMMANDS);
2129
+ entry.pendingFlushAttempts = 0;
2130
+ this.setEntryStatus(entry, entry.status);
2131
+ }
2132
+ dispatchNow(entry, command) {
2133
+ if (this.views.get(entry.runtimeId) !== entry || !entry.url) return Promise.resolve(false);
2134
+ const requestId = `${entry.runtimeId}:${Date.now()}:${++this.commandSequence}`;
2135
+ const outbound = { ...command, requestId };
2136
+ return new Promise((resolve3) => {
2137
+ const fallbackTimer = setTimeout(() => {
2138
+ if (!this.pendingAcks.has(requestId)) return;
2139
+ if (this.views.get(entry.runtimeId) !== entry || entry.view.webContents.isDestroyed()) return;
2140
+ void entry.view.webContents.executeJavaScript(buildWebuiCommandFallbackScript(outbound), true).catch(() => void 0);
2141
+ }, WEBUI_COMMAND_FALLBACK_MS);
2142
+ const timer = setTimeout(() => this.settleAck(requestId, false), WEBUI_COMMAND_ACK_TIMEOUT_MS);
2143
+ this.pendingAcks.set(requestId, { runtimeId: entry.runtimeId, timer, fallbackTimer, resolve: resolve3 });
2144
+ try {
2145
+ entry.view.webContents.send(IPC.webuiCommand, outbound);
2146
+ if (this.activeRuntimeId === entry.runtimeId) entry.view.webContents.focus();
2147
+ } catch {
2148
+ this.settleAck(requestId, false);
2149
+ }
2150
+ });
2151
+ }
2152
+ settleAck(requestId, handled) {
2153
+ const pending = this.pendingAcks.get(requestId);
2154
+ if (!pending) return;
2155
+ this.pendingAcks.delete(requestId);
2156
+ clearTimeout(pending.timer);
2157
+ if (pending.fallbackTimer) clearTimeout(pending.fallbackTimer);
2158
+ if (handled) {
2159
+ const entry = this.views.get(pending.runtimeId);
2160
+ if (entry) {
2161
+ entry.bridgeReady = true;
2162
+ this.setEntryStatus(entry, { ...entry.status, status: "ready" });
2163
+ }
2164
+ }
2165
+ pending.resolve(handled);
2166
+ }
2167
+ settleRuntimeAcks(runtimeId, handled) {
2168
+ for (const [id, pending] of [...this.pendingAcks])
2169
+ if (pending.runtimeId === runtimeId) this.settleAck(id, handled);
2170
+ }
2171
+ scheduleFlush(entry) {
2172
+ if (entry.pendingFlushTimer) return;
2173
+ entry.pendingFlushTimer = setTimeout(() => {
2174
+ entry.pendingFlushTimer = null;
2175
+ void this.flush(entry);
2176
+ }, 250);
2177
+ }
2178
+ async flush(entry) {
2179
+ if (this.views.get(entry.runtimeId) !== entry || entry.pendingCommands.length === 0) return;
2180
+ if (!entry.bridgeReady) {
2181
+ entry.pendingFlushAttempts += 1;
2182
+ const shouldExecuteFallback = !entry.view.webContents.isLoading() && entry.pendingFlushAttempts >= 4;
2183
+ if (!shouldExecuteFallback && entry.pendingFlushAttempts <= MAX_PENDING_FLUSH_ATTEMPTS) {
2184
+ this.scheduleFlush(entry);
2185
+ this.setEntryStatus(entry, entry.status);
2186
+ return;
2187
+ }
2188
+ if (!shouldExecuteFallback) {
2189
+ entry.pendingCommands.length = 0;
2190
+ this.setEntryStatus(entry, { runtimeId: entry.runtimeId, status: "error", error: "WebUI command bridge did not become ready." });
2191
+ return;
2192
+ }
2193
+ }
2194
+ entry.pendingFlushAttempts = 0;
2195
+ const commands = entry.pendingCommands.splice(0);
2196
+ this.setEntryStatus(entry, entry.status);
2197
+ for (const command of commands) await this.dispatchNow(entry, command).catch(() => void 0);
2198
+ }
2199
+ };
2200
+
2201
+ // src/main/app-icon.ts
2202
+ import * as fs3 from "node:fs/promises";
2203
+ import { nativeImage } from "electron";
2204
+ async function readIcon(relativePath) {
2205
+ try {
2206
+ const iconPath = new URL(relativePath, import.meta.url).pathname;
2207
+ await fs3.stat(iconPath);
2208
+ const icon = nativeImage.createFromPath(iconPath);
2209
+ return icon.isEmpty() ? void 0 : icon;
2210
+ } catch {
2211
+ return void 0;
2212
+ }
2213
+ }
2214
+ async function loadDesktopAppIcon() {
2215
+ if (process.platform !== "darwin") return readIcon("../../assets/icon.svg");
2216
+ return await readIcon("../../assets/icon.png") ?? readIcon("../../assets/icon.icns");
2217
+ }
2218
+
2219
+ // src/main/window-state-controller.ts
2220
+ var DesktopWindowStateController = class {
2221
+ constructor(ctx) {
2222
+ this.ctx = ctx;
2223
+ }
2224
+ ctx;
2225
+ saveTimer = null;
2226
+ scheduleSave() {
2227
+ if (this.saveTimer) clearTimeout(this.saveTimer);
2228
+ this.saveTimer = setTimeout(() => {
2229
+ this.saveTimer = null;
2230
+ void this.save();
2231
+ }, 350);
2232
+ }
2233
+ async save() {
2234
+ const window = this.ctx.getWindow();
2235
+ if (!window || window.isDestroyed?.()) return;
2236
+ const bounds = window.getNormalBounds();
2237
+ await this.ctx.save({ ...bounds, maximized: window.isMaximized() });
2238
+ }
2239
+ validated(state) {
2240
+ if (!state || !Number.isFinite(state.width) || !Number.isFinite(state.height) || state.width < MIN_WINDOW_WIDTH2 || state.height < MIN_WINDOW_HEIGHT2)
2241
+ return null;
2242
+ if (state.x === void 0 || state.y === void 0) {
2243
+ return { width: state.width, height: state.height, maximized: state.maximized };
2244
+ }
2245
+ const candidate = { x: state.x, y: state.y, width: state.width, height: state.height };
2246
+ return this.ctx.getDisplays().some(({ workArea }) => intersects(candidate, workArea)) ? { x: state.x, y: state.y, width: state.width, height: state.height, maximized: state.maximized } : null;
2247
+ }
2248
+ };
2249
+ function intersects(left, right) {
2250
+ return left.x < right.x + right.width && left.x + left.width > right.x && left.y < right.y + right.height && left.y + left.height > right.y;
2251
+ }
2252
+
2253
+ // src/main/runtime/operations.ts
2254
+ import * as fs4 from "node:fs/promises";
2255
+ async function openProject(ctx, requestedRoot) {
2256
+ let projectRoot = requestedRoot;
2257
+ if (!projectRoot) {
2258
+ projectRoot = await ctx.chooseProjectRoot("open");
2259
+ }
2260
+ if (!projectRoot) return ctx.getRuntimeManager().snapshot();
2261
+ await ctx.getRuntimeManager().openProject(projectRoot);
2262
+ ctx.syncActiveWebuiView();
2263
+ ctx.broadcastState();
2264
+ return ctx.getRuntimeManager().snapshot();
2265
+ }
2266
+ async function registerProject(ctx, requestedRoot) {
2267
+ let projectRoot = requestedRoot;
2268
+ if (!projectRoot) {
2269
+ projectRoot = await ctx.chooseProjectRoot("register");
2270
+ }
2271
+ if (!projectRoot) return ctx.getRuntimeManager().snapshot();
2272
+ await ctx.getRuntimeManager().registerProject(projectRoot);
2273
+ ctx.broadcastState();
2274
+ return ctx.getRuntimeManager().snapshot();
2275
+ }
2276
+ async function unregisterProject(ctx, root) {
2277
+ if (!root || typeof root !== "string") return ctx.getRuntimeManager().snapshot();
2278
+ await ctx.getRuntimeManager().unregisterProject(root);
2279
+ ctx.broadcastState();
2280
+ return ctx.getRuntimeManager().snapshot();
2281
+ }
2282
+ async function openProjectSession(ctx, runtimeId) {
2283
+ const snapshot = ctx.getRuntimeManager().snapshot();
2284
+ const runtime = (runtimeId ? snapshot.runtimes.find((candidate) => candidate.id === runtimeId) : void 0) ?? snapshot.runtimes.find((candidate) => candidate.id === snapshot.activeRuntimeId);
2285
+ if (runtime?.kind !== "project") {
2286
+ return openProject(ctx);
2287
+ }
2288
+ await ctx.getRuntimeManager().openProject(runtime.root, { forceNew: true });
2289
+ ctx.syncActiveWebuiView();
2290
+ ctx.broadcastState();
2291
+ return ctx.getRuntimeManager().snapshot();
2292
+ }
2293
+ async function openSettings(ctx) {
2294
+ const snapshot = ctx.getRuntimeManager().snapshot();
2295
+ const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
2296
+ if (!active || active.kind === "global-settings" || active.status !== "running") {
2297
+ const root = desktopSettingsWorkspaceRoot();
2298
+ await fs4.mkdir(root, { recursive: true });
2299
+ await ctx.getRuntimeManager().openProject(root, {
2300
+ name: "Global Settings",
2301
+ kind: "global-settings",
2302
+ touchRecent: false
2303
+ });
2304
+ ctx.syncActiveWebuiView();
2305
+ ctx.broadcastState();
2306
+ }
2307
+ await ctx.dispatchWebuiCommand({ view: "settings" });
2308
+ return ctx.getRuntimeManager().snapshot();
2309
+ }
2310
+ async function activateRuntime(ctx, id) {
2311
+ await ctx.getRuntimeManager().activateRuntime(id);
2312
+ ctx.syncActiveWebuiView();
2313
+ ctx.broadcastState();
2314
+ return ctx.getRuntimeManager().snapshot();
2315
+ }
2316
+ async function closeRuntime(ctx, id) {
2317
+ ctx.getAgentBridge().close(id);
2318
+ await ctx.getRuntimeManager().closeRuntime(id);
2319
+ ctx.syncActiveWebuiView();
2320
+ ctx.broadcastState();
2321
+ return ctx.getRuntimeManager().snapshot();
2322
+ }
2323
+ async function restoreLastWorkspace(ctx) {
2324
+ await ctx.getRuntimeManager().restoreLastWorkspace();
2325
+ ctx.syncActiveWebuiView();
2326
+ ctx.broadcastState();
2327
+ }
1761
2328
 
1762
2329
  // src/main/layout/sidebar.ts
1763
2330
  function getSidebarWidth(windowWidth, collapsed) {
@@ -1771,7 +2338,7 @@ function getSidebarWidth(windowWidth, collapsed) {
1771
2338
  import { Menu as Menu2 } from "electron";
1772
2339
 
1773
2340
  // src/main/menu/projects-menu.ts
1774
- import path3 from "node:path";
2341
+ import path4 from "node:path";
1775
2342
  function buildProjectsMenu(runtimes, actions, t) {
1776
2343
  const projectGroups = groupProjectRuntimesForMenu(runtimes);
1777
2344
  const menu = [
@@ -1905,7 +2472,7 @@ function groupProjectRuntimesForMenu(runtimes) {
1905
2472
  }
1906
2473
  groups.set(key, {
1907
2474
  key,
1908
- name: path3.basename(runtime.root) || runtime.name,
2475
+ name: path4.basename(runtime.root) || runtime.name,
1909
2476
  root: runtime.root,
1910
2477
  sessions: [runtime]
1911
2478
  });
@@ -2394,52 +2961,74 @@ initMacOS();
2394
2961
  if (process.platform === "win32") {
2395
2962
  app2.setAppUserModelId("com.wrongstack.desktop");
2396
2963
  }
2397
- app2.setPath("userData", path4.join(wstackGlobalRoot3(), "desktop", "electron-profile"));
2398
- var manager = new DesktopRuntimeManager();
2964
+ app2.setPath(
2965
+ "userData",
2966
+ path5.join(
2967
+ resolveWstackPaths3({ projectRoot: process.cwd() }).configDir,
2968
+ "desktop",
2969
+ "electron-profile"
2970
+ )
2971
+ );
2972
+ var desktopTrustBoundary = desktopCompatibilityTrustBoundary;
2973
+ var manager = new DesktopRuntimeManager(desktopTrustBoundary);
2399
2974
  var bridge = new DesktopAgentBridge();
2400
2975
  var mainWindow = null;
2401
2976
  var shellView = null;
2402
- var webuiViews = /* @__PURE__ */ new Map();
2403
- var activeWebuiRuntimeId = null;
2404
- var webuiStatus = { runtimeId: null, status: "idle" };
2405
- var webuiCommandSequence = 0;
2406
2977
  var shellSidebarCollapsed = false;
2407
- var pendingWebuiCommandAcks = /* @__PURE__ */ new Map();
2408
- var saveWindowStateTimer = null;
2409
2978
  var quittingAfterCleanup = false;
2410
- function safeOpenExternal(target) {
2411
- let protocol;
2412
- try {
2413
- protocol = new URL(target).protocol;
2414
- } catch {
2415
- return;
2979
+ var webuiController = new DesktopWebuiController({
2980
+ manager,
2981
+ trustBoundary: desktopTrustBoundary,
2982
+ getMainWindow: () => mainWindow,
2983
+ getShellView: () => shellView,
2984
+ getLocale: getMainLocale,
2985
+ layoutViews: () => layoutWebuiViews(),
2986
+ onPrefsChanged: (previous, next) => {
2987
+ if (menuRelevantPrefsChanged(previous, next)) configureApplicationMenu2();
2416
2988
  }
2417
- if (OPEN_EXTERNAL_ALLOWED_PROTOCOLS.has(protocol)) {
2418
- void shell.openExternal(target);
2419
- }
2420
- }
2421
- function sameOrigin(candidate, base) {
2422
- if (!base) return false;
2423
- try {
2424
- return new URL(candidate).origin === new URL(base).origin;
2425
- } catch {
2426
- return false;
2989
+ });
2990
+ var windowStateController = new DesktopWindowStateController({
2991
+ getWindow: () => mainWindow,
2992
+ getDisplays: () => screen.getAllDisplays(),
2993
+ save: (state) => manager.saveWindowState(state)
2994
+ });
2995
+ function safeOpenExternal(target) {
2996
+ const protocol = allowedExternalProtocol(target);
2997
+ if (protocol) {
2998
+ void authorizeDesktopAction(desktopTrustBoundary, {
2999
+ capability: "url.open-external",
3000
+ subject: { kind: "url", id: target, attributes: { protocol } },
3001
+ risk: "elevated",
3002
+ metadata: { operation: "open-external" }
3003
+ }).then((authorization) => {
3004
+ if (authorization.allowed) return shell2.openExternal(target);
3005
+ return void 0;
3006
+ });
2427
3007
  }
2428
3008
  }
2429
3009
  function revealInExplorer(root) {
2430
- shell.openPath(root).catch((err) => {
2431
- if (process.platform === "darwin") {
2432
- void shell.openPath(path4.dirname(root)).catch(() => void 0);
2433
- }
2434
- console.error(
2435
- JSON.stringify({
2436
- level: "warn",
2437
- event: "desktop.reveal_in_explorer_failed",
2438
- root,
2439
- message: err instanceof Error ? err.message : String(err),
2440
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2441
- })
2442
- );
3010
+ void authorizeDesktopAction(desktopTrustBoundary, {
3011
+ capability: "filesystem.open-native",
3012
+ subject: { kind: "path", id: root, attributes: { target: "file-manager" } },
3013
+ risk: "elevated",
3014
+ cwd: root,
3015
+ metadata: { operation: "reveal-in-explorer" }
3016
+ }).then((authorization) => {
3017
+ if (!authorization.allowed) return;
3018
+ return shell2.openPath(root).catch((err) => {
3019
+ if (process.platform === "darwin") {
3020
+ void shell2.openPath(path5.dirname(root)).catch(() => void 0);
3021
+ }
3022
+ console.error(
3023
+ JSON.stringify({
3024
+ level: "warn",
3025
+ event: "desktop.reveal_in_explorer_failed",
3026
+ root,
3027
+ message: err instanceof Error ? err.message : String(err),
3028
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3029
+ })
3030
+ );
3031
+ });
2443
3032
  });
2444
3033
  }
2445
3034
  function setShellSidebarCollapsed(collapsed) {
@@ -2452,57 +3041,6 @@ function setShellSidebarCollapsed(collapsed) {
2452
3041
  function menuRelevantPrefsChanged(previous, next) {
2453
3042
  return previous?.yolo !== next?.yolo || previous?.nextPrediction !== next?.nextPrediction || previous?.contextAutoCompact !== next?.contextAutoCompact;
2454
3043
  }
2455
- function setEntryWebuiStatus(entry, next) {
2456
- const previousPrefs = entry.status.prefs;
2457
- entry.status = {
2458
- ...next,
2459
- prefs: next.prefs ?? entry.status.prefs,
2460
- pendingCommands: entry.pendingCommands.length || void 0
2461
- };
2462
- if (activeWebuiRuntimeId === entry.runtimeId) {
2463
- publishWebuiStatus(entry.status);
2464
- if (menuRelevantPrefsChanged(previousPrefs, entry.status.prefs)) {
2465
- configureApplicationMenu2();
2466
- }
2467
- }
2468
- }
2469
- function scheduleWindowStateSave() {
2470
- if (saveWindowStateTimer) clearTimeout(saveWindowStateTimer);
2471
- saveWindowStateTimer = setTimeout(() => {
2472
- saveWindowStateTimer = null;
2473
- void saveWindowState();
2474
- }, 350);
2475
- }
2476
- async function saveWindowState() {
2477
- if (!mainWindow) return;
2478
- const bounds = mainWindow.getNormalBounds();
2479
- await manager.saveWindowState({
2480
- x: bounds.x,
2481
- y: bounds.y,
2482
- width: bounds.width,
2483
- height: bounds.height,
2484
- maximized: mainWindow.isMaximized()
2485
- });
2486
- }
2487
- function validatedWindowState(state) {
2488
- if (!state) return null;
2489
- if (state.width < MIN_WINDOW_WIDTH2 || state.height < MIN_WINDOW_HEIGHT2) return null;
2490
- if (state.x === void 0 || state.y === void 0) return state;
2491
- const candidate = {
2492
- x: state.x,
2493
- y: state.y,
2494
- width: state.width,
2495
- height: state.height
2496
- };
2497
- const visibleOnSomeDisplay = screen.getAllDisplays().some((display) => {
2498
- const area = display.workArea;
2499
- return rectanglesIntersect(candidate, area);
2500
- });
2501
- return visibleOnSomeDisplay ? state : null;
2502
- }
2503
- function rectanglesIntersect(left, right) {
2504
- return left.x < right.x + right.width && left.x + left.width > right.x && left.y < right.y + right.height && left.y + left.height > right.y;
2505
- }
2506
3044
  function layoutViews() {
2507
3045
  if (!mainWindow || !shellView) return;
2508
3046
  const size = mainWindow.getContentSize();
@@ -2520,7 +3058,7 @@ function layoutWebuiViews() {
2520
3058
  const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
2521
3059
  const sidebarWidth = getSidebarWidth(width, shellSidebarCollapsed);
2522
3060
  const contentWidth = Math.max(0, width - sidebarWidth);
2523
- for (const entry of webuiViews.values()) {
3061
+ for (const entry of webuiController.views.values()) {
2524
3062
  const runtime = snapshot.runtimes.find((r) => r.id === entry.runtimeId);
2525
3063
  if (active?.id === entry.runtimeId && runtime?.status === "running") {
2526
3064
  entry.view.setBounds({ x: sidebarWidth, y: 0, width: contentWidth, height });
@@ -2529,413 +3067,32 @@ function layoutWebuiViews() {
2529
3067
  }
2530
3068
  }
2531
3069
  }
2532
- function ensureWebuiEntry(runtimeId) {
2533
- if (!mainWindow) return null;
2534
- const existing = webuiViews.get(runtimeId);
2535
- if (existing) return existing;
2536
- const view = new WebContentsView({
2537
- webPreferences: {
2538
- preload: webuiPreloadPath(),
2539
- contextIsolation: true,
2540
- nodeIntegration: false,
2541
- sandbox: false
2542
- }
2543
- });
2544
- const entry = {
2545
- runtimeId,
2546
- view,
2547
- url: null,
2548
- status: { runtimeId, status: "idle" },
2549
- bridgeReady: false,
2550
- attached: false,
2551
- pendingCommands: [],
2552
- pendingFlushTimer: null,
2553
- pendingFlushAttempts: 0
2554
- };
2555
- view.webContents.setWindowOpenHandler(({ url }) => {
2556
- safeOpenExternal(url);
2557
- return { action: "deny" };
2558
- });
2559
- view.webContents.on("will-navigate", (event, url) => {
2560
- if (sameOrigin(url, entry.url)) return;
2561
- event.preventDefault();
2562
- safeOpenExternal(url);
2563
- });
2564
- view.webContents.on("did-start-loading", () => {
2565
- if (webuiViews.get(runtimeId) !== entry) return;
2566
- entry.bridgeReady = false;
2567
- setEntryWebuiStatus(entry, { runtimeId, status: "loading" });
2568
- });
2569
- view.webContents.on("did-finish-load", () => {
2570
- if (webuiViews.get(runtimeId) !== entry) return;
2571
- schedulePendingWebuiFlush(entry);
2572
- try {
2573
- entry.view.webContents.send(IPC.webuiLocaleChanged, getMainLocale());
2574
- } catch {
2575
- }
2576
- });
2577
- view.webContents.on("did-fail-load", (_event, errorCode, errorDescription) => {
2578
- if (webuiViews.get(runtimeId) !== entry || errorCode === -3) return;
2579
- setEntryWebuiStatus(entry, { runtimeId, status: "error", error: errorDescription });
2580
- });
2581
- view.webContents.on("render-process-gone", (_event, details) => {
2582
- if (webuiViews.get(runtimeId) !== entry) return;
2583
- setEntryWebuiStatus(entry, {
2584
- runtimeId,
2585
- status: "error",
2586
- error: `WebUI renderer exited: ${details.reason}`
2587
- });
2588
- });
2589
- webuiViews.set(runtimeId, entry);
2590
- return entry;
2591
- }
2592
- function attachWebuiEntry(entry) {
2593
- if (!mainWindow) return;
2594
- if (entry.attached) return;
2595
- mainWindow.contentView.addChildView(entry.view);
2596
- entry.attached = true;
2597
- }
2598
- function disposeWebuiEntry(entry) {
2599
- webuiViews.delete(entry.runtimeId);
2600
- entry.pendingCommands.length = 0;
2601
- settlePendingWebuiCommandAcksForRuntime(entry.runtimeId, false);
2602
- if (entry.pendingFlushTimer) {
2603
- clearTimeout(entry.pendingFlushTimer);
2604
- entry.pendingFlushTimer = null;
2605
- }
2606
- if (mainWindow && entry.attached) {
2607
- mainWindow.contentView.removeChildView(entry.view);
2608
- }
2609
- entry.attached = false;
2610
- if (!entry.view.webContents.isDestroyed()) {
2611
- entry.view.webContents.close();
2612
- }
2613
- if (activeWebuiRuntimeId === entry.runtimeId) activeWebuiRuntimeId = null;
2614
- }
2615
- function disposeAllWebuiEntries() {
2616
- for (const entry of Array.from(webuiViews.values())) {
2617
- disposeWebuiEntry(entry);
2618
- }
2619
- webuiViews.clear();
2620
- }
2621
- function pruneWebuiEntries(runtimeIds) {
2622
- const live = new Set(runtimeIds);
2623
- for (const [id, entry] of webuiViews) {
2624
- if (!live.has(id)) {
2625
- disposeWebuiEntry(entry);
2626
- }
2627
- }
2628
- }
2629
- function findWebuiEntryBySenderId(senderId) {
2630
- return Array.from(webuiViews.values()).find(
2631
- (candidate) => candidate.view.webContents.id === senderId
2632
- );
2633
- }
2634
- function syncActiveWebuiView() {
2635
- if (!mainWindow) return;
2636
- const snapshot = manager.snapshot();
2637
- pruneWebuiEntries(
2638
- snapshot.runtimes.filter((runtime) => runtime.status === "running").map((runtime) => runtime.id)
2639
- );
2640
- const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
2641
- if (active?.status !== "running") {
2642
- activeWebuiRuntimeId = active?.id ?? null;
2643
- publishWebuiStatus({ runtimeId: active?.id ?? null, status: "idle" });
2644
- layoutWebuiViews();
2645
- return;
2646
- }
2647
- const url = manager.getRuntimeUrlWithToken(active.id);
2648
- if (!url) {
2649
- activeWebuiRuntimeId = active.id;
2650
- publishWebuiStatus({ runtimeId: active.id, status: "idle" });
2651
- layoutWebuiViews();
2652
- return;
2653
- }
2654
- const entry = ensureWebuiEntry(active.id);
2655
- if (!entry) return;
2656
- activeWebuiRuntimeId = active.id;
2657
- attachWebuiEntry(entry);
2658
- layoutWebuiViews();
2659
- publishWebuiStatus(entry.status);
2660
- if (entry.url !== url) {
2661
- entry.url = url;
2662
- entry.bridgeReady = false;
2663
- setEntryWebuiStatus(entry, { runtimeId: active.id, status: "loading" });
2664
- void entry.view.webContents.loadURL(url).catch((err) => {
2665
- setEntryWebuiStatus(entry, {
2666
- runtimeId: active.id,
2667
- status: "error",
2668
- error: err instanceof Error ? err.message : String(err)
2669
- });
2670
- console.error(
2671
- JSON.stringify({
2672
- level: "error",
2673
- event: "desktop.webui_view_load_failed",
2674
- runtimeId: active.id,
2675
- message: err instanceof Error ? err.message : String(err),
2676
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2677
- })
2678
- );
2679
- });
2680
- }
2681
- }
2682
3070
  function broadcastState() {
2683
3071
  if (!shellView || shellView.webContents.isDestroyed()) return;
2684
3072
  shellView.webContents.send(IPC.stateChanged, manager.snapshot());
2685
3073
  }
2686
- function publishWebuiStatus(next) {
2687
- webuiStatus = next;
2688
- if (!shellView || shellView.webContents.isDestroyed()) return;
2689
- shellView.webContents.send(IPC.webuiStatusChanged, webuiStatus);
2690
- }
2691
- function broadcastLocaleToEmbeddedWebuis(locale) {
2692
- for (const entry of webuiViews.values()) {
2693
- if (entry.view.webContents.isDestroyed()) continue;
2694
- try {
2695
- entry.view.webContents.send(IPC.webuiLocaleChanged, locale);
2696
- } catch {
2697
- }
2698
- }
2699
- }
2700
- function getActiveWebuiEntry() {
2701
- const activeId = manager.snapshot().activeRuntimeId;
2702
- return activeId ? webuiViews.get(activeId) : void 0;
2703
- }
2704
- async function isWebuiCommandBridgeReady(entry) {
2705
- if (webuiViews.get(entry.runtimeId) !== entry || !entry.url) return false;
2706
- return entry.bridgeReady;
2707
- }
2708
- function queueWebuiCommand(entry, command) {
2709
- entry.pendingCommands.push(command);
2710
- if (entry.pendingCommands.length > MAX_PENDING_WEBUI_COMMANDS) {
2711
- entry.pendingCommands.splice(0, entry.pendingCommands.length - MAX_PENDING_WEBUI_COMMANDS);
2712
- }
2713
- entry.pendingFlushAttempts = 0;
2714
- setEntryWebuiStatus(entry, entry.status);
2715
- }
2716
- function nextWebuiCommandRequestId(runtimeId) {
2717
- webuiCommandSequence += 1;
2718
- return `${runtimeId}:${Date.now()}:${webuiCommandSequence}`;
2719
- }
2720
- async function dispatchWebuiCommand(commandInput) {
2721
- const command = normalizeDesktopWebuiCommand(commandInput);
2722
- if (!command) return false;
2723
- const entry = getActiveWebuiEntry();
2724
- if (!entry?.url) return false;
2725
- if (entry.status.status !== "ready") {
2726
- if (!entry.view.webContents.isLoading() && entry.status.status !== "error") {
2727
- return dispatchWebuiCommandNow(entry, command);
2728
- }
2729
- queueWebuiCommand(entry, command);
2730
- schedulePendingWebuiFlush(entry);
2731
- return true;
2732
- }
2733
- if (!await isWebuiCommandBridgeReady(entry)) {
2734
- if (!entry.view.webContents.isLoading()) {
2735
- return dispatchWebuiCommandNow(entry, command);
2736
- }
2737
- queueWebuiCommand(entry, command);
2738
- schedulePendingWebuiFlush(entry);
2739
- return true;
2740
- }
2741
- return dispatchWebuiCommandNow(entry, command);
2742
- }
2743
- async function reloadActiveWebuiView() {
2744
- const entry = getActiveWebuiEntry();
2745
- if (!entry?.url) return false;
2746
- entry.bridgeReady = false;
2747
- setEntryWebuiStatus(entry, { runtimeId: entry.runtimeId, status: "loading" });
2748
- return entry.view.webContents.loadURL(entry.url).then(() => true).catch((err) => {
2749
- setEntryWebuiStatus(entry, {
2750
- runtimeId: entry.runtimeId,
2751
- status: "error",
2752
- error: err instanceof Error ? err.message : String(err)
2753
- });
2754
- console.error(
2755
- JSON.stringify({
2756
- level: "error",
2757
- event: "desktop.webui_view_reload_failed",
2758
- runtimeId: entry.runtimeId,
2759
- message: err instanceof Error ? err.message : String(err),
2760
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2761
- })
2762
- );
2763
- return false;
2764
- });
2765
- }
2766
- async function dispatchWebuiCommandNow(entry, command) {
2767
- if (webuiViews.get(entry.runtimeId) !== entry || !entry.url) return false;
2768
- const requestId = nextWebuiCommandRequestId(entry.runtimeId);
2769
- const commandWithRequestId = { ...command, requestId };
2770
- return new Promise((resolve2) => {
2771
- const fallbackTimer = setTimeout(() => {
2772
- const pending = pendingWebuiCommandAcks.get(requestId);
2773
- if (!pending) return;
2774
- sendWebuiCommandDomFallback(entry, commandWithRequestId);
2775
- }, WEBUI_COMMAND_FALLBACK_MS);
2776
- const timer = setTimeout(() => {
2777
- settlePendingWebuiCommandAck(requestId, false);
2778
- }, WEBUI_COMMAND_ACK_TIMEOUT_MS);
2779
- pendingWebuiCommandAcks.set(requestId, {
2780
- runtimeId: entry.runtimeId,
2781
- timer,
2782
- fallbackTimer,
2783
- resolve: resolve2
2784
- });
2785
- try {
2786
- entry.view.webContents.send(IPC.webuiCommand, commandWithRequestId);
2787
- if (activeWebuiRuntimeId === entry.runtimeId) {
2788
- entry.view.webContents.focus();
2789
- }
2790
- } catch {
2791
- settlePendingWebuiCommandAck(requestId, false);
2792
- }
2793
- });
2794
- }
2795
- function sendWebuiCommandDomFallback(entry, command) {
2796
- if (webuiViews.get(entry.runtimeId) !== entry || entry.view.webContents.isDestroyed()) return;
2797
- void entry.view.webContents.executeJavaScript(buildWebuiCommandFallbackScript(command), true).catch(() => void 0);
2798
- }
2799
- function settlePendingWebuiCommandAck(requestId, handled) {
2800
- const pending = pendingWebuiCommandAcks.get(requestId);
2801
- if (!pending) return;
2802
- pendingWebuiCommandAcks.delete(requestId);
2803
- clearTimeout(pending.timer);
2804
- if (pending.fallbackTimer) clearTimeout(pending.fallbackTimer);
2805
- if (handled) {
2806
- const entry = webuiViews.get(pending.runtimeId);
2807
- if (entry) {
2808
- entry.bridgeReady = true;
2809
- setEntryWebuiStatus(entry, { ...entry.status, status: "ready" });
2810
- }
2811
- }
2812
- pending.resolve(handled);
2813
- }
2814
- function settlePendingWebuiCommandAcksForRuntime(runtimeId, handled) {
2815
- for (const [requestId, pending] of [...pendingWebuiCommandAcks]) {
2816
- if (pending.runtimeId === runtimeId) {
2817
- settlePendingWebuiCommandAck(requestId, handled);
2818
- }
2819
- }
2820
- }
2821
- async function flushPendingWebuiCommands(entry) {
2822
- if (webuiViews.get(entry.runtimeId) !== entry) return;
2823
- if (entry.pendingCommands.length === 0) return;
2824
- if (!await isWebuiCommandBridgeReady(entry)) {
2825
- entry.pendingFlushAttempts += 1;
2826
- const canFallback = !entry.view.webContents.isLoading() && entry.pendingFlushAttempts >= 4;
2827
- if (!canFallback && entry.pendingFlushAttempts <= MAX_PENDING_FLUSH_ATTEMPTS) {
2828
- schedulePendingWebuiFlush(entry);
2829
- setEntryWebuiStatus(entry, entry.status);
2830
- return;
2831
- }
2832
- if (!canFallback) {
2833
- entry.pendingCommands.length = 0;
2834
- setEntryWebuiStatus(entry, {
2835
- runtimeId: entry.runtimeId,
2836
- status: "error",
2837
- error: "WebUI command bridge did not become ready."
2838
- });
2839
- return;
2840
- }
2841
- }
2842
- entry.pendingFlushAttempts = 0;
2843
- const commands = entry.pendingCommands.splice(0, entry.pendingCommands.length);
2844
- setEntryWebuiStatus(entry, entry.status);
2845
- for (const command of commands) {
2846
- await dispatchWebuiCommandNow(entry, command).catch(() => void 0);
2847
- }
2848
- }
2849
- function schedulePendingWebuiFlush(entry) {
2850
- if (entry.pendingFlushTimer) return;
2851
- entry.pendingFlushTimer = setTimeout(() => {
2852
- entry.pendingFlushTimer = null;
2853
- void flushPendingWebuiCommands(entry);
2854
- }, 250);
2855
- }
2856
- async function openProject(requestedRoot) {
2857
- let projectRoot = requestedRoot;
2858
- if (!projectRoot) {
2859
- const result = await dialog.showOpenDialog({
2860
- title: tMain("openProject"),
2861
- properties: ["openDirectory"]
2862
- });
2863
- projectRoot = result.filePaths[0];
2864
- }
2865
- if (!projectRoot) return manager.snapshot();
2866
- await manager.openProject(projectRoot);
2867
- syncActiveWebuiView();
2868
- broadcastState();
2869
- return manager.snapshot();
2870
- }
2871
- async function registerProject(requestedRoot) {
2872
- let projectRoot = requestedRoot;
2873
- if (!projectRoot) {
3074
+ var runtimeOperationsContext = {
3075
+ getRuntimeManager: () => manager,
3076
+ getAgentBridge: () => bridge,
3077
+ broadcastState,
3078
+ syncActiveWebuiView: () => webuiController.syncActive(),
3079
+ dispatchWebuiCommand: (command) => webuiController.dispatch(command),
3080
+ chooseProjectRoot: async (kind) => {
2874
3081
  const result = await dialog.showOpenDialog({
2875
- title: tMain("registerProject"),
3082
+ title: tMain(kind === "open" ? "openProject" : "registerProject"),
2876
3083
  properties: ["openDirectory"]
2877
3084
  });
2878
- projectRoot = result.filePaths[0];
3085
+ return result.filePaths[0];
2879
3086
  }
2880
- if (!projectRoot) return manager.snapshot();
2881
- await manager.registerProject(projectRoot);
2882
- broadcastState();
2883
- return manager.snapshot();
2884
- }
2885
- async function unregisterProject(root) {
2886
- if (!root || typeof root !== "string") return manager.snapshot();
2887
- await manager.unregisterProject(root);
2888
- broadcastState();
2889
- return manager.snapshot();
2890
- }
2891
- async function openProjectSession(runtimeId) {
2892
- const snapshot = manager.snapshot();
2893
- const runtime = (runtimeId ? snapshot.runtimes.find((candidate) => candidate.id === runtimeId) : void 0) ?? snapshot.runtimes.find((candidate) => candidate.id === snapshot.activeRuntimeId);
2894
- if (runtime?.kind !== "project") {
2895
- return openProject();
2896
- }
2897
- await manager.openProject(runtime.root, { forceNew: true });
2898
- syncActiveWebuiView();
2899
- broadcastState();
2900
- return manager.snapshot();
2901
- }
2902
- async function openSettings() {
2903
- const snapshot = manager.snapshot();
2904
- const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
2905
- if (!active || active.kind === "global-settings" || active.status !== "running") {
2906
- const root = desktopSettingsWorkspaceRoot();
2907
- await fs3.mkdir(root, { recursive: true });
2908
- await manager.openProject(root, {
2909
- name: "Global Settings",
2910
- kind: "global-settings",
2911
- touchRecent: false
2912
- });
2913
- syncActiveWebuiView();
2914
- broadcastState();
2915
- }
2916
- await dispatchWebuiCommand({ view: "settings" });
2917
- return manager.snapshot();
2918
- }
2919
- async function activateRuntime(id) {
2920
- await manager.activateRuntime(id);
2921
- syncActiveWebuiView();
2922
- broadcastState();
2923
- return manager.snapshot();
2924
- }
2925
- async function closeRuntime(id) {
2926
- bridge.close(id);
2927
- await manager.closeRuntime(id);
2928
- const entry = webuiViews.get(id);
2929
- if (entry) disposeWebuiEntry(entry);
2930
- syncActiveWebuiView();
2931
- broadcastState();
2932
- return manager.snapshot();
2933
- }
2934
- async function restoreLastWorkspace() {
2935
- await manager.restoreLastWorkspace();
2936
- syncActiveWebuiView();
2937
- broadcastState();
2938
- }
3087
+ };
3088
+ var openProject2 = (root) => openProject(runtimeOperationsContext, root);
3089
+ var registerProject2 = (root) => registerProject(runtimeOperationsContext, root);
3090
+ var unregisterProject2 = (root) => unregisterProject(runtimeOperationsContext, root);
3091
+ var openProjectSession2 = (id) => openProjectSession(runtimeOperationsContext, id);
3092
+ var openSettings2 = () => openSettings(runtimeOperationsContext);
3093
+ var activateRuntime2 = (id) => activateRuntime(runtimeOperationsContext, id);
3094
+ var closeRuntime2 = (id) => closeRuntime(runtimeOperationsContext, id);
3095
+ var restoreLastWorkspace2 = () => restoreLastWorkspace(runtimeOperationsContext);
2939
3096
  function createMenuContext() {
2940
3097
  return {
2941
3098
  getSnapshot: () => manager.snapshot(),
@@ -2946,44 +3103,44 @@ function createMenuContext() {
2946
3103
  getActiveWebuiPrefs: () => {
2947
3104
  const snapshot = manager.snapshot();
2948
3105
  if (!snapshot.activeRuntimeId) return void 0;
2949
- return webuiViews.get(snapshot.activeRuntimeId)?.status.prefs;
3106
+ return webuiController.views.get(snapshot.activeRuntimeId)?.status.prefs;
2950
3107
  },
2951
3108
  getShellSidebarCollapsed: () => shellSidebarCollapsed,
2952
3109
  t: tMain,
2953
3110
  getRuntimeManager: () => manager,
2954
- getWebuiViews: () => webuiViews,
2955
- dispatchWebuiCommand,
2956
- reloadActiveWebuiView,
3111
+ getWebuiViews: () => webuiController.views,
3112
+ dispatchWebuiCommand: (command) => webuiController.dispatch(command),
3113
+ reloadActiveWebuiView: () => webuiController.reload(),
2957
3114
  activateRuntime: async (id) => {
2958
- await activateRuntime(id);
3115
+ await activateRuntime2(id);
2959
3116
  },
2960
3117
  openProject: async () => {
2961
- await openProject();
3118
+ await openProject2();
2962
3119
  },
2963
3120
  registerProject: async () => {
2964
- await registerProject();
3121
+ await registerProject2();
2965
3122
  },
2966
3123
  openSettings: async () => {
2967
- await openSettings();
3124
+ await openSettings2();
2968
3125
  },
2969
3126
  openProjectSession: async (id) => {
2970
- await openProjectSession(id);
3127
+ await openProjectSession2(id);
2971
3128
  },
2972
3129
  closeRuntime: async (id) => {
2973
- await closeRuntime(id);
3130
+ await closeRuntime2(id);
2974
3131
  },
2975
3132
  unregisterProject: async (root) => {
2976
- await unregisterProject(root);
3133
+ await unregisterProject2(root);
2977
3134
  },
2978
- getActiveRuntimeId: () => activeWebuiRuntimeId,
3135
+ getActiveRuntimeId: () => webuiController.activeRuntimeId,
2979
3136
  setShellSidebarCollapsed: (collapsed) => {
2980
3137
  setShellSidebarCollapsed(collapsed);
2981
3138
  },
2982
3139
  restoreLastWorkspace: async () => {
2983
- await restoreLastWorkspace();
3140
+ await restoreLastWorkspace2();
2984
3141
  },
2985
3142
  openExternal: (url) => {
2986
- shell.openExternal(url);
3143
+ safeOpenExternal(url);
2987
3144
  },
2988
3145
  revealInExplorer: (root) => {
2989
3146
  revealInExplorer(root);
@@ -2997,8 +3154,8 @@ function buildIpcHandlerContext() {
2997
3154
  return {
2998
3155
  getMainWindow: () => mainWindow,
2999
3156
  getShellView: () => shellView,
3000
- getWebuiViews: () => webuiViews,
3001
- getWebuiStatus: () => webuiStatus,
3157
+ getWebuiViews: () => webuiController.views,
3158
+ getWebuiStatus: () => webuiController.status,
3002
3159
  getRuntimeManager: () => manager,
3003
3160
  getAgentBridge: () => bridge,
3004
3161
  getI18n: () => ({ getMainLocale, setMainLocale, tMain }),
@@ -3006,37 +3163,40 @@ function buildIpcHandlerContext() {
3006
3163
  getShellSidebarCollapsed: () => shellSidebarCollapsed,
3007
3164
  setShellSidebarCollapsed: (collapsed) => setShellSidebarCollapsed(collapsed),
3008
3165
  broadcastState: () => broadcastState(),
3009
- publishWebuiStatus: (next) => publishWebuiStatus(next),
3010
- syncActiveWebuiView: () => syncActiveWebuiView(),
3166
+ publishWebuiStatus: (next) => {
3167
+ const entry = next.runtimeId ? webuiController.views.get(next.runtimeId) : void 0;
3168
+ if (entry) webuiController.setEntryStatus(entry, next);
3169
+ },
3170
+ syncActiveWebuiView: () => webuiController.syncActive(),
3011
3171
  configureApplicationMenu: () => configureApplicationMenu2(),
3012
- broadcastLocaleToEmbeddedWebuis: (locale) => broadcastLocaleToEmbeddedWebuis(locale),
3013
- dispatchWebuiCommand: (command) => dispatchWebuiCommand(command),
3014
- reloadActiveWebuiView: () => reloadActiveWebuiView(),
3015
- openProject: (root) => openProject(root),
3016
- registerProject: (root) => registerProject(root),
3017
- unregisterProject: (root) => unregisterProject(root),
3018
- openProjectSession: (id) => openProjectSession(id),
3019
- activateRuntime: (id) => activateRuntime(id),
3020
- closeRuntime: (id) => closeRuntime(id),
3021
- openSettings: () => openSettings(),
3172
+ broadcastLocaleToEmbeddedWebuis: (locale) => webuiController.broadcastLocale(locale),
3173
+ dispatchWebuiCommand: (command) => webuiController.dispatch(command),
3174
+ reloadActiveWebuiView: () => webuiController.reload(),
3175
+ openProject: (root) => openProject2(root),
3176
+ registerProject: (root) => registerProject2(root),
3177
+ unregisterProject: (root) => unregisterProject2(root),
3178
+ openProjectSession: (id) => openProjectSession2(id),
3179
+ activateRuntime: (id) => activateRuntime2(id),
3180
+ closeRuntime: (id) => closeRuntime2(id),
3181
+ openSettings: () => openSettings2(),
3022
3182
  sendMessage: (id, wsUrl, content) => bridge.sendMessage(id, wsUrl, content),
3023
3183
  abortRuntime: (id, wsUrl) => bridge.abort(id, wsUrl),
3024
3184
  openExternal: (url) => safeOpenExternal(url),
3025
3185
  revealInExplorer: (root) => {
3026
3186
  revealInExplorer(root);
3027
3187
  },
3028
- findWebuiEntryBySenderId: (senderId) => findWebuiEntryBySenderId(senderId),
3029
- getPendingWebuiCommandAcks: () => pendingWebuiCommandAcks,
3030
- settlePendingWebuiCommandAck: (requestId, handled) => settlePendingWebuiCommandAck(requestId, handled),
3031
- setEntryWebuiStatus: (entry, next) => setEntryWebuiStatus(entry, next),
3032
- schedulePendingWebuiFlush: (entry) => schedulePendingWebuiFlush(entry)
3188
+ findWebuiEntryBySenderId: (senderId) => webuiController.findBySenderId(senderId),
3189
+ getPendingWebuiCommandAcks: () => webuiController.pendingAcks,
3190
+ settlePendingWebuiCommandAck: (requestId, handled) => webuiController.settleAck(requestId, handled),
3191
+ setEntryWebuiStatus: (entry, next) => webuiController.setEntryStatus(entry, next),
3192
+ schedulePendingWebuiFlush: (entry) => webuiController.scheduleFlush(entry)
3033
3193
  };
3034
3194
  }
3035
3195
  async function boot() {
3036
3196
  const locale = await readUiLocale();
3037
3197
  if (locale) setMainLocale(locale);
3038
3198
  const shellUrl = rendererIndexPath();
3039
- shellView = new WebContentsView({
3199
+ shellView = new WebContentsView2({
3040
3200
  webPreferences: {
3041
3201
  preload: preloadPath(),
3042
3202
  contextIsolation: true,
@@ -3050,34 +3210,10 @@ async function boot() {
3050
3210
  });
3051
3211
  registerIpcHandlers(buildIpcHandlerContext());
3052
3212
  await shellView.webContents.loadURL(shellUrl);
3053
- const prevState = validatedWindowState(manager.getWindowState());
3213
+ const prevState = windowStateController.validated(manager.getWindowState());
3054
3214
  const defaultWidth = 1180;
3055
3215
  const defaultHeight = 720;
3056
- let appIcon;
3057
- if (process.platform !== "darwin") {
3058
- try {
3059
- const iconPath = new URL("../../assets/icon.svg", import.meta.url).pathname;
3060
- await fs3.stat(iconPath);
3061
- appIcon = nativeImage.createFromPath(iconPath);
3062
- if (appIcon.isEmpty()) appIcon = void 0;
3063
- } catch {
3064
- }
3065
- } else {
3066
- try {
3067
- const pngPath = new URL("../../assets/icon.png", import.meta.url).pathname;
3068
- await fs3.stat(pngPath);
3069
- appIcon = nativeImage.createFromPath(pngPath);
3070
- if (appIcon.isEmpty()) appIcon = void 0;
3071
- } catch {
3072
- try {
3073
- const icnsPath = new URL("../../assets/icon.icns", import.meta.url).pathname;
3074
- await fs3.stat(icnsPath);
3075
- appIcon = nativeImage.createFromPath(icnsPath);
3076
- if (appIcon.isEmpty()) appIcon = void 0;
3077
- } catch {
3078
- }
3079
- }
3080
- }
3216
+ const appIcon = await loadDesktopAppIcon();
3081
3217
  const winOptions = {
3082
3218
  width: prevState?.width ?? defaultWidth,
3083
3219
  height: prevState?.height ?? defaultHeight,
@@ -3092,10 +3228,10 @@ async function boot() {
3092
3228
  if (prevState.y !== void 0) winOptions.y = prevState.y;
3093
3229
  }
3094
3230
  mainWindow = new BaseWindow(winOptions);
3095
- mainWindow.on("resized", scheduleWindowStateSave);
3096
- mainWindow.on("moved", scheduleWindowStateSave);
3097
- mainWindow.on("maximize", scheduleWindowStateSave);
3098
- mainWindow.on("unmaximize", scheduleWindowStateSave);
3231
+ mainWindow.on("resized", () => windowStateController.scheduleSave());
3232
+ mainWindow.on("moved", () => windowStateController.scheduleSave());
3233
+ mainWindow.on("maximize", () => windowStateController.scheduleSave());
3234
+ mainWindow.on("unmaximize", () => windowStateController.scheduleSave());
3099
3235
  if (prevState?.maximized) {
3100
3236
  mainWindow.maximize();
3101
3237
  }
@@ -3108,7 +3244,7 @@ async function boot() {
3108
3244
  shellView.webContents.send(IPC.conversationChanged, conversation);
3109
3245
  });
3110
3246
  manager.on("changed", () => {
3111
- syncActiveWebuiView();
3247
+ webuiController.syncActive();
3112
3248
  configureApplicationMenu2();
3113
3249
  broadcastState();
3114
3250
  });
@@ -3118,8 +3254,9 @@ async function boot() {
3118
3254
  }
3119
3255
  });
3120
3256
  let lastWatchedLocale;
3257
+ const activeProfileConfigPath = await resolveActiveProfileConfigPath();
3121
3258
  watchProviderConfig(
3122
- desktopConfigPaths.globalConfigPath,
3259
+ activeProfileConfigPath,
3123
3260
  desktopConfigPaths.vault,
3124
3261
  (snapshot) => {
3125
3262
  const updated = snapshot.uiLocale;
@@ -3127,7 +3264,7 @@ async function boot() {
3127
3264
  lastWatchedLocale = updated;
3128
3265
  setMainLocale(updated);
3129
3266
  configureApplicationMenu2();
3130
- broadcastLocaleToEmbeddedWebuis(updated);
3267
+ webuiController.broadcastLocale(updated);
3131
3268
  if (shellView && !shellView.webContents.isDestroyed()) {
3132
3269
  shellView.webContents.send(IPC.localeChanged, updated);
3133
3270
  }
@@ -3137,12 +3274,12 @@ async function boot() {
3137
3274
  if (quittingAfterCleanup) return;
3138
3275
  event.preventDefault();
3139
3276
  bridge.closeAll();
3140
- disposeAllWebuiEntries();
3141
- void saveWindowState();
3277
+ webuiController.disposeAll();
3278
+ void windowStateController.save();
3142
3279
  quittingAfterCleanup = true;
3143
3280
  app2.exit(0);
3144
3281
  });
3145
- await restoreLastWorkspace();
3282
+ await restoreLastWorkspace2();
3146
3283
  const argvOpenPath = firstOpenFileArg(process.argv);
3147
3284
  const queuedOpenPath = drainPendingOpenFilePath();
3148
3285
  const openPath = argvOpenPath ?? queuedOpenPath;
@@ -3161,10 +3298,10 @@ app2.on("window-all-closed", () => {
3161
3298
  app2.on("before-quit", () => {
3162
3299
  if (mainWindow) {
3163
3300
  mainWindow.removeAllListeners("close");
3164
- void saveWindowState();
3301
+ void windowStateController.save();
3165
3302
  }
3166
3303
  bridge.closeAll();
3167
- disposeAllWebuiEntries();
3304
+ webuiController.disposeAll();
3168
3305
  });
3169
3306
  app2.on("activate", () => {
3170
3307
  if (!mainWindow) return;