omk-tui 1.2.4 → 1.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Changed
6
+
7
+ - `Markdown.setText` / `Text.setText` no-op when the string is unchanged, so streaming updates can refresh message views without discarding wrap caches.
8
+ - `SelectList` caches the primary column width until the filter or item set changes, so opening `/` with many skills no longer rescans every label on each render frame.
9
+
10
+ ### Fixed
11
+
12
+ - A streaming Markdown message no longer re-parses and re-wraps its whole text every frame. Finished top-level blocks keep their tokens and lines while the text only grows (about 8× less work per frame on a 27k-character reply).
13
+
14
+ ## [1.3.1] - 2026-10-08
15
+
16
+ > Prepared source version, not published. The full OMK release suite remains a publication gate.
17
+
18
+ ### Changed
19
+
20
+ - Lockstep source-version alignment with the OMK packages. The memory-state display lives in the coding-agent `/session` command.
21
+
22
+ ## [1.3.0] - 2026-10-04
23
+
24
+ ### Changed
25
+
26
+ - Lockstep version alignment with the OMK packages. This cycle's changes live in `open-multi-agent-kit`.
27
+
5
28
  ## [1.2.4] - 2026-09-23
6
29
 
7
30
  ### Changed
@@ -0,0 +1,18 @@
1
+ import type { Token } from "marked";
2
+ /**
3
+ * Streaming cache for one Markdown component. While a message only grows, top-level blocks
4
+ * before the trailing ones cannot change, so their lex result and finished lines are reused
5
+ * instead of re-parsing and re-wrapping the whole message every frame.
6
+ */
7
+ export declare class MarkdownStreamCache {
8
+ private stableText;
9
+ private stableTokens;
10
+ private tokenLines;
11
+ reset(): void;
12
+ lex(text: string, lexer: (src: string) => Token[]): Token[];
13
+ /** Finished lines for tokens[index]; cached only for tokens in the stable prefix. */
14
+ linesFor(token: Token, index: number, width: number, nextType: string | undefined, compute: () => string[]): string[];
15
+ }
16
+ /** Wrap rendered token lines and apply margins plus background (pure per line). */
17
+ export declare function finishMarkdownLines(renderedLines: readonly string[], width: number, contentWidth: number, paddingX: number, bgFn: ((text: string) => string) | undefined): string[];
18
+ //# sourceMappingURL=markdown-stream-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-stream-cache.d.ts","sourceRoot":"","sources":["../../src/components/markdown-stream-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAepC;;;;GAIG;AACH,qBAAa,mBAAmB;IAC/B,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,UAAU,CAA0C;IAE5D,KAAK,IAAI,IAAI,CAIZ;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,KAAK,EAAE,GAAG,KAAK,EAAE,CAoB1D;IAED,qFAAqF;IACrF,QAAQ,CACP,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,EAAE,MAAM,MAAM,EAAE,GACrB,MAAM,EAAE,CAOV;CACD;AAED,mFAAmF;AACnF,wBAAgB,mBAAmB,CAClC,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,GAC1C,MAAM,EAAE,CAeV","sourcesContent":["import type { Token } from \"marked\";\nimport { isImageLine } from \"../terminal-image.ts\";\nimport { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from \"../utils.ts\";\n\n/** Re-lex the last N non-space top-level blocks on every append; earlier blocks are final. */\nconst STREAMING_TAIL_TOKENS = 2;\n/** Link reference definitions change inline parsing of earlier blocks; fall back to a full lex. */\nconst REFERENCE_DEFINITION = /^ {0,3}\\[[^\\]\\n]+\\]:/m;\n\ninterface CachedTokenLines {\n\twidth: number;\n\tnextType: string | undefined;\n\tlines: string[];\n}\n\n/**\n * Streaming cache for one Markdown component. While a message only grows, top-level blocks\n * before the trailing ones cannot change, so their lex result and finished lines are reused\n * instead of re-parsing and re-wrapping the whole message every frame.\n */\nexport class MarkdownStreamCache {\n\tprivate stableText = \"\";\n\tprivate stableTokens: Token[] = [];\n\tprivate tokenLines = new WeakMap<Token, CachedTokenLines>();\n\n\treset(): void {\n\t\tthis.stableText = \"\";\n\t\tthis.stableTokens = [];\n\t\tthis.tokenLines = new WeakMap();\n\t}\n\n\tlex(text: string, lexer: (src: string) => Token[]): Token[] {\n\t\tconst canReuse =\n\t\t\tthis.stableText.length > 0 && text.startsWith(this.stableText) && !REFERENCE_DEFINITION.test(text);\n\t\tconst tokens = canReuse ? this.stableTokens.concat(lexer(text.slice(this.stableText.length))) : lexer(text);\n\n\t\t// Appended text can only extend or merge into the trailing blocks (lists, fences, tables, setext headings).\n\t\tlet cut = tokens.length;\n\t\tlet seen = 0;\n\t\twhile (cut > 0 && seen < STREAMING_TAIL_TOKENS) {\n\t\t\tcut -= 1;\n\t\t\tif (tokens[cut]?.type !== \"space\") seen += 1;\n\t\t}\n\t\tconst stable = tokens.slice(0, cut);\n\t\tlet stableText = \"\";\n\t\tfor (const token of stable) stableText += token.raw;\n\t\t// Never reuse a prefix that token raws do not reproduce exactly.\n\t\tconst verified = text.startsWith(stableText);\n\t\tthis.stableTokens = verified ? stable : [];\n\t\tthis.stableText = verified ? stableText : \"\";\n\t\treturn tokens;\n\t}\n\n\t/** Finished lines for tokens[index]; cached only for tokens in the stable prefix. */\n\tlinesFor(\n\t\ttoken: Token,\n\t\tindex: number,\n\t\twidth: number,\n\t\tnextType: string | undefined,\n\t\tcompute: () => string[],\n\t): string[] {\n\t\tconst stable = index < this.stableTokens.length;\n\t\tconst cached = stable ? this.tokenLines.get(token) : undefined;\n\t\tif (cached && cached.width === width && cached.nextType === nextType) return cached.lines;\n\t\tconst lines = compute();\n\t\tif (stable) this.tokenLines.set(token, { width, nextType, lines });\n\t\treturn lines;\n\t}\n}\n\n/** Wrap rendered token lines and apply margins plus background (pure per line). */\nexport function finishMarkdownLines(\n\trenderedLines: readonly string[],\n\twidth: number,\n\tcontentWidth: number,\n\tpaddingX: number,\n\tbgFn: ((text: string) => string) | undefined,\n): string[] {\n\tconst margin = \" \".repeat(paddingX);\n\tconst out: string[] = [];\n\tfor (const rendered of renderedLines) {\n\t\tif (isImageLine(rendered)) {\n\t\t\tout.push(rendered);\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const line of wrapTextWithAnsi(rendered, contentWidth)) {\n\t\t\tconst lineWithMargins = margin + line + margin;\n\t\t\tif (bgFn) out.push(applyBackgroundToLine(lineWithMargins, width, bgFn));\n\t\t\telse out.push(lineWithMargins + \" \".repeat(Math.max(0, width - visibleWidth(lineWithMargins))));\n\t\t}\n\t}\n\treturn out;\n}\n"]}
@@ -0,0 +1,73 @@
1
+ import { isImageLine } from "../terminal-image.js";
2
+ import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
3
+ /** Re-lex the last N non-space top-level blocks on every append; earlier blocks are final. */
4
+ const STREAMING_TAIL_TOKENS = 2;
5
+ /** Link reference definitions change inline parsing of earlier blocks; fall back to a full lex. */
6
+ const REFERENCE_DEFINITION = /^ {0,3}\[[^\]\n]+\]:/m;
7
+ /**
8
+ * Streaming cache for one Markdown component. While a message only grows, top-level blocks
9
+ * before the trailing ones cannot change, so their lex result and finished lines are reused
10
+ * instead of re-parsing and re-wrapping the whole message every frame.
11
+ */
12
+ export class MarkdownStreamCache {
13
+ stableText = "";
14
+ stableTokens = [];
15
+ tokenLines = new WeakMap();
16
+ reset() {
17
+ this.stableText = "";
18
+ this.stableTokens = [];
19
+ this.tokenLines = new WeakMap();
20
+ }
21
+ lex(text, lexer) {
22
+ const canReuse = this.stableText.length > 0 && text.startsWith(this.stableText) && !REFERENCE_DEFINITION.test(text);
23
+ const tokens = canReuse ? this.stableTokens.concat(lexer(text.slice(this.stableText.length))) : lexer(text);
24
+ // Appended text can only extend or merge into the trailing blocks (lists, fences, tables, setext headings).
25
+ let cut = tokens.length;
26
+ let seen = 0;
27
+ while (cut > 0 && seen < STREAMING_TAIL_TOKENS) {
28
+ cut -= 1;
29
+ if (tokens[cut]?.type !== "space")
30
+ seen += 1;
31
+ }
32
+ const stable = tokens.slice(0, cut);
33
+ let stableText = "";
34
+ for (const token of stable)
35
+ stableText += token.raw;
36
+ // Never reuse a prefix that token raws do not reproduce exactly.
37
+ const verified = text.startsWith(stableText);
38
+ this.stableTokens = verified ? stable : [];
39
+ this.stableText = verified ? stableText : "";
40
+ return tokens;
41
+ }
42
+ /** Finished lines for tokens[index]; cached only for tokens in the stable prefix. */
43
+ linesFor(token, index, width, nextType, compute) {
44
+ const stable = index < this.stableTokens.length;
45
+ const cached = stable ? this.tokenLines.get(token) : undefined;
46
+ if (cached && cached.width === width && cached.nextType === nextType)
47
+ return cached.lines;
48
+ const lines = compute();
49
+ if (stable)
50
+ this.tokenLines.set(token, { width, nextType, lines });
51
+ return lines;
52
+ }
53
+ }
54
+ /** Wrap rendered token lines and apply margins plus background (pure per line). */
55
+ export function finishMarkdownLines(renderedLines, width, contentWidth, paddingX, bgFn) {
56
+ const margin = " ".repeat(paddingX);
57
+ const out = [];
58
+ for (const rendered of renderedLines) {
59
+ if (isImageLine(rendered)) {
60
+ out.push(rendered);
61
+ continue;
62
+ }
63
+ for (const line of wrapTextWithAnsi(rendered, contentWidth)) {
64
+ const lineWithMargins = margin + line + margin;
65
+ if (bgFn)
66
+ out.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
67
+ else
68
+ out.push(lineWithMargins + " ".repeat(Math.max(0, width - visibleWidth(lineWithMargins))));
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+ //# sourceMappingURL=markdown-stream-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-stream-cache.js","sourceRoot":"","sources":["../../src/components/markdown-stream-cache.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Token } from \"marked\";\nimport { isImageLine } from \"../terminal-image.ts\";\nimport { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from \"../utils.ts\";\n\n/** Re-lex the last N non-space top-level blocks on every append; earlier blocks are final. */\nconst STREAMING_TAIL_TOKENS = 2;\n/** Link reference definitions change inline parsing of earlier blocks; fall back to a full lex. */\nconst REFERENCE_DEFINITION = /^ {0,3}\\[[^\\]\\n]+\\]:/m;\n\ninterface CachedTokenLines {\n\twidth: number;\n\tnextType: string | undefined;\n\tlines: string[];\n}\n\n/**\n * Streaming cache for one Markdown component. While a message only grows, top-level blocks\n * before the trailing ones cannot change, so their lex result and finished lines are reused\n * instead of re-parsing and re-wrapping the whole message every frame.\n */\nexport class MarkdownStreamCache {\n\tprivate stableText = \"\";\n\tprivate stableTokens: Token[] = [];\n\tprivate tokenLines = new WeakMap<Token, CachedTokenLines>();\n\n\treset(): void {\n\t\tthis.stableText = \"\";\n\t\tthis.stableTokens = [];\n\t\tthis.tokenLines = new WeakMap();\n\t}\n\n\tlex(text: string, lexer: (src: string) => Token[]): Token[] {\n\t\tconst canReuse =\n\t\t\tthis.stableText.length > 0 && text.startsWith(this.stableText) && !REFERENCE_DEFINITION.test(text);\n\t\tconst tokens = canReuse ? this.stableTokens.concat(lexer(text.slice(this.stableText.length))) : lexer(text);\n\n\t\t// Appended text can only extend or merge into the trailing blocks (lists, fences, tables, setext headings).\n\t\tlet cut = tokens.length;\n\t\tlet seen = 0;\n\t\twhile (cut > 0 && seen < STREAMING_TAIL_TOKENS) {\n\t\t\tcut -= 1;\n\t\t\tif (tokens[cut]?.type !== \"space\") seen += 1;\n\t\t}\n\t\tconst stable = tokens.slice(0, cut);\n\t\tlet stableText = \"\";\n\t\tfor (const token of stable) stableText += token.raw;\n\t\t// Never reuse a prefix that token raws do not reproduce exactly.\n\t\tconst verified = text.startsWith(stableText);\n\t\tthis.stableTokens = verified ? stable : [];\n\t\tthis.stableText = verified ? stableText : \"\";\n\t\treturn tokens;\n\t}\n\n\t/** Finished lines for tokens[index]; cached only for tokens in the stable prefix. */\n\tlinesFor(\n\t\ttoken: Token,\n\t\tindex: number,\n\t\twidth: number,\n\t\tnextType: string | undefined,\n\t\tcompute: () => string[],\n\t): string[] {\n\t\tconst stable = index < this.stableTokens.length;\n\t\tconst cached = stable ? this.tokenLines.get(token) : undefined;\n\t\tif (cached && cached.width === width && cached.nextType === nextType) return cached.lines;\n\t\tconst lines = compute();\n\t\tif (stable) this.tokenLines.set(token, { width, nextType, lines });\n\t\treturn lines;\n\t}\n}\n\n/** Wrap rendered token lines and apply margins plus background (pure per line). */\nexport function finishMarkdownLines(\n\trenderedLines: readonly string[],\n\twidth: number,\n\tcontentWidth: number,\n\tpaddingX: number,\n\tbgFn: ((text: string) => string) | undefined,\n): string[] {\n\tconst margin = \" \".repeat(paddingX);\n\tconst out: string[] = [];\n\tfor (const rendered of renderedLines) {\n\t\tif (isImageLine(rendered)) {\n\t\t\tout.push(rendered);\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const line of wrapTextWithAnsi(rendered, contentWidth)) {\n\t\t\tconst lineWithMargins = margin + line + margin;\n\t\t\tif (bgFn) out.push(applyBackgroundToLine(lineWithMargins, width, bgFn));\n\t\t\telse out.push(lineWithMargins + \" \".repeat(Math.max(0, width - visibleWidth(lineWithMargins))));\n\t\t}\n\t}\n\treturn out;\n}\n"]}
@@ -53,6 +53,7 @@ export declare class Markdown implements Component {
53
53
  private cachedText?;
54
54
  private cachedWidth?;
55
55
  private cachedLines?;
56
+ private streamCache;
56
57
  constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, options?: MarkdownOptions);
57
58
  setText(text: string): void;
58
59
  invalidate(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["import { Marked, type Token, type Tokens } from \"marked\";\nimport { isImageLine } from \"../terminal-image.ts\";\nimport type { Component } from \"../tui.ts\";\nimport { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from \"../utils.ts\";\nimport { type MarkdownLinkTheme, renderMarkdownCodeLink, renderMarkdownLink } from \"./markdown-links.ts\";\nimport { StrictStrikethroughTokenizer, trimPartialClosingFences } from \"./markdown-tokenizer.ts\";\n\nconst markdownParser = new Marked();\nmarkdownParser.setOptions({\n\ttokenizer: new StrictStrikethroughTokenizer(),\n});\n\n/**\n * Default text styling for markdown content.\n * Applied to all text unless overridden by markdown formatting.\n */\nexport interface DefaultTextStyle {\n\t/** Foreground color function */\n\tcolor?: (text: string) => string;\n\t/** Background color function */\n\tbgColor?: (text: string) => string;\n\t/** Bold text */\n\tbold?: boolean;\n\t/** Italic text */\n\titalic?: boolean;\n\t/** Strikethrough text */\n\tstrikethrough?: boolean;\n\t/** Underline text */\n\tunderline?: boolean;\n}\n\n/**\n * Theme functions for markdown elements.\n * Each function takes text and returns styled text with ANSI codes.\n */\nexport interface MarkdownTheme extends MarkdownLinkTheme {\n\theading: (text: string) => string;\n\tcode: (text: string) => string;\n\tcodeBlock: (text: string) => string;\n\tcodeBlockBorder: (text: string) => string;\n\tquote: (text: string) => string;\n\tquoteBorder: (text: string) => string;\n\thr: (text: string) => string;\n\tlistBullet: (text: string) => string;\n\tbold: (text: string) => string;\n\titalic: (text: string) => string;\n\tstrikethrough: (text: string) => string;\n\thighlightCode?: (code: string, lang?: string) => string[];\n\t/** Prefix applied to each rendered code block line (default: \" \") */\n\tcodeBlockIndent?: string;\n}\n\nexport interface MarkdownOptions {\n\t/** Preserve source ordered-list markers instead of normalizing them from the list start. */\n\tpreserveOrderedListMarkers?: boolean;\n}\n\ninterface InlineStyleContext {\n\tapplyText: (text: string) => string;\n\tstylePrefix: string;\n\tinsideLink?: boolean;\n}\n\nexport class Markdown implements Component {\n\tprivate text: string;\n\tprivate paddingX: number; // Left/right padding\n\tprivate paddingY: number; // Top/bottom padding\n\tprivate defaultTextStyle?: DefaultTextStyle;\n\tprivate theme: MarkdownTheme;\n\tprivate options: MarkdownOptions;\n\tprivate defaultStylePrefix?: string;\n\n\t// Cache for rendered output\n\tprivate cachedText?: string;\n\tprivate cachedWidth?: number;\n\tprivate cachedLines?: string[];\n\n\tconstructor(\n\t\ttext: string,\n\t\tpaddingX: number,\n\t\tpaddingY: number,\n\t\ttheme: MarkdownTheme,\n\t\tdefaultTextStyle?: DefaultTextStyle,\n\t\toptions?: MarkdownOptions,\n\t) {\n\t\tthis.text = text;\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.theme = theme;\n\t\tthis.defaultTextStyle = defaultTextStyle;\n\t\tthis.options = options ? { ...options } : {};\n\t}\n\n\tsetText(text: string): void {\n\t\tthis.text = text;\n\t\tthis.invalidate();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedText = undefined;\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Check cache\n\t\tif (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\t// Calculate available width for content (subtract horizontal padding)\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\n\t\t// Don't render anything if there's no actual text\n\t\tif (!this.text || this.text.trim() === \"\") {\n\t\t\tconst result: string[] = [];\n\t\t\t// Update cache\n\t\t\tthis.cachedText = this.text;\n\t\t\tthis.cachedWidth = width;\n\t\t\tthis.cachedLines = result;\n\t\t\treturn result;\n\t\t}\n\n\t\t// Replace tabs with 3 spaces for consistent rendering\n\t\tconst normalizedText = this.text.replace(/\\t/g, \" \");\n\n\t\t// Parse markdown to HTML-like tokens\n\t\tconst tokens = markdownParser.lexer(normalizedText);\n\t\ttrimPartialClosingFences(tokens);\n\n\t\t// Convert tokens to styled terminal output\n\t\tconst renderedLines: string[] = [];\n\n\t\tfor (let i = 0; i < tokens.length; i++) {\n\t\t\tconst token = tokens[i];\n\t\t\tconst nextToken = tokens[i + 1];\n\t\t\tconst tokenLines = this.renderToken(token, contentWidth, nextToken?.type);\n\t\t\tfor (const tokenLine of tokenLines) {\n\t\t\t\trenderedLines.push(tokenLine);\n\t\t\t}\n\t\t}\n\n\t\t// Wrap lines (NO padding, NO background yet)\n\t\tconst wrappedLines: string[] = [];\n\t\tfor (const line of renderedLines) {\n\t\t\tif (isImageLine(line)) {\n\t\t\t\twrappedLines.push(line);\n\t\t\t} else {\n\t\t\t\tfor (const wrappedLine of wrapTextWithAnsi(line, contentWidth)) {\n\t\t\t\t\twrappedLines.push(wrappedLine);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add margins and background to each wrapped line\n\t\tconst leftMargin = \" \".repeat(this.paddingX);\n\t\tconst rightMargin = \" \".repeat(this.paddingX);\n\t\tconst bgFn = this.defaultTextStyle?.bgColor;\n\t\tconst contentLines: string[] = [];\n\n\t\tfor (const line of wrappedLines) {\n\t\t\tif (isImageLine(line)) {\n\t\t\t\tcontentLines.push(line);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst lineWithMargins = leftMargin + line + rightMargin;\n\n\t\t\tif (bgFn) {\n\t\t\t\tcontentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));\n\t\t\t} else {\n\t\t\t\t// No background - just pad to width\n\t\t\t\tconst visibleLen = visibleWidth(lineWithMargins);\n\t\t\t\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\t\t\t\tcontentLines.push(lineWithMargins + \" \".repeat(paddingNeeded));\n\t\t\t}\n\t\t}\n\n\t\t// Add top/bottom padding (empty lines)\n\t\tconst emptyLine = \" \".repeat(width);\n\t\tconst emptyLines: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tconst line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;\n\t\t\temptyLines.push(line);\n\t\t}\n\n\t\t// Combine top padding, content, and bottom padding\n\t\tconst result = emptyLines.concat(contentLines, emptyLines);\n\n\t\t// Update cache\n\t\tthis.cachedText = this.text;\n\t\tthis.cachedWidth = width;\n\t\tthis.cachedLines = result;\n\n\t\treturn result.length > 0 ? result : [\"\"];\n\t}\n\n\t/**\n\t * Apply default text style to a string.\n\t * This is the base styling applied to all text content.\n\t * NOTE: Background color is NOT applied here - it's applied at the padding stage\n\t * to ensure it extends to the full line width.\n\t */\n\tprivate applyDefaultStyle(text: string): string {\n\t\tif (!this.defaultTextStyle) {\n\t\t\treturn text;\n\t\t}\n\n\t\tlet styled = text;\n\n\t\t// Apply foreground color (NOT background - that's applied at padding stage)\n\t\tif (this.defaultTextStyle.color) {\n\t\t\tstyled = this.defaultTextStyle.color(styled);\n\t\t}\n\n\t\t// Apply text decorations using this.theme\n\t\tif (this.defaultTextStyle.bold) {\n\t\t\tstyled = this.theme.bold(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.italic) {\n\t\t\tstyled = this.theme.italic(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.strikethrough) {\n\t\t\tstyled = this.theme.strikethrough(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.underline) {\n\t\t\tstyled = this.theme.underline(styled);\n\t\t}\n\n\t\treturn styled;\n\t}\n\n\tprivate getDefaultStylePrefix(): string {\n\t\tif (!this.defaultTextStyle) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tif (this.defaultStylePrefix !== undefined) {\n\t\t\treturn this.defaultStylePrefix;\n\t\t}\n\n\t\tconst sentinel = \"\\u0000\";\n\t\tlet styled = sentinel;\n\n\t\tif (this.defaultTextStyle.color) {\n\t\t\tstyled = this.defaultTextStyle.color(styled);\n\t\t}\n\n\t\tif (this.defaultTextStyle.bold) {\n\t\t\tstyled = this.theme.bold(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.italic) {\n\t\t\tstyled = this.theme.italic(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.strikethrough) {\n\t\t\tstyled = this.theme.strikethrough(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.underline) {\n\t\t\tstyled = this.theme.underline(styled);\n\t\t}\n\n\t\tconst sentinelIndex = styled.indexOf(sentinel);\n\t\tthis.defaultStylePrefix = sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : \"\";\n\t\treturn this.defaultStylePrefix;\n\t}\n\n\tprivate getStylePrefix(styleFn: (text: string) => string): string {\n\t\tconst sentinel = \"\\u0000\";\n\t\tconst styled = styleFn(sentinel);\n\t\tconst sentinelIndex = styled.indexOf(sentinel);\n\t\treturn sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : \"\";\n\t}\n\n\tprivate getDefaultInlineStyleContext(): InlineStyleContext {\n\t\treturn {\n\t\t\tapplyText: (text: string) => this.applyDefaultStyle(text),\n\t\t\tstylePrefix: this.getDefaultStylePrefix(),\n\t\t};\n\t}\n\n\tprivate renderToken(\n\t\ttoken: Token,\n\t\twidth: number,\n\t\tnextTokenType?: string,\n\t\tstyleContext?: InlineStyleContext,\n\t): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tswitch (token.type) {\n\t\t\tcase \"heading\": {\n\t\t\t\tconst headingLevel = token.depth;\n\t\t\t\tconst headingPrefix = `${\"#\".repeat(headingLevel)} `;\n\n\t\t\t\t// Build a heading-specific style context so inline tokens (codespan, bold, etc.)\n\t\t\t\t// restore heading styling after their own ANSI resets instead of falling back to\n\t\t\t\t// the default text style.\n\t\t\t\tlet headingStyleFn: (text: string) => string;\n\t\t\t\tif (headingLevel === 1) {\n\t\t\t\t\theadingStyleFn = (text: string) => this.theme.heading(this.theme.bold(this.theme.underline(text)));\n\t\t\t\t} else {\n\t\t\t\t\theadingStyleFn = (text: string) => this.theme.heading(this.theme.bold(text));\n\t\t\t\t}\n\n\t\t\t\tconst headingStyleContext: InlineStyleContext = {\n\t\t\t\t\tapplyText: headingStyleFn,\n\t\t\t\t\tstylePrefix: this.getStylePrefix(headingStyleFn),\n\t\t\t\t};\n\n\t\t\t\tconst headingText = this.renderInlineTokens(token.tokens || [], headingStyleContext);\n\t\t\t\tconst styledHeading = headingLevel >= 3 ? headingStyleFn(headingPrefix) + headingText : headingText;\n\t\t\t\tlines.push(styledHeading);\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after headings (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"paragraph\": {\n\t\t\t\tconst paragraphText = this.renderInlineTokens(token.tokens || [], styleContext);\n\t\t\t\tlines.push(paragraphText);\n\t\t\t\t// Don't add spacing if next token is space or list\n\t\t\t\tif (nextTokenType && nextTokenType !== \"list\" && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"text\":\n\t\t\t\tlines.push(this.renderInlineTokens([token], styleContext));\n\t\t\t\tbreak;\n\n\t\t\tcase \"code\": {\n\t\t\t\tconst indent = this.theme.codeBlockIndent ?? \" \";\n\t\t\t\tlines.push(this.theme.codeBlockBorder(`\\`\\`\\`${token.lang || \"\"}`));\n\t\t\t\tif (this.theme.highlightCode) {\n\t\t\t\t\tconst highlightedLines = this.theme.highlightCode(token.text, token.lang);\n\t\t\t\t\tfor (const hlLine of highlightedLines) {\n\t\t\t\t\t\tlines.push(`${indent}${hlLine}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Split code by newlines and style each line\n\t\t\t\t\tconst codeLines = token.text.split(\"\\n\");\n\t\t\t\t\tfor (const codeLine of codeLines) {\n\t\t\t\t\t\tlines.push(`${indent}${this.theme.codeBlock(codeLine)}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlines.push(this.theme.codeBlockBorder(\"```\"));\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after code blocks (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"list\": {\n\t\t\t\tconst listLines = this.renderList(token as Tokens.List, 0, width, styleContext);\n\t\t\t\tlines.push(...listLines);\n\t\t\t\t// Don't add spacing after lists if a space token follows\n\t\t\t\t// (the space token will handle it)\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"table\": {\n\t\t\t\tconst tableLines = this.renderTable(token as Tokens.Table, width, nextTokenType, styleContext);\n\t\t\t\tlines.push(...tableLines);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"blockquote\": {\n\t\t\t\tconst quoteStyle = (text: string) => this.theme.quote(this.theme.italic(text));\n\t\t\t\tconst quoteStylePrefix = this.getStylePrefix(quoteStyle);\n\t\t\t\tconst applyQuoteStyle = (line: string): string => {\n\t\t\t\t\tif (!quoteStylePrefix) {\n\t\t\t\t\t\treturn quoteStyle(line);\n\t\t\t\t\t}\n\t\t\t\t\tconst lineWithReappliedStyle = line.replace(/\\x1b\\[0m/g, `\\x1b[0m${quoteStylePrefix}`);\n\t\t\t\t\treturn quoteStyle(lineWithReappliedStyle);\n\t\t\t\t};\n\n\t\t\t\t// Calculate available width for quote content (subtract border \"│ \" = 2 chars)\n\t\t\t\tconst quoteContentWidth = Math.max(1, width - 2);\n\n\t\t\t\t// Blockquotes contain block-level tokens (paragraph, list, code, etc.), so render\n\t\t\t\t// children with renderToken() instead of renderInlineTokens().\n\t\t\t\t// Default message style should not apply inside blockquotes.\n\t\t\t\tconst quoteInlineStyleContext: InlineStyleContext = {\n\t\t\t\t\tapplyText: (text: string) => text,\n\t\t\t\t\tstylePrefix: quoteStylePrefix,\n\t\t\t\t};\n\t\t\t\tconst quoteTokens = token.tokens || [];\n\t\t\t\tconst renderedQuoteLines: string[] = [];\n\t\t\t\tfor (let i = 0; i < quoteTokens.length; i++) {\n\t\t\t\t\tconst quoteToken = quoteTokens[i];\n\t\t\t\t\tconst nextQuoteToken = quoteTokens[i + 1];\n\t\t\t\t\trenderedQuoteLines.push(\n\t\t\t\t\t\t...this.renderToken(quoteToken, quoteContentWidth, nextQuoteToken?.type, quoteInlineStyleContext),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// Avoid rendering an extra empty quote line before the outer blockquote spacing.\n\t\t\t\twhile (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1] === \"\") {\n\t\t\t\t\trenderedQuoteLines.pop();\n\t\t\t\t}\n\n\t\t\t\tfor (const quoteLine of renderedQuoteLines) {\n\t\t\t\t\tconst styledLine = applyQuoteStyle(quoteLine);\n\t\t\t\t\tconst wrappedLines = wrapTextWithAnsi(styledLine, quoteContentWidth);\n\t\t\t\t\tfor (const wrappedLine of wrappedLines) {\n\t\t\t\t\t\tlines.push(this.theme.quoteBorder(\"│ \") + wrappedLine);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after blockquotes (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"hr\":\n\t\t\t\tlines.push(this.theme.hr(\"─\".repeat(Math.min(width, 80))));\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after horizontal rules (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"html\":\n\t\t\t\t// Render HTML as plain text (escaped for terminal)\n\t\t\t\tif (\"raw\" in token && typeof token.raw === \"string\") {\n\t\t\t\t\tlines.push(this.applyDefaultStyle(token.raw.trim()));\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"space\":\n\t\t\t\t// Space tokens represent blank lines in markdown\n\t\t\t\tlines.push(\"\");\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t// Handle any other token types as plain text\n\t\t\t\tif (\"text\" in token && typeof token.text === \"string\") {\n\t\t\t\t\tlines.push(token.text);\n\t\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\tprivate renderInlineTokens(tokens: Token[], styleContext?: InlineStyleContext): string {\n\t\tlet result = \"\";\n\t\tconst resolvedStyleContext = styleContext ?? this.getDefaultInlineStyleContext();\n\t\tconst { applyText, stylePrefix } = resolvedStyleContext;\n\t\tconst codeLinkTheme = resolvedStyleContext.insideLink ? undefined : this.theme;\n\t\tconst applyTextWithNewlines = (text: string): string => {\n\t\t\tconst segments: string[] = text.split(\"\\n\");\n\t\t\treturn segments.map((segment: string) => applyText(segment)).join(\"\\n\");\n\t\t};\n\n\t\tfor (const token of tokens) {\n\t\t\tswitch (token.type) {\n\t\t\t\tcase \"text\":\n\t\t\t\t\t// Text tokens in list items can have nested tokens for inline formatting\n\t\t\t\t\tif (token.tokens && token.tokens.length > 0) {\n\t\t\t\t\t\tresult += this.renderInlineTokens(token.tokens, resolvedStyleContext);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult += applyTextWithNewlines(token.text);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"paragraph\":\n\t\t\t\t\t// Paragraph tokens contain nested inline tokens\n\t\t\t\t\tresult += this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"strong\": {\n\t\t\t\t\tconst boldContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.bold(boldContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"em\": {\n\t\t\t\t\tconst italicContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.italic(italicContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"codespan\":\n\t\t\t\t\tresult += renderMarkdownCodeLink(this.theme.code(token.text), token.text, codeLinkTheme) + stylePrefix;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"image\":\n\t\t\t\t\tresult += resolvedStyleContext.insideLink\n\t\t\t\t\t\t? applyTextWithNewlines(token.text || token.href)\n\t\t\t\t\t\t: renderMarkdownLink({ text: token.text, href: token.href }, token.text || token.href, this.theme) +\n\t\t\t\t\t\t\tstylePrefix;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"link\": {\n\t\t\t\t\tconst linkText = this.renderInlineTokens(token.tokens || [], {\n\t\t\t\t\t\t...resolvedStyleContext,\n\t\t\t\t\t\tinsideLink: true,\n\t\t\t\t\t});\n\t\t\t\t\tresult += renderMarkdownLink({ text: token.text, href: token.href }, linkText, this.theme) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"br\":\n\t\t\t\t\tresult += \"\\n\";\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"del\": {\n\t\t\t\t\tconst delContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.strikethrough(delContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"html\":\n\t\t\t\t\t// Render inline HTML as plain text\n\t\t\t\t\tif (\"raw\" in token && typeof token.raw === \"string\") {\n\t\t\t\t\t\tresult += applyTextWithNewlines(token.raw);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\t// Handle any other inline token types as plain text\n\t\t\t\t\tif (\"text\" in token && typeof token.text === \"string\") {\n\t\t\t\t\t\tresult += applyTextWithNewlines(token.text);\n\t\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\twhile (stylePrefix && result.endsWith(stylePrefix)) {\n\t\t\tresult = result.slice(0, -stylePrefix.length);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate getOrderedListMarker(item: Tokens.ListItem): string | undefined {\n\t\tconst match = /^(?: {0,3})(\\d{1,9}[.)])[ \\t]+/.exec(item.raw);\n\t\treturn match ? `${match[1]} ` : undefined;\n\t}\n\n\t/**\n\t * Render a list with proper nesting support\n\t */\n\tprivate renderList(token: Tokens.List, depth: number, width: number, styleContext?: InlineStyleContext): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst indent = \" \".repeat(depth);\n\t\t// Use the list's start property (defaults to 1 for ordered lists)\n\t\tconst startNumber = typeof token.start === \"number\" ? token.start : 1;\n\n\t\tfor (let i = 0; i < token.items.length; i++) {\n\t\t\tconst item = token.items[i];\n\t\t\tconst isLastItem = i === token.items.length - 1;\n\t\t\tconst bullet = token.ordered\n\t\t\t\t? this.options.preserveOrderedListMarkers\n\t\t\t\t\t? (this.getOrderedListMarker(item) ?? `${startNumber + i}. `)\n\t\t\t\t\t: `${startNumber + i}. `\n\t\t\t\t: \"- \";\n\t\t\tconst taskMarker = item.task ? `[${item.checked ? \"x\" : \" \"}] ` : \"\";\n\t\t\tconst marker = bullet + taskMarker;\n\t\t\tconst firstPrefix = indent + this.theme.listBullet(marker);\n\t\t\tconst continuationPrefix = indent + \" \".repeat(visibleWidth(marker));\n\t\t\tconst itemWidth = Math.max(1, width - visibleWidth(firstPrefix));\n\t\t\tlet renderedAnyLine = false;\n\n\t\t\tfor (const itemToken of item.tokens) {\n\t\t\t\tif (itemToken.type === \"list\") {\n\t\t\t\t\tlines.push(...this.renderList(itemToken as Tokens.List, depth + 1, width, styleContext));\n\t\t\t\t\trenderedAnyLine = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst itemLines = this.renderToken(itemToken, itemWidth, undefined, styleContext);\n\t\t\t\tfor (const line of itemLines) {\n\t\t\t\t\tfor (const wrappedLine of wrapTextWithAnsi(line, itemWidth)) {\n\t\t\t\t\t\tconst linePrefix = renderedAnyLine ? continuationPrefix : firstPrefix;\n\t\t\t\t\t\tlines.push(linePrefix + wrappedLine);\n\t\t\t\t\t\trenderedAnyLine = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!renderedAnyLine) {\n\t\t\t\tlines.push(firstPrefix);\n\t\t\t}\n\n\t\t\tif (token.loose && !isLastItem) {\n\t\t\t\tlines.push(\"\");\n\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\t/**\n\t * Get the visible width of the longest word in a string.\n\t */\n\tprivate getLongestWordWidth(text: string, maxWidth?: number): number {\n\t\tconst words = text.split(/\\s+/).filter((word) => word.length > 0);\n\t\tlet longest = 0;\n\t\tfor (const word of words) {\n\t\t\tlongest = Math.max(longest, visibleWidth(word));\n\t\t}\n\t\tif (maxWidth === undefined) {\n\t\t\treturn longest;\n\t\t}\n\t\treturn Math.min(longest, maxWidth);\n\t}\n\n\t/**\n\t * Wrap a table cell to fit into a column.\n\t *\n\t * Delegates to wrapTextWithAnsi() so ANSI codes + long tokens are handled\n\t * consistently with the rest of the renderer.\n\t */\n\tprivate wrapCellText(text: string, maxWidth: number): string[] {\n\t\treturn wrapTextWithAnsi(text, Math.max(1, maxWidth));\n\t}\n\n\t/**\n\t * Render a table with width-aware cell wrapping.\n\t * Cells that don't fit are wrapped to multiple lines.\n\t */\n\tprivate renderTable(\n\t\ttoken: Tokens.Table,\n\t\tavailableWidth: number,\n\t\tnextTokenType?: string,\n\t\tstyleContext?: InlineStyleContext,\n\t): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst numCols = token.header.length;\n\n\t\tif (numCols === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate border overhead: \"│ \" + (n-1) * \" │ \" + \" │\"\n\t\t// = 2 + (n-1) * 3 + 2 = 3n + 1\n\t\tconst borderOverhead = 3 * numCols + 1;\n\t\tconst availableForCells = availableWidth - borderOverhead;\n\t\tif (availableForCells < numCols) {\n\t\t\t// Too narrow to render a stable table. Fall back to raw markdown.\n\t\t\tconst fallbackLines = token.raw ? wrapTextWithAnsi(token.raw, availableWidth) : [];\n\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\tfallbackLines.push(\"\");\n\t\t\t}\n\t\t\treturn fallbackLines;\n\t\t}\n\n\t\tconst maxUnbrokenWordWidth = 30;\n\n\t\t// Calculate natural column widths (what each column needs without constraints)\n\t\tconst naturalWidths: number[] = [];\n\t\tconst minWordWidths: number[] = [];\n\t\tfor (let i = 0; i < numCols; i++) {\n\t\t\tconst headerText = this.renderInlineTokens(token.header[i].tokens || [], styleContext);\n\t\t\tnaturalWidths[i] = visibleWidth(headerText);\n\t\t\tminWordWidths[i] = Math.max(1, this.getLongestWordWidth(headerText, maxUnbrokenWordWidth));\n\t\t}\n\t\tfor (const row of token.rows) {\n\t\t\tfor (let i = 0; i < row.length; i++) {\n\t\t\t\tconst cellText = this.renderInlineTokens(row[i].tokens || [], styleContext);\n\t\t\t\tnaturalWidths[i] = Math.max(naturalWidths[i] || 0, visibleWidth(cellText));\n\t\t\t\tminWordWidths[i] = Math.max(\n\t\t\t\t\tminWordWidths[i] || 1,\n\t\t\t\t\tthis.getLongestWordWidth(cellText, maxUnbrokenWordWidth),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tlet minColumnWidths = minWordWidths;\n\t\tlet minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);\n\n\t\tif (minCellsWidth > availableForCells) {\n\t\t\tminColumnWidths = new Array(numCols).fill(1);\n\t\t\tconst remaining = availableForCells - numCols;\n\n\t\t\tif (remaining > 0) {\n\t\t\t\tconst totalWeight = minWordWidths.reduce((total, width) => total + Math.max(0, width - 1), 0);\n\t\t\t\tconst growth = minWordWidths.map((width) => {\n\t\t\t\t\tconst weight = Math.max(0, width - 1);\n\t\t\t\t\treturn totalWeight > 0 ? Math.floor((weight / totalWeight) * remaining) : 0;\n\t\t\t\t});\n\n\t\t\t\tfor (let i = 0; i < numCols; i++) {\n\t\t\t\t\tminColumnWidths[i] += growth[i] ?? 0;\n\t\t\t\t}\n\n\t\t\t\tconst allocated = growth.reduce((total, width) => total + width, 0);\n\t\t\t\tlet leftover = remaining - allocated;\n\t\t\t\tfor (let i = 0; leftover > 0 && i < numCols; i++) {\n\t\t\t\t\tminColumnWidths[i]++;\n\t\t\t\t\tleftover--;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tminCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);\n\t\t}\n\n\t\t// Calculate column widths that fit within available width\n\t\tconst totalNaturalWidth = naturalWidths.reduce((a, b) => a + b, 0) + borderOverhead;\n\t\tlet columnWidths: number[];\n\n\t\tif (totalNaturalWidth <= availableWidth) {\n\t\t\t// Everything fits naturally\n\t\t\tcolumnWidths = naturalWidths.map((width, index) => Math.max(width, minColumnWidths[index]));\n\t\t} else {\n\t\t\t// Need to shrink columns to fit\n\t\t\tconst totalGrowPotential = naturalWidths.reduce((total, width, index) => {\n\t\t\t\treturn total + Math.max(0, width - minColumnWidths[index]);\n\t\t\t}, 0);\n\t\t\tconst extraWidth = Math.max(0, availableForCells - minCellsWidth);\n\t\t\tcolumnWidths = minColumnWidths.map((minWidth, index) => {\n\t\t\t\tconst naturalWidth = naturalWidths[index];\n\t\t\t\tconst minWidthDelta = Math.max(0, naturalWidth - minWidth);\n\t\t\t\tlet grow = 0;\n\t\t\t\tif (totalGrowPotential > 0) {\n\t\t\t\t\tgrow = Math.floor((minWidthDelta / totalGrowPotential) * extraWidth);\n\t\t\t\t}\n\t\t\t\treturn minWidth + grow;\n\t\t\t});\n\n\t\t\t// Adjust for rounding errors - distribute remaining space\n\t\t\tconst allocated = columnWidths.reduce((a, b) => a + b, 0);\n\t\t\tlet remaining = availableForCells - allocated;\n\t\t\twhile (remaining > 0) {\n\t\t\t\tlet grew = false;\n\t\t\t\tfor (let i = 0; i < numCols && remaining > 0; i++) {\n\t\t\t\t\tif (columnWidths[i] < naturalWidths[i]) {\n\t\t\t\t\t\tcolumnWidths[i]++;\n\t\t\t\t\t\tremaining--;\n\t\t\t\t\t\tgrew = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!grew) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Render top border\n\t\tconst topBorderCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tlines.push(`┌─${topBorderCells.join(\"─┬─\")}─┐`);\n\n\t\t// Render header with wrapping\n\t\tconst headerCellLines: string[][] = token.header.map((cell, i) => {\n\t\t\tconst text = this.renderInlineTokens(cell.tokens || [], styleContext);\n\t\t\treturn this.wrapCellText(text, columnWidths[i]);\n\t\t});\n\t\tconst headerLineCount = Math.max(...headerCellLines.map((c) => c.length));\n\n\t\tfor (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {\n\t\t\tconst rowParts = headerCellLines.map((cellLines, colIdx) => {\n\t\t\t\tconst text = cellLines[lineIdx] || \"\";\n\t\t\t\tconst padded = text + \" \".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));\n\t\t\t\treturn this.theme.bold(padded);\n\t\t\t});\n\t\t\tlines.push(`│ ${rowParts.join(\" │ \")} │`);\n\t\t}\n\n\t\t// Render separator\n\t\tconst separatorCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tconst separatorLine = `├─${separatorCells.join(\"─┼─\")}─┤`;\n\t\tlines.push(separatorLine);\n\n\t\t// Render rows with wrapping\n\t\tfor (let rowIndex = 0; rowIndex < token.rows.length; rowIndex++) {\n\t\t\tconst row = token.rows[rowIndex];\n\t\t\tconst rowCellLines: string[][] = row.map((cell, i) => {\n\t\t\t\tconst text = this.renderInlineTokens(cell.tokens || [], styleContext);\n\t\t\t\treturn this.wrapCellText(text, columnWidths[i]);\n\t\t\t});\n\t\t\tconst rowLineCount = Math.max(...rowCellLines.map((c) => c.length));\n\n\t\t\tfor (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {\n\t\t\t\tconst rowParts = rowCellLines.map((cellLines, colIdx) => {\n\t\t\t\t\tconst text = cellLines[lineIdx] || \"\";\n\t\t\t\t\treturn text + \" \".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));\n\t\t\t\t});\n\t\t\t\tlines.push(`│ ${rowParts.join(\" │ \")} │`);\n\t\t\t}\n\n\t\t\tif (rowIndex < token.rows.length - 1) {\n\t\t\t\tlines.push(separatorLine);\n\t\t\t}\n\t\t}\n\n\t\t// Render bottom border\n\t\tconst bottomBorderCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tlines.push(`└─${bottomBorderCells.join(\"─┴─\")}─┘`);\n\n\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\tlines.push(\"\"); // Add spacing after table\n\t\t}\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["import { Marked, type Token, type Tokens } from \"marked\";\nimport type { Component } from \"../tui.ts\";\nimport { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from \"../utils.ts\";\nimport { type MarkdownLinkTheme, renderMarkdownCodeLink, renderMarkdownLink } from \"./markdown-links.ts\";\nimport { finishMarkdownLines, MarkdownStreamCache } from \"./markdown-stream-cache.ts\";\nimport { StrictStrikethroughTokenizer, trimPartialClosingFences } from \"./markdown-tokenizer.ts\";\n\nconst markdownParser = new Marked();\nmarkdownParser.setOptions({\n\ttokenizer: new StrictStrikethroughTokenizer(),\n});\n\n/**\n * Default text styling for markdown content.\n * Applied to all text unless overridden by markdown formatting.\n */\nexport interface DefaultTextStyle {\n\t/** Foreground color function */\n\tcolor?: (text: string) => string;\n\t/** Background color function */\n\tbgColor?: (text: string) => string;\n\t/** Bold text */\n\tbold?: boolean;\n\t/** Italic text */\n\titalic?: boolean;\n\t/** Strikethrough text */\n\tstrikethrough?: boolean;\n\t/** Underline text */\n\tunderline?: boolean;\n}\n\n/**\n * Theme functions for markdown elements.\n * Each function takes text and returns styled text with ANSI codes.\n */\nexport interface MarkdownTheme extends MarkdownLinkTheme {\n\theading: (text: string) => string;\n\tcode: (text: string) => string;\n\tcodeBlock: (text: string) => string;\n\tcodeBlockBorder: (text: string) => string;\n\tquote: (text: string) => string;\n\tquoteBorder: (text: string) => string;\n\thr: (text: string) => string;\n\tlistBullet: (text: string) => string;\n\tbold: (text: string) => string;\n\titalic: (text: string) => string;\n\tstrikethrough: (text: string) => string;\n\thighlightCode?: (code: string, lang?: string) => string[];\n\t/** Prefix applied to each rendered code block line (default: \" \") */\n\tcodeBlockIndent?: string;\n}\n\nexport interface MarkdownOptions {\n\t/** Preserve source ordered-list markers instead of normalizing them from the list start. */\n\tpreserveOrderedListMarkers?: boolean;\n}\n\ninterface InlineStyleContext {\n\tapplyText: (text: string) => string;\n\tstylePrefix: string;\n\tinsideLink?: boolean;\n}\n\nexport class Markdown implements Component {\n\tprivate text: string;\n\tprivate paddingX: number; // Left/right padding\n\tprivate paddingY: number; // Top/bottom padding\n\tprivate defaultTextStyle?: DefaultTextStyle;\n\tprivate theme: MarkdownTheme;\n\tprivate options: MarkdownOptions;\n\tprivate defaultStylePrefix?: string;\n\n\t// Cache for rendered output\n\tprivate cachedText?: string;\n\tprivate cachedWidth?: number;\n\tprivate cachedLines?: string[];\n\tprivate streamCache = new MarkdownStreamCache();\n\n\tconstructor(\n\t\ttext: string,\n\t\tpaddingX: number,\n\t\tpaddingY: number,\n\t\ttheme: MarkdownTheme,\n\t\tdefaultTextStyle?: DefaultTextStyle,\n\t\toptions?: MarkdownOptions,\n\t) {\n\t\tthis.text = text;\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.theme = theme;\n\t\tthis.defaultTextStyle = defaultTextStyle;\n\t\tthis.options = options ? { ...options } : {};\n\t}\n\n\tsetText(text: string): void {\n\t\tif (text === this.text) return;\n\t\tthis.text = text;\n\t\tthis.cachedLines = undefined; // keep the streaming prefix; render() checks it still applies\n\t}\n\n\tinvalidate(): void {\n\t\tthis.streamCache.reset();\n\t\tthis.cachedText = undefined;\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Check cache\n\t\tif (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\t// Calculate available width for content (subtract horizontal padding)\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\n\t\t// Don't render anything if there's no actual text\n\t\tif (!this.text || this.text.trim() === \"\") {\n\t\t\tconst result: string[] = [];\n\t\t\t// Update cache\n\t\t\tthis.cachedText = this.text;\n\t\t\tthis.cachedWidth = width;\n\t\t\tthis.cachedLines = result;\n\t\t\treturn result;\n\t\t}\n\n\t\t// Replace tabs with 3 spaces for consistent rendering\n\t\tconst normalizedText = this.text.replace(/\\t/g, \" \");\n\n\t\t// Parse markdown to tokens, reusing the stable prefix while a message streams\n\t\tconst tokens = this.streamCache.lex(normalizedText, (src) => markdownParser.lexer(src));\n\t\ttrimPartialClosingFences(tokens);\n\n\t\tconst bgFn = this.defaultTextStyle?.bgColor;\n\t\tconst contentLines: string[] = [];\n\t\tfor (let i = 0; i < tokens.length; i++) {\n\t\t\tconst nextType = tokens[i + 1]?.type;\n\t\t\tconst lines = this.streamCache.linesFor(tokens[i], i, width, nextType, () =>\n\t\t\t\tfinishMarkdownLines(\n\t\t\t\t\tthis.renderToken(tokens[i], contentWidth, nextType),\n\t\t\t\t\twidth,\n\t\t\t\t\tcontentWidth,\n\t\t\t\t\tthis.paddingX,\n\t\t\t\t\tbgFn,\n\t\t\t\t),\n\t\t\t);\n\t\t\tfor (const line of lines) contentLines.push(line);\n\t\t}\n\n\t\t// Add top/bottom padding (empty lines)\n\t\tconst emptyLine = \" \".repeat(width);\n\t\tconst emptyLines: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tconst line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;\n\t\t\temptyLines.push(line);\n\t\t}\n\n\t\t// Combine top padding, content, and bottom padding\n\t\tconst result = emptyLines.concat(contentLines, emptyLines);\n\n\t\t// Update cache\n\t\tthis.cachedText = this.text;\n\t\tthis.cachedWidth = width;\n\t\tthis.cachedLines = result;\n\n\t\treturn result.length > 0 ? result : [\"\"];\n\t}\n\n\t/**\n\t * Apply default text style to a string.\n\t * This is the base styling applied to all text content.\n\t * NOTE: Background color is NOT applied here - it's applied at the padding stage\n\t * to ensure it extends to the full line width.\n\t */\n\tprivate applyDefaultStyle(text: string): string {\n\t\tif (!this.defaultTextStyle) {\n\t\t\treturn text;\n\t\t}\n\n\t\tlet styled = text;\n\n\t\t// Apply foreground color (NOT background - that's applied at padding stage)\n\t\tif (this.defaultTextStyle.color) {\n\t\t\tstyled = this.defaultTextStyle.color(styled);\n\t\t}\n\n\t\t// Apply text decorations using this.theme\n\t\tif (this.defaultTextStyle.bold) {\n\t\t\tstyled = this.theme.bold(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.italic) {\n\t\t\tstyled = this.theme.italic(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.strikethrough) {\n\t\t\tstyled = this.theme.strikethrough(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.underline) {\n\t\t\tstyled = this.theme.underline(styled);\n\t\t}\n\n\t\treturn styled;\n\t}\n\n\tprivate getDefaultStylePrefix(): string {\n\t\tif (!this.defaultTextStyle) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tif (this.defaultStylePrefix !== undefined) {\n\t\t\treturn this.defaultStylePrefix;\n\t\t}\n\n\t\tconst sentinel = \"\\u0000\";\n\t\tlet styled = sentinel;\n\n\t\tif (this.defaultTextStyle.color) {\n\t\t\tstyled = this.defaultTextStyle.color(styled);\n\t\t}\n\n\t\tif (this.defaultTextStyle.bold) {\n\t\t\tstyled = this.theme.bold(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.italic) {\n\t\t\tstyled = this.theme.italic(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.strikethrough) {\n\t\t\tstyled = this.theme.strikethrough(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.underline) {\n\t\t\tstyled = this.theme.underline(styled);\n\t\t}\n\n\t\tconst sentinelIndex = styled.indexOf(sentinel);\n\t\tthis.defaultStylePrefix = sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : \"\";\n\t\treturn this.defaultStylePrefix;\n\t}\n\n\tprivate getStylePrefix(styleFn: (text: string) => string): string {\n\t\tconst sentinel = \"\\u0000\";\n\t\tconst styled = styleFn(sentinel);\n\t\tconst sentinelIndex = styled.indexOf(sentinel);\n\t\treturn sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : \"\";\n\t}\n\n\tprivate getDefaultInlineStyleContext(): InlineStyleContext {\n\t\treturn {\n\t\t\tapplyText: (text: string) => this.applyDefaultStyle(text),\n\t\t\tstylePrefix: this.getDefaultStylePrefix(),\n\t\t};\n\t}\n\n\tprivate renderToken(\n\t\ttoken: Token,\n\t\twidth: number,\n\t\tnextTokenType?: string,\n\t\tstyleContext?: InlineStyleContext,\n\t): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tswitch (token.type) {\n\t\t\tcase \"heading\": {\n\t\t\t\tconst headingLevel = token.depth;\n\t\t\t\tconst headingPrefix = `${\"#\".repeat(headingLevel)} `;\n\n\t\t\t\t// Build a heading-specific style context so inline tokens (codespan, bold, etc.)\n\t\t\t\t// restore heading styling after their own ANSI resets instead of falling back to\n\t\t\t\t// the default text style.\n\t\t\t\tlet headingStyleFn: (text: string) => string;\n\t\t\t\tif (headingLevel === 1) {\n\t\t\t\t\theadingStyleFn = (text: string) => this.theme.heading(this.theme.bold(this.theme.underline(text)));\n\t\t\t\t} else {\n\t\t\t\t\theadingStyleFn = (text: string) => this.theme.heading(this.theme.bold(text));\n\t\t\t\t}\n\n\t\t\t\tconst headingStyleContext: InlineStyleContext = {\n\t\t\t\t\tapplyText: headingStyleFn,\n\t\t\t\t\tstylePrefix: this.getStylePrefix(headingStyleFn),\n\t\t\t\t};\n\n\t\t\t\tconst headingText = this.renderInlineTokens(token.tokens || [], headingStyleContext);\n\t\t\t\tconst styledHeading = headingLevel >= 3 ? headingStyleFn(headingPrefix) + headingText : headingText;\n\t\t\t\tlines.push(styledHeading);\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after headings (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"paragraph\": {\n\t\t\t\tconst paragraphText = this.renderInlineTokens(token.tokens || [], styleContext);\n\t\t\t\tlines.push(paragraphText);\n\t\t\t\t// Don't add spacing if next token is space or list\n\t\t\t\tif (nextTokenType && nextTokenType !== \"list\" && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"text\":\n\t\t\t\tlines.push(this.renderInlineTokens([token], styleContext));\n\t\t\t\tbreak;\n\n\t\t\tcase \"code\": {\n\t\t\t\tconst indent = this.theme.codeBlockIndent ?? \" \";\n\t\t\t\tlines.push(this.theme.codeBlockBorder(`\\`\\`\\`${token.lang || \"\"}`));\n\t\t\t\tif (this.theme.highlightCode) {\n\t\t\t\t\tconst highlightedLines = this.theme.highlightCode(token.text, token.lang);\n\t\t\t\t\tfor (const hlLine of highlightedLines) {\n\t\t\t\t\t\tlines.push(`${indent}${hlLine}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Split code by newlines and style each line\n\t\t\t\t\tconst codeLines = token.text.split(\"\\n\");\n\t\t\t\t\tfor (const codeLine of codeLines) {\n\t\t\t\t\t\tlines.push(`${indent}${this.theme.codeBlock(codeLine)}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlines.push(this.theme.codeBlockBorder(\"```\"));\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after code blocks (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"list\": {\n\t\t\t\tconst listLines = this.renderList(token as Tokens.List, 0, width, styleContext);\n\t\t\t\tlines.push(...listLines);\n\t\t\t\t// Don't add spacing after lists if a space token follows\n\t\t\t\t// (the space token will handle it)\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"table\": {\n\t\t\t\tconst tableLines = this.renderTable(token as Tokens.Table, width, nextTokenType, styleContext);\n\t\t\t\tlines.push(...tableLines);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"blockquote\": {\n\t\t\t\tconst quoteStyle = (text: string) => this.theme.quote(this.theme.italic(text));\n\t\t\t\tconst quoteStylePrefix = this.getStylePrefix(quoteStyle);\n\t\t\t\tconst applyQuoteStyle = (line: string): string => {\n\t\t\t\t\tif (!quoteStylePrefix) {\n\t\t\t\t\t\treturn quoteStyle(line);\n\t\t\t\t\t}\n\t\t\t\t\tconst lineWithReappliedStyle = line.replace(/\\x1b\\[0m/g, `\\x1b[0m${quoteStylePrefix}`);\n\t\t\t\t\treturn quoteStyle(lineWithReappliedStyle);\n\t\t\t\t};\n\n\t\t\t\t// Calculate available width for quote content (subtract border \"│ \" = 2 chars)\n\t\t\t\tconst quoteContentWidth = Math.max(1, width - 2);\n\n\t\t\t\t// Blockquotes contain block-level tokens (paragraph, list, code, etc.), so render\n\t\t\t\t// children with renderToken() instead of renderInlineTokens().\n\t\t\t\t// Default message style should not apply inside blockquotes.\n\t\t\t\tconst quoteInlineStyleContext: InlineStyleContext = {\n\t\t\t\t\tapplyText: (text: string) => text,\n\t\t\t\t\tstylePrefix: quoteStylePrefix,\n\t\t\t\t};\n\t\t\t\tconst quoteTokens = token.tokens || [];\n\t\t\t\tconst renderedQuoteLines: string[] = [];\n\t\t\t\tfor (let i = 0; i < quoteTokens.length; i++) {\n\t\t\t\t\tconst quoteToken = quoteTokens[i];\n\t\t\t\t\tconst nextQuoteToken = quoteTokens[i + 1];\n\t\t\t\t\trenderedQuoteLines.push(\n\t\t\t\t\t\t...this.renderToken(quoteToken, quoteContentWidth, nextQuoteToken?.type, quoteInlineStyleContext),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// Avoid rendering an extra empty quote line before the outer blockquote spacing.\n\t\t\t\twhile (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1] === \"\") {\n\t\t\t\t\trenderedQuoteLines.pop();\n\t\t\t\t}\n\n\t\t\t\tfor (const quoteLine of renderedQuoteLines) {\n\t\t\t\t\tconst styledLine = applyQuoteStyle(quoteLine);\n\t\t\t\t\tconst wrappedLines = wrapTextWithAnsi(styledLine, quoteContentWidth);\n\t\t\t\t\tfor (const wrappedLine of wrappedLines) {\n\t\t\t\t\t\tlines.push(this.theme.quoteBorder(\"│ \") + wrappedLine);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after blockquotes (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"hr\":\n\t\t\t\tlines.push(this.theme.hr(\"─\".repeat(Math.min(width, 80))));\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after horizontal rules (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"html\":\n\t\t\t\t// Render HTML as plain text (escaped for terminal)\n\t\t\t\tif (\"raw\" in token && typeof token.raw === \"string\") {\n\t\t\t\t\tlines.push(this.applyDefaultStyle(token.raw.trim()));\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"space\":\n\t\t\t\t// Space tokens represent blank lines in markdown\n\t\t\t\tlines.push(\"\");\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t// Handle any other token types as plain text\n\t\t\t\tif (\"text\" in token && typeof token.text === \"string\") {\n\t\t\t\t\tlines.push(token.text);\n\t\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\tprivate renderInlineTokens(tokens: Token[], styleContext?: InlineStyleContext): string {\n\t\tlet result = \"\";\n\t\tconst resolvedStyleContext = styleContext ?? this.getDefaultInlineStyleContext();\n\t\tconst { applyText, stylePrefix } = resolvedStyleContext;\n\t\tconst codeLinkTheme = resolvedStyleContext.insideLink ? undefined : this.theme;\n\t\tconst applyTextWithNewlines = (text: string): string => {\n\t\t\tconst segments: string[] = text.split(\"\\n\");\n\t\t\treturn segments.map((segment: string) => applyText(segment)).join(\"\\n\");\n\t\t};\n\n\t\tfor (const token of tokens) {\n\t\t\tswitch (token.type) {\n\t\t\t\tcase \"text\":\n\t\t\t\t\t// Text tokens in list items can have nested tokens for inline formatting\n\t\t\t\t\tif (token.tokens && token.tokens.length > 0) {\n\t\t\t\t\t\tresult += this.renderInlineTokens(token.tokens, resolvedStyleContext);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult += applyTextWithNewlines(token.text);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"paragraph\":\n\t\t\t\t\t// Paragraph tokens contain nested inline tokens\n\t\t\t\t\tresult += this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"strong\": {\n\t\t\t\t\tconst boldContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.bold(boldContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"em\": {\n\t\t\t\t\tconst italicContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.italic(italicContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"codespan\":\n\t\t\t\t\tresult += renderMarkdownCodeLink(this.theme.code(token.text), token.text, codeLinkTheme) + stylePrefix;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"image\":\n\t\t\t\t\tresult += resolvedStyleContext.insideLink\n\t\t\t\t\t\t? applyTextWithNewlines(token.text || token.href)\n\t\t\t\t\t\t: renderMarkdownLink({ text: token.text, href: token.href }, token.text || token.href, this.theme) +\n\t\t\t\t\t\t\tstylePrefix;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"link\": {\n\t\t\t\t\tconst linkText = this.renderInlineTokens(token.tokens || [], {\n\t\t\t\t\t\t...resolvedStyleContext,\n\t\t\t\t\t\tinsideLink: true,\n\t\t\t\t\t});\n\t\t\t\t\tresult += renderMarkdownLink({ text: token.text, href: token.href }, linkText, this.theme) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"br\":\n\t\t\t\t\tresult += \"\\n\";\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"del\": {\n\t\t\t\t\tconst delContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.strikethrough(delContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"html\":\n\t\t\t\t\t// Render inline HTML as plain text\n\t\t\t\t\tif (\"raw\" in token && typeof token.raw === \"string\") {\n\t\t\t\t\t\tresult += applyTextWithNewlines(token.raw);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\t// Handle any other inline token types as plain text\n\t\t\t\t\tif (\"text\" in token && typeof token.text === \"string\") {\n\t\t\t\t\t\tresult += applyTextWithNewlines(token.text);\n\t\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\twhile (stylePrefix && result.endsWith(stylePrefix)) {\n\t\t\tresult = result.slice(0, -stylePrefix.length);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate getOrderedListMarker(item: Tokens.ListItem): string | undefined {\n\t\tconst match = /^(?: {0,3})(\\d{1,9}[.)])[ \\t]+/.exec(item.raw);\n\t\treturn match ? `${match[1]} ` : undefined;\n\t}\n\n\t/**\n\t * Render a list with proper nesting support\n\t */\n\tprivate renderList(token: Tokens.List, depth: number, width: number, styleContext?: InlineStyleContext): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst indent = \" \".repeat(depth);\n\t\t// Use the list's start property (defaults to 1 for ordered lists)\n\t\tconst startNumber = typeof token.start === \"number\" ? token.start : 1;\n\n\t\tfor (let i = 0; i < token.items.length; i++) {\n\t\t\tconst item = token.items[i];\n\t\t\tconst isLastItem = i === token.items.length - 1;\n\t\t\tconst bullet = token.ordered\n\t\t\t\t? this.options.preserveOrderedListMarkers\n\t\t\t\t\t? (this.getOrderedListMarker(item) ?? `${startNumber + i}. `)\n\t\t\t\t\t: `${startNumber + i}. `\n\t\t\t\t: \"- \";\n\t\t\tconst taskMarker = item.task ? `[${item.checked ? \"x\" : \" \"}] ` : \"\";\n\t\t\tconst marker = bullet + taskMarker;\n\t\t\tconst firstPrefix = indent + this.theme.listBullet(marker);\n\t\t\tconst continuationPrefix = indent + \" \".repeat(visibleWidth(marker));\n\t\t\tconst itemWidth = Math.max(1, width - visibleWidth(firstPrefix));\n\t\t\tlet renderedAnyLine = false;\n\n\t\t\tfor (const itemToken of item.tokens) {\n\t\t\t\tif (itemToken.type === \"list\") {\n\t\t\t\t\tlines.push(...this.renderList(itemToken as Tokens.List, depth + 1, width, styleContext));\n\t\t\t\t\trenderedAnyLine = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst itemLines = this.renderToken(itemToken, itemWidth, undefined, styleContext);\n\t\t\t\tfor (const line of itemLines) {\n\t\t\t\t\tfor (const wrappedLine of wrapTextWithAnsi(line, itemWidth)) {\n\t\t\t\t\t\tconst linePrefix = renderedAnyLine ? continuationPrefix : firstPrefix;\n\t\t\t\t\t\tlines.push(linePrefix + wrappedLine);\n\t\t\t\t\t\trenderedAnyLine = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!renderedAnyLine) {\n\t\t\t\tlines.push(firstPrefix);\n\t\t\t}\n\n\t\t\tif (token.loose && !isLastItem) {\n\t\t\t\tlines.push(\"\");\n\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\t/**\n\t * Get the visible width of the longest word in a string.\n\t */\n\tprivate getLongestWordWidth(text: string, maxWidth?: number): number {\n\t\tconst words = text.split(/\\s+/).filter((word) => word.length > 0);\n\t\tlet longest = 0;\n\t\tfor (const word of words) {\n\t\t\tlongest = Math.max(longest, visibleWidth(word));\n\t\t}\n\t\tif (maxWidth === undefined) {\n\t\t\treturn longest;\n\t\t}\n\t\treturn Math.min(longest, maxWidth);\n\t}\n\n\t/**\n\t * Wrap a table cell to fit into a column.\n\t *\n\t * Delegates to wrapTextWithAnsi() so ANSI codes + long tokens are handled\n\t * consistently with the rest of the renderer.\n\t */\n\tprivate wrapCellText(text: string, maxWidth: number): string[] {\n\t\treturn wrapTextWithAnsi(text, Math.max(1, maxWidth));\n\t}\n\n\t/**\n\t * Render a table with width-aware cell wrapping.\n\t * Cells that don't fit are wrapped to multiple lines.\n\t */\n\tprivate renderTable(\n\t\ttoken: Tokens.Table,\n\t\tavailableWidth: number,\n\t\tnextTokenType?: string,\n\t\tstyleContext?: InlineStyleContext,\n\t): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst numCols = token.header.length;\n\n\t\tif (numCols === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate border overhead: \"│ \" + (n-1) * \" │ \" + \" │\"\n\t\t// = 2 + (n-1) * 3 + 2 = 3n + 1\n\t\tconst borderOverhead = 3 * numCols + 1;\n\t\tconst availableForCells = availableWidth - borderOverhead;\n\t\tif (availableForCells < numCols) {\n\t\t\t// Too narrow to render a stable table. Fall back to raw markdown.\n\t\t\tconst fallbackLines = token.raw ? wrapTextWithAnsi(token.raw, availableWidth) : [];\n\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\tfallbackLines.push(\"\");\n\t\t\t}\n\t\t\treturn fallbackLines;\n\t\t}\n\n\t\tconst maxUnbrokenWordWidth = 30;\n\n\t\t// Calculate natural column widths (what each column needs without constraints)\n\t\tconst naturalWidths: number[] = [];\n\t\tconst minWordWidths: number[] = [];\n\t\tfor (let i = 0; i < numCols; i++) {\n\t\t\tconst headerText = this.renderInlineTokens(token.header[i].tokens || [], styleContext);\n\t\t\tnaturalWidths[i] = visibleWidth(headerText);\n\t\t\tminWordWidths[i] = Math.max(1, this.getLongestWordWidth(headerText, maxUnbrokenWordWidth));\n\t\t}\n\t\tfor (const row of token.rows) {\n\t\t\tfor (let i = 0; i < row.length; i++) {\n\t\t\t\tconst cellText = this.renderInlineTokens(row[i].tokens || [], styleContext);\n\t\t\t\tnaturalWidths[i] = Math.max(naturalWidths[i] || 0, visibleWidth(cellText));\n\t\t\t\tminWordWidths[i] = Math.max(\n\t\t\t\t\tminWordWidths[i] || 1,\n\t\t\t\t\tthis.getLongestWordWidth(cellText, maxUnbrokenWordWidth),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tlet minColumnWidths = minWordWidths;\n\t\tlet minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);\n\n\t\tif (minCellsWidth > availableForCells) {\n\t\t\tminColumnWidths = new Array(numCols).fill(1);\n\t\t\tconst remaining = availableForCells - numCols;\n\n\t\t\tif (remaining > 0) {\n\t\t\t\tconst totalWeight = minWordWidths.reduce((total, width) => total + Math.max(0, width - 1), 0);\n\t\t\t\tconst growth = minWordWidths.map((width) => {\n\t\t\t\t\tconst weight = Math.max(0, width - 1);\n\t\t\t\t\treturn totalWeight > 0 ? Math.floor((weight / totalWeight) * remaining) : 0;\n\t\t\t\t});\n\n\t\t\t\tfor (let i = 0; i < numCols; i++) {\n\t\t\t\t\tminColumnWidths[i] += growth[i] ?? 0;\n\t\t\t\t}\n\n\t\t\t\tconst allocated = growth.reduce((total, width) => total + width, 0);\n\t\t\t\tlet leftover = remaining - allocated;\n\t\t\t\tfor (let i = 0; leftover > 0 && i < numCols; i++) {\n\t\t\t\t\tminColumnWidths[i]++;\n\t\t\t\t\tleftover--;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tminCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);\n\t\t}\n\n\t\t// Calculate column widths that fit within available width\n\t\tconst totalNaturalWidth = naturalWidths.reduce((a, b) => a + b, 0) + borderOverhead;\n\t\tlet columnWidths: number[];\n\n\t\tif (totalNaturalWidth <= availableWidth) {\n\t\t\t// Everything fits naturally\n\t\t\tcolumnWidths = naturalWidths.map((width, index) => Math.max(width, minColumnWidths[index]));\n\t\t} else {\n\t\t\t// Need to shrink columns to fit\n\t\t\tconst totalGrowPotential = naturalWidths.reduce((total, width, index) => {\n\t\t\t\treturn total + Math.max(0, width - minColumnWidths[index]);\n\t\t\t}, 0);\n\t\t\tconst extraWidth = Math.max(0, availableForCells - minCellsWidth);\n\t\t\tcolumnWidths = minColumnWidths.map((minWidth, index) => {\n\t\t\t\tconst naturalWidth = naturalWidths[index];\n\t\t\t\tconst minWidthDelta = Math.max(0, naturalWidth - minWidth);\n\t\t\t\tlet grow = 0;\n\t\t\t\tif (totalGrowPotential > 0) {\n\t\t\t\t\tgrow = Math.floor((minWidthDelta / totalGrowPotential) * extraWidth);\n\t\t\t\t}\n\t\t\t\treturn minWidth + grow;\n\t\t\t});\n\n\t\t\t// Adjust for rounding errors - distribute remaining space\n\t\t\tconst allocated = columnWidths.reduce((a, b) => a + b, 0);\n\t\t\tlet remaining = availableForCells - allocated;\n\t\t\twhile (remaining > 0) {\n\t\t\t\tlet grew = false;\n\t\t\t\tfor (let i = 0; i < numCols && remaining > 0; i++) {\n\t\t\t\t\tif (columnWidths[i] < naturalWidths[i]) {\n\t\t\t\t\t\tcolumnWidths[i]++;\n\t\t\t\t\t\tremaining--;\n\t\t\t\t\t\tgrew = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!grew) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Render top border\n\t\tconst topBorderCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tlines.push(`┌─${topBorderCells.join(\"─┬─\")}─┐`);\n\n\t\t// Render header with wrapping\n\t\tconst headerCellLines: string[][] = token.header.map((cell, i) => {\n\t\t\tconst text = this.renderInlineTokens(cell.tokens || [], styleContext);\n\t\t\treturn this.wrapCellText(text, columnWidths[i]);\n\t\t});\n\t\tconst headerLineCount = Math.max(...headerCellLines.map((c) => c.length));\n\n\t\tfor (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {\n\t\t\tconst rowParts = headerCellLines.map((cellLines, colIdx) => {\n\t\t\t\tconst text = cellLines[lineIdx] || \"\";\n\t\t\t\tconst padded = text + \" \".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));\n\t\t\t\treturn this.theme.bold(padded);\n\t\t\t});\n\t\t\tlines.push(`│ ${rowParts.join(\" │ \")} │`);\n\t\t}\n\n\t\t// Render separator\n\t\tconst separatorCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tconst separatorLine = `├─${separatorCells.join(\"─┼─\")}─┤`;\n\t\tlines.push(separatorLine);\n\n\t\t// Render rows with wrapping\n\t\tfor (let rowIndex = 0; rowIndex < token.rows.length; rowIndex++) {\n\t\t\tconst row = token.rows[rowIndex];\n\t\t\tconst rowCellLines: string[][] = row.map((cell, i) => {\n\t\t\t\tconst text = this.renderInlineTokens(cell.tokens || [], styleContext);\n\t\t\t\treturn this.wrapCellText(text, columnWidths[i]);\n\t\t\t});\n\t\t\tconst rowLineCount = Math.max(...rowCellLines.map((c) => c.length));\n\n\t\t\tfor (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {\n\t\t\t\tconst rowParts = rowCellLines.map((cellLines, colIdx) => {\n\t\t\t\t\tconst text = cellLines[lineIdx] || \"\";\n\t\t\t\t\treturn text + \" \".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));\n\t\t\t\t});\n\t\t\t\tlines.push(`│ ${rowParts.join(\" │ \")} │`);\n\t\t\t}\n\n\t\t\tif (rowIndex < token.rows.length - 1) {\n\t\t\t\tlines.push(separatorLine);\n\t\t\t}\n\t\t}\n\n\t\t// Render bottom border\n\t\tconst bottomBorderCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tlines.push(`└─${bottomBorderCells.join(\"─┴─\")}─┘`);\n\n\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\tlines.push(\"\"); // Add spacing after table\n\t\t}\n\t\treturn lines;\n\t}\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import { Marked } from "marked";
2
- import { isImageLine } from "../terminal-image.js";
3
2
  import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
4
3
  import { renderMarkdownCodeLink, renderMarkdownLink } from "./markdown-links.js";
4
+ import { finishMarkdownLines, MarkdownStreamCache } from "./markdown-stream-cache.js";
5
5
  import { StrictStrikethroughTokenizer, trimPartialClosingFences } from "./markdown-tokenizer.js";
6
6
  const markdownParser = new Marked();
7
7
  markdownParser.setOptions({
@@ -19,6 +19,7 @@ export class Markdown {
19
19
  cachedText;
20
20
  cachedWidth;
21
21
  cachedLines;
22
+ streamCache = new MarkdownStreamCache();
22
23
  constructor(text, paddingX, paddingY, theme, defaultTextStyle, options) {
23
24
  this.text = text;
24
25
  this.paddingX = paddingX;
@@ -28,10 +29,13 @@ export class Markdown {
28
29
  this.options = options ? { ...options } : {};
29
30
  }
30
31
  setText(text) {
32
+ if (text === this.text)
33
+ return;
31
34
  this.text = text;
32
- this.invalidate();
35
+ this.cachedLines = undefined; // keep the streaming prefix; render() checks it still applies
33
36
  }
34
37
  invalidate() {
38
+ this.streamCache.reset();
35
39
  this.cachedText = undefined;
36
40
  this.cachedWidth = undefined;
37
41
  this.cachedLines = undefined;
@@ -54,51 +58,16 @@ export class Markdown {
54
58
  }
55
59
  // Replace tabs with 3 spaces for consistent rendering
56
60
  const normalizedText = this.text.replace(/\t/g, " ");
57
- // Parse markdown to HTML-like tokens
58
- const tokens = markdownParser.lexer(normalizedText);
61
+ // Parse markdown to tokens, reusing the stable prefix while a message streams
62
+ const tokens = this.streamCache.lex(normalizedText, (src) => markdownParser.lexer(src));
59
63
  trimPartialClosingFences(tokens);
60
- // Convert tokens to styled terminal output
61
- const renderedLines = [];
62
- for (let i = 0; i < tokens.length; i++) {
63
- const token = tokens[i];
64
- const nextToken = tokens[i + 1];
65
- const tokenLines = this.renderToken(token, contentWidth, nextToken?.type);
66
- for (const tokenLine of tokenLines) {
67
- renderedLines.push(tokenLine);
68
- }
69
- }
70
- // Wrap lines (NO padding, NO background yet)
71
- const wrappedLines = [];
72
- for (const line of renderedLines) {
73
- if (isImageLine(line)) {
74
- wrappedLines.push(line);
75
- }
76
- else {
77
- for (const wrappedLine of wrapTextWithAnsi(line, contentWidth)) {
78
- wrappedLines.push(wrappedLine);
79
- }
80
- }
81
- }
82
- // Add margins and background to each wrapped line
83
- const leftMargin = " ".repeat(this.paddingX);
84
- const rightMargin = " ".repeat(this.paddingX);
85
64
  const bgFn = this.defaultTextStyle?.bgColor;
86
65
  const contentLines = [];
87
- for (const line of wrappedLines) {
88
- if (isImageLine(line)) {
66
+ for (let i = 0; i < tokens.length; i++) {
67
+ const nextType = tokens[i + 1]?.type;
68
+ const lines = this.streamCache.linesFor(tokens[i], i, width, nextType, () => finishMarkdownLines(this.renderToken(tokens[i], contentWidth, nextType), width, contentWidth, this.paddingX, bgFn));
69
+ for (const line of lines)
89
70
  contentLines.push(line);
90
- continue;
91
- }
92
- const lineWithMargins = leftMargin + line + rightMargin;
93
- if (bgFn) {
94
- contentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
95
- }
96
- else {
97
- // No background - just pad to width
98
- const visibleLen = visibleWidth(lineWithMargins);
99
- const paddingNeeded = Math.max(0, width - visibleLen);
100
- contentLines.push(lineWithMargins + " ".repeat(paddingNeeded));
101
- }
102
71
  }
103
72
  // Add top/bottom padding (empty lines)
104
73
  const emptyLine = " ".repeat(width);