@illuminis/comprism 0.1.4 → 0.1.6

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 (116) hide show
  1. package/out/agent/command.js +5 -5
  2. package/out/agent/render.d.ts +39 -6
  3. package/out/agent/render.js +287 -28
  4. package/out/agent/session.d.ts +21 -1
  5. package/out/agent/session.js +81 -17
  6. package/out/commands/agents.js +3 -3
  7. package/out/commands/ask.js +20 -11
  8. package/out/commands/codemap.d.ts +1 -1
  9. package/out/commands/codemap.js +3 -3
  10. package/out/commands/commands-thin.js +4 -4
  11. package/out/commands/config.js +1 -1
  12. package/out/commands/cost.js +4 -4
  13. package/out/commands/hooks.js +1 -1
  14. package/out/commands/install.d.ts +1 -1
  15. package/out/commands/install.js +3 -2
  16. package/out/commands/instructions.js +1 -1
  17. package/out/commands/integrations.js +4 -4
  18. package/out/commands/keys.js +6 -6
  19. package/out/commands/login.js +97 -27
  20. package/out/commands/permissions.js +2 -2
  21. package/out/commands/plugins.js +3 -3
  22. package/out/commands/privacy.js +2 -2
  23. package/out/commands/repl.d.ts +4 -0
  24. package/out/commands/repl.js +563 -101
  25. package/out/commands/review.js +6 -6
  26. package/out/commands/settings.js +30 -123
  27. package/out/commands/skills.js +2 -2
  28. package/out/commands/unattended.js +6 -6
  29. package/out/commands/update.js +1 -1
  30. package/out/commands/welcome.d.ts +25 -1
  31. package/out/commands/welcome.js +65 -38
  32. package/out/commands/worktrees.d.ts +1 -1
  33. package/out/graph/sync.d.ts +1 -1
  34. package/out/graph/sync.js +6 -6
  35. package/out/lib/attach.js +4 -3
  36. package/out/lib/commandlist.d.ts +1 -1
  37. package/out/lib/commandlist.js +2 -2
  38. package/out/lib/config.d.ts +3 -2
  39. package/out/lib/config.js +7 -8
  40. package/out/lib/connection.js +1 -1
  41. package/out/lib/gateway.d.ts +9 -5
  42. package/out/lib/keyboard.d.ts +3 -0
  43. package/out/lib/keyboard.js +7 -0
  44. package/out/lib/lastlogin.d.ts +14 -0
  45. package/out/lib/lastlogin.js +105 -0
  46. package/out/lib/machine.js +1 -1
  47. package/out/lib/picker.d.ts +1 -0
  48. package/out/lib/picker.js +158 -0
  49. package/out/lib/project-ops.d.ts +1 -1
  50. package/out/lib/project-ops.js +4 -4
  51. package/out/lib/prompt.d.ts +3 -0
  52. package/out/lib/prompt.js +21 -0
  53. package/out/lib/queue.js +1 -1
  54. package/out/lib/readiness.d.ts +3 -30
  55. package/out/lib/readiness.js +13 -82
  56. package/out/lib/servicecommand.d.ts +14 -0
  57. package/out/lib/servicecommand.js +74 -0
  58. package/out/lib/sessions.js +3 -3
  59. package/out/lib/startcache.d.ts +13 -0
  60. package/out/lib/startcache.js +78 -0
  61. package/out/lib/ui.d.ts +11 -0
  62. package/out/lib/ui.js +28 -0
  63. package/out/lib/voice.d.ts +36 -0
  64. package/out/lib/voice.js +137 -2
  65. package/out/lib/words.d.ts +20 -0
  66. package/out/lib/words.js +27 -0
  67. package/out/machine/codemap/build.d.ts +45 -0
  68. package/out/machine/codemap/build.js +91 -0
  69. package/out/machine/codemap/facts.d.ts +47 -0
  70. package/out/machine/codemap/facts.js +12 -0
  71. package/out/machine/codemap/files.d.ts +45 -0
  72. package/out/machine/codemap/files.js +207 -0
  73. package/out/machine/codemap/read-locales.d.ts +29 -0
  74. package/out/machine/codemap/read-locales.js +246 -0
  75. package/out/machine/codemap/read-python.d.ts +11 -0
  76. package/out/machine/codemap/read-python.js +116 -0
  77. package/out/machine/codemap/read-typescript.d.ts +16 -0
  78. package/out/machine/codemap/read-typescript.js +292 -0
  79. package/out/machine/executor/browser.d.ts +14 -0
  80. package/out/machine/executor/browser.js +270 -0
  81. package/out/machine/executor/diagnostics.d.ts +2 -0
  82. package/out/machine/executor/diagnostics.js +181 -0
  83. package/out/machine/executor/documents.d.ts +40 -0
  84. package/out/machine/executor/documents.js +170 -0
  85. package/out/machine/executor/files.d.ts +2 -0
  86. package/out/machine/executor/files.js +590 -0
  87. package/out/machine/executor/git.d.ts +48 -0
  88. package/out/machine/executor/git.js +145 -0
  89. package/out/machine/executor/hooks.d.ts +51 -0
  90. package/out/machine/executor/hooks.js +154 -0
  91. package/out/machine/executor/index.d.ts +45 -0
  92. package/out/machine/executor/index.js +367 -0
  93. package/out/machine/executor/notebook.d.ts +2 -0
  94. package/out/machine/executor/notebook.js +147 -0
  95. package/out/machine/executor/paths.d.ts +20 -0
  96. package/out/machine/executor/paths.js +154 -0
  97. package/out/machine/executor/sandbox.d.ts +40 -0
  98. package/out/machine/executor/sandbox.js +299 -0
  99. package/out/machine/executor/shell.d.ts +86 -0
  100. package/out/machine/executor/shell.js +582 -0
  101. package/out/machine/executor/toolservers.d.ts +20 -0
  102. package/out/machine/executor/toolservers.js +189 -0
  103. package/out/machine/executor/worktree.d.ts +9 -0
  104. package/out/machine/executor/worktree.js +119 -0
  105. package/out/machine/folder/project.d.ts +28 -0
  106. package/out/machine/folder/project.js +114 -0
  107. package/out/machine/runtime/home.d.ts +2 -0
  108. package/out/machine/runtime/home.js +48 -0
  109. package/out/machine/runtime/needs.d.ts +30 -0
  110. package/out/machine/runtime/needs.js +89 -0
  111. package/out/machine/runtime/self.d.ts +23 -0
  112. package/out/machine/runtime/self.js +124 -0
  113. package/out/native/dictate +0 -0
  114. package/out/providers/index.js +2 -1
  115. package/out/thin.js +13 -12
  116. package/package.json +5 -4
@@ -56,11 +56,11 @@ exports.runAgent = runAgent;
56
56
  const fs = __importStar(require("fs"));
57
57
  const readline = __importStar(require("readline"));
58
58
  const connection_1 = require("../lib/connection");
59
- const project_1 = require("../lib/project");
59
+ const project_1 = require("../machine/folder/project");
60
60
  const session_1 = require("./session");
61
61
  const ui = __importStar(require("./render"));
62
62
  const output_1 = require("../lib/output");
63
- const self_1 = require("../lib/self");
63
+ const self_1 = require("../machine/runtime/self");
64
64
  /** The WebSocket the session needs is a global from Node 22.
65
65
  *
66
66
  * Checked here with a message somebody can act on. Without it the failure is
@@ -219,7 +219,7 @@ async function runAgent(args) {
219
219
  // `--sandbox` never runs without one (manual 4.12): refused before anything
220
220
  // runs or is charged.
221
221
  if (args.sandbox) {
222
- const { availability } = await Promise.resolve().then(() => __importStar(require("../executor/sandbox")));
222
+ const { availability } = await Promise.resolve().then(() => __importStar(require("../machine/executor/sandbox")));
223
223
  const found = availability();
224
224
  if (!found.system) {
225
225
  process.stderr.write(ui.red(` --sandbox cannot start: ${found.why}.`)
@@ -414,7 +414,7 @@ ${ui.bold("comprism work")} — a coding agent in your terminal
414
414
  comprism work "add a test for the parser and make it pass"
415
415
 
416
416
  ${ui.bold("What it does")}
417
- Reads your project, changes files, runs commands and runs your tests. It
417
+ Reads your workspace, changes files, runs commands and runs your tests. It
418
418
  shows you every change as it makes it, asks before deleting a file or
419
419
  running a command, and can undo the whole job. Each step is routed to whichever
420
420
  model finishes the work for least, and the receipt at the end says what it
@@ -428,7 +428,7 @@ ${ui.bold("Options")}
428
428
  being asked about one at a time; commands still ask
429
429
  --model <id> tell it which model you are running, so the receipt can
430
430
  price the saving against it. It may still move you up
431
- --pin run --model and nothing else, whatever it would prefer
431
+ --no-optimize Optimize off: run --model and nothing else (also --pin)
432
432
  --max-spend <usd> stop at this much rather than running on
433
433
  --max-steps <n> stop after this many steps
434
434
  --max-minutes <n> start no new step after this many minutes
@@ -77,6 +77,24 @@ export declare function stoppedLines(stopped: string[]): string;
77
77
  export declare function ending(line: string, detail: string | null | undefined): string;
78
78
  /** How the job ended, in the words a person reads. */
79
79
  export declare function outcome(o: string, detail: string | null | undefined): string;
80
+ /**
81
+ * A link a person can click, where the terminal supports one.
82
+ *
83
+ * Modern terminals understand an escape that carries an address behind a piece
84
+ * of text; older ones print it as ordinary characters, so the address is shown
85
+ * in words there instead. Never a bare escape with no fallback: a receipt whose
86
+ * proof is unreachable is not proof.
87
+ */
88
+ export declare function link(text: string, url: string): string;
89
+ /**
90
+ * A file the agent made, as a link that opens it, with where it lives beneath.
91
+ *
92
+ * The name alone is not enough: somebody who asked from their home folder for a
93
+ * diagram cannot tell which folder "saved diagram.png" means. So the full path
94
+ * is written out underneath in gray, where it can be copied into Finder or a
95
+ * terminal, and the name above it opens the file on a click.
96
+ */
97
+ export declare function savedFile(name: string, fullPath: string, bytes: string): string;
80
98
  /** One model's share of a job, as our server reported it. */
81
99
  export interface ModelShare {
82
100
  model: string | null;
@@ -84,6 +102,18 @@ export interface ModelShare {
84
102
  usd: number | null;
85
103
  unpriced?: number;
86
104
  }
105
+ /** The receipt card, exactly as the server words it for every client. */
106
+ export interface ReceiptCard {
107
+ model: string | null;
108
+ title: string | null;
109
+ vs: string | null;
110
+ amount: string | null;
111
+ saved: boolean;
112
+ /** The label of the link to the whole record. */
113
+ details?: string | null;
114
+ badge: string | null;
115
+ warning: string | null;
116
+ }
87
117
  /**
88
118
  * The receipt under a finished job.
89
119
  *
@@ -100,13 +130,16 @@ export interface ModelShare {
100
130
  * it, quieten it, and turn the address in it into something clickable. That is
101
131
  * formatting, which belongs to the client, and it is the only part that does.
102
132
  */
103
- export declare function receipt(text: string, models?: ModelShare[], testsPassed?: boolean): string;
133
+ export declare function receipt(text: string, models?: ModelShare[], testsPassed?: boolean, card?: ReceiptCard | null): string;
104
134
  /**
105
- * Plain text, from a model that writes markdown.
135
+ * An answer, laid out for reading in a terminal (manual 6.17).
106
136
  *
107
- * A terminal has no bold and no bullet glyphs, so `**like this**` arrives on
108
- * screen as literal asterisks and a heading looks like a typing mistake. The
109
- * emphasis is real, so it is kept as terminal bold rather than thrown away;
110
- * everything that only exists to be rendered by a browser is removed.
137
+ * The approved design (docs/product/design/cli-answer, option B): the whole
138
+ * answer on one left margin behind a violet rail, so where the question ends
139
+ * and the answer begins is never in doubt; words wrapped at a reading width
140
+ * and never split by the window edge; a blank line between every block;
141
+ * headings in the brand violet; code in its own color; wrapped list items
142
+ * continuing under their own text. Piped to a file or another program, the
143
+ * same answer comes out without the rail or the color, as a log wants it.
111
144
  */
112
145
  export declare function plain(text: string): string;
@@ -36,10 +36,13 @@ exports.diff = diff;
36
36
  exports.stoppedLines = stoppedLines;
37
37
  exports.ending = ending;
38
38
  exports.outcome = outcome;
39
+ exports.link = link;
40
+ exports.savedFile = savedFile;
39
41
  exports.receipt = receipt;
40
42
  exports.plain = plain;
41
43
  /** Built from a character code rather than written as a literal escape, so this
42
44
  * file contains no control characters and stays safe to grep, diff and paste. */
45
+ const url_1 = require("url");
43
46
  const CSI = String.fromCharCode(27) + "[";
44
47
  /** Decided when drawn, so plain mode set at start up applies (manual 6.12). */
45
48
  const supportsColour = () => process.stdout.isTTY && !process.env.NO_COLOR;
@@ -260,6 +263,32 @@ function link(text, url) {
260
263
  return `${text} (${url})`;
261
264
  return `]8;;${url}${text}]8;;`;
262
265
  }
266
+ /**
267
+ * A file the agent made, as a link that opens it, with where it lives beneath.
268
+ *
269
+ * The name alone is not enough: somebody who asked from their home folder for a
270
+ * diagram cannot tell which folder "saved diagram.png" means. So the full path
271
+ * is written out underneath in gray, where it can be copied into Finder or a
272
+ * terminal, and the name above it opens the file on a click.
273
+ */
274
+ function savedFile(name, fullPath, bytes) {
275
+ const href = (0, url_1.pathToFileURL)(fullPath).href;
276
+ return `${(0, exports.green)(" saved ")}${link(name, href)}${(0, exports.dim)(` ${bytes} bytes`)}\n`
277
+ + `${(0, exports.gray)(` ${fullPath}`)}\n`;
278
+ }
279
+ /** The card's two lines, as the approved look draws them. */
280
+ function cardRows(card) {
281
+ const rows = [];
282
+ const head = [card.title ? (0, exports.bold)(card.title) : "", card.badge ? (0, exports.violet)(card.badge) : "",
283
+ card.vs ? (0, exports.dim)(card.vs) : ""].filter(Boolean).join(" ");
284
+ if (head)
285
+ rows.push(head);
286
+ if (card.amount)
287
+ rows.push(card.saved ? (0, exports.bold)((0, exports.green)(card.amount)) : (0, exports.dim)(card.amount));
288
+ if (card.warning)
289
+ rows.push((0, exports.dim)(card.warning));
290
+ return rows;
291
+ }
263
292
  /**
264
293
  * The receipt under a finished job.
265
294
  *
@@ -276,9 +305,9 @@ function link(text, url) {
276
305
  * it, quieten it, and turn the address in it into something clickable. That is
277
306
  * formatting, which belongs to the client, and it is the only part that does.
278
307
  */
279
- function receipt(text, models = [], testsPassed) {
280
- const lines = [];
281
- for (const line of String(text || "").split("\n")) {
308
+ function receipt(text, models = [], testsPassed, card) {
309
+ const lines = card ? cardRows(card).map((r) => ` ${r}`) : [];
310
+ for (const line of card ? [] : String(text || "").split("\n")) {
282
311
  if (!line.trim())
283
312
  continue;
284
313
  // The address our server put in the sentence becomes a real link, and the
@@ -309,7 +338,13 @@ function receipt(text, models = [], testsPassed) {
309
338
  return "";
310
339
  // On a screen, the receipt is a highlighted box, as on the website. Piped to
311
340
  // a file or another program it stays plain lines, which is what a log wants.
312
- return process.stdout.isTTY ? boxed(text, models, testsPassed) : `${lines.join("\n")}\n`;
341
+ if (card) {
342
+ // The link the server put in its sentence still goes last.
343
+ const found = /\[([^\]]+)\]\(([^)]+)\)/.exec(String(text || ""));
344
+ if (found)
345
+ lines.push((0, exports.dim)(` ${link(String(card.details || found[1]), String(found[2]))}`));
346
+ }
347
+ return process.stdout.isTTY ? boxed(text, models, testsPassed, card) : `${lines.join("\n")}\n`;
313
348
  }
314
349
  /** Words wrapped to a width, never breaking a word. */
315
350
  function wrap(text, room) {
@@ -332,12 +367,20 @@ function wrap(text, room) {
332
367
  * everything else quiet. The server's words are unchanged; each clause it
333
368
  * separated with `|` gets its own line, and its link goes last, clickable.
334
369
  */
335
- function boxed(text, models, testsPassed) {
370
+ function boxed(text, models, testsPassed, card) {
336
371
  // Room for the border, the padding and the `receipt` label on the first line.
337
372
  const inner = width() - 16;
338
- const rows = [];
373
+ const rows = card ? cardRows(card) : [];
339
374
  const links = [];
340
375
  for (const raw of String(text || "").split("\n")) {
376
+ if (card) {
377
+ // The card carries the words; only the sentence's link is kept.
378
+ raw.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label, url) => {
379
+ links.push({ label: String(card.details || label), url: String(url) });
380
+ return "";
381
+ });
382
+ continue;
383
+ }
341
384
  const withoutLinks = raw.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label, url) => {
342
385
  links.push({ label: String(label), url: String(url) });
343
386
  return "";
@@ -369,30 +412,246 @@ function boxed(text, models, testsPassed) {
369
412
  const body = rows.map((r) => ` ${(0, exports.violet)("\u2502")} ${r}${" ".repeat(widest - visible(r).length)} ${(0, exports.violet)("\u2502")}`);
370
413
  return [` ${(0, exports.violet)(`\u256d${bar}\u256e`)}`, ...body, ` ${(0, exports.violet)(`\u2570${bar}\u256f`)}`].join("\n") + "\n";
371
414
  }
415
+ /** A markdown table's cells, with emphasis and code marks taken off and a
416
+ * `<br>` kept as a line break inside the cell. */
417
+ function cells(row) {
418
+ return row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim()
419
+ .replace(/\*\*([^*]+)\*\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/`([^`]+)`/g, "$1")
420
+ .replace(/<br\s*\/?>/gi, "\n"));
421
+ }
422
+ /** Widths that fit the terminal: each column its natural width when they all
423
+ * fit, otherwise the narrow ones keep theirs and the wide ones share the rest. */
424
+ function fitWidths(natural, room) {
425
+ const out = natural.map(() => 0);
426
+ let left = room;
427
+ let open = natural.map((_, i) => i).sort((a, b) => natural[a] - natural[b]);
428
+ while (open.length) {
429
+ const share = Math.floor(left / open.length);
430
+ const i = open[0];
431
+ if (natural[i] <= share) {
432
+ out[i] = natural[i];
433
+ left -= natural[i];
434
+ open = open.slice(1);
435
+ continue;
436
+ }
437
+ for (const j of open)
438
+ out[j] = Math.max(6, share);
439
+ break;
440
+ }
441
+ return out;
442
+ }
443
+ /** One cell's text as lines no wider than its column. */
444
+ function cellLines(text, w) {
445
+ const lines = [];
446
+ for (const part of text.split("\n")) {
447
+ const words = wrap(part, w);
448
+ for (const l of words.length ? words : [""]) {
449
+ // A single word longer than the column is cut, never allowed to push the border.
450
+ for (let k = 0; k < Math.max(1, l.length); k += w)
451
+ lines.push(l.slice(k, k + w));
452
+ }
453
+ }
454
+ return lines;
455
+ }
456
+ /**
457
+ * Markdown tables drawn as terminal tables.
458
+ *
459
+ * A model asked for "a summary table" writes pipes and dashes, which a browser
460
+ * draws and a terminal shows as raw markup. Each block of `|` rows with a
461
+ * `|---|` line under its header becomes a boxed table, with long cells wrapped
462
+ * inside their column so the table always fits the window.
463
+ */
464
+ function tables(text) {
465
+ const lines = text.split("\n");
466
+ const out = [];
467
+ const isRow = (l) => l !== undefined && /^\s*\|.*\|\s*$/.test(l);
468
+ const isRule = (l) => l !== undefined && /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(l);
469
+ for (let i = 0; i < lines.length; i++) {
470
+ if (!(isRow(lines[i]) && isRule(lines[i + 1]))) {
471
+ out.push(lines[i]);
472
+ continue;
473
+ }
474
+ const head = cells(lines[i]);
475
+ const body = [];
476
+ i += 2;
477
+ while (isRow(lines[i]))
478
+ body.push(cells(lines[i++]));
479
+ i--;
480
+ const cols = head.length;
481
+ const rows = [head, ...body].map((r) => Array.from({ length: cols }, (_, k) => r[k] ?? ""));
482
+ const natural = Array.from({ length: cols }, (_, k) => Math.max(3, ...rows.map((r) => Math.max(...r[k].split("\n").map((s) => s.length)))));
483
+ const w = fitWidths(natural, width() - 4 - (3 * cols + 1));
484
+ const bar = (l, m, r) => (0, exports.dim)(` ${l}${w.map((n) => "─".repeat(n + 2)).join(m)}${r}`);
485
+ const draw = (r, strong) => {
486
+ const wrapped = r.map((c, k) => cellLines(c, w[k]));
487
+ const height = Math.max(...wrapped.map((c) => c.length));
488
+ for (let h = 0; h < height; h++) {
489
+ out.push(` ${(0, exports.dim)("│")}${wrapped.map((c, k) => {
490
+ const s = (c[h] ?? "").padEnd(w[k]);
491
+ return ` ${strong ? (0, exports.bold)(s) : s} `;
492
+ }).join((0, exports.dim)("│"))}${(0, exports.dim)("│")}`);
493
+ }
494
+ };
495
+ out.push(bar("┌", "┬", "┐"));
496
+ draw(rows[0], true);
497
+ out.push(bar("├", "┼", "┤"));
498
+ rows.slice(1).forEach((r, n) => {
499
+ draw(r, false);
500
+ if (n < rows.length - 2)
501
+ out.push(bar("├", "┼", "┤"));
502
+ });
503
+ out.push(bar("└", "┴", "┘"));
504
+ }
505
+ return out.join("\n");
506
+ }
507
+ /** The answer's two accents, one pair per terminal background (manual 6.10):
508
+ * the brand violet and amber on a dark terminal, the brand purple and deep
509
+ * blue on a light one, each above 4.5 to 1 against its background. */
510
+ const onLight = () => process.env.COMPRISM_THEME === "light";
511
+ const headingPaint = (t) => paint(onLight() ? "1;38;2;107;91;167" : "1;38;5;141", t);
512
+ const codePaint = (t) => paint(onLight() ? "38;2;31;77;120" : "38;2;217;119;6", t);
513
+ /** Characters of text per line, the width Glow and Claude Code read at. */
514
+ const READING_WIDTH = 80;
515
+ const same = (t) => t;
516
+ /** A line of markdown as words, each piece carrying its own emphasis, so a
517
+ * wrapped line never starts inside a color it cannot see the start of. */
518
+ function words(text) {
519
+ const linked = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, t, u) => `${t} (${u})`);
520
+ const parts = [];
521
+ const re = /(\*\*[^*]+\*\*|__[^_]+__|`[^`]+`|\*[^*\s][^*]*\*)/g;
522
+ let last = 0;
523
+ for (let m = re.exec(linked); m; m = re.exec(linked)) {
524
+ parts.push({ t: linked.slice(last, m.index), p: same });
525
+ const s = m[0];
526
+ parts.push(s.startsWith("`") ? { t: s.slice(1, -1), p: codePaint }
527
+ : s.startsWith("**") || s.startsWith("__") ? { t: s.slice(2, -2), p: exports.bold }
528
+ : { t: s.slice(1, -1), p: (t) => paint("3", t) });
529
+ last = m.index + s.length;
530
+ }
531
+ parts.push({ t: linked.slice(last), p: same });
532
+ const out = [];
533
+ let cur = [];
534
+ for (const part of parts) {
535
+ for (const piece of part.t.split(/(\s+)/)) {
536
+ if (!piece)
537
+ continue;
538
+ if (/^\s+$/.test(piece)) {
539
+ if (cur.length) {
540
+ out.push(cur);
541
+ cur = [];
542
+ }
543
+ }
544
+ else
545
+ cur.push({ t: piece, p: part.p });
546
+ }
547
+ }
548
+ if (cur.length)
549
+ out.push(cur);
550
+ return out;
551
+ }
552
+ const show = (w) => w.map((x) => x.p(x.t)).join("");
553
+ const size = (w) => w.reduce((n, x) => n + x.t.length, 0);
554
+ /** Words filled into lines no wider than `room`, a word never split, every
555
+ * line after the first starting under the first line's text. */
556
+ function fill(text, lead, hang, room) {
557
+ const lines = [];
558
+ let line = lead, used = visible(lead).length, empty = true;
559
+ for (const w of words(text)) {
560
+ if (!empty && used + 1 + size(w) > room) {
561
+ lines.push(line);
562
+ line = hang;
563
+ used = visible(hang).length;
564
+ empty = true;
565
+ }
566
+ line += (empty ? "" : " ") + show(w);
567
+ used += (empty ? 0 : 1) + size(w);
568
+ empty = false;
569
+ }
570
+ lines.push(line);
571
+ return lines;
572
+ }
372
573
  /**
373
- * Plain text, from a model that writes markdown.
574
+ * An answer, laid out for reading in a terminal (manual 6.17).
374
575
  *
375
- * A terminal has no bold and no bullet glyphs, so `**like this**` arrives on
376
- * screen as literal asterisks and a heading looks like a typing mistake. The
377
- * emphasis is real, so it is kept as terminal bold rather than thrown away;
378
- * everything that only exists to be rendered by a browser is removed.
576
+ * The approved design (docs/product/design/cli-answer, option B): the whole
577
+ * answer on one left margin behind a violet rail, so where the question ends
578
+ * and the answer begins is never in doubt; words wrapped at a reading width
579
+ * and never split by the window edge; a blank line between every block;
580
+ * headings in the brand violet; code in its own color; wrapped list items
581
+ * continuing under their own text. Piped to a file or another program, the
582
+ * same answer comes out without the rail or the color, as a log wants it.
379
583
  */
380
584
  function plain(text) {
381
- return text
382
- // Fenced code keeps its content and loses its fence markers.
383
- .replace(/^```[a-zA-Z0-9]*\n?/gm, '')
384
- .replace(/^```$/gm, '')
385
- // Headings become their own words, emphasized.
386
- .replace(/^#{1,6}\s+(.+)$/gm, (_m, t) => (0, exports.bold)(String(t)))
387
- // Bold and italic become the terminal's own emphasis.
388
- .replace(/\*\*([^*]+)\*\*/g, (_m, t) => (0, exports.bold)(String(t)))
389
- .replace(/__([^_]+)__/g, (_m, t) => (0, exports.bold)(String(t)))
390
- .replace(/(^|[^*])\*([^*\n]+)\*/g, (_m, pre, t) => pre + String(t))
391
- // A bullet is a bullet, not an asterisk.
392
- .replace(/^\s*[*-]\s+/gm, ' . ')
393
- // Inline code keeps its text; the backticks were never for a terminal.
394
- .replace(/`([^`]+)`/g, (_m, t) => String(t))
395
- // A link reads as its words, with the address after it.
396
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, t, u) => `${t} (${u})`)
397
- .replace(/\n{3,}/g, '\n\n');
585
+ const screen = supportsColour();
586
+ const lead = screen ? ` ${paint(onLight() ? "38;2;107;91;167" : "38;5;141", "│")} ` : " ";
587
+ const railOnly = screen ? lead.trimEnd() : "";
588
+ const room = Math.min(width() - 1, visible(lead).length + READING_WIDTH);
589
+ const src = text.replace(/\r\n/g, "\n").split("\n");
590
+ const out = [];
591
+ const gap = () => { if (out.length && out[out.length - 1] !== railOnly)
592
+ out.push(railOnly); };
593
+ const isTableRow = (l) => l !== undefined && /^\s*\|.*\|\s*$/.test(l);
594
+ const startsBlock = (l) => /^\s*([-*+•]\s|\d+[.)]\s|#{1,6}\s|```|>|\|)/.test(l) || /^\s*([-*_])\s*\1\s*\1[\s\-*_]*$/.test(l);
595
+ for (let i = 0; i < src.length; i++) {
596
+ const l = src[i];
597
+ if (!l.trim())
598
+ continue;
599
+ if (/^\s*```/.test(l)) {
600
+ gap();
601
+ for (i++; i < src.length && !/^\s*```/.test(src[i]); i++)
602
+ out.push(`${lead} ${codePaint(src[i])}`);
603
+ continue;
604
+ }
605
+ const h = /^\s*#{1,6}\s+(.+?)\s*#*\s*$/.exec(l);
606
+ if (h) {
607
+ gap();
608
+ out.push(lead + headingPaint(h[1].replace(/\*\*/g, "")));
609
+ gap();
610
+ continue;
611
+ }
612
+ if (/^\s*([-*_])\s*\1\s*\1[\s\-*_]*$/.test(l)) {
613
+ gap();
614
+ continue;
615
+ }
616
+ if (isTableRow(l)) {
617
+ const block = [];
618
+ while (isTableRow(src[i]))
619
+ block.push(src[i++]);
620
+ i--;
621
+ gap();
622
+ // The table's own two space indent gives way to the rail, whether or
623
+ // not a color code comes before it.
624
+ const indent = new RegExp(`^((?:${String.fromCharCode(27)}\\[[0-9;]*m)*) {2}`);
625
+ for (const row of tables(block.join("\n")).split("\n"))
626
+ out.push(lead + row.replace(indent, "$1"));
627
+ gap();
628
+ continue;
629
+ }
630
+ const bullet = /^(\s*)([-*+•]|\d+[.)])\s+(.*)$/.exec(l);
631
+ if (bullet) {
632
+ if (out.length && !/^\s*([-*+•]|\d+[.)])\s/.test(src[i - 1] ?? ""))
633
+ gap();
634
+ const depth = Math.floor(bullet[1].replace(/\t/g, " ").length / 2);
635
+ const mark = /\d/.test(bullet[2]) ? `${bullet[2].replace(")", ".")} ` : "• ";
636
+ let body = bullet[3];
637
+ // A list item that runs onto an indented next line is one item.
638
+ while (i + 1 < src.length && src[i + 1].trim() && /^\s{2,}\S/.test(src[i + 1]) && !startsBlock(src[i + 1]))
639
+ body += " " + src[++i].trim();
640
+ const indent = " ".repeat(depth);
641
+ out.push(...fill(body, lead + indent + (0, exports.dim)(mark), lead + indent + " ".repeat(mark.length), room));
642
+ if (i + 1 < src.length && !/^\s*([-*+•]|\d+[.)])\s/.test(src[i + 1]))
643
+ gap();
644
+ continue;
645
+ }
646
+ // A paragraph is every line up to the next blank line or block.
647
+ let para = l.trim().replace(/^>\s?/, "");
648
+ while (i + 1 < src.length && src[i + 1].trim() && !startsBlock(src[i + 1]))
649
+ para += " " + src[++i].trim();
650
+ gap();
651
+ out.push(...fill(para, lead, lead, room));
652
+ gap();
653
+ }
654
+ while (out.length && out[out.length - 1] === railOnly)
655
+ out.pop();
656
+ return out.join("\n");
398
657
  }
@@ -1,4 +1,4 @@
1
- import { type ProjectFacts } from "../lib/project";
1
+ import { type ProjectFacts } from "../machine/folder/project";
2
2
  import * as ui from "./render";
3
3
  export interface SessionOptions {
4
4
  /** Where the workspace lives, and the URL of the server that owns it. */
@@ -106,7 +106,12 @@ export interface SessionOptions {
106
106
  /** `--output stream`: handed the service's stream lines as they arrive. */
107
107
  onStream?: (items: Array<Record<string, unknown>>) => void;
108
108
  }
109
+ /** How many next steps the last job offered (web manual 13.4), so a bare number
110
+ * typed next is sent to the service to run one rather than to the agent. */
111
+ export declare let waitingNextSteps: number;
109
112
  export interface JobResult {
113
+ /** The service's lines for the next steps it offered, printed as sent. */
114
+ nextSteps?: string[];
110
115
  outcome: string;
111
116
  detail?: string | null;
112
117
  jobId?: string;
@@ -122,6 +127,8 @@ export interface JobResult {
122
127
  * model and the arithmetic are all on the server, so a client that wrote its
123
128
  * own wording would be a second answer to the same question. */
124
129
  receipt: string;
130
+ /** The receipt card, worded by the server. Drawn as sent. */
131
+ card?: ui.ReceiptCard | null;
125
132
  /** Which models did the work, and each one's share, as the server reported
126
133
  * it. Taken as sent, never recalculated here. */
127
134
  models: ui.ModelShare[];
@@ -169,6 +176,15 @@ export declare class TerminalSession {
169
176
  * with nothing between the copies. */
170
177
  private lastSaid;
171
178
  private canceled;
179
+ /** The live "thinking 12s" line while the model works on a step. Drawn in
180
+ * place and erased before anything else prints, so it never lands in the
181
+ * answer. Without it a step is a blank screen for as long as the model takes,
182
+ * and "thinking" and "hung" look identical. */
183
+ private working;
184
+ /** When the current wait began, kept across the line being redrawn. Cleared
185
+ * when a step finishes, so the next step counts from its own start. */
186
+ private workFrom;
187
+ private readonly write;
172
188
  constructor(opts: SessionOptions, write?: (s: string) => void);
173
189
  /** Where the socket lives, derived from the workspace URL.
174
190
  *
@@ -292,6 +308,10 @@ export declare class TerminalSession {
292
308
  /** An action as approved: its name, its arguments and the real folder it
293
309
  * runs in, so a changed argument or a moved folder is caught. */
294
310
  private fingerprint;
311
+ /** Starts the working line with the service's own words. Only on a real
312
+ * terminal: a piped or unattended run gets the answer and nothing else. */
313
+ private startWorking;
314
+ private stopWorking;
295
315
  private narrate;
296
316
  private targetOf;
297
317
  /** Put a change in front of the person and wait.