* perf: drastically improve cold-start, bundle size, and streaming performance
Cold-start optimizations:
- main.tsx: Remove blocking await on prefs I/O — render immediately with
defaults, hydrate persisted settings asynchronously. Cuts 50-200ms from
time-to-first-paint.
- bootstrap.ts: Split directory bootstrap into 3 phases:
* Phase 1 (blocking): path, config, provider, session status — minimum
data needed to render UI. Mark status complete after this phase.
path.get and session.status must both succeed; they have no fallback.
* Phase 2 (deferred): agents, commands, mcp, lsp, vcs, questions,
permissions — fetched after first paint without blocking.
* Phase 3 (lazy): session messages — loaded without blocking init.
- App.tsx: Keep identical provider tree before/after init to prevent
full subtree remount when isInitialized flips. FireworksProvider and
VoiceProvider are lightweight shells; overlays deferred until init.
Bundle-size optimizations:
- App.tsx + MainLayout.tsx + VSCodeLayout.tsx: Code-split heavy views
(SettingsView, GitView, DiffView, TerminalView, FilesView, PlanView,
OnboardingScreen, SettingsWindow, MultiRunWindow) with React.lazy.
Views load on demand when user switches panels.
- vite.config.ts: Lower chunkSizeWarningLimit from 1200KB to 500KB.
Streaming render optimizations:
- streaming.ts: Throttle streaming store writes ~60Hz → ~1Hz. Busy-session
only scan (Set, O(1)).
- MessageList.tsx: Lower virtualization threshold 40 → 15.
- ChatMessage.tsx: React.memo with areRenderRelevantMessagesEqual.
- MarkdownRenderer.tsx: React.memo with explicit prop comparators.
* fix: address Greptile review feedback on bootstrap and provider tree
- bootstrap.ts: Tighten Phase 1 error guard. path.get and session.status
must both succeed; they have no global fallback.
- bootstrap.ts: Replace dead .catch() on Promise.allSettled() with .then()
that inspects individual results for errors.
- App.tsx: Keep identical provider tree before/after init to prevent full
subtree remount when isInitialized flips.
* perf: lazy-load heavy dependencies (MarkdownRenderer + CodeMirror languages)
MarkdownRenderer dynamic import:
- Move heavy implementation (marked, react-markdown, beautiful-mermaid,
react-syntax-highlighter, ~1500 lines) to MarkdownRendererImpl.tsx
- Replace MarkdownRenderer.tsx with thin lazy wrapper using React.lazy
- All 11 existing imports work unchanged — no consumer code modified
- Full markdown stack loads on first render of markdown content
CodeMirror language lazy loading:
- languageByExtension.ts: remove static imports for 10+ less-common
language packages (@codemirror/lang-go, lang-rust, lang-sql, etc.)
- Keep only 6 most common languages static: javascript, json, css, html,
markdown, python, shell
- Less common languages return null from languageByExtension, causing
callers to fall back to loadLanguageByExtension which dynamically
loads from @codemirror/language-data
- Reduces initial bundle by ~200KB+ of language parsers
---------
Co-authored-by: Shyamalan Kannan <yabuku@Shyamalans-MacBook-Pro.local>
176 lines
5.1 KiB
TypeScript
176 lines
5.1 KiB
TypeScript
import type { Extension } from '@codemirror/state';
|
|
|
|
// Static imports for the most common languages only.
|
|
// Less common languages are loaded dynamically via loadLanguageByExtension
|
|
// to keep the initial bundle lean.
|
|
import { javascript } from '@codemirror/lang-javascript';
|
|
import { json } from '@codemirror/lang-json';
|
|
import { css } from '@codemirror/lang-css';
|
|
import { html } from '@codemirror/lang-html';
|
|
import { markdown } from '@codemirror/lang-markdown';
|
|
import { languages } from '@codemirror/language-data';
|
|
import { python } from '@codemirror/lang-python';
|
|
|
|
import { Language, LanguageDescription, StreamLanguage, HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
import { tags as t } from '@lezer/highlight';
|
|
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
|
|
|
const shellLanguage = StreamLanguage.define(shell);
|
|
|
|
function codeBlockLanguageResolver(info: string): Language | LanguageDescription | null {
|
|
const normalized = info.trim().toLowerCase();
|
|
|
|
switch (normalized) {
|
|
case 'bash':
|
|
case 'sh':
|
|
case 'zsh':
|
|
case 'shell':
|
|
case 'shellsession':
|
|
case 'console':
|
|
return shellLanguage;
|
|
case 'json':
|
|
case 'jsonc':
|
|
case 'json5':
|
|
return json().language;
|
|
case 'js':
|
|
case 'javascript':
|
|
return javascript().language;
|
|
case 'jsx':
|
|
return javascript({ jsx: true }).language;
|
|
case 'ts':
|
|
case 'typescript':
|
|
return javascript({ typescript: true }).language;
|
|
case 'tsx':
|
|
return javascript({ typescript: true, jsx: true }).language;
|
|
case 'html':
|
|
return html().language;
|
|
case 'css':
|
|
return css().language;
|
|
case 'py':
|
|
case 'python':
|
|
return python().language;
|
|
case 'heex':
|
|
case 'eex':
|
|
case 'leex':
|
|
return html().language;
|
|
default:
|
|
return LanguageDescription.matchLanguageName(languages, normalized, true);
|
|
}
|
|
}
|
|
|
|
const normalizeFileName = (filePath: string) => filePath.split('/').pop()?.toLowerCase() ?? '';
|
|
|
|
const matchLanguageDescriptionForFile = (filePath: string): LanguageDescription | null => {
|
|
const filename = normalizeFileName(filePath);
|
|
if (!filename) {
|
|
return null;
|
|
}
|
|
return LanguageDescription.matchFilename(languages, filename);
|
|
};
|
|
|
|
const markdownHighlight = () => syntaxHighlighting(HighlightStyle.define([
|
|
{ tag: [t.heading1, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6], fontWeight: '600' },
|
|
{ tag: t.strong, fontWeight: '600' },
|
|
{ tag: t.emphasis, fontStyle: 'italic' },
|
|
{ tag: t.strikethrough, textDecoration: 'line-through' },
|
|
{ tag: [t.link, t.url], color: 'var(--markdown-link, currentColor)', textDecoration: 'underline' },
|
|
{ tag: t.monospace, color: 'var(--markdown-inline-code, currentColor)', backgroundColor: 'var(--markdown-inline-code-bg, transparent)' },
|
|
{ tag: t.quote, color: 'var(--markdown-blockquote, currentColor)', fontStyle: 'italic' },
|
|
{ tag: t.list, color: 'color-mix(in srgb, var(--muted-foreground) 40%, var(--foreground) 60%)' },
|
|
{ tag: t.heading, color: 'var(--markdown-heading1, currentColor)' },
|
|
]));
|
|
|
|
export function languageByExtension(filePath: string): Extension | null {
|
|
const normalized = filePath.toLowerCase();
|
|
const filename = normalizeFileName(normalized);
|
|
|
|
// Special filenames
|
|
switch (filename) {
|
|
case 'makefile':
|
|
case 'gnumakefile':
|
|
// No dedicated mode; shell is a decent fallback for Make-ish files.
|
|
return shellLanguage;
|
|
}
|
|
|
|
const idx = normalized.lastIndexOf('.');
|
|
const ext = idx >= 0 ? normalized.slice(idx + 1) : '';
|
|
|
|
switch (ext) {
|
|
// JavaScript/TypeScript (most common — keep static)
|
|
case 'ts':
|
|
case 'tsx':
|
|
case 'mts':
|
|
case 'cts':
|
|
return javascript({ typescript: true, jsx: ext === 'tsx' });
|
|
case 'js':
|
|
case 'jsx':
|
|
case 'mjs':
|
|
case 'cjs':
|
|
return javascript({ typescript: false, jsx: ext === 'jsx' });
|
|
|
|
// Web (keep static)
|
|
case 'json':
|
|
case 'jsonc':
|
|
case 'json5':
|
|
case 'jsonl':
|
|
case 'ndjson':
|
|
case 'geojson':
|
|
return json();
|
|
case 'css':
|
|
case 'scss':
|
|
case 'sass':
|
|
case 'less':
|
|
return css();
|
|
case 'html':
|
|
case 'htm':
|
|
return html();
|
|
case 'md':
|
|
case 'mdx':
|
|
case 'markdown':
|
|
case 'mdown':
|
|
case 'mkd':
|
|
return [
|
|
markdown({
|
|
codeLanguages: codeBlockLanguageResolver,
|
|
}),
|
|
markdownHighlight(),
|
|
];
|
|
|
|
// Shell (keep static)
|
|
case 'sh':
|
|
case 'bash':
|
|
case 'zsh':
|
|
case 'fish':
|
|
case 'env':
|
|
return shellLanguage;
|
|
|
|
// Python (very common — keep static)
|
|
case 'py':
|
|
case 'pyw':
|
|
case 'pyi':
|
|
return python();
|
|
|
|
// Less common languages: return null so callers fall back to
|
|
// loadLanguageByExtension which dynamically imports from @codemirror/language-data.
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function loadLanguageByExtension(filePath: string): Promise<Extension | null> {
|
|
const description = matchLanguageDescriptionForFile(filePath);
|
|
if (!description) {
|
|
return null;
|
|
}
|
|
|
|
if (description.support) {
|
|
return description.support;
|
|
}
|
|
|
|
try {
|
|
return await description.load();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|