feat: vscode extension (#59)

* feat: add initial VS Code extension plan and implementation tasks

* feat(vscode): added initial version of an Openchamber VSCode extension

* feat(vscode): enhance VS Code extension with theme integration and session management

* feat(vscode): implement connection status handling and overlay in VSCode layout

* feat: move extension to secondary sidebar

* chore: upgrade @opencode-ai/sdk to 1.0.150

* vscode: editor bridge, file picker, click-to-open in tool parts

* vscode: layout session lifecycle, theme sync, typography overrides

* ui: compact mode for vscode, model search, autocomplete width fixes

* perf: scroll force flag, raf placeholder, git polling backoff

* ui: tool output styling, markdown code block fix, gitignore

* refactor: update typography handling for VSCode runtime, remove unused styles

* docs: update README with VS Code extension details and add extension image

* docs: update changelog with new features and performance improvements
This commit is contained in:
Bohdan Triapitsyn
2025-12-13 16:34:17 +02:00
committed by GitHub
parent 610ccf4c62
commit bb72c0fb0c
76 changed files with 6097 additions and 296 deletions
@@ -6,7 +6,7 @@ import React, {
} from 'react';
import type { Theme, ThemeMode } from '@/types/theme';
import type { DesktopSettings } from '@/lib/desktop';
import { isDesktopRuntime } from '@/lib/desktop';
import { isDesktopRuntime, isVSCodeRuntime } from '@/lib/desktop';
import { CSSVariableGenerator } from '@/lib/theme/cssGenerator';
import { updateDesktopSettings } from '@/lib/persistence';
import {
@@ -16,6 +16,8 @@ import {
flexokiDarkTheme,
} from '@/lib/theme/themes';
import { ThemeSystemContext, type ThemeContextValue } from './theme-system-context';
import type { VSCodeThemePayload } from '@/lib/theme/vscode/adapter';
import { useUIStore } from '@/stores/useUIStore';
type ThemePreferences = {
themeMode: ThemeMode;
@@ -50,6 +52,8 @@ const ensureThemeById = (themeId: string, variant: 'light' | 'dark'): Theme => {
return fallback ?? fallbackThemeForVariant(variant);
};
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
const validateThemeId = (themeId: string | null, variant: 'light' | 'dark'): string => {
if (!themeId) {
return variant === 'light' ? DEFAULT_LIGHT_ID : DEFAULT_DARK_ID;
@@ -126,8 +130,19 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
const cssGenerator = useMemo(() => new CSSVariableGenerator(), []);
const [preferences, setPreferences] = useState<ThemePreferences>(() => buildInitialPreferences(defaultThemeId));
const [systemPrefersDark, setSystemPrefersDark] = useState<boolean>(() => getSystemPreference());
const [vscodeTheme, setVSCodeTheme] = useState<Theme | null>(() => {
if (typeof window === 'undefined' || !isVSCodeRuntime()) {
return null;
}
const existing = (window as unknown as { __OPENCHAMBER_VSCODE_THEME__?: Theme }).__OPENCHAMBER_VSCODE_THEME__;
return existing || null;
});
const isVSCode = useMemo(() => isVSCodeRuntime(), []);
const currentTheme = useMemo(() => {
if (isVSCode && vscodeTheme) {
return vscodeTheme;
}
if (preferences.themeMode === 'light') {
return ensureThemeById(preferences.lightThemeId, 'light');
}
@@ -137,9 +152,42 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
return systemPrefersDark
? ensureThemeById(preferences.darkThemeId, 'dark')
: ensureThemeById(preferences.lightThemeId, 'light');
}, [preferences, systemPrefersDark]);
}, [isVSCode, preferences, systemPrefersDark, vscodeTheme]);
const availableThemes = themes;
const availableThemes = useMemo(
() => (isVSCode && vscodeTheme ? [vscodeTheme, ...themes] : themes),
[isVSCode, vscodeTheme],
);
useEffect(() => {
if (!isVSCode) {
return;
}
const applyVSCodeTheme = (theme: Theme) => {
setVSCodeTheme(theme);
const variant: ThemeMode = theme.metadata.variant === 'dark' ? 'dark' : 'light';
const uiStore = useUIStore.getState();
if (uiStore.theme !== variant) {
uiStore.setTheme(variant);
}
};
const handleThemeEvent = (event: Event) => {
const detail = (event as CustomEvent<VSCodeThemePayload>).detail;
if (detail?.theme) {
applyVSCodeTheme(detail.theme);
}
};
const existing = (window as unknown as { __OPENCHAMBER_VSCODE_THEME__?: Theme }).__OPENCHAMBER_VSCODE_THEME__;
if (existing) {
applyVSCodeTheme(existing);
}
window.addEventListener('openchamber:vscode-theme', handleThemeEvent as EventListener);
return () => window.removeEventListener('openchamber:vscode-theme', handleThemeEvent as EventListener);
}, [isVSCode]);
const updateBrowserChrome = useCallback((theme: Theme) => {
if (typeof document === 'undefined') {
@@ -177,17 +225,25 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
metaThemeColorMedia.setAttribute('content', chromeColor);
}, []);
useEffect(() => {
const applyVSCodeRuntimeClass = useCallback((enabled: boolean) => {
if (typeof document === 'undefined') {
return;
}
document.documentElement.classList.toggle('vscode-runtime', enabled);
}, []);
useIsomorphicLayoutEffect(() => {
if (typeof window === 'undefined') {
return;
}
cssGenerator.apply(currentTheme);
applyVSCodeRuntimeClass(isVSCode);
updateBrowserChrome(currentTheme);
const root = document.documentElement;
root.classList.remove('light', 'dark');
root.classList.add(currentTheme.metadata.variant);
}, [cssGenerator, currentTheme, updateBrowserChrome]);
}, [applyVSCodeRuntimeClass, cssGenerator, currentTheme, isVSCode, updateBrowserChrome]);
useEffect(() => {
if (preferences.themeMode !== 'system' || typeof window === 'undefined') {