Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
7d943e9
🤖 Add pluggable runtime abstraction layer
ammar-agent Oct 11, 2025
d88b333
🤖 Fix lint: remove unused import
ammar-agent Oct 11, 2025
6b28165
🤖 Fix prettier formatting
ammar-agent Oct 11, 2025
cfbd07f
🤖 Fix test and type errors after rebase
ammar-agent Oct 22, 2025
940c6e1
🤖 Fix prettier formatting
ammar-agent Oct 22, 2025
49e23e5
🤖 Add SSH runtime implementation
ammar-agent Oct 22, 2025
7a217da
🤖 Integrate runtime config with workspace metadata and AIService
ammar-agent Oct 22, 2025
ca9504c
🤖 Fix prettier formatting
ammar-agent Oct 22, 2025
3a7daef
🤖 Fix lint errors in SSH runtime
ammar-agent Oct 22, 2025
e84300a
🤖 Add no-op rebuild script for electron-builder
ammar-agent Oct 22, 2025
8eb2bc6
Extract git env vars to shared constant to avoid duplication
ammar-agent Oct 22, 2025
733ab9d
Remove exists() from Runtime interface, use shared utility
ammar-agent Oct 23, 2025
30df2e6
Fix rebase conflicts and lockfile
ammar-agent Oct 23, 2025
ad75d45
Clean up extra whitespace
ammar-agent Oct 23, 2025
fdd67a0
Rewrite SSH runtime to use ssh command instead of ssh2 library
ammar-agent Oct 23, 2025
8a927b7
Convert Runtime interface to streaming with convenience helpers
ammar-agent Oct 23, 2025
00e8eb8
Address review feedback: remove isFile from FileStat
ammar-agent Oct 23, 2025
26bc138
🤖 Add runtime integration tests with Docker SSH server
ammar-agent Oct 23, 2025
c3d8546
Remove stray test README, update AGENTS.md to prevent test READMEs
ammar-agent Oct 23, 2025
b6564a5
🤖 Make timeout mandatory in ExecOptions to prevent zombies
ammar-agent Oct 23, 2025
cf300d0
🤖 Add macOS runtime integration tests to CI
ammar-agent Oct 23, 2025
ca72cfd
🤖 Use depot macOS runners for runtime integration tests
ammar-agent Oct 24, 2025
4acc725
🤖 Use depot-macos-15 runner for runtime tests
ammar-agent Oct 24, 2025
82d8251
🤖 Install Docker on macOS runners for runtime tests
ammar-agent Oct 24, 2025
70368b4
🤖 Remove macOS runtime integration tests from CI
ammar-agent Oct 24, 2025
84ee926
🤖 Add workdir to LocalRuntime for symmetry with SSHRuntime
ammar-agent Oct 24, 2025
0c69d9e
🤖 Add comprehensive runtime tests for git operations and edge cases
ammar-agent Oct 24, 2025
b90dae5
🤖 Remove rebase backup files
ammar-agent Oct 24, 2025
e39a25f
🤖 Integrate init hooks with Runtime.createWorkspace()
ammar-agent Oct 24, 2025
16f311d
🤖 Add Runtime.createWorkspace() interface and implementations
ammar-agent Oct 24, 2025
68a58d9
🤖 Add runtime config passthrough to WORKSPACE_CREATE IPC
ammar-agent Oct 24, 2025
f8c9665
🤖 Refactor createWorkspace tests for DRY, clarity, and robustness
ammar-agent Oct 24, 2025
a0dd05d
🤖 Add runtime config support to workspace creation flow
ammar-agent Oct 25, 2025
4c7400c
🤖 Add runtime selection UI to New Workspace Modal (DRY)
ammar-agent Oct 25, 2025
a6f17cd
🤖 Fix type signature after rebase - add RuntimeConfig to AppContext
ammar-agent Oct 25, 2025
ef44480
🤖 Simplify command palette - open modal instead of direct creation
ammar-agent Oct 25, 2025
6bdec59
🤖 Accept SSH hostnames without explicit user
ammar-agent Oct 25, 2025
2d1d976
🤖 Split workspace creation into createWorkspace + initWorkspace
ammar-agent Oct 25, 2025
8ba2f55
🤖 Add command logging, force bash shell for SSH commands
ammar-agent Oct 25, 2025
3f4a38b
🤖 Replace rsync/scp with git archive for SSH sync
ammar-agent Oct 25, 2025
3fbf11e
🤖 Replace git archive with git bundle for SSH workspace sync
ammar-agent Oct 25, 2025
2e5e5d9
🤖 Fix tilde (~/) path expansion in SSH exec
ammar-agent Oct 25, 2025
9cbd035
🤖 Fix tilde expansion in git clone target path
ammar-agent Oct 25, 2025
fa9eaa7
🤖 Fix tilde expansion in init hook path
ammar-agent Oct 25, 2025
73dce73
🤖 Fix bash tool to use runtime interface for SSH support
ammar-agent Oct 25, 2025
05cbe62
🤖 Fix zombie process handling in LocalRuntime with 'exit' event
ammar-agent Oct 25, 2025
879cd7d
🤖 Fix bash tool SSH support - use runtime's workdir and writeFile
ammar-agent Oct 25, 2025
e1c7089
🤖 Skip local path validation for SSH runtime in file tools
ammar-agent Oct 25, 2025
0e303de
🤖 Auto-format code with Prettier
ammar-agent Oct 25, 2025
0c9c49e
🤖 Fix SSH runtime shell escaping bug & add file tools integration tests
ammar-agent Oct 25, 2025
a92b1b0
Fix static check issues (lint + typecheck)
ammar-agent Oct 25, 2025
57035a1
Update workspaceInitHook tests to expect workspace creation logs
ammar-agent Oct 25, 2025
6f7276a
Fix formatting
ammar-agent Oct 25, 2025
596f5a5
Fix timing test to filter workspace creation logs
ammar-agent Oct 25, 2025
bf5cc80
🤖 Fix SSH runtime environment variable handling
ammar-agent Oct 25, 2025
4b40f74
🤖 Retry CI
ammar-agent Oct 25, 2025
b60d0ed
🤖 Fix /Users/ammar expansion in SSH cd commands
ammar-agent Oct 25, 2025
1572c06
🤖 Replace manual shell escaping with shescape library
ammar-agent Oct 25, 2025
6d5bcdd
🤖 Add runtimeExecuteBash test with shared test helpers
ammar-agent Oct 25, 2025
87b43ea
🐛 Fix SSH runtime tilde path expansion in cd commands
ammar-agent Oct 25, 2025
845f672
⚡ Add SSH connection multiplexing and increase server limits
ammar-agent Oct 25, 2025
6013b98
🤖 Fix tilde path expansion in SSH runtime
ammar-agent Oct 25, 2025
bd379ec
🤖 Fix formatting
ammar-agent Oct 25, 2025
aedcfa3
🤖 Upgrade integration test runner and increase parallelism
ammar-agent Oct 25, 2025
2cde0e5
🤖 Fix 'spawn bash ENOENT' by using full bash paths
ammar-agent Oct 25, 2025
378ea14
🤖 Check workdir exists before spawning in LocalRuntime
ammar-agent Oct 25, 2025
820fabd
🤖 Make exec() async to properly check workdir exists
ammar-agent Oct 25, 2025
67b44c1
🤖 Fix remaining test-helpers await
ammar-agent Oct 25, 2025
8901c44
🤖 Fix linting issues
ammar-agent Oct 25, 2025
33fe278
🤖 Fix(jest): ESM provider preload caused Integration Test failures\n\…
ammar-agent Oct 25, 2025
c75efe5
🤖 Fix Jest integration tests: remove preloading, optimize tokenizer
ammar-agent Oct 25, 2025
5998055
🤖 Fix runtimeExecuteBash tests: use tool-call-start events
ammar-agent Oct 25, 2025
b408a7a
🤖 Add tokenizer loading beforeAll to all integration test files
ammar-agent Oct 25, 2025
f8ba9ff
🤖 Pass test_filter to e2e and storybook tests
ammar-agent Oct 25, 2025
902cc0a
🤖 Skip storybook and e2e tests when test_filter is set
ammar-agent Oct 25, 2025
95c0a14
🤖 Refactor workspace deletion to Runtime interface
ammar-agent Oct 26, 2025
48efcd8
🤖 Fix lint errors in deleteWorkspace implementation
ammar-agent Oct 26, 2025
e951624
Add maxWorkers=100% and --silent to integration tests for better output
ammar-agent Oct 26, 2025
ed03395
🤖 Fix integration test race condition in AI SDK provider imports
ammar-agent Oct 26, 2025
2be2942
Use maxWorkers=200% for integration tests in CI
ammar-agent Oct 26, 2025
5c5049b
Add workflow_dispatch suggestion to wait_pr_checks for faster iteration
ammar-agent Oct 26, 2025
f16addb
🤖 Fix SSH runtime test timeout issues
ammar-agent Oct 26, 2025
7149cf1
🤖 Delete config.getWorkspacePath() - Runtime is single source of truth
ammar-agent Oct 26, 2025
304375a
Fix SSH runtime path handling - use workspacePath instead of srcBaseDir
ammar-agent Oct 26, 2025
0aab8bb
Fix test expectations for srcBaseDir refactor
ammar-agent Oct 26, 2025
d190e24
Add test-workspaces to gitignore
ammar-agent Oct 26, 2025
ae9211f
Run prettier
ammar-agent Oct 26, 2025
f98e447
Fix SSH deleteWorkspace to handle non-existent directories
ammar-agent Oct 26, 2025
003cc53
Fix bash tool hanging by closing stdin immediately
ammar-agent Oct 26, 2025
0a78353
Fix tool cwd to use workspacePath and enhance test diagnostics
ammar-agent Oct 26, 2025
04804ef
Remove unused runtimeConfig variable
ammar-agent Oct 26, 2025
18326ba
Fix SSH workspace rename tilde expansion bug
ammar-agent Oct 26, 2025
9465e44
Fix formatting in renameWorkspace test
ammar-agent Oct 26, 2025
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
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ on:
description: 'Optional test filter (e.g., "workspace", "tests/file.test.ts", or "-t pattern")'
required: false
type: string
# This filter is passed to unit tests, integration tests, e2e tests, and storybook tests
# to enable faster iteration when debugging specific test failures in CI

jobs:
static-check:
Expand Down Expand Up @@ -85,7 +87,7 @@ jobs:

integration-test:
name: Integration Tests
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-24.04-32' || 'ubuntu-latest' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -95,7 +97,7 @@ jobs:
- uses: ./.github/actions/setup-cmux

- name: Run integration tests with coverage
run: TEST_INTEGRATION=1 bun x jest --coverage ${{ github.event.inputs.test_filter || 'tests' }}
run: TEST_INTEGRATION=1 bun x jest --coverage --maxWorkers=200% --silent ${{ github.event.inputs.test_filter || 'tests' }}
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
Expand All @@ -111,6 +113,7 @@ jobs:
storybook-test:
name: Storybook Interaction Tests
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
if: github.event.inputs.test_filter == ''
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -136,6 +139,7 @@ jobs:
e2e-test:
name: End-to-End Tests
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
if: github.event.inputs.test_filter == ''
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,4 @@ tmpfork
.cmux-agent-cli
storybook-static/
*.tgz
src/test-workspaces/
219 changes: 89 additions & 130 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Verify with React DevTools Profiler - MarkdownCore should only re-render when co

## Documentation Guidelines

**Free-floating markdown docs are not permitted.** Documentation must be organized:
**Free-floating markdown docs are not permitted.** Documentation must be organized. Do not create standalone markdown files in the project root or random locations, even for implementation summaries or planning documents - use the propose_plan tool or inline comments instead.

- **User-facing docs** → `./docs/` directory
- **IMPORTANT**: Read `docs/README.md` first before writing user-facing documentation
Expand All @@ -119,6 +119,7 @@ Verify with React DevTools Profiler - MarkdownCore should only re-render when co
- Use standard markdown + mermaid diagrams
- **Developer docs** → inline with the code its documenting as comments. Consider them notes as notes to future Assistants to understand the logic more quickly.
**DO NOT** create standalone documentation files in the project root or random locations.
- **Test documentation** → inline comments in test files explaining complex test setup or edge cases, NOT separate README files.

**NEVER create markdown documentation files (README, guides, summaries, etc.) in the project root during feature development unless the user explicitly requests documentation.** Code + tests + inline comments are complete documentation.

Expand Down Expand Up @@ -204,6 +205,7 @@ This project uses **Make** as the primary build orchestrator. See `Makefile` for
- **Integration tests:**
- Run specific integration test: `TEST_INTEGRATION=1 bun x jest tests/ipcMain/sendMessage.test.ts -t "test name pattern"`
- Run all integration tests: `TEST_INTEGRATION=1 bun x jest tests` (~35 seconds, runs 40 tests)
- **⚠️ Running `tests/ipcMain` locally takes a very long time.** Prefer running specific test files or use `-t` to filter to specific tests.
- **Performance**: Tests use `test.concurrent()` to run in parallel within each file
- **NEVER bypass IPC in integration tests** - Integration tests must use the real IPC communication paths (e.g., `mockIpcRenderer.invoke()`) even when it's harder. Directly accessing services (HistoryService, PartialService, etc.) or manipulating config/state directly bypasses the integration layer and defeats the purpose of the test.

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"docs:watch": "make docs-watch",
"storybook": "make storybook",
"storybook:build": "make storybook-build",
"test:storybook": "make test-storybook"
"test:storybook": "make test-storybook",
"rebuild": "echo \"No native modules to rebuild\""
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.29",
Expand Down Expand Up @@ -69,6 +70,7 @@
"markdown-it": "^14.1.0",
"minimist": "^1.2.8",
"rehype-harden": "^1.1.5",
"shescape": "^2.1.6",
"source-map-support": "^0.5.21",
"streamdown": "^1.4.0",
"undici": "^7.16.0",
Expand Down
4 changes: 4 additions & 0 deletions scripts/wait_pr_checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ while true; do
echo "💡 To extract detailed logs from the failed run:"
echo " ./scripts/extract_pr_logs.sh $PR_NUMBER"
echo " ./scripts/extract_pr_logs.sh $PR_NUMBER <job_pattern> # e.g., Integration"
echo ""
echo "💡 To re-run a subset of integration tests faster with workflow_dispatch:"
echo " gh workflow run ci.yml --ref $(git rev-parse --abbrev-ref HEAD) -f test_filter=\"tests/ipcMain/specificTest.test.ts\""
echo " gh workflow run ci.yml --ref $(git rev-parse --abbrev-ref HEAD) -f test_filter=\"-t 'specific test name'\""
exit 1
fi

Expand Down
42 changes: 24 additions & 18 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import { CommandPalette } from "./components/CommandPalette";
import { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources";

import type { ThinkingLevel } from "./types/thinking";
import type { RuntimeConfig } from "./types/runtime";
import { CUSTOM_EVENTS } from "./constants/events";
import { isWorkspaceForkSwitchEvent } from "./utils/workspaceFork";
import { getThinkingLevelKey } from "./constants/storage";
import type { BranchListResult } from "./types/ipc";
import { useTelemetry } from "./hooks/useTelemetry";
import { parseRuntimeString } from "./utils/chatCommands";

const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"];

Expand Down Expand Up @@ -233,15 +235,35 @@ function AppInner() {
[handleRemoveProject]
);

const handleCreateWorkspace = async (branchName: string, trunkBranch: string) => {
const handleCreateWorkspace = async (
branchName: string,
trunkBranch: string,
runtime?: string
) => {
if (!workspaceModalProject) return;

console.assert(
typeof trunkBranch === "string" && trunkBranch.trim().length > 0,
"Expected trunk branch to be provided by the workspace modal"
);

const newWorkspace = await createWorkspace(workspaceModalProject, branchName, trunkBranch);
// Parse runtime config if provided
let runtimeConfig: RuntimeConfig | undefined;
if (runtime) {
try {
runtimeConfig = parseRuntimeString(runtime, branchName);
} catch (err) {
console.error("Failed to parse runtime config:", err);
throw err; // Let modal handle the error
}
}

const newWorkspace = await createWorkspace(
workspaceModalProject,
branchName,
trunkBranch,
runtimeConfig
);
if (newWorkspace) {
// Track workspace creation
telemetry.workspaceCreated(newWorkspace.workspaceId);
Expand Down Expand Up @@ -406,21 +428,6 @@ function AppInner() {
[handleAddWorkspace]
);

const createWorkspaceFromPalette = useCallback(
async (projectPath: string, branchName: string, trunkBranch: string) => {
console.assert(
typeof trunkBranch === "string" && trunkBranch.trim().length > 0,
"Expected trunk branch to be provided by the command palette"
);
const newWs = await createWorkspace(projectPath, branchName, trunkBranch);
if (newWs) {
telemetry.workspaceCreated(newWs.workspaceId);
setSelectedWorkspace(newWs);
}
},
[createWorkspace, setSelectedWorkspace, telemetry]
);

const getBranchesForProject = useCallback(
async (projectPath: string): Promise<BranchListResult> => {
const branchResult = await window.api.projects.listBranches(projectPath);
Expand Down Expand Up @@ -488,7 +495,6 @@ function AppInner() {
getThinkingLevel: getThinkingLevelForWorkspace,
onSetThinkingLevel: setThinkingLevelFromPalette,
onOpenNewWorkspaceModal: openNewWorkspaceFromPalette,
onCreateWorkspace: createWorkspaceFromPalette,
getBranchesForProject,
onSelectWorkspace: selectWorkspaceFromPalette,
onRemoveWorkspace: removeWorkspaceFromPalette,
Expand Down
80 changes: 74 additions & 6 deletions src/components/NewWorkspaceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ interface NewWorkspaceModalProps {
defaultTrunkBranch?: string;
loadErrorMessage?: string | null;
onClose: () => void;
onAdd: (branchName: string, trunkBranch: string) => Promise<void>;
onAdd: (branchName: string, trunkBranch: string, runtime?: string) => Promise<void>;
}

// Shared form field styles
const formFieldClasses =
"[&_label]:text-foreground [&_input]:bg-modal-bg [&_input]:border-border-medium [&_input]:focus:border-accent [&_select]:bg-modal-bg [&_select]:border-border-medium [&_select]:focus:border-accent [&_option]:bg-modal-bg mb-5 [&_input]:w-full [&_input]:rounded [&_input]:border [&_input]:px-3 [&_input]:py-2 [&_input]:text-sm [&_input]:text-white [&_input]:focus:outline-none [&_input]:disabled:cursor-not-allowed [&_input]:disabled:opacity-60 [&_label]:mb-2 [&_label]:block [&_label]:text-sm [&_option]:text-white [&_select]:w-full [&_select]:cursor-pointer [&_select]:rounded [&_select]:border [&_select]:px-3 [&_select]:py-2 [&_select]:text-sm [&_select]:text-white [&_select]:focus:outline-none [&_select]:disabled:cursor-not-allowed [&_select]:disabled:opacity-60";

const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
isOpen,
projectName,
Expand All @@ -24,6 +28,8 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
}) => {
const [branchName, setBranchName] = useState("");
const [trunkBranch, setTrunkBranch] = useState(defaultTrunkBranch ?? branches[0] ?? "");
const [runtimeMode, setRuntimeMode] = useState<"local" | "ssh">("local");
const [sshHost, setSshHost] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const infoId = useId();
Expand Down Expand Up @@ -53,6 +59,8 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
const handleCancel = () => {
setBranchName("");
setTrunkBranch(defaultTrunkBranch ?? branches[0] ?? "");
setRuntimeMode("local");
setSshHost("");
setError(loadErrorMessage ?? null);
onClose();
};
Expand All @@ -74,13 +82,29 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
console.assert(normalizedTrunkBranch.length > 0, "Expected trunk branch name to be validated");
console.assert(trimmedBranchName.length > 0, "Expected branch name to be validated");

// Validate SSH host if SSH runtime selected
if (runtimeMode === "ssh") {
const trimmedHost = sshHost.trim();
if (trimmedHost.length === 0) {
setError("SSH host is required (e.g., hostname or user@host)");
return;
}
// Accept both "hostname" and "user@hostname" formats
// SSH will use current user or ~/.ssh/config if user not specified
}

setIsLoading(true);
setError(null);

try {
await onAdd(trimmedBranchName, normalizedTrunkBranch);
// Build runtime string if SSH selected
const runtime = runtimeMode === "ssh" ? `ssh ${sshHost.trim()}` : undefined;

await onAdd(trimmedBranchName, normalizedTrunkBranch, runtime);
setBranchName("");
setTrunkBranch(defaultTrunkBranch ?? branches[0] ?? "");
setRuntimeMode("local");
setSshHost("");
onClose();
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to create workspace";
Expand All @@ -100,7 +124,7 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
describedById={infoId}
>
<form onSubmit={(event) => void handleSubmit(event)}>
<div className="[&_label]:text-foreground [&_input]:bg-modal-bg [&_input]:border-border-medium [&_input]:focus:border-accent [&_select]:bg-modal-bg [&_select]:border-border-medium [&_select]:focus:border-accent [&_option]:bg-modal-bg mb-5 [&_input]:w-full [&_input]:rounded [&_input]:border [&_input]:px-3 [&_input]:py-2 [&_input]:text-sm [&_input]:text-white [&_input]:focus:outline-none [&_input]:disabled:cursor-not-allowed [&_input]:disabled:opacity-60 [&_label]:mb-2 [&_label]:block [&_label]:text-sm [&_option]:text-white [&_select]:w-full [&_select]:cursor-pointer [&_select]:rounded [&_select]:border [&_select]:px-3 [&_select]:py-2 [&_select]:text-sm [&_select]:text-white [&_select]:focus:outline-none [&_select]:disabled:cursor-not-allowed [&_select]:disabled:opacity-60">
<div className={formFieldClasses}>
<label htmlFor="branchName">
<TooltipWrapper inline>
<span className="cursor-help underline decoration-[#666] decoration-dotted underline-offset-2">
Expand Down Expand Up @@ -137,7 +161,7 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
{error && <div className="text-danger-light mt-1.5 text-[13px]">{error}</div>}
</div>

<div className="[&_label]:text-foreground [&_input]:bg-modal-bg [&_input]:border-border-medium [&_input]:focus:border-accent [&_select]:bg-modal-bg [&_select]:border-border-medium [&_select]:focus:border-accent [&_option]:bg-modal-bg mb-5 [&_input]:w-full [&_input]:rounded [&_input]:border [&_input]:px-3 [&_input]:py-2 [&_input]:text-sm [&_input]:text-white [&_input]:focus:outline-none [&_input]:disabled:cursor-not-allowed [&_input]:disabled:opacity-60 [&_label]:mb-2 [&_label]:block [&_label]:text-sm [&_option]:text-white [&_select]:w-full [&_select]:cursor-pointer [&_select]:rounded [&_select]:border [&_select]:px-3 [&_select]:py-2 [&_select]:text-sm [&_select]:text-white [&_select]:focus:outline-none [&_select]:disabled:cursor-not-allowed [&_select]:disabled:opacity-60">
<div className={formFieldClasses}>
<label htmlFor="trunkBranch">Trunk Branch:</label>
{hasBranches ? (
<select
Expand Down Expand Up @@ -173,18 +197,62 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
)}
</div>

<div className={formFieldClasses}>
<label htmlFor="runtimeMode">Runtime:</label>
<select
id="runtimeMode"
value={runtimeMode}
onChange={(event) => {
setRuntimeMode(event.target.value as "local" | "ssh");
setError(null);
}}
disabled={isLoading}
>
<option value="local">Local</option>
<option value="ssh">SSH Remote</option>
</select>
</div>

{runtimeMode === "ssh" && (
<div className={formFieldClasses}>
<label htmlFor="sshHost">SSH Host:</label>
<input
id="sshHost"
type="text"
value={sshHost}
onChange={(event) => {
setSshHost(event.target.value);
setError(null);
}}
placeholder="hostname or user@hostname"
disabled={isLoading}
required
aria-required="true"
/>
<div className="text-muted mt-1.5 text-[13px]">
Workspace will be created at ~/cmux/{branchName || "<branch-name>"} on remote host
</div>
</div>
)}

<ModalInfo id={infoId}>
<p>This will create a git worktree at:</p>
<code className="block break-all">
~/.cmux/src/{projectName}/{branchName || "<branch-name>"}
{runtimeMode === "ssh"
? `${sshHost || "<host>"}:~/cmux/${branchName || "<branch-name>"}`
: `~/.cmux/src/${projectName}/${branchName || "<branch-name>"}`}
</code>
</ModalInfo>

{branchName.trim() && (
<div>
<div className="text-muted mb-2 font-sans text-xs">Equivalent command:</div>
<div className="bg-dark border-border-light text-light mt-5 rounded border p-3 font-mono text-[13px] break-all whitespace-pre-wrap">
{formatNewCommand(branchName.trim(), trunkBranch.trim() || undefined)}
{formatNewCommand(
branchName.trim(),
trunkBranch.trim() || undefined,
runtimeMode === "ssh" && sshHost.trim() ? `ssh ${sshHost.trim()}` : undefined
)}
</div>
</div>
)}
Expand Down
25 changes: 6 additions & 19 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,24 +129,6 @@ export class Config {
* Get the workspace worktree path for a given directory name.
* The directory name is the workspace name (branch name).
*/
getWorkspacePath(projectPath: string, directoryName: string): string {
const projectName = this.getProjectName(projectPath);
return path.join(this.srcDir, projectName, directoryName);
}

/**
* Compute workspace path from metadata.
* Directory uses workspace name (e.g., ~/.cmux/src/project/workspace-name).
*/
getWorkspacePaths(metadata: WorkspaceMetadata): {
/** Worktree path (uses workspace name as directory) */
namedWorkspacePath: string;
} {
const path = this.getWorkspacePath(metadata.projectPath, metadata.name);
return {
namedWorkspacePath: path,
};
}

/**
* Add paths to WorkspaceMetadata to create FrontendWorkspaceMetadata.
Expand Down Expand Up @@ -274,6 +256,8 @@ export class Config {
projectPath,
// GUARANTEE: All workspaces must have createdAt (assign now if missing)
createdAt: workspace.createdAt ?? new Date().toISOString(),
// Include runtime config if present (for SSH workspaces)
runtimeConfig: workspace.runtimeConfig,
};

// Migrate missing createdAt to config for next load
Expand Down Expand Up @@ -383,7 +367,10 @@ export class Config {
// Check if workspace already exists (by ID)
const existingIndex = project.workspaces.findIndex((w) => w.id === metadata.id);

const workspacePath = this.getWorkspacePath(projectPath, metadata.name);
// Compute workspace path - this is only for legacy config migration
// New code should use Runtime.getWorkspacePath() directly
const projectName = this.getProjectName(projectPath);
const workspacePath = path.join(this.srcDir, projectName, metadata.name);
const workspaceEntry: Workspace = {
path: workspacePath,
id: metadata.id,
Expand Down
14 changes: 14 additions & 0 deletions src/constants/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Standard environment variables for non-interactive command execution.
* These prevent tools from blocking on editor/credential prompts.
*/
export const NON_INTERACTIVE_ENV_VARS = {
// Prevent interactive editors from blocking execution
// Critical for git operations like rebase/commit that try to open editors
GIT_EDITOR: "true", // Git-specific editor (highest priority)
GIT_SEQUENCE_EDITOR: "true", // For interactive rebase sequences
EDITOR: "true", // General fallback for non-git commands
VISUAL: "true", // Another common editor environment variable
// Prevent git from prompting for credentials
GIT_TERMINAL_PROMPT: "0", // Disables git credential prompts
} as const;
Loading