feat(tui): add mermaid rendering toggle

This commit is contained in:
arg3t
2026-06-26 04:34:44 -07:00
parent dacce3bc53
commit e894753603
13 changed files with 243 additions and 14 deletions
@@ -790,6 +790,17 @@ export const SETTINGS_SCHEMA = {
},
},
"tui.renderMermaid": {
type: "boolean",
default: true,
ui: {
tab: "appearance",
group: "Display",
label: "Render Mermaid Diagrams",
description: "Render Mermaid fenced code blocks as ASCII diagrams",
},
},
"tui.hyperlinks": {
type: "enum",
values: ["off", "auto", "always"] as const,
@@ -22,6 +22,7 @@ import {
getSymbolTheme,
previewTheme,
setColorBlindMode,
setMarkdownMermaidRendering,
setSymbolPreset,
setTheme,
theme,
@@ -374,6 +375,15 @@ export class SelectorController {
this.ctx.ui.requestRender();
break;
case "tui.renderMermaid":
setMarkdownMermaidRendering(value as boolean);
this.ctx.session.refreshBaseSystemPrompt().catch(err => {
this.ctx.showError(`Failed to apply Mermaid rendering setting: ${err}`);
});
this.ctx.rebuildChatFromMessages();
this.ctx.ui.resetDisplay();
break;
case "theme": {
setTheme(value as string, true).then(result => {
this.ctx.statusLine.invalidate();
@@ -167,6 +167,7 @@ import {
getSymbolTheme,
onTerminalAppearanceChange,
onThemeChange,
setMarkdownMermaidRendering,
theme,
} from "./theme/theme";
import type {
@@ -588,6 +589,7 @@ export class InteractiveMode implements InteractiveModeContext {
}
setTuiTight(settings.get("tui.tight"));
setMarkdownMermaidRendering(settings.get("tui.renderMermaid"));
this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages"));
// OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's
@@ -0,0 +1,53 @@
import { afterEach, beforeAll, describe, expect, it } from "bun:test";
import { Markdown } from "@oh-my-pi/pi-tui";
import { Settings } from "../../config/settings";
import { buildSystemPrompt } from "../../system-prompt";
import { getMarkdownTheme, getThemeByName, setMarkdownMermaidRendering, setThemeInstance } from "./theme";
const workspaceTree = {
rootPath: "/tmp/project",
rendered: "",
truncated: false,
totalLines: 0,
agentsMdFiles: [],
};
function stripAnsi(text: string): string {
return text.replace(/\x1b\[[0-9;]*m/g, "");
}
beforeAll(async () => {
await Settings.init({ inMemory: true });
const theme = await getThemeByName("dark");
if (!theme) throw new Error("theme unavailable");
setThemeInstance(theme);
});
afterEach(() => {
setMarkdownMermaidRendering(true);
});
describe("Mermaid rendering setting", () => {
it("removes the Mermaid prompt note when rendering is disabled", async () => {
const { systemPrompt } = await buildSystemPrompt({
renderMermaid: false,
contextFiles: [],
skills: [],
toolNames: [],
workspaceTree,
});
expect(systemPrompt.join("\n")).not.toContain("```mermaid");
});
it("falls back to a highlighted code fence when rendering is disabled", () => {
setMarkdownMermaidRendering(false);
const markdown = new Markdown("```mermaid\ngraph TD\n A --> B\n```", 0, 0, getMarkdownTheme());
const lines = stripAnsi(markdown.render(80).join("\n"));
expect(lines).toContain("```mermaid");
expect(lines).toContain("graph TD");
expect(lines).toContain("-->");
});
});
+33 -14
View File
@@ -2824,23 +2824,36 @@ export function getSymbolTheme(): SymbolTheme {
let cachedMarkdownTheme: MarkdownTheme | undefined;
let cachedMarkdownThemeRef: Theme | undefined;
let markdownMermaidRendering = true;
export function setMarkdownMermaidRendering(enabled: boolean): void {
if (markdownMermaidRendering === enabled) return;
markdownMermaidRendering = enabled;
cachedMarkdownTheme = undefined;
}
export function getMarkdownTheme(): MarkdownTheme {
if (cachedMarkdownTheme !== undefined && cachedMarkdownThemeRef === theme) {
return cachedMarkdownTheme;
}
// Mermaid ASCII diagrams render with the active palette so they read as
// content rather than raw monochrome. Roles mirror the SVG renderer's
// mapping; `text`/`muted`/`border`/`borderMuted`/`accent` exist in every theme.
const mermaidColorMode = theme.getColorMode() === "truecolor" ? "truecolor" : "ansi256";
const mermaidTheme = {
fg: theme.getColorHex("text"),
border: theme.getColorHex("border"),
line: theme.getColorHex("muted"),
arrow: theme.getColorHex("accent"),
corner: theme.getColorHex("muted"),
junction: theme.getColorHex("borderMuted"),
};
const mermaid = markdownMermaidRendering
? (() => {
// Mermaid ASCII diagrams render with the active palette so they read as
// content rather than raw monochrome. Roles mirror the SVG renderer's
// mapping; `text`/`muted`/`border`/`borderMuted`/`accent` exist in every theme.
const mermaidColorMode =
theme.getColorMode() === "truecolor" ? ("truecolor" as const) : ("ansi256" as const);
const mermaidTheme = {
fg: theme.getColorHex("text"),
border: theme.getColorHex("border"),
line: theme.getColorHex("muted"),
arrow: theme.getColorHex("accent"),
corner: theme.getColorHex("muted"),
junction: theme.getColorHex("borderMuted"),
};
return { mermaidColorMode, mermaidTheme };
})()
: undefined;
const markdownTheme: MarkdownTheme = {
heading: (text: string) => theme.fg("mdHeading", text),
link: (text: string) => theme.fg("mdLink", text),
@@ -2857,8 +2870,14 @@ export function getMarkdownTheme(): MarkdownTheme {
underline: (text: string) => theme.underline(text),
strikethrough: (text: string) => chalk.strikethrough(text),
symbols: getSymbolTheme(),
resolveMermaidAscii: (source, maxWidth) =>
resolveMermaidAscii(source, { maxWidth, theme: mermaidTheme, colorMode: mermaidColorMode }),
resolveMermaidAscii: mermaid
? (source, maxWidth) =>
resolveMermaidAscii(source, {
maxWidth,
theme: mermaid.mermaidTheme,
colorMode: mermaid.mermaidColorMode,
})
: undefined,
highlightCode: (code: string, lang?: string): string[] => {
const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
const highlighted = highlightCached(code, validLang, theme);
@@ -16,7 +16,9 @@ You are a helpful assistant the team trusts with load-bearing changes, operating
- Consider what code compiles to. NEVER allocate avoidably; no needless copies or computation.
- You are not alone in this repo. Treat unexpected changes as the user's work and adapt.
- In terminal prose and final chat, you MAY use LaTeX math (`$`, `$$`, `\text`, `\times`) and color (`\textcolor`, `\colorbox`, `\fcolorbox`).
{{#if renderMermaid}}
- To show a diagram, you MAY emit a ` ```mermaid ` block — the terminal renders it as ASCII. Use it for genuine structure or flow, not trivia.
{{/if}}
RUNTIME
==============
+1
View File
@@ -2221,6 +2221,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
memoryRootEnabled: memoryBackend.id === "local",
model: settings.get("includeModelInPrompt") ? getActiveModelString() : undefined,
personality: agentKind === "sub" ? "none" : settings.get("personality"),
renderMermaid: settings.get("tui.renderMermaid"),
});
if (options.systemPrompt === undefined) {
@@ -423,6 +423,8 @@ export interface BuildSystemPromptOptions {
personality?: Personality;
/** Whether to include the workspace directory tree in the system prompt. Default: false */
includeWorkspaceTree?: boolean;
/** Whether Mermaid fenced blocks render as terminal ASCII diagrams. Default: true */
renderMermaid?: boolean;
}
/** Result of building provider-facing system prompt messages. */
@@ -464,6 +466,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
model,
personality = "default",
includeWorkspaceTree = false,
renderMermaid = true,
} = options;
const inlineToolDescriptors = providedInlineToolDescriptors ?? false;
const resolvedCwd = cwd ?? getProjectDir();
@@ -682,6 +685,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
hasMemoryRoot: memoryRootEnabled,
hasObsidian: hasObsidian(),
includeWorkspaceTree,
renderMermaid,
};
const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
const systemPrompt = [rendered];