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.
- package/out/main/index.js +183 -18
- 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: {
|
|
17118
|
-
|
|
17119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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",
|