Skip to content

Conversation

@ammar-agent
Copy link
Collaborator

@ammar-agent ammar-agent commented Nov 8, 2025

Introduces a VS Code-style extension system for cmux, allowing users to extend functionality via custom extensions loaded from ~/.cmux/ext/.

Features

Extension Discovery:

  • Loads extensions from ~/.cmux/ext/ (global directory)
  • Supports single-file (.js) and folder-based extensions (with manifest.json)
  • Validates manifests and filters invalid extensions

Extension Execution:

  • Extensions run in isolated child process (Node.js fork)
  • Single global extension host serves all workspaces (VS Code architecture)
  • Scales efficiently: 50 workspaces = 1 process (not 50)

Extension API:

  • Post-tool-use hook: Called after any tool execution in any workspace
  • Runtime interface: Provides workspace context (path, project, read/write files)
  • Workspace registration: Runtimes created/destroyed as workspaces are used/removed

Architecture:

Main Process → spawns once → Extension Host (singleton)
                                    ↓
                        Map<workspaceId, Runtime>
                                    ↓
                            Extensions (loaded once)

Implementation

Core Components

ExtensionManager (src/services/extensions/extensionManager.ts):

  • 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

ExtensionHost (src/services/extensions/extensionHost.ts):

  • Runs in isolated child process
  • Maintains Map<workspaceId, Runtime> for all active workspaces
  • Loads extensions once, routes hook calls to appropriate workspace runtime
  • Handles: init, register-workspace, unregister-workspace, post-tool-use

Discovery (src/utils/extensions/discovery.ts):

  • Scans directory for valid extensions
  • Validates manifests and entry points
  • Returns array of ExtensionInfo objects

Integration

AIService (src/services/aiService.ts):

  • Calls initializeGlobal() in constructor (spawns host once)
  • Calls registerWorkspace() on first message from workspace
  • Extension host hooks into tool execution pipeline

IpcMain (src/services/ipcMain.ts):

  • Calls unregisterWorkspace() when workspace removed
  • Ensures clean runtime lifecycle

Message Protocol

// Host initialization
{ type: "init"; extensions: ExtensionInfo[] }

// Workspace lifecycle
{ type: "register-workspace"; workspaceId, workspacePath, projectPath, runtimeConfig, runtimeTempDir }
{ type: "workspace-registered"; workspaceId }
{ type: "unregister-workspace"; workspaceId }
{ type: "workspace-unregistered"; workspaceId }

// Extension hooks
{ type: "post-tool-use"; workspaceId, payload: { toolName, args, result } }

Testing

Unit Tests (14 tests, all pass):

  • src/utils/extensions/discovery.test.ts - 9 tests covering discovery edge cases
  • src/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:

  • TypeCheck: Both main and renderer processes pass
  • Lint: No errors or warnings
  • Format: All files properly formatted

Example Extensions

Single-file extension (~/.cmux/ext/tool-logger.js):

module.exports = {
  postToolUse: async (toolName, args, result, runtime) => {
    console.log(`Tool used: ${toolName}`);
  }
};

Folder-based extension (~/.cmux/ext/echo-extension/):

// manifest.json
{
  "name": "echo-extension",
  "version": "1.0.0",
  "main": "index.js"
}

Architecture Decision: Global vs Per-Workspace

Chose global host architecture (consistent with VS Code) over per-workspace hosts:

Benefits:

  • Scalability: 50 workspaces = 1 process (not 50)
  • Memory: Extensions loaded once, not duplicated per workspace
  • Performance: No process spawn on workspace creation
  • Standards: Matches VS Code's proven architecture for multi-folder workspaces

Trade-offs:

  • Extension state must be workspace-aware (Runtime interface provides context)
  • More complex message protocol (workspace registration vs simple init)

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 system
  • src/services/extensions/extensionHost.ts - Extension host process entry point
  • src/services/extensions/extensionManager.ts - Extension manager service
  • src/utils/extensions/discovery.ts - Extension discovery utility

Tests (3 files):

  • src/utils/extensions/discovery.test.ts - Discovery unit tests
  • src/services/extensions/extensionManager.test.ts - Manager unit tests
  • tests/extensions/extensions.test.ts - Integration tests

Files Modified

Integration points (3 files):

  • src/services/aiService.ts - Initialize global host, register workspaces
  • src/services/ipcMain.ts - Unregister workspaces on removal
  • src/services/streamManager.ts - Import extension types

Next Steps

  • Update integration tests for new architecture (currently passing but using old patterns)
  • Add more hook types (pre-tool-use, on-message, on-workspace-created, etc.)
  • Document extension API in user-facing docs (docs/)
  • Consider project-local extensions (.cmux/ext/) in addition to global

Generated with cmux

@ammar-agent ammar-agent changed the title 🤖 refactor: single global extension host (VS Code architecture) 🤖 feat: VS Code-style extension system with global host architecture Nov 8, 2025
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_
@ammar-agent ammar-agent force-pushed the plugins branch 8 times, most recently from 1b151f7 to 9372691 Compare November 8, 2025 20:34
…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`_
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.

1 participant