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