fix(app): make Add Server form fields editable and persist credentials
Some checks are pending
deploy / deploy (push) Waiting to run
generate / generate (push) Waiting to run
nix-eval / nix-eval (push) Waiting to run
publish / version (push) Waiting to run
publish / build-cli (push) Blocked by required conditions
publish / sign-cli-windows (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=arm64 host:macos-26 platform_flag:--mac --arm64 target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=x64 host:macos-26-intel platform_flag:--mac --x64 target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404-arm platform_flag:--linux --arm64 target:aarch64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-windows-2025 platform_flag:--win target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / build-electron (map[host:windows-2025 platform_flag:--win --arm64 target:aarch64-pc-windows-msvc]) (push) Blocked by required conditions
publish / publish (push) Blocked by required conditions
test / unit (linux) (push) Waiting to run
test / unit (windows) (push) Waiting to run
test / e2e (linux) (push) Waiting to run
test / e2e (windows) (push) Waiting to run
typecheck / typecheck (push) Waiting to run

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
axcho 2026-08-25 14:27:14 -04:00
parent 74372f6840
commit c7e53a1958
3 changed files with 42 additions and 34 deletions

View file

@ -0,0 +1,24 @@
# 2026-08-25 — Fix Add Server form field entry and save
## Summary
Fixed the Settings Add/Edit Server form so name, username, and password can be typed and persisted on save.
## Changes
### `packages/app/src/components/settings-v2/dialog-server-v2.tsx`
- Initialize add/edit form state during setup (before first paint) so uncontrolled defaults apply correctly.
- Keep URL controlled for validation/preview; make name, username, and password write-once initial values (plain strings, not reactive) so typed characters are not rolled back on Windows/Electron.
- Stop disabling text fields while the health check runs; only the submit button stays disabled.
- Allow Cancel during a pending check.
### `packages/app/src/components/dialog-select-server.tsx`
- Persist username independently of password on add (aligned with edit), so credentials are not dropped when only one is set.
- Remove `isPending` early-returns from field change handlers so input continues updating the store during health checks.
- Keep optional legacy `ServerForm` fields enabled while checking; only the submit button is disabled.
## Verification
- `bun typecheck` in `packages/app`

View file

@ -29,7 +29,6 @@ interface ServerFormProps {
username: string
password: string
placeholder: string
busy: boolean
error: string
status: boolean | undefined
onChange: (value: string) => void
@ -136,7 +135,6 @@ function ServerForm(props: ServerFormProps) {
autofocus
validationState={props.error ? "invalid" : "valid"}
error={props.error}
disabled={props.busy}
onChange={props.onChange}
onKeyDown={keyDown}
/>
@ -146,7 +144,6 @@ function ServerForm(props: ServerFormProps) {
label={language.t("dialog.server.add.name")}
placeholder={language.t("dialog.server.add.namePlaceholder")}
defaultValue={props.name}
disabled={props.busy}
onChange={props.onNameChange}
onKeyDown={keyDown}
/>
@ -156,7 +153,6 @@ function ServerForm(props: ServerFormProps) {
label={language.t("dialog.server.add.username")}
placeholder={language.t("dialog.server.add.usernamePlaceholder")}
defaultValue={props.username}
disabled={props.busy}
onChange={props.onUsernameChange}
onKeyDown={keyDown}
/>
@ -165,7 +161,6 @@ function ServerForm(props: ServerFormProps) {
label={language.t("dialog.server.add.password")}
placeholder={language.t("dialog.server.add.passwordPlaceholder")}
defaultValue={props.password}
disabled={props.busy}
onChange={props.onPasswordChange}
onKeyDown={keyDown}
/>
@ -252,13 +247,14 @@ export function useServerManagementController(options: { onSelect?: () => void;
return
}
const name = store.addServer.name.trim() || undefined
const username = store.addServer.username || undefined
const password = store.addServer.password || undefined
const conn: ServerConnection.Http = {
type: "http",
http: { url: normalized },
displayName: name,
http: { url: normalized, username, password },
}
if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim()
if (store.addServer.password) conn.http.password = store.addServer.password
if (store.addServer.password && store.addServer.username) conn.http.username = store.addServer.username
const result = await checkServerHealth(conn.http)
if (!result.healthy) {
setStore("addServer", { error: language.t("dialog.server.add.error") })
@ -394,7 +390,6 @@ export function useServerManagementController(options: { onSelect?: () => void;
}
const handleAddChange = (value: string) => {
if (addMutation.isPending) return
setStore("addServer", { url: value, error: "" })
void previewStatus(value, store.addServer.username, store.addServer.password, (next) =>
setStore("addServer", { status: next }),
@ -402,12 +397,10 @@ export function useServerManagementController(options: { onSelect?: () => void;
}
const handleAddNameChange = (value: string) => {
if (addMutation.isPending) return
setStore("addServer", { name: value, error: "" })
}
const handleAddUsernameChange = (value: string) => {
if (addMutation.isPending) return
setStore("addServer", { username: value, error: "" })
void previewStatus(store.addServer.url, value, store.addServer.password, (next) =>
setStore("addServer", { status: next }),
@ -415,7 +408,6 @@ export function useServerManagementController(options: { onSelect?: () => void;
}
const handleAddPasswordChange = (value: string) => {
if (addMutation.isPending) return
setStore("addServer", { password: value, error: "" })
void previewStatus(store.addServer.url, store.addServer.username, value, (next) =>
setStore("addServer", { status: next }),
@ -423,7 +415,6 @@ export function useServerManagementController(options: { onSelect?: () => void;
}
const handleEditChange = (value: string) => {
if (editMutation.isPending) return
setStore("editServer", { value, error: "" })
void previewStatus(value, store.editServer.username, store.editServer.password, (next) =>
setStore("editServer", { status: next }),
@ -431,12 +422,10 @@ export function useServerManagementController(options: { onSelect?: () => void;
}
const handleEditNameChange = (value: string) => {
if (editMutation.isPending) return
setStore("editServer", { name: value, error: "" })
}
const handleEditUsernameChange = (value: string) => {
if (editMutation.isPending) return
setStore("editServer", { username: value, error: "" })
void previewStatus(store.editServer.value, value, store.editServer.password, (next) =>
setStore("editServer", { status: next }),
@ -444,7 +433,6 @@ export function useServerManagementController(options: { onSelect?: () => void;
}
const handleEditPasswordChange = (value: string) => {
if (editMutation.isPending) return
setStore("editServer", { password: value, error: "" })
void previewStatus(store.editServer.value, store.editServer.username, value, (next) =>
setStore("editServer", { status: next }),
@ -692,7 +680,6 @@ export function ServerConnectionForm(props: { controller: ReturnType<typeof useS
username={props.controller.formUsername()}
password={props.controller.formPassword()}
placeholder={language.t("dialog.server.add.placeholder")}
busy={props.controller.formBusy()}
error={props.controller.formError()}
status={props.controller.formStatus()}
onChange={props.controller.handleFormChange()}

View file

@ -3,7 +3,7 @@ import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@op
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { type Component, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js"
import { type Component, Show, createEffect, onCleanup } from "solid-js"
import { useLanguage } from "@/context/language"
import { type ServerConnection } from "@/context/server"
import { useServerManagementController } from "../dialog-select-server"
@ -19,20 +19,21 @@ export const DialogServerV2: Component<{
onSelect: () => dialog.close(),
navigateOnAdd: false,
})
const [opened, setOpened] = createSignal(false)
onMount(() => {
if (props.mode === "add") controller.startAdd()
if (props.mode === "edit" && props.server) controller.startEdit(props.server)
setOpened(true)
})
// Initialize before first paint so optional-field initial values stick.
if (props.mode === "add") controller.startAdd()
if (props.mode === "edit" && props.server) controller.startEdit(props.server)
// Plain strings (not accessors) so Solid treats these as initial values, not controlled.
const initialName = controller.formName()
const initialUsername = controller.formUsername()
const initialPassword = controller.formPassword()
onCleanup(() => {
controller.resetForm()
})
createEffect(() => {
if (!opened()) return
if (controller.isFormMode()) return
dialog.close()
})
@ -69,7 +70,6 @@ export const DialogServerV2: Component<{
value={controller.formValue()}
placeholder={language.t("dialog.server.add.placeholder")}
invalid={!!controller.formError()}
disabled={controller.formBusy()}
autofocus
onInput={(event) => controller.handleFormChange()(event.currentTarget.value)}
onKeyDown={keyDown}
@ -84,9 +84,8 @@ export const DialogServerV2: Component<{
type="text"
appearance="large"
class="!w-full self-stretch"
value={controller.formName()}
value={initialName}
placeholder={language.t("dialog.server.add.namePlaceholder")}
disabled={controller.formBusy()}
onInput={(event) => controller.handleFormNameChange()(event.currentTarget.value)}
onKeyDown={keyDown}
/>
@ -98,9 +97,8 @@ export const DialogServerV2: Component<{
type="text"
appearance="large"
class="!w-full self-stretch"
value={controller.formUsername()}
value={initialUsername}
placeholder={language.t("dialog.server.add.usernamePlaceholder")}
disabled={controller.formBusy()}
onInput={(event) => controller.handleFormUsernameChange()(event.currentTarget.value)}
onKeyDown={keyDown}
/>
@ -111,9 +109,8 @@ export const DialogServerV2: Component<{
type="password"
appearance="large"
class="!w-full self-stretch"
value={controller.formPassword()}
value={initialPassword}
placeholder={language.t("dialog.server.add.passwordPlaceholder")}
disabled={controller.formBusy()}
onInput={(event) => controller.handleFormPasswordChange()(event.currentTarget.value)}
onKeyDown={keyDown}
/>
@ -122,7 +119,7 @@ export const DialogServerV2: Component<{
</div>
</DialogBody>
<DialogFooter>
<ButtonV2 variant="neutral" disabled={controller.formBusy()} onClick={() => dialog.close()}>
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2 variant="contrast" disabled={controller.formBusy()} onClick={controller.submitForm}>