feat(coding-agent): added inline markdown rendering with theme-aware styling
- Added renderInlineMarkdown() utility function to support inline markdown rendering with optional base color styling. - Refactored ask tool to render questions and option labels with markdown formatting for improved text styling. - Updated hook-input and hook-selector components to render titles as markdown with theme-aware styling. - Implemented recursive token processing for nested markdown elements including bold, italic, code, links, and strikethrough. Fixes #491
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added ACP (Agent Client Protocol) mode for headless agent operation via `--mode acp`
|
||||
@@ -9,6 +10,8 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated ask tool rendering to support markdown formatting in questions and option labels
|
||||
- Refactored hook input and selector components to render titles as markdown for richer text formatting
|
||||
- Changed session collection to include sessions with zero messages, enabling ACP mode to create discoverable sessions immediately
|
||||
- Changed session persistence logic to use atomic file rewrite when flushing unflushed sessions to prevent duplication
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Simple text input component for hooks.
|
||||
*/
|
||||
import { Container, Input, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
import { Container, Input, Markdown, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
||||
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
||||
import { CountdownTimer } from "./countdown-timer";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
@@ -16,7 +16,7 @@ export class HookInputComponent extends Container {
|
||||
#input: Input;
|
||||
#onSubmitCallback: (value: string) => void;
|
||||
#onCancelCallback: () => void;
|
||||
#titleText: Text;
|
||||
#titleComponent: Markdown;
|
||||
#baseTitle: string;
|
||||
#countdown: CountdownTimer | undefined;
|
||||
|
||||
@@ -36,15 +36,15 @@ export class HookInputComponent extends Container {
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
this.#titleText = new Text(theme.fg("accent", title), 1, 0);
|
||||
this.addChild(this.#titleText);
|
||||
this.#titleComponent = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
|
||||
this.addChild(this.#titleComponent);
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
if (opts?.timeout && opts.timeout > 0 && opts.tui) {
|
||||
this.#countdown = new CountdownTimer(
|
||||
opts.timeout,
|
||||
opts.tui,
|
||||
s => this.#titleText.setText(theme.fg("accent", `${this.#baseTitle} (${s}s)`)),
|
||||
s => this.#titleComponent.setText(`${this.#baseTitle} (${s}s)`),
|
||||
() => {
|
||||
opts.onTimeout?.();
|
||||
this.#onCancelCallback();
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
*/
|
||||
import {
|
||||
Container,
|
||||
Markdown,
|
||||
matchesKey,
|
||||
padding,
|
||||
renderInlineMarkdown,
|
||||
replaceTabs,
|
||||
Spacer,
|
||||
Text,
|
||||
@@ -13,7 +15,7 @@ import {
|
||||
truncateToWidth,
|
||||
visibleWidth,
|
||||
} from "@oh-my-pi/pi-tui";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
||||
import { CountdownTimer } from "./countdown-timer";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
@@ -59,7 +61,7 @@ export class HookSelectorComponent extends Container {
|
||||
#outlinedList: OutlinedList | undefined;
|
||||
#onSelectCallback: (option: string) => void;
|
||||
#onCancelCallback: () => void;
|
||||
#titleText: Text;
|
||||
#titleComponent: Markdown;
|
||||
#baseTitle: string;
|
||||
#countdown: CountdownTimer | undefined;
|
||||
#onLeftCallback: (() => void) | undefined;
|
||||
@@ -85,15 +87,15 @@ export class HookSelectorComponent extends Container {
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
this.#titleText = new Text(theme.fg("accent", title), 1, 0);
|
||||
this.addChild(this.#titleText);
|
||||
this.#titleComponent = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
|
||||
this.addChild(this.#titleComponent);
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
if (opts?.timeout && opts.timeout > 0 && opts.tui) {
|
||||
this.#countdown = new CountdownTimer(
|
||||
opts.timeout,
|
||||
opts.tui,
|
||||
s => this.#titleText.setText(theme.fg("accent", `${this.#baseTitle} (${s}s)`)),
|
||||
s => this.#titleComponent.setText(`${this.#baseTitle} (${s}s)`),
|
||||
() => {
|
||||
opts?.onTimeout?.();
|
||||
// Auto-select current option on timeout (typically the first/recommended option)
|
||||
@@ -131,12 +133,14 @@ export class HookSelectorComponent extends Container {
|
||||
);
|
||||
const endIndex = Math.min(startIndex + this.#maxVisible, this.#options.length);
|
||||
|
||||
const mdTheme = getMarkdownTheme();
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const isSelected = i === this.#selectedIndex;
|
||||
const text = isSelected
|
||||
? theme.fg("accent", `${theme.nav.cursor} `) + theme.fg("accent", this.#options[i])
|
||||
: ` ${theme.fg("text", this.#options[i])}`;
|
||||
lines.push(text);
|
||||
const label = isSelected
|
||||
? renderInlineMarkdown(this.#options[i], mdTheme, t => theme.fg("accent", t))
|
||||
: renderInlineMarkdown(this.#options[i], mdTheme, t => theme.fg("text", t));
|
||||
const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
||||
lines.push(prefix + label);
|
||||
}
|
||||
|
||||
if (startIndex > 0 || endIndex < this.#options.length) {
|
||||
|
||||
@@ -16,13 +16,12 @@
|
||||
*/
|
||||
|
||||
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
||||
import { type Component, Container, Markdown, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
||||
import { untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { renderPromptTemplate } from "../config/prompt-templates";
|
||||
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
||||
import { type Theme, theme } from "../modes/theme/theme";
|
||||
import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
|
||||
import askDescription from "../prompts/tools/ask.md" with { type: "text" };
|
||||
import { renderStatusLine } from "../tui";
|
||||
import type { ToolSession } from ".";
|
||||
@@ -574,10 +573,13 @@ interface AskRenderArgs {
|
||||
export const askToolRenderer = {
|
||||
renderCall(args: AskRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
||||
const label = formatTitle("Ask", uiTheme);
|
||||
const mdTheme = getMarkdownTheme();
|
||||
const accentStyle = { color: (t: string) => uiTheme.fg("accent", t) };
|
||||
|
||||
// Multi-part questions
|
||||
if (args.questions && args.questions.length > 0) {
|
||||
let text = `${label} ${uiTheme.fg("muted", `${args.questions.length} questions`)}`;
|
||||
const container = new Container();
|
||||
container.addChild(new Text(`${label} ${uiTheme.fg("muted", `${args.questions.length} questions`)}`, 0, 0));
|
||||
|
||||
for (let i = 0; i < args.questions.length; i++) {
|
||||
const q = args.questions[i];
|
||||
@@ -585,25 +587,29 @@ export const askToolRenderer = {
|
||||
const qBranch = isLastQ ? uiTheme.tree.last : uiTheme.tree.branch;
|
||||
const continuation = isLastQ ? " " : uiTheme.tree.vertical;
|
||||
|
||||
// Question line with metadata
|
||||
const meta: string[] = [];
|
||||
if (q.multi) meta.push("multi");
|
||||
if (q.options?.length) meta.push(`options:${q.options.length}`);
|
||||
const metaStr = meta.length > 0 ? uiTheme.fg("dim", ` · ${meta.join(" · ")}`) : "";
|
||||
|
||||
text += `\n ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${q.id}]`)} ${uiTheme.fg("accent", q.question)}${metaStr}`;
|
||||
container.addChild(
|
||||
new Text(` ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${q.id}]`)}${metaStr}`, 0, 0),
|
||||
);
|
||||
container.addChild(new Markdown(q.question, 3, 0, mdTheme, accentStyle));
|
||||
|
||||
// Options under question
|
||||
if (q.options?.length) {
|
||||
let optText = "";
|
||||
for (let j = 0; j < q.options.length; j++) {
|
||||
const opt = q.options[j];
|
||||
const isLastOpt = j === q.options.length - 1;
|
||||
const optBranch = isLastOpt ? uiTheme.tree.last : uiTheme.tree.branch;
|
||||
text += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} ${uiTheme.fg("muted", opt.label)}`;
|
||||
const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
|
||||
optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} ${optLabel}`;
|
||||
}
|
||||
container.addChild(new Text(optText, 0, 0));
|
||||
}
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
return container;
|
||||
}
|
||||
|
||||
// Single question
|
||||
@@ -611,22 +617,26 @@ export const askToolRenderer = {
|
||||
return new Text(formatErrorMessage("No question provided", uiTheme), 0, 0);
|
||||
}
|
||||
|
||||
let text = `${label} ${uiTheme.fg("accent", args.question)}`;
|
||||
const container = new Container();
|
||||
const meta: string[] = [];
|
||||
if (args.multi) meta.push("multi");
|
||||
if (args.options?.length) meta.push(`options:${args.options.length}`);
|
||||
text += formatMeta(meta, uiTheme);
|
||||
container.addChild(new Text(`${label}${formatMeta(meta, uiTheme)}`, 0, 0));
|
||||
container.addChild(new Markdown(args.question, 1, 0, mdTheme, accentStyle));
|
||||
|
||||
if (args.options?.length) {
|
||||
let optText = "";
|
||||
for (let i = 0; i < args.options.length; i++) {
|
||||
const opt = args.options[i];
|
||||
const isLast = i === args.options.length - 1;
|
||||
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
||||
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} ${uiTheme.fg("muted", opt.label)}`;
|
||||
const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
|
||||
optText += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} ${optLabel}`;
|
||||
}
|
||||
container.addChild(new Text(optText, 0, 0));
|
||||
}
|
||||
|
||||
return new Text(text, 0, 0);
|
||||
return container;
|
||||
},
|
||||
|
||||
renderResult(
|
||||
@@ -635,6 +645,9 @@ export const askToolRenderer = {
|
||||
uiTheme: Theme,
|
||||
): Component {
|
||||
const { details } = result;
|
||||
const mdTheme = getMarkdownTheme();
|
||||
const accentStyle = { color: (t: string) => uiTheme.fg("accent", t) };
|
||||
|
||||
if (!details) {
|
||||
const txt = result.content[0];
|
||||
const fallback = txt?.type === "text" && txt.text ? txt.text : "";
|
||||
@@ -655,7 +668,8 @@ export const askToolRenderer = {
|
||||
},
|
||||
uiTheme,
|
||||
);
|
||||
let text = header;
|
||||
const container = new Container();
|
||||
container.addChild(new Text(header, 0, 0));
|
||||
|
||||
for (let i = 0; i < details.results.length; i++) {
|
||||
const r = details.results[i];
|
||||
@@ -667,22 +681,31 @@ export const askToolRenderer = {
|
||||
? uiTheme.styledSymbol("status.success", "success")
|
||||
: uiTheme.styledSymbol("status.warning", "warning");
|
||||
|
||||
text += `\n ${uiTheme.fg("dim", branch)} ${statusIcon} ${uiTheme.fg("dim", `[${r.id}]`)} ${uiTheme.fg("accent", r.question)}`;
|
||||
container.addChild(
|
||||
new Text(` ${uiTheme.fg("dim", branch)} ${statusIcon} ${uiTheme.fg("dim", `[${r.id}]`)}`, 0, 0),
|
||||
);
|
||||
container.addChild(new Markdown(r.question, 3, 0, mdTheme, accentStyle));
|
||||
|
||||
let answerText = "";
|
||||
if (r.customInput) {
|
||||
text += `\n${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", r.customInput)}`;
|
||||
answerText = `${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", r.customInput)}`;
|
||||
} else if (r.selectedOptions.length > 0) {
|
||||
for (let j = 0; j < r.selectedOptions.length; j++) {
|
||||
const isLast = j === r.selectedOptions.length - 1;
|
||||
const optBranch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
||||
text += `\n${continuation}${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${uiTheme.fg("toolOutput", r.selectedOptions[j])}`;
|
||||
const selectedLabel = renderInlineMarkdown(r.selectedOptions[j], mdTheme, t =>
|
||||
uiTheme.fg("toolOutput", t),
|
||||
);
|
||||
answerText += `\n${continuation}${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${selectedLabel}`;
|
||||
}
|
||||
} else {
|
||||
text += `\n${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
||||
answerText = `${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
||||
}
|
||||
if (answerText) {
|
||||
container.addChild(new Text(answerText, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
return new Text(text, 0, 0);
|
||||
return container;
|
||||
}
|
||||
|
||||
// Single question result
|
||||
@@ -693,25 +716,28 @@ export const askToolRenderer = {
|
||||
}
|
||||
|
||||
const hasSelection = details.customInput || (details.selectedOptions && details.selectedOptions.length > 0);
|
||||
const header = renderStatusLine(
|
||||
{ icon: hasSelection ? "success" : "warning", title: "Ask", description: details.question },
|
||||
uiTheme,
|
||||
);
|
||||
|
||||
let text = header;
|
||||
const header = renderStatusLine({ icon: hasSelection ? "success" : "warning", title: "Ask" }, uiTheme);
|
||||
const container = new Container();
|
||||
container.addChild(new Text(header, 0, 0));
|
||||
container.addChild(new Markdown(details.question, 1, 0, mdTheme, accentStyle));
|
||||
|
||||
let answerText = "";
|
||||
if (details.customInput) {
|
||||
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", details.customInput)}`;
|
||||
answerText = ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", details.customInput)}`;
|
||||
} else if (details.selectedOptions && details.selectedOptions.length > 0) {
|
||||
for (let i = 0; i < details.selectedOptions.length; i++) {
|
||||
const isLast = i === details.selectedOptions.length - 1;
|
||||
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
||||
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${uiTheme.fg("toolOutput", details.selectedOptions[i])}`;
|
||||
const selectedLabel = renderInlineMarkdown(details.selectedOptions[i], mdTheme, t =>
|
||||
uiTheme.fg("toolOutput", t),
|
||||
);
|
||||
answerText += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${selectedLabel}`;
|
||||
}
|
||||
} else {
|
||||
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
||||
answerText = ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
||||
}
|
||||
container.addChild(new Text(answerText, 0, 0));
|
||||
|
||||
return new Text(text, 0, 0);
|
||||
return container;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
|
||||
- Added `renderInlineMarkdown()` function to render inline markdown (bold, italic, code, links, strikethrough) to styled strings
|
||||
|
||||
## [13.14.1] - 2026-03-21
|
||||
### Added
|
||||
@@ -652,4 +655,4 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Readline-style Ctrl+W**: Now skips trailing whitespace before deleting the preceding word, matching standard readline behavior. ([#306](https://github.com/badlogic/pi-mono/pull/306) by [@kim0](https://github.com/kim0))
|
||||
- **Readline-style Ctrl+W**: Now skips trailing whitespace before deleting the preceding word, matching standard readline behavior. ([#306](https://github.com/badlogic/pi-mono/pull/306) by [@kim0](https://github.com/kim0))
|
||||
@@ -1,4 +1,4 @@
|
||||
import { marked, type Token } from "marked";
|
||||
import { marked, type Token, type Tokens } from "marked";
|
||||
import type { SymbolTheme } from "../symbols";
|
||||
import { TERMINAL } from "../terminal-capabilities";
|
||||
import type { Component } from "../tui";
|
||||
@@ -838,3 +838,68 @@ export class Markdown implements Component {
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render inline markdown (bold, italic, code, links, strikethrough) to a styled string.
|
||||
* Unlike the full Markdown component, this produces a single line with no block-level elements.
|
||||
*/
|
||||
export function renderInlineMarkdown(text: string, mdTheme: MarkdownTheme, baseColor?: (t: string) => string): string {
|
||||
const tokens = marked.lexer(text);
|
||||
const applyText = baseColor ?? ((t: string) => t);
|
||||
let result = "";
|
||||
for (const token of tokens) {
|
||||
if (token.type === "paragraph" && token.tokens) {
|
||||
result += renderInlineTokens(token.tokens, mdTheme, applyText);
|
||||
} else if (token.type === "list") {
|
||||
result += token.items
|
||||
.map((item: Tokens.ListItem, index: number) => {
|
||||
const prefix = token.ordered ? `${(token.start || 1) + index}. ` : "• ";
|
||||
const content = item.tokens ? renderInlineTokens(item.tokens, mdTheme, applyText) : applyText(item.text);
|
||||
return `${applyText(prefix)}${content}`;
|
||||
})
|
||||
.join(applyText(" "));
|
||||
} else if ("text" in token && typeof token.text === "string") {
|
||||
result += applyText(token.text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderInlineTokens(tokens: Token[], mdTheme: MarkdownTheme, applyText: (t: string) => string): string {
|
||||
let result = "";
|
||||
const styleReset = applyText("");
|
||||
for (const token of tokens) {
|
||||
switch (token.type) {
|
||||
case "text":
|
||||
if (token.tokens && token.tokens.length > 0) {
|
||||
result += renderInlineTokens(token.tokens, mdTheme, applyText);
|
||||
} else {
|
||||
result += applyText(token.text);
|
||||
}
|
||||
break;
|
||||
case "strong":
|
||||
result += mdTheme.bold(renderInlineTokens(token.tokens || [], mdTheme, applyText)) + styleReset;
|
||||
break;
|
||||
case "em":
|
||||
result += mdTheme.italic(renderInlineTokens(token.tokens || [], mdTheme, applyText)) + styleReset;
|
||||
break;
|
||||
case "codespan":
|
||||
result += mdTheme.code(token.text) + styleReset;
|
||||
break;
|
||||
case "del":
|
||||
result += mdTheme.strikethrough(renderInlineTokens(token.tokens || [], mdTheme, applyText)) + styleReset;
|
||||
break;
|
||||
case "link": {
|
||||
const linkText = renderInlineTokens(token.tokens || [], mdTheme, applyText);
|
||||
result += mdTheme.link(mdTheme.underline(linkText)) + styleReset;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if ("text" in token && typeof token.text === "string") {
|
||||
result += applyText(token.text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import type { Terminal as XtermTerminalType } from "@xterm/headless";
|
||||
import { Chalk } from "chalk";
|
||||
import { Markdown } from "../src/components/markdown.js";
|
||||
import { Markdown, renderInlineMarkdown } from "../src/components/markdown.js";
|
||||
import { type Component, TUI } from "../src/tui.js";
|
||||
import { defaultMarkdownTheme } from "./test-themes.js";
|
||||
import { VirtualTerminal } from "./virtual-terminal.js";
|
||||
@@ -19,6 +19,15 @@ function getCellItalic(terminal: VirtualTerminal, row: number, col: number): num
|
||||
return cell!.isItalic();
|
||||
}
|
||||
|
||||
describe("renderInlineMarkdown", () => {
|
||||
it("preserves ordered list items as visible inline text", () => {
|
||||
const rendered = renderInlineMarkdown("1. Review against a base branch (PR Style)", defaultMarkdownTheme);
|
||||
const plain = rendered.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
|
||||
expect(plain).toBe("1. Review against a base branch (PR Style)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Markdown component", () => {
|
||||
describe("Nested lists", () => {
|
||||
it("should render simple nested list", () => {
|
||||
|
||||
Reference in New Issue
Block a user