Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,31 +99,11 @@ For project goals, see [GOALS.md](./GOALS.md). For completed work, see [DONE.md]

- [ ] **Extract `useSwipeNav` hook + `lib/clipboard.js`.** `MediaLightbox` swipe nav; clipboard inlined across 8+ call sites. Clipboard can move now.
- [ ] **Route `MediaLightbox` settings drawer through `components/Drawer.jsx`.** Reconcile `Drawer`'s flat Esc handler with the lightbox's layered Escape cascade.
- [ ] **Extract `<ModelSelect>` component for the active+Legacy optgroup pattern.** `VideoGen.jsx` + `CreativeDirector.jsx` render identical blocks differing only in `m.name` vs `m.name || m.id`.
- [ ] **Extract `mockPathsDataRoot()` test helper.** 6 test files open with byte-identical PATHS mocking setup.
- [ ] **`useAsyncAction` post-unmount setState guard.** Add `mountedRef` to gate `setRunning(false)`. YAGNI today; do at 4th consumer.
- [ ] **Extract `CollectionPickerShell` from `AddToCollectionMenu` + `BulkTargetPicker`.** Two near-identical portal popovers. While extracting, accept a `collections` prop so `MediaCollectionDetail` avoids per-mount `listMediaCollections` round-trip.
- [ ] **Server-side bulk endpoint for collection items.** `POST /api/media/collections/:id/items/bulk` taking `{ add, remove }` — single read-modify-write per collection. Halves wall-clock for Move; dodges N-call race window. Real use case: "select all" on world-builder collections with 50+ items.
- [ ] **Drop legacy `description` fallback in `sanitizeCharacter`.** Migrate `series.characters[].description` → `physicalDescription`; drop `|| raw.description` at `storyBible.js:246`.
- [ ] **Migrate `worldBuilderRefine.runRefine` onto `runPromptThroughProvider`.** Last LLM-runner site still hand-rolling the createRun → branch → executeApi/CliRun → accumulate-text pattern. ~30 LOC dedup.
- [ ] **Extract `usePersistedState` hook.** Six components repeat `useState(() => localStorage.getItem(KEY) === '1')` + setter. Add `useLocalStorageBool(key, default)` + JSON-blob variant.
- [ ] **Lift `runFfmpegProcess({ args, signal, stderrTailBytes }) → { ok, reason }` into `server/lib/ffmpeg.js`.** Three sites share spawn → stderr-tail → close → SIGTERM-on-abort. Leave `videoTimeline/local.js` (broadcast complicates it).
- [ ] **Scene-level wardrobe picking.** Per-scene `characterAppearances: [{ characterId, wardrobeId? }]` on storyboard scenes with wardrobe-picker dropdown. Decide first: does the extractor guess or does the user pick? Append wardrobe after physicalDescription vs substitute body fields?
- [ ] **Extract `sanitizeListWith(raw, sanitizer, cap)` helper in `storyBible.js`.** Three array-walk + per-item sanitize + cap sites.
- [ ] **Extract `useCanonPatch(universe, setUniverse, universeId, mountedRef)`.** `UniverseCanon.jsx` + `NounsStage.jsx` 95% identical optimistic-patch handlers. Extract when a 3rd caller appears.
- [ ] **AbortSignal listener cleanup in `audioMux.js#runFfmpeg`.** `addEventListener('abort', kill, { once: true })` never removed on normal completion. Theoretical until the stitch step passes a signal.
- [ ] **Extract `listDirectoryByExtension(dir, { extensions, mapEntry })`.** Three readdir + filter + stat-per-entry sites in `fileUtils.js`.
- [ ] **Teach `request()` in `apiCore.js` about FormData.** Drop hard-coded `Content-Type: application/json` when body is FormData. Two helpers (`apiHealth#uploadAppleHealthXml`, `apiPipeline#uploadPipelineMusicTrack`) bypass `request()` today.
- [ ] **`mergeVariations` NPE guard in `UniverseBuilder.jsx`.** Add `.label?.toLowerCase()` + `.filter(Boolean)` parity to the locked variations Set (post-rename file/line drift — agent confirmed line 57 still lacks the guard).
- [ ] **Client tests for deep routing + drag.** Smoke tests for `goToWorld(id)` URL transitions and chip-reorder ordering (mock `useSortable`).
- [ ] **`useMediaAnnotations.getCardProps(key)` helper.** Single `{ starred, hasNote, onToggleStar }` lookup at 6 sites across 4 gallery pages.
- [ ] **Cache `resolveGlobalDisplayName()` settings read.** Memoize for ~30s or invalidate on `settings:updated` event.
- [ ] **Shallow-equal guard in `useMediaAnnotations` socket handler.** Speculative micro-opt; theoretical until observed.
- [ ] **Hoist identity lookup out of `liftLegacyEntry` loop in `mediaAnnotations.js#readAll()`.** Pass `localInstanceId`, `defaultAuthorName` in once; make `liftLegacyEntry` synchronous.
- [ ] **Bulk reassign helper to collapse N+1 writes in `deleteSeason`.** Add `issuesSvc.bulkReassignSeason(seriesId, fromSeasonId, toSeasonId)` — one readState + N in-memory mutations + one writeState + one renumber.
- [ ] **Extract shared task block + review-loop section in `agentPromptBuilder.js`.** The light path (`buildLightContextPrompt` ~line 510) and full path (`buildAgentPrompt` fallback ~line 425) now emit nearly identical task blocks (`description` + optional `**Target App**:` + optional `**Screenshots**:`). The review-loop follow-up section is duplicated between the full-context block (~line 312-343) and the light-context block (~line 575-602) — same 7-step loop, same `gh pr merge --squash --delete-branch` command, same `gh pr view --json state` verification, same "10 iterations" hard stop. The auto-merge bug fix (PR removing `--auto`) had to be applied in both places. Extract `buildTaskBlock(task, { screenshotsAsList })` + `buildReviewLoopFollowUpSection(metadata, { verbose })` so the next sync edit doesn't drift.
- [ ] **Flatten the giant `isReadOnly ? … : isTui ? … : worktreeInfo && willOpenPR ? …` ternary in `agentPromptBuilder.js` Guidelines bullet (~line 458).** Pre-existing 4-branch one-liner that covers readOnly/TUI/worktree+PR/worktree-only/default-empty. Extract a `buildCompletionGuidelineBullet({ isReadOnly, isTui, tuiCompletionCommand, worktreeInfo, willOpenPR, willReviewLoop })` helper mirroring the pattern the light path already uses (`worktreeCommitGuidance`, `buildTuiCompletionSection`). Returns string or `null`.
- [ ] **Test the untested `worktreeCommitGuidance` branches in `agentPromptBuilder.js`.** Five-branch decision tree at ~line 617; tests don't cover `isWorktreeOnExistingBranch=true` or `hasSlashdo && !willOpenPR`.
- [ ] **Extract a `tryReadFile(path, encoding='utf8')` helper into `server/lib/fileUtils.js`.** The `readFile(path).catch(() => null)` pattern is inlined 15+ times across the codebase (`server/routes/apps.js:38` `safeReadJson`, `server/lib/fileUtils.js:505`, `server/lib/hfToken.js:16`, `server/services/agentDrafts.js:22`, `server/services/messageAccounts.js:10`, `server/services/missions.js:35`, `server/lib/tuiPromptRunner.js` new at line ~202, etc.). One helper + migrate; small win, prevents future drift. Defer until next infra pass.
- [ ] **Skip the `text` accumulator in `promptRunner.runPromptThroughProvider` for TUI providers.** `APPEND_CHUNK(acc, chunk)` runs per stream chunk regardless of provider type, but the TUI branch (post `fix-prose-stage-override`) discards the accumulated `text` and uses `result.text` from `executeTuiRun` instead. Streams can hit hundreds of KB of screen redraws per run — gate the accumulator on `effectiveProvider.type !== 'tui'` in `onData` to skip the per-chunk string concat. Pre-existing buffer cap on `outputBuffer` inside `executeTuiRun` already bounds memory; this is a CPU/alloc micro-opt, not a correctness fix.

Expand Down
34 changes: 34 additions & 0 deletions client/src/components/ModelSelect.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Active + Legacy optgroup `<select>` shared by media-gen pages (VideoGen,
// CreativeDirector). Splits `models` into active and `m.deprecated === true`
// groups; the Legacy optgroup only renders when at least one deprecated model
// exists. `getLabel` picks the option text — defaults to `m.name`; callers
// whose model shape may omit `name` pass `(m) => m.name || m.id`.
const defaultGetLabel = (m) => m.name;
export default function ModelSelect({
models,
value,
onChange,
getLabel = defaultGetLabel,
id,
className = 'w-full bg-port-bg border border-port-border rounded-lg px-2 py-2 text-sm text-white focus:outline-none focus:border-port-accent disabled:opacity-50',
disabled = false,
}) {
const active = models.filter((m) => !m.deprecated);
const legacy = models.filter((m) => m.deprecated);
return (
<select
id={id}
value={value}
onChange={onChange}
disabled={disabled}
className={className}
>
{active.map((m) => <option key={m.id} value={m.id}>{getLabel(m)}</option>)}
{legacy.length > 0 && (
<optgroup label="Legacy">
{legacy.map((m) => <option key={m.id} value={m.id}>{getLabel(m)}</option>)}
</optgroup>
)}
</select>
);
}
4 changes: 3 additions & 1 deletion client/src/components/meatspace/tabs/SettingsTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ export default function SettingsTab({ onRefresh }) {
setXmlProgress(0);
setXmlImporting(true);

api.uploadAppleHealthXml(file).catch(err => {
// Owns its own error UI (xmlError) so suppress the helper's toast — per
// CLAUDE.md "custom catch ⇒ silent: true" convention.
api.uploadAppleHealthXml(file, { silent: true }).catch(err => {
setXmlError(err.message);
setXmlImporting(false);
});
Expand Down
Loading