diff --git a/.github/workflows/vscode-extension.yml b/.github/workflows/vscode-extension.yml
new file mode 100644
index 00000000..e52eb9de
--- /dev/null
+++ b/.github/workflows/vscode-extension.yml
@@ -0,0 +1,61 @@
+name: Publish VS Code Extension
+
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ env:
+ VSCE_PAT: ${{ secrets.VSCE_PAT }}
+ OVSX_PAT: ${{ secrets.OVSX_PAT }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build VS Code extension
+ run: pnpm -C packages/vscode run build
+
+ - name: Package extension
+ run: pnpm -C packages/vscode exec vsce package --no-dependencies
+
+ - name: Publish to VS Code Marketplace
+ if: ${{ env.VSCE_PAT != '' }}
+ run: pnpm -C packages/vscode exec vsce publish -p "$VSCE_PAT" --no-dependencies
+
+ - name: Publish to Open VSX
+ if: ${{ env.OVSX_PAT != '' }}
+ run: pnpm -C packages/vscode dlx ovsx publish packages/vscode/*.vsix -p "$OVSX_PAT"
+
+ - name: Upload VSIX artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: openchamber-vscode-vsix
+ path: packages/vscode/*.vsix
+
+ - name: Attach VSIX to GitHub Release
+ if: startsWith(github.ref, 'refs/tags/')
+ uses: softprops/action-gh-release@v2
+ with:
+ files: packages/vscode/*.vsix
+ generate_release_notes: false
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 4ac51bd5..0c90caa8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ dist-ssr
release
*.local
*.tgz
+*.vsix
/npm
/tsc
/openchamber@*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44451420..059df327 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
- Added assistant answer fork flow so users can start a new session from an assistant plan/response with inherited context.
+- Added OpenChamber VS Code extension with editor integration: file picker, click-to-open in tool parts
+- Improved scroll performance with force flag and RAF placeholder
+- Added git polling backoff optimization
## [1.0.9] - 2025-12-08
diff --git a/README.md b/README.md
index 8f8cb945..eb38fa83 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ The whole project was built entirely with AI coding agents under my supervision.



+
@@ -45,6 +46,10 @@ The whole project was built entirely with AI coding agents under my supervision.
## Installation
+### VS Code Extension
+
+Install from [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=fedaykindev.openchamber) or search "OpenChamber" in Extensions.
+
### CLI (Web Server)
```bash
diff --git a/docs/references/vscode_extension.png b/docs/references/vscode_extension.png
new file mode 100644
index 00000000..bff21f98
Binary files /dev/null and b/docs/references/vscode_extension.png differ
diff --git a/docs/vscode-extension-plan.md b/docs/vscode-extension-plan.md
new file mode 100644
index 00000000..0613f8b3
--- /dev/null
+++ b/docs/vscode-extension-plan.md
@@ -0,0 +1,165 @@
+# VS Code Extension Plan
+
+Chat UI sidebar panel for VS Code that connects to OpenCode backend.
+
+## Goals
+
+- Sidebar webview panel with OpenChamber chat UI
+- Reuse `@openchamber/ui` components (ChatView, stores, hooks)
+- Auto-detect VS Code theme (light/dark) and adapt
+- Use workspace folder as OpenCode directory
+- Support `@file` mentions via VS Code workspace API
+
+## Package Structure
+
+```
+packages/vscode/
+├── src/
+│ ├── extension.ts # Extension entry, registers webview
+│ ├── ChatViewProvider.ts # WebviewViewProvider for sidebar
+│ ├── bridge.ts # Webview <-> Extension host messaging
+│ └── theme.ts # VS Code theme detection
+├── webview/
+│ ├── main.tsx # Webview React entry
+│ ├── App.tsx # Minimal App (chat-only, no layout)
+│ └── api/
+│ ├── index.ts # createVSCodeAPIs()
+│ ├── files.ts # File listing via extension host
+│ └── settings.ts # Theme/config from VS Code
+├── package.json # Extension manifest
+├── tsconfig.json
+├── tsconfig.webview.json
+└── vite.config.ts # Webview bundler
+```
+
+## Implementation Tasks
+
+### Phase 1: Extension Scaffold
+
+1. Create `packages/vscode/` directory structure
+2. Create `package.json` with extension manifest
+ - Sidebar view contribution
+ - Commands (new session)
+ - Configuration (API URL)
+3. Create `tsconfig.json` for extension host (Node.js)
+4. Create `tsconfig.webview.json` for webview (browser)
+5. Create `vite.config.ts` for webview bundling
+6. Add workspace reference in root `pnpm-workspace.yaml`
+
+### Phase 2: Extension Host
+
+1. `src/extension.ts` - activate/deactivate, register provider
+2. `src/ChatViewProvider.ts` - WebviewViewProvider implementation
+ - Generate HTML with CSP
+ - Handle webview lifecycle
+ - Pass workspace folder to webview
+3. `src/bridge.ts` - Message handler for webview requests
+ - `files:list` - list directory contents
+ - `files:search` - fuzzy file search
+ - `workspace:folder` - get workspace root
+4. `src/theme.ts` - Detect VS Code color theme kind
+
+### Phase 3: Webview Runtime
+
+1. `webview/main.tsx` - React entry point
+2. `webview/App.tsx` - Simplified app shell
+ - No MainLayout/Header/Sidebar
+ - Just ChatView + essential providers
+ - Theme sync with VS Code
+3. `webview/api/index.ts` - createVSCodeAPIs()
+ - RuntimeAPIs implementation
+ - IPC bridge to extension host
+4. `webview/api/files.ts` - FilesAPI via postMessage
+5. `webview/api/settings.ts` - SettingsAPI (theme from VS Code)
+
+### Phase 4: Theme Integration
+
+1. Listen to `vscode.window.onDidChangeActiveColorTheme`
+2. Map VS Code theme kind to OpenChamber light/dark
+3. Post theme changes to webview
+4. Apply theme CSS variables dynamically
+
+### Phase 5: File Context Support
+
+1. Implement `files:list` in extension host using `vscode.workspace.fs`
+2. Implement `files:search` using `vscode.workspace.findFiles`
+3. Wire up to ChatInput `@file` autocomplete
+
+### Phase 6: Build & Package
+
+1. Add build scripts to `packages/vscode/package.json`
+2. Add root-level scripts (`vscode:dev`, `vscode:build`, `vscode:package`)
+3. Configure `.vscodeignore` for clean packaging
+4. Test extension in VS Code Extension Host
+
+## Dependencies
+
+### Extension Host (Node.js)
+- `@types/vscode` - VS Code API types
+- `esbuild` - Bundle extension.ts
+
+### Webview (Browser)
+- `@openchamber/ui` (workspace) - Shared UI components
+- `vite` + `@vitejs/plugin-react` - Bundle React app
+- `@opencode-ai/sdk` - OpenCode API client
+
+## Extension Manifest Highlights
+
+```json
+{
+ "contributes": {
+ "views": {
+ "explorer": [{
+ "type": "webview",
+ "id": "openchamber.chatView",
+ "name": "OpenChamber"
+ }]
+ },
+ "commands": [{
+ "command": "openchamber.newSession",
+ "title": "OpenChamber: New Chat Session"
+ }],
+ "configuration": {
+ "properties": {
+ "openchamber.apiUrl": {
+ "type": "string",
+ "default": "http://localhost:47339"
+ }
+ }
+ }
+ }
+}
+```
+
+## Reused from @openchamber/ui
+
+| Module | Status |
+|--------|--------|
+| `components/chat/*` | Reuse all |
+| `components/views/ChatView` | Reuse |
+| `stores/useSessionStore` | Reuse |
+| `stores/useConfigStore` | Reuse |
+| `stores/useUIStore` | Partial (no sidebar state) |
+| `hooks/useEventStream` | Reuse |
+| `hooks/useMessageSync` | Reuse |
+| `lib/opencode/client` | Reuse |
+| `lib/theme/*` | Reuse (CSS generation) |
+| `components/layout/*` | Skip |
+| `components/views/GitView` | Skip |
+| `components/views/DiffView` | Skip |
+| `components/views/TerminalView` | Skip |
+
+## Not Needed (handled by VS Code)
+
+- Terminal API (VS Code integrated terminal)
+- Git API (VS Code SCM)
+- Notifications API (VS Code notifications)
+- Permissions API (VS Code handles sandbox)
+- Directory picker (workspace folder used)
+
+## Decisions
+
+- **View location**: Explorer sidebar
+- **Sidebar icon**: Reuse from `packages/web/public/` (tab icon)
+- **Marketplace icon**: Reuse from `packages/desktop/src-tauri/icons/app-icon-checkpoint.svg`
+- **Publisher**: `fedaykindev` (dev.azure.com/fedaykindev, artmore@protonmail.com)
diff --git a/package.json b/package.json
index a0b1fea7..8ccb3d02 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,10 @@
"desktop:build": "pnpm -C packages/desktop build && pnpm -C packages/desktop tauri build",
"desktop:lint": "pnpm -C packages/desktop run lint && cargo fmt --manifest-path packages/desktop/src-tauri/Cargo.toml -- --check && cargo clippy --manifest-path packages/desktop/src-tauri/Cargo.toml -- -D warnings",
"desktop:type-check": "pnpm -C packages/desktop run type-check && cargo fmt --manifest-path packages/desktop/src-tauri/Cargo.toml -- --check && cargo clippy --manifest-path packages/desktop/src-tauri/Cargo.toml -- -D warnings",
+ "vscode:dev": "pnpm -C packages/vscode run dev",
+ "vscode:build": "pnpm -C packages/vscode run build",
+ "vscode:package": "pnpm -C packages/vscode run package",
+ "vscode:type-check": "pnpm -C packages/vscode run type-check",
"version:bump": "node scripts/bump-version.mjs",
"release:prepare": "pnpm run build && pnpm run type-check && pnpm run lint"
},
@@ -55,7 +59,7 @@
"@heroui/system": "^2.4.23",
"@heroui/theme": "^2.4.23",
"@ibm/plex": "^6.4.1",
- "@opencode-ai/sdk": "^1.0.133",
+ "@opencode-ai/sdk": "^1.0.150",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
diff --git a/packages/desktop/src/api/index.ts b/packages/desktop/src/api/index.ts
index 949c3110..ff926b7f 100644
--- a/packages/desktop/src/api/index.ts
+++ b/packages/desktop/src/api/index.ts
@@ -32,7 +32,7 @@ export const createDesktopAPIs = (): RuntimeAPIs & { cleanup?: () => void } => {
};
return {
- runtime: { platform: 'desktop', isDesktop: true, label: 'tauri-bootstrap' },
+ runtime: { platform: 'desktop', isDesktop: true, isVSCode: false, label: 'tauri-bootstrap' },
terminal: wrappedTerminalAPI,
git: createDesktopGitAPI(),
files: createDesktopFilesAPI(),
diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx
index 54457498..25ffc9f7 100644
--- a/packages/ui/src/App.tsx
+++ b/packages/ui/src/App.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { MainLayout } from '@/components/layout/MainLayout';
+import { VSCodeLayout } from '@/components/layout/VSCodeLayout';
import { FireworksProvider } from '@/contexts/FireworksContext';
import { Toaster } from '@/components/ui/sonner';
import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel';
@@ -34,6 +35,7 @@ function App({ apis }: AppProps) {
const [showMemoryDebug, setShowMemoryDebug] = React.useState(false);
const { uiFont, monoFont } = useFontPreferences();
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState(() => apis.runtime.isDesktop);
+ const [isVSCodeRuntime, setIsVSCodeRuntime] = React.useState(() => apis.runtime.isVSCode);
const [cliAvailable, setCliAvailable] = React.useState(() => {
if (!apis.runtime.isDesktop) return true;
return isCliAvailable();
@@ -41,7 +43,8 @@ function App({ apis }: AppProps) {
React.useEffect(() => {
setIsDesktopRuntime(apis.runtime.isDesktop);
- }, [apis.runtime.isDesktop]);
+ setIsVSCodeRuntime(apis.runtime.isVSCode);
+ }, [apis.runtime.isDesktop, apis.runtime.isVSCode]);
React.useEffect(() => {
registerRuntimeAPIs(apis);
@@ -165,6 +168,22 @@ function App({ apis }: AppProps) {
);
}
+ // VS Code runtime - simplified layout without git/terminal views
+ if (isVSCodeRuntime) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
return (
diff --git a/packages/ui/src/components/auth/SessionAuthGate.tsx b/packages/ui/src/components/auth/SessionAuthGate.tsx
index 15be9273..8de6c434 100644
--- a/packages/ui/src/components/auth/SessionAuthGate.tsx
+++ b/packages/ui/src/components/auth/SessionAuthGate.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { OpenCodeIcon } from '@/components/ui/OpenCodeIcon';
-import { isDesktopRuntime } from '@/lib/desktop';
+import { isDesktopRuntime, isVSCodeRuntime } from '@/lib/desktop';
import { syncDesktopSettings, initializeAppearancePreferences } from '@/lib/persistence';
import { applyPersistedDirectoryPreferences } from '@/lib/directoryPersistence';
@@ -89,15 +89,17 @@ type GateState = 'pending' | 'authenticated' | 'locked' | 'error';
export const SessionAuthGate: React.FC = ({ children }) => {
const desktopRuntime = React.useMemo(() => isDesktopRuntime(), []);
- const [state, setState] = React.useState(() => (desktopRuntime ? 'authenticated' : 'pending'));
+ const vscodeRuntime = React.useMemo(() => isVSCodeRuntime(), []);
+ const skipAuth = desktopRuntime || vscodeRuntime;
+ const [state, setState] = React.useState(() => (skipAuth ? 'authenticated' : 'pending'));
const [password, setPassword] = React.useState('');
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState('');
const passwordInputRef = React.useRef(null);
- const hasResyncedRef = React.useRef(desktopRuntime);
+ const hasResyncedRef = React.useRef(skipAuth);
const checkStatus = React.useCallback(async () => {
- if (desktopRuntime) {
+ if (skipAuth) {
setState('authenticated');
return;
}
@@ -119,20 +121,20 @@ export const SessionAuthGate: React.FC = ({ children }) =>
console.warn('Failed to check session status:', error);
setState('error');
}
- }, [desktopRuntime]);
+ }, [skipAuth]);
React.useEffect(() => {
- if (desktopRuntime) {
+ if (skipAuth) {
return;
}
void checkStatus();
- }, [checkStatus, desktopRuntime]);
+ }, [checkStatus, skipAuth]);
React.useEffect(() => {
- if (!desktopRuntime && state === 'locked') {
+ if (!skipAuth && state === 'locked') {
hasResyncedRef.current = false;
}
- }, [desktopRuntime, state]);
+ }, [skipAuth, state]);
React.useEffect(() => {
if (state === 'locked' && passwordInputRef.current) {
@@ -142,7 +144,7 @@ export const SessionAuthGate: React.FC = ({ children }) =>
}, [state]);
React.useEffect(() => {
- if (desktopRuntime) {
+ if (skipAuth) {
return;
}
if (state === 'authenticated' && !hasResyncedRef.current) {
@@ -153,7 +155,7 @@ export const SessionAuthGate: React.FC = ({ children }) =>
await applyPersistedDirectoryPreferences();
})();
}
- }, [desktopRuntime, state]);
+ }, [skipAuth, state]);
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
diff --git a/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx b/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx
index a241222b..90a85970 100644
--- a/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx
+++ b/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx
@@ -129,7 +129,7 @@ export const AgentMentionAutocomplete = React.forwardRef
{agents.length ? (
diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx
index f6a5964a..462370c1 100644
--- a/packages/ui/src/components/chat/ChatContainer.tsx
+++ b/packages/ui/src/components/chat/ChatContainer.tsx
@@ -48,9 +48,9 @@ export const ChatContainer: React.FC = () => {
const {
scrollRef,
handleMessageContentChange,
- getAnimationHandlers,
- showScrollButton,
- scrollToBottom,
+ getAnimationHandlers,
+ showScrollButton,
+ scrollToBottom,
spacerHeight,
pendingAnchorId,
hasActiveAnchor,
@@ -179,7 +179,7 @@ export const ChatContainer: React.FC = () => {
if (sessionMessages.length === 0 && !streamingMessageId) {
return (
-
+
@@ -239,7 +239,7 @@ export const ChatContainer: React.FC = () => {