@wrongstack/desktop 0.292.1 → 0.295.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,16 +1,13 @@
1
1
  // src/main/main.ts
2
2
  import * as path4 from "node:path";
3
- import * as fs3 from "node:fs/promises";
4
- import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
3
+ import { resolveWstackPaths as resolveWstackPaths2 } 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
@@ -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,6 +141,7 @@ 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");
@@ -141,7 +160,7 @@ 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
166
  resolve2();
@@ -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,7 +902,8 @@ 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
@@ -811,15 +918,30 @@ import * as net from "node:net";
811
918
  import * as os from "node:os";
812
919
  import * as path2 from "node:path";
813
920
  import { fileURLToPath } from "node:url";
814
- import { atomicWrite as atomicWrite2, projectSlug, toErrorMessage, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
921
+ import {
922
+ atomicWrite as atomicWrite2,
923
+ buildChildEnv,
924
+ projectSlug,
925
+ resolveWstackPaths,
926
+ toErrorMessage,
927
+ wstackGlobalRoot as wstackGlobalRoot2
928
+ } from "@wrongstack/core/utils";
815
929
  var HTTP_PORT_START = 34560;
816
930
  var WS_PORT_START = 34660;
817
931
  var START_TIMEOUT_MS = 3e4;
818
932
  var MIN_WINDOW_WIDTH = 760;
819
933
  var MIN_WINDOW_HEIGHT = 520;
820
934
  var DesktopRuntimeManager = class extends EventEmitter2 {
935
+ constructor(trustBoundary = desktopCompatibilityTrustBoundary) {
936
+ super();
937
+ this.trustBoundary = trustBoundary;
938
+ }
939
+ trustBoundary;
821
940
  runtimes = /* @__PURE__ */ new Map();
822
- stateFile = path2.join(wstackGlobalRoot2(), "desktop.json");
941
+ stateFile = path2.join(
942
+ resolveWstackPaths({ projectRoot: process.cwd() }).configDir,
943
+ "desktop.json"
944
+ );
823
945
  recentProjects = [];
824
946
  registeredProjects = [];
825
947
  restoreProjectSessions = [];
@@ -930,6 +1052,10 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
930
1052
  const kind = options.kind ?? "project";
931
1053
  const touchRecent = options.touchRecent ?? kind === "project";
932
1054
  const forceNew = options.forceNew === true;
1055
+ const authorization = await authorizeDesktopRuntimeStart(this.trustBoundary, resolved, kind);
1056
+ if (!authorization.allowed) {
1057
+ throw new Error(`Desktop runtime start denied: ${authorization.reason}`);
1058
+ }
933
1059
  if (!forceNew) {
934
1060
  const existing = Array.from(this.runtimes.values()).find(
935
1061
  (runtime2) => samePath(runtime2.root, resolved) && runtime2.kind === kind && (runtime2.status === "starting" || runtime2.status === "running")
@@ -1009,7 +1135,7 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1009
1135
  {
1010
1136
  cwd: resolved,
1011
1137
  env: {
1012
- ...process.env,
1138
+ ...buildChildEnv(),
1013
1139
  ELECTRON_RUN_AS_NODE: "1",
1014
1140
  WEBUI_STRICT_PORT: "1",
1015
1141
  WRONGSTACK_DESKTOP: "1"
@@ -1088,6 +1214,13 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1088
1214
  this.emitChanged();
1089
1215
  }
1090
1216
  async closeRuntime(id) {
1217
+ const runtime = this.runtimes.get(id);
1218
+ if (runtime) {
1219
+ const authorization = await authorizeDesktopRuntimeStop(this.trustBoundary, runtime);
1220
+ if (!authorization.allowed) {
1221
+ throw new Error(`Desktop runtime stop denied: ${authorization.reason}`);
1222
+ }
1223
+ }
1091
1224
  await this.closeRuntimeInternal(id, { persistWorkspace: true });
1092
1225
  }
1093
1226
  async closeAll(options = {}) {
@@ -1589,12 +1722,28 @@ function webuiPreloadPath() {
1589
1722
  return fileURLToPath(new URL("../preload/webui-preload.cjs", import.meta.url));
1590
1723
  }
1591
1724
  function desktopSettingsWorkspaceRoot() {
1592
- return path2.join(wstackGlobalRoot2(), "settings");
1725
+ return path2.join(resolveWstackPaths({ projectRoot: process.cwd() }).configDir, "settings");
1593
1726
  }
1594
1727
 
1595
1728
  // src/main/main.ts
1596
1729
  import { watchProviderConfig } from "@wrongstack/core/storage";
1597
1730
 
1731
+ // src/main/webui/controller.ts
1732
+ import { shell, WebContentsView } from "electron";
1733
+
1734
+ // src/main/state/constants.ts
1735
+ var OPEN_EXTERNAL_ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
1736
+ var SIDEBAR_WIDTH_WIDE = 292;
1737
+ var SIDEBAR_WIDTH_MEDIUM = 276;
1738
+ var SIDEBAR_WIDTH_NARROW = 252;
1739
+ var SIDEBAR_WIDTH_COLLAPSED = 56;
1740
+ var MIN_WINDOW_WIDTH2 = 760;
1741
+ var MIN_WINDOW_HEIGHT2 = 520;
1742
+ var MAX_PENDING_WEBUI_COMMANDS = 50;
1743
+ var MAX_PENDING_FLUSH_ATTEMPTS = 80;
1744
+ var WEBUI_COMMAND_FALLBACK_MS = 350;
1745
+ var WEBUI_COMMAND_ACK_TIMEOUT_MS = 2e3;
1746
+
1598
1747
  // src/main/webui-command-bridge.ts
1599
1748
  var DESKTOP_WEBUI_ACTIONS = /* @__PURE__ */ new Set([
1600
1749
  "new-session",
@@ -1746,18 +1895,430 @@ function isRecord(value) {
1746
1895
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1747
1896
  }
1748
1897
 
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;
1898
+ // src/main/webui/navigation.ts
1899
+ function allowedExternalProtocol(target) {
1900
+ try {
1901
+ const protocol = new URL(target).protocol;
1902
+ return OPEN_EXTERNAL_ALLOWED_PROTOCOLS.has(protocol) ? protocol : void 0;
1903
+ } catch {
1904
+ return void 0;
1905
+ }
1906
+ }
1907
+ function sameOrigin(candidate, base) {
1908
+ if (!base) return false;
1909
+ try {
1910
+ return new URL(candidate).origin === new URL(base).origin;
1911
+ } catch {
1912
+ return false;
1913
+ }
1914
+ }
1915
+
1916
+ // src/main/webui/controller.ts
1917
+ var DesktopWebuiController = class {
1918
+ constructor(ctx) {
1919
+ this.ctx = ctx;
1920
+ }
1921
+ ctx;
1922
+ views = /* @__PURE__ */ new Map();
1923
+ pendingAcks = /* @__PURE__ */ new Map();
1924
+ activeRuntimeId = null;
1925
+ status = { runtimeId: null, status: "idle" };
1926
+ commandSequence = 0;
1927
+ openExternal(target) {
1928
+ const protocol = allowedExternalProtocol(target);
1929
+ if (!protocol) return;
1930
+ void authorizeDesktopAction(this.ctx.trustBoundary, {
1931
+ capability: "url.open-external",
1932
+ subject: { kind: "url", id: target, attributes: { protocol } },
1933
+ risk: "elevated",
1934
+ metadata: { operation: "webui-navigation" }
1935
+ }).then(
1936
+ (decision) => decision.allowed ? void shell.openExternal(target) : void 0
1937
+ ).catch(() => void 0);
1938
+ }
1939
+ publishStatus(next) {
1940
+ this.status = next;
1941
+ const shellView2 = this.ctx.getShellView();
1942
+ if (!shellView2 || shellView2.webContents.isDestroyed()) return;
1943
+ shellView2.webContents.send(IPC.webuiStatusChanged, next);
1944
+ }
1945
+ setEntryStatus(entry, next) {
1946
+ const previousPrefs = entry.status.prefs;
1947
+ entry.status = {
1948
+ ...next,
1949
+ ...next.prefs === void 0 && previousPrefs !== void 0 ? { prefs: previousPrefs } : {},
1950
+ // `pendingCommands` is always derived from the entry's own array (source of truth).
1951
+ // Any `next.pendingCommands` value is intentionally overwritten.
1952
+ pendingCommands: entry.pendingCommands.length
1953
+ };
1954
+ if (this.activeRuntimeId === entry.runtimeId) {
1955
+ this.publishStatus(entry.status);
1956
+ this.ctx.onPrefsChanged?.(previousPrefs, entry.status.prefs);
1957
+ }
1958
+ }
1959
+ ensure(runtimeId) {
1960
+ const mainWindow2 = this.ctx.getMainWindow();
1961
+ if (!mainWindow2) return null;
1962
+ const existing = this.views.get(runtimeId);
1963
+ if (existing) return existing;
1964
+ const view = new WebContentsView({
1965
+ webPreferences: {
1966
+ preload: webuiPreloadPath(),
1967
+ contextIsolation: true,
1968
+ nodeIntegration: false,
1969
+ sandbox: false
1970
+ }
1971
+ });
1972
+ const entry = {
1973
+ runtimeId,
1974
+ view,
1975
+ url: null,
1976
+ status: { runtimeId, status: "idle" },
1977
+ bridgeReady: false,
1978
+ attached: false,
1979
+ pendingCommands: [],
1980
+ pendingFlushTimer: null,
1981
+ pendingFlushAttempts: 0
1982
+ };
1983
+ view.webContents.setWindowOpenHandler(({ url }) => {
1984
+ this.openExternal(url);
1985
+ return { action: "deny" };
1986
+ });
1987
+ view.webContents.on("will-navigate", (event, url) => {
1988
+ if (sameOrigin(url, entry.url)) return;
1989
+ event.preventDefault();
1990
+ this.openExternal(url);
1991
+ });
1992
+ view.webContents.on("did-start-loading", () => {
1993
+ if (this.views.get(runtimeId) !== entry) return;
1994
+ entry.bridgeReady = false;
1995
+ this.setEntryStatus(entry, { runtimeId, status: "loading" });
1996
+ });
1997
+ view.webContents.on("did-finish-load", () => {
1998
+ if (this.views.get(runtimeId) !== entry) return;
1999
+ this.scheduleFlush(entry);
2000
+ try {
2001
+ entry.view.webContents.send(IPC.webuiLocaleChanged, this.ctx.getLocale());
2002
+ } catch {
2003
+ }
2004
+ });
2005
+ view.webContents.on("did-fail-load", (_event, errorCode, errorDescription) => {
2006
+ if (this.views.get(runtimeId) !== entry || errorCode === -3) return;
2007
+ this.setEntryStatus(entry, { runtimeId, status: "error", error: errorDescription });
2008
+ });
2009
+ view.webContents.on("render-process-gone", (_event, details) => {
2010
+ if (this.views.get(runtimeId) !== entry) return;
2011
+ this.setEntryStatus(entry, {
2012
+ runtimeId,
2013
+ status: "error",
2014
+ error: `WebUI renderer exited: ${details.reason}`
2015
+ });
2016
+ });
2017
+ this.views.set(runtimeId, entry);
2018
+ return entry;
2019
+ }
2020
+ attach(entry) {
2021
+ const mainWindow2 = this.ctx.getMainWindow();
2022
+ if (!mainWindow2 || entry.attached) return;
2023
+ mainWindow2.contentView.addChildView(entry.view);
2024
+ entry.attached = true;
2025
+ }
2026
+ dispose(entry) {
2027
+ this.views.delete(entry.runtimeId);
2028
+ entry.pendingCommands.length = 0;
2029
+ this.settleRuntimeAcks(entry.runtimeId, false);
2030
+ if (entry.pendingFlushTimer) clearTimeout(entry.pendingFlushTimer);
2031
+ entry.pendingFlushTimer = null;
2032
+ const mainWindow2 = this.ctx.getMainWindow();
2033
+ if (mainWindow2 && entry.attached) mainWindow2.contentView.removeChildView(entry.view);
2034
+ entry.attached = false;
2035
+ if (!entry.view.webContents.isDestroyed()) entry.view.webContents.close();
2036
+ if (this.activeRuntimeId === entry.runtimeId) this.activeRuntimeId = null;
2037
+ }
2038
+ disposeAll() {
2039
+ for (const entry of [...this.views.values()]) this.dispose(entry);
2040
+ }
2041
+ findBySenderId(senderId) {
2042
+ return [...this.views.values()].find((entry) => entry.view.webContents.id === senderId);
2043
+ }
2044
+ syncActive() {
2045
+ if (!this.ctx.getMainWindow()) return;
2046
+ const snapshot = this.ctx.manager.snapshot();
2047
+ const live = new Set(snapshot.runtimes.filter((r) => r.status === "running").map((r) => r.id));
2048
+ for (const [id, entry2] of this.views) if (!live.has(id)) this.dispose(entry2);
2049
+ const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
2050
+ if (active?.status !== "running") {
2051
+ this.activeRuntimeId = active?.id ?? null;
2052
+ this.publishStatus({ runtimeId: active?.id ?? null, status: "idle" });
2053
+ this.ctx.layoutViews();
2054
+ return;
2055
+ }
2056
+ const url = this.ctx.manager.getRuntimeUrlWithToken(active.id);
2057
+ if (!url) {
2058
+ this.activeRuntimeId = active.id;
2059
+ this.publishStatus({ runtimeId: active.id, status: "idle" });
2060
+ this.ctx.layoutViews();
2061
+ return;
2062
+ }
2063
+ const entry = this.ensure(active.id);
2064
+ if (!entry) return;
2065
+ this.activeRuntimeId = active.id;
2066
+ this.attach(entry);
2067
+ this.ctx.layoutViews();
2068
+ this.publishStatus(entry.status);
2069
+ if (entry.url === url) return;
2070
+ entry.url = url;
2071
+ entry.bridgeReady = false;
2072
+ this.setEntryStatus(entry, { runtimeId: active.id, status: "loading" });
2073
+ void entry.view.webContents.loadURL(url).catch((error) => {
2074
+ this.setEntryStatus(entry, {
2075
+ runtimeId: active.id,
2076
+ status: "error",
2077
+ error: error instanceof Error ? error.message : String(error)
2078
+ });
2079
+ });
2080
+ }
2081
+ broadcastLocale(locale) {
2082
+ for (const entry of this.views.values()) {
2083
+ if (!entry.view.webContents.isDestroyed())
2084
+ entry.view.webContents.send(IPC.webuiLocaleChanged, locale);
2085
+ }
2086
+ }
2087
+ activeEntry() {
2088
+ const id = this.ctx.manager.snapshot().activeRuntimeId;
2089
+ return id ? this.views.get(id) : void 0;
2090
+ }
2091
+ async dispatch(commandInput) {
2092
+ const command = normalizeDesktopWebuiCommand(commandInput);
2093
+ if (!command) return false;
2094
+ const entry = this.activeEntry();
2095
+ if (!entry?.url) return false;
2096
+ if (entry.status.status !== "ready" || !entry.bridgeReady) {
2097
+ if (!entry.view.webContents.isLoading() && entry.status.status !== "error")
2098
+ return this.dispatchNow(entry, command);
2099
+ this.queue(entry, command);
2100
+ this.scheduleFlush(entry);
2101
+ return true;
2102
+ }
2103
+ return this.dispatchNow(entry, command);
2104
+ }
2105
+ async reload() {
2106
+ const entry = this.activeEntry();
2107
+ if (!entry?.url) return false;
2108
+ entry.bridgeReady = false;
2109
+ this.setEntryStatus(entry, { runtimeId: entry.runtimeId, status: "loading" });
2110
+ return entry.view.webContents.loadURL(entry.url).then(() => true).catch((error) => {
2111
+ this.setEntryStatus(entry, {
2112
+ runtimeId: entry.runtimeId,
2113
+ status: "error",
2114
+ error: error instanceof Error ? error.message : String(error)
2115
+ });
2116
+ return false;
2117
+ });
2118
+ }
2119
+ queue(entry, command) {
2120
+ entry.pendingCommands.push(command);
2121
+ if (entry.pendingCommands.length > MAX_PENDING_WEBUI_COMMANDS)
2122
+ entry.pendingCommands.splice(0, entry.pendingCommands.length - MAX_PENDING_WEBUI_COMMANDS);
2123
+ entry.pendingFlushAttempts = 0;
2124
+ this.setEntryStatus(entry, entry.status);
2125
+ }
2126
+ dispatchNow(entry, command) {
2127
+ if (this.views.get(entry.runtimeId) !== entry || !entry.url) return Promise.resolve(false);
2128
+ const requestId = `${entry.runtimeId}:${Date.now()}:${++this.commandSequence}`;
2129
+ const outbound = { ...command, requestId };
2130
+ return new Promise((resolve2) => {
2131
+ const fallbackTimer = setTimeout(() => {
2132
+ if (!this.pendingAcks.has(requestId)) return;
2133
+ if (this.views.get(entry.runtimeId) !== entry || entry.view.webContents.isDestroyed()) return;
2134
+ void entry.view.webContents.executeJavaScript(buildWebuiCommandFallbackScript(outbound), true).catch(() => void 0);
2135
+ }, WEBUI_COMMAND_FALLBACK_MS);
2136
+ const timer = setTimeout(() => this.settleAck(requestId, false), WEBUI_COMMAND_ACK_TIMEOUT_MS);
2137
+ this.pendingAcks.set(requestId, { runtimeId: entry.runtimeId, timer, fallbackTimer, resolve: resolve2 });
2138
+ try {
2139
+ entry.view.webContents.send(IPC.webuiCommand, outbound);
2140
+ if (this.activeRuntimeId === entry.runtimeId) entry.view.webContents.focus();
2141
+ } catch {
2142
+ this.settleAck(requestId, false);
2143
+ }
2144
+ });
2145
+ }
2146
+ settleAck(requestId, handled) {
2147
+ const pending = this.pendingAcks.get(requestId);
2148
+ if (!pending) return;
2149
+ this.pendingAcks.delete(requestId);
2150
+ clearTimeout(pending.timer);
2151
+ if (pending.fallbackTimer) clearTimeout(pending.fallbackTimer);
2152
+ if (handled) {
2153
+ const entry = this.views.get(pending.runtimeId);
2154
+ if (entry) {
2155
+ entry.bridgeReady = true;
2156
+ this.setEntryStatus(entry, { ...entry.status, status: "ready" });
2157
+ }
2158
+ }
2159
+ pending.resolve(handled);
2160
+ }
2161
+ settleRuntimeAcks(runtimeId, handled) {
2162
+ for (const [id, pending] of [...this.pendingAcks])
2163
+ if (pending.runtimeId === runtimeId) this.settleAck(id, handled);
2164
+ }
2165
+ scheduleFlush(entry) {
2166
+ if (entry.pendingFlushTimer) return;
2167
+ entry.pendingFlushTimer = setTimeout(() => {
2168
+ entry.pendingFlushTimer = null;
2169
+ void this.flush(entry);
2170
+ }, 250);
2171
+ }
2172
+ async flush(entry) {
2173
+ if (this.views.get(entry.runtimeId) !== entry || entry.pendingCommands.length === 0) return;
2174
+ if (!entry.bridgeReady) {
2175
+ entry.pendingFlushAttempts += 1;
2176
+ const shouldExecuteFallback = !entry.view.webContents.isLoading() && entry.pendingFlushAttempts >= 4;
2177
+ if (!shouldExecuteFallback && entry.pendingFlushAttempts <= MAX_PENDING_FLUSH_ATTEMPTS) {
2178
+ this.scheduleFlush(entry);
2179
+ this.setEntryStatus(entry, entry.status);
2180
+ return;
2181
+ }
2182
+ if (!shouldExecuteFallback) {
2183
+ entry.pendingCommands.length = 0;
2184
+ this.setEntryStatus(entry, { runtimeId: entry.runtimeId, status: "error", error: "WebUI command bridge did not become ready." });
2185
+ return;
2186
+ }
2187
+ }
2188
+ entry.pendingFlushAttempts = 0;
2189
+ const commands = entry.pendingCommands.splice(0);
2190
+ this.setEntryStatus(entry, entry.status);
2191
+ for (const command of commands) await this.dispatchNow(entry, command).catch(() => void 0);
2192
+ }
2193
+ };
2194
+
2195
+ // src/main/app-icon.ts
2196
+ import * as fs3 from "node:fs/promises";
2197
+ import { nativeImage } from "electron";
2198
+ async function readIcon(relativePath) {
2199
+ try {
2200
+ const iconPath = new URL(relativePath, import.meta.url).pathname;
2201
+ await fs3.stat(iconPath);
2202
+ const icon = nativeImage.createFromPath(iconPath);
2203
+ return icon.isEmpty() ? void 0 : icon;
2204
+ } catch {
2205
+ return void 0;
2206
+ }
2207
+ }
2208
+ async function loadDesktopAppIcon() {
2209
+ if (process.platform !== "darwin") return readIcon("../../assets/icon.svg");
2210
+ return await readIcon("../../assets/icon.png") ?? readIcon("../../assets/icon.icns");
2211
+ }
2212
+
2213
+ // src/main/window-state-controller.ts
2214
+ var DesktopWindowStateController = class {
2215
+ constructor(ctx) {
2216
+ this.ctx = ctx;
2217
+ }
2218
+ ctx;
2219
+ saveTimer = null;
2220
+ scheduleSave() {
2221
+ if (this.saveTimer) clearTimeout(this.saveTimer);
2222
+ this.saveTimer = setTimeout(() => {
2223
+ this.saveTimer = null;
2224
+ void this.save();
2225
+ }, 350);
2226
+ }
2227
+ async save() {
2228
+ const window = this.ctx.getWindow();
2229
+ if (!window || window.isDestroyed?.()) return;
2230
+ const bounds = window.getNormalBounds();
2231
+ await this.ctx.save({ ...bounds, maximized: window.isMaximized() });
2232
+ }
2233
+ validated(state) {
2234
+ if (!state || !Number.isFinite(state.width) || !Number.isFinite(state.height) || state.width < MIN_WINDOW_WIDTH2 || state.height < MIN_WINDOW_HEIGHT2)
2235
+ return null;
2236
+ if (state.x === void 0 || state.y === void 0) {
2237
+ return { width: state.width, height: state.height, maximized: state.maximized };
2238
+ }
2239
+ const candidate = { x: state.x, y: state.y, width: state.width, height: state.height };
2240
+ 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;
2241
+ }
2242
+ };
2243
+ function intersects(left, right) {
2244
+ 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;
2245
+ }
2246
+
2247
+ // src/main/runtime/operations.ts
2248
+ import * as fs4 from "node:fs/promises";
2249
+ async function openProject(ctx, requestedRoot) {
2250
+ let projectRoot = requestedRoot;
2251
+ if (!projectRoot) {
2252
+ projectRoot = await ctx.chooseProjectRoot("open");
2253
+ }
2254
+ if (!projectRoot) return ctx.getRuntimeManager().snapshot();
2255
+ await ctx.getRuntimeManager().openProject(projectRoot);
2256
+ ctx.syncActiveWebuiView();
2257
+ ctx.broadcastState();
2258
+ return ctx.getRuntimeManager().snapshot();
2259
+ }
2260
+ async function registerProject(ctx, requestedRoot) {
2261
+ let projectRoot = requestedRoot;
2262
+ if (!projectRoot) {
2263
+ projectRoot = await ctx.chooseProjectRoot("register");
2264
+ }
2265
+ if (!projectRoot) return ctx.getRuntimeManager().snapshot();
2266
+ await ctx.getRuntimeManager().registerProject(projectRoot);
2267
+ ctx.broadcastState();
2268
+ return ctx.getRuntimeManager().snapshot();
2269
+ }
2270
+ async function unregisterProject(ctx, root) {
2271
+ if (!root || typeof root !== "string") return ctx.getRuntimeManager().snapshot();
2272
+ await ctx.getRuntimeManager().unregisterProject(root);
2273
+ ctx.broadcastState();
2274
+ return ctx.getRuntimeManager().snapshot();
2275
+ }
2276
+ async function openProjectSession(ctx, runtimeId) {
2277
+ const snapshot = ctx.getRuntimeManager().snapshot();
2278
+ const runtime = (runtimeId ? snapshot.runtimes.find((candidate) => candidate.id === runtimeId) : void 0) ?? snapshot.runtimes.find((candidate) => candidate.id === snapshot.activeRuntimeId);
2279
+ if (runtime?.kind !== "project") {
2280
+ return openProject(ctx);
2281
+ }
2282
+ await ctx.getRuntimeManager().openProject(runtime.root, { forceNew: true });
2283
+ ctx.syncActiveWebuiView();
2284
+ ctx.broadcastState();
2285
+ return ctx.getRuntimeManager().snapshot();
2286
+ }
2287
+ async function openSettings(ctx) {
2288
+ const snapshot = ctx.getRuntimeManager().snapshot();
2289
+ const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
2290
+ if (!active || active.kind === "global-settings" || active.status !== "running") {
2291
+ const root = desktopSettingsWorkspaceRoot();
2292
+ await fs4.mkdir(root, { recursive: true });
2293
+ await ctx.getRuntimeManager().openProject(root, {
2294
+ name: "Global Settings",
2295
+ kind: "global-settings",
2296
+ touchRecent: false
2297
+ });
2298
+ ctx.syncActiveWebuiView();
2299
+ ctx.broadcastState();
2300
+ }
2301
+ await ctx.dispatchWebuiCommand({ view: "settings" });
2302
+ return ctx.getRuntimeManager().snapshot();
2303
+ }
2304
+ async function activateRuntime(ctx, id) {
2305
+ await ctx.getRuntimeManager().activateRuntime(id);
2306
+ ctx.syncActiveWebuiView();
2307
+ ctx.broadcastState();
2308
+ return ctx.getRuntimeManager().snapshot();
2309
+ }
2310
+ async function closeRuntime(ctx, id) {
2311
+ ctx.getAgentBridge().close(id);
2312
+ await ctx.getRuntimeManager().closeRuntime(id);
2313
+ ctx.syncActiveWebuiView();
2314
+ ctx.broadcastState();
2315
+ return ctx.getRuntimeManager().snapshot();
2316
+ }
2317
+ async function restoreLastWorkspace(ctx) {
2318
+ await ctx.getRuntimeManager().restoreLastWorkspace();
2319
+ ctx.syncActiveWebuiView();
2320
+ ctx.broadcastState();
2321
+ }
1761
2322
 
1762
2323
  // src/main/layout/sidebar.ts
1763
2324
  function getSidebarWidth(windowWidth, collapsed) {
@@ -2394,52 +2955,74 @@ initMacOS();
2394
2955
  if (process.platform === "win32") {
2395
2956
  app2.setAppUserModelId("com.wrongstack.desktop");
2396
2957
  }
2397
- app2.setPath("userData", path4.join(wstackGlobalRoot3(), "desktop", "electron-profile"));
2398
- var manager = new DesktopRuntimeManager();
2958
+ app2.setPath(
2959
+ "userData",
2960
+ path4.join(
2961
+ resolveWstackPaths2({ projectRoot: process.cwd() }).configDir,
2962
+ "desktop",
2963
+ "electron-profile"
2964
+ )
2965
+ );
2966
+ var desktopTrustBoundary = desktopCompatibilityTrustBoundary;
2967
+ var manager = new DesktopRuntimeManager(desktopTrustBoundary);
2399
2968
  var bridge = new DesktopAgentBridge();
2400
2969
  var mainWindow = null;
2401
2970
  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
2971
  var shellSidebarCollapsed = false;
2407
- var pendingWebuiCommandAcks = /* @__PURE__ */ new Map();
2408
- var saveWindowStateTimer = null;
2409
2972
  var quittingAfterCleanup = false;
2410
- function safeOpenExternal(target) {
2411
- let protocol;
2412
- try {
2413
- protocol = new URL(target).protocol;
2414
- } catch {
2415
- return;
2973
+ var webuiController = new DesktopWebuiController({
2974
+ manager,
2975
+ trustBoundary: desktopTrustBoundary,
2976
+ getMainWindow: () => mainWindow,
2977
+ getShellView: () => shellView,
2978
+ getLocale: getMainLocale,
2979
+ layoutViews: () => layoutWebuiViews(),
2980
+ onPrefsChanged: (previous, next) => {
2981
+ if (menuRelevantPrefsChanged(previous, next)) configureApplicationMenu2();
2416
2982
  }
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;
2983
+ });
2984
+ var windowStateController = new DesktopWindowStateController({
2985
+ getWindow: () => mainWindow,
2986
+ getDisplays: () => screen.getAllDisplays(),
2987
+ save: (state) => manager.saveWindowState(state)
2988
+ });
2989
+ function safeOpenExternal(target) {
2990
+ const protocol = allowedExternalProtocol(target);
2991
+ if (protocol) {
2992
+ void authorizeDesktopAction(desktopTrustBoundary, {
2993
+ capability: "url.open-external",
2994
+ subject: { kind: "url", id: target, attributes: { protocol } },
2995
+ risk: "elevated",
2996
+ metadata: { operation: "open-external" }
2997
+ }).then((authorization) => {
2998
+ if (authorization.allowed) return shell2.openExternal(target);
2999
+ return void 0;
3000
+ });
2427
3001
  }
2428
3002
  }
2429
3003
  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
- );
3004
+ void authorizeDesktopAction(desktopTrustBoundary, {
3005
+ capability: "filesystem.open-native",
3006
+ subject: { kind: "path", id: root, attributes: { target: "file-manager" } },
3007
+ risk: "elevated",
3008
+ cwd: root,
3009
+ metadata: { operation: "reveal-in-explorer" }
3010
+ }).then((authorization) => {
3011
+ if (!authorization.allowed) return;
3012
+ return shell2.openPath(root).catch((err) => {
3013
+ if (process.platform === "darwin") {
3014
+ void shell2.openPath(path4.dirname(root)).catch(() => void 0);
3015
+ }
3016
+ console.error(
3017
+ JSON.stringify({
3018
+ level: "warn",
3019
+ event: "desktop.reveal_in_explorer_failed",
3020
+ root,
3021
+ message: err instanceof Error ? err.message : String(err),
3022
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3023
+ })
3024
+ );
3025
+ });
2443
3026
  });
2444
3027
  }
2445
3028
  function setShellSidebarCollapsed(collapsed) {
@@ -2452,57 +3035,6 @@ function setShellSidebarCollapsed(collapsed) {
2452
3035
  function menuRelevantPrefsChanged(previous, next) {
2453
3036
  return previous?.yolo !== next?.yolo || previous?.nextPrediction !== next?.nextPrediction || previous?.contextAutoCompact !== next?.contextAutoCompact;
2454
3037
  }
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
3038
  function layoutViews() {
2507
3039
  if (!mainWindow || !shellView) return;
2508
3040
  const size = mainWindow.getContentSize();
@@ -2520,7 +3052,7 @@ function layoutWebuiViews() {
2520
3052
  const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
2521
3053
  const sidebarWidth = getSidebarWidth(width, shellSidebarCollapsed);
2522
3054
  const contentWidth = Math.max(0, width - sidebarWidth);
2523
- for (const entry of webuiViews.values()) {
3055
+ for (const entry of webuiController.views.values()) {
2524
3056
  const runtime = snapshot.runtimes.find((r) => r.id === entry.runtimeId);
2525
3057
  if (active?.id === entry.runtimeId && runtime?.status === "running") {
2526
3058
  entry.view.setBounds({ x: sidebarWidth, y: 0, width: contentWidth, height });
@@ -2529,413 +3061,32 @@ function layoutWebuiViews() {
2529
3061
  }
2530
3062
  }
2531
3063
  }
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
3064
  function broadcastState() {
2683
3065
  if (!shellView || shellView.webContents.isDestroyed()) return;
2684
3066
  shellView.webContents.send(IPC.stateChanged, manager.snapshot());
2685
3067
  }
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) {
3068
+ var runtimeOperationsContext = {
3069
+ getRuntimeManager: () => manager,
3070
+ getAgentBridge: () => bridge,
3071
+ broadcastState,
3072
+ syncActiveWebuiView: () => webuiController.syncActive(),
3073
+ dispatchWebuiCommand: (command) => webuiController.dispatch(command),
3074
+ chooseProjectRoot: async (kind) => {
2859
3075
  const result = await dialog.showOpenDialog({
2860
- title: tMain("openProject"),
3076
+ title: tMain(kind === "open" ? "openProject" : "registerProject"),
2861
3077
  properties: ["openDirectory"]
2862
3078
  });
2863
- projectRoot = result.filePaths[0];
3079
+ return result.filePaths[0];
2864
3080
  }
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) {
2874
- const result = await dialog.showOpenDialog({
2875
- title: tMain("registerProject"),
2876
- properties: ["openDirectory"]
2877
- });
2878
- projectRoot = result.filePaths[0];
2879
- }
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
- }
3081
+ };
3082
+ var openProject2 = (root) => openProject(runtimeOperationsContext, root);
3083
+ var registerProject2 = (root) => registerProject(runtimeOperationsContext, root);
3084
+ var unregisterProject2 = (root) => unregisterProject(runtimeOperationsContext, root);
3085
+ var openProjectSession2 = (id) => openProjectSession(runtimeOperationsContext, id);
3086
+ var openSettings2 = () => openSettings(runtimeOperationsContext);
3087
+ var activateRuntime2 = (id) => activateRuntime(runtimeOperationsContext, id);
3088
+ var closeRuntime2 = (id) => closeRuntime(runtimeOperationsContext, id);
3089
+ var restoreLastWorkspace2 = () => restoreLastWorkspace(runtimeOperationsContext);
2939
3090
  function createMenuContext() {
2940
3091
  return {
2941
3092
  getSnapshot: () => manager.snapshot(),
@@ -2946,44 +3097,44 @@ function createMenuContext() {
2946
3097
  getActiveWebuiPrefs: () => {
2947
3098
  const snapshot = manager.snapshot();
2948
3099
  if (!snapshot.activeRuntimeId) return void 0;
2949
- return webuiViews.get(snapshot.activeRuntimeId)?.status.prefs;
3100
+ return webuiController.views.get(snapshot.activeRuntimeId)?.status.prefs;
2950
3101
  },
2951
3102
  getShellSidebarCollapsed: () => shellSidebarCollapsed,
2952
3103
  t: tMain,
2953
3104
  getRuntimeManager: () => manager,
2954
- getWebuiViews: () => webuiViews,
2955
- dispatchWebuiCommand,
2956
- reloadActiveWebuiView,
3105
+ getWebuiViews: () => webuiController.views,
3106
+ dispatchWebuiCommand: (command) => webuiController.dispatch(command),
3107
+ reloadActiveWebuiView: () => webuiController.reload(),
2957
3108
  activateRuntime: async (id) => {
2958
- await activateRuntime(id);
3109
+ await activateRuntime2(id);
2959
3110
  },
2960
3111
  openProject: async () => {
2961
- await openProject();
3112
+ await openProject2();
2962
3113
  },
2963
3114
  registerProject: async () => {
2964
- await registerProject();
3115
+ await registerProject2();
2965
3116
  },
2966
3117
  openSettings: async () => {
2967
- await openSettings();
3118
+ await openSettings2();
2968
3119
  },
2969
3120
  openProjectSession: async (id) => {
2970
- await openProjectSession(id);
3121
+ await openProjectSession2(id);
2971
3122
  },
2972
3123
  closeRuntime: async (id) => {
2973
- await closeRuntime(id);
3124
+ await closeRuntime2(id);
2974
3125
  },
2975
3126
  unregisterProject: async (root) => {
2976
- await unregisterProject(root);
3127
+ await unregisterProject2(root);
2977
3128
  },
2978
- getActiveRuntimeId: () => activeWebuiRuntimeId,
3129
+ getActiveRuntimeId: () => webuiController.activeRuntimeId,
2979
3130
  setShellSidebarCollapsed: (collapsed) => {
2980
3131
  setShellSidebarCollapsed(collapsed);
2981
3132
  },
2982
3133
  restoreLastWorkspace: async () => {
2983
- await restoreLastWorkspace();
3134
+ await restoreLastWorkspace2();
2984
3135
  },
2985
3136
  openExternal: (url) => {
2986
- shell.openExternal(url);
3137
+ safeOpenExternal(url);
2987
3138
  },
2988
3139
  revealInExplorer: (root) => {
2989
3140
  revealInExplorer(root);
@@ -2997,8 +3148,8 @@ function buildIpcHandlerContext() {
2997
3148
  return {
2998
3149
  getMainWindow: () => mainWindow,
2999
3150
  getShellView: () => shellView,
3000
- getWebuiViews: () => webuiViews,
3001
- getWebuiStatus: () => webuiStatus,
3151
+ getWebuiViews: () => webuiController.views,
3152
+ getWebuiStatus: () => webuiController.status,
3002
3153
  getRuntimeManager: () => manager,
3003
3154
  getAgentBridge: () => bridge,
3004
3155
  getI18n: () => ({ getMainLocale, setMainLocale, tMain }),
@@ -3006,37 +3157,40 @@ function buildIpcHandlerContext() {
3006
3157
  getShellSidebarCollapsed: () => shellSidebarCollapsed,
3007
3158
  setShellSidebarCollapsed: (collapsed) => setShellSidebarCollapsed(collapsed),
3008
3159
  broadcastState: () => broadcastState(),
3009
- publishWebuiStatus: (next) => publishWebuiStatus(next),
3010
- syncActiveWebuiView: () => syncActiveWebuiView(),
3160
+ publishWebuiStatus: (next) => {
3161
+ const entry = next.runtimeId ? webuiController.views.get(next.runtimeId) : void 0;
3162
+ if (entry) webuiController.setEntryStatus(entry, next);
3163
+ },
3164
+ syncActiveWebuiView: () => webuiController.syncActive(),
3011
3165
  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(),
3166
+ broadcastLocaleToEmbeddedWebuis: (locale) => webuiController.broadcastLocale(locale),
3167
+ dispatchWebuiCommand: (command) => webuiController.dispatch(command),
3168
+ reloadActiveWebuiView: () => webuiController.reload(),
3169
+ openProject: (root) => openProject2(root),
3170
+ registerProject: (root) => registerProject2(root),
3171
+ unregisterProject: (root) => unregisterProject2(root),
3172
+ openProjectSession: (id) => openProjectSession2(id),
3173
+ activateRuntime: (id) => activateRuntime2(id),
3174
+ closeRuntime: (id) => closeRuntime2(id),
3175
+ openSettings: () => openSettings2(),
3022
3176
  sendMessage: (id, wsUrl, content) => bridge.sendMessage(id, wsUrl, content),
3023
3177
  abortRuntime: (id, wsUrl) => bridge.abort(id, wsUrl),
3024
3178
  openExternal: (url) => safeOpenExternal(url),
3025
3179
  revealInExplorer: (root) => {
3026
3180
  revealInExplorer(root);
3027
3181
  },
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)
3182
+ findWebuiEntryBySenderId: (senderId) => webuiController.findBySenderId(senderId),
3183
+ getPendingWebuiCommandAcks: () => webuiController.pendingAcks,
3184
+ settlePendingWebuiCommandAck: (requestId, handled) => webuiController.settleAck(requestId, handled),
3185
+ setEntryWebuiStatus: (entry, next) => webuiController.setEntryStatus(entry, next),
3186
+ schedulePendingWebuiFlush: (entry) => webuiController.scheduleFlush(entry)
3033
3187
  };
3034
3188
  }
3035
3189
  async function boot() {
3036
3190
  const locale = await readUiLocale();
3037
3191
  if (locale) setMainLocale(locale);
3038
3192
  const shellUrl = rendererIndexPath();
3039
- shellView = new WebContentsView({
3193
+ shellView = new WebContentsView2({
3040
3194
  webPreferences: {
3041
3195
  preload: preloadPath(),
3042
3196
  contextIsolation: true,
@@ -3050,34 +3204,10 @@ async function boot() {
3050
3204
  });
3051
3205
  registerIpcHandlers(buildIpcHandlerContext());
3052
3206
  await shellView.webContents.loadURL(shellUrl);
3053
- const prevState = validatedWindowState(manager.getWindowState());
3207
+ const prevState = windowStateController.validated(manager.getWindowState());
3054
3208
  const defaultWidth = 1180;
3055
3209
  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
- }
3210
+ const appIcon = await loadDesktopAppIcon();
3081
3211
  const winOptions = {
3082
3212
  width: prevState?.width ?? defaultWidth,
3083
3213
  height: prevState?.height ?? defaultHeight,
@@ -3092,10 +3222,10 @@ async function boot() {
3092
3222
  if (prevState.y !== void 0) winOptions.y = prevState.y;
3093
3223
  }
3094
3224
  mainWindow = new BaseWindow(winOptions);
3095
- mainWindow.on("resized", scheduleWindowStateSave);
3096
- mainWindow.on("moved", scheduleWindowStateSave);
3097
- mainWindow.on("maximize", scheduleWindowStateSave);
3098
- mainWindow.on("unmaximize", scheduleWindowStateSave);
3225
+ mainWindow.on("resized", () => windowStateController.scheduleSave());
3226
+ mainWindow.on("moved", () => windowStateController.scheduleSave());
3227
+ mainWindow.on("maximize", () => windowStateController.scheduleSave());
3228
+ mainWindow.on("unmaximize", () => windowStateController.scheduleSave());
3099
3229
  if (prevState?.maximized) {
3100
3230
  mainWindow.maximize();
3101
3231
  }
@@ -3108,7 +3238,7 @@ async function boot() {
3108
3238
  shellView.webContents.send(IPC.conversationChanged, conversation);
3109
3239
  });
3110
3240
  manager.on("changed", () => {
3111
- syncActiveWebuiView();
3241
+ webuiController.syncActive();
3112
3242
  configureApplicationMenu2();
3113
3243
  broadcastState();
3114
3244
  });
@@ -3118,8 +3248,9 @@ async function boot() {
3118
3248
  }
3119
3249
  });
3120
3250
  let lastWatchedLocale;
3251
+ const activeProfileConfigPath = await resolveActiveProfileConfigPath();
3121
3252
  watchProviderConfig(
3122
- desktopConfigPaths.globalConfigPath,
3253
+ activeProfileConfigPath,
3123
3254
  desktopConfigPaths.vault,
3124
3255
  (snapshot) => {
3125
3256
  const updated = snapshot.uiLocale;
@@ -3127,7 +3258,7 @@ async function boot() {
3127
3258
  lastWatchedLocale = updated;
3128
3259
  setMainLocale(updated);
3129
3260
  configureApplicationMenu2();
3130
- broadcastLocaleToEmbeddedWebuis(updated);
3261
+ webuiController.broadcastLocale(updated);
3131
3262
  if (shellView && !shellView.webContents.isDestroyed()) {
3132
3263
  shellView.webContents.send(IPC.localeChanged, updated);
3133
3264
  }
@@ -3137,12 +3268,12 @@ async function boot() {
3137
3268
  if (quittingAfterCleanup) return;
3138
3269
  event.preventDefault();
3139
3270
  bridge.closeAll();
3140
- disposeAllWebuiEntries();
3141
- void saveWindowState();
3271
+ webuiController.disposeAll();
3272
+ void windowStateController.save();
3142
3273
  quittingAfterCleanup = true;
3143
3274
  app2.exit(0);
3144
3275
  });
3145
- await restoreLastWorkspace();
3276
+ await restoreLastWorkspace2();
3146
3277
  const argvOpenPath = firstOpenFileArg(process.argv);
3147
3278
  const queuedOpenPath = drainPendingOpenFilePath();
3148
3279
  const openPath = argvOpenPath ?? queuedOpenPath;
@@ -3161,10 +3292,10 @@ app2.on("window-all-closed", () => {
3161
3292
  app2.on("before-quit", () => {
3162
3293
  if (mainWindow) {
3163
3294
  mainWindow.removeAllListeners("close");
3164
- void saveWindowState();
3295
+ void windowStateController.save();
3165
3296
  }
3166
3297
  bridge.closeAll();
3167
- disposeAllWebuiEntries();
3298
+ webuiController.disposeAll();
3168
3299
  });
3169
3300
  app2.on("activate", () => {
3170
3301
  if (!mainWindow) return;