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.
Files changed (125) hide show
  1. package/dist/app/features/accounts/accounts.def.d.ts +1 -0
  2. package/dist/app/features/accounts/accounts.def.js +12 -3
  3. package/dist/app/features/accounts/accounts.run.d.ts +1 -0
  4. package/dist/app/features/accounts/accounts.run.js +20 -11
  5. package/dist/app/features/defs.d.ts +1 -0
  6. package/dist/app/features/settings/settings-file.d.ts +7 -0
  7. package/dist/app/features/settings/settings-file.js +19 -2
  8. package/dist/app/features/settings/settings-rows.js +9 -1
  9. package/dist/app/features/storage/storage.run.js +1 -1
  10. package/dist/bin/agent-sandbox.js +7 -1
  11. package/dist/bin/chunk-BQ23GBLC.js +78790 -0
  12. package/dist/bin/chunk-F7IMME5G.js +10432 -0
  13. package/dist/bin/chunk-LQHEUN2A.js +7 -0
  14. package/dist/bin/claude-host.js +10 -4
  15. package/dist/bin/claude-sbx.js +8 -2
  16. package/dist/bin/codex-sbx.js +8 -2
  17. package/dist/bin/gemini-sbx.js +8 -2
  18. package/dist/bin/sandbox.js +7 -2
  19. package/dist/bin/sbx-hide.js +8 -2
  20. package/dist/bin/sbx-show.js +8 -2
  21. package/dist/cli/commands/launch/launch.js +1 -1
  22. package/dist/cli/commands/launch/prepare.js +24 -7
  23. package/dist/cli/commands/launch/running.js +4 -0
  24. package/dist/cli/commands/repair/checks/contained.js +1 -1
  25. package/dist/cli/commands/storage/offer.js +1 -1
  26. package/dist/cli/menu/live.d.ts +29 -0
  27. package/dist/cli/menu/live.js +92 -21
  28. package/dist/cli/menu/menu.d.ts +2 -1
  29. package/dist/cli/menu/menu.js +16 -2
  30. package/dist/cli/menu/pages/list-page.d.ts +5 -0
  31. package/dist/cli/menu/pages/list-page.js +19 -0
  32. package/dist/cli/menu/shell.d.ts +2 -0
  33. package/dist/cli/menu/shell.js +1 -1
  34. package/dist/cli/services/accounts-service.d.ts +7 -0
  35. package/dist/cli/services/accounts-service.js +20 -0
  36. package/dist/cli/tui/app.d.ts +4 -0
  37. package/dist/cli/tui/app.js +5 -0
  38. package/dist/cli/tui/terminal.d.ts +6 -0
  39. package/dist/cli/tui/terminal.js +8 -1
  40. package/dist/cli/validate.js +8 -4
  41. package/dist/core/package-info.js +1 -1
  42. package/dist/guest/daemon/sessions.d.ts +7 -0
  43. package/dist/guest/daemon/sessions.js +23 -2
  44. package/dist/guest/entries/daemon.js +1 -0
  45. package/dist/guest/sessions/pty-host.d.ts +10 -0
  46. package/dist/guest/sessions/pty-host.js +48 -3
  47. package/dist/host/channel/channel.js +1 -1
  48. package/dist/host/install/install-record.d.ts +1 -0
  49. package/dist/host/install/install-record.js +2 -0
  50. package/dist/host/sandbox/contained-deps.d.ts +2 -1
  51. package/dist/host/sandbox/contained-deps.js +6 -4
  52. package/dist/host/sandbox/platform.d.ts +2 -0
  53. package/dist/host/sandbox/platform.js +19 -4
  54. package/dist/host/sharing/lifecycle.js +9 -1
  55. package/dist/host/storage/contained/contained-storage.js +13 -0
  56. package/dist/host/storage/contained/disk-port.d.ts +5 -0
  57. package/dist/host/storage/contained/helper-port.d.ts +72 -0
  58. package/dist/host/storage/contained/helper-port.js +162 -0
  59. package/dist/host/storage/contained/index.d.ts +1 -0
  60. package/dist/host/storage/contained/index.js +1 -0
  61. package/dist/host/storage/contained/state.d.ts +3 -2
  62. package/dist/host/storage/contained/state.js +5 -2
  63. package/dist/host/web/bridge.d.ts +5 -0
  64. package/dist/host/web/bridge.js +11 -1
  65. package/dist/host/web/server.d.ts +2 -0
  66. package/dist/host/web/server.js +11 -1
  67. package/dist/runtime/agent.js +9 -0
  68. package/dist/runtime/daemon.js +57 -3
  69. package/dist/runtime/db.js +9 -0
  70. package/dist/runtime/main.js +195 -12
  71. package/dist/runtime/netproxy.js +9 -0
  72. package/dist/runtime/view.js +195 -12
  73. package/dist/selftest/channel.test.js +72 -1
  74. package/dist/selftest/contained.test.js +1 -37
  75. package/dist/selftest/hcs.test.js +5 -2
  76. package/dist/selftest/index.js +2 -0
  77. package/dist/selftest/install.test.js +11 -0
  78. package/dist/selftest/layout.test.js +10 -1
  79. package/dist/selftest/live-daemon.test.js +51 -1
  80. package/dist/selftest/live-terminal.test.js +112 -4
  81. package/dist/selftest/menu-perf.test.js +198 -12
  82. package/dist/selftest/menu.test.js +4 -0
  83. package/dist/selftest/storage-helper.test.d.ts +2 -0
  84. package/dist/selftest/storage-helper.test.js +493 -0
  85. package/dist/selftest/support/vhdx.d.ts +14 -0
  86. package/dist/selftest/support/vhdx.js +72 -0
  87. package/dist/selftest/web.test.js +14 -2
  88. package/dist/shared/channel/run-dir.d.ts +6 -0
  89. package/dist/shared/channel/run-dir.js +9 -0
  90. package/dist/shared/pty/index.d.ts +3 -1
  91. package/dist/shared/pty/index.js +3 -1
  92. package/dist/shared/pty/key-gate.d.ts +24 -0
  93. package/dist/shared/pty/key-gate.js +45 -0
  94. package/dist/shared/pty/mouse-modes.d.ts +27 -0
  95. package/dist/shared/pty/mouse-modes.js +155 -0
  96. package/dist/shared/pty/terminal.d.ts +0 -5
  97. package/dist/shared/pty/terminal.js +51 -20
  98. package/dist/sync/watcher.js +1 -1
  99. package/dist/web/index.html +1 -1
  100. package/dist/web/{main-Z2WYAE2Z.js → main-6XNBTJLT.js} +34 -34
  101. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  102. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  103. package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
  104. package/native/csharp/StorageHelper/Client.cs +297 -0
  105. package/native/csharp/StorageHelper/Log.cs +177 -0
  106. package/native/csharp/StorageHelper/MountRecords.cs +160 -0
  107. package/native/csharp/StorageHelper/Operations.cs +596 -0
  108. package/native/csharp/StorageHelper/Pins.cs +270 -0
  109. package/native/csharp/StorageHelper/Privileges.cs +78 -0
  110. package/native/csharp/StorageHelper/Program.cs +349 -0
  111. package/native/csharp/StorageHelper/Protocol.cs +107 -0
  112. package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
  113. package/native/csharp/StorageHelper/Rules.cs +156 -0
  114. package/native/csharp/StorageHelper/Server.cs +656 -0
  115. package/native/csharp/StorageHelper/Settings.cs +297 -0
  116. package/native/csharp/StorageHelper/StorageService.cs +53 -0
  117. package/native/csharp/StorageHelper/UserSettings.cs +206 -0
  118. package/native/csharp/StorageHelper/Vhdx.cs +230 -0
  119. package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
  120. package/native/csharp/StorageHelper/Volumes.cs +379 -0
  121. package/native/csharp/StorageHelper/Watchdog.cs +128 -0
  122. package/native/csharp/StorageHelper/Win32.cs +250 -0
  123. package/native/scripts/set-storage-helper.ps1 +203 -0
  124. package/package.json +1 -1
  125. package/sandbox/csharp/PtyHost.cs +236 -1
@@ -44,6 +44,11 @@ export class RunDir {
44
44
  projectsDir;
45
45
  /** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
46
46
  auditDir;
47
+ /**
48
+ * Each running session's process tree, recorded by its ptyhost (guest only: the daemon ends what a ptyhost that was
49
+ * itself ended left running).
50
+ */
51
+ treesDir;
47
52
  /** One-shot secrets the host hands over (see secrets.ts). Next to run, not in it: shared-use users can change run. */
48
53
  secretsDir;
49
54
  constructor(dataDir) {
@@ -57,6 +62,7 @@ export class RunDir {
57
62
  this.sessionsDir = path.join(this.root, 'sessions');
58
63
  this.projectsDir = path.join(this.root, 'projects');
59
64
  this.auditDir = path.join(this.root, 'audit');
65
+ this.treesDir = path.join(this.root, 'trees');
60
66
  this.secretsDir = path.join(dataDir, 'secrets');
61
67
  }
62
68
  requestFile(id) {
@@ -68,6 +74,9 @@ export class RunDir {
68
74
  sessionFile(id) {
69
75
  return path.join(this.sessionsDir, safeId(id) + '.json');
70
76
  }
77
+ treeFile(id) {
78
+ return path.join(this.treesDir, safeId(id) + '.json');
79
+ }
71
80
  projectFile(name) {
72
81
  return path.join(this.projectsDir, safeId(name) + '.json');
73
82
  }
@@ -1,6 +1,8 @@
1
1
  export { FrameType, HelloFlag, PING_INTERVAL_MS, type Frame, DETACH_KEY, encodeFrame, helloFrame, resizeFrame, MAX_FRAME_BYTES, FrameReader, } from './protocol.js';
2
2
  export { type Endpoint, connect, sendControl, type Viewers, queryViewers } from './client.js';
3
- export { type ClientResult, MOUSE_OFF, fixWindowsKeys, runTerminalClient } from './terminal.js';
3
+ export { type ClientResult, MOUSE_OFF, runTerminalClient } from './terminal.js';
4
+ export { fixWindowsKeys, KeyGate } from './key-gate.js';
5
+ export { MouseModes } from './mouse-modes.js';
4
6
  export { JOIN_MS, InputJoiner } from './input-joiner.js';
5
7
  export { CTRL_C_CONFIRM_MS, CTRL_C_HINT, CtrlCGuard } from './ctrl-c.js';
6
8
  export { type VtInput, enableVtInput } from './console-input.js';
@@ -1,6 +1,8 @@
1
1
  export { FrameType, HelloFlag, PING_INTERVAL_MS, DETACH_KEY, encodeFrame, helloFrame, resizeFrame, MAX_FRAME_BYTES, FrameReader, } from './protocol.js';
2
2
  export { connect, sendControl, queryViewers } from './client.js';
3
- export { MOUSE_OFF, fixWindowsKeys, runTerminalClient } from './terminal.js';
3
+ export { MOUSE_OFF, runTerminalClient } from './terminal.js';
4
+ export { fixWindowsKeys, KeyGate } from './key-gate.js';
5
+ export { MouseModes } from './mouse-modes.js';
4
6
  export { JOIN_MS, InputJoiner } from './input-joiner.js';
5
7
  export { CTRL_C_CONFIRM_MS, CTRL_C_HINT, CtrlCGuard } from './ctrl-c.js';
6
8
  export { enableVtInput } from './console-input.js';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Node on Windows reads keys through the console API, where Backspace is ^H and Ctrl+Backspace is DEL: the reverse of
3
+ * what terminals send. ConPTY reads ^H as Ctrl+Backspace (delete a word), so swap them back.
4
+ */
5
+ export declare function fixWindowsKeys(chunk: Buffer): Buffer;
6
+ /**
7
+ * Typed input before the console's mode is settled. Once ENABLE_VIRTUAL_TERMINAL_INPUT is on (enableVtInput), the
8
+ * console hands over the terminal's own bytes (DEL for Backspace, ^H for Ctrl+Backspace) and nothing may be swapped;
9
+ * only when it could not be turned on are keys read as console key records and swapped (fixWindowsKeys). Turning it
10
+ * on takes a moment (a PowerShell helper), and input read meanwhile used to be swapped although it was the terminal's
11
+ * own: a Backspace pressed then reached the agent as ^H and deleted a word. So input waits here until the mode is
12
+ * known.
13
+ */
14
+ export declare class KeyGate {
15
+ private pending;
16
+ private decided;
17
+ private swap;
18
+ /** windowsConsole: input comes from a Windows console in raw mode (its mode is decided later); else it goes as is. */
19
+ constructor(windowsConsole: boolean);
20
+ /** The bytes to send now (empty while the mode is not known yet). */
21
+ push(data: Buffer): Buffer;
22
+ /** The console's mode is known: what was held, ready to send. Later calls only change how new input is read. */
23
+ decide(vtInput: boolean): Buffer;
24
+ }
@@ -0,0 +1,45 @@
1
+ // Windows: holds what you type until the client knows how the console hands keys over, so Backspace stays Backspace.
2
+ /**
3
+ * Node on Windows reads keys through the console API, where Backspace is ^H and Ctrl+Backspace is DEL: the reverse of
4
+ * what terminals send. ConPTY reads ^H as Ctrl+Backspace (delete a word), so swap them back.
5
+ */
6
+ export function fixWindowsKeys(chunk) {
7
+ if (!chunk.includes(0x08) && !chunk.includes(0x7f))
8
+ return chunk;
9
+ return Buffer.from(chunk.map((b) => (b === 0x08 ? 0x7f : b === 0x7f ? 0x08 : b)));
10
+ }
11
+ /**
12
+ * Typed input before the console's mode is settled. Once ENABLE_VIRTUAL_TERMINAL_INPUT is on (enableVtInput), the
13
+ * console hands over the terminal's own bytes (DEL for Backspace, ^H for Ctrl+Backspace) and nothing may be swapped;
14
+ * only when it could not be turned on are keys read as console key records and swapped (fixWindowsKeys). Turning it
15
+ * on takes a moment (a PowerShell helper), and input read meanwhile used to be swapped although it was the terminal's
16
+ * own: a Backspace pressed then reached the agent as ^H and deleted a word. So input waits here until the mode is
17
+ * known.
18
+ */
19
+ export class KeyGate {
20
+ pending = [];
21
+ decided;
22
+ swap = false;
23
+ /** windowsConsole: input comes from a Windows console in raw mode (its mode is decided later); else it goes as is. */
24
+ constructor(windowsConsole) {
25
+ this.decided = !windowsConsole;
26
+ }
27
+ /** The bytes to send now (empty while the mode is not known yet). */
28
+ push(data) {
29
+ if (!data.length)
30
+ return data;
31
+ if (!this.decided) {
32
+ this.pending.push(data);
33
+ return Buffer.alloc(0);
34
+ }
35
+ return this.swap ? fixWindowsKeys(data) : data;
36
+ }
37
+ /** The console's mode is known: what was held, ready to send. Later calls only change how new input is read. */
38
+ decide(vtInput) {
39
+ this.swap = !vtInput;
40
+ this.decided = true;
41
+ const held = Buffer.concat(this.pending);
42
+ this.pending = [];
43
+ return this.swap ? fixWindowsKeys(held) : held;
44
+ }
45
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Follows the private modes a program turns on and off in its output (DECSET / DECRST, kept across chunks: mouse
3
+ * tracking 9, 1000, 1002, 1003, the encodings 1005 and 1015, bracketed paste 2004 ...) and drops the mouse reports it
4
+ * did not ask for: none without tracking, motion only with 1002 (while a button is held: a drag) or 1003 (every move).
5
+ * Your terminal reports what the program's modes say; what is dropped is what was still on its way when the program
6
+ * turned tracking off, exited or restarted, and would reach it as text.
7
+ */
8
+ export declare class MouseModes {
9
+ private readonly on;
10
+ /** 0 text, 1 after ESC, 2 after ESC[, 3 in ESC[? parameters, 4 after ESC[! */
11
+ private state;
12
+ private params;
13
+ /** Reads the program's output (after OutputFilter): the mode changes in it. */
14
+ watch(output: Buffer): void;
15
+ /** The program is gone (exited or restarting): its modes went with it. */
16
+ reset(): void;
17
+ /** True while the program tracks the mouse in any way. */
18
+ tracking(): boolean;
19
+ /** True while the program has a mode on (2004: bracketed paste). */
20
+ has(mode: number): boolean;
21
+ /** The mode numbers on now (for the report and tests). */
22
+ modes(): number[];
23
+ /** `input` without the mouse reports the program has not asked for. Sequences must be whole (InputJoiner). */
24
+ filter(input: Buffer): Buffer;
25
+ private allows;
26
+ private apply;
27
+ }
@@ -0,0 +1,155 @@
1
+ // The mouse tracking a program asked for, read from its output, and the mouse reports it may receive.
2
+ /** X10 compatibility (press only), normal (press/release/wheel), button-event (+ drag), any-event (+ every motion). */
3
+ const TRACKING = [9, 1000, 1002, 1003];
4
+ /** Longest DECSET / DECRST parameter list followed; anything longer is not a mode change. */
5
+ const MAX_PARAMS = 64;
6
+ /** Button code bits of a mouse report: motion (drag or move), and "no button" in the low two bits. */
7
+ const MOTION = 32;
8
+ const NO_BUTTON = 3;
9
+ /**
10
+ * Follows the private modes a program turns on and off in its output (DECSET / DECRST, kept across chunks: mouse
11
+ * tracking 9, 1000, 1002, 1003, the encodings 1005 and 1015, bracketed paste 2004 ...) and drops the mouse reports it
12
+ * did not ask for: none without tracking, motion only with 1002 (while a button is held: a drag) or 1003 (every move).
13
+ * Your terminal reports what the program's modes say; what is dropped is what was still on its way when the program
14
+ * turned tracking off, exited or restarted, and would reach it as text.
15
+ */
16
+ export class MouseModes {
17
+ on = new Set();
18
+ /** 0 text, 1 after ESC, 2 after ESC[, 3 in ESC[? parameters, 4 after ESC[! */
19
+ state = 0;
20
+ params = '';
21
+ /** Reads the program's output (after OutputFilter): the mode changes in it. */
22
+ watch(output) {
23
+ for (const b of output) {
24
+ if (b === 0x1b) {
25
+ this.state = 1;
26
+ continue;
27
+ }
28
+ if (this.state === 1) {
29
+ // RIS (ESC c), a full reset, turns every mode off.
30
+ if (b === 0x63)
31
+ this.on.clear();
32
+ this.state = b === 0x5b ? 2 : 0;
33
+ }
34
+ else if (this.state === 2) {
35
+ // DECSTR (ESC[!p), a soft reset, turns mouse tracking off too.
36
+ this.state = b === 0x3f ? 3 : b === 0x21 ? 4 : 0;
37
+ this.params = '';
38
+ }
39
+ else if (this.state === 4) {
40
+ if (b === 0x70)
41
+ this.on.clear();
42
+ this.state = 0;
43
+ }
44
+ else if (this.state === 3) {
45
+ if ((b >= 0x30 && b <= 0x39) || b === 0x3b) {
46
+ this.params += String.fromCharCode(b);
47
+ if (this.params.length > MAX_PARAMS)
48
+ this.state = 0;
49
+ }
50
+ else {
51
+ if (b === 0x68 || b === 0x6c)
52
+ this.apply(this.params, b === 0x68);
53
+ this.state = 0;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ /** The program is gone (exited or restarting): its modes went with it. */
59
+ reset() {
60
+ this.on.clear();
61
+ this.state = 0;
62
+ this.params = '';
63
+ }
64
+ /** True while the program tracks the mouse in any way. */
65
+ tracking() {
66
+ return TRACKING.some((m) => this.on.has(m));
67
+ }
68
+ /** True while the program has a mode on (2004: bracketed paste). */
69
+ has(mode) {
70
+ return this.on.has(mode);
71
+ }
72
+ /** The mode numbers on now (for the report and tests). */
73
+ modes() {
74
+ return [...this.on].sort((a, b) => a - b);
75
+ }
76
+ /** `input` without the mouse reports the program has not asked for. Sequences must be whole (InputJoiner). */
77
+ filter(input) {
78
+ if (!input.includes(0x1b))
79
+ return input;
80
+ const parts = [];
81
+ let from = 0;
82
+ let i = input.indexOf(0x1b);
83
+ while (i !== -1) {
84
+ const report = mouseReport(input, i, this.on.has(1005), this.on.has(1015));
85
+ if (report && !this.allows(report.button)) {
86
+ parts.push(input.subarray(from, i));
87
+ from = i + report.length;
88
+ i = input.indexOf(0x1b, from);
89
+ }
90
+ else
91
+ i = input.indexOf(0x1b, i + 1);
92
+ }
93
+ if (from === 0)
94
+ return input;
95
+ parts.push(input.subarray(from));
96
+ return Buffer.concat(parts);
97
+ }
98
+ allows(button) {
99
+ if (!this.tracking())
100
+ return false;
101
+ if ((button & MOTION) === 0)
102
+ return true;
103
+ if (this.on.has(1003))
104
+ return true;
105
+ return this.on.has(1002) && (button & NO_BUTTON) !== NO_BUTTON;
106
+ }
107
+ apply(params, set) {
108
+ for (const p of params.split(';')) {
109
+ const mode = Number(p);
110
+ if (!p || !Number.isSafeInteger(mode))
111
+ continue;
112
+ if (set)
113
+ this.on.add(mode);
114
+ else
115
+ this.on.delete(mode);
116
+ }
117
+ }
118
+ }
119
+ /**
120
+ * The mouse report starting at `at`: SGR (ESC[<b;x;yM or m, 1006), urxvt (ESC[b;x;yM, 1015, b offset by 32; only while
121
+ * 1015 is on) or X10
122
+ * (ESC[M and three bytes; not with UTF-8 coordinates, 1005, where its length cannot be told). Null when there is none.
123
+ */
124
+ function mouseReport(data, at, utf8, urxvt) {
125
+ if (data[at + 1] !== 0x5b)
126
+ return null;
127
+ const sgr = data[at + 2] === 0x3c;
128
+ if (sgr || (urxvt && data[at + 2] >= 0x30 && data[at + 2] <= 0x39)) {
129
+ let i = at + (sgr ? 3 : 2);
130
+ let digits = '';
131
+ let fields = 1;
132
+ for (; i < data.length && i - at < 32; i++) {
133
+ const b = data[i];
134
+ if (b >= 0x30 && b <= 0x39) {
135
+ if (fields === 1)
136
+ digits += String.fromCharCode(b);
137
+ }
138
+ else if (b === 0x3b)
139
+ fields++;
140
+ else
141
+ break;
142
+ }
143
+ if (i >= data.length || fields !== 3 || !digits)
144
+ return null;
145
+ if (sgr ? data[i] !== 0x4d && data[i] !== 0x6d : data[i] !== 0x4d)
146
+ return null;
147
+ const button = Number(digits) - (sgr ? 0 : 32);
148
+ return button >= 0 ? { button, length: i - at + 1 } : null;
149
+ }
150
+ if (data[at + 2] === 0x4d && !utf8 && data.length - at >= 6) {
151
+ const button = data[at + 3] - 32;
152
+ return button >= 0 ? { button, length: 6 } : null;
153
+ }
154
+ return null;
155
+ }
@@ -13,11 +13,6 @@ export type ClientResult = {
13
13
  * urxvt).
14
14
  */
15
15
  export declare const MOUSE_OFF = "\u001B[?1000l\u001B[?1002l\u001B[?1003l\u001B[?1005l\u001B[?1006l\u001B[?1015l";
16
- /**
17
- * Node on Windows reads keys through the console API, where Backspace is ^H and Ctrl+Backspace is DEL: the reverse of
18
- * what terminals send. ConPTY reads ^H as Ctrl+Backspace (delete a word), so swap them back.
19
- */
20
- export declare function fixWindowsKeys(chunk: Buffer): Buffer;
21
16
  /**
22
17
  * Bridges this process's terminal to the remote program until it exits, the connection drops, or the user presses
23
18
  * the detach key. Restores the terminal before resolving.
@@ -3,20 +3,37 @@ import { OutputFilter } from './output-filter.js';
3
3
  import { enableVtInput } from './console-input.js';
4
4
  import { InputJoiner, JOIN_MS } from './input-joiner.js';
5
5
  import { CTRL_C_CONFIRM_MS, CtrlCGuard } from './ctrl-c.js';
6
+ import { KeyGate } from './key-gate.js';
7
+ import { MouseModes } from './mouse-modes.js';
6
8
  /**
7
9
  * Turns off every mouse tracking mode a program may have turned on in your terminal (normal, button, any, UTF-8, SGR,
8
10
  * urxvt).
9
11
  */
10
12
  export const MOUSE_OFF = '\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?1015l';
11
- /**
12
- * Node on Windows reads keys through the console API, where Backspace is ^H and Ctrl+Backspace is DEL: the reverse of
13
- * what terminals send. ConPTY reads ^H as Ctrl+Backspace (delete a word), so swap them back.
14
- */
15
- export function fixWindowsKeys(chunk) {
16
- if (!chunk.includes(0x08) && !chunk.includes(0x7f))
17
- return chunk;
18
- return Buffer.from(chunk.map((b) => (b === 0x08 ? 0x7f : b === 0x7f ? 0x08 : b)));
13
+ /** After leaving: mouse reports still on their way are dropped until input is this quiet (at most SETTLE_MAX_MS). */
14
+ const SETTLE_QUIET_MS = 50;
15
+ const SETTLE_MAX_MS = 300;
16
+ /** Waits until `stream` has been quiet for SETTLE_QUIET_MS (or SETTLE_MAX_MS passed), dropping what it reads. */
17
+ function drainInput(stream) {
18
+ return new Promise((done) => {
19
+ let quiet;
20
+ const end = () => {
21
+ clearTimeout(quiet);
22
+ clearTimeout(cap);
23
+ stream.off('data', onData);
24
+ done();
25
+ };
26
+ const onData = () => {
27
+ clearTimeout(quiet);
28
+ quiet = setTimeout(end, SETTLE_QUIET_MS);
29
+ };
30
+ const cap = setTimeout(end, SETTLE_MAX_MS);
31
+ stream.on('data', onData);
32
+ onData();
33
+ });
19
34
  }
35
+ /** Longest wait for the console's input mode before typed keys are sent anyway (it normally takes about 2 s). */
36
+ const VT_WAIT_MS = 10000;
20
37
  /**
21
38
  * Bridges this process's terminal to the remote program until it exits, the connection drops, or the user presses
22
39
  * the detach key. Restores the terminal before resolving.
@@ -32,8 +49,8 @@ options = {}) {
32
49
  const [c, r] = stdout.isTTY ? stdout.getWindowSize() : [0, 0];
33
50
  return [c || 120, r || 30];
34
51
  };
35
- // Until the console passes the terminal's input through as it is (enableVtInput), Node turns Backspace into ^H.
36
- let swapKeys = process.platform === 'win32' && stdin.isTTY;
52
+ // Until the console's input mode is known (enableVtInput), what is typed waits: see KeyGate.
53
+ const keys = new KeyGate(process.platform === 'win32' && Boolean(stdin.isTTY));
37
54
  return new Promise((resolve) => {
38
55
  let done = false;
39
56
  const reader = new FrameReader();
@@ -42,7 +59,10 @@ options = {}) {
42
59
  let resizeTimer;
43
60
  // Set once raw mode is on (below); finish() may run before that.
44
61
  // eslint-disable-next-line prefer-const
45
- let vtInput;
62
+ let vtInput, vtTimer;
63
+ // The agent's mouse modes, read from its output; only known when it is passed through (conpty.dll, see below).
64
+ const mouse = new MouseModes();
65
+ let passthrough = false;
46
66
  const finish = (result) => {
47
67
  if (done)
48
68
  return;
@@ -54,11 +74,16 @@ options = {}) {
54
74
  clearInterval(sizeWatch);
55
75
  clearTimeout(joinTimer);
56
76
  clearTimeout(hintTimer);
77
+ clearTimeout(vtTimer);
57
78
  socket.destroy();
58
- // The agent's mouse tracking ends with it: your terminal's wheel scrolls again (and clicks select text).
59
- stdout.write('\x1b[0m\x1b[?25h' + MOUSE_OFF + '\r\n');
79
+ // The agent's mouse tracking ends with it: your terminal's wheel scrolls again (and clicks select text). Reports
80
+ // the terminal sent before it read that are dropped here, or they would reach your shell as text.
81
+ const tracked = mouse.tracking();
82
+ const reset = '\x1b[0m\x1b[?25h' + MOUSE_OFF + (mouse.has(2004) ? '\x1b[?2004l' : '') + '\r\n';
83
+ const flushed = new Promise((written) => stdout.write(reset, () => written()));
84
+ const settled = tracked ? flushed.then(() => drainInput(stdin)) : Promise.resolve();
60
85
  // Wait for a console-mode change still in progress, then give the console back in its normal mode.
61
- void (vtInput?.stop() ?? Promise.resolve()).then(() => {
86
+ void Promise.all([settled, vtInput?.stop()]).then(() => {
62
87
  if (stdin.isTTY)
63
88
  stdin.setRawMode(false);
64
89
  stdin.pause();
@@ -67,10 +92,12 @@ options = {}) {
67
92
  };
68
93
  const joiner = new InputJoiner();
69
94
  let joinTimer;
70
- const sendInput = (data) => {
95
+ const write = (data) => {
71
96
  if (data.length && !done)
72
- socket.write(encodeFrame(FrameType.Input, swapKeys ? fixWindowsKeys(data) : data));
97
+ socket.write(encodeFrame(FrameType.Input, data));
73
98
  };
99
+ // Mouse reports the agent has not asked for (motion without 1002/1003 ...) are dropped: see MouseModes.
100
+ const sendInput = (data) => write(keys.push(passthrough ? mouse.filter(data) : data));
74
101
  const guard = options.confirmCtrlC ? new CtrlCGuard() : null;
75
102
  let hintTimer;
76
103
  const onInput = (chunk) => {
@@ -99,7 +126,6 @@ options = {}) {
99
126
  // repaints racing each other left the screen garbled. Wait until the size settles, clear what this terminal
100
127
  // reflowed on its own, then let ptyhost repaint everything once at the new size. With conpty.dll (passthrough)
101
128
  // nothing is repainted: this terminal's own reflow and scrollback stay, and the program redraws what it needs.
102
- let passthrough = false;
103
129
  const sendSize = () => {
104
130
  const [c, r] = size();
105
131
  if (c === sent[0] && r === sent[1])
@@ -134,13 +160,16 @@ options = {}) {
134
160
  for (const frame of frames) {
135
161
  if (frame.type === FrameType.Output) {
136
162
  const out = filter.push(frame.payload);
163
+ mouse.watch(out);
137
164
  guard?.watchOutput(out.toString('latin1'));
138
165
  stdout.write(out);
139
166
  }
140
167
  else if (frame.type === FrameType.Console)
141
168
  passthrough = frame.payload.toString('latin1') === 'conpty.dll';
142
- else if (frame.type === FrameType.Restarting)
169
+ else if (frame.type === FrameType.Restarting) {
170
+ mouse.reset();
143
171
  stdout.write(MOUSE_OFF + '\x1b[2J\x1b[3J\x1b[H');
172
+ }
144
173
  else if (frame.type === FrameType.Exit)
145
174
  finish({ reason: 'exit', exitCode: frame.payload.length >= 4 ? frame.payload.readInt32LE(0) : 1 });
146
175
  else if (frame.type === FrameType.Error)
@@ -163,9 +192,11 @@ options = {}) {
163
192
  // After setRawMode (which sets the console mode): mouse reports, focus and keys then arrive as the terminal sent
164
193
  // them.
165
194
  vtInput = enableVtInput();
195
+ // A helper that hangs must not hold the keyboard: after VT_WAIT_MS keys go as console keys until it is done.
196
+ vtTimer = setTimeout(() => write(keys.decide(false)), VT_WAIT_MS);
166
197
  void vtInput.ready.then((on) => {
167
- if (on)
168
- swapKeys = false;
198
+ clearTimeout(vtTimer);
199
+ write(keys.decide(on));
169
200
  });
170
201
  stdin.on('data', onInput);
171
202
  stdin.resume();
@@ -137,7 +137,7 @@ export class Watcher {
137
137
  /** Contained storage of this data set (only when a mounted image is on record), else null. */
138
138
  contained() {
139
139
  try {
140
- const storage = new ContainedStorage(realContainedDeps(this.storageRoot, 64), setNameOf(this.dataPath));
140
+ const storage = new ContainedStorage(realContainedDeps(this.storageRoot, 64, Boolean(this.resume.config)), setNameOf(this.dataPath));
141
141
  return storage.isRecorded() ? storage : null;
142
142
  }
143
143
  catch {
@@ -6,7 +6,7 @@
6
6
  <title>Sandbox</title>
7
7
  <link rel="icon" href="data:," />
8
8
  <link rel="stylesheet" href="/main-UAQPI5XD.css" />
9
- <script type="module" src="/main-Z2WYAE2Z.js"></script>
9
+ <script type="module" src="/main-6XNBTJLT.js"></script>
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>