From 7f4d112a635a88c548decb3f5f08f331201768c3 Mon Sep 17 00:00:00 2001 From: tang <12411175+thanning@user.noreply.gitee.com> Date: Sun, 16 Aug 2026 23:12:36 +0800 Subject: [PATCH] fix: follow DSH shell dark theme for code blocks and Mermaid MarkstreamMarkdown never passed isDark to MarkdownRender, so markstream defaulted to its light theme regardless of the page appearance: Shiki code blocks rendered with a white background on dark pages and Mermaid diagrams used light-theme colors. Add useDshIsDark(), which resolves the shell's current color scheme via the computed --dsw-alias-bg-base token luminance (fallbacks: the data-ds-dark-theme attribute, then prefers-color-scheme) and re-renders on theme switches through a debounced MutationObserver. Verified in DSH 0.1.0-rc.6 web profile with a patched bundle before upstreaming. --- src/client/renderer.tsx | 55 +++++++++++++++++++++++++++++++++++++++++ tests/plugin.spec.tsx | 17 +++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/client/renderer.tsx b/src/client/renderer.tsx index 4738e35..239a270 100644 --- a/src/client/renderer.tsx +++ b/src/client/renderer.tsx @@ -92,12 +92,66 @@ export function DshCodeBlockNode({ node, ctx }: NodeComponentProps`, then the OS preference. + */ +function detectDshDark(): boolean { + try { + const value = getComputedStyle(document.body).getPropertyValue('--dsw-alias-bg-base').trim() + if (value !== '') { + const match = /(?:rgba?\(\s*)?(\d+)[,\s]+(\d+)[,\s]+(\d+)/.exec(value) + if (match !== null) { + return 0.2126 * Number(match[1]) + 0.7152 * Number(match[2]) + 0.0722 * Number(match[3]) < DARK_BG_LUMINANCE + } + } + } catch { /* document body not available yet */ } + try { + if (document.body.hasAttribute('data-ds-dark-theme')) return true + } catch { /* ignore */ } + try { + return window.matchMedia('(prefers-color-scheme: dark)').matches + } catch { + return false + } +} + +/** Reactive DSH dark-mode state; re-renders when the shell flips theme tokens or attributes. */ +export function useDshIsDark(): boolean { + const [dark, setDark] = useState(detectDshDark) + useEffect(() => { + let timer: ReturnType | undefined + const schedule = () => { + if (timer !== undefined) clearTimeout(timer) + timer = setTimeout(() => setDark(detectDshDark()), 80) + } + try { + const observer = new MutationObserver(schedule) + const options: MutationObserverInit = { attributes: true, attributeFilter: ['data-ds-dark-theme', 'style'] } + observer.observe(document.documentElement, options) + observer.observe(document.body, options) + return () => { + if (timer !== undefined) clearTimeout(timer) + observer.disconnect() + } + } catch { /* MutationObserver unavailable */ } + return undefined + }, []) + return dark +} + /** Markstream wrapper configured for untrusted assistant output. */ export const MarkstreamMarkdown = memo(function MarkstreamMarkdown({ text, streaming, fileMentions }: { text: string streaming: boolean fileMentions?: MarkdownFileMentions | undefined }) { + const isDark = useDshIsDark() const codeBlockProps = useMemo(() => ({ fileMentions: streaming ? undefined : fileMentions, }), [fileMentions, streaming]) @@ -106,6 +160,7 @@ export const MarkstreamMarkdown = memo(function MarkstreamMarkdown({ text, strea { expect(view.container.querySelector('.md-code-block')).toBeNull() plugin.dispose() }) + + it('follows the DSH shell dark theme and reacts to live switches', async () => { + const plugin = mountPlugin() + document.body.setAttribute('data-ds-dark-theme', '') + const view = render( + , + ) + await waitFor(() => { + expect(view.container.querySelector('.markstream-react.dark')).not.toBeNull() + }) + document.body.removeAttribute('data-ds-dark-theme') + await waitFor(() => { + expect(view.container.querySelector('.markstream-react.dark')).toBeNull() + }) + view.unmount() + plugin.dispose() + }) })