windows-development-sandbox 1.27.3 → 1.28.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.
Files changed (68) hide show
  1. package/dist/bin/agent-sandbox.js +2 -2
  2. package/dist/bin/{chunk-RU7XRN62.js → chunk-CB44HTTN.js} +196 -75
  3. package/dist/bin/{chunk-FA5VWLP4.js → chunk-UZ5S3EMB.js} +1 -1
  4. package/dist/bin/{chunk-4M5SZZ5P.js → chunk-ZMZI3625.js} +1 -1
  5. package/dist/bin/claude-host.js +2 -2
  6. package/dist/bin/claude-sbx.js +2 -2
  7. package/dist/bin/codex-sbx.js +2 -2
  8. package/dist/bin/gemini-sbx.js +2 -2
  9. package/dist/bin/sandbox.js +2 -2
  10. package/dist/bin/sbx-hide.js +2 -2
  11. package/dist/bin/sbx-show.js +2 -2
  12. package/dist/core/schema/sandbox.js +1 -1
  13. package/dist/guest/daemon/heartbeat.js +1 -0
  14. package/dist/guest/governor/core.d.ts +53 -1
  15. package/dist/guest/governor/core.js +95 -1
  16. package/dist/guest/governor/decide.d.ts +4 -0
  17. package/dist/guest/governor/decide.js +2 -0
  18. package/dist/guest/governor/governor.js +23 -9
  19. package/dist/guest/toolchains/native/unreal.js +22 -3
  20. package/dist/guest/toolchains/runtimes.js +7 -6
  21. package/dist/host/life/crash-report.d.ts +4 -0
  22. package/dist/host/life/crash-report.js +30 -1
  23. package/dist/host/life/last-end.js +1 -0
  24. package/dist/host/tools/windows-terminal.d.ts +4 -1
  25. package/dist/host/tools/windows-terminal.js +25 -9
  26. package/dist/runtime/agent.js +5 -1
  27. package/dist/runtime/daemon.js +183 -70
  28. package/dist/runtime/db.js +5 -1
  29. package/dist/runtime/main.js +242 -56
  30. package/dist/runtime/netproxy.js +5 -1
  31. package/dist/runtime/tool.js +5 -1
  32. package/dist/runtime/view.js +125 -27
  33. package/dist/selftest/agent-paths.test.js +3 -1
  34. package/dist/selftest/channel.test.js +33 -0
  35. package/dist/selftest/heavy-load.test.js +116 -4
  36. package/dist/selftest/live-terminal.test.js +103 -0
  37. package/dist/selftest/personalization.test.js +2 -1
  38. package/dist/selftest/support/engine-golden.js +2 -0
  39. package/dist/shared/channel/schema.d.ts +2 -0
  40. package/dist/shared/channel/schema.js +4 -0
  41. package/dist/shared/memory/index.d.ts +1 -0
  42. package/dist/shared/memory/index.js +1 -0
  43. package/dist/shared/memory/policy.d.ts +12 -0
  44. package/dist/shared/memory/policy.js +4 -0
  45. package/dist/shared/memory/unreal-build.d.ts +2 -0
  46. package/dist/shared/memory/unreal-build.js +21 -0
  47. package/dist/shared/pty/clipboard-watch.d.ts +4 -0
  48. package/dist/shared/pty/clipboard-watch.js +40 -0
  49. package/dist/shared/pty/ctrl-c.d.ts +13 -7
  50. package/dist/shared/pty/ctrl-c.js +35 -19
  51. package/dist/shared/pty/index.d.ts +1 -0
  52. package/dist/shared/pty/index.js +1 -0
  53. package/dist/shared/pty/terminal.d.ts +1 -1
  54. package/dist/shared/pty/terminal.js +16 -13
  55. package/dist/shared/web/ctrl-c.d.ts +26 -0
  56. package/dist/shared/web/ctrl-c.js +64 -0
  57. package/dist/shared/web/index.d.ts +1 -0
  58. package/dist/shared/web/index.js +1 -0
  59. package/dist/web/index.html +1 -1
  60. package/dist/web/{main-NKM7ECSB.js → main-7NY4X6YU.js} +67 -67
  61. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  62. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  63. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  64. package/package.json +1 -1
  65. package/sandbox/csharp/ClipboardWatch.cs +59 -0
  66. package/sandbox/csharp/MemGov.cs +752 -7
  67. package/sandbox/scripts/watch-clipboard.ps1 +8 -0
  68. package/schemas/config.schema.json +1 -1
@@ -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
  });
@@ -1 +1,2 @@
1
1
  export * from './policy.js';
2
+ export * from './unreal-build.js';
@@ -1 +1,2 @@
1
1
  export * from './policy.js';
2
+ export * from './unreal-build.js';
@@ -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,2 @@
1
+ /** BuildConfiguration.xml with at most `jobs` compiles and links at once (pure). */
2
+ export declare function ubtBuildConfiguration(jobs: number): string;
@@ -0,0 +1,21 @@
1
+ // UnrealBuildTool's parallel compiles in the sandbox. Left alone it starts one compiler per core whenever the sandbox
2
+ // looks free (free RAM / 1.5 GB), and each cl.exe with Unreal's precompiled headers commits about 2.6 GB: a build start
3
+ // asks for 50 GB within seconds, faster than the governor can step in, and Windows closes the sandbox.
4
+ // MaxParallelActions caps it before the build starts; UnrealBuildTool runs one build per engine at a time, so the cap
5
+ // is the whole budget.
6
+ /** The XML namespace UnrealBuildTool reads (XmlConfigFile.SchemaNamespaceURI). */
7
+ const NAMESPACE = 'https://www.unrealengine.com/BuildConfiguration';
8
+ /** BuildConfiguration.xml with at most `jobs` compiles and links at once (pure). */
9
+ export function ubtBuildConfiguration(jobs) {
10
+ const max = Math.max(1, Math.floor(jobs));
11
+ return [
12
+ '<?xml version="1.0" encoding="utf-8"?>',
13
+ '<!-- Written by the sandbox at boot: compiles at once from the build budget (run\\budget.json, buildJobs). -->',
14
+ '<Configuration xmlns="' + NAMESPACE + '">',
15
+ ' <BuildConfiguration>',
16
+ ' <MaxParallelActions>' + max + '</MaxParallelActions>',
17
+ ' </BuildConfiguration>',
18
+ '</Configuration>',
19
+ '',
20
+ ].join('\r\n');
21
+ }
@@ -0,0 +1,4 @@
1
+ /** Calls `onChange` for each clipboard change from now on. No-op off Windows. */
2
+ export declare function watchClipboard(onChange: () => void): {
3
+ stop: () => void;
4
+ };
@@ -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
- /** 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)";
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 armedAt;
4
+ private readonly decider;
7
5
  private lastTitle;
8
- /** What to send for one read from the keyboard: a lone Ctrl+C only when another came just before. */
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 lone Ctrl+C is easy to send by accident: it is "copy" everywhere else, and a terminal sends it to the program when
2
- // the selection it was meant for has just been cleared by a redraw (an agent's spinner redraws all the time). With
3
- // attach.confirmCtrlC the terminal client holds a lone Ctrl+C back until it is pressed again within a moment; Esc still
4
- // stops an agent at once. Pure, so it is tested without a terminal.
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)';
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
- armedAt = Number.NEGATIVE_INFINITY;
11
+ decider;
12
12
  lastTitle = '';
13
- /** What to send for one read from the keyboard: a lone Ctrl+C only when another came just before. */
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 !== 1 || chunk[0] !== 0x03)
16
- return { send: chunk, held: false };
17
- if (nowMs - this.armedAt <= CTRL_C_CONFIRM_MS) {
18
- this.armedAt = Number.NEGATIVE_INFINITY;
19
- return { send: chunk, held: false };
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
- this.armedAt = nowMs;
22
- return { send: Buffer.alloc(0), held: true };
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';
@@ -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 it is pressed again (CtrlCGuard, attach.confirmCtrlC). */
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 it is pressed again (CtrlCGuard, attach.confirmCtrlC). */
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 ? new CtrlCGuard() : null;
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
- if (guard) {
109
- const decided = guard.push(chunk, Date.now());
110
- clearTimeout(hintTimer);
111
- if (decided.held) {
112
- stdout.write(guard.hintTitle());
113
- hintTimer = setTimeout(() => stdout.write(guard.restoreTitle()), CTRL_C_CONFIRM_MS);
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?.watchOutput(out.toString('latin1'));
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
+ }
@@ -1 +1,2 @@
1
1
  export * from './protocol.js';
2
+ export * from './ctrl-c.js';
@@ -1 +1,2 @@
1
1
  export * from './protocol.js';
2
+ export * from './ctrl-c.js';
@@ -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-NKM7ECSB.js"></script>
9
+ <script type="module" src="/main-7NY4X6YU.js"></script>
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>