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
+9 -7
View File
@@ -1,5 +1,6 @@
import type { Theme } from '@/types/theme';
import { SEMANTIC_TYPOGRAPHY } from '@/lib/typography';
import { SEMANTIC_TYPOGRAPHY, VSCODE_TYPOGRAPHY } from '@/lib/typography';
import { isVSCodeRuntime } from '@/lib/desktop';
const hexToRgb = (value: string | undefined | null): string | null => {
if (!value || typeof value !== 'string') {
@@ -525,22 +526,23 @@ export class CSSVariableGenerator {
private generateTypographyVariables(): string[] {
const vars: string[] = [];
const typography = isVSCodeRuntime() ? VSCODE_TYPOGRAPHY : SEMANTIC_TYPOGRAPHY;
vars.push(' /* Semantic Typography Variables */');
vars.push(' --ui-regular-font-weight: 400;');
vars.push(' /* Markdown content - all markdown elements use same size */');
vars.push(` --text-markdown: ${SEMANTIC_TYPOGRAPHY.markdown};`);
vars.push(` --text-markdown: ${typography.markdown};`);
vars.push(' /* Code content - all code elements use same size */');
vars.push(` --text-code: ${SEMANTIC_TYPOGRAPHY.code};`);
vars.push(` --text-code: ${typography.code};`);
vars.push(' /* UI headers - dialog titles, panel headers */');
vars.push(` --text-ui-header: ${SEMANTIC_TYPOGRAPHY.uiHeader};`);
vars.push(` --text-ui-header: ${typography.uiHeader};`);
vars.push(' /* UI labels - buttons, menus, navigation */');
vars.push(` --text-ui-label: ${SEMANTIC_TYPOGRAPHY.uiLabel};`);
vars.push(` --text-ui-label: ${typography.uiLabel};`);
vars.push(' /* Metadata - timestamps, status, helper text */');
vars.push(` --text-meta: ${SEMANTIC_TYPOGRAPHY.meta};`);
vars.push(` --text-meta: ${typography.meta};`);
vars.push(' /* Micro text - badges, shortcuts, indicators */');
vars.push(` --text-micro: ${SEMANTIC_TYPOGRAPHY.micro};`);
vars.push(` --text-micro: ${typography.micro};`);
vars.push(' /* Heading line height and letter spacing */');
vars.push(' --h1-line-height: 1.25rem;');
+268
View File
@@ -0,0 +1,268 @@
import type { Theme } from '@/types/theme';
import type { ThemeMode } from '@/types/theme';
import { flexokiDarkTheme, flexokiLightTheme } from '@/lib/theme/themes';
export type VSCodeThemeKind = 'light' | 'dark' | 'high-contrast';
export type VSCodeThemeColorToken =
| 'editor.background'
| 'editor.foreground'
| 'editor.selectionBackground'
| 'editor.selectionForeground'
| 'editor.lineHighlightBackground'
| 'editorCursor.foreground'
| 'focusBorder'
| 'sideBar.background'
| 'sideBar.foreground'
| 'panel.background'
| 'panel.foreground'
| 'panel.border'
| 'input.background'
| 'input.foreground'
| 'input.border'
| 'button.background'
| 'button.foreground'
| 'button.hoverBackground'
| 'textLink.foreground'
| 'descriptionForeground'
| 'terminal.ansiRed'
| 'terminal.ansiGreen'
| 'terminal.ansiBlue'
| 'terminal.ansiYellow'
| 'terminal.ansiCyan'
| 'editorError.foreground'
| 'editorError.background'
| 'editorWarning.foreground'
| 'editorWarning.background'
| 'editorInfo.foreground'
| 'editorInfo.background'
| 'testing.iconPassed'
| 'badge.background'
| 'badge.foreground'
| 'statusBar.background'
| 'statusBar.foreground'
| 'list.hoverBackground'
| 'list.activeSelectionBackground'
| 'textPreformat.foreground'
| 'textPreformat.background';
export type VSCodeThemePalette = {
kind: VSCodeThemeKind;
colors: Partial<Record<VSCodeThemeColorToken, string>>;
mode?: ThemeMode;
};
export type VSCodeThemePayload = {
theme: Theme;
palette: VSCodeThemePalette;
};
const VARIABLE_MAP: Record<VSCodeThemeColorToken, string> = {
'editor.background': '--vscode-editor-background',
'editor.foreground': '--vscode-editor-foreground',
'editor.selectionBackground': '--vscode-editor-selectionBackground',
'editor.selectionForeground': '--vscode-editor-selectionForeground',
'editor.lineHighlightBackground': '--vscode-editor-lineHighlightBackground',
'editorCursor.foreground': '--vscode-editorCursor-foreground',
focusBorder: '--vscode-focusBorder',
'sideBar.background': '--vscode-sideBar-background',
'sideBar.foreground': '--vscode-sideBar-foreground',
'panel.background': '--vscode-panel-background',
'panel.foreground': '--vscode-panel-foreground',
'panel.border': '--vscode-panel-border',
'input.background': '--vscode-input-background',
'input.foreground': '--vscode-input-foreground',
'input.border': '--vscode-input-border',
'button.background': '--vscode-button-background',
'button.foreground': '--vscode-button-foreground',
'button.hoverBackground': '--vscode-button-hoverBackground',
'textLink.foreground': '--vscode-textLink-foreground',
descriptionForeground: '--vscode-descriptionForeground',
'terminal.ansiRed': '--vscode-terminal-ansiRed',
'terminal.ansiGreen': '--vscode-terminal-ansiGreen',
'terminal.ansiBlue': '--vscode-terminal-ansiBlue',
'terminal.ansiYellow': '--vscode-terminal-ansiYellow',
'terminal.ansiCyan': '--vscode-terminal-ansiCyan',
'editorError.foreground': '--vscode-editorError-foreground',
'editorError.background': '--vscode-editorError-background',
'editorWarning.foreground': '--vscode-editorWarning-foreground',
'editorWarning.background': '--vscode-editorWarning-background',
'editorInfo.foreground': '--vscode-editorInfo-foreground',
'editorInfo.background': '--vscode-editorInfo-background',
'testing.iconPassed': '--vscode-testing-iconPassed',
'badge.background': '--vscode-badge-background',
'badge.foreground': '--vscode-badge-foreground',
'statusBar.background': '--vscode-statusBar-background',
'statusBar.foreground': '--vscode-statusBar-foreground',
'list.hoverBackground': '--vscode-list-hoverBackground',
'list.activeSelectionBackground': '--vscode-list-activeSelectionBackground',
'textPreformat.foreground': '--vscode-textPreformat-foreground',
'textPreformat.background': '--vscode-textPreformat-background',
};
const normalizeColor = (value?: string | null): string | undefined => {
if (!value) return undefined;
const trimmed = value.trim();
if (!trimmed) return undefined;
return trimmed;
};
const readKind = (preferred?: VSCodeThemeKind): VSCodeThemeKind => {
if (preferred === 'light' || preferred === 'dark' || preferred === 'high-contrast') {
return preferred;
}
if (typeof window !== 'undefined') {
const prefersLight = typeof window.matchMedia === 'function' &&
window.matchMedia('(prefers-color-scheme: light)').matches;
return prefersLight ? 'light' : 'dark';
}
return 'dark';
};
export const readVSCodeThemePalette = (
preferredKind?: VSCodeThemeKind,
preferredMode?: ThemeMode,
): VSCodeThemePalette | null => {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return null;
}
const styles = getComputedStyle(document.documentElement);
const colors: Partial<Record<VSCodeThemeColorToken, string>> = {};
(Object.keys(VARIABLE_MAP) as VSCodeThemeColorToken[]).forEach((token) => {
const cssVar = VARIABLE_MAP[token];
const value = normalizeColor(styles.getPropertyValue(cssVar));
if (value) {
colors[token] = value;
}
});
return {
kind: readKind(preferredKind),
colors,
mode: preferredMode,
};
};
export const buildVSCodeThemeFromPalette = (palette: VSCodeThemePalette): Theme => {
const base = palette.kind === 'light' ? flexokiLightTheme : flexokiDarkTheme;
const read = (token: VSCodeThemeColorToken, fallback: string): string =>
palette.colors[token] ?? fallback;
const sidebarBg = read('sideBar.background', base.colors.surface.background);
const sidebarFg = read('sideBar.foreground', read('descriptionForeground', base.colors.surface.mutedForeground));
const panelBg = read('panel.background', read('editor.background', base.colors.surface.elevated));
const panelFg = read('panel.foreground', read('editor.foreground', base.colors.surface.foreground));
const background = sidebarBg;
const foreground = read('editor.foreground', base.colors.surface.foreground);
const accent = read('textLink.foreground', read('button.background', base.colors.primary.base));
const accentFg = read('button.foreground', base.colors.primary.foreground || base.colors.surface.background);
const hoverBg = read('list.hoverBackground', read('editor.selectionBackground', base.colors.interactive.hover));
const activeBg = read('list.activeSelectionBackground', hoverBg);
const selection = read('editor.selectionBackground', activeBg);
const selectionFg = read('editor.selectionForeground', foreground);
const focus = read('focusBorder', selection);
const border = read('input.border', read('panel.border', base.colors.interactive.border));
const cursor = read('editorCursor.foreground', base.colors.interactive.cursor);
const badgeBg = read('badge.background', accent);
const badgeFg = read('badge.foreground', foreground);
const inlineCode = read('textPreformat.foreground', read('terminal.ansiGreen', base.colors.syntax.base.string));
// Tailwind's `--accent` drives hovered/selected menu items in Radix/shadcn; prefer VS Code list hover/selection.
const subtle = hoverBg;
return {
...base,
metadata: {
...base.metadata,
id: 'vscode-auto',
name: 'VS Code Theme',
description: 'Mirrors your current VS Code color theme',
author: 'VS Code',
version: '1.0.0',
variant: palette.kind === 'light' ? 'light' : 'dark',
tags: ['vscode', 'auto'],
},
colors: {
...base.colors,
primary: {
base: accent,
hover: read('button.hoverBackground', accent),
active: read('button.hoverBackground', accent),
foreground: accentFg,
muted: read('textLink.foreground', accent),
emphasis: accent,
},
surface: {
...base.colors.surface,
background,
foreground,
muted: panelBg,
mutedForeground: sidebarFg,
elevated: panelBg,
elevatedForeground: panelFg,
overlay: read('statusBar.background', base.colors.surface.overlay),
subtle,
},
interactive: {
...base.colors.interactive,
border,
borderHover: border,
borderFocus: focus,
selection,
selectionForeground: selectionFg,
focus,
focusRing: focus,
cursor,
hover: hoverBg,
active: activeBg,
},
status: {
...base.colors.status,
error: read('editorError.foreground', base.colors.status.error),
errorForeground: read('editorError.foreground', base.colors.status.errorForeground),
errorBackground: read('editorError.background', base.colors.status.errorBackground),
errorBorder: read('editorError.foreground', base.colors.status.errorBorder),
warning: read('editorWarning.foreground', base.colors.status.warning),
warningForeground: read('editorWarning.foreground', base.colors.status.warningForeground),
warningBackground: read('editorWarning.background', base.colors.status.warningBackground),
warningBorder: read('editorWarning.foreground', base.colors.status.warningBorder),
success: read('testing.iconPassed', base.colors.status.success),
successForeground: read('testing.iconPassed', base.colors.status.successForeground),
successBackground: read('testing.iconPassed', base.colors.status.successBackground),
successBorder: read('testing.iconPassed', base.colors.status.successBorder),
info: read('editorInfo.foreground', base.colors.status.info),
infoForeground: read('editorInfo.foreground', base.colors.status.infoForeground),
infoBackground: read('editorInfo.background', base.colors.status.infoBackground),
infoBorder: read('editorInfo.foreground', base.colors.status.infoBorder),
},
syntax: {
...base.colors.syntax,
base: {
...base.colors.syntax.base,
background,
foreground,
comment: read('editor.lineHighlightBackground', base.colors.syntax.base.comment),
keyword: accent,
string: inlineCode,
number: read('terminal.ansiYellow', base.colors.syntax.base.number),
function: read('terminal.ansiBlue', base.colors.syntax.base.function),
variable: read('terminal.ansiCyan', base.colors.syntax.base.variable),
type: read('terminal.ansiCyan', base.colors.syntax.base.type),
operator: accent,
},
},
badges: {
...(base.colors.badges || {}),
default: {
bg: badgeBg,
fg: badgeFg,
border: border,
},
},
},
};
};