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.
- package/LICENSE +21 -0
- package/README.md +434 -0
- package/dist/agent/budget.js +25 -0
- package/dist/agent/compactor.js +78 -0
- package/dist/agent/contextWarning.js +6 -0
- package/dist/agent/killSwitch.js +111 -0
- package/dist/agent/loop.js +543 -0
- package/dist/agent/memory.js +145 -0
- package/dist/agent/plugins.js +408 -0
- package/dist/agent/skills.js +210 -0
- package/dist/agent/skillsCli.js +51 -0
- package/dist/agent/systemPrompt.js +122 -0
- package/dist/agent/tokens.js +55 -0
- package/dist/agent/toolExecutor.js +331 -0
- package/dist/agent/workflow.js +496 -0
- package/dist/agent/worktree.js +116 -0
- package/dist/atomicWrite.js +109 -0
- package/dist/audit/audit.js +230 -0
- package/dist/audit/cli.js +135 -0
- package/dist/commands/custom.js +69 -0
- package/dist/commands/mcpCommand.js +433 -0
- package/dist/commands/pluginsCommand.js +204 -0
- package/dist/commands/registry.js +739 -0
- package/dist/config/config.js +214 -0
- package/dist/config/debug.js +19 -0
- package/dist/config/models.js +82 -0
- package/dist/config/retention.js +24 -0
- package/dist/config/winAcl.js +38 -0
- package/dist/crash.js +102 -0
- package/dist/engine.js +113 -0
- package/dist/git/git.js +46 -0
- package/dist/headless.js +274 -0
- package/dist/hooks/hooks.js +122 -0
- package/dist/index.js +670 -0
- package/dist/lsp/client.js +413 -0
- package/dist/lsp/manager.js +0 -0
- package/dist/lsp/registry.js +62 -0
- package/dist/mcp/callback.js +141 -0
- package/dist/mcp/client.js +945 -0
- package/dist/mcp/login.js +117 -0
- package/dist/mcp/oauth.js +345 -0
- package/dist/mcp/servers.js +114 -0
- package/dist/mcp/spawnWin.js +86 -0
- package/dist/mcp/tokens.js +97 -0
- package/dist/mcp/transport.js +295 -0
- package/dist/net/urlSafety.js +158 -0
- package/dist/permissions/danger.js +96 -0
- package/dist/permissions/permissions.js +62 -0
- package/dist/permissions/rules.js +69 -0
- package/dist/plugins/discover.js +108 -0
- package/dist/plugins/mcp.js +80 -0
- package/dist/provider/client.js +460 -0
- package/dist/provider/switchyardClient.js +46 -0
- package/dist/provider/switchyardSidecar.js +245 -0
- package/dist/provider/textToolCalls.js +140 -0
- package/dist/repomap/repoMap.js +168 -0
- package/dist/repomap/symbols.js +190 -0
- package/dist/session/store.js +328 -0
- package/dist/shell/background.js +131 -0
- package/dist/shell/sandbox.js +396 -0
- package/dist/telemetry/metrics.js +172 -0
- package/dist/telemetry/otlp.js +128 -0
- package/dist/telemetry/tracer.js +240 -0
- package/dist/tools/askUser.js +64 -0
- package/dist/tools/bg.js +59 -0
- package/dist/tools/common.js +158 -0
- package/dist/tools/deepResearch.js +126 -0
- package/dist/tools/diff.js +70 -0
- package/dist/tools/document/docx.js +44 -0
- package/dist/tools/document/pdf.js +190 -0
- package/dist/tools/document/pptx.js +148 -0
- package/dist/tools/document/types.js +26 -0
- package/dist/tools/document/xlsx.js +87 -0
- package/dist/tools/document.js +389 -0
- package/dist/tools/edit.js +56 -0
- package/dist/tools/fetchUrl.js +218 -0
- package/dist/tools/fuzzyMatch.js +74 -0
- package/dist/tools/glob.js +34 -0
- package/dist/tools/grep.js +85 -0
- package/dist/tools/ignore.js +28 -0
- package/dist/tools/index.js +66 -0
- package/dist/tools/ls.js +27 -0
- package/dist/tools/lsp.js +239 -0
- package/dist/tools/notebook.js +215 -0
- package/dist/tools/read.js +32 -0
- package/dist/tools/repoMap.js +26 -0
- package/dist/tools/secretScan.js +143 -0
- package/dist/tools/shell.js +134 -0
- package/dist/tools/skills.js +69 -0
- package/dist/tools/subagent.js +57 -0
- package/dist/tools/tasks.js +43 -0
- package/dist/tools/webSearch.js +97 -0
- package/dist/tools/write.js +47 -0
- package/dist/tools/writeAgent.js +72 -0
- package/dist/trust/aiDisclosure.js +45 -0
- package/dist/trust/mcpTrust.js +111 -0
- package/dist/trust/trust.js +231 -0
- package/dist/types.js +1 -0
- package/dist/ui/AiDisclosurePrompt.js +23 -0
- package/dist/ui/App.js +449 -0
- package/dist/ui/Banner.js +71 -0
- package/dist/ui/ElicitationPrompt.js +46 -0
- package/dist/ui/Markdown.js +148 -0
- package/dist/ui/McpTrustPrompt.js +42 -0
- package/dist/ui/ModelPicker.js +19 -0
- package/dist/ui/PermissionPrompt.js +27 -0
- package/dist/ui/SelectList.js +24 -0
- package/dist/ui/Spinner.js +12 -0
- package/dist/ui/StatusLine.js +11 -0
- package/dist/ui/TranscriptItem.js +13 -0
- package/dist/ui/TrustPrompt.js +12 -0
- package/dist/ui/highlight.js +137 -0
- package/dist/ui/inline.js +241 -0
- package/dist/ui/mermaid.js +98 -0
- package/dist/ui/table.js +148 -0
- package/dist/ui/toolOutputPreview.js +34 -0
- package/dist/ui/useAgent.js +534 -0
- package/dist/ui/useKillSwitch.js +65 -0
- package/dist/ui/useSessionResume.js +39 -0
- package/dist/ui/useUsageBudget.js +149 -0
- package/dist/ui/viewport.js +71 -0
- package/dist/undo/undo.js +293 -0
- package/dist/version.js +4 -0
- 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 — `|`
|
|
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 `*` 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
|
+
}
|
package/dist/ui/table.js
ADDED
|
@@ -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
|
+
}
|