mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-05 04:52:12 +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 { useEffect, useMemo, useState } from "react";
|
||||||
import { getSingletonHighlighter, type ThemedToken } from "shiki";
|
import { getSingletonHighlighter, type ThemedToken } from "shiki";
|
||||||
|
import { useResolvedTheme } from "@/lib/theme";
|
||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
text: string;
|
text: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SHIKI_THEME = { light: "github-light", dark: "github-dark" } as const;
|
||||||
|
|
||||||
const TOKEN_CACHE = new Map<string, ThemedToken[][]>();
|
const TOKEN_CACHE = new Map<string, ThemedToken[][]>();
|
||||||
|
|
||||||
function normalizeLanguage(language?: string): string {
|
function normalizeLanguage(language?: string): string {
|
||||||
|
|
@ -44,6 +47,8 @@ function languageLabel(language: string): string {
|
||||||
export function CodeBlock({ text, language }: CodeBlockProps) {
|
export function CodeBlock({ text, language }: CodeBlockProps) {
|
||||||
const [tokens, setTokens] = useState<ThemedToken[][] | null>(null);
|
const [tokens, setTokens] = useState<ThemedToken[][] | null>(null);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const resolvedTheme = useResolvedTheme();
|
||||||
|
const shikiTheme = SHIKI_THEME[resolvedTheme];
|
||||||
const normalizedLanguage = useMemo(() => normalizeLanguage(language), [language]);
|
const normalizedLanguage = useMemo(() => normalizeLanguage(language), [language]);
|
||||||
const displayLanguage = useMemo(() => languageLabel(normalizedLanguage), [normalizedLanguage]);
|
const displayLanguage = useMemo(() => languageLabel(normalizedLanguage), [normalizedLanguage]);
|
||||||
|
|
||||||
|
|
@ -53,7 +58,7 @@ export function CodeBlock({ text, language }: CodeBlockProps) {
|
||||||
|
|
||||||
if (normalizedLanguage === "text") return;
|
if (normalizedLanguage === "text") return;
|
||||||
|
|
||||||
const cacheKey = `${normalizedLanguage}::${text}`;
|
const cacheKey = `${shikiTheme}::${normalizedLanguage}::${text}`;
|
||||||
const cached = TOKEN_CACHE.get(cacheKey);
|
const cached = TOKEN_CACHE.get(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
setTokens(cached);
|
setTokens(cached);
|
||||||
|
|
@ -61,7 +66,7 @@ export function CodeBlock({ text, language }: CodeBlockProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSingletonHighlighter({
|
getSingletonHighlighter({
|
||||||
themes: ["github-dark"],
|
themes: [shikiTheme],
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
langs: [normalizedLanguage as any],
|
langs: [normalizedLanguage as any],
|
||||||
})
|
})
|
||||||
|
|
@ -70,7 +75,7 @@ export function CodeBlock({ text, language }: CodeBlockProps) {
|
||||||
const result = highlighter.codeToTokens(text, {
|
const result = highlighter.codeToTokens(text, {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
lang: normalizedLanguage as any,
|
lang: normalizedLanguage as any,
|
||||||
theme: "github-dark",
|
theme: shikiTheme,
|
||||||
});
|
});
|
||||||
if (TOKEN_CACHE.size >= 500) TOKEN_CACHE.clear();
|
if (TOKEN_CACHE.size >= 500) TOKEN_CACHE.clear();
|
||||||
TOKEN_CACHE.set(cacheKey, result.tokens);
|
TOKEN_CACHE.set(cacheKey, result.tokens);
|
||||||
|
|
@ -86,7 +91,7 @@ export function CodeBlock({ text, language }: CodeBlockProps) {
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [text, normalizedLanguage]);
|
}, [text, normalizedLanguage, shikiTheme]);
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -73,3 +73,27 @@ export function useTheme() {
|
||||||
|
|
||||||
return { theme, resolvedTheme, setTheme, toggleTheme }
|
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