windows-development-sandbox 1.13.0 → 1.14.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/app/features/accounts/accounts.def.d.ts +1 -0
- package/dist/app/features/accounts/accounts.def.js +12 -3
- package/dist/app/features/accounts/accounts.run.d.ts +1 -0
- package/dist/app/features/accounts/accounts.run.js +20 -11
- package/dist/app/features/defs.d.ts +1 -0
- package/dist/app/features/settings/settings-file.d.ts +7 -0
- package/dist/app/features/settings/settings-file.js +19 -2
- package/dist/app/features/settings/settings-rows.js +9 -1
- package/dist/app/features/storage/storage.run.js +1 -1
- package/dist/bin/agent-sandbox.js +7 -1
- package/dist/bin/chunk-BQ23GBLC.js +78790 -0
- package/dist/bin/chunk-F7IMME5G.js +10432 -0
- package/dist/bin/chunk-LQHEUN2A.js +7 -0
- package/dist/bin/claude-host.js +10 -4
- package/dist/bin/claude-sbx.js +8 -2
- package/dist/bin/codex-sbx.js +8 -2
- package/dist/bin/gemini-sbx.js +8 -2
- package/dist/bin/sandbox.js +7 -2
- package/dist/bin/sbx-hide.js +8 -2
- package/dist/bin/sbx-show.js +8 -2
- package/dist/cli/commands/launch/launch.js +1 -1
- package/dist/cli/commands/launch/prepare.js +24 -7
- package/dist/cli/commands/launch/running.js +4 -0
- package/dist/cli/commands/repair/checks/contained.js +1 -1
- package/dist/cli/commands/storage/offer.js +1 -1
- package/dist/cli/menu/live.d.ts +29 -0
- package/dist/cli/menu/live.js +92 -21
- package/dist/cli/menu/menu.d.ts +2 -1
- package/dist/cli/menu/menu.js +16 -2
- package/dist/cli/menu/pages/list-page.d.ts +5 -0
- package/dist/cli/menu/pages/list-page.js +19 -0
- package/dist/cli/menu/shell.d.ts +2 -0
- package/dist/cli/menu/shell.js +1 -1
- package/dist/cli/services/accounts-service.d.ts +7 -0
- package/dist/cli/services/accounts-service.js +20 -0
- package/dist/cli/tui/app.d.ts +4 -0
- package/dist/cli/tui/app.js +5 -0
- package/dist/cli/tui/terminal.d.ts +6 -0
- package/dist/cli/tui/terminal.js +8 -1
- package/dist/cli/validate.js +8 -4
- package/dist/core/package-info.js +1 -1
- package/dist/guest/daemon/sessions.d.ts +7 -0
- package/dist/guest/daemon/sessions.js +23 -2
- package/dist/guest/entries/daemon.js +1 -0
- package/dist/guest/sessions/pty-host.d.ts +10 -0
- package/dist/guest/sessions/pty-host.js +48 -3
- package/dist/host/channel/channel.js +1 -1
- package/dist/host/install/install-record.d.ts +1 -0
- package/dist/host/install/install-record.js +2 -0
- package/dist/host/sandbox/contained-deps.d.ts +2 -1
- package/dist/host/sandbox/contained-deps.js +6 -4
- package/dist/host/sandbox/platform.d.ts +2 -0
- package/dist/host/sandbox/platform.js +19 -4
- package/dist/host/sharing/lifecycle.js +9 -1
- package/dist/host/storage/contained/contained-storage.js +13 -0
- package/dist/host/storage/contained/disk-port.d.ts +5 -0
- package/dist/host/storage/contained/helper-port.d.ts +72 -0
- package/dist/host/storage/contained/helper-port.js +162 -0
- package/dist/host/storage/contained/index.d.ts +1 -0
- package/dist/host/storage/contained/index.js +1 -0
- package/dist/host/storage/contained/state.d.ts +3 -2
- package/dist/host/storage/contained/state.js +5 -2
- package/dist/host/web/bridge.d.ts +5 -0
- package/dist/host/web/bridge.js +11 -1
- package/dist/host/web/server.d.ts +2 -0
- package/dist/host/web/server.js +11 -1
- package/dist/runtime/agent.js +9 -0
- package/dist/runtime/daemon.js +57 -3
- package/dist/runtime/db.js +9 -0
- package/dist/runtime/main.js +195 -12
- package/dist/runtime/netproxy.js +9 -0
- package/dist/runtime/view.js +195 -12
- package/dist/selftest/channel.test.js +72 -1
- package/dist/selftest/contained.test.js +1 -37
- package/dist/selftest/hcs.test.js +5 -2
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/install.test.js +11 -0
- package/dist/selftest/layout.test.js +10 -1
- package/dist/selftest/live-daemon.test.js +51 -1
- package/dist/selftest/live-terminal.test.js +112 -4
- package/dist/selftest/menu-perf.test.js +198 -12
- package/dist/selftest/menu.test.js +4 -0
- package/dist/selftest/storage-helper.test.d.ts +2 -0
- package/dist/selftest/storage-helper.test.js +493 -0
- package/dist/selftest/support/vhdx.d.ts +14 -0
- package/dist/selftest/support/vhdx.js +72 -0
- package/dist/selftest/web.test.js +14 -2
- package/dist/shared/channel/run-dir.d.ts +6 -0
- package/dist/shared/channel/run-dir.js +9 -0
- package/dist/shared/pty/index.d.ts +3 -1
- package/dist/shared/pty/index.js +3 -1
- package/dist/shared/pty/key-gate.d.ts +24 -0
- package/dist/shared/pty/key-gate.js +45 -0
- package/dist/shared/pty/mouse-modes.d.ts +27 -0
- package/dist/shared/pty/mouse-modes.js +155 -0
- package/dist/shared/pty/terminal.d.ts +0 -5
- package/dist/shared/pty/terminal.js +51 -20
- package/dist/sync/watcher.js +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/{main-Z2WYAE2Z.js → main-6XNBTJLT.js} +34 -34
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
- package/native/csharp/StorageHelper/Client.cs +297 -0
- package/native/csharp/StorageHelper/Log.cs +177 -0
- package/native/csharp/StorageHelper/MountRecords.cs +160 -0
- package/native/csharp/StorageHelper/Operations.cs +596 -0
- package/native/csharp/StorageHelper/Pins.cs +270 -0
- package/native/csharp/StorageHelper/Privileges.cs +78 -0
- package/native/csharp/StorageHelper/Program.cs +349 -0
- package/native/csharp/StorageHelper/Protocol.cs +107 -0
- package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
- package/native/csharp/StorageHelper/Rules.cs +156 -0
- package/native/csharp/StorageHelper/Server.cs +656 -0
- package/native/csharp/StorageHelper/Settings.cs +297 -0
- package/native/csharp/StorageHelper/StorageService.cs +53 -0
- package/native/csharp/StorageHelper/UserSettings.cs +206 -0
- package/native/csharp/StorageHelper/Vhdx.cs +230 -0
- package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
- package/native/csharp/StorageHelper/Volumes.cs +379 -0
- package/native/csharp/StorageHelper/Watchdog.cs +128 -0
- package/native/csharp/StorageHelper/Win32.cs +250 -0
- package/native/scripts/set-storage-helper.ps1 +203 -0
- package/package.json +1 -1
- package/sandbox/csharp/PtyHost.cs +236 -1
package/dist/bin/claude-host.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
run
|
|
5
|
+
} from "./chunk-F7IMME5G.js";
|
|
6
|
+
import "./chunk-BQ23GBLC.js";
|
|
7
|
+
|
|
8
|
+
// src/bin/claude-host.ts
|
|
9
|
+
var args = process.argv.slice(2);
|
|
10
|
+
var personal = args.includes("--personal") ? ["--personal"] : [];
|
|
11
|
+
run(["host-claude", ...personal, "--", ...args.filter((a) => a !== "--personal")]);
|
package/dist/bin/claude-sbx.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
run
|
|
5
|
+
} from "./chunk-F7IMME5G.js";
|
|
6
|
+
import "./chunk-BQ23GBLC.js";
|
|
7
|
+
|
|
8
|
+
// src/bin/claude-sbx.ts
|
|
9
|
+
run(process.argv.slice(2), { harness: "claude" });
|
package/dist/bin/codex-sbx.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
run
|
|
5
|
+
} from "./chunk-F7IMME5G.js";
|
|
6
|
+
import "./chunk-BQ23GBLC.js";
|
|
7
|
+
|
|
8
|
+
// src/bin/codex-sbx.ts
|
|
9
|
+
run(process.argv.slice(2), { harness: "codex" });
|
package/dist/bin/gemini-sbx.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
run
|
|
5
|
+
} from "./chunk-F7IMME5G.js";
|
|
6
|
+
import "./chunk-BQ23GBLC.js";
|
|
7
|
+
|
|
8
|
+
// src/bin/gemini-sbx.ts
|
|
9
|
+
run(process.argv.slice(2), { harness: "gemini" });
|
package/dist/bin/sandbox.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
run
|
|
5
|
+
} from "./chunk-F7IMME5G.js";
|
|
6
|
+
import "./chunk-BQ23GBLC.js";
|
|
7
|
+
|
|
8
|
+
// src/bin/sandbox.ts
|
|
4
9
|
run(process.argv.slice(2));
|
package/dist/bin/sbx-hide.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
run
|
|
5
|
+
} from "./chunk-F7IMME5G.js";
|
|
6
|
+
import "./chunk-BQ23GBLC.js";
|
|
7
|
+
|
|
8
|
+
// src/bin/sbx-hide.ts
|
|
9
|
+
run(process.argv.slice(2), { command: "hide" });
|
package/dist/bin/sbx-show.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
run
|
|
5
|
+
} from "./chunk-F7IMME5G.js";
|
|
6
|
+
import "./chunk-BQ23GBLC.js";
|
|
7
|
+
|
|
8
|
+
// src/bin/sbx-show.ts
|
|
9
|
+
run(process.argv.slice(2), { command: "show" });
|
|
@@ -130,7 +130,7 @@ class Launcher {
|
|
|
130
130
|
const engine = await openEngine(config);
|
|
131
131
|
const provider = providerFor(config, config.storage.contained === 'off'
|
|
132
132
|
? {}
|
|
133
|
-
: { contained: realContainedDeps(ctx.storageRoot, config.storage.containedMaxGb) });
|
|
133
|
+
: { contained: realContainedDeps(ctx.storageRoot, config.storage.containedMaxGb, Boolean(ctx.options.config)) });
|
|
134
134
|
if (!generateOnly)
|
|
135
135
|
assertCanLaunch(isSandboxRunning() ? runningOwner() : undefined);
|
|
136
136
|
// Contained storage: the image is mounted now (before anything reads or writes the data set), and let go of again
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
-
import {
|
|
3
|
+
import { AccountsService } from '../../services/index.js';
|
|
4
4
|
import { SandboxChannel } from '../../../host/channel/index.js';
|
|
5
5
|
import { describePrune, hostDataFolder, pruneStorage } from '../../../host/storage/index.js';
|
|
6
6
|
import { RepoGuardStore, readDataSize, saveMarkedProjects } from '../../../host/guard/index.js';
|
|
@@ -73,16 +73,33 @@ async function prepareWorkspaceView(ctx, run) {
|
|
|
73
73
|
'; ' +
|
|
74
74
|
plan.options.view);
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Every agent that keeps a login starts the new sandbox on its default (the launched agent on --account when given),
|
|
78
|
+
* so a session of any agent opened later uses the login you chose. Returns the launched agent's login.
|
|
79
|
+
*/
|
|
80
|
+
function launchAccounts(ctx, launched, wanted) {
|
|
81
|
+
const accounts = new AccountsService(ctx);
|
|
82
|
+
let account = null;
|
|
83
|
+
for (const agent of accounts.agents().filter((a) => a.kept)) {
|
|
84
|
+
if (agent.name === launched) {
|
|
85
|
+
account = accounts.ensure(launched, wanted);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
accounts.ensure(agent.name, null);
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
// Another agent's saved login could not be put in place: the launch goes on, and that agent says so.
|
|
93
|
+
console.warn(c.yellow('Could not switch ' + agent.name + ' to its default account: ' + errMsg(e)));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return account;
|
|
97
|
+
}
|
|
76
98
|
/** Everything a real launch does before the sandbox starts (skipped by `generate`, which only writes files). */
|
|
77
99
|
export async function prepare(ctx, run) {
|
|
78
100
|
const { options, dataPath, storageRoot } = ctx;
|
|
79
101
|
const { config } = run;
|
|
80
|
-
const
|
|
81
|
-
const account = options.account ?? accounts.defaultName();
|
|
82
|
-
if (account && run.harnessName === 'claude' && account !== accounts.activeFor(dataPath))
|
|
83
|
-
accounts.activate(dataPath, account);
|
|
84
|
-
else if (run.harnessName === 'claude')
|
|
85
|
-
accounts.syncPlugins(dataPath);
|
|
102
|
+
const account = launchAccounts(ctx, run.harnessName, options.account ?? null);
|
|
86
103
|
const terminal = config.provisioning.terminal.enabled
|
|
87
104
|
? await ensureTerminal(dataPath, config.provisioning.refresh, { pwsh: config.toolchains.pwsh.enabled !== false })
|
|
88
105
|
: { exe: null, message: 'disabled' };
|
|
@@ -6,6 +6,7 @@ import { NeedsRestart } from '../../../host/projects/index.js';
|
|
|
6
6
|
import { RunDir } from '../../../shared/channel/index.js';
|
|
7
7
|
import { errMsg, fail, sleep } from '../../../core/util/index.js';
|
|
8
8
|
import { c, follow, working, table } from '../../output/index.js';
|
|
9
|
+
import { AccountsService } from '../../services/index.js';
|
|
9
10
|
import { bootLine, readyIn, stillWorking } from './boot-progress.js';
|
|
10
11
|
import { hereDir, joinHere, readyHere, offerRestart } from '../projects/index.js';
|
|
11
12
|
/** How long a session may take to start (its project's setup, the agent's install). */
|
|
@@ -65,6 +66,9 @@ export function waitForStart(ctx, requestId, what) {
|
|
|
65
66
|
/** Asks the sandbox for a session; the agent's definition goes along (the sandbox may have booted with another). */
|
|
66
67
|
function requestSession(ctx, channel, harness, args, joined) {
|
|
67
68
|
const harnessSpec = ctx.loaded.config.harnesses[harness];
|
|
69
|
+
// --account into a running sandbox switches its login first (sessions already running switch on their restart).
|
|
70
|
+
if (ctx.options.account && !ctx.borrowed)
|
|
71
|
+
new AccountsService(ctx).ensure(harness, ctx.options.account);
|
|
68
72
|
return channel.requestSession(harness, args, joined.guestDir, {
|
|
69
73
|
...(joined.spec ? { project: joined.spec } : {}),
|
|
70
74
|
...(harnessSpec ? { harnessSpec } : {}),
|
|
@@ -10,7 +10,7 @@ import { containedAvailability, ContainedStorage, containedPaths } from '../../.
|
|
|
10
10
|
import { ok } from './common.js';
|
|
11
11
|
function storageOf(env) {
|
|
12
12
|
const { config } = env.ctx.loaded;
|
|
13
|
-
return new ContainedStorage(realContainedDeps(env.storageRoot, config.storage.containedMaxGb), config.storage.dataName);
|
|
13
|
+
return new ContainedStorage(realContainedDeps(env.storageRoot, config.storage.containedMaxGb, Boolean(env.ctx.options.config)), config.storage.dataName);
|
|
14
14
|
}
|
|
15
15
|
/** Only when contained storage is on, or an image or a record of one exists. */
|
|
16
16
|
const inUse = (env) => {
|
|
@@ -88,7 +88,7 @@ export async function storageBeforeCommand(ctx, command) {
|
|
|
88
88
|
ensureStorageRoot(ctx.storageRoot, protectPrivateFolder);
|
|
89
89
|
// Contained storage (beta): a mount an earlier sandbox left behind is only mentioned here; repair fixes it.
|
|
90
90
|
if (command !== 'launch' && !ctx.options.json) {
|
|
91
|
-
const stale = staleMountNote(ctx.storageRoot, ctx.loaded.config.storage.dataName, isSandboxRunning
|
|
91
|
+
const stale = staleMountNote(ctx.storageRoot, ctx.loaded.config.storage.dataName, isSandboxRunning);
|
|
92
92
|
if (stale)
|
|
93
93
|
console.error(c.yellow(stale));
|
|
94
94
|
}
|
package/dist/cli/menu/live.d.ts
CHANGED
|
@@ -5,20 +5,49 @@ export interface LiveOptions {
|
|
|
5
5
|
readonly derive: () => string;
|
|
6
6
|
/** The line changed: draw again. */
|
|
7
7
|
readonly onChange: () => void;
|
|
8
|
+
/** How often the line is derived again (and the shortest gap between two process lists). */
|
|
8
9
|
readonly intervalMs?: number;
|
|
10
|
+
/** The longest gap between two process lists while no sandbox runs and nobody presses a key. */
|
|
11
|
+
readonly idleMaxMs?: number;
|
|
12
|
+
/** Whether a process still runs (default: a signal-0 check, which starts no program). */
|
|
13
|
+
readonly alive?: (pid: number) => boolean;
|
|
14
|
+
/** Whether the process list is needed (default: not inside a sandbox, where "is it running?" is always yes). */
|
|
15
|
+
readonly needsList?: () => boolean;
|
|
16
|
+
/**
|
|
17
|
+
* A cheap fingerprint of what a sandbox start changes (the data folder's run folder): while none runs, a change
|
|
18
|
+
* fetches the list at the next tick instead of after the idle gap.
|
|
19
|
+
*/
|
|
20
|
+
readonly signal?: () => string;
|
|
9
21
|
}
|
|
22
|
+
/** The gap before the next process list after `unchanged` lists in a row found the same: doubling, up to `max`. */
|
|
23
|
+
export declare function listGap(unchanged: number, base: number, max: number): number;
|
|
10
24
|
export declare class LiveStatus {
|
|
11
25
|
private readonly o;
|
|
12
26
|
private tasklist;
|
|
27
|
+
/** The sandbox's processes in the last list (none: no sandbox was running). */
|
|
28
|
+
private pids;
|
|
29
|
+
private fetchedAt;
|
|
30
|
+
/** Lists in a row that found the same answer (the gap to the next one grows with it). */
|
|
31
|
+
private unchanged;
|
|
32
|
+
/** The signal when the list was last fetched. */
|
|
33
|
+
private signalled;
|
|
13
34
|
private text;
|
|
14
35
|
private timer;
|
|
15
36
|
private pending;
|
|
16
37
|
private running;
|
|
17
38
|
constructor(o: LiveOptions);
|
|
39
|
+
private get interval();
|
|
18
40
|
/** The status line as of the last refresh. */
|
|
19
41
|
line(): string;
|
|
20
42
|
/** Reads the process list now and derives the line (one fetch at a time; a caller during one waits for it). */
|
|
21
43
|
refresh(): Promise<void>;
|
|
44
|
+
private fetch;
|
|
45
|
+
private load;
|
|
46
|
+
/** Whether the process list may have changed since it was fetched (checked on every tick, cheaply). */
|
|
47
|
+
private listDue;
|
|
48
|
+
private tick;
|
|
49
|
+
/** A key was pressed: someone is looking, so a list older than one interval is fetched again (after the draw). */
|
|
50
|
+
touch(): void;
|
|
22
51
|
private derive;
|
|
23
52
|
/** Starts keeping the line fresh and answers the host's process questions from it (until stop). */
|
|
24
53
|
start(): Promise<void>;
|
package/dist/cli/menu/live.js
CHANGED
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
// What the menu shows about the running sandbox (the status line), kept fresh in the background. Drawing and keys only
|
|
2
|
-
// read the last copy
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// read the last copy, and the screen is told when what it shows has changed. While it runs, the host's own "is the
|
|
3
|
+
// sandbox running?" questions are answered from the same copy, so no page waits for a child process.
|
|
4
|
+
//
|
|
5
|
+
// The process list (tasklist) is the expensive part: on a busy PC one run costs a large share of a second of CPU. So it
|
|
6
|
+
// is not fetched on a timer: while a sandbox runs, its processes are checked by id (no program started) and the list is
|
|
7
|
+
// fetched again only when one of them has ended; while none runs, the list is fetched less and less often while nothing
|
|
8
|
+
// changes (and again soon after a key press, someone is looking, or when the data folder's run folder changes, a start
|
|
9
|
+
// writes there), and after every action.
|
|
5
10
|
import { sandboxStatus } from '../../app/features/sandbox/index.js';
|
|
6
|
-
import { readTasklistAsync, useTasklistCache } from '../../host/sandbox/index.js';
|
|
11
|
+
import { insideSandbox, readTasklistAsync, sandboxPids, useTasklistCache } from '../../host/sandbox/index.js';
|
|
7
12
|
/** The same rule as the web's live status: the sandbox status query's interval. */
|
|
8
13
|
const DEFAULT_INTERVAL_MS = sandboxStatus.live?.everyMs ?? 2500;
|
|
14
|
+
/** While no sandbox runs, nobody presses a key and the data folder is quiet, the list is fetched this seldom. */
|
|
15
|
+
const DEFAULT_IDLE_MAX_MS = 30000;
|
|
16
|
+
/** The gap before the next process list after `unchanged` lists in a row found the same: doubling, up to `max`. */
|
|
17
|
+
export function listGap(unchanged, base, max) {
|
|
18
|
+
return Math.min(max, base * 2 ** Math.max(0, Math.min(unchanged, 16)));
|
|
19
|
+
}
|
|
20
|
+
/** A process with this id exists (EPERM: it does, but belongs to someone else, such as the sandbox's own service). */
|
|
21
|
+
function processAlive(pid) {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
return e.code === 'EPERM';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
9
30
|
export class LiveStatus {
|
|
10
31
|
o;
|
|
11
32
|
tasklist;
|
|
33
|
+
/** The sandbox's processes in the last list (none: no sandbox was running). */
|
|
34
|
+
pids = [];
|
|
35
|
+
fetchedAt = 0;
|
|
36
|
+
/** Lists in a row that found the same answer (the gap to the next one grows with it). */
|
|
37
|
+
unchanged = 0;
|
|
38
|
+
/** The signal when the list was last fetched. */
|
|
39
|
+
signalled = '';
|
|
12
40
|
text = '';
|
|
13
41
|
timer;
|
|
14
42
|
pending;
|
|
@@ -16,30 +44,72 @@ export class LiveStatus {
|
|
|
16
44
|
constructor(o) {
|
|
17
45
|
this.o = o;
|
|
18
46
|
}
|
|
47
|
+
get interval() {
|
|
48
|
+
return this.o.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
49
|
+
}
|
|
19
50
|
/** The status line as of the last refresh. */
|
|
20
51
|
line() {
|
|
21
52
|
return this.text;
|
|
22
53
|
}
|
|
23
54
|
/** Reads the process list now and derives the line (one fetch at a time; a caller during one waits for it). */
|
|
24
55
|
refresh() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.derive();
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
// The line keeps what it showed; the next refresh tries again.
|
|
36
|
-
}
|
|
37
|
-
finally {
|
|
38
|
-
this.pending = undefined;
|
|
39
|
-
}
|
|
40
|
-
})();
|
|
56
|
+
// The next gap starts again from the shortest one.
|
|
57
|
+
this.unchanged = -1;
|
|
58
|
+
return this.fetch();
|
|
59
|
+
}
|
|
60
|
+
fetch() {
|
|
61
|
+
// Cleared in a callback, so also after a fetch that finished without waiting for anything.
|
|
62
|
+
this.pending ??= this.load().finally(() => (this.pending = undefined));
|
|
41
63
|
return this.pending;
|
|
42
64
|
}
|
|
65
|
+
async load() {
|
|
66
|
+
try {
|
|
67
|
+
const needed = (this.o.needsList ?? (() => !insideSandbox()))();
|
|
68
|
+
this.signalled = this.o.signal?.() ?? '';
|
|
69
|
+
// Not needed: an empty copy, so nothing runs tasklist itself either.
|
|
70
|
+
const list = needed ? await (this.o.fetch ?? readTasklistAsync)() : '';
|
|
71
|
+
// Stopped meanwhile: a command may be running, and nothing here may touch the host's facts any more.
|
|
72
|
+
if (!this.running)
|
|
73
|
+
return;
|
|
74
|
+
const pids = sandboxPids(list);
|
|
75
|
+
const same = this.tasklist !== undefined && pids.length > 0 === this.pids.length > 0;
|
|
76
|
+
this.unchanged = same ? this.unchanged + 1 : 0;
|
|
77
|
+
this.tasklist = list;
|
|
78
|
+
this.pids = pids;
|
|
79
|
+
this.fetchedAt = Date.now();
|
|
80
|
+
this.derive();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// The line keeps what it showed; the next refresh tries again.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Whether the process list may have changed since it was fetched (checked on every tick, cheaply). */
|
|
87
|
+
listDue(now) {
|
|
88
|
+
if (this.tasklist === undefined)
|
|
89
|
+
return true;
|
|
90
|
+
if (this.pids.length)
|
|
91
|
+
return !this.pids.every((pid) => (this.o.alive ?? processAlive)(pid));
|
|
92
|
+
if (this.o.signal && this.o.signal() !== this.signalled)
|
|
93
|
+
return true;
|
|
94
|
+
return now - this.fetchedAt >= listGap(this.unchanged, this.interval, this.o.idleMaxMs ?? DEFAULT_IDLE_MAX_MS);
|
|
95
|
+
}
|
|
96
|
+
tick() {
|
|
97
|
+
if (this.pending)
|
|
98
|
+
return;
|
|
99
|
+
if (this.listDue(Date.now()))
|
|
100
|
+
void this.fetch();
|
|
101
|
+
else
|
|
102
|
+
this.derive();
|
|
103
|
+
}
|
|
104
|
+
/** A key was pressed: someone is looking, so a list older than one interval is fetched again (after the draw). */
|
|
105
|
+
touch() {
|
|
106
|
+
if (!this.running || this.pending || this.pids.length || Date.now() - this.fetchedAt < this.interval)
|
|
107
|
+
return;
|
|
108
|
+
setImmediate(() => {
|
|
109
|
+
if (this.running)
|
|
110
|
+
void this.refresh();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
43
113
|
derive() {
|
|
44
114
|
const next = this.o.derive();
|
|
45
115
|
if (next === this.text)
|
|
@@ -54,7 +124,7 @@ export class LiveStatus {
|
|
|
54
124
|
return;
|
|
55
125
|
this.running = true;
|
|
56
126
|
useTasklistCache(() => this.tasklist);
|
|
57
|
-
this.timer = setInterval(() =>
|
|
127
|
+
this.timer = setInterval(() => this.tick(), this.interval);
|
|
58
128
|
this.timer.unref();
|
|
59
129
|
await this.refresh();
|
|
60
130
|
}
|
|
@@ -65,5 +135,6 @@ export class LiveStatus {
|
|
|
65
135
|
this.timer = undefined;
|
|
66
136
|
useTasklistCache(null);
|
|
67
137
|
this.tasklist = undefined;
|
|
138
|
+
this.pids = [];
|
|
68
139
|
}
|
|
69
140
|
}
|
package/dist/cli/menu/menu.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Context } from '../commands/context.js';
|
|
2
2
|
import { Terminal } from '../tui/index.js';
|
|
3
|
-
/** What a test may replace: the terminal, the process list fetch
|
|
3
|
+
/** What a test may replace: the terminal, the process list fetch, how often it is fetched and the process check. */
|
|
4
4
|
export interface MenuOptions {
|
|
5
5
|
readonly terminal?: Terminal;
|
|
6
6
|
readonly fetchTasklist?: () => Promise<string>;
|
|
7
7
|
readonly refreshMs?: number;
|
|
8
|
+
readonly processAlive?: (pid: number) => boolean;
|
|
8
9
|
}
|
|
9
10
|
export declare class Menu {
|
|
10
11
|
private readonly ctx;
|
package/dist/cli/menu/menu.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { loadConfig } from '../../core/config/index.js';
|
|
7
7
|
import { errMsg } from '../../core/util/index.js';
|
|
8
8
|
import { SandboxChannel } from '../../host/channel/index.js';
|
|
9
|
+
import { RunDir } from '../../shared/channel/index.js';
|
|
9
10
|
import { isSandboxRunning } from '../../host/sandbox/index.js';
|
|
10
11
|
import { lookAtSandbox } from '../commands/shared-use.js';
|
|
11
12
|
import { commandLine } from '../history.js';
|
|
@@ -26,6 +27,15 @@ import { readMenuView, saveMenuView } from './memory.js';
|
|
|
26
27
|
import { SECTION_LABELS, Shell } from './shell.js';
|
|
27
28
|
/** Where the menu was last left: it opens there again (also in a later run, from menu-state.json). */
|
|
28
29
|
let lastView;
|
|
30
|
+
/** When the data folder's run folder last changed (a start writes there): a stat, never a read of what is in it. */
|
|
31
|
+
function runFolderStamp(dataPath) {
|
|
32
|
+
try {
|
|
33
|
+
return String(fs.lstatSync(new RunDir(dataPath).root).mtimeMs);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
29
39
|
export class Menu {
|
|
30
40
|
ctx;
|
|
31
41
|
runArgv;
|
|
@@ -40,9 +50,12 @@ export class Menu {
|
|
|
40
50
|
derive: () => this.status(),
|
|
41
51
|
onChange: () => this.app.requestRedraw(),
|
|
42
52
|
intervalMs: options.refreshMs,
|
|
53
|
+
alive: options.processAlive,
|
|
54
|
+
signal: () => runFolderStamp(ctx.dataPath),
|
|
43
55
|
});
|
|
44
56
|
this.app.status = () => this.live.line();
|
|
45
57
|
this.app.afterAction = () => this.live.refresh();
|
|
58
|
+
this.app.onKey = () => this.live.touch();
|
|
46
59
|
try {
|
|
47
60
|
Frame.version =
|
|
48
61
|
JSON.parse(fs.readFileSync(path.join(ctx.packageRoot, 'package.json'), 'utf8'))
|
|
@@ -95,7 +108,8 @@ export class Menu {
|
|
|
95
108
|
}
|
|
96
109
|
try {
|
|
97
110
|
const shell = this.shell();
|
|
98
|
-
|
|
111
|
+
// The first screen does not wait for the process list: the pages show "Loading…" until it is there.
|
|
112
|
+
this.app.ready = this.live.start();
|
|
99
113
|
await shell.restore(lastView ?? readMenuView(SECTION_LABELS) ?? { section: 0, screens: {} });
|
|
100
114
|
for (;;) {
|
|
101
115
|
const exit = await this.app.show(shell);
|
|
@@ -112,7 +126,7 @@ export class Menu {
|
|
|
112
126
|
if (code !== undefined)
|
|
113
127
|
return code;
|
|
114
128
|
}
|
|
115
|
-
|
|
129
|
+
this.app.ready = this.live.start();
|
|
116
130
|
await shell.reload();
|
|
117
131
|
}
|
|
118
132
|
}
|
|
@@ -57,6 +57,11 @@ export declare abstract class ListPage<T> implements Page {
|
|
|
57
57
|
protected ticked(): T[];
|
|
58
58
|
/** Loads the data again; a slow load does not hold up the keys (the tab keeps what it showed, then updates). */
|
|
59
59
|
open(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* The section came on screen again: it shows what it had at once and loads the data again after that screen is
|
|
62
|
+
* drawn (the list is rebuilt on the entry and search the user is on). The first time, it is open().
|
|
63
|
+
*/
|
|
64
|
+
show(): Promise<void>;
|
|
60
65
|
/**
|
|
61
66
|
* Loads the data and waits for it at most QUICK_LOAD_MS. In time, the list is built here (`build`) or by the caller;
|
|
62
67
|
* later, the list is built when the data arrives, on the entry and search the user has moved to meanwhile.
|
|
@@ -97,6 +97,23 @@ export class ListPage {
|
|
|
97
97
|
async open() {
|
|
98
98
|
await this.reload(true);
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The section came on screen again: it shows what it had at once and loads the data again after that screen is
|
|
102
|
+
* drawn (the list is rebuilt on the entry and search the user is on). The first time, it is open().
|
|
103
|
+
*/
|
|
104
|
+
async show() {
|
|
105
|
+
if (!this.loaded)
|
|
106
|
+
return this.open();
|
|
107
|
+
const mine = ++this.generation;
|
|
108
|
+
setImmediate(() => {
|
|
109
|
+
if (mine !== this.generation)
|
|
110
|
+
return;
|
|
111
|
+
void this.loadData().then(() => {
|
|
112
|
+
if (mine === this.generation)
|
|
113
|
+
this.rebuildLate();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
100
117
|
/**
|
|
101
118
|
* Loads the data and waits for it at most QUICK_LOAD_MS. In time, the list is built here (`build`) or by the caller;
|
|
102
119
|
* later, the list is built when the data arrives, on the entry and search the user has moved to meanwhile.
|
|
@@ -125,6 +142,8 @@ export class ListPage {
|
|
|
125
142
|
}
|
|
126
143
|
async loadData() {
|
|
127
144
|
try {
|
|
145
|
+
// The first screen is drawn before the menu's facts (the process list) are known; loads wait for them.
|
|
146
|
+
await this.app.ready;
|
|
128
147
|
await this.load();
|
|
129
148
|
}
|
|
130
149
|
catch (e) {
|
package/dist/cli/menu/shell.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface Page {
|
|
|
47
47
|
setFocus(list: boolean): void;
|
|
48
48
|
/** The section was opened (or the shell came back from a command): load what it shows again. */
|
|
49
49
|
open(): Promise<void>;
|
|
50
|
+
/** The section came on screen again: show what it had at once and load it again in the background (default open). */
|
|
51
|
+
show?(): Promise<void>;
|
|
50
52
|
render(size: Size): FrameParts;
|
|
51
53
|
/** A `quit` from the page means "Esc with nothing left to close": the shell moves to the navigation. */
|
|
52
54
|
handle(key: Key): Promise<Step<ShellExit>> | Step<ShellExit>;
|
package/dist/cli/menu/shell.js
CHANGED
|
@@ -76,7 +76,7 @@ export class Shell {
|
|
|
76
76
|
this.remember();
|
|
77
77
|
this.active = Math.max(0, Math.min(this.pages.length - 1, index));
|
|
78
78
|
this.page.setFocus(this.focus === 'list');
|
|
79
|
-
await this.page.open();
|
|
79
|
+
await (this.page.show ?? this.page.open).call(this.page);
|
|
80
80
|
if (this.restored.has(this.active))
|
|
81
81
|
return;
|
|
82
82
|
this.restored.add(this.active);
|
|
@@ -31,6 +31,13 @@ export declare class AccountsService {
|
|
|
31
31
|
save(name: string, fromHost: boolean, agent?: string): AccountRef;
|
|
32
32
|
/** Makes the sandbox use a saved login. Running sessions keep the old one until restart(). */
|
|
33
33
|
activate(name: string, agent?: string): AccountRef;
|
|
34
|
+
/** The agent's default login, if it keeps logins and one is set. */
|
|
35
|
+
defaultName(agent?: string): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Makes the sandbox use `wanted`, else the agent's default, when it uses another login now (a launch, or a session
|
|
38
|
+
* started with --account). Returns the login it then uses; null when the agent keeps no login or none is saved.
|
|
39
|
+
*/
|
|
40
|
+
ensure(agent: string, wanted: string | null): string | null;
|
|
34
41
|
/** Whether a session of this agent is running that would pick up a new login on restart. */
|
|
35
42
|
hasRunningSessions(agent?: string): boolean;
|
|
36
43
|
/** Restarts the agent's running sessions (conversations continue); returns the sandbox's own message. */
|
|
@@ -74,6 +74,26 @@ export class AccountsService {
|
|
|
74
74
|
store.activate(this.ctx.dataPath, name, stateDir);
|
|
75
75
|
return { name, email: null };
|
|
76
76
|
}
|
|
77
|
+
/** The agent's default login, if it keeps logins and one is set. */
|
|
78
|
+
defaultName(agent = CLAUDE) {
|
|
79
|
+
return agent === CLAUDE ? this.claude.defaultName() : this.other(agent).store.defaultName();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Makes the sandbox use `wanted`, else the agent's default, when it uses another login now (a launch, or a session
|
|
83
|
+
* started with --account). Returns the login it then uses; null when the agent keeps no login or none is saved.
|
|
84
|
+
*/
|
|
85
|
+
ensure(agent, wanted) {
|
|
86
|
+
if (!this.agents().some((a) => a.name === agent && a.kept))
|
|
87
|
+
return null;
|
|
88
|
+
const name = wanted ?? this.defaultName(agent);
|
|
89
|
+
if (!name)
|
|
90
|
+
return this.activeName(agent);
|
|
91
|
+
if (name !== this.activeName(agent))
|
|
92
|
+
this.activate(name, agent);
|
|
93
|
+
else if (agent === CLAUDE)
|
|
94
|
+
this.claude.syncPlugins(this.ctx.dataPath);
|
|
95
|
+
return name;
|
|
96
|
+
}
|
|
77
97
|
/** Whether a session of this agent is running that would pick up a new login on restart. */
|
|
78
98
|
hasRunningSessions(agent = CLAUDE) {
|
|
79
99
|
return (isSandboxRunning() && new SandboxChannel(this.ctx.dataPath).runningSessions().some((s) => s.harness === agent));
|
package/dist/cli/tui/app.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export declare class App {
|
|
|
13
13
|
status: () => string;
|
|
14
14
|
/** Awaited after an action changed something, before the screen loads what it shows again. */
|
|
15
15
|
afterAction: () => Promise<void>;
|
|
16
|
+
/** Settles when the facts pages load from are known (the first screen is drawn without waiting for it). */
|
|
17
|
+
ready: Promise<void>;
|
|
18
|
+
/** Told about every key (after it was handled and drawn): someone is looking at the screen. */
|
|
19
|
+
onKey: () => void;
|
|
16
20
|
/** The navigation on the left of every screen (none: no navigation column). */
|
|
17
21
|
nav: () => NavState | undefined;
|
|
18
22
|
/** The screens being shown, innermost last: a dialog is drawn over the one below it. */
|
package/dist/cli/tui/app.js
CHANGED
|
@@ -19,6 +19,10 @@ export class App {
|
|
|
19
19
|
status = () => '';
|
|
20
20
|
/** Awaited after an action changed something, before the screen loads what it shows again. */
|
|
21
21
|
afterAction = () => Promise.resolve();
|
|
22
|
+
/** Settles when the facts pages load from are known (the first screen is drawn without waiting for it). */
|
|
23
|
+
ready = Promise.resolve();
|
|
24
|
+
/** Told about every key (after it was handled and drawn): someone is looking at the screen. */
|
|
25
|
+
onKey = () => undefined;
|
|
22
26
|
/** The navigation on the left of every screen (none: no navigation column). */
|
|
23
27
|
nav = () => undefined;
|
|
24
28
|
/** The screens being shown, innermost last: a dialog is drawn over the one below it. */
|
|
@@ -67,6 +71,7 @@ export class App {
|
|
|
67
71
|
drawnAt = Date.now();
|
|
68
72
|
}
|
|
69
73
|
const step = await screen.handle(await this.terminal.key());
|
|
74
|
+
this.onKey();
|
|
70
75
|
if (step)
|
|
71
76
|
return step.done;
|
|
72
77
|
}
|
|
@@ -10,6 +10,12 @@ export interface Key {
|
|
|
10
10
|
export declare class ExitRequested extends Error {
|
|
11
11
|
constructor();
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* How long a lone ESC waits for the rest of an escape sequence before it counts as the Esc key. Node's default is
|
|
15
|
+
* 500 ms, which every Esc (back, close) waited for. Terminals send a key's whole sequence in one write, so a short wait
|
|
16
|
+
* is enough.
|
|
17
|
+
*/
|
|
18
|
+
export declare const ESCAPE_TIMEOUT_MS = 25;
|
|
13
19
|
/** Whether this terminal is known to take synchronized updates (AGENT_SANDBOX_SYNC_UPDATE=1 or 0 overrides). */
|
|
14
20
|
export declare function supportsSyncUpdate(env?: NodeJS.ProcessEnv): boolean;
|
|
15
21
|
export declare class Terminal {
|