u-foo 2.5.14 → 3.0.0

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 (59) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +517 -112
  4. package/src/code/commands.js +77 -0
  5. package/src/code/context/artifactGc.js +292 -0
  6. package/src/code/context/artifactIndex.js +161 -0
  7. package/src/code/context/artifacts.js +183 -0
  8. package/src/code/context/assembler.js +703 -0
  9. package/src/code/context/executionSegment.js +292 -0
  10. package/src/code/context/index.js +28 -0
  11. package/src/code/context/planGraph.js +1410 -0
  12. package/src/code/context/planGraphService.js +857 -0
  13. package/src/code/context/planMode.js +398 -0
  14. package/src/code/context/planProjection.js +432 -0
  15. package/src/code/context/projectSnapshot.js +201 -0
  16. package/src/code/context/promptLayers.js +175 -0
  17. package/src/code/context/reducers.js +328 -0
  18. package/src/code/context/stableJson.js +29 -0
  19. package/src/code/context/stateCommit.js +414 -0
  20. package/src/code/context/toolRuntime.js +172 -0
  21. package/src/code/context/transcript.js +182 -0
  22. package/src/code/context/transcriptSync.js +106 -0
  23. package/src/code/context/userInteraction.js +457 -0
  24. package/src/code/context/userNudge.js +116 -0
  25. package/src/code/context/workingSet.js +323 -0
  26. package/src/code/dispatch.js +20 -1
  27. package/src/code/index.js +8 -0
  28. package/src/code/modelCommand.js +87 -0
  29. package/src/code/nativeRunner.js +625 -34
  30. package/src/code/repl.js +196 -50
  31. package/src/code/runtime/agentWakeup.js +58 -0
  32. package/src/code/runtime/graphOwner.js +41 -0
  33. package/src/code/runtime/graphYieldRouter.js +42 -0
  34. package/src/code/runtime/index.js +15 -0
  35. package/src/code/runtime/loopMailbox.js +124 -0
  36. package/src/code/runtime/runtimeEvents.js +39 -0
  37. package/src/code/runtime/taskControl.js +565 -0
  38. package/src/code/runtime/taskFocus.js +165 -0
  39. package/src/code/runtime/taskLoop.js +383 -0
  40. package/src/code/runtime/taskRun.js +187 -0
  41. package/src/code/runtime/toolProvenance.js +70 -0
  42. package/src/code/runtime/workspaceLease.js +208 -0
  43. package/src/code/sessionStore.js +217 -15
  44. package/src/code/skills/index.js +10 -0
  45. package/src/code/skills/injection.js +66 -3
  46. package/src/code/skills/loader.js +21 -0
  47. package/src/code/skills/manifest.js +87 -0
  48. package/src/code/skills/render.js +15 -1
  49. package/src/code/taskDecomposer.js +56 -2
  50. package/src/code/tools/artifactRead.js +40 -0
  51. package/src/code/tools/askUser.js +11 -0
  52. package/src/code/tools/planGraph.js +29 -0
  53. package/src/code/tui.js +2 -0
  54. package/src/code/usageStore.js +15 -0
  55. package/src/ui/format/index.js +285 -45
  56. package/src/ui/format/markdownRenderer.js +436 -71
  57. package/src/ui/ink/ChatApp.js +39 -8
  58. package/src/ui/ink/UcodeApp.js +592 -43
  59. package/src/ui/ink/chatLogModel.js +102 -21
@@ -1,10 +1,14 @@
1
1
  /**
2
- * Shared blessed-compatible markdown renderer for TUI output.
2
+ * Shared markdown renderer for TUI log output.
3
3
  *
4
- * Used by both ucode TUI and ufoo chat to render agent responses
5
- * with fenced code blocks, headings, quotes, bullets, inline code, etc.
4
+ * Produces either blessed tags (chat / legacy) or chalk ANSI (Ink ucode).
5
+ * Terminals cannot change font size, so headings/emphasis use color + weight
6
+ * instead of literal `#` / `**` markers. GFM pipe tables are aligned into a
7
+ * compact spreadsheet-like grid.
6
8
  */
7
9
 
10
+ const chalk = require("chalk");
11
+
8
12
  function stripLeakedEscapeTags(text = "") {
9
13
  const source = String(text == null ? "" : text);
10
14
  const withoutClosedTags = source.replace(/\{[^{}\n]*escape[^{}\n]*\}/gi, "");
@@ -12,110 +16,471 @@ function stripLeakedEscapeTags(text = "") {
12
16
  return withoutDanglingEscape.replace(/\{\s*\/?\s*e?s?c?a?p?e?[^{}\n]*$/gi, "");
13
17
  }
14
18
 
15
- function renderMarkdownLines(text = "", state = {}, escapeFn = (value) => String(value || "")) {
16
- const renderState = state && typeof state === "object" ? state : {};
17
- if (typeof renderState.inCodeBlock !== "boolean") {
18
- renderState.inCodeBlock = false;
19
+ /** Visible width for CJK-aware table padding (no ANSI). */
20
+ function visibleWidth(text = "") {
21
+ let width = 0;
22
+ for (const char of String(text || "")) {
23
+ const code = char.codePointAt(0) || 0;
24
+ if (code < 32 || (code >= 0x7f && code < 0xa0)) continue;
25
+ if (
26
+ (code >= 0x1100 && code <= 0x115f)
27
+ || code === 0x2329
28
+ || code === 0x232a
29
+ || (code >= 0x2e80 && code <= 0xa4cf)
30
+ || (code >= 0xac00 && code <= 0xd7a3)
31
+ || (code >= 0xf900 && code <= 0xfaff)
32
+ || (code >= 0xfe10 && code <= 0xfe19)
33
+ || (code >= 0xfe30 && code <= 0xfe6f)
34
+ || (code >= 0xff00 && code <= 0xff60)
35
+ || (code >= 0xffe0 && code <= 0xffe6)
36
+ ) {
37
+ width += 2;
38
+ continue;
39
+ }
40
+ width += 1;
41
+ }
42
+ return width;
43
+ }
44
+
45
+ function isTableSeparatorLine(line = "") {
46
+ const raw = String(line || "").trim();
47
+ if (!raw.includes("-")) return false;
48
+ return /^\|?(\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?$/.test(raw);
49
+ }
50
+
51
+ function isTableRowLine(line = "") {
52
+ const raw = String(line || "").trim();
53
+ if (!raw.includes("|")) return false;
54
+ if (isTableSeparatorLine(raw)) return true;
55
+ // Prefer GFM pipe rows (`| a | b |`). Also allow compact `a | b | c`
56
+ // (at least two pipes). Lone prose like `A | B` is not a table row.
57
+ if (/^\|.*\|$/.test(raw)) return true;
58
+ const parts = raw.split("|");
59
+ return parts.length >= 3 && parts.some((p) => p.trim().length > 0);
60
+ }
61
+
62
+ /**
63
+ * Buffers consecutive GFM table rows so they can be rendered as one block
64
+ * (column widths need the full table). Call flush() before non-table output.
65
+ */
66
+ function createMarkdownTableBuffer() {
67
+ const rows = [];
68
+ return {
69
+ get size() {
70
+ return rows.length;
71
+ },
72
+ push(line = "") {
73
+ const raw = String(line == null ? "" : line);
74
+ if (!isTableRowLine(raw)) return false;
75
+ rows.push(raw);
76
+ return true;
77
+ },
78
+ flush() {
79
+ if (rows.length === 0) return null;
80
+ const text = rows.join("\n");
81
+ rows.length = 0;
82
+ return text;
83
+ },
84
+ };
85
+ }
86
+
87
+ function parseTableCells(line = "") {
88
+ let raw = String(line || "").trim();
89
+ if (raw.startsWith("|")) raw = raw.slice(1);
90
+ if (raw.endsWith("|")) raw = raw.slice(0, -1);
91
+ return raw.split("|").map((cell) => cell.trim());
92
+ }
93
+
94
+ function parseSeparatorAlignments(line = "", columnCount = 0) {
95
+ const cells = parseTableCells(line);
96
+ const aligns = cells.map((cell) => {
97
+ const left = cell.startsWith(":");
98
+ const right = cell.endsWith(":");
99
+ if (left && right) return "center";
100
+ if (right) return "right";
101
+ return "left";
102
+ });
103
+ while (aligns.length < columnCount) aligns.push("left");
104
+ return aligns.slice(0, Math.max(columnCount, aligns.length));
105
+ }
106
+
107
+ function alignCell(text = "", width = 0, align = "left") {
108
+ const value = String(text || "");
109
+ const current = visibleWidth(value);
110
+ if (current >= width) return value;
111
+ const pad = width - current;
112
+ if (align === "right") return `${" ".repeat(pad)}${value}`;
113
+ if (align === "center") {
114
+ const left = Math.floor(pad / 2);
115
+ const right = pad - left;
116
+ return `${" ".repeat(left)}${value}${" ".repeat(right)}`;
117
+ }
118
+ return `${value}${" ".repeat(pad)}`;
119
+ }
120
+
121
+ function plainCellWidth(cell = "") {
122
+ const plain = String(cell || "")
123
+ .replace(/\*\*|__/g, "")
124
+ .replace(/`/g, "")
125
+ .replace(/\*/g, "");
126
+ return visibleWidth(plain);
127
+ }
128
+
129
+ function createBlessedAdapters(escapeFn = (value) => String(value || "")) {
130
+ const escape = (value) => escapeFn(value);
131
+ return {
132
+ escape,
133
+ bold: (value) => `{bold}{white-fg}${escape(value)}{/white-fg}{/bold}`,
134
+ italic: (value) => `{italic}{gray-fg}${escape(value)}{/gray-fg}{/italic}`,
135
+ code: (value) => `{yellow-fg}${escape(value)}{/yellow-fg}`,
136
+ heading: (level, value) => {
137
+ const depth = Math.max(1, Math.min(6, Number(level) || 1));
138
+ if (depth <= 2) return `{cyan-fg}{bold}${value}{/bold}{/cyan-fg}`;
139
+ if (depth === 3) return `{blue-fg}{bold}${value}{/bold}{/blue-fg}`;
140
+ return `{bold}${value}{/bold}`;
141
+ },
142
+ quoteMarker: () => "{gray-fg}│{/gray-fg}",
143
+ bulletMarker: () => "{gray-fg}•{/gray-fg}",
144
+ orderedMarker: (value) => `{gray-fg}${escape(value)}.{/gray-fg}`,
145
+ rule: () => "{gray-fg}────────────────────────{/gray-fg}",
146
+ fenceOpen: (language) => (
147
+ language
148
+ ? `{gray-fg}┌ code:${escape(language)}{/gray-fg}`
149
+ : "{gray-fg}┌ code{/gray-fg}"
150
+ ),
151
+ fenceClose: () => "{gray-fg}└{/gray-fg}",
152
+ fenceBody: (value) => `{gray-fg}│{/gray-fg} {white-fg}${escape(value)}{/white-fg}`,
153
+ error: (value) => `{red-fg}${value}{/red-fg}`,
154
+ tablePipe: () => "{gray-fg}│{/gray-fg}",
155
+ tableSepCross: () => "{gray-fg}┼{/gray-fg}",
156
+ tableSepH: () => "{gray-fg}─{/gray-fg}",
157
+ tableHeaderCell: (value) => `{bold}{white-fg}${value}{/white-fg}{/bold}`,
158
+ tableCell: (value) => value,
159
+ };
160
+ }
161
+
162
+ function createAnsiAdapters() {
163
+ // Ink always paints into a TTY-capable stdout; force color so bold/heading
164
+ // styles survive even when chalk's autodetection thinks we're non-TTY
165
+ // (e.g. piped test harnesses that still render Ink).
166
+ const paint = typeof chalk.Instance === "function"
167
+ ? new chalk.Instance({ level: Math.max(Number(chalk.level) || 0, 2) })
168
+ : chalk;
169
+ return {
170
+ escape: (value) => String(value || ""),
171
+ // Bold gets weight + brighter foreground so it reads even when the
172
+ // terminal theme barely differentiates ANSI bold.
173
+ bold: (value) => paint.bold.whiteBright(String(value || "")),
174
+ italic: (value) => paint.italic.dim(String(value || "")),
175
+ code: (value) => paint.yellow(String(value || "")),
176
+ heading: (level, value) => {
177
+ const depth = Math.max(1, Math.min(6, Number(level) || 1));
178
+ const text = String(value || "");
179
+ if (depth <= 2) return paint.bold.cyan(text);
180
+ if (depth === 3) return paint.bold.blue(text);
181
+ return paint.bold(text);
182
+ },
183
+ quoteMarker: () => paint.gray("│"),
184
+ bulletMarker: () => paint.gray("•"),
185
+ orderedMarker: (value) => paint.gray(`${value}.`),
186
+ rule: () => paint.gray("────────────────────────"),
187
+ fenceOpen: (language) => (
188
+ language
189
+ ? paint.gray(`┌ code:${language}`)
190
+ : paint.gray("┌ code")
191
+ ),
192
+ fenceClose: () => paint.gray("└"),
193
+ fenceBody: (value) => `${paint.gray("│")} ${paint.white(String(value || ""))}`,
194
+ error: (value) => paint.red(String(value || "")),
195
+ tablePipe: () => paint.gray("│"),
196
+ tableSepCross: () => paint.gray("┼"),
197
+ tableSepH: () => paint.gray("─"),
198
+ tableHeaderCell: (value) => paint.bold.whiteBright(String(value || "")),
199
+ tableCell: (value) => String(value || ""),
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Apply inline markdown to a single line.
205
+ * Scans left-to-right so **bold** wins over nested `code`/`*` patterns
206
+ * (LLMs often emit **`name`** which previously left literal asterisks).
207
+ */
208
+ function renderInlineMarkdown(input = "", adapters = createBlessedAdapters()) {
209
+ const source = String(input || "");
210
+ if (!source) return "";
211
+
212
+ const escape = adapters.escape || ((value) => String(value || ""));
213
+ const styleBold = adapters.bold || escape;
214
+ const styleItalic = adapters.italic || escape;
215
+ const styleCode = adapters.code || escape;
216
+
217
+ if (!source.includes("`") && !source.includes("*") && !source.includes("_")) {
218
+ return escape(source);
19
219
  }
20
220
 
21
- const renderInlineCode = (input = "") => {
22
- const source = String(input || "");
23
- if (!source) return "";
24
- if (!source.includes("`")) return escapeFn(source);
25
-
26
- let out = "";
27
- let cursor = 0;
28
- const pattern = /`([^`\n]+)`/g;
29
- let match = pattern.exec(source);
30
- while (match) {
31
- const index = Number(match.index) || 0;
32
- if (index > cursor) {
33
- out += escapeFn(source.slice(cursor, index));
221
+ const renderInner = (chunk) => renderInlineMarkdown(chunk, adapters);
222
+
223
+ let out = "";
224
+ let i = 0;
225
+ while (i < source.length) {
226
+ // **bold** / __bold__
227
+ if (source.startsWith("**", i) || source.startsWith("__", i)) {
228
+ const mark = source.slice(i, i + 2);
229
+ const close = source.indexOf(mark, i + 2);
230
+ if (close !== -1) {
231
+ const inner = source.slice(i + 2, close);
232
+ out += styleBold(renderInner(inner));
233
+ i = close + 2;
234
+ continue;
235
+ }
236
+ }
237
+
238
+ // `code`
239
+ if (source[i] === "`") {
240
+ const close = source.indexOf("`", i + 1);
241
+ if (close !== -1) {
242
+ let inner = source.slice(i + 1, close);
243
+ // Code that is only a bold/italic wrapper → treat as emphasis.
244
+ const boldOnly = inner.match(/^\*\*(.+)\*\*$/) || inner.match(/^__(.+)__$/);
245
+ const italicOnly = !boldOnly && (inner.match(/^\*(.+)\*$/) || inner.match(/^_(.+)_$/));
246
+ if (boldOnly) out += styleBold(renderInner(boldOnly[1]));
247
+ else if (italicOnly) out += styleItalic(renderInner(italicOnly[1]));
248
+ else out += styleCode(inner);
249
+ i = close + 1;
250
+ continue;
34
251
  }
35
- out += `{yellow-fg}${escapeFn(match[1])}{/yellow-fg}`;
36
- cursor = index + match[0].length;
37
- match = pattern.exec(source);
38
252
  }
39
- if (cursor < source.length) {
40
- out += escapeFn(source.slice(cursor));
253
+
254
+ // *italic* / _italic_ (single delimiter; avoid ** / __)
255
+ if (
256
+ (source[i] === "*" && source[i + 1] !== "*")
257
+ || (source[i] === "_" && source[i + 1] !== "_")
258
+ ) {
259
+ const mark = source[i];
260
+ const close = source.indexOf(mark, i + 1);
261
+ if (close !== -1 && source[close + 1] !== mark) {
262
+ const inner = source.slice(i + 1, close);
263
+ if (inner && !inner.includes("\n")) {
264
+ out += styleItalic(renderInner(inner));
265
+ i = close + 1;
266
+ continue;
267
+ }
268
+ }
41
269
  }
42
- return out;
43
- };
270
+
271
+ // Accumulate plain run until the next markup candidate.
272
+ let next = source.length;
273
+ for (const ch of ["*", "_", "`"]) {
274
+ const at = source.indexOf(ch, i + 1);
275
+ if (at !== -1 && at < next) next = at;
276
+ }
277
+ // Also stop at `**` start from current if we failed to parse above.
278
+ out += escape(source.slice(i, next));
279
+ i = next === i ? i + 1 : next;
280
+ }
281
+
282
+ return out;
283
+ }
284
+
285
+ function renderTableBlock(rawRows = [], adapters = createBlessedAdapters()) {
286
+ if (!Array.isArray(rawRows) || rawRows.length === 0) return [];
287
+
288
+ const rows = [];
289
+ let alignments = [];
290
+ let headerUsed = false;
291
+
292
+ for (let i = 0; i < rawRows.length; i += 1) {
293
+ const line = rawRows[i];
294
+ if (isTableSeparatorLine(line)) {
295
+ if (rows.length > 0 && !headerUsed) {
296
+ rows[rows.length - 1].isHeader = true;
297
+ headerUsed = true;
298
+ }
299
+ alignments = parseSeparatorAlignments(line, Math.max(alignments.length, parseTableCells(line).length));
300
+ continue;
301
+ }
302
+ rows.push({
303
+ cells: parseTableCells(line),
304
+ isHeader: false,
305
+ });
306
+ }
307
+
308
+ if (rows.length === 0) return [];
309
+
310
+ const columnCount = rows.reduce((max, row) => Math.max(max, row.cells.length), 0);
311
+ while (alignments.length < columnCount) alignments.push("left");
312
+
313
+ const widths = Array.from({ length: columnCount }, () => 1);
314
+ for (const row of rows) {
315
+ for (let c = 0; c < columnCount; c += 1) {
316
+ widths[c] = Math.max(widths[c], plainCellWidth(row.cells[c] || ""));
317
+ }
318
+ }
319
+
320
+ const pipe = adapters.tablePipe || (() => "│");
321
+ const sepH = adapters.tableSepH || (() => "─");
322
+ const sepCross = adapters.tableSepCross || (() => "┼");
323
+ const styleHeader = adapters.tableHeaderCell || ((v) => v);
324
+ const styleCell = adapters.tableCell || ((v) => v);
325
+
326
+ const out = [];
327
+ let wroteHeaderSep = false;
328
+
329
+ for (const row of rows) {
330
+ const renderedCells = [];
331
+ for (let c = 0; c < columnCount; c += 1) {
332
+ const rawCell = row.cells[c] || "";
333
+ const inline = renderInlineMarkdown(rawCell, adapters);
334
+ const plain = String(rawCell)
335
+ .replace(/\*\*|__/g, "")
336
+ .replace(/`/g, "")
337
+ .replace(/\*/g, "");
338
+ const alignedPlain = alignCell(plain, widths[c], alignments[c] || "left");
339
+ const padRight = Math.max(0, visibleWidth(alignedPlain) - visibleWidth(plain));
340
+ const styled = row.isHeader ? styleHeader(inline) : styleCell(inline);
341
+ renderedCells.push(`${styled}${" ".repeat(padRight)}`);
342
+ }
343
+ out.push(`${pipe()} ${renderedCells.join(` ${pipe()} `)} ${pipe()}`);
344
+
345
+ if (row.isHeader && !wroteHeaderSep) {
346
+ const segments = widths.map((w) => {
347
+ const unit = sepH();
348
+ // sepH may be a styled string longer than 1 codepoint; repeat by width.
349
+ return unit.repeat(Math.max(1, w));
350
+ });
351
+ out.push(
352
+ `${pipe()}${sepH()}${segments.join(`${sepH()}${sepCross()}${sepH()}`)}${sepH()}${pipe()}`,
353
+ );
354
+ wroteHeaderSep = true;
355
+ }
356
+ }
357
+
358
+ return out;
359
+ }
360
+
361
+ function renderMarkdownLinesWithAdapters(text = "", state = {}, adapters = createBlessedAdapters()) {
362
+ const renderState = state && typeof state === "object" ? state : {};
363
+ if (typeof renderState.inCodeBlock !== "boolean") {
364
+ renderState.inCodeBlock = false;
365
+ }
44
366
 
45
367
  const lines = String(text || "").split(/\r?\n/);
46
368
  const out = [];
47
369
 
48
- for (const line of lines) {
370
+ for (let index = 0; index < lines.length; index += 1) {
371
+ const line = lines[index];
49
372
  const raw = stripLeakedEscapeTags(String(line || ""));
50
373
  const fenceMatch = raw.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
51
374
  if (fenceMatch) {
52
375
  if (!renderState.inCodeBlock) {
53
376
  const language = String(fenceMatch[3] || "").trim();
54
- const label = language
55
- ? `┌ code:${escapeFn(language)}`
56
- : "┌ code";
57
- out.push(`{gray-fg}${label}{/gray-fg}`);
377
+ out.push(adapters.fenceOpen(language));
58
378
  renderState.inCodeBlock = true;
59
379
  } else {
60
- out.push("{gray-fg}└{/gray-fg}");
380
+ out.push(adapters.fenceClose());
61
381
  renderState.inCodeBlock = false;
62
382
  }
63
383
  continue;
64
384
  }
65
385
 
66
386
  if (renderState.inCodeBlock) {
67
- out.push(`{gray-fg}│{/gray-fg} {white-fg}${escapeFn(raw)}{/white-fg}`);
68
- } else {
69
- const headingMatch = raw.match(/^(\s*)(#{1,6})\s+(.*)$/);
70
- if (headingMatch) {
71
- const indent = escapeFn(headingMatch[1] || "");
72
- const marks = escapeFn(headingMatch[2] || "");
73
- const content = renderInlineCode(headingMatch[3] || "");
74
- out.push(`${indent}{cyan-fg}${marks}{/cyan-fg} {bold}${content}{/bold}`);
75
- continue;
76
- }
387
+ out.push(adapters.fenceBody(raw));
388
+ continue;
389
+ }
77
390
 
78
- const quoteMatch = raw.match(/^(\s*)>\s?(.*)$/);
79
- if (quoteMatch) {
80
- const indent = escapeFn(quoteMatch[1] || "");
81
- const content = renderInlineCode(quoteMatch[2] || "");
82
- out.push(`${indent}{gray-fg}▍{/gray-fg} ${content}`);
83
- continue;
391
+ // GFM table block — collect contiguous pipe rows for column alignment.
392
+ if (isTableRowLine(raw)) {
393
+ const block = [raw];
394
+ let look = index + 1;
395
+ while (look < lines.length) {
396
+ const nextRaw = stripLeakedEscapeTags(String(lines[look] || ""));
397
+ if (!isTableRowLine(nextRaw)) break;
398
+ block.push(nextRaw);
399
+ look += 1;
84
400
  }
85
-
86
- const bulletMatch = raw.match(/^(\s*)([-*+])\s+(.*)$/);
87
- if (bulletMatch) {
88
- const indent = escapeFn(bulletMatch[1] || "");
89
- const content = renderInlineCode(bulletMatch[3] || "");
90
- out.push(`${indent}{gray-fg}•{/gray-fg} ${content}`);
401
+ const hasSep = block.some((row) => isTableSeparatorLine(row));
402
+ if (hasSep || block.length >= 2) {
403
+ out.push(...renderTableBlock(block, adapters));
404
+ index = look - 1;
91
405
  continue;
92
406
  }
407
+ }
93
408
 
94
- const orderedMatch = raw.match(/^(\s*)(\d+)\.\s+(.*)$/);
95
- if (orderedMatch) {
96
- const indent = escapeFn(orderedMatch[1] || "");
97
- const order = escapeFn(orderedMatch[2] || "");
98
- const content = renderInlineCode(orderedMatch[3] || "");
99
- out.push(`${indent}{gray-fg}${order}.{/gray-fg} ${content}`);
100
- continue;
101
- }
409
+ if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(raw)) {
410
+ out.push(adapters.rule());
411
+ continue;
412
+ }
102
413
 
103
- const errorMatch = raw.match(/^(\s*)(Error:\s+.*)$/i);
104
- if (errorMatch) {
105
- const indent = escapeFn(errorMatch[1] || "");
106
- const content = renderInlineCode(errorMatch[2] || "");
107
- out.push(`${indent}{red-fg}${content}{/red-fg}`);
108
- continue;
109
- }
414
+ const headingMatch = raw.match(/^(\s*)(#{1,6})\s+(.*)$/);
415
+ if (headingMatch) {
416
+ const indent = adapters.escape(headingMatch[1] || "");
417
+ const level = String(headingMatch[2] || "#").length;
418
+ const content = adapters.heading(
419
+ level,
420
+ renderInlineMarkdown(headingMatch[3] || "", adapters),
421
+ );
422
+ out.push(`${indent}${content}`);
423
+ continue;
424
+ }
425
+
426
+ const quoteMatch = raw.match(/^(\s*)>\s?(.*)$/);
427
+ if (quoteMatch) {
428
+ const indent = adapters.escape(quoteMatch[1] || "");
429
+ const content = renderInlineMarkdown(quoteMatch[2] || "", adapters);
430
+ out.push(`${indent}${adapters.quoteMarker()} ${content}`);
431
+ continue;
432
+ }
433
+
434
+ const bulletMatch = raw.match(/^(\s*)([-*+])\s+(.*)$/);
435
+ if (bulletMatch) {
436
+ const indent = adapters.escape(bulletMatch[1] || "");
437
+ const content = renderInlineMarkdown(bulletMatch[3] || "", adapters);
438
+ out.push(`${indent}${adapters.bulletMarker()} ${content}`);
439
+ continue;
440
+ }
441
+
442
+ const orderedMatch = raw.match(/^(\s*)(\d+)\.\s+(.*)$/);
443
+ if (orderedMatch) {
444
+ const indent = adapters.escape(orderedMatch[1] || "");
445
+ const content = renderInlineMarkdown(orderedMatch[3] || "", adapters);
446
+ out.push(`${indent}${adapters.orderedMarker(orderedMatch[2] || "")} ${content}`);
447
+ continue;
448
+ }
110
449
 
111
- out.push(renderInlineCode(raw));
450
+ const errorMatch = raw.match(/^(\s*)(Error:\s+.*)$/i);
451
+ if (errorMatch) {
452
+ const indent = adapters.escape(errorMatch[1] || "");
453
+ const content = renderInlineMarkdown(errorMatch[2] || "", adapters);
454
+ out.push(`${indent}${adapters.error(content)}`);
455
+ continue;
112
456
  }
457
+
458
+ out.push(renderInlineMarkdown(raw, adapters));
113
459
  }
114
460
 
115
461
  return out;
116
462
  }
117
463
 
464
+ function renderMarkdownLines(text = "", state = {}, escapeFn = (value) => String(value || "")) {
465
+ return renderMarkdownLinesWithAdapters(text, state, createBlessedAdapters(escapeFn));
466
+ }
467
+
468
+ function renderMarkdownLinesAnsi(text = "", state = {}) {
469
+ return renderMarkdownLinesWithAdapters(text, state, createAnsiAdapters());
470
+ }
471
+
118
472
  module.exports = {
119
473
  stripLeakedEscapeTags,
474
+ renderInlineMarkdown,
120
475
  renderMarkdownLines,
476
+ renderMarkdownLinesAnsi,
477
+ renderMarkdownLinesWithAdapters,
478
+ createBlessedAdapters,
479
+ createAnsiAdapters,
480
+ isTableRowLine,
481
+ isTableSeparatorLine,
482
+ parseTableCells,
483
+ renderTableBlock,
484
+ createMarkdownTableBuffer,
485
+ visibleWidth,
121
486
  };
@@ -457,7 +457,13 @@ const CHAT_LOG_ROW_PALETTE = {
457
457
  // margin-top on the next entry, because per-item rendering can't know a
458
458
  // group's end until the following entry arrives.
459
459
  function decorateStaticLogEntry(prev, entry) {
460
- const row = buildChatLogLineModel(entry);
460
+ const markdownState = prev && prev.markdownState && typeof prev.markdownState === "object"
461
+ ? { inCodeBlock: Boolean(prev.markdownState.inCodeBlock) }
462
+ : { inCodeBlock: false };
463
+ const sourceText = entry && typeof entry === "object" && entry.text != null
464
+ ? String(entry.text)
465
+ : entry;
466
+ const row = buildChatLogLineModel(sourceText, { markdownState });
461
467
  const continuation = Boolean(
462
468
  prev
463
469
  && (row.kind === "plain" || row.kind === "spacer")
@@ -468,7 +474,7 @@ function decorateStaticLogEntry(prev, entry) {
468
474
  // block, and only when the previous block was a transcript group (whose
469
475
  // old dynamic renderer contributed a trailing marginBottom).
470
476
  const marginBefore = Boolean(!continuation && prev && STATIC_GROUPABLE_KINDS.has(prev.groupKind));
471
- return { entry, row, groupKind, continuation, marginBefore };
477
+ return { entry, row, groupKind, continuation, marginBefore, markdownState };
472
478
  }
473
479
 
474
480
  function createInkStreamState({
@@ -713,8 +719,9 @@ function buildInternalLogRows(lines = [], width = 80, maxRows = 20) {
713
719
  let rendered = [classified.text];
714
720
  if (classified.markdown) {
715
721
  try {
716
- rendered = fmt.renderLogLinesWithMarkdown(classified.text, markdownState, (value) => String(value || ""))
717
- .map(stripInternalLogMarkup);
722
+ // Share ucode's ANSI markdown renderer so Ink can show bold/code
723
+ // without blessed tags (which would otherwise be stripped).
724
+ rendered = fmt.renderLogLinesWithMarkdownAnsi(classified.text, markdownState);
718
725
  } catch {
719
726
  rendered = [classified.text];
720
727
  }
@@ -2484,7 +2491,17 @@ function createChatApp({ React, ink, props, interactive = true }) {
2484
2491
  exit,
2485
2492
  ]);
2486
2493
 
2487
- const onArrowUpAtTop = useCallback(() => {
2494
+ const onArrowUpAtTop = useCallback((currentValue) => {
2495
+ // Clear @-target before history so Up from an empty ›@agent prompt
2496
+ // restores the bare › prompt instead of recalling a prior draft.
2497
+ const inputValue = currentValue != null ? currentValue : state.draft;
2498
+ if (fmt.shouldClearAgentSelectionOnUp({
2499
+ agentSelectionMode: state.agentSelectionMode,
2500
+ inputValue,
2501
+ })) {
2502
+ dispatch({ type: "agents/clearTarget" });
2503
+ return;
2504
+ }
2488
2505
  if (state.inputHistory.length > 0) {
2489
2506
  const next = Math.max(0, state.historyIndex - 1);
2490
2507
  if (next !== state.historyIndex || state.draft !== state.inputHistory[next]) {
@@ -2492,10 +2509,8 @@ function createChatApp({ React, ink, props, interactive = true }) {
2492
2509
  dispatch({ type: "draft/set", value: state.inputHistory[next] || "" });
2493
2510
  setCompletionSuppressedDraft(state.inputHistory[next] || "");
2494
2511
  setDraftVersion((v) => v + 1);
2495
- return;
2496
2512
  }
2497
2513
  }
2498
- if (state.agentSelectionMode) dispatch({ type: "agents/clearTarget" });
2499
2514
  }, [state.inputHistory, state.historyIndex, state.draft, state.agentSelectionMode]);
2500
2515
 
2501
2516
  const onArrowDownAtBottom = useCallback((currentValue) => {
@@ -2985,7 +3000,23 @@ function createChatApp({ React, ink, props, interactive = true }) {
2985
3000
  });
2986
3001
  return;
2987
3002
  }
2988
- if (key.return || key.tab) { acceptCompletion(); return; }
3003
+ if (key.return) {
3004
+ // Final/leaf completions submit immediately; parents only fill draft.
3005
+ const item = completions[Math.max(0, Math.min(completions.length - 1, completionIndex))];
3006
+ if (item && !item.hasChildren) {
3007
+ const cmd = String(item.replace || "").trim();
3008
+ setCompletionIndex(0);
3009
+ setCompletionSuppressedDraft(null);
3010
+ if (cmd) void submit(cmd);
3011
+ return;
3012
+ }
3013
+ acceptCompletion();
3014
+ return;
3015
+ }
3016
+ if (key.tab) {
3017
+ acceptCompletion();
3018
+ return;
3019
+ }
2989
3020
  if (key.escape) {
2990
3021
  setCompletionSuppressedDraft(null);
2991
3022
  dispatch({ type: "draft/clear" });