Skip to content
Merged
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
54 changes: 17 additions & 37 deletions src/browser/components/ChatInput/CreationControls.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import React from "react";
import { RUNTIME_MODE, type RuntimeMode } from "@/common/types/runtime";
import { TooltipWrapper, Tooltip } from "../Tooltip";
import { Select } from "../Select";
import { RuntimeIconSelector } from "../RuntimeIconSelector";

interface CreationControlsProps {
branches: string[];
trunkBranch: string;
onTrunkBranchChange: (branch: string) => void;
runtimeMode: RuntimeMode;
defaultRuntimeMode: RuntimeMode;
sshHost: string;
onRuntimeChange: (mode: RuntimeMode, host: string) => void;
/** Called when user clicks a runtime icon to select it (does not persist) */
onRuntimeModeChange: (mode: RuntimeMode) => void;
/** Called when user checks "Default for project" checkbox (persists) */
onSetDefaultRuntime: (mode: RuntimeMode) => void;
/** Called when user changes SSH host */
onSshHostChange: (host: string) => void;
disabled: boolean;
}

Expand All @@ -25,40 +31,14 @@ export function CreationControls(props: CreationControlsProps) {

return (
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
{/* Runtime Selector - first */}
<div
className="flex items-center gap-1"
data-component="RuntimeSelectorGroup"
data-tutorial="runtime-selector"
>
<label className="text-muted text-xs">Runtime:</label>
<Select
value={props.runtimeMode}
options={[
{ value: RUNTIME_MODE.LOCAL, label: "Local" },
{ value: RUNTIME_MODE.WORKTREE, label: "Worktree" },
{ value: RUNTIME_MODE.SSH, label: "SSH" },
]}
onChange={(newMode) => {
const mode = newMode as RuntimeMode;
// Preserve SSH host across mode switches so it's remembered when returning to SSH
props.onRuntimeChange(mode, props.sshHost);
}}
disabled={props.disabled}
aria-label="Runtime mode"
/>
<TooltipWrapper inline>
<span className="text-muted cursor-help text-xs">?</span>
<Tooltip className="tooltip" align="center" width="wide">
<strong>Runtime:</strong>
<br />
• Local: work directly in project directory (no isolation)
<br />
• Worktree: git worktree in ~/.mux/src (isolated)
<br />• SSH: remote clone on SSH host
</Tooltip>
</TooltipWrapper>
</div>
{/* Runtime Selector - icon-based with tooltips */}
<RuntimeIconSelector
value={props.runtimeMode}
onChange={props.onRuntimeModeChange}
defaultMode={props.defaultRuntimeMode}
onSetDefault={props.onSetDefaultRuntime}
disabled={props.disabled}
/>

{/* Trunk Branch Selector - hidden for Local runtime */}
{showTrunkBranchSelector && (
Expand Down Expand Up @@ -86,7 +66,7 @@ export function CreationControls(props: CreationControlsProps) {
<input
type="text"
value={props.sshHost}
onChange={(e) => props.onRuntimeChange(RUNTIME_MODE.SSH, e.target.value)}
onChange={(e) => props.onSshHostChange(e.target.value)}
placeholder="user@host"
disabled={props.disabled}
className="bg-separator text-foreground border-border-medium focus:border-accent w-32 rounded border px-1 py-0.5 text-xs focus:outline-none disabled:opacity-50"
Expand Down
5 changes: 4 additions & 1 deletion src/browser/components/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1382,8 +1382,11 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
trunkBranch={creationState.trunkBranch}
onTrunkBranchChange={creationState.setTrunkBranch}
runtimeMode={creationState.runtimeMode}
defaultRuntimeMode={creationState.defaultRuntimeMode}
sshHost={creationState.sshHost}
onRuntimeChange={creationState.setRuntimeOptions}
onRuntimeModeChange={creationState.setRuntimeMode}
onSetDefaultRuntime={creationState.setDefaultRuntimeMode}
onSshHostChange={creationState.setSshHost}
disabled={creationState.isSending || isSending}
/>
)}
Expand Down
38 changes: 29 additions & 9 deletions src/browser/components/ChatInput/useCreationWorkspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -473,41 +473,58 @@ function createDraftSettingsHarness(
sshHost: string;
trunkBranch: string;
runtimeString?: string | undefined;
defaultRuntimeMode?: RuntimeMode;
}>
) {
const state = {
runtimeMode: initial?.runtimeMode ?? ("local" as RuntimeMode),
defaultRuntimeMode: initial?.defaultRuntimeMode ?? ("worktree" as RuntimeMode),
sshHost: initial?.sshHost ?? "",
trunkBranch: initial?.trunkBranch ?? "main",
runtimeString: initial?.runtimeString,
} satisfies {
runtimeMode: RuntimeMode;
defaultRuntimeMode: RuntimeMode;
sshHost: string;
trunkBranch: string;
runtimeString: string | undefined;
};

const setRuntimeOptions = mock((mode: RuntimeMode, host: string) => {
const setTrunkBranch = mock((branch: string) => {
state.trunkBranch = branch;
});

const getRuntimeString = mock(() => state.runtimeString);

const setRuntimeMode = mock((mode: RuntimeMode) => {
state.runtimeMode = mode;
state.sshHost = host;
const trimmedHost = host.trim();
const trimmedHost = state.sshHost.trim();
state.runtimeString = mode === "ssh" ? (trimmedHost ? `ssh ${trimmedHost}` : "ssh") : undefined;
});

const setTrunkBranch = mock((branch: string) => {
state.trunkBranch = branch;
const setDefaultRuntimeMode = mock((mode: RuntimeMode) => {
state.defaultRuntimeMode = mode;
state.runtimeMode = mode;
const trimmedHost = state.sshHost.trim();
state.runtimeString = mode === "ssh" ? (trimmedHost ? `ssh ${trimmedHost}` : "ssh") : undefined;
});

const getRuntimeString = mock(() => state.runtimeString);
const setSshHost = mock((host: string) => {
state.sshHost = host;
});

return {
state,
setRuntimeOptions,
setRuntimeMode,
setDefaultRuntimeMode,
setSshHost,
setTrunkBranch,
getRuntimeString,
snapshot(): {
settings: DraftWorkspaceSettings;
setRuntimeOptions: typeof setRuntimeOptions;
setRuntimeMode: typeof setRuntimeMode;
setDefaultRuntimeMode: typeof setDefaultRuntimeMode;
setSshHost: typeof setSshHost;
setTrunkBranch: typeof setTrunkBranch;
getRuntimeString: typeof getRuntimeString;
} {
Expand All @@ -516,12 +533,15 @@ function createDraftSettingsHarness(
thinkingLevel: "medium",
mode: "exec",
runtimeMode: state.runtimeMode,
defaultRuntimeMode: state.defaultRuntimeMode,
sshHost: state.sshHost,
trunkBranch: state.trunkBranch,
};
return {
settings,
setRuntimeOptions,
setRuntimeMode,
setDefaultRuntimeMode,
setSshHost,
setTrunkBranch,
getRuntimeString,
};
Expand Down
23 changes: 19 additions & 4 deletions src/browser/components/ChatInput/useCreationWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ interface UseCreationWorkspaceReturn {
trunkBranch: string;
setTrunkBranch: (branch: string) => void;
runtimeMode: RuntimeMode;
defaultRuntimeMode: RuntimeMode;
sshHost: string;
setRuntimeOptions: (mode: RuntimeMode, host: string) => void;
/** Set the currently selected runtime mode (does not persist) */
setRuntimeMode: (mode: RuntimeMode) => void;
/** Set the default runtime mode for this project (persists via checkbox) */
setDefaultRuntimeMode: (mode: RuntimeMode) => void;
/** Set the SSH host (persisted separately from runtime mode) */
setSshHost: (host: string) => void;
toast: Toast | null;
setToast: (toast: Toast | null) => void;
isSending: boolean;
Expand All @@ -72,8 +78,14 @@ export function useCreationWorkspace({
const [isSending, setIsSending] = useState(false);

// Centralized draft workspace settings with automatic persistence
const { settings, setRuntimeOptions, setTrunkBranch, getRuntimeString } =
useDraftWorkspaceSettings(projectPath, branches, recommendedTrunk);
const {
settings,
setRuntimeMode,
setDefaultRuntimeMode,
setSshHost,
setTrunkBranch,
getRuntimeString,
} = useDraftWorkspaceSettings(projectPath, branches, recommendedTrunk);

// Get send options from shared hook (uses project-scoped storage key)
const sendMessageOptions = useSendMessageOptions(getProjectScopeId(projectPath));
Expand Down Expand Up @@ -180,8 +192,11 @@ export function useCreationWorkspace({
trunkBranch: settings.trunkBranch,
setTrunkBranch,
runtimeMode: settings.runtimeMode,
defaultRuntimeMode: settings.defaultRuntimeMode,
sshHost: settings.sshHost,
setRuntimeOptions,
setRuntimeMode,
setDefaultRuntimeMode,
setSshHost,
toast,
setToast,
isSending,
Expand Down
67 changes: 1 addition & 66 deletions src/browser/components/RuntimeBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { RuntimeConfig } from "@/common/types/runtime";
import { isSSHRuntime, isWorktreeRuntime, isLocalProjectRuntime } from "@/common/types/runtime";
import { extractSshHostname } from "@/browser/utils/ui/runtimeBadge";
import { TooltipWrapper, Tooltip } from "./Tooltip";
import { SSHIcon, WorktreeIcon, LocalIcon } from "./icons/RuntimeIcons";

interface RuntimeBadgeProps {
runtimeConfig?: RuntimeConfig;
Expand All @@ -12,72 +13,6 @@ interface RuntimeBadgeProps {
isWorking?: boolean;
}

/** Server rack icon for SSH runtime */
function SSHIcon() {
return (
<svg
width="10"
height="10"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="SSH Runtime"
>
<rect x="2" y="2" width="12" height="5" rx="1" />
<rect x="2" y="9" width="12" height="5" rx="1" />
<circle cx="5" cy="4.5" r="0.5" fill="currentColor" />
<circle cx="5" cy="11.5" r="0.5" fill="currentColor" />
</svg>
);
}

/** Git branch icon for worktree runtime */
function WorktreeIcon() {
return (
<svg
width="10"
height="10"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Worktree Runtime"
>
{/* Simplified git branch: vertical line with branch off */}
<circle cx="8" cy="3" r="2" />
<circle cx="8" cy="13" r="2" />
<line x1="8" y1="5" x2="8" y2="11" />
<circle cx="12" cy="7" r="2" />
<path d="M10 7 L8 9" />
</svg>
);
}

/** Folder icon for local project-dir runtime */
function LocalIcon() {
return (
<svg
width="10"
height="10"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Local Runtime"
>
{/* Folder icon */}
<path d="M2 4 L2 13 L14 13 L14 5 L8 5 L7 3 L2 3 L2 4" />
</svg>
);
}

// Runtime-specific color schemes - each type has consistent colors in idle/working states
// Idle: subtle with visible colored border for discrimination
// Working: brighter colors with pulse animation
Expand Down
Loading