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.
Files changed (56) hide show
  1. package/package.json +1 -1
  2. package/src/code/agent.js +350 -246
  3. package/src/code/commands.js +16 -0
  4. package/src/code/context/assembler.js +18 -13
  5. package/src/code/context/executionSegment.js +102 -119
  6. package/src/code/context/index.js +11 -1
  7. package/src/code/context/planGraph.js +1410 -0
  8. package/src/code/context/planGraphService.js +857 -0
  9. package/src/code/context/planMode.js +405 -0
  10. package/src/code/context/planProjection.js +432 -0
  11. package/src/code/context/promptLayers.js +21 -5
  12. package/src/code/context/stateCommit.js +2 -0
  13. package/src/code/context/toolRuntime.js +172 -0
  14. package/src/code/context/userInteraction.js +457 -0
  15. package/src/code/context/userNudge.js +116 -0
  16. package/src/code/dispatch.js +17 -1
  17. package/src/code/index.js +4 -0
  18. package/src/code/nativeRunner.js +589 -172
  19. package/src/code/protocol/controlPlane.js +93 -0
  20. package/src/code/protocol/faultHarness.js +90 -0
  21. package/src/code/protocol/index.js +20 -0
  22. package/src/code/protocol/loopEvents.js +102 -0
  23. package/src/code/protocol/materialize.js +107 -0
  24. package/src/code/protocol/messageFixtures.js +116 -0
  25. package/src/code/protocol/ownership.js +147 -0
  26. package/src/code/protocol/protocolValidator.js +165 -0
  27. package/src/code/protocol/suspension.js +173 -0
  28. package/src/code/protocol/toolCallLedger.js +222 -0
  29. package/src/code/protocol/transitions.js +97 -0
  30. package/src/code/providers/anthropicMessagesTransport.js +93 -0
  31. package/src/code/providers/index.js +7 -0
  32. package/src/code/providers/openaiChatTransport.js +98 -0
  33. package/src/code/providers/transportContract.js +46 -0
  34. package/src/code/repl.js +147 -18
  35. package/src/code/runtime/agentWakeup.js +58 -0
  36. package/src/code/runtime/graphOwner.js +41 -0
  37. package/src/code/runtime/graphYieldRouter.js +42 -0
  38. package/src/code/runtime/index.js +15 -0
  39. package/src/code/runtime/loopMailbox.js +124 -0
  40. package/src/code/runtime/runtimeEvents.js +39 -0
  41. package/src/code/runtime/taskControl.js +565 -0
  42. package/src/code/runtime/taskFocus.js +165 -0
  43. package/src/code/runtime/taskLoop.js +394 -0
  44. package/src/code/runtime/taskRun.js +348 -0
  45. package/src/code/runtime/toolProvenance.js +70 -0
  46. package/src/code/runtime/workspaceLease.js +249 -0
  47. package/src/code/sessionStore.js +1 -10
  48. package/src/code/skills/injection.js +1 -0
  49. package/src/code/taskDecomposer.js +32 -8
  50. package/src/code/taskRoute.js +73 -0
  51. package/src/code/tools/askUser.js +11 -0
  52. package/src/code/tools/planGraph.js +29 -0
  53. package/src/ui/format/index.js +25 -1
  54. package/src/ui/format/markdownRenderer.js +224 -2
  55. package/src/ui/ink/UcodeApp.js +268 -22
  56. 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 (useV2 && state) {
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
- contextV2: useV2,
201
- onArtifactPersisted: useV2 && state
225
+ onArtifactPersisted: state
202
226
  ? (persisted) => recordToolCallInSession(state, persisted, workspaceRoot)
203
227
  : null,
204
228
  });
205
229
 
206
- if (useV2 && state && stepResult && Array.isArray(stepResult.messages)) {
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
+ };
@@ -129,7 +129,16 @@ function normalizeModelLabel(model = "") {
129
129
  return "default";
130
130
  }
131
131
 
132
- function buildUcodeBannerLines({ model = "", engine = "ufoo-core", nickname = "", agentId = "", workspaceRoot = "", sessionId = "", width = 0 } = {}) {
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 (const line of lines) {
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
  };