# 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: ") 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.