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.
- package/package.json +1 -1
- package/src/agents/prompts/native/environment.js +20 -8
- package/src/code/agent.js +517 -112
- package/src/code/commands.js +77 -0
- package/src/code/context/artifactGc.js +292 -0
- package/src/code/context/artifactIndex.js +161 -0
- package/src/code/context/artifacts.js +183 -0
- package/src/code/context/assembler.js +703 -0
- package/src/code/context/executionSegment.js +292 -0
- package/src/code/context/index.js +28 -0
- package/src/code/context/planGraph.js +1410 -0
- package/src/code/context/planGraphService.js +857 -0
- package/src/code/context/planMode.js +398 -0
- package/src/code/context/planProjection.js +432 -0
- package/src/code/context/projectSnapshot.js +201 -0
- package/src/code/context/promptLayers.js +175 -0
- package/src/code/context/reducers.js +328 -0
- package/src/code/context/stableJson.js +29 -0
- package/src/code/context/stateCommit.js +414 -0
- package/src/code/context/toolRuntime.js +172 -0
- package/src/code/context/transcript.js +182 -0
- package/src/code/context/transcriptSync.js +106 -0
- package/src/code/context/userInteraction.js +457 -0
- package/src/code/context/userNudge.js +116 -0
- package/src/code/context/workingSet.js +323 -0
- package/src/code/dispatch.js +20 -1
- package/src/code/index.js +8 -0
- package/src/code/modelCommand.js +87 -0
- package/src/code/nativeRunner.js +625 -34
- package/src/code/repl.js +196 -50
- package/src/code/runtime/agentWakeup.js +58 -0
- package/src/code/runtime/graphOwner.js +41 -0
- package/src/code/runtime/graphYieldRouter.js +42 -0
- package/src/code/runtime/index.js +15 -0
- package/src/code/runtime/loopMailbox.js +124 -0
- package/src/code/runtime/runtimeEvents.js +39 -0
- package/src/code/runtime/taskControl.js +565 -0
- package/src/code/runtime/taskFocus.js +165 -0
- package/src/code/runtime/taskLoop.js +383 -0
- package/src/code/runtime/taskRun.js +187 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +208 -0
- package/src/code/sessionStore.js +217 -15
- package/src/code/skills/index.js +10 -0
- package/src/code/skills/injection.js +66 -3
- package/src/code/skills/loader.js +21 -0
- package/src/code/skills/manifest.js +87 -0
- package/src/code/skills/render.js +15 -1
- package/src/code/taskDecomposer.js +56 -2
- package/src/code/tools/artifactRead.js +40 -0
- package/src/code/tools/askUser.js +11 -0
- package/src/code/tools/planGraph.js +29 -0
- package/src/code/tui.js +2 -0
- package/src/code/usageStore.js +15 -0
- package/src/ui/format/index.js +285 -45
- package/src/ui/format/markdownRenderer.js +436 -71
- package/src/ui/ink/ChatApp.js +39 -8
- package/src/ui/ink/UcodeApp.js +592 -43
- package/src/ui/ink/chatLogModel.js +102 -21
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared
|
|
2
|
+
* Shared markdown renderer for TUI log output.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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(
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
};
|
package/src/ui/ink/ChatApp.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
717
|
-
|
|
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
|
|
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" });
|