Tutorial: 2026-06-26

2026-06-26

Newest entry on top.

Shipped 2.7.25 (gallery + single view + provider epic) to main

"Close and ship." Merged feature/gallerydev (--no-ff), which delivered the whole stacked epic (feature/providers + the gallery/single-view work — 36 commits). First CI run on dev failed: only the Playwright visual-regression specs (topbar / sidebar / full-page snapshots), since the top-bar now has the Generate/Gallery/Single tabs and the providers+gallery UI shifted the layout — the functional + a11y specs passed. (The local format:check "182 files" scare was just the Windows CRLF working copy vs the LF committed blobs; CI on Linux is LF, so format:check was green there — nothing to commit.)

Fixed the visual-baselines.yml workflow, which had broken in the engine-split (root npm ci with no root package.json): added working-directory: engine-v3, npm install, and a repo-root-relative artifact path. Ran it on dev, downloaded the linux-visual-baselines artifact, committed the three refreshed *-chromium-linux.png baselines. CI on dev then green (check + gui + e2e).

Released PATCH dev → main (--no-ff; VERSION already 2.7.25, tag absent): release.yml cut tag v2.7.25 + the GitHub Release, CI green on main. Netlify deploys the gui from main. The Pages docs site is intentionally paused (pages.yml is workflow_dispatch only until npm run docs is reconciled to the engine-v3 split) — not deployed this release, as expected. Cleanup: deleted the spent feature/gallery + feature/providers branches (local + remote); removed the throwaway 175100000001–003 files from engine-v1-2/output; stopped the v1-2 server.

Three persistent views (Generate / Gallery / Single) — state + scroll retained

Owner wanted the views to retain state across tab switches for the session, the single-image view promoted to its own header tab (Generate · Gallery · Single), generated images to open into that full view (Back pops right back to Generate where they left off), and scroll position kept only if there's a non-hacky way.

The clean answer: keep all three views mounted the whole session and toggle visibility with a CSS class (.view-pane / .view-pane.ondisplay:none/flex). Because the scroll containers are never unmounted, React state, form inputs, and scrollTop all persist for free — no manual save/restore (which would've been the hacky route, so it's not done). Shared coordination state (items feed, gallery query, current single image, returnTo, magick) lifted into App.

  • App.jsx rewritten: lifted state, three .view-panes, the 3-tab switch, feed/magick load, and the open/navigate/delete/search handlers. go("single") lands on the last image or a random one the first time; provider picker shows only in Generate.
  • Gallery.jsx → controlled grid-only (props items/loading/query/onQueryChange/onOpen/onRefresh); the single page extracted to SingleView.jsx (standalone view; keyboard nav gated on active so the hidden view doesn't eat arrow keys; empty state when no image).
  • Home.jsx + PromptResult.jsx: a generated image now opens in the Single view via onOpenImage(path) (App resolves the served path to a feed item, refreshing the feed if needed) instead of a new tab; middle-click still opens a new tab.
  • CSS: .view-pane show/hide + generic .view-pane > * fill.

Verified: lint 0 errors, smoke, gui build, test:web 43. No VERSION bump (unreleased 2.7.25 line).

Photo gallery + per-image JSON metadata sidecars (2.7.25)

Owner asked to bring the old v1-2 image feed to v3: a header switch (upper-left) flipping between image generation and a photo gallery, with the gallery driven by JSON sidecars like v1-2 — every image in output/ paired with a .json holding the original prompt, the DPL, the AI translation, and the full provider + settings snapshot. Built it on feature/gallery (stacked off feature/providers, which holds the provider/output infra the gallery depends on — dev is 31 commits behind and lacks vite-plugin-api.js/output.js).

What changed:

  • Sidecar on save. POST /api/image (gui/vite-plugin-api.js) now accepts a meta object and writes <base>.json next to the image. Home.makeBatch assembles the record (prompt sent, promptOriginal engine roll, aiTranslation auto-fix rewrite, dpl, negative, provider, and a settings snapshot with API keys stripped) and ingestImage(src, meta) passes it through. Image delete now also unlinks the sidecar.
  • Feed. New GET /api/feed scans output/, pairs each image with its parsed sidecar, returns newest first (savedAt → mtime fallback). Tolerates a missing folder / corrupt sidecar.
  • Gallery view. Repurposed the previously-unused Gallery.jsx into the full view: masonry grid of lazy thumbnails (wide/tall by natural aspect), keyword search (searchHaystack over prompt/DPL/provider), and a click-through detail modal showing the whole sidecar with copy/open/reveal/delete. New src/lib/gallery.js + styles for .view-switch / .gallery-view / .g-grid / .g-detail.
  • Header switch. App.jsx gained a Generate | Gallery segmented control in the brand row and renders Home or Gallery by view; the provider picker hides in gallery mode.

Local-only by design (the feed needs the dev server's fs) — a static/online build yields an empty gallery with an explanatory note. Verified green: lint (0 errors), format, smoke, gui build (browser-glob gate), test:unit 86, test:web 40 (added gallery.test.js, 5 tests). Bumped VERSION/package.json 2.7.24 → 2.7.25 (PATCH per the VERSION file's "ordinary features are PATCH" guidance). Committed ce398c3 on feature/gallery, pushed.

Follow-up fix: the gallery clipped past the bottom instead of scrolling — body is overflow:hidden and <main> is a flex frame, so (like Home's .main-col) the view must own its scroll. Made .gallery-view a flex:1; min-height:0; overflow-y:auto scroll container with an inner .g-inner (max-width:1500px; margin:0 auto) preserving the centered width. gui build green.

Follow-up: single-image view is now its own page, not a popover. Per owner, replaced the modal Detail overlay with a dedicated Single page that replaces the grid (matches v1-2's /single): a back bar + prev/next (arrow-key) nav through the search-filtered feed, image left (sticky) / metadata right, reusing the .g-meta-* / .g-settings / action styles. Swapped the .g-detail* modal CSS for .g-single* page CSS. Delete now lands on a neighbor image (or back to the grid when it was the last). Lint 0 errors, gui build green, test:web 40 (gallery lib tests unaffected).

Single page built out properly: layered prompts, negative AI translation, ImageMagick export

Owner asked to migrate the v1-2 single full-view page into v3 "but actually have it good," with the richer v3 data (DPL + AI-translation versions of both prompt and negative), an app-like look, the keyword/button cloud, and the server reporting ImageMagick so the format-conversion actions can be gated. Dropped the obsolete actions (animation/variations/upscale/rerolls/select/parent).

  • Layered sidecar + negative AI translation (generation change). Sidecar is now nested: prompt:{dpl,roll,ai,final} + negative:{dpl,roll,ai,final}. Home.makeBatch now also AI-translates the rolled negative when auto-fix is on (was DPL-expand only), sends the translated negative, and caches both prompt+negative rewrites on the prompt entry (no re-billing on re-gen). Fixed an existing gap where auto-render lost the DPL in the sidecar (now passed via a promptDplArg).
  • lib/gallery.js gained promptLayers/negativeLayers/promptText (nested + legacy-flat tolerant); searchHaystack now indexes every layer. lib/magick.js added (fetchMagick, convertUrl).
  • Rebuilt Gallery.jsx Single: Prompt/Negative cards (layers as copy-able blocks, dupes collapsed), curated Details table + collapsible All-settings + Raw JSON, clickable keyword cloud (chips filter the gallery), actions row (Open/Reveal/Download PNG/Convert & download dropdown/Delete). New .g-card / .g-text-row / .g-detail-table / .g-cloud / .g-actions styles replacing the old .g-meta-*/.g-detail-actions.
  • vite-plugin-api.js: GET /api/magick (cached detect + magick -list format → writable still formats, allowlist-filtered) and GET /api/image/convert?file=&format= (first-frame convert → download). Magick isn't on this machine's PATH, so the menu stays hidden (verified the unavailable path).

gallery.test.js rewritten for the nested schema (+ layer-helper tests). Verified: lint 0 errors, smoke, gui build, test:web 43, node --check on the plugin + libs. No VERSION bump (unreleased 2.7.25 line).

fairyfox adopt run 3: verification-floor clarification (hub 0.9.3)

Scheduled fairyfox check refreshed the mirror (hub dev force-pushed again → 42b6ea4 → 6777a73, 0.9.2 → 0.9.4). Changelog-span scoping found the only node-facing change is 0.9.3: the express-auth language now states the verification floor is never skipped — an express-authorized/automated adopt skips only the redundant confirmation pause, never verification (full floor: build/tests + ## Verify/ compliance + project-constraint checks, run before and after), with a hard "can't verify → don't auto-apply, fall back to check-report-wait". The scheduled phase reported and applied nothing; owner then green-lit ("yes please"). Adopted the wording into CLAUDE.md + cross-project-sync.md. Skipped 0.9.3's reports_through filename-list change (hub-internal .last-seen.yml, not carried here) and the 0.9.4 hub data/blog/meta pass. Notes/CLAUDE.md-only — no VERSION bump. Committed on feature/providers (where the prior 0.9.2 adoption f0cf652 lives; dev is behind). Report: notes/fairyfox-reports/2026-06-26-adopting-updates-3.md.

fairyfox adopt run 2: express-auth ledger + release-tagging fix (hub 0.9.2)

Scheduled fairyfox check (hub dev force-pushed → mirror 42263fe → 42b6ea4, 0.9.2) surfaced the 0.9.0–0.9.1 standards delta plus two flags; owner green-lit adoption and asked to fix the tagging flag. Adopted, notes/CLAUDE.md-only (no VERSION bump):

  • Release tagging (the real fix). CLAUDE.md + git-workflow.md hand-tagged (git tag … && git push --tags) in all three release paths, but release.yml derives/creates the tag itself and skips if the tag already exists — so a hand-pushed tag = silent no-op release. Removed every hand-tag; added a "Who creates the tag — CI, not by hand" section (recorded as a deliberate divergence from the hub's hand-tag example). deployment.md already described CI-owned tagging, so this just makes the docs consistent.
  • Express-authorization ledger (hub/authorizations.yml): carve-out added to CLAUDE.md + cross-project-sync.md — an active ledger entry that covers a change lets an interactive adopt skip only the redundant confirmation pause; every other safety step stays, and an unattended/scheduled check still applies nothing.
  • process-reports: combined check→adopt = one report; real-hub_version anchor rule; check-only-on-not-yet-adopted inline exception. compliance: ledger note on the sync row.

This run is the combined report notes/fairyfox-reports/2026-06-26-adopting-updates-2.md; folded in and deleted the interim 2026-06-26-check-only.md. Verify loop run; committed on dev.

Image-gen UI build-out (2.7.6 → 2.7.10) — feature/providers

After the first live ComfyUI run (2.7.5), iterated the UI hard with the owner. Arc: 2.7.6 sampler + scheduler also coerced against ComfyUI's /object_info (after the checkpoint fix, "Euler" vs "euler" failed validation); 2.7.7 central output folder/api/image ingest + /api/output/<file> serve, so every provider's images land in engine-v3/output/ and display same-origin (fixed Comfy Desktop's 403 on direct /view); 2.7.8 UI redesign — provider picker in the header, capability-driven provider-controls box, gear slimmed to prompt knobs, prompt counter, dropped keyword/artist counts + auto-add; 2.7.9 provider box collapse + info tooltips (_shared/fieldInfo.js) + no-auto-save key (sessionKeys.js, explicit Save/Clear) + negative as a DPL textarea (expanded before send) with a random roll; 2.7.10 per-prompt image batches (PromptResult.jsx) with a clear Generate/More button and per image/batch/prompt management — remove, delete-from-disk, reveal in Explorer, open in default app (/api/image/delete|reveal|open).

Verified each step: web build + test:web (35). Dev-middleware changes (2.7.7, 2.7.10) need a dev-server restart. Still open from the owner's spec: tighter visual polish (can't see render here), galleries (image click currently opens a new tab), and Phase 3 submit/poll hosted adapters.

ComfyUI live run: checkpoint auto-resolve + readable errors (2.7.5) — feature/providers

Got ComfyUI working end-to-end on the owner's machine (RTX 5060 Laptop, 8GB). Downloaded SD1.5 fp16 (~2GB) into Comfy Desktop's shared checkpoints folder, confirmed ComfyUI sees it, and ran a real generation through ComfyUI's /prompt API (produced raptest_00001_.png) — the exact submit→poll→/view path our adapter uses. Then the app's image button showed "[object Object]": the app sent model.safetensors (stale/blank checkpoint — the user's persisted providerParams.comfyui predates the default I set, and saved params override schema defaults), ComfyUI 400'd with value_not_in_list, and forward() threw new Error(errObject). Fixes: ComfyUI adapter now auto-resolves the checkpoint from /object_info (configured → first-installed → clear error); localDirect.js readableError() surfaces error.message + node_errors. Verified: web build, test:web 33. (Reminder: saved per-provider params override schema defaults — that's the namespacing working as designed, but stale saved values can shadow new defaults.)

Route local-direct through the dev server — no browser CORS (2.7.4) — feature/providers

Hit while helping the owner get a live ComfyUI run working. Diagnosis on their machine: ComfyUI (Comfy Desktop) was up on 8188 but (a) had no checkpoint model installed (its model list was empty — so it can't generate anything, even in its own UI), and (b) Desktop launches without --enable-cors-header, so a browser→8188 call is CORS-blocked. Owner's call: "all those calls should go through a server, no reason to hit it from the browser."

  • Fix: localDirect.js (postJson/getJson) now posts to a new /api/forward route in the Vite middleware (vite-plugin-api.js) — localhost-only, server-side fetch — so no CORS. Image URLs stay direct (<img> display isn't CORS-gated). Hosted already went through /api/generate, so the browser now makes zero direct provider calls. Updated the local-webui contract test (asserts the forwarded url+body).
  • Set ComfyUI default checkpoint to v1-5-pruned-emaonly-fp16.safetensors.
  • User-side (in progress): downloading SD1.5 fp16 (~2GB) into Comfy Desktop's shared checkpoints folder (%LOCALAPPDATA%\Comfy-Desktop\ComfyUI-Shared\models\checkpoints); ComfyUI can't generate until a checkpoint exists. Dev server must be run by the user in a terminal (npm run web) — backgrounding it via remote automation port-flapped against their own instances.

Verified: web build, test:web 33. VERSION/pkg → 2.7.4.

Provider UI re-add: capability-driven settings + image gen (2.7.3) — feature/providers

Phase 2, continuing the same branch. Re-added the image-generation UI that was stripped during the SPA rework, now on the new provider framework.

What changed.

  • Capability-driven Settings. Settings.jsx backend section renders the selected provider's own settings.js fields (a small ProviderField switch over type; select options from the schema's data lists). New hook gui/src/lib/useProvider.jsuseProviderSettings(id) lazy-loads the schema + resolves option-data; providerMode(id) (dialect → engine mode); flattenForProvider(settings, defaults) merges the namespaced params + mode for adapters/formatters.
  • Per-provider param namespacing. Knobs persist under settings.providerParams[id]; an effect fills a provider's defaults on first load without clobbering saved values. Switching providers no longer cross-contaminates (fixed the would-be SD "Euler" vs Comfy "euler" shadowing).
  • Provider owns the dialect. mode is set from the provider on select; the standalone Mode dropdown and the hardcoded global Image/Negative groups were removed (those knobs are per-provider now).
  • App re-gained the Settings drawer + a gear button. Home gained image generation (per-prompt image button for api-tier → Gallery, in-memory only) and Copy-prompt now formats through the provider for the syntax tier (Midjourney params). buildPrompts injects the provider dialect mode.
  • Added providerParams: {} to default settings.

Verified. lint 0 errors; web build (provider chunks code-split); test:web 33 (+ Settings.test.jsx: provider select present, BYOK key appears for OpenAI, no Mode control). VERSION/pkg → 2.7.3.

Heads-up. Playwright visual baselines need a refresh (home gained a gear + image results); not in the headless gate — run test:e2e:update deliberately. Next: submit/poll hosted adapters (Stability/fal/ Replicate/BFL/Ideogram/Leonardo); surface per-provider presets in the UI; live ComfyUI + OpenAI runs.

Provider framework foundation + plain dialect (2.7.2) — feature/providers

Owner asked for real, native, first-class support for many image providers — not a one-size-fits-all minimum. Worked through the model together: a provider sits on a support ladder (API → syntax/dialect → plain text), each declares only the capabilities it has (expand/contract), and providers own everything (config, code, data, settings schema, presets). Key correction from the owner: plain must keep emphasis, rendered in words — don't discard valuable behavior, just reformat it.

What changed (Phase 1, all on feature/providers off feature/v3-layout-reorg).

  • Native core plain dialect. src/helpers/randomEmphasis.js gained processPlain + a "Plain" branch: rolls the same emphasis level, then prefixes an intensifier (prominent/strongly emphasized/ dominant) or hedge (subtle/faint/barely-there), overridable via settings.plainEmphasisWords / plainDeEmphasisWords. No list-stage change (non-NAI/MDJ already use the SD function set; Plain adds no parens so the NAI brace-rewrite is untouched).
  • gui/providers/ framework. Self-contained folder per provider (config.js manifest, settings.js, presets/, code/, data/), auto-discovered via import.meta.glob. _shared/dialects.js maps a provider's dialect → engine mode. _shared/transport/ = localDirect, hostedProxy (client), submitPoll (server, for the Phase-3 async APIs).
  • Storage subsystem gui/storage/ — browser (online/stripped localStorage) + localFile (local .json via /api/storage, browser fallback); presetStore(providerId) for provider-owned presets.
  • Transport plumbinggui/server/dispatch.js (shared), the Netlify function rewired to it, and a new Vite dev-middleware (gui/vite-plugin-api.js, wired into vite.config.js) serving /api/generate + /api/storage locally. .gui-storage.json gitignored.
  • First providers — local-webui (ported real), comfyui (real workflow-graph local-direct), openai (hosted client + code/server.js), midjourney (syntax tier, data-driven --param catalog).
  • Old gui/src/lib/providers/{localWebui,hostedProxy}.js removed; index.js re-exports the new registry (keeps Settings.jsx/imports working). providers.test.js rewritten (registry + Midjourney contract); added tests/unit/randomEmphasis.plain.test.js.

Verified. lint 0 errors; smoke; test:unit 86; test:web 31; web build. VERSION/engine-v3 pkg → 2.7.2.

Next. Re-add the image-gen UI in Home/Settings (provider dropdown, capability-driven knobs, image results, Copy-prompt for syntax tier; drop the standalone Mode control); submit/poll hosted adapters; live ComfyUI run once it finishes installing. Design captured in notes/plans/providers.md.

Layout reorg: flat blocks, data/sources, web-app → gui (2.7.1)

Owner-requested cleanup on feature/v3-layout-reorg, committed in focused chunks.

What changed.

  • Flat blocks. Moved data/blocks/v3/* up to data/blocks/* and ripped out the now-vestigial version machinery (v1/v2 were deleted in 2.7.0; v3/ was the lone tree). block.js became a single flat pool; the v3/-prefix filters left nodeLoader/browserLoader; promptFilesAndSuggestions shed v1Files/v2Files. Critically, the SPA carried the same machinery — promptEngine.js built a per-generation GENS keyed on startsWith("v3/") (now empty → empty Blocks picker) and isExpansionKey matched v3/expansion/ (now never) — so Home.jsx's version superset switch + promptEngine were de-versioned too. Added a test:web assertion that the Blocks list has real chips (guards exactly that regression).
  • data/sources/. artists.csv / danbooru.csv / nai-tag-expirement.json moved there; build-script paths and .prettierignore updated (the minified nai JSON must stay out of prettier).
  • web-app → gui. Folder rename + every config/code reference (68 occurrences across 39 files). vite.config resolves the engine root via .., so it's name-independent.
  • Upgrade-2-0.md root duplicate removed (canonical copy is in engine-v1-2/); dropped from build-docs REPO_DOCS.

Landmines hit.

  • npm run format on this Windows checkout reflows every file's line endings (CRLF↔LF) — ~200 files show as modified but git diff is empty (git normalizes them to no-ops). It also genuinely expanded the minified nai-tag-expirement.json by 8k lines once it left the data/*.json ignore glob — hence the new data/sources/ prettier-ignore. Lesson: stage explicitly, never git add -A, and git restore . clears the no-op churn.
  • The v3/ prefix was load-bearing, not just a folder — it was the canonical address of the active catalog and gated a real feature (auto-begin/end). Flattening required code edits, not just a git mv.

Verified: smoke, npm --prefix gui run build, test:unit (83), test:web (28), lint 0 errors. PATCH → 2.7.1. Not merged to main (awaiting owner go-ahead). Still pending: a fuller notes sweep of v1/v2/web-app in the deeper reference/ docs (e.g. blocks-architecture.md); the long-standing netlify.toml engine-v3 path reconciliation; and the preset-system rethink (26 combinatorial JSONs — see the chat summary).

Adopted hub updates: process-reports + compliance audit

Scheduled fairyfox check-for-updates run surfaced new hub standards; owner gave the go-ahead to adopt in the same session.

Refresh snag. Upstream hub dev was force-pushed (c5659f842263fe), so the documented pull --ff-only aborted. Recovered with git fetch + git reset --hard origin/dev on the reference clone only (disposable, git-ignored mirror — no project history touched). Both old + new commits were retained, so the old→new diff was exact.

What changed upstream (three themes): a new process-reports feedback loop, a new standards compliance audit, and git-flow wording finalized to --no-ff tagged releases. The git-flow piece was already adopted here (2026-06-25), so only the first two were genuinely new.

Adopted (on a feature/adopt-hub-process-reports-compliance branch; notes/process-docs only, no VERSION bump, no main release):

  • notes/fairyfox-reports/ — new folder: README + this run's process report (2026-06-26-adopting-updates.md).
  • notes/reference/process-reports.md + notes/reference/compliance.md — project's-eye-view copies of the two new standards.
  • ## Verify (is it being followed?) sections added to reference/git-workflow.md and reference/versioning.md.
  • reference/cross-project-sync.md — "hub also reads notes/fairyfox-reports/" model, a process-report step in the check flow, the force-push refresh fallback, an anti-recursion line.
  • CLAUDE.md — fairyfox flow now ends with a process report (even check-only) and names the compliance audit; new notes-maintenance trigger row; force-push fallback noted.
  • notes/README.md, status.md, changelog — wired in.

Report written for the hub. Per the new standard, the run's process report flags the force-push/ --ff-only friction and the check-only chicken-and-egg (a check-only run can't write into notes/fairyfox-reports/ if the folder isn't adopted yet) as feedback for the hub to improve the runbook.

Used PowerShell throughout (never the bash sandbox).

Released 2.7.0; synced deep-dive notes; fixed CI

Wrapped the engine-split / v3-only work into a tagged release and cleared the remaining safe follow-ups.

Deeper notes sync. Added structure banners to the systems/* deep-dives (overview, core-engine, cli, server, web-app, README) + context/project.md: engine-v3 = active project (v3-only, no expansions), engine-v1-2 = frozen pre-revival CLI/server. The cli/server deep-dives now read as engine-v1-2 history. (Full line-by-line body rewrites deferred — banners orient the reader; low value to rewrite each para.)

CI was red — fixed. npm ci failed on Linux with npm's @emnapi optional-dependency lockfile bug (two @emnapi/runtime versions pulled transitively; a Windows-generated lock omits the Linux-resolved edges, and I can't generate a Linux lock — no bash sandbox per standing rule). Tried regenerating the lock

  • overrides (pinned @emnapi/*) — still red on Linux. Settled on switching CI/release/pages installs to npm install (reconciles the lock on Linux at runtime; locks + overrides still pin). CI green after.

Released 2.7.0 (MINOR). Retargeted release.yml + pages.yml to run in engine-v3/ (VERSION read stays at repo root; release docs-zip is continue-on-error; pages auto-deploy paused to workflow_dispatch only — see below). Bumped VERSION + engine-v3/package.json to 2.7.0 on a release/2.7.0 branch, merged --no-ff to main + tagged v2.7.0, merged back to dev, pushed.

Release process trap (note for next time): release.yml gates on "tag v$VERSION doesn't exist yet" (it expects to create the release+tag from a main push), but the git-flow standard has us create + push the tag manually. Pushing main --tags together made release.yml see the tag already present → it skipped (10s no-op, no GitHub Release). Published the v2.7.0 release directly with gh release create + a source tarball. To reconcile: either push main without the tag (let release.yml tag), or relax the gate for workflow_dispatch.

Doc-site (still broken — the main remaining follow-up). npm run docs (build-docs.mjs) resolves notes/ + assets/ at the repo root while src//web-app/ moved under engine-v3/, and it runs jsdoc with a config that's no longer co-located. Needs build-docs.mjs to use two base dirs (repo root for notes/assets/README, engine-v3 for src/data/web-app + jsdoc cwd/config). Until then the release docs-zip is skipped and Pages auto-deploy is paused.

Used PowerShell throughout (never the bash sandbox).