mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Problem: `pi.registerMarkdownTransformer()` only runs on user, assistant
and thinking Markdown. Pi renders other Markdown that extensions cannot
reach: custom messages, skill blocks, branch and compaction summaries,
and the changelog notices (What's New, `/changelog`, Extension Updates,
the Update Available note). For example, a transformer cannot rewrite
text in an extension's changelog shown under "Extension Updates",
because that call site passes no transform.
Running every transformer everywhere would break existing ones. The
documented example only skips thinking blocks and replaces `-->` with
`→`; on a changelog it would turn `<!-- note -->` into `<!-- note →`.
Solution: an optional `{ messageTypes }` lists the surfaces a
transformer runs on. Without options it runs on the same three types as
before.
| `messageType` | Surfaces |
|---|---|
| `user`, `assistant`, `assistant-thinking` | transcript messages
(default) |
| `custom` (new) | custom messages without a message renderer; expanded
skill blocks |
| `summary` (new) | expanded branch and compaction summaries |
| `changelog` (new) | What's New, `/changelog`, Extension Updates, the
Update Available note |
```typescript
pi.registerMarkdownTransformer((markdown) => markdown.replaceAll("⚠️", "⚠"), {
messageTypes: ["changelog"],
});
```
Details:
- Filtering happens in one place,
`ExtensionRunner.getMarkdownTransformers()`: each transformer is wrapped
so Markdown of unlisted types passes through unchanged. Components still
take a plain list of transformers.
- Skill blocks and summaries pass only their content to transformers; pi
adds its bold header after the transform.
- Custom, skill and summary messages now get `messageId` and `timestamp`
when loaded from the session. Changelog notices have no session entry,
so both are `undefined`.
- The built-in Mermaid transformer renders in user and assistant text
and now also in summaries, which are model-written like assistant text.
It still skips thinking, custom messages and changelogs.
- `/hotkeys` does not run transformers.
- New exports: `MarkdownMessageType`, `MarkdownTransformerOptions`,
`MarkdownTransformerRegistration`.
Tests: runner type filtering (default vs. opted in), custom message,
skill and summary components (type, header excluded, `messageId`),
Mermaid on the new types, and startup notices running `changelog`
transformers on What's New and Extension Updates while default
transformers are skipped.
Related: #11710 (HTML comments in Extension Updates). #11755 already
fixed it in the renderer; this PR lets extensions transform those
notices in general.
112 lines
4.9 KiB
TypeScript
112 lines
4.9 KiB
TypeScript
import { 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";
|
|
|
|
interface TransformOptions {
|
|
maxWidth?: number;
|
|
isStreaming?: boolean;
|
|
messageType?: MarkdownTransformContext["messageType"];
|
|
mode?: MermaidRenderingMode;
|
|
theme?: Theme;
|
|
}
|
|
|
|
function transformMermaid(markdown: string, options: TransformOptions = {}): string {
|
|
const transformer = createMermaidMarkdownTransformer({
|
|
getMode: () => options.mode ?? "streaming",
|
|
theme: options.theme,
|
|
});
|
|
return transformer(markdown, {
|
|
availableWidth: options.maxWidth ?? 100,
|
|
isStreaming: options.isStreaming ?? false,
|
|
messageType: options.messageType ?? "assistant",
|
|
});
|
|
}
|
|
|
|
describe("Mermaid rendering", () => {
|
|
it("replaces Mermaid code blocks with Unicode diagrams", () => {
|
|
const markdown = "Before\n\n```mermaid\nflowchart LR\n A[Start] --> B[Done]\n```\nAfter";
|
|
const rendered = transformMermaid(markdown);
|
|
|
|
expect(rendered).toContain("Before");
|
|
expect(rendered).toContain("┌───────┐");
|
|
expect(rendered).toContain("│ Start ├───▶│ Done │");
|
|
expect(rendered).toContain("└───────┘ └──────┘`\nAfter");
|
|
expect(rendered).not.toContain("```mermaid");
|
|
expect(rendered).toContain("After");
|
|
});
|
|
|
|
it("leaves unsupported and oversized diagrams unchanged", () => {
|
|
const unsupported = '```mermaid\npie\n title Pets\n "Dogs" : 4\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("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;
|
|
const rendered = transformMermaid("```mermaid\nflowchart LR\n A --> B\n```", { theme });
|
|
|
|
expect(rendered).toContain("<borderMuted>");
|
|
expect(rendered).toContain("<accent>");
|
|
});
|
|
|
|
it("renders incomplete Mermaid blocks during streaming", () => {
|
|
const partialMarkdown = "```mermaid\nflowchart LR\n A --> B";
|
|
|
|
expect(transformMermaid(partialMarkdown, { isStreaming: true })).toContain("───▶");
|
|
});
|
|
|
|
it("renders diagrams with class assignments", () => {
|
|
const markdown = "```mermaid\nflowchart LR\n A[Foo]:::highlight --> B[Bar]\n```";
|
|
const rendered = transformMermaid(markdown);
|
|
|
|
expect(rendered).not.toContain("```mermaid");
|
|
expect(rendered).not.toContain("Mermaid diagram not rendered");
|
|
expect(rendered).toContain("│ Foo ├───▶│ Bar │");
|
|
});
|
|
|
|
it("falls back to the code block with a warning after streaming", () => {
|
|
const markdown = "```mermaid\nflowchart LR\n A[Foo] invalid\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).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 │");
|
|
});
|
|
|
|
it("summarizes additional partial-render warnings", () => {
|
|
const markdown = "```mermaid\nflowchart LR\n A[Foo] invalid\n B[Bar] also-invalid\n```";
|
|
const rendered = transformMermaid(markdown);
|
|
|
|
expect(rendered).toContain(markdown);
|
|
expect(rendered).toContain('dropped, expected a link: "invalid"');
|
|
expect(rendered).toContain("(+1 more)");
|
|
expect(rendered).not.toContain('dropped, expected a link: "also-invalid"');
|
|
});
|
|
|
|
it("respects rendering modes and skips thinking blocks and non-transcript surfaces", () => {
|
|
const markdown = "```mermaid\nflowchart LR\n A --> B\n```";
|
|
|
|
expect(transformMermaid(markdown, { mode: "off" })).toBe(markdown);
|
|
expect(transformMermaid(markdown, { mode: "final", isStreaming: true })).toBe(markdown);
|
|
expect(transformMermaid(markdown, { mode: "final" })).not.toContain("```mermaid");
|
|
expect(transformMermaid(markdown, { messageType: "assistant-thinking" })).toBe(markdown);
|
|
expect(transformMermaid(markdown, { messageType: "user" })).not.toContain("```mermaid");
|
|
expect(transformMermaid(markdown, { messageType: "summary" })).not.toContain("```mermaid");
|
|
expect(transformMermaid(markdown, { messageType: "custom" })).toBe(markdown);
|
|
expect(transformMermaid(markdown, { messageType: "changelog" })).toBe(markdown);
|
|
});
|
|
});
|