@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.
Files changed (43) hide show
  1. package/build/commands/ask/harness/bridgeOutput.d.ts +19 -0
  2. package/build/commands/ask/harness/bridgeOutput.d.ts.map +1 -0
  3. package/build/commands/ask/harness/bridgeOutput.js +42 -0
  4. package/build/commands/ask/harness/create.d.ts.map +1 -1
  5. package/build/commands/ask/harness/create.js +2 -1
  6. package/build/commands/ask/harness/index.d.ts +4 -3
  7. package/build/commands/ask/harness/index.d.ts.map +1 -1
  8. package/build/commands/ask/harness/index.js +3 -2
  9. package/build/commands/ask/harness/toolCallReplay.d.ts +31 -0
  10. package/build/commands/ask/harness/toolCallReplay.d.ts.map +1 -1
  11. package/build/commands/ask/harness/toolCallReplay.js +89 -8
  12. package/build/commands/ask/harness/turn.d.ts +17 -0
  13. package/build/commands/ask/harness/turn.d.ts.map +1 -1
  14. package/build/commands/ask/harness/turn.js +85 -3
  15. package/build/commands/ask/harness/usage.d.ts +19 -0
  16. package/build/commands/ask/harness/usage.d.ts.map +1 -1
  17. package/build/commands/ask/harness/usage.js +46 -0
  18. package/build/commands/ask/input/completer.d.ts +1 -1
  19. package/build/commands/ask/input/completer.d.ts.map +1 -1
  20. package/build/commands/ask/input/completer.js +2 -0
  21. package/build/commands/ask/input/prompt.d.ts.map +1 -1
  22. package/build/commands/ask/input/prompt.js +54 -8
  23. package/build/commands/ask/input/slash.d.ts +7 -1
  24. package/build/commands/ask/input/slash.d.ts.map +1 -1
  25. package/build/commands/ask/input/slash.js +10 -0
  26. package/build/commands/ask/session/index.d.ts +2 -1
  27. package/build/commands/ask/session/index.d.ts.map +1 -1
  28. package/build/commands/ask/session/index.js +2 -1
  29. package/build/commands/ask/session/markdown.d.ts +20 -0
  30. package/build/commands/ask/session/markdown.d.ts.map +1 -0
  31. package/build/commands/ask/session/markdown.js +451 -0
  32. package/build/commands/ask/session/run.d.ts.map +1 -1
  33. package/build/commands/ask/session/run.js +194 -12
  34. package/build/commands/ask/session/tree.d.ts +81 -0
  35. package/build/commands/ask/session/tree.d.ts.map +1 -1
  36. package/build/commands/ask/session/tree.js +322 -51
  37. package/build/commands/runHandler.d.ts +1 -0
  38. package/build/commands/runHandler.d.ts.map +1 -1
  39. package/build/commands/runHandler.js +6 -3
  40. package/build/utils/textWidth.d.ts +25 -0
  41. package/build/utils/textWidth.d.ts.map +1 -0
  42. package/build/utils/textWidth.js +147 -0
  43. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  import * as readline from "node:readline";
2
- import { colors } from "../../runHandler.js";
2
+ import { cellWidth, textCells } from "../../../utils/textWidth.js";
3
+ import { clipToWidth, colors } from "../../runHandler.js";
3
4
  import { applyCompletion, ASK_COMPLETION_MENU_LIMIT, listCompletions, } from "./completer.js";
4
5
  /**
5
6
  * Ctrl-C on the Ask line.
@@ -37,6 +38,9 @@ initialLine = "") {
37
38
  let cursor = initialLine.length;
38
39
  let selected = 0;
39
40
  let painted = false;
41
+ // Rows between the ◆ row and the cursor. A line longer than the terminal
42
+ // wraps, and the next clear has to start from the ◆ row, not the cursor's.
43
+ let cursorRow = 0;
40
44
  let finished = false;
41
45
  const wasRaw = isTTYStream(input) ? input.isRaw === true : false;
42
46
  if (isTTYStream(input) === true) {
@@ -171,9 +175,13 @@ initialLine = "") {
171
175
  if (painted === false || process.stderr.isTTY !== true) {
172
176
  return;
173
177
  }
174
- // Cursor sits on the ◆ line; wipe from there through the menu below.
178
+ // Back up to the ◆ row; wipe from there through the menu below.
179
+ if (cursorRow > 0) {
180
+ process.stderr.write(`\x1B[${String(cursorRow)}A`);
181
+ }
175
182
  process.stderr.write("\r\x1B[J");
176
183
  painted = false;
184
+ cursorRow = 0;
177
185
  }
178
186
  function paint() {
179
187
  const matches = visibleMatches();
@@ -181,7 +189,16 @@ initialLine = "") {
181
189
  selected = matches.length === 0 ? 0 : matches.length - 1;
182
190
  }
183
191
  clearFrame();
192
+ const columns = process.stderr.columns === undefined ? 80 : process.stderr.columns;
193
+ // The terminal wraps the line itself; this says where it ends up.
194
+ const end = layoutCells(`◆ ${line}`, columns);
184
195
  process.stderr.write(`${colors.cyan("◆")} ${line}`);
196
+ // Text ending exactly at the edge leaves the cursor parked on that row
197
+ // until something is written. Step onto the next row so the count holds.
198
+ if (line.length > 0 && end.column === columns) {
199
+ process.stderr.write("\r\n");
200
+ }
201
+ let row = end.column === columns ? end.row + 1 : end.row;
185
202
  if (matches.length > 0) {
186
203
  process.stderr.write("\n");
187
204
  for (let index = 0; index < matches.length; index += 1) {
@@ -191,18 +208,25 @@ initialLine = "") {
191
208
  }
192
209
  const active = index === selected;
193
210
  const bullet = active === true ? colors.cyan("●") : colors.dim("○");
194
- const label = active === true ? colors.cyan(hit) : colors.dim(hit);
211
+ // One row each, or the count of rows to climb back is off.
212
+ const text = clipToWidth(hit, columns - 5);
213
+ const label = active === true ? colors.cyan(text) : colors.dim(text);
195
214
  process.stderr.write(`${colors.dim("│")} ${bullet} ${label}`);
196
215
  if (index < matches.length - 1) {
197
216
  process.stderr.write("\n");
198
217
  }
199
218
  }
200
- // Return to the ◆ line so the next clearFrame / keystroke is correct.
201
- process.stderr.write(`\x1B[${String(matches.length)}A`);
219
+ row += matches.length;
220
+ }
221
+ // Back to the cursor's cell inside the (possibly wrapped) line.
222
+ const at = layoutCells(`◆ ${line.slice(0, cursor)}`, columns);
223
+ const targetRow = at.column === columns ? at.row + 1 : at.row;
224
+ const targetColumn = (at.column === columns ? 0 : at.column) + 1;
225
+ if (row > targetRow) {
226
+ process.stderr.write(`\x1B[${String(row - targetRow)}A`);
202
227
  }
203
- // Column after `◆ ` (3 visible cells) plus the cursor in the line.
204
- const column = 3 + cursor + 1;
205
- process.stderr.write(`\r\x1B[${String(column)}G`);
228
+ process.stderr.write(`\r\x1B[${String(targetColumn)}G`);
229
+ cursorRow = targetRow;
206
230
  painted = true;
207
231
  }
208
232
  function cleanup() {
@@ -232,3 +256,25 @@ initialLine = "") {
232
256
  }
233
257
  });
234
258
  }
259
+ /**
260
+ * Where the terminal leaves the cursor after writing `text` from the start of
261
+ * a row: the row it is on, and how many columns of it are filled. A filled
262
+ * row stays the cursor's row until the next character, which is why `column`
263
+ * can equal `columns`.
264
+ *
265
+ * Walked character by character rather than divided out: an emoji or a CJK
266
+ * character takes two columns, and one that does not fit in the last column
267
+ * starts the next row and leaves that column empty.
268
+ */
269
+ function layoutCells(text, columns) {
270
+ const position = { row: 0, column: 0 };
271
+ for (const cell of textCells(text)) {
272
+ const width = cellWidth(cell);
273
+ if (position.column + width > columns) {
274
+ position.row += 1;
275
+ position.column = 0;
276
+ }
277
+ position.column += width;
278
+ }
279
+ return position;
280
+ }
@@ -6,7 +6,13 @@ export type SlashAction = {
6
6
  kind: "action";
7
7
  action: "exit" | "new" | "clear" | "pick-agent" | "cancel";
8
8
  };
9
- export type SlashResult = SlashCard | SlashAction;
9
+ /** `/tools` or `/reasoning`, with the display asked for if one was given. */
10
+ export type SlashDisplay = {
11
+ kind: "display";
12
+ setting: "tools" | "reasoning";
13
+ value: string | undefined;
14
+ };
15
+ export type SlashResult = SlashCard | SlashAction | SlashDisplay;
10
16
  /**
11
17
  * Parse a user line that starts with `/`. Anything else is prose for the agent.
12
18
  *
@@ -1 +1 @@
1
- {"version":3,"file":"slash.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/input/slash.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,YAAY,GAAG,QAAQ,CAAC;CAC5D,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,CAAC;AAmBlD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAqDhE;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;gBAE3B,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC;CAK1C"}
1
+ {"version":3,"file":"slash.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/input/slash.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,YAAY,GAAG,QAAQ,CAAC;CAC5D,CAAC;AAEF,6EAA6E;AAC7E,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,OAAO,GAAG,WAAW,CAAC;IAC/B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,YAAY,CAAC;AAqBjE;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CA+DhE;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;gBAE3B,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC;CAK1C"}
@@ -6,6 +6,8 @@ const HELP_TEXT = [
6
6
  "/clear Start a fresh local session (no agent)",
7
7
  "/new Same as /clear",
8
8
  "/agent Pick a workspace agent, or Local workspace (default). Stays on this machine.",
9
+ "/tools How tool calls show: full, collapsed (default), hidden",
10
+ "/reasoning How thinking shows: full, auto-collapsed (default), collapsed, hidden",
9
11
  "/cancel Explain how to stop a turn (press Ctrl-C while it runs)",
10
12
  "/exit Leave without asking (same as an empty line)",
11
13
  "",
@@ -52,6 +54,14 @@ export function parseSlash(text) {
52
54
  if (command === "agent") {
53
55
  return { kind: "action", action: "pick-agent" };
54
56
  }
57
+ if (command === "tools" || command === "reasoning") {
58
+ const value = trimmed.split(/\s+/)[1];
59
+ return {
60
+ kind: "display",
61
+ setting: command,
62
+ value: value === undefined ? undefined : value.toLowerCase(),
63
+ };
64
+ }
55
65
  if (command === "cancel") {
56
66
  return { kind: "action", action: "cancel" };
57
67
  }
@@ -1,5 +1,6 @@
1
1
  export { type ChatTurnClaim, claimChatTurn, proxyTokenDeadlineMs, } from "./claim.js";
2
+ export { renderMarkdown } from "./markdown.js";
2
3
  export { type AskSessionOptions, doesChatExist, runAskPrint, runAskSession, } from "./run.js";
3
4
  export { ASK_THINKING_WORDS, pickThinkingWord } from "./thinking.js";
4
- export { displayPath, wrapLine } from "./tree.js";
5
+ export { displayPath, showTurnStats, wrapLine } from "./tree.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/session/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,aAAa,EACb,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,KAAK,iBAAiB,EACtB,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/session/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,aAAa,EACb,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EACL,KAAK,iBAAiB,EACtB,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export { claimChatTurn, proxyTokenDeadlineMs, } from "./claim.js";
2
+ export { renderMarkdown } from "./markdown.js";
2
3
  export { doesChatExist, runAskPrint, runAskSession, } from "./run.js";
3
4
  export { ASK_THINKING_WORDS, pickThinkingWord } from "./thinking.js";
4
- export { displayPath, wrapLine } from "./tree.js";
5
+ export { displayPath, showTurnStats, wrapLine } from "./tree.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The model's markdown, drawn for the tree: headings, lists, quotes, code
3
+ * fences, rules and tables, with bold, italic, code and links inline.
4
+ *
5
+ * The block and table handling follows `@ai-sdk/tui`'s renderer
6
+ * (`src/tui/markdown.ts`). It is not exported from that package, and its
7
+ * output assumes it owns the whole width, so it is redone here around the
8
+ * rail: every line comes back already wrapped to `width`, because a line the
9
+ * terminal soft-wraps starts its next row at column zero, outside the tree.
10
+ *
11
+ * Styles are applied word by word rather than across a span. A colour code
12
+ * here ends with a full reset, so a bold phrase that wraps would lose its
13
+ * bold at the rail drawn in between.
14
+ *
15
+ * Exported for tests.
16
+ */
17
+ export declare function renderMarkdown(text: string, width: number, tone?: MarkdownTone): string[];
18
+ /** `dim` is for text that is not the answer, such as the model's reasoning. */
19
+ export type MarkdownTone = "normal" | "dim";
20
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/session/markdown.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,YAAuB,GAC5B,MAAM,EAAE,CA6CV;AAED,+EAA+E;AAC/E,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,KAAK,CAAC"}
@@ -0,0 +1,451 @@
1
+ import { clipToCells, sliceToWidth, textCells, textWidth, } from "../../../utils/textWidth.js";
2
+ import { colorize } from "../../runHandler.js";
3
+ /**
4
+ * The model's markdown, drawn for the tree: headings, lists, quotes, code
5
+ * fences, rules and tables, with bold, italic, code and links inline.
6
+ *
7
+ * The block and table handling follows `@ai-sdk/tui`'s renderer
8
+ * (`src/tui/markdown.ts`). It is not exported from that package, and its
9
+ * output assumes it owns the whole width, so it is redone here around the
10
+ * rail: every line comes back already wrapped to `width`, because a line the
11
+ * terminal soft-wraps starts its next row at column zero, outside the tree.
12
+ *
13
+ * Styles are applied word by word rather than across a span. A colour code
14
+ * here ends with a full reset, so a bold phrase that wraps would lose its
15
+ * bold at the rail drawn in between.
16
+ *
17
+ * Exported for tests.
18
+ */
19
+ export function renderMarkdown(text, width, tone = "normal") {
20
+ const lines = text.split("\n");
21
+ const output = [];
22
+ const cursor = { index: 0, inFence: false };
23
+ while (cursor.index < lines.length) {
24
+ const line = lines[cursor.index];
25
+ if (line === undefined) {
26
+ break;
27
+ }
28
+ if (FENCE.test(line) === true) {
29
+ cursor.inFence = cursor.inFence === false;
30
+ cursor.index += 1;
31
+ continue;
32
+ }
33
+ if (cursor.inFence === true) {
34
+ // Code keeps its shape: clipped, never re-flowed, behind a bar that
35
+ // sets it apart from the prose around it.
36
+ output.push(`${paint(CODE_BAR, "url", tone)}${paint(clipToCells(line.replace(/\t/g, " "), width - CODE_BAR.length), "code", tone)}`);
37
+ cursor.index += 1;
38
+ continue;
39
+ }
40
+ const table = parseTable(lines, cursor.index);
41
+ if (table !== undefined) {
42
+ output.push(...renderTable(table, width, tone));
43
+ cursor.index = table.endIndex;
44
+ continue;
45
+ }
46
+ output.push(...renderBlockLine(line, width, tone));
47
+ cursor.index += 1;
48
+ }
49
+ return output;
50
+ }
51
+ const STYLE_CODES = {
52
+ plain: [],
53
+ bold: ["1"],
54
+ italic: ["3"],
55
+ code: ["36"],
56
+ link: ["4"],
57
+ url: ["2"],
58
+ };
59
+ const FENCE = /^\s*(```|~~~)/;
60
+ const HEADING = /^#{1,6}\s+(.*)$/;
61
+ const RULE = /^\s*([-*_])(\s*\1){2,}\s*$/;
62
+ const LIST_ITEM = /^(\s*)([-+*]|\d+[.)])\s+(.*)$/;
63
+ const QUOTE = /^\s*>\s?(.*)$/;
64
+ /**
65
+ * Code, bold, italic and links. `_` only opens italics at a word boundary, so
66
+ * `snake_case_names` stay as written.
67
+ */
68
+ const INLINE = /`([^`]+)`|\*\*(.+?)\*\*|__(.+?)__|(?<![\w*])\*(?![\s*])(.+?)(?<![\s*])\*(?![\w*])|(?<!\w)_(?![\s_])(.+?)(?<![\s_])_(?!\w)|\[([^\]]+)\]\(([^)\s]+)\)/g;
69
+ const TABLE_COLUMN_GAP = " ";
70
+ const CODE_BAR = "▎ ";
71
+ const STACKED_INDENT = " ";
72
+ /** How narrow a table column may be squeezed before it is left to overflow. */
73
+ const MIN_TABLE_COLUMN_WIDTH = 6;
74
+ function renderBlockLine(line, width, tone) {
75
+ if (line.trim() === "") {
76
+ return [""];
77
+ }
78
+ const heading = line.match(HEADING);
79
+ if (heading !== null) {
80
+ const words = parseInline(heading[1] === undefined ? "" : heading[1]);
81
+ return wrapWords(emphasize(words), width).map((row) => renderWords(row, tone));
82
+ }
83
+ if (RULE.test(line) === true) {
84
+ return [paint("─".repeat(Math.min(width, 40)), "url", tone)];
85
+ }
86
+ const item = line.match(LIST_ITEM);
87
+ if (item !== null) {
88
+ const indent = item[1] === undefined ? "" : item[1];
89
+ const marker = item[2] === undefined ? "-" : item[2];
90
+ const body = item[3] === undefined ? "" : item[3];
91
+ const bullet = /\d/.test(marker) === true ? marker : "•";
92
+ return renderHanging(`${indent}${bullet} `, parseInline(body), width, tone);
93
+ }
94
+ const quote = line.match(QUOTE);
95
+ if (quote !== null) {
96
+ const body = quote[1] === undefined ? "" : quote[1];
97
+ return wrapWords(parseInline(body), Math.max(1, width - 2)).map((row) => `${paint("│", "url", tone)} ${renderWords(row, tone)}`);
98
+ }
99
+ return wrapWords(parseInline(line), width).map((row) => renderWords(row, tone));
100
+ }
101
+ /** A list item: the marker on the first row, its width of indent after. */
102
+ function renderHanging(prefix, words, width, tone) {
103
+ const rows = wrapWords(words, Math.max(1, width - textWidth(prefix)));
104
+ if (rows.length === 0) {
105
+ return [prefix.trimEnd()];
106
+ }
107
+ const pad = " ".repeat(prefix.length);
108
+ return rows.map((row, index) => `${index === 0 ? prefix : pad}${renderWords(row, tone)}`);
109
+ }
110
+ function parseInline(text) {
111
+ const pieces = [];
112
+ const cursor = { at: 0 };
113
+ for (const match of text.matchAll(INLINE)) {
114
+ const start = match.index === undefined ? cursor.at : match.index;
115
+ if (start > cursor.at) {
116
+ pieces.push({ text: text.slice(cursor.at, start), style: "plain" });
117
+ }
118
+ pieces.push(...inlineMatchPieces(match));
119
+ cursor.at = start + match[0].length;
120
+ }
121
+ if (cursor.at < text.length) {
122
+ pieces.push({ text: text.slice(cursor.at), style: "plain" });
123
+ }
124
+ return toWords(pieces);
125
+ }
126
+ function inlineMatchPieces(match) {
127
+ const [, code, bold, boldUnderscore, italic, italicUnderscore, label, url] = match;
128
+ if (code !== undefined) {
129
+ return [{ text: code, style: "code" }];
130
+ }
131
+ if (bold !== undefined) {
132
+ return [{ text: bold, style: "bold" }];
133
+ }
134
+ if (boldUnderscore !== undefined) {
135
+ return [{ text: boldUnderscore, style: "bold" }];
136
+ }
137
+ if (italic !== undefined) {
138
+ return [{ text: italic, style: "italic" }];
139
+ }
140
+ if (italicUnderscore !== undefined) {
141
+ return [{ text: italicUnderscore, style: "italic" }];
142
+ }
143
+ if (label !== undefined && url !== undefined) {
144
+ // A bare link is written `[url](url)`; saying it twice adds nothing.
145
+ if (label === url) {
146
+ return [{ text: label, style: "link", href: url }];
147
+ }
148
+ return [
149
+ { text: label, style: "link", href: url },
150
+ { text: ` (${url})`, style: "url" },
151
+ ];
152
+ }
153
+ return [{ text: match[0], style: "plain" }];
154
+ }
155
+ /** Split styled runs on whitespace, gluing runs that touch into one word. */
156
+ function toWords(pieces) {
157
+ const words = [];
158
+ const state = { glued: false };
159
+ for (const piece of pieces) {
160
+ for (const part of piece.text.split(/(\s+)/)) {
161
+ if (part === "") {
162
+ continue;
163
+ }
164
+ if (/^\s+$/.test(part) === true) {
165
+ state.glued = false;
166
+ continue;
167
+ }
168
+ const last = words[words.length - 1];
169
+ if (state.glued === true && last !== undefined) {
170
+ last.push({ ...piece, text: part });
171
+ }
172
+ else {
173
+ words.push([{ ...piece, text: part }]);
174
+ }
175
+ state.glued = true;
176
+ }
177
+ }
178
+ return words;
179
+ }
180
+ /** Headings and table headers: everything plain becomes bold. */
181
+ function emphasize(words) {
182
+ return words.map((word) => word.map((piece) => piece.style === "plain" ? { ...piece, style: "bold" } : piece));
183
+ }
184
+ function wordWidth(word) {
185
+ return word.reduce((total, piece) => total + textWidth(piece.text), 0);
186
+ }
187
+ function rowWidth(row) {
188
+ if (row.length === 0) {
189
+ return 0;
190
+ }
191
+ return (row.reduce((total, word) => total + wordWidth(word), 0) + row.length - 1);
192
+ }
193
+ /**
194
+ * Greedy wrap on spaces. A word wider than the line is cut, unlike the plain
195
+ * wrap in `tree.ts`: a table cell or a long URL left whole would overflow,
196
+ * and the terminal would wrap it off the rail.
197
+ */
198
+ function wrapWords(words, width) {
199
+ const rows = [];
200
+ for (const word of words.flatMap((entry) => splitWord(entry, width))) {
201
+ const current = rows[rows.length - 1];
202
+ if (current === undefined ||
203
+ rowWidth(current) + 1 + wordWidth(word) > width) {
204
+ rows.push([word]);
205
+ continue;
206
+ }
207
+ current.push(word);
208
+ }
209
+ return rows;
210
+ }
211
+ function splitWord(word, width) {
212
+ if (wordWidth(word) <= width) {
213
+ return [word];
214
+ }
215
+ const chunks = [[]];
216
+ for (const piece of word) {
217
+ const remaining = { text: piece.text };
218
+ while (remaining.text.length > 0) {
219
+ const chunk = chunks[chunks.length - 1];
220
+ if (chunk === undefined) {
221
+ break;
222
+ }
223
+ const room = width - wordWidth(chunk);
224
+ if (room <= 0 && chunk.length > 0) {
225
+ chunks.push([]);
226
+ continue;
227
+ }
228
+ const fits = sliceToWidth(remaining.text, room);
229
+ // A two-column character with one column left: start the next row,
230
+ // unless this row is empty and the line is narrower than the character.
231
+ if (fits === "" && chunk.length > 0) {
232
+ chunks.push([]);
233
+ continue;
234
+ }
235
+ const taken = fits === "" ? textCells(remaining.text)[0] : fits;
236
+ if (taken === undefined) {
237
+ break;
238
+ }
239
+ chunk.push({ ...piece, text: taken });
240
+ remaining.text = remaining.text.slice(taken.length);
241
+ }
242
+ }
243
+ return chunks.filter((chunk) => chunk.length > 0);
244
+ }
245
+ function renderWords(row, tone) {
246
+ return row
247
+ .map((word) => word
248
+ .map((piece) => hyperlink(paint(piece.text, piece.style, tone), piece))
249
+ .join(""))
250
+ .join(" ");
251
+ }
252
+ /**
253
+ * OSC 8: the terminal shows `text` and opens `href` when it is clicked. Each
254
+ * word of a link is wrapped on its own, like its colour, so a link that wraps
255
+ * onto the next row stays clickable on both. Terminals without OSC 8 ignore
256
+ * the sequence, and the URL printed after the label still shows where it
257
+ * goes. Left out when stderr is not a terminal, along with the colours.
258
+ */
259
+ function hyperlink(text, piece) {
260
+ if (piece.href === undefined || process.stderr.isTTY !== true) {
261
+ return text;
262
+ }
263
+ return `\x1B]8;;${piece.href}\x1B\\${text}\x1B]8;;\x1B\\`;
264
+ }
265
+ function paint(text, style, tone) {
266
+ const codes = tone === "dim" ? ["2", ...STYLE_CODES[style]] : STYLE_CODES[style];
267
+ if (codes.length === 0) {
268
+ return text;
269
+ }
270
+ return colorize(text, codes.join(";"));
271
+ }
272
+ function parseTable(lines, startIndex) {
273
+ const headerLine = lines[startIndex];
274
+ const separatorLine = lines[startIndex + 1];
275
+ if (headerLine === undefined || separatorLine === undefined) {
276
+ return undefined;
277
+ }
278
+ const header = parseTableCells(headerLine);
279
+ if (header === undefined || header.length < 2) {
280
+ return undefined;
281
+ }
282
+ const separator = parseTableCells(separatorLine);
283
+ if (separator === undefined || separator.length !== header.length) {
284
+ return undefined;
285
+ }
286
+ const alignments = parseTableAlignments(separator);
287
+ if (alignments === undefined) {
288
+ return undefined;
289
+ }
290
+ const rows = [];
291
+ const cursor = { index: startIndex + 2 };
292
+ while (cursor.index < lines.length) {
293
+ const line = lines[cursor.index];
294
+ const row = line === undefined ? undefined : parseTableCells(line);
295
+ if (row === undefined) {
296
+ break;
297
+ }
298
+ rows.push(Array.from({ length: header.length }, (_, index) => {
299
+ const cell = row[index];
300
+ return cell === undefined ? "" : cell;
301
+ }));
302
+ cursor.index += 1;
303
+ }
304
+ return { alignments, endIndex: cursor.index, header, rows };
305
+ }
306
+ function parseTableCells(line) {
307
+ const trimmed = line.trim();
308
+ if (trimmed.includes("|") === false) {
309
+ return undefined;
310
+ }
311
+ const inner = trimmed.replace(/^\|/, "").replace(/(?<!\\)\|$/, "");
312
+ const cells = [""];
313
+ for (let index = 0; index < inner.length; index += 1) {
314
+ const character = inner[index];
315
+ if (character === "\\" && inner[index + 1] === "|") {
316
+ cells[cells.length - 1] += "|";
317
+ index += 1;
318
+ continue;
319
+ }
320
+ if (character === "|") {
321
+ cells.push("");
322
+ continue;
323
+ }
324
+ cells[cells.length - 1] += character === undefined ? "" : character;
325
+ }
326
+ return cells.map((cell) => cell.trim());
327
+ }
328
+ function parseTableAlignments(cells) {
329
+ const alignments = [];
330
+ for (const cell of cells) {
331
+ const match = cell.match(/^(:)?-{3,}(:)?$/);
332
+ if (match === null) {
333
+ return undefined;
334
+ }
335
+ const [, left, right] = match;
336
+ if (left !== undefined && right !== undefined) {
337
+ alignments.push("center");
338
+ }
339
+ else if (right !== undefined) {
340
+ alignments.push("right");
341
+ }
342
+ else {
343
+ alignments.push("left");
344
+ }
345
+ }
346
+ return alignments;
347
+ }
348
+ /**
349
+ * Columns sized to their content, then the widest squeezed until the table
350
+ * fits beside the rail; a cell that no longer fits wraps inside its column.
351
+ */
352
+ function renderTable(table, width, tone) {
353
+ const header = table.header.map((cell) => emphasize(parseInline(cell)));
354
+ const rows = table.rows.map((row) => row.map((cell) => parseInline(cell)));
355
+ const widths = fitColumns(table.alignments.map((_, column) => Math.max(3, ...[header, ...rows].map((row) => {
356
+ const cell = row[column];
357
+ return cell === undefined ? 0 : rowWidth(cell);
358
+ }))), width);
359
+ const tableWidth = widths.reduce((total, columnWidth) => total + columnWidth, 0) +
360
+ TABLE_COLUMN_GAP.length * (widths.length - 1);
361
+ // Too many columns to fit even squeezed: the terminal would wrap every row
362
+ // off the rail. One record per row reads better than six-column slivers.
363
+ if (tableWidth > width) {
364
+ return renderStackedTable(header, rows, width, tone);
365
+ }
366
+ return [
367
+ ...renderTableRow(header, widths, table.alignments, tone),
368
+ paint(widths
369
+ .map((columnWidth) => "─".repeat(columnWidth))
370
+ .join(TABLE_COLUMN_GAP), "url", tone),
371
+ ...rows.flatMap((row) => renderTableRow(row, widths, table.alignments, tone)),
372
+ ];
373
+ }
374
+ /**
375
+ * A table too wide for the screen, as one block per row:
376
+ *
377
+ * Name: Acme
378
+ * Industry: Software
379
+ *
380
+ * Name: Globex
381
+ * …
382
+ *
383
+ * Each field wraps with its continuation indented under the header.
384
+ */
385
+ function renderStackedTable(header, rows, width, tone) {
386
+ // Only the header so far, mid-stream or for good: show the field names
387
+ // rather than nothing.
388
+ if (rows.length === 0) {
389
+ return wrapWords(header.flatMap((label, column) => column < header.length - 1 ? appendText(label, ",") : label), width).map((line) => renderWords(line, tone));
390
+ }
391
+ return rows.flatMap((row, index) => {
392
+ const fields = header.flatMap((label, column) => {
393
+ const cell = row[column];
394
+ if (cell === undefined || cell.length === 0) {
395
+ return [];
396
+ }
397
+ return wrapWords([...appendText(label, ":"), ...cell], Math.max(1, width - STACKED_INDENT.length)).map((line, at) => at === 0
398
+ ? renderWords(line, tone)
399
+ : `${STACKED_INDENT}${renderWords(line, tone)}`);
400
+ });
401
+ return index === 0 ? fields : ["", ...fields];
402
+ });
403
+ }
404
+ /** `Name` → `Name:`, the mark glued to the header's last word. */
405
+ function appendText(label, mark) {
406
+ const last = label[label.length - 1];
407
+ if (last === undefined) {
408
+ return [];
409
+ }
410
+ return [...label.slice(0, -1), [...last, { text: mark, style: "bold" }]];
411
+ }
412
+ function fitColumns(natural, width) {
413
+ const widths = [...natural];
414
+ const available = width - TABLE_COLUMN_GAP.length * (widths.length - 1);
415
+ while (widths.reduce((total, columnWidth) => total + columnWidth, 0) > available) {
416
+ const widest = Math.max(...widths);
417
+ if (widest <= MIN_TABLE_COLUMN_WIDTH) {
418
+ break;
419
+ }
420
+ widths[widths.indexOf(widest)] = widest - 1;
421
+ }
422
+ return widths;
423
+ }
424
+ function renderTableRow(cells, widths, alignments, tone) {
425
+ const wrapped = widths.map((columnWidth, column) => {
426
+ const cell = cells[column];
427
+ return wrapWords(cell === undefined ? [] : cell, columnWidth);
428
+ });
429
+ const height = Math.max(1, ...wrapped.map((lines) => lines.length));
430
+ return Array.from({ length: height }, (_, line) => widths
431
+ .map((columnWidth, column) => {
432
+ const columnLines = wrapped[column];
433
+ const row = columnLines === undefined ? undefined : columnLines[line];
434
+ const alignment = alignments[column];
435
+ return alignCell(row === undefined ? [] : row, columnWidth, alignment === undefined ? "left" : alignment, tone);
436
+ })
437
+ .join(TABLE_COLUMN_GAP)
438
+ .trimEnd());
439
+ }
440
+ function alignCell(row, width, alignment, tone) {
441
+ const text = renderWords(row, tone);
442
+ const padding = Math.max(0, width - rowWidth(row));
443
+ if (alignment === "right") {
444
+ return `${" ".repeat(padding)}${text}`;
445
+ }
446
+ if (alignment === "center") {
447
+ const left = Math.floor(padding / 2);
448
+ return `${" ".repeat(left)}${text}${" ".repeat(padding - left)}`;
449
+ }
450
+ return `${text}${" ".repeat(padding)}`;
451
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/session/run.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAoD3C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAkHF;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAyND;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC,CAsmB7B;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE;IACvC,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,IAAI,CAAC,CAqGhB"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/session/run.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAkE3C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAwHF;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAyND;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC,CA0zB7B;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE;IACvC,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,IAAI,CAAC,CAqGhB"}