windows-development-sandbox 1.23.1 → 1.25.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 (85) hide show
  1. package/config.example.json +0 -1
  2. package/dist/bin/agent-sandbox.js +2 -2
  3. package/dist/bin/{chunk-USTRSHIE.js → chunk-IZRGTSQ5.js} +158 -131
  4. package/dist/bin/{chunk-QFSJX2KW.js → chunk-KH4MI2U5.js} +1 -1
  5. package/dist/bin/{chunk-7YPEDAJD.js → chunk-SUYYVIDW.js} +1 -1
  6. package/dist/bin/claude-host.js +2 -2
  7. package/dist/bin/claude-sbx.js +2 -2
  8. package/dist/bin/codex-sbx.js +2 -2
  9. package/dist/bin/gemini-sbx.js +2 -2
  10. package/dist/bin/sandbox.js +2 -2
  11. package/dist/bin/sbx-hide.js +2 -2
  12. package/dist/bin/sbx-show.js +2 -2
  13. package/dist/core/config/defaults.js +0 -1
  14. package/dist/core/detect/toolchains.js +0 -6
  15. package/dist/core/schema/config.d.ts +1 -2
  16. package/dist/core/schema/toolchains.d.ts +1 -2
  17. package/dist/core/schema/toolchains.js +3 -6
  18. package/dist/guest/agents/agent-files.d.ts +11 -7
  19. package/dist/guest/agents/agent-files.js +37 -11
  20. package/dist/guest/agents/local-state.d.ts +2 -1
  21. package/dist/guest/agents/local-state.js +2 -1
  22. package/dist/guest/boot/boot-console.d.ts +28 -0
  23. package/dist/guest/boot/boot-console.js +98 -0
  24. package/dist/guest/boot/boot.d.ts +8 -1
  25. package/dist/guest/boot/boot.js +66 -42
  26. package/dist/guest/boot/graph.d.ts +9 -5
  27. package/dist/guest/boot/graph.js +8 -2
  28. package/dist/guest/boot/progress-view.d.ts +116 -0
  29. package/dist/guest/boot/progress-view.js +456 -0
  30. package/dist/guest/boot/runner.d.ts +20 -10
  31. package/dist/guest/boot/runner.js +39 -47
  32. package/dist/guest/runtime/log-line.d.ts +7 -0
  33. package/dist/guest/runtime/log-line.js +18 -0
  34. package/dist/guest/runtime/log.d.ts +17 -4
  35. package/dist/guest/runtime/log.js +40 -17
  36. package/dist/guest/runtime/process.d.ts +9 -0
  37. package/dist/guest/runtime/process.js +69 -8
  38. package/dist/guest/setup/environment.js +9 -4
  39. package/dist/guest/setup/hooks.d.ts +2 -0
  40. package/dist/guest/setup/hooks.js +4 -0
  41. package/dist/guest/setup/path-plan.d.ts +12 -0
  42. package/dist/guest/setup/path-plan.js +18 -0
  43. package/dist/guest/toolchains/native/unreal.js +4 -6
  44. package/dist/guest/toolchains/native/visual-studio.d.ts +14 -14
  45. package/dist/guest/toolchains/native/visual-studio.js +145 -174
  46. package/dist/guest/toolchains/native/vs-persist.d.ts +189 -0
  47. package/dist/guest/toolchains/native/vs-persist.js +256 -0
  48. package/dist/guest/toolchains/native/vs-request.d.ts +6 -22
  49. package/dist/guest/toolchains/native/vs-request.js +16 -46
  50. package/dist/guest/toolchains/plan.js +11 -14
  51. package/dist/host/migrate/migrations/drop-vs-install-files.d.ts +5 -0
  52. package/dist/host/migrate/migrations/drop-vs-install-files.js +34 -0
  53. package/dist/host/migrate/registry.js +7 -1
  54. package/dist/runtime/agent.js +67 -26
  55. package/dist/runtime/daemon.js +958 -716
  56. package/dist/runtime/db.js +67 -26
  57. package/dist/runtime/main.js +1792 -1038
  58. package/dist/runtime/netproxy.js +3 -6
  59. package/dist/runtime/tool.js +44 -24
  60. package/dist/runtime/view.js +4 -7
  61. package/dist/selftest/agent-paths.test.js +17 -1
  62. package/dist/selftest/agent-settings.test.js +36 -5
  63. package/dist/selftest/boot-console.test.d.ts +2 -0
  64. package/dist/selftest/boot-console.test.js +291 -0
  65. package/dist/selftest/boot-runner.test.js +10 -12
  66. package/dist/selftest/guest-boot.test.js +7 -1
  67. package/dist/selftest/index.js +2 -0
  68. package/dist/selftest/layout.test.js +4 -0
  69. package/dist/selftest/migrate.test.js +21 -0
  70. package/dist/selftest/persisted-installs.test.js +169 -32
  71. package/dist/selftest/support/engine-golden.js +2 -0
  72. package/dist/selftest/support/fixtures.d.ts +1 -2
  73. package/dist/shared/channel/schema.d.ts +3 -6
  74. package/dist/shared/pty/terminal.js +2 -1
  75. package/dist/sync/watcher.js +5 -1
  76. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  77. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  78. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  79. package/package.json +1 -1
  80. package/sandbox/scripts/restore-visual-studio.ps1 +78 -0
  81. package/sandbox/scripts/save-visual-studio.ps1 +106 -0
  82. package/schemas/config.schema.json +3 -7
  83. package/schemas/sandbox-config.schema.json +3 -7
  84. package/dist/guest/toolchains/native/autosdk.d.ts +0 -3
  85. package/dist/guest/toolchains/native/autosdk.js +0 -50
@@ -15,7 +15,7 @@ import { linkProfileDirsAsync } from '../agents/profile-links.js';
15
15
  import { restoreClaudeMem } from '../setup/claude-mem.js';
16
16
  import { syncClaudeProfile } from '../setup/claude-profile.js';
17
17
  import { agentEnv, persistEnvironment } from '../setup/environment.js';
18
- import { runHooks } from '../setup/hooks.js';
18
+ import { hasHooks, runHooks } from '../setup/hooks.js';
19
19
  import { applyAllowOnly, proxyEnv } from '../network/allow-only.js';
20
20
  import { applyNetworkPolicy, networkProblems } from '../network/firewall.js';
21
21
  import { writeManagedSettings } from '../setup/managed-policy.js';
@@ -37,12 +37,14 @@ import { now } from '../../shared/channel/run-dir.js';
37
37
  import { writeJsonAtomic } from '../../shared/io/json.js';
38
38
  import { viewSession } from '../sessions/viewer.js';
39
39
  import { saveBootEnv } from './boot-env.js';
40
- import { BOOT_DEFS, STAGES, stageTitle } from './graph.js';
41
- import { BootRunner, bootSummary, seconds } from './runner.js';
40
+ import { BOOT_DEFS, STAGES, stageName, stageTitle } from './graph.js';
41
+ import { BootRunner, duration } from './runner.js';
42
+ import { BootConsole } from './boot-console.js';
43
+ import { stepsDetail } from './progress-view.js';
42
44
  import { installPools } from '../runtime/pools.js';
43
45
  import { primaryProject, saveProjectStatus } from './project-status.js';
44
46
  import { dryRun, isWin, localDir, logsDir, run, toolsDir } from '../runtime/paths.js';
45
- import { log, logPath, runQuiet, warn, withLogContext } from '../runtime/log.js';
47
+ import { log, logPath, notice, record, runQuiet, warn, withLogContext } from '../runtime/log.js';
46
48
  import { mkdirp } from '../runtime/fs.js';
47
49
  import { session, workspace } from '../runtime/session.js';
48
50
  import { applyBootEnvironment, BOOT_SECRETS_VARIABLE, takeSecrets } from '../runtime/secrets.js';
@@ -159,6 +161,12 @@ export class Boot {
159
161
  secrets = Object.freeze({});
160
162
  basePath = '';
161
163
  startedAt = Date.now();
164
+ who = {
165
+ ...(session.vpn ? { vpnServer: session.vpn.server } : {}),
166
+ agent: session.harnessName,
167
+ };
168
+ /** The console while the boot runs: the stage rows, warnings and summary (released before the first session). */
169
+ progress = BootConsole.create(Array.from({ length: STAGES }, (_, i) => ({ stage: i + 1, name: stageName(i + 1, this.who) })), BOOT_DEFS.filter((d) => !d.background), { plain: dryRun });
162
170
  openGate = () => undefined;
163
171
  gate = new Promise((resolve) => {
164
172
  this.openGate = resolve;
@@ -170,14 +178,39 @@ export class Boot {
170
178
  stepsDone = 0;
171
179
  stepsTotal = 0;
172
180
  async run() {
173
- log('Agent sandbox: ' +
174
- session.project +
175
- ' | workspace ' +
176
- workspace +
177
- ' | agent ' +
178
- session.harnessName +
179
- ' | log ' +
180
- logPath);
181
+ const agent = session.harness ? session.harnessName : 'no agent';
182
+ this.progress.start('Agent sandbox · ' + session.project + ' · ' + agent + ' · ' + workspace);
183
+ let runner;
184
+ try {
185
+ runner = await this.foreground();
186
+ }
187
+ catch (e) {
188
+ this.progress.release(null);
189
+ throw e;
190
+ }
191
+ this.progress.release({
192
+ ms: Date.now() - this.startedAt,
193
+ outcomes: runner.results(),
194
+ background: this.backgroundNames(),
195
+ logPath,
196
+ });
197
+ if (dryRun) {
198
+ // Same graph, one task at a time: the background tasks run in place, right after the session.
199
+ this.openGate();
200
+ await runner.run('background');
201
+ return;
202
+ }
203
+ this.openGate();
204
+ const later = runner.run('background');
205
+ // The viewer owns the console now: the background tasks write to the log file only.
206
+ if (session.harness && session.autoSession !== false && process.stdin.isTTY) {
207
+ await viewSession(session.launchId);
208
+ log('\nThis window stays open in ' + workspace + '. Other sessions keep running in their own tabs.');
209
+ }
210
+ await later.catch((e) => warn('Background work failed: ' + errMsg(e)));
211
+ }
212
+ /** The prelude, then every foreground task (the console shows them). */
213
+ async foreground() {
181
214
  if (!fs.statSync(workspace, { throwIfNoEntry: false })?.isDirectory())
182
215
  throw new Error('Workspace folder is missing in the sandbox: ' + workspace);
183
216
  this.basePath = process.env.PATH ?? '';
@@ -189,7 +222,7 @@ export class Boot {
189
222
  prepareGithubFolder(this.secrets.githubToken);
190
223
  installPools();
191
224
  const runner = new BootRunner(BOOT_DEFS, this.tasks(), {
192
- say: (line, quiet) => (quiet ? runQuiet(() => log(line)) : log(line)),
225
+ trace: (line) => record(line),
193
226
  warn: (line, quiet) => (quiet ? runQuiet(() => warn(line)) : warn(line)),
194
227
  now: () => Date.now(),
195
228
  dryRun,
@@ -199,36 +232,26 @@ export class Boot {
199
232
  this.lastStatus = s;
200
233
  this.publish();
201
234
  },
202
- inContext: (ctx, work) => withLogContext({ prefix: ctx.prefix, quiet: ctx.quiet }, work),
235
+ observer: this.progress,
236
+ inContext: (ctx, work) => withLogContext({ stage: ctx.stage, task: ctx.title, quiet: ctx.quiet }, work),
203
237
  });
204
238
  await runner.run('foreground');
205
- const background = this.backgroundNames();
206
- log(bootSummary(Date.now() - this.startedAt, runner.results(), background, logPath));
207
- if (dryRun) {
208
- // Same graph, one task at a time: the background tasks run in place, right after the session.
209
- this.openGate();
210
- await runner.run('background');
211
- return;
212
- }
213
- this.openGate();
214
- const later = runner.run('background');
215
- // The viewer owns the console now: the background tasks write to the log file only.
216
- if (session.harness && session.autoSession !== false && process.stdin.isTTY) {
217
- await viewSession(session.launchId);
218
- log('\nThis window stays open in ' + workspace + '. Other sessions keep running in their own tabs.');
219
- }
220
- await later.catch((e) => warn('Background work failed: ' + errMsg(e)));
239
+ return runner;
221
240
  }
222
241
  stageTitle(n) {
223
- return stageTitle(n, { ...(session.vpn ? { vpnServer: session.vpn.server } : {}), agent: session.harnessName });
242
+ return stageTitle(n, this.who);
224
243
  }
225
- /** The names shown for work that goes on after the boot is ready. */
244
+ /** The toolchain steps that go on after the boot is ready (Visual Studio and what needs it), in plan order. */
226
245
  backgroundNames() {
227
246
  const t = this.tools;
228
247
  if (!t?.later)
229
248
  return [];
230
- const later = t.names.filter((n) => !(n in t.steps));
231
- return later.length ? [later.slice(0, 3).join(', ') + (later.length > 3 ? ' ...' : '')] : [];
249
+ return t.names.filter((n) => !(n in t.steps));
250
+ }
251
+ /** Runs a stage's hooks; they write to the terminal themselves, so the progress block steps aside for them. */
252
+ hooks(stage, env) {
253
+ if (hasHooks(stage))
254
+ this.progress.aside(() => runHooks(stage, env));
232
255
  }
233
256
  /**
234
257
  * The host's one-shot file, read once. Your saved service sign-ins (accounts.<kind>.share) go to their tools before
@@ -262,7 +285,7 @@ export class Boot {
262
285
  setSandboxStatus(s.phase, s.message, {
263
286
  step: s.step,
264
287
  steps: STAGES,
265
- title: this.stageTitle(s.step).replace(/ \(.*\)$/, ''),
288
+ title: stageName(s.step, this.who),
266
289
  active: [...s.running.map((name) => ({ name })), ...this.activeSteps],
267
290
  done: this.stepsDone,
268
291
  total: this.stepsTotal,
@@ -281,7 +304,7 @@ export class Boot {
281
304
  },
282
305
  audit: () => enableProcessAudit(),
283
306
  // 2 - pre-provision hooks and desktop
284
- hooksPreProvision: () => runHooks('pre-provision', process.env),
307
+ hooksPreProvision: () => this.hooks('pre-provision', process.env),
285
308
  desktop: () => applyPersonalization(),
286
309
  browserGpu: () => keepBrowsersOffGpu(),
287
310
  // 3 - developer tools
@@ -343,7 +366,7 @@ export class Boot {
343
366
  await agents.provision(session.harnessName, session.harness, this.env);
344
367
  },
345
368
  // 8 - pre-agent hooks
346
- hooksPreAgent: () => runHooks('pre-agent', this.env),
369
+ hooksPreAgent: () => this.hooks('pre-agent', this.env),
347
370
  // 9 - start
348
371
  session: () => this.start(),
349
372
  // After the first session runs
@@ -371,6 +394,7 @@ export class Boot {
371
394
  ...(session.pcInstalls ? { pcInstalls: session.pcInstalls } : {}),
372
395
  }, {
373
396
  progress: (p) => {
397
+ this.progress.detail('toolchains', stepsDetail(p.active.map((a) => a.name + (a.percent === undefined ? '' : ' ' + Math.round(a.percent) + '%')), p.done, p.total));
374
398
  this.activeSteps = p.active;
375
399
  this.stepsDone = p.done;
376
400
  this.stepsTotal = p.total;
@@ -424,8 +448,7 @@ export class Boot {
424
448
  lan: this.lan,
425
449
  });
426
450
  saveProjectStatus(primaryProject(), workspace, 'ready', 'Ready', session.databases ?? [], this.summary);
427
- for (const line of this.summary)
428
- log(' ' + line);
451
+ this.progress.summary(this.summary);
429
452
  }
430
453
  /** The toolchains' lines, then the missing protections, allowOnly and VS Code (in this order). */
431
454
  summaryLines(toolchainLines) {
@@ -441,7 +464,8 @@ export class Boot {
441
464
  const tools = this.tools;
442
465
  if (!tools?.later)
443
466
  return;
444
- const name = this.backgroundNames()[0] ?? 'Visual Studio';
467
+ const names = this.backgroundNames();
468
+ const name = names.length ? names.slice(0, 3).join(', ') + (names.length > 3 ? ' ...' : '') : 'Visual Studio';
445
469
  const began = Date.now();
446
470
  setBackgroundWork([{ name, state: 'running' }]);
447
471
  log('Background: ' + name + ' (installing)');
@@ -468,7 +492,7 @@ export class Boot {
468
492
  saveProjectStatus(primaryProject(), workspace, 'ready', 'Ready', session.databases ?? [], this.summary);
469
493
  await soft('Sandbox guide for the agent', () => writeGuides(this.summary, this.lan));
470
494
  setBackgroundWork([{ name, state: 'done' }]);
471
- log('Background done in ' + seconds(Date.now() - began) + ': ' + name);
495
+ log('Background done in ' + duration(Date.now() - began) + ': ' + name);
472
496
  }
473
497
  catch (e) {
474
498
  setBackgroundWork([{ name, state: 'failed' }]);
@@ -512,7 +536,7 @@ export class Boot {
512
536
  });
513
537
  if (!(await waitForPrimary()))
514
538
  throw new Error('The agent session did not start. Logs: ' + logsDir);
515
- log('If Claude asks you to sign in, do it once; the login is saved in persistent storage.');
539
+ notice('If Claude asks you to sign in, do it once; the login is saved in persistent storage.');
516
540
  }
517
541
  }
518
542
  export function runBoot() {
@@ -1,10 +1,14 @@
1
1
  import type { BootTaskDef } from './runner.js';
2
2
  export declare const STAGES = 9;
3
- /** The titles of the nine stages (the host shows the short form without the brackets). */
4
- export declare function stageTitle(stage: number, who?: {
5
- vpnServer?: string;
6
- agent?: string;
7
- }): string;
3
+ /** Who the stage titles name. */
4
+ export interface StageWho {
5
+ readonly vpnServer?: string;
6
+ readonly agent?: string;
7
+ }
8
+ /** The titles of the nine stages, with a note where one helps (the host's status message). */
9
+ export declare function stageTitle(stage: number, who?: StageWho): string;
10
+ /** The short titles of the nine stages: the console's stage rows and the host's progress line. */
11
+ export declare function stageName(stage: number, who?: StageWho): string;
8
12
  /** The boot state the tasks share. */
9
13
  export type BootStateKey = 'ptyHost' | 'lan' | 'tools' | 'dbBin' | 'allowSummary' | 'vscodeSummary' | 'env' | 'toolVars' | 'summary' | 'headroom' | 'later';
10
14
  export interface BootTaskInfo {
@@ -1,13 +1,19 @@
1
1
  export const STAGES = 9;
2
- /** The titles of the nine stages (the host shows the short form without the brackets). */
2
+ /** The titles of the nine stages, with a note where one helps (the host's status message). */
3
3
  export function stageTitle(stage, who = {}) {
4
+ return stage === 3
5
+ ? stageName(stage, who) + ' (the first start downloads them once; later starts are quick)'
6
+ : stageName(stage, who);
7
+ }
8
+ /** The short titles of the nine stages: the console's stage rows and the host's progress line. */
9
+ export function stageName(stage, who = {}) {
4
10
  switch (stage) {
5
11
  case 1:
6
12
  return 'Network and session host';
7
13
  case 2:
8
14
  return 'Pre-provision hooks and desktop';
9
15
  case 3:
10
- return 'Developer tools (the first start downloads them once; later starts are quick)';
16
+ return 'Developer tools';
11
17
  case 4:
12
18
  return who.vpnServer ? 'VPN (' + who.vpnServer + ')' : 'VPN (off)';
13
19
  case 5:
@@ -0,0 +1,116 @@
1
+ import { type TaskOutcome } from './runner.js';
2
+ export interface ViewStage {
3
+ readonly stage: number;
4
+ readonly name: string;
5
+ }
6
+ export interface ViewTask {
7
+ readonly id: string;
8
+ readonly stage: number;
9
+ readonly title: string;
10
+ /** Its failure fails the boot (a soft failure is a warning). */
11
+ readonly critical?: boolean;
12
+ }
13
+ /** The marks of a stage row. */
14
+ export interface Glyphs {
15
+ readonly done: string;
16
+ readonly failed: string;
17
+ readonly warned: string;
18
+ readonly skipped: string;
19
+ readonly waiting: string;
20
+ readonly more: string;
21
+ readonly spinner: readonly string[];
22
+ }
23
+ /** For Windows Terminal (Cascadia has braille and check marks). */
24
+ export declare const RICH_GLYPHS: Glyphs;
25
+ /** For the classic console (Consolas has neither braille nor check marks). */
26
+ export declare const CONSOLE_GLYPHS: Glyphs;
27
+ export interface Screen {
28
+ /** Writes text as it is (the view writes its own line ends and, when live, its cursor moves). */
29
+ write(text: string): void;
30
+ /** The terminal's width (live mode). */
31
+ columns(): number;
32
+ }
33
+ export interface ViewOptions {
34
+ /** A terminal: the stage block redraws in place. Otherwise plain, ordered lines. */
35
+ readonly live: boolean;
36
+ readonly screen: Screen;
37
+ /** Milliseconds. */
38
+ readonly now: () => number;
39
+ readonly glyphs?: Glyphs;
40
+ }
41
+ /** The end of the boot: how long it took, which tasks took longest and what goes on in the background. */
42
+ export interface ReadyInfo {
43
+ readonly ms: number;
44
+ readonly outcomes: readonly TaskOutcome[];
45
+ readonly background: readonly string[];
46
+ readonly logPath: string;
47
+ }
48
+ /** "Node, Python, Rust +2 (5/13)": what the toolchain scheduler runs now. */
49
+ export declare function stepsDetail(active: readonly string[], done: number, total: number): string;
50
+ /** "Ready in 46.8s (slowest: ...)" and, when work goes on, the line that says what still installs. */
51
+ export declare function readyLines(info: ReadyInfo): string[];
52
+ /** Word-wraps `text` to `width` columns; a word longer than that is cut. Linear in the text. */
53
+ export declare function wrap(text: string, width: number): string[];
54
+ /** One boot's console: fed by the runner's events and the boot's lines, drawn on a screen. */
55
+ export declare class ProgressView {
56
+ private readonly o;
57
+ private readonly stages;
58
+ private readonly byStage;
59
+ private readonly taskById;
60
+ private readonly nameWidth;
61
+ private readonly glyphs;
62
+ private readonly warnings;
63
+ private readonly notices;
64
+ private tools;
65
+ /** Lines of the live block on the screen now (the cursor is just below them). */
66
+ private drawn;
67
+ private lastFrame;
68
+ private asideDepth;
69
+ private released;
70
+ constructor(stages: readonly ViewStage[], tasks: readonly ViewTask[], o: ViewOptions);
71
+ /**
72
+ * Whether programs should hand their output to the log now: it would draw over the live block, or come out between
73
+ * the plain stages. Not while a hook has the terminal.
74
+ */
75
+ get capturing(): boolean;
76
+ /** The header line, then (live) the first frame. */
77
+ start(header: string): void;
78
+ started(task: ViewTask): void;
79
+ finished(task: ViewTask, outcome: TaskOutcome): void;
80
+ /** What a running task's place on its row says instead of its title (null: its title again). */
81
+ detail(taskId: string, text: string | null): void;
82
+ /** An info line: the file has it; plain output shows it under its stage (or at once, outside a stage). */
83
+ line(text: string, stage: number | undefined): void;
84
+ warning(text: string): void;
85
+ notice(text: string): void;
86
+ /** The tool summary ("name: detail" lines), printed once at the end. */
87
+ summary(lines: readonly string[]): void;
88
+ /** Redraws the live block (the boot calls it a few times a second). */
89
+ tick(): void;
90
+ /** Runs `work`, which writes to the terminal itself (the hooks): the live block steps aside until it is done. */
91
+ aside<T>(work: () => T): T;
92
+ /**
93
+ * The boot is over (`ready`) or failed (null): the final block, warnings, tools, notices and the ready line, once.
94
+ * The console is free afterwards (the first session takes it); nothing of this view prints again.
95
+ */
96
+ release(ready: ReadyInfo | null): void;
97
+ /** The live block as it would be drawn now (no colours when `colour` is false): the tests read it. */
98
+ frame(colour?: boolean): string[];
99
+ private draw;
100
+ private erase;
101
+ /** Plain output: every finished stage whose earlier stages are printed, in stage order. */
102
+ private flushPlain;
103
+ private stageBlock;
104
+ private indent;
105
+ /** One stage row; `now` null: the final, still row (no spinner). */
106
+ private row;
107
+ /** The tool summary as two aligned columns, the detail wrapped under itself. */
108
+ private toolRows;
109
+ /** `lead` + `text`, wrapped to `width` with the next lines indented under the text. */
110
+ private hanging;
111
+ /** The segments as one line of at most `width` columns, cut with the "more" mark. */
112
+ private render;
113
+ private paint;
114
+ /** The usable width: one column less than the terminal (a full line would wrap the cursor). */
115
+ private width;
116
+ }