=> {
continue;
}
- let lang = requested;
-
- if (lang !== 'text' && !highlighter.getLoadedLanguages().includes(lang)) {
- try {
- await highlighter.loadLanguage(lang as SupportedLanguages);
- } catch {
- lang = 'text';
- }
- }
-
- try {
- const highlighted = highlighter
- .codeToHtml(code, {
- lang: lang as SupportedLanguages,
- theme: MARKDOWN_SHIKI_THEME as DiffsThemeNames,
- tabindex: false,
- })
- // Stamp the language so the decorate pass can show a header label.
- .replace(/^ highlighted);
- } catch {
- // Leave the original (escaped, sanitized) in place.
+ // Tokenize off the main thread. On failure the worker resolves to null and
+ // we keep the original escaped (no main-thread highlight).
+ const highlighted = await highlightCodeInWorker(code, requested);
+ if (highlighted) {
+ // Stamp the language so the decorate pass can show a header label.
+ const stamped = highlighted.replace(/^ stamped);
}
}
diff --git a/packages/ui/src/components/chat/markdown/markdownShikiThemeDefinition.ts b/packages/ui/src/components/chat/markdown/markdownShikiThemeDefinition.ts
new file mode 100644
index 00000000..98ae1301
--- /dev/null
+++ b/packages/ui/src/components/chat/markdown/markdownShikiThemeDefinition.ts
@@ -0,0 +1,100 @@
+// Static, CSS-variable-driven Shiki theme definition.
+//
+// Token colors reference CSS custom properties (`--md-syntax-*`) instead of
+// concrete colors, so a highlighted code block does NOT need to be re-tokenized
+// when the app theme changes — only the CSS variables on the markdown container
+// update, and the browser repaints. This mirrors OpenCode's `var(--syntax-*)`
+// theme approach and keeps highlighting results cacheable across theme switches.
+//
+// This module is intentionally dependency-free (no `@pierre/diffs`, no React) so
+// it can be imported from inside the Shiki Web Worker bundle without dragging in
+// main-thread-only modules.
+
+export const MARKDOWN_SHIKI_THEME = 'openchamber-md';
+
+// Loosely typed on purpose: consumers (`@pierre/diffs` registration and the raw
+// Shiki worker) each cast to their own theme type. The shape is a standard
+// TextMate-style theme registration.
+export const MARKDOWN_SHIKI_THEME_DEFINITION = {
+ name: MARKDOWN_SHIKI_THEME,
+ colors: {
+ 'editor.background': 'transparent',
+ 'editor.foreground': 'var(--md-syntax-foreground)',
+ },
+ tokenColors: [
+ {
+ scope: ['comment', 'punctuation.definition.comment', 'string.comment'],
+ settings: { foreground: 'var(--md-syntax-comment)', fontStyle: 'italic' },
+ },
+ {
+ scope: ['string', 'punctuation.definition.string', 'string.template'],
+ settings: { foreground: 'var(--md-syntax-string)' },
+ },
+ {
+ scope: ['constant.numeric', 'constant.language', 'constant.character', 'constant'],
+ settings: { foreground: 'var(--md-syntax-number)' },
+ },
+ {
+ scope: ['keyword', 'storage', 'storage.type', 'storage.modifier', 'keyword.control'],
+ settings: { foreground: 'var(--md-syntax-keyword)' },
+ },
+ {
+ scope: ['keyword.operator', 'punctuation.separator', 'punctuation.terminator'],
+ settings: { foreground: 'var(--md-syntax-operator)' },
+ },
+ {
+ scope: ['entity.name.function', 'support.function', 'meta.function-call'],
+ settings: { foreground: 'var(--md-syntax-function)' },
+ },
+ {
+ scope: [
+ 'entity.name.type',
+ 'entity.name.class',
+ 'support.type',
+ 'support.class',
+ 'entity.other.inherited-class',
+ ],
+ settings: { foreground: 'var(--md-syntax-type)' },
+ },
+ {
+ scope: ['variable', 'variable.other', 'variable.parameter', 'meta.definition.variable'],
+ settings: { foreground: 'var(--md-syntax-variable)' },
+ },
+ {
+ scope: ['variable.other.property', 'meta.property-name', 'support.type.property-name'],
+ settings: { foreground: 'var(--md-syntax-property)' },
+ },
+ {
+ scope: ['entity.name.tag', 'punctuation.definition.tag'],
+ settings: { foreground: 'var(--md-syntax-keyword)' },
+ },
+ {
+ scope: ['entity.other.attribute-name'],
+ settings: { foreground: 'var(--md-syntax-property)' },
+ },
+ {
+ scope: ['markup.bold', 'punctuation.definition.bold'],
+ settings: { fontStyle: 'bold' },
+ },
+ {
+ scope: ['markup.italic', 'punctuation.definition.italic'],
+ settings: { fontStyle: 'italic' },
+ },
+ {
+ scope: ['markup.heading', 'markup.heading entity.name'],
+ settings: { foreground: 'var(--md-syntax-keyword)', fontStyle: 'bold' },
+ },
+ {
+ scope: ['markup.inserted', 'punctuation.definition.inserted'],
+ settings: { foreground: 'var(--md-syntax-inserted)' },
+ },
+ {
+ scope: ['markup.deleted', 'punctuation.definition.deleted'],
+ settings: { foreground: 'var(--md-syntax-deleted)' },
+ },
+ {
+ scope: ['invalid', 'invalid.illegal'],
+ settings: { foreground: 'var(--md-syntax-deleted)' },
+ },
+ ],
+} as const;
diff --git a/packages/ui/src/components/chat/markdown/markdownTheme.ts b/packages/ui/src/components/chat/markdown/markdownTheme.ts
index fbdd370a..f78161cd 100644
--- a/packages/ui/src/components/chat/markdown/markdownTheme.ts
+++ b/packages/ui/src/components/chat/markdown/markdownTheme.ts
@@ -1,108 +1,30 @@
import { registerCustomTheme, type ThemeRegistrationResolved } from '@pierre/diffs';
import type { Theme } from '@/types/theme';
+import { MARKDOWN_SHIKI_THEME, MARKDOWN_SHIKI_THEME_DEFINITION } from './markdownShikiThemeDefinition';
-// Name of the static Shiki theme we register once. Its token colors reference
-// CSS custom properties (`--md-syntax-*`) instead of concrete colors, so a
-// highlighted code block does NOT need to be re-tokenized when the app theme
-// changes — only the CSS variables on the markdown container update, and the
-// browser repaints. This mirrors OpenCode's `var(--syntax-*)` theme approach
-// and keeps highlighting results cacheable across theme switches.
-export const MARKDOWN_SHIKI_THEME = 'openchamber-md';
+// The static Shiki theme name. Its definition (token colors referencing
+// `--md-syntax-*` CSS variables) lives in the dependency-free
+// `markdownShikiThemeDefinition` module so it can also be imported inside the
+// Shiki Web Worker. See that module for the rationale.
+export { MARKDOWN_SHIKI_THEME };
let registered = false;
/**
- * Register the static, CSS-variable-driven Shiki theme. Safe to call multiple
- * times; only the first call registers.
+ * Register the static, CSS-variable-driven Shiki theme with `@pierre/diffs`.
+ * Safe to call multiple times; only the first call registers.
+ *
+ * NOTE: markdown code highlighting now runs through the dedicated Shiki worker
+ * (`markdown-worker`), which uses the raw theme definition directly. This
+ * registration remains only for any `@pierre/diffs`-based consumer of the
+ * `openchamber-md` theme name.
*/
export const ensureMarkdownShikiTheme = (): void => {
if (registered) return;
registered = true;
registerCustomTheme(MARKDOWN_SHIKI_THEME, () =>
- Promise.resolve({
- name: MARKDOWN_SHIKI_THEME,
- colors: {
- 'editor.background': 'transparent',
- 'editor.foreground': 'var(--md-syntax-foreground)',
- },
- tokenColors: [
- {
- scope: ['comment', 'punctuation.definition.comment', 'string.comment'],
- settings: { foreground: 'var(--md-syntax-comment)', fontStyle: 'italic' },
- },
- {
- scope: ['string', 'punctuation.definition.string', 'string.template'],
- settings: { foreground: 'var(--md-syntax-string)' },
- },
- {
- scope: ['constant.numeric', 'constant.language', 'constant.character', 'constant'],
- settings: { foreground: 'var(--md-syntax-number)' },
- },
- {
- scope: ['keyword', 'storage', 'storage.type', 'storage.modifier', 'keyword.control'],
- settings: { foreground: 'var(--md-syntax-keyword)' },
- },
- {
- scope: ['keyword.operator', 'punctuation.separator', 'punctuation.terminator'],
- settings: { foreground: 'var(--md-syntax-operator)' },
- },
- {
- scope: ['entity.name.function', 'support.function', 'meta.function-call'],
- settings: { foreground: 'var(--md-syntax-function)' },
- },
- {
- scope: [
- 'entity.name.type',
- 'entity.name.class',
- 'support.type',
- 'support.class',
- 'entity.other.inherited-class',
- ],
- settings: { foreground: 'var(--md-syntax-type)' },
- },
- {
- scope: ['variable', 'variable.other', 'variable.parameter', 'meta.definition.variable'],
- settings: { foreground: 'var(--md-syntax-variable)' },
- },
- {
- scope: ['variable.other.property', 'meta.property-name', 'support.type.property-name'],
- settings: { foreground: 'var(--md-syntax-property)' },
- },
- {
- scope: ['entity.name.tag', 'punctuation.definition.tag'],
- settings: { foreground: 'var(--md-syntax-keyword)' },
- },
- {
- scope: ['entity.other.attribute-name'],
- settings: { foreground: 'var(--md-syntax-property)' },
- },
- {
- scope: ['markup.bold', 'punctuation.definition.bold'],
- settings: { fontStyle: 'bold' },
- },
- {
- scope: ['markup.italic', 'punctuation.definition.italic'],
- settings: { fontStyle: 'italic' },
- },
- {
- scope: ['markup.heading', 'markup.heading entity.name'],
- settings: { foreground: 'var(--md-syntax-keyword)', fontStyle: 'bold' },
- },
- {
- scope: ['markup.inserted', 'punctuation.definition.inserted'],
- settings: { foreground: 'var(--md-syntax-inserted)' },
- },
- {
- scope: ['markup.deleted', 'punctuation.definition.deleted'],
- settings: { foreground: 'var(--md-syntax-deleted)' },
- },
- {
- scope: ['invalid', 'invalid.illegal'],
- settings: { foreground: 'var(--md-syntax-deleted)' },
- },
- ],
- } as unknown as ThemeRegistrationResolved),
+ Promise.resolve(MARKDOWN_SHIKI_THEME_DEFINITION as unknown as ThemeRegistrationResolved),
);
};