@cargo-ai/cli 1.0.125 → 1.0.127
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/build/commands/ask/harness/bridgeOutput.d.ts +19 -0
- package/build/commands/ask/harness/bridgeOutput.d.ts.map +1 -0
- package/build/commands/ask/harness/bridgeOutput.js +42 -0
- package/build/commands/ask/harness/create.d.ts.map +1 -1
- package/build/commands/ask/harness/create.js +2 -1
- package/build/commands/ask/harness/index.d.ts +4 -3
- package/build/commands/ask/harness/index.d.ts.map +1 -1
- package/build/commands/ask/harness/index.js +3 -2
- package/build/commands/ask/harness/toolCallReplay.d.ts +31 -0
- package/build/commands/ask/harness/toolCallReplay.d.ts.map +1 -1
- package/build/commands/ask/harness/toolCallReplay.js +89 -8
- package/build/commands/ask/harness/turn.d.ts +17 -0
- package/build/commands/ask/harness/turn.d.ts.map +1 -1
- package/build/commands/ask/harness/turn.js +85 -3
- package/build/commands/ask/harness/usage.d.ts +19 -0
- package/build/commands/ask/harness/usage.d.ts.map +1 -1
- package/build/commands/ask/harness/usage.js +46 -0
- package/build/commands/ask/input/completer.d.ts +1 -1
- package/build/commands/ask/input/completer.d.ts.map +1 -1
- package/build/commands/ask/input/completer.js +2 -0
- package/build/commands/ask/input/prompt.d.ts.map +1 -1
- package/build/commands/ask/input/prompt.js +54 -8
- package/build/commands/ask/input/slash.d.ts +7 -1
- package/build/commands/ask/input/slash.d.ts.map +1 -1
- package/build/commands/ask/input/slash.js +10 -0
- package/build/commands/ask/session/index.d.ts +2 -1
- package/build/commands/ask/session/index.d.ts.map +1 -1
- package/build/commands/ask/session/index.js +2 -1
- package/build/commands/ask/session/markdown.d.ts +20 -0
- package/build/commands/ask/session/markdown.d.ts.map +1 -0
- package/build/commands/ask/session/markdown.js +451 -0
- package/build/commands/ask/session/run.d.ts.map +1 -1
- package/build/commands/ask/session/run.js +194 -12
- package/build/commands/ask/session/tree.d.ts +81 -0
- package/build/commands/ask/session/tree.d.ts.map +1 -1
- package/build/commands/ask/session/tree.js +322 -51
- package/build/commands/runHandler.d.ts +1 -0
- package/build/commands/runHandler.d.ts.map +1 -1
- package/build/commands/runHandler.js +6 -3
- package/build/utils/textWidth.d.ts +25 -0
- package/build/utils/textWidth.d.ts.map +1 -0
- package/build/utils/textWidth.js +147 -0
- package/package.json +3 -3
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { isAbsolute, relative } from "node:path";
|
|
1
|
+
import { basename, isAbsolute, relative } from "node:path";
|
|
2
2
|
import { selectOne } from "@cargo-ai/cdk/cli";
|
|
3
|
+
import { clipToCells, textWidth } from "../../../utils/textWidth.js";
|
|
3
4
|
import { clipToWidth, colors } from "../../runHandler.js";
|
|
4
5
|
import { ASK_USER_QUESTION_TOOL_NAME, readAskUserQuestions, } from "../harness/askUserQuestion.js";
|
|
6
|
+
import { renderMarkdown } from "./markdown.js";
|
|
5
7
|
/**
|
|
6
8
|
* What an assistant turn is marked with, alongside `◇` for a step and `│` for
|
|
7
9
|
* the rail.
|
|
@@ -35,7 +37,7 @@ export function wrapLine(text, width) {
|
|
|
35
37
|
const lines = [];
|
|
36
38
|
for (const word of text.split(/\s+/).filter((part) => part.length > 0)) {
|
|
37
39
|
const current = lines[lines.length - 1];
|
|
38
|
-
if (current === undefined || `${current} ${word}
|
|
40
|
+
if (current === undefined || textWidth(`${current} ${word}`) > width) {
|
|
39
41
|
lines.push(word);
|
|
40
42
|
continue;
|
|
41
43
|
}
|
|
@@ -63,16 +65,6 @@ function writeRailWrapped(body) {
|
|
|
63
65
|
writeRail(line);
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Light cleanup so model markdown does not fight the tree. Keeps the words;
|
|
68
|
-
* drops heading marks, bold markers, and horizontal rules.
|
|
69
|
-
*/
|
|
70
|
-
function forTerminal(text) {
|
|
71
|
-
return text
|
|
72
|
-
.replace(/^#{1,6}\s+/gm, "")
|
|
73
|
-
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
74
|
-
.replace(/^---+$/gm, "");
|
|
75
|
-
}
|
|
76
68
|
/**
|
|
77
69
|
* A `◇` step: the first line beside the marker, the rest continuing on the
|
|
78
70
|
* rail under it.
|
|
@@ -115,44 +107,66 @@ export function showUserTurn(body) {
|
|
|
115
107
|
writeDiamondBlock(body);
|
|
116
108
|
}
|
|
117
109
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
110
|
+
* Where the finished part of a reply ends: just past its last blank line, as
|
|
111
|
+
* long as that line is not inside a code fence. Never before `from`.
|
|
120
112
|
*/
|
|
121
|
-
function
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
lines.push("");
|
|
131
|
-
continue;
|
|
113
|
+
function commitPoint(buffer, from) {
|
|
114
|
+
const cursor = { at: buffer.lastIndexOf("\n\n") };
|
|
115
|
+
while (cursor.at >= from) {
|
|
116
|
+
const fences = buffer
|
|
117
|
+
.slice(0, cursor.at)
|
|
118
|
+
.split("\n")
|
|
119
|
+
.filter((line) => /^\s*(```|~~~)/.test(line) === true).length;
|
|
120
|
+
if (fences % 2 === 0) {
|
|
121
|
+
return cursor.at + 2;
|
|
132
122
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
123
|
+
cursor.at = buffer.lastIndexOf("\n\n", cursor.at - 1);
|
|
124
|
+
}
|
|
125
|
+
return from;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Write reply lines: the first beside `■` until one has been printed for
|
|
129
|
+
* good, the rest on the │ rail. Returns how many rows were written.
|
|
130
|
+
*/
|
|
131
|
+
function writeAssistantLines(live, lines) {
|
|
132
|
+
lines.forEach((line, index) => {
|
|
133
|
+
if (index === 0 && live.markerCommitted === false) {
|
|
134
|
+
process.stderr.write(`${ASSISTANT_MARKER} ${line}\n`);
|
|
135
|
+
return;
|
|
140
136
|
}
|
|
137
|
+
writeRail(line);
|
|
138
|
+
});
|
|
139
|
+
return lines.length;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Markdown lines for part of a reply, after the blank row that separates it
|
|
143
|
+
* from paragraphs already printed for good.
|
|
144
|
+
*/
|
|
145
|
+
function assistantLines(text, withGap) {
|
|
146
|
+
const body = text.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
147
|
+
if (body === "") {
|
|
148
|
+
return [];
|
|
141
149
|
}
|
|
150
|
+
const lines = renderMarkdown(body, treeBodyWidth());
|
|
151
|
+
return withGap === true ? ["", ...lines] : lines;
|
|
152
|
+
}
|
|
153
|
+
/** Print reply lines for good: they are never cleared or drawn again. */
|
|
154
|
+
function commitAssistantLines(live, lines) {
|
|
142
155
|
if (lines.length === 0) {
|
|
143
|
-
return
|
|
156
|
+
return;
|
|
144
157
|
}
|
|
145
|
-
|
|
158
|
+
writeAssistantLines(live, lines);
|
|
159
|
+
live.markerCommitted = true;
|
|
146
160
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
/**
|
|
162
|
+
* How many rows the redrawn part of a reply may take. Clearing it means
|
|
163
|
+
* moving the cursor up past every row, and the cursor stops at the top of the
|
|
164
|
+
* screen: rows that scrolled off would be drawn again under themselves. Two
|
|
165
|
+
* rows are kept for the typed line and the margin.
|
|
166
|
+
*/
|
|
167
|
+
function liveRowLimit() {
|
|
168
|
+
const rows = process.stderr.rows === undefined ? 24 : process.stderr.rows;
|
|
169
|
+
return Math.max(3, rows - 2);
|
|
156
170
|
}
|
|
157
171
|
function clearPaintedLines(count) {
|
|
158
172
|
if (count <= 0 || process.stderr.isTTY !== true) {
|
|
@@ -164,7 +178,15 @@ function clearPaintedLines(count) {
|
|
|
164
178
|
export function beginAssistantTurn() {
|
|
165
179
|
writeRail("");
|
|
166
180
|
process.stderr.write(`${ASSISTANT_MARKER} \n`);
|
|
167
|
-
return {
|
|
181
|
+
return {
|
|
182
|
+
buffer: "",
|
|
183
|
+
committed: 0,
|
|
184
|
+
tailLinesCommitted: 0,
|
|
185
|
+
tailHasGap: false,
|
|
186
|
+
markerCommitted: false,
|
|
187
|
+
paintedLines: 1,
|
|
188
|
+
footer: "",
|
|
189
|
+
};
|
|
168
190
|
}
|
|
169
191
|
export function appendAssistantDelta(live, delta) {
|
|
170
192
|
// A tool between two pieces of narration inserts a paragraph break, so that
|
|
@@ -182,10 +204,27 @@ export function appendAssistantDelta(live, delta) {
|
|
|
182
204
|
}
|
|
183
205
|
/** Draw the block again from its buffer, with the typed line under it. */
|
|
184
206
|
function repaintAssistantTurn(live) {
|
|
185
|
-
const lines = assistantDisplayLines(live.buffer);
|
|
186
207
|
clearPaintedLines(live.paintedLines);
|
|
187
|
-
|
|
188
|
-
|
|
208
|
+
live.paintedLines = 0;
|
|
209
|
+
const cut = commitPoint(live.buffer, live.committed);
|
|
210
|
+
if (cut > live.committed) {
|
|
211
|
+
// Rows of it may already be out, from when it was the unfinished part.
|
|
212
|
+
commitAssistantLines(live, assistantLines(live.buffer.slice(live.committed, cut), live.tailHasGap).slice(live.tailLinesCommitted));
|
|
213
|
+
live.committed = cut;
|
|
214
|
+
live.tailLinesCommitted = 0;
|
|
215
|
+
live.tailHasGap = live.markerCommitted;
|
|
216
|
+
}
|
|
217
|
+
const tail = assistantLines(live.buffer.slice(live.committed), live.tailHasGap).slice(live.tailLinesCommitted);
|
|
218
|
+
const overflow = Math.max(0, tail.length - liveRowLimit());
|
|
219
|
+
// A table or a code block taller than the screen: its top rows go out for
|
|
220
|
+
// good. A table can still widen below them; that beats drawing it twice.
|
|
221
|
+
if (overflow > 0) {
|
|
222
|
+
commitAssistantLines(live, tail.slice(0, overflow));
|
|
223
|
+
live.tailLinesCommitted += overflow;
|
|
224
|
+
}
|
|
225
|
+
const visible = tail.slice(overflow);
|
|
226
|
+
// Nothing said yet: keep the bare marker up, as `beginAssistantTurn` drew it.
|
|
227
|
+
live.paintedLines = writeAssistantLines(live, visible.length === 0 && live.markerCommitted === false ? [""] : visible);
|
|
189
228
|
if (live.footer !== "") {
|
|
190
229
|
// Clipped for the same reason the spinner's is: it has to occupy exactly
|
|
191
230
|
// one row, or the next repaint clears from the wrong line.
|
|
@@ -216,6 +255,171 @@ export function showAssistantTurn(body) {
|
|
|
216
255
|
appendAssistantDelta(live, body);
|
|
217
256
|
endAssistantTurn(live);
|
|
218
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* How Claude Code's thinking is drawn, the same choices `@ai-sdk/tui` offers:
|
|
260
|
+
*
|
|
261
|
+
* - `full`: the whole summary stays in the transcript.
|
|
262
|
+
* - `auto-collapsed`: the latest lines show while it thinks, then fold into
|
|
263
|
+
* one "Thought for 4s" line once it moves on.
|
|
264
|
+
* - `collapsed`: only that one line.
|
|
265
|
+
* - `hidden`: nothing beyond the spinner.
|
|
266
|
+
*/
|
|
267
|
+
export const REASONING_DISPLAYS = [
|
|
268
|
+
"full",
|
|
269
|
+
"auto-collapsed",
|
|
270
|
+
"collapsed",
|
|
271
|
+
"hidden",
|
|
272
|
+
];
|
|
273
|
+
/**
|
|
274
|
+
* How finished tool calls are drawn: `full` adds the start of each result
|
|
275
|
+
* under its line, `collapsed` keeps one line per call (and the first line of
|
|
276
|
+
* an error), `hidden` leaves only the spinner while they run.
|
|
277
|
+
*/
|
|
278
|
+
export const TOOL_DISPLAYS = ["full", "collapsed", "hidden"];
|
|
279
|
+
const REASONING_MARKER = "∴";
|
|
280
|
+
/** Rows of thinking kept on screen while it streams. */
|
|
281
|
+
const REASONING_LIVE_LINES = 6;
|
|
282
|
+
/** Rows of a tool result shown under it in `full`. */
|
|
283
|
+
const TOOL_OUTPUT_PREVIEW_LINES = 6;
|
|
284
|
+
/**
|
|
285
|
+
* Open a thinking block. Only the expanded displays draw while it streams;
|
|
286
|
+
* `collapsed` leaves the spinner up and adds its one line at the end.
|
|
287
|
+
*/
|
|
288
|
+
export function beginReasoning(display) {
|
|
289
|
+
const live = {
|
|
290
|
+
buffer: "",
|
|
291
|
+
display,
|
|
292
|
+
startedAtMs: Date.now(),
|
|
293
|
+
paintedLines: 0,
|
|
294
|
+
footer: "",
|
|
295
|
+
};
|
|
296
|
+
// No gap before it: thinking sits with the tool lines, not the replies.
|
|
297
|
+
if (drawsWhileThinking(display) === true) {
|
|
298
|
+
repaintReasoning(live);
|
|
299
|
+
}
|
|
300
|
+
return live;
|
|
301
|
+
}
|
|
302
|
+
export function appendReasoningDelta(live, delta) {
|
|
303
|
+
live.buffer += delta;
|
|
304
|
+
if (drawsWhileThinking(live.display) === true) {
|
|
305
|
+
repaintReasoning(live);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
export function setReasoningFooter(live, footer) {
|
|
309
|
+
if (live.footer === footer) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
live.footer = footer;
|
|
313
|
+
if (drawsWhileThinking(live.display) === true) {
|
|
314
|
+
repaintReasoning(live);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/** Close the block, leaving what its display keeps in the transcript. */
|
|
318
|
+
export function endReasoning(live, spinner) {
|
|
319
|
+
clearPaintedLines(live.paintedLines);
|
|
320
|
+
live.paintedLines = 0;
|
|
321
|
+
if (live.display === "hidden") {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const header = `${colors.dim(REASONING_MARKER)} ${colors.dim(`Thought for ${formatDuration(Date.now() - live.startedAtMs)}`)}`;
|
|
325
|
+
if (live.display === "collapsed") {
|
|
326
|
+
// The spinner stayed up through this one; log above it.
|
|
327
|
+
if (spinner !== undefined) {
|
|
328
|
+
spinner.log(header);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
process.stderr.write(`${header}\n`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
process.stderr.write(`${header}\n`);
|
|
335
|
+
if (live.display === "full") {
|
|
336
|
+
for (const line of reasoningLines(live.buffer)) {
|
|
337
|
+
writeRail(line);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function drawsWhileThinking(display) {
|
|
342
|
+
return display === "full" || display === "auto-collapsed";
|
|
343
|
+
}
|
|
344
|
+
function repaintReasoning(live) {
|
|
345
|
+
clearPaintedLines(live.paintedLines);
|
|
346
|
+
const lines = reasoningLines(live.buffer).slice(-REASONING_LIVE_LINES);
|
|
347
|
+
process.stderr.write(`${colors.dim(REASONING_MARKER)} ${colors.dim("Thinking…")}\n`);
|
|
348
|
+
for (const line of lines) {
|
|
349
|
+
writeRail(line);
|
|
350
|
+
}
|
|
351
|
+
live.paintedLines = lines.length + 1;
|
|
352
|
+
if (live.footer !== "") {
|
|
353
|
+
process.stderr.write(`${colors.dim(clipToWidth(live.footer))}\n`);
|
|
354
|
+
live.paintedLines += 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function reasoningLines(text) {
|
|
358
|
+
const body = text.trim();
|
|
359
|
+
if (body === "") {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
return renderMarkdown(body, treeBodyWidth(), "dim");
|
|
363
|
+
}
|
|
364
|
+
/** `4s`, `1m 12s`. */
|
|
365
|
+
function formatDuration(ms) {
|
|
366
|
+
const seconds = Math.max(1, Math.round(ms / 1000));
|
|
367
|
+
if (seconds < 60) {
|
|
368
|
+
return `${String(seconds)}s`;
|
|
369
|
+
}
|
|
370
|
+
return `${String(Math.floor(seconds / 60))}m ${String(seconds % 60)}s`;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* What a turn cost, under the reply: tokens written, how fast they came, how
|
|
374
|
+
* full the context window is, and the credits it was billed.
|
|
375
|
+
*
|
|
376
|
+
* │ 2.4k tokens written · 38 tok/s · 92k of 1M context (9%) · 6.10 credits
|
|
377
|
+
*/
|
|
378
|
+
export function showTurnStats(stats) {
|
|
379
|
+
if (stats.outputTokens === 0) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
// Written, not read: what the model read is the context share below.
|
|
383
|
+
const parts = [`${formatTokens(stats.outputTokens)} tokens written`];
|
|
384
|
+
// Under a second of generation is too short to measure a rate from.
|
|
385
|
+
if (stats.generationMs >= 1000) {
|
|
386
|
+
parts.push(`${String(Math.round(stats.outputTokens / (stats.generationMs / 1000)))} tok/s`);
|
|
387
|
+
}
|
|
388
|
+
if (stats.contextTokens !== undefined && stats.contextTokens > 0) {
|
|
389
|
+
const percent = Math.min(100, Math.max(1, Math.round((stats.contextTokens / stats.contextWindow) * 100)));
|
|
390
|
+
parts.push(`${formatTokens(stats.contextTokens)} of ${formatTokens(stats.contextWindow)} context (${String(percent)}%)`);
|
|
391
|
+
}
|
|
392
|
+
if (stats.credits > 0) {
|
|
393
|
+
parts.push(`${stats.credits.toFixed(2)} credit${stats.credits === 1 ? "" : "s"}`);
|
|
394
|
+
}
|
|
395
|
+
// Broken between items, never inside one, when the line is wider than the
|
|
396
|
+
// rail leaves room for: wrapped by the terminal it would start its next row
|
|
397
|
+
// at column zero, outside the tree.
|
|
398
|
+
const width = treeBodyWidth();
|
|
399
|
+
const rows = [];
|
|
400
|
+
for (const part of parts.map((entry) => clipToCells(entry, width))) {
|
|
401
|
+
const last = rows[rows.length - 1];
|
|
402
|
+
if (last !== undefined && textWidth(`${last} · ${part}`) <= width) {
|
|
403
|
+
rows[rows.length - 1] = `${last} · ${part}`;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
rows.push(part);
|
|
407
|
+
}
|
|
408
|
+
for (const row of rows) {
|
|
409
|
+
writeRail(colors.dim(row));
|
|
410
|
+
}
|
|
411
|
+
writeRail("");
|
|
412
|
+
}
|
|
413
|
+
/** `950`, `2.4k`, `1M`. */
|
|
414
|
+
function formatTokens(count) {
|
|
415
|
+
if (count >= 1_000_000) {
|
|
416
|
+
return `${String(Math.round(count / 100_000) / 10)}M`;
|
|
417
|
+
}
|
|
418
|
+
if (count >= 1_000) {
|
|
419
|
+
return `${String(Math.round(count / 100) / 10)}k`;
|
|
420
|
+
}
|
|
421
|
+
return String(count);
|
|
422
|
+
}
|
|
219
423
|
/** Common tool names arrive lowercase (`bash`); natives keep their case. */
|
|
220
424
|
function toolLabel(toolName) {
|
|
221
425
|
if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
@@ -233,9 +437,7 @@ export function describeToolActivity(toolName, summary) {
|
|
|
233
437
|
return label;
|
|
234
438
|
}
|
|
235
439
|
const line = `${label}: ${summary}`;
|
|
236
|
-
return line
|
|
237
|
-
? `${line.slice(0, ACTIVITY_MAX_LENGTH - 1)}…`
|
|
238
|
-
: line;
|
|
440
|
+
return clipToCells(line, ACTIVITY_MAX_LENGTH);
|
|
239
441
|
}
|
|
240
442
|
/**
|
|
241
443
|
* A tool that has finished, left in the transcript.
|
|
@@ -250,15 +452,84 @@ export function describeToolActivity(toolName, summary) {
|
|
|
250
452
|
* line, leaves this behind, and lets the next frame redraw underneath.
|
|
251
453
|
*/
|
|
252
454
|
export function showToolRun(input) {
|
|
455
|
+
if (input.display === "hidden") {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
253
458
|
const marker = input.isError === true ? colors.red("✗") : colors.dim(TOOL_MARKER);
|
|
254
459
|
const label = describeToolActivity(input.toolName, input.summary);
|
|
255
|
-
const
|
|
460
|
+
const lines = [
|
|
461
|
+
`${marker} ${colors.dim(label)}`,
|
|
462
|
+
...toolOutputPreview(input.output, input.display, input.isError).map((line) => `${colors.dim("│")} ${colors.dim(line)}`),
|
|
463
|
+
];
|
|
464
|
+
for (const line of lines) {
|
|
465
|
+
if (input.spinner !== undefined) {
|
|
466
|
+
input.spinner.log(line);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
process.stderr.write(`${line}\n`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Several calls of one tool that finished in a row, as one line:
|
|
475
|
+
*
|
|
476
|
+
* ⏺ Read 6 files: run.ts, tree.ts, markdown.ts, …
|
|
477
|
+
*
|
|
478
|
+
* Six `Read` lines say less than this one, and push the reply off screen.
|
|
479
|
+
* Only for successful calls under `collapsed`: a failure keeps its own line
|
|
480
|
+
* and its error, and `full` shows each result.
|
|
481
|
+
*/
|
|
482
|
+
export function showToolGroup(input) {
|
|
483
|
+
const count = input.summaries.length;
|
|
484
|
+
const verb = TOOL_GROUP_VERBS.find((entry) => entry.toolName === input.toolName.toLowerCase());
|
|
485
|
+
const head = verb === undefined
|
|
486
|
+
? `${toolLabel(input.toolName)} ×${String(count)}`
|
|
487
|
+
: `${verb.label} ${String(count)} ${verb.noun}`;
|
|
488
|
+
// Paths shown by file name: the directory is usually the same for all.
|
|
489
|
+
const items = input.summaries
|
|
490
|
+
.filter((summary) => summary !== "")
|
|
491
|
+
.map((summary) => isAbsolute(summary) === true ? basename(summary) : summary);
|
|
492
|
+
const label = clipToCells(items.length === 0 ? head : `${head}: ${items.join(", ")}`, treeBodyWidth());
|
|
493
|
+
const line = `${colors.dim(TOOL_MARKER)} ${colors.dim(label)}`;
|
|
256
494
|
if (input.spinner !== undefined) {
|
|
257
495
|
input.spinner.log(line);
|
|
258
496
|
return;
|
|
259
497
|
}
|
|
260
498
|
process.stderr.write(`${line}\n`);
|
|
261
499
|
}
|
|
500
|
+
/** How a run of one built-in tool is counted. Names arrive lowercase. */
|
|
501
|
+
const TOOL_GROUP_VERBS = [
|
|
502
|
+
{ toolName: "read", label: "Read", noun: "files" },
|
|
503
|
+
{ toolName: "edit", label: "Edited", noun: "files" },
|
|
504
|
+
{ toolName: "multiedit", label: "Edited", noun: "files" },
|
|
505
|
+
{ toolName: "write", label: "Wrote", noun: "files" },
|
|
506
|
+
{ toolName: "bash", label: "Ran", noun: "commands" },
|
|
507
|
+
{ toolName: "grep", label: "Ran", noun: "searches" },
|
|
508
|
+
{ toolName: "glob", label: "Listed", noun: "patterns" },
|
|
509
|
+
{ toolName: "webfetch", label: "Fetched", noun: "pages" },
|
|
510
|
+
{ toolName: "websearch", label: "Ran", noun: "web searches" },
|
|
511
|
+
];
|
|
512
|
+
/**
|
|
513
|
+
* The start of a tool's result, clipped to the rail. A failure always gets
|
|
514
|
+
* its first line, so a ✗ says why without switching the display.
|
|
515
|
+
*/
|
|
516
|
+
function toolOutputPreview(output, display, isError) {
|
|
517
|
+
const limit = display === "full" ? TOOL_OUTPUT_PREVIEW_LINES : isError === true ? 1 : 0;
|
|
518
|
+
if (limit === 0) {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
const lines = output
|
|
522
|
+
.replace(/\t/g, " ")
|
|
523
|
+
.split("\n")
|
|
524
|
+
.map((line) => line.trimEnd())
|
|
525
|
+
.filter((line) => line !== "");
|
|
526
|
+
const width = treeBodyWidth();
|
|
527
|
+
const shown = lines.slice(0, limit).map((line) => clipToCells(line, width));
|
|
528
|
+
if (lines.length > limit) {
|
|
529
|
+
shown.push(`… ${String(lines.length - limit)} more line${lines.length - limit === 1 ? "" : "s"}`);
|
|
530
|
+
}
|
|
531
|
+
return shown;
|
|
532
|
+
}
|
|
262
533
|
/**
|
|
263
534
|
* A path inside the session directory reads better relative to it; anything
|
|
264
535
|
* outside stays absolute so it is obvious the turn left the project.
|
|
@@ -7,6 +7,7 @@ export declare const ExitCodes: {
|
|
|
7
7
|
readonly NotFound: 5;
|
|
8
8
|
};
|
|
9
9
|
export type ExitCode = (typeof ExitCodes)[keyof typeof ExitCodes];
|
|
10
|
+
export declare const colorize: (text: string, code: string) => string;
|
|
10
11
|
export declare const colors: {
|
|
11
12
|
red: (s: string) => string;
|
|
12
13
|
green: (s: string) => string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runHandler.d.ts","sourceRoot":"","sources":["../../src/commands/runHandler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runHandler.d.ts","sourceRoot":"","sources":["../../src/commands/runHandler.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,SAAS;;;;;;;CAOZ,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAOlE,eAAO,MAAM,QAAQ,SAAU,MAAM,QAAQ,MAAM,KAAG,MAKrD,CAAC;AAEF,eAAO,MAAM,MAAM;aACR,MAAM,KAAG,MAAM;eACb,MAAM,KAAG,MAAM;gBACd,MAAM,KAAG,MAAM;cACjB,MAAM,KAAG,MAAM;aAChB,MAAM,KAAG,MAAM;cACd,MAAM,KAAG,MAAM;CAC1B,CAAC;AAEF,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAM/C;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAID,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUhE;AAED,KAAK,YAAY,GAAG;IAClB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,KAAK,CAgCpE;AAQD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAmBlE;AAED,MAAM,MAAM,OAAO,GAAG;IAEpB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAGlC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAU5B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAIF,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAwBrD;AAgED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,GAAG,CAQhE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ1E;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,OAAO,EAAE,CAAC,EAAE,EACZ,KAAK,EAAE,MAAM,GACZ;IAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CASxD;AAKD,KAAK,iBAAiB,GAAG;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACzB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,wBAAsB,aAAa,CAAC,CAAC,EACnC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,CAAC,CAAC,CAgEZ;AA0ED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,aAAa,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EAC9D,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC,CAyBZ"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { determineIfIsFetcherError } from "@cargo-ai/api";
|
|
2
|
+
import { clipToCells } from "../utils/textWidth.js";
|
|
2
3
|
export const ExitCodes = {
|
|
3
4
|
Success: 0,
|
|
4
5
|
GenericError: 1,
|
|
@@ -10,7 +11,7 @@ export const ExitCodes = {
|
|
|
10
11
|
const useColor = process.stderr.isTTY === true &&
|
|
11
12
|
process.env["NO_COLOR"] === undefined &&
|
|
12
13
|
process.env["CI"] !== "true";
|
|
13
|
-
const colorize = (text, code) => {
|
|
14
|
+
export const colorize = (text, code) => {
|
|
14
15
|
if (useColor !== true) {
|
|
15
16
|
return text;
|
|
16
17
|
}
|
|
@@ -99,10 +100,12 @@ export function clipToWidth(text, columns) {
|
|
|
99
100
|
// One column short of the edge: writing into the last cell makes some
|
|
100
101
|
// terminals wrap to the next row, which is the thing being avoided.
|
|
101
102
|
const limit = width - 1;
|
|
102
|
-
if (limit <= 1
|
|
103
|
+
if (limit <= 1) {
|
|
103
104
|
return text;
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
+
// Counted in columns: an emoji or a CJK character is two of them, and a
|
|
107
|
+
// line that wraps throws off every redraw that assumed one row.
|
|
108
|
+
return clipToCells(text, limit);
|
|
106
109
|
}
|
|
107
110
|
let activeSpinner;
|
|
108
111
|
export function startSpinner(message) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many terminal columns text takes. `length` counts UTF-16 units, which
|
|
3
|
+
* is wrong both ways: an emoji or a CJK character is one code point drawn two
|
|
4
|
+
* columns wide, and a combining mark or a variation selector is drawn in none.
|
|
5
|
+
* Every width the terminal UI lines up — wrapping, table columns, clipping a
|
|
6
|
+
* line to one row — has to use this, or a single emoji pushes the row past
|
|
7
|
+
* the edge and the next redraw starts from the wrong line.
|
|
8
|
+
*
|
|
9
|
+
* A range table rather than full Unicode East Asian Width data: it covers
|
|
10
|
+
* what a model writes (CJK, Hangul, fullwidth forms, emoji) and nothing else.
|
|
11
|
+
*/
|
|
12
|
+
export declare function textWidth(text: string): number;
|
|
13
|
+
/**
|
|
14
|
+
* Text split into what the terminal draws as one character: a code point
|
|
15
|
+
* with the zero-width marks after it (a combining accent, `U+FE0F` asking for
|
|
16
|
+
* the emoji form, a skin tone). Walk these, not code points, to lay text out.
|
|
17
|
+
*/
|
|
18
|
+
export declare function textCells(text: string): string[];
|
|
19
|
+
/** Columns one entry of `textCells` takes: 0, 1 or 2. */
|
|
20
|
+
export declare function cellWidth(cell: string): number;
|
|
21
|
+
/** The longest prefix of `text` that fits in `width` columns. */
|
|
22
|
+
export declare function sliceToWidth(text: string, width: number): string;
|
|
23
|
+
/** `text` cut to `width` columns, ending in `…` when anything was cut. */
|
|
24
|
+
export declare function clipToCells(text: string, width: number): string;
|
|
25
|
+
//# sourceMappingURL=textWidth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"textWidth.d.ts","sourceRoot":"","sources":["../../src/utils/textWidth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAehD;AAED,yDAAyD;AACzD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsB9C;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAehE;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM/D"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many terminal columns text takes. `length` counts UTF-16 units, which
|
|
3
|
+
* is wrong both ways: an emoji or a CJK character is one code point drawn two
|
|
4
|
+
* columns wide, and a combining mark or a variation selector is drawn in none.
|
|
5
|
+
* Every width the terminal UI lines up — wrapping, table columns, clipping a
|
|
6
|
+
* line to one row — has to use this, or a single emoji pushes the row past
|
|
7
|
+
* the edge and the next redraw starts from the wrong line.
|
|
8
|
+
*
|
|
9
|
+
* A range table rather than full Unicode East Asian Width data: it covers
|
|
10
|
+
* what a model writes (CJK, Hangul, fullwidth forms, emoji) and nothing else.
|
|
11
|
+
*/
|
|
12
|
+
export function textWidth(text) {
|
|
13
|
+
return textCells(text).reduce((total, cell) => total + cellWidth(cell), 0);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Text split into what the terminal draws as one character: a code point
|
|
17
|
+
* with the zero-width marks after it (a combining accent, `U+FE0F` asking for
|
|
18
|
+
* the emoji form, a skin tone). Walk these, not code points, to lay text out.
|
|
19
|
+
*/
|
|
20
|
+
export function textCells(text) {
|
|
21
|
+
const cells = [];
|
|
22
|
+
for (const character of Array.from(text)) {
|
|
23
|
+
const last = cells[cells.length - 1];
|
|
24
|
+
if (last !== undefined && isZeroWidth(character) === true) {
|
|
25
|
+
cells[cells.length - 1] = `${last}${character}`;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
cells.push(character);
|
|
29
|
+
}
|
|
30
|
+
return cells;
|
|
31
|
+
}
|
|
32
|
+
/** Columns one entry of `textCells` takes: 0, 1 or 2. */
|
|
33
|
+
export function cellWidth(cell) {
|
|
34
|
+
const base = cell.codePointAt(0);
|
|
35
|
+
if (base === undefined) {
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
if (isZeroWidth(String.fromCodePoint(base)) === true) {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
// `⚠️`: a symbol that is narrow on its own is drawn as a wide emoji when
|
|
42
|
+
// `U+FE0F` follows it.
|
|
43
|
+
if (cell.includes(EMOJI_PRESENTATION) === true) {
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
if (inRanges(base, DOUBLE_WIDTH) === true) {
|
|
47
|
+
return 2;
|
|
48
|
+
}
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
/** The longest prefix of `text` that fits in `width` columns. */
|
|
52
|
+
export function sliceToWidth(text, width) {
|
|
53
|
+
const state = { used: 0, prefix: "" };
|
|
54
|
+
for (const cell of textCells(text)) {
|
|
55
|
+
const cells = cellWidth(cell);
|
|
56
|
+
if (state.used + cells > width) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
state.used += cells;
|
|
60
|
+
state.prefix += cell;
|
|
61
|
+
}
|
|
62
|
+
return state.prefix;
|
|
63
|
+
}
|
|
64
|
+
/** `text` cut to `width` columns, ending in `…` when anything was cut. */
|
|
65
|
+
export function clipToCells(text, width) {
|
|
66
|
+
if (textWidth(text) <= width) {
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
return `${sliceToWidth(text, Math.max(0, width - 1))}…`;
|
|
70
|
+
}
|
|
71
|
+
function isZeroWidth(character) {
|
|
72
|
+
const code = character.codePointAt(0);
|
|
73
|
+
return code !== undefined && inRanges(code, ZERO_WIDTH) === true;
|
|
74
|
+
}
|
|
75
|
+
function inRanges(code, ranges) {
|
|
76
|
+
return ranges.some(([from, to]) => code >= from && code <= to);
|
|
77
|
+
}
|
|
78
|
+
const EMOJI_PRESENTATION = "️";
|
|
79
|
+
const ZERO_WIDTH = [
|
|
80
|
+
[0x0300, 0x036f], // combining diacritical marks
|
|
81
|
+
[0x200b, 0x200f], // zero-width space, joiners, direction marks
|
|
82
|
+
[0x20d0, 0x20ff], // combining marks for symbols
|
|
83
|
+
[0xfe00, 0xfe0f], // variation selectors
|
|
84
|
+
[0x1f3fb, 0x1f3ff], // emoji skin tones, drawn into the emoji before them
|
|
85
|
+
];
|
|
86
|
+
/**
|
|
87
|
+
* East Asian Wide ranges, and the symbols Unicode draws as emoji by default
|
|
88
|
+
* (`Emoji_Presentation`) inside otherwise narrow blocks: ✅ ✨ ❌ ⭐ and the
|
|
89
|
+
* like, which terminals draw two columns wide.
|
|
90
|
+
*/
|
|
91
|
+
const DOUBLE_WIDTH = [
|
|
92
|
+
[0x1100, 0x115f], // Hangul Jamo
|
|
93
|
+
[0x231a, 0x231b], // ⌚ ⌛
|
|
94
|
+
[0x23e9, 0x23ec], // ⏩ … ⏬
|
|
95
|
+
[0x23f0, 0x23f0], // ⏰
|
|
96
|
+
[0x23f3, 0x23f3], // ⏳
|
|
97
|
+
[0x25fd, 0x25fe], // ◽ ◾
|
|
98
|
+
[0x2614, 0x2615], // ☔ ☕
|
|
99
|
+
[0x2648, 0x2653], // ♈ … ♓
|
|
100
|
+
[0x267f, 0x267f], // ♿
|
|
101
|
+
[0x2693, 0x2693], // ⚓
|
|
102
|
+
[0x26a1, 0x26a1], // ⚡
|
|
103
|
+
[0x26aa, 0x26ab], // ⚪ ⚫
|
|
104
|
+
[0x26bd, 0x26be], // ⚽ ⚾
|
|
105
|
+
[0x26c4, 0x26c5], // ⛄ ⛅
|
|
106
|
+
[0x26ce, 0x26ce], // ⛎
|
|
107
|
+
[0x26d4, 0x26d4], // ⛔
|
|
108
|
+
[0x26ea, 0x26ea], // ⛪
|
|
109
|
+
[0x26f2, 0x26f3], // ⛲ ⛳
|
|
110
|
+
[0x26f5, 0x26f5], // ⛵
|
|
111
|
+
[0x26fa, 0x26fa], // ⛺
|
|
112
|
+
[0x26fd, 0x26fd], // ⛽
|
|
113
|
+
[0x2705, 0x2705], // ✅
|
|
114
|
+
[0x270a, 0x270b], // ✊ ✋
|
|
115
|
+
[0x2728, 0x2728], // ✨
|
|
116
|
+
[0x274c, 0x274c], // ❌
|
|
117
|
+
[0x274e, 0x274e], // ❎
|
|
118
|
+
[0x2753, 0x2755], // ❓ ❔ ❕
|
|
119
|
+
[0x2757, 0x2757], // ❗
|
|
120
|
+
[0x2795, 0x2797], // ➕ ➖ ➗
|
|
121
|
+
[0x27b0, 0x27b0], // ➰
|
|
122
|
+
[0x27bf, 0x27bf], // ➿
|
|
123
|
+
[0x2b1b, 0x2b1c], // ⬛ ⬜
|
|
124
|
+
[0x2b50, 0x2b50], // ⭐
|
|
125
|
+
[0x2b55, 0x2b55], // ⭕
|
|
126
|
+
[0x2e80, 0x303e], // CJK radicals, punctuation
|
|
127
|
+
[0x3041, 0x33ff], // kana, CJK compatibility
|
|
128
|
+
[0x3400, 0x4dbf], // CJK extension A
|
|
129
|
+
[0x4e00, 0x9fff], // CJK unified ideographs
|
|
130
|
+
[0xa000, 0xa4cf], // Yi
|
|
131
|
+
[0xac00, 0xd7a3], // Hangul syllables
|
|
132
|
+
[0xf900, 0xfaff], // CJK compatibility ideographs
|
|
133
|
+
[0xfe30, 0xfe4f], // CJK compatibility forms
|
|
134
|
+
[0xff00, 0xff60], // fullwidth forms
|
|
135
|
+
[0xffe0, 0xffe6], // fullwidth signs
|
|
136
|
+
[0x1f004, 0x1f004], // 🀄
|
|
137
|
+
[0x1f0cf, 0x1f0cf], // 🃏
|
|
138
|
+
[0x1f18e, 0x1f18e], // 🆎
|
|
139
|
+
[0x1f191, 0x1f19a], // 🆑 … 🆚
|
|
140
|
+
[0x1f200, 0x1f2ff], // enclosed ideographic supplement
|
|
141
|
+
[0x1f300, 0x1f64f], // pictographs, emoticons
|
|
142
|
+
[0x1f680, 0x1f6ff], // transport and map symbols
|
|
143
|
+
[0x1f7e0, 0x1f7eb], // 🟠 … 🟫
|
|
144
|
+
[0x1f900, 0x1f9ff], // supplemental symbols and pictographs
|
|
145
|
+
[0x1fa70, 0x1faff], // symbols and pictographs extended-A
|
|
146
|
+
[0x20000, 0x3fffd], // CJK extensions B and on
|
|
147
|
+
];
|