From b6448952e73e3a29b21a7126fb60c237467a2183 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 23 Feb 2026 11:34:58 -0600 Subject: [PATCH 1/6] fix: improve workspace default runtime UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename 'configure' to 'set defaults' with accent color styling - Pass project scope when opening Settings → Runtimes - Add '(modified)' badge when runtime differs from default - Show runtime options in Settings → Runtimes page - Add options field to RuntimeUiSpec for single-ownership RFC: rfc/260223_workspace-default-runtime.md --- rfc/260223_workspace-default-runtime.md | 32 +++++++++++++++++++ rfc/README.md | 20 ++++++++++++ .../components/ChatInput/CreationControls.tsx | 11 ++++--- .../Settings/sections/RuntimesSection.tsx | 22 ++++++++++++- src/browser/contexts/SettingsContext.tsx | 15 +++++++++ src/browser/utils/runtimeUi.ts | 6 ++++ 6 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 rfc/260223_workspace-default-runtime.md create mode 100644 rfc/README.md diff --git a/rfc/260223_workspace-default-runtime.md b/rfc/260223_workspace-default-runtime.md new file mode 100644 index 0000000000..22869e61c5 --- /dev/null +++ b/rfc/260223_workspace-default-runtime.md @@ -0,0 +1,32 @@ +--- +author: @ammario +date: 2026-02-23 +--- + +# Workspace Default Runtime + +Commit 88a2be88e2 moved the workspace default runtime config from a tooltip checkbox to a +dedicated sections page. It left the following outstanding UX gaps: + +- Unclear path for user to set runtime config to default +- Unclear persistence behavior for runtime options + + +Two fixes are required: + +- Make the `configure` runtime more prominent, and call it `set defaults` instead. + - Create distinct visual style when the current runtime options are not the default so the user + can more quickly see how they would persist their changes. +- Include runtime options in the new runtime settings page to clarify how the defaults work there. + +There's also a bug where clicking the configure button on a project page takes you to the +settings page with a global scope instead of the project scope. We should fix this as well. + + +## Code Structure + +During this change, it is imperative that we have single-ownership of: + +- What options are available per runtime +- The setting / getting of defaults +- The list of runtime types \ No newline at end of file diff --git a/rfc/README.md b/rfc/README.md new file mode 100644 index 0000000000..4f1e8814db --- /dev/null +++ b/rfc/README.md @@ -0,0 +1,20 @@ +# RFC + +This folder contains human-written but AI-assisted RFCs for implementing major work items in Mux. + +It is an adjunct to AI planning but shifts content ownership to humans for more precise steering. + +RFCs should be in files named `_.md` in this folder. + +The file should be structured as follows: + +```markdown +--- +author: @<github-username> +date: <YYYY-MM-DD> +--- + +# <Title> + +... body ... +``` \ No newline at end of file diff --git a/src/browser/components/ChatInput/CreationControls.tsx b/src/browser/components/ChatInput/CreationControls.tsx index 6f95b6d6a6..6db298f927 100644 --- a/src/browser/components/ChatInput/CreationControls.tsx +++ b/src/browser/components/ChatInput/CreationControls.tsx @@ -871,16 +871,19 @@ export function CreationControls(props: CreationControlsProps) { {/* Runtime type - button group */} <div className="flex flex-col gap-1.5" data-component="RuntimeTypeGroup"> - {/* User request: keep the configure shortcut but render it in muted gray. */} <div className="flex items-center gap-1"> <label className="text-muted-foreground text-xs font-medium">Workspace Type</label> + {/* Visual cue when the user has changed the runtime from the project default. */} + {runtimeChoice !== props.defaultRuntimeMode && ( + <span className="text-warning text-[10px] font-medium">(modified)</span> + )} <span className="text-muted-foreground text-xs">-</span> <button type="button" - onClick={() => settings.open("runtimes")} - className="text-muted-foreground hover:text-foreground cursor-pointer text-xs font-medium hover:underline" + onClick={() => settings.open("runtimes", { runtimesProjectPath: props.projectPath })} + className="text-accent hover:text-accent/80 cursor-pointer text-xs font-medium hover:underline" > - configure + set defaults </button> </div> <div className="flex flex-col gap-2"> diff --git a/src/browser/components/Settings/sections/RuntimesSection.tsx b/src/browser/components/Settings/sections/RuntimesSection.tsx index c464273c5d..3f240a0761 100644 --- a/src/browser/components/Settings/sections/RuntimesSection.tsx +++ b/src/browser/components/Settings/sections/RuntimesSection.tsx @@ -13,6 +13,7 @@ import { Switch } from "@/browser/components/ui/switch"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/browser/components/ui/tooltip"; import { useAPI } from "@/browser/contexts/API"; import { useProjectContext } from "@/browser/contexts/ProjectContext"; +import { useSettings } from "@/browser/contexts/SettingsContext"; import { useRuntimeEnablement } from "@/browser/hooks/useRuntimeEnablement"; import { RUNTIME_CHOICE_UI, type RuntimeUiSpec } from "@/browser/utils/runtimeUi"; import { cn } from "@/common/lib/utils"; @@ -66,10 +67,16 @@ export function RuntimesSection() { const { projects, refreshProjects } = useProjectContext(); const { enablement, setRuntimeEnabled, defaultRuntime, setDefaultRuntime } = useRuntimeEnablement(); + const { runtimesProjectPath, setRuntimesProjectPath } = useSettings(); const projectList = Array.from(projects.keys()); - const [selectedScope, setSelectedScope] = useState(ALL_SCOPE_VALUE); + // Consume one-shot project scope hint from "set defaults" button in creation controls. + const initialScope = + runtimesProjectPath && projects.has(runtimesProjectPath) + ? runtimesProjectPath + : ALL_SCOPE_VALUE; + const [selectedScope, setSelectedScope] = useState(initialScope); const [projectOverrideEnabled, setProjectOverrideEnabled] = useState(false); const [projectEnablement, setProjectEnablement] = useState<RuntimeEnablement>(enablement); const [projectDefaultRuntime, setProjectDefaultRuntime] = useState<RuntimeEnablementId | null>( @@ -82,6 +89,16 @@ export function RuntimesSection() { // Cache pending per-project overrides locally while config updates propagate. const overrideCacheRef = useRef<Map<string, RuntimeOverrideCacheEntry>>(new Map()); + // When re-opened with a new project hint (e.g., clicking "set defaults" again for + // a different project), sync the scope and clear the one-shot hint. + useEffect(() => { + if (!runtimesProjectPath) return; + if (projects.has(runtimesProjectPath)) { + setSelectedScope(runtimesProjectPath); + } + setRuntimesProjectPath(null); + }, [runtimesProjectPath, projects, setRuntimesProjectPath]); + const selectedProjectPath = selectedScope === ALL_SCOPE_VALUE ? null : selectedScope; const isProjectScope = Boolean(selectedProjectPath); const isProjectOverrideActive = isProjectScope && projectOverrideEnabled; @@ -550,6 +567,9 @@ export function RuntimesSection() { ) : null} </div> <div className="text-muted text-xs">{runtime.description}</div> + {runtime.options ? ( + <div className="text-muted/70 text-[11px]">Options: {runtime.options}</div> + ) : null} </div> </div> <div className="flex items-center gap-3"> diff --git a/src/browser/contexts/SettingsContext.tsx b/src/browser/contexts/SettingsContext.tsx index c00e5241b9..cced1112e8 100644 --- a/src/browser/contexts/SettingsContext.tsx +++ b/src/browser/contexts/SettingsContext.tsx @@ -13,6 +13,8 @@ import { useRouter } from "@/browser/contexts/RouterContext"; interface OpenSettingsOptions { /** When opening the Providers settings, expand the given provider. */ expandProvider?: string; + /** When opening the Runtimes settings, pre-select this project scope. */ + runtimesProjectPath?: string; } interface SettingsContextValue { @@ -28,6 +30,10 @@ interface SettingsContextValue { /** One-shot hint for ProvidersSection to expand a provider. */ providersExpandedProvider: string | null; setProvidersExpandedProvider: (provider: string | null) => void; + + /** One-shot hint for RuntimesSection to pre-select a project scope. */ + runtimesProjectPath: string | null; + setRuntimesProjectPath: (path: string | null) => void; } const SettingsContext = createContext<SettingsContextValue | null>(null); @@ -43,6 +49,7 @@ const DEFAULT_SECTION = "general"; export function SettingsProvider(props: { children: ReactNode }) { const router = useRouter(); const [providersExpandedProvider, setProvidersExpandedProvider] = useState<string | null>(null); + const [runtimesProjectPath, setRuntimesProjectPath] = useState<string | null>(null); const closeCallbacksRef = useRef(new Set<() => void>()); @@ -57,6 +64,11 @@ export function SettingsProvider(props: { children: ReactNode }) { } else { setProvidersExpandedProvider(null); } + if (nextSection === "runtimes") { + setRuntimesProjectPath(options?.runtimesProjectPath ?? null); + } else { + setRuntimesProjectPath(null); + } router.navigateToSettings(nextSection); }, [router] @@ -106,6 +118,8 @@ export function SettingsProvider(props: { children: ReactNode }) { registerOnClose, providersExpandedProvider, setProvidersExpandedProvider, + runtimesProjectPath, + setRuntimesProjectPath, }), [ isOpen, @@ -115,6 +129,7 @@ export function SettingsProvider(props: { children: ReactNode }) { setActiveSection, registerOnClose, providersExpandedProvider, + runtimesProjectPath, ] ); diff --git a/src/browser/utils/runtimeUi.ts b/src/browser/utils/runtimeUi.ts index a8ab1dd511..fa7d239e2b 100644 --- a/src/browser/utils/runtimeUi.ts +++ b/src/browser/utils/runtimeUi.ts @@ -17,6 +17,8 @@ export interface RuntimeIconProps { export interface RuntimeUiSpec { label: string; description: string; + /** What user-provided options this runtime requires at creation time. */ + options?: string; docsPath: string; Icon: ComponentType<RuntimeIconProps>; button: { @@ -86,6 +88,7 @@ export const RUNTIME_UI = { ssh: { label: "SSH", description: "Remote clone on SSH host", + options: "Host (user@host)", docsPath: "/runtime/ssh", Icon: SSHIcon, button: { @@ -109,6 +112,7 @@ export const RUNTIME_UI = { docker: { label: "Docker", description: "Isolated container per workspace", + options: "Image name (e.g. node:20)", docsPath: "/runtime/docker", Icon: DockerIcon, button: { @@ -132,6 +136,7 @@ export const RUNTIME_UI = { devcontainer: { label: "Dev container", description: "Uses project's devcontainer.json configuration", + options: "Config path (devcontainer.json)", docsPath: "/runtime/devcontainer", Icon: DevcontainerIcon, button: { @@ -158,6 +163,7 @@ const CODER_RUNTIME_UI: RuntimeUiSpec = { ...RUNTIME_UI.ssh, label: "Coder", description: "Coder workspace via the Coder CLI", + options: "Coder workspace template", docsPath: "/runtime/coder", Icon: CoderIcon, }; From dbb794d65d69a5030292b21ea7c83ab04df49c43 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala <ammar@ammar.io> Date: Mon, 23 Feb 2026 11:43:17 -0600 Subject: [PATCH 2/6] fix: improve workspace default runtime UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename 'configure' to 'set defaults' with accent color styling - Pass project scope when opening Settings → Runtimes (bug fix) - Style 'set defaults' button distinctly when runtime differs from default - Add configurable runtime option inputs (SSH host, Docker image, etc.) in Settings → Runtimes page for per-project scope - Add options field to RuntimeUiSpec for single-ownership RFC: rfc/260223_workspace-default-runtime.md --- docs/AGENTS.md | 1 + rfc/260223_workspace-default-runtime.md | 4 +- .../components/ChatInput/CreationControls.tsx | 20 +++-- .../Settings/sections/RuntimesSection.tsx | 81 +++++++++++++++++-- src/browser/utils/runtimeUi.ts | 49 ++++++++++- 5 files changed, 136 insertions(+), 19 deletions(-) diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 3ad1874664..c4f6c30dd0 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -24,6 +24,7 @@ description: Agent instructions for AI assistants working on the Mux codebase ## Documentation Rules - No free-floating Markdown. User docs live in `docs/` (read `docs/README.md`, add pages to `docs.json` navigation, use standard Markdown + mermaid). Developer notes belong inline as comments. + - Exception: the `rfc` folder contains human-written RFCs for implementation planning. - For planning artifacts, use the `propose_plan` tool or inline comments instead of ad-hoc docs. - Do not add new root-level docs without explicit request; during feature work rely on code + tests + inline comments. - External API docs already live inside `/tmp/ai-sdk-docs/**.mdx`; never browse `https://sdk.vercel.ai/docs/ai-sdk-core` directly. diff --git a/rfc/260223_workspace-default-runtime.md b/rfc/260223_workspace-default-runtime.md index 22869e61c5..499ab2133c 100644 --- a/rfc/260223_workspace-default-runtime.md +++ b/rfc/260223_workspace-default-runtime.md @@ -16,8 +16,10 @@ Two fixes are required: - Make the `configure` runtime more prominent, and call it `set defaults` instead. - Create distinct visual style when the current runtime options are not the default so the user - can more quickly see how they would persist their changes. + can more quickly see how they would persist their changes. Only the button itself should change, + and the re-style must not create a layout shift. - Include runtime options in the new runtime settings page to clarify how the defaults work there. + - These defaults should be configurable just as they are in the new workspace page. There's also a bug where clicking the configure button on a project page takes you to the settings page with a global scope instead of the project scope. We should fix this as well. diff --git a/src/browser/components/ChatInput/CreationControls.tsx b/src/browser/components/ChatInput/CreationControls.tsx index 6db298f927..23101093dd 100644 --- a/src/browser/components/ChatInput/CreationControls.tsx +++ b/src/browser/components/ChatInput/CreationControls.tsx @@ -32,6 +32,7 @@ import { Skeleton } from "../ui/skeleton"; import { DocsLink } from "../DocsLink"; import { RUNTIME_CHOICE_UI, + RUNTIME_OPTION_FIELDS, type RuntimeChoice, type RuntimeIconProps, } from "@/browser/utils/runtimeUi"; @@ -871,17 +872,20 @@ export function CreationControls(props: CreationControlsProps) { {/* Runtime type - button group */} <div className="flex flex-col gap-1.5" data-component="RuntimeTypeGroup"> - <div className="flex items-center gap-1"> + <div className="flex items-center gap-1.5"> <label className="text-muted-foreground text-xs font-medium">Workspace Type</label> - {/* Visual cue when the user has changed the runtime from the project default. */} - {runtimeChoice !== props.defaultRuntimeMode && ( - <span className="text-warning text-[10px] font-medium">(modified)</span> - )} <span className="text-muted-foreground text-xs">-</span> + {/* Distinct button style when runtime selection differs from the project default. + Uses consistent padding so the button dimensions never shift. */} <button type="button" onClick={() => settings.open("runtimes", { runtimesProjectPath: props.projectPath })} - className="text-accent hover:text-accent/80 cursor-pointer text-xs font-medium hover:underline" + className={cn( + "cursor-pointer rounded-sm px-1 text-xs font-medium transition-colors", + runtimeChoice !== props.defaultRuntimeMode + ? "bg-warning/15 text-warning hover:bg-warning/25" + : "text-muted-foreground underline decoration-dotted underline-offset-2 hover:text-foreground" + )} > set defaults </button> @@ -1004,7 +1008,7 @@ export function CreationControls(props: CreationControlsProps) { label="host" value={selectedRuntime.host} onChange={(value) => onSelectedRuntimeChange({ mode: "ssh", host: value })} - placeholder="user@host" + placeholder={RUNTIME_OPTION_FIELDS.ssh.placeholder} disabled={props.disabled} hasError={props.runtimeFieldError === "ssh"} /> @@ -1023,7 +1027,7 @@ export function CreationControls(props: CreationControlsProps) { shareCredentials: selectedRuntime.shareCredentials, }) } - placeholder="node:20" + placeholder={RUNTIME_OPTION_FIELDS.docker.placeholder} disabled={props.disabled} hasError={props.runtimeFieldError === "docker"} id="docker-image" diff --git a/src/browser/components/Settings/sections/RuntimesSection.tsx b/src/browser/components/Settings/sections/RuntimesSection.tsx index 3f240a0761..55f4da4418 100644 --- a/src/browser/components/Settings/sections/RuntimesSection.tsx +++ b/src/browser/components/Settings/sections/RuntimesSection.tsx @@ -14,9 +14,15 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/browser/components/ui import { useAPI } from "@/browser/contexts/API"; import { useProjectContext } from "@/browser/contexts/ProjectContext"; import { useSettings } from "@/browser/contexts/SettingsContext"; +import { usePersistedState } from "@/browser/hooks/usePersistedState"; import { useRuntimeEnablement } from "@/browser/hooks/useRuntimeEnablement"; -import { RUNTIME_CHOICE_UI, type RuntimeUiSpec } from "@/browser/utils/runtimeUi"; +import { + RUNTIME_CHOICE_UI, + getRuntimeOptionField, + type RuntimeUiSpec, +} from "@/browser/utils/runtimeUi"; import { cn } from "@/common/lib/utils"; +import { getLastRuntimeConfigKey } from "@/common/constants/storage"; import type { CoderInfo } from "@/common/orpc/schemas/coder"; import { normalizeRuntimeEnablement, RUNTIME_MODE } from "@/common/types/runtime"; import type { @@ -91,11 +97,12 @@ export function RuntimesSection() { // When re-opened with a new project hint (e.g., clicking "set defaults" again for // a different project), sync the scope and clear the one-shot hint. + // Only clear the hint once the project is actually found in the project list; + // projects load asynchronously, so we must keep the hint alive until then. useEffect(() => { if (!runtimesProjectPath) return; - if (projects.has(runtimesProjectPath)) { - setSelectedScope(runtimesProjectPath); - } + if (!projects.has(runtimesProjectPath)) return; + setSelectedScope(runtimesProjectPath); setRuntimesProjectPath(null); }, [runtimesProjectPath, projects, setRuntimesProjectPath]); @@ -103,6 +110,33 @@ export function RuntimesSection() { const isProjectScope = Boolean(selectedProjectPath); const isProjectOverrideActive = isProjectScope && projectOverrideEnabled; + // Per-project runtime option defaults (SSH host, Docker image, etc.). + // Same localStorage keys the creation flow reads, so edits here are reflected immediately. + const runtimeConfigKey = selectedProjectPath + ? getLastRuntimeConfigKey(selectedProjectPath) + : "__no_project_defaults__"; + type RuntimeOptionConfigs = Partial<Record<string, Record<string, unknown>>>; + const [runtimeOptionConfigs, setRuntimeOptionConfigs] = usePersistedState<RuntimeOptionConfigs>( + runtimeConfigKey, + {}, + { listener: true } + ); + + const readOptionField = (runtimeMode: string, field: string): string => { + const modeConfig = runtimeOptionConfigs[runtimeMode]; + if (!modeConfig || typeof modeConfig !== "object") return ""; + const val = modeConfig[field]; + return typeof val === "string" ? val : ""; + }; + + const setOptionField = (runtimeMode: string, field: string, value: string) => { + setRuntimeOptionConfigs((prev) => { + const existing = prev[runtimeMode]; + const existingObj = existing && typeof existing === "object" ? existing : {}; + return { ...prev, [runtimeMode]: { ...existingObj, [field]: value } }; + }); + }; + const syncProjects = () => refreshProjects().catch(() => { // Best-effort only. @@ -567,9 +601,42 @@ export function RuntimesSection() { ) : null} </div> <div className="text-muted text-xs">{runtime.description}</div> - {runtime.options ? ( - <div className="text-muted/70 text-[11px]">Options: {runtime.options}</div> - ) : null} + {/* Configurable option inputs — only for project scope, matching the + same localStorage keys the creation flow reads as initial defaults. */} + {(() => { + const optionSpec = getRuntimeOptionField(runtime.id); + if (!optionSpec || !selectedProjectPath) { + // Global scope: show option description if available + if (runtime.options && !selectedProjectPath) { + return ( + <div className="text-muted/70 text-[11px]"> + Options: {runtime.options} + </div> + ); + } + return null; + } + return ( + <input + type="text" + value={readOptionField( + runtime.id === "coder" ? "ssh" : runtime.id, + optionSpec.field + )} + onChange={(e) => + setOptionField( + runtime.id === "coder" ? "ssh" : runtime.id, + optionSpec.field, + e.target.value + ) + } + placeholder={optionSpec.placeholder} + disabled={rowDisabled} + className="border-border-medium bg-background-secondary text-foreground placeholder:text-muted focus:border-accent mt-1.5 h-7 w-full max-w-[260px] rounded border px-2 text-xs focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" + aria-label={`${optionSpec.label} for ${runtime.label}`} + /> + ); + })()} </div> </div> <div className="flex items-center gap-3"> diff --git a/src/browser/utils/runtimeUi.ts b/src/browser/utils/runtimeUi.ts index fa7d239e2b..4a8ffe616f 100644 --- a/src/browser/utils/runtimeUi.ts +++ b/src/browser/utils/runtimeUi.ts @@ -1,5 +1,5 @@ import type { ComponentType } from "react"; -import type { RuntimeMode } from "@/common/types/runtime"; +import type { RuntimeEnablementId, RuntimeMode } from "@/common/types/runtime"; import { SSHIcon, WorktreeIcon, @@ -14,6 +14,49 @@ export interface RuntimeIconProps { className?: string; } +export interface RuntimeOptionFieldSpec { + readonly field: string; + readonly label: string; + readonly placeholder: string; + readonly summary: string; +} + +export const RUNTIME_OPTION_FIELDS = { + ssh: { + field: "host", + label: "Default host", + placeholder: "user@host", + summary: "Host (user@host)", + }, + docker: { + field: "image", + label: "Default image", + placeholder: "node:20", + summary: "Image name (e.g. node:20)", + }, + devcontainer: { + field: "configPath", + label: "Default config", + placeholder: ".devcontainer/devcontainer.json", + summary: "Config path (devcontainer.json)", + }, +} as const satisfies Partial<Record<RuntimeEnablementId, RuntimeOptionFieldSpec>>; + +export function getRuntimeOptionField( + runtimeId: RuntimeEnablementId +): RuntimeOptionFieldSpec | null { + switch (runtimeId) { + case "ssh": + return RUNTIME_OPTION_FIELDS.ssh; + case "docker": + return RUNTIME_OPTION_FIELDS.docker; + case "devcontainer": + return RUNTIME_OPTION_FIELDS.devcontainer; + default: + return null; + } +} + export interface RuntimeUiSpec { label: string; description: string; @@ -112,7 +155,7 @@ export const RUNTIME_UI = { docker: { label: "Docker", description: "Isolated container per workspace", - options: "Image name (e.g. node:20)", + options: RUNTIME_OPTION_FIELDS.docker.summary, docsPath: "/runtime/docker", Icon: DockerIcon, button: { @@ -136,7 +179,7 @@ export const RUNTIME_UI = { devcontainer: { label: "Dev container", description: "Uses project's devcontainer.json configuration", - options: "Config path (devcontainer.json)", + options: RUNTIME_OPTION_FIELDS.devcontainer.summary, docsPath: "/runtime/devcontainer", Icon: DevcontainerIcon, button: { From 18023ff01d87ff0401710135ea8f7ea8b1fb4bad Mon Sep 17 00:00:00 2001 From: Ammar <ammar+ai@ammar.io> Date: Mon, 23 Feb 2026 13:12:31 -0600 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=A4=96=20fix:=20unify=20runtime=20opt?= =?UTF-8?q?ion=20inputs=20and=20defaults=20button=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reuse a shared RuntimeConfigInput in both creation controls and Settings → Runtimes - add visible runtime option labels in project-scope defaults for parity with new workspace UI - remove the hyphen delimiter and restyle "set defaults" as a subtle secondary button --- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$13.73`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=13.73 --> --- .../components/ChatInput/CreationControls.tsx | 56 +++++------------- src/browser/components/RuntimeConfigInput.tsx | 49 ++++++++++++++++ .../Settings/sections/RuntimesSection.tsx | 57 +++++++------------ src/browser/utils/runtimeUi.ts | 8 +-- 4 files changed, 90 insertions(+), 80 deletions(-) create mode 100644 src/browser/components/RuntimeConfigInput.tsx diff --git a/src/browser/components/ChatInput/CreationControls.tsx b/src/browser/components/ChatInput/CreationControls.tsx index 23101093dd..e0f3d84918 100644 --- a/src/browser/components/ChatInput/CreationControls.tsx +++ b/src/browser/components/ChatInput/CreationControls.tsx @@ -25,6 +25,7 @@ import { PlatformPaths } from "@/common/utils/paths"; import { useProjectContext } from "@/browser/contexts/ProjectContext"; import { useSettings } from "@/browser/contexts/SettingsContext"; import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext"; +import { RuntimeConfigInput } from "@/browser/components/RuntimeConfigInput"; import { cn } from "@/common/lib/utils"; import { formatNameGenerationError } from "@/common/utils/errors/formatNameGenerationError"; import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip"; @@ -56,36 +57,6 @@ import { const INLINE_CONTROL_CLASSES = "h-7 w-[140px] rounded border border-border-medium bg-separator px-2 text-xs text-foreground focus:border-accent focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"; -/** Shared runtime config text input - used for SSH host, Docker image, etc. */ -function RuntimeConfigInput(props: { - label: string; - value: string; - onChange: (value: string) => void; - placeholder: string; - disabled?: boolean; - hasError?: boolean; - id?: string; - ariaLabel?: string; -}) { - return ( - <div className="flex items-center gap-2"> - <label htmlFor={props.id} className="text-muted-foreground text-xs"> - {props.label} - </label> - <input - id={props.id} - aria-label={props.ariaLabel} - type="text" - value={props.value} - onChange={(e) => props.onChange(e.target.value)} - placeholder={props.placeholder} - disabled={props.disabled} - className={cn(INLINE_CONTROL_CLASSES, props.hasError && "border-red-500")} - /> - </div> - ); -} - /** Credential sharing checkbox - used by Docker and Devcontainer runtimes */ function CredentialSharingCheckbox(props: { checked: boolean; @@ -872,19 +843,17 @@ export function CreationControls(props: CreationControlsProps) { {/* Runtime type - button group */} <div className="flex flex-col gap-1.5" data-component="RuntimeTypeGroup"> - <div className="flex items-center gap-1.5"> + <div className="flex items-center gap-2"> <label className="text-muted-foreground text-xs font-medium">Workspace Type</label> - <span className="text-muted-foreground text-xs">-</span> - {/* Distinct button style when runtime selection differs from the project default. - Uses consistent padding so the button dimensions never shift. */} + {/* Keep this subtle so it reads like a secondary action, while still signaling + unsaved differences when the current runtime differs from the project default. */} <button type="button" onClick={() => settings.open("runtimes", { runtimesProjectPath: props.projectPath })} className={cn( - "cursor-pointer rounded-sm px-1 text-xs font-medium transition-colors", - runtimeChoice !== props.defaultRuntimeMode - ? "bg-warning/15 text-warning hover:bg-warning/25" - : "text-muted-foreground underline decoration-dotted underline-offset-2 hover:text-foreground" + "border-border-medium bg-background-secondary text-muted-foreground hover:bg-hover hover:text-foreground inline-flex h-6 cursor-pointer items-center rounded border px-2 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent", + runtimeChoice !== props.defaultRuntimeMode && + "border-warning/40 bg-warning/10 text-warning hover:bg-warning/20" )} > set defaults @@ -1005,12 +974,14 @@ export function CreationControls(props: CreationControlsProps) { // Also hide when Coder is still checking but has saved config (will enable after check) !(props.coderProps?.coderInfo === null && props.coderProps?.coderConfig) && ( <RuntimeConfigInput - label="host" + id="ssh-host" + label={RUNTIME_OPTION_FIELDS.ssh.label} value={selectedRuntime.host} onChange={(value) => onSelectedRuntimeChange({ mode: "ssh", host: value })} placeholder={RUNTIME_OPTION_FIELDS.ssh.placeholder} disabled={props.disabled} hasError={props.runtimeFieldError === "ssh"} + inputClassName={INLINE_CONTROL_CLASSES} /> )} @@ -1018,7 +989,7 @@ export function CreationControls(props: CreationControlsProps) { {selectedRuntime.mode === "docker" && ( <RuntimeConfigInput - label="image" + label={RUNTIME_OPTION_FIELDS.docker.label} value={selectedRuntime.image} onChange={(value) => onSelectedRuntimeChange({ @@ -1032,6 +1003,7 @@ export function CreationControls(props: CreationControlsProps) { hasError={props.runtimeFieldError === "docker"} id="docker-image" ariaLabel="Docker image" + inputClassName={INLINE_CONTROL_CLASSES} /> )} </div> @@ -1045,7 +1017,9 @@ export function CreationControls(props: CreationControlsProps) { {selectedRuntime.mode === "devcontainer" && devcontainerSelection.uiMode !== "hidden" && ( <div className="border-border-medium flex w-fit flex-col gap-1.5 rounded-md border p-2"> <div className="flex flex-col gap-1"> - <label className="text-muted-foreground text-xs">Config</label> + <label className="text-muted-foreground text-xs"> + {RUNTIME_OPTION_FIELDS.devcontainer.label} + </label> {devcontainerSelection.uiMode === "loading" ? ( // Skeleton placeholder while loading - matches dropdown dimensions <Skeleton className="h-6 w-[280px] rounded-md" /> diff --git a/src/browser/components/RuntimeConfigInput.tsx b/src/browser/components/RuntimeConfigInput.tsx new file mode 100644 index 0000000000..101c30eb31 --- /dev/null +++ b/src/browser/components/RuntimeConfigInput.tsx @@ -0,0 +1,49 @@ +import { useId } from "react"; + +import { cn } from "@/common/lib/utils"; + +/** + * Shared runtime option input used by creation and settings screens. + * Keeps labels/inputs visually and behaviorally aligned across both flows. + */ +export function RuntimeConfigInput(props: { + label: string; + value: string; + onChange: (value: string) => void; + placeholder: string; + disabled?: boolean; + hasError?: boolean; + id?: string; + ariaLabel?: string; + className?: string; + labelClassName?: string; + inputClassName?: string; +}) { + const autoId = useId(); + const inputId = props.id ?? autoId; + + return ( + <div className={cn("flex items-center gap-2", props.className)}> + <label + htmlFor={inputId} + className={cn("text-muted-foreground text-xs", props.labelClassName)} + > + {props.label} + </label> + <input + id={inputId} + aria-label={props.ariaLabel ?? props.label} + type="text" + value={props.value} + onChange={(e) => props.onChange(e.target.value)} + placeholder={props.placeholder} + disabled={props.disabled} + className={cn( + "border-border-medium bg-background-secondary text-foreground placeholder:text-muted focus:border-accent h-7 rounded border px-2 text-xs focus:outline-none disabled:cursor-not-allowed disabled:opacity-50", + props.inputClassName, + props.hasError && "border-red-500" + )} + /> + </div> + ); +} diff --git a/src/browser/components/Settings/sections/RuntimesSection.tsx b/src/browser/components/Settings/sections/RuntimesSection.tsx index 55f4da4418..a43fd048e4 100644 --- a/src/browser/components/Settings/sections/RuntimesSection.tsx +++ b/src/browser/components/Settings/sections/RuntimesSection.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { AlertTriangle, Loader2 } from "lucide-react"; import { resolveCoderAvailability } from "@/browser/components/ChatInput/CoderControls"; +import { RuntimeConfigInput } from "@/browser/components/RuntimeConfigInput"; import { Select, SelectContent, @@ -552,6 +553,8 @@ export function RuntimesSection() { const rowDisabled = isProjectScope && !projectOverrideEnabled; const isLastEnabled = effectiveEnablement[runtime.id] && enabledRuntimeCount <= 1; const switchDisabled = rowDisabled || isLastEnabled; + const optionSpec = getRuntimeOptionField(runtime.id); + const optionRuntimeMode = runtime.id === "coder" ? "ssh" : runtime.id; const switchControl = ( <Switch checked={effectiveEnablement[runtime.id]} @@ -601,42 +604,26 @@ export function RuntimesSection() { ) : null} </div> <div className="text-muted text-xs">{runtime.description}</div> - {/* Configurable option inputs — only for project scope, matching the - same localStorage keys the creation flow reads as initial defaults. */} - {(() => { - const optionSpec = getRuntimeOptionField(runtime.id); - if (!optionSpec || !selectedProjectPath) { - // Global scope: show option description if available - if (runtime.options && !selectedProjectPath) { - return ( - <div className="text-muted/70 text-[11px]"> - Options: {runtime.options} - </div> - ); + {/* Configurable option inputs — project scope uses the same labeled + input component and localStorage defaults as the creation flow. */} + {!optionSpec || !selectedProjectPath ? ( + runtime.options && !selectedProjectPath ? ( + <div className="text-muted/70 text-[11px]">Options: {runtime.options}</div> + ) : null + ) : ( + <RuntimeConfigInput + label={optionSpec.label} + value={readOptionField(optionRuntimeMode, optionSpec.field)} + onChange={(value) => + setOptionField(optionRuntimeMode, optionSpec.field, value) } - return null; - } - return ( - <input - type="text" - value={readOptionField( - runtime.id === "coder" ? "ssh" : runtime.id, - optionSpec.field - )} - onChange={(e) => - setOptionField( - runtime.id === "coder" ? "ssh" : runtime.id, - optionSpec.field, - e.target.value - ) - } - placeholder={optionSpec.placeholder} - disabled={rowDisabled} - className="border-border-medium bg-background-secondary text-foreground placeholder:text-muted focus:border-accent mt-1.5 h-7 w-full max-w-[260px] rounded border px-2 text-xs focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" - aria-label={`${optionSpec.label} for ${runtime.label}`} - /> - ); - })()} + placeholder={optionSpec.placeholder} + disabled={rowDisabled} + className="mt-1.5" + inputClassName="w-full max-w-[260px]" + ariaLabel={`${optionSpec.label} for ${runtime.label}`} + /> + )} </div> </div> <div className="flex items-center gap-3"> diff --git a/src/browser/utils/runtimeUi.ts b/src/browser/utils/runtimeUi.ts index 4a8ffe616f..0192b343cb 100644 --- a/src/browser/utils/runtimeUi.ts +++ b/src/browser/utils/runtimeUi.ts @@ -24,19 +24,19 @@ export interface RuntimeOptionFieldSpec { export const RUNTIME_OPTION_FIELDS = { ssh: { field: "host", - label: "Default host", + label: "Host", placeholder: "user@host", summary: "Host (user@host)", }, docker: { field: "image", - label: "Default image", + label: "Image", placeholder: "node:20", summary: "Image name (e.g. node:20)", }, devcontainer: { field: "configPath", - label: "Default config", + label: "Config", placeholder: ".devcontainer/devcontainer.json", summary: "Config path (devcontainer.json)", }, @@ -131,7 +131,7 @@ export const RUNTIME_UI = { ssh: { label: "SSH", description: "Remote clone on SSH host", - options: "Host (user@host)", + options: RUNTIME_OPTION_FIELDS.ssh.summary, docsPath: "/runtime/ssh", Icon: SSHIcon, button: { From 95b2bd476bde7180bd9e9ef4e21a3ad7e705a708 Mon Sep 17 00:00:00 2001 From: Ammar <ammar+ai@ammar.io> Date: Mon, 23 Feb 2026 13:16:28 -0600 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=A4=96=20fix:=20clear=20runtimes=20sc?= =?UTF-8?q?ope=20hint=20on=20settings=20navigation/close?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - clear one-shot runtimesProjectPath when switching away from the runtimes section - clear runtimes scope hint on close transitions so it cannot leak across settings sessions - keep providers/runtimes hint state explicitly one-shot and section-scoped --- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$13.73`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=13.73 --> --- src/browser/contexts/SettingsContext.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/browser/contexts/SettingsContext.tsx b/src/browser/contexts/SettingsContext.tsx index cced1112e8..e74b4adb32 100644 --- a/src/browser/contexts/SettingsContext.tsx +++ b/src/browser/contexts/SettingsContext.tsx @@ -86,6 +86,8 @@ export function SettingsProvider(props: { children: ReactNode }) { const wasOpenRef = useRef(isOpen); useEffect(() => { if (wasOpenRef.current && !isOpen) { + setProvidersExpandedProvider(null); + setRuntimesProjectPath(null); for (const callback of closeCallbacksRef.current) { callback(); } @@ -95,6 +97,7 @@ export function SettingsProvider(props: { children: ReactNode }) { const close = useCallback(() => { setProvidersExpandedProvider(null); + setRuntimesProjectPath(null); router.navigateFromSettings(); }, [router]); @@ -103,6 +106,10 @@ export function SettingsProvider(props: { children: ReactNode }) { if (section !== "providers") { setProvidersExpandedProvider(null); } + if (section !== "runtimes") { + // Runtime scope hints are one-shot and should not persist across section changes. + setRuntimesProjectPath(null); + } router.navigateToSettings(section); }, [router] From 92e1461bfbaca1f4a9e9c2ce93b72c4cee99b67e Mon Sep 17 00:00:00 2001 From: Ammar <ammar+ai@ammar.io> Date: Mon, 23 Feb 2026 13:55:30 -0600 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=A4=96=20fix:=20update=20workspace=20?= =?UTF-8?q?default=20runtime=20RFC=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - update RFC 260223 notes for the latest runtime defaults UX implementation details - keep implementation/documentation context aligned in the active PR branch --- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$13.73`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=13.73 --> --- rfc/260223_workspace-default-runtime.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rfc/260223_workspace-default-runtime.md b/rfc/260223_workspace-default-runtime.md index 499ab2133c..5e75341ce5 100644 --- a/rfc/260223_workspace-default-runtime.md +++ b/rfc/260223_workspace-default-runtime.md @@ -20,6 +20,7 @@ Two fixes are required: and the re-style must not create a layout shift. - Include runtime options in the new runtime settings page to clarify how the defaults work there. - These defaults should be configurable just as they are in the new workspace page. + - The options should have labels to match parity with the new workspace page. There's also a bug where clicking the configure button on a project page takes you to the settings page with a global scope instead of the project scope. We should fix this as well. @@ -31,4 +32,5 @@ During this change, it is imperative that we have single-ownership of: - What options are available per runtime - The setting / getting of defaults -- The list of runtime types \ No newline at end of file +- The list of runtime types +- Display code for the runtime options \ No newline at end of file From 6d2ad9d0bdf6d1591de5723ff39505a49376470e Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala <ammar@ammar.io> Date: Mon, 23 Feb 2026 17:47:00 -0600 Subject: [PATCH 6/6] RFC fixups --- rfc/260223_workspace-default-runtime.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfc/260223_workspace-default-runtime.md b/rfc/260223_workspace-default-runtime.md index 5e75341ce5..89a0b288f8 100644 --- a/rfc/260223_workspace-default-runtime.md +++ b/rfc/260223_workspace-default-runtime.md @@ -1,5 +1,5 @@ --- -author: @ammario +author: "@ammario" date: 2026-02-23 ---