# Domain Pitfalls **Domain:** Brownfield runtime/settings workspace UX for a local-first desktop app **Researched:** 2026-03-28 ## Critical Pitfalls ### Pitfall 1: Polling overwrites dirty draft state **What goes wrong:** A live runtime snapshot is treated as the same thing as the editable form, so background refresh rehydrates in-progress fields like models folder, selected model, API URL, or start policy. **Why it happens:** Brownfield code usually starts with "load snapshot into local state" and later adds polling. In this repo, `useRuntimeManager` polls every 3 seconds in `tauri-app/src/features/runtime/useRuntimeManager.ts:33`, while `RuntimeSettingsDrawer` resets local form state whenever `snapshot` changes in `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx:76`. **Consequences:** Users lose folder-picker selections, selected models jump, save writes unexpected values, and trust in the settings surface collapses. **Warning signs:** - Editing a field and seeing it snap back after a status refresh - Folder picker or model dropdown changing without explicit user action - Bugs phrased as "save worked but UI changed before I clicked save" - `useEffect` blocks that copy server/runtime props into form state whenever snapshot props change **Prevention:** - Separate `liveSnapshot`, `draft`, and `savedBaseline` as distinct states - Freeze draft reinitialization while form is dirty; only rebase on explicit save/cancel/reopen - Show live runtime changes as non-destructive notices instead of mutating draft fields - Add tests that simulate polling during edits and assert draft preservation **Phase should address it:** Workspace state-model phase, before any UI reorganization or control consolidation **Detection:** Repro test: open settings, edit `models_dir`, trigger polling/refresh, verify draft stays untouched. ### Pitfall 2: Event actions are coupled to render-sync effects instead of explicit user intents **What goes wrong:** Save/start/stop behavior is inferred from snapshot changes or effect chains instead of being modeled as explicit commands with predictable sequencing. **Why it happens:** Brownfield React screens often grow from separate cards/drawers and then unify later. React's current guidance explicitly warns against using Effects for event-specific logic because it creates stale or duplicated actions. **Consequences:** Double start attempts, stop-before-save confusion, operations firing after unrelated refreshes, and hard-to-debug race conditions during "save and start" flows. **Warning signs:** - One button both mutates draft state and infers an operational action from subsequent render state - Bugs that only appear under slow I/O or repeated clicks - Operational state described as "sometimes it starts twice" or "clicked stop but save still ran" - Shared busy flags masking which operation is in flight **Prevention:** - Model `save`, `start`, `stop`, and `saveThenStart` as explicit commands with one operation state machine - Keep event logic in action handlers, not in snapshot-sync effects - Add idempotency/operation guards so a second click cannot enqueue conflicting work - Track operation-specific status (`saving`, `starting`, `stopping`) instead of one generic `busy` **Phase should address it:** Runtime control orchestration phase **Detection:** Integration tests for rapid repeated clicks, slow save + start, and stop while save is pending. ### Pitfall 3: Async side effects write back after the user has already moved on **What goes wrong:** Model scans, GPU queries, Hugging Face searches, and folder pickers resolve later and overwrite newer UI intent. **Why it happens:** These tasks are launched from the settings drawer and update local state independently. In `RuntimeSettingsDrawer`, `scanRuntimeModels`, `queryRuntimeGpu`, `getModelLibraryState`, and search flows all update drawer state after async completion. **Consequences:** Wrong model gets auto-selected, stale scan results replace newer folder choices, GPU recommendations overwrite manual values, and users save a configuration they did not mean to create. **Warning signs:** - Changing folders quickly causes the first scan result to win - Search results from an old query appear after entering a new one - "Use recommended" values reappear after a user typed something else - No request tokens, abort logic, or latest-request guards around async setters **Prevention:** - Guard async responses with request IDs or abort/cancel semantics - Apply async results only if they still match the current draft context - Never auto-apply a recommendation after the field becomes dirty - Record whether a value is user-authored vs system-suggested before mutating it **Phase should address it:** Async data-source hardening phase for model discovery and device/runtime metadata **Detection:** Tests that change folders/modes mid-request and assert stale responses are ignored. ### Pitfall 4: One surface mixes draft validation, runtime health, and operational errors with no ownership boundaries **What goes wrong:** Users see one error banner, but it is unclear whether the issue is an unsaved field problem, a runtime health failure, a file-dialog issue, or a failed start/stop command. **Why it happens:** Brownfield runtime screens often merge settings and operations without redesigning the message model. This repo already carries both `localError` and `snapshot.last_error` into the same banner area in `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx:614`. **Consequences:** Users retry the wrong action, overwrite a recoverable draft, or think save failed when only health revalidation failed. **Warning signs:** - A single banner renders validation, transport, and runtime-health problems interchangeably - Support tickets include "I don't know what failed" - Start/stop buttons remain visible but their failure cause is ambiguous **Prevention:** - Split error ownership into validation, draft-save, runtime-operation, and background-health channels - Keep background degradation visible without blocking draft editing - Attach remediation copy to each error type (`fix field`, `retry save`, `restart runtime`, `recheck health`) - Log structured error sources so QA can verify the right message appears for each failure class **Phase should address it:** Error-handling and operator feedback phase **Detection:** UX acceptance tests that inject each failure type independently and verify distinct copy and CTA behavior. ## Moderate Pitfalls ### Pitfall 5: Hidden-mode fields leak across local/remote switching **What goes wrong:** Switching between local and remote modes either drops meaningful local draft state or silently carries stale values that should be isolated. **Why it happens:** Brownfield forms often reuse one object for multiple conceptual modes. `createRuntimeConfigPatch` conditionally preserves some snapshot values based on `form.mode` in `tauri-app/src/features/runtime/runtime-settings-form.ts:76`, which is safer than wiping everything, but still requires explicit draft semantics to avoid confusion. **Consequences:** Users think a local model choice was saved when it was only temporarily hidden, or remote credentials and local runtime state appear partially applied. **Warning signs:** - Toggling local/remote changes the primary CTA wording but preserves unexplained hidden values - Save behavior differs depending on prior mode history - Bugs mention "switching back brought old values back" or "my local setup disappeared" **Prevention:** - Maintain mode-specific draft slices with a shared shell state - Show when hidden values are preserved vs discarded - Require explicit save to commit a mode switch and its dependent fields - Add tests for local -> remote -> local round trips without data loss or accidental carryover **Phase should address it:** Settings information architecture phase **Detection:** Round-trip tests across mode switches with unsaved changes. ### Pitfall 6: Polling ignores editing context and transient operation states **What goes wrong:** Background refresh keeps firing during `starting`, `stopping`, picker dialogs, and active editing, producing UI jitter and misleading status transitions. **Why it happens:** A fixed interval poller is simple to add in brownfield code, but runtime workspaces need context-aware refresh rules. **Consequences:** Status flashes between stale and optimistic values, controls disable unexpectedly, and users mistrust whether the runtime is actually ready. **Warning signs:** - Same poll interval regardless of `starting`, `ready`, `stopping`, or drawer-open state - Snapshot refresh racing against optimistic local status updates - Intermittent test failures tied to timers **Prevention:** - Make refresh policy state-aware: pause or downshift polling while editing, during modal OS dialogs, and while a command is in flight - Prefer explicit revalidation on operation completion over constant polling during transitions - Distinguish optimistic local operation state from confirmed backend snapshot state **Phase should address it:** Runtime observability and refresh-policy phase **Detection:** Timer-based tests covering idle, editing, starting, stopping, and degraded states. ### Pitfall 7: Unsaved-change boundaries stop at Save/Cancel and ignore close/navigation paths **What goes wrong:** Closing the drawer, switching surfaces, or triggering a mode change drops dirty edits without warning. **Why it happens:** Brownfield teams add explicit Save buttons first, then forget every other exit path. **Consequences:** Data loss reports continue even after form-state bugs are "fixed." **Warning signs:** - Dirty tracking exists only to enable/disable the save button - Drawer close/backdrop click has no unsaved-change interception - Bug reports say "I never clicked cancel" **Prevention:** - Centralize dirty-state tracking and route all dismiss/navigation actions through it - Prompt on destructive exits; allow explicit discard - Suppress the warning only for confirmed save completion or explicit cancel **Phase should address it:** Draft-safety and navigation-guard phase **Detection:** Interaction tests for backdrop close, Escape, mode switch, and app-surface navigation with dirty drafts. ## Minor Pitfalls ### Pitfall 8: Brownfield tests cover happy-path flows but not interleavings **What goes wrong:** The app looks correct in straightforward tests yet still fails under timer ticks, stale async responses, and cross-surface transitions. **Why it happens:** Brownfield teams usually add tests for save/start success, but not for overlapping events. Current tests already cover important happy paths and degraded polling, but not the full draft-vs-polling conflict space. **Consequences:** Regressions return during refactors because the highest-risk state interleavings remain unpinned. **Warning signs:** - Tests assert final text only, not intermediate states - No fake-timer coverage for edits during polling - No tests for stale async completion after newer user input **Prevention:** - Add interleaving-focused tests: edit + poll, pick folder + rescan + save, save + start + poll, mode toggle + stale search result - Treat runtime workspace as a small state machine and test transitions, not just screens - Keep fixtures for degraded health, missing binary, dirty draft, and mode-switch edge cases **Phase should address it:** QA/regression phase, but test scaffolding should begin in the first state-model phase **Detection:** Red-team test matrix covering timing, cancellation, and multi-action overlap. ## Phase-Specific Warnings | Phase Topic | Likely Pitfall | Mitigation | |-------------|---------------|------------| | Workspace state model | Polling clobbers drafts | Split live snapshot, saved baseline, and editable draft before changing UI layout | | Runtime control orchestration | Save/start/stop commands race or double-fire | Introduce explicit command state machine and per-operation statuses | | Settings IA and mode switching | Local/remote values leak or vanish | Use mode-scoped draft slices and explicit mode-commit behavior | | Async model discovery | Stale scans/searches overwrite newer intent | Use request IDs/abort logic and apply only matching results | | Error handling | One banner hides multiple failure sources | Separate validation, save, operation, and health error channels | | Refresh policy | Polling during edits and transitions causes jitter | Make polling context-aware and favor event-driven revalidation | | QA/regression | Happy-path tests miss timing bugs | Add interleaving tests with fake timers and stale-response scenarios | ## Sources - Project context and current architecture: `D:\Documents\Github\ForgeLM\.planning\PROJECT.md`, `D:\Documents\Github\ForgeLM\.planning\codebase\ARCHITECTURE.md` - Current runtime implementation: `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/components/RuntimeSurface.test.tsx` - React official docs, "You Might Not Need an Effect" (official, high confidence): `https://react.dev/learn/you-might-not-need-an-effect` - React official docs, `useSyncExternalStore` (official, high confidence): `https://react.dev/reference/react/useSyncExternalStore` - React official docs, "Preserving and Resetting State" (official, high confidence): `https://react.dev/learn/preserving-and-resetting-state` - Ecosystem evidence for recurring unsaved-change and race-condition bugs (community/GitHub, medium confidence): - `https://github.com/calcom/cal.com/issues/28179` - `https://github.com/calcom/cal.com/pull/22485` - `https://github.com/formbricks/formbricks/pull/6199` - `https://github.com/TanStack/form/issues/1457` - `https://github.com/payloadcms/payload/issues/12691`