Skip to content

Commit 9ac514b

Browse files
committed
🤖 Deduplicate runtime mode types and constants
Eliminates duplication of "local" | "ssh" union and magic strings across codebase: **New shared types/constants in src/types/runtime.ts:** - RuntimeMode type: "local" | "ssh" - RUNTIME_MODE constants: { LOCAL: "local", SSH: "ssh" } - SSH_RUNTIME_PREFIX constant: "ssh " - parseRuntimeModeAndHost(): Parse runtime strings to mode+host - buildRuntimeString(): Build runtime strings from mode+host **Updated to use shared types:** - useNewWorkspaceOptions hook - NewWorkspaceModal component - chatCommands utilities **Benefits:** - Single source of truth for runtime mode values - No more magic strings scattered across files - Type-safe runtime mode handling - Easier refactoring if runtime types expand in future - Reduced code duplication (41 lines removed, 88 added with utilities)
1 parent 0aa029a commit 9ac514b

File tree

4 files changed

+88
-41
lines changed

4 files changed

+88
-41
lines changed

src/components/NewWorkspaceModal.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Modal, ModalInfo, ModalActions, CancelButton, PrimaryButton } from "./M
33
import { TooltipWrapper, Tooltip } from "./Tooltip";
44
import { formatNewCommand } from "@/utils/chatCommands";
55
import { useNewWorkspaceOptions } from "@/hooks/useNewWorkspaceOptions";
6+
import { RUNTIME_MODE } from "@/types/runtime";
67

78
interface NewWorkspaceModalProps {
89
isOpen: boolean;
@@ -64,7 +65,7 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
6465
const handleCancel = () => {
6566
setBranchName("");
6667
setTrunkBranch(defaultTrunkBranch ?? branches[0] ?? "");
67-
setRuntimeOptions("local", "");
68+
setRuntimeOptions(RUNTIME_MODE.LOCAL, "");
6869
setError(loadErrorMessage ?? null);
6970
onClose();
7071
};
@@ -87,7 +88,7 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
8788
console.assert(trimmedBranchName.length > 0, "Expected branch name to be validated");
8889

8990
// Validate SSH host if SSH runtime selected
90-
if (runtimeMode === "ssh") {
91+
if (runtimeMode === RUNTIME_MODE.SSH) {
9192
const trimmedHost = sshHost.trim();
9293
if (trimmedHost.length === 0) {
9394
setError("SSH host is required (e.g., hostname or user@host)");
@@ -107,7 +108,7 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
107108
await onAdd(trimmedBranchName, normalizedTrunkBranch, runtime);
108109
setBranchName("");
109110
setTrunkBranch(defaultTrunkBranch ?? branches[0] ?? "");
110-
setRuntimeOptions("local", "");
111+
setRuntimeOptions(RUNTIME_MODE.LOCAL, "");
111112
onClose();
112113
} catch (err) {
113114
const message = err instanceof Error ? err.message : "Failed to create workspace";
@@ -206,26 +207,26 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
206207
id="runtimeMode"
207208
value={runtimeMode}
208209
onChange={(event) => {
209-
const newMode = event.target.value as "local" | "ssh";
210-
setRuntimeOptions(newMode, newMode === "local" ? "" : sshHost);
210+
const newMode = event.target.value as typeof RUNTIME_MODE.LOCAL | typeof RUNTIME_MODE.SSH;
211+
setRuntimeOptions(newMode, newMode === RUNTIME_MODE.LOCAL ? "" : sshHost);
211212
setError(null);
212213
}}
213214
disabled={isLoading}
214215
>
215-
<option value="local">Local</option>
216-
<option value="ssh">SSH Remote</option>
216+
<option value={RUNTIME_MODE.LOCAL}>Local</option>
217+
<option value={RUNTIME_MODE.SSH}>SSH Remote</option>
217218
</select>
218219
</div>
219220

220-
{runtimeMode === "ssh" && (
221+
{runtimeMode === RUNTIME_MODE.SSH && (
221222
<div className={formFieldClasses}>
222223
<label htmlFor="sshHost">SSH Host:</label>
223224
<input
224225
id="sshHost"
225226
type="text"
226227
value={sshHost}
227228
onChange={(event) => {
228-
setRuntimeOptions("ssh", event.target.value);
229+
setRuntimeOptions(RUNTIME_MODE.SSH, event.target.value);
229230
setError(null);
230231
}}
231232
placeholder="hostname or user@hostname"
@@ -242,7 +243,7 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
242243
<ModalInfo id={infoId}>
243244
<p>This will create a git worktree at:</p>
244245
<code className="block break-all">
245-
{runtimeMode === "ssh"
246+
{runtimeMode === RUNTIME_MODE.SSH
246247
? `${sshHost || "<host>"}:~/cmux/${branchName || "<branch-name>"}`
247248
: `~/.cmux/src/${projectName}/${branchName || "<branch-name>"}`}
248249
</code>
@@ -255,7 +256,7 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
255256
{formatNewCommand(
256257
branchName.trim(),
257258
trunkBranch.trim() || undefined,
258-
runtimeMode === "ssh" && sshHost.trim() ? `ssh ${sshHost.trim()}` : undefined
259+
getRuntimeString()
259260
)}
260261
</div>
261262
</div>

src/hooks/useNewWorkspaceOptions.ts

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { useState, useEffect } from "react";
22
import { getRuntimeKey } from "@/constants/storage";
3+
import {
4+
type RuntimeMode,
5+
RUNTIME_MODE,
6+
parseRuntimeModeAndHost,
7+
buildRuntimeString,
8+
} from "@/types/runtime";
39

410
export interface WorkspaceRuntimeOptions {
5-
runtimeMode: "local" | "ssh";
11+
runtimeMode: RuntimeMode;
612
sshHost: string;
713
/**
814
* Returns the runtime string for IPC calls (format: "ssh <host>" or undefined for local)
@@ -21,52 +27,36 @@ export interface WorkspaceRuntimeOptions {
2127
export function useNewWorkspaceOptions(
2228
projectPath: string | null | undefined,
2329
enabled = true
24-
): [WorkspaceRuntimeOptions, (mode: "local" | "ssh", host?: string) => void] {
25-
const [runtimeMode, setRuntimeMode] = useState<"local" | "ssh">("local");
30+
): [WorkspaceRuntimeOptions, (mode: RuntimeMode, host?: string) => void] {
31+
const [runtimeMode, setRuntimeMode] = useState<RuntimeMode>(RUNTIME_MODE.LOCAL);
2632
const [sshHost, setSshHost] = useState("");
2733

2834
// Load saved runtime preference when projectPath changes
2935
useEffect(() => {
3036
if (!enabled || !projectPath) {
3137
// Reset to defaults when disabled or no project
32-
setRuntimeMode("local");
38+
setRuntimeMode(RUNTIME_MODE.LOCAL);
3339
setSshHost("");
3440
return;
3541
}
3642

3743
const runtimeKey = getRuntimeKey(projectPath);
3844
const savedRuntime = localStorage.getItem(runtimeKey);
45+
const parsed = parseRuntimeModeAndHost(savedRuntime);
3946

40-
if (savedRuntime) {
41-
// Parse the saved runtime string (format: "ssh <host>" or undefined for local)
42-
if (savedRuntime.startsWith("ssh ")) {
43-
const host = savedRuntime.substring(4).trim();
44-
setRuntimeMode("ssh");
45-
setSshHost(host);
46-
} else {
47-
setRuntimeMode("local");
48-
setSshHost("");
49-
}
50-
} else {
51-
// No saved preference, use defaults
52-
setRuntimeMode("local");
53-
setSshHost("");
54-
}
47+
setRuntimeMode(parsed.mode);
48+
setSshHost(parsed.host);
5549
}, [projectPath, enabled]);
5650

5751
// Setter for updating both mode and host
58-
const setRuntimeOptions = (mode: "local" | "ssh", host?: string) => {
52+
const setRuntimeOptions = (mode: RuntimeMode, host?: string) => {
5953
setRuntimeMode(mode);
6054
setSshHost(host ?? "");
6155
};
6256

6357
// Helper to get runtime string for IPC calls
6458
const getRuntimeString = (): string | undefined => {
65-
if (runtimeMode === "ssh") {
66-
const trimmedHost = sshHost.trim();
67-
return trimmedHost ? `ssh ${trimmedHost}` : undefined;
68-
}
69-
return undefined;
59+
return buildRuntimeString(runtimeMode, sshHost);
7060
};
7161

7262
return [

src/types/runtime.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22
* Runtime configuration types for workspace execution environments
33
*/
44

5+
/** Runtime mode type - used in UI and runtime string parsing */
6+
export type RuntimeMode = "local" | "ssh";
7+
8+
/** Runtime mode constants */
9+
export const RUNTIME_MODE = {
10+
LOCAL: "local" as const,
11+
SSH: "ssh" as const,
12+
} as const;
13+
14+
/** Runtime string prefix for SSH mode (e.g., "ssh hostname") */
15+
export const SSH_RUNTIME_PREFIX = "ssh ";
16+
517
export type RuntimeConfig =
618
| {
719
type: "local";
@@ -19,3 +31,46 @@ export type RuntimeConfig =
1931
/** Optional: SSH port (default: 22) */
2032
port?: number;
2133
};
34+
35+
/**
36+
* Parse runtime string from localStorage or UI input into mode and host
37+
* Format: "ssh <host>" -> { mode: "ssh", host: "<host>" }
38+
* "local" or undefined -> { mode: "local", host: "" }
39+
*
40+
* Use this for UI state management (localStorage, form inputs)
41+
*/
42+
export function parseRuntimeModeAndHost(runtime: string | null | undefined): {
43+
mode: RuntimeMode;
44+
host: string;
45+
} {
46+
if (!runtime) {
47+
return { mode: RUNTIME_MODE.LOCAL, host: "" };
48+
}
49+
50+
const trimmed = runtime.trim();
51+
const lowerTrimmed = trimmed.toLowerCase();
52+
53+
if (lowerTrimmed === RUNTIME_MODE.LOCAL) {
54+
return { mode: RUNTIME_MODE.LOCAL, host: "" };
55+
}
56+
57+
if (lowerTrimmed.startsWith(SSH_RUNTIME_PREFIX)) {
58+
const host = trimmed.substring(SSH_RUNTIME_PREFIX.length).trim();
59+
return { mode: RUNTIME_MODE.SSH, host };
60+
}
61+
62+
// Default to local for unrecognized strings
63+
return { mode: RUNTIME_MODE.LOCAL, host: "" };
64+
}
65+
66+
/**
67+
* Build runtime string for storage/IPC from mode and host
68+
* Returns: "ssh <host>" for SSH, undefined for local
69+
*/
70+
export function buildRuntimeString(mode: RuntimeMode, host: string): string | undefined {
71+
if (mode === RUNTIME_MODE.SSH) {
72+
const trimmedHost = host.trim();
73+
return trimmedHost ? `${SSH_RUNTIME_PREFIX}${trimmedHost}` : undefined;
74+
}
75+
return undefined;
76+
}

src/utils/chatCommands.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { SendMessageOptions } from "@/types/ipc";
1010
import type { CmuxFrontendMetadata, CompactionRequestData } from "@/types/message";
1111
import type { FrontendWorkspaceMetadata } from "@/types/workspace";
1212
import type { RuntimeConfig } from "@/types/runtime";
13+
import { RUNTIME_MODE, SSH_RUNTIME_PREFIX } from "@/types/runtime";
1314
import { CUSTOM_EVENTS } from "@/constants/events";
1415
import type { Toast } from "@/components/ChatInputToast";
1516
import type { ParsedCommand } from "@/utils/slashCommands/types";
@@ -22,7 +23,7 @@ import { getRuntimeKey } from "@/constants/storage";
2223
// ============================================================================
2324

2425
/**
25-
* Parse runtime string from -r flag into RuntimeConfig
26+
* Parse runtime string from -r flag into RuntimeConfig for backend
2627
* Supports formats:
2728
* - "ssh <host>" or "ssh <user@host>" -> SSH runtime
2829
* - "local" -> Local runtime (explicit)
@@ -39,21 +40,21 @@ export function parseRuntimeString(
3940
const trimmed = runtime.trim();
4041
const lowerTrimmed = trimmed.toLowerCase();
4142

42-
if (lowerTrimmed === "local") {
43+
if (lowerTrimmed === RUNTIME_MODE.LOCAL) {
4344
return undefined; // Explicit local - let backend use default
4445
}
4546

4647
// Parse "ssh <host>" or "ssh <user@host>" format
47-
if (lowerTrimmed === "ssh" || lowerTrimmed.startsWith("ssh ")) {
48-
const hostPart = trimmed.slice(3).trim(); // Preserve original case for host, skip "ssh"
48+
if (lowerTrimmed === RUNTIME_MODE.SSH || lowerTrimmed.startsWith(SSH_RUNTIME_PREFIX)) {
49+
const hostPart = trimmed.slice(SSH_RUNTIME_PREFIX.length - 1).trim(); // Preserve original case for host
4950
if (!hostPart) {
5051
throw new Error("SSH runtime requires host (e.g., 'ssh hostname' or 'ssh user@host')");
5152
}
5253

5354
// Accept both "hostname" and "user@hostname" formats
5455
// SSH will use current user or ~/.ssh/config if user not specified
5556
return {
56-
type: "ssh",
57+
type: RUNTIME_MODE.SSH,
5758
host: hostPart,
5859
srcBaseDir: "~/cmux", // Default remote base directory (NOT including workspace name)
5960
};

0 commit comments

Comments
 (0)