local-operator-ui 0.34.30 → 0.34.31
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 +213 -114
- package/package.json +1 -1
package/out/main/index.js
CHANGED
|
@@ -15725,10 +15725,10 @@ class ConsoleRegistry {
|
|
|
15725
15725
|
count() {
|
|
15726
15726
|
return this.entries.size;
|
|
15727
15727
|
}
|
|
15728
|
-
/** How many were created by that session's agent. */
|
|
15728
|
+
/** How many live surfaces were created by that session's agent. */
|
|
15729
15729
|
agentCount(sessionId2) {
|
|
15730
15730
|
return this.list(sessionId2).filter(
|
|
15731
|
-
(entry) => entry.record.origin === "agent"
|
|
15731
|
+
(entry) => entry.record.origin === "agent" && entry.record.live
|
|
15732
15732
|
).length;
|
|
15733
15733
|
}
|
|
15734
15734
|
/**
|
|
@@ -15747,7 +15747,7 @@ class ConsoleRegistry {
|
|
|
15747
15747
|
{ limit: MAX_SURFACES_PER_APP, scope: "app" }
|
|
15748
15748
|
);
|
|
15749
15749
|
}
|
|
15750
|
-
if (input.origin === "agent" && this.agentCount(input.sessionId) >= MAX_AGENT_SURFACES_PER_SESSION) {
|
|
15750
|
+
if (input.live !== false && input.origin === "agent" && this.agentCount(input.sessionId) >= MAX_AGENT_SURFACES_PER_SESSION) {
|
|
15751
15751
|
throw new ConsoleError(
|
|
15752
15752
|
"tab_limit",
|
|
15753
15753
|
`this session's agent is already running ${MAX_AGENT_SURFACES_PER_SESSION} console surfaces; close one first`,
|
|
@@ -15766,10 +15766,10 @@ class ConsoleRegistry {
|
|
|
15766
15766
|
cwd: input.cwd,
|
|
15767
15767
|
cols: input.cols,
|
|
15768
15768
|
rows: input.rows,
|
|
15769
|
-
running:
|
|
15769
|
+
running: input.live !== false,
|
|
15770
15770
|
exitCode: null,
|
|
15771
15771
|
lastActivity: this.now(),
|
|
15772
|
-
live:
|
|
15772
|
+
live: input.live !== false,
|
|
15773
15773
|
retain: input.retain,
|
|
15774
15774
|
secure: false,
|
|
15775
15775
|
lastActor: null,
|
|
@@ -15897,6 +15897,10 @@ const SURFACE_COLORTERM = "truecolor";
|
|
|
15897
15897
|
const STRIPPED_ENV_PREFIXES = ["CMUX_", "LOP_"];
|
|
15898
15898
|
class ConsoleHost {
|
|
15899
15899
|
registry;
|
|
15900
|
+
rollbackDisposals = [];
|
|
15901
|
+
// Acquisition precedes runtime construction/registry adoption. Keep the exact
|
|
15902
|
+
// emulator until adoption succeeds; startup rollback must also own that gap.
|
|
15903
|
+
unboundRestoreEmulators = /* @__PURE__ */ new Set();
|
|
15900
15904
|
options;
|
|
15901
15905
|
now;
|
|
15902
15906
|
env;
|
|
@@ -16058,6 +16062,7 @@ class ConsoleHost {
|
|
|
16058
16062
|
// The handle the history recorded, so a caller holding one across a relaunch
|
|
16059
16063
|
// finds the same surface rather than a lookalike with a new nonce.
|
|
16060
16064
|
surface: meta.surface,
|
|
16065
|
+
live: false,
|
|
16061
16066
|
sessionId: meta.session_id,
|
|
16062
16067
|
origin: meta.origin,
|
|
16063
16068
|
command: meta.command,
|
|
@@ -16081,7 +16086,7 @@ class ConsoleHost {
|
|
|
16081
16086
|
rows: grid.rows,
|
|
16082
16087
|
scrollback: SCROLLBACK_LINES
|
|
16083
16088
|
});
|
|
16084
|
-
|
|
16089
|
+
this.unboundRestoreEmulators.add(emulator);
|
|
16085
16090
|
const runtime = {
|
|
16086
16091
|
// A reconstructed surface has no pty. `null` rather than a fake process:
|
|
16087
16092
|
// every path that would signal one checks `running` first, and the record
|
|
@@ -16103,8 +16108,10 @@ class ConsoleHost {
|
|
|
16103
16108
|
exitDelivered: true,
|
|
16104
16109
|
exitWaiters: []
|
|
16105
16110
|
};
|
|
16106
|
-
runtime.log.append(bytes);
|
|
16107
16111
|
this.registry.attach(entry.record.surface, runtime);
|
|
16112
|
+
this.unboundRestoreEmulators.delete(emulator);
|
|
16113
|
+
emulator.write(bytes);
|
|
16114
|
+
runtime.log.append(bytes);
|
|
16108
16115
|
this.registry.update(entry.record.surface, {
|
|
16109
16116
|
running: false,
|
|
16110
16117
|
exitCode: meta.exit_code,
|
|
@@ -16818,17 +16825,63 @@ class ConsoleHost {
|
|
|
16818
16825
|
this.options.onChanged?.();
|
|
16819
16826
|
}
|
|
16820
16827
|
/** Stop everything: every pty, every record, every subscriber. */
|
|
16821
|
-
dispose() {
|
|
16822
|
-
|
|
16823
|
-
const
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
|
|
16828
|
-
|
|
16829
|
-
|
|
16830
|
-
|
|
16831
|
-
|
|
16828
|
+
dispose(persistHistory = true) {
|
|
16829
|
+
if (!persistHistory || this.rollbackDisposals.length > 0 || this.unboundRestoreEmulators.size > 0) {
|
|
16830
|
+
const entries = [...this.rollbackDisposals, ...this.registry.clear()];
|
|
16831
|
+
this.rollbackDisposals = [];
|
|
16832
|
+
const failures = [];
|
|
16833
|
+
for (const entry of entries) {
|
|
16834
|
+
if (entry.runtime)
|
|
16835
|
+
this.unboundRestoreEmulators.delete(entry.runtime.emulator);
|
|
16836
|
+
}
|
|
16837
|
+
for (const entry of entries) {
|
|
16838
|
+
const runtime = entry.runtime;
|
|
16839
|
+
if (!runtime) continue;
|
|
16840
|
+
let incomplete = false;
|
|
16841
|
+
for (const release of [
|
|
16842
|
+
() => {
|
|
16843
|
+
if (runtime.pendingTimer) clearTimeout(runtime.pendingTimer);
|
|
16844
|
+
if (runtime.persistTimer) clearTimeout(runtime.persistTimer);
|
|
16845
|
+
runtime.subscribers.clear();
|
|
16846
|
+
},
|
|
16847
|
+
() => {
|
|
16848
|
+
if (entry.record.running) runtime.pty?.kill();
|
|
16849
|
+
},
|
|
16850
|
+
() => runtime.emulator.dispose()
|
|
16851
|
+
]) {
|
|
16852
|
+
try {
|
|
16853
|
+
release();
|
|
16854
|
+
} catch (error) {
|
|
16855
|
+
incomplete = true;
|
|
16856
|
+
failures.push(error);
|
|
16857
|
+
}
|
|
16858
|
+
}
|
|
16859
|
+
if (incomplete) this.rollbackDisposals.push(entry);
|
|
16860
|
+
}
|
|
16861
|
+
for (const emulator of this.unboundRestoreEmulators) {
|
|
16862
|
+
try {
|
|
16863
|
+
emulator.dispose();
|
|
16864
|
+
this.unboundRestoreEmulators.delete(emulator);
|
|
16865
|
+
} catch (error) {
|
|
16866
|
+
failures.push(error);
|
|
16867
|
+
}
|
|
16868
|
+
}
|
|
16869
|
+
if (failures.length)
|
|
16870
|
+
throw new AggregateError(
|
|
16871
|
+
failures,
|
|
16872
|
+
"Partial console runtime cleanup incomplete."
|
|
16873
|
+
);
|
|
16874
|
+
} else
|
|
16875
|
+
for (const entry of this.registry.clear()) {
|
|
16876
|
+
const runtime = entry.runtime;
|
|
16877
|
+
if (!runtime) continue;
|
|
16878
|
+
if (runtime.pendingTimer) clearTimeout(runtime.pendingTimer);
|
|
16879
|
+
if (runtime.persistTimer) clearTimeout(runtime.persistTimer);
|
|
16880
|
+
runtime.subscribers.clear();
|
|
16881
|
+
if (persistHistory && entry.record.retain) this.persist(entry, runtime);
|
|
16882
|
+
if (entry.record.running) runtime.pty?.kill();
|
|
16883
|
+
runtime.emulator.dispose();
|
|
16884
|
+
}
|
|
16832
16885
|
this.createdAt.clear();
|
|
16833
16886
|
this.persistedBytes.clear();
|
|
16834
16887
|
this.metaSignatures.clear();
|
|
@@ -17864,112 +17917,158 @@ async function startConsoleHost(options) {
|
|
|
17864
17917
|
`[console] restored the exec bit on ${load.helper.path ?? "spawn-helper"}`
|
|
17865
17918
|
);
|
|
17866
17919
|
}
|
|
17867
|
-
const configDir = options.configDir ?? localOperatorConfigDir();
|
|
17868
|
-
const history = new ConsoleHistory(
|
|
17869
|
-
path$1.join(configDir, "run", "ui-console", "history")
|
|
17870
|
-
);
|
|
17871
|
-
const registry = new ConsoleRegistry();
|
|
17872
17920
|
let hostRef = null;
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
window: () => options.window,
|
|
17887
|
-
log: log2,
|
|
17888
|
-
history,
|
|
17889
|
-
now: options.now,
|
|
17890
|
-
// Resolved BEFORE the host exists, so no surface can be created against the
|
|
17891
|
-
// launch environment and then start behaving like the user's terminal later:
|
|
17892
|
-
// a surface's PATH is decided here, once, and every surface this host makes
|
|
17893
|
-
// carries the same one. Bounded by the resolver's own timeout (it never
|
|
17894
|
-
// rejects and answers null when the shell cannot speak), and no part of the
|
|
17895
|
-
// app's own startup waits on it: the browser host is attached to the window
|
|
17896
|
-
// without being awaited, so the cost of a slow rc here is this host coming
|
|
17897
|
-
// online later, not the app failing to come up.
|
|
17898
|
-
env: await withUserShellPath(process.env, options.userShellPath),
|
|
17899
|
-
captureOffscreen: capture ? (request) => capture.capture(request) : void 0,
|
|
17900
|
-
spawn: options.spawn ?? ((spawnOptions) => spawnPty(load.module, spawnOptions, (report) => {
|
|
17901
|
-
log2(
|
|
17902
|
-
`[console] restored the exec bit on ${report.path ?? "spawn-helper"}`
|
|
17903
|
-
);
|
|
17904
|
-
})),
|
|
17905
|
-
/*
|
|
17906
|
-
* ONE PUSHER, and it is A's. `broadcastConsoleState` was already the app's
|
|
17907
|
-
* single "the projection moved" signal when this branch arrived; the
|
|
17908
|
-
* completion seam below therefore does NOT push a second copy of the same
|
|
17909
|
-
* frame. It only raises a banner, which is the half a push cannot express —
|
|
17910
|
-
* and the host calls `onChanged` for a mark and an exit too, so the renderer
|
|
17911
|
-
* still refetches on both (see `ConsoleHostOptions.onExit`).
|
|
17912
|
-
*/
|
|
17913
|
-
onChanged: () => {
|
|
17914
|
-
broadcastConsoleState(options.window);
|
|
17915
|
-
},
|
|
17916
|
-
onMark: completion.onMark,
|
|
17917
|
-
onExit: completion.onExit,
|
|
17918
|
-
onReveal: (request) => {
|
|
17919
|
-
const window = options.window;
|
|
17920
|
-
if (window.isDestroyed()) return;
|
|
17921
|
-
window.webContents.send("console-reveal", {
|
|
17922
|
-
surface: request.surface,
|
|
17923
|
-
session_id: request.sessionId,
|
|
17924
|
-
mode: request.mode
|
|
17925
|
-
});
|
|
17921
|
+
let capture = null;
|
|
17922
|
+
let host = null;
|
|
17923
|
+
let rollbackOwners = [];
|
|
17924
|
+
const cleanupStartupOwners = () => {
|
|
17925
|
+
const pending = [];
|
|
17926
|
+
const failures = [];
|
|
17927
|
+
for (const release of rollbackOwners.splice(0).reverse()) {
|
|
17928
|
+
try {
|
|
17929
|
+
release();
|
|
17930
|
+
} catch (error) {
|
|
17931
|
+
pending.unshift(release);
|
|
17932
|
+
failures.push(error);
|
|
17933
|
+
}
|
|
17926
17934
|
}
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
|
|
17930
|
-
|
|
17931
|
-
|
|
17935
|
+
rollbackOwners = pending;
|
|
17936
|
+
if (failures.length)
|
|
17937
|
+
throw new AggregateError(failures, "Partial console cleanup incomplete.");
|
|
17938
|
+
};
|
|
17939
|
+
try {
|
|
17940
|
+
const configDir = options.configDir ?? localOperatorConfigDir();
|
|
17941
|
+
const history = new ConsoleHistory(
|
|
17942
|
+
path$1.join(configDir, "run", "ui-console", "history")
|
|
17943
|
+
);
|
|
17944
|
+
const registry = new ConsoleRegistry();
|
|
17945
|
+
const completion = consoleCompletionHooks({
|
|
17946
|
+
host: () => hostRef,
|
|
17947
|
+
registry,
|
|
17948
|
+
notifier: () => options.notifier ?? null,
|
|
17949
|
+
log: log2,
|
|
17950
|
+
now: options.now
|
|
17951
|
+
});
|
|
17952
|
+
capture = options.consoleCaptureUrl && options.preloadPath ? new ConsoleCaptureView({
|
|
17953
|
+
url: options.consoleCaptureUrl,
|
|
17954
|
+
preload: options.preloadPath,
|
|
17955
|
+
log: log2
|
|
17956
|
+
}) : null;
|
|
17957
|
+
const captureView = capture;
|
|
17958
|
+
if (captureView) rollbackOwners.push(() => captureView.dispose());
|
|
17959
|
+
host = new ConsoleHost(registry, {
|
|
17960
|
+
window: () => options.window,
|
|
17961
|
+
log: log2,
|
|
17962
|
+
history,
|
|
17963
|
+
now: options.now,
|
|
17964
|
+
// Resolved BEFORE the host exists, so no surface can be created against the
|
|
17965
|
+
// launch environment and then start behaving like the user's terminal later:
|
|
17966
|
+
// a surface's PATH is decided here, once, and every surface this host makes
|
|
17967
|
+
// carries the same one. Bounded by the resolver's own timeout (it never
|
|
17968
|
+
// rejects and answers null when the shell cannot speak), and no part of the
|
|
17969
|
+
// app's own startup waits on it: the browser host is attached to the window
|
|
17970
|
+
// without being awaited, so the cost of a slow rc here is this host coming
|
|
17971
|
+
// online later, not the app failing to come up.
|
|
17972
|
+
env: await withUserShellPath(process.env, options.userShellPath),
|
|
17973
|
+
captureOffscreen: captureView ? (request) => captureView.capture(request) : void 0,
|
|
17974
|
+
spawn: options.spawn ?? ((spawnOptions) => spawnPty(load.module, spawnOptions, (report) => {
|
|
17975
|
+
log2(
|
|
17976
|
+
`[console] restored the exec bit on ${report.path ?? "spawn-helper"}`
|
|
17977
|
+
);
|
|
17978
|
+
})),
|
|
17979
|
+
/*
|
|
17980
|
+
* ONE PUSHER, and it is A's. `broadcastConsoleState` was already the app's
|
|
17981
|
+
* single "the projection moved" signal when this branch arrived; the
|
|
17982
|
+
* completion seam below therefore does NOT push a second copy of the same
|
|
17983
|
+
* frame. It only raises a banner, which is the half a push cannot express —
|
|
17984
|
+
* and the host calls `onChanged` for a mark and an exit too, so the renderer
|
|
17985
|
+
* still refetches on both (see `ConsoleHostOptions.onExit`).
|
|
17986
|
+
*/
|
|
17987
|
+
onChanged: () => {
|
|
17988
|
+
broadcastConsoleState(options.window);
|
|
17989
|
+
},
|
|
17990
|
+
onMark: completion.onMark,
|
|
17991
|
+
onExit: completion.onExit,
|
|
17992
|
+
onReveal: (request) => {
|
|
17993
|
+
const window = options.window;
|
|
17994
|
+
if (window.isDestroyed()) return;
|
|
17995
|
+
window.webContents.send("console-reveal", {
|
|
17996
|
+
surface: request.surface,
|
|
17997
|
+
session_id: request.sessionId,
|
|
17998
|
+
mode: request.mode
|
|
17999
|
+
});
|
|
18000
|
+
}
|
|
18001
|
+
});
|
|
18002
|
+
const ownedHost = host;
|
|
18003
|
+
rollbackOwners.push(() => ownedHost.dispose(false));
|
|
18004
|
+
if (options.restoreHistory !== false) {
|
|
18005
|
+
const retained = history.list().sort((a, b) => b.meta.last_seen_at - a.meta.last_seen_at).slice(0, MAX_SURFACES_PER_APP);
|
|
18006
|
+
for (const surface of retained) {
|
|
18007
|
+
host.restore(surface.meta, surface.bytes);
|
|
18008
|
+
}
|
|
18009
|
+
if (retained.length > 0) {
|
|
18010
|
+
log2(
|
|
18011
|
+
`[console] restored ${retained.length} retained surface(s) from history; none of them is running`
|
|
18012
|
+
);
|
|
18013
|
+
}
|
|
17932
18014
|
}
|
|
17933
|
-
|
|
18015
|
+
const collected = history.collect();
|
|
18016
|
+
if (collected.removedSessions.length > 0) {
|
|
17934
18017
|
log2(
|
|
17935
|
-
`[console]
|
|
18018
|
+
`[console] history GC removed ${collected.removedSurfaces} surface(s) from ${collected.removedSessions.length} session(s) older than the window`
|
|
17936
18019
|
);
|
|
17937
18020
|
}
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
18021
|
+
hostRef = host;
|
|
18022
|
+
const readyHost = host;
|
|
18023
|
+
registerConsoleIpc({
|
|
18024
|
+
window: () => options.window,
|
|
18025
|
+
expectedUrl: options.expectedUrl,
|
|
18026
|
+
host: () => readyHost,
|
|
18027
|
+
log: log2
|
|
18028
|
+
});
|
|
17941
18029
|
log2(
|
|
17942
|
-
`[console]
|
|
18030
|
+
`[console] host ready on the existing endpoint: ${registry.count()} surface(s) (node-pty ${helperStatus(load.helper)})`
|
|
17943
18031
|
);
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
|
|
17949
|
-
|
|
17950
|
-
|
|
17951
|
-
|
|
17952
|
-
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
dispatch: async (method, params) => {
|
|
17961
|
-
if (!isConsoleDispatchMethod(method)) {
|
|
17962
|
-
throw new Error(`not a console method: ${method}`);
|
|
18032
|
+
return {
|
|
18033
|
+
ok: true,
|
|
18034
|
+
host,
|
|
18035
|
+
registry,
|
|
18036
|
+
counts: () => readyHost.counts(),
|
|
18037
|
+
dispatch: async (method, params) => {
|
|
18038
|
+
if (!isConsoleDispatchMethod(method)) {
|
|
18039
|
+
throw new Error(`not a console method: ${method}`);
|
|
18040
|
+
}
|
|
18041
|
+
return dispatchConsole(readyHost, method, params);
|
|
18042
|
+
},
|
|
18043
|
+
stop: async () => {
|
|
18044
|
+
unregisterConsoleIpc();
|
|
18045
|
+
capture?.dispose();
|
|
18046
|
+
readyHost.dispose();
|
|
18047
|
+
log2("[console] host stopped");
|
|
17963
18048
|
}
|
|
17964
|
-
|
|
17965
|
-
|
|
17966
|
-
|
|
17967
|
-
|
|
17968
|
-
|
|
17969
|
-
|
|
17970
|
-
|
|
18049
|
+
};
|
|
18050
|
+
} catch (error) {
|
|
18051
|
+
try {
|
|
18052
|
+
cleanupStartupOwners();
|
|
18053
|
+
} catch (cleanupError) {
|
|
18054
|
+
log2(
|
|
18055
|
+
`[console] partial cleanup retained for stop: ${String(cleanupError)}`
|
|
18056
|
+
);
|
|
17971
18057
|
}
|
|
17972
|
-
|
|
18058
|
+
hostRef = null;
|
|
18059
|
+
const detail = String(error);
|
|
18060
|
+
log2(`[console] startup failed: ${detail}`);
|
|
18061
|
+
registerRefusal("startup_failed", detail);
|
|
18062
|
+
return {
|
|
18063
|
+
ok: false,
|
|
18064
|
+
reason: "startup_failed",
|
|
18065
|
+
detail,
|
|
18066
|
+
stop: async () => {
|
|
18067
|
+
unregisterConsoleIpc();
|
|
18068
|
+
cleanupStartupOwners();
|
|
18069
|
+
}
|
|
18070
|
+
};
|
|
18071
|
+
}
|
|
17973
18072
|
}
|
|
17974
18073
|
const WEBAUTHN_GROUP_SUFFIX = ".webauthn";
|
|
17975
18074
|
const WEBAUTHN_PROMPT_REASON = "use a passkey for $1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-operator-ui",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.31",
|
|
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",
|