Skip to content

Commit faec32c

Browse files
author
Grapedge
authored
feat(tui): render mermaid code blocks as diagrams in the terminal (#3851)
1 parent 95d4a9d commit faec32c

32 files changed

Lines changed: 1557 additions & 29 deletions

‎.agents/skills/write-tui/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ For any list dialog, selector, input box, or status/toggle list, the interaction
2323
- `editor-keyboard.ts` — editor keyboard handling, exit shortcuts, external editor, clipboard image.
2424
- `auth-flow.ts` — login/auth orchestration (`refreshConfigAfterLogin`, etc.).
2525
- `src/tui/commands/` — slash-command declaration, parsing, ordering, and dynamic skill-command generation. Parsing and types only; execution is dispatched from `KimiTUI`'s slash-command handler section, and complex execution sinks into `utils` or focused components.
26-
- `src/tui/components/` — pi-tui components by UI type: `chrome/` (footer, todo, welcome, loader, device code), `dialogs/` (selectors, approval/question panels, settings popups that replace the editor), `editor/` (input box + mention provider), `media/` (image, diff, code highlight), `messages/` (transcript blocks + tool-renderers), `panes/` (activity, queue).
26+
- `src/tui/components/` — pi-tui components by UI type: `chrome/` (footer, todo, welcome, loader, device code), `dialogs/` (selectors, approval/question panels, settings popups that replace the editor), `editor/` (input box + mention provider), `media/` (image, diff, code highlight), `markdown/` (the app's Markdown wrapper and mermaid art), `messages/` (transcript blocks + tool-renderers), `panes/` (activity, queue).
2727
- `src/tui/reverse-rpc/` — adapts SDK approval/question callbacks into UI panel data and the user's choice back into an SDK response.
2828
- `src/tui/theme/` — themes, color tokens, style helpers, pi-tui markdown theme, terminal-background detection. The single source of truth for color.
2929
- `src/tui/utils/` — TUI-only utilities (need `TUIState` or a component). App-wide, UI-independent helpers go in `src/utils/`.

‎.changeset/tui-mermaid-diagrams.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": minor
3+
---
4+
5+
Render mermaid code blocks as diagrams in the terminal; turn it off under /settings → Mermaid diagrams, or set `mermaid = "off"` in the `[markdown]` section of tui.toml.

‎apps/kimi-code/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
"cli-highlight": "^2.1.11",
103103
"commander": "^13.1.0",
104104
"jimp": "^1.6.1",
105+
"lovely-mermaid": "0.3.3",
105106
"pathe": "^2.0.3",
106107
"postject": "1.0.0-alpha.6",
107108
"semver": "^7.7.4",

‎apps/kimi-code/src/tui/commands/config.ts‎

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@ import {
1515
ExperimentsSelectorComponent,
1616
type ExperimentalFeatureDraftChange,
1717
} from '../components/dialogs/experiments-selector';
18+
import { MermaidPreferenceSelectorComponent } from '../components/dialogs/mermaid-preference-selector';
1819
import { modelDisplayName, segmentsFor } from '../components/dialogs/model-selector';
1920
import { TabbedModelSelectorComponent } from '../components/dialogs/tabbed-model-selector';
2021
import { PermissionSelectorComponent } from '../components/dialogs/permission-selector';
2122
import { SettingsSelectorComponent, type SettingsSelection } from '../components/dialogs/settings-selector';
2223
import { SurveyPreferenceSelectorComponent } from '../components/dialogs/survey-preference-selector';
2324
import { ThemeSelectorComponent } from '../components/dialogs/theme-selector';
2425
import { UpdatePreferenceSelectorComponent } from '../components/dialogs/update-preference-selector';
25-
import { DEFAULT_TUI_CONFIG, saveTuiConfig, type TuiConfig } from '../config';
26+
import { DEFAULT_MARKDOWN_CONFIG, DEFAULT_TUI_CONFIG, saveTuiConfig, type MarkdownConfig, type TuiConfig } from '../config';
2627
import type { ThemeName } from '#/tui/theme';
2728
import { currentTheme, isBuiltInTheme, lightColors, loadCustomThemeMerged } from '#/tui/theme';
2829
import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui';
2930
import { formatErrorMessage } from '../utils/event-payload';
31+
import { setMarkdownMermaidMode, type MermaidRenderMode } from '../utils/markdown-options';
3032
import { PERMISSION_MODE_DESCRIPTIONS, PERMISSION_MODE_DISPLAY_NAMES } from '../utils/permission-mode';
3133
import { thinkingEffortToConfig } from '../utils/thinking-config';
3234
import { showUsage } from './info';
@@ -66,6 +68,7 @@ export function currentTuiConfig(host: Pick<SlashCommandHost, 'state'>): TuiConf
6668
notifications: host.state.appState.notifications,
6769
upgrade: host.state.appState.upgrade,
6870
statusLine: host.state.appState.statusLine ?? DEFAULT_TUI_CONFIG.statusLine,
71+
markdown: host.state.appState.markdown ?? DEFAULT_MARKDOWN_CONFIG,
6972
};
7073
}
7174

@@ -884,6 +887,65 @@ export async function applySurveyPreferenceChoice(
884887
host.showStatus(`Feedback survey ${enabled ? 'enabled' : 'disabled'}.`);
885888
}
886889

890+
export function showMermaidPreferencePicker(host: SlashCommandHost): void {
891+
host.mountEditorReplacement(
892+
new MermaidPreferenceSelectorComponent({
893+
currentValue: host.state.appState.markdown?.mermaid !== 'off',
894+
onSelect: (value) => {
895+
host.restoreEditor();
896+
void applyMermaidPreferenceChoice(host, value);
897+
},
898+
onCancel: () => {
899+
host.restoreEditor();
900+
},
901+
}),
902+
);
903+
}
904+
905+
type MermaidPreferenceHost = {
906+
readonly state: {
907+
readonly appState: Pick<
908+
SlashCommandHost['state']['appState'],
909+
'theme' | 'editorCommand' | 'notifications' | 'upgrade' | 'markdown'
910+
>;
911+
readonly transcriptContainer: { invalidate(): void };
912+
readonly ui: { requestRender(force?: boolean): void };
913+
};
914+
setAppState(patch: Pick<SlashCommandHost['state']['appState'], 'markdown'>): void;
915+
showStatus(msg: string, color?: string): void;
916+
};
917+
918+
export async function applyMermaidPreferenceChoice(
919+
host: MermaidPreferenceHost,
920+
enabled: boolean,
921+
): Promise<void> {
922+
const mermaid: MermaidRenderMode = enabled ? 'final' : 'off';
923+
if (mermaid === (host.state.appState.markdown?.mermaid ?? DEFAULT_MARKDOWN_CONFIG.mermaid)) {
924+
host.showStatus(`Mermaid diagrams already ${enabled ? 'enabled' : 'disabled'}.`);
925+
return;
926+
}
927+
928+
const markdown: MarkdownConfig = { mermaid };
929+
try {
930+
await saveTuiConfig({
931+
...currentTuiConfig(host as unknown as SlashCommandHost),
932+
markdown,
933+
});
934+
} catch (error) {
935+
host.showStatus(
936+
`Failed to save mermaid diagram setting: ${formatErrorMessage(error)}`,
937+
'error',
938+
);
939+
return;
940+
}
941+
942+
setMarkdownMermaidMode(mermaid);
943+
host.setAppState({ markdown });
944+
host.state.transcriptContainer.invalidate();
945+
host.state.ui.requestRender(true);
946+
host.showStatus(`Mermaid diagrams ${enabled ? 'enabled' : 'disabled'}.`);
947+
}
948+
887949
export function showSettingsSelector(host: SlashCommandHost): void {
888950
host.mountEditorReplacement(
889951
new SettingsSelectorComponent({
@@ -903,6 +965,7 @@ function handleSettingsSelection(host: SlashCommandHost, value: SettingsSelectio
903965
case 'model': showModelPicker(host); return;
904966
case 'permission': showPermissionPicker(host); return;
905967
case 'theme': showThemePicker(host); return;
968+
case 'mermaid': showMermaidPreferencePicker(host); return;
906969
case 'editor': showEditorPicker(host); return;
907970
case 'survey': showSurveyPreferencePicker(host); return;
908971
case 'experiments': void showExperimentsPanel(host); return;

‎apps/kimi-code/src/tui/commands/plugins.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import {
88
type PluginSummary,
99
type Session,
1010
} from '@moonshot-ai/kimi-code-sdk';
11-
import { Markdown, Spacer } from '@moonshot-ai/pi-tui';
11+
import { Spacer } from '@moonshot-ai/pi-tui';
12+
13+
import { Markdown } from '#/tui/components/markdown/markdown';
1214

1315
import {
1416
PluginInstallTrustConfirmComponent,
@@ -592,7 +594,10 @@ async function installCapabilityFromPanel(
592594
host.showNotice(`${label} is installed.`);
593595
host.state.transcriptContainer.addChild(new Spacer(1));
594596
host.state.transcriptContainer.addChild(
595-
new Markdown(WEBBRIDGE_POST_INSTALL_MARKDOWN, 2, 0, createMarkdownTheme(), undefined, createMarkdownOptions()),
597+
new Markdown(WEBBRIDGE_POST_INSTALL_MARKDOWN, 2, 0, createMarkdownTheme(), undefined, {
598+
...createMarkdownOptions(),
599+
copySource: true,
600+
}),
596601
);
597602
host.state.ui.requestRender();
598603
return;

‎apps/kimi-code/src/tui/commands/reload.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { KimiConfig } from '@moonshot-ai/kimi-code-sdk';
22

33
import { currentTheme, lightColors } from '#/tui/theme';
4-
import { loadTuiConfig, type TuiConfig } from '../config';
5-
import { setMarkdownRenderLatex } from '../utils/markdown-options';
4+
import { DEFAULT_MARKDOWN_CONFIG, loadTuiConfig, type TuiConfig } from '../config';
5+
import { setMarkdownMermaidMode, setMarkdownRenderLatex } from '../utils/markdown-options';
66
import type { SlashCommandHost } from './dispatch';
77
import { setExperimentalFeatures } from './experimental-flags';
88

@@ -60,6 +60,7 @@ export async function applyReloadedTuiConfig(
6060
// transcript components, which rebuild their Markdown children and copy the
6161
// options at construction — so the new value must be live by then.
6262
setMarkdownRenderLatex(config.renderLatex ?? true);
63+
setMarkdownMermaidMode(config.markdown?.mermaid ?? DEFAULT_MARKDOWN_CONFIG.mermaid);
6364
const resolved = config.theme === 'auto'
6465
? (currentTheme.palette === lightColors ? 'light' : 'dark')
6566
: undefined;
@@ -74,6 +75,7 @@ export async function applyReloadedTuiConfig(
7475
notifications: config.notifications,
7576
upgrade: config.upgrade,
7677
statusLine: config.statusLine,
78+
markdown: config.markdown,
7779
});
7880
host.state.editor.setDisablePasteBurst(config.disablePasteBurst);
7981
}

‎apps/kimi-code/src/tui/components/chrome/notify-panel.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
*/
1515

1616
import type { Component } from '@moonshot-ai/pi-tui';
17-
import { Markdown, truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui';
17+
import { truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui';
1818
import chalk from 'chalk';
1919

20+
import { Markdown } from '#/tui/components/markdown/markdown';
21+
2022
import { MAIN_AGENT_ID } from '#/tui/constant/kimi-tui';
2123
import { currentTheme } from '#/tui/theme';
2224
import { createMarkdownTheme } from '#/tui/theme/pi-tui-theme';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ChoicePickerComponent, type ChoiceOption } from './choice-picker';
2+
3+
const MERMAID_PREFERENCE_OPTIONS: readonly ChoiceOption[] = [
4+
{
5+
value: 'on',
6+
label: 'On',
7+
description: 'Draw mermaid code blocks as diagrams in the terminal.',
8+
},
9+
{
10+
value: 'off',
11+
label: 'Off',
12+
description: 'Keep mermaid code blocks as highlighted source.',
13+
},
14+
];
15+
16+
export interface MermaidPreferenceSelectorOptions {
17+
readonly currentValue: boolean;
18+
readonly onSelect: (value: boolean) => void;
19+
readonly onCancel: () => void;
20+
}
21+
22+
export class MermaidPreferenceSelectorComponent extends ChoicePickerComponent {
23+
constructor(opts: MermaidPreferenceSelectorOptions) {
24+
super({
25+
title: 'Mermaid diagrams',
26+
options: [...MERMAID_PREFERENCE_OPTIONS],
27+
currentValue: opts.currentValue ? 'on' : 'off',
28+
onSelect: (value) => {
29+
opts.onSelect(value === 'on');
30+
},
31+
onCancel: opts.onCancel,
32+
});
33+
}
34+
}

‎apps/kimi-code/src/tui/components/dialogs/settings-selector.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ChoicePickerComponent, type ChoiceOption } from './choice-picker';
33
export type SettingsSelection =
44
| 'model'
55
| 'theme'
6+
| 'mermaid'
67
| 'editor'
78
| 'permission'
89
| 'survey'
@@ -26,6 +27,11 @@ const SETTINGS_OPTIONS: readonly ChoiceOption[] = [
2627
label: 'Theme',
2728
description: 'Change the terminal UI theme.',
2829
},
30+
{
31+
value: 'mermaid',
32+
label: 'Mermaid diagrams',
33+
description: 'Draw mermaid code blocks as diagrams, or keep them as source.',
34+
},
2935
{
3036
value: 'editor',
3137
label: 'Editor',
@@ -57,6 +63,7 @@ function isSettingsSelection(value: string): value is SettingsSelection {
5763
return (
5864
value === 'model' ||
5965
value === 'theme' ||
66+
value === 'mermaid' ||
6067
value === 'editor' ||
6168
value === 'permission' ||
6269
value === 'survey' ||

0 commit comments

Comments
 (0)