kritya 0.8.2-beta

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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +434 -0
  3. package/dist/agent/budget.js +25 -0
  4. package/dist/agent/compactor.js +78 -0
  5. package/dist/agent/contextWarning.js +6 -0
  6. package/dist/agent/killSwitch.js +111 -0
  7. package/dist/agent/loop.js +543 -0
  8. package/dist/agent/memory.js +145 -0
  9. package/dist/agent/plugins.js +408 -0
  10. package/dist/agent/skills.js +210 -0
  11. package/dist/agent/skillsCli.js +51 -0
  12. package/dist/agent/systemPrompt.js +122 -0
  13. package/dist/agent/tokens.js +55 -0
  14. package/dist/agent/toolExecutor.js +331 -0
  15. package/dist/agent/workflow.js +496 -0
  16. package/dist/agent/worktree.js +116 -0
  17. package/dist/atomicWrite.js +109 -0
  18. package/dist/audit/audit.js +230 -0
  19. package/dist/audit/cli.js +135 -0
  20. package/dist/commands/custom.js +69 -0
  21. package/dist/commands/mcpCommand.js +433 -0
  22. package/dist/commands/pluginsCommand.js +204 -0
  23. package/dist/commands/registry.js +739 -0
  24. package/dist/config/config.js +214 -0
  25. package/dist/config/debug.js +19 -0
  26. package/dist/config/models.js +82 -0
  27. package/dist/config/retention.js +24 -0
  28. package/dist/config/winAcl.js +38 -0
  29. package/dist/crash.js +102 -0
  30. package/dist/engine.js +113 -0
  31. package/dist/git/git.js +46 -0
  32. package/dist/headless.js +274 -0
  33. package/dist/hooks/hooks.js +122 -0
  34. package/dist/index.js +670 -0
  35. package/dist/lsp/client.js +413 -0
  36. package/dist/lsp/manager.js +0 -0
  37. package/dist/lsp/registry.js +62 -0
  38. package/dist/mcp/callback.js +141 -0
  39. package/dist/mcp/client.js +945 -0
  40. package/dist/mcp/login.js +117 -0
  41. package/dist/mcp/oauth.js +345 -0
  42. package/dist/mcp/servers.js +114 -0
  43. package/dist/mcp/spawnWin.js +86 -0
  44. package/dist/mcp/tokens.js +97 -0
  45. package/dist/mcp/transport.js +295 -0
  46. package/dist/net/urlSafety.js +158 -0
  47. package/dist/permissions/danger.js +96 -0
  48. package/dist/permissions/permissions.js +62 -0
  49. package/dist/permissions/rules.js +69 -0
  50. package/dist/plugins/discover.js +108 -0
  51. package/dist/plugins/mcp.js +80 -0
  52. package/dist/provider/client.js +460 -0
  53. package/dist/provider/switchyardClient.js +46 -0
  54. package/dist/provider/switchyardSidecar.js +245 -0
  55. package/dist/provider/textToolCalls.js +140 -0
  56. package/dist/repomap/repoMap.js +168 -0
  57. package/dist/repomap/symbols.js +190 -0
  58. package/dist/session/store.js +328 -0
  59. package/dist/shell/background.js +131 -0
  60. package/dist/shell/sandbox.js +396 -0
  61. package/dist/telemetry/metrics.js +172 -0
  62. package/dist/telemetry/otlp.js +128 -0
  63. package/dist/telemetry/tracer.js +240 -0
  64. package/dist/tools/askUser.js +64 -0
  65. package/dist/tools/bg.js +59 -0
  66. package/dist/tools/common.js +158 -0
  67. package/dist/tools/deepResearch.js +126 -0
  68. package/dist/tools/diff.js +70 -0
  69. package/dist/tools/document/docx.js +44 -0
  70. package/dist/tools/document/pdf.js +190 -0
  71. package/dist/tools/document/pptx.js +148 -0
  72. package/dist/tools/document/types.js +26 -0
  73. package/dist/tools/document/xlsx.js +87 -0
  74. package/dist/tools/document.js +389 -0
  75. package/dist/tools/edit.js +56 -0
  76. package/dist/tools/fetchUrl.js +218 -0
  77. package/dist/tools/fuzzyMatch.js +74 -0
  78. package/dist/tools/glob.js +34 -0
  79. package/dist/tools/grep.js +85 -0
  80. package/dist/tools/ignore.js +28 -0
  81. package/dist/tools/index.js +66 -0
  82. package/dist/tools/ls.js +27 -0
  83. package/dist/tools/lsp.js +239 -0
  84. package/dist/tools/notebook.js +215 -0
  85. package/dist/tools/read.js +32 -0
  86. package/dist/tools/repoMap.js +26 -0
  87. package/dist/tools/secretScan.js +143 -0
  88. package/dist/tools/shell.js +134 -0
  89. package/dist/tools/skills.js +69 -0
  90. package/dist/tools/subagent.js +57 -0
  91. package/dist/tools/tasks.js +43 -0
  92. package/dist/tools/webSearch.js +97 -0
  93. package/dist/tools/write.js +47 -0
  94. package/dist/tools/writeAgent.js +72 -0
  95. package/dist/trust/aiDisclosure.js +45 -0
  96. package/dist/trust/mcpTrust.js +111 -0
  97. package/dist/trust/trust.js +231 -0
  98. package/dist/types.js +1 -0
  99. package/dist/ui/AiDisclosurePrompt.js +23 -0
  100. package/dist/ui/App.js +449 -0
  101. package/dist/ui/Banner.js +71 -0
  102. package/dist/ui/ElicitationPrompt.js +46 -0
  103. package/dist/ui/Markdown.js +148 -0
  104. package/dist/ui/McpTrustPrompt.js +42 -0
  105. package/dist/ui/ModelPicker.js +19 -0
  106. package/dist/ui/PermissionPrompt.js +27 -0
  107. package/dist/ui/SelectList.js +24 -0
  108. package/dist/ui/Spinner.js +12 -0
  109. package/dist/ui/StatusLine.js +11 -0
  110. package/dist/ui/TranscriptItem.js +13 -0
  111. package/dist/ui/TrustPrompt.js +12 -0
  112. package/dist/ui/highlight.js +137 -0
  113. package/dist/ui/inline.js +241 -0
  114. package/dist/ui/mermaid.js +98 -0
  115. package/dist/ui/table.js +148 -0
  116. package/dist/ui/toolOutputPreview.js +34 -0
  117. package/dist/ui/useAgent.js +534 -0
  118. package/dist/ui/useKillSwitch.js +65 -0
  119. package/dist/ui/useSessionResume.js +39 -0
  120. package/dist/ui/useUsageBudget.js +149 -0
  121. package/dist/ui/viewport.js +71 -0
  122. package/dist/undo/undo.js +293 -0
  123. package/dist/version.js +4 -0
  124. package/package.json +122 -0
@@ -0,0 +1,241 @@
1
+ import stringWidth from "string-width";
2
+ /**
3
+ * Inline markdown: bold, italic, code spans, links. Shared by every block type
4
+ * (headers, bullets, paragraphs, table cells) so emphasis markers never leak
5
+ * through as literal asterisks, and so column widths can be measured against
6
+ * what is actually shown rather than the source text.
7
+ */
8
+ export function parseInline(s) {
9
+ const out = [];
10
+ let plain = "";
11
+ const flush = () => {
12
+ if (plain) {
13
+ out.push({ text: decodeEntities(plain) });
14
+ plain = "";
15
+ }
16
+ };
17
+ const push = (tokens, style) => {
18
+ flush();
19
+ for (const t of tokens)
20
+ out.push({ ...t, ...style });
21
+ };
22
+ let i = 0;
23
+ while (i < s.length) {
24
+ const rest = s.slice(i);
25
+ // Code spans win over everything else, so `**` inside one stays literal.
26
+ if (s[i] === "`") {
27
+ const end = s.indexOf("`", i + 1);
28
+ if (end > i + 1) {
29
+ flush();
30
+ out.push({ text: s.slice(i + 1, end), code: true });
31
+ i = end + 1;
32
+ continue;
33
+ }
34
+ }
35
+ // An emphasis run can't open right after a word or another marker, and
36
+ // can't be followed by a space — between them that keeps "2 * 3", "a*b",
37
+ // and glob patterns like **/*.ts from turning into stray italics, while
38
+ // still emphasising content that happens to be punctuation, like **+ / –**.
39
+ if (!/[*_\w]/.test(s[i - 1] ?? "")) {
40
+ const strong = rest.match(/^(\*\*|__)(?=\S)([\s\S]*?\S)\1/);
41
+ if (strong) {
42
+ push(parseInline(strong[2]), { bold: true });
43
+ i += strong[0].length;
44
+ continue;
45
+ }
46
+ const em = rest.match(/^\*(?=\S)([^*]*\S)\*/);
47
+ if (em) {
48
+ push(parseInline(em[1]), { italic: true });
49
+ i += em[0].length;
50
+ continue;
51
+ }
52
+ }
53
+ // Some providers cite sources as 【3†L1-L4】. Keep the reference, drop the
54
+ // internal span bookkeeping, which is unreadable and means nothing here.
55
+ const citation = rest.match(/^【(\d+)[†:][^】]*】/);
56
+ if (citation) {
57
+ flush();
58
+ out.push({ text: `[${citation[1]}]`, dim: true });
59
+ i += citation[0].length;
60
+ continue;
61
+ }
62
+ // Links keep their URL, dimmed — a terminal can't hide it behind the text.
63
+ const link = rest.match(/^\[([^\]]*)\]\((\S+?)(?:\s+"[^"]*")?\)/);
64
+ if (link) {
65
+ push(parseInline(link[1]), {});
66
+ out.push({ text: ` (${link[2]})`, dim: true });
67
+ i += link[0].length;
68
+ continue;
69
+ }
70
+ plain += s[i];
71
+ i++;
72
+ }
73
+ flush();
74
+ return mergeTokens(out);
75
+ }
76
+ const NAMED_ENTITIES = {
77
+ amp: "&",
78
+ lt: "<",
79
+ gt: ">",
80
+ quot: '"',
81
+ apos: "'",
82
+ nbsp: " ",
83
+ hellip: "…",
84
+ mdash: "—",
85
+ ndash: "–",
86
+ lsquo: "‘",
87
+ rsquo: "’",
88
+ ldquo: "“",
89
+ rdquo: "”",
90
+ times: "×",
91
+ bull: "•",
92
+ deg: "°",
93
+ check: "✓",
94
+ };
95
+ /**
96
+ * Models escape markdown-significant characters as HTML entities — `&#124;`
97
+ * for a pipe inside a table cell above all — and a terminal has no HTML layer
98
+ * to undo that. Decoded after the markup is parsed, so `&#42;` can't turn
99
+ * itself into emphasis, and never inside a code span.
100
+ */
101
+ export function decodeEntities(s) {
102
+ if (!s.includes("&"))
103
+ return s;
104
+ return s.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, body) => {
105
+ if (body[0] === "#") {
106
+ const code = Number(body[1] === "x" || body[1] === "X" ? `0x${body.slice(2)}` : body.slice(1));
107
+ return Number.isInteger(code) && code > 0 && code <= 0x10ffff
108
+ ? String.fromCodePoint(code)
109
+ : match;
110
+ }
111
+ return NAMED_ENTITIES[body.toLowerCase()] ?? match;
112
+ });
113
+ }
114
+ function mergeTokens(tokens) {
115
+ const merged = [];
116
+ for (const t of tokens) {
117
+ const last = merged[merged.length - 1];
118
+ if (last && sameStyle(last, t))
119
+ last.text += t.text;
120
+ else
121
+ merged.push({ ...t });
122
+ }
123
+ return merged;
124
+ }
125
+ function sameStyle(a, b) {
126
+ return (!!a.bold === !!b.bold &&
127
+ !!a.italic === !!b.italic &&
128
+ !!a.code === !!b.code &&
129
+ !!a.dim === !!b.dim);
130
+ }
131
+ export function tokensWidth(tokens) {
132
+ return tokens.reduce((n, t) => n + stringWidth(t.text), 0);
133
+ }
134
+ /** Cut a line to `width` display columns, marking it when something was lost. */
135
+ export function truncateToWidth(text, width) {
136
+ if (width <= 0)
137
+ return "";
138
+ if (stringWidth(text) <= width)
139
+ return text;
140
+ let out = "";
141
+ let used = 0;
142
+ for (const ch of text) {
143
+ const w = stringWidth(ch);
144
+ if (used + w > width - 1)
145
+ break;
146
+ out += ch;
147
+ used += w;
148
+ }
149
+ return `${out}…`;
150
+ }
151
+ /** Display width of one line of markdown, ignoring the markup itself. */
152
+ export function inlineWidth(s) {
153
+ return Math.max(0, ...s.split("\n").map((line) => tokensWidth(parseInline(line))));
154
+ }
155
+ /**
156
+ * Wrap to `width` display columns, honouring hard line breaks. Returns one
157
+ * token list per output line, each guaranteed to fit — table cells wrap
158
+ * themselves so the row never has to.
159
+ */
160
+ export function wrapInline(s, width) {
161
+ const limit = Math.max(1, width);
162
+ const lines = [];
163
+ for (const segment of s.split("\n")) {
164
+ let cur = [];
165
+ let curWidth = 0;
166
+ let pendingSpace = false;
167
+ for (const word of toWords(parseInline(segment))) {
168
+ if (/^\s+$/.test(word.text)) {
169
+ if (curWidth)
170
+ pendingSpace = true;
171
+ continue;
172
+ }
173
+ const pieces = splitToWidth(word.text, limit);
174
+ for (let k = 0; k < pieces.length; k++) {
175
+ const pieceWidth = stringWidth(pieces[k]);
176
+ if (curWidth && curWidth + (pendingSpace ? 1 : 0) + pieceWidth > limit) {
177
+ lines.push(mergeTokens(cur));
178
+ cur = [];
179
+ curWidth = 0;
180
+ }
181
+ else if (pendingSpace) {
182
+ // Carry the word's style onto the space so a run of styled words
183
+ // merges back into a single token instead of fragmenting.
184
+ cur.push({ ...word, text: " " });
185
+ curWidth += 1;
186
+ }
187
+ pendingSpace = false;
188
+ cur.push({ ...word, text: pieces[k] });
189
+ curWidth += pieceWidth;
190
+ if (k < pieces.length - 1) {
191
+ lines.push(mergeTokens(cur));
192
+ cur = [];
193
+ curWidth = 0;
194
+ }
195
+ }
196
+ }
197
+ lines.push(mergeTokens(cur));
198
+ }
199
+ return lines;
200
+ }
201
+ function toWords(tokens) {
202
+ const words = [];
203
+ for (const t of tokens) {
204
+ for (const piece of t.text.split(/(\s+)/)) {
205
+ if (piece)
206
+ words.push({ ...t, text: piece });
207
+ }
208
+ }
209
+ return words;
210
+ }
211
+ /**
212
+ * Hard-split a word too long to fit (a URL, a path) at a display-width
213
+ * boundary, preferring a separator near the end of the piece so a long URL
214
+ * breaks after a "/" instead of mid-segment.
215
+ */
216
+ function splitToWidth(text, width) {
217
+ if (stringWidth(text) <= width)
218
+ return [text];
219
+ const pieces = [];
220
+ let cur = "";
221
+ let curWidth = 0;
222
+ let lastBreak = -1;
223
+ for (const ch of text) {
224
+ const w = stringWidth(ch);
225
+ if (curWidth + w > width && cur) {
226
+ // Only rewind to a separator if it doesn't waste much of the line.
227
+ const cut = lastBreak >= 0 && lastBreak >= Math.floor(cur.length * 0.6) ? lastBreak + 1 : cur.length;
228
+ pieces.push(cur.slice(0, cut));
229
+ cur = cur.slice(cut);
230
+ curWidth = stringWidth(cur);
231
+ lastBreak = -1;
232
+ }
233
+ cur += ch;
234
+ curWidth += w;
235
+ if (/[/\\?&=_.-]/.test(ch))
236
+ lastBreak = cur.length - 1;
237
+ }
238
+ if (cur)
239
+ pieces.push(cur);
240
+ return pieces;
241
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Minimal mermaid flowchart parser, scoped to what an LLM typically emits for
3
+ * a hierarchy explanation: `graph TD` / `graph LR` / `flowchart TD` with
4
+ * `id --> id2` or `id[Label] --> id2[Label2]` edges. Anything that isn't a
5
+ * single-rooted tree (multiple parents, cycles, unparsable lines) returns
6
+ * null so the caller can fall back to rendering the raw code block.
7
+ */
8
+ const HEADER_RE = /^\s*(graph|flowchart)\s+(TD|TB|LR|RL|BT)\s*$/i;
9
+ // id, or id[Label] / id(Label) / id{Label} / id((Label)) — brackets are
10
+ // stripped, not matched by shape, since we only render text. Each bracket
11
+ // type is matched separately (rather than a shared character class) so a
12
+ // label containing the other kind of bracket, e.g. `[Cosmos (Universe)]`,
13
+ // isn't cut short at its first inner paren.
14
+ const NODE_RE = /^([A-Za-z0-9_-]+)(?:\[(.*)\]|\((.*)\)|\{(.*)\})?$/;
15
+ const EDGE_RE = /^(.+?)\s*(?:-->|---|-\.->|==>)\s*(.+)$/;
16
+ function parseNode(raw) {
17
+ const m = NODE_RE.exec(raw.trim());
18
+ if (!m)
19
+ return null;
20
+ const label = m[2] ?? m[3] ?? m[4];
21
+ return { id: m[1], label: label ?? m[1] };
22
+ }
23
+ /**
24
+ * Parses the body of a ```mermaid fence. Returns a tree if the graph has
25
+ * exactly one root and every node has at most one parent; otherwise null.
26
+ */
27
+ export function parseMermaidTree(lines) {
28
+ const body = lines.map((l) => l.trim()).filter(Boolean);
29
+ if (!body.length)
30
+ return null;
31
+ if (!HEADER_RE.test(body[0]))
32
+ return null; // require an explicit graph/flowchart header to opt in
33
+ const start = 1;
34
+ const nodes = new Map();
35
+ const parentOf = new Map();
36
+ const order = [];
37
+ const ensure = (id, label) => {
38
+ let n = nodes.get(id);
39
+ if (!n) {
40
+ n = { id, label, children: [] };
41
+ nodes.set(id, n);
42
+ order.push(id);
43
+ }
44
+ else if (label !== id) {
45
+ n.label = label;
46
+ }
47
+ return n;
48
+ };
49
+ for (let i = start; i < body.length; i++) {
50
+ const line = body[i];
51
+ const edge = EDGE_RE.exec(line);
52
+ if (!edge)
53
+ return null; // not the simple edge-list subset we support
54
+ const from = parseNode(edge[1]);
55
+ const to = parseNode(edge[2]);
56
+ if (!from || !to)
57
+ return null;
58
+ const parent = ensure(from.id, from.label);
59
+ const child = ensure(to.id, to.label);
60
+ if (parentOf.has(child.id) && parentOf.get(child.id) !== parent.id)
61
+ return null; // multiple parents
62
+ if (child.id === parent.id)
63
+ return null; // self-loop
64
+ parentOf.set(child.id, parent.id);
65
+ if (!parent.children.includes(child))
66
+ parent.children.push(child);
67
+ }
68
+ const roots = order.filter((id) => !parentOf.has(id));
69
+ if (roots.length !== 1)
70
+ return null;
71
+ // Cycle check: walking from the root must reach every node exactly once.
72
+ const root = nodes.get(roots[0]);
73
+ const visited = new Set();
74
+ const stack = [root];
75
+ while (stack.length) {
76
+ const n = stack.pop();
77
+ if (visited.has(n.id))
78
+ return null;
79
+ visited.add(n.id);
80
+ stack.push(...n.children);
81
+ }
82
+ if (visited.size !== nodes.size)
83
+ return null;
84
+ return root;
85
+ }
86
+ /** Renders a tree as `tree`-style box-drawing lines. */
87
+ export function renderMermaidTree(root) {
88
+ const lines = [root.label];
89
+ const walk = (node, prefix) => {
90
+ node.children.forEach((child, i) => {
91
+ const last = i === node.children.length - 1;
92
+ lines.push(`${prefix}${last ? "└─ " : "├─ "}${child.label}`);
93
+ walk(child, `${prefix}${last ? " " : "│ "}`);
94
+ });
95
+ };
96
+ walk(root, "");
97
+ return lines;
98
+ }
@@ -0,0 +1,148 @@
1
+ // Render and format GitHub-flavored markdown tables in the terminal.
2
+ import { inlineWidth } from "./inline.js";
3
+ /** Gap between columns, in spaces. */
4
+ export const COLUMN_GAP = 2;
5
+ /** A column squeezed below this is unreadable — stack the table instead. */
6
+ export const MIN_READABLE_COLUMN = 14;
7
+ /** More columns than this and a grid is unreadable at any sane terminal width. */
8
+ export const MAX_GRID_COLUMNS = 5;
9
+ const DELIMITER_CELL = /^:?-+:?$/;
10
+ /** Cells may carry their own line breaks: models emit <br> inside them constantly. */
11
+ function normalizeCell(cell) {
12
+ return cell
13
+ .replace(/<br\s*\/?>/gi, "\n")
14
+ .split("\n")
15
+ .map((s) => s.trim())
16
+ .join("\n")
17
+ .trim();
18
+ }
19
+ export function splitRow(line) {
20
+ let s = line.trim();
21
+ if (s.startsWith("|"))
22
+ s = s.slice(1);
23
+ if (s.endsWith("|") && !s.endsWith("\\|"))
24
+ s = s.slice(0, -1);
25
+ const cells = [];
26
+ let cur = "";
27
+ for (let i = 0; i < s.length; i++) {
28
+ if (s[i] === "\\" && s[i + 1] === "|") {
29
+ cur += "|";
30
+ i++;
31
+ continue;
32
+ }
33
+ if (s[i] === "|") {
34
+ cells.push(cur);
35
+ cur = "";
36
+ continue;
37
+ }
38
+ cur += s[i];
39
+ }
40
+ cells.push(cur);
41
+ return cells.map(normalizeCell);
42
+ }
43
+ export function isTableRow(line) {
44
+ return line.includes("|") && splitRow(line).length >= 2;
45
+ }
46
+ function isDelimiterRow(line) {
47
+ if (!line.includes("|"))
48
+ return false;
49
+ const cells = splitRow(line);
50
+ return cells.length >= 2 && cells.every((c) => DELIMITER_CELL.test(c));
51
+ }
52
+ function alignOf(cell) {
53
+ const left = cell.startsWith(":");
54
+ const right = cell.endsWith(":");
55
+ if (left && right)
56
+ return "center";
57
+ if (right)
58
+ return "right";
59
+ return "left";
60
+ }
61
+ /**
62
+ * Recognise a GFM table starting at `lines[start]`. A delimiter row is
63
+ * required, so ordinary prose containing a pipe is never mistaken for one.
64
+ * Returns the index to resume parsing from.
65
+ */
66
+ export function detectTable(lines, start) {
67
+ if (start + 1 >= lines.length)
68
+ return null;
69
+ if (!isTableRow(lines[start]) || !isDelimiterRow(lines[start + 1]))
70
+ return null;
71
+ const header = splitRow(lines[start]);
72
+ const delimiter = splitRow(lines[start + 1]);
73
+ if (delimiter.length !== header.length)
74
+ return null;
75
+ // Once the header and delimiter have committed, any following line with a
76
+ // pipe belongs to the table — including ragged ones with too few cells.
77
+ const rows = [];
78
+ let i = start + 2;
79
+ while (i < lines.length && lines[i].includes("|")) {
80
+ rows.push(fit(splitRow(lines[i]), header.length));
81
+ i++;
82
+ }
83
+ return { table: { header, rows, align: delimiter.map(alignOf) }, next: i };
84
+ }
85
+ function fit(cells, n) {
86
+ if (cells.length === n)
87
+ return cells;
88
+ if (cells.length > n) {
89
+ // Keep overflow visible rather than dropping it on the floor.
90
+ return [...cells.slice(0, n - 1), cells.slice(n - 1).join(" | ")];
91
+ }
92
+ return [...cells, ...Array(n - cells.length).fill("")];
93
+ }
94
+ /** What each column would take if nothing had to give. */
95
+ function naturalWidths(table) {
96
+ return table.header.map((cell, c) => Math.max(inlineWidth(cell), ...table.rows.map((r) => inlineWidth(r[c] ?? "")), 1));
97
+ }
98
+ /**
99
+ * Column widths that fit `maxWidth` display columns. Over budget, the widest
100
+ * columns give ground first; their cells then wrap internally, which is what
101
+ * keeps the row itself from wrapping and destroying the alignment.
102
+ */
103
+ export function columnWidths(table, maxWidth) {
104
+ const n = table.header.length;
105
+ const widths = naturalWidths(table);
106
+ const gaps = COLUMN_GAP * (n - 1);
107
+ const budget = Math.max(n, maxWidth - gaps);
108
+ const shrinkTo = (floor) => {
109
+ let total = widths.reduce((a, b) => a + b, 0);
110
+ while (total > budget) {
111
+ let idx = 0;
112
+ for (let i = 1; i < n; i++)
113
+ if (widths[i] > widths[idx])
114
+ idx = i;
115
+ if (widths[idx] <= floor)
116
+ return;
117
+ widths[idx]--;
118
+ total--;
119
+ }
120
+ };
121
+ shrinkTo(8);
122
+ shrinkTo(1);
123
+ return widths;
124
+ }
125
+ /**
126
+ * Whether a grid is hopeless at this width. Judged against what the table
127
+ * actually needs, not a fixed terminal width — a two-column table of short
128
+ * cells reads fine in a narrow terminal, and a six-column one never does.
129
+ */
130
+ export function shouldStack(table, maxWidth) {
131
+ if (table.header.length > MAX_GRID_COLUMNS)
132
+ return true;
133
+ const needed = naturalWidths(table).reduce((total, w) => total + Math.min(w, MIN_READABLE_COLUMN), COLUMN_GAP * (table.header.length - 1));
134
+ return needed > maxWidth;
135
+ }
136
+ /**
137
+ * While a response is still streaming, hold back a trailing run of table-ish
138
+ * lines that has not yet produced a delimiter row — otherwise it renders as
139
+ * raw pipes for a moment and then snaps into a grid.
140
+ */
141
+ export function dropPartialTrailingTable(lines) {
142
+ let start = lines.length;
143
+ while (start > 0 && isTableRow(lines[start - 1]))
144
+ start--;
145
+ if (start === lines.length)
146
+ return lines;
147
+ return detectTable(lines, start) ? lines : lines.slice(0, start);
148
+ }
@@ -0,0 +1,34 @@
1
+ import { truncateToWidth } from "./inline.js";
2
+ /**
3
+ * Preview of tool output: a few lines by default, everything when verbose.
4
+ * Tabs are expanded first — `read` emits a padded line number and a tab, which
5
+ * the terminal takes to the next 8-column stop, leaving a ragged gap that made
6
+ * previews look broken. Lines are clipped rather than wrapped, so a preview
7
+ * stays the size it claims to be.
8
+ */
9
+ export function toolOutputPreview(output, verbose, width, isError = false) {
10
+ const lines = output
11
+ .replace(/\s+$/, "")
12
+ .split("\n")
13
+ // The untrusted-content fence is an instruction to the model about how to
14
+ // treat what follows. It says nothing to the person reading the screen.
15
+ .filter((l) => !/^<<<(end_)?external_untrusted_content/i.test(l.trim()))
16
+ .map((l) => l.replace(/^(\s*\d+)\t/, "$1 ").replace(/\t/g, " "));
17
+ while (lines.length && !lines[0].trim())
18
+ lines.shift();
19
+ // Drop indentation every line shares — `read` pads its line numbers to five
20
+ // columns, which is dead space in a preview that is already indented.
21
+ const common = Math.min(...lines.filter((l) => l.trim()).map((l) => /^ */.exec(l)[0].length), Infinity);
22
+ if (common > 0 && common < Infinity) {
23
+ for (let i = 0; i < lines.length; i++)
24
+ lines[i] = lines[i].slice(common);
25
+ }
26
+ if (verbose)
27
+ return lines.join("\n");
28
+ // A failure is exactly when the detail is worth the rows.
29
+ const keep = isError ? 8 : 3;
30
+ const head = lines.slice(0, keep).map((l) => truncateToWidth(l, width));
31
+ return lines.length > keep
32
+ ? `${head.join("\n")}\n… (+${lines.length - keep} lines · Ctrl+O)`
33
+ : head.join("\n");
34
+ }