--- phase: 01-draft-state-ownership plan: 03 type: execute wave: 3 depends_on: [01-02] files_modified: - tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx - tauri-app/src/features/runtime/runtime-settings-form.ts - tauri-app/src/features/runtime/runtime-settings-form.test.ts - tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx autonomous: true requirements: [CONF-01, CONF-03, CONF-04] must_haves: truths: - Folder, model, and provider/source choices stay stable until the user saves or discards them. - Older async responses do not overwrite newer field intent. - Invalid draft values remain visible with inline errors and block save instead of being silently removed. artifacts: - path: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx provides: field-scoped async handlers and inline invalid-state rendering - path: tauri-app/src/features/runtime/runtime-settings-form.ts provides: helper logic for preserving selections and determining invalid draft state - path: tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx provides: regression coverage for stale async responses and invalid draft preservation - path: tauri-app/src/features/runtime/runtime-settings-form.test.ts provides: unit coverage for selection-preservation helpers and invalid-save blocking rules key_links: - from: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx to: tauri-app/src/features/runtime/runtime-settings-form.ts via: helper calls that preserve current selections and compute invalid draft state pattern: chooseModel|createRuntimeConfigPatch - from: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx to: tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx via: async picker and rescan regression tests pattern: scanRuntimeModels|pickRuntimeModelsDir|searchHuggingFaceModels --- Harden the controlled draft flow so field-scoped async work, rescans, and validation failures preserve user intent instead of silently mutating the draft. Purpose: finish the phase by protecting the highest-risk interactions: folder pickers, model rescans, stale async responses, and save failure recovery. Output: field-scoped async safety, invalid-draft preservation, save blocking, and regression tests for the full draft-safety contract. @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-hf-model-discovery/01-CONTEXT.md @.planning/phases/01-hf-model-discovery/01-RESEARCH.md @.planning/phases/01-hf-model-discovery/01-UI-SPEC.md @.planning/phases/01-hf-model-discovery/01-02-SUMMARY.md @tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx @tauri-app/src/features/runtime/runtime-settings-form.ts @tauri-app/src/features/runtime/runtime-settings-form.test.ts @tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx From `tauri-app/src/features/runtime/runtime-settings-form.ts`: ```ts export function chooseModel(current: string, scanned: RuntimeModelFile[], extra: RuntimeModelFile[]): string; export function createRuntimeConfigPatch( snapshotConfig: RuntimeConfig, form: RuntimeSettingsFormState, maxLayers: number, ): RuntimeConfig; ``` Locked behavior from `01-CONTEXT.md`: ```text - D-07: picker updates only the launched field - D-08: rescans preserve current draft selection unless invalid - D-10: stale responses for the same field are ignored - D-11: invalid drafted values remain visible, inline, and block save ``` Task 1: Make async picker and rescan handlers field-scoped and stale-safe tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx, tauri-app/src/features/runtime/runtime-settings-form.ts, tauri-app/src/features/runtime/runtime-settings-form.test.ts - tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx - tauri-app/src/features/runtime/runtime-settings-form.ts - tauri-app/src/features/runtime/runtime-settings-form.test.ts - .planning/phases/01-hf-model-discovery/01-CONTEXT.md - Test 1: folder/file pickers update only their target draft fields and do not rehydrate unrelated inputs (D-07). - Test 2: model rescans preserve the current draft selection when it is still valid and only mark it invalid when it disappears (D-08, D-11). - Test 3: only the latest request for a field can update draft-facing UI; older responses are ignored (D-10). Refactor async handlers in `RuntimeSettingsDrawer.tsx` so each operation has field-scoped state and a latest-request guard. Concretely: `handlePickDir`, `handlePickFiles`, `handleRescan`, and any async validation/search path must update only the field(s) they own rather than resetting the full draft. Add explicit request sequencing per field or operation key so older responses are ignored when a newer request for that same field has started. Update `runtime-settings-form.ts` helpers so rescans preserve the current drafted model path when it still exists, and return an invalid-draft signal when the drafted path no longer exists instead of auto-falling back to another model silently. Add helper-level tests in `runtime-settings-form.test.ts` for preserved selection, invalid-selection detection, and non-silent fallback rules. - `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` contains explicit stale-request protection for field-level async handlers. - `tauri-app/src/features/runtime/runtime-settings-form.ts` exposes helper logic that distinguishes valid preservation from invalid drafted selections. - `tauri-app/src/features/runtime/runtime-settings-form.test.ts` covers preserved selection and invalid-selection outcomes. npm test -- runtime-settings-form Async field helpers can no longer silently rewrite unrelated fields or let older responses override the latest user intent. Task 2: Preserve invalid drafts visibly and block save until resolved tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx, tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx - tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx - tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx - .planning/phases/01-hf-model-discovery/01-UI-SPEC.md - .planning/phases/01-hf-model-discovery/01-CONTEXT.md - Test 1: an invalid model or folder draft remains visible with inline error text and destructive styling instead of being cleared (CONF-04, D-11). - Test 2: `Save changes` is disabled while invalid draft metadata exists. - Test 3: a failed save keeps the current draft values in place and surfaces the failure message instead of resetting the form (CONF-04). In `RuntimeSettingsDrawer.tsx`, render inline invalid-state messaging for drafted values that fail validation or disappear during rescan. Preserve the user's typed or selected value in the control, add the destructive invalid styling from the Phase 1 color contract, and disable `Save changes` whenever invalid draft metadata exists. Keep save failures drawer-visible without resetting the draft or clearing helper state. Extend `RuntimeSurface.test.tsx` to assert concrete UI outcomes: invalid drafted values stay in the control, inline error copy is rendered near the affected field, `Save changes` is disabled until corrected, and a rejected save preserves the user's current draft state. - `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` renders inline invalid/error UI near affected fields and disables `Save changes` when invalid draft state exists. - `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` asserts that invalid draft values remain visible and save failures preserve the draft. npm test -- RuntimeSurface Phase 1 ends with durable draft safety: invalid values are visible, saves are blocked until fixed, stale async work is ignored, and user intent survives failures. - `npm test -- runtime-settings-form` - `npm test -- RuntimeSurface` - `npm run build` - Async field updates never overwrite unrelated draft fields. - Invalid model/folder drafts remain visible, explain the problem inline, and block save until fixed. - Save failures and rescans preserve user intent rather than silently replacing or clearing values. After completion, create `.planning/phases/01-hf-model-discovery/01-03-SUMMARY.md`