@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
|
|
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.
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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 = {
|
|
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(
|
|
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
|
|
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 {
|
|
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(
|
|
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
|
-
...
|
|
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(
|
|
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/
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
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(
|
|
2398
|
-
|
|
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
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
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
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
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
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
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
|
|
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
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
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
|
-
|
|
3079
|
+
return result.filePaths[0];
|
|
2864
3080
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
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
|
|
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: () =>
|
|
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
|
|
3109
|
+
await activateRuntime2(id);
|
|
2959
3110
|
},
|
|
2960
3111
|
openProject: async () => {
|
|
2961
|
-
await
|
|
3112
|
+
await openProject2();
|
|
2962
3113
|
},
|
|
2963
3114
|
registerProject: async () => {
|
|
2964
|
-
await
|
|
3115
|
+
await registerProject2();
|
|
2965
3116
|
},
|
|
2966
3117
|
openSettings: async () => {
|
|
2967
|
-
await
|
|
3118
|
+
await openSettings2();
|
|
2968
3119
|
},
|
|
2969
3120
|
openProjectSession: async (id) => {
|
|
2970
|
-
await
|
|
3121
|
+
await openProjectSession2(id);
|
|
2971
3122
|
},
|
|
2972
3123
|
closeRuntime: async (id) => {
|
|
2973
|
-
await
|
|
3124
|
+
await closeRuntime2(id);
|
|
2974
3125
|
},
|
|
2975
3126
|
unregisterProject: async (root) => {
|
|
2976
|
-
await
|
|
3127
|
+
await unregisterProject2(root);
|
|
2977
3128
|
},
|
|
2978
|
-
getActiveRuntimeId: () =>
|
|
3129
|
+
getActiveRuntimeId: () => webuiController.activeRuntimeId,
|
|
2979
3130
|
setShellSidebarCollapsed: (collapsed) => {
|
|
2980
3131
|
setShellSidebarCollapsed(collapsed);
|
|
2981
3132
|
},
|
|
2982
3133
|
restoreLastWorkspace: async () => {
|
|
2983
|
-
await
|
|
3134
|
+
await restoreLastWorkspace2();
|
|
2984
3135
|
},
|
|
2985
3136
|
openExternal: (url) => {
|
|
2986
|
-
|
|
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: () =>
|
|
3001
|
-
getWebuiStatus: () =>
|
|
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) =>
|
|
3010
|
-
|
|
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) =>
|
|
3013
|
-
dispatchWebuiCommand: (command) =>
|
|
3014
|
-
reloadActiveWebuiView: () =>
|
|
3015
|
-
openProject: (root) =>
|
|
3016
|
-
registerProject: (root) =>
|
|
3017
|
-
unregisterProject: (root) =>
|
|
3018
|
-
openProjectSession: (id) =>
|
|
3019
|
-
activateRuntime: (id) =>
|
|
3020
|
-
closeRuntime: (id) =>
|
|
3021
|
-
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) =>
|
|
3029
|
-
getPendingWebuiCommandAcks: () =>
|
|
3030
|
-
settlePendingWebuiCommandAck: (requestId, handled) =>
|
|
3031
|
-
setEntryWebuiStatus: (entry, next) =>
|
|
3032
|
-
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
|
|
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 =
|
|
3207
|
+
const prevState = windowStateController.validated(manager.getWindowState());
|
|
3054
3208
|
const defaultWidth = 1180;
|
|
3055
3209
|
const defaultHeight = 720;
|
|
3056
|
-
|
|
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",
|
|
3096
|
-
mainWindow.on("moved",
|
|
3097
|
-
mainWindow.on("maximize",
|
|
3098
|
-
mainWindow.on("unmaximize",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3141
|
-
void
|
|
3271
|
+
webuiController.disposeAll();
|
|
3272
|
+
void windowStateController.save();
|
|
3142
3273
|
quittingAfterCleanup = true;
|
|
3143
3274
|
app2.exit(0);
|
|
3144
3275
|
});
|
|
3145
|
-
await
|
|
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
|
|
3295
|
+
void windowStateController.save();
|
|
3165
3296
|
}
|
|
3166
3297
|
bridge.closeAll();
|
|
3167
|
-
|
|
3298
|
+
webuiController.disposeAll();
|
|
3168
3299
|
});
|
|
3169
3300
|
app2.on("activate", () => {
|
|
3170
3301
|
if (!mainWindow) return;
|