Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ Here are some specific use cases we enable:

## Features

- **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))
- **Isolated workspaces** with central view on git divergence ([docs](https://cmux.io/runtime.html))
- **[Local](https://cmux.io/runtime/local.html)**: run directly in your project directory
- **[Worktree](https://cmux.io/runtime/worktree.html)**: git worktrees on your local machine
- **[SSH](https://cmux.io/runtime/ssh.html)**: remote execution on a server over SSH
- **Multi-model** (`sonnet-4-*`, `grok-*`, `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
6 changes: 4 additions & 2 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
# Features

- [Workspaces](./workspaces.md)
- [Local](./local.md)
- [SSH](./ssh.md)
- [Runtimes](./runtime.md)
- [Local](./runtime/local.md)
- [Worktree](./runtime/worktree.md)
- [SSH](./runtime/ssh.md)
- [Forking](./fork.md)
- [Init Hooks](./init-hooks.md)
- [VS Code Extension](./vscode-extension.md)
Expand Down
23 changes: 23 additions & 0 deletions docs/runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Runtimes

Runtimes determine where and how mux executes agent workspaces.

| Runtime | Isolation | Best For |
| ------------------------------------- | ------------------------------------------ | ---------------------------------------- |
| **[Local](./runtime/local.md)** | All workspaces share the project directory | Quick edits to your working copy |
| **[Worktree](./runtime/worktree.md)** | Each workspace gets its own directory | Running multiple agents in parallel |
| **[SSH](./runtime/ssh.md)** | Remote execution over SSH | Security, performance, heavy parallelism |

## Choosing a Runtime

When creating a workspace, select the runtime from the dropdown in the workspace creation UI.

## Init Hooks

[Init hooks](./init-hooks.md) can detect the runtime type via the `MUX_RUNTIME` environment variable:

- `local` — Local runtime
- `worktree` — Worktree runtime
- `ssh` — SSH runtime

This lets your init hook adapt behavior, e.g., skip worktree-specific setup when running in local mode.
19 changes: 19 additions & 0 deletions docs/runtime/local.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Local Runtime

Local runtime runs the agent directly in your project directory—the same directory you use for development. There's no worktree isolation; the agent works in your actual working copy.

## When to Use

- Quick one-off tasks in your current working copy
- Reviewing agent work alongside your own uncommitted changes
- Projects where worktrees don't work well (e.g., some monorepos)

## Caveats

⚠️ **No isolation**: Multiple local workspaces for the same project see and modify the same files. Running them simultaneously can cause conflicts. mux shows a warning when another local workspace is actively streaming.

⚠️ **Affects your working copy**: Agent changes happen in your actual project directory.

## Filesystem

The workspace path is your project directory itself. No additional directories are created.
8 changes: 4 additions & 4 deletions docs/ssh.md → docs/runtime/ssh.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# SSH Workspaces
# SSH Runtime

mux supports using remote hosts over SSH for workspaces. When configured, all tool operations will
execute over SSH and the agent is securely isolated from your local machine.

Our security architecture considers the remote machine potentially hostile. No keys or credentials are implicitly transferred there—just the git archive and [Project Secrets](./project-secrets.md).
Our security architecture considers the remote machine potentially hostile. No keys or credentials are implicitly transferred there—just the git archive and [Project Secrets](../project-secrets.md).

We highly recommend using SSH workspaces for an optimal experience:

- **Security**: Prompt injection risk is contained to the credentials / files on the remote machine.
- SSH remotes pair nicely with [agentic git identities](./agentic-git-identity.md)
- SSH remotes pair nicely with [agentic git identities](../agentic-git-identity.md)
- **Performance**: Run many, many agents in parallel while maintaining good battery life and UI performance

![ssh workspaces](./img/new-workspace-ssh.webp)
![ssh workspaces](../img/new-workspace-ssh.webp)

The Host can be:

Expand Down
6 changes: 3 additions & 3 deletions docs/local.md → docs/runtime/worktree.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Local Workspaces
# Worktree Runtime

Local workspaces use [git worktrees](https://git-scm.com/docs/git-worktree) on your local machine. Worktrees share the `.git` directory with your main repository while maintaining independent working changes and checkout state.
Worktree runtime uses [git worktrees](https://git-scm.com/docs/git-worktree) on your local machine. Worktrees share the `.git` directory with your main repository while maintaining independent working changes and checkout state.

## How Worktrees Work

Expand All @@ -10,7 +10,7 @@ It's important to note that a **worktree is not locked to a branch**. The agent

## Filesystem Layout

Local workspaces are stored in `~/.mux/src/<project-name>/<workspace-name>`.
Worktree workspaces are stored in `~/.mux/src/<project-name>/<workspace-name>`.

Example layout:

Expand Down
2 changes: 1 addition & 1 deletion docs/vscode-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,5 @@ repository.
## Related

- [Workspaces Overview](./workspaces.md)
- [SSH Workspaces](./ssh.md)
- [SSH Runtime](./runtime/ssh.md)
- [VS Code Remote-SSH Documentation](https://code.visualstudio.com/docs/remote/ssh)
19 changes: 11 additions & 8 deletions docs/workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@

Workspaces in mux provide isolated development environments for parallel agent work. Each workspace maintains its own Git state, allowing you to explore different approaches, run multiple tasks simultaneously, or test changes without affecting your main repository.

## Workspace Types
## Runtimes

mux supports two workspace backends:
mux supports three [runtime types](./runtime.md):

- **[Local Workspaces](./local.md)**: Use [git worktrees](https://git-scm.com/docs/git-worktree) on your local machine. Worktrees share the `.git` directory with your main repository while maintaining independent working changes.
- **[Local](./runtime/local.md)**: Run directly in your project directory. No isolation—best for quick edits to your working copy.

- **[SSH Workspaces](./ssh.md)**: Regular git clones on a remote server accessed via SSH. These are completely independent repositories stored on the remote machine.
- **[Worktree](./runtime/worktree.md)**: Isolated directories using [git worktrees](https://git-scm.com/docs/git-worktree). Worktrees share `.git` with your main repository while maintaining independent working changes.

## Choosing a Backend
- **[SSH](./runtime/ssh.md)**: Remote execution over SSH. Ideal for heavy workloads, security isolation, or leveraging remote infrastructure.

The workspace backend is selected when you create a workspace:
## Choosing a Runtime

- **Local**: Best for fast iteration, local testing, and when you want to leverage your local machine's resources
- **SSH**: Ideal for heavy workloads, long-running tasks, or when you need access to remote infrastructure
The runtime is selected when you create a workspace:

- **Local**: Quick tasks in your current working copy
- **Worktree**: Best for parallel agent work with isolation
- **SSH**: Heavy workloads, security, or remote infrastructure

## Key Concepts

Expand Down
45 changes: 25 additions & 20 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -559,26 +559,31 @@ function AppInner() {
<div className="mobile-main-content flex min-w-0 flex-1 flex-col overflow-hidden">
<div className="mobile-layout flex flex-1 overflow-hidden">
{selectedWorkspace ? (
<ErrorBoundary
workspaceInfo={`${selectedWorkspace.projectName}/${selectedWorkspace.namedWorkspacePath?.split("/").pop() ?? selectedWorkspace.workspaceId}`}
>
<AIView
key={selectedWorkspace.workspaceId}
workspaceId={selectedWorkspace.workspaceId}
projectName={selectedWorkspace.projectName}
branch={
selectedWorkspace.namedWorkspacePath?.split("/").pop() ??
selectedWorkspace.workspaceId
}
namedWorkspacePath={selectedWorkspace.namedWorkspacePath ?? ""}
runtimeConfig={
workspaceMetadata.get(selectedWorkspace.workspaceId)?.runtimeConfig
}
incompatibleRuntime={
workspaceMetadata.get(selectedWorkspace.workspaceId)?.incompatibleRuntime
}
/>
</ErrorBoundary>
(() => {
const currentMetadata = workspaceMetadata.get(selectedWorkspace.workspaceId);
// Use metadata.name for workspace name (works for both worktree and local runtimes)
// Fallback to path-based derivation for legacy compatibility
const workspaceName =
currentMetadata?.name ??
selectedWorkspace.namedWorkspacePath?.split("/").pop() ??
selectedWorkspace.workspaceId;
return (
<ErrorBoundary
workspaceInfo={`${selectedWorkspace.projectName}/${workspaceName}`}
>
<AIView
key={selectedWorkspace.workspaceId}
workspaceId={selectedWorkspace.workspaceId}
projectPath={selectedWorkspace.projectPath}
projectName={selectedWorkspace.projectName}
branch={workspaceName}
namedWorkspacePath={selectedWorkspace.namedWorkspacePath ?? ""}
runtimeConfig={currentMetadata?.runtimeConfig}
incompatibleRuntime={currentMetadata?.incompatibleRuntime}
/>
</ErrorBoundary>
);
})()
) : creationProjectPath ? (
(() => {
const projectPath = creationProjectPath;
Expand Down
8 changes: 8 additions & 0 deletions src/browser/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { useAIViewKeybinds } from "@/browser/hooks/useAIViewKeybinds";
import { evictModelFromLRU } from "@/browser/hooks/useModelLRU";
import { QueuedMessage } from "./Messages/QueuedMessage";
import { CompactionWarning } from "./CompactionWarning";
import { ConcurrentLocalWarning } from "./ConcurrentLocalWarning";
import { checkAutoCompaction } from "@/browser/utils/compaction/autoCompactionCheck";
import { executeCompaction } from "@/browser/utils/chatCommands";
import { useProviderOptions } from "@/browser/hooks/useProviderOptions";
Expand All @@ -44,6 +45,7 @@ import { useSendMessageOptions } from "@/browser/hooks/useSendMessageOptions";

interface AIViewProps {
workspaceId: string;
projectPath: string;
projectName: string;
branch: string;
namedWorkspacePath: string; // User-friendly path for display and terminal
Expand All @@ -55,6 +57,7 @@ interface AIViewProps {

const AIViewInner: React.FC<AIViewProps> = ({
workspaceId,
projectPath,
projectName,
branch,
namedWorkspacePath,
Expand Down Expand Up @@ -561,6 +564,11 @@ const AIViewInner: React.FC<AIViewProps> = ({
onEdit={() => void handleEditQueuedMessage()}
/>
)}
<ConcurrentLocalWarning
workspaceId={workspaceId}
projectPath={projectPath}
runtimeConfig={runtimeConfig}
/>
</div>
</div>
{!autoScroll && (
Expand Down
22 changes: 15 additions & 7 deletions src/browser/components/ChatInput/CreationControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ interface CreationControlsProps {

/**
* Additional controls shown only during workspace creation
* - Trunk branch selector (which branch to fork from)
* - Runtime mode (local vs SSH)
* - Trunk branch selector (which branch to fork from) - hidden for Local runtime
* - Runtime mode (Local, Worktree, SSH)
*/
export function CreationControls(props: CreationControlsProps) {
// Local runtime doesn't need a trunk branch selector (uses project dir as-is)
const showTrunkBranchSelector =
props.branches.length > 0 && props.runtimeMode !== RUNTIME_MODE.LOCAL;

return (
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
{/* Trunk Branch Selector */}
{props.branches.length > 0 && (
{/* Trunk Branch Selector - hidden for Local runtime */}
{showTrunkBranchSelector && (
<div
className="flex items-center gap-1"
data-component="TrunkBranchGroup"
Expand Down Expand Up @@ -53,11 +57,13 @@ export function CreationControls(props: CreationControlsProps) {
value={props.runtimeMode}
options={[
{ value: RUNTIME_MODE.LOCAL, label: "Local" },
{ value: RUNTIME_MODE.WORKTREE, label: "Worktree" },
{ value: RUNTIME_MODE.SSH, label: "SSH" },
]}
onChange={(newMode) => {
const mode = newMode as RuntimeMode;
props.onRuntimeChange(mode, mode === RUNTIME_MODE.LOCAL ? "" : props.sshHost);
// Clear SSH host when switching away from SSH
props.onRuntimeChange(mode, mode === RUNTIME_MODE.SSH ? props.sshHost : "");
}}
disabled={props.disabled}
aria-label="Runtime mode"
Expand All @@ -77,8 +83,10 @@ export function CreationControls(props: CreationControlsProps) {
<Tooltip className="tooltip" align="center" width="wide">
<strong>Runtime:</strong>
<br />
• Local: git worktree in ~/.mux/src
<br />• SSH: remote clone in ~/mux on SSH host
• Local: work directly in project directory (no isolation)
<br />
• Worktree: git worktree in ~/.mux/src (isolated)
<br />• SSH: remote clone on SSH host
</Tooltip>
</TooltipWrapper>
</div>
Expand Down
72 changes: 72 additions & 0 deletions src/browser/components/ConcurrentLocalWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useMemo } from "react";
import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";
import { useWorkspaceStoreRaw } from "@/browser/stores/WorkspaceStore";
import { isLocalProjectRuntime } from "@/common/types/runtime";
import type { RuntimeConfig } from "@/common/types/runtime";
import { useSyncExternalStore } from "react";

/**
* Subtle indicator shown when a local project-dir workspace has another workspace
* for the same project that is currently streaming.
*/
export const ConcurrentLocalWarning: React.FC<{
workspaceId: string;
projectPath: string;
runtimeConfig?: RuntimeConfig;
}> = (props) => {
// Only show for local project-dir runtimes (not worktree or SSH)
const isLocalProject = isLocalProjectRuntime(props.runtimeConfig);

const { workspaceMetadata } = useWorkspaceContext();
const store = useWorkspaceStoreRaw();

// Find other local project-dir workspaces for the same project
const otherLocalWorkspaceIds = useMemo(() => {
if (!isLocalProject) return [];

const result: string[] = [];
for (const [id, meta] of workspaceMetadata) {
// Skip current workspace
if (id === props.workspaceId) continue;
// Must be same project
if (meta.projectPath !== props.projectPath) continue;
// Must also be local project-dir runtime
if (!isLocalProjectRuntime(meta.runtimeConfig)) continue;
result.push(id);
}
return result;
}, [isLocalProject, workspaceMetadata, props.workspaceId, props.projectPath]);

// Subscribe to streaming state of other local workspaces
const streamingWorkspaceName = useSyncExternalStore(
(listener) => {
const unsubscribers = otherLocalWorkspaceIds.map((id) => store.subscribeKey(id, listener));
return () => unsubscribers.forEach((unsub) => unsub());
},
() => {
for (const id of otherLocalWorkspaceIds) {
try {
const state = store.getWorkspaceSidebarState(id);
if (state.canInterrupt) {
const meta = workspaceMetadata.get(id);
return meta?.name ?? id;
}
} catch {
// Workspace may not be registered yet, skip
}
}
return null;
}
);

if (!isLocalProject || !streamingWorkspaceName) {
return null;
}

return (
<div className="text-center text-xs text-yellow-600/80">
⚠ <span className="text-yellow-500">{streamingWorkspaceName}</span> is also running in this
project directory — agents may interfere
</div>
);
};
Loading