Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
]
},
"plan-mode",
"editor",
"vscode-extension",
"models",
{
Expand Down
173 changes: 173 additions & 0 deletions docs/editor.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
---
title: Editor Configuration
description: Configure which editor opens your workspaces
---

mux can open workspaces in your preferred code editor. Click the pencil icon in the workspace header or use the keyboard shortcut to open the current workspace in your editor.

## Selecting Your Editor

Go to **Settings → General → Editor** to choose your default editor. mux ships with support for:

- **VS Code** - Opens with Remote-SSH for SSH workspaces
- **Cursor** - Opens with Remote-SSH for SSH workspaces
- **Zed** - Local workspaces only (no SSH support)
- **Vim/Neovim** - Opens in mux's web terminal

## Editor Types

Editors fall into two categories:

### Native Editors

Native editors (VS Code, Cursor, Zed) spawn as separate GUI applications. They work best in **desktop mode** (the Electron app). In browser mode, native editors aren't available since the browser can't launch applications on your computer.

For SSH workspaces, VS Code and Cursor use their Remote-SSH extension to connect directly to the remote host.

### Terminal Editors

Terminal editors (Vim/Neovim) run inside mux's web terminal. They work in **all modes**:

- Desktop (Electron) - Opens in a terminal window
- Browser mode - Opens in a browser popup
- SSH workspaces - Runs on the remote host via the terminal

This makes terminal editors the most portable option.

## Custom Editors

You can customize the editor configuration by editing `~/.mux/editors.js`. This file is created automatically on first run with the default editors.

### File Structure

```javascript
export default {
// Which editor to use by default
default: "vscode",

// Editor definitions
editors: {
vscode: {
name: "VS Code",
open: async (ctx) => {
// Return instructions for opening
},
},
// ... more editors
},
};
```

### Adding a Custom Editor

Each editor has a `name` and an `open` function. The `open` function receives a context object and returns instructions:

```javascript
// Example: Add Sublime Text
sublime: {
name: "Sublime Text",
open: async (ctx) => {
if (ctx.isBrowser) {
return { error: "Sublime Text requires the desktop app" };
}
if (ctx.isSSH) {
return { error: "Sublime Text does not support SSH workspaces" };
}
return { type: "native", command: "subl", args: [ctx.path] };
},
},
```

### Context Object

The `open` function receives these properties:

| Property | Type | Description |
| ------------- | ---------- | ------------------------------------------ |
| `path` | `string` | Absolute path to open |
| `host` | `string?` | SSH host (if SSH workspace) |
| `isSSH` | `boolean` | Whether this is an SSH workspace |
| `isBrowser` | `boolean` | Whether running in browser mode |
| `isDesktop` | `boolean` | Whether running in desktop (Electron) mode |
| `platform` | `string` | OS platform: "darwin", "linux", or "win32" |
| `findCommand` | `function` | Find first available command from a list |

### Return Values

Return one of these objects:

**Native editor (GUI application):**

```javascript
{ type: "native", command: "code", args: ["--new-window", ctx.path] }
```

**Terminal editor (runs in web terminal):**

```javascript
{ type: "web_term", command: `nvim ${ctx.path}` }
```

**Error (show message to user):**

```javascript
{
error: "This editor doesn't support SSH workspaces";
}
```

### Example: Emacs

```javascript
emacs: {
name: "Emacs",
open: async (ctx) => {
// Use emacsclient for GUI, or run in terminal for SSH
if (ctx.isSSH) {
return { type: "web_term", command: `emacs -nw ${ctx.path}` };
}
if (ctx.isBrowser) {
return { type: "web_term", command: `emacs -nw ${ctx.path}` };
}
// Desktop mode - use GUI emacs
return { type: "native", command: "emacsclient", args: ["-c", ctx.path] };
},
},
```

### Example: Helix

```javascript
helix: {
name: "Helix",
open: async (ctx) => {
const cmd = await ctx.findCommand(["hx", "helix"]);
if (!cmd) {
return { error: "Helix not found (tried hx, helix)" };
}
return { type: "web_term", command: `${cmd} ${ctx.path}` };
},
},
```

## SSH Workspace Support

| Editor | SSH Support | Method |
| ---------- | ----------- | ------------------------------ |
| VS Code | ✅ | Remote-SSH extension |
| Cursor | ✅ | Remote-SSH extension |
| Zed | ❌ | Not supported |
| Vim/Neovim | ✅ | Runs in web terminal on remote |

## Keyboard Shortcut

Open the current workspace in your editor:

- **macOS**: `Cmd+Shift+E`
- **Windows/Linux**: `Ctrl+Shift+E`

## Related

- [VS Code Extension](/vscode-extension) - Deeper VS Code integration
- [Workspaces](/workspaces) - Workspace management
- [SSH Runtime](/runtime/ssh) - SSH workspace setup
122 changes: 69 additions & 53 deletions src/browser/components/Settings/sections/GeneralSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,43 @@ import {
SelectValue,
} from "@/browser/components/ui/select";
import { Input } from "@/browser/components/ui/input";
import { usePersistedState } from "@/browser/hooks/usePersistedState";
import { useAPI } from "@/browser/contexts/API";
import {
EDITOR_CONFIG_KEY,
DEFAULT_EDITOR_CONFIG,
type EditorConfig,
type EditorType,
} from "@/common/constants/storage";

const EDITOR_OPTIONS: Array<{ value: EditorType; label: string }> = [
{ value: "vscode", label: "VS Code" },
{ value: "cursor", label: "Cursor" },
{ value: "zed", label: "Zed" },
{ value: "custom", label: "Custom" },
];
import type { EditorInfo } from "@/common/types/editor";

// Browser mode: window.api is not set (only exists in Electron via preload)
const isBrowserMode = typeof window !== "undefined" && !window.api;

export function GeneralSection() {
const { theme, setTheme } = useTheme();
const { api } = useAPI();
const [editorConfig, setEditorConfig] = usePersistedState<EditorConfig>(
EDITOR_CONFIG_KEY,
DEFAULT_EDITOR_CONFIG
);
const [editors, setEditors] = useState<EditorInfo[]>([]);
const [defaultEditor, setDefaultEditor] = useState<string>("");
const [loading, setLoading] = useState(true);
const [sshHost, setSshHost] = useState<string>("");
const [sshHostLoaded, setSshHostLoaded] = useState(false);

// Load editors from backend
useEffect(() => {
if (!api) return;

const loadEditors = async () => {
try {
const editorList = await api.general.listEditors();
setEditors(editorList);
const current = editorList.find((e) => e.isDefault);
if (current) {
setDefaultEditor(current.id);
}
} catch (err) {
console.error("Failed to load editors:", err);
} finally {
setLoading(false);
}
};

void loadEditors();
}, [api]);

// Load SSH host from server on mount (browser mode only)
useEffect(() => {
if (isBrowserMode && api) {
Expand All @@ -47,12 +55,30 @@ export function GeneralSection() {
}
}, [api]);

const handleEditorChange = (editor: EditorType) => {
setEditorConfig((prev) => ({ ...prev, editor }));
};
const handleEditorChange = async (editorId: string) => {
if (!api) return;

const handleCustomCommandChange = (customCommand: string) => {
setEditorConfig((prev) => ({ ...prev, customCommand }));
// Optimistic update
setDefaultEditor(editorId);
setEditors((prev) =>
prev.map((e) => ({
...e,
isDefault: e.id === editorId,
}))
);

try {
await api.general.setDefaultEditor({ editorId });
} catch (err) {
console.error("Failed to set default editor:", err);
// Revert on error
const editorList = await api.general.listEditors();
setEditors(editorList);
const current = editorList.find((e) => e.isDefault);
if (current) {
setDefaultEditor(current.id);
}
}
};

const handleSshHostChange = useCallback(
Expand Down Expand Up @@ -91,46 +117,36 @@ export function GeneralSection() {
<div className="flex items-center justify-between">
<div>
<div className="text-foreground text-sm">Editor</div>
<div className="text-muted text-xs">Editor to open files in</div>
<div className="text-muted text-xs">
Default editor for opening workspaces.{" "}
<a
href="https://mux.coder.com/docs/editor"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:underline"
>
Learn more
</a>
</div>
</div>
<Select value={editorConfig.editor} onValueChange={handleEditorChange}>
<Select
value={defaultEditor}
onValueChange={(value) => void handleEditorChange(value)}
disabled={loading}
>
<SelectTrigger className="border-border-medium bg-background-secondary hover:bg-hover h-9 w-auto cursor-pointer rounded-md border px-3 text-sm transition-colors">
<SelectValue />
<SelectValue placeholder={loading ? "Loading..." : "Select editor"} />
</SelectTrigger>
<SelectContent>
{EDITOR_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
{editors.map((editor) => (
<SelectItem key={editor.id} value={editor.id}>
{editor.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

{editorConfig.editor === "custom" && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<div className="text-foreground text-sm">Custom Command</div>
<div className="text-muted text-xs">Command to run (path will be appended)</div>
</div>
<Input
value={editorConfig.customCommand ?? ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleCustomCommandChange(e.target.value)
}
placeholder="e.g., nvim"
className="border-border-medium bg-background-secondary h-9 w-40"
/>
</div>
{isBrowserMode && (
<div className="text-warning text-xs">
Custom editors are not supported in browser mode. Use VS Code or Cursor instead.
</div>
)}
</div>
)}

{isBrowserMode && sshHostLoaded && (
<div className="flex items-center justify-between">
<div>
Expand Down
18 changes: 16 additions & 2 deletions src/browser/components/TerminalView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ import { useAPI } from "@/browser/contexts/API";
interface TerminalViewProps {
workspaceId: string;
sessionId?: string;
initialCommand?: string;
visible: boolean;
}

export function TerminalView({ workspaceId, sessionId, visible }: TerminalViewProps) {
export function TerminalView({
workspaceId,
sessionId,
initialCommand,
visible,
}: TerminalViewProps) {
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<Terminal | null>(null);
const fitAddonRef = useRef<FitAddon | null>(null);
Expand Down Expand Up @@ -57,7 +63,15 @@ export function TerminalView({ workspaceId, sessionId, visible }: TerminalViewPr
sendInput,
resize,
error: sessionError,
} = useTerminalSession(workspaceId, sessionId, visible, terminalSize, handleOutput, handleExit);
} = useTerminalSession(
workspaceId,
sessionId,
initialCommand,
visible,
terminalSize,
handleOutput,
handleExit
);

// Keep refs to latest functions so callbacks always use current version
const sendInputRef = useRef(sendInput);
Expand Down
Loading
Loading