-
Notifications
You must be signed in to change notification settings - Fork 14
🤖 feat: VS Code-style extension system with global host architecture #534
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
Draft
ammar-agent
wants to merge
12
commits into
main
Choose a base branch
from
plugins
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
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
53cd7f3 to
0c9d87c
Compare
Introduces a VS Code-style extension system for cmux, allowing users to extend
functionality via custom extensions loaded from ~/.cmux/ext/.
**Features:**
- Extension discovery from global directory (~/.cmux/ext)
- Single global extension host serves all workspaces (VS Code architecture)
- Post-tool-use hook: Called after any tool execution in any workspace
- Runtime interface: Provides workspace context (path, project, read/write files)
- Scales efficiently: 50 workspaces = 1 process (not 50)
**Architecture:**
Main Process → spawns once → Extension Host (singleton)
↓
Map<workspaceId, Runtime>
↓
Extensions (loaded once)
**ExtensionManager API:**
- initializeGlobal() - Discovers extensions and spawns global host once at startup
- registerWorkspace() - Creates runtime for workspace on first use
- unregisterWorkspace() - Removes workspace runtime when workspace removed
- shutdown() - Cleans up global host on app exit
**Integration:**
- AIService: Calls initializeGlobal() in constructor, registerWorkspace() on first message
- IpcMain: Calls unregisterWorkspace() when workspace removed
**Testing:**
- All unit tests pass (14 tests: 9 discovery + 5 manager)
- Integration tests pass (3 tests)
- Static checks pass (typecheck, lint, format)
_Generated with `cmux`_
…ck usage in extensionManager.test.ts - Document testing without mocks, prefer real IPC/processes and temp dirs - Remove unused bun:test mock patterns from unit tests _Generated with cmux_
…ction - Add 'Testing without Mocks (preferred)' guidance in both AGENTS.md and docs/AGENTS.md - Deduplicate accidentally duplicated section in root AGENTS.md _Generated with cmux_
…rent in extensionManager tests
**aiService.ts:**
- Extract duplicated `metadata.runtimeConfig ?? { type: 'local', ... }` pattern into getWorkspaceRuntimeConfig() helper
**extensionManager.test.ts:**
- Use async fs (fs/promises) instead of sync fs operations
- Remove global test variables, use local vars in beforeEach/afterEach for isolation
- Add test.concurrent() to all tests for parallel execution
- Remove eslint-disable for sync fs methods
**AGENTS.md & docs/AGENTS.md:**
- Document preference for async fs in tests (never sync fs)
- Document preference for test.concurrent() to enable parallelization
- Note to avoid global vars in test files for proper isolation
_Generated with `cmux`_
…ypes Create extension test fixtures as actual files with JSDoc type imports: - tests/extensions/fixtures/simple-logger.js - tests/extensions/fixtures/folder-extension/ - tests/extensions/fixtures/broken-extension.js - tests/extensions/fixtures/working-extension.js - tests/extensions/fixtures/minimal-extension.js Benefits: - Syntax highlighting and IDE support when editing test extensions - Type-safe via JSDoc imports from @/types/extensions - Serve as examples for extension developers - Easier to debug than inline strings Updated integration tests to copy fixtures instead of writing inline strings. _Generated with `cmux`_
- One host process per extension for crash isolation - Capnweb RPC for type-safe IPC communication - Full path extension IDs (eliminates conflicts) - Workspace filtering (project extensions see only their project) - NodeIpcTransport adapter for capnweb over Node.js IPC - Fixed extensionHost compilation in tsconfig.main.json - Removed unused legacy message types - All extension tests passing (3/3)
- Extensions receive full Runtime interface with file/bash operations - Extensions can modify tool results before AI sees them - Result modification via return value (undefined = unchanged) - Extensions called via completeToolCall before result stored - Added comprehensive extension documentation to docs/extensions.md - Updated test fixtures to demonstrate Runtime API and result modification - Added result-modifier.ts fixture as example
Changed from 'cmux' to '@coder/cmux/ext' throughout docs/extensions.md
- Runtime.exec() is the primary API (not writeFile with options) - Updated all examples to use runtime.exec() for file operations - Documented actual Runtime interface (streaming primitives) - Added examples for write/append/read using exec
- Better typed Extension interface with detailed JSDoc comments - Removed non-existent workspace operations from Runtime docs - Added ExecOptions, ExecStream, FileStat interfaces - Added 'Common Patterns' section for typical runtime.exec() usage - Clarified that result/args types are unknown (tool-specific)
- Use runtime.exec() instead of non-existent writeFile(path, content, options) - All fixtures now use bash commands for file operations - Rebuilt types so fixtures see updated Extension interface (returns unknown) - Tests still pass
- Created ToolUsePayload discriminated union by toolName
- Each tool has specific args and result types (BashToolArgs, BashToolResult, etc.)
- PostToolUseHookPayload = ToolUsePayload & { runtime: Runtime }
- TypeScript narrows args/result types based on toolName check
- Updated docs with type-safe example showing discrimination
- Removed unknown types in favor of specific tool types
- Tests pass, types are fully type-safe
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.
Introduces a VS Code-style extension system for cmux, allowing users to extend functionality via custom extensions loaded from
~/.cmux/ext/.Features
Extension Discovery:
~/.cmux/ext/(global directory).js) and folder-based extensions (withmanifest.json)Extension Execution:
Extension API:
Architecture:
Implementation
Core Components
ExtensionManager (
src/services/extensions/extensionManager.ts):initializeGlobal()- Discovers extensions and spawns global host once at startupregisterWorkspace()- Creates runtime for workspace on first useunregisterWorkspace()- Removes workspace runtime when workspace removedshutdown()- Cleans up global host on app exitExtensionHost (
src/services/extensions/extensionHost.ts):Map<workspaceId, Runtime>for all active workspacesinit,register-workspace,unregister-workspace,post-tool-useDiscovery (
src/utils/extensions/discovery.ts):ExtensionInfoobjectsIntegration
AIService (
src/services/aiService.ts):initializeGlobal()in constructor (spawns host once)registerWorkspace()on first message from workspaceIpcMain (
src/services/ipcMain.ts):unregisterWorkspace()when workspace removedMessage Protocol
Testing
✅ Unit Tests (14 tests, all pass):
src/utils/extensions/discovery.test.ts- 9 tests covering discovery edge casessrc/services/extensions/extensionManager.test.ts- 5 tests covering manager lifecycle✅ Integration Tests (3 tests, all pass):
tests/extensions/extensions.test.ts- End-to-end extension execution✅ Static Checks:
Example Extensions
Single-file extension (
~/.cmux/ext/tool-logger.js):Folder-based extension (
~/.cmux/ext/echo-extension/):Architecture Decision: Global vs Per-Workspace
Chose global host architecture (consistent with VS Code) over per-workspace hosts:
Benefits:
Trade-offs:
This is the right choice for cmux's "parallel agentic development" use case where users frequently have many workspaces open.
Files Added
Core (7 files):
src/types/extensions.ts- Type definitions for extension systemsrc/services/extensions/extensionHost.ts- Extension host process entry pointsrc/services/extensions/extensionManager.ts- Extension manager servicesrc/utils/extensions/discovery.ts- Extension discovery utilityTests (3 files):
src/utils/extensions/discovery.test.ts- Discovery unit testssrc/services/extensions/extensionManager.test.ts- Manager unit teststests/extensions/extensions.test.ts- Integration testsFiles Modified
Integration points (3 files):
src/services/aiService.ts- Initialize global host, register workspacessrc/services/ipcMain.ts- Unregister workspaces on removalsrc/services/streamManager.ts- Import extension typesNext Steps
docs/).cmux/ext/) in addition to globalGenerated with
cmux