@ouro.bot/cli 0.1.0-alpha.175 → 0.1.0-alpha.177

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.
@@ -0,0 +1,486 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.StreamingMarkdown = StreamingMarkdown;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = __importDefault(require("react"));
9
+ const ink_1 = require("ink");
10
+ // ─── Block Parser ──────────────────────────────────────────────────
11
+ const HR_PATTERN = /^(\s*[-*_]){3,}\s*$/;
12
+ /** Detect if a line is a diff header or hunk marker. */
13
+ function isDiffLine(line) {
14
+ return (line.startsWith("diff --git") ||
15
+ line.startsWith("index ") ||
16
+ line.startsWith("--- ") ||
17
+ line.startsWith("+++ ") ||
18
+ line.startsWith("@@") ||
19
+ line.startsWith("+") ||
20
+ line.startsWith("-"));
21
+ }
22
+ /** Check if a block of consecutive lines looks like a unified diff. */
23
+ function looksLikeDiff(lines) {
24
+ return lines.some(l => l.startsWith("diff --git") ||
25
+ l.startsWith("@@") ||
26
+ l.startsWith("--- a/") ||
27
+ l.startsWith("+++ b/"));
28
+ }
29
+ /** Check if a line is a table row (starts and ends with pipe, has inner pipes). */
30
+ function isTableRow(line) {
31
+ const trimmed = line.trim();
32
+ return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.length > 2;
33
+ }
34
+ /** Check if a line is a table separator (e.g., |---|---|). */
35
+ function isTableSeparator(line) {
36
+ const trimmed = line.trim();
37
+ if (!trimmed.startsWith("|") || !trimmed.endsWith("|"))
38
+ return false;
39
+ // Between outer pipes, each cell must be only dashes, colons, and spaces
40
+ const inner = trimmed.slice(1, -1);
41
+ return inner.split("|").every(cell => /^\s*:?-+:?\s*$/.test(cell));
42
+ }
43
+ function parseBlocks(input) {
44
+ const rawLines = input.split("\n");
45
+ const blocks = [];
46
+ let i = 0;
47
+ while (i < rawLines.length) {
48
+ const line = rawLines[i];
49
+ // Fenced code block (``` with optional language)
50
+ if (line.trimStart().startsWith("```")) {
51
+ const langMatch = line.trimStart().match(/^```(\S*)/);
52
+ const language = langMatch?.[1] ?? "";
53
+ // Special case: ```diff -> render as diff block
54
+ if (language === "diff") {
55
+ const codeLines = [];
56
+ i++;
57
+ while (i < rawLines.length && !rawLines[i].trimStart().startsWith("```")) {
58
+ codeLines.push(rawLines[i]);
59
+ i++;
60
+ }
61
+ if (i < rawLines.length)
62
+ i++;
63
+ blocks.push({ type: "diff", lines: codeLines });
64
+ continue;
65
+ }
66
+ const codeLines = [];
67
+ i++;
68
+ while (i < rawLines.length && !rawLines[i].trimStart().startsWith("```")) {
69
+ codeLines.push(rawLines[i]);
70
+ i++;
71
+ }
72
+ // Skip closing fence if present (if not, it's a streaming partial)
73
+ if (i < rawLines.length)
74
+ i++;
75
+ blocks.push({ type: "codeblock", language, lines: codeLines });
76
+ continue;
77
+ }
78
+ // Horizontal rule (---, ***, ___) -- must come before heading check
79
+ // to avoid "---" being treated differently
80
+ if (HR_PATTERN.test(line)) {
81
+ blocks.push({ type: "hr", lines: [line] });
82
+ i++;
83
+ continue;
84
+ }
85
+ // Heading (# through ######)
86
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
87
+ if (headingMatch) {
88
+ blocks.push({ type: "heading", level: headingMatch[1].length, lines: [headingMatch[2]] });
89
+ i++;
90
+ continue;
91
+ }
92
+ // Blockquote (> text) -- handle nested levels
93
+ if (/^>/.test(line)) {
94
+ const quoteLines = [];
95
+ // Determine nesting depth from first line
96
+ const depthMatch = line.match(/^(>+)\s?/);
97
+ const depth = depthMatch ? depthMatch[1].length : 1;
98
+ while (i < rawLines.length && /^>/.test(rawLines[i])) {
99
+ // Strip the leading > markers (up to depth levels)
100
+ let stripped = rawLines[i];
101
+ for (let d = 0; d < depth; d++) {
102
+ stripped = stripped.replace(/^>\s?/, "");
103
+ }
104
+ quoteLines.push(stripped);
105
+ i++;
106
+ }
107
+ blocks.push({ type: "blockquote", level: depth, lines: quoteLines });
108
+ continue;
109
+ }
110
+ // Table detection: line is a table row, and next line (or line after) is a separator
111
+ if (isTableRow(line)) {
112
+ const tableLines = [];
113
+ const startI = i;
114
+ // Collect consecutive table-like rows
115
+ while (i < rawLines.length && isTableRow(rawLines[i])) {
116
+ tableLines.push(rawLines[i]);
117
+ i++;
118
+ }
119
+ // Validate: must have at least a header, separator, and one data row
120
+ if (tableLines.length >= 2 && tableLines.some(l => isTableSeparator(l))) {
121
+ blocks.push({ type: "table", lines: tableLines });
122
+ continue;
123
+ }
124
+ // Not a valid table, backtrack
125
+ i = startI;
126
+ }
127
+ // Diff block detection: consecutive diff-like lines with a strong signal
128
+ if (isDiffLine(line)) {
129
+ const diffLines = [];
130
+ const startI = i;
131
+ while (i < rawLines.length && isDiffLine(rawLines[i])) {
132
+ diffLines.push(rawLines[i]);
133
+ i++;
134
+ }
135
+ if (looksLikeDiff(diffLines)) {
136
+ blocks.push({ type: "diff", lines: diffLines });
137
+ continue;
138
+ }
139
+ // Not actually a diff -- backtrack and treat as paragraph
140
+ i = startI;
141
+ }
142
+ // Blank line
143
+ if (line.trim() === "") {
144
+ blocks.push({ type: "blank", lines: [""] });
145
+ i++;
146
+ continue;
147
+ }
148
+ // Regular paragraph (collect consecutive non-special lines)
149
+ const paraLines = [];
150
+ while (i < rawLines.length &&
151
+ rawLines[i].trim() !== "" &&
152
+ !rawLines[i].trimStart().startsWith("```") &&
153
+ !rawLines[i].match(/^#{1,6}\s/) &&
154
+ !/^>/.test(rawLines[i]) &&
155
+ !HR_PATTERN.test(rawLines[i])) {
156
+ paraLines.push(rawLines[i]);
157
+ i++;
158
+ }
159
+ if (paraLines.length > 0) {
160
+ blocks.push({ type: "paragraph", lines: paraLines });
161
+ }
162
+ }
163
+ return blocks;
164
+ }
165
+ // ─── HTML Entity Decoding ─────────────────────────────────────────
166
+ const HTML_ENTITIES = {
167
+ "&lt;": "<",
168
+ "&gt;": ">",
169
+ "&amp;": "&",
170
+ "&quot;": "\"",
171
+ "&#39;": "'",
172
+ "&apos;": "'",
173
+ "&nbsp;": " ",
174
+ };
175
+ function decodeHtmlEntities(text) {
176
+ return text.replace(/&(?:#39|lt|gt|amp|quot|apos|nbsp);/g, match => HTML_ENTITIES[match] ?? match);
177
+ }
178
+ // ─── Escape Handling ──────────────────────────────────────────────
179
+ /** Sentinel character for protected escapes. */
180
+ const ESC_SENTINEL = "\x01";
181
+ /** Replace \*, \_, \`, \\ with sentinels before inline parsing. */
182
+ function protectEscapes(text) {
183
+ const escapes = [];
184
+ const processed = text.replace(/\\([*_`\\[\]()~<>])/g, (_m, char) => {
185
+ const idx = escapes.length;
186
+ escapes.push(char);
187
+ return `${ESC_SENTINEL}ESC${idx}${ESC_SENTINEL}`;
188
+ });
189
+ return { text: processed, escapes };
190
+ }
191
+ /** Restore escape sentinels to their literal characters. */
192
+ function restoreEscapes(text, escapes) {
193
+ return text.replace(new RegExp(`${ESC_SENTINEL}ESC(\\d+)${ESC_SENTINEL}`, "g"), (_m, idx) => {
194
+ return escapes[parseInt(idx)] ?? "";
195
+ });
196
+ }
197
+ // ─── Inline Parser ─────────────────────────────────────────────────
198
+ /**
199
+ * Parse inline markdown (bold, italic, bold-italic, code, links,
200
+ * autolinks, strikethrough, escapes, HTML entities) from a single line.
201
+ */
202
+ function parseInline(text) {
203
+ // Step 0: decode HTML entities
204
+ let processed = decodeHtmlEntities(text);
205
+ // Step 1: protect escape sequences
206
+ const { text: escaped, escapes } = protectEscapes(processed);
207
+ processed = escaped;
208
+ const segments = [];
209
+ const placeholders = [];
210
+ // Step 2: extract inline code
211
+ processed = processed.replace(/`([^`\n]+)`/g, (_m, code) => {
212
+ const idx = placeholders.length;
213
+ placeholders.push({ idx, segment: { text: restoreEscapes(code, escapes), code: true } });
214
+ return `\x00PH${idx}\x00`;
215
+ });
216
+ // Step 3: extract links [text](url)
217
+ processed = processed.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_m, linkText, url) => {
218
+ const idx = placeholders.length;
219
+ const restoredText = restoreEscapes(linkText, escapes);
220
+ const restoredUrl = restoreEscapes(url, escapes);
221
+ placeholders.push({ idx, segment: { text: restoredText, link: { text: restoredText, url: restoredUrl } } });
222
+ return `\x00PH${idx}\x00`;
223
+ });
224
+ // Step 4: extract autolinks <https://...>
225
+ processed = processed.replace(/<(https?:\/\/[^>]+)>/g, (_m, url) => {
226
+ const idx = placeholders.length;
227
+ const restoredUrl = restoreEscapes(url, escapes);
228
+ placeholders.push({ idx, segment: { text: restoredUrl, autolink: true } });
229
+ return `\x00PH${idx}\x00`;
230
+ });
231
+ // Step 5: extract strikethrough ~~text~~
232
+ processed = processed.replace(/~~(.+?)~~/g, (_m, content) => {
233
+ const idx = placeholders.length;
234
+ placeholders.push({ idx, segment: { text: restoreEscapes(content, escapes), strikethrough: true } });
235
+ return `\x00PH${idx}\x00`;
236
+ });
237
+ // Step 6: parse bold-italic, bold, and italic from remaining text
238
+ const parts = processed.split(/(\x00PH\d+\x00)/);
239
+ for (const part of parts) {
240
+ const phMatch = part.match(/^\x00PH(\d+)\x00$/);
241
+ if (phMatch) {
242
+ segments.push(placeholders[parseInt(phMatch[1])].segment);
243
+ continue;
244
+ }
245
+ // Parse ***bold italic***, **bold**, *italic*, _italic_
246
+ parseInlineStyles(part, segments, escapes);
247
+ }
248
+ return segments;
249
+ }
250
+ function parseInlineStyles(text, segments, escapes) {
251
+ // Match ***bold italic***, **_bold italic_**, **bold**, *italic*, _italic_
252
+ // Order matters: triple-star before double-star before single-star
253
+ const pattern = /(\*\*\*(.+?)\*\*\*)|(\*\*_(.+?)_\*\*)|(\*\*(.+?)\*\*)|(\*(.+?)\*)|(_(.+?)_)/g;
254
+ let lastIndex = 0;
255
+ let match;
256
+ while ((match = pattern.exec(text)) !== null) {
257
+ if (match.index > lastIndex) {
258
+ const plain = restoreEscapes(text.slice(lastIndex, match.index), escapes);
259
+ if (plain)
260
+ segments.push({ text: plain });
261
+ }
262
+ if (match[2]) {
263
+ // ***bold italic***
264
+ segments.push({ text: restoreEscapes(match[2], escapes), bold: true, italic: true });
265
+ }
266
+ else if (match[4]) {
267
+ // **_bold italic_**
268
+ segments.push({ text: restoreEscapes(match[4], escapes), bold: true, italic: true });
269
+ }
270
+ else if (match[6]) {
271
+ // **bold**
272
+ segments.push({ text: restoreEscapes(match[6], escapes), bold: true });
273
+ }
274
+ else if (match[8]) {
275
+ // *italic*
276
+ segments.push({ text: restoreEscapes(match[8], escapes), italic: true });
277
+ }
278
+ else if (match[10]) {
279
+ // _italic_
280
+ segments.push({ text: restoreEscapes(match[10], escapes), italic: true });
281
+ }
282
+ lastIndex = match.index + match[0].length;
283
+ }
284
+ if (lastIndex < text.length) {
285
+ const plain = restoreEscapes(text.slice(lastIndex), escapes);
286
+ if (plain)
287
+ segments.push({ text: plain });
288
+ }
289
+ }
290
+ function parseTaskListItem(line) {
291
+ const match = line.match(/^(\s*)-\s+\[([ xX])\]\s(.*)$/);
292
+ if (!match)
293
+ return null;
294
+ return {
295
+ indent: match[1],
296
+ checked: match[2].toLowerCase() === "x",
297
+ text: match[3],
298
+ };
299
+ }
300
+ function parseTable(lines) {
301
+ if (lines.length < 2)
302
+ return null;
303
+ // Find the separator row
304
+ let sepIdx = -1;
305
+ for (let i = 0; i < lines.length; i++) {
306
+ if (isTableSeparator(lines[i])) {
307
+ sepIdx = i;
308
+ break;
309
+ }
310
+ }
311
+ if (sepIdx < 1)
312
+ return null; // separator must not be the first line
313
+ const parseCells = (line) => {
314
+ const trimmed = line.trim();
315
+ // Remove leading and trailing pipes, split by pipe
316
+ const inner = trimmed.slice(1, -1);
317
+ return inner.split("|").map(c => c.trim());
318
+ };
319
+ const headers = parseCells(lines[sepIdx - 1]);
320
+ const rows = [];
321
+ for (let i = sepIdx + 1; i < lines.length; i++) {
322
+ if (!isTableSeparator(lines[i])) {
323
+ rows.push(parseCells(lines[i]));
324
+ }
325
+ }
326
+ // Calculate column widths
327
+ const columnWidths = headers.map((h, ci) => {
328
+ let max = h.length;
329
+ for (const row of rows) {
330
+ const cell = row[ci] ?? "";
331
+ if (cell.length > max)
332
+ max = cell.length;
333
+ }
334
+ return Math.max(max, 3); // minimum width of 3
335
+ });
336
+ return { headers, rows, columnWidths };
337
+ }
338
+ // ─── Word Wrap ─────────────────────────────────────────────────────
339
+ function wrapText(text, maxWidth) {
340
+ /* v8 ignore next -- defensive guard for non-positive width @preserve */
341
+ if (maxWidth <= 0)
342
+ return text;
343
+ const lines = text.split("\n");
344
+ const result = [];
345
+ for (const line of lines) {
346
+ if (line.length <= maxWidth) {
347
+ result.push(line);
348
+ }
349
+ else {
350
+ let remaining = line;
351
+ while (remaining.length > maxWidth) {
352
+ let breakAt = remaining.lastIndexOf(" ", maxWidth);
353
+ if (breakAt <= 0)
354
+ breakAt = maxWidth;
355
+ result.push(remaining.slice(0, breakAt));
356
+ remaining = remaining.slice(breakAt).replace(/^ /, "");
357
+ }
358
+ /* v8 ignore next -- defensive guard for empty remaining after word-wrap @preserve */
359
+ if (remaining)
360
+ result.push(remaining);
361
+ }
362
+ }
363
+ return result.join("\n");
364
+ }
365
+ // ─── Segment Renderer ──────────────────────────────────────────────
366
+ function SegmentRenderer({ segment }) {
367
+ if (segment.codeBlock) {
368
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: segment.text });
369
+ }
370
+ if (segment.code) {
371
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "cyan", children: segment.text });
372
+ }
373
+ if (segment.autolink) {
374
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "cyan", children: segment.text });
375
+ }
376
+ if (segment.link) {
377
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { children: segment.link.text }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "cyan", dimColor: true, children: ` (${segment.link.url})` })] }));
378
+ }
379
+ if (segment.strikethrough) {
380
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, strikethrough: true, children: segment.text });
381
+ }
382
+ if (segment.bold && segment.italic) {
383
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, italic: true, children: segment.text });
384
+ }
385
+ if (segment.bold) {
386
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, children: segment.text });
387
+ }
388
+ if (segment.italic) {
389
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { italic: true, children: segment.text });
390
+ }
391
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: segment.text });
392
+ }
393
+ // ─── Inline Line Renderer ──────────────────────────────────────────
394
+ function InlineLine({ text: lineText }) {
395
+ const segs = parseInline(lineText);
396
+ return ((0, jsx_runtime_1.jsx)(ink_1.Text, { children: segs.map((seg, i) => ((0, jsx_runtime_1.jsx)(SegmentRenderer, { segment: seg }, i))) }));
397
+ }
398
+ // ─── Diff Line Renderer ───────────────────────────────────────────
399
+ function DiffLine({ line }) {
400
+ if (line.startsWith("@@")) {
401
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "cyan", children: line });
402
+ }
403
+ if (line.startsWith("diff --git") ||
404
+ line.startsWith("index ") ||
405
+ line.startsWith("--- ") ||
406
+ line.startsWith("+++ ")) {
407
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, children: line });
408
+ }
409
+ if (line.startsWith("+")) {
410
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "green", children: line });
411
+ }
412
+ if (line.startsWith("-")) {
413
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "red", children: line });
414
+ }
415
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: line });
416
+ }
417
+ // ─── Table Renderer ───────────────────────────────────────────────
418
+ function TableRenderer({ lines }) {
419
+ const table = parseTable(lines);
420
+ if (!table) {
421
+ // Fallback: render raw lines
422
+ return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: lines.map((line, i) => ((0, jsx_runtime_1.jsx)(ink_1.Text, { children: line }, i))) }));
423
+ }
424
+ const { headers, rows, columnWidths } = table;
425
+ const padCell = (text, width) => {
426
+ return text + " ".repeat(Math.max(0, width - text.length));
427
+ };
428
+ const renderRow = (cells, bold) => {
429
+ const cellTexts = columnWidths.map((w, ci) => padCell(cells[ci] ?? "", w));
430
+ const rowText = "| " + cellTexts.join(" | ") + " |";
431
+ if (bold)
432
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, children: rowText });
433
+ return (0, jsx_runtime_1.jsx)(InlineLine, { text: rowText });
434
+ };
435
+ const separatorRow = "| " + columnWidths.map(w => "-".repeat(w)).join(" | ") + " |";
436
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [renderRow(headers, true), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: separatorRow }), rows.map((row, i) => ((0, jsx_runtime_1.jsx)(react_1.default.Fragment, { children: renderRow(row, false) }, i)))] }));
437
+ }
438
+ // ─── Block Renderer ────────────────────────────────────────────────
439
+ function BlockRenderer({ block, isFirst, isLast }) {
440
+ switch (block.type) {
441
+ case "blank":
442
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" });
443
+ case "hr":
444
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [!isFirst && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "\u2500".repeat(40) }), !isLast && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" })] }));
445
+ case "heading": {
446
+ const text = block.lines.join(" ");
447
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [!isFirst && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), (block.level ?? 1) === 1
448
+ ? (0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, underline: true, children: text })
449
+ : (0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, children: text }), !isLast && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" })] }));
450
+ }
451
+ case "codeblock": {
452
+ const langLabel = block.language || undefined;
453
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [!isFirst && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), langLabel && (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: langLabel }), block.lines.map((line, i) => ((0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: line }, i))), !isLast && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" })] }));
454
+ }
455
+ case "diff":
456
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [!isFirst && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "\u2500".repeat(40) }), block.lines.map((line, i) => ((0, jsx_runtime_1.jsx)(DiffLine, { line: line }, i))), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "\u2500".repeat(40) }), !isLast && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" })] }));
457
+ case "table":
458
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [!isFirst && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), (0, jsx_runtime_1.jsx)(TableRenderer, { lines: block.lines }), !isLast && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" })] }));
459
+ case "blockquote": {
460
+ const depth = block.level ?? 1;
461
+ const barPrefix = "\u2502 ".repeat(depth);
462
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [!isFirst && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), block.lines.map((line, i) => ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: barPrefix }), (0, jsx_runtime_1.jsx)(ink_1.Text, { italic: true, children: (0, jsx_runtime_1.jsx)(InlineLine, { text: line }) })] }, i))), !isLast && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" })] }));
463
+ }
464
+ case "paragraph": {
465
+ // Check for task list items and render them specially
466
+ const renderedLines = block.lines.map((line, i) => {
467
+ const task = parseTaskListItem(line);
468
+ if (task) {
469
+ const checkbox = task.checked ? "\u2611" : "\u2610";
470
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [task.indent, checkbox, " "] }), (0, jsx_runtime_1.jsx)(InlineLine, { text: task.text })] }, i));
471
+ }
472
+ return (0, jsx_runtime_1.jsx)(InlineLine, { text: line }, i);
473
+ });
474
+ return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: renderedLines }));
475
+ }
476
+ }
477
+ }
478
+ // ─── Main Component ────────────────────────────────────────────────
479
+ function StreamingMarkdown({ text, maxWidth }) {
480
+ const processed = maxWidth ? wrapText(text, maxWidth) : text;
481
+ const blocks = parseBlocks(processed);
482
+ if (blocks.length === 0) {
483
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" });
484
+ }
485
+ return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: blocks.map((block, i) => ((0, jsx_runtime_1.jsx)(BlockRenderer, { block: block, isFirst: i === 0, isLast: i === blocks.length - 1 }, i))) }));
486
+ }
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ /**
3
+ * Imperative tool result rendering for the CLI.
4
+ *
5
+ * Writes colored one-liners to stderr — no React, no Ink.
6
+ * Features:
7
+ * - Green check / red cross prefix
8
+ * - Tool name in ouroboros teal
9
+ * - Parsed first argument (file path, command, query) dimmed
10
+ * - For edit_file: compact inline diff summary (+N/-M lines)
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.writeToolStart = writeToolStart;
14
+ exports.writeToolEnd = writeToolEnd;
15
+ const runtime_1 = require("../../nerves/runtime");
16
+ // Ouroboros teal: #4ec9b0 -> RGB escape
17
+ const OURO_TEAL = "\x1b[38;2;78;201;176m";
18
+ const GREEN = "\x1b[32m";
19
+ const RED = "\x1b[31m";
20
+ const DIM = "\x1b[2m";
21
+ const BOLD = "\x1b[1m";
22
+ const RESET = "\x1b[0m";
23
+ // Tool name -> primary parameter key mapping
24
+ const PRIMARY_PARAM = {
25
+ edit_file: "path",
26
+ write_file: "path",
27
+ read_file: "path",
28
+ glob: "pattern",
29
+ grep: "pattern",
30
+ shell: "command",
31
+ coding_spawn: "task",
32
+ coding_status: "session_id",
33
+ coding_tail: "session_id",
34
+ };
35
+ function truncate(s, max) {
36
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
37
+ }
38
+ /**
39
+ * Extract the primary display parameter from tool args.
40
+ */
41
+ function getPrimaryArg(name, args) {
42
+ const primaryKey = PRIMARY_PARAM[name];
43
+ if (primaryKey && typeof args[primaryKey] === "string") {
44
+ return truncate(args[primaryKey], 80);
45
+ }
46
+ // Fallback: show first string value
47
+ for (const val of Object.values(args)) {
48
+ if (typeof val === "string") {
49
+ return truncate(val, 80);
50
+ }
51
+ }
52
+ return "";
53
+ }
54
+ /**
55
+ * Format and write a tool start line to stderr.
56
+ * Shows: [teal tool name] [dim primary arg]
57
+ */
58
+ function writeToolStart(name, args) {
59
+ const primary = getPrimaryArg(name, args);
60
+ const argDisplay = primary ? ` ${DIM}${primary}${RESET}` : "";
61
+ process.stderr.write(`\r\x1b[K${OURO_TEAL}${BOLD}${name}${RESET}${argDisplay}\n`);
62
+ }
63
+ /**
64
+ * Format and write a tool result line to stderr.
65
+ * Shows: [green check / red cross] [teal tool name] [dim summary]
66
+ */
67
+ function writeToolEnd(name, argSummary, success) {
68
+ const icon = success ? `${GREEN}\u2713` : `${RED}\u2717`;
69
+ const summary = argSummary ? ` ${DIM}${truncate(argSummary, 100)}${RESET}` : "";
70
+ process.stderr.write(`${icon}${RESET} ${OURO_TEAL}${name}${RESET}${summary}\n`);
71
+ (0, runtime_1.emitNervesEvent)({
72
+ component: "senses",
73
+ event: "senses.tool_display",
74
+ message: "tool result displayed",
75
+ meta: { name, success },
76
+ });
77
+ }
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ToolBadge = ToolBadge;
7
+ exports.ToolParams = ToolParams;
8
+ exports.ToolProgress = ToolProgress;
9
+ exports.ToolResultCard = ToolResultCard;
10
+ exports.ToolExecutionBlock = ToolExecutionBlock;
11
+ const jsx_runtime_1 = require("react/jsx-runtime");
12
+ const react_1 = __importDefault(require("react"));
13
+ const ink_1 = require("ink");
14
+ const inline_diff_1 = require("./inline-diff");
15
+ /**
16
+ * Tool execution display components for the CLI TUI.
17
+ *
18
+ * Provides: ToolBadge, ToolParams, ToolProgress, ToolResultCard, ToolExecutionBlock.
19
+ *
20
+ * Design: Ouroboros-themed. Inline diffs use green/red with +/- markers and line numbers.
21
+ * No padding -- visual separation via color/bold/dim only.
22
+ *
23
+ * Files >50KB get a summary instead of inline diff (scrutiny finding 17).
24
+ */
25
+ // Ouroboros brand palette
26
+ const OURO_GREEN = "#2ecc40";
27
+ const OURO_RED = "#e74c3c";
28
+ const OURO_TEAL = "#4ec9b0";
29
+ // 50KB threshold for inline diff vs summary
30
+ const LARGE_FILE_THRESHOLD = 50 * 1024;
31
+ // Tool name -> primary parameter key mapping
32
+ const PRIMARY_PARAM = {
33
+ edit_file: "path",
34
+ write_file: "path",
35
+ read_file: "path",
36
+ glob: "pattern",
37
+ grep: "pattern",
38
+ shell: "command",
39
+ coding_spawn: "task",
40
+ coding_status: "session_id",
41
+ coding_tail: "session_id",
42
+ };
43
+ function truncate(s, max) {
44
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
45
+ }
46
+ function ToolBadge({ name }) {
47
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO_TEAL, bold: true, children: name });
48
+ }
49
+ function ToolParams({ name, args, }) {
50
+ // Find primary parameter to show inline
51
+ const primaryKey = PRIMARY_PARAM[name];
52
+ let display = "";
53
+ if (primaryKey && typeof args[primaryKey] === "string") {
54
+ display = truncate(args[primaryKey], 80);
55
+ }
56
+ else {
57
+ // Fallback: show first string value
58
+ for (const val of Object.values(args)) {
59
+ if (typeof val === "string") {
60
+ display = truncate(val, 80);
61
+ break;
62
+ }
63
+ }
64
+ }
65
+ if (!display)
66
+ return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" });
67
+ return (0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [" ", display] });
68
+ }
69
+ function ToolProgress({ name }) {
70
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO_GREEN, children: "\u25CF " }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [name, " running..."] })] }));
71
+ }
72
+ function ToolResultCard({ name, result, success, expanded, }) {
73
+ const icon = success ? "\u2713" : "\u2717";
74
+ const color = success ? OURO_GREEN : OURO_RED;
75
+ if (expanded) {
76
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { color: color, children: [icon, " "] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO_TEAL, bold: true, children: name })] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { children: result })] }));
77
+ }
78
+ // Collapsed: one-line summary
79
+ const summary = truncate(result.replace(/\n/g, " "), 100);
80
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { color: color, children: [icon, " "] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO_TEAL, bold: true, children: name }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [" ", summary] })] }));
81
+ }
82
+ function ToolExecutionBlock({ name, args, result, success, fileBefore, fileAfter, }) {
83
+ const filePath = typeof args.path === "string" ? args.path : undefined;
84
+ const showDiff = name === "edit_file" && fileBefore !== undefined && fileAfter !== undefined;
85
+ // Check file size threshold
86
+ const tooLarge = showDiff && ((fileBefore?.length ?? 0) > LARGE_FILE_THRESHOLD ||
87
+ (fileAfter?.length ?? 0) > LARGE_FILE_THRESHOLD);
88
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ToolBadge, { name: name }), (0, jsx_runtime_1.jsx)(ToolParams, { name: name, args: args })] }), showDiff && !tooLarge ? ((0, jsx_runtime_1.jsx)(inline_diff_1.InlineDiff, { before: fileBefore, after: fileAfter, filePath: filePath ?? "unknown" })) : /* v8 ignore next -- tooLarge branch not exercised in render tests @preserve */ showDiff && tooLarge ? ((0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: ["large file changed (", Math.round((fileAfter?.length ?? 0) / 1024), "KB)"] })) : ((0, jsx_runtime_1.jsx)(ToolResultCard, { name: name, result: result, success: success }))] }));
89
+ }