Skip to content

Commit 0a2572e

Browse files
committed
🤖 feat: add in-place local runtime mode
1 parent 7d2f8cc commit 0a2572e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+609
-232
lines changed

docs/SUMMARY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
# Features
1010

1111
- [Workspaces](./workspaces.md)
12-
- [Local](./local.md)
12+
- [Worktree](./worktree.md)
13+
- [Local (In-Place)](./local-in-place.md)
1314
- [SSH](./ssh.md)
1415
- [Forking](./fork.md)
1516
- [Init Hooks](./init-hooks.md)

docs/init-hooks.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Init hooks receive the following environment variables:
3434
- `MUX_PROJECT_PATH` - Absolute path to the project root on the **local machine**
3535
- Always refers to your local project path, even on SSH workspaces
3636
- Useful for logging, debugging, or runtime-specific logic
37-
- `MUX_RUNTIME` - Runtime type: `"local"` or `"ssh"`
38-
- Use this to detect whether the hook is running locally or remotely
37+
- `MUX_RUNTIME` - Runtime type: `"worktree"`, `"local"`, or `"ssh"`
38+
- Use this to detect whether the hook is running in a worktree, directly in your project directory, or on a remote machine
3939

4040
**Note for SSH workspaces:** Since the project is synced to the remote machine, files exist in both locations. The init hook runs in the workspace directory (`$PWD`), so use relative paths to reference project files:
4141

@@ -54,11 +54,17 @@ if [ -f "../.env" ]; then
5454
fi
5555

5656
# Runtime-specific behavior
57-
if [ "$MUX_RUNTIME" = "local" ]; then
58-
echo "Running on local machine"
59-
else
60-
echo "Running on SSH remote"
61-
fi
57+
case "$MUX_RUNTIME" in
58+
worktree)
59+
echo "Running in local worktree"
60+
;;
61+
local)
62+
echo "Running directly in project directory"
63+
;;
64+
ssh)
65+
echo "Running on SSH remote"
66+
;;
67+
esac
6268

6369
bun install
6470
```

docs/local-in-place.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Local (In-Place) Workspaces
2+
3+
Local (in-place) workspaces operate directly inside your project directory. Unlike [worktree workspaces](./worktree.md), they do **not** create a separate checkout. The agent works with the branch, index, and working tree that you already have checked out.
4+
5+
## When to Use
6+
7+
- You want zero-copy workflows (benchmarks, quick experiments, or short-lived agents)
8+
- You need the agent to see files that are too large to duplicate efficiently
9+
- You plan to drive the session from another terminal that is already inside the project directory
10+
11+
## Key Behavior
12+
13+
- **Single workspace per project**: mux enforces one in-place workspace per project to avoid conflicting agent sessions.
14+
- **Current branch only**: the agent starts on whatever branch is currently checked out in your repository. Branch switches affect your main working tree immediately.
15+
- **Shared Git state**: any uncommitted changes are visible to both you and the agent. The agent can stage, commit, and push directly from your checkout.
16+
- **No automatic cleanup**: deleting the workspace inside mux only removes it from the workspace list—your project directory remains untouched.
17+
- **Init hooks**: `.mux/init` still runs in the project directory. Use `MUX_RUNTIME=local` to special-case logic if needed.
18+
19+
## Recommended Safeguards
20+
21+
- Commit or stash important work before starting an in-place session.
22+
- Consider creating a temporary branch manually before opening the workspace if you want to isolate commits.
23+
- Use descriptive workspace names so it is clear what the agent is attempting.
24+
- Review the agent's Git operations carefully—there is no isolation layer to fall back on.
25+
26+
## Switching Between Modes
27+
28+
You can select **Local (in-place)** from the workspace creation dialog or via the `/new` command (`/new -r local`). Switch back to **Worktree** or **SSH** at any time for isolated workspaces.

docs/workspaces.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,26 @@ Workspaces in mux provide isolated development environments for parallel agent w
44

55
## Workspace Types
66

7-
mux supports two workspace backends:
8-
9-
- **[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.
7+
mux supports three workspace backends:
108

9+
- **[Worktree Workspaces](./worktree.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.
10+
- **[Local (In-Place) Workspaces](./local-in-place.md)**: Reuse your existing project directory with no additional checkout. The agent operates on the branch and working tree you already have checked out.
1111
- **[SSH Workspaces](./ssh.md)**: Regular git clones on a remote server accessed via SSH. These are completely independent repositories stored on the remote machine.
1212

1313
## Choosing a Backend
1414

1515
The workspace backend is selected when you create a workspace:
1616

17-
- **Local**: Best for fast iteration, local testing, and when you want to leverage your local machine's resources
18-
- **SSH**: Ideal for heavy workloads, long-running tasks, or when you need access to remote infrastructure
17+
- **Worktree**: Best for fast iteration, local testing, and when you want an isolated checkout that still lives on your machine.
18+
- **Local (in-place)**: Ideal for zero-copy workflows or when you need the agent to operate on your existing working tree without creating a new worktree.
19+
- **SSH**: Ideal for heavy workloads, long-running tasks, or when you need access to remote infrastructure.
1920

2021
## Key Concepts
2122

2223
- **Isolation**: Each workspace has independent working changes and Git state
2324
- **Branch flexibility**: Workspaces can switch branches, enter detached HEAD state, or create new branches as needed
2425
- **Parallel execution**: Run multiple workspaces simultaneously on different tasks
25-
- **Shared commits**: Local workspaces (using worktrees) share commits with the main repository immediately
26+
- **Shared commits**: Worktree and local (in-place) workspaces share commits with the main repository immediately; SSH workspaces require pushing to sync
2627

2728
## Reviewing Code
2829

@@ -39,9 +40,9 @@ Here are a few practical approaches to reviewing changes from workspaces, depend
3940
Some changes (especially UI ones) require the Human to determine acceptability. An effective approach for this is:
4041

4142
1. Ask agent to commit WIP when it's ready for Human review
42-
2. Human, in main repository, checks out the workspace branch in a detached HEAD state: `git checkout --detach <workspace-branch>` (for local workspaces)
43+
2. Human, in main repository, checks out the workspace branch in a detached HEAD state: `git checkout --detach <workspace-branch>` (for worktree workspaces)
4344

44-
**Note**: For local workspaces, this workflow uses the detached HEAD state because the branch is already checked out in the workspace and you cannot check out the same branch multiple times across worktrees.
45+
**Note**: For worktree workspaces, this workflow uses the detached HEAD state because the branch is already checked out in the workspace and you cannot check out the same branch multiple times across worktrees. For local (in-place) workspaces, the agent and human are literally sharing the same checkout—coordinate commits carefully.
4546

4647
If you want faster iteration in between commits, you can hop into the workspace directory and run a dev server (e.g. `bun dev`) there directly and observe the agent's work in real-time.
4748

docs/local.md renamed to docs/worktree.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Local Workspaces
1+
# Worktree Workspaces
22

3-
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.
3+
Worktree 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.
44

55
## How Worktrees Work
66

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

1111
## Filesystem Layout
1212

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

1515
Example layout:
1616

src/browser/components/ChatInput/CreationControls.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function CreationControls(props: CreationControlsProps) {
2222
return (
2323
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
2424
{/* Trunk Branch Selector */}
25-
{props.branches.length > 0 && (
25+
{props.branches.length > 0 && props.runtimeMode !== RUNTIME_MODE.LOCAL && (
2626
<div className="flex items-center gap-1" data-component="TrunkBranchGroup">
2727
<label htmlFor="trunk-branch" className="text-muted text-xs">
2828
From:
@@ -44,12 +44,14 @@ export function CreationControls(props: CreationControlsProps) {
4444
<Select
4545
value={props.runtimeMode}
4646
options={[
47-
{ value: RUNTIME_MODE.LOCAL, label: "Local" },
47+
{ value: RUNTIME_MODE.WORKTREE, label: "Worktree" },
48+
{ value: RUNTIME_MODE.LOCAL, label: "Local (in-place)" },
4849
{ value: RUNTIME_MODE.SSH, label: "SSH" },
4950
]}
5051
onChange={(newMode) => {
5152
const mode = newMode as RuntimeMode;
52-
props.onRuntimeChange(mode, mode === RUNTIME_MODE.LOCAL ? "" : props.sshHost);
53+
const nextHost = mode === RUNTIME_MODE.SSH ? props.sshHost : "";
54+
props.onRuntimeChange(mode, nextHost);
5355
}}
5456
disabled={props.disabled}
5557
aria-label="Runtime mode"
@@ -69,7 +71,8 @@ export function CreationControls(props: CreationControlsProps) {
6971
<Tooltip className="tooltip" align="center" width="wide">
7072
<strong>Runtime:</strong>
7173
<br />
72-
• Local: git worktree in ~/.mux/src
74+
• Worktree: git worktree in ~/.mux/src
75+
<br />• Local (in-place): work directly in the project directory
7376
<br />• SSH: remote clone in ~/mux on SSH host
7477
</Tooltip>
7578
</TooltipWrapper>

src/browser/contexts/WorkspaceContext.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const createWorkspaceMetadata = (
1818
name: "main",
1919
namedWorkspacePath: "/test-main",
2020
createdAt: "2025-01-01T00:00:00.000Z",
21-
runtimeConfig: { type: "local", srcBaseDir: "/home/user/.mux/src" },
21+
runtimeConfig: { type: "worktree", srcBaseDir: "/home/user/.mux/src" },
2222
...overrides,
2323
});
2424

src/browser/hooks/useStartWorkspaceCreation.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ type PersistFn = typeof updatePersistedState;
2121
type PersistCall = [string, unknown, unknown?];
2222

2323
describe("normalizeRuntimePreference", () => {
24-
test("returns undefined for local or empty runtime", () => {
24+
test("returns undefined for empty or worktree runtime", () => {
2525
expect(normalizeRuntimePreference(undefined)).toBeUndefined();
2626
expect(normalizeRuntimePreference(" ")).toBeUndefined();
27-
expect(normalizeRuntimePreference("local")).toBeUndefined();
28-
expect(normalizeRuntimePreference("LOCAL")).toBeUndefined();
27+
expect(normalizeRuntimePreference("worktree")).toBeUndefined();
28+
expect(normalizeRuntimePreference("WORKTREE")).toBeUndefined();
29+
});
30+
31+
test("normalizes local runtime tokens", () => {
32+
expect(normalizeRuntimePreference("local")).toBe("local");
33+
expect(normalizeRuntimePreference("LOCAL")).toBe("local");
34+
expect(normalizeRuntimePreference(" local-in-place ")).toBe("local");
2935
});
3036

3137
test("normalizes ssh runtimes", () => {

src/browser/hooks/useStartWorkspaceCreation.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,15 @@ export function normalizeRuntimePreference(runtime: string | undefined): string
2727
}
2828

2929
const lower = trimmed.toLowerCase();
30-
if (lower === RUNTIME_MODE.LOCAL) {
30+
const normalized = lower.replace(/[\s_()-]/g, "");
31+
if (normalized === RUNTIME_MODE.WORKTREE) {
3132
return undefined;
3233
}
3334

35+
if (normalized === RUNTIME_MODE.LOCAL || normalized === "localinplace") {
36+
return RUNTIME_MODE.LOCAL;
37+
}
38+
3439
if (lower === RUNTIME_MODE.SSH) {
3540
return RUNTIME_MODE.SSH;
3641
}

src/browser/utils/chatCommands.test.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@ import { parseRuntimeString } from "./chatCommands";
33
describe("parseRuntimeString", () => {
44
const workspaceName = "test-workspace";
55

6-
test("returns undefined for undefined runtime (default to local)", () => {
6+
test("returns undefined for undefined runtime (default to worktree)", () => {
77
expect(parseRuntimeString(undefined, workspaceName)).toBeUndefined();
88
});
99

10-
test("returns undefined for explicit 'local' runtime", () => {
11-
expect(parseRuntimeString("local", workspaceName)).toBeUndefined();
12-
expect(parseRuntimeString("LOCAL", workspaceName)).toBeUndefined();
13-
expect(parseRuntimeString(" local ", workspaceName)).toBeUndefined();
10+
test("returns undefined for explicit 'worktree' runtime", () => {
11+
expect(parseRuntimeString("worktree", workspaceName)).toBeUndefined();
12+
expect(parseRuntimeString(" WORKTREE ", workspaceName)).toBeUndefined();
13+
});
14+
15+
test("parses local runtime token", () => {
16+
expect(parseRuntimeString("local", workspaceName)).toEqual({ type: "local" });
17+
expect(parseRuntimeString("LOCAL", workspaceName)).toEqual({ type: "local" });
18+
expect(parseRuntimeString(" local-in-place ", workspaceName)).toEqual({ type: "local" });
1419
});
1520

1621
test("parses valid SSH runtime", () => {
@@ -77,10 +82,10 @@ describe("parseRuntimeString", () => {
7782

7883
test("throws error for unknown runtime type", () => {
7984
expect(() => parseRuntimeString("docker", workspaceName)).toThrow(
80-
"Unknown runtime type: 'docker'"
85+
"Unknown runtime type: 'docker'. Use 'worktree', 'local', or 'ssh <host>'"
8186
);
8287
expect(() => parseRuntimeString("remote", workspaceName)).toThrow(
83-
"Unknown runtime type: 'remote'"
88+
"Unknown runtime type: 'remote'. Use 'worktree', 'local', or 'ssh <host>'"
8489
);
8590
});
8691
});

0 commit comments

Comments
 (0)