mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
fix: theme-aware syntax highlighting in chat code blocks (#1479)
Inline chat code blocks hardcoded the github-dark Shiki theme, so in light mode dark-theme token colors rendered on a light bubble with poor contrast. Resolve the Shiki theme from the active light/dark mode and cache tokens per theme. Adds a reactive useResolvedTheme hook so blocks update live on toggle. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
parent
8f596a421d
commit
9475afdf2e
2 changed files with 33 additions and 4 deletions
|
|
@ -1,11 +1,14 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { getSingletonHighlighter, type ThemedToken } from "shiki";
|
||||
import { useResolvedTheme } from "@/lib/theme";
|
||||
|
||||
interface CodeBlockProps {
|
||||
text: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
const SHIKI_THEME = { light: "github-light", dark: "github-dark" } as const;
|
||||
|
||||
const TOKEN_CACHE = new Map<string, ThemedToken[][]>();
|
||||
|
||||
function normalizeLanguage(language?: string): string {
|
||||
|
|
@ -44,6 +47,8 @@ function languageLabel(language: string): string {
|
|||
export function CodeBlock({ text, language }: CodeBlockProps) {
|
||||
const [tokens, setTokens] = useState<ThemedToken[][] | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const resolvedTheme = useResolvedTheme();
|
||||
const shikiTheme = SHIKI_THEME[resolvedTheme];
|
||||
const normalizedLanguage = useMemo(() => normalizeLanguage(language), [language]);
|
||||
const displayLanguage = useMemo(() => languageLabel(normalizedLanguage), [normalizedLanguage]);
|
||||
|
||||
|
|
@ -53,7 +58,7 @@ export function CodeBlock({ text, language }: CodeBlockProps) {
|
|||
|
||||
if (normalizedLanguage === "text") return;
|
||||
|
||||
const cacheKey = `${normalizedLanguage}::${text}`;
|
||||
const cacheKey = `${shikiTheme}::${normalizedLanguage}::${text}`;
|
||||
const cached = TOKEN_CACHE.get(cacheKey);
|
||||
if (cached) {
|
||||
setTokens(cached);
|
||||
|
|
@ -61,7 +66,7 @@ export function CodeBlock({ text, language }: CodeBlockProps) {
|
|||
}
|
||||
|
||||
getSingletonHighlighter({
|
||||
themes: ["github-dark"],
|
||||
themes: [shikiTheme],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
langs: [normalizedLanguage as any],
|
||||
})
|
||||
|
|
@ -70,7 +75,7 @@ export function CodeBlock({ text, language }: CodeBlockProps) {
|
|||
const result = highlighter.codeToTokens(text, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
lang: normalizedLanguage as any,
|
||||
theme: "github-dark",
|
||||
theme: shikiTheme,
|
||||
});
|
||||
if (TOKEN_CACHE.size >= 500) TOKEN_CACHE.clear();
|
||||
TOKEN_CACHE.set(cacheKey, result.tokens);
|
||||
|
|
@ -86,7 +91,7 @@ export function CodeBlock({ text, language }: CodeBlockProps) {
|
|||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [text, normalizedLanguage]);
|
||||
}, [text, normalizedLanguage, shikiTheme]);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -73,3 +73,27 @@ export function useTheme() {
|
|||
|
||||
return { theme, resolvedTheme, setTheme, toggleTheme }
|
||||
}
|
||||
|
||||
function readDomResolvedTheme(): ResolvedTheme {
|
||||
if (typeof document === "undefined") return "light"
|
||||
return document.documentElement.classList.contains("dark") ? "dark" : "light"
|
||||
}
|
||||
|
||||
/** Reactive resolved theme that tracks the root `.dark` class set by `useTheme`. */
|
||||
export function useResolvedTheme(): ResolvedTheme {
|
||||
const [resolved, setResolved] = useState<ResolvedTheme>(readDomResolvedTheme)
|
||||
|
||||
useEffect(() => {
|
||||
setResolved(readDomResolvedTheme())
|
||||
const observer = new MutationObserver(() =>
|
||||
setResolved(readDomResolvedTheme())
|
||||
)
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
})
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue