* fix(ui): update session-switch-resync test to current handleEvent/setSessionTodos signatures
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(build): split Shiki grammars/themes, CodeMirror legacy modes, and @pierre/diffs into on-demand chunks
Merging @shikijs/langs into one manual vendor chunk made the first language
request download every grammar (7.4 MB raw / 1 MB gzip). Letting Rollup split
these packages per dynamically imported module downloads only the languages,
themes, and modes actually used — matching how the worker build already
behaves. @pierre/diffs is split the same way so its pure patch parser (used by
the eager tool renderer) no longer drags the Shiki-importing render stack into
the startup graph.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(ui): restore lazy heavy views and stop eager settings-graph loading
- MainLayout: DiffView/FilesView/GitView/PlanView return to lazyWithChunkRecovery
(they were silently made static in 2031e3b4 while their Suspense wrappers
remained), keeping the CodeMirror and @pierre/diffs stacks out of startup.
- ContextPanel: same lazy treatment for its Diff/Files/Git/Plan/Walkthrough
tabs, with null Suspense fallbacks.
- CommandPalette imported getSettingsNavIcon from SettingsView, statically
pulling the entire settings surface (SkillsPage -> CodeMirrorEditor -> vim
mode, theme registry -> @pierre/diffs) into the eager graph; the helper now
lives in lib/settings/metadata.
- The windowed SettingsWindow mounts only after its first open: rendering the
lazy component closed made React fetch the SettingsView chunk graph at
startup.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(ui): keep @pierre/diffs + Shiki out of the eager chat graph and defer diff worker warmup
- DiffWorkerProvider no longer statically imports @pierre/diffs/worker or the
theme registry, and no longer spawns 3 workers plus a main-thread shared
highlighter during mount. Pools are created on demand through a dynamic
module load, warmed via requestIdleCallback after startup settles, and
useWorkerPool notifies consumers when a pool becomes available.
- ToolPart's rich diff preview moves to lazily loaded ToolPartDiffPreview;
the plain-text patch (PlainDiffFallback) renders while the chunk loads,
mirroring the existing error fallback. Theme registration happens during
render inside the lazy module so PatchDiff never renders unregistered ids.
- ChatInput mounts its lazy ToolOutputDialog only after the first attachment
preview opens instead of fetching the dialog chunk on the draft screen.
- getMarkdownSyntaxVars moves to a pierre-free markdownSyntaxVars module so
eager code-rendering consumers stop importing the registration module.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(web): load ghostty-web and Nerd Fonts on first terminal use
- ghostty-web (638 KB raw JS + WASM VT) is dynamically imported when a
terminal actually mounts; TerminalView stays eagerly importable for the
bottom dock.
- The ~2 MB of CDN Nerd Fonts are no longer preloaded and force-loaded on
every cold start. index.html exposes an idempotent
__openchamberEnsureNerdFonts hook; TerminalViewport requests it on mount
and waits up to 2s so a cached font is in place before the glyph atlas is
built, while a cold CDN fetch never blocks the terminal. Runtimes without
the hook (VS Code, mobile) resolve immediately, matching their existing
fallback-font behavior.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
---------
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
import React from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
|
import { getMarkdownSyntaxVars } from '@/components/chat/markdown/markdownSyntaxVars';
|
|
import { highlightCodeInWorker } from '@/components/chat/markdown/markdown-worker';
|
|
|
|
// Shared static code highlighter backed by the markdown Shiki Web Worker.
|
|
//
|
|
// Replaces `react-syntax-highlighter` for non-streaming code surfaces (tool
|
|
// output, permission previews, diffs, sidebar file contents). The escaped code
|
|
// paints synchronously; the worker tokenizes off the main thread and swaps in
|
|
// the highlighted markup. Colors resolve through the `--md-syntax-*` CSS
|
|
// variables on the host, so theme changes never require re-highlighting.
|
|
|
|
const escapeHtml = (text: string): string =>
|
|
text
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
|
|
const styleString = (style?: React.CSSProperties): string => {
|
|
if (!style) return '';
|
|
return Object.entries(style)
|
|
.map(([key, value]) => {
|
|
if (value == null) return '';
|
|
const prop = key.startsWith('--') ? key : key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
return `${prop}:${typeof value === 'number' ? `${value}px` : value};`;
|
|
})
|
|
.join('');
|
|
};
|
|
|
|
// Normalize a worker/plain `<pre><code>` so it sits flush inside the host: drop
|
|
// Shiki's own background/margin and apply wrap + caller code styles.
|
|
const applyPreStyles = (host: HTMLElement, wrap: boolean, codeStyle?: React.CSSProperties): void => {
|
|
const pre = host.querySelector('pre');
|
|
if (pre) {
|
|
pre.style.margin = '0';
|
|
pre.style.background = 'transparent';
|
|
pre.style.whiteSpace = wrap ? 'pre-wrap' : 'pre';
|
|
if (wrap) {
|
|
pre.style.wordBreak = 'break-word';
|
|
pre.style.overflowWrap = 'break-word';
|
|
}
|
|
}
|
|
const code = host.querySelector('code');
|
|
if (code) {
|
|
const extra = styleString(codeStyle);
|
|
if (extra) code.setAttribute('style', `${code.getAttribute('style') ?? ''}${extra}`);
|
|
}
|
|
};
|
|
|
|
const plainHtml = (code: string): string => `<pre><code>${escapeHtml(code)}</code></pre>`;
|
|
|
|
export interface WorkerHighlightedCodeProps {
|
|
code: string;
|
|
language: string;
|
|
className?: string;
|
|
/** Inline styles for the host container (mirrors react-syntax-highlighter `customStyle`). */
|
|
style?: React.CSSProperties;
|
|
/** Inline styles applied to the `<code>` element (mirrors `codeTagProps.style`). */
|
|
codeStyle?: React.CSSProperties;
|
|
/** Wrap long lines instead of horizontal scroll. */
|
|
wrap?: boolean;
|
|
}
|
|
|
|
export const WorkerHighlightedCode: React.FC<WorkerHighlightedCodeProps> = ({
|
|
code,
|
|
language,
|
|
className,
|
|
style,
|
|
codeStyle,
|
|
wrap = false,
|
|
}) => {
|
|
const { currentTheme } = useThemeSystem();
|
|
const hostRef = React.useRef<HTMLDivElement>(null);
|
|
const syntaxVars = React.useMemo(() => getMarkdownSyntaxVars(currentTheme), [currentTheme]);
|
|
|
|
// Synchronous escaped first paint — no blank frame before highlighting lands.
|
|
React.useLayoutEffect(() => {
|
|
const host = hostRef.current;
|
|
if (!host) return;
|
|
host.innerHTML = plainHtml(code);
|
|
applyPreStyles(host, wrap, codeStyle);
|
|
}, [code, wrap, codeStyle]);
|
|
|
|
// Highlight off the main thread, then swap in. Guarded against stale results.
|
|
React.useEffect(() => {
|
|
const host = hostRef.current;
|
|
if (!host) return;
|
|
let active = true;
|
|
void highlightCodeInWorker(code, (language || 'text').toLowerCase()).then((html) => {
|
|
if (!active || !host || !html) return;
|
|
host.innerHTML = html;
|
|
applyPreStyles(host, wrap, codeStyle);
|
|
});
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [code, language, wrap, codeStyle]);
|
|
|
|
return (
|
|
<div
|
|
ref={hostRef}
|
|
className={cn('typography-code', className)}
|
|
style={{ ...(syntaxVars as React.CSSProperties), ...style }}
|
|
/>
|
|
);
|
|
};
|