Files
openchamber/packages/ui/src/lib/settings/metadata.ts
T
Serhii DziupinandSerhii Dziupin bc380e6e1b perf: cut cold-start download 58% and startup heap 22% via measured chunk-graph fixes (#2742)
* 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>
2026-08-07 09:01:31 +03:00

303 lines
8.1 KiB
TypeScript

import type { SidebarSection } from '@/constants/sidebar';
import type { IconName } from '@/components/icon/icons';
export type SettingsPageSlug =
| 'home'
| 'general'
| 'projects'
| 'remote-instances'
| 'providers'
| 'usage'
| 'agents'
| 'behavior'
| 'commands'
| 'mcp'
| 'plugins'
| 'skills.installed'
| 'skills.catalog'
| 'git'
| 'appearance'
| 'chat'
| 'shortcuts'
| 'sessions'
| 'magic-prompts'
| 'snippets'
| 'notifications'
| 'voice'
| 'tunnel'
| 'about';
type SettingsPageGroup =
| 'general'
| 'projects'
| 'opencode'
| 'content';
export interface SettingsRuntimeContext {
isVSCode: boolean;
isWeb: boolean;
isDesktop: boolean;
isMobile: boolean;
}
export interface SettingsPageMeta {
slug: SettingsPageSlug;
title: string;
group: SettingsPageGroup;
kind: 'single' | 'split';
description?: string;
keywords?: string[];
isAvailable?: (ctx: SettingsRuntimeContext) => boolean;
}
export const SETTINGS_PAGE_METADATA: readonly SettingsPageMeta[] = [
{
slug: 'home',
title: 'Settings',
group: 'general',
kind: 'single',
description: 'Search and jump to common pages.',
keywords: ['search', 'settings'],
},
{
slug: 'general',
title: 'General',
group: 'general',
kind: 'single',
keywords: ['general', 'startup', 'launch at login', 'autostart', 'tray', 'password', 'passkey', 'security', 'privacy', 'telemetry', 'transport', 'network', 'lan', 'binary', 'cli'],
},
{
slug: 'projects',
title: 'Projects',
group: 'projects',
kind: 'split',
keywords: ['project', 'projects', 'worktree', 'worktrees', 'repo', 'repository', 'directory'],
},
{
slug: 'remote-instances',
title: 'Remote Instances',
group: 'projects',
kind: 'single',
keywords: ['ssh', 'remote', 'instances', 'tunnels', 'forwarding', 'connection'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
slug: 'providers',
title: 'Providers',
group: 'opencode',
kind: 'split',
keywords: ['provider', 'providers', 'models', 'model', 'api key', 'api keys', 'openai', 'anthropic', 'ollama', 'credentials'],
},
{
slug: 'usage',
title: 'Usage',
group: 'general',
kind: 'split',
keywords: ['quota', 'billing', 'tokens', 'usage', 'limits'],
},
{
slug: 'agents',
title: 'Agents',
group: 'opencode',
kind: 'split',
keywords: ['agent', 'agents', 'prompts', 'tools', 'permissions'],
},
{
slug: 'behavior',
title: 'Behavior',
group: 'opencode',
kind: 'single',
keywords: ['behavior', 'agents.md', 'system prompt', 'global rules', 'instructions', 'override'],
},
{
slug: 'commands',
title: 'Commands',
group: 'opencode',
kind: 'split',
keywords: ['command', 'commands', 'slash', 'macros', 'automation'],
},
{
slug: 'mcp',
title: 'MCP',
group: 'opencode',
kind: 'split',
keywords: ['mcp', 'model context protocol', 'servers', 'tools', 'remote', 'stdio'],
},
{
slug: 'plugins',
title: 'Plugins',
group: 'opencode',
kind: 'split',
keywords: ['plugin', 'plugins', 'extensions', 'addons', 'npm', 'opencode-wakatime'],
},
{
slug: 'skills.installed',
title: 'Skills',
group: 'content',
kind: 'split',
keywords: ['skill', 'skills', 'instructions', 'install', 'catalog'],
},
{
slug: 'skills.catalog',
title: 'Skills Catalog',
group: 'content',
kind: 'single',
keywords: ['install', 'catalog', 'external', 'repository', 'skills catalog'],
},
{
slug: 'git',
title: 'Git',
group: 'projects',
kind: 'single',
keywords: ['git', 'github', 'identity', 'identities', 'ssh', 'profiles', 'credentials', 'keys', 'commit', 'gitmoji', 'oauth', 'prs', 'issues'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
slug: 'appearance',
title: 'Appearance',
group: 'general',
kind: 'single',
keywords: ['theme', 'font', 'spacing', 'padding', 'corner radius', 'radius', 'input bar', 'keyboard', 'viewport', 'mobile', 'terminal', 'pwa', 'install name', 'app shortcuts'],
},
{
slug: 'chat',
title: 'Chat',
group: 'general',
kind: 'single',
keywords: ['tools', 'diff', 'reasoning', 'dotfiles', 'draft', 'queue', 'output', 'copy', 'image', 'split messages', 'message actions'],
},
{
slug: 'shortcuts',
title: 'Shortcuts',
group: 'general',
kind: 'single',
keywords: ['keyboard', 'hotkeys', 'shortcuts', 'bindings'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
slug: 'sessions',
title: 'Sessions',
group: 'general',
kind: 'single',
keywords: ['defaults', 'default agent', 'default model', 'retention', 'memory', 'limits', 'zen'],
},
{
slug: 'magic-prompts',
title: 'Magic Prompts',
group: 'content',
kind: 'split',
keywords: ['prompts', 'templates', 'git', 'github', 'review', 'commit', 'pull request'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
slug: 'snippets',
title: 'Snippets',
group: 'content',
kind: 'split',
keywords: ['prompt', 'templates', 'multi-run', 'strategy', 'approach'],
},
{ slug: 'notifications', title: 'Notifications', group: 'general', kind: 'single', keywords: ['alerts', 'native', 'summary', 'summarization'], },
{ slug: 'voice', title: 'Voice', group: 'general', kind: 'single', keywords: ['tts', 'speech', 'voice'], isAvailable: (ctx) => !ctx.isVSCode },
{ slug: 'tunnel', title: 'External Tunnel', group: 'projects', kind: 'single', keywords: ['tunnel', 'external', 'cloudflare', 'qr', 'remote', 'mobile', 'share'], isAvailable: (ctx) => !ctx.isVSCode },
{ slug: 'about', title: 'About', group: 'general', kind: 'single', keywords: ['about', 'version', 'updates', 'release', 'changelog'], isAvailable: (ctx) => ctx.isMobile && !ctx.isVSCode },
] as const;
const LEGACY_SIDEBAR_SECTION_TO_SETTINGS_SLUG: Record<SidebarSection, SettingsPageSlug> = {
sessions: 'sessions',
agents: 'agents',
commands: 'commands',
mcp: 'mcp',
skills: 'skills.installed',
providers: 'providers',
usage: 'usage',
'git-identities': 'git',
settings: 'home',
};
export function getSettingsPageMeta(slug: string): SettingsPageMeta | null {
const normalized = slug.trim().toLowerCase();
return (SETTINGS_PAGE_METADATA as readonly SettingsPageMeta[]).find((page) => page.slug === normalized) ?? null;
}
export function resolveSettingsSlug(value: string | null | undefined): SettingsPageSlug {
const normalized = (value ?? '').trim().toLowerCase();
if (!normalized) {
return 'home';
}
const legacy = (LEGACY_SIDEBAR_SECTION_TO_SETTINGS_SLUG as Record<string, SettingsPageSlug>)[normalized];
if (legacy) {
return legacy;
}
const direct = getSettingsPageMeta(normalized);
if (direct) {
return direct.slug;
}
return 'home';
}
// Lives here (not in SettingsView) so light consumers such as the command
// palette can render settings entries without statically importing the whole
// settings surface into the eager startup graph.
export function getSettingsNavIcon(slug: SettingsPageSlug): IconName | null {
switch (slug) {
case 'general':
return 'settings-3';
case 'projects':
return 'folders';
case 'remote-instances':
return 'computer';
case 'appearance':
return 'palette';
case 'chat':
return 'chat-ai-3';
case 'magic-prompts':
return 'ai-generate-2';
case 'snippets':
return 'chat-thread';
case 'notifications':
return 'notification-3';
case 'shortcuts':
return 'command';
case 'sessions':
return 'chat-history';
case 'providers':
return 'cloud';
case 'agents':
return 'ai-agent';
case 'behavior':
return 'brain';
case 'commands':
return 'slash-commands-2';
case 'mcp':
return null;
case 'plugins':
return 'plug-2';
case 'skills.installed':
return 'book-open';
case 'skills.catalog':
return 'book';
case 'git':
return 'git-branch';
case 'usage':
return 'bar-chart-2';
case 'voice':
return 'mic';
case 'tunnel':
return 'home-office';
case 'about':
return 'information';
case 'home':
return null;
default:
return 'robot-2';
}
}