windows-development-sandbox 1.27.3 → 1.29.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/README.md +5 -4
- package/dist/app/features/sandbox/sandbox.run.js +1 -1
- package/dist/bin/{chunk-FA5VWLP4.js → chunk-O4EL32TM.js} +1 -1
- package/dist/bin/{chunk-RU7XRN62.js → chunk-QYRRWGFY.js} +1637 -1354
- package/dist/bin/sandbox.js +10772 -3
- package/dist/cli/commands/launch/launch.js +1 -1
- package/dist/cli/commands/launch/running.js +2 -2
- package/dist/cli/index.js +1 -1
- package/dist/cli/menu/pages/agents-page.js +1 -1
- package/dist/cli/services/sandbox-service.js +1 -1
- package/dist/core/schema/sandbox.js +2 -2
- package/dist/guest/apps/app-installer.js +1 -1
- package/dist/guest/apps/store-check.d.ts +2 -0
- package/dist/guest/apps/store-check.js +3 -1
- package/dist/guest/boot/guide-lines.d.ts +2 -0
- package/dist/guest/boot/guide-lines.js +7 -0
- package/dist/guest/boot/guide.js +2 -1
- package/dist/guest/daemon/heartbeat.js +1 -0
- package/dist/guest/governor/core.d.ts +53 -1
- package/dist/guest/governor/core.js +95 -1
- package/dist/guest/governor/decide.d.ts +4 -0
- package/dist/guest/governor/decide.js +2 -0
- package/dist/guest/governor/governor.js +23 -9
- package/dist/guest/toolchains/runtimes.js +7 -6
- package/dist/host/accounts/deliver.js +16 -2
- package/dist/host/apps/store-package.d.ts +2 -0
- package/dist/host/apps/store-package.js +16 -15
- package/dist/host/channel/attach.js +1 -1
- package/dist/host/channel/channel.js +3 -3
- package/dist/host/guard/commands.js +1 -0
- package/dist/host/health/path-copies.d.ts +1 -1
- package/dist/host/health/path-copies.js +2 -2
- package/dist/host/install/index.d.ts +1 -1
- package/dist/host/install/index.js +1 -1
- package/dist/host/install/shims.d.ts +8 -1
- package/dist/host/install/shims.js +39 -1
- package/dist/host/launch/generate.js +4 -0
- package/dist/host/launch/index.d.ts +1 -0
- package/dist/host/launch/index.js +1 -0
- package/dist/host/launch/pc-logs.d.ts +6 -0
- package/dist/host/launch/pc-logs.js +129 -0
- package/dist/host/life/crash-report.d.ts +4 -0
- package/dist/host/life/crash-report.js +30 -1
- package/dist/host/life/last-end.js +1 -0
- package/dist/host/sandbox/platform.js +1 -1
- package/dist/host/sandbox/window.js +1 -1
- package/dist/host/tools/windows-terminal.d.ts +4 -1
- package/dist/host/tools/windows-terminal.js +25 -9
- package/dist/runtime/agent.js +6 -2
- package/dist/runtime/daemon.js +111 -16
- package/dist/runtime/db.js +6 -2
- package/dist/runtime/main.js +199 -35
- package/dist/runtime/netproxy.js +6 -2
- package/dist/runtime/tool.js +6 -2
- package/dist/runtime/view.js +126 -28
- package/dist/selftest/agent-paths.test.js +3 -1
- package/dist/selftest/channel.test.js +33 -0
- package/dist/selftest/cli.test.js +5 -5
- package/dist/selftest/config.test.js +1 -1
- package/dist/selftest/credentials.test.js +1 -1
- package/dist/selftest/engine.test.js +1 -1
- package/dist/selftest/generate.test.js +66 -1
- package/dist/selftest/hcs.test.js +1 -1
- package/dist/selftest/health.test.js +21 -10
- package/dist/selftest/heavy-load.test.js +112 -4
- package/dist/selftest/host-sim.test.js +13 -14
- package/dist/selftest/install.test.js +6 -2
- package/dist/selftest/live-terminal.test.js +103 -0
- package/dist/selftest/personalization.test.js +2 -1
- package/dist/selftest/store-app.test.js +62 -4
- package/dist/selftest/support/engine-golden.js +2 -0
- package/dist/selftest/support/web-preview.js +2 -2
- package/dist/shared/accounts/kinds.d.ts +5 -0
- package/dist/shared/accounts/kinds.js +8 -0
- package/dist/shared/apps/index.d.ts +1 -1
- package/dist/shared/apps/index.js +1 -1
- package/dist/shared/apps/selection.d.ts +1 -0
- package/dist/shared/apps/selection.js +4 -3
- package/dist/shared/apps/store-package.d.ts +13 -0
- package/dist/shared/apps/store-package.js +24 -0
- package/dist/shared/channel/schema.d.ts +2 -0
- package/dist/shared/channel/schema.js +4 -0
- package/dist/shared/memory/policy.d.ts +12 -0
- package/dist/shared/memory/policy.js +4 -0
- package/dist/shared/pty/clipboard-watch.d.ts +4 -0
- package/dist/shared/pty/clipboard-watch.js +40 -0
- package/dist/shared/pty/ctrl-c.d.ts +13 -7
- package/dist/shared/pty/ctrl-c.js +35 -19
- package/dist/shared/pty/index.d.ts +1 -0
- package/dist/shared/pty/index.js +1 -0
- package/dist/shared/pty/terminal.d.ts +1 -1
- package/dist/shared/pty/terminal.js +16 -13
- package/dist/shared/web/ctrl-c.d.ts +26 -0
- package/dist/shared/web/ctrl-c.js +64 -0
- package/dist/shared/web/index.d.ts +1 -0
- package/dist/shared/web/index.js +1 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-NKM7ECSB.js → main-7NY4X6YU.js} +67 -67
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/Window.cs +1 -1
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/native/scripts/get-store-package.ps1 +24 -13
- package/package.json +4 -14
- package/sandbox/csharp/ClipboardWatch.cs +59 -0
- package/sandbox/csharp/MemGov.cs +752 -7
- package/sandbox/scripts/install-store-package.ps1 +11 -4
- package/sandbox/scripts/watch-clipboard.ps1 +8 -0
- package/schemas/config.schema.json +2 -2
- package/schemas/sandbox-config.schema.json +1 -1
- package/dist/bin/agent-sandbox.d.ts +0 -2
- package/dist/bin/agent-sandbox.js +0 -9
- package/dist/bin/chunk-4M5SZZ5P.js +0 -10778
- package/dist/bin/claude-host.d.ts +0 -2
- package/dist/bin/claude-host.js +0 -11
- package/dist/bin/claude-sbx.d.ts +0 -2
- package/dist/bin/claude-sbx.js +0 -9
- package/dist/bin/codex-sbx.d.ts +0 -2
- package/dist/bin/codex-sbx.js +0 -9
- package/dist/bin/gemini-sbx.d.ts +0 -2
- package/dist/bin/gemini-sbx.js +0 -9
- package/dist/bin/sbx-hide.d.ts +0 -2
- package/dist/bin/sbx-hide.js +0 -9
- package/dist/bin/sbx-show.d.ts +0 -2
- package/dist/bin/sbx-show.js +0 -9
|
@@ -853,6 +853,7 @@ export declare const PressureStatusSchema: z.ZodObject<{
|
|
|
853
853
|
pid: z.ZodNumber;
|
|
854
854
|
seconds: z.ZodNumber;
|
|
855
855
|
}, z.core.$strip>>;
|
|
856
|
+
held: z.ZodOptional<z.ZodNumber>;
|
|
856
857
|
cpuPercent: z.ZodOptional<z.ZodNumber>;
|
|
857
858
|
pcLow: z.ZodBoolean;
|
|
858
859
|
lastAction: z.ZodString;
|
|
@@ -875,6 +876,7 @@ export declare const HeartbeatSchema: z.ZodObject<{
|
|
|
875
876
|
privateMb: z.ZodNumber;
|
|
876
877
|
}, z.core.$strip>>;
|
|
877
878
|
diskQueueReported: z.ZodOptional<z.ZodNumber>;
|
|
879
|
+
held: z.ZodOptional<z.ZodNumber>;
|
|
878
880
|
phase: z.ZodEnum<{
|
|
879
881
|
ready: "ready";
|
|
880
882
|
working: "working";
|
|
@@ -133,6 +133,8 @@ export const PressureStatusSchema = z.object({
|
|
|
133
133
|
lowered: z.number().int().min(0).max(1024),
|
|
134
134
|
/** Compilers waiting their turn right now (name, process id, seconds until they may go on). */
|
|
135
135
|
paused: z.array(z.object({ name: Text(64), pid: z.number().int(), seconds: z.number().int().min(0).max(60) })).max(8),
|
|
136
|
+
/** Build programs the memory gate holds right now (let go one at a time). */
|
|
137
|
+
held: z.number().int().min(0).max(1024).optional(),
|
|
136
138
|
/** The CPU share the compilers get together (percent; 100 = no cap). */
|
|
137
139
|
cpuPercent: z.number().int().min(0).max(100).optional(),
|
|
138
140
|
/** The PC itself asked for slowing down (its memory or CPU is low). */
|
|
@@ -157,6 +159,8 @@ export const HeartbeatSchema = z.object({
|
|
|
157
159
|
topProcesses: z.array(z.object({ name: Text(64), privateMb: z.number().int().min(0).max(1e7) })).max(5),
|
|
158
160
|
/** The disk queue this PC last reported while asking the sandbox to slow down. */
|
|
159
161
|
diskQueueReported: z.number().min(0).max(100000).optional(),
|
|
162
|
+
/** Build programs the memory gate holds right now (let go one at a time). */
|
|
163
|
+
held: z.number().int().min(0).max(1024).optional(),
|
|
160
164
|
/** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
|
|
161
165
|
phase: z.enum(['ready', 'working', 'ending']),
|
|
162
166
|
});
|
|
@@ -72,3 +72,15 @@ export declare function maxCompilers(cpus: number, profile: LoadProfile): number
|
|
|
72
72
|
* `CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER`).
|
|
73
73
|
*/
|
|
74
74
|
export declare function cargoLinkerVar(host: string): string;
|
|
75
|
+
/** The memory gate in memgov: new build programs wait when the agents' programs grow faster than this. */
|
|
76
|
+
export interface BurstGate {
|
|
77
|
+
/** Growth (MB of committed memory) that counts as a burst... */
|
|
78
|
+
readonly growthMb: number;
|
|
79
|
+
/** ...within this many ms. */
|
|
80
|
+
readonly windowMs: number;
|
|
81
|
+
/** Held programs go on one at a time, this far apart (oldest first). */
|
|
82
|
+
readonly releaseMs: number;
|
|
83
|
+
/** No program waits longer than this (memgov's own cap is 30000); never killed. */
|
|
84
|
+
readonly maxHoldMs: number;
|
|
85
|
+
}
|
|
86
|
+
export declare const BURST_GATE: Readonly<Record<LoadProfile, BurstGate>>;
|
|
@@ -115,3 +115,7 @@ export function maxCompilers(cpus, profile) {
|
|
|
115
115
|
export function cargoLinkerVar(host) {
|
|
116
116
|
return 'CARGO_TARGET_' + host.toUpperCase().replace(/[^A-Z0-9]/g, '_') + '_LINKER';
|
|
117
117
|
}
|
|
118
|
+
export const BURST_GATE = {
|
|
119
|
+
normal: { growthMb: 4096, windowMs: 3000, releaseMs: 1500, maxHoldMs: 30000 },
|
|
120
|
+
gentle: { growthMb: 3072, windowMs: 3000, releaseMs: 2000, maxHoldMs: 30000 },
|
|
121
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Windows: tells the terminal client when the clipboard changes, e.g. you just copied text in the sandbox window (which
|
|
2
|
+
// copies on select). sandbox\scripts\watch-clipboard.ps1 prints a line per change and ends with this process.
|
|
3
|
+
// Only built-ins here: view.js bundles this.
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { SYSTEM } from '../system.js';
|
|
6
|
+
import { scriptArgs } from '../scripts.js';
|
|
7
|
+
/** The most output kept while waiting for the end of a line. */
|
|
8
|
+
const MAX_PENDING = 1024;
|
|
9
|
+
/** Calls `onChange` for each clipboard change from now on. No-op off Windows. */
|
|
10
|
+
export function watchClipboard(onChange) {
|
|
11
|
+
if (process.platform !== 'win32')
|
|
12
|
+
return { stop: () => undefined };
|
|
13
|
+
let child = null;
|
|
14
|
+
try {
|
|
15
|
+
child = spawn(SYSTEM.powershell(), scriptArgs('watch-clipboard.ps1', ['-ProcessId', String(process.pid)]), {
|
|
16
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Without the watcher a copy is only recognised from the program's own selection: nothing else is lost.
|
|
22
|
+
return { stop: () => undefined };
|
|
23
|
+
}
|
|
24
|
+
let pending = '';
|
|
25
|
+
child.stdout?.setEncoding('latin1');
|
|
26
|
+
child.stdout?.on('data', (text) => {
|
|
27
|
+
const lines = (pending + text).split('\n');
|
|
28
|
+
pending = (lines.pop() ?? '').slice(-MAX_PENDING);
|
|
29
|
+
for (const line of lines)
|
|
30
|
+
if (line.trim() === 'changed')
|
|
31
|
+
onChange();
|
|
32
|
+
});
|
|
33
|
+
child.on('error', () => undefined);
|
|
34
|
+
return {
|
|
35
|
+
stop: () => {
|
|
36
|
+
if (child && child.exitCode === null && child.signalCode === null)
|
|
37
|
+
child.kill();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
/** What the title shows while a Ctrl+C is held back. */
|
|
4
|
-
export declare const CTRL_C_HINT = "Ctrl+C again to send it to the program (Esc stops an agent)";
|
|
1
|
+
import { CTRL_C_CONFIRM_MS, CTRL_C_COPY_HINT, CTRL_C_HINT } from '../web/index.js';
|
|
2
|
+
export { CTRL_C_CONFIRM_MS, CTRL_C_COPY_HINT, CTRL_C_HINT };
|
|
5
3
|
export declare class CtrlCGuard {
|
|
6
|
-
private
|
|
4
|
+
private readonly decider;
|
|
7
5
|
private lastTitle;
|
|
8
|
-
|
|
6
|
+
constructor(options?: {
|
|
7
|
+
confirm?: boolean;
|
|
8
|
+
});
|
|
9
|
+
/** What to send for one read from the keyboard: a lone Ctrl+C only when it is not held back or taken as "copy". */
|
|
9
10
|
push(chunk: Buffer, nowMs: number): {
|
|
10
11
|
send: Buffer;
|
|
11
12
|
held: boolean;
|
|
13
|
+
copied: boolean;
|
|
12
14
|
};
|
|
15
|
+
/** The clipboard changed since the last key: you just copied. */
|
|
16
|
+
clipboardChanged(): void;
|
|
17
|
+
/** The program wrote to the clipboard: that is a selection too. */
|
|
18
|
+
watchClipboard(raw: string): void;
|
|
13
19
|
/** Remembers the title the program last set (from its output), to put it back after the hint. */
|
|
14
20
|
watchOutput(text: string): void;
|
|
15
21
|
/** The escape sequence that shows the hint, and the one that puts the program's title back. */
|
|
16
|
-
hintTitle(): string;
|
|
22
|
+
hintTitle(kind?: 'hold' | 'copy'): string;
|
|
17
23
|
restoreTitle(): string;
|
|
18
24
|
}
|
|
@@ -1,25 +1,41 @@
|
|
|
1
|
-
// A
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
1
|
+
// A Ctrl+C is easy to send by accident: it is "copy" everywhere else. Two rules keep it from stopping an agent.
|
|
2
|
+
// 1. After you select text in the program (a mouse drag it received, or it wrote the clipboard with OSC 52), or the
|
|
3
|
+
// clipboard changed (you copied: the sandbox window copies on select), every Ctrl+C is "copy" and is never sent,
|
|
4
|
+
// until you press another key or click.
|
|
5
|
+
// 2. With attach.confirmCtrlC a lone Ctrl+C is held back until it is pressed again within a moment.
|
|
6
|
+
// Esc still stops an agent at once. The rules live in CtrlCDecider (pure, shared with the web terminal).
|
|
7
|
+
import { CTRL_C_CONFIRM_MS, CTRL_C_COPY_HINT, CTRL_C_HINT, CtrlCDecider, classifyInput, hasClipboardWrite, } from '../web/index.js';
|
|
8
|
+
export { CTRL_C_CONFIRM_MS, CTRL_C_COPY_HINT, CTRL_C_HINT };
|
|
9
9
|
const OSC_TITLE = /\x1b\][02];([^\x07\x1b]{0,256})(?:\x07|\x1b\\)/g;
|
|
10
10
|
export class CtrlCGuard {
|
|
11
|
-
|
|
11
|
+
decider;
|
|
12
12
|
lastTitle = '';
|
|
13
|
-
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.decider = new CtrlCDecider(options.confirm !== false);
|
|
15
|
+
}
|
|
16
|
+
/** What to send for one read from the keyboard: a lone Ctrl+C only when it is not held back or taken as "copy". */
|
|
14
17
|
push(chunk, nowMs) {
|
|
15
|
-
if (chunk.length
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return { send:
|
|
18
|
+
if (chunk.length === 1 && chunk[0] === 0x03) {
|
|
19
|
+
const action = this.decider.ctrlC(nowMs);
|
|
20
|
+
if (action === 'send')
|
|
21
|
+
return { send: chunk, held: false, copied: false };
|
|
22
|
+
return { send: Buffer.alloc(0), held: action === 'hold', copied: action === 'copy' };
|
|
20
23
|
}
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
const c = classifyInput(chunk.toString('latin1'));
|
|
25
|
+
if (c.keys || c.mouse === 'click')
|
|
26
|
+
this.decider.otherInput();
|
|
27
|
+
else if (c.mouse === 'drag')
|
|
28
|
+
this.decider.selection();
|
|
29
|
+
return { send: chunk, held: false, copied: false };
|
|
30
|
+
}
|
|
31
|
+
/** The clipboard changed since the last key: you just copied. */
|
|
32
|
+
clipboardChanged() {
|
|
33
|
+
this.decider.selection();
|
|
34
|
+
}
|
|
35
|
+
/** The program wrote to the clipboard: that is a selection too. */
|
|
36
|
+
watchClipboard(raw) {
|
|
37
|
+
if (hasClipboardWrite(raw))
|
|
38
|
+
this.decider.selection();
|
|
23
39
|
}
|
|
24
40
|
/** Remembers the title the program last set (from its output), to put it back after the hint. */
|
|
25
41
|
watchOutput(text) {
|
|
@@ -27,8 +43,8 @@ export class CtrlCGuard {
|
|
|
27
43
|
this.lastTitle = m[1];
|
|
28
44
|
}
|
|
29
45
|
/** The escape sequence that shows the hint, and the one that puts the program's title back. */
|
|
30
|
-
hintTitle() {
|
|
31
|
-
return '\x1b]0;' + CTRL_C_HINT + '\x07';
|
|
46
|
+
hintTitle(kind = 'hold') {
|
|
47
|
+
return '\x1b]0;' + (kind === 'copy' ? CTRL_C_COPY_HINT : CTRL_C_HINT) + '\x07';
|
|
32
48
|
}
|
|
33
49
|
restoreTitle() {
|
|
34
50
|
return '\x1b]0;' + this.lastTitle.replace(/[\x00-\x1f\x7f]/g, '') + '\x07';
|
|
@@ -6,4 +6,5 @@ export { MouseModes } from './mouse-modes.js';
|
|
|
6
6
|
export { JOIN_MS, InputJoiner } from './input-joiner.js';
|
|
7
7
|
export { CTRL_C_CONFIRM_MS, CTRL_C_HINT, CtrlCGuard } from './ctrl-c.js';
|
|
8
8
|
export { type VtInput, enableVtInput } from './console-input.js';
|
|
9
|
+
export { watchClipboard } from './clipboard-watch.js';
|
|
9
10
|
export { OutputFilter } from './output-filter.js';
|
package/dist/shared/pty/index.js
CHANGED
|
@@ -6,4 +6,5 @@ export { MouseModes } from './mouse-modes.js';
|
|
|
6
6
|
export { JOIN_MS, InputJoiner } from './input-joiner.js';
|
|
7
7
|
export { CTRL_C_CONFIRM_MS, CTRL_C_HINT, CtrlCGuard } from './ctrl-c.js';
|
|
8
8
|
export { enableVtInput } from './console-input.js';
|
|
9
|
+
export { watchClipboard } from './clipboard-watch.js';
|
|
9
10
|
export { OutputFilter } from './output-filter.js';
|
|
@@ -19,7 +19,7 @@ export declare const MOUSE_OFF = "\u001B[?1000l\u001B[?1002l\u001B[?1003l\u001B[
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function runTerminalClient(socket: net.Socket, token: string,
|
|
21
21
|
/** clipboardWrites: the agent's "copy" (OSC 52) reaches the clipboard; see OutputFilterOptions. */
|
|
22
|
-
/** confirmCtrlC: a lone Ctrl+C is held back until
|
|
22
|
+
/** confirmCtrlC (default true): a lone Ctrl+C is held back until pressed again; after a selection it is "copy". */
|
|
23
23
|
options?: {
|
|
24
24
|
detachKey?: number;
|
|
25
25
|
clipboardWrites?: boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FrameReader, encodeFrame, FrameType, resizeFrame, PING_INTERVAL_MS, helloFrame, } from './protocol.js';
|
|
2
2
|
import { OutputFilter } from './output-filter.js';
|
|
3
|
+
import { watchClipboard } from './clipboard-watch.js';
|
|
3
4
|
import { enableVtInput } from './console-input.js';
|
|
4
5
|
import { InputJoiner, JOIN_MS } from './input-joiner.js';
|
|
5
6
|
import { CTRL_C_CONFIRM_MS, CtrlCGuard } from './ctrl-c.js';
|
|
@@ -40,7 +41,7 @@ const VT_WAIT_MS = 10000;
|
|
|
40
41
|
*/
|
|
41
42
|
export function runTerminalClient(socket, token,
|
|
42
43
|
/** clipboardWrites: the agent's "copy" (OSC 52) reaches the clipboard; see OutputFilterOptions. */
|
|
43
|
-
/** confirmCtrlC: a lone Ctrl+C is held back until
|
|
44
|
+
/** confirmCtrlC (default true): a lone Ctrl+C is held back until pressed again; after a selection it is "copy". */
|
|
44
45
|
options = {}) {
|
|
45
46
|
const stdin = process.stdin;
|
|
46
47
|
const stdout = process.stdout;
|
|
@@ -75,6 +76,7 @@ options = {}) {
|
|
|
75
76
|
clearTimeout(joinTimer);
|
|
76
77
|
clearTimeout(hintTimer);
|
|
77
78
|
clearTimeout(vtTimer);
|
|
79
|
+
clipboard.stop();
|
|
78
80
|
socket.destroy();
|
|
79
81
|
// The agent's mouse tracking ends with it: your terminal's wheel scrolls again (and clicks select text). Reports
|
|
80
82
|
// the terminal sent before it read that are dropped here, or they would reach your shell as text.
|
|
@@ -98,24 +100,24 @@ options = {}) {
|
|
|
98
100
|
};
|
|
99
101
|
// Mouse reports the agent has not asked for (motion without 1002/1003 ...) are dropped: see MouseModes.
|
|
100
102
|
const sendInput = (data) => write(keys.push(passthrough ? mouse.filter(data) : data));
|
|
101
|
-
const guard = options.confirmCtrlC
|
|
103
|
+
const guard = new CtrlCGuard({ confirm: options.confirmCtrlC !== false });
|
|
102
104
|
let hintTimer;
|
|
105
|
+
// Copying in your terminal (select) changes the clipboard: the Ctrl+C that follows is "copy", not an interrupt.
|
|
106
|
+
const clipboard = watchClipboard(() => guard.clipboardChanged());
|
|
103
107
|
const onInput = (chunk) => {
|
|
104
108
|
if (options.detachKey !== undefined && chunk.includes(options.detachKey)) {
|
|
105
109
|
finish({ reason: 'detach' });
|
|
106
110
|
return;
|
|
107
111
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (decided.send.length === 1 && decided.send[0] === 0x03)
|
|
117
|
-
stdout.write(guard.restoreTitle());
|
|
112
|
+
const decided = guard.push(chunk, Date.now());
|
|
113
|
+
clearTimeout(hintTimer);
|
|
114
|
+
if (decided.held || decided.copied) {
|
|
115
|
+
stdout.write(guard.hintTitle(decided.copied ? 'copy' : 'hold'));
|
|
116
|
+
hintTimer = setTimeout(() => stdout.write(guard.restoreTitle()), CTRL_C_CONFIRM_MS);
|
|
117
|
+
return;
|
|
118
118
|
}
|
|
119
|
+
if (decided.send.length === 1 && decided.send[0] === 0x03)
|
|
120
|
+
stdout.write(guard.restoreTitle());
|
|
119
121
|
clearTimeout(joinTimer);
|
|
120
122
|
sendInput(joiner.push(chunk));
|
|
121
123
|
// A lone Esc key press is a "sequence" that never completes: send it after a moment.
|
|
@@ -159,9 +161,10 @@ options = {}) {
|
|
|
159
161
|
}
|
|
160
162
|
for (const frame of frames) {
|
|
161
163
|
if (frame.type === FrameType.Output) {
|
|
164
|
+
guard.watchClipboard(frame.payload.toString('latin1'));
|
|
162
165
|
const out = filter.push(frame.payload);
|
|
163
166
|
mouse.watch(out);
|
|
164
|
-
guard
|
|
167
|
+
guard.watchOutput(out.toString('latin1'));
|
|
165
168
|
stdout.write(out);
|
|
166
169
|
}
|
|
167
170
|
else if (frame.type === FrameType.Console)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** A second Ctrl+C within this long sends it. */
|
|
2
|
+
export declare const CTRL_C_CONFIRM_MS = 1500;
|
|
3
|
+
/** What the title shows while a Ctrl+C is held back. */
|
|
4
|
+
export declare const CTRL_C_HINT = "Ctrl+C again to send it to the program (Esc stops an agent)";
|
|
5
|
+
/** What the title shows when a Ctrl+C was taken as "copy". */
|
|
6
|
+
export declare const CTRL_C_COPY_HINT = "Copied: Ctrl+C after selecting text is not sent to the program (Esc stops an agent)";
|
|
7
|
+
export type CtrlCAction = 'send' | 'hold' | 'copy';
|
|
8
|
+
export declare class CtrlCDecider {
|
|
9
|
+
private readonly confirm;
|
|
10
|
+
private armedAt;
|
|
11
|
+
private selected;
|
|
12
|
+
constructor(confirm?: boolean);
|
|
13
|
+
/** Text was selected in the program: Ctrl+C is "copy" from now on. */
|
|
14
|
+
selection(): void;
|
|
15
|
+
/** Any other key or a click: Ctrl+C is an interrupt again. */
|
|
16
|
+
otherInput(): void;
|
|
17
|
+
/** What to do with a lone Ctrl+C pressed at `nowMs`. */
|
|
18
|
+
ctrlC(nowMs: number): CtrlCAction;
|
|
19
|
+
}
|
|
20
|
+
/** What typed input holds: the strongest mouse report (a drag, else a click), and whether anything else is in it. */
|
|
21
|
+
export declare function classifyInput(text: string): {
|
|
22
|
+
readonly mouse: 'drag' | 'click' | null;
|
|
23
|
+
readonly keys: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** True when program output asks the terminal to write the clipboard (OSC 52). */
|
|
26
|
+
export declare function hasClipboardWrite(text: string): boolean;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// When a Ctrl+C is "copy" and when it is an interrupt, shared by the terminal client and the web terminal. Agents turn
|
|
2
|
+
// mouse tracking on, so selecting text happens inside the program (a mouse drag, or an OSC 52 clipboard write) and the
|
|
3
|
+
// terminal never sees a selection. After that every Ctrl+C is "copy" and is not sent, until another key or a click.
|
|
4
|
+
// Outside that, a lone Ctrl+C is held and a second one soon after is sent. Pure: no node imports.
|
|
5
|
+
/** A second Ctrl+C within this long sends it. */
|
|
6
|
+
export const CTRL_C_CONFIRM_MS = 1500;
|
|
7
|
+
/** What the title shows while a Ctrl+C is held back. */
|
|
8
|
+
export const CTRL_C_HINT = 'Ctrl+C again to send it to the program (Esc stops an agent)';
|
|
9
|
+
/** What the title shows when a Ctrl+C was taken as "copy". */
|
|
10
|
+
export const CTRL_C_COPY_HINT = 'Copied: Ctrl+C after selecting text is not sent to the program (Esc stops an agent)';
|
|
11
|
+
export class CtrlCDecider {
|
|
12
|
+
confirm;
|
|
13
|
+
armedAt = Number.NEGATIVE_INFINITY;
|
|
14
|
+
selected = false;
|
|
15
|
+
constructor(confirm = true) {
|
|
16
|
+
this.confirm = confirm;
|
|
17
|
+
}
|
|
18
|
+
/** Text was selected in the program: Ctrl+C is "copy" from now on. */
|
|
19
|
+
selection() {
|
|
20
|
+
this.selected = true;
|
|
21
|
+
this.armedAt = Number.NEGATIVE_INFINITY;
|
|
22
|
+
}
|
|
23
|
+
/** Any other key or a click: Ctrl+C is an interrupt again. */
|
|
24
|
+
otherInput() {
|
|
25
|
+
this.selected = false;
|
|
26
|
+
}
|
|
27
|
+
/** What to do with a lone Ctrl+C pressed at `nowMs`. */
|
|
28
|
+
ctrlC(nowMs) {
|
|
29
|
+
if (this.selected)
|
|
30
|
+
return 'copy';
|
|
31
|
+
if (!this.confirm)
|
|
32
|
+
return 'send';
|
|
33
|
+
if (nowMs - this.armedAt <= CTRL_C_CONFIRM_MS) {
|
|
34
|
+
this.armedAt = Number.NEGATIVE_INFINITY;
|
|
35
|
+
return 'send';
|
|
36
|
+
}
|
|
37
|
+
this.armedAt = nowMs;
|
|
38
|
+
return 'hold';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// SGR reports (ESC [ < b ; x ; y M|m) and X10 reports (ESC [ M and three characters); bounded, no backtracking.
|
|
42
|
+
const MOUSE_REPORT = /\x1b\[<(\d{1,4});\d{1,5};\d{1,5}([Mm])|\x1b\[M([\s\S])[\s\S]{2}/g;
|
|
43
|
+
/** What typed input holds: the strongest mouse report (a drag, else a click), and whether anything else is in it. */
|
|
44
|
+
export function classifyInput(text) {
|
|
45
|
+
let drag = false;
|
|
46
|
+
let click = false;
|
|
47
|
+
for (const m of text.matchAll(MOUSE_REPORT)) {
|
|
48
|
+
const sgr = m[1] !== undefined;
|
|
49
|
+
const b = sgr ? Number(m[1]) : (m[3] ?? ' ').charCodeAt(0) - 32;
|
|
50
|
+
if (b & 64)
|
|
51
|
+
continue;
|
|
52
|
+
if (b & 32) {
|
|
53
|
+
if ((b & 3) !== 3)
|
|
54
|
+
drag = true;
|
|
55
|
+
}
|
|
56
|
+
else if (sgr ? m[2] === 'M' : (b & 3) !== 3)
|
|
57
|
+
click = true;
|
|
58
|
+
}
|
|
59
|
+
return { mouse: drag ? 'drag' : click ? 'click' : null, keys: text.replace(MOUSE_REPORT, '').length > 0 };
|
|
60
|
+
}
|
|
61
|
+
/** True when program output asks the terminal to write the clipboard (OSC 52). */
|
|
62
|
+
export function hasClipboardWrite(text) {
|
|
63
|
+
return /\x1b\]0{0,2}52;/.test(text) || /\u009d0{0,2}52;/.test(text);
|
|
64
|
+
}
|
package/dist/shared/web/index.js
CHANGED
package/dist/web/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Sandbox</title>
|
|
7
7
|
<link rel="icon" href="data:," />
|
|
8
8
|
<link rel="stylesheet" href="/main-XT7T6ODM.css" />
|
|
9
|
-
<script type="module" src="/main-
|
|
9
|
+
<script type="module" src="/main-7NY4X6YU.js"></script>
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|