Responding to ds-review-bot round 2 on #662: - LANG_ALIASES is a Map: an assistant-authored fence label like constructor or __proto__ now misses (plain render) instead of resolving an inherited object property and crashing shiki mid-conversation. Test sweeps the inherited-key labels. - The singleton is pre-warmed in a deferred task at plugin boot (the ~120-175ms engine+grammar construction long task moves off the first finalized fence's render); the lazy path remains the correctness fallback, and unref keeps non-browser imports from pinning the loop. Agent Note updated (both languages).
4.8 KiB
Agent Note: Web client syntax highlighting — synchronous fine-grained shiki
Status: implemented
English | 中文
Scope: the web client's one syntax-highlighting system — the dependency ruling, the singleton shape, the token-sheet contract, and the consuming surfaces. Fifth PR of the Code Mode UI stack; the chat sub-call rows note shipped the
run_codeprogram body this exists to make readable. Styling ground rules are owned by the web styling ruling.
Problem
The client rendered every code surface — markdown fences in assistant prose, the run_code program body, the details panel's args — as flat monospace text. The stack's primary payload is model-written TypeScript; unhighlighted programs are measurably harder to scan, and the repo already ships shiki-highlighted code on its VitePress site, so the web app was the one code-rendering surface without it.
Decision
Shiki in its synchronous fine-grained form, as one ui-primitives singleton, themed exclusively through CSS custom properties.
- Dependency:
shiki/core+@shikijs/langs, composed viacreateHighlighterCoreSyncwithcreateJavaScriptRegexEngine({ forgiving: true })— no oniguruma WASM, no async init, bundle-friendly. Grammar allowlist:typescript(embeds JS),shellscript,json— the languages the harness actually renders; everything else falls back to a geometry-identical plain block, never an error. Prior art: the VitePress site already renders all documentation code through shiki, and TextMate grammars materially beat regex highlighters on TypeScript — the payload that matters here. - Singleton:
ui-primitives/src/markdown/highlight.tscreates oneHighlighterCoreper document and exposeshighlightToHtml(code, lang)(undefined = render plain). Engine + grammar construction is a ~120-175ms long task, so the module pre-warms the singleton in a deferred task at plugin boot (the lazy path stays as the correctness fallback), keeping the cost off the render path where a stream's finalize swap would jank. The alias table is aMap, not an object: fence info strings are assistant-authored, so a label likeconstructormust miss instead of resolving an inherited property and crashing shiki. The sharedCodeBlockcomponent owns both arms; its shiki arm injects the generated span tree viadangerouslySetInnerHTML— sanctioned because shiki emits a static span tree computed from the code text (no user HTML passes through, no scripts/handlers), shiki's own documented consumption path. - Theming: shiki's
createCssVariablesThemeroutes every token color through--shiki-*custom properties; the VALUES live in a newui-theme/styles/shiki.csstoken sheet (light on:root, dark onbody[data-ds-dark-theme]— the same cascade as every other sheet), imported by the shell'sbase.csschain. Component CSS stays tokens-only; no literal color ever enters JS or component sheets. Background/foreground alias the existing markdown code-block tokens so highlighted and plain blocks agree. - Surfaces: markdown fences (
MarkdownText'sprecomponent routes single-string fences throughCodeBlock), therun_codeexpanded program body (ToolRow's code variant,lang="typescript"), and the details panel's Input args (lang="json"). Output stays plain deliberately — tool output is arbitrary text, and guessing a grammar would mis-highlight more than it helps.
Alternatives considered
rehype-highlight/lowlight. Runner-up: naturally sync and ~⅓ the bundle, but regex-grammar fidelity on TypeScript is visibly worse, and the repo would then run two highlighter systems (site: shiki, app: highlight.js) with two theming vocabularies.
Full shiki bundle or the oniguruma WASM engine. Rejected: the full bundle ships every grammar/theme; WASM needs async loading the sync client boot deliberately avoids. The fine-grained core with three grammars keeps the cost proportional to actual use.
Highlight in a worker / async. Rejected: the payloads are small (programs, fences, args); the synchronous JS engine tokenizes them in microseconds, and async introduces a flash-of-unhighlighted-code plus render-machinery churn for no measured need.
Consequences
One code surface for every consumer — a future surface imports CodeBlock and inherits highlighting, theming, and the plain fallback. The bundle grows by the shiki core + three grammars (paid once in ui-primitives). Token colors are the first --shiki-* sheet; a theme package registering alias overrides extends them like any other token. jsdom specs pin the token-span structure, alias resolution, both fallback arms, and the fence route; the existing built-bundle snapshot and browser e2e cover the assembled path.