@wrongstack/desktop 0.286.0 → 0.289.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main/main.js CHANGED
@@ -1,17 +1,64 @@
1
1
  // src/main/main.ts
2
- import * as path3 from "node:path";
2
+ import * as path4 from "node:path";
3
3
  import * as fs3 from "node:fs/promises";
4
4
  import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
5
5
  import {
6
- app,
6
+ app as app2,
7
7
  BaseWindow,
8
8
  dialog,
9
- ipcMain,
9
+ ipcMain as ipcMain2,
10
+ nativeImage,
10
11
  screen,
11
12
  shell,
12
13
  WebContentsView
13
14
  } from "electron";
14
15
 
16
+ // src/main/macos-platform.ts
17
+ import { app, Menu } from "electron";
18
+ function initMacOS() {
19
+ if (process.platform !== "darwin") return;
20
+ app.setActivationPolicy("regular");
21
+ app.on("open-file", (_event, filePath) => {
22
+ if (app.isReady()) {
23
+ handleFileOpen(filePath);
24
+ }
25
+ });
26
+ app.dock?.setMenu(
27
+ Menu.buildFromTemplate([
28
+ {
29
+ label: "New Window",
30
+ click: () => {
31
+ }
32
+ }
33
+ ])
34
+ );
35
+ app.setAboutPanelOptions({
36
+ applicationName: "WrongStack Desktop",
37
+ applicationVersion: app.getVersion(),
38
+ version: process.versions.electron ? `Electron ${process.versions.electron}` : ""
39
+ });
40
+ }
41
+ function handleFileOpen(filePath) {
42
+ pendingOpenFilePath = filePath;
43
+ }
44
+ var pendingOpenFilePath = null;
45
+ function drainPendingOpenFilePath() {
46
+ const path5 = pendingOpenFilePath;
47
+ pendingOpenFilePath = null;
48
+ return path5;
49
+ }
50
+ function firstOpenFileArg(argv) {
51
+ if (process.platform !== "darwin") return null;
52
+ for (let index = 1; index < argv.length; index++) {
53
+ const arg = argv[index];
54
+ if (arg == null) continue;
55
+ if (arg.startsWith("-")) continue;
56
+ if (arg === "." || arg === "--") continue;
57
+ return arg;
58
+ }
59
+ return null;
60
+ }
61
+
15
62
  // src/main/agent-bridge.ts
16
63
  import { randomUUID } from "node:crypto";
17
64
  import { EventEmitter } from "node:events";
@@ -397,7 +444,11 @@ var IPC = {
397
444
  // Embedded WebUI view side — the desktop shell pushes locale changes here
398
445
  // so the React WebUI inside Electron can swap i18n instantly, without waiting
399
446
  // for the config-file watcher → WS prefs.updated round-trip.
400
- webuiLocaleChanged: "desktop:webui-locale-changed"
447
+ webuiLocaleChanged: "desktop:webui-locale-changed",
448
+ // macOS open-file event forwarded from main process to shell renderer.
449
+ // The shell uses this to decide whether to open a dragged/double-clicked
450
+ // path as a project directory.
451
+ openFile: "desktop:open-file"
401
452
  };
402
453
 
403
454
  // src/main/i18n-main.ts
@@ -874,8 +925,8 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
874
925
  }
875
926
  async openProject(projectRoot, options = {}) {
876
927
  const resolved = path2.resolve(projectRoot);
877
- const stat2 = await fs2.stat(resolved).catch(() => null);
878
- if (!stat2?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
928
+ const stat3 = await fs2.stat(resolved).catch(() => null);
929
+ if (!stat3?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
879
930
  const kind = options.kind ?? "project";
880
931
  const touchRecent = options.touchRecent ?? kind === "project";
881
932
  const forceNew = options.forceNew === true;
@@ -1049,8 +1100,8 @@ var DesktopRuntimeManager = class extends EventEmitter2 {
1049
1100
  }
1050
1101
  async registerProject(projectRoot) {
1051
1102
  const resolved = path2.resolve(projectRoot);
1052
- const stat2 = await fs2.stat(resolved).catch(() => null);
1053
- if (!stat2?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
1103
+ const stat3 = await fs2.stat(resolved).catch(() => null);
1104
+ if (!stat3?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
1054
1105
  const now = (/* @__PURE__ */ new Date()).toISOString();
1055
1106
  const entry = {
1056
1107
  name: path2.basename(resolved) || resolved,
@@ -1186,6 +1237,26 @@ async function terminateProcessTree(child) {
1186
1237
  if (!child || child.killed || !child.pid) return;
1187
1238
  if (process.platform !== "win32") {
1188
1239
  child.kill("SIGTERM");
1240
+ const deadline = Date.now() + 5e3;
1241
+ await new Promise((resolve2) => {
1242
+ const check = () => {
1243
+ if (child.killed || !child.pid) {
1244
+ resolve2();
1245
+ return;
1246
+ }
1247
+ if (Date.now() >= deadline) {
1248
+ try {
1249
+ process.kill(-child.pid, "SIGKILL");
1250
+ } catch {
1251
+ child.kill("SIGKILL");
1252
+ }
1253
+ resolve2();
1254
+ return;
1255
+ }
1256
+ setTimeout(check, 100);
1257
+ };
1258
+ check();
1259
+ });
1189
1260
  return;
1190
1261
  }
1191
1262
  await new Promise((resolve2) => {
@@ -1389,16 +1460,16 @@ function resolveWebUiEntry() {
1389
1460
  if (existsSync(candidate)) return candidate;
1390
1461
  } catch {
1391
1462
  }
1392
- const serverIndex = require2.resolve("@wrongstack/webui/server");
1393
- return path2.join(path2.dirname(serverIndex), "entry.js");
1463
+ const serverIndex = require2.resolve("@wrongstack/webui-server");
1464
+ return path2.join(path2.dirname(serverIndex), "server", "entry.js");
1394
1465
  }
1395
1466
  function resolveWebUiDistDir() {
1396
1467
  if (process.env["WRONGSTACK_WEBUI_DIST"]) {
1397
1468
  return path2.resolve(process.env["WRONGSTACK_WEBUI_DIST"]);
1398
1469
  }
1399
1470
  const require2 = createRequire(import.meta.url);
1400
- const serverIndex = require2.resolve("@wrongstack/webui/server");
1401
- const candidate = path2.resolve(path2.dirname(serverIndex), "..");
1471
+ const serverEntry = require2.resolve("@wrongstack/webui");
1472
+ const candidate = path2.dirname(serverEntry);
1402
1473
  if (existsSync(candidate)) return candidate;
1403
1474
  throw new Error(
1404
1475
  `WebUI frontend assets not found at ${candidate}. Build @wrongstack/webui or set WRONGSTACK_WEBUI_DIST.`
@@ -1525,7 +1596,7 @@ var DESKTOP_WEBUI_ACTIONS = /* @__PURE__ */ new Set([
1525
1596
  var DESKTOP_WEBUI_VIEWS = /* @__PURE__ */ new Set([
1526
1597
  "chat",
1527
1598
  "settings",
1528
- "autophase",
1599
+ "goal",
1529
1600
  "specs",
1530
1601
  "sddboard",
1531
1602
  "sddwizard",
@@ -1560,7 +1631,6 @@ var DESKTOP_WEBUI_OVERLAYS = /* @__PURE__ */ new Set([
1560
1631
  "queue"
1561
1632
  ]);
1562
1633
  var DESKTOP_WEBUI_DOCKS = /* @__PURE__ */ new Set([
1563
- "autophase",
1564
1634
  "goal",
1565
1635
  "fleet",
1566
1636
  "work",
@@ -1663,10 +1733,17 @@ function isRecord(value) {
1663
1733
  }
1664
1734
 
1665
1735
  // src/main/state/constants.ts
1736
+ var OPEN_EXTERNAL_ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
1666
1737
  var SIDEBAR_WIDTH_WIDE = 292;
1667
1738
  var SIDEBAR_WIDTH_MEDIUM = 276;
1668
1739
  var SIDEBAR_WIDTH_NARROW = 252;
1669
1740
  var SIDEBAR_WIDTH_COLLAPSED = 56;
1741
+ var MIN_WINDOW_WIDTH2 = 760;
1742
+ var MIN_WINDOW_HEIGHT2 = 520;
1743
+ var MAX_PENDING_WEBUI_COMMANDS = 50;
1744
+ var MAX_PENDING_FLUSH_ATTEMPTS = 80;
1745
+ var WEBUI_COMMAND_FALLBACK_MS = 350;
1746
+ var WEBUI_COMMAND_ACK_TIMEOUT_MS = 2e3;
1670
1747
 
1671
1748
  // src/main/layout/sidebar.ts
1672
1749
  function getSidebarWidth(windowWidth, collapsed) {
@@ -1677,10 +1754,10 @@ function getSidebarWidth(windowWidth, collapsed) {
1677
1754
  }
1678
1755
 
1679
1756
  // src/main/menu/index.ts
1680
- import { Menu } from "electron";
1757
+ import { Menu as Menu2 } from "electron";
1681
1758
 
1682
1759
  // src/main/menu/projects-menu.ts
1683
- var posix = (await import("node:path")).posix;
1760
+ import path3 from "node:path";
1684
1761
  function buildProjectsMenu(runtimes, actions, t) {
1685
1762
  const projectGroups = groupProjectRuntimesForMenu(runtimes);
1686
1763
  const menu = [
@@ -1688,9 +1765,11 @@ function buildProjectsMenu(runtimes, actions, t) {
1688
1765
  label: t("openProjectEllipsis"),
1689
1766
  accelerator: "CmdOrCtrl+O",
1690
1767
  click: () => actions.newSession("")
1691
- // Will open dialog
1692
1768
  },
1693
- { label: t("registerProjectEllipsis"), click: () => actions.newSession("") },
1769
+ {
1770
+ label: t("registerProjectEllipsis"),
1771
+ click: () => actions.registerProject?.()
1772
+ },
1694
1773
  { type: "separator" }
1695
1774
  ];
1696
1775
  if (projectGroups.length === 0) {
@@ -1812,7 +1891,7 @@ function groupProjectRuntimesForMenu(runtimes) {
1812
1891
  }
1813
1892
  groups.set(key, {
1814
1893
  key,
1815
- name: posix.basename(runtime.root) || runtime.name,
1894
+ name: path3.basename(runtime.root) || runtime.name,
1816
1895
  root: runtime.root,
1817
1896
  sessions: [runtime]
1818
1897
  });
@@ -1983,6 +2062,7 @@ function configureApplicationMenu(ctx) {
1983
2062
  const actions = {
1984
2063
  activate: (runtimeId) => void ctx.activateRuntime(runtimeId),
1985
2064
  activateAndNavigate,
2065
+ registerProject: () => void ctx.registerProject(),
1986
2066
  newSession: (runtimeId) => {
1987
2067
  if (runtimeId) void ctx.openProjectSession(runtimeId);
1988
2068
  else void ctx.openProject();
@@ -2007,19 +2087,301 @@ function configureApplicationMenu(ctx) {
2007
2087
  buildWorkspaceMenu(ctx, actions, hasActiveWebui, activeWebuiPrefs, navigate),
2008
2088
  buildViewMenu(ctx)
2009
2089
  ];
2010
- Menu.setApplicationMenu(Menu.buildFromTemplate(template));
2090
+ Menu2.setApplicationMenu(Menu2.buildFromTemplate(template));
2091
+ }
2092
+
2093
+ // src/main/ipc-handlers/index.ts
2094
+ import { ipcMain } from "electron";
2095
+
2096
+ // src/main/validation/index.ts
2097
+ function validate(schema, data) {
2098
+ try {
2099
+ const result = schema.safeParse(data);
2100
+ if (result.success) {
2101
+ return { success: true, data: result.data };
2102
+ }
2103
+ return { success: false, error: formatZodError(result.error) };
2104
+ } catch (err) {
2105
+ return { success: false, error: String(err) };
2106
+ }
2107
+ }
2108
+ function validateOrDefault(schema, data, defaultValue) {
2109
+ const result = validate(schema, data);
2110
+ return result.success ? result.data : defaultValue;
2111
+ }
2112
+ function formatZodError(error) {
2113
+ return error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
2114
+ }
2115
+ function createValidationLogger(prefix) {
2116
+ const loggedErrors = /* @__PURE__ */ new Set();
2117
+ const maxErrors = 100;
2118
+ return {
2119
+ log(error) {
2120
+ if (loggedErrors.size >= maxErrors) return;
2121
+ const key = `${prefix}:${error}`;
2122
+ if (!loggedErrors.has(key)) {
2123
+ loggedErrors.add(key);
2124
+ console.warn(
2125
+ JSON.stringify({
2126
+ level: "warn",
2127
+ event: "desktop.validation_error",
2128
+ prefix,
2129
+ message: error,
2130
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2131
+ })
2132
+ );
2133
+ }
2134
+ },
2135
+ reset() {
2136
+ loggedErrors.clear();
2137
+ }
2138
+ };
2139
+ }
2140
+
2141
+ // src/main/validation/schemas.ts
2142
+ import { z } from "zod";
2143
+ var RUNTIME_ID_PATTERN = /^[a-zA-Z0-9._:-]{3,120}$/;
2144
+ var runtimeIdSchema = z.string().regex(RUNTIME_ID_PATTERN, "Invalid runtime ID format");
2145
+ var pathSchema = z.string().min(1).max(1e4);
2146
+ var nonEmptyStringSchema = z.string().min(1);
2147
+ var booleanSchema = z.boolean();
2148
+ var numberSchema = z.number().int().finite();
2149
+ var openProjectSchema = z.object({
2150
+ requestedRoot: pathSchema.optional()
2151
+ });
2152
+ var registerProjectSchema = z.object({
2153
+ requestedRoot: pathSchema.optional()
2154
+ });
2155
+ var unregisterProjectSchema = z.object({
2156
+ root: pathSchema
2157
+ });
2158
+ var openProjectSessionSchema = z.object({
2159
+ runtimeId: runtimeIdSchema.optional()
2160
+ });
2161
+ var activateRuntimeSchema = z.object({
2162
+ id: runtimeIdSchema
2163
+ });
2164
+ var closeRuntimeSchema = z.object({
2165
+ id: runtimeIdSchema
2166
+ });
2167
+ var sendMessageSchema = z.object({
2168
+ id: runtimeIdSchema,
2169
+ content: z.string()
2170
+ });
2171
+ var abortRuntimeSchema = z.object({
2172
+ id: runtimeIdSchema
2173
+ });
2174
+ var openRuntimeInBrowserSchema = z.object({
2175
+ id: runtimeIdSchema
2176
+ });
2177
+ var revealRuntimeRootSchema = z.object({
2178
+ id: runtimeIdSchema
2179
+ });
2180
+ var setShellSidebarCollapsedSchema = z.object({
2181
+ collapsed: booleanSchema
2182
+ });
2183
+ var navigateWebuiSchema = z.object({
2184
+ command: z.unknown()
2185
+ });
2186
+ var getConversationSchema = z.object({
2187
+ runtimeId: runtimeIdSchema
2188
+ });
2189
+ var webuiReadyChangedSchema = z.object({
2190
+ ready: booleanSchema
2191
+ });
2192
+ var webuiPrefsChangedSchema = z.object({
2193
+ prefs: z.record(z.string(), z.unknown())
2194
+ });
2195
+ var webuiCommandAckSchema = z.object({
2196
+ requestId: z.string(),
2197
+ handled: z.boolean(),
2198
+ message: z.string().optional()
2199
+ });
2200
+ var setLocaleSchema = z.object({
2201
+ locale: z.string().min(2).max(10)
2202
+ });
2203
+
2204
+ // src/main/ipc-handlers/index.ts
2205
+ var validationLogger = createValidationLogger("IPC");
2206
+ function registerIpcHandlers(ctx) {
2207
+ ipcMain.handle(IPC.getState, () => ctx.getRuntimeManager().snapshot());
2208
+ ipcMain.handle(IPC.getConversation, (_event, runtimeId) => {
2209
+ const result = validate(runtimeIdSchema, runtimeId);
2210
+ if (!result.success) {
2211
+ validationLogger.log(`getConversation: ${result.error}`);
2212
+ return ctx.getAgentBridge().snapshot("");
2213
+ }
2214
+ return ctx.getAgentBridge().snapshot(result.data);
2215
+ });
2216
+ ipcMain.handle(IPC.getWebuiStatus, () => ctx.getWebuiStatus());
2217
+ ipcMain.handle(IPC.navigateWebui, async (_event, command) => {
2218
+ return ctx.dispatchWebuiCommand(command);
2219
+ });
2220
+ ipcMain.handle(IPC.reloadWebui, async () => ctx.reloadActiveWebuiView());
2221
+ ipcMain.handle(IPC.setShellSidebarCollapsed, (_event, collapsed) => {
2222
+ const result = validateOrDefault(
2223
+ pathSchema.transform(() => collapsed === true),
2224
+ collapsed,
2225
+ true
2226
+ );
2227
+ ctx.setShellSidebarCollapsed(result);
2228
+ return true;
2229
+ });
2230
+ ipcMain.handle(IPC.openSettings, async () => ctx.openSettings());
2231
+ ipcMain.handle(IPC.openProjectSession, async (_event, runtimeId) => {
2232
+ const validated = validateOptional(runtimeIdSchema, runtimeId);
2233
+ return ctx.openProjectSession(validated);
2234
+ });
2235
+ ipcMain.handle(IPC.openProject, async (_event, requestedRoot) => {
2236
+ const validated = validateOptional(pathSchema, requestedRoot);
2237
+ return ctx.openProject(validated);
2238
+ });
2239
+ ipcMain.handle(IPC.registerProject, async (_event, requestedRoot) => {
2240
+ const validated = validateOptional(pathSchema, requestedRoot);
2241
+ return ctx.registerProject(validated);
2242
+ });
2243
+ ipcMain.handle(IPC.unregisterProject, async (_event, root) => {
2244
+ const result = validate(pathSchema, root);
2245
+ if (!result.success) {
2246
+ validationLogger.log(`unregisterProject: ${result.error}`);
2247
+ return ctx.getRuntimeManager().snapshot();
2248
+ }
2249
+ return ctx.unregisterProject(result.data);
2250
+ });
2251
+ ipcMain.handle(IPC.activateRuntime, async (_event, id) => {
2252
+ const result = validate(runtimeIdSchema, id);
2253
+ if (!result.success) {
2254
+ validationLogger.log(`activateRuntime: ${result.error}`);
2255
+ return ctx.getRuntimeManager().snapshot();
2256
+ }
2257
+ return ctx.activateRuntime(result.data);
2258
+ });
2259
+ ipcMain.handle(IPC.closeRuntime, async (_event, id) => {
2260
+ const result = validate(runtimeIdSchema, id);
2261
+ if (!result.success) {
2262
+ validationLogger.log(`closeRuntime: ${result.error}`);
2263
+ return ctx.getRuntimeManager().snapshot();
2264
+ }
2265
+ return ctx.closeRuntime(result.data);
2266
+ });
2267
+ ipcMain.handle(IPC.sendMessage, async (_event, id, content) => {
2268
+ const idResult = validate(runtimeIdSchema, id);
2269
+ if (!idResult.success) {
2270
+ validationLogger.log(`sendMessage (id): ${idResult.error}`);
2271
+ return ctx.sendMessage("", "", "");
2272
+ }
2273
+ const contentResult = validate(pathSchema.transform(() => String(content ?? "")), content);
2274
+ const safeContent = contentResult.success ? contentResult.data : String(content ?? "");
2275
+ return ctx.sendMessage(
2276
+ idResult.data,
2277
+ ctx.getRuntimeManager().getRuntimeWsUrlWithToken(idResult.data) ?? "",
2278
+ safeContent
2279
+ );
2280
+ });
2281
+ ipcMain.handle(IPC.abortRuntime, async (_event, id) => {
2282
+ const result = validate(runtimeIdSchema, id);
2283
+ if (!result.success) {
2284
+ validationLogger.log(`abortRuntime: ${result.error}`);
2285
+ return ctx.abortRuntime("", "");
2286
+ }
2287
+ return ctx.abortRuntime(
2288
+ result.data,
2289
+ ctx.getRuntimeManager().getRuntimeWsUrlWithToken(result.data) ?? ""
2290
+ );
2291
+ });
2292
+ ipcMain.handle(IPC.openRuntimeInBrowser, async (_event, id) => {
2293
+ const result = validate(runtimeIdSchema, id);
2294
+ if (!result.success) {
2295
+ validationLogger.log(`openRuntimeInBrowser: ${result.error}`);
2296
+ return;
2297
+ }
2298
+ const url = ctx.getRuntimeManager().getRuntimeUrlWithToken(result.data);
2299
+ if (url) ctx.openExternal(url);
2300
+ });
2301
+ ipcMain.handle(IPC.revealRuntimeRoot, async (_event, id) => {
2302
+ const result = validate(runtimeIdSchema, id);
2303
+ if (!result.success) {
2304
+ validationLogger.log(`revealRuntimeRoot: ${result.error}`);
2305
+ return;
2306
+ }
2307
+ const runtime = ctx.getRuntimeManager().getRuntime(result.data);
2308
+ if (runtime) ctx.revealInExplorer(runtime.root);
2309
+ });
2310
+ ipcMain.on(IPC.webuiReadyChanged, (event, ready) => {
2311
+ const entry = ctx.findWebuiEntryBySenderId(event.sender.id);
2312
+ if (!entry) return;
2313
+ const isReady = ready === true;
2314
+ entry.bridgeReady = isReady;
2315
+ if (entry.bridgeReady) {
2316
+ ctx.setEntryWebuiStatus(entry, { ...entry.status, status: "ready" });
2317
+ ctx.schedulePendingWebuiFlush(entry);
2318
+ } else if (entry.status.status === "ready") {
2319
+ ctx.setEntryWebuiStatus(entry, { ...entry.status, status: "loading" });
2320
+ }
2321
+ });
2322
+ ipcMain.on(IPC.webuiPrefsChanged, (event, prefs) => {
2323
+ const entry = ctx.findWebuiEntryBySenderId(event.sender.id);
2324
+ if (!entry) return;
2325
+ const sanitized = sanitizeWebuiPrefs(prefs);
2326
+ if (Object.keys(sanitized).length === 0) return;
2327
+ ctx.setEntryWebuiStatus(entry, {
2328
+ ...entry.status,
2329
+ prefs: { ...entry.status.prefs ?? {}, ...sanitized }
2330
+ });
2331
+ });
2332
+ ipcMain.on(IPC.webuiCommandAck, (event, requestId, handled, _message) => {
2333
+ const entry = ctx.findWebuiEntryBySenderId(event.sender.id);
2334
+ if (!entry) return;
2335
+ const result = validate(webuiCommandAckSchema, {
2336
+ requestId,
2337
+ handled,
2338
+ message: _message
2339
+ });
2340
+ if (!result.success) {
2341
+ validationLogger.log(`webuiCommandAck: ${result.error}`);
2342
+ return;
2343
+ }
2344
+ const pending = ctx.getPendingWebuiCommandAcks().get(result.data.requestId);
2345
+ if (!pending || pending.runtimeId !== entry.runtimeId) return;
2346
+ ctx.settlePendingWebuiCommandAck(result.data.requestId, result.data.handled);
2347
+ });
2348
+ ipcMain.on(IPC.setLocale, (_event, locale) => {
2349
+ const result = validate(setLocaleSchema, { locale });
2350
+ if (!result.success) {
2351
+ validationLogger.log(`setLocale: ${result.error}`);
2352
+ return;
2353
+ }
2354
+ ctx.getI18n().setMainLocale(result.data.locale);
2355
+ ctx.configureApplicationMenu();
2356
+ ctx.broadcastLocaleToEmbeddedWebuis(result.data.locale);
2357
+ void ctx.getConfigIo().writeUiLocale(result.data.locale);
2358
+ });
2359
+ }
2360
+ function validateOptional(schema, value) {
2361
+ if (value === void 0 || value === null) return void 0;
2362
+ const result = schema.safeParse(value);
2363
+ return result.success ? result.data : void 0;
2364
+ }
2365
+ function sanitizeWebuiPrefs(prefs) {
2366
+ const next = {};
2367
+ if (!isRecord2(prefs)) return next;
2368
+ if (typeof prefs["yolo"] === "boolean") next.yolo = prefs["yolo"];
2369
+ if (typeof prefs["nextPrediction"] === "boolean") next.nextPrediction = prefs["nextPrediction"];
2370
+ if (typeof prefs["contextAutoCompact"] === "boolean") {
2371
+ next.contextAutoCompact = prefs["contextAutoCompact"];
2372
+ }
2373
+ return next;
2374
+ }
2375
+ function isRecord2(value) {
2376
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2011
2377
  }
2012
2378
 
2013
2379
  // src/main/main.ts
2014
- var OPEN_EXTERNAL_ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
2015
- var MIN_WINDOW_WIDTH2 = 760;
2016
- var MIN_WINDOW_HEIGHT2 = 520;
2017
- var MAX_PENDING_WEBUI_COMMANDS = 50;
2018
- var MAX_PENDING_FLUSH_ATTEMPTS = 80;
2019
- var WEBUI_COMMAND_FALLBACK_MS = 350;
2020
- var WEBUI_COMMAND_ACK_TIMEOUT_MS = 2e3;
2021
- app.setAppUserModelId("com.wrongstack.desktop");
2022
- app.setPath("userData", path3.join(wstackGlobalRoot3(), "desktop", "electron-profile"));
2380
+ initMacOS();
2381
+ if (process.platform === "win32") {
2382
+ app2.setAppUserModelId("com.wrongstack.desktop");
2383
+ }
2384
+ app2.setPath("userData", path4.join(wstackGlobalRoot3(), "desktop", "electron-profile"));
2023
2385
  var manager = new DesktopRuntimeManager();
2024
2386
  var bridge = new DesktopAgentBridge();
2025
2387
  var mainWindow = null;
@@ -2051,13 +2413,21 @@ function sameOrigin(candidate, base) {
2051
2413
  return false;
2052
2414
  }
2053
2415
  }
2054
- function isRecord2(value) {
2055
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2056
- }
2057
- function runtimeWsUrlOrThrow(runtimeId) {
2058
- const wsUrl = manager.getRuntimeWsUrlWithToken(runtimeId);
2059
- if (!wsUrl) throw new Error(`Runtime not found: ${runtimeId}`);
2060
- return wsUrl;
2416
+ function revealInExplorer(root) {
2417
+ shell.openPath(root).catch((err) => {
2418
+ if (process.platform === "darwin") {
2419
+ void shell.openPath(path4.dirname(root)).catch(() => void 0);
2420
+ }
2421
+ console.error(
2422
+ JSON.stringify({
2423
+ level: "warn",
2424
+ event: "desktop.reveal_in_explorer_failed",
2425
+ root,
2426
+ message: err instanceof Error ? err.message : String(err),
2427
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2428
+ })
2429
+ );
2430
+ });
2061
2431
  }
2062
2432
  function setShellSidebarCollapsed(collapsed) {
2063
2433
  shellSidebarCollapsed = collapsed;
@@ -2603,105 +2973,51 @@ function createMenuContext() {
2603
2973
  shell.openExternal(url);
2604
2974
  },
2605
2975
  revealInExplorer: (root) => {
2606
- void shell.openPath(root);
2976
+ revealInExplorer(root);
2607
2977
  }
2608
2978
  };
2609
2979
  }
2610
2980
  function configureApplicationMenu2() {
2611
2981
  configureApplicationMenu(createMenuContext());
2612
2982
  }
2613
- function registerIpcHandlers() {
2614
- ipcMain.handle(IPC.getState, () => manager.snapshot());
2615
- ipcMain.handle(
2616
- IPC.getConversation,
2617
- (_event, runtimeId) => bridge.snapshot(runtimeId)
2618
- );
2619
- ipcMain.handle(IPC.getWebuiStatus, () => webuiStatus);
2620
- ipcMain.handle(
2621
- IPC.navigateWebui,
2622
- async (_event, command) => dispatchWebuiCommand(command)
2623
- );
2624
- ipcMain.handle(IPC.reloadWebui, async () => reloadActiveWebuiView());
2625
- ipcMain.handle(IPC.setShellSidebarCollapsed, (_event, collapsed) => {
2626
- setShellSidebarCollapsed(collapsed === true);
2627
- return true;
2628
- });
2629
- ipcMain.handle(IPC.openSettings, async () => openSettings());
2630
- ipcMain.handle(
2631
- IPC.openProjectSession,
2632
- async (_event, runtimeId) => openProjectSession(runtimeId)
2633
- );
2634
- ipcMain.handle(
2635
- IPC.openProject,
2636
- async (_event, requestedRoot) => openProject(requestedRoot)
2637
- );
2638
- ipcMain.handle(
2639
- IPC.registerProject,
2640
- async (_event, requestedRoot) => registerProject(requestedRoot)
2641
- );
2642
- ipcMain.handle(IPC.unregisterProject, async (_event, root) => unregisterProject(root));
2643
- ipcMain.handle(IPC.activateRuntime, async (_event, id) => activateRuntime(id));
2644
- ipcMain.handle(IPC.closeRuntime, async (_event, id) => closeRuntime(id));
2645
- ipcMain.handle(
2646
- IPC.sendMessage,
2647
- async (_event, id, content) => bridge.sendMessage(id, runtimeWsUrlOrThrow(id), content)
2648
- );
2649
- ipcMain.handle(
2650
- IPC.abortRuntime,
2651
- async (_event, id) => bridge.abort(id, runtimeWsUrlOrThrow(id))
2652
- );
2653
- ipcMain.handle(IPC.openRuntimeInBrowser, async (_event, id) => {
2654
- const url = manager.getRuntimeUrlWithToken(id);
2655
- if (url) safeOpenExternal(url);
2656
- });
2657
- ipcMain.handle(IPC.revealRuntimeRoot, async (_event, id) => {
2658
- const runtime = manager.getRuntime(id);
2659
- if (runtime) void shell.openPath(runtime.root);
2660
- });
2661
- ipcMain.on(IPC.webuiReadyChanged, (event, ready) => {
2662
- const entry = findWebuiEntryBySenderId(event.sender.id);
2663
- if (!entry) return;
2664
- entry.bridgeReady = ready === true;
2665
- if (entry.bridgeReady) {
2666
- setEntryWebuiStatus(entry, { ...entry.status, status: "ready" });
2667
- schedulePendingWebuiFlush(entry);
2668
- } else if (entry.status.status === "ready") {
2669
- setEntryWebuiStatus(entry, { ...entry.status, status: "loading" });
2670
- }
2671
- });
2672
- ipcMain.on(IPC.webuiPrefsChanged, (event, prefs) => {
2673
- const entry = findWebuiEntryBySenderId(event.sender.id);
2674
- if (!entry) return;
2675
- const next = {};
2676
- if (isRecord2(prefs) && typeof prefs["yolo"] === "boolean") next.yolo = prefs["yolo"];
2677
- if (isRecord2(prefs) && typeof prefs["nextPrediction"] === "boolean") {
2678
- next.nextPrediction = prefs["nextPrediction"];
2679
- }
2680
- if (isRecord2(prefs) && typeof prefs["contextAutoCompact"] === "boolean") {
2681
- next.contextAutoCompact = prefs["contextAutoCompact"];
2682
- }
2683
- if (Object.keys(next).length === 0) return;
2684
- setEntryWebuiStatus(entry, {
2685
- ...entry.status,
2686
- prefs: { ...entry.status.prefs ?? {}, ...next }
2687
- });
2688
- });
2689
- ipcMain.on(
2690
- IPC.webuiCommandAck,
2691
- (event, requestId, handled, _message) => {
2692
- const entry = findWebuiEntryBySenderId(event.sender.id);
2693
- if (!entry || typeof requestId !== "string") return;
2694
- const pending = pendingWebuiCommandAcks.get(requestId);
2695
- if (!pending || pending.runtimeId !== entry.runtimeId) return;
2696
- settlePendingWebuiCommandAck(requestId, handled === true);
2697
- }
2698
- );
2699
- ipcMain.on(IPC.setLocale, (_event, locale) => {
2700
- setMainLocale(locale);
2701
- configureApplicationMenu2();
2702
- broadcastLocaleToEmbeddedWebuis(locale);
2703
- void writeUiLocale(locale);
2704
- });
2983
+ function buildIpcHandlerContext() {
2984
+ return {
2985
+ getMainWindow: () => mainWindow,
2986
+ getShellView: () => shellView,
2987
+ getWebuiViews: () => webuiViews,
2988
+ getWebuiStatus: () => webuiStatus,
2989
+ getRuntimeManager: () => manager,
2990
+ getAgentBridge: () => bridge,
2991
+ getI18n: () => ({ getMainLocale, setMainLocale, tMain }),
2992
+ getConfigIo: () => ({ desktopConfigPaths, readUiLocale, writeUiLocale }),
2993
+ getShellSidebarCollapsed: () => shellSidebarCollapsed,
2994
+ setShellSidebarCollapsed: (collapsed) => setShellSidebarCollapsed(collapsed),
2995
+ broadcastState: () => broadcastState(),
2996
+ publishWebuiStatus: (next) => publishWebuiStatus(next),
2997
+ syncActiveWebuiView: () => syncActiveWebuiView(),
2998
+ configureApplicationMenu: () => configureApplicationMenu2(),
2999
+ broadcastLocaleToEmbeddedWebuis: (locale) => broadcastLocaleToEmbeddedWebuis(locale),
3000
+ dispatchWebuiCommand: (command) => dispatchWebuiCommand(command),
3001
+ reloadActiveWebuiView: () => reloadActiveWebuiView(),
3002
+ openProject: (root) => openProject(root),
3003
+ registerProject: (root) => registerProject(root),
3004
+ unregisterProject: (root) => unregisterProject(root),
3005
+ openProjectSession: (id) => openProjectSession(id),
3006
+ activateRuntime: (id) => activateRuntime(id),
3007
+ closeRuntime: (id) => closeRuntime(id),
3008
+ openSettings: () => openSettings(),
3009
+ sendMessage: (id, wsUrl, content) => bridge.sendMessage(id, wsUrl, content),
3010
+ abortRuntime: (id, wsUrl) => bridge.abort(id, wsUrl),
3011
+ openExternal: (url) => safeOpenExternal(url),
3012
+ revealInExplorer: (root) => {
3013
+ revealInExplorer(root);
3014
+ },
3015
+ findWebuiEntryBySenderId: (senderId) => findWebuiEntryBySenderId(senderId),
3016
+ getPendingWebuiCommandAcks: () => pendingWebuiCommandAcks,
3017
+ settlePendingWebuiCommandAck: (requestId, handled) => settlePendingWebuiCommandAck(requestId, handled),
3018
+ setEntryWebuiStatus: (entry, next) => setEntryWebuiStatus(entry, next),
3019
+ schedulePendingWebuiFlush: (entry) => schedulePendingWebuiFlush(entry)
3020
+ };
2705
3021
  }
2706
3022
  async function boot() {
2707
3023
  const locale = await readUiLocale();
@@ -2719,18 +3035,44 @@ async function boot() {
2719
3035
  safeOpenExternal(url);
2720
3036
  return { action: "deny" };
2721
3037
  });
2722
- registerIpcHandlers();
3038
+ registerIpcHandlers(buildIpcHandlerContext());
2723
3039
  await shellView.webContents.loadURL(shellUrl);
2724
3040
  const prevState = validatedWindowState(manager.getWindowState());
2725
3041
  const defaultWidth = 1180;
2726
3042
  const defaultHeight = 720;
3043
+ let appIcon;
3044
+ if (process.platform !== "darwin") {
3045
+ try {
3046
+ const iconPath = new URL("../../assets/icon.svg", import.meta.url).pathname;
3047
+ await fs3.stat(iconPath);
3048
+ appIcon = nativeImage.createFromPath(iconPath);
3049
+ if (appIcon.isEmpty()) appIcon = void 0;
3050
+ } catch {
3051
+ }
3052
+ } else {
3053
+ try {
3054
+ const pngPath = new URL("../../assets/icon.png", import.meta.url).pathname;
3055
+ await fs3.stat(pngPath);
3056
+ appIcon = nativeImage.createFromPath(pngPath);
3057
+ if (appIcon.isEmpty()) appIcon = void 0;
3058
+ } catch {
3059
+ try {
3060
+ const icnsPath = new URL("../../assets/icon.icns", import.meta.url).pathname;
3061
+ await fs3.stat(icnsPath);
3062
+ appIcon = nativeImage.createFromPath(icnsPath);
3063
+ if (appIcon.isEmpty()) appIcon = void 0;
3064
+ } catch {
3065
+ }
3066
+ }
3067
+ }
2727
3068
  const winOptions = {
2728
3069
  width: prevState?.width ?? defaultWidth,
2729
3070
  height: prevState?.height ?? defaultHeight,
2730
3071
  show: false,
2731
3072
  minWidth: MIN_WINDOW_WIDTH2,
2732
3073
  minHeight: MIN_WINDOW_HEIGHT2,
2733
- title: tMain("windowTitle")
3074
+ title: tMain("windowTitle"),
3075
+ ...appIcon ? { icon: appIcon } : {}
2734
3076
  };
2735
3077
  if (prevState) {
2736
3078
  if (prevState.x !== void 0) winOptions.x = prevState.x;
@@ -2757,6 +3099,11 @@ async function boot() {
2757
3099
  configureApplicationMenu2();
2758
3100
  broadcastState();
2759
3101
  });
3102
+ app2.on("open-file", (_event, filePath) => {
3103
+ if (shellView && !shellView.webContents.isDestroyed()) {
3104
+ shellView.webContents.send(IPC.openFile, filePath);
3105
+ }
3106
+ });
2760
3107
  let lastWatchedLocale;
2761
3108
  watchProviderConfig(
2762
3109
  desktopConfigPaths.globalConfigPath,
@@ -2780,17 +3127,25 @@ async function boot() {
2780
3127
  disposeAllWebuiEntries();
2781
3128
  void saveWindowState();
2782
3129
  quittingAfterCleanup = true;
2783
- app.exit(0);
3130
+ app2.exit(0);
2784
3131
  });
2785
3132
  await restoreLastWorkspace();
3133
+ const argvOpenPath = firstOpenFileArg(process.argv);
3134
+ const queuedOpenPath = drainPendingOpenFilePath();
3135
+ const openPath = argvOpenPath ?? queuedOpenPath;
3136
+ if (openPath && shellView && !shellView.webContents.isDestroyed()) {
3137
+ shellView.webContents.send(IPC.openFile, openPath);
3138
+ }
2786
3139
  mainWindow.show();
2787
3140
  shellView.webContents.focus();
2788
3141
  }
2789
- app.whenReady().then(boot);
2790
- app.on("window-all-closed", () => {
2791
- app.quit();
3142
+ app2.whenReady().then(boot);
3143
+ app2.on("window-all-closed", () => {
3144
+ if (process.platform !== "darwin") {
3145
+ app2.quit();
3146
+ }
2792
3147
  });
2793
- app.on("before-quit", () => {
3148
+ app2.on("before-quit", () => {
2794
3149
  if (mainWindow) {
2795
3150
  mainWindow.removeAllListeners("close");
2796
3151
  void saveWindowState();
@@ -2798,7 +3153,7 @@ app.on("before-quit", () => {
2798
3153
  bridge.closeAll();
2799
3154
  disposeAllWebuiEntries();
2800
3155
  });
2801
- app.on("activate", () => {
3156
+ app2.on("activate", () => {
2802
3157
  if (!mainWindow) return;
2803
3158
  mainWindow.show();
2804
3159
  shellView?.webContents.focus();