-
Notifications
You must be signed in to change notification settings - Fork 14
🤖 feat: add VS Code extension for cmux workspace switching #553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ac90e58 to
196894d
Compare
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`_
_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`_
…ion typing\n\n_Generated with _
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
a155fae to
fb4ae34
Compare
- 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`
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`
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Overview
Adds a VS Code/Cursor extension that allows users to quickly open cmux workspaces directly from the editor.
Features
cmux: Open Workspacecommandms-vscode-remote.remote-ssh)anysphere.remote-ssh)~/.cmux/config.jsonInstallation
Usage
Cmd+Shift+P)Users can configure custom keyboard shortcuts via editor settings.
Implementation
vscode/src/vscode/cmux-0.1.0.vsix(~35 KB)vscode-ext,vscode-ext-installdocs/vscode-extension.md,vscode/README.mdTesting
Generated with
cmux