u-foo 2.5.13 → 2.5.15

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +339 -24
  4. package/src/code/commands.js +61 -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 +698 -0
  9. package/src/code/context/executionSegment.js +314 -0
  10. package/src/code/context/featureFlag.js +13 -0
  11. package/src/code/context/index.js +18 -0
  12. package/src/code/context/projectSnapshot.js +201 -0
  13. package/src/code/context/promptLayers.js +159 -0
  14. package/src/code/context/reducers.js +328 -0
  15. package/src/code/context/stableJson.js +29 -0
  16. package/src/code/context/stateCommit.js +412 -0
  17. package/src/code/context/transcript.js +182 -0
  18. package/src/code/context/transcriptSync.js +106 -0
  19. package/src/code/context/workingSet.js +323 -0
  20. package/src/code/dispatch.js +4 -1
  21. package/src/code/index.js +6 -0
  22. package/src/code/modelCommand.js +87 -0
  23. package/src/code/nativeRunner.js +187 -31
  24. package/src/code/repl.js +36 -32
  25. package/src/code/sessionStore.js +227 -15
  26. package/src/code/skills/index.js +10 -0
  27. package/src/code/skills/injection.js +65 -3
  28. package/src/code/skills/loader.js +21 -0
  29. package/src/code/skills/manifest.js +87 -0
  30. package/src/code/skills/render.js +15 -1
  31. package/src/code/taskDecomposer.js +32 -2
  32. package/src/code/tools/artifactRead.js +40 -0
  33. package/src/code/tui.js +2 -0
  34. package/src/code/usageStore.js +15 -0
  35. package/src/ui/format/index.js +260 -44
  36. package/src/ui/format/markdownRenderer.js +215 -72
  37. package/src/ui/ink/ChatApp.js +39 -8
  38. package/src/ui/ink/UcodeApp.js +408 -55
  39. package/src/ui/ink/chatLogModel.js +102 -21
@@ -1,10 +1,13 @@
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.
6
7
  */
7
8
 
9
+ const chalk = require("chalk");
10
+
8
11
  function stripLeakedEscapeTags(text = "") {
9
12
  const source = String(text == null ? "" : text);
10
13
  const withoutClosedTags = source.replace(/\{[^{}\n]*escape[^{}\n]*\}/gi, "");
@@ -12,35 +15,157 @@ function stripLeakedEscapeTags(text = "") {
12
15
  return withoutDanglingEscape.replace(/\{\s*\/?\s*e?s?c?a?p?e?[^{}\n]*$/gi, "");
13
16
  }
14
17
 
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;
18
+ function createBlessedAdapters(escapeFn = (value) => String(value || "")) {
19
+ const escape = (value) => escapeFn(value);
20
+ return {
21
+ escape,
22
+ bold: (value) => `{bold}{white-fg}${escape(value)}{/white-fg}{/bold}`,
23
+ italic: (value) => `{italic}{gray-fg}${escape(value)}{/gray-fg}{/italic}`,
24
+ code: (value) => `{yellow-fg}${escape(value)}{/yellow-fg}`,
25
+ heading: (level, value) => {
26
+ const depth = Math.max(1, Math.min(6, Number(level) || 1));
27
+ if (depth <= 2) return `{cyan-fg}{bold}${value}{/bold}{/cyan-fg}`;
28
+ if (depth === 3) return `{blue-fg}{bold}${value}{/bold}{/blue-fg}`;
29
+ return `{bold}${value}{/bold}`;
30
+ },
31
+ quoteMarker: () => "{gray-fg}│{/gray-fg}",
32
+ bulletMarker: () => "{gray-fg}•{/gray-fg}",
33
+ orderedMarker: (value) => `{gray-fg}${escape(value)}.{/gray-fg}`,
34
+ rule: () => "{gray-fg}────────────────────────{/gray-fg}",
35
+ fenceOpen: (language) => (
36
+ language
37
+ ? `{gray-fg}┌ code:${escape(language)}{/gray-fg}`
38
+ : "{gray-fg}┌ code{/gray-fg}"
39
+ ),
40
+ fenceClose: () => "{gray-fg}└{/gray-fg}",
41
+ fenceBody: (value) => `{gray-fg}│{/gray-fg} {white-fg}${escape(value)}{/white-fg}`,
42
+ error: (value) => `{red-fg}${value}{/red-fg}`,
43
+ };
44
+ }
45
+
46
+ function createAnsiAdapters() {
47
+ // Ink always paints into a TTY-capable stdout; force color so bold/heading
48
+ // styles survive even when chalk's autodetection thinks we're non-TTY
49
+ // (e.g. piped test harnesses that still render Ink).
50
+ const paint = typeof chalk.Instance === "function"
51
+ ? new chalk.Instance({ level: Math.max(Number(chalk.level) || 0, 2) })
52
+ : chalk;
53
+ return {
54
+ escape: (value) => String(value || ""),
55
+ // Bold gets weight + brighter foreground so it reads even when the
56
+ // terminal theme barely differentiates ANSI bold.
57
+ bold: (value) => paint.bold.whiteBright(String(value || "")),
58
+ italic: (value) => paint.italic.dim(String(value || "")),
59
+ code: (value) => paint.yellow(String(value || "")),
60
+ heading: (level, value) => {
61
+ const depth = Math.max(1, Math.min(6, Number(level) || 1));
62
+ const text = String(value || "");
63
+ if (depth <= 2) return paint.bold.cyan(text);
64
+ if (depth === 3) return paint.bold.blue(text);
65
+ return paint.bold(text);
66
+ },
67
+ quoteMarker: () => paint.gray("│"),
68
+ bulletMarker: () => paint.gray("•"),
69
+ orderedMarker: (value) => paint.gray(`${value}.`),
70
+ rule: () => paint.gray("────────────────────────"),
71
+ fenceOpen: (language) => (
72
+ language
73
+ ? paint.gray(`┌ code:${language}`)
74
+ : paint.gray("┌ code")
75
+ ),
76
+ fenceClose: () => paint.gray("└"),
77
+ fenceBody: (value) => `${paint.gray("│")} ${paint.white(String(value || ""))}`,
78
+ error: (value) => paint.red(String(value || "")),
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Apply inline markdown to a single line.
84
+ * Scans left-to-right so **bold** wins over nested `code`/`*` patterns
85
+ * (LLMs often emit **`name`** which previously left literal asterisks).
86
+ */
87
+ function renderInlineMarkdown(input = "", adapters = createBlessedAdapters()) {
88
+ const source = String(input || "");
89
+ if (!source) return "";
90
+
91
+ const escape = adapters.escape || ((value) => String(value || ""));
92
+ const styleBold = adapters.bold || escape;
93
+ const styleItalic = adapters.italic || escape;
94
+ const styleCode = adapters.code || escape;
95
+
96
+ if (!source.includes("`") && !source.includes("*") && !source.includes("_")) {
97
+ return escape(source);
19
98
  }
20
99
 
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));
100
+ const renderInner = (chunk) => renderInlineMarkdown(chunk, adapters);
101
+
102
+ let out = "";
103
+ let i = 0;
104
+ while (i < source.length) {
105
+ // **bold** / __bold__
106
+ if (source.startsWith("**", i) || source.startsWith("__", i)) {
107
+ const mark = source.slice(i, i + 2);
108
+ const close = source.indexOf(mark, i + 2);
109
+ if (close !== -1) {
110
+ const inner = source.slice(i + 2, close);
111
+ out += styleBold(renderInner(inner));
112
+ i = close + 2;
113
+ continue;
34
114
  }
35
- out += `{yellow-fg}${escapeFn(match[1])}{/yellow-fg}`;
36
- cursor = index + match[0].length;
37
- match = pattern.exec(source);
38
115
  }
39
- if (cursor < source.length) {
40
- out += escapeFn(source.slice(cursor));
116
+
117
+ // `code`
118
+ if (source[i] === "`") {
119
+ const close = source.indexOf("`", i + 1);
120
+ if (close !== -1) {
121
+ let inner = source.slice(i + 1, close);
122
+ // Code that is only a bold/italic wrapper → treat as emphasis.
123
+ const boldOnly = inner.match(/^\*\*(.+)\*\*$/) || inner.match(/^__(.+)__$/);
124
+ const italicOnly = !boldOnly && (inner.match(/^\*(.+)\*$/) || inner.match(/^_(.+)_$/));
125
+ if (boldOnly) out += styleBold(renderInner(boldOnly[1]));
126
+ else if (italicOnly) out += styleItalic(renderInner(italicOnly[1]));
127
+ else out += styleCode(inner);
128
+ i = close + 1;
129
+ continue;
130
+ }
41
131
  }
42
- return out;
43
- };
132
+
133
+ // *italic* / _italic_ (single delimiter; avoid ** / __)
134
+ if (
135
+ (source[i] === "*" && source[i + 1] !== "*")
136
+ || (source[i] === "_" && source[i + 1] !== "_")
137
+ ) {
138
+ const mark = source[i];
139
+ const close = source.indexOf(mark, i + 1);
140
+ if (close !== -1 && source[close + 1] !== mark) {
141
+ const inner = source.slice(i + 1, close);
142
+ if (inner && !inner.includes("\n")) {
143
+ out += styleItalic(renderInner(inner));
144
+ i = close + 1;
145
+ continue;
146
+ }
147
+ }
148
+ }
149
+
150
+ // Accumulate plain run until the next markup candidate.
151
+ let next = source.length;
152
+ for (const ch of ["*", "_", "`"]) {
153
+ const at = source.indexOf(ch, i + 1);
154
+ if (at !== -1 && at < next) next = at;
155
+ }
156
+ // Also stop at `**` start from current if we failed to parse above.
157
+ out += escape(source.slice(i, next));
158
+ i = next === i ? i + 1 : next;
159
+ }
160
+
161
+ return out;
162
+ }
163
+
164
+ function renderMarkdownLinesWithAdapters(text = "", state = {}, adapters = createBlessedAdapters()) {
165
+ const renderState = state && typeof state === "object" ? state : {};
166
+ if (typeof renderState.inCodeBlock !== "boolean") {
167
+ renderState.inCodeBlock = false;
168
+ }
44
169
 
45
170
  const lines = String(text || "").split(/\r?\n/);
46
171
  const out = [];
@@ -51,71 +176,89 @@ function renderMarkdownLines(text = "", state = {}, escapeFn = (value) => String
51
176
  if (fenceMatch) {
52
177
  if (!renderState.inCodeBlock) {
53
178
  const language = String(fenceMatch[3] || "").trim();
54
- const label = language
55
- ? `┌ code:${escapeFn(language)}`
56
- : "┌ code";
57
- out.push(`{gray-fg}${label}{/gray-fg}`);
179
+ out.push(adapters.fenceOpen(language));
58
180
  renderState.inCodeBlock = true;
59
181
  } else {
60
- out.push("{gray-fg}└{/gray-fg}");
182
+ out.push(adapters.fenceClose());
61
183
  renderState.inCodeBlock = false;
62
184
  }
63
185
  continue;
64
186
  }
65
187
 
66
188
  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
- }
189
+ out.push(adapters.fenceBody(raw));
190
+ continue;
191
+ }
77
192
 
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;
84
- }
193
+ if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(raw)) {
194
+ out.push(adapters.rule());
195
+ continue;
196
+ }
85
197
 
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}`);
91
- continue;
92
- }
198
+ const headingMatch = raw.match(/^(\s*)(#{1,6})\s+(.*)$/);
199
+ if (headingMatch) {
200
+ const indent = adapters.escape(headingMatch[1] || "");
201
+ const level = String(headingMatch[2] || "#").length;
202
+ const content = adapters.heading(
203
+ level,
204
+ renderInlineMarkdown(headingMatch[3] || "", adapters),
205
+ );
206
+ out.push(`${indent}${content}`);
207
+ continue;
208
+ }
93
209
 
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
- }
210
+ const quoteMatch = raw.match(/^(\s*)>\s?(.*)$/);
211
+ if (quoteMatch) {
212
+ const indent = adapters.escape(quoteMatch[1] || "");
213
+ const content = renderInlineMarkdown(quoteMatch[2] || "", adapters);
214
+ out.push(`${indent}${adapters.quoteMarker()} ${content}`);
215
+ continue;
216
+ }
102
217
 
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
- }
218
+ const bulletMatch = raw.match(/^(\s*)([-*+])\s+(.*)$/);
219
+ if (bulletMatch) {
220
+ const indent = adapters.escape(bulletMatch[1] || "");
221
+ const content = renderInlineMarkdown(bulletMatch[3] || "", adapters);
222
+ out.push(`${indent}${adapters.bulletMarker()} ${content}`);
223
+ continue;
224
+ }
225
+
226
+ const orderedMatch = raw.match(/^(\s*)(\d+)\.\s+(.*)$/);
227
+ if (orderedMatch) {
228
+ const indent = adapters.escape(orderedMatch[1] || "");
229
+ const content = renderInlineMarkdown(orderedMatch[3] || "", adapters);
230
+ out.push(`${indent}${adapters.orderedMarker(orderedMatch[2] || "")} ${content}`);
231
+ continue;
232
+ }
110
233
 
111
- out.push(renderInlineCode(raw));
234
+ const errorMatch = raw.match(/^(\s*)(Error:\s+.*)$/i);
235
+ if (errorMatch) {
236
+ const indent = adapters.escape(errorMatch[1] || "");
237
+ const content = renderInlineMarkdown(errorMatch[2] || "", adapters);
238
+ out.push(`${indent}${adapters.error(content)}`);
239
+ continue;
112
240
  }
241
+
242
+ out.push(renderInlineMarkdown(raw, adapters));
113
243
  }
114
244
 
115
245
  return out;
116
246
  }
117
247
 
248
+ function renderMarkdownLines(text = "", state = {}, escapeFn = (value) => String(value || "")) {
249
+ return renderMarkdownLinesWithAdapters(text, state, createBlessedAdapters(escapeFn));
250
+ }
251
+
252
+ function renderMarkdownLinesAnsi(text = "", state = {}) {
253
+ return renderMarkdownLinesWithAdapters(text, state, createAnsiAdapters());
254
+ }
255
+
118
256
  module.exports = {
119
257
  stripLeakedEscapeTags,
258
+ renderInlineMarkdown,
120
259
  renderMarkdownLines,
260
+ renderMarkdownLinesAnsi,
261
+ renderMarkdownLinesWithAdapters,
262
+ createBlessedAdapters,
263
+ createAnsiAdapters,
121
264
  };
@@ -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" });