openvisengine/agent-ctx/10-presets-ui.md

5 KiB
Raw Blame History

Task 10 — Presets UI (folder/file tree manager + Script tab save targets)

Task ID: 10 Agent: full-stack-developer (Presets UI) Status: COMPLETE — lint clean, tsc clean on touched files, verified end-to-end in a headless browser.

What was built

1. src/components/ava/presets-tab.tsx — complete rewrite

Left column (58% on sm+, full width on mobile): the folder/file tree

  • Built from store.folders + store.presets. Defensive: presets whose folderPath has no Folder row synthesize their node + ancestors (buildTree). Folders sorted alphabetically per level (segment-wise compare, parents before children); presets alphabetically inside folders.
  • Folder row: chevron button (size-9 hit area, rotates ChevronRight) is the ONLY expand/collapse control; clicking the row selects the folder (selectFolder — also updates the randomize scope). FolderOpen/Folder icon swaps with expansion. Subtree preset count badge (font-mono, tooltip "N presets in this folder and below").
  • Default expansion: expandedFolders[path] ?? path.split('/').length <= 2 (same rule as the store) — root, root/embedded, root/user created open.
  • Preset row: FileText icon; single click selects, double-click loads. Running preset (activePresetId): emerald Play icon + emerald-300 name + bg-emerald-400/10. Selected row: emerald left border + bg-zinc-800/70.
  • originalFile rows get a .avs download anchor (title "Download original Winamp .avs"), always visible on mobile, hover-visible on sm+.
  • Empty folders show an italic "(empty)" hint row. Tree body scrolls (max-h-[420px] lg:max-h-[480px], thinScrollbar). role="tree" / treeitem / group, aria-expanded/selected/level throughout.

Right column (42%): context-sensitive management panel

  • Header: icon + name + font-mono full path (+ "N presets · M subfolders" for folders).
  • Preset context: LOAD PRESET (emerald; applyScriptText → toast → setActivePresetId + setEditTarget; compile error → destructive toast + switch to script tab), RENAME, DELETE.
  • Folder context: CREATE PRESET (POST STARTER_TEMPLATE → refresh → editTarget + editor + engine + select + setTab('script') + toast "created — now edit and Save"), NEW SUBFOLDER (POST → expand parent + select new), RENAME (PATCH folder / PUT preset; selection, randomScope and editTarget follow the renamed paths), DELETE (AlertDialog with client-side blast radius — "removes N presets in M folders", empty-folder and preset variants), EXPORT .ZIP (asChild anchor to /api/presets/export?path=… with download attr; root exports everything), IMPORT .ZIP (hidden .zip input → FormData POST → summary toast with duplicates-skipped / folders-created).
  • PROTECTED_FOLDERS (root, root/embedded, root/user created): Rename + Delete disabled with "Protected folder" tooltip. Import/export allowed.
  • All 409/400/404/500 responses → destructive toasts with the server message. Empty names → inline dialog validation.
  • RANDOMIZE section (border-t): Checkbox + delay input (3–60 s; invalid input shows a red hint and reverts to the committed store value on blur), scope line ("Random from: <folder | all presets>") with amber "no presets in this folder yet" hint, pulsing emerald "ON · every Ns" badge while active. The timer runs in the store; the UI only wires it.
  • NameDialog is fully controlled (parent owns value/error) — this was needed to pass the react-hooks/set-state-in-effect lint rule.

2. src/components/ava/script-tab.tsx — edits

  • "Saving to: root/…/Name" chip (middle-truncated path, emerald, X button clears the target) shown while editTarget is set.
  • Save button (emerald, editTarget only): PUT /api/presets/[id] {content, author from validateScript meta} → refresh + toast Saved to "Name"; disabled while saving.
  • Save As… button (editTarget only): Dialog prefilled with current name → POST same folder → setEditTarget(new) + refresh + toast; 409 handled.
  • New button also clears the edit target.

Verification performed (headless browser + curl)

  • Tree structure/counts/expansion/selection; Load (toast, chip appears, Save PUT returns 200); Save As (copy created server-side, chip retargeted); delete preset + folder (blast-radius + empty wording); create preset (editor + tab switch); new subfolder; rename folder (paths follow); import .zip end-to-end; randomize persistence (localStorage ava.randomize), scope line, invalid-delay revert, ON badge in DOM with pulse animation. Zero page/console errors.
  • Library restored to pristine state after testing (95 presets / 7 folders).

Notes for future agents

  • Do NOT re-add setState-in-effect in NameDialog — the react-hooks v6 rule forbids it; keep it controlled.
  • setRandomIntervalSec is typed (sec: number); convert with Number() before calling (the store would tolerate strings at runtime).
  • agent-browser find … dblclick does not exist — double-click was verified through the identical Load Preset code path.