Skip to content

Commit 4e1ceb1

Browse files
authored
Merge branch 'main' into mux-server-docker-image
2 parents 48c6273 + 76d8779 commit 4e1ceb1

Some content is hidden

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

45 files changed

+1431
-299
lines changed

README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,21 @@ Here are some specific use cases we enable:
3333

3434
## Features
3535

36-
- **Isolated workspaces** with central view on git divergence ([docs](https://cmux.io/runtime.html))
37-
- **[Local](https://cmux.io/runtime/local.html)**: run directly in your project directory
38-
- **[Worktree](https://cmux.io/runtime/worktree.html)**: git worktrees on your local machine
39-
- **[SSH](https://cmux.io/runtime/ssh.html)**: remote execution on a server over SSH
36+
- **Isolated workspaces** with central view on git divergence ([docs](https://mux.coder.com/runtime.html))
37+
- **[Local](https://mux.coder.com/runtime/local.html)**: run directly in your project directory
38+
- **[Worktree](https://mux.coder.com/runtime/worktree.html)**: git worktrees on your local machine
39+
- **[SSH](https://mux.coder.com/runtime/ssh.html)**: remote execution on a server over SSH
4040
- **Multi-model** (`sonnet-4-*`, `grok-*`, `gpt-5-*`, `opus-4-*`)
41-
- Ollama supported for local LLMs ([docs](https://cmux.io/models.html#ollama-local))
42-
- OpenRouter supported for long-tail of LLMs ([docs](https://cmux.io/models.html#openrouter-cloud))
43-
- **VS Code Extension**: Jump into mux workspaces directly from VS Code ([docs](https://cmux.io/vscode-extension.html))
41+
- Ollama supported for local LLMs ([docs](https://mux.coder.com/models.html#ollama-local))
42+
- OpenRouter supported for long-tail of LLMs ([docs](https://mux.coder.com/models.html#openrouter-cloud))
43+
- **VS Code Extension**: Jump into mux workspaces directly from VS Code ([docs](https://mux.coder.com/vscode-extension.html))
4444
- Supporting UI and keybinds for efficiently managing a suite of agents
4545
- Rich markdown outputs (mermaid diagrams, LaTeX, etc.)
4646

4747
mux has a custom agent loop but much of the core UX is inspired by Claude Code. You'll find familiar features like Plan/Exec mode, vim inputs, `/compact` and new ones
48-
like [opportunistic compaction](https://cmux.io/context-management.html) and [mode prompts](https://cmux.io/instruction-files.html#mode-prompts).
48+
like [opportunistic compaction](https://mux.coder.com/context-management.html) and [mode prompts](https://mux.coder.com/instruction-files.html#mode-prompts).
4949

50-
**[Read the full documentation →](https://cmux.io)**
50+
**[Read the full documentation →](https://mux.coder.com)**
5151

5252
## Install
5353

@@ -58,7 +58,7 @@ like [opportunistic compaction](https://cmux.io/context-management.html) and [mo
5858
Download pre-built binaries from [the releases page](https://github.com/coder/mux/releases) for
5959
macOS and Linux.
6060

61-
[More on installation →](https://cmux.io/install.html)
61+
[More on installation →](https://mux.coder.com/install.html)
6262

6363
## Screenshots
6464

@@ -99,7 +99,7 @@ macOS and Linux.
9999

100100
## More reading
101101

102-
See [the documentation](https://cmux.io) for more details.
102+
See [the documentation](https://mux.coder.com) for more details.
103103

104104
## Development
105105

docs/CNAME

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
cmux.io
1+
mux.coder.com

docs/models.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ mux supports multiple AI providers through its flexible provider architecture.
1313
Best supported provider with full feature support:
1414

1515
- `anthropic:claude-sonnet-4-5`
16-
- `anthropic:claude-opus-4-1`
16+
- `anthropic:claude-opus-4-5`
17+
- `anthropic:claude-haiku-4-5`
1718

1819
**Setup:**
1920

@@ -40,8 +41,10 @@ Or set environment variables:
4041

4142
GPT-5 family of models:
4243

43-
- `openai:gpt-5`
44+
- `openai:gpt-5.1`
4445
- `openai:gpt-5-pro`
46+
- `openai:gpt-5.1-codex`
47+
- `openai:gpt-5.1-codex-mini`
4548

4649
#### Google (Cloud)
4750

@@ -64,8 +67,6 @@ Access Gemini models directly via Google's generative AI API:
6467
}
6568
```
6669

67-
- `openai:gpt-5-codex`
68-
6970
**Note:** Anthropic models are better supported than GPT-5 class models due to an outstanding issue in the Vercel AI SDK.
7071

7172
TODO: add issue link here.
@@ -74,8 +75,8 @@ TODO: add issue link here.
7475

7576
Frontier reasoning models from xAI with built-in search orchestration:
7677

77-
- `xai:grok-4-1` — Fast unified model (switches between reasoning/non-reasoning based on thinking toggle)
78-
- `xai:grok-code` — Optimized for coding tasks
78+
- `xai:grok-4-1-fast` — Fast unified model (switches between reasoning/non-reasoning based on thinking toggle)
79+
- `xai:grok-code-fast-1` — Optimized for coding tasks
7980

8081
**Setup:**
8182

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mux",
3-
"version": "0.8.1",
3+
"version": "0.8.4",
44
"description": "mux - coder multiplexer",
55
"author": "Coder",
66
"main": "dist/cli/index.js",

scripts/bump_tag.sh

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Bump version in package.json, commit, and create a git tag.
5+
# Usage: ./scripts/bump_tag.sh [--minor]
6+
# --minor Bump minor version (0.8.x -> 0.9.0), otherwise bumps patch (0.8.3 -> 0.8.4)
7+
8+
MINOR=false
9+
if [[ "${1:-}" == "--minor" ]]; then
10+
MINOR=true
11+
fi
12+
13+
# Get current version from package.json
14+
CURRENT_VERSION=$(jq -r '.version' package.json)
15+
if [[ -z "$CURRENT_VERSION" || "$CURRENT_VERSION" == "null" ]]; then
16+
echo "Error: Could not read version from package.json" >&2
17+
exit 1
18+
fi
19+
20+
# Parse semver components
21+
IFS='.' read -r MAJOR MINOR_V PATCH <<< "$CURRENT_VERSION"
22+
23+
# Calculate new version
24+
if [[ "$MINOR" == "true" ]]; then
25+
NEW_VERSION="${MAJOR}.$((MINOR_V + 1)).0"
26+
else
27+
NEW_VERSION="${MAJOR}.${MINOR_V}.$((PATCH + 1))"
28+
fi
29+
30+
echo "Bumping version: $CURRENT_VERSION -> $NEW_VERSION"
31+
32+
# Update package.json
33+
jq --arg v "$NEW_VERSION" '.version = $v' package.json > package.json.tmp
34+
mv package.json.tmp package.json
35+
36+
# Commit and tag
37+
git add package.json
38+
git commit -m "release: v${NEW_VERSION}"
39+
git tag "v${NEW_VERSION}"
40+
41+
echo "Created tag v${NEW_VERSION}"
42+
echo "Run 'git push && git push --tags' to publish"

scripts/check_codex_comments.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ RESULT=$(gh api graphql \
5555
-F repo="$REPO" \
5656
-F pr="$PR_NUMBER")
5757

58-
# Filter regular comments from bot that aren't minimized and don't say "Didn't find any major issues"
59-
REGULAR_COMMENTS=$(echo "$RESULT" | jq "[.data.repository.pullRequest.comments.nodes[] | select(.author.login == \"${BOT_LOGIN_GRAPHQL}\" and .isMinimized == false and (.body | test(\"Didn't find any major issues\") | not))]")
58+
# Filter regular comments from bot that aren't minimized, excluding:
59+
# - "Didn't find any major issues" (no issues found)
60+
# - "usage limits have been reached" (rate limit error, not a real review)
61+
REGULAR_COMMENTS=$(echo "$RESULT" | jq "[.data.repository.pullRequest.comments.nodes[] | select(.author.login == \"${BOT_LOGIN_GRAPHQL}\" and .isMinimized == false and (.body | test(\"Didn't find any major issues|usage limits have been reached\") | not))]")
6062
REGULAR_COUNT=$(echo "$REGULAR_COMMENTS" | jq 'length')
6163

6264
# Filter unresolved review threads from bot

src/browser/App.tsx

Lines changed: 29 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import "./styles/globals.css";
33
import { useWorkspaceContext } from "./contexts/WorkspaceContext";
44
import { useProjectContext } from "./contexts/ProjectContext";
55
import type { WorkspaceSelection } from "./components/ProjectSidebar";
6-
import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
76
import { LeftSidebar } from "./components/LeftSidebar";
87
import { ProjectCreateModal } from "./components/ProjectCreateModal";
98
import { AIView } from "./components/AIView";
109
import { ErrorBoundary } from "./components/ErrorBoundary";
1110
import { usePersistedState, updatePersistedState } from "./hooks/usePersistedState";
1211
import { matchesKeybind, KEYBINDS } from "./utils/ui/keybinds";
12+
import { buildSortedWorkspacesByProject } from "./utils/ui/workspaceFiltering";
1313
import { useResumeManager } from "./hooks/useResumeManager";
1414
import { useUnreadTracking } from "./hooks/useUnreadTracking";
1515
import { useWorkspaceStoreRaw, useWorkspaceRecency } from "./stores/WorkspaceStore";
@@ -198,46 +198,24 @@ function AppInner() {
198198
// NEW: Get workspace recency from store
199199
const workspaceRecency = useWorkspaceRecency();
200200

201-
// Sort workspaces by recency (most recent first)
202-
// Returns Map<projectPath, FrontendWorkspaceMetadata[]> for direct component use
201+
// Build sorted workspaces map including pending workspaces
203202
// Use stable reference to prevent sidebar re-renders when sort order hasn't changed
204203
const sortedWorkspacesByProject = useStableReference(
205-
() => {
206-
const result = new Map<string, FrontendWorkspaceMetadata[]>();
207-
for (const [projectPath, config] of projects) {
208-
// Transform Workspace[] to FrontendWorkspaceMetadata[] using workspace ID
209-
const metadataList = config.workspaces
210-
.map((ws) => (ws.id ? workspaceMetadata.get(ws.id) : undefined))
211-
.filter((meta): meta is FrontendWorkspaceMetadata => meta !== undefined && meta !== null);
212-
213-
// Sort by recency
214-
metadataList.sort((a, b) => {
215-
const aTimestamp = workspaceRecency[a.id] ?? 0;
216-
const bTimestamp = workspaceRecency[b.id] ?? 0;
217-
return bTimestamp - aTimestamp;
204+
() => buildSortedWorkspacesByProject(projects, workspaceMetadata, workspaceRecency),
205+
(prev, next) =>
206+
compareMaps(prev, next, (a, b) => {
207+
if (a.length !== b.length) return false;
208+
// Check ID, name, and status to detect changes
209+
return a.every((meta, i) => {
210+
const other = b[i];
211+
return (
212+
other &&
213+
meta.id === other.id &&
214+
meta.name === other.name &&
215+
meta.status === other.status
216+
);
218217
});
219-
220-
result.set(projectPath, metadataList);
221-
}
222-
return result;
223-
},
224-
(prev, next) => {
225-
// Compare Maps: check if size, workspace order, and metadata content are the same
226-
if (
227-
!compareMaps(prev, next, (a, b) => {
228-
if (a.length !== b.length) return false;
229-
// Check both ID and name to detect renames
230-
return a.every((metadata, i) => {
231-
const bMeta = b[i];
232-
if (!bMeta || !metadata) return false; // Null-safe
233-
return metadata.id === bMeta.id && metadata.name === bMeta.name;
234-
});
235-
})
236-
) {
237-
return false;
238-
}
239-
return true;
240-
},
218+
}),
241219
[projects, workspaceMetadata, workspaceRecency]
242220
);
243221

@@ -605,12 +583,19 @@ function AppInner() {
605583
new Map(prev).set(metadata.id, metadata)
606584
);
607585

608-
// Switch to new workspace
609-
setSelectedWorkspace({
610-
workspaceId: metadata.id,
611-
projectPath: metadata.projectPath,
612-
projectName: metadata.projectName,
613-
namedWorkspacePath: metadata.namedWorkspacePath,
586+
// Only switch to new workspace if user hasn't selected another one
587+
// during the creation process (selectedWorkspace was null when creation started)
588+
setSelectedWorkspace((current) => {
589+
if (current !== null) {
590+
// User has already selected another workspace - don't override
591+
return current;
592+
}
593+
return {
594+
workspaceId: metadata.id,
595+
projectPath: metadata.projectPath,
596+
projectName: metadata.projectName,
597+
namedWorkspacePath: metadata.namedWorkspacePath,
598+
};
614599
});
615600

616601
// Track telemetry

src/browser/components/AIView.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,12 @@ const AIViewInner: React.FC<AIViewProps> = ({
245245
}
246246
}, [workspaceId, workspaceState?.queuedMessage, chatInputAPI]);
247247

248+
// Handler for sending queued message immediately (interrupt + send)
249+
const handleSendQueuedImmediately = useCallback(async () => {
250+
if (!workspaceState?.queuedMessage || !workspaceState.canInterrupt) return;
251+
await window.api.workspace.interruptStream(workspaceId, { sendQueuedImmediately: true });
252+
}, [workspaceId, workspaceState?.queuedMessage, workspaceState?.canInterrupt]);
253+
248254
const handleEditLastUserMessage = useCallback(async () => {
249255
if (!workspaceState) return;
250256

@@ -562,6 +568,9 @@ const AIViewInner: React.FC<AIViewProps> = ({
562568
<QueuedMessage
563569
message={workspaceState.queuedMessage}
564570
onEdit={() => void handleEditQueuedMessage()}
571+
onSendImmediately={
572+
workspaceState.canInterrupt ? handleSendQueuedImmediately : undefined
573+
}
565574
/>
566575
)}
567576
<ConcurrentLocalWarning

src/browser/components/ChatInput/CreationControls.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ export function CreationControls(props: CreationControlsProps) {
4141
]}
4242
onChange={(newMode) => {
4343
const mode = newMode as RuntimeMode;
44-
// Clear SSH host when switching away from SSH
45-
props.onRuntimeChange(mode, mode === RUNTIME_MODE.SSH ? props.sshHost : "");
44+
// Preserve SSH host across mode switches so it's remembered when returning to SSH
45+
props.onRuntimeChange(mode, props.sshHost);
4646
}}
4747
disabled={props.disabled}
4848
aria-label="Runtime mode"

src/browser/components/ChatInput/index.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
173173
},
174174
onSend: () => void handleSend(),
175175
openAIKeySet,
176+
useRecordingKeybinds: true,
176177
});
177178

178179
// Start creation tutorial when entering creation mode
@@ -496,23 +497,9 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
496497
voiceInput.toggle();
497498
};
498499

499-
// Global keybinds only active during recording
500-
const handleKeyDown = (e: KeyboardEvent) => {
501-
if (voiceInput.state !== "recording") return;
502-
if (e.key === " ") {
503-
e.preventDefault();
504-
voiceInput.stop({ send: true });
505-
} else if (e.key === "Escape") {
506-
e.preventDefault();
507-
voiceInput.cancel();
508-
}
509-
};
510-
511500
window.addEventListener(CUSTOM_EVENTS.TOGGLE_VOICE_INPUT, handleToggle as EventListener);
512-
window.addEventListener("keydown", handleKeyDown);
513501
return () => {
514502
window.removeEventListener(CUSTOM_EVENTS.TOGGLE_VOICE_INPUT, handleToggle as EventListener);
515-
window.removeEventListener("keydown", handleKeyDown);
516503
};
517504
}, [voiceInput, setToast]);
518505

@@ -862,9 +849,10 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
862849
return;
863850
}
864851

865-
// Space on empty input starts voice recording
852+
// Space on empty input starts voice recording (ignore key repeat from holding)
866853
if (
867854
e.key === " " &&
855+
!e.repeat &&
868856
input.trim() === "" &&
869857
voiceInput.shouldShowUI &&
870858
voiceInput.isApiKeySet &&

0 commit comments

Comments
 (0)