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
11 changes: 11 additions & 0 deletions .github/actions/setup-cmux/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,14 @@ runs:
sudo apt-get install -y --no-install-recommends imagemagick
fi
convert --version | head -1
- name: Install ImageMagick (Windows)
if: inputs.install-imagemagick == 'true' && runner.os == 'Windows'
shell: powershell
run: |
if (Get-Command magick -ErrorAction SilentlyContinue) {
Write-Host "✅ ImageMagick already available"
} else {
Write-Host "📦 Installing ImageMagick..."
choco install -y imagemagick
}
magick --version | Select-Object -First 1
31 changes: 31 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,34 @@ jobs:
run: bun x electron-builder --linux --publish always
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

build-windows:
name: Build and Release Windows
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git describe to find tags

- uses: ./.github/actions/setup-cmux
with:
install-imagemagick: true

- name: Install GNU Make (for build)
run: choco install -y make

- name: Verify tools
shell: bash
run: |
make --version
bun --version
magick --version | head -1
- name: Build application
run: bun run build

- name: Package and publish for Windows (.exe)
run: bun x electron-builder --win --publish always
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28 changes: 19 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@
# Branches reduce reproducibility - builds should fail fast with clear errors
# if dependencies are missing, not silently fall back to different behavior.

# Use PATH-resolved bash on Windows to avoid hardcoded /usr/bin/bash which doesn't
# exist in Chocolatey's make environment or on GitHub Actions windows-latest.
ifeq ($(OS),Windows_NT)
SHELL := bash
else
SHELL := /bin/bash
endif
.SHELLFLAGS := -eu -o pipefail -c

# Enable parallel execution by default (only if user didn't specify -j)
ifeq (,$(filter -j%,$(MAKEFLAGS)))
MAKEFLAGS += -j
Expand Down Expand Up @@ -92,20 +101,21 @@ help: ## Show this help message

## Development
dev: node_modules/.installed build-main ## Start development server (Vite + tsgo watcher for 10x faster type checking)
@bun x concurrently -k \
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
"vite"
@npx concurrently -k --raw \
"$(TSGO) -w -p tsconfig.main.json" \
"bun tsc-alias -w -p tsconfig.main.json" \
"bun x vite"

dev-server: node_modules/.installed build-main ## Start server mode with hot reload (backend :3000 + frontend :5173). Use VITE_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0 for remote access
@echo "Starting dev-server..."
@echo " Backend (IPC/WebSocket): http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000)"
@echo " Frontend (with HMR): http://$(or $(VITE_HOST),localhost):$(or $(VITE_PORT),5173)"
@echo ""
@echo "For remote access: make dev-server VITE_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0"
@bun x concurrently -k \
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
@npx concurrently -k \
"npx concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
"bun x nodemon --watch dist/main.js --watch dist/main-server.js --delay 500ms --exec 'node dist/main.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)'" \
"CMUX_VITE_HOST=$(or $(VITE_HOST),127.0.0.1) CMUX_VITE_PORT=$(or $(VITE_PORT),5173) VITE_BACKEND_URL=http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000) vite"
"CMUX_VITE_HOST=$(or $(VITE_HOST),127.0.0.1) CMUX_VITE_PORT=$(or $(VITE_PORT),5173) VITE_BACKEND_URL=http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000) bun x vite"



Expand Down Expand Up @@ -162,16 +172,16 @@ MAGICK_CMD := $(shell command -v magick 2>/dev/null || command -v convert 2>/dev
build/icon.png: docs/img/logo.webp
@echo "Generating Linux icon..."
@mkdir -p build
@$(MAGICK_CMD) docs/img/logo.webp -resize 512x512 build/icon.png
@"$(MAGICK_CMD)" docs/img/logo.webp -resize 512x512 build/icon.png

build/icon.icns: docs/img/logo.webp
@echo "Generating macOS icon..."
@mkdir -p build/icon.iconset
@for size in 16 32 64 128 256 512; do \
$(MAGICK_CMD) docs/img/logo.webp -resize $${size}x$${size} build/icon.iconset/icon_$${size}x$${size}.png; \
"$(MAGICK_CMD)" docs/img/logo.webp -resize $${size}x$${size} build/icon.iconset/icon_$${size}x$${size}.png; \
if [ $$size -le 256 ]; then \
double=$$((size * 2)); \
$(MAGICK_CMD) docs/img/logo.webp -resize $${double}x$${double} build/icon.iconset/icon_$${size}x$${size}@2x.png; \
"$(MAGICK_CMD)" docs/img/logo.webp -resize $${double}x$${double} build/icon.iconset/icon_$${size}x$${size}@2x.png; \
fi; \
done
@iconutil -c icns build/icon.iconset -o build/icon.icns
Expand Down
9 changes: 3 additions & 6 deletions bun.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "@coder/cmux",
Expand Down Expand Up @@ -70,7 +71,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"concurrently": "^8.2.0",
"concurrently": "^9.2.1",
"dotenv": "^17.2.3",
"electron": "^38.2.1",
"electron-builder": "^24.6.0",
Expand Down Expand Up @@ -1198,7 +1199,7 @@

"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],

"concurrently": ["concurrently@8.2.2", "", { "dependencies": { "chalk": "^4.1.2", "date-fns": "^2.30.0", "lodash": "^4.17.21", "rxjs": "^7.8.1", "shell-quote": "^1.8.1", "spawn-command": "0.0.2", "supports-color": "^8.1.1", "tree-kill": "^1.2.2", "yargs": "^17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg=="],
"concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],

"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],

Expand Down Expand Up @@ -1316,8 +1317,6 @@

"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],

"date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="],

"dayjs": ["dayjs@1.11.18", "", {}, "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="],

"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
Expand Down Expand Up @@ -2572,8 +2571,6 @@

"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],

"spawn-command": ["spawn-command@0.0.2", "", {}, "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="],

"spawn-wrap": ["spawn-wrap@2.0.0", "", { "dependencies": { "foreground-child": "^2.0.0", "is-windows": "^1.0.2", "make-dir": "^3.0.0", "rimraf": "^3.0.0", "signal-exit": "^3.0.2", "which": "^2.0.1" } }, "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg=="],

"spawnd": ["spawnd@5.0.0", "", { "dependencies": { "exit": "^0.1.2", "signal-exit": "^3.0.3", "tree-kill": "^1.2.2", "wait-port": "^0.2.9" } }, "sha512-28+AJr82moMVWolQvlAIv3JcYDkjkFTEmfDc503wxrF5l2rQ3dFz6DpbXp3kD4zmgGGldfM4xM4v1sFj/ZaIOA=="],
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export default defineConfig([
"src/services/**",
"src/runtime/**",
"src/utils/main/**",
"src/utils/platform/**",
"src/utils/providers/**",
"src/telemetry/**",
"src/git.ts",
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"concurrently": "^8.2.0",
"concurrently": "^9.2.1",
"dotenv": "^17.2.3",
"electron": "^38.2.1",
"electron-builder": "^24.6.0",
Expand Down Expand Up @@ -202,7 +202,9 @@
"artifactName": "${productName}-${version}-${arch}.${ext}"
},
"win": {
"target": "nsis"
"target": "nsis",
"icon": "build/icon.png",
"artifactName": "${productName}-${version}-${arch}.${ext}"
}
}
}
15 changes: 11 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ function AppInner() {
);

const handleAddWorkspace = useCallback(async (projectPath: string) => {
const projectName = projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "project";
const projectName = projectPath.replace(/\\/g, "/").split("/").pop() ?? "project";

workspaceModalProjectRef.current = projectPath;
setWorkspaceModalProject(projectPath);
Expand Down Expand Up @@ -637,15 +637,22 @@ function AppInner() {
<div className="mobile-layout flex flex-1 overflow-hidden">
{selectedWorkspace ? (
<ErrorBoundary
workspaceInfo={`${selectedWorkspace.projectName}/${selectedWorkspace.namedWorkspacePath?.split("/").pop() ?? selectedWorkspace.workspaceId}`}
workspaceInfo={`${selectedWorkspace.projectName}/${
(selectedWorkspace.namedWorkspacePath ?? "")
.replace(/\\/g, "/")
.split("/")
.pop() ?? selectedWorkspace.workspaceId
}`}
>
<AIView
key={selectedWorkspace.workspaceId}
workspaceId={selectedWorkspace.workspaceId}
projectName={selectedWorkspace.projectName}
branch={
selectedWorkspace.namedWorkspacePath?.split("/").pop() ??
selectedWorkspace.workspaceId
(selectedWorkspace.namedWorkspacePath ?? "")
.replace(/\\/g, "/")
.split("/")
.pop() ?? selectedWorkspace.workspaceId
}
namedWorkspacePath={selectedWorkspace.namedWorkspacePath ?? ""}
runtimeConfig={
Expand Down
5 changes: 3 additions & 2 deletions src/components/NewWorkspaceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TooltipWrapper, Tooltip } from "./Tooltip";
import { formatNewCommand } from "@/utils/chatCommands";
import { useNewWorkspaceOptions } from "@/hooks/useNewWorkspaceOptions";
import { RUNTIME_MODE } from "@/types/runtime";
import { formatWorkspaceLocation, formatSSHHostPath } from "@/utils/ui/pathFormatting";

interface NewWorkspaceModalProps {
isOpen: boolean;
Expand Down Expand Up @@ -247,8 +248,8 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
<p>This will create a workspace at:</p>
<code className="block break-all">
{runtimeMode === RUNTIME_MODE.SSH
? `${sshHost || "<host>"}:~/cmux/${branchName || "<branch-name>"}`
: `~/.cmux/src/${projectName}/${branchName || "<branch-name>"}`}
? formatSSHHostPath(sshHost || "<host>", `cmux/${branchName || "<branch-name>"}`)
: formatWorkspaceLocation(projectName, branchName || "<branch-name>", false)}
</code>
</ModalInfo>

Expand Down
8 changes: 2 additions & 6 deletions src/components/ProjectSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useDrag, useDrop, useDragLayer } from "react-dnd";
import { sortProjectsByOrder, reorderProjects, normalizeOrder } from "@/utils/projectOrdering";
import { matchesKeybind, formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
import { abbreviatePath, splitAbbreviatedPath } from "@/utils/ui/pathAbbreviation";
import { PlatformPaths } from "@/utils/platform/paths";
import {
partitionWorkspacesByAge,
formatOldWorkspaceThreshold,
Expand Down Expand Up @@ -234,12 +235,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
anchor: { top: number; left: number } | null;
} | null>(null);

const getProjectName = (path: string) => {
if (!path || typeof path !== "string") {
return "Unknown";
}
return path.split("/").pop() ?? path.split("\\").pop() ?? path;
};
const getProjectName = (path: string) => PlatformPaths.getProjectName(path);

const toggleProject = (projectPath: string) => {
const newExpanded = new Set(expandedProjects);
Expand Down
5 changes: 3 additions & 2 deletions src/components/SecretsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Modal, ModalInfo, ModalActions, CancelButton, PrimaryButton } from "./Modal";
import type { Secret } from "@/types/secrets";
import { formatConfigPath } from "@/utils/ui/pathFormatting";

// Visibility toggle icon component
const ToggleVisibilityIcon: React.FC<{ visible: boolean }> = ({ visible }) => {
Expand Down Expand Up @@ -132,8 +133,8 @@ const SecretsModal: React.FC<SecretsModalProps> = ({
>
<ModalInfo>
<p>
Secrets are stored in <code>~/.cmux/secrets.json</code> (kept away from source code) but
namespaced per project.
Secrets are stored in <code>{formatConfigPath("secrets.json")}</code> (kept away from
source code) but namespaced per project.
</p>
<p>Secrets are injected as environment variables to compute commands (e.g. Bash)</p>
</ModalInfo>
Expand Down
12 changes: 5 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import writeFileAtomic from "write-file-atomic";
import type { WorkspaceMetadata, FrontendWorkspaceMetadata } from "./types/workspace";
import type { Secret, SecretsConfig } from "./types/secrets";
import type { Workspace, ProjectConfig, ProjectsConfig } from "./types/project";
import { PlatformPaths } from "./utils/platform/paths";

// Re-export project types from dedicated types file (for preload usage)
export type { Workspace, ProjectConfig, ProjectsConfig };
Expand Down Expand Up @@ -96,7 +97,7 @@ export class Config {
}

private getProjectName(projectPath: string): string {
return projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "unknown";
return PlatformPaths.getProjectName(projectPath);
}

/**
Expand All @@ -120,8 +121,7 @@ export class Config {
*/
generateLegacyId(projectPath: string, workspacePath: string): string {
const projectBasename = this.getProjectName(projectPath);
const workspaceBasename =
workspacePath.split("/").pop() ?? workspacePath.split("\\").pop() ?? "unknown";
const workspaceBasename = PlatformPaths.basename(workspacePath) || "unknown";
return `${projectBasename}-${workspaceBasename}`;
}

Expand Down Expand Up @@ -162,8 +162,7 @@ export class Config {
// LEGACY FORMAT: Fall back to metadata.json and legacy ID for unmigrated workspaces
if (!workspace.id) {
// Extract workspace basename (could be stable ID or legacy name)
const workspaceBasename =
workspace.path.split("/").pop() ?? workspace.path.split("\\").pop() ?? "unknown";
const workspaceBasename = PlatformPaths.basename(workspace.path) || "unknown";

// Try loading metadata with basename as ID (works for old workspaces)
const metadataPath = path.join(this.getSessionDir(workspaceBasename), "metadata.json");
Expand Down Expand Up @@ -243,8 +242,7 @@ export class Config {

for (const workspace of projectConfig.workspaces) {
// Extract workspace basename from path (could be stable ID or legacy name)
const workspaceBasename =
workspace.path.split("/").pop() ?? workspace.path.split("\\").pop() ?? "unknown";
const workspaceBasename = path.basename(workspace.path);

try {
// NEW FORMAT: If workspace has metadata in config, use it directly
Expand Down
Loading
Loading