Files

186 lines
13 KiB
Markdown

# Technology Stack
**Project:** AI Backend Studio - runtime workspace UX and settings reliability
**Researched:** 2026-03-28
**Scope:** Brownfield runtime workspace/settings flow inside the existing Tauri + React + TypeScript desktop app
## Recommended Stack
### Core Framework
| Technology | Version | Purpose | Why | Confidence |
|------------|---------|---------|-----|------------|
| React | 18.x (keep current) | Existing desktop UI runtime surface | The app already uses React 18 in `tauri-app/package.json`. This milestone is a state-boundary problem, not a framework problem, so upgrading React is unnecessary churn. | HIGH |
| Tauri | 2.x (keep current) | Desktop shell, commands, file dialogs, runtime bridge | The current app already uses Tauri 2. The official Tauri 2 guidance supports both `invoke` commands and frontend event listeners. For this milestone, commands plus scoped polling are the lowest-risk fit; events can be added only for start/stop/status nudges later. | HIGH |
| TypeScript | 5.5+ (keep current) | Typed runtime snapshot, config draft, and mutation contracts | The bug is fundamentally about mixing live server state with editable client state. Strong, explicit types across `RuntimeSnapshot`, `RuntimeConfigDraft`, and mutation payloads reduce accidental overwrite paths. | HIGH |
### State and Form Layer
| Technology | Version | Purpose | Why | Confidence |
|------------|---------|---------|-----|------------|
| `@tanstack/react-query` | 5.x | Own live runtime snapshot polling, cache, refetch policy, and save/start/stop mutations | In 2025, TanStack Query is the standard React choice for server-state synchronization. Its model matches this exact problem: runtime snapshot is server state, while unsaved settings are client draft state. Use it to poll `RuntimeSnapshot` and invalidate on save/start/stop instead of hand-rolled `setInterval` state wiring in `tauri-app/src/features/runtime/useRuntimeManager.ts`. | HIGH |
| `react-hook-form` | 7.x | Own editable settings draft and dirty tracking inside the settings surface | React Hook Form is the pragmatic brownfield default for settings forms because it minimizes rerenders, supports async defaults, and supports `reset` / `values` with `keepDirtyValues`. That makes it a strong fit for "refresh live state without clobbering dirty inputs". | HIGH |
| `zod` | 4.x | Runtime-safe config schema for draft validation and payload parsing | Zod gives one schema for parsing persisted config/snapshot payloads and validating form submissions, which is valuable in a Tauri app where Rust commands return external data into the frontend. Use it at the boundary, not everywhere. | HIGH |
| `@hookform/resolvers` | 3.x | Connect Zod schema to React Hook Form | This is the standard bridge for schema-backed React Hook Form validation and keeps validation logic out of ad hoc component effects. | HIGH |
### Tauri Communication Pattern
| Technology | Version | Purpose | Why | Confidence |
|------------|---------|---------|-----|------------|
| Tauri commands via `invoke` | Existing | Fetch snapshot, save config, start runtime, stop runtime | Commands already exist and are the right brownfield default for request/response actions. Keep them as the authoritative mutation boundary. | HIGH |
| Tauri frontend events via `@tauri-apps/api/event` | Existing optional capability | Optional push updates for status transitions only | Tauri events are suitable for small status messages, but the docs explicitly note they are not meant for low-latency or high-throughput streaming. That makes them useful as a later enhancement for process state transitions, but not necessary to solve the draft-overwrite bug right now. | MEDIUM |
## Brownfield Implementation Approach
### 1. Split live snapshot state from draft settings state
Use two explicit models in the frontend:
- `RuntimeSnapshotQuery` - the live polled backend state (`status`, `ready`, `process_running`, `resolved_url`, `last_error`, saved `config`)
- `RuntimeConfigDraft` - the unsaved user-editable form state that only exists while the settings UI is open
Why:
- The current bug exists because `RuntimeSettingsDrawer.tsx` rehydrates local input state from `snapshot.config` in an effect keyed on `[open, snapshot]`, so any poll can rewrite draft inputs.
- Server state and form draft state have different lifecycles and should not share the same source of truth.
### 2. Move polling into TanStack Query and stop mirroring it into form state
Recommended query shape:
```ts
useQuery({
queryKey: ["runtime", "snapshot"],
queryFn: getRuntimeSnapshot,
refetchInterval: 3000,
refetchOnWindowFocus: false,
staleTime: 1000,
})
```
Use mutations for:
- `saveRuntimeConfig`
- `startRuntime`
- `stopRuntime`
After mutation success, invalidate or directly update the `runtime/snapshot` query.
Why:
- Query owns live synchronization concerns better than a custom `useEffect` + `setInterval` hook.
- Per-query refetch controls let the runtime snapshot keep polling without coupling that refresh cycle to the settings draft.
- This is a local, focused dependency addition, not an architectural rewrite.
### 3. Initialize the settings form once per editing session
Recommended pattern with React Hook Form:
- On drawer open, map `snapshot.config` into `defaultValues`
- While the form is pristine, allow controlled resets from fresher saved config
- Once the form is dirty, do not overwrite dirty fields from polling
- On explicit `Cancel`, reset from the latest saved snapshot
- On successful `Save`, reset the form to the newly saved config
Two safe brownfield patterns:
1. **Preferred:** initialize `useForm({ defaultValues, resolver })` when the settings panel opens and call `reset(savedConfig)` only on open, cancel, or successful save.
2. **If external updates must appear while editing:** use `values` plus `resetOptions: { keepDirtyValues: true }` so untouched fields can refresh without clobbering edited ones.
Why:
- This matches React Hook Form's documented async/default/reset model.
- It solves the exact overwrite failure without inventing a custom dirty-field reconciliation system first.
### 4. Keep runtime status visible outside the form, but move actionable runtime management into Runtime Settings
Recommended UI/state split:
- Keep a lightweight runtime summary card in the workspace for at-a-glance status
- Move authoritative runtime controls and editable configuration into one runtime settings surface
- Treat the summary card as read-mostly and query-backed
- Treat the settings surface as draft-backed and mutation-backed
Why:
- The current split between `RuntimeStatusCard.tsx` and `RuntimeSettingsDrawer.tsx` duplicates control responsibility.
- A thin summary + authoritative settings surface is the common brownfield cleanup because it reduces duplicate actions without removing useful status visibility.
### 5. Add schema parsing at the frontend boundary, not a full new domain layer
Use Zod for:
- parsing the runtime snapshot returned from Tauri before it enters UI state
- validating the settings payload before save
- normalizing fields like URL trimming, nullable paths, and numeric ranges in one place
Do **not** build a large new frontend domain framework for this milestone.
Why:
- This app already has working runtime commands and types.
- The brownfield win is better contracts and form discipline, not a new architecture stack.
## Supporting Libraries
| Library | Version | Purpose | When to Use | Confidence |
|---------|---------|---------|-------------|------------|
| `@tanstack/react-query-devtools` | 5.x | Debug query cache and refetch behavior in development | Dev-only while stabilizing polling/mutation interactions; not required for production. | MEDIUM |
| `immer` | 10.x | Optional helper for reducer-based draft transforms | Only if the form keeps some nested local helper state outside React Hook Form and updates become noisy. Skip if not needed. | LOW |
## What NOT to Introduce
| Category | Avoid | Why Not |
|----------|-------|---------|
| Global client store | Redux / Zustand for this milestone | The problem is not app-wide client state explosion; it is confusion between server snapshot state and local draft state in one feature. TanStack Query + form-local state is smaller and more standard here. |
| Workflow/state machine framework | XState | The runtime lifecycle is already implemented in Rust/Tauri. Adding a frontend state machine now increases modeling overhead without directly solving the overwrite bug. |
| New form stack | TanStack Form / Formik / custom reducer form rewrite | React Hook Form is the lower-risk brownfield option with mature dirty tracking and async reset behavior. Switching to a less-established or heavier form stack adds migration cost with little benefit. |
| Real-time transport rewrite | Replacing polling with channels/events everywhere | Tauri events/channels are useful, but this bug is caused by how polling results are applied, not by polling itself. Fix the state boundary first; consider event-assisted refresh later if UX still needs faster transitions. |
| Design system rewrite | shadcn/ui, MUI, or CSS framework migration | The milestone is reliability and IA cleanup, not component-system replacement. Keep the existing CSS and components unless a very small extraction helps. |
## Alternatives Considered
| Category | Recommended | Alternative | Why Not |
|----------|-------------|-------------|---------|
| Live server state | TanStack Query 5 | Keep custom polling in `useRuntimeManager` | The current custom hook already caused accidental coupling between refetches and form resets. Query gives better separation, cache semantics, and mutation invalidation. |
| Draft form state | React Hook Form 7 | Component-local `useState` for every field | The current drawer already shows the maintenance cost of hand-managed field state and effect-based rehydration. RHF adds dirty tracking and controlled reset semantics with less code. |
| Validation | Zod 4 | TypeScript types only | TypeScript does not validate runtime payloads from Tauri commands, local storage, or persisted config. This boundary benefits from runtime parsing. |
| Live updates | Polling + optional event nudges | Tauri event-first rewrite | Event-first can help later, but brownfield reliability improves fastest by separating snapshot and draft state while keeping the current command model. |
## Installation
```bash
# Core additions
npm install @tanstack/react-query react-hook-form zod @hookform/resolvers
# Optional dev tooling
npm install -D @tanstack/react-query-devtools
```
## Implementation Notes for This Repo
- Replace the hand-rolled polling lifecycle in `tauri-app/src/features/runtime/useRuntimeManager.ts` with a query-backed runtime hook.
- Stop using `[open, snapshot]` as a blanket form-reset trigger in `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`.
- Keep `RuntimeSnapshot` as the saved/live shape from the backend, but introduce a frontend-only `RuntimeConfigDraft` mapper instead of editing `snapshot.config` by implication.
- Keep save/start/stop in the existing Tauri command layer in `tauri-app/src/features/runtime/api.ts`; do not move business logic into the React components.
- Keep `RuntimeStatusCard.tsx` query-backed and read-mostly; move the authoritative editing and runtime-management flow into the runtime settings surface.
## Recommendation Summary
- **Recommended default stack:** existing React 18 + Tauri 2 + TypeScript 5.5, plus `@tanstack/react-query`, `react-hook-form`, `zod`, and `@hookform/resolvers`
- **Recommended implementation:** poll live runtime snapshot through Query, edit config through a separate RHF draft, and only reconcile on open/save/cancel or through `keepDirtyValues`
- **Recommended scope discipline:** do not introduce a global store, state machine framework, design system rewrite, or event-stream rewrite for this milestone
## Sources
- Local project context: `.planning/PROJECT.md`
- Local architecture: `.planning/codebase/ARCHITECTURE.md`
- Local stack: `.planning/codebase/STACK.md`
- Current runtime polling implementation: `tauri-app/src/features/runtime/useRuntimeManager.ts`
- Current settings reset behavior: `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`
- Current runtime API boundary: `tauri-app/src/features/runtime/api.ts`
- React Hook Form `useForm` docs: https://react-hook-form.com/docs/useform
- React Hook Form FAQ / async defaults and reactive values: https://react-hook-form.com/faqs
- TanStack Query React `useQuery` reference: https://tanstack.com/query/latest/docs/framework/react/reference/useQuery
- TanStack Query window focus refetching guide: https://tanstack.com/query/latest/docs/framework/react/guides/window-focus-refetching
- TanStack Query guidance on server state vs client state: Context7 `/tanstack/query`
- Zod docs: https://zod.dev/
- Tauri 2 frontend/backed communication docs: https://v2.tauri.app/develop/calling-frontend/