u-foo 2.5.15 → 3.0.1
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/code/agent.js +350 -246
- package/src/code/commands.js +16 -0
- package/src/code/context/assembler.js +18 -13
- package/src/code/context/executionSegment.js +102 -119
- package/src/code/context/index.js +11 -1
- package/src/code/context/planGraph.js +1410 -0
- package/src/code/context/planGraphService.js +857 -0
- package/src/code/context/planMode.js +405 -0
- package/src/code/context/planProjection.js +432 -0
- package/src/code/context/promptLayers.js +21 -5
- package/src/code/context/stateCommit.js +2 -0
- package/src/code/context/toolRuntime.js +172 -0
- package/src/code/context/userInteraction.js +457 -0
- package/src/code/context/userNudge.js +116 -0
- package/src/code/dispatch.js +17 -1
- package/src/code/index.js +4 -0
- package/src/code/nativeRunner.js +589 -172
- package/src/code/protocol/controlPlane.js +93 -0
- package/src/code/protocol/faultHarness.js +90 -0
- package/src/code/protocol/index.js +20 -0
- package/src/code/protocol/loopEvents.js +102 -0
- package/src/code/protocol/materialize.js +107 -0
- package/src/code/protocol/messageFixtures.js +116 -0
- package/src/code/protocol/ownership.js +147 -0
- package/src/code/protocol/protocolValidator.js +165 -0
- package/src/code/protocol/suspension.js +173 -0
- package/src/code/protocol/toolCallLedger.js +222 -0
- package/src/code/protocol/transitions.js +97 -0
- package/src/code/providers/anthropicMessagesTransport.js +93 -0
- package/src/code/providers/index.js +7 -0
- package/src/code/providers/openaiChatTransport.js +98 -0
- package/src/code/providers/transportContract.js +46 -0
- package/src/code/repl.js +147 -18
- 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 +394 -0
- package/src/code/runtime/taskRun.js +348 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +249 -0
- package/src/code/sessionStore.js +1 -10
- package/src/code/skills/injection.js +1 -0
- package/src/code/taskDecomposer.js +32 -8
- package/src/code/taskRoute.js +73 -0
- package/src/code/tools/askUser.js +11 -0
- package/src/code/tools/planGraph.js +29 -0
- package/src/ui/format/index.js +25 -1
- package/src/ui/format/markdownRenderer.js +224 -2
- package/src/ui/ink/UcodeApp.js +268 -22
- package/src/code/context/featureFlag.js +0 -13
|
@@ -4,8 +4,34 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { runNativeAgentTask } = require("./nativeRunner");
|
|
7
|
-
const { isContextV2Enabled } = require("./context/featureFlag");
|
|
8
7
|
const { assembleModelContext, recordToolCallInSession } = require("./context/assembler");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const {
|
|
10
|
+
buildSkillManifest,
|
|
11
|
+
renderActiveSkillBlock,
|
|
12
|
+
} = require("./skills");
|
|
13
|
+
const { sanitizeSkillContent } = require("./skills/injection");
|
|
14
|
+
|
|
15
|
+
function renderActiveSkillBodiesFromState(state = {}) {
|
|
16
|
+
const skills = Array.isArray(state.activeSkills) ? state.activeSkills : [];
|
|
17
|
+
const blocks = [];
|
|
18
|
+
for (const skill of skills) {
|
|
19
|
+
const skillPath = String(skill && skill.path || "").trim();
|
|
20
|
+
if (!skillPath) continue;
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(skillPath, "utf8");
|
|
23
|
+
const content = sanitizeSkillContent(raw);
|
|
24
|
+
const manifest = buildSkillManifest({
|
|
25
|
+
name: skill.name || "",
|
|
26
|
+
path: skillPath,
|
|
27
|
+
}, { bodyArtifactId: skill.bodyArtifactId || "" });
|
|
28
|
+
blocks.push(renderActiveSkillBlock(manifest, content));
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore missing skill bodies
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return blocks;
|
|
34
|
+
}
|
|
9
35
|
|
|
10
36
|
/**
|
|
11
37
|
* Decompose a bug fix task into manageable steps
|
|
@@ -135,13 +161,11 @@ async function runDecomposedTask({
|
|
|
135
161
|
messages = [],
|
|
136
162
|
sessionId = "",
|
|
137
163
|
state = null,
|
|
138
|
-
contextV2 = false,
|
|
139
164
|
systemBlocks = null,
|
|
140
165
|
}) {
|
|
141
166
|
const steps = decomposeBugFixTask(task);
|
|
142
167
|
const results = [];
|
|
143
168
|
let aborted = false;
|
|
144
|
-
const useV2 = Boolean(contextV2 || isContextV2Enabled());
|
|
145
169
|
|
|
146
170
|
// Check if already aborted
|
|
147
171
|
if (signal && signal.aborted) {
|
|
@@ -174,12 +198,13 @@ async function runDecomposedTask({
|
|
|
174
198
|
let stepMessages = messages;
|
|
175
199
|
let stepSystemPrompt = systemPrompt;
|
|
176
200
|
let stepSystemBlocks = systemBlocks;
|
|
177
|
-
if (
|
|
201
|
+
if (state) {
|
|
202
|
+
const skillBodies = renderActiveSkillBodiesFromState(state);
|
|
178
203
|
const assembled = assembleModelContext(state, {
|
|
179
204
|
workspaceRoot,
|
|
180
205
|
model,
|
|
181
206
|
provider,
|
|
182
|
-
turnDynamic: stepPrompt,
|
|
207
|
+
turnDynamic: [...skillBodies, stepPrompt].filter(Boolean).join("\n\n"),
|
|
183
208
|
});
|
|
184
209
|
stepMessages = assembled.messages;
|
|
185
210
|
stepSystemPrompt = assembled.systemPrompt;
|
|
@@ -197,13 +222,12 @@ async function runDecomposedTask({
|
|
|
197
222
|
timeoutMs: step.timeoutMs,
|
|
198
223
|
onToolEvent,
|
|
199
224
|
signal,
|
|
200
|
-
|
|
201
|
-
onArtifactPersisted: useV2 && state
|
|
225
|
+
onArtifactPersisted: state
|
|
202
226
|
? (persisted) => recordToolCallInSession(state, persisted, workspaceRoot)
|
|
203
227
|
: null,
|
|
204
228
|
});
|
|
205
229
|
|
|
206
|
-
if (
|
|
230
|
+
if (state && stepResult && Array.isArray(stepResult.messages)) {
|
|
207
231
|
const { syncMessagesToTranscript } = require("./context/assembler");
|
|
208
232
|
syncMessagesToTranscript(state, stepResult.messages, workspaceRoot);
|
|
209
233
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Structural / explicit decomposition upgrade decisions (R7).
|
|
5
|
+
* Not a language-specific bug-keyword classifier.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function shouldUpgradeToDecomposition(task = "", options = {}) {
|
|
9
|
+
const text = String(task || "").trim();
|
|
10
|
+
const reasons = [];
|
|
11
|
+
|
|
12
|
+
if (options.forceDirect === true || options.disableDecomposition === true) {
|
|
13
|
+
return {
|
|
14
|
+
upgrade: false,
|
|
15
|
+
reason: "forced_direct",
|
|
16
|
+
reasons: ["forced_direct"],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (options.forceDecomposition === true || options.forceDecompose === true) {
|
|
20
|
+
return {
|
|
21
|
+
upgrade: true,
|
|
22
|
+
reason: "forced_decomposition",
|
|
23
|
+
reasons: ["forced_decomposition"],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (!text) {
|
|
27
|
+
return { upgrade: false, reason: "empty", reasons: ["empty"] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Explicit user/model requests (EN + ZH).
|
|
31
|
+
if (/(?:\bdecompos(?:e|ition)\b|\bbreak\s+(?:this|it)\s+down\b|\bmulti[- ]?step\s+plan\b|拆解|分步|分解任务|制定计划)/i.test(text)) {
|
|
32
|
+
reasons.push("explicit_decompose_request");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Multiple independently verifiable goals (numbered / bulleted / conjunctions).
|
|
36
|
+
const numbered = (text.match(/(?:^|\n)\s*(?:\d+[\).]|[-*•])\s+\S+/g) || []).length;
|
|
37
|
+
if (numbered >= 2) reasons.push("multiple_listed_goals");
|
|
38
|
+
|
|
39
|
+
const multiClause = /(?:^|[;;。\n])\s*(?:and\s+also|also|然后|并且|同时|另外|以及)\b/i.test(text)
|
|
40
|
+
|| (text.split(/[;;]/).map((s) => s.trim()).filter((s) => s.length > 12).length >= 3);
|
|
41
|
+
if (multiClause) reasons.push("multi_clause_objectives");
|
|
42
|
+
|
|
43
|
+
// High-risk / multi-file structural cues (not "fix" alone).
|
|
44
|
+
if (/\b(?:across\s+(?:files?|modules?|packages?)|multiple\s+files?|refactor\s+the\s+\w+|迁移|跨文件|多文件)\b/i.test(text)) {
|
|
45
|
+
reasons.push("multi_file_or_refactor_scope");
|
|
46
|
+
}
|
|
47
|
+
if (/\b(?:checkpoint|rollback|migration|schema\s+change|生产环境|回滚)\b/i.test(text)) {
|
|
48
|
+
reasons.push("high_risk_change");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Runtime budget / prior failures may request upgrade.
|
|
52
|
+
if (Number(options.failureCount || 0) >= 2) {
|
|
53
|
+
reasons.push("repeated_failures");
|
|
54
|
+
}
|
|
55
|
+
if (options.modelRequestedUpgrade === true) {
|
|
56
|
+
reasons.push("model_route_decision");
|
|
57
|
+
}
|
|
58
|
+
if (options.hasPlanGraph === true) {
|
|
59
|
+
// Already structured — keep direct loop unless other reasons fire.
|
|
60
|
+
// (graph exists is not itself a decompose trigger)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const upgrade = reasons.length > 0;
|
|
64
|
+
return {
|
|
65
|
+
upgrade,
|
|
66
|
+
reason: upgrade ? reasons[0] : "direct_default",
|
|
67
|
+
reasons,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
shouldUpgradeToDecomposition,
|
|
73
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { runAskUserTool } = require("../context/userInteraction");
|
|
4
|
+
|
|
5
|
+
function runAskUserToolDispatch(args = {}, options = {}) {
|
|
6
|
+
return runAskUserTool(args, options);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
runAskUserTool: runAskUserToolDispatch,
|
|
11
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
runPlanGraphCommand,
|
|
5
|
+
normalizePlanGraphCommand,
|
|
6
|
+
} = require("../context/planGraphService");
|
|
7
|
+
|
|
8
|
+
function runPlanGraphTool(args = {}, options = {}) {
|
|
9
|
+
const command = normalizePlanGraphCommand(args) || args;
|
|
10
|
+
const result = runPlanGraphCommand(command, {
|
|
11
|
+
executionState: options.executionState,
|
|
12
|
+
runTool: options.runTool,
|
|
13
|
+
autoAdvance: options.autoAdvance !== false,
|
|
14
|
+
parallel: options.parallel !== false,
|
|
15
|
+
knownTools: options.knownTools,
|
|
16
|
+
maxNodeRuns: options.maxNodeRuns,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const payload = result.modelPayload || result;
|
|
20
|
+
return {
|
|
21
|
+
ok: payload.status === "accepted",
|
|
22
|
+
...payload,
|
|
23
|
+
executionState: result.executionState,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
runPlanGraphTool,
|
|
29
|
+
};
|
package/src/ui/format/index.js
CHANGED
|
@@ -129,7 +129,16 @@ function normalizeModelLabel(model = "") {
|
|
|
129
129
|
return "default";
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
function buildUcodeBannerLines({
|
|
132
|
+
function buildUcodeBannerLines({
|
|
133
|
+
model = "",
|
|
134
|
+
engine = "ufoo-core",
|
|
135
|
+
nickname = "",
|
|
136
|
+
agentId = "",
|
|
137
|
+
workspaceRoot = "",
|
|
138
|
+
sessionId = "",
|
|
139
|
+
width = 0,
|
|
140
|
+
planMode = false,
|
|
141
|
+
} = {}) {
|
|
133
142
|
const modelLabel = normalizeModelLabel(model);
|
|
134
143
|
void width;
|
|
135
144
|
void engine;
|
|
@@ -151,6 +160,9 @@ function buildUcodeBannerLines({ model = "", engine = "ufoo-core", nickname = ""
|
|
|
151
160
|
const infoLines = [];
|
|
152
161
|
infoLines.push(`${chalk.dim("Version:")} ${chalk.cyan.bold(UCODE_VERSION)}`);
|
|
153
162
|
infoLines.push(`${chalk.dim("Model:")} ${chalk.yellow(modelLabel)}`);
|
|
163
|
+
if (planMode) {
|
|
164
|
+
infoLines.push(`${chalk.dim("Mode:")} ${chalk.magenta.bold("PLAN")}`);
|
|
165
|
+
}
|
|
154
166
|
infoLines.push(`${chalk.dim("Dictionary:")} ${chalk.gray(shortPath)}`);
|
|
155
167
|
const normalizedSessionId = String(sessionId || "").trim();
|
|
156
168
|
if (normalizedSessionId) {
|
|
@@ -254,6 +266,16 @@ function renderLogLinesWithMarkdownAnsi(text = "", state = {}) {
|
|
|
254
266
|
return renderMarkdownLinesAnsi(text, state);
|
|
255
267
|
}
|
|
256
268
|
|
|
269
|
+
function createMarkdownTableBuffer() {
|
|
270
|
+
const { createMarkdownTableBuffer: create } = require("./markdownRenderer");
|
|
271
|
+
return create();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function isTableRowLine(line = "") {
|
|
275
|
+
const { isTableRowLine: check } = require("./markdownRenderer");
|
|
276
|
+
return check(line);
|
|
277
|
+
}
|
|
278
|
+
|
|
257
279
|
function messageContentText(message = {}) {
|
|
258
280
|
if (!message || typeof message !== "object") return "";
|
|
259
281
|
const content = message.content;
|
|
@@ -1233,6 +1255,8 @@ module.exports = {
|
|
|
1233
1255
|
planProjectsRail,
|
|
1234
1256
|
renderLogLinesWithMarkdown,
|
|
1235
1257
|
renderLogLinesWithMarkdownAnsi,
|
|
1258
|
+
createMarkdownTableBuffer,
|
|
1259
|
+
isTableRowLine,
|
|
1236
1260
|
resolveAgentSelectionOnDown,
|
|
1237
1261
|
resolveHistoryDownTransition,
|
|
1238
1262
|
shouldClearAgentSelectionOnUp,
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Produces either blessed tags (chat / legacy) or chalk ANSI (Ink ucode).
|
|
5
5
|
* Terminals cannot change font size, so headings/emphasis use color + weight
|
|
6
|
-
* instead of literal `#` / `**` markers.
|
|
6
|
+
* instead of literal `#` / `**` markers. GFM pipe tables are aligned into a
|
|
7
|
+
* compact spreadsheet-like grid.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
const chalk = require("chalk");
|
|
@@ -15,6 +16,116 @@ function stripLeakedEscapeTags(text = "") {
|
|
|
15
16
|
return withoutDanglingEscape.replace(/\{\s*\/?\s*e?s?c?a?p?e?[^{}\n]*$/gi, "");
|
|
16
17
|
}
|
|
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
|
+
|
|
18
129
|
function createBlessedAdapters(escapeFn = (value) => String(value || "")) {
|
|
19
130
|
const escape = (value) => escapeFn(value);
|
|
20
131
|
return {
|
|
@@ -40,6 +151,11 @@ function createBlessedAdapters(escapeFn = (value) => String(value || "")) {
|
|
|
40
151
|
fenceClose: () => "{gray-fg}└{/gray-fg}",
|
|
41
152
|
fenceBody: (value) => `{gray-fg}│{/gray-fg} {white-fg}${escape(value)}{/white-fg}`,
|
|
42
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,
|
|
43
159
|
};
|
|
44
160
|
}
|
|
45
161
|
|
|
@@ -76,6 +192,11 @@ function createAnsiAdapters() {
|
|
|
76
192
|
fenceClose: () => paint.gray("└"),
|
|
77
193
|
fenceBody: (value) => `${paint.gray("│")} ${paint.white(String(value || ""))}`,
|
|
78
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 || ""),
|
|
79
200
|
};
|
|
80
201
|
}
|
|
81
202
|
|
|
@@ -161,6 +282,82 @@ function renderInlineMarkdown(input = "", adapters = createBlessedAdapters()) {
|
|
|
161
282
|
return out;
|
|
162
283
|
}
|
|
163
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
|
+
|
|
164
361
|
function renderMarkdownLinesWithAdapters(text = "", state = {}, adapters = createBlessedAdapters()) {
|
|
165
362
|
const renderState = state && typeof state === "object" ? state : {};
|
|
166
363
|
if (typeof renderState.inCodeBlock !== "boolean") {
|
|
@@ -170,7 +367,8 @@ function renderMarkdownLinesWithAdapters(text = "", state = {}, adapters = creat
|
|
|
170
367
|
const lines = String(text || "").split(/\r?\n/);
|
|
171
368
|
const out = [];
|
|
172
369
|
|
|
173
|
-
for (
|
|
370
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
371
|
+
const line = lines[index];
|
|
174
372
|
const raw = stripLeakedEscapeTags(String(line || ""));
|
|
175
373
|
const fenceMatch = raw.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
|
|
176
374
|
if (fenceMatch) {
|
|
@@ -190,6 +388,24 @@ function renderMarkdownLinesWithAdapters(text = "", state = {}, adapters = creat
|
|
|
190
388
|
continue;
|
|
191
389
|
}
|
|
192
390
|
|
|
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;
|
|
400
|
+
}
|
|
401
|
+
const hasSep = block.some((row) => isTableSeparatorLine(row));
|
|
402
|
+
if (hasSep || block.length >= 2) {
|
|
403
|
+
out.push(...renderTableBlock(block, adapters));
|
|
404
|
+
index = look - 1;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
193
409
|
if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(raw)) {
|
|
194
410
|
out.push(adapters.rule());
|
|
195
411
|
continue;
|
|
@@ -261,4 +477,10 @@ module.exports = {
|
|
|
261
477
|
renderMarkdownLinesWithAdapters,
|
|
262
478
|
createBlessedAdapters,
|
|
263
479
|
createAnsiAdapters,
|
|
480
|
+
isTableRowLine,
|
|
481
|
+
isTableSeparatorLine,
|
|
482
|
+
parseTableCells,
|
|
483
|
+
renderTableBlock,
|
|
484
|
+
createMarkdownTableBuffer,
|
|
485
|
+
visibleWidth,
|
|
264
486
|
};
|