Skip to content

Conversation

@ammar-agent
Copy link
Collaborator

Overview

Adds a VS Code/Cursor extension that allows users to quickly open cmux workspaces directly from the editor.

Features

  • Command Palette Integration: Access cmux workspaces via cmux: Open Workspace command
  • Local Workspaces: Opens local cmux workspaces (git worktrees) in new editor windows
  • SSH Workspaces: Opens remote SSH workspaces using Remote-SSH extension
    • Automatically detects VS Code Remote-SSH (ms-vscode-remote.remote-ssh)
    • Automatically detects Cursor Remote-SSH (anysphere.remote-ssh)
  • UI-Only Extension: Runs locally, available in all contexts (local and remote workspaces)
  • Smart Detection: Reads workspace list from ~/.cmux/config.json
  • Error Handling: Provides clear guidance for SSH setup and missing extensions

Installation

# Build extension
make vscode-ext

# Install locally
make vscode-ext-install

Usage

  1. Open Command Palette (Cmd+Shift+P)
  2. Type "cmux: Open Workspace"
  3. Select workspace from list
  4. Workspace opens in new window

Users can configure custom keyboard shortcuts via editor settings.

Implementation

  • Extension source: vscode/src/
  • Package: Built to vscode/cmux-0.1.0.vsix (~35 KB)
  • Makefile targets: vscode-ext, vscode-ext-install
  • Documentation: docs/vscode-extension.md, vscode/README.md

Testing

  • ✅ Compiles without errors
  • ✅ Config reading verified with real data (59 workspaces)
  • ✅ Packages successfully
  • ✅ Installs on Cursor and VS Code
  • ✅ SSH workspace detection works

Generated with cmux

Adds a VS Code/Cursor extension that allows users to quickly open cmux
workspaces directly from the editor.

Features:
- Command to open cmux workspaces from Command Palette
- Support for local workspaces (git worktrees)
- Support for SSH workspaces via Remote-SSH extension
- Automatically detects VS Code and Cursor Remote-SSH extensions
- Extension runs locally (UI-only) for availability in all contexts
- Smart workspace detection from ~/.cmux/config.json
- Error handling and setup guidance for SSH workspaces

Users can set custom keyboard shortcuts via editor preferences.

_Generated with `cmux`_
mdbook linkcheck doesn't allow links outside the docs directory.
Changed to a plain text reference instead.

_Generated with `cmux`_
Add build-vscode-extension job to release workflow that:
- Builds the VS Code extension (make vscode-ext)
- Uploads .vsix file to GitHub Releases
- Runs in parallel with macOS/Linux builds

The extension will now be available for download from GitHub
Releases alongside the DMG and AppImage files.

_Generated with `cmux`_
Create composite action for building VS Code extension to avoid
duplication between build and release workflows.

Changes:
- Created .github/actions/build-vscode-extension/action.yml
- Updated release.yml to use the composite action
- Added build-vscode-extension job to build.yml (CI workflow)
- Extension now builds on every PR and uploads as artifact

This ensures the extension builds are tested in CI and available
as artifacts for every PR.

_Generated with `cmux`_
- Remove redundant condition check in formatWorkspaceLabel
- Remove console.log from activate function
- Simplify path handling in getRemoteWorkspacePath
- Replace .then() chains with async/await for better readability
- Remove unused getRemoteSshExtensionId function
… status

Implements backend-only metadata store using SQLite to track:
- Workspace recency (last user interaction)
- Streaming status (active/idle)
- Last model used

Backend changes:
- New MetadataStore service (171 LoC) with WAL mode for concurrent access
- Integrated into IpcMain with event listeners for stream lifecycle
- Updates on sendMessage, stream start/end/abort, workspace deletion
- Clears stale streaming flags on app startup

Extension changes:
- Reads from ~/.cmux/metadata.db with readonly access
- Sorts workspaces by recency (most recent first)
- Shows $(sync~spin) icon for actively streaming workspaces
- Gracefully falls back to createdAt if metadata missing

Performance:
- Sub-millisecond reads/writes
- WAL mode prevents reader/writer contention
- Scales to 1000+ workspaces

Future: Documented as experimental foundation for migrating full config.json
to SQLite for better performance and queryability.

_Generated with `cmux`_
Problem: better-sqlite3 was compiled for system Node.js (v20), but Electron
uses its own embedded Node.js version (v38), causing MODULE_VERSION mismatch
errors at runtime.

Solution:
- Added @electron/rebuild to devDependencies
- Added postinstall hook to automatically rebuild native modules
- Updated Makefile to rebuild on `bun install`
- Added `make rebuild` target for manual rebuilds
- Created docs/development/native-modules.md explaining the issue

This ensures:
- Native modules always match Electron's Node.js version
- New developers don't hit module version errors
- CI builds work consistently across platforms
- Build system is resilient to native module additions

The postinstall hook runs after every `bun install`, automatically rebuilding
better-sqlite3 for Electron's Node.js version. This is slightly slower but
prevents hard-to-debug runtime errors.

_Generated with `cmux`_
Display 'Last used: X time ago' instead of 'Created: date' when recency
metadata is available. Falls back to created date for workspaces without
recent usage.

_Generated with `cmux`_
Add build-time script that copies shared utilities and types from main app
to extension, eliminating ~60 LoC of duplication and preventing drift.

Changes:
- Created vscode/scripts/sync-shared.sh to copy dateTime.ts and extract types
- Hooked sync into build pipeline via precompile script
- Updated extension to import formatRelativeTime from shared/dateTime
- Updated extension to import RuntimeConfig and WorkspaceMetadata from shared/types
- Added vscode/src/shared/ to .gitignore (generated at build time)

Benefits:
- Zero drift risk: extension always uses latest code from main app
- Zero maintenance: automatic sync on every build
- Single source of truth: main app types are canonical
- Fail-fast: build breaks if types become incompatible

_Generated with `cmux`_
Remove package-lock.json and switch package.json scripts to use 'bun run'
instead of 'npm run' for consistency with main app.

_Generated with `cmux`_
Always force reinstall the extension to ensure latest version is used,
avoiding stale extension issues.

_Generated with `cmux`_
Add package-lock.json to .gitignore and set packageManager field to tell
vsce to use bun. npm creates package-lock.json whenever 'npm run' is invoked,
which vsce does during the prepublish phase.

_Generated with `cmux`_
Remove out/, src/shared/, and .vsix before building to ensure
changes are picked up. Previously, incremental builds could miss
source changes.

Also add debug logging to extension to diagnose metadata reading issues.

_Generated with `cmux`_
Add electron-rebuild to recompile better-sqlite3 native module for
Electron 34 / Node 24 (MODULE_VERSION 136) which Cursor uses.

Previously, the module was compiled against Node 20 (MODULE_VERSION 115)
causing a version mismatch error when the extension tried to load it.

_Generated with `cmux`_
Migrates from SQLite-based MetadataStore to JSON-based ExtensionMetadataService
to eliminate native module version conflicts across different Electron versions.

Changes:
- New ExtensionMetadataService using ~/.cmux/extensionMetadata.json
- Removed better-sqlite3 dependency from both main app and extension
- Updated ipcMain.ts to use new service (same API surface)
- Updated VS Code extension to read JSON file directly
- Removed electron-rebuild scripts and dependencies
- Fixed Makefile to use npx vsce package --no-dependencies

Benefits:
- Extension size: 3.8 MB → 56 KB (98.5% reduction)
- No native module version conflicts
- Simpler architecture, easier to debug
- Same functionality preserved

Generated with `cmux`
Extracts metadata reading logic to shared utility to eliminate duplication
between main app and VS Code extension.

Changes:
- New src/utils/extensionMetadata.ts with shared types and reading logic
- ExtensionMetadataService now uses shared types/helpers
- Updated sync-shared.sh to copy extensionMetadata.ts to extension
- Extension cmuxConfig.ts now imports from shared/extensionMetadata
- Removed ~40 LoC of duplicate code from extension

Benefits:
- Single source of truth for metadata reading
- Easier to maintain (changes only in one place)
- Extension continues to work without node_modules bloat

Generated with `cmux`
Replace file copying with direct imports from main app using esbuild bundler.
Tree-shaking eliminates unused code while maintaining single source of truth.

Changes:
- Add esbuild as build tool (replaces tsc for bundling)
- Add main app as local dependency (file:..)
- Create esbuild.config.js with custom cmux/* resolver plugin
- Update extension imports: cmux/utils/*, cmux/types/*
- Remove sync-shared.sh script (no longer needed)
- Update Makefile build target

Results:
- Bundle size: 5.2 KB (down from 60 KB with separate files)
- Extension size: 53 KB (down from 56 KB)
- Zero code duplication
- Single source of truth for all shared code
- Build time: ~100ms (esbuild is extremely fast)

Generated with `cmux`
Replace duplicated config reading logic with direct import of Config class
from main app. Extension now uses getAllWorkspaceMetadata() and enriches
with extension-specific metadata (recency, streaming).

Changes:
- Import Config class from cmux/config
- Remove readCmuxConfig() - use Config.loadConfigOrDefault()
- Remove ProjectConfig, CmuxConfig type duplicates
- Simplify getAllWorkspaces() to use Config.getAllWorkspaceMetadata()
- Keep only extension-specific enrichment logic

Results:
- Code: 135 LoC → 92 LoC (43 LoC removed, 32% reduction)
- Bundle: 5.2 KB → 21 KB (includes Config class, still small)
- Extension: 53 KB → 58 KB (5 KB increase, acceptable)
- Zero config logic duplication
- Single source of truth for workspace metadata

Generated with `cmux`
- Move extension build targets to vscode/Makefile for better isolation
- Remove unnecessary 'cmux: file:..' dependency that caused hanging
  - Extension uses esbuild custom resolver to import main app code
  - No runtime dependencies needed, only devDependencies (esbuild, vsce)
- Main Makefile now delegates to vscode/Makefile via $(MAKE) -C
- Add clean target to extension Makefile
- Add sentinel file for dependency tracking in extension

Build time: ~2.5s (was hanging indefinitely with runtime dependency)
Bundle size: 21 KB (unchanged)
Extension package: 59 KB (includes Makefile, scripts, docs)
The extension was failing to activate with:
  Error: Cannot find module './impl/format'

Root cause: jsonc-parser and write-file-atomic were not being bundled.
When we removed 'cmux: file:..' dependency, extension no longer had
access to main app's dependencies at runtime.

Solution:
- Configure esbuild to resolve dependencies from ../node_modules
- Use 'mainFields: ["module", "main"]' to prefer ESM over UMD
  - UMD bundles have AMD/require issues with relative paths
  - ESM bundles properly with esbuild

Results:
- Bundle size: 21 KB → 27 KB (includes jsonc-parser, write-file-atomic)
- Package size: 59 KB → 74 KB
- Extension now includes all required dependencies
VS Code's showQuickPick automatically re-sorts results by fuzzy match score,
which broke recency ordering during search. Most recently used workspaces
would get pushed down the list if they didn't match well.

Solution: Use createQuickPick with manual filtering to preserve recency order.
- Custom onDidChangeValue handler filters items with simple .includes()
- Filtered results maintain their original recency-based sort order
- Most recently used workspaces stay at top during search

This matches the main cmux app behavior where recency is always primary sort.
IDE was showing errors like 'Property runtimeConfig does not exist on type WorkspaceWithContext'
because TypeScript couldn't resolve cmux/* imports.

Solution: Add path mappings to tsconfig.json so IDE can resolve types:
- cmux/* -> ../src/*
- @/* -> ../src/*

Removed rootDir constraint so TypeScript can resolve types from parent directory
without trying to compile the entire main app.

TypeScript now properly resolves all types from main app without errors.
Extension was duplicating path computation logic from LocalRuntime and SSHRuntime.

Solution: Import getProjectName() helper and implement path logic using same
approach as Runtime classes, but without importing entire Runtime implementations
(which would add 60+ KB to bundle).

Changes:
- Import getProjectName() from cmux/utils/runtime/helpers
- Implement getWorkspacePath() using same logic as LocalRuntime
- Implement getSSHWorkspacePath() using same logic as SSHRuntime
- Renamed getRemoteWorkspacePath -> getSSHWorkspacePath for clarity

Result:
- Path logic stays in sync with main app (uses shared helper)
- Bundle size unchanged: 27 KB (no Runtime bloat)
- Zero duplication of path extraction logic (getProjectName shared)
- Comments reference Runtime implementations for maintainability
WorkspaceMetadata.runtimeConfig is now always present, initialized to local
runtime defaults on startup. This eliminates optional chaining throughout
the codebase and simplifies runtime path computation.

Changes:
- Created DEFAULT_RUNTIME_CONFIG constant in src/constants/workspace.ts
- Updated Config.getAllWorkspaceMetadata() to ensure all workspaces have runtimeConfig
- Made WorkspaceMetadata.runtimeConfig required (removed optional '?')
- Updated VS Code extension to remove all optional chaining (runtimeConfig?.type)
- Fixed all test files to include runtimeConfig in workspace metadata objects
- Fixed service files (agentSession.ts, ipcMain.ts) to include runtimeConfig
Instead of applying DEFAULT_RUNTIME_CONFIG on-the-fly, we now migrate
missing runtimeConfig to the config file during load. This makes the
config file the single source of truth and eliminates runtime conversion.

Changes:
- Removed ensureRuntimeConfig() and getDefaultRuntimeConfig() methods
- Apply DEFAULT_RUNTIME_CONFIG directly when loading workspaces
- Save normalized config back to disk (configModified = true)
- All 4 workspace loading paths now write runtimeConfig to config

Benefits:
- Config file is normalized once, not on every read
- Simpler code - no runtime conversions needed
- Easier to remove this migration code in the future
Replaced synchronous fs operations with async fs/promises throughout
ExtensionMetadataService. Updated IpcMain and all callers to use static
factory methods for async initialization.

Changes:
- ExtensionMetadataService: Use fs/promises (readFile, writeFile, mkdir)
- Added ExtensionMetadataService.create() static factory method
- Made all write methods async (updateRecency, setStreaming, deleteWorkspace)
- Added IpcMain.create() static factory method for async initialization
- Updated all callers: main-desktop.ts, main-server.ts, tests/ipcMain/setup.ts, bench/headlessEnvironment.ts
- Used 'void' keyword for fire-and-forget async calls in event handlers

Benefits:
- Non-blocking I/O operations
- Consistent async patterns throughout codebase
- Better error handling potential
- No blocking main thread on file operations
Redesigned ExtensionMetadataService to be stateless and use atomic writes.
Created reusable atomic write utilities to centralize write-file-atomic usage.

Changes:
- Created src/utils/atomicWrite.ts with writeFileAtomically() and writeFileAtomicallySync()
- ExtensionMetadataService now uses load-modify-save pattern (no in-memory state)
- All read methods now async (load from disk each time)
- All write methods use atomic writes via writeFileAtomically()
- Simplified IpcMain to use regular constructor + initialize() method
- Updated Config.ts to use new writeFileAtomicallySync() utility

Benefits:
- Stateless: no stale data, always fresh from disk
- Atomic writes: prevents corruption from crashes/concurrent writes
- Reusable: atomic write logic centralized for use across codebase
- Simpler: no cache invalidation or state management needed

Trade-offs:
- Slightly higher disk I/O (acceptable for read-heavy workload)
- Simplicity and correctness over performance optimization
Removed the atomicWrite.ts proxy file and import write-file-atomic directly.
Converted all Config file writes to async, eliminating sync operations.

Changes:
- Deleted src/utils/atomicWrite.ts (unnecessary proxy)
- Config.saveConfig() now async
- Config.saveSecretsConfig() now async
- Config.editConfig() now async
- Config.updateProjectSecrets() now async
- Updated all callers to await these methods
- ExtensionMetadataService imports write-file-atomic directly
- Updated tests to handle async config methods

Benefits:
- Simpler: no proxy layer, direct imports
- Consistent: all file writes are now async
- Non-blocking: no sync I/O operations blocking the event loop
- Better performance: async I/O allows Node.js to handle other work
…vice

Replaced all sync fs operations (existsSync) with async alternatives using
fs/promises access(). This eliminates the last sync operations in the service.

Changes:
- Import access() and constants from fs/promises and fs
- Replace existsSync checks with try/catch on access()
- initialize(): Check directory existence with access() before mkdir
- load(): Check file existence with access() before readFile

Why ESLint didn't catch this:
- The 'local/no-sync-fs-methods' rule only catches fs.methodSync() usage
- Direct imports like 'import { existsSync } from "fs"' are not detected
- Rule needs enhancement to check ImportSpecifier nodes

Next steps:
- Enhance ESLint rule to catch direct imports of sync methods
- Or add src/utils/extensionMetadata.ts to allow list if sync is needed there
ammar-agent and others added 8 commits November 12, 2025 13:06
- Made Config.addWorkspace() async (editConfig is async)
- Made AgentSession.ensureMetadata() async and await addWorkspace
- Fixed ipcMain type guards to avoid 'as any' casts
- Fixed config tests to wait for async save to complete
- Removed unused RuntimeConfig import from config.ts
- Made getAllWorkspaceMetadata() async to properly await config saves
- Made getWorkspaceMetadata() async since it calls getAllWorkspaceMetadata
- Updated all callers throughout codebase to await these methods
- VS Code extension functions now properly async
- Config tests no longer need artificial delays
- Fire-and-forget pattern wrapped with void for event handlers

This fixes E2E test race conditions where config saves weren't
completing before reads, causing workspaces to not appear in UI.
Created src/constants/paths.ts with CMUX_HOME and related path constants.
All hardcoded ~/.cmux paths now import from a single source of truth.

Changes:
- New file: src/constants/paths.ts with getCmuxHome() function and path constants
- Updated Config class to use CMUX_HOME instead of inline path.join()
- Updated ExtensionMetadataService to use CMUX_EXTENSION_METADATA_FILE
- Updated systemMessage.ts to use getCmuxHome() for dynamic path resolution
- Updated debug scripts to use CMUX_SESSIONS_DIR and CMUX_SRC_DIR
- Updated main-desktop.ts to use CMUX_HOME for e2e test userData

Benefits:
- Single source of truth for config root directory
- Easier to maintain and update paths
- Better test isolation with getCmuxHome() supporting homedir mocking
- Reduced code duplication across 7+ files

Generated with `cmux`
…rootDir

Removed all module-level CMUX_* constants and replaced them with
functions that accept an optional rootDir parameter. This eliminates
brittle process-level state and enables proper dependency injection.

Changes:
- src/constants/paths.ts: All constants → functions with optional rootDir
  - getCmuxSrcDir(rootDir?)
  - getCmuxSessionsDir(rootDir?)
  - getCmuxConfigFile(rootDir?)
  - getCmuxProvidersFile(rootDir?)
  - getCmuxSecretsFile(rootDir?)
  - getCmuxExtensionMetadataPath(rootDir?)
- src/config.ts: Use getCmuxHome() instead of CMUX_HOME constant
- src/utils/extensionMetadata.ts: Accept rootDir parameter
- src/services/ipcMain.ts: Pass config.rootDir to ExtensionMetadataService
- src/debug/*.ts: Call functions instead of referencing constants
- src/main-desktop.ts: Use getCmuxHome() instead of CMUX_HOME

Benefits:
- No process-level state (CMUX_TEST_ROOT only in getCmuxHome())
- Tests can mock os.homedir() and it works correctly
- Consistent with Config class dependency injection pattern
- Explicit path dependencies through the call chain

All tests passing (1001 pass, 1 skip, 0 fail).

Generated with `cmux`
Standard VS Code extension development workflow - nothing non-obvious here.
Build commands are in Makefile/package.json, F5 to debug is standard.

Generated with `cmux`
Reduced from 156 lines to 34 lines. Removed:
- Verbose step-by-step instructions for obvious tasks
- Redundant feature list (it's a workspace opener, that's the feature)
- Excessive troubleshooting (error messages are self-explanatory)
- Standard VS Code development workflow explanations
- Unnecessary sections (License, Contributing, Related Links)

Generated with `cmux`
Not referenced anywhere (Makefile, CI, docs). Redundant checks:
- TypeScript compilation fails if source files missing
- Package command fails if compilation fails
- File existence checks are obvious

Generated with `cmux`
@ammario ammario marked this pull request as ready for review November 12, 2025 21:37
ammar-agent and others added 2 commits November 12, 2025 15:40
Fixed three lint issues:
1. Removed unused 'os' import from src/config.ts
2. Removed unused 'os' and 'path' imports from src/services/systemMessage.ts
3. Added eslint-disable for process.env in getCmuxHome() (main-only code)

The process.env access in paths.ts is safe - file is only used by main
process code despite living in constants/ for organization.

Generated with `cmux`
@ammario ammario mentioned this pull request Nov 12, 2025
@ammario ammario enabled auto-merge November 12, 2025 21:50
@ammario ammario added this pull request to the merge queue Nov 12, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Nov 12, 2025
@ammario ammario merged commit 818a3d9 into main Nov 13, 2025
16 checks passed
@ammario ammario deleted the vscode-ext branch November 13, 2025 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants