@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.
- package/out/agent/command.js +5 -5
- package/out/agent/render.d.ts +39 -6
- package/out/agent/render.js +287 -28
- package/out/agent/session.d.ts +21 -1
- package/out/agent/session.js +81 -17
- package/out/commands/agents.js +3 -3
- package/out/commands/ask.js +20 -11
- package/out/commands/codemap.d.ts +1 -1
- package/out/commands/codemap.js +3 -3
- package/out/commands/commands-thin.js +4 -4
- package/out/commands/config.js +1 -1
- package/out/commands/cost.js +4 -4
- package/out/commands/hooks.js +1 -1
- package/out/commands/install.d.ts +1 -1
- package/out/commands/install.js +3 -2
- package/out/commands/instructions.js +1 -1
- package/out/commands/integrations.js +4 -4
- package/out/commands/keys.js +6 -6
- package/out/commands/login.js +97 -27
- package/out/commands/permissions.js +2 -2
- package/out/commands/plugins.js +3 -3
- package/out/commands/privacy.js +2 -2
- package/out/commands/repl.d.ts +4 -0
- package/out/commands/repl.js +563 -101
- package/out/commands/review.js +6 -6
- package/out/commands/settings.js +30 -123
- package/out/commands/skills.js +2 -2
- package/out/commands/unattended.js +6 -6
- package/out/commands/update.js +1 -1
- package/out/commands/welcome.d.ts +25 -1
- package/out/commands/welcome.js +65 -38
- package/out/commands/worktrees.d.ts +1 -1
- package/out/graph/sync.d.ts +1 -1
- package/out/graph/sync.js +6 -6
- package/out/lib/attach.js +4 -3
- package/out/lib/commandlist.d.ts +1 -1
- package/out/lib/commandlist.js +2 -2
- package/out/lib/config.d.ts +3 -2
- package/out/lib/config.js +7 -8
- package/out/lib/connection.js +1 -1
- package/out/lib/gateway.d.ts +9 -5
- package/out/lib/keyboard.d.ts +3 -0
- package/out/lib/keyboard.js +7 -0
- package/out/lib/lastlogin.d.ts +14 -0
- package/out/lib/lastlogin.js +105 -0
- package/out/lib/machine.js +1 -1
- package/out/lib/picker.d.ts +1 -0
- package/out/lib/picker.js +158 -0
- package/out/lib/project-ops.d.ts +1 -1
- package/out/lib/project-ops.js +4 -4
- package/out/lib/prompt.d.ts +3 -0
- package/out/lib/prompt.js +21 -0
- package/out/lib/queue.js +1 -1
- package/out/lib/readiness.d.ts +3 -30
- package/out/lib/readiness.js +13 -82
- package/out/lib/servicecommand.d.ts +14 -0
- package/out/lib/servicecommand.js +74 -0
- package/out/lib/sessions.js +3 -3
- package/out/lib/startcache.d.ts +13 -0
- package/out/lib/startcache.js +78 -0
- package/out/lib/ui.d.ts +11 -0
- package/out/lib/ui.js +28 -0
- package/out/lib/voice.d.ts +36 -0
- package/out/lib/voice.js +137 -2
- package/out/lib/words.d.ts +20 -0
- package/out/lib/words.js +27 -0
- package/out/machine/codemap/build.d.ts +45 -0
- package/out/machine/codemap/build.js +91 -0
- package/out/machine/codemap/facts.d.ts +47 -0
- package/out/machine/codemap/facts.js +12 -0
- package/out/machine/codemap/files.d.ts +45 -0
- package/out/machine/codemap/files.js +207 -0
- package/out/machine/codemap/read-locales.d.ts +29 -0
- package/out/machine/codemap/read-locales.js +246 -0
- package/out/machine/codemap/read-python.d.ts +11 -0
- package/out/machine/codemap/read-python.js +116 -0
- package/out/machine/codemap/read-typescript.d.ts +16 -0
- package/out/machine/codemap/read-typescript.js +292 -0
- package/out/machine/executor/browser.d.ts +14 -0
- package/out/machine/executor/browser.js +270 -0
- package/out/machine/executor/diagnostics.d.ts +2 -0
- package/out/machine/executor/diagnostics.js +181 -0
- package/out/machine/executor/documents.d.ts +40 -0
- package/out/machine/executor/documents.js +170 -0
- package/out/machine/executor/files.d.ts +2 -0
- package/out/machine/executor/files.js +590 -0
- package/out/machine/executor/git.d.ts +48 -0
- package/out/machine/executor/git.js +145 -0
- package/out/machine/executor/hooks.d.ts +51 -0
- package/out/machine/executor/hooks.js +154 -0
- package/out/machine/executor/index.d.ts +45 -0
- package/out/machine/executor/index.js +367 -0
- package/out/machine/executor/notebook.d.ts +2 -0
- package/out/machine/executor/notebook.js +147 -0
- package/out/machine/executor/paths.d.ts +20 -0
- package/out/machine/executor/paths.js +154 -0
- package/out/machine/executor/sandbox.d.ts +40 -0
- package/out/machine/executor/sandbox.js +299 -0
- package/out/machine/executor/shell.d.ts +86 -0
- package/out/machine/executor/shell.js +582 -0
- package/out/machine/executor/toolservers.d.ts +20 -0
- package/out/machine/executor/toolservers.js +189 -0
- package/out/machine/executor/worktree.d.ts +9 -0
- package/out/machine/executor/worktree.js +119 -0
- package/out/machine/folder/project.d.ts +28 -0
- package/out/machine/folder/project.js +114 -0
- package/out/machine/runtime/home.d.ts +2 -0
- package/out/machine/runtime/home.js +48 -0
- package/out/machine/runtime/needs.d.ts +30 -0
- package/out/machine/runtime/needs.js +89 -0
- package/out/machine/runtime/self.d.ts +23 -0
- package/out/machine/runtime/self.js +124 -0
- package/out/native/dictate +0 -0
- package/out/providers/index.js +2 -1
- package/out/thin.js +13 -12
- package/package.json +5 -4
package/out/agent/command.js
CHANGED
|
@@ -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("../
|
|
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("../
|
|
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
|
|
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
|
-
--
|
|
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
|
package/out/agent/render.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
135
|
+
* An answer, laid out for reading in a terminal (manual 6.17).
|
|
106
136
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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;
|
package/out/agent/render.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
574
|
+
* An answer, laid out for reading in a terminal (manual 6.17).
|
|
374
575
|
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
}
|
package/out/agent/session.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ProjectFacts } from "../
|
|
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.
|