@pentoshi/clai 3.11.4 → 3.11.5

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.
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Fenced code block presentation for chat + pager markdown.
3
+ *
4
+ * Renders a bordered panel with a language label, real syntax colors, exact
5
+ * source indentation, and column-accurate soft wrapping (no ellipsis, no
6
+ * dropped characters, wide glyphs never straddle the right border).
7
+ */
8
+ import chalk from "chalk";
9
+ import stringWidth from "string-width";
10
+ import { detectThemeHint } from "../tui-v2/bootstrap/capabilities.js";
11
+ import { themeFor } from "../tui-v2/rendering/theme.js";
12
+ import { emptyCarry, highlightLineForPath, } from "../tui-v2/rendering/syntax-highlight.js";
13
+ /** Columns consumed by `│ ` + body + ` │`. */
14
+ export const CODE_BLOCK_CHROME = 4;
15
+ const TAB_WIDTH = 2;
16
+ const HANGING_INDENT = 2;
17
+ /** Narrowest code body worth rendering — the panel is sized to protect it. */
18
+ const MIN_BODY = 8;
19
+ const MIN_WIDTH = CODE_BLOCK_CHROME + MIN_BODY;
20
+ const MAX_WIDTH = 120;
21
+ /** Only honour a soft break past this fraction of the row budget. */
22
+ const SOFT_BREAK_FLOOR = 0.5;
23
+ const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, {
24
+ granularity: "grapheme",
25
+ });
26
+ /**
27
+ * Colors resolve from the shared theme tokens — the same syntax palette the
28
+ * diff cards use — against the terminal's light/dark hint, so a light terminal
29
+ * is never handed dark-tuned code colors.
30
+ */
31
+ function buildPalette() {
32
+ const theme = themeFor(detectThemeHint(process.env));
33
+ return {
34
+ border: chalk.hex(theme.diffGutter),
35
+ label: chalk.hex(theme.muted),
36
+ syntax: {
37
+ plain: chalk.hex(theme.foreground),
38
+ keyword: chalk.hex(theme.synKeyword),
39
+ string: chalk.hex(theme.synString),
40
+ comment: chalk.hex(theme.synComment),
41
+ number: chalk.hex(theme.synNumber),
42
+ function: chalk.hex(theme.synFunction),
43
+ type: chalk.hex(theme.synType),
44
+ property: chalk.hex(theme.synProperty),
45
+ operator: chalk.hex(theme.synOperator),
46
+ punctuation: chalk.hex(theme.muted),
47
+ regex: chalk.hex(theme.synRegex),
48
+ },
49
+ };
50
+ }
51
+ let resolvedPalette;
52
+ function palette() {
53
+ resolvedPalette ??= buildPalette();
54
+ return resolvedPalette;
55
+ }
56
+ /** Fence info strings that are not already file extensions. */
57
+ const INFO_EXTENSION = {
58
+ typescript: "ts",
59
+ javascript: "js",
60
+ node: "js",
61
+ nodejs: "js",
62
+ react: "jsx",
63
+ python: "py",
64
+ python3: "py",
65
+ ipython: "py",
66
+ shell: "sh",
67
+ shellsession: "sh",
68
+ console: "sh",
69
+ terminal: "sh",
70
+ golang: "go",
71
+ rust: "rs",
72
+ ruby: "rb",
73
+ kotlin: "kt",
74
+ csharp: "cs",
75
+ "c#": "cs",
76
+ "c++": "cpp",
77
+ cplusplus: "cpp",
78
+ objc: "m",
79
+ "objective-c": "m",
80
+ haskell: "hs",
81
+ elixir: "ex",
82
+ erlang: "erl",
83
+ clojure: "clj",
84
+ scheme: "scm",
85
+ julia: "jl",
86
+ docker: "dockerfile",
87
+ make: "makefile",
88
+ terraform: "tf",
89
+ protobuf: "proto",
90
+ postgres: "sql",
91
+ postgresql: "sql",
92
+ plpgsql: "sql",
93
+ sqlite: "sql",
94
+ powershell: "ps1",
95
+ batch: "bat",
96
+ vim: "txt",
97
+ tex: "txt",
98
+ latex: "txt",
99
+ plaintext: "txt",
100
+ plain: "txt",
101
+ text: "txt",
102
+ output: "txt",
103
+ none: "txt",
104
+ };
105
+ /** Short fence tags shown under their full language name. */
106
+ const INFO_LABEL = {
107
+ ts: "typescript",
108
+ js: "javascript",
109
+ py: "python",
110
+ rb: "ruby",
111
+ rs: "rust",
112
+ kt: "kotlin",
113
+ cs: "c#",
114
+ cpp: "c++",
115
+ sh: "shell",
116
+ yml: "yaml",
117
+ md: "markdown",
118
+ ps1: "powershell",
119
+ ex: "elixir",
120
+ hs: "haskell",
121
+ jl: "julia",
122
+ clj: "clojure",
123
+ tf: "terraform",
124
+ };
125
+ const FENCE_OPEN_RE = /^\s*(`{3,}|~{3,})[ \t]*(.*)$/;
126
+ /** Match an opening fence and capture its marker + info string. */
127
+ export function matchCodeFenceOpen(line) {
128
+ const match = FENCE_OPEN_RE.exec(line);
129
+ if (!match)
130
+ return undefined;
131
+ return { marker: match[1], info: (match[2] ?? "").trim() };
132
+ }
133
+ /** A fence closes on a bare run of the same character, at least as long. */
134
+ export function isCodeFenceClose(line, marker) {
135
+ const match = /^\s*(`{3,}|~{3,})\s*$/.exec(line);
136
+ if (!match)
137
+ return false;
138
+ const run = match[1];
139
+ return run[0] === marker[0] && run.length >= marker.length;
140
+ }
141
+ function looksLikePath(info) {
142
+ if (info.includes("/") || info.includes("\\"))
143
+ return true;
144
+ return /^[\w.-]+\.[A-Za-z][\w]*$/.test(info) && !info.startsWith(".");
145
+ }
146
+ export function openCodeFence(marker, info) {
147
+ // Info strings carry attributes models pick up from docs: `ts {1,3}`,
148
+ // `py title="x"`. Only the first token identifies the language.
149
+ const token = (info.split(/[\s,;]+/)[0] ?? "").replace(/^[{"']|["']$/g, "");
150
+ const tag = token.toLowerCase();
151
+ if (looksLikePath(token)) {
152
+ return { marker, label: token, langPath: token, carry: emptyCarry(), pendingBlanks: 0 };
153
+ }
154
+ const extension = INFO_EXTENSION[tag] ?? tag;
155
+ return {
156
+ marker,
157
+ label: INFO_LABEL[tag] ?? (tag || "code"),
158
+ langPath: extension ? `code.${extension}` : "code.txt",
159
+ carry: emptyCarry(),
160
+ pendingBlanks: 0,
161
+ };
162
+ }
163
+ /** Clamp the panel to the wrap budget so it never overflows the pane. */
164
+ export function codeBlockWidth(wrapWidth) {
165
+ return Math.max(MIN_WIDTH, Math.min(Math.floor(wrapWidth), MAX_WIDTH));
166
+ }
167
+ function rule(count) {
168
+ return "─".repeat(Math.max(0, count));
169
+ }
170
+ function truncateLabel(label, maxWidth) {
171
+ if (maxWidth < 1)
172
+ return "";
173
+ let text = "";
174
+ let width = 0;
175
+ for (const cluster of graphemes(label)) {
176
+ const clusterWidth = stringWidth(cluster);
177
+ if (width + clusterWidth > maxWidth)
178
+ break;
179
+ text += cluster;
180
+ width += clusterWidth;
181
+ }
182
+ return text;
183
+ }
184
+ export function codeBlockTop(label, width) {
185
+ const { border, label: paintLabel } = palette();
186
+ const span = codeBlockWidth(width) - 2;
187
+ const room = span - 4;
188
+ const labelWidth = stringWidth(label);
189
+ const text = room < 1
190
+ ? ""
191
+ : labelWidth <= room
192
+ ? label
193
+ : `${truncateLabel(label, room - 1)}…`;
194
+ if (!text)
195
+ return border(`╭${rule(span)}╮`);
196
+ return (border("╭─") +
197
+ paintLabel(` ${text} `) +
198
+ border(`${rule(span - 3 - stringWidth(text))}╮`));
199
+ }
200
+ export function codeBlockBottom(width) {
201
+ return palette().border(`╰${rule(codeBlockWidth(width) - 2)}╯`);
202
+ }
203
+ /** Pad a painted body to the panel's inner width and add both borders. */
204
+ export function codeBlockRow(body, bodyWidth, width) {
205
+ const { border } = palette();
206
+ const inner = codeBlockWidth(width) - CODE_BLOCK_CHROME;
207
+ const pad = " ".repeat(Math.max(0, inner - bodyWidth));
208
+ return `${border("│")} ${body}${pad} ${border("│")}`;
209
+ }
210
+ function graphemes(text) {
211
+ return Array.from(GRAPHEME_SEGMENTER.segment(text), ({ segment }) => segment);
212
+ }
213
+ function expandTabs(line) {
214
+ if (!line.includes("\t"))
215
+ return line;
216
+ let out = "";
217
+ let col = 0;
218
+ for (const ch of graphemes(line)) {
219
+ if (ch === "\t") {
220
+ const gap = TAB_WIDTH - (col % TAB_WIDTH);
221
+ out += " ".repeat(gap);
222
+ col += gap;
223
+ continue;
224
+ }
225
+ out += ch;
226
+ col += stringWidth(ch);
227
+ }
228
+ return out;
229
+ }
230
+ function toCells(spans) {
231
+ const text = spans.map((span) => span.text).join("");
232
+ const kinds = [];
233
+ for (const span of spans) {
234
+ for (let i = 0; i < span.text.length; i += 1)
235
+ kinds.push(span.kind);
236
+ }
237
+ const cells = [];
238
+ for (const { segment, index } of GRAPHEME_SEGMENTER.segment(text)) {
239
+ cells.push({
240
+ ch: segment,
241
+ kind: kinds[index] ?? "plain",
242
+ w: stringWidth(segment),
243
+ });
244
+ }
245
+ return cells;
246
+ }
247
+ /** Break after separators so wrapped code splits at token edges when it can. */
248
+ function breaksAfter(ch) {
249
+ return ch === " " || ",;)]}>".includes(ch);
250
+ }
251
+ /**
252
+ * Split one highlighted source line into rows that fit its column budget.
253
+ * Breaks at a token edge when one falls in the back half of the row, otherwise
254
+ * hard-breaks. Separator cells remain in the output so wrapping never changes
255
+ * meaningful code whitespace. Always consumes at least one cell so a glyph
256
+ * wider than the budget cannot stall the loop.
257
+ */
258
+ function sliceRows(cells, firstBudget, restBudget) {
259
+ const rows = [];
260
+ let start = 0;
261
+ while (start < cells.length) {
262
+ const budget = Math.max(1, rows.length === 0 ? firstBudget : restBudget);
263
+ const floor = budget * SOFT_BREAK_FLOOR;
264
+ let used = 0;
265
+ let end = start;
266
+ let soft = -1;
267
+ while (end < cells.length && used + cells[end].w <= budget) {
268
+ used += cells[end].w;
269
+ end += 1;
270
+ if (used >= floor && breaksAfter(cells[end - 1].ch))
271
+ soft = end;
272
+ }
273
+ if (end >= cells.length) {
274
+ rows.push(cells.slice(start));
275
+ return rows;
276
+ }
277
+ const cut = soft > start ? soft : Math.max(end, start + 1);
278
+ rows.push(cells.slice(start, cut));
279
+ start = cut;
280
+ }
281
+ return rows.length > 0 ? rows : [[]];
282
+ }
283
+ function paint(cells) {
284
+ const { syntax } = palette();
285
+ let text = "";
286
+ let width = 0;
287
+ let run = "";
288
+ let kind = "plain";
289
+ for (const cell of cells) {
290
+ if (cell.kind !== kind) {
291
+ if (run)
292
+ text += syntax[kind](run);
293
+ run = "";
294
+ kind = cell.kind;
295
+ }
296
+ run += cell.ch;
297
+ width += cell.w;
298
+ }
299
+ if (run)
300
+ text += syntax[kind](run);
301
+ return { text, width };
302
+ }
303
+ function leadingSpaces(line) {
304
+ const match = /^ */.exec(line);
305
+ return match ? match[0].length : 0;
306
+ }
307
+ /**
308
+ * Streaming re-renders the whole open fence on every frame, so highlighting +
309
+ * wrapping one source line is memoised on its language, width, and inbound
310
+ * carry. The carry after the line is replayed on a hit so multi-line strings
311
+ * and block comments still chain correctly.
312
+ */
313
+ const ROW_CACHE = new Map();
314
+ const ROW_CACHE_MAX = 4096;
315
+ function carryKey(carry) {
316
+ return `${carry.inBlockComment ? 1 : 0}${carry.inTripleString ? 1 : 0}${carry.tripleQuote ?? ""}`;
317
+ }
318
+ function snapshotCarry(carry) {
319
+ return {
320
+ inBlockComment: carry.inBlockComment,
321
+ inTripleString: carry.inTripleString,
322
+ tripleQuote: carry.tripleQuote,
323
+ };
324
+ }
325
+ function restoreCarry(target, source) {
326
+ target.inBlockComment = source.inBlockComment;
327
+ target.inTripleString = source.inTripleString;
328
+ target.tripleQuote = source.tripleQuote;
329
+ }
330
+ function cacheRows(key, rows, carry) {
331
+ if (ROW_CACHE.size >= ROW_CACHE_MAX) {
332
+ const oldest = ROW_CACHE.keys().next();
333
+ if (!oldest.done)
334
+ ROW_CACHE.delete(oldest.value);
335
+ }
336
+ ROW_CACHE.set(key, { rows, carry: snapshotCarry(carry) });
337
+ }
338
+ /**
339
+ * Render one source line as complete panel rows. Blank lines are buffered so a
340
+ * block that ends with padding does not push empty rows against the footer.
341
+ */
342
+ export function codeBlockRows(source, fence, width) {
343
+ const panel = codeBlockWidth(width);
344
+ const inner = panel - CODE_BLOCK_CHROME;
345
+ const line = expandTabs(source.replace(/\s+$/, ""));
346
+ // Leading blanks are dropped outright; interior ones survive as padding once
347
+ // a following content row proves they were not trailing.
348
+ if (line.length === 0) {
349
+ fence.pendingBlanks += 1;
350
+ return [];
351
+ }
352
+ const rows = [];
353
+ for (let i = 0; i < fence.pendingBlanks; i += 1) {
354
+ rows.push(codeBlockRow("", 0, panel));
355
+ }
356
+ fence.pendingBlanks = 0;
357
+ const key = `${panel}\u0000${fence.langPath}\u0000${carryKey(fence.carry)}\u0000${line}`;
358
+ const cached = ROW_CACHE.get(key);
359
+ if (cached) {
360
+ restoreCarry(fence.carry, cached.carry);
361
+ rows.push(...cached.rows);
362
+ return rows;
363
+ }
364
+ const cells = toCells(highlightLineForPath(line, fence.langPath, fence.carry));
365
+ // Continuation rows are indented under the source line so a wrap reads as a
366
+ // wrap. It never eats more than half the body, nor the minimum code columns,
367
+ // so `prefix + body` always fits `inner` and the right border stays flush.
368
+ const indent = Math.min(leadingSpaces(line) + HANGING_INDENT, Math.floor(inner / 2), Math.max(0, inner - MIN_BODY));
369
+ const hang = " ".repeat(indent);
370
+ const chunks = sliceRows(cells, inner, inner - indent);
371
+ const content = chunks.map((chunk, i) => {
372
+ const { text, width: bodyWidth } = paint(chunk);
373
+ const prefix = i === 0 ? "" : hang;
374
+ return codeBlockRow(prefix + text, prefix.length + bodyWidth, panel);
375
+ });
376
+ cacheRows(key, content, fence.carry);
377
+ rows.push(...content);
378
+ return rows;
379
+ }
380
+ //# sourceMappingURL=code-block.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-block.js","sourceRoot":"","sources":["../../src/ui/code-block.ts"],"names":[],"mappings":"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"}
@@ -1,26 +1,13 @@
1
1
  import chalk from "chalk";
2
2
  import stringWidth from "string-width";
3
+ import { codeBlockBottom, codeBlockRows, codeBlockTop, codeBlockWidth, isCodeFenceClose, matchCodeFenceOpen, openCodeFence, } from "./code-block.js";
3
4
  // Lightweight terminal markdown renderer that styles **bold**, *italic*,
4
5
  // `code`, links, headings, lists, blockquotes, hrules, and ```fenced```
5
6
  // code blocks. Designed to work both for one-shot strings and for
6
7
  // token-streaming inputs (line-buffered).
7
- const FENCE_OPEN = chalk.dim;
8
- const FENCE_LINE = chalk.cyan;
9
- /** Continuation gutter for soft-wrapped code lines (keeps block readable). */
10
- const FENCE_CONT = chalk.dim("│ ");
11
8
  function repeat(char, count) {
12
9
  return char.repeat(Math.max(0, count));
13
10
  }
14
- function renderFenceHeader(lang, width = 60) {
15
- const label = lang || "code";
16
- const head = `─── ${label} `;
17
- const barWidth = Math.max(8, Math.min(width, 120));
18
- return FENCE_OPEN(head + repeat("─", Math.max(0, barWidth - head.length)));
19
- }
20
- function renderFenceFooter(width = 60) {
21
- const barWidth = Math.max(8, Math.min(width, 120));
22
- return FENCE_OPEN(repeat("─", barWidth));
23
- }
24
11
  // Walk inline markdown tokens and convert them to ANSI styles. Designed to
25
12
  // handle one logical line at a time (so it can run after a newline arrives
26
13
  // in the token stream).
@@ -137,23 +124,34 @@ export function renderInlineMarkdown(text) {
137
124
  }
138
125
  return out;
139
126
  }
127
+ const DEFAULT_FENCE_WIDTH = 60;
128
+ function panelWidth(state) {
129
+ return codeBlockWidth(state.fenceWidth ?? DEFAULT_FENCE_WIDTH);
130
+ }
131
+ function openFencePanel(state, marker, info) {
132
+ state.inFence = true;
133
+ state.fence = openCodeFence(marker, info);
134
+ return codeBlockTop(state.fence.label, panelWidth(state));
135
+ }
136
+ function closeFencePanel(state) {
137
+ state.inFence = false;
138
+ state.fence = undefined;
139
+ return codeBlockBottom(panelWidth(state));
140
+ }
141
+ /** Fence lines and code bodies as complete panel rows (may be empty). */
142
+ function fencePanelRows(line, state) {
143
+ if (state.inFence && state.fence) {
144
+ if (isCodeFenceClose(line, state.fence.marker)) {
145
+ return [closeFencePanel(state)];
146
+ }
147
+ return codeBlockRows(line, state.fence, panelWidth(state));
148
+ }
149
+ const open = matchCodeFenceOpen(line);
150
+ if (!open)
151
+ return undefined;
152
+ return [openFencePanel(state, open.marker, open.info)];
153
+ }
140
154
  function renderBlockLine(line, state) {
141
- const fenceWidth = state.fenceWidth ?? 60;
142
- // Code fence open/close
143
- const fenceMatch = line.match(/^(\s*)```(\w*)\s*(.*)$/);
144
- if (fenceMatch) {
145
- if (state.inFence) {
146
- state.inFence = false;
147
- state.fenceLang = "";
148
- return renderFenceFooter(fenceWidth);
149
- }
150
- state.inFence = true;
151
- state.fenceLang = fenceMatch[2] ?? "";
152
- return renderFenceHeader(state.fenceLang, fenceWidth);
153
- }
154
- if (state.inFence) {
155
- return FENCE_LINE(line);
156
- }
157
155
  // Headings
158
156
  const heading = line.match(/^(#{1,6})\s+(.*)$/);
159
157
  if (heading) {
@@ -560,19 +558,11 @@ export function indentAndWrapText(text, indent = " ") {
560
558
  .join("\n");
561
559
  }
562
560
  function wrapMarkdownLine(line, wrapWidth, state) {
563
- // Keep fence chrome + content within wrapWidth (never truncate with …).
561
+ // Keep panel chrome + code within wrapWidth (never truncate with …).
564
562
  state.fenceWidth = wrapWidth;
565
- if (/^(\s*)```/.test(line)) {
566
- return [renderBlockLine(line, state)];
567
- }
568
- // Soft-wrap long code lines so they stay inside the fence border.
569
- // Never truncate with ellipsis — split by columns and keep every character.
570
- if (state.inFence) {
571
- // Budget 2 cols for the continuation gutter on wrap lines.
572
- const budget = Math.max(10, wrapWidth - 2);
573
- const chunks = wrapAnsiLine(line, budget);
574
- return chunks.map((chunk, idx) => idx === 0 ? FENCE_LINE(chunk) : FENCE_CONT + FENCE_LINE(chunk));
575
- }
563
+ const fenceRows = fencePanelRows(line, state);
564
+ if (fenceRows)
565
+ return fenceRows;
576
566
  // If it's a horizontal rule, don't wrap it
577
567
  if (/^\s*[-*_]{3,}\s*$/.test(line)) {
578
568
  return [renderBlockLine(line, state)];
@@ -837,12 +827,12 @@ function renderTableBlock(rawLines, availWidth) {
837
827
  export function renderMarkdown(text, width) {
838
828
  if (!text)
839
829
  return text;
840
- const state = { inFence: false, fenceLang: "" };
830
+ const state = { inFence: false };
841
831
  const lines = text.split("\n");
842
832
  const resultLines = [];
843
833
  const hasWidth = typeof width === "number";
844
834
  const cols = width ?? (process.stdout.columns || 80);
845
- const wrapWidth = hasWidth ? Math.max(20, cols - 2) : Math.max(40, cols - 6);
835
+ const wrapWidth = hasWidth ? Math.max(12, cols - 2) : Math.max(40, cols - 6);
846
836
  let i = 0;
847
837
  while (i < lines.length) {
848
838
  const line = lines[i];
@@ -874,19 +864,31 @@ export function renderMarkdown(text, width) {
874
864
  }
875
865
  i++;
876
866
  }
867
+ // A fence still open at the end means the reply is mid-stream or the model
868
+ // never closed it — footer it so the panel always reads as a finished box.
869
+ if (state.inFence) {
870
+ resultLines.push(`${OUTPUT_INDENT}${closeFencePanel(state)}`);
871
+ }
877
872
  return resultLines.join("\n");
878
873
  }
879
874
  // Streaming variant: buffers tokens and emits ANSI-rendered output
880
- // whenever a complete line arrives. Inside a fenced code block, lines
881
- // are emitted as cyan text with header/footer borders.
875
+ // whenever a complete line arrives. Fenced code blocks stream as a bordered,
876
+ // syntax-highlighted panel.
882
877
  export function createMarkdownStreamWriter(write) {
883
- const state = { inFence: false, fenceLang: "" };
878
+ const state = { inFence: false };
884
879
  let buffer = "";
880
+ let outputEndsWithNewline = true;
885
881
  // Table rows are buffered until the block ends so columns can be
886
882
  // sized across every row before anything is emitted.
887
883
  let tableBuffer = [];
888
884
  const cols = process.stdout.columns || 80;
889
885
  const wrapWidth = Math.max(40, cols - 6);
886
+ const emit = (chunk) => {
887
+ if (!chunk)
888
+ return;
889
+ write(chunk);
890
+ outputEndsWithNewline = chunk.endsWith("\n");
891
+ };
890
892
  const emitLine = (line, withNewline) => {
891
893
  const pieces = !state.inFence && BR_RE.test(line) ? line.split(BR_RE_GLOBAL) : [line];
892
894
  for (let p = 0; p < pieces.length; p++) {
@@ -894,10 +896,10 @@ export function createMarkdownStreamWriter(write) {
894
896
  const lastPiece = p === pieces.length - 1;
895
897
  const physical = wrapMarkdownLine(piece, wrapWidth, state);
896
898
  for (let q = 0; q < physical.length; q++) {
897
- write(`${OUTPUT_INDENT}${physical[q]}`);
899
+ emit(`${OUTPUT_INDENT}${physical[q]}`);
898
900
  const isVeryLast = lastPiece && q === physical.length - 1;
899
901
  if (!isVeryLast || withNewline)
900
- write("\n");
902
+ emit("\n");
901
903
  }
902
904
  }
903
905
  };
@@ -909,7 +911,7 @@ export function createMarkdownStreamWriter(write) {
909
911
  isTableSeparatorLine(tableBuffer[1]);
910
912
  if (looksLikeTable) {
911
913
  for (const rendered of renderTableBlock(tableBuffer, wrapWidth)) {
912
- write(`${OUTPUT_INDENT}${rendered}\n`);
914
+ emit(`${OUTPUT_INDENT}${rendered}\n`);
913
915
  }
914
916
  }
915
917
  else {
@@ -949,8 +951,9 @@ export function createMarkdownStreamWriter(write) {
949
951
  }
950
952
  flushTable();
951
953
  if (state.inFence) {
952
- // Emit a closing rule so unterminated fences still look tidy.
953
- write("\n" + renderFenceFooter());
954
+ // Close the panel without inserting a blank row after completed lines.
955
+ const separator = outputEndsWithNewline ? "" : "\n";
956
+ emit(`${separator}${OUTPUT_INDENT}${closeFencePanel(state)}`);
954
957
  }
955
958
  },
956
959
  };