Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
793087f
🤖 feat: add VS Code extension for cmux workspace switching
ammar-agent Nov 11, 2025
d10d02b
🤖 fix: format vscode-extension.md with prettier
ammar-agent Nov 11, 2025
278ea3d
🤖 fix: remove external link from docs
ammar-agent Nov 11, 2025
5f26366
🤖 feat: add VS Code extension to release workflow
ammar-agent Nov 11, 2025
22c26c2
🤖 refactor: add reusable build-vscode-extension action
ammar-agent Nov 11, 2025
d8aa60c
🤖 refactor: clean up VS Code extension code
ammar-agent Nov 12, 2025
8a3565a
🤖 feat: add SQLite metadata store for workspace recency and streaming…
ammar-agent Nov 12, 2025
62c5d9d
🤖 fix: ensure native modules are rebuilt for Electron
ammar-agent Nov 12, 2025
5fdeac0
🤖 refactor: tidy metadata store dir handling, type guards, and extens…
ammar-agent Nov 12, 2025
2b4e576
🤖 feat: show recency timestamp in VS Code extension QuickPick
ammar-agent Nov 12, 2025
66d9c86
🤖 feat: automate shared code sync between main app and VS Code extension
ammar-agent Nov 12, 2025
064f35e
🤖 refactor: use bun instead of npm for VS Code extension build
ammar-agent Nov 12, 2025
bbd35a7
🤖 fix: add --force flag to vscode-ext-install target
ammar-agent Nov 12, 2025
f2e7172
🤖 fix: prevent npm from creating package-lock.json
ammar-agent Nov 12, 2025
d5e3444
🤖 fix: force clean rebuild in vscode-ext target
ammar-agent Nov 12, 2025
450563e
🤖 fix: rebuild better-sqlite3 for Cursor's Node.js version
ammar-agent Nov 12, 2025
5111b94
🤖 refactor: replace SQLite metadata store with JSON
ammar-agent Nov 12, 2025
3b9030a
🤖 refactor: share extension metadata code via sync-shared.sh
ammar-agent Nov 12, 2025
c1cd44b
🤖 refactor: use esbuild to import main app directly
ammar-agent Nov 12, 2025
c1adbe8
🤖 refactor: use Config class from main app in extension
ammar-agent Nov 12, 2025
2295498
🤖 refactor: isolate VS Code extension build system
ammar-agent Nov 12, 2025
77d896b
🤖 fix: bundle extension dependencies from main app's node_modules
ammar-agent Nov 12, 2025
e5f2b73
🤖 fix: preserve recency order when searching workspaces
ammar-agent Nov 12, 2025
046df23
🤖 fix: add TypeScript path mappings for cmux imports
ammar-agent Nov 12, 2025
371e9b6
🤖 refactor: use getProjectName helper for path computation
ammar-agent Nov 12, 2025
b2af101
🤖 refactor: make runtimeConfig required in WorkspaceMetadata
ammar-agent Nov 12, 2025
28ed10a
🤖 refactor: normalize runtimeConfig in config file on load
ammar-agent Nov 12, 2025
8170760
🤖 refactor: convert ExtensionMetadataService to use fs promises
ammar-agent Nov 12, 2025
044fdaf
🤖 refactor: make ExtensionMetadataService stateless with atomic writes
ammar-agent Nov 12, 2025
0a73310
🤖 refactor: remove atomicWrite proxy and make all writes async
ammar-agent Nov 12, 2025
fb4ae34
🤖 fix: replace existsSync with async access() in ExtensionMetadataSer…
ammar-agent Nov 12, 2025
b5e978d
🤖 fix: make addWorkspace async and fix type guards
ammar-agent Nov 12, 2025
aa44b05
🤖 refactor: make config operations properly async
ammar-agent Nov 12, 2025
d31ad50
🤖 refactor: centralize .cmux config root path
ammar-agent Nov 12, 2025
71591f7
docs: add screenshot
ammario Nov 12, 2025
736a853
🤖 refactor: replace path constants with functions accepting optional …
ammar-agent Nov 12, 2025
99d438c
🤖 refactor: remove redundant vscode/DEVELOPMENT.md
ammar-agent Nov 12, 2025
c634bec
🤖 refactor: simplify vscode/README.md
ammar-agent Nov 12, 2025
cea73ba
🤖 refactor: remove unused verify-extension.sh
ammar-agent Nov 12, 2025
c9d76f2
🤖 fix: resolve lint errors from path refactoring
ammar-agent Nov 12, 2025
fa259d3
Simplify docs
ammario Nov 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/actions/build-vscode-extension/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: "Build VS Code Extension"
description: "Build the cmux VS Code extension"

runs:
using: "composite"
steps:
- name: Build VS Code extension
shell: bash
run: make vscode-ext
21 changes: 21 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,24 @@ jobs:
path: release/*.AppImage
retention-days: 30
if-no-files-found: error

build-vscode-extension:
name: Build VS Code Extension
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git describe to find tags

- uses: ./.github/actions/setup-cmux

- uses: ./.github/actions/build-vscode-extension

- name: Upload VS Code extension artifact
uses: actions/upload-artifact@v4
with:
name: vscode-extension
path: vscode/cmux-*.vsix
retention-days: 30
if-no-files-found: error
21 changes: 21 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,24 @@ jobs:
run: bun x electron-builder --linux --publish always
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

build-vscode-extension:
name: Build and Release VS Code Extension
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git describe to find tags

- uses: ./.github/actions/setup-cmux

- uses: ./.github/actions/build-vscode-extension

- name: Upload VS Code extension to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload ${{ github.event.release.tag_name }} \
vscode/cmux-*.vsix \
--clobber
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ include fmt.mk
.PHONY: lint lint-fix typecheck static-check
.PHONY: test test-unit test-integration test-watch test-coverage test-e2e
.PHONY: dist dist-mac dist-win dist-linux
.PHONY: vscode-ext vscode-ext-install
.PHONY: docs docs-build docs-watch
.PHONY: storybook storybook-build test-storybook chromatic
.PHONY: benchmark-terminal
Expand Down Expand Up @@ -83,6 +84,10 @@ node_modules/.installed: package.json bun.lock
# Legacy target for backwards compatibility
ensure-deps: node_modules/.installed





## Help
help: ## Show this help message
@echo 'Usage: make [target]'
Expand Down Expand Up @@ -267,6 +272,14 @@ dist-win: build ## Build Windows distributable
dist-linux: build ## Build Linux distributable
@bun x electron-builder --linux --publish never

## VS Code Extension (delegates to vscode/Makefile)

vscode-ext: ## Build VS Code extension (.vsix)
@$(MAKE) -C vscode build

vscode-ext-install: ## Build and install VS Code extension locally
@$(MAKE) -C vscode install

## Documentation
docs: ## Serve documentation locally
@./scripts/docs.sh
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Here are some specific use cases we enable:
- Isolated workspaces with central view on git divergence
- **Local**: git worktrees on your local machine ([docs](https://cmux.io/local.html))
- **SSH**: regular git clones on a remote server ([docs](https://cmux.io/ssh.html))
- **VS Code Extension**: Jump into cmux workspaces directly from VS Code ([docs](https://cmux.io/vscode-extension.html))
- Multi-model (`sonnet-4-*`, `gpt-5-*`, `opus-4-*`)
- Ollama supported for local LLMs ([docs](https://cmux.io/models.html#ollama-local))
- OpenRouter supported for long-tail of LLMs ([docs](https://cmux.io/models.html#openrouter-cloud))
Expand Down
289 changes: 181 additions & 108 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [SSH](./ssh.md)
- [Forking](./fork.md)
- [Init Hooks](./init-hooks.md)
- [VS Code Extension](./vscode-extension.md)
- [Models](./models.md)
- [Keyboard Shortcuts](./keybinds.md)
- [Vim Mode](./vim-mode.md)
Expand Down
Binary file added docs/img/vscode-ext.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 88 additions & 0 deletions docs/vscode-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# VS Code Extension

The cmux VS Code extension allows you to quickly jump into your cmux workspaces directly from Visual Studio Code or Cursor. This enables a more seamless back and forth between a purely agentic workflow and traditional editing. It's
especially useful for completing the "last mile" of a task or establishing the initial architecture.

## Overview

The extension has a small initial surface area: a command to open a workspace.

![cmux VS Code extension screenshot](./img/vscode-ext.webp)

1. Press `Cmd+Shift+P` (or `Ctrl+Shift+P` on Windows/Linux)
2. Type "cmux: Open Workspace"
- Optional: Set a custom keybinding in the Command Palette settings
3. Select your workspace
4. It opens in a new editor window

The extension works seamlessly with both local and SSH workspaces.

## Installation

### Download

Download the latest `.vsix` file from the [GitHub releases page](https://github.com/coder/cmux/releases).

### Install

**Command line:**

```bash
# For VS Code
code --install-extension cmux-*.vsix

# For Cursor
cursor --install-extension cmux-*.vsix
```

**From editor UI:**

1. Open Command Palette (`Cmd+Shift+P`)
2. Type "Extensions: Install from VSIX..."
3. Select the downloaded file

### Workspace Types

The extension displays workspaces differently based on their type:

- **Local**: `📁 [project-name] workspace-name`
- **SSH**: `🔗 [project-name] workspace-name (ssh: hostname)`

## SSH Workspaces

### Requirements

For SSH workspaces to work, you need:

1. **Remote-SSH Extension** installed
- VS Code: `ms-vscode-remote.remote-ssh`
- Cursor: `anysphere.remote-ssh`
- The extension automatically detects which one you have
2. **SSH host configured** in `~/.ssh/config` or in the Remote-SSH extension

### Setup SSH Host

If you haven't configured the SSH host yet:

1. Open `~/.ssh/config` and add:

```ssh
Host myserver
HostName 192.168.1.100
User username
IdentityFile ~/.ssh/id_rsa
```

2. Or use VS Code's Remote-SSH command:
- `Cmd+Shift+P` → "Remote-SSH: Add New SSH Host..."

## Development

For development instructions, see `vscode/README.md` and `vscode/DEVELOPMENT.md` in the
repository.

## Related

- [Workspaces Overview](./workspaces.md)
- [SSH Workspaces](./ssh.md)
- [VS Code Remote-SSH Documentation](https://code.visualstudio.com/docs/remote/ssh)
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@
"docs:watch": "make docs-watch",
"storybook": "make storybook",
"storybook:build": "make storybook-build",
"test:storybook": "make test-storybook",
"rebuild": "echo \"No native modules to rebuild\""
"test:storybook": "make test-storybook"
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.29",
Expand Down Expand Up @@ -82,6 +81,7 @@
"zod-to-json-schema": "^3.24.6"
},
"devDependencies": {
"@electron/rebuild": "^4.0.1",
"@eslint/js": "^9.36.0",
"@playwright/test": "^1.56.0",
"@storybook/addon-docs": "^10.0.0",
Expand Down
13 changes: 13 additions & 0 deletions src/App.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ProjectConfig } from "./config";
import type { FrontendWorkspaceMetadata } from "./types/workspace";
import type { IPCApi } from "./types/ipc";
import type { ChatStats } from "./types/chatStats";
import { DEFAULT_RUNTIME_CONFIG } from "@/constants/workspace";

// Stable timestamp for visual testing (Apple demo time: Jan 24, 2024, 9:41 AM PST)
const STABLE_TIMESTAMP = new Date("2024-01-24T09:41:00-08:00").getTime();
Expand Down Expand Up @@ -47,6 +48,7 @@ function setupMockAPI(options: {
projectPath,
projectName: projectPath.split("/").pop() ?? "project",
namedWorkspacePath: `/mock/workspace/${branchName}`,
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
}),
list: () => Promise.resolve(mockWorkspaces),
Expand Down Expand Up @@ -173,6 +175,7 @@ export const SingleProject: Story = {
projectPath: "/home/user/projects/my-app",
projectName: "my-app",
namedWorkspacePath: "/home/user/.cmux/src/my-app/main",
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
{
id: "f6g7h8i9j0",
Expand All @@ -192,6 +195,7 @@ export const SingleProject: Story = {
projectPath: "/home/user/projects/my-app",
projectName: "my-app",
namedWorkspacePath: "/home/user/.cmux/src/my-app/bugfix",
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
];

Expand Down Expand Up @@ -247,20 +251,23 @@ export const MultipleProjects: Story = {
projectPath: "/home/user/projects/frontend",
projectName: "frontend",
namedWorkspacePath: "/home/user/.cmux/src/frontend/main",
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
{
id: "2b3c4d5e6f",
name: "redesign",
projectPath: "/home/user/projects/frontend",
projectName: "frontend",
namedWorkspacePath: "/home/user/.cmux/src/frontend/redesign",
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
{
id: "3c4d5e6f7a",
name: "main",
projectPath: "/home/user/projects/backend",
projectName: "backend",
namedWorkspacePath: "/home/user/.cmux/src/backend/main",
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
{
id: "4d5e6f7a8b",
Expand Down Expand Up @@ -292,6 +299,7 @@ export const MultipleProjects: Story = {
projectPath: "/home/user/projects/mobile",
projectName: "mobile",
namedWorkspacePath: "/home/user/.cmux/src/mobile/main",
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
];

Expand Down Expand Up @@ -335,6 +343,7 @@ export const ManyWorkspaces: Story = {
projectPath: "/home/user/projects/big-app",
projectName: "big-app",
namedWorkspacePath: `/home/user/.cmux/src/big-app/${name}`,
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
}));

return <AppWithMocks projects={projects} workspaces={workspaces} />;
Expand Down Expand Up @@ -362,6 +371,7 @@ export const ActiveWorkspaceWithChat: Story = {
projectPath: "/home/user/projects/my-app",
projectName: "my-app",
namedWorkspacePath: "/home/user/.cmux/src/my-app/feature",
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
];

Expand All @@ -388,6 +398,7 @@ export const ActiveWorkspaceWithChat: Story = {
projectPath,
projectName: projectPath.split("/").pop() ?? "project",
namedWorkspacePath: `/mock/workspace/${branchName}`,
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
}),
list: () => Promise.resolve(workspaces),
Expand Down Expand Up @@ -696,6 +707,7 @@ export const MarkdownTables: Story = {
projectPath: "/home/user/projects/my-app",
projectName: "my-app",
namedWorkspacePath: "/home/user/.cmux/src/my-app/feature",
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
];

Expand Down Expand Up @@ -723,6 +735,7 @@ export const MarkdownTables: Story = {
projectPath,
projectName: projectPath.split("/").pop() ?? "project",
namedWorkspacePath: `/mock/workspace/${branchName}`,
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
},
}),
list: () => Promise.resolve(workspaces),
Expand Down
1 change: 1 addition & 0 deletions src/bench/headlessEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export async function createHeadlessEnvironment(
const mockIpcRendererModule = mockedElectron.ipcRenderer;

const ipcMain = new IpcMain(config);
await ipcMain.initialize();
ipcMain.register(mockIpcMainModule, mockWindow);

const dispose = async () => {
Expand Down
12 changes: 6 additions & 6 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,23 @@ describe("Config", () => {
});

describe("getAllWorkspaceMetadata with migration", () => {
it("should migrate legacy workspace without metadata file", () => {
it("should migrate legacy workspace without metadata file", async () => {
const projectPath = "/fake/project";
const workspacePath = path.join(config.srcDir, "project", "feature-branch");

// Create workspace directory
fs.mkdirSync(workspacePath, { recursive: true });

// Add workspace to config without metadata file
config.editConfig((cfg) => {
await config.editConfig((cfg) => {
cfg.projects.set(projectPath, {
workspaces: [{ path: workspacePath }],
});
return cfg;
});

// Get all metadata (should trigger migration)
const allMetadata = config.getAllWorkspaceMetadata();
const allMetadata = await config.getAllWorkspaceMetadata();

expect(allMetadata).toHaveLength(1);
const metadata = allMetadata[0];
Expand All @@ -71,7 +71,7 @@ describe("Config", () => {
expect(workspace.name).toBe("feature-branch");
});

it("should use existing metadata file if present (legacy format)", () => {
it("should use existing metadata file if present (legacy format)", async () => {
const projectPath = "/fake/project";
const workspaceName = "my-feature";
const workspacePath = path.join(config.srcDir, "project", workspaceName);
Expand All @@ -95,15 +95,15 @@ describe("Config", () => {
fs.writeFileSync(metadataPath, JSON.stringify(existingMetadata));

// Add workspace to config (without id/name, simulating legacy format)
config.editConfig((cfg) => {
await config.editConfig((cfg) => {
cfg.projects.set(projectPath, {
workspaces: [{ path: workspacePath }],
});
return cfg;
});

// Get all metadata (should use existing metadata and migrate to config)
const allMetadata = config.getAllWorkspaceMetadata();
const allMetadata = await config.getAllWorkspaceMetadata();

expect(allMetadata).toHaveLength(1);
const metadata = allMetadata[0];
Expand Down
Loading