local-operator-ui 0.25.0 → 0.25.1

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.
Files changed (2) hide show
  1. package/out/main/index.js +183 -18
  2. package/package.json +3 -2
package/out/main/index.js CHANGED
@@ -14216,6 +14216,51 @@ function registerDevDriverIPC(options) {
14216
14216
  };
14217
14217
  });
14218
14218
  }
14219
+ const LAUNCHER_MISSES_BEFORE_GONE = 2;
14220
+ function createLauncherWatch(input) {
14221
+ const threshold = input.missesBeforeGone ?? LAUNCHER_MISSES_BEFORE_GONE;
14222
+ let misses = 0;
14223
+ let fired = false;
14224
+ let stopped = false;
14225
+ return {
14226
+ poll: () => {
14227
+ if (fired || stopped) return false;
14228
+ const parent = input.parentPid();
14229
+ const reparented = parent !== input.launcherPid;
14230
+ misses = reparented || !input.isAlive(input.launcherPid) ? misses + 1 : 0;
14231
+ if (misses < threshold) return false;
14232
+ fired = true;
14233
+ input.onLauncherGone({
14234
+ message: `the process that launched this run (pid ${input.launcherPid}) has exited`,
14235
+ detail: reparented ? `pid ${input.launcherPid} is gone (this process was reparented to pid ${parent})` : `pid ${input.launcherPid} is gone`
14236
+ });
14237
+ return true;
14238
+ },
14239
+ stop: () => {
14240
+ stopped = true;
14241
+ }
14242
+ };
14243
+ }
14244
+ function startLauncherWatch(input) {
14245
+ const watch = createLauncherWatch(input);
14246
+ const timer = setInterval(() => watch.poll(), input.intervalMs);
14247
+ timer.unref?.();
14248
+ return {
14249
+ poll: watch.poll,
14250
+ stop: () => {
14251
+ clearInterval(timer);
14252
+ watch.stop();
14253
+ }
14254
+ };
14255
+ }
14256
+ function isProcessAlive(pid) {
14257
+ try {
14258
+ process.kill(pid, 0);
14259
+ return true;
14260
+ } catch (error) {
14261
+ return error.code !== "ESRCH";
14262
+ }
14263
+ }
14219
14264
  const lastPickedDirectory = /* @__PURE__ */ new Map();
14220
14265
  function withRememberedDirectory(kind, options, fallbackDirectory) {
14221
14266
  if (options.defaultPath) return options;
@@ -17108,15 +17153,35 @@ const AGENT_LAUNCH_FLAGS = [
17108
17153
  "--user-data-dir",
17109
17154
  "--remote-debugging-port"
17110
17155
  ];
17156
+ const LAUNCHER_KEEP_ALIVE_ENV = "LOCAL_OPERATOR_UI_HEADLESS_KEEP_ALIVE";
17157
+ const LAUNCHER_POLL_INTERVAL_MS = 2e3;
17158
+ const LAUNCHER_EXIT_DEADLINE_MS = 1e4;
17111
17159
  const WINDOW_MIN_WIDTH = 800;
17112
17160
  const WINDOW_MIN_HEIGHT = 600;
17113
17161
  const WINDOW_MAX_EDGE = 16384;
17114
17162
  const DEFAULT_WINDOW_WIDTH = 1380;
17115
17163
  const DEFAULT_WINDOW_HEIGHT = 900;
17116
17164
  const WINDOW_BEHAVIOUR = {
17117
- normal: { show: "focus", focusable: true, backgroundThrottling: true },
17118
- inactive: { show: "inactive", focusable: true, backgroundThrottling: false },
17119
- headless: { show: "never", focusable: false, backgroundThrottling: false }
17165
+ normal: {
17166
+ show: "focus",
17167
+ focusable: true,
17168
+ backgroundThrottling: true,
17169
+ hideDock: false
17170
+ },
17171
+ inactive: {
17172
+ show: "inactive",
17173
+ focusable: true,
17174
+ backgroundThrottling: false,
17175
+ // Visible on purpose, so it keeps its tile: a run somebody wants to
17176
+ // watch has to be findable in the Dock.
17177
+ hideDock: false
17178
+ },
17179
+ headless: {
17180
+ show: "never",
17181
+ focusable: false,
17182
+ backgroundThrottling: false,
17183
+ hideDock: true
17184
+ }
17120
17185
  };
17121
17186
  function readFlag(argv, name) {
17122
17187
  for (let index = 0; index < argv.length; index += 1) {
@@ -17206,10 +17271,45 @@ function resolveWindowLaunchPlan(input = {}) {
17206
17271
  problems
17207
17272
  };
17208
17273
  }
17209
- function describeWindowLaunch(plan) {
17274
+ const KEEP_ALIVE_VALUES = ["1", "true", "yes", "on"];
17275
+ function resolveLauncherWatchPlan(input) {
17276
+ const env = input.env ?? {};
17277
+ const keepAlive = (env[LAUNCHER_KEEP_ALIVE_ENV] ?? "").trim().toLowerCase();
17278
+ if (KEEP_ALIVE_VALUES.includes(keepAlive)) {
17279
+ return {
17280
+ watch: false,
17281
+ launcherPid: null,
17282
+ reason: `${LAUNCHER_KEEP_ALIVE_ENV}=${keepAlive} was set, so this run outlives its launcher`
17283
+ };
17284
+ }
17285
+ if (input.mode !== "headless") {
17286
+ return {
17287
+ watch: false,
17288
+ launcherPid: null,
17289
+ reason: `window mode ${input.mode} is not launcher-bound: a person can close it`
17290
+ };
17291
+ }
17292
+ if (!Number.isInteger(input.launcherPid) || input.launcherPid <= 1) {
17293
+ return {
17294
+ watch: false,
17295
+ launcherPid: null,
17296
+ reason: "headless run already detached (no launcher to outlive)"
17297
+ };
17298
+ }
17299
+ return {
17300
+ watch: true,
17301
+ launcherPid: input.launcherPid,
17302
+ reason: `headless run launched by pid ${input.launcherPid}; it quits when that process goes`
17303
+ };
17304
+ }
17305
+ function describeWindowLaunch(plan, platform = process.platform) {
17210
17306
  const behaviour = plan.show === "never" ? "window created and never shown" : plan.show === "inactive" ? "window shown without activating the app" : "window shown and focused";
17211
- const assumption = plan.assumed ? ` (assumed: ${plan.assumed})` : "";
17212
- return `window mode ${plan.mode}${assumption}: ${plan.width}x${plan.height}, ${behaviour}, page throttling ${plan.backgroundThrottling ? "on" : "off"}`;
17307
+ const assumption = plan.assumed ? ` (mode assumed: ${plan.assumed})` : "";
17308
+ const extras = [
17309
+ plan.hideDock && platform === "darwin" ? "no Dock tile" : null
17310
+ ].filter((part) => part !== null);
17311
+ const suffix = extras.length === 0 ? "" : `, ${extras.join(", ")}`;
17312
+ return `window mode ${plan.mode}: ${plan.width}x${plan.height}, ${behaviour}, page throttling ${plan.backgroundThrottling ? "on" : "off"}${suffix}${assumption}`;
17213
17313
  }
17214
17314
  const BASE64_FILE_EXTENSIONS = ["csv", "tsv", "xls", "xlsx", "ods"];
17215
17315
  const resolveUserPath = (filePath, cwd) => {
@@ -17220,10 +17320,50 @@ const resolveUserPath = (filePath, cwd) => {
17220
17320
  return path$1.join(cwd.startsWith("~/") ? resolveUserPath(cwd) : cwd, filePath);
17221
17321
  return filePath;
17222
17322
  };
17323
+ const windowLaunch = resolveWindowLaunchPlan({
17324
+ env: launchEnv,
17325
+ argv: process.argv
17326
+ });
17327
+ for (const problem of windowLaunch.problems) {
17328
+ logger.warn(`[window-mode] ${problem}`, LogFileType.BACKEND);
17329
+ console.log(`[window-mode] ${problem}`);
17330
+ }
17331
+ if (windowLaunch.mode !== "normal") {
17332
+ console.log(`[window-mode] ${describeWindowLaunch(windowLaunch)}`);
17333
+ }
17334
+ let headlessExitDeadline = null;
17335
+ function armHeadlessExitDeadline(why) {
17336
+ if (headlessExitDeadline) return;
17337
+ const deadline2 = setTimeout(() => {
17338
+ const line = `${why}; the quit did not finish within ${LAUNCHER_EXIT_DEADLINE_MS} ms, exiting`;
17339
+ logger.error(`[window-mode] ${line}`, LogFileType.BACKEND);
17340
+ console.log(`[window-mode] ${line}`);
17341
+ electron.app.exit(0);
17342
+ }, LAUNCHER_EXIT_DEADLINE_MS);
17343
+ deadline2.unref();
17344
+ headlessExitDeadline = deadline2;
17345
+ }
17346
+ function endHeadlessRun(why, detail) {
17347
+ logger.info(`[window-mode] ${why}; quitting`, LogFileType.BACKEND);
17348
+ console.log(`[window-mode] ${why}; quitting`);
17349
+ if (detail) {
17350
+ logger.debug(
17351
+ `[window-mode] launcher probe: ${detail}`,
17352
+ LogFileType.BACKEND
17353
+ );
17354
+ }
17355
+ armHeadlessExitDeadline(why);
17356
+ electron.app.quit();
17357
+ }
17358
+ let launcherWatch = null;
17223
17359
  electron.app.setName("Local Operator");
17224
17360
  const image = electron.nativeImage.createFromPath(icon);
17225
17361
  if (process.platform === "darwin" && electron.app.dock) {
17226
- electron.app.dock.setIcon(image);
17362
+ if (windowLaunch.hideDock) {
17363
+ electron.app.dock.hide();
17364
+ } else {
17365
+ electron.app.dock.setIcon(image);
17366
+ }
17227
17367
  }
17228
17368
  const posthogClient = new posthogNode.PostHog(backendConfig.VITE_PUBLIC_POSTHOG_KEY, {
17229
17369
  host: backendConfig.VITE_PUBLIC_POSTHOG_HOST,
@@ -17545,17 +17685,6 @@ const reportBackendFailure = (message, fileType) => {
17545
17685
  }
17546
17686
  electron.dialog.showErrorBox("Backend Error", message);
17547
17687
  };
17548
- const windowLaunch = resolveWindowLaunchPlan({
17549
- env: launchEnv,
17550
- argv: process.argv
17551
- });
17552
- for (const problem of windowLaunch.problems) {
17553
- logger.warn(`[window-mode] ${problem}`, LogFileType.BACKEND);
17554
- console.log(`[window-mode] ${problem}`);
17555
- }
17556
- if (windowLaunch.mode !== "normal") {
17557
- console.log(`[window-mode] ${describeWindowLaunch(windowLaunch)}`);
17558
- }
17559
17688
  const devDriverArming = resolveDevDriverArming({
17560
17689
  env: launchEnv,
17561
17690
  windowMode: windowLaunch.mode
@@ -17656,6 +17785,29 @@ electron.app.whenReady().then(async () => {
17656
17785
  electron.app.exit(0);
17657
17786
  return;
17658
17787
  }
17788
+ const launcher = resolveLauncherWatchPlan({
17789
+ mode: windowLaunch.mode,
17790
+ launcherPid: process.ppid,
17791
+ env: launchEnv
17792
+ });
17793
+ logger.info(`[window-mode] ${launcher.reason}`, LogFileType.BACKEND);
17794
+ if (windowLaunch.mode !== "normal") {
17795
+ console.log(`[window-mode] ${launcher.reason}`);
17796
+ }
17797
+ if (launcher.watch && launcher.launcherPid !== null) {
17798
+ launcherWatch = startLauncherWatch({
17799
+ launcherPid: launcher.launcherPid,
17800
+ intervalMs: LAUNCHER_POLL_INTERVAL_MS,
17801
+ isAlive: isProcessAlive,
17802
+ parentPid: () => process.ppid,
17803
+ onLauncherGone: ({ message, detail }) => endHeadlessRun(message, detail)
17804
+ });
17805
+ }
17806
+ if (windowLaunch.mode === "headless") {
17807
+ for (const signal of ["SIGTERM", "SIGINT"]) {
17808
+ process.on(signal, () => endHeadlessRun(`received ${signal}`));
17809
+ }
17810
+ }
17659
17811
  electron.app.on("browser-window-created", (_, window) => {
17660
17812
  utils.optimizer.watchWindowShortcuts(window);
17661
17813
  });
@@ -18255,6 +18407,15 @@ electron.app.whenReady().then(async () => {
18255
18407
  electron.app.quit();
18256
18408
  });
18257
18409
  electron.app.on("window-all-closed", () => {
18410
+ if (windowLaunch.mode === "headless") {
18411
+ logger.info(
18412
+ "[window-mode] all windows closed in a headless run; quitting",
18413
+ LogFileType.BACKEND
18414
+ );
18415
+ console.log("[window-mode] all windows closed in a headless run; quitting");
18416
+ electron.app.quit();
18417
+ return;
18418
+ }
18258
18419
  if (process.platform === "darwin") {
18259
18420
  if (activeUpdateService?.quitForInFlightInstall("last window closed")) {
18260
18421
  electron.app.quit();
@@ -18319,6 +18480,10 @@ const holdQuitForSessionCookieSnapshot = createSessionCookieQuitHold({
18319
18480
  electron.app.on("before-quit", async (event) => {
18320
18481
  if (await holdQuitForSessionCookieSnapshot(event)) return;
18321
18482
  logger.info("App is about to quit", LogFileType.BACKEND);
18483
+ if (windowLaunch.mode === "headless") {
18484
+ launcherWatch?.stop();
18485
+ armHeadlessExitDeadline("the app is quitting");
18486
+ }
18322
18487
  electron.globalShortcut.unregisterAll();
18323
18488
  activeUpdateService?.quitForInFlightInstall("app quit");
18324
18489
  void stopBrowserHost();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-operator-ui",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "productName": "Local Operator",
5
5
  "description": "User interface for AI agent assistants that run Python code safely on your device through an intuitive chat interface. Supports local and cloud LLM models with built-in security features.",
6
6
  "main": "./out/main/index.js",
@@ -71,7 +71,8 @@
71
71
  "check-types:renderer": "tsc --noEmit -p tsconfig.app.json",
72
72
  "check-types": "pnpm run check-types:main && pnpm run check-types:renderer",
73
73
  "test:session-cookies": "node scripts/session-cookie-electron.test.mjs",
74
- "test:desktop": "node scripts/run-desktop-tests.mjs scripts/slash-token.test.mjs scripts/slash-rank.test.mjs scripts/slash-submit.test.mjs scripts/slash-contract.test.mjs scripts/slash-row-format.test.mjs scripts/owned-serve-lifecycle.test.mjs scripts/linux-sandbox.test.mjs scripts/capture-evidence.test.mjs scripts/desktop-contract.test.mjs scripts/transcript-reducer.test.mjs scripts/transcript-paging.test.mjs scripts/provider-state.test.mjs scripts/stale-agent.test.mjs scripts/desktop-renderer-transport.test.mjs scripts/desktop-proxy.test.mjs scripts/session-stream-token.test.mjs scripts/session-load-recovery.test.mjs scripts/provider-grid-error-copy.test.mjs scripts/settings-time-to-error.test.mjs scripts/backend-error-surfaces.test.mjs scripts/canonical-chat.test.mjs scripts/stale-caption.test.mjs scripts/desktop-watch-lease.test.mjs scripts/session-switch.test.mjs scripts/chat-search.test.mjs scripts/palette-search.test.mjs scripts/palette-shortcut.test.mjs scripts/palette-contract.test.mjs scripts/palette-panel-request.test.mjs scripts/run-detail-model.test.mjs scripts/subagent-pulse.test.mjs scripts/chat-title.test.mjs scripts/clear-search.test.mjs scripts/tool-row.test.mjs scripts/attachment-url.test.mjs scripts/desktop-notifier.test.mjs scripts/launch-session.test.mjs scripts/desktop-feed.test.mjs scripts/viewer-record.test.mjs scripts/desktop-stream.test.mjs scripts/python-bytecode-cache.test.mjs scripts/update-affirmation.test.mjs scripts/update-robustness.test.mjs scripts/prune-python-resource.test.mjs scripts/managed-python.test.mjs scripts/usage-view.test.mjs scripts/usage-container.test.mjs scripts/session-status.test.mjs scripts/composer-readings.test.mjs scripts/composer-tabs.test.mjs scripts/ask-options.test.mjs scripts/window-mode.test.mjs scripts/evidence-manifest.test.mjs scripts/canonical-notice.test.mjs scripts/evidence-run-guard.test.mjs scripts/desktop-test-concurrency.test.mjs scripts/run-desktop-tests.test.mjs scripts/mcp-foreign-config-origin.test.mjs scripts/mentioned-files.test.mjs scripts/picker-feedback.test.mjs scripts/panel-chart-motion.test.mjs scripts/panel-info-credentials.test.mjs scripts/warm-session.test.mjs scripts/submit-latency.test.mjs scripts/completion-view-ack.test.mjs scripts/echo-delivery.test.mjs scripts/check-runtime-deps.test.mjs scripts/new-chat-row.test.mjs scripts/new-chat-row-evidence.test.mjs scripts/reconnect-page-gap.test.mjs scripts/browser-host.test.mjs scripts/check-vendored.test.mjs scripts/chat-session-status.test.mjs scripts/draft-selection.test.mjs scripts/toast-lifetime.test.mjs scripts/storybook-query-fixture.test.mjs scripts/mcp-list-query.test.mjs scripts/mcp-target-resolution.test.mjs scripts/mcp-auth-surface.test.mjs scripts/mcp-failure-routing.test.mjs scripts/daemon-discovery.test.mjs scripts/daemon-health-state.test.mjs scripts/daemon-observation.test.mjs scripts/session-cookies.test.mjs scripts/move-session.test.mjs scripts/frontend-replace.test.mjs scripts/browser-chrome.test.mjs scripts/dev-driver-gate.test.mjs scripts/logger-log-dir.test.mjs scripts/draft-splash.test.mjs scripts/suggestion-stack.test.mjs scripts/suggestion-stack-react.test.mjs scripts/notifications-off.test.mjs scripts/notification-launch.test.mjs scripts/notification-spawn-sites.test.mjs scripts/interrupt-control.test.mjs",
74
+
75
+ "test:desktop": "node scripts/run-desktop-tests.mjs scripts/slash-token.test.mjs scripts/slash-rank.test.mjs scripts/slash-submit.test.mjs scripts/slash-contract.test.mjs scripts/slash-row-format.test.mjs scripts/owned-serve-lifecycle.test.mjs scripts/launcher-watch.test.mjs scripts/linux-sandbox.test.mjs scripts/capture-evidence.test.mjs scripts/desktop-contract.test.mjs scripts/transcript-reducer.test.mjs scripts/transcript-paging.test.mjs scripts/provider-state.test.mjs scripts/stale-agent.test.mjs scripts/desktop-renderer-transport.test.mjs scripts/desktop-proxy.test.mjs scripts/session-stream-token.test.mjs scripts/session-load-recovery.test.mjs scripts/provider-grid-error-copy.test.mjs scripts/settings-time-to-error.test.mjs scripts/backend-error-surfaces.test.mjs scripts/canonical-chat.test.mjs scripts/stale-caption.test.mjs scripts/desktop-watch-lease.test.mjs scripts/session-switch.test.mjs scripts/chat-search.test.mjs scripts/palette-search.test.mjs scripts/palette-shortcut.test.mjs scripts/palette-contract.test.mjs scripts/palette-panel-request.test.mjs scripts/run-detail-model.test.mjs scripts/subagent-pulse.test.mjs scripts/chat-title.test.mjs scripts/clear-search.test.mjs scripts/tool-row.test.mjs scripts/attachment-url.test.mjs scripts/desktop-notifier.test.mjs scripts/launch-session.test.mjs scripts/desktop-feed.test.mjs scripts/viewer-record.test.mjs scripts/desktop-stream.test.mjs scripts/python-bytecode-cache.test.mjs scripts/update-affirmation.test.mjs scripts/update-robustness.test.mjs scripts/prune-python-resource.test.mjs scripts/managed-python.test.mjs scripts/usage-view.test.mjs scripts/usage-container.test.mjs scripts/session-status.test.mjs scripts/composer-readings.test.mjs scripts/composer-tabs.test.mjs scripts/ask-options.test.mjs scripts/window-mode.test.mjs scripts/evidence-manifest.test.mjs scripts/canonical-notice.test.mjs scripts/evidence-run-guard.test.mjs scripts/desktop-test-concurrency.test.mjs scripts/run-desktop-tests.test.mjs scripts/mcp-foreign-config-origin.test.mjs scripts/mentioned-files.test.mjs scripts/picker-feedback.test.mjs scripts/panel-chart-motion.test.mjs scripts/panel-info-credentials.test.mjs scripts/warm-session.test.mjs scripts/submit-latency.test.mjs scripts/completion-view-ack.test.mjs scripts/echo-delivery.test.mjs scripts/check-runtime-deps.test.mjs scripts/new-chat-row.test.mjs scripts/new-chat-row-evidence.test.mjs scripts/reconnect-page-gap.test.mjs scripts/browser-host.test.mjs scripts/check-vendored.test.mjs scripts/chat-session-status.test.mjs scripts/draft-selection.test.mjs scripts/toast-lifetime.test.mjs scripts/storybook-query-fixture.test.mjs scripts/mcp-list-query.test.mjs scripts/mcp-target-resolution.test.mjs scripts/mcp-auth-surface.test.mjs scripts/mcp-failure-routing.test.mjs scripts/daemon-discovery.test.mjs scripts/daemon-health-state.test.mjs scripts/daemon-observation.test.mjs scripts/session-cookies.test.mjs scripts/move-session.test.mjs scripts/frontend-replace.test.mjs scripts/browser-chrome.test.mjs scripts/dev-driver-gate.test.mjs scripts/logger-log-dir.test.mjs scripts/draft-splash.test.mjs scripts/suggestion-stack.test.mjs scripts/suggestion-stack-react.test.mjs scripts/notifications-off.test.mjs scripts/notification-launch.test.mjs scripts/notification-spawn-sites.test.mjs scripts/chrome-keychain.test.mjs scripts/interrupt-control.test.mjs",
75
76
  "check-themes": "node scripts/generate-theme-css.mjs --check && node scripts/contrast-contract.mjs",
76
77
  "check-vendored": "node scripts/check-vendored.mjs",
77
78
  "check-evidence": "node scripts/check-evidence.mjs",