Initial ForgeLM import
This commit is contained in:
commit
a392b9dc60
4296 files changed
+1212675
No files matched your search
@@ -0,0 +1,261 @@
|
||||
# Architecture Patterns
|
||||
|
||||
**Domain:** Runtime workspace state flow for existing Tauri + React desktop app
|
||||
**Researched:** 2026-03-28
|
||||
**Overall confidence:** MEDIUM-HIGH
|
||||
|
||||
## Recommended Architecture
|
||||
|
||||
Use a feature-scoped runtime workspace controller with **three explicit state domains** instead of one polling snapshot flowing directly into both status UI and form inputs.
|
||||
|
||||
1. **Live runtime state** - polled, read-only, owned by query state
|
||||
2. **Persisted settings state** - latest saved config from Tauri, read-only except after save success
|
||||
3. **Draft settings state** - local editable form state, owned only by the settings editor
|
||||
|
||||
That separation is the core fix. The current problem comes from `RuntimeSettingsDrawer.tsx` rehydrating local input state from `snapshot.config` on every `snapshot` change while `useRuntimeManager.ts` refreshes the whole `RuntimeSnapshot` every 3 seconds. Polling is correct for live runtime health, but it must not be the owner of editable inputs.
|
||||
|
||||
Recommended shape:
|
||||
|
||||
```text
|
||||
Tauri commands / file persistence / process state
|
||||
|
|
||||
v
|
||||
Runtime query layer (polling live snapshot + persisted config)
|
||||
|
|
||||
+--> Status/summary components (read-only)
|
||||
|
|
||||
+--> Draft controller initializer/reset source
|
||||
|
|
||||
v
|
||||
Editable settings form (local reducer/state)
|
||||
|
|
||||
v
|
||||
Save mutation
|
||||
|
|
||||
v
|
||||
update persisted config -> invalidate/refetch live query
|
||||
```
|
||||
|
||||
## Recommended Component Boundaries
|
||||
|
||||
| Component | Responsibility | Communicates With |
|
||||
|-----------|---------------|-------------------|
|
||||
| `RuntimeWorkspaceProvider` | Feature root; composes live query, command mutations, and draft provider | Tauri API adapter, child panels |
|
||||
| `useRuntimeWorkspaceQuery` | Polls runtime status and loads latest persisted config; exposes `data`, `isFetching`, `refetch` | `api.ts`, query cache |
|
||||
| `useRuntimeCommands` | Starts, stops, saves, rescans; serializes mutations and invalidates/refetches workspace query after success | Tauri commands, query cache |
|
||||
| `RuntimeStatusPanel` | Renders runtime readiness, last error, endpoint, process state, restart-required banners | live query only |
|
||||
| `RuntimeSettingsDraftProvider` | Owns draft settings, dirty fields, reset/cancel, stale-baseline detection | workspace query, settings form |
|
||||
| `RuntimeSettingsForm` | Pure controlled inputs for local/remote settings | draft provider only |
|
||||
| `RuntimeModelLibraryPanel` | Reads managed library, scanned models, search results; writes selections into draft only | aux queries, draft provider |
|
||||
| `RuntimePrimaryActions` | Save, cancel, start, stop, save-and-start logic; decides enablement from live state + draft state | commands, live query, draft provider |
|
||||
|
||||
## Data Model Recommendation
|
||||
|
||||
Do not let the frontend treat one `RuntimeSnapshot` object as the source of truth for everything. Normalize it into these view models inside the runtime feature:
|
||||
|
||||
### 1. Live Snapshot
|
||||
|
||||
Read-only runtime state used for polling-sensitive UI.
|
||||
|
||||
```ts
|
||||
type LiveRuntimeSnapshot = {
|
||||
status: RuntimeStatus;
|
||||
ready: boolean;
|
||||
processRunning: boolean;
|
||||
resolvedUrl: string;
|
||||
binaryPath: string | null;
|
||||
lastError: RuntimeErrorInfo | null;
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Persisted Config Baseline
|
||||
|
||||
Latest saved config returned from Tauri. This is the reset target for the form.
|
||||
|
||||
```ts
|
||||
type PersistedRuntimeConfig = RuntimeConfig & {
|
||||
revision: number | string;
|
||||
};
|
||||
```
|
||||
|
||||
If possible, add a monotonic `revision` or `updated_at` field on the Tauri side. Without it, the frontend can still compare deep equality, but a revision is cleaner for stale-draft detection and cancel/reset behavior.
|
||||
|
||||
### 3. Draft Config
|
||||
|
||||
Local editable state that never changes because polling ran.
|
||||
|
||||
```ts
|
||||
type RuntimeDraftState = {
|
||||
values: RuntimeConfig;
|
||||
dirtyFields: Set<string>;
|
||||
initializedFromRevision: number | string | null;
|
||||
saveState: "idle" | "saving" | "error";
|
||||
};
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Polling Flow: live snapshot vs draft settings
|
||||
|
||||
1. `useRuntimeWorkspaceQuery` bootstraps from Tauri and continues polling on an interval.
|
||||
2. Poll responses update **live snapshot** immediately.
|
||||
3. Poll responses update **persisted config baseline** only when the returned config revision changed.
|
||||
4. `RuntimeStatusPanel` rerenders from live snapshot on every poll.
|
||||
5. `RuntimeSettingsDraftProvider` does **not** overwrite draft values during normal polling.
|
||||
6. If the baseline revision changes while the draft is dirty, show a non-destructive "saved settings changed" notice with actions: `Reload saved`, `Keep editing`.
|
||||
|
||||
This matches React guidance: controlled inputs should stay owned by local state, and state should not be repeatedly reset from props/effects unless the user is intentionally switching context.
|
||||
|
||||
### Open / Close Drawer Flow
|
||||
|
||||
1. Opening settings initializes draft from the latest persisted config baseline.
|
||||
2. While the drawer stays open, the draft is the only owner of form fields.
|
||||
3. Closing with cancel discards the draft and clears dirty state.
|
||||
4. Reopening creates a fresh draft from the current persisted baseline.
|
||||
|
||||
If the app keeps the editor mounted, key the draft provider by `baseline.revision` only when the user explicitly resets/cancels. Do not key it by every poll result.
|
||||
|
||||
### Save Flow
|
||||
|
||||
1. User edits draft values locally.
|
||||
2. `saveRuntimeConfig` mutation sends the full draft payload.
|
||||
3. On success, mutation updates the persisted baseline from the response and clears dirty state.
|
||||
4. Then invalidate/refetch the live workspace query.
|
||||
5. Status components pick up any restart-required or readiness change from the fresh live snapshot.
|
||||
|
||||
If the response already contains the updated snapshot/config, update query cache directly first and refetch after only when needed. That follows TanStack Query guidance: mutations own writes; queries are refreshed by invalidation or cache updates.
|
||||
|
||||
### Start / Stop Flow
|
||||
|
||||
1. Start/stop actions never mutate draft state directly.
|
||||
2. `Start` uses the latest persisted config, not unsaved inputs.
|
||||
3. `Save and start` is a composed action: `save draft -> if success and local mode valid -> start runtime -> refetch live snapshot`.
|
||||
4. `Stop` affects live runtime state only, then refetches.
|
||||
|
||||
That prevents ambiguous ownership such as starting from a half-edited form that the backend has not persisted yet.
|
||||
|
||||
### Auxiliary Query Flow
|
||||
|
||||
GPU info, model library contents, scanned folders, and Hugging Face search results should be separate queries from the workspace snapshot.
|
||||
|
||||
- They are not core runtime status.
|
||||
- Their failures should not poison runtime readiness state.
|
||||
- Their data should write into the draft only through explicit user actions like selecting a model or choosing a folder.
|
||||
|
||||
## Patterns to Follow
|
||||
|
||||
### Pattern 1: Server state vs client draft split
|
||||
|
||||
**What:** Treat polling data as server/external state and editable fields as client state.
|
||||
|
||||
**When:** Always, for runtime settings forms that stay open while health/status continues changing.
|
||||
|
||||
**Why:** It removes the current overwrite bug at the ownership boundary rather than patching around it with conditional `useEffect` logic.
|
||||
|
||||
### Pattern 2: Feature-local state, not app-global store
|
||||
|
||||
**What:** Keep runtime workspace state inside a dedicated feature provider or feature hooks instead of introducing a broad app-wide store.
|
||||
|
||||
**When:** Brownfield restructure limited to runtime workspace.
|
||||
|
||||
**Why:** The existing app does not use a global frontend store. This change is a feature-boundary problem, not a whole-app state-management problem.
|
||||
|
||||
### Pattern 3: Query/mutation orchestration for Tauri commands
|
||||
|
||||
**What:** Model polling reads as queries and start/stop/save as mutations, even though transport is `invoke()` instead of HTTP.
|
||||
|
||||
**When:** Any operation that returns asynchronous backend state and needs post-write refresh.
|
||||
|
||||
**Why:** It gives explicit ownership for polling, mutation in-flight state, invalidation, and optimistic UI guards.
|
||||
|
||||
### Pattern 4: Separate read model from write model
|
||||
|
||||
**What:** Prefer a workspace response shaped like:
|
||||
|
||||
```ts
|
||||
type RuntimeWorkspaceResponse = {
|
||||
live: LiveRuntimeSnapshot;
|
||||
persistedConfig: PersistedRuntimeConfig;
|
||||
};
|
||||
```
|
||||
|
||||
**When:** As soon as the team touches the Tauri contract.
|
||||
|
||||
**Why:** Nesting writable config inside the polled runtime snapshot is what makes accidental rehydration easy.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### Anti-Pattern 1: Rehydrating the form from every snapshot refresh
|
||||
|
||||
**What:** `useEffect(() => setForm(snapshot.config), [snapshot])`
|
||||
|
||||
**Why bad:** Polling becomes an uncontrolled reset mechanism that discards in-progress edits, folder picks, and model selections.
|
||||
|
||||
**Instead:** Initialize draft once per open/reset or once per confirmed baseline revision change.
|
||||
|
||||
### Anti-Pattern 2: One `busy` flag for unrelated operations
|
||||
|
||||
**What:** A single boolean covering save, start, stop, scan, and search.
|
||||
|
||||
**Why bad:** It couples unrelated interactions and makes action enablement ambiguous.
|
||||
|
||||
**Instead:** Track mutation states independently: `isSaving`, `isStarting`, `isStopping`, `isScanning`, `isSearchingHub`.
|
||||
|
||||
### Anti-Pattern 3: Letting runtime actions read unsaved form state implicitly
|
||||
|
||||
**What:** Start/stop logic reaching directly into form inputs.
|
||||
|
||||
**Why bad:** Operators cannot tell whether the runtime is using saved config or draft config.
|
||||
|
||||
**Instead:** Runtime operations use persisted config; composed actions explicitly save first.
|
||||
|
||||
## Scalability Considerations
|
||||
|
||||
| Concern | At current scope | As runtime surface grows |
|
||||
|---------|------------------|---------------------------|
|
||||
| Polling frequency | 3-second polling is acceptable | Move to adaptive polling by state: faster while starting, slower while stopped/remote |
|
||||
| Mutation contention | Manual guarding is sufficient | Add serialized command queue to prevent overlapping start/stop/save races |
|
||||
| Contract clarity | Frontend normalization can bridge current shape | Introduce dedicated workspace DTO with `live` + `persistedConfig` + `revision` |
|
||||
| UI complexity | Feature-local provider is enough | Split drawer into tabs/sections without changing ownership model |
|
||||
|
||||
## Suggested Build Order And Dependency Implications
|
||||
|
||||
1. **Normalize the runtime workspace contract**
|
||||
- Introduce frontend selectors/types for `live snapshot`, `persisted baseline`, and `draft` even if the backend still returns the old combined payload.
|
||||
- Dependency implication: everything else should build on this split, not on raw `RuntimeSnapshot`.
|
||||
|
||||
2. **Extract query/mutation hooks from `useRuntimeManager`**
|
||||
- Create distinct hooks for workspace polling and runtime commands.
|
||||
- Dependency implication: status panel and action bar can migrate first without touching the form internals.
|
||||
|
||||
3. **Add the draft provider and migrate `RuntimeSettingsDrawer` to controlled draft state**
|
||||
- Replace `useEffect` rehydration on every `snapshot` change with explicit `initialize/reset/cancel` semantics.
|
||||
- Dependency implication: this is the critical prerequisite for reliable save/cancel behavior.
|
||||
|
||||
4. **Refactor status and actions into read-only/live consumers**
|
||||
- `RuntimeStatusCard` and any future summary header should read from live query only.
|
||||
- Dependency implication: once status no longer reads draft state, polling can remain aggressive without UX regressions.
|
||||
|
||||
5. **Move auxiliary lookups into separate query slices**
|
||||
- GPU, model library, model scan, and Hub search should become isolated resources.
|
||||
- Dependency implication: failures and loading states become localized instead of blocking save/start controls.
|
||||
|
||||
6. **Optionally add backend config revision support**
|
||||
- Persist and return `revision` or `updated_at` with config reads/writes.
|
||||
- Dependency implication: enables stale-draft warnings and cleaner reset rules, but can follow the frontend split if roadmap pressure is high.
|
||||
|
||||
## Architecture Recommendation For Roadmap
|
||||
|
||||
Phase the work around ownership boundaries, not around visual components:
|
||||
|
||||
1. **State split foundation** - separate live polling state from persisted config and draft state
|
||||
2. **Workspace action orchestration** - isolate save/start/stop/restart-required behavior
|
||||
3. **UI restructuring on top of stable ownership** - merge status and settings into one control center after the state model is safe
|
||||
|
||||
That order matters. If the team starts with UI rearrangement before fixing ownership, the new surface will keep the same overwrite bug under a different layout.
|
||||
|
||||
## Sources
|
||||
|
||||
- Codebase evidence: `tauri-app/src/features/runtime/useRuntimeManager.ts`, `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`, `tauri-app/src/features/runtime/components/RuntimeStatusCard.tsx`, `tauri-app/src-tauri/src/runtime/mod.rs`, `tauri-app/src-tauri/src/runtime/state.rs`
|
||||
- React official docs via Context7 (`/reactjs/react.dev`) on controlled inputs, preserving/resetting state with keys, and avoiding unnecessary effect-driven derived state - HIGH confidence
|
||||
- TanStack Query official docs via Context7 (`/tanstack/query/v5.90.3`) on polling with `refetchInterval`, mutation cache updates, and invalidation after writes - HIGH confidence
|
||||
Reference in new issue
Block a user