Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/webui.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,28 @@ plain `<pre>` view that shipped in slice 02:
for `cp file.js file.js.bak; copy in panel; paste back`) and never
leaks the gutter line numbers into the copied text.

## Markdown rendering and Mermaid diagrams (slice 23)

Assistant messages and Markdown file previews render through
`webapp/lib/markdown.ts` (`marked`, already a workspace dependency — no
CDN). A fenced code block whose language token is `mermaid` renders as a
diagram instead of a code block. The behaviour is a contract, not an
implementation accident:

| Aspect | Contract | Backed by |
| --- | --- | --- |
| Fence language | The bare token after the fence opener must be `mermaid`, case-insensitive; trailing metadata (```` ```mermaid {theme: dark} ````) still matches | `webapp/lib/markdown.ts:133-136` |
| Renderer seam | Fence languages dispatch through a language→renderer registry; the markdown main flow never branches on a language name. Any other fence language can be taken over the same way — one `registerLanguageRenderer(...)` call. That is the seam a future `minimax-code-plugin` renderer will install through | `webapp/lib/markdown.ts:54-113`, `webapp/lib/mermaid-renderer.ts:64-71` |
| Theme | Diagrams re-render when the app switches light/dark: the host watches `<html>`'s class and mermaid is re-initialised per theme | `components/markdown-html.tsx:52-66`, `components/mermaid-block.tsx:156-166` + `223-229` |
| Failure state | A syntax error does not blank the page. The failing diagram shows its error text plus the **original source in a copyable `<pre>`** — the copy round-trips byte-exact, including `-->|label|` edge syntax — and the rest of the document renders normally. (If the chart library itself fails to load — offline, say — the same failure card appears with the source still copyable. If a language renderer throws, the fence falls back to the plain code block — same "never blank the document" rule at the parser level.) | `components/mermaid-block.tsx:285-307`, `components/markdown-html.tsx:183-230`, `webapp/lib/markdown.ts:149-165` |
| Sizing | Diagrams scale to the column width; a diagram wider than its card scrolls inside the card | `webapp/styles/mermaid.css:62-80` |
| CJK labels | Node and edge labels render through a font stack with PingFang SC / Microsoft YaHei / Noto Sans CJK SC fallbacks, so Chinese text does not come out as tofu | `components/mermaid-block.tsx:109` |
| Loading | The chart library is several megabytes and is `import()`-ed when the **first** diagram of a page mounts; the chunk ships with a one-year immutable cache, so later page loads fetch it from the browser cache. A page with no mermaid fence never requests the chunk | `components/mermaid-block.tsx:54-66`, `server/lib/static.js:64-66` |
| Outline | A diagram is never a heading: the fence emits a `<pre>`/`<div>` placeholder pair, not `h1`–`h6`, so diagrams appear in no heading-derived outline (the webui itself renders no Markdown outline today) | `webapp/lib/mermaid-renderer.ts:44-57` |

The `mermaid` dependency (11.12.1, MIT) is recorded in
`release/dependency-licenses.json`.

## Persistence keys (client-side `localStorage` / `sessionStorage`)

| Key | Channel | Owner | Introduced by | Shape |
Expand Down
51 changes: 51 additions & 0 deletions docs/webui.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,57 @@ slice 22 增强:
文件字节互为往返 ——`cp file.js file.js.bak; 在面板里复制; 粘回去`),
且绝不让行号槽混入复制文本。

## Markdown 里的 Mermaid 图(slice 23)

助手回复和 Markdown 文件预览共用一套渲染管线
(`webapp/lib/markdown.ts`,`marked` 是工作区既有依赖,不走 CDN)。在
代码围栏的语言位置写 `mermaid`,围栏内容就会被画成图,而不是显示为
代码块:

````markdown
```mermaid
flowchart LR
需求 --> 开发 --> 验收
```
````

**你会得到什么**

- 只要围栏语言是 `mermaid` 就出图,大小写不敏感;围栏后跟的
`{...}` 参数不影响识别(`webapp/lib/markdown.ts:133-136`)。
- 中文标签正常显示:节点和边上的文字走 PingFang SC / Microsoft
YaHei / Noto Sans CJK SC 字体栈,不会画成方块
(`components/mermaid-block.tsx:109`)。
- 图跟随界面浅色/深色主题,切换主题时已渲染的图会重新画
(`components/markdown-html.tsx:52-66`)。
- 图按列宽缩放;特别宽的图在卡片内横向滚动,不撑破版面
(`webapp/styles/mermaid.css:62-80`)。

**图坏了会怎样**

- **语法写错**:出错的那张图显示"Mermaid 渲染失败"卡片——错误原因
加原始源码。源码可以原样选中复制,复制回来的内容和当初写的逐字节
一致,包括 `-->|标签|` 这类箭头语法
(`components/mermaid-block.tsx:285-307`、
`components/markdown-html.tsx:183-230`)。文档其余部分照常渲染,
一张图坏了不会让整篇白屏。
- **图表库加载失败**(如断网):同样落入失败卡片,源码仍可复制,
其余内容不受影响。

**限制**

- **首次遇到图需要加载**:图表库有几 MB,页面里第一张图出现时才从
服务端加载(没有任何 mermaid 图的页面完全不请求它,
`components/mermaid-block.tsx:54-66`);**图表库文件**带一年期 immutable
强缓存(`server/lib/static.js:64-66`),之后的页面加载直接用浏览
器缓存,不重复下载。
- **不进目录大纲**:图不产生标题。围栏渲染为 `<pre>`/`<div>` 占位
元素而不是 `h1`-`h6`(`webapp/lib/mermaid-renderer.ts:44-57`),
因此图永远不会出现在按标题组织的大纲或导航里(webui 目前的
Markdown 渲染本身也不生成大纲)。

依赖:`mermaid` 11.12.1(MIT),已登记于 `release/dependency-licenses.json`。

## 持久化键(客户端 `localStorage` / `sessionStorage`)

| 键 | 通道 | 归属 | 引入 ticket | 数据形态 |
Expand Down
3 changes: 2 additions & 1 deletion packages/webui/docs/CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ single index that satisfies the check.
| Long chat list virtualization (≥ 200 messages) | ✅ | `webapp/lib/transcript.ts` virtual-window branch (N ≥ 200) with scroll/resize rAF handler; covered by `webapp/test/transcript.test.ts`. |
| Markdown rendering (headings, lists, code) | ✅ | `webapp/lib/markdown.ts` wrapping the workspace `marked` package (`packages/tui` already depends on it; no CDN) |
| Syntax highlighting in code blocks | ✅ | `marked` code renderer (`webapp/lib/markdown.ts`) + CSS classes from `webapp/styles/official-utilities.css` |
| Mermaid diagrams in markdown (slice 23) | ✅ | a fenced code block whose language token is `mermaid` renders as a diagram instead of a code block. Fence languages dispatch through the language→renderer registry (`webapp/lib/markdown.ts#registerLanguageRenderer`, lines 107-113); mermaid self-registers on import (`webapp/lib/mermaid-renderer.ts:64-71`), and a renderer for any other fence language plugs into the same seam — the markdown main flow never branches on a language name. The chart library is `import()`-ed on the first diagram of a page (`webapp/components/mermaid-block.tsx:62-66`) and its chunk is served with a one-year immutable cache (`server/lib/static.js:66`); a page without a mermaid fence never requests it. Diagrams follow the light/dark theme (`components/markdown-html.tsx:52-66` re-renders on the `<html>` class flip; `components/mermaid-block.tsx:156-166,223-229` re-initialises mermaid per theme). A syntax error renders a legible failure card — error text plus the original source in a copyable `<pre>` (`components/mermaid-block.tsx:285-307`) — while the rest of the document renders normally. Limits: diagrams scale to the column width and scroll when wider (`webapp/styles/mermaid.css:62-80`); CJK labels render via the font stack at `components/mermaid-block.tsx:109`; a diagram is never a heading, so it appears in no heading-derived outline (the fence emits a `<pre>`/`<div>` pair, not `h1`-`h6` — `lib/mermaid-renderer.ts:44-57`). Dependency `mermaid` 11.12.1 (MIT) is recorded in `release/dependency-licenses.json`. |
| Cancel mid-run | ✅ | acp `session/cancel` is sent as a notification, pinned on the cid's active child (`/api/protocol/cancel` → `server/lib/mcode-rpc.js#cancelSession`). The hard-kill fallback (`/api/stop` → SIGTERM/SIGKILL) is only used when the notification cannot be delivered. The acp session may emit a few extra events before draining. |
| Rewind / fork a message | ⚠ | The engine implements `session/fork` and `session/resume` (`MCODE_ACP_CAPABILITIES.fork / .resume = true`), but no webui route exposes them yet — see [§13](CAPABILITIES.md#13-what-mcode-would-need-to-add-to-enable-the--rows). |
| Edit a sent message and resend | ❌ | Not exposed by the acp protocol |
Expand Down Expand Up @@ -181,7 +182,7 @@ single index that satisfies the check.
| Custom CSS themes | ❌ | no theme loader; would need a CSS-vars system |
| User-defined hotkeys | ❌ | shortcuts are hard-coded |
| Four-column shell (sidebar · conversation · preview · tree) | ✅ | slice 17 + slice 21 (`webapp/components/workspace-columns.tsx`). Conversation column is fluid in `[280, 768]` px while at least one fixed column is visible; with **both** on-demand columns folded the conversation column lifts past 768 and takes the full remainder — measured at 1280 / 240-px chrome = 1040 px, at 1920 = 1680 px (`webapp/test/workspace-tabs-state.test.ts#computeColumnLayout — slice 21 idle state`). The preview and tree columns are **on demand** (slice 21): each appears when at least one matching-role tab is open and auto-closes when the last tab in that role closes. `syncColumnVisibility(tabStrip, layout)` re-derives the visibility flags on every change; the deserializer normalises a stale "column open but empty" payload to closed so a stale disk write cannot conjure an empty column. Surface kinds are split by `columnRoleForKind` — `file:<path> \| browser` lives on the preview column, `files \| git \| tasks \| search \| plugins` on the tree column. The previously-shipped `search`, `alerts`, and `progress` `PanelKind` values were removed from the union (`webapp/lib/persist.ts#PanelKind`). The sidebar tree column's "搜索" surface is **real as of slice 19b** (`webapp/components/workspace-tree-column.tsx#SearchSurface`); the engine contract for plugins is not yet shipped, so the Plugins surface still renders an i18n "this is coming" card rather than a silent no-op. |
| IDE-grade code preview (slice 22 — line gutter, per-language lazy syntax highlighting, byte-faithful copy) | ✅ | `webapp/components/code-view.tsx` is mounted inside `file-preview`. (1) **Line-number gutter** via `splitHighlightedLines` (`webapp/lib/code-highlight.ts`) — line numbers are aligned to code lines and independent of horizontal scroll; cross-line `<span>` from highlight.js is balanced per-line so each row is hover-stable and copy-faithful. (2) **Per-language lazy highlighting** via `loadHljsLanguage` — only the open file's grammar is imported. The switch / if-ladder of literal `import("highlight.js/lib/languages/<name>.js")` branches is what lets webpack code-split each grammar into its own chunk (a Record-driven dynamic import would have bundled all 191 grammars). Bounded work: 32 KiB / 1500 lines; larger files are truncated before highlight and the UI renders an honest `truncated` notice. Unknown languages fall through to a plain monospace view — the contract is total. (3) **Byte-faithful copy** — `endsWithNewline` is tracked across the highlight → split → copy chain so the clipboard text round-trips to the file bytes (`cp file.js file.js.bak; copy in panel; paste back`); line numbers never leak into the copied text. The server labels the file via `EXT_LANGUAGE` (`server/lib/fs-util.js#languageForExtension`); the view does not re-guess. Mermaid rendering in markdown (a candidate slice 23) is **not** implemented in `webapp/lib/markdown.ts` and is not documented as a capability — it is being built and has not landed. |
| IDE-grade code preview (slice 22 — line gutter, per-language lazy syntax highlighting, byte-faithful copy) | ✅ | `webapp/components/code-view.tsx` is mounted inside `file-preview`. (1) **Line-number gutter** via `splitHighlightedLines` (`webapp/lib/code-highlight.ts`) — line numbers are aligned to code lines and independent of horizontal scroll; cross-line `<span>` from highlight.js is balanced per-line so each row is hover-stable and copy-faithful. (2) **Per-language lazy highlighting** via `loadHljsLanguage` — only the open file's grammar is imported. The switch / if-ladder of literal `import("highlight.js/lib/languages/<name>.js")` branches is what lets webpack code-split each grammar into its own chunk (a Record-driven dynamic import would have bundled all 191 grammars). Bounded work: 32 KiB / 1500 lines; larger files are truncated before highlight and the UI renders an honest `truncated` notice. Unknown languages fall through to a plain monospace view — the contract is total. (3) **Byte-faithful copy** — `endsWithNewline` is tracked across the highlight → split → copy chain so the clipboard text round-trips to the file bytes (`cp file.js file.js.bak; copy in panel; paste back`); line numbers never leak into the copied text. The server labels the file via `EXT_LANGUAGE` (`server/lib/fs-util.js#languageForExtension`); the view does not re-guess. |

## 11. Network & access control

Expand Down
1 change: 1 addition & 0 deletions packages/webui/docs/CAPABILITIES.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ CI 会对上述每一个名称是否出现在本文档中进行断言
| 长聊天列表虚拟化(≥ 200 条消息) | ✅ | `webapp/lib/transcript.ts` 虚拟窗口分支(N ≥ 200),带滚动/缩放 rAF 处理器;由 `webapp/test/transcript.test.ts` 覆盖。 |
| Markdown 渲染(标题、列表、代码) | ✅ | `lib/marked.min.js` 本地内置(不走 CDN) |
| 代码块语法高亮 | ✅ | highlight.js(本地副本) |
| Markdown 内 Mermaid 图渲染(slice 23) | ✅ | 代码围栏语言写 `mermaid` 即渲染为图,而非代码块。围栏语言经"语言→渲染器"注册表分发(`webapp/lib/markdown.ts` 的 `registerLanguageRenderer`,107-113 行);mermaid 在模块导入时自注册(`webapp/lib/mermaid-renderer.ts:64-71`),其他语言的渲染器可经同一接缝接入——markdown 主流程不针对语言名写分支。图表库在页面首张图出现时才动态加载(`components/mermaid-block.tsx:62-66`),该文件带一年 immutable 强缓存(`server/lib/static.js:66`);没有 mermaid 围栏的页面完全不加载。图跟随浅色/深色主题(`components/markdown-html.tsx:52-66` 监听 `<html>` class;`components/mermaid-block.tsx:156-166、223-229` 按主题重新初始化)。语法错误时渲染可读的失败卡片——错误信息 + 可复制的原始源码(`components/mermaid-block.tsx:285-307`)——文档其余部分照常渲染。限制:图按列宽缩放、超宽时在卡片内横向滚动(`webapp/styles/mermaid.css:62-80`);中文标签经字体栈正常显示(`components/mermaid-block.tsx:109`);图不产生标题,因此不会进入任何按标题组织的大纲(围栏只产出 `<pre>`/`<div>` 占位对,不产出 `h1`-`h6`,见 `lib/mermaid-renderer.ts:44-57`)。依赖 `mermaid` 11.12.1(MIT)已登记于 `release/dependency-licenses.json`。 |
| 运行中取消 | ✅ | acp `session/cancel` 以 notification 形式发送,并钉在该 cid 的活动子进程上(`/api/protocol/cancel` → `server/lib/mcode-rpc.js#cancelSession`)。只有当 notification 无法投递时,才会走硬杀兜底(`/api/stop` → SIGTERM/SIGKILL)。acp 会话在排空前可能还会再发出几个事件。 |
| 回退 / 分叉某条消息 | ⚠ | 引擎已实现 `session/fork` 和 `session/resume`(`MCODE_ACP_CAPABILITIES.fork / .resume = true`),但目前 webui 还没有路由暴露它们——参见 [§13](CAPABILITIES.zh-CN.md#13-要启用--行-mcode-需要增加什么)。 |
| 编辑已发送的消息并重新发送 | ❌ | acp 协议未暴露 |
Expand Down
1 change: 1 addition & 0 deletions packages/webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"autoprefixer": "10.6.1",
"highlight.js": "10.7.3",
"marked": "18.0.12",
"mermaid": "11.12.1",
"next": "14.2.35",
"postcss": "8.5.28",
"react": "18.3.1",
Expand Down
1 change: 1 addition & 0 deletions packages/webui/webapp/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "../styles/official-utilities.css";
import "../styles/mavis-dropdown.css";
import "../styles/desktop-typography.css";
import "../styles/code-preview.css";
import "../styles/mermaid.css";

export const metadata: Metadata = {
title: "MiniMax Code",
Expand Down
16 changes: 10 additions & 6 deletions packages/webui/webapp/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import * as api from "@/lib/api";
import { renderMarkdown } from "@/lib/markdown";
import { MarkdownHtml } from "./markdown-html";
import "../lib/mermaid-renderer"; // registers the mermaid language renderer
import { reportActionError } from "@/lib/action-errors";
import { findSubagentForBlock } from "@/lib/agent-team-lookup";
import { badgeLabelAndGlyph, agentLabel } from "@/lib/i18n-agent-team";
Expand Down Expand Up @@ -531,12 +533,14 @@ function MarkdownBody({ text, streaming }: { text: string; streaming?: boolean }
const html = useMemo(() => renderMarkdown(text), [text]);
return (
<div className="matrix-markdown message-content relative max-w-full flex-1 overflow-hidden text-pretty">
<div
className="matrix-markdown matrix-markdown--shifted mavis-chat-markdown-flow"
// Sanitised by lib/markdown.ts: only a small allowlist of tags and
// attributes survives, and only http(s)/mailto/#/relative hrefs.
dangerouslySetInnerHTML={{ __html: html }}
/>
<div className="matrix-markdown matrix-markdown--shifted mavis-chat-markdown-flow">
{/* Slice 23 — MarkdownHtml walks the rendered DOM, finds the
mermaid-block placeholders, and mounts the lazy mermaid
component into each one. The parser seam
(`registerLanguageRenderer` in lib/markdown.ts) means
third-party diagram renderers can attach here too. */}
<MarkdownHtml html={html} />
</div>
{streaming ? (
<span className="ml-[2px] inline-block animate-pulse text-text_default_accent">▍</span>
) : null}
Expand Down
19 changes: 13 additions & 6 deletions packages/webui/webapp/components/file-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
type FsFilePayload,
} from "@/lib/api";
import { renderMarkdown } from "@/lib/markdown";
import "@/lib/mermaid-renderer"; // registers the mermaid language renderer
import { MarkdownHtml } from "@/components/markdown-html";
import { CodeView as IdeCodeView } from "@/components/code-view";
import {
basenameOf,
Expand Down Expand Up @@ -295,14 +297,19 @@ function MarkdownView({ content }: { content: string }) {
// lib/markdown.ts#sanitize) — the same policy used by chat.tsx for
// assistant output. Reusing it keeps the threat model and allow-list
// identical across surfaces.
//
// Slice 23 — the markdown is no longer injected directly. The
// MarkdownHtml component walks the rendered DOM, finds the
// mermaid-block placeholders, and mounts the lazy mermaid
// component into each one. The seam (the language → renderer
// registry in lib/markdown.ts) means the renderer used here is
// plugin-extensible without an `if (lang === "mermaid")` inside
// this component.
const html = useMemo(() => renderMarkdown(content), [content]);
return (
<div
className="file-preview-markdown"
data-testid="file-preview-markdown"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: html }}
/>
<div className="file-preview-markdown" data-testid="file-preview-markdown">
<MarkdownHtml html={html} />
</div>
);
}

Expand Down
Loading
Loading