diff --git a/frontend/src/features/first-run/FirstRunWizard.test.tsx b/frontend/src/features/first-run/FirstRunWizard.test.tsx
index 7011ff5..4711f07 100644
--- a/frontend/src/features/first-run/FirstRunWizard.test.tsx
+++ b/frontend/src/features/first-run/FirstRunWizard.test.tsx
@@ -83,6 +83,30 @@ describe("FirstRunWizard (with MockProfileGateway)", () => {
expect(cmd.value).toBe("codex-2");
});
+ it("keeps args spaces while typing and parses them on blur", async () => {
+ const { profile } = renderWizard();
+ await waitForLoaded();
+ await waitFor(() =>
+ expect(
+ (screen.getByLabelText("use Claude Code") as HTMLInputElement).checked,
+ ).toBe(true),
+ );
+
+ const args = screen.getByLabelText("Claude Code args") as HTMLInputElement;
+ fireEvent.change(args, {
+ target: { value: "--model claude-sonnet-4-5 --verbose " },
+ });
+ expect(args.value).toBe("--model claude-sonnet-4-5 --verbose ");
+
+ fireEvent.blur(args);
+ fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));
+
+ await waitFor(async () => {
+ const [saved] = await profile.listProfiles();
+ expect(saved.args).toEqual(["--model", "claude-sonnet-4-5", "--verbose"]);
+ });
+ });
+
it("detection shows ✓ for claude and ✗ for the rest", async () => {
renderWizard();
await waitForLoaded();
diff --git a/frontend/src/features/first-run/FirstRunWizard.tsx b/frontend/src/features/first-run/FirstRunWizard.tsx
index edfcb7b..14bfd0e 100644
--- a/frontend/src/features/first-run/FirstRunWizard.tsx
+++ b/frontend/src/features/first-run/FirstRunWizard.tsx
@@ -20,6 +20,7 @@ import type {
HttpChatConfig,
LocalModelServerConfig,
} from "@/domain";
+import { useEffect, useState } from "react";
import { Button, IconButton, Input, Panel, Toolbar, cn } from "@/shared";
import {
ModelServersPanel,
@@ -183,6 +184,16 @@ function ProfileRow({
const { profile, selected, available } = entry;
const errors = validateProfile(profile);
const isOpenCode = profile.structuredAdapter === "openCode";
+ const canonicalArgsText = profile.args.join(" ");
+ const [argsText, setArgsText] = useState(canonicalArgsText);
+
+ useEffect(() => {
+ setArgsText(canonicalArgsText);
+ }, [canonicalArgsText, profile.id]);
+
+ function commitArgs() {
+ onChange({ ...profile, args: parseArgs(argsText) });
+ }
return (
@@ -260,8 +271,9 @@ function ProfileRow({
Arguments
onChange({ ...profile, args: parseArgs(e.target.value) })}
+ value={argsText}
+ onChange={(e) => setArgsText(e.target.value)}
+ onBlur={commitArgs}
/>
diff --git a/frontend/src/features/model-servers/ModelServersPanel.tsx b/frontend/src/features/model-servers/ModelServersPanel.tsx
index ce7eb6d..e5236c1 100644
--- a/frontend/src/features/model-servers/ModelServersPanel.tsx
+++ b/frontend/src/features/model-servers/ModelServersPanel.tsx
@@ -97,9 +97,10 @@ export function ModelServersPanel({ vm }: ModelServersPanelProps) {
setDraft({ ...server });
}
- async function submit() {
- if (!draft) return;
- const saved = await vm.save(draft);
+ async function submit(nextDraft?: LocalModelServerConfig) {
+ const submitted = nextDraft ?? draft;
+ if (!submitted) return;
+ const saved = await vm.save(submitted);
if (saved) setDraft(null);
}
@@ -225,7 +226,7 @@ export function ModelServersPanel({ vm }: ModelServersPanelProps) {
vm.clearError();
setDraft(null);
}}
- onSubmit={() => void submit()}
+ onSubmit={(nextDraft) => void submit(nextDraft)}
/>
)}
@@ -343,12 +344,27 @@ function ServerEditor({
) => Promise;
onChange: (next: LocalModelServerConfig) => void;
onCancel: () => void;
- onSubmit: () => void;
+ onSubmit: (nextDraft?: LocalModelServerConfig) => void;
}) {
const errors: ModelServerErrors = validateModelServer(draft);
const valid = Object.keys(errors).length === 0;
const patch = (next: Partial) =>
onChange({ ...draft, ...next });
+ const canonicalArgsText = draft.args.join(" ");
+ const [argsText, setArgsText] = useState(canonicalArgsText);
+
+ useEffect(() => {
+ setArgsText(canonicalArgsText);
+ }, [canonicalArgsText, draft.id]);
+
+ function draftWithCommittedArgs(): LocalModelServerConfig {
+ return { ...draft, args: parseArgs(argsText) };
+ }
+
+ function commitArgs() {
+ const next = draftWithCommittedArgs();
+ onChange(next);
+ }
// Sticky source tab: keep the chosen kind even when the field is momentarily
// blank (an empty field clears `modelSource`, which must not snap the tab).
@@ -411,7 +427,7 @@ function ServerEditor({
// `draftKey` captures the argv-affecting fields; `preview` is stable.
}, [draftKey, preview]); // eslint-disable-line react-hooks/exhaustive-deps
- const reservedHits = reservedFlagsIn(draft.args);
+ const reservedHits = reservedFlagsIn(parseArgs(argsText));
return (