Initial ForgeLM import

This commit is contained in:
Evandro Fonseca Junior committed 2026-03-29 19:10:57 -03:00
commit a392b9dc60
4296 files changed
+1212675

No files matched your search

@@ -0,0 +1,177 @@
---
phase: 01-draft-state-ownership
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- tauri-app/src/features/runtime/types.ts
- tauri-app/src/features/runtime/useRuntimeManager.ts
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
autonomous: true
requirements: [WORK-02, CONF-01]
must_haves:
truths:
- User can keep editing a runtime draft while background polling continues without draft fields being overwritten.
- User can reopen the settings drawer in the same session and find the same unsaved draft.
- User can save a draft and have the saved values become the new clean baseline immediately.
artifacts:
- path: tauri-app/src/features/runtime/useRuntimeManager.ts
provides: separate ownership for live snapshot, saved baseline, and editable draft actions
- path: tauri-app/src/features/runtime/types.ts
provides: runtime draft contracts shared by the manager and drawer
- path: tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
provides: regression coverage for polling-safe draft ownership
key_links:
- from: tauri-app/src/features/runtime/useRuntimeManager.ts
to: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
via: manager props for live values, draft values, dirty state, and draft actions
pattern: draft|baseline|liveSnapshot
- from: tauri-app/src/features/runtime/useRuntimeManager.ts
to: tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
via: polling and save flow regression tests
pattern: bootstrapRuntime|getRuntimeSnapshot|saveRuntimeConfig
---
<objective>
Create the draft ownership foundation so the runtime feature has distinct live, baseline, and editable draft state before the drawer UI is refactored.
Purpose: eliminate the current poll-driven overwrite path at the feature-state layer instead of patching individual fields ad hoc.
Output: explicit runtime draft contracts and a manager API that preserves unsaved state across refresh, close, reopen, and save.
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.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
@tauri-app/src/features/runtime/useRuntimeManager.ts
@tauri-app/src/features/runtime/types.ts
@tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
<interfaces>
From `tauri-app/src/features/runtime/types.ts`:
```ts
export interface RuntimeConfig {
connection_mode: ConnectionMode;
start_policy: StartPolicy;
server_url: string;
api_key: string;
model_path: string | null;
models_dir: string | null;
addr: string;
gpu_layers: number;
ctx_size: number;
extra_model_paths: string[];
selected_model_id: string | null;
remote_presets: RemotePreset[];
hugging_face_token: string;
}
export interface RuntimeSnapshot {
status: RuntimeStatus;
ready: boolean;
process_running: boolean;
resolved_url: string;
binary_path: string | null;
last_error: RuntimeErrorInfo | null;
config: RuntimeConfig;
}
```
From `tauri-app/src/features/runtime/useRuntimeManager.ts`:
```ts
export function useRuntimeManager() {
return {
snapshot,
connection,
loading,
busy,
refresh,
saveConfig,
startLocal,
stopLocal,
};
}
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Define runtime draft ownership contracts</name>
<files>tauri-app/src/features/runtime/types.ts</files>
<read_first>
- tauri-app/src/features/runtime/types.ts
- .planning/phases/01-hf-model-discovery/01-CONTEXT.md
- .planning/phases/01-hf-model-discovery/01-UI-SPEC.md
</read_first>
<behavior>
- Test 1: the runtime feature can represent live values separately from editable draft values and the saved baseline.
- Test 2: the draft contract exposes enough state for the drawer to show dirty, hidden, invalid, and clean states without reading raw snapshot internals.
</behavior>
<action>Extend `tauri-app/src/features/runtime/types.ts` with explicit draft-facing contracts for Phase 1 per D-01 through D-06 and D-09 through D-11. Add concrete exported types for manager-owned draft state, including: a live snapshot reference, a saved baseline `RuntimeConfig`, an editable draft `RuntimeConfig`, a boolean dirty flag, a boolean open/hidden state, and field-level invalid/pending metadata keyed at least for `models_dir`, `model_path`, `selected_model_id`, `server_url`, and `api_key`. Keep existing `RuntimeSnapshot` and `RuntimeConfig` intact so brownfield callers still compile; add new exports rather than replacing old ones.</action>
<acceptance_criteria>
- `tauri-app/src/features/runtime/types.ts` exports draft-specific interfaces or types in addition to `RuntimeSnapshot` and `RuntimeConfig`.
- The draft contract includes explicit baseline and draft config properties rather than relying on one mutable `snapshot.config`.
- The draft contract includes field-level invalid/pending metadata needed for inline save blocking.
</acceptance_criteria>
<verify>
<automated>npm test -- runtime-settings-form</automated>
</verify>
<done>Runtime draft state is a first-class typed contract that downstream code can consume without inferring ownership rules from comments or snapshot mutations.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Move draft, baseline, and polling reconciliation into the runtime manager</name>
<files>tauri-app/src/features/runtime/useRuntimeManager.ts, tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx</files>
<read_first>
- tauri-app/src/features/runtime/useRuntimeManager.ts
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
- tauri-app/src/features/runtime/types.ts
- .planning/phases/01-hf-model-discovery/01-CONTEXT.md
</read_first>
<behavior>
- Test 1: polling updates operational live state without overwriting dirty draft values (D-02, D-09).
- Test 2: closing and reopening the drawer in the same session restores the unsaved draft (D-06).
- Test 3: successful save promotes the returned config into the new clean baseline while keeping the draft open (D-03).
</behavior>
<action>Refactor `useRuntimeManager.ts` so it owns three distinct sources of truth: `liveSnapshot` from polling/lifecycle commands, `savedBaseline` from the last committed `RuntimeConfig`, and `draftConfig` for user edits. Add explicit actions named for executor clarity: create/open draft from `savedBaseline`, close draft without discard, replace the full draft with `savedBaseline`, update a targeted draft field, and save the current draft. Polling via `getRuntimeSnapshot()` must still run every 3000 ms, but when `draftDirty` is true it may only update operational live fields (`status`, `ready`, `process_running`, `resolved_url`, `binary_path`, `last_error`, and managed-library metadata if present), not overwrite `draftConfig`. After `saveRuntimeConfig()` resolves, set both `liveSnapshot.config` and `savedBaseline` to the returned config, clear dirty state, and keep the draft available/open per D-03. Add or update `RuntimeSurface.test.tsx` coverage so these manager behaviors are enforced through the feature harness.</action>
<acceptance_criteria>
- `tauri-app/src/features/runtime/useRuntimeManager.ts` exposes separate live and draft-facing state instead of only `snapshot`.
- The polling path no longer writes the full fetched snapshot into the editable draft when the draft is dirty.
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` contains regression tests for poll-safe drafts, close/reopen preservation, and save promotion.
</acceptance_criteria>
<verify>
<automated>npm test -- RuntimeSurface</automated>
</verify>
<done>The runtime manager becomes the source of truth for Phase 1 draft ownership, with automated coverage proving polling and save flows preserve draft safety.</done>
</task>
</tasks>
<verification>
- `npm test -- RuntimeSurface`
- `npm test -- runtime-settings-form`
</verification>
<success_criteria>
- Live runtime refresh continues while dirty draft fields remain unchanged.
- Saving a draft promotes the saved config into the new baseline without closing the drawer.
- Closing and reopening the drawer in the same session restores the unsaved draft.
</success_criteria>
<output>
After completion, create `.planning/phases/01-hf-model-discovery/01-01-SUMMARY.md`
</output>
@@ -0,0 +1,94 @@
---
phase: 01-draft-state-ownership
plan: 01
subsystem: ui
tags: [react, typescript, tauri, vitest, runtime-settings]
requires: []
provides:
- runtime draft ownership contracts for live, baseline, and editable runtime state
- manager-owned polling reconciliation that preserves dirty drafts
- regression coverage for poll-safe draft edits, close or reopen preservation, and save promotion
affects: [phase-1-drawer-refactor, runtime-settings-drawer, lifecycle-semantics]
tech-stack:
added: []
patterns: [manager-owned live-baseline-draft state, operational-only polling reconciliation while dirty, hook-level draft regression harness]
key-files:
created: []
modified:
- tauri-app/src/features/runtime/types.ts
- tauri-app/src/features/runtime/useRuntimeManager.ts
- tauri-app/src/features/runtime/runtime-settings-form.test.ts
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
key-decisions:
- "Keep existing snapshot/saveConfig outputs while adding explicit draft APIs so brownfield consumers keep compiling during the Phase 1 transition."
- "When the draft is dirty, polling updates only operational live fields and leaves baseline plus draft config untouched."
patterns-established:
- "Runtime manager owns liveSnapshot, savedBaseline, and draftConfig separately instead of deriving editable state from polling output."
- "Draft behavior is verified with hook-level regression tests that exercise open, close, update, poll, and save flows independently from the drawer UI."
requirements-completed: [WORK-02, CONF-01]
duration: 10min
completed: 2026-03-29
---
# Phase 1 Plan 1: Draft State Ownership Summary
**Manager-owned runtime draft contracts with polling-safe reconciliation and save-promotion regressions for the Tauri runtime workspace**
## Performance
- **Duration:** 10 min
- **Started:** 2026-03-29T02:43:00Z
- **Completed:** 2026-03-29T02:52:44Z
- **Tasks:** 2
- **Files modified:** 4
## Accomplishments
- Added explicit draft-state contracts for live snapshot, saved baseline, editable draft config, and field metadata in `tauri-app/src/features/runtime/types.ts`
- Refactored `useRuntimeManager` to own `liveSnapshot`, `savedBaseline`, and `draftConfig` separately while preserving legacy `snapshot` and `saveConfig` compatibility
- Added regression coverage proving dirty drafts survive polling, hidden drafts survive close or reopen in-session, and save promotes the returned config into the new clean baseline
## Task Commits
Each task was committed atomically:
1. **Task 1: Define runtime draft ownership contracts** - `929a516` (feat)
2. **Task 2: Move draft, baseline, and polling reconciliation into the runtime manager** - `18501e9` (feat)
**Plan metadata:** pending
## Files Created/Modified
- `tauri-app/src/features/runtime/types.ts` - Defines reusable draft-state contracts and default field metadata for manager-owned runtime editing
- `tauri-app/src/features/runtime/runtime-settings-form.test.ts` - Verifies the new draft contract surface with runtime-backed and type-level assertions
- `tauri-app/src/features/runtime/useRuntimeManager.ts` - Separates live polling state from saved baseline and editable draft state while keeping legacy consumers working
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` - Covers dirty polling reconciliation, close or reopen draft persistence, and save promotion behavior through the runtime manager harness
## Decisions Made
- Kept `snapshot` and `saveConfig` on the manager return value so the current drawer keeps working while Phase 1 transitions draft ownership into the manager
- Treated dirty polling as an operational refresh only, updating status and process health without replacing draft or baseline config values
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
- The first RED attempt used type-only assertions, which Vitest transpiled without failing on missing exports; the contract test was adjusted to use runtime-backed draft helpers so the TDD red phase became real before implementation
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- Phase 1 now has a typed draft-state foundation and manager behaviors the drawer refactor can consume directly
- The next plan can move the drawer UI onto the manager-owned draft API without re-solving polling or save-baseline ownership rules
## Self-Check
PASSED
- FOUND: `.planning/phases/01-hf-model-discovery/01-01-SUMMARY.md`
- FOUND: `929a516`
- FOUND: `18501e9`
---
*Phase: 01-draft-state-ownership*
*Completed: 2026-03-29*
@@ -0,0 +1,154 @@
---
phase: 01-draft-state-ownership
plan: 02
type: execute
wave: 2
depends_on: [01-01]
files_modified:
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
autonomous: true
requirements: [WORK-02, CONF-02]
must_haves:
truths:
- User can tell which values are draft edits versus live or saved values while editing.
- User can explicitly discard changes back to the last saved baseline.
- Closing the drawer hides the draft without discarding it, and reopening restores it.
artifacts:
- path: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
provides: controlled draft editor with explicit live-vs-draft presentation and discard flow
- path: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
provides: draft-state visual treatment matching the approved UI contract
- path: tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
provides: coverage for visible draft and discard semantics
key_links:
- from: tauri-app/src/features/runtime/useRuntimeManager.ts
to: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
via: controlled props for draft values and draft actions
pattern: draft|discard|openDraft|closeDraft
- from: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
to: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
via: classes for draft badges, helper rows, sticky header, and discard confirmation
pattern: draft|live-now|discard
---
<objective>
Refactor the settings drawer into a controlled draft editor that visibly distinguishes draft values from live values and exposes explicit discard behavior.
Purpose: satisfy the user-facing trust requirements once manager-owned draft state exists.
Output: a drawer that renders `Draft settings`, `Live now`, `Save changes`, `Discard changes`, and in-session draft preservation per the approved UI contract.
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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-01-SUMMARY.md
@tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
@tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
@tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
<interfaces>
From `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`:
```tsx
interface Props {
open: boolean;
snapshot: RuntimeSnapshot;
busy: boolean;
onClose: () => void;
onSave: (patch: Partial<RuntimeConfig>) => Promise<RuntimeSnapshot>;
onStart: () => Promise<unknown>;
onStop: () => Promise<unknown>;
}
```
From `01-UI-SPEC.md`:
```text
- Sticky header title: Draft settings
- Visible live reference label: Live now
- Footer actions: Save changes, Discard changes
- Confirmation row: Keep editing, Revert to saved
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Convert the drawer to a controlled draft/live presentation</name>
<files>tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx, tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css</files>
<read_first>
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
- .planning/phases/01-hf-model-discovery/01-UI-SPEC.md
- .planning/phases/01-hf-model-discovery/01-CONTEXT.md
- .planning/phases/01-hf-model-discovery/01-01-SUMMARY.md
</read_first>
<behavior>
- Test 1: changed fields show visible draft treatment plus a live/saved reference row, not color-only state (WORK-02).
- Test 2: the drawer header and footer use `Draft settings`, `Live now`, `Save changes`, and `Discard changes` copy from the UI contract.
- Test 3: close hides the draft without resetting it; discard is the explicit full-form reset path (D-04, D-05, D-06).
</behavior>
<action>Refactor `RuntimeSettingsDrawer.tsx` to consume manager-owned draft props from Plan 01 instead of repopulating local form state from `snapshot` in a blanket effect. Replace the current header copy (`Runtime workspace`, `One control plane for startup, models, and API routing`) with the Phase 1 contract: sticky `Draft settings` title, a compact dirty-state summary, and `Live now` reference content. For every dirty editable field, render the draft control with a draft accent treatment plus a helper row showing the current saved/live value beneath it. Replace footer labels with `Save changes` and `Discard changes`. Implement the inline discard confirmation row exactly with `Keep editing` and destructive `Revert to saved`; `Discard changes` opens the confirmation row and only `Revert to saved` actually resets the full draft to baseline. Backdrop click and close button must call the manager's hide-draft action, not discard.</action>
<acceptance_criteria>
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` no longer has a `useEffect` that fully rehydrates all form fields from `snapshot.config` on every snapshot change.
- The file contains the strings `Draft settings`, `Live now`, `Save changes`, `Discard changes`, `Keep editing`, and `Revert to saved`.
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css` contains styles for visible draft-state and live-reference treatment rather than only neutral field styling.
</acceptance_criteria>
<verify>
<automated>npm test -- RuntimeSurface</automated>
</verify>
<done>The drawer becomes a controlled, explicit draft editor that makes unsaved edits and saved/live references obvious and never discards draft state on close.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Add drawer-level tests for draft visibility and explicit discard semantics</name>
<files>tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx</files>
<read_first>
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
- .planning/phases/01-hf-model-discovery/01-UI-SPEC.md
</read_first>
<behavior>
- Test 1: editing a field marks it as draft and shows the saved/live reference below the control.
- Test 2: clicking close or backdrop preserves the unsaved draft for reopen in the same session.
- Test 3: `Discard changes` does not reset immediately; `Revert to saved` performs the reset while `Keep editing` cancels it.
</behavior>
<action>Add or update `RuntimeSurface.test.tsx` so it exercises the controlled drawer through the runtime manager harness. Assert concrete UI strings and behaviors: a dirty field shows the `Draft` signal and the `Live now` or saved reference row; close/backdrop preserves the edited value on reopen; `Discard changes` reveals the confirmation row; `Keep editing` leaves the draft untouched; `Revert to saved` restores the last saved baseline for the full form. Keep the tests in the existing jsdom harness instead of introducing a new test framework.</action>
<acceptance_criteria>
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` contains assertions for `Draft settings`, `Discard changes`, `Keep editing`, and `Revert to saved`.
- The tests verify close/reopen preservation and full-form discard reset behavior.
</acceptance_criteria>
<verify>
<automated>npm test -- RuntimeSurface</automated>
</verify>
<done>Automated tests prove the drawer exposes clear draft state and that discard behavior is explicit, reversible at the confirmation step, and never triggered by simple close.</done>
</task>
</tasks>
<verification>
- `npm test -- RuntimeSurface`
- `npm run build`
</verification>
<success_criteria>
- The drawer clearly distinguishes draft versus live/saved values while editing.
- Users must explicitly confirm a full-form discard; close merely hides the draft.
- The approved Phase 1 copy and layout contract is reflected in the drawer implementation.
</success_criteria>
<output>
After completion, create `.planning/phases/01-hf-model-discovery/01-02-SUMMARY.md`
</output>
@@ -0,0 +1,109 @@
---
phase: 01-draft-state-ownership
plan: 02
subsystem: ui
tags: [react, typescript, runtime-drawer, css, vitest]
requires:
- phase: 01-01
provides: manager-owned draft, baseline, and live runtime state
provides:
- controlled runtime settings drawer wired to manager-owned draft state
- explicit draft-vs-live presentation with sticky Draft settings header and Live now references
- inline discard confirmation flow that preserves draft state on close and reverts only on explicit confirmation
affects: [phase-1-async-safety, runtime-workspace, lifecycle-semantics]
tech-stack:
added: []
patterns: [controlled drawer over manager draft state, explicit discard confirmation row, dirty-field live reference rows]
key-files:
created: []
modified:
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
- tauri-app/src/components/Sidebar.tsx
key-decisions:
- "Wire drawer visibility through manager open or close actions so backdrop and close hide the in-session draft instead of discarding it."
- "Use inline draft badges plus Live now helper rows so changed values are distinguishable without relying on color alone."
patterns-established:
- "Runtime settings UI reads draftConfig and savedBaseline directly rather than mirroring snapshot.config into local form state."
- "Discard changes opens a confirmation row; only Revert to saved performs the reset."
requirements-completed: [WORK-02, CONF-02]
duration: 18min
completed: 2026-03-29
---
# Phase 1 Plan 2: Draft Presentation Summary
**Controlled runtime drawer with explicit draft or live presentation, sticky draft copy, and confirmed discard semantics**
## Performance
- **Duration:** 18 min
- **Started:** 2026-03-29T02:53:00Z
- **Completed:** 2026-03-29T03:10:41Z
- **Tasks:** 2
- **Files modified:** 4
## Accomplishments
- Refactored `RuntimeSettingsDrawer.tsx` into a controlled draft editor that reads manager-owned draft and baseline values
- Added sticky `Draft settings` and `Live now` presentation, draft badges, and explicit `Save changes` and `Discard changes` copy
- Added drawer-level regression coverage for visible draft state, close or reopen preservation, and explicit discard confirmation behavior
## Task Commits
Each task was committed atomically:
1. **Task 1: Convert the drawer to a controlled draft/live presentation** - `80883c2` (feat)
2. **Task 2: Add drawer-level tests for draft visibility and explicit discard semantics** - `80883c2` (feat)
**Plan metadata:** pending
## Files Created/Modified
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` - Controlled draft editor with sticky header, live reference rows, and discard confirmation flow
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css` - Draft-state, live-reference, sticky header and footer, and discard confirmation styling
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` - Drawer-level regression tests for draft copy, close or reopen preservation, and discard confirmation
- `tauri-app/src/components/Sidebar.tsx` - App-shell wiring so opening and closing the drawer goes through manager draft actions
## Decisions Made
- Routed drawer open and close through the manager API so hidden drafts survive close and reopen within the same session
- Kept the local runtime apply CTA separate from `Save changes` while still putting draft save and discard semantics in the primary footer flow
## Deviations from Plan
### Auto-fixed Issues
**1. [Integration] Added sidebar wiring for controlled draft opening and closing**
- **Found during:** Task 1 (controlled drawer refactor)
- **Issue:** The plan focused on drawer files, but the app shell still opened the drawer through local UI state only
- **Fix:** Updated `tauri-app/src/components/Sidebar.tsx` to call `openDraft`, `closeDraft`, `resetDraft`, and draft update handlers from `useRuntimeManager`
- **Files modified:** `tauri-app/src/components/Sidebar.tsx`
- **Verification:** `npm test -- RuntimeSurface`; `npm run build`
- **Committed in:** `80883c2`
---
**Total deviations:** 1 auto-fixed (integration)
**Impact on plan:** Necessary brownfield wiring only; no scope creep beyond enabling the planned controlled drawer behavior.
## Issues Encountered
- None
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- The drawer now exposes explicit draft and live semantics, which gave Plan 03 a stable surface for async safety and invalid-draft handling
- Controlled draft UI is in place for future lifecycle-semantics work in Phase 2
## Self-Check
PASSED
- Verified explicit draft and live copy in `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx`
- Verified discard confirmation and close or reopen draft preservation with `npm test -- RuntimeSurface`
- Verified production compile with `npm run build`
---
*Phase: 01-draft-state-ownership*
*Completed: 2026-03-29*
@@ -0,0 +1,157 @@
---
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
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
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
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Make async picker and rescan handlers field-scoped and stale-safe</name>
<files>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</files>
<read_first>
- 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
</read_first>
<behavior>
- 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).
</behavior>
<action>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.</action>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<verify>
<automated>npm test -- runtime-settings-form</automated>
</verify>
<done>Async field helpers can no longer silently rewrite unrelated fields or let older responses override the latest user intent.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Preserve invalid drafts visibly and block save until resolved</name>
<files>tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx, tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx</files>
<read_first>
- 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
</read_first>
<behavior>
- 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).
</behavior>
<action>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.</action>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<verify>
<automated>npm test -- RuntimeSurface</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<verification>
- `npm test -- runtime-settings-form`
- `npm test -- RuntimeSurface`
- `npm run build`
</verification>
<success_criteria>
- 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.
</success_criteria>
<output>
After completion, create `.planning/phases/01-hf-model-discovery/01-03-SUMMARY.md`
</output>
@@ -0,0 +1,111 @@
---
phase: 01-draft-state-ownership
plan: 03
subsystem: ui
tags: [react, typescript, vitest, async-state, validation]
requires:
- phase: 01-02
provides: controlled draft drawer and explicit draft/live UI semantics
provides:
- stale-safe field-scoped async handling for model folders, rescans, and picker flows
- invalid draft preservation with inline errors and save blocking
- save failure recovery that keeps current draft values visible and editable
affects: [phase-2-lifecycle-safety, runtime-validation, runtime-workspace]
tech-stack:
added: []
patterns: [shared async request guard for field-scoped operations, invalid draft preservation instead of silent fallback, helper-level model-selection validation]
key-files:
created: []
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
- tauri-app/src/features/runtime/useRuntimeManager.ts
key-decisions:
- "Use a shared request key for model-selection async work so stale scan responses cannot overwrite later browse or picker intent."
- "Preserve invalid drafted model values and surface inline errors rather than auto-selecting a replacement model."
patterns-established:
- "Async folder and model operations update only their owned draft fields and attach pending or invalid metadata instead of rehydrating the full form."
- "Save stays disabled while invalid or pending field metadata exists, and failed saves keep the draft intact."
requirements-completed: [CONF-01, CONF-03, CONF-04]
duration: 18min
completed: 2026-03-29
---
# Phase 1 Plan 3: Async Draft Safety Summary
**Field-scoped async guards, invalid draft preservation, and save-failure recovery for runtime settings**
## Performance
- **Duration:** 18 min
- **Started:** 2026-03-29T02:53:00Z
- **Completed:** 2026-03-29T03:10:41Z
- **Tasks:** 2
- **Files modified:** 5
## Accomplishments
- Added helper logic in `runtime-settings-form.ts` to distinguish valid selection preservation from invalid drafted model paths
- Hardened `RuntimeSettingsDrawer.tsx` so async scans and pickers update only their owned fields, ignore stale results, and keep invalid draft values visible inline
- Added regression coverage for stale async protection, invalid draft save blocking, and failed-save draft preservation
## Task Commits
Each task was committed atomically:
1. **Task 1: Make async picker and rescan handlers field-scoped and stale-safe** - `80883c2` (feat)
2. **Task 2: Preserve invalid drafts visibly and block save until resolved** - `80883c2` (feat)
**Plan metadata:** pending
## Files Created/Modified
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` - Shared async request guarding, inline invalid-state messaging, and save blocking for pending or invalid draft fields
- `tauri-app/src/features/runtime/runtime-settings-form.ts` - Draft model resolution helper that keeps missing selections visible and marked invalid
- `tauri-app/src/features/runtime/runtime-settings-form.test.ts` - Unit coverage for preserved selections and invalid draft outcomes
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` - Runtime regression tests for stale async responses, invalid draft preservation, and failed-save recovery
- `tauri-app/src/features/runtime/useRuntimeManager.ts` - Draft metadata update helpers used by the controlled drawer for pending and invalid field state
## Decisions Made
- Unified scan, folder browse, and file-pick draft updates under one model-selection request channel so later user intent always wins over earlier async work
- Disabled `Save changes` whenever invalid or pending draft metadata exists to match the locked Phase 1 trust contract
## Deviations from Plan
### Auto-fixed Issues
**1. [Integration] Extended manager metadata APIs for pending and invalid field state**
- **Found during:** Task 2 (invalid draft preservation)
- **Issue:** The typed manager contract existed, but the drawer needed a write path for field-level pending and invalid metadata
- **Fix:** Added `updateDraft` and `updateDraftFieldMeta` support in `tauri-app/src/features/runtime/useRuntimeManager.ts`
- **Files modified:** `tauri-app/src/features/runtime/useRuntimeManager.ts`
- **Verification:** `npm test -- RuntimeSurface`; `npm test -- runtime-settings-form`; `npm run build`
- **Committed in:** `80883c2`
---
**Total deviations:** 1 auto-fixed (integration)
**Impact on plan:** Required to complete the planned invalid-state behavior. No user-facing scope expansion beyond Phase 1 draft safety.
## Issues Encountered
- The stale-response test originally used two rescan requests, but the UI correctly disables a second rescan while one is already in flight; the regression was adjusted to cover a pending initial scan followed by a later folder browse request against the same model-selection guard
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- Phase 1 draft safety is complete: polling, discard, invalid values, async rescans, and save failures now preserve user intent
- Phase 2 can build explicit lifecycle command semantics on top of a trusted settings surface instead of compensating for draft-state bugs
## Self-Check
PASSED
- Verified helper-level selection and invalid-draft behavior with `npm test -- runtime-settings-form`
- Verified stale async guards, invalid draft blocking, and save-failure recovery with `npm test -- RuntimeSurface`
- Verified production compile with `npm run build`
---
*Phase: 01-draft-state-ownership*
*Completed: 2026-03-29*
@@ -0,0 +1,105 @@
# Phase 1: Draft State Ownership - Context
**Gathered:** 2026-03-28
**Status:** Ready for planning
<domain>
## Phase Boundary
Separate editable runtime settings from live runtime state so polling, folder/model refresh work, and other background activity cannot clobber in-progress edits before the user explicitly saves or discards them.
This phase covers draft ownership, reset/save/reopen behavior, and safe async updates inside the runtime settings experience. It does not yet consolidate the external runtime status card into a single workspace; that belongs to Phase 3.
</domain>
<decisions>
## Implementation Decisions
### Draft lifecycle
- **D-01:** Opening the runtime settings surface creates the editable draft from the latest saved baseline.
- **D-02:** Live polling continues while the drawer is open, but only live operational state updates from polling; the draft itself is not rehydrated while dirty.
- **D-03:** After a successful save, the saved values become the new clean baseline immediately and the settings surface stays open.
- **D-04:** `Cancel` resets the full form back to the last saved baseline.
- **D-05:** `Revert` is the same as full-form `Cancel` for Phase 1; no separate per-field undo behavior is required.
- **D-06:** Closing the drawer or backdrop with unsaved edits hides the draft without discarding it, so reopening in the same session restores that hidden draft.
### Async field behavior
- **D-07:** Folder pickers and file choosers may update only the field they were launched for; they must not rehydrate or rewrite unrelated draft fields.
- **D-08:** Model rescans may refresh available options, but should preserve the current draft selection unless that selection becomes invalid.
- **D-09:** While the form is dirty, background polling may continue to refresh live status-oriented data (for example runtime health, readiness, or library status) but must not overwrite draft-facing fields or local async selections.
- **D-10:** Older async responses for the same field or operation must be ignored; only the latest in-flight request for that field may update the draft-facing UI.
- **D-11:** If a rescan or validation pass shows the currently drafted model or folder choice is invalid, keep the user's draft value, mark it invalid inline, and block save until the user fixes it.
### the agent's Discretion
- Exact UI treatment for showing live value versus draft value, as long as users can clearly tell when a field is unsaved.
- Exact request identity/cancellation mechanism used to ignore stale async results.
- Exact wording and placement of inline validation and dirty-state messaging, as long as draft safety remains clear.
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Project scope and phase requirements
- `.planning/PROJECT.md` — Project context, brownfield constraints, and the core value that unsaved edits must not be overwritten.
- `.planning/REQUIREMENTS.md` — Phase 1 requirement set (`WORK-02`, `CONF-01`, `CONF-02`, `CONF-03`, `CONF-04`) and requirement wording.
- `.planning/ROADMAP.md` — Phase 1 goal, success criteria, and dependency boundary.
- `.planning/STATE.md` — Current focus and locked project-level decisions carried into this phase.
### Existing architecture and codebase context
- `.planning/codebase/ARCHITECTURE.md` — Runtime management architecture, ownership boundaries, and frontend/backend integration path.
- `.planning/codebase/STACK.md` — Current stack and runtime/config persistence context.
### Runtime workspace implementation
- `tauri-app/src/features/runtime/useRuntimeManager.ts` — Current live snapshot polling, save/start/stop mutation flow, and snapshot overwrite behavior.
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` — Current form state, full rehydration effect, async helpers, and save actions.
- `tauri-app/src/features/runtime/runtime-settings-form.ts` — Patch creation rules, model selection helper behavior, and local-vs-remote field preservation.
- `tauri-app/src/features/runtime/types.ts` — `RuntimeSnapshot` and `RuntimeConfig` ownership boundary.
- `tauri-app/src/features/runtime/components/RuntimeStatusCard.tsx` — Adjacent runtime surface kept in mind for Phase 3 separation, not for Phase 1 scope.
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- `tauri-app/src/features/runtime/useRuntimeManager.ts`: already centralizes runtime snapshot fetching, polling, and lifecycle mutations; Phase 1 should likely evolve this into a clearer live-state owner rather than duplicating runtime orchestration elsewhere.
- `tauri-app/src/features/runtime/runtime-settings-form.ts`: already knows how to derive a config patch from baseline config plus form values; useful as the baseline-vs-draft merge boundary.
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`: already contains the relevant settings controls and async hooks, so Phase 1 can refactor ownership inside the existing surface before any larger UI reorganization.
### Established Patterns
- Runtime state is currently polled every 3 seconds and stored as one `RuntimeSnapshot`; Phase 1 must break the assumption that this same object can also be the editable form source of truth.
- Save/start/stop flows already return refreshed snapshots from the backend; Phase 1 should preserve that backend contract while changing how the frontend promotes saved state into the draft baseline.
- The app is brownfield React + TypeScript without a global store; planning should prefer a feature-scoped ownership model over a whole-app state rewrite.
### Integration Points
- Draft creation and promotion connect directly to `useRuntimeManager.ts` save and refresh behavior.
- Dirty-state-safe field updates connect directly to the rehydration effect and async picker/rescan logic inside `RuntimeSettingsDrawer.tsx`.
- Validation and invalid-draft blocking connect to `runtime-settings-form.ts` patch generation and any field-level validation added during Phase 1.
</code_context>
<specifics>
## Specific Ideas
- Closing with unsaved edits should behave differently from `Cancel`: the draft is hidden and restored on reopen within the same session.
- Async helpers should be narrow and field-scoped: a folder pick should update that folder field, not cause a whole-form refresh.
- Invalid async outcomes should preserve user intent visibly instead of silently auto-correcting the draft.
- Note: the current phase directory slug (`01-hf-model-discovery`) is stale metadata from earlier planning artifacts; ROADMAP Phase 1 (`Draft State Ownership`) is the canonical phase definition for downstream work.
</specifics>
<deferred>
## Deferred Ideas
- Full runtime workspace consolidation and moving the runtime manager into a single control center — Phase 3 (`Unified Runtime Workspace`).
- Source-specific path/model validation UX beyond draft preservation basics — Phase 4 (`Model Source Validation`).
</deferred>
---
*Phase: 01-hf-model-discovery*
*Context gathered: 2026-03-28*
@@ -0,0 +1,139 @@
# Phase 1: Draft State Ownership - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** 2026-03-28
**Phase:** 01-draft-state-ownership
**Areas discussed:** Draft lifecycle, Async field behavior
---
## Draft lifecycle
### When should the editable draft be created?
| Option | Description | Selected |
|--------|-------------|----------|
| On open | Create a working draft from the last saved config when the settings surface opens. | ✓ |
| On first edit | Do not create a draft until the user changes something. | |
| Persisted always | Keep one always-live draft even while the settings surface is closed. | |
**User's choice:** On open
**Notes:** Opening Runtime Settings should create the working draft; live polling keeps updating operational status only.
### After a successful save, what should happen to the draft?
| Option | Description | Selected |
|--------|-------------|----------|
| Promote and keep open | Saved values become the new clean baseline and the settings surface stays open. | ✓ |
| Save and close | Save, clear the draft, and close the drawer immediately. | |
| Save but keep draft dirty | Save to backend while preserving a separate unsaved local copy. | |
**User's choice:** Promote and keep open
**Notes:** Saved values should become the new baseline immediately.
### What should Cancel do while editing?
| Option | Description | Selected |
|--------|-------------|----------|
| Reset to last saved | Discard unsaved changes and restore the last saved baseline in the form. | ✓ |
| Close and keep draft | Close without resetting any unsaved draft values. | |
| Ask every time | Require confirmation before resetting changes. | |
**User's choice:** Reset to last saved
**Notes:** Cancel is the explicit discard action.
### How should Revert differ from Cancel?
| Option | Description | Selected |
|--------|-------------|----------|
| Same as cancel | One full-form revert is enough for Phase 1. | ✓ |
| Revert field-by-field | Allow a more granular undo path than Cancel. | |
| Revert to effective runtime | Reset to whatever is live, even if it differs from last saved config. | |
**User's choice:** Same as cancel
**Notes:** No per-field undo is needed yet.
### If the user closes the drawer with unsaved edits, what should happen?
| Option | Description | Selected |
|--------|-------------|----------|
| Keep draft hidden | Close the drawer but restore the same in-session draft on reopen. | ✓ |
| Discard draft | Treat close the same as Cancel. | |
| Force confirm dialog | Require an explicit choice before allowing close. | |
**User's choice:** Keep draft hidden
**Notes:** Close/backdrop behavior should differ from Cancel.
---
## Async field behavior
### When a folder picker or file chooser finishes after the draft is already dirty, how should its result be applied?
| Option | Description | Selected |
|--------|-------------|----------|
| Apply only if same field | Update only the field the user just picked, without rehydrating other draft fields. | ✓ |
| Apply everything from latest scan | Let the picker or scan refresh the broader form state too. | |
| Ask before applying | Show a confirmation before changing the draft. | |
**User's choice:** Apply only if same field
**Notes:** Async helpers should be field-scoped, not whole-form rehydration triggers.
### If a model rescan finishes while the user is editing, what should happen to model-related state?
| Option | Description | Selected |
|--------|-------------|----------|
| Refresh options, keep selection | Update the available model list but preserve the current draft selection unless it becomes invalid. | ✓ |
| Auto-pick best model | Replace the current selection with the first valid or recommended model. | |
| Defer until save | Do not update model options until the user saves. | |
**User's choice:** Refresh options, keep selection
**Notes:** Current draft selection should remain stable unless rescan proves it invalid.
### While the drawer has unsaved edits, how should background polling affect async-derived data like health, GPU hints, or library status?
| Option | Description | Selected |
|--------|-------------|----------|
| Status only, no draft overwrite | Keep live operational status fresh, but never rewrite draft fields or local async selections. | ✓ |
| Pause all refreshes | Freeze both status and async derived data until the draft is clean. | |
| Full live sync | Continue updating the full form from polling even while dirty. | |
**User's choice:** Status only, no draft overwrite
**Notes:** Polling remains valuable, but only for live status surfaces.
### If an older async request returns after a newer one for the same field, what should happen?
| Option | Description | Selected |
|--------|-------------|----------|
| Ignore stale results | Only the latest request for that field may update the draft-facing UI. | ✓ |
| Last response wins | Whichever request finishes last overwrites the UI. | |
| Show conflict warning | Keep both and ask the user which one to accept. | |
**User's choice:** Ignore stale results
**Notes:** Stale async results must not overwrite newer user intent.
### If a rescan shows the currently drafted model or folder choice is no longer valid, what should the UI do before save?
| Option | Description | Selected |
|--------|-------------|----------|
| Keep draft, mark invalid | Preserve the draft value, show validation, and block save until fixed. | ✓ |
| Auto-switch to valid option | Replace it immediately with the first valid option. | |
| Clear the field | Remove the invalid choice and force a new selection. | |
**User's choice:** Keep draft, mark invalid
**Notes:** The UI should preserve user intent visibly instead of silently auto-correcting it.
---
## the agent's Discretion
- Exact live-vs-draft visual treatment.
- Exact request identity/cancellation mechanism for stale async response handling.
- Exact inline validation copy and placement.
## Deferred Ideas
- Consolidating the status card and settings surface into one runtime control center belongs to Phase 3.
- Broader source/model/path validation improvements beyond draft-safety behavior belong to Phase 4.
@@ -0,0 +1,160 @@
# Phase 1: Draft State Ownership - Research
**Researched:** 2026-03-28
**Domain:** React/Tauri runtime settings draft ownership, polling isolation, and async form safety
**Confidence:** HIGH
## Summary
Phase 1 should stay entirely inside the existing React runtime workspace and `useRuntimeManager` boundary. The current failure mode is structural: `useRuntimeManager` owns a single polled `snapshot`, and `RuntimeSettingsDrawer.tsx` rehydrates all local field state from `snapshot.config` whenever the drawer is open and `snapshot` changes. Because polling runs every 3 seconds, any refresh, scan result, or save response can overwrite in-progress edits.
The lowest-risk fix is to split ownership into three explicit layers:
1. **Live runtime snapshot** - continues to refresh from polling and lifecycle commands.
2. **Saved baseline config** - the last persisted config the user can safely revert to.
3. **Editable draft state** - drawer-local intent that survives close/reopen in the same app session and is never overwritten by polling while dirty.
This phase should not introduce a global store, backend schema change, or Phase 3 workspace consolidation. Keep the brownfield shape: `useRuntimeManager` remains the feature-scoped source of truth, `RuntimeSettingsDrawer.tsx` becomes a renderer/editor over manager-owned draft state, and current save/start/stop backend contracts stay intact.
**Primary recommendation:** move draft/baseline ownership into the runtime feature state layer, expose explicit draft actions from `useRuntimeManager`, and refactor the drawer so async helpers update only targeted fields while stale responses are ignored.
## Current-State Findings
### Existing overwrite risk
- `tauri-app/src/features/runtime/useRuntimeManager.ts` polls `getRuntimeSnapshot()` every 3 seconds and writes the whole `snapshot` object back into React state.
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` has a single `useEffect(..., [open, snapshot])` that repopulates all local form fields from `snapshot.config` whenever polling refreshes the snapshot while the drawer is open.
- `saveConfig()` in `useRuntimeManager.ts` builds the save payload from the hook's current `snapshot.config`, so the save baseline is also tied to the same mutable polled object.
### Existing reusable boundaries
- `tauri-app/src/features/runtime/runtime-settings-form.ts` already centralizes patch construction and model selection helpers. It is the right place for draft-vs-baseline merge rules and invalid-selection handling helpers.
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` already covers the runtime status card + drawer integration and is the best place to add polling/draft regression tests.
- `tauri-app/src/features/runtime/types.ts` is the existing contract surface for the runtime feature and should carry any new draft-specific UI contracts.
### UX constraints already locked
- Per `01-CONTEXT.md`, opening creates a draft from the latest saved baseline (D-01), close preserves unsaved draft in-session (D-06), cancel/discard resets to the last saved baseline (D-04/D-05), async helpers are field-scoped (D-07), stale responses must be ignored (D-10), and invalid values must be preserved inline with save blocked (D-11).
- Per `01-UI-SPEC.md`, the drawer must present `Draft settings`, `Live now`, `Save changes`, `Discard changes`, `Keep editing`, and sticky live-vs-draft affordances instead of silently mutating values.
## Recommended Architecture
### Pattern 1: Manager owns live snapshot + baseline + draft
Add explicit runtime feature state instead of deriving editable form state directly from `snapshot` on every render.
Recommended shape:
- `liveSnapshot: RuntimeSnapshot` - latest polling/lifecycle truth
- `savedBaseline: RuntimeConfig` - last committed config
- `draftConfig: RuntimeConfig | null` - active or hidden in-session draft
- `isDraftDirty: boolean`
- `isDraftOpen: boolean`
- `draftErrors` / `invalidFields` / `pendingFieldOps`
Recommended actions:
- `openDraft()` - create draft from `savedBaseline` only if draft does not already exist
- `closeDraft()` - hide draft without discarding it
- `discardDraft()` - replace full draft with `savedBaseline`
- `updateDraftField(field, value)` - targeted field update
- `saveDraft()` - persist current draft, promote saved values to baseline, keep drawer open
- `applyLiveRefresh(snapshot)` - update operational live state every poll; update baseline only when draft is absent or clean
### Pattern 2: Drawer becomes a controlled draft editor
`RuntimeSettingsDrawer.tsx` should stop treating `snapshot` as the editable source of truth. Instead it should receive:
- current draft values
- saved/live reference values
- dirty/invalid metadata
- explicit actions for field updates, discard, save, and close
The drawer may keep only ephemeral UI state that is not part of the draft contract, such as an expanded discard confirmation row or transient search text when that text is intentionally not preserved.
### Pattern 3: Field-scoped async operations
Folder pickers, rescans, GPU detection, and any validation should update a targeted field or helper state only. Do not run a full-form rehydrate after any async operation.
Recommended approach:
- track per-field request ids or sequence counters
- before applying an async result, verify its id matches the latest request for that field
- preserve the drafted field value if validation says it is invalid; attach inline invalid metadata instead of auto-correcting or clearing the field
This is especially important for:
- `models_dir`
- `selectedModel`
- `selectedModelId`
- file-picker-added extra model paths
### Pattern 4: Save promotes baseline immediately
After a successful save:
- the returned config becomes the new `savedBaseline`
- the visible draft is replaced with the saved values and marked clean
- the drawer stays open
- polling continues, but future polls must not regress the saved baseline or draft-facing UI
### Pattern 5: Failure preserves draft
Save or validation failures should not reset the draft. Keep the user's current values in the editor, surface inline or drawer-level error messaging, and block save until invalid fields are resolved.
## Anti-Patterns to Avoid
- Rehydrating local input state from `snapshot.config` in a `useEffect` that depends on the full `snapshot`
- Using one `busy` flag as the only async state for all field-level operations
- Clearing invalid model/folder choices automatically after rescan
- Coupling discard behavior to drawer close/backdrop click
- Introducing Redux/Zustand/global app state for a feature-scoped ownership problem
- Moving runtime status card consolidation into this phase (deferred to Phase 3)
## Testing Strategy
### Recommended automated coverage
| Behavior | Test file | Automated command |
|----------|-----------|-------------------|
| Poll refresh does not overwrite dirty draft fields | `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` | `npm test -- RuntimeSurface` |
| Close/reopen preserves in-session draft but discard resets to baseline | `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` | `npm test -- RuntimeSurface` |
| Save promotes baseline and keeps drawer open | `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` | `npm test -- RuntimeSurface` |
| Field-scoped rescan preserves draft selection unless invalid | `tauri-app/src/features/runtime/runtime-settings-form.test.ts` and/or `RuntimeSurface.test.tsx` | `npm test -- runtime-settings-form` |
| Invalid draft values remain visible and block save | `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` | `npm test -- RuntimeSurface` |
### Phase verification commands
- Targeted UI/unit tests: `npm test -- RuntimeSurface`
- Helper/unit tests: `npm test -- runtime-settings-form`
- Phase gate: `npm run build`
## Open Questions Resolved For Planning
1. **Where should draft ownership live?**
- Recommendation: feature-scoped runtime manager state, not backend and not drawer-local snapshot rehydration.
2. **Do we need backend revision/timestamp support for Phase 1?**
- Recommendation: no backend change required for the first pass. The immediate bug is frontend ownership. Reconcile against the latest saved config returned by existing save/refresh calls.
3. **Should close prompt to discard?**
- Recommendation: no. Close hides the draft; `Discard changes` is the explicit reset path.
4. **Should invalid rescans clear selection?**
- Recommendation: no. Preserve the value, mark invalid inline, disable save.
## Sources
- `tauri-app/src/features/runtime/useRuntimeManager.ts`
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`
- `tauri-app/src/features/runtime/runtime-settings-form.ts`
- `tauri-app/src/features/runtime/types.ts`
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx`
- `tauri-app/src/features/runtime/runtime-settings-form.test.ts`
- `.planning/phases/01-hf-model-discovery/01-CONTEXT.md`
- `.planning/phases/01-hf-model-discovery/01-UI-SPEC.md`
- `.planning/ROADMAP.md`
- `.planning/REQUIREMENTS.md`
## Metadata
- Research focus: brownfield-safe draft-state separation for the runtime settings drawer
- New external dependencies required: none
- Suggested discovery level: Level 0 (existing patterns only; no new library selection)
@@ -0,0 +1,160 @@
---
phase: 1
slug: draft-state-ownership
status: draft
shadcn_initialized: false
preset: none
created: 2026-03-28
---
# Phase 1 — UI Design Contract
> Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none — existing hand-authored CSS remains the source of truth for Phase 1; shadcn init deferred by default because this phase only defines the contract and does not require component-library adoption |
| Preset | not applicable |
| Component library | none — preserve existing runtime drawer, status card, inline chips, segmented controls, and footer actions |
| Icon library | none required for Phase 1; use text labels and existing native controls before introducing new iconography |
| Font | "Segoe UI Variable", "SF Pro Display", "IBM Plex Sans", system-ui, sans-serif |
---
## Spacing Scale
Declared values (must be multiples of 4):
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Status dots, inline separators, badge internals |
| sm | 8px | Input-support gaps, chip gaps, tight action spacing |
| md | 16px | Default field spacing, card padding, inline validation offset |
| lg | 24px | Section padding, grouped control spacing |
| xl | 32px | Drawer section breaks, footer-to-body separation |
| 2xl | 48px | Major content breaks inside long settings drawers |
| 3xl | 64px | Reserved for page-level or modal-level spacing only |
Exceptions: minimum 44px input and button height for all primary interactive controls; destructive confirmation rows may use 12px vertical padding inside a 44px container.
---
## Typography
| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 16px | 400 | 1.5 |
| Label | 12px | 700 | 1.4 |
| Heading | 20px | 700 | 1.2 |
| Display | 28px | 700 | 1.1 |
---
## Color
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | #0a1016 | App background, drawer shell, page canvas |
| Secondary (30%) | #14202d | Cards, grouped sections, saved/live reference surfaces |
| Accent (10%) | #48a6ff | Draft-state badges, selected segmented controls, focused fields, primary save CTA, latest-valid async result indicators |
| Destructive | #f04040 | Discard/revert confirmations, inline invalid-state borders, blocking save errors |
Accent reserved for: dirty badges, active local-vs-remote selectors, selected model chips, the primary `Save changes` action, focus-visible outlines, and explicit live-vs-draft comparison labels; never use accent for all neutral buttons.
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Primary CTA | Save changes |
| Secondary footer action | Discard changes |
| Confirmation escape action | Keep editing |
| Empty state heading | No draft changes yet |
| Empty state body | Edit any runtime setting to start a draft. Live runtime values keep updating until you choose to save or discard. |
| Error state | Couldn't save your draft. Review the highlighted fields and try again — your unsaved changes are still here. |
| Destructive confirmation | Discard changes: Discard this draft and return to the last saved settings? / Revert to saved: Replace all unsaved edits with the last saved values. |
---
## State Presentation Contract
- The sticky drawer header is the first visual anchor on open: `Draft settings` title, dirty-state summary, and compact `Live now` status reference appear before any editable fields.
- Opening the settings drawer creates a draft from the latest saved baseline and labels the drawer as `Draft settings`.
- The current effective runtime state remains visible as `Live now` reference content in a secondary surface or read-only inline value for any field changed in the draft.
- Dirty fields show three signals together: draft accent border, `Draft` label, and saved/live reference value below the control; color alone is never the only indicator.
- Clean fields render with neutral borders and no draft badge.
- Closing the drawer with unsaved edits hides the drawer and preserves the draft for the same app session; do not show a discard prompt on close or backdrop click.
- `Discard changes` is the UI label for the Phase 1 full-form reset behavior locked in `01-CONTEXT.md`; `Revert to saved` is the destructive confirmation label for completing that same reset. Per-field undo is out of scope.
---
## Interaction Contract
- Polling continues every 3 seconds for operational state, but polling updates may affect only live status surfaces such as readiness, process state, health messaging, and managed-library metadata.
- Polling and async helpers must never overwrite dirty input values, selected folders, provider choice, or selected model while the draft is dirty.
- Folder pickers, file pickers, model rescans, and validation passes are field-scoped updates only; they may update the invoked field and its dependent helper text, not the whole form.
- If multiple async requests exist for the same field, only the latest response may update draft-facing UI; stale responses are ignored silently.
- When a drafted value becomes invalid after rescan or validation, preserve the typed or selected value, mark the field invalid inline, and disable `Save changes` until fixed.
- Resting footer actions for this phase are `Save changes` and `Discard changes`; selecting `Discard changes` expands an inline confirmation row with `Keep editing` and destructive `Revert to saved`. Local-runtime apply/start text remains separate and must always reflect whether saving is required first.
- Busy states disable the triggering button, preserve current field values, and replace button text with in-place progress copy such as `Saving...`, `Scanning...`, or `Searching...`.
- Error messaging appears inline near the affected field when possible; fall back to a single drawer-level error banner only for save failures or cross-field failures.
---
## Component Inventory
- Drawer shell: right-side fixed drawer, full-height, max width 560px, blurred backdrop, sticky header and footer.
- Primary focal point: sticky header block with `Draft settings` title, dirty-state summary, and `Live now` chip/status summary before the overview strip.
- Overview strip: three read-only summary cells for current status, endpoint, and saved model.
- Field groups: card-based sections with 16px internal padding and 18px corner radius.
- Segmented controls: two-option mode switch with clear active state and no icon-only affordances.
- Draft comparison rows: editable control on top, saved/live helper row beneath when dirty.
- Inline validation: destructive border plus one-line message directly under the affected field.
- Confirmation pattern: inline footer confirmation row for discard/revert actions instead of a separate modal.
---
## Accessibility and Motion
- All interactive controls must meet a 44x44px minimum hit target.
- Keyboard order must follow visual order from overview to footer actions.
- `:focus-visible` keeps the 2px accent outline already established in `tauri-app/src/index.css`.
- Body copy and helper text must maintain at least 4.5:1 contrast against their surfaces.
- State changes use opacity, border, and background transitions only, with durations between 150ms and 200ms.
- Respect `prefers-reduced-motion`; suppress scale transforms and animated status pulses when reduced motion is enabled.
---
## Source Notes
- Locked from `D:\Documents\Github\ForgeLM\.planning\ROADMAP.md`: Phase 1 goal and success criteria require explicit live-versus-draft separation, explicit save/cancel/revert semantics, and draft-safe failure recovery.
- Locked from `D:\Documents\Github\ForgeLM\.planning\phases\01-hf-model-discovery\01-CONTEXT.md`: draft lifecycle, preserved close behavior, field-scoped async updates, stale-request ignoring, and invalid-draft preservation.
- Grounded in existing UI from `D:\Documents\Github\ForgeLM\tauri-app\src\index.css`, `D:\Documents\Github\ForgeLM\tauri-app\src\features\runtime\components\RuntimeSettingsDrawer.css`, `D:\Documents\Github\ForgeLM\tauri-app\src\features\runtime\components\RuntimeStatusCard.css`, and `D:\Documents\Github\ForgeLM\tauri-app\src\features\runtime\components\RuntimeSettingsDrawer.tsx`.
- Brand output conventions referenced from `C:\Users\Evandro\.config\opencode\get-shit-done\references\ui-brand.md`.
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable — shadcn not initialized |
| third-party | none | not applicable |
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS
**Approval:** pending