mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Merge branch 'pr-8158' into HEAD
# Conflicts: # package-lock.json # packages/coding-agent/install-lock/package-lock.json # packages/coding-agent/npm-shrinkwrap.json # packages/coding-agent/package.json
This commit is contained in:
commit
12ee2016e1
5 changed files with 119 additions and 49 deletions
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -4434,6 +4434,15 @@
|
|||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lovely-mermaid": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lovely-mermaid/-/lovely-mermaid-0.3.3.tgz",
|
||||
"integrity": "sha512-EMLJS934qwt2MLO/a5o+T/89HGbuXZPwmCpHIhHO+DVrm+y5tHlEHgpZzlC2o4yW/rFPHRFO9xSMafzqH4wH5A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/lowlight": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
|
||||
|
|
@ -6033,11 +6042,11 @@
|
|||
"chalk": "6.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "8.0.4",
|
||||
"grok-mermaid": "0.2.3",
|
||||
"highlight.js": "10.7.3",
|
||||
"hosted-git-info": "9.0.3",
|
||||
"ignore": "7.0.8",
|
||||
"jiti": "2.7.0",
|
||||
"lovely-mermaid": "0.3.3",
|
||||
"minimatch": "11.0.3",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"quickjs-wasi": "3.6.2",
|
||||
|
|
|
|||
20
packages/coding-agent/install-lock/package-lock.json
generated
20
packages/coding-agent/install-lock/package-lock.json
generated
|
|
@ -506,11 +506,11 @@
|
|||
"chalk": "6.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "8.0.4",
|
||||
"grok-mermaid": "0.2.3",
|
||||
"highlight.js": "10.7.3",
|
||||
"hosted-git-info": "9.0.3",
|
||||
"ignore": "7.0.8",
|
||||
"jiti": "2.7.0",
|
||||
"lovely-mermaid": "0.3.3",
|
||||
"minimatch": "11.0.3",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"quickjs-wasi": "3.6.2",
|
||||
|
|
@ -1015,15 +1015,6 @@
|
|||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/grok-mermaid": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/grok-mermaid/-/grok-mermaid-0.2.3.tgz",
|
||||
"integrity": "sha512-/4KopAbsjvuRP9MdPtlDjOHUmUVEohOX73JNcsWpzAtFxh+bq5+Dhb6gzvRieLDwIPQIR3/vy8V1NNTuz4Zsmg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
|
|
@ -1154,6 +1145,15 @@
|
|||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lovely-mermaid": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lovely-mermaid/-/lovely-mermaid-0.3.3.tgz",
|
||||
"integrity": "sha512-EMLJS934qwt2MLO/a5o+T/89HGbuXZPwmCpHIhHO+DVrm+y5tHlEHgpZzlC2o4yW/rFPHRFO9xSMafzqH4wH5A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz",
|
||||
|
|
|
|||
|
|
@ -51,11 +51,11 @@
|
|||
"chalk": "6.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "8.0.4",
|
||||
"grok-mermaid": "0.2.3",
|
||||
"highlight.js": "10.7.3",
|
||||
"hosted-git-info": "9.0.3",
|
||||
"ignore": "7.0.8",
|
||||
"jiti": "2.7.0",
|
||||
"lovely-mermaid": "0.3.3",
|
||||
"minimatch": "11.0.3",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"quickjs-wasi": "3.6.2",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { Marked, type Token } from "@earendil-works/pi-tui";
|
||||
import { type MermaidArt, render, type Span } from "grok-mermaid";
|
||||
import { getCapabilities, Marked, type Token } from "@earendil-works/pi-tui";
|
||||
import { type AnsiTheme, type MermaidArt, render, toAnsi } from "lovely-mermaid";
|
||||
import type { MarkdownTransformer } from "../../../core/extensions/types.ts";
|
||||
import type { MermaidRenderingMode } from "../../../core/settings-manager.ts";
|
||||
import type { Theme } from "../theme/theme.ts";
|
||||
import type { Theme, ThemeColor } from "../theme/theme.ts";
|
||||
|
||||
const markdownParser = new Marked();
|
||||
|
||||
type MermaidTheme = Pick<Theme, "fg" | "getFgAnsi">;
|
||||
|
||||
interface MermaidTransformerOptions {
|
||||
getMode: () => MermaidRenderingMode;
|
||||
theme?: Theme;
|
||||
theme?: MermaidTheme;
|
||||
}
|
||||
|
||||
function isMermaid(token: Token): token is Token & { type: "code"; text: string; lang?: string } {
|
||||
|
|
@ -35,25 +37,29 @@ function codeSpan(line: string): string {
|
|||
return `${fence}${padding}${content}${padding}${fence}`;
|
||||
}
|
||||
|
||||
function styleSpan(span: Span, theme: Theme): string {
|
||||
switch (span.cls) {
|
||||
case "border":
|
||||
return theme.fg("borderMuted", span.text);
|
||||
case "text":
|
||||
return theme.fg("text", span.text);
|
||||
case "edge":
|
||||
return theme.fg("accent", span.text);
|
||||
case "edgeLabel":
|
||||
return theme.fg("muted", span.text);
|
||||
case "title":
|
||||
return theme.fg("accent", theme.bold(span.text));
|
||||
case "none":
|
||||
return span.text;
|
||||
}
|
||||
}
|
||||
|
||||
function themedLines(art: MermaidArt, theme: Theme): string[] {
|
||||
return art.styled.map((row) => row.map((span) => styleSpan(span, theme)).join(""));
|
||||
function themedLines(art: MermaidArt, theme: MermaidTheme): string[] {
|
||||
const foreground = (color: ThemeColor) => theme.getFgAnsi(color).slice(2, -1);
|
||||
const ansiTheme: AnsiTheme = {
|
||||
border: foreground("borderMuted"),
|
||||
text: foreground("text"),
|
||||
edge: foreground("accent"),
|
||||
edgeLabel: foreground("text"),
|
||||
title: `1;${foreground("accent")}`,
|
||||
};
|
||||
const linkLabels = getCapabilities().hyperlinks;
|
||||
const displayArt: MermaidArt = {
|
||||
...art,
|
||||
// Only osc 8 the text, not the whole box.
|
||||
styled: art.styled.map((row) =>
|
||||
row.map((span) => {
|
||||
if (span.href === undefined || (linkLabels && span.role === "text")) return span;
|
||||
const unlinked = { ...span };
|
||||
delete unlinked.href;
|
||||
return unlinked;
|
||||
}),
|
||||
),
|
||||
};
|
||||
return toAnsi(displayArt, ansiTheme);
|
||||
}
|
||||
|
||||
/** Create a transformer that replaces top-level Mermaid code blocks with Unicode terminal diagrams. */
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { resetCapabilitiesCache, setCapabilities } from "@earendil-works/pi-tui";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { MarkdownTransformContext } from "../src/core/extensions/types.ts";
|
||||
import type { MermaidRenderingMode } from "../src/core/settings-manager.ts";
|
||||
import { createMermaidMarkdownTransformer } from "../src/modes/interactive/components/mermaid.ts";
|
||||
import type { Theme } from "../src/modes/interactive/theme/theme.ts";
|
||||
|
||||
afterEach(() => resetCapabilitiesCache());
|
||||
|
||||
interface TransformOptions {
|
||||
maxWidth?: number;
|
||||
isStreaming?: boolean;
|
||||
messageType?: MarkdownTransformContext["messageType"];
|
||||
mode?: MermaidRenderingMode;
|
||||
theme?: Theme;
|
||||
theme?: Pick<Theme, "fg" | "getFgAnsi">;
|
||||
}
|
||||
|
||||
function transformMermaid(markdown: string, options: TransformOptions = {}): string {
|
||||
|
|
@ -38,22 +41,74 @@ describe("Mermaid rendering", () => {
|
|||
});
|
||||
|
||||
it("leaves unsupported and oversized diagrams unchanged", () => {
|
||||
const unsupported = '```mermaid\npie\n title Pets\n "Dogs" : 4\n```';
|
||||
const unsupported = "```mermaid\ngantt\n title Plan\n```";
|
||||
const oversized = "```mermaid\nflowchart LR\n A[Start] --> B[Done]\n```";
|
||||
|
||||
expect(transformMermaid(unsupported)).toBe(unsupported);
|
||||
expect(transformMermaid(oversized, { maxWidth: 10 })).toBe(oversized);
|
||||
});
|
||||
|
||||
it("renders diagram types added by lovely-mermaid", () => {
|
||||
const rendered = transformMermaid('```mermaid\npie\n title Pets\n "Dogs" : 4\n```');
|
||||
|
||||
expect(rendered).toContain("Pets");
|
||||
expect(rendered).toContain("Dogs");
|
||||
expect(rendered).toContain("████████████████████");
|
||||
expect(rendered).not.toContain("```mermaid");
|
||||
});
|
||||
|
||||
it("maps semantic spans through the Pi theme", () => {
|
||||
const theme = {
|
||||
fg: (color: string, text: string) => `<${color}>${text}</${color}>`,
|
||||
bold: (text: string) => `<bold>${text}</bold>`,
|
||||
} as Theme;
|
||||
fg: (_color: string, text: string) => text,
|
||||
getFgAnsi: (color: string) => {
|
||||
const colors: Record<string, number> = {
|
||||
accent: 45,
|
||||
borderMuted: 240,
|
||||
muted: 245,
|
||||
text: 255,
|
||||
};
|
||||
return `\x1b[38;5;${colors[color]}m`;
|
||||
},
|
||||
};
|
||||
const rendered = transformMermaid("```mermaid\nflowchart LR\n A --> B\n```", { theme });
|
||||
|
||||
expect(rendered).toContain("<borderMuted>");
|
||||
expect(rendered).toContain("<accent>");
|
||||
expect(rendered).toContain("\x1b[38;5;240m");
|
||||
expect(rendered).toContain("\x1b[38;5;45m");
|
||||
});
|
||||
|
||||
it("applies author class styles over the Pi theme", () => {
|
||||
const theme = {
|
||||
fg: (_color: string, text: string) => text,
|
||||
getFgAnsi: () => "\x1b[39m",
|
||||
};
|
||||
const rendered = transformMermaid(
|
||||
"```mermaid\nflowchart LR\n A[Hot]:::highlight --> B[Plain]\n classDef highlight fill:#f96,stroke:#333,color:#000,font-weight:bold\n```",
|
||||
{ theme },
|
||||
);
|
||||
|
||||
expect(rendered).toContain("\x1b[1;38;2;51;51;51;48;2;255;153;102m");
|
||||
expect(rendered).toContain("\x1b[1;38;2;0;0;0;48;2;255;153;102m");
|
||||
expect(rendered).not.toContain("```mermaid");
|
||||
});
|
||||
|
||||
it("links only Mermaid labels and only when the terminal supports hyperlinks", () => {
|
||||
const theme = {
|
||||
fg: (_color: string, text: string) => text,
|
||||
getFgAnsi: () => "\x1b[39m",
|
||||
};
|
||||
const markdown = '```mermaid\nflowchart TD\n A[Docs] --> B[Done]\n click A "https://example.com/docs"\n```';
|
||||
|
||||
setCapabilities({ images: null, trueColor: true, hyperlinks: true });
|
||||
const linked = transformMermaid(markdown, { theme });
|
||||
const linkOpen = "\x1b]8;;https://example.com/docs\x1b\\";
|
||||
expect(linked.split(linkOpen)).toHaveLength(2);
|
||||
expect(linked).not.toContain("```mermaid");
|
||||
|
||||
setCapabilities({ images: null, trueColor: true, hyperlinks: false });
|
||||
const unlinked = transformMermaid(markdown, { theme });
|
||||
expect(unlinked).not.toContain("\x1b]8;;");
|
||||
expect(unlinked).not.toContain("https://example.com/docs");
|
||||
expect(unlinked).not.toContain("```mermaid");
|
||||
});
|
||||
|
||||
it("renders incomplete Mermaid blocks during streaming", () => {
|
||||
|
|
@ -72,29 +127,29 @@ describe("Mermaid rendering", () => {
|
|||
});
|
||||
|
||||
it("falls back to the code block with a warning after streaming", () => {
|
||||
const markdown = "```mermaid\nflowchart LR\n A[Foo] invalid\n```";
|
||||
const markdown = "```mermaid\nflowchart LR\n A --> B\n broken !\n```";
|
||||
const final = transformMermaid(markdown);
|
||||
const followedByText = transformMermaid(`${markdown}\nFollowing text`);
|
||||
const streaming = transformMermaid(markdown, { isStreaming: true });
|
||||
|
||||
expect(final).toContain(markdown);
|
||||
expect(final).toContain("```\n`Mermaid diagram not rendered");
|
||||
expect(final).toContain('dropped, expected a link: "invalid"');
|
||||
expect(final).toContain('dropped, expected a link: "!"');
|
||||
expect(final).not.toContain("more)");
|
||||
expect(followedByText).toContain(" \nFollowing text");
|
||||
expect(streaming).not.toContain("Mermaid diagram not rendered");
|
||||
expect(streaming).not.toContain("```mermaid");
|
||||
expect(streaming).toContain("│ Foo │");
|
||||
expect(streaming).toContain("│ broken │");
|
||||
});
|
||||
|
||||
it("summarizes additional partial-render warnings", () => {
|
||||
const markdown = "```mermaid\nflowchart LR\n A[Foo] invalid\n B[Bar] also-invalid\n```";
|
||||
const markdown = "```mermaid\nflowchart LR\n A --> B\n broken !\n other ?\n```";
|
||||
const rendered = transformMermaid(markdown);
|
||||
|
||||
expect(rendered).toContain(markdown);
|
||||
expect(rendered).toContain('dropped, expected a link: "invalid"');
|
||||
expect(rendered).toContain('dropped, expected a link: "!"');
|
||||
expect(rendered).toContain("(+1 more)");
|
||||
expect(rendered).not.toContain('dropped, expected a link: "also-invalid"');
|
||||
expect(rendered).not.toContain('dropped, expected a link: "?"');
|
||||
});
|
||||
|
||||
it("respects rendering modes and skips thinking blocks and non-transcript surfaces", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue