Newest entry on top.
2026-06-30 — Security & code-health integrations (Dependabot, CodeQL, Scorecard, Sonar, CodeRabbit) (docs/CI, no bump)
Added free-for-OSS analysis/automation services on top of CI (owner picked all five). GitHub-native,
committed and live now: Dependabot (.github/dependabot.yml — grouped weekly dep PRs for engine +
gui + actions), CodeQL (.github/workflows/codeql.yml + config — JS/TS SAST, security-and-quality,
frozen/vendored paths ignored), OpenSSF Scorecard (.github/workflows/scorecard.yml — supply-chain
score + badge). Account-based, wired but dormant until the owner signs up: SonarQube Cloud (formerly
SonarCloud — sonar-project.properties + .github/workflows/sonar.yml, gated on vars.SONAR_ENABLED so
no red X pre-setup; imports the lcov coverage; tech-debt + quality-gate badges) and CodeRabbit
(.coderabbit.yaml — AI PR reviews on app install). README gained OpenSSF Scorecard + Sonar quality-gate
& tech-debt badges. All CI/build-only (no app/user-data touch) → no version bump, legal pages
unaffected. Setup links + steps: deployment.md.
2026-06-30 — README badges → shields.io; Codecov coverage in CI (docs/CI, no bump)
Reworked the README badge strip to a single cohesive shields.io set (owner's call, replacing the
shieldcn.dev set): Contributors first, dropped the branches count, and added a real Netlify
deploy-status badge (shields' netlify/<site-id>), plus CI (Actions), Coverage (Codecov),
Code quality (CodeFactor), a Docs badge → the doc site, version (from engine-v3/package.json),
Node ≥24, created-at, code-size, top-language, and the issues/PR counts — all linked to their pages.
Wired Codecov: added an lcov reporter to both Vitest coverage configs (Node + gui) and two
non-blocking codecov/codecov-action@v4 upload steps in ci.yml (flags node / gui). Owner action:
enable the Codecov GitHub app + add a CODECOV_TOKEN secret; the badge populates after the first main
CI run. CodeFactor is zero-config (enable its app). CI/test-config/docs only → no version bump; the
app bundle is untouched, and Codecov is CI-only so the legal pages are unaffected. See
deployment.md.
2026-06-30 — Align the app description everywhere to the new positioning (2.35.1)
Settled on a single plain-English description of the app and used it consistently everywhere it's
described: "An open-source generator for AI image and text prompts that automatically builds richer,
more detailed prompts than most people write by hand," then run through 40+ models (Midjourney,
DALL·E, Gemini, FLUX, Stable Diffusion, and more). Per the owner, these blurbs lead with what the app
does — not "DPL" (unknown jargon) and no ad-speak. Updated: the GitHub repo About; the README intro
(DPL demoted to an under-the-hood mention); the app's gui/index.html <title>/meta/OG/Twitter/JSON-LD
(the shipping bit → PATCH); engine-v3/package.json description; CLAUDE.md's opening line; and
notes/context/project.md's definition. Left session logs and DPL/CLI mechanics docs untouched (history
/ technical, not the pitch). No data-practice change → legal pages unaffected.
2026-06-30 — Seedable, deterministic engine + async batch boundary (2.35.0)
Replaced the "swap global Math.random" seeding hack with a real, threaded PRNG so a generation is
reproducible from a seed. New src/core/rng.js: cyrb128 (string→128-bit state) + sfc32 + an Rng
class (float/int/chance/pick/sample/shuffle + fork(label) sub-streams) + createRng
(records a random seed when none is given). src/helpers/random.js now draws from an ambient
source that defaults to Math.random (so unseeded behavior and the legacy Math.random-swap test path
are unchanged) and that the engine swaps for a seeded Rng per generation via withAmbientRng. The
DPL ctx.rng seam delegates to it, and the raw Math.random() leak in the renderer's
weightedSampleN was fixed to draw from ctx.rng.
Closed the determinism holes: four generators still used lodash _.random/_.sample (which capture
Math.random at import and can't be seeded) — subject/entity, prompt/d, style/silhouette,
style/vibrant-art — now use helpers/random.
Engine API (src/core/engine.js): generate({seed}) is deterministic when seeded; new
generateWithSeed() returns {prompt, seed} (auto-seed when absent) for reproduce-later; generateMany
gives each prompt its own fork(i) sub-stream (reproducible batch); new generateManyAsync yields
between prompts (the async-capable batch boundary — the per-prompt render stays sync by design so the
live preview stays instant). settings.seed already flows through the GUI facade, so a seed UI is a
thin follow-up. Tests: new tests/unit/rng.test.js + seeded cases in engine.test.js; snapshots stay
byte-identical (default path unchanged). Design note: notes/reference/rng-design.md. Full gate green
(lint + smoke + 260 Node + 271 web) and the SPA builds. No data-practice change → legal pages unaffected.
Decided (owner, 2026-06-30): we are not pursuing full-pipeline async — it can't be done safely
and stably without hacks (the pure-CPU render path is called during React render to drive the instant
live preview; forcing it async would ripple through render code for no real gain). Async lives at the
batch boundary (generateManyAsync); the render path stays synchronous by design. Settled, not an open
item.
2026-06-30 — In-app dialogs replace native alert/confirm/prompt (2.34.0)
Replaced every blocking browser dialog in the SPA with a proper in-app modal system. New
gui/src/lib/dialog.js is a tiny Promise-based external store exposing a singleton dialog with
alert / confirm / prompt (resolve contract mirrors the natives: confirm→boolean, prompt→string|null,
alert→undefined). Because it's a store (not React context), it's callable identically from components
and from non-component hook/lib files. gui/src/components/DialogHost.jsx (mounted once at the app
root inside the i18n boundary) renders the active request via a portal, reusing the existing
.modal / .modal-overlay / .modal-actions styling — accessible (role="dialog" + aria-modal,
Escape/backdrop cancel, Enter accepts, Tab trapped, focus moved in on open and restored on close), with
a localized OK/Cancel default, optional custom labels, a destructive (red) accept style, and a text
input for prompt. Added .modal-input + .btn-destructive CSS.
Migrated all 16 native call sites across 11 files (App, ApiKeyField, Home, ManageFolderEditor,
ManageListEditor, Settings, SingleView, and the useImageBatches / useManageTree hooks); the
formerly-sync confirm/prompt callers became await. Delete confirms now use a red Delete
button. New messages dialog.ok / dialog.cancel / app.deleteAction (en.json + en-XA regenerated).
Tests: new dialog.test.js (store) + DialogHost.test.jsx (interaction), and the three tests that
stubbed window.confirm/prompt now mock the dialog service (+ async act). Full headless gate
green (lint + smoke + 271 Vitest) and the SPA builds. No data-practice change → legal pages unaffected.
2026-06-30 — README revamp + SEO repositioning (2.33.3)
Rewrote the root README.md to lead with what the project actually is — a creative DPL
block engine + web app that's provider-agnostic (~40 image/text backends), not a
Stable-Diffusion-first tool (SD is still supported but no longer the headline). New structure: a
prominent ▶ Try it now block linking the online edition (prompt.fairyfox.io),
the docs (fairyfox.io/random-ai-prompt), and the parent
site (fairyfox.io); a "what makes it interesting" capabilities section; a DPL
example; build/run, project layout, development, and a links section. No images/videos. Added Node
and license badges.
SEO pass on the actual indexable page (gui/index.html): retitled to the new positioning, refreshed
the meta/OG/Twitter descriptions (kept high-volume model keywords for discoverability), added a
canonical link, og:url, absolute og:image URLs, upgraded the Twitter card to
summary_large_image, and added a WebApplication JSON-LD block. Also refreshed the GitHub repo
About description and topics (prompt-generator, prompt-engineering, ai-art, generative-ai,
text-to-image, stable-diffusion, midjourney, blocks, react, vite, open-source). No data-practice
change (same self-hosted assets, no new third-party requests), so the legal pages are unaffected.
2026-06-30 — Image provider "Unset" is just Plain text renamed (2.33.2)
Walked back the separate image "Unset = no provider" state added in 2.32.1. Per the owner: for the
image provider, "Unset" is simply Plain text renamed — the plain provider already means "no
real image API, just emit the prompt." So the plain provider's label is now "Unset" (config) and
the extra provider === "none" image branch (ProvidersMenu / Home / ProviderGear) was reverted. The
Text and Upscale rows keep their genuine Unset = none/off (those really can be unselected).
2026-06-30 — Fix: provider gear accordion showed image settings for the Text/Upscale roles (2.33.1)
The provider-settings accordion (2.31.0) rendered every role's section with the chosen provider's only settings schema — which is image-generation for image providers (and image-gen for dual image+upscale providers). So OpenAI in the Text role showed the image "Model" field, and Comfy in the Upscale role showed image-gen knobs. A provider has just one (native) schema, so each section now renders it only for the role it serves: Image → the image schema; Text → a note (a rewrite AI uses a fixed model, no extra settings); Upscale → the schema only for upscale-only enhancers, else a note (a dual provider's image settings are edited in its Image section). No more image-gen knobs under Text/Upscale.
2026-06-30 — Natural-language artists & styles + a styles building block (2.33.0)
- New setting
naturalArtistStyle(default on) frames artists as "by <artist>" and styles as "in the style of <style>" in the generated prompt, so a reader (and the model) can tell an artist from a style from a plain keyword. Off = raw names. Lives in the engine settings (src/settings.js) and the SPA defaults, with a toggle in the prompt-settings gear (Vocabulary). {#artists}is framed ("by …") when the setting is on (data/blocks/prompt/artists.js), via a post-process so the rawartistRepeater(and its unit tests) are untouched.- New
{#styles}generator (data/blocks/prompt/styles.js+.dpl/.json) emits 1–2 tokens from the existingstylelist, framed "in the style of …" — the styles list finally has a building block (the companion to{#artists}). - Verified end-to-end (both gates):
npm run smoke, SPA build, 242 unit + 259 web tests pass (no snapshot churn), and a manual expand confirmed "by Edward Ruscha…" / "in the style of Abstract expressionism" with the toggle on and raw names off.
2026-06-30 — Providers-menu polish + language moved to the links menu (2.32.1)
Follow-up refinements to the 2.31.0 header batch, per owner feedback.
- Language picker moved to the existing links menu (the far-right ⋯ menu,
LinksMenu.jsx, alongside GitHub / docs / legal) — and the separate app-settings cog was removed (AppMenu.jsxdeleted). One menu, not two. - "Unset" offered on every provider row. Image and Upscaler now offer an Unset option (the
Upscaler's "Off" became "Unset"); Text already had it. Unset is an option, not the default — the
image default stays Plain text. Choosing Unset for the image generates prompts only, no images
(handled in
Home.jsx; the gear shows a hint when nothing's selected). - Hint text moved into tooltips. The two always-on description paragraphs under the Text and
Upscaler pickers became hover tooltips (a
hintprop onProviderPicker), decluttering the panel.
2026-06-30 — Local edition gets a real release stage + full file-watch hot reload (2.32.0)
Fixes the dev-build-as-release defect and makes the running app aware of changes to its own files.
- The local edition now has a proper release stage. The whole
/api/*backend (hosted-generation proxy, local-file image storage + gallery feed, ImageMagick convert/resize, Manage, the file-watch, the settings store) was wired only into Vite's dev server — so a realvite buildhad no backend, andnpm run web(the dev server) was being used as the de-facto release. Wrong and unprofessional. The handler is now factored intogui/server/apiHandler.jsand mounted by both the dev-server plugin (gui/vite-plugin-api.js, now thin) and a new standalone release server (gui/server/serve.js) — one backend, two transports, can't drift. New scripts:npm start(build → serve the built app + backend) andnpm run serve(serve a prebuiltdist/). Default port 4173;NO_OPEN=1skips the browser. - Latent bug fixed in the extraction: the convert / resize / reveal / open routes called
exec/execPthat were never imported in the old plugin (a runtimeReferenceError); the shared handler imports them properly. - Full file-watch hot reload.
/api/manage/watchis now a scoped SSE stream —data(lists/blocks → live catalog refresh),output(the gallery feed), andsettings(user-settings). The app opens one stream inApp.jsx: data edits hot-apply to Generate + Manage, new/removed/changed images appear in the gallery, and external settings edits are re-read. Manage dropped its own EventSource and rides the shared catalog refresh. - Never-corrupt-settings safeguards. On-disk settings writes are now atomic (temp file +
rename), so a watch reload can't read a half-written file; the settings reload also ignores events
caused by the app's own writes (
msSinceLastWrite) and only swaps when the value actually changed — so it never fights or clobbers the app's saves.
2026-06-30 — Header & provider UX batch: app-settings cog, settings accordion, unified derived grid (2.31.0)
A cluster of GUI refinements across the header and the single-image view (feature/gui-ux-batch).
- New app-settings cog in the header (
AppMenu.jsx) — a distinct sliders button next to the provider gear, on every tab. The display-language picker moved here out of the prompt-settings gear (Settings.jsx), where an app-wide preference was mixed in with prompt knobs. The cog is the home for future app-level settings. - Provider gear is now an accordion (
ProviderGear.jsx) covering all three provider roles — Image (always), Text (when a rewrite AI is set), and Upscale (when an upscaler is set).ProviderBoxnow takes aproviderIdso one box drives any role; each section header shows the provider's label + tier and toggles its body. - Upscaler options grouped Local / Online, matching the image picker (
ProvidersMenu.jsx). - Text provider "Off" → "Unset" for clarity (the image/upscale "Off" is unchanged).
- Plain text is the default image provider and is pinned to the top of the Local group — it needs no machine, key, or network, so it's the one provider that works for everyone out of the box.
- Unified, colour-coded derived grid below the single-image view (
DerivedStrips.jsx): the three separate Re-Roll / Variation / Resize strips became one grid, each thumbnail colour-coded by kind with a hover tag, a tooltip, and a small legend.
2026-06-30 — In-app legal pages (Privacy / Terms / Cookies) + menu links (2.30.1)
Added the app's own legal documents and surfaced them in the header menu.
- Three self-hosted, app-styled static pages under
engine-v3/gui/public/legal/:privacy.html,terms.html,cookies.html(dark-first theme with light fallback, app logo, a "← Back to the app" link, and cross-links between the three). Served at/legal/*.htmlin dev (Vitepublic/) and on Netlify (real files beat the non-forced/*→index.htmlSPA fallback). - Rewritten to match reality, not generic boilerplate. The TermsFeed-generated drafts described
accounts, marketing emails, camera access, login cookies and 24-month server logs — none of which
this app has. The rewrites state the truth: no accounts, no analytics/cookies/tracking, settings +
BYO API keys stored only on the user's device (
rap.store.localStorage / local files), prompts + keys sent directly from the device to the chosen provider (no server relay; providers that can't be called directly from a browser are simply unavailable in the web build), Netlify named as the hosting processor. Covers web + desktop + future mobile. Age standardized at 18+ (desktop can generate NSFW). Cookies Policy honestly says "we use no cookies." Contact: fairy@fairyfox.io. - Self-hosted fonts (GDPR improvement). Removed the Google Fonts
<link>/preconnect fromindex.htmland the three legal pages; fonts now load fromgui/public/fonts/fonts.css+ local.woff2(Maven Pro 400/500/600/700, Space Grotesk 500/600/700, latin,font-display: swap). This ends the IP-to-Google transfer on the live site — the only third-party data flow left is the AI provider you pick + Netlify's hosting logs. The.woff2files are sourced from the@fontsource/maven-pro/@fontsource/space-groteskpackages (copy step in the session log) and committed as static assets. Privacy/Cookies docs updated to say fonts are served locally. - Header menu links —
LinksMenu.jsxnow renders the three legal pages below a.links-sepseparator (newShieldIcon/FileTextIcon/CookieIconinicons.jsx); legal items open in a new tab (preserving SPA state) but omit the outbound glyph since they're same-origin. NewlinksMenu.*i18n messages;.links-group/.links-sepstyles added. - Removed the retired serverless proxy. Deleted the dead Netlify function files
(
gui/netlify/functions/generate.js+rewrite.js) and dropped thefunctionssetting +/api/*redirect fromnetlify.toml(the online build has been browser-direct/static since 2.11.0 and never called them). Trimmedtests/providers/netlifyFunctions.test.jsto thedispatch/dispatchRewritehub tests it shares. Keptserver/dispatch.js+ the Vite dev middleware (vite-plugin-api.js,/api/generate+/api/rewrite) — that's the local dev proxy the desktop full version uses for non-CORS providers.deployment.mdupdated. Also corrected the Privacy Policy wording, which had wrongly described a "stateless proxy we run." - Standing instruction: keeping these three documents accurate as the app's data practices change is
now an owned, by-default responsibility (recorded in
CLAUDE.md).
2026-06-30 — GUI polish pass: header links menu, JS-highlighted code editor, all-tab header, UX fixes (2.30.0)
A batch of GUI quality-of-life improvements (branch feature/gui-polish):
- Header links menu — a new overflow-menu button (
LinksMenu.jsx,MenuIcon/GitHubIcon/BookIcon/HomeIcon/ExternalLinkIconadded toicons.jsx) in the top bar opens GitHub, the docs site (fairyfox.io/random-ai-prompt/), and the fairyfox home — on every tab. - Header controls on all tabs — the Providers dropdown + provider gear are no longer gated to
the Generate view; they (and the NSFW switch + the new menu) show on every tab (
App.jsx). - JS code editor —
CodeEditor.jsxnow applies real syntax highlighting (aHighlightStylereusing the DPL palette via.cm-tok-*classes), bracket matching, auto-close brackets, indent-on-input, and Tab-to-indent — but only when a language is supplied (raw list editing keeps plain-text behaviour).@codemirror/lang-javascriptwas already wired inManageBlockEditor; this makes its tokens actually colour. No new dependencies. - Validity icon flipped to the upper-LEFT of the prompt box (
Home.jsxcomposer-corner-left); the editor's first line is indented so it never sits on typed text. - Manage entries are fully clickable — the whole pill opens the editor (click or Enter/Space);
the separate Edit icon is gone, Delete stays (stops propagation).
EditIconimport dropped. - Description UI redesign — the block/list picker hint became a left-aligned info callout with an accent edge + ⓘ icon (was centred text with an awkward underline), and now also surfaces the selected folder's own description, not just the group hint. (Audited the data: every list and generator already carries a description — the gap was the UI hiding folder descriptions.)
- Smaller fixes — breathing room below the DPL insert bar; lowercased the Blocks/Lists all sub-tab; per-tab descriptive tooltips in the header; brand tooltip.
README now features the live app (prompt.fairyfox.io) + docs link; the GitHub repo About + homepage
were set to the app URL. Verify: lint 0 errors, smoke, gui build green, Node + web Vitest 265 web
tests pass (full npm test green). Visual e2e baselines will need refreshing for the deliberate UI
changes (npm run test:e2e:update). MINOR 2.29.1 → 2.30.0.
2026-06-30 — Doc fix: correct the user-settings folder path (2.29.1)
Notes-only follow-up to 2.29.0: the design note showed the GUI's user-settings folder at
engine-v3/user-settings/, but the code writes it under the dev-server root at
engine-v3/gui/user-settings/. Corrected the path in plans/storage-and-settings.md and clarified
that the folder is created lazily on first write, is gitignored, and is distinct from the Node engine's
user-settings.json. No code change. PATCH 2.29.0 → 2.29.1 (cut so the fix ships to main as a tagged
release rather than an untagged commit).
2026-06-30 — Unified, versioned, cookie-free storage: one user-settings folder + per-provider files (2.29.0)
The storage/settings overhaul wired end-to-end (design in plans/storage-and-settings.md). All GUI
user data now flows through the storage layer (gui/storage/): local mode persists to one
user-settings/ folder on disk (one JSON file per namespace; providers/<id>.json per provider) and
the online build uses localStorage only — the sole place browser storage is used; zero cookies
in any mode. The dev-server /api/storage endpoint became a per-namespace folder writer
(nsToFile/readNs/writeNs/removeNs/listNs, traversal-safe) with a one-time
migrateLegacyStore that folds the old flat .gui-storage.json in. Each static provider now ships its
defaults as a literal <id>.json sidecar (imported by settings.js); per-provider user overrides
live in user-settings/providers/<id>.json and cascade over those defaults (midjourney keeps deriving
defaults from its catalog). The app's stores (settings, customStore, wrapperStore, provider params)
were rerouted off direct localStorage onto a synchronous boot-time hydration cache
(gui/storage/cache.js) that migrates legacy keys forward — so component code is unchanged except a
one-line hydrate gate in App.jsx. Verify: lint 0 errors, smoke, Node unit 242, web 265, gui
build green. MINOR 2.28.18 → 2.29.0. (The in-app storage-management UI is deferred — a first-cut panel
was built then reverted at the owner's request; surfacing it, especially online, is the owner's design call.)
2026-06-30 — Versioned config + deep-merge foundation for the storage overhaul (no version move)
First step of the unified storage/settings rework (design in plans/storage-and-settings.md). Adds the
pure, isomorphic core the rest builds on, with no app wiring yet so the running app is unchanged:
gui/storage/merge.js (deepMerge — recursive objects, arrays replace by default / concat opt-in,
null clears, undefined keeps base, never mutates; diff — the sparse inverse; equal) and
gui/storage/config.js (versioned documents { __v, …data } over the existing StorageBackend:
loadConfig/saveConfig, forward migrations with a one-time self-healing re-save, and the
defaults→override loadCascade/saveCascade that stores only the diff). Tested by
gui/tests/storage/merge.test.js (13) + config.test.js (9, backend mocked). Web suite 237 → 259
green; eslint clean. Internal scaffolding only — VERSION bumps when the layer is wired to real
settings.
2026-06-29 — Memoize the data loaders' static catalog: ~280× faster prompt generation (2.28.18)
Profiling pass over the whole project. The dominant cost was that the data loaders re-did all of
their catalog work on every generated prompt: nodeLoader.readListLines rebuilt the entire
list-name set (≈4 full recursive readdirSync walks of data/lists per distinct list), and the
reserved keyword wildcard re-unioned the entire vocabulary (~48 ms) once per prompt — the list
store clears its per-prompt cache on reset(), so it recurred every time. The on-disk (Node) and
build-time bundled (browser) catalog is static for the life of a process/page, so all of it is now
memoized: src/core/nodeLoader.js caches the directory walks, the sorted catalogs, the full
name set, and the keyed reads (readListLines by name|includeAdult, the meta/group sidecars), with
a new nodeLoader.refresh() to drop the caches; src/core/browserLoader.js memoizes its name set +
resolved-line sets (kills the per-prompt wildcard re-union in the SPA at runtime); and
src/core/stages/block.js hoists resolvePool out of the per-token loop. A single
generate() went from ~56 ms → ~0.2 ms (≈280×), and one prompt's filesystem pressure from
208 readdirSync + 222 readFileSync → ~0 once warm. No behavior change — safe because the
loaders only read a static catalog (the Manage tab's live edits go through a separate runtimeLoader).
Verify: lint 0 errors, smoke, Node unit 235, web 237, gui build (785 KB gzip, within budget) —
all green. Full report: reference/performance.md. PATCH 2.28.17 →
2.28.18.
2026-06-29 — Split the dev-server API helpers out of vite-plugin-api.js, with tests (2.28.17)
Final Phase 6 file. The 673-line gui/vite-plugin-api.js dev middleware had no test coverage; its
helpers move to gui/vite-api-helpers.js (the output-dir constants, ImageMagick detection, the
path-traversal-safe resolveOutputFile, the JSON request/response helpers, and the BYOK key store),
leaving the plugin file (~570 lines) as just the route wiring. The previously-untested helpers now
have a test net — tests/unit/viteApiHelpers.test.js (8 tests, in the Node suite since it's a
Node module) covering the resolveOutputFile security boundary (rejects .., separators, and
URL-encoded traversal) and the send / readJson helpers. Node unit 227 → 235. No behavior change.
2026-06-29 — Extract ManageListEditor's pure ops (tested) + messages (2.28.16)
Phase 6 of the refactor. Two extractions from the 522-line ManageListEditor.jsx: the gnarly,
previously-untested pure logic — parsing an AI "expand" reply into clean entries, merging in only the
new ones, de-dupe, and sort — moves to lib/manage/listEditorOps.js with a test net
(gui/tests/lib/listEditorOps.test.js, 8 tests covering list-prefix stripping, comma fallback,
case-insensitive merge/dedupe, and non-mutating sort); and the ~75-line defineMessages block moves
to lib/manage/listEditorMessages.js. The component drops to ~460 lines and uses the imported ops.
Web suite 214 → 222. No behavior change — lint + web + e2e + build green.
2026-06-29 — Lift the wrapper-preset control into a tested useWrapperPresets hook (2.28.15)
Phase 6 of the refactor. The wrapper FAB's whole behavior — the popover/modal view state + anchor,
the saved-preset library (localStorage via wrapperStore), the Start/End editor draft, and every
action (apply, load, save / rename / delete, per-pane revert) — moves out of WrapperFab.jsx into
lib/wrapper/useWrapperPresets.js. The component drops from 428 to ~340 lines as pure rendering over
what the hook returns. This previously-untested logic now has a test net —
gui/tests/lib/useWrapperPresets.test.jsx (9 tests) mocks wrapperStore and asserts apply (None /
Default / named), save (new preset vs editing the Default in place), delete (incl. the Default
no-op), per-pane revert, and load-into-editor. Web suite 205 → 214. No behavior change.
2026-06-29 — Extract the DPL insert-toolbar messages from dplInserts.js (2.28.14)
Phase 6 of the refactor. The ~135-line defineMessages block (category + per-construct labels and
descriptions) moves out of gui/src/lib/dpl/dplInserts.js into gui/src/lib/dpl/dplInsertsMessages.js,
leaving the catalog builder (getDplInserts) and its literal syntax/template/example DPL in a
~280-line file that imports { m }. Public surface unchanged (getDplInserts default + named). No
behavior change — the existing dplInserts unit suite (5) + the web suite + build all green.
2026-06-29 — Split the DPL CodeMirror autocomplete out of dplLanguage.js (2.28.13)
Phase 6 of the refactor. The 501-line gui/src/lib/dpl/dplLanguage.js is cut along its natural seam:
the context-aware autocomplete half — the completion source, its option renderers (kind badge,
section header, the live-example info tooltip), and the front-matter / section-name context
detectors — moves into gui/src/lib/dpl/dplComplete.js (~260 lines). dplLanguage.js keeps the
StreamLanguage tokenizer, the highlight style, and the section-heading decorations (~285 lines) and
re-exports dplKindBadge / inFrontMatter / dplCompletionSource, so the editor's imports are
unchanged. The two halves don't cross-reference, so it's a pure relocation. No behavior change —
lint + the SPA web suite (205) + e2e (8) + build green; the editor still lives in the lazy chunks.
2026-06-29 — Lighthouse polish: SEO + A11y to 100, Perf to 97 (2.28.12)
Pushed the Lighthouse scores up across the board (Workstream B/C). SEO 92 → 100: added a real
gui/public/robots.txt (the SPA fallback was serving HTML for /robots.txt, failing the audit).
Accessibility 96 → 100: lightened the --muted/--faint text tokens (#9a9aa4→#aaaab2,
#74747e→#97979f) so they clear WCAG AA 4.5:1 on the darkest panels and the green active-tab
background (the two flagged contrast failures: .cat-name and .dpl-insert-lead). Performance
95 → 97: the render-blocking Google Fonts stylesheet now loads non-blocking via the
media="print"/onload swap (with a <noscript> fallback), removing ~284 ms of render block off
first paint. Verified by a real Lighthouse run — Performance 97, Accessibility 100, Best-Practices
100, SEO 100; visual baselines refreshed for the slightly lighter muted text.
2026-06-29 — SEO meta tags + Lighthouse SEO gate (page-rank greenlight) (2.28.11)
Workstream C of the refactor. The SPA's index.html gains the SEO essentials it was missing: a
descriptive <title>, a <meta name="description">, robots: index, follow, an author tag, and a
full Open Graph + Twitter-card set (type/site_name/title/description/image) for social previews. A
self-referencing canonical/og:url is intentionally omitted (the production URL isn't recorded in-repo,
and a wrong canonical is worse than none). lighthouserc.json now also gates categories:seo ≥ 0.9
alongside performance/accessibility, so the page-rank greenlight can't silently regress. Verified with a
real Lighthouse run: Performance 95, Accessibility 96, Best-Practices 100, SEO 92 — all green.
2026-06-29 — Split the content-safety lexicons out of contentSafety.js (2.28.10)
Phase 6 of the refactor. The ~270 lines of term lists (slurs, minor-sexual, extreme, the whitelist,
and the NSFW lexicon) move verbatim into src/safetyLexicons.js as pure data; contentSafety.js
keeps the matcher logic (normalize, classifyRemoval, isNsfw, the built matchers, _sets) and
imports the lexicons, dropping from 421 to ~150 lines. Public surface unchanged — every importer (the
CSV build scripts, the list-cleanup tooling, and the unit suite) keeps importing from
contentSafety.js. No behavior change — the contentSafety unit suite (13) + full unit (227) + build green.
2026-06-29 — Split listManifest.js into focused modules behind a re-export barrel (2.28.9)
Phase 6 of the refactor. The 504-line src/listManifest.js splits — verbatim — into three focused,
browser-safe modules: listTags.js (the per-list metadata table), nameOrder.js (variant suffixes,
the reserved keyword wildcard, the natural-order comparator, physical→logical names, suffix-path
resolveName, and computeButtonNames), and listResolve.js (resolveListLines + autoGroupListDirs
with the SFW/NSFW + composite-group model). listManifest.js becomes a thin re-export barrel, so
every importer (both engine loaders, the runtime loader, the manage tree, the suggestion builder, and
the test suites) keeps importing from the same path unchanged — zero import-graph churn. No behavior
change — smoke + the listManifest unit suites (27) + full unit (227) + web (205) + build all green.
2026-06-29 — Lift the Manage tree-CRUD into a tested useManageTree hook (2.28.8)
Finishes the Manage decomposition the right way — floor up, tests under it. The tree state (on-disk
tree + stable-branch manifest, expand/select/search/drag state), the catalog/SSE refresh effects, the
built+filtered models, and every file operation (new file/folder, move, delete, restore-ghost) move
out of Manage.jsx into lib/manage/useManageTree.js. The component drops from ~545 to ~350 lines and
becomes pure rendering over what the hook returns. Crucially this lands with a new test net —
gui/tests/lib/useManageTree.test.jsx (9 tests) mocks the manage backend + catalog and asserts the
load→models build and each file op (mkfile ext/boilerplate, delete confirmed vs cancelled, move,
restore-ghost, toggle) — so the previously-untested CRUD path is now covered. Web suite 196 → 205. No
behavior change; Manage stays its own lazy chunk.
2026-06-29 — Decompose Manage.jsx: extract icons + ManageDetail pane (2.28.7)
Phase 5 of the refactor. The Manage tab's standalone pieces move out of the 727-line file: the six
tree icons (caret/gear/edit/refresh/restore/trash) → components/manage/icons.jsx, and the right-pane
detail/preview component (plus its previewText cap and its own colocated messages) →
components/manage/ManageDetail.jsx. Manage.jsx drops to ~545 lines. The tree-CRUD flow stays in
place for now — it's tightly coupled to inner render components and isn't covered by the e2e suite
(Manage is gated behind the local backend), so lifting it into a hook is deferred until Manage gets
component tests, rather than risk an untested regression. No behavior change — web suite + e2e green;
Manage stays its own lazy chunk (CodeMirror still loads only when the tab opens).
2026-06-29 — Decompose SingleView.jsx: 915 → ~440 lines across single/* modules (2.28.6)
Phase 4 of the refactor. The single-image view's sub-pieces are promoted out of the one 915-line file
into focused modules: the shared react-intl strings + derive-layer label map → components/single/ messages.js; the pure exporters → lib/single/markdown.js (toMarkdown) and lib/single/json.js
(syntaxHighlightJson); and each presentational piece to its own file under components/single/ —
PromptCard (with its TextRow), DetailTable (with DetailRow), CopyButton, LineageHead,
DerivedStrips, and KeywordsCard. SingleView.jsx keeps the orchestration + the pick/REST_DROP/
FRAC helpers and imports the rest, dropping to ~440 lines. No behavior change — web component suite +
e2e + visual/a11y green; SingleView stays its own lazy chunk (7.2 KB gzip).
2026-06-29 — Home.jsx: lift the image-batch flow into a useImageBatches hook (2.28.5)
Finishes the Home decomposition. The ~220-line image-generation flow — makeBatch (the prose +
keyword rewrite passes, the per-image sidecar, the busy/placeholder bookkeeping) and the
remove/clear image/batch/all handlers — moves verbatim into lib/home/useImageBatches.js, which now
owns the generated-prompt list (prompts), the running id counter, and the in-flight image error,
and reports them back. Home.jsx drops from 861 to ~625 lines and is now a thin coordinator over the
palette, the composer, and the hook (no more giant generation routine inline). The confirm/error
messages move with the logic. No behavior change — web component suite + Home e2e + visual/a11y green.
2026-06-29 — Seedable engine RNG: drop lodash randomness, make the pipeline reproducible (2.28.4)
Workstream A of the refactor (notes/plans/refactor-2026-06.md) — the deep fix behind the earlier
test band-aid. The engine drew randomness from lodash (_.random/_.sample/_.shuffle), and lodash
captures Math.random at import, so the test seam (withSeed, which swaps Math.random) could never
control it — only the DPL renderer was seedable. Every emphasis/list/suggestion test had to either
disable emphasis or use single-entry lists to dodge the un-seedable draws. Now a new
src/helpers/random.js (randomFloat/randomInt/sample/shuffle, all reading the live
Math.random) replaces lodash across the keyword randomizers (randomEmphasis, randomAlternating,
randomEditing, keywordRepeater), the list/block stages, listStore, prompt-salt, and
the suggestion builder — so the whole pipeline (emphasis included) is reproducible under a seed.
The engine's exact-output unit tests drop the emphasis-off workaround and assert real seeded output,
plus a new end-to-end determinism test (same seed → byte-identical). A dead lodash import in
cleanup.js is removed; src/ no longer uses lodash for randomness. No behavior change for users
(production still runs on real Math.random); snapshots are unchanged.
2026-06-29 — Decompose Home.jsx: extract palette, icons, and pure helpers (2.28.3)
Phase 3 of the refactor (notes/plans/refactor-2026-06.md). The 1161-line Home.jsx is reduced to
~860 by pulling out self-contained pieces: the six inline toolbar SVGs → a shared
components/icons.jsx; the image-sidecar snapshot helper → lib/home/snapshot.js; the building-block
category split (foldersOf/splitCats) → lib/home/blockCategories.js; and the entire left-pane
building-block palette (its search box, Blocks/Lists tabs, folder sub-tabs, and chip cloud, plus the
search/active-tab state and catalog derivation it alone uses) → a new components/home/BlockPalette.jsx
that reports up only the cross-pane actions (insert a token, hover tooltip). No behavior or visual change
— verified by the web component suite, the Home e2e (generate + filter), and the visual/a11y snapshots.
A follow-up can lift the image-batch/rewrite flow (makeBatch et al.) into a hook.
2026-06-29 — Modularize the DPL compiler: dpl.js 882 → ~120 lines (2.28.2)
Phase 2 of the refactor (notes/plans/refactor-2026-06.md). The single 882-line src/core/dpl/dpl.js
is split — by moving code verbatim — into focused sibling modules under src/core/dpl/: words.js
(the two 100-step intensity/focus word scales + intensityWord/focusWord), intensity.js (clamp /
count-scale / relative-modifier / condition math), rng.js (the default Math.random RNG seam),
parser.js (front-matter, lexing, sectioning, the indentation tree, per-line node parsing), and
renderer.js (the gate/choice/repeat/ref/weight-sort render). dpl.js keeps only compileDpl (the
orchestrator) and re-exports intensityWord so its public surface is unchanged. No behavior change —
verified by the smoke test (every block compiles), the DPL unit + snapshot suites (identical
output), and the browser build (the import.meta.glob loader compiles through the new modules).
2026-06-29 — Build code-splitting: lazy views + focused vendor/data chunks (2.28.1)
First step of the codebase refactor (plan: notes/plans/refactor-2026-06.md). The SPA used to ship as
one 2.4 MB JS bundle. Now the three local-only views — Manage (which pulls in all of CodeMirror),
Gallery, and SingleView — are React.lazy + <Suspense> chunks that the browser fetches only
when the view is first opened (then they stay mounted, so per-view state is preserved as before). The
Vite/Rolldown build also splits React, react-intl, lodash, and the eagerly-globbed
prompt-data into their own cacheable chunks (advancedChunks). Net effect: the initial Generate-view
JS drops from ~793 KB to ~178 KB gzip (app/engine) with CodeMirror (≈55 KB gzip) no longer on the first
paint; total shipped JS is 784 KB gzip, within the 900 KB budget. No behavior, UX, or prompt-output change.
2026-06-29 — Hugging Face + Meta Llama text providers; 18 total (2.28.0)
Two more Text AI options: Hugging Face Inference (its OpenAI-compatible router with an HF token) and the Meta Llama API. The Text dropdown now lists eighteen providers — the full set of simple-key, no-fancy-auth LLM APIs from the list.
2026-06-29 — Seven more Text providers; 16 total (2.27.0)
The Text AI dropdown reaches sixteen: added Fireworks, Cerebras, Qwen (DashScope), Moonshot / Kimi, and AI21 (OpenAI-compatible, via the shared adapter) plus Anthropic Claude and Cohere (their own API shapes, via small bespoke adapters). All take a simple API key and run through the rewrite proxy — no fancy auth. They drive the prompt auto-fix / keyword-rewrite buttons.
2026-06-29 — Six more Text (prompt-rewrite) providers (2.26.0)
The Text AI dropdown grows from 3 to 9: added OpenRouter and Groq (fast, CORS-enabled, so they work online too) plus DeepSeek, Mistral, Together, and Perplexity (local-only, like the other non-CORS providers). They power the prompt auto-fix / keyword-rewrite buttons. Since most LLM APIs speak the OpenAI chat format, a new provider is now just a base URL + a model — a shared adapter does the rest, so the long tail (Fireworks, Cerebras, Qwen, Kimi, local Ollama/LM Studio, …) is easy to add next.
2026-06-29 — Five async-job enhancers: WaveSpeed, Claid, Deep-Image, neural.love, VanceAI (2.25.0)
Five more upscale enhancers, the async/submit-poll kind: WaveSpeed (Real-ESRGAN), Claid / Let's Enhance, Deep-Image.ai, neural.love, and VanceAI. That's ten hosted enhancers and seventeen AI upscalers total. (Pixelbin/Upscale.media was skipped — it's a CDN transform-URL model that doesn't fit a "send image, get image back" API.) These five are best-effort and can't be live-tested, so a couple may need a small tweak against the provider's current API.
2026-06-29 — Two more enhancers: Clipdrop + Venice (2.24.0)
Two more upscale enhancers — Clipdrop (Upscale) and Venice AI (Upscale) — bringing the enhancer count to five (with DeepAI, Picsart, Segmind) and twelve AI upscalers total. Clipdrop needs explicit output dimensions, so the source size is decoded to request a 4× target. Best-effort BYOK.
2026-06-29 — ComfyUI AI upscale (the local SD upscaler set is complete) (2.23.0)
ComfyUI can now AI-upscale a saved image: it uploads the image, runs an upscale-model graph
(LoadImage → UpscaleModelLoader → ImageUpscaleWithModel → SaveImage), and brings the result back. You
just need an upscale model in ComfyUI/models/upscale_models (the model name auto-detects if left
blank). With Forge and SD.Next, that's the full set of local, no-cost upscalers — ten AI upscalers
in all now (4 in-repo, 3 hosted enhancers, 3 local). Best-effort; verify against your ComfyUI.
2026-06-29 — Modernized the local SD (A1111) adapter + Forge/SD.Next upscalers (2.22.0)
Fixed the stale local Stable Diffusion WebUI adapter: it now sends sampler_name (the old
sampler_index was silently ignored by current A1111/Forge/SD.Next, so your sampler/seed didn't
apply), plus a scheduler field and batch_size. On top of that, Forge and SD.Next can now AI-upscale
a saved image via their Extras tab (R-ESRGAN 4x+), no key needed — a real, local, no-cost upscaler.
Best-effort; verify against your live WebUI. (ComfyUI upscale still to come — it needs a node graph.)
2026-06-29 — More enhancers (Picsart, Segmind) + the Upscaler row shows locked online (2.21.0)
Two more upscale enhancers — Picsart (Upscale) and Segmind (ESRGAN) — bringing the enhancer count to three (with DeepAI). And per feedback, the Upscaler / Enhancer row now stays visible but locked in the online build (with a tooltip pointing to the desktop app) instead of being hidden, so visitors can see the feature exists.
2026-06-29 — Upscale-only providers + an Upscaler / Enhancer row; first one: DeepAI (2.20.0)
A new provider category — upscale-only enhancers (no image generation) — plus a third row in the Providers menu: Upscaler / Enhancer, where you pick the service to enhance a saved image and add its key (below Image and Text). It's a local-only feature (the single-image view), so the row is hidden in the online build. Upscale-only providers no longer clutter the image-generation picker. First enhancer: DeepAI (Super Resolution). The ~15 other services (Topaz, Magnific, Claid, Picsart, …) slot in the same way next.
2026-06-29 — Provider expansion, phase 2c: Leonardo + Replicate upscale (the in-repo set is complete) (2.19.0)
Two more AI upscalers, completing the four providers already in the app: Leonardo AI (Universal Upscaler, up to 2×) and Replicate (Real-ESRGAN, ~4×). With Stability and fal, that's the full in-repo set — pick any of them from a saved image's resize menu (with that provider's key). Replicate runs through a new server-side upscale proxy (it can't be called straight from the browser). Local Stable-Diffusion upscalers and the brand-new hosted services are next.
2026-06-29 — Provider expansion, phase 2b: fal.ai upscale (2.18.2)
A second AI upscaler: fal.ai (Real-ESRGAN, ~4×). Pick AI Upscale · fal.ai from a saved image's
resize menu with your fal key. Two upscalers now (Stability + fal); Leonardo and Replicate are next
(each needs a heavier flow — see notes/plans/provider-expansion.md).
2026-06-29 — Provider expansion, phase 2a: Stability AI upscale (2.18.1)
The first real AI Upscale provider is live: Stability AI's fast ~4× upscaler. Pick AI Upscale · Stability AI from a saved image's resize menu (with your Stability key set) and the upscaled image lands in its Resizes strip, tracked like a re-roll. fal, Replicate, and Leonardo upscalers come next.
2026-06-29 — Deploy fixes: GitHub Pages re-enabled + Netlify continuous deploy (CI/infra — no version change)
(CI/infra only — no version change.)
The docs site and the web app weren't auto-updating after a release. Fixed both: the JSDoc doc-build
(npm run docs) now works across the engine-v3 split (code under engine-v3/, docs/notes/config at the
repo root) and tolerates JSDoc's non-fatal type warnings, so the GitHub Pages deploy on push to
main is re-enabled. A new workflow gives the Netlify app continuous deploy on push to main (it
was a manual deploy before) — it activates once a NETLIFY_AUTH_TOKEN secret is added.
2026-06-29 — Provider expansion, phase 1: AI-upscale framework + NSFW soft-lock (2.18.0)
Groundwork for supporting many more image providers. The AI Upscale option in the single view's
resize menu is now real: any provider that ships an upscale adapter shows up there (keyed providers
prompt for a key), and running it saves the upscaled result into your Resizes strip — same live, stay-on-
the-page flow as a re-roll. No provider implements it yet; the per-provider integrations come next (see
notes/plans/provider-expansion.md for the full capability assessment and roadmap).
Also new: NSFW content-policy soft-locks. Providers built for safe-for-work content (OpenAI, Gemini, Ideogram, Stability) get a small lock icon and a neutral tooltip in the Providers menu while NSFW mode is on, and a one-tap "proceed?" confirmation before they're used. It never blocks anything and never tells you what you can or can't do — just a heads-up that NSFW mode is on.
2026-06-29 — Single view, round 2: inline actions, live strips, resize, View Raw, uniform locks (2.17.0)
The re-roll / variation actions moved inline onto the prompt itself: the DPL source line has re-roll and vary links; Sent to model and Translated each have a vary link (all ask to confirm). Making another no longer jumps you to a new page — a live placeholder appears in a strip below the image and fills in when it's ready. There are now up to three strips under the image — Re-Rolls, Variations, and Resizes — each a clickable row of thumbnails.
Upsize / downsize is here: resize an image (¼× ½× 2× 4×) via ImageMagick into a new tracked image. An AI Upscale option sits alongside it, lit up only for providers that offer it (none yet — coming).
Tool menus that need ImageMagick (Convert, Resize) are no longer hidden when it's missing — they show greyed with a lock and a tooltip, so you can see the feature exists. And the details panel gains a View Raw toggle (table ↔ syntax-highlighted JSON) plus Copy as Markdown and Copy as JSON.
2026-06-29 — Single view: re-roll / variation, tracked ancestry, Markdown export, table fix (2.16.0)
The single-image view regains its v1-2 powers, adapted to v3's provider abstraction. A "Make another" cluster re-rolls or makes a variation of an image: Re-roll re-resolves the DPL recipe; Variation can draw from the DPL recipe, the AI translation, or the original engine roll. Each makes a brand-new image with a fresh seed, so the cluster is locked (with a tooltip) for providers that don't support seeds (e.g. OpenAI). The view shows a loading placeholder while it generates, then lands on the new image.
Ancestry is tracked again, the lightweight v1-2 way: every derived image keeps its parent's id, and the gallery feed scan rebuilds the reverse child list (self-healing if a parent is deleted). A new Lineage card shows whether an image is a Base / Re-roll / Variation, links up to its parent, and shows a strip of its derived children — all clickable.
Also on the details panel: a Copy as Markdown button (prompt + negative + details as a tidy block), and a fixed table — sampler/steps/cfg/seed rows now appear only for providers that actually support them, so an OpenAI image no longer shows another provider's settings. New image sidecars also store a provider-scoped settings snapshot, fixing the leak at the source.
2026-06-29 — Test coverage, phase 5: cross-browser, performance & CI (no version change)
(Test/CI-only — no version change.)
Closed out the coverage build-out with the cross-cutting layers. Cross-browser: Playwright
gains Firefox + WebKit + Pixel-7 mobile projects behind PLAYWRIGHT_ALL_BROWSERS (the new
test:e2e:all wrapper, cross-platform via scripts/run-e2e-all.mjs); visual-regression stays
Chromium-only. Performance: a gzipped-JS bundle-size budget (scripts/check-bundle-size.mjs,
npm run test:perf — 763 KB vs a 900 KB budget) as a hard gate, plus Lighthouse CI
(lighthouserc.json, npm run test:lhci) as an informational report. Coverage gates are now
enforced in CI: the check and gui jobs run test:coverage so the Node thresholds (≈engine 90%)
and the SPA src/lib/** floor actually fail a regression. CI adds cross-browser and perf
jobs. Living testing notes updated (notes/plans/testing.md). Total: 419 headless tests pass
(Node 226 + SPA 193).
2026-06-29 — Test coverage, phase 4: component + interaction tests (no version change)
(Test-only — no version change.)
React Testing Library + user-event component tests for the highest-value interactive
components (render, the key interaction, and disabled/locked/empty/error states):
NsfwToggle (confirm-on-enable gate, immediate disable, Escape, online-locked →
full-version), PromptResult (generate button, click-to-copy, busy skeleton, per-image
remove, clear), DplStatus (✓/✕/warn from the real validator), DplInsertBar (category
popovers, snippet insertion at the cursor, Escape), ProviderPicker (grouped dropdown,
selection, key badge, locked option), ApiKeyField (renders only for BYOK providers,
session entry, explicit save/confirm). 193 SPA tests pass (was 165). Heavier
components (the CodeMirror editors, Home, the Manage tree editors) are exercised by the
Playwright e2e flows rather than unit-mounted, and remain a documented follow-up in
notes/plans/testing-coverage-plan.md.
2026-06-29 — Test coverage, phase 3: provider adapters + transport (no version change)
(Test-only — no version change.)
MSW-backed contract tests for the image/text provider layer that was almost entirely
untested (only local-webui + midjourney before). Covers the shared transports
(hostedProxy.callProxy, localDirect.postJson/getJson/normalizeBase incl. the
object-error + node_errors readable-error path, the generic submitPoll done/failed/timeout),
the hosted server adapters asserting real request shape + response mapping + errors
(OpenAI images, Replicate Prefer:wait, Stability multipart, fal Key auth, Gemini inline
base64), the rewrite adapters (OpenAI/Grok chat, Gemini generateContent), the
browser-facing generate wrappers (OpenAI/Gemini browser-direct, Replicate via proxy,
ComfyUI local submit→poll over /api/forward), and the proxy hub (dispatch /
dispatchRewrite + the Netlify generate/rewrite handlers: 405/400/200/502 paths). Also
prettier-formatted four phase-1 test files. 165 SPA tests pass (was 128).
2026-06-29 — Test coverage, phase 2: SPA lib + MSW (no version change)
(Test-only — no version change.)
Stood up MSW (Mock Service Worker) for the SPA suite (gui/tests/msw/, wired into
tests/setup.js with onUnhandledRequest: "bypass" so existing fetch-stub tests are
unaffected) and added unit coverage for the previously-untested gui/src/lib modules:
keywords (weighting/attention/BREAK-AND stripping, accent-folded dedupe, caps),
manageTree (categories, implied groups, force-prefix, NSFW hiding, ghosts, filter),
output (MSW-backed ingest/file-actions/meta + the data→blob new-tab opener), rewrite
(browser-direct vs proxy fallback + error, registry mocked), online, sessionKeys,
providerMeta, wrapperStore, the DPL insert catalog (dplInserts), the dialects
map, and the useProvider hooks. 128 SPA tests pass (was 60).
2026-06-29 — Test coverage, phase 1: engine core (no version change)
(Test-only — no VERSION/package.json change, per the versioning rule that test/CI/notes
commits don't move the number.)
First phase of the comprehensive test-coverage build-out (plan:
notes/plans/testing-coverage-plan.md, owner-approved). Added direct unit coverage for the
engine modules that were previously exercised only indirectly: the random* keyword helpers
(randomEmphasis across SD/NAI/MDJ/Plain, randomEditing, randomAlternating), the
loader-injected stages (listStore, list, block), blockManifest,
promptFilesAndSuggestions, engine API edges, aliases, and a settings shape guard;
plus extra listManifest cases (auto-prefix collision, keyword wildcard, group cycle/safety)
and a real-data nodeLoader integration test. Each module is driven across valid / invalid /
boundary inputs; the lodash-RNG landmine is handled with chance extremes (0/1) and min==max
settings. 226 Node tests pass (was 138). Fixed the stale vitest.config.js coverage
include (it pointed at a non-existent src/diffSettings.js) and added an enforced CI coverage
gate (statements 88 / branches 76 / functions 88 / lines 90; engine now ~93% lines).
2026-06-28 — 2.15.0: SPA internationalization (react-intl + FormatJS pipeline)
(MINOR — a notable feature set. VERSION + package.json → 2.15.0.)
The whole React SPA (gui/) is now internationalized with react-intl. Every user-facing string
across all ~28 components — visible text, title/placeholder/aria-label attributes, confirm/
prompt dialogs, and Intl-correct plurals/numbers — is wrapped in a defineMessages /
FormattedMessage / intl.formatMessage call, ~480 messages in total (incl. the DPL lint + insert-catalog modules). English output is byte-identical
to before, so the Playwright text/visual baselines are unaffected.
New i18n module gui/src/i18n/: config.js (locale registry + resolveLocale browser-preference
resolution), loadMessages.js (Vite import.meta.glob over compiled catalogs), and I18nProvider.jsx
(wraps the app in react-intl's IntlProvider, syncs <html lang/dir>, quietly falls back to English on a
missing key). App.jsx was split into a thin root (owns settings + the i18n boundary) and an AppShell.
A Display language selector was added to Settings; the choice persists in settings.locale
("auto" follows the browser).
Full FormatJS tooling pipeline. babel-plugin-formatjs (wired into @vitejs/plugin-react) auto-fills
message IDs to match the extractor; @formatjs/cli powers npm run i18n:extract (→ src/i18n/messages/en.json)
and npm run i18n:pseudo (→ a compiled en-XA pseudo-locale — accented/expanded English that makes any
un-internationalized string obvious). A focused gui-scoped ESLint config (gui/eslint.config.js,
npm run lint:i18n) runs eslint-plugin-formatjs's enforce-default-message.
Scope: only the shipped real locale is English — the app's DPL/prompt domain jargon would make
machine-translated languages low-quality, so none are shipped (the pipeline makes adding one a one-file
job). Coverage is complete, including the two DPL-technical lib modules: validateDpl.js (editor lint
diagnostics) now takes an optional intl and uses a built-in react-intl createIntl English fallback
so its message-asserting tests stay green, and dplInserts.js (the DPL syntax teaching catalog) became a
getDplInserts(intl) builder — the DplEditor linter and DplInsertBar thread their intl through.
~480 messages total. Tests: a tests/testUtils.jsx IntlProvider render wrapper keeps the component
suite green (60 web tests pass; the 9 validateDpl tests pass unchanged on the English fallback).
2026-06-28 — 2.13.0: DPL focus dial + global layer auto-merge; dial keywords move to $ sigil
(MINOR — a notable language feature set. VERSION + package.json → 2.13.0.)
Three related additions to the DPL engine (src/core/dpl/dpl.js, src/core/stages/block.js,
both loaders):
- Focus dial — a sibling of intensity. A second per-reference dial (1–100, default 50), carried as
{#name f80%}, with[f<NN%]line conditions and the$focus/$focus-wordkeyword. Focus is "how pure / how narrow" the render is: low focus admits fluff / extra / unrelated detail, high focus keeps only what is strictly essential (which also makes a generator stack cleanly as a layer). Unlike intensity it does not auto-scale gates/counts — it is author-judged per line (an AI/human decides what is fluff at what focus), which is exactly the lever the deleted "fluff" keywords needed. Threaded to generators as a 5th argument (ctx.focusin.dpl; 5th param in.jssidecars). - Mandatory
i/fprefixes + the$keyword sigil. Conditions and token args must prefix the dial —[i<10%]/[f<40%],{#name i25% f80%}— because the two percents are visually identical; an unprefixed25%/<10%is not dial syntax (a bare[10]is still a weight). Keyword interpolation moved off{intensity}(which collided with{list}syntax) to the$sigil: since the dial is a percent,$intensityrenders50%(no separate%form) and$intensity-wordthe magnitude word, plus the$focusset. All in-tree content was migrated losslessly via byte/EOL-preserving PowerShell passes —{intensity…}→$intensity-word(28 files) and the bare[<NN%]conditions →[i<NN%](18 files). - Global layer auto-merge (dedup). Imported generators behave as one-time global layers: a generator
renders once per prompt, so a second nested import of the same singular generator (e.g. two scenes
both pulling
{#weather}) is dropped. User-typed duplicates always render. A generator that legitimately repeats opts out withstacking: truefront-matter — set on the chained decoratorscolor/glow/neon/crystalso the change doesn't regress them. - Editor support for all of the above. The shared CodeMirror DPL language (
gui/src/lib/dpl/) now highlights the dial args (i25%/f80%),[i<NN%]/[f<NN%]conditions,$intensity/$focus, the front-matter block (fences / keys / values), section heading names (withStart/Auto Begin/Auto Endemphasized), and thego to/insert/+callname targets. New context-aware autocompletes: thei/fdials (with intensity-vs-focus descriptions) pop up on a space inside{#name …}; front-matter keys inside---; section names aftergo to; and generator- section names after
insert/+. Also normalized every block to a blank line after the front-matter fence (89 files).
- section names after
Tests: DPL unit + engine-integration suites extended (focus conditions/keywords, dedup, stacking,
back-compat); 119 Node + 51 SPA tests green, smoke + gui build clean. Design notes:
reference/focus-design.md + reference/layering-design.md added, reference/intensity-design.md
updated to the $/i/f syntax.
2026-06-28 — 2.12.1: Manage tab only mounts when the real backend answers (not a static 200)
(PATCH — fixes shipped behavior + main CI.)
managerAvailable() gated the Manage tab on res.ok from GET /api/manage/ping. On a static host
(the online build / Playwright's vite preview) unknown routes fall back to the SPA's index.html with
HTTP 200, so the probe was a false positive — Manage mounted where it shouldn't (its API calls then
fail). It also broke main CI: with Manage mounted, the E2E visual spec's .sidebar locator matched
two elements (Generate's + Manage's — both view-panes stay mounted) → strict-mode violation. Fix: the
probe now parses the response and requires the real JSON { ok: true }; an HTML fallback fails the
parse and reports unavailable. So Manage shows only in true local mode, and E2E sees a single sidebar
again (no test changes needed). VERSION + package.json → 2.12.1.
2026-06-28 — 2.12.0: the Manage tab (in-app content manager) + tests/docs
(MINOR — a feature milestone. VERSION + package.json → 2.12.0. Built across phases 1–5 on
feature/manage-tab, summarized here for the release.)
A new 4th SPA tab, Manage — edit the catalog (blocks, lists, folders/categories, sidecar options)
right in the app, on the real data/ files, with live hot-apply. Local mode only (gated on a
file-backend capability probe; locked online). Highlights: a runtime disk-snapshot loader the engine
reads through (runtimeLoader.js) so edits apply with no reload (except an edited .js module body —
reloads); the real nested folder tree with category/subfolder color-coding, force-prefix/group badges,
abstracted _-markers, NSFW gating, and search; block editor (DPL + JS-sidecar tabs / create-from-
boilerplate), folder editor (rename, priority/description/forceList, marker toggles), and list editor
(virtualized entry mode + raw CodeMirror, seamless at 27k lines); add/delete, drag-to-move, restore-
default from main, ghost pills for locally-deleted-but-upstream files (diffed against a published
data/manifest.json, disk-cached a day), and external-edit auto-refresh (SSE fs.watch). Backend:
gui/server/manageFs.js + /api/manage/*. New dep @codemirror/lang-javascript. Contract tests in
tests/integration/manageFs.test.js (11, green); full npm test green (Node 111 + SPA 51). Docs:
plans/manage-tab.md, reference/dependencies.md, status.md. Follow-up: jsdom component tests +
Playwright e2e/visual baselines for the tab.
2026-06-28 — Manage tab, phase 5: add/delete, drag-to-move, external-edit watch
(No version change yet; feature/manage-tab.)
- Add controls: an always-visible "+" on every folder and each root opens a small menu — New block/list (creates the file, with minimal DPL boilerplate for blocks, and opens it for editing) or New (sub)folder (empty folders now show in the tree). Names sanitized.
- Delete: entry pills now have a Delete action (removes content + JS/JSON sidecars, confirmed); the folder editor has Delete folder (recursive, confirmed) and clears the selection.
- Drag-and-drop move: entry pills are draggable onto any folder (or a root) in the same root, moving the content + sidecars; the drop target highlights. (Per the owner, blocks ride the app's existing name/priority sort — drag is move-between-folders, not a custom order; category priority stays a numeric field in the folder editor.)
- External-edit watch:
GET /api/manage/watchis an SSE stream backed byfs.watchon both roots; the client debounce-refreshes on change, so editing a file in another editor updates the tab live. The manual Refresh button remains the fallback. manageTree: the folder filter now keeps genuinely-empty folders (so a new one shows) and only hides NSFW-named folders when adult is off.- Build green.
2026-06-28 — Manage tab, phase 4b: ghost (restorable) entries + published file manifest
(No version change yet; feature/manage-tab.)
Files deleted locally but still present upstream now show as ghost pills in the tree — faded, dashed, with a single Restore action (no edit/delete); wholly-deleted folders reappear as ghost folders (no gear). Restoring fetches the file from the stable branch and it becomes a real entry.
scripts/build-data-manifest.mjs(new) +npm run manifest+data/manifest.json: a published static manifest listing every content file per root, regenerated at release. Ghost detection is a simple set difference (manifest − local), so there's no GitHub-API tree scrape (owner's suggestion — simpler, no rate limits).manageFs.remoteManifest: fetchesmain/.../data/manifest.jsonand disk-caches it in the OS temp dir for a day (filename carries arepo@branchhash) — checked on boot, re-downloaded at most ~once/day, and falls back to the stale cache when offline (owner's caching request).GET /api/manage/remote-manifest(+?fresh=1).manageTree.js:computeGhosts(manifest − local tree, entry files only, NSFW-gated) +injectGhosts(places ghosts in their folder, synthesizing ghost folders);Manage.jsxfetches the manifest, injects ghosts, and renders restore-only ghost pills.- Verified: ghost detection is exact (none when intact; the one deleted file flagged with correct kind); manifest generates (76 lists / 89 generators); build green.
2026-06-28 — Manage tab, phase 4: list editor (entry + raw) + restore-default
(No version change yet; feature/manage-tab.)
ManageListEditor.jsx(new): edits.txtlists and.groupfiles in two modes — Entries (default): a searchable, virtualized row list (only the visible window renders) with quick add / inline-edit / delete, smooth even on the 20k–27k-line lists, no size warnings; and Raw: the whole file in the plain-textCodeEditor. The file is fetched once and entry edits mutate an in-memory line array (nothing re-serializes per keystroke); trailing-newline is preserved. Plus rename, a description sidecar, and Restore default.- Restore-default:
POST /api/manage/restore+manageFs.restoreFromRepofetch the original from the stable branch and overwrite the local copy (404 upstream ⇒ delete local). Branch ismain, notmaster:masteris a stale old-layout branch with noengine-v3/tree (restore would 404); the owner confirmedmain, which carries the current layout. Verifiedmain's raw URL serves the files (200) and the 404→delete path works. Manage.jsx: routes list/group entries to the list editor;openEntryno longer pre-loads (each editor loads its own file).
2026-06-28 — Manage tab, phase 3: block + folder/category editors (save + hot-apply)
(No version change yet; feature/manage-tab. Adds dep @codemirror/lang-javascript — to be recorded
in reference/dependencies.md + list-credits.md at ship.)
ManageBlockEditor.jsx(new): edit a generator's.dpl(reusesDplEditor), name (rename moves the.dpl/.js/.jsontogether), description, and NSFW flag (gated — disabled with the "NSFW option only available in NSFW mode" tooltip when adult is off, and shown forced when the name carries annsfwtoken). JS sidecar support: a DPL ⇄ JS tab when a.jssidecar exists, a one-click Create JS sidecar that scaffolds from boilerplate into a JavaScript CodeMirror, and a save that notes "reload to run the changed JS" (DPL + sidecar metadata hot-apply; only JS execution waits for a reload).ManageFolderEditor.jsx(new): rename a folder, edit its sidecar (priority, description, and — lists only — forceList), and toggle its_-markers as plain controls: a Force-prefix checkbox and a group mode (Auto / Always / Never → the enable/disable markers). Saves hot-apply.CodeEditor.jsx(new): a small reusable plain-CodeMirror editor (JS now, raw lists next), built for very large documents (viewport rendering).- API:
POST /api/manage/{sidecar,marker,fs}(+ themanageFshelpersmergeSidecar,setMarker,fsOpfor mkdir/mkfile/delete/move) and client wrappers. Sidecars self-delete when emptied; all ops traversal-guarded; atomic writes. Manage.jsx: routes a generator's Edit to the block editor and a folder gear to the folder editor;handleChangedhot-applies and (on rename) reselects the moved item.manageTreenow exposes per-folderenableGroup/disableGroupso the editor shows the true group state.- Verified: create → sidecar → marker → rename(+sidecar) → delete all round-trip through the snapshot, traversal is blocked, and the throwaway folder is cleaned up (9/9 headless checks); SPA build green with the new dep.
2026-06-28 — Manage tab, phase 2: the tab + the real folder tree (read-only)
(No version change yet; feature/manage-tab. MINOR bump lands when the feature ships.)
gui/src/components/Manage.jsx(new): the Manage tab — Generate's two-pane skeleton, but a content manager. Left: the real nested folder tree of both roots (Blocks = blocks, Lists = lists) with categories vs subfolders color-coded, force-prefix / implied-group folders badged,_-markers abstracted into those badges, a per-folder gear by the name, entry pills with a hover Edit action (clicking a pill never inserts), a search filter, and a Refresh catalog button. Right: a detail/preview pane (the real editors come next). NSFW is gated by the header toggle.gui/src/lib/manageTree.js(new): builds the display model fromGET /api/manage/tree, computing categories / force-prefix / implied-group semantics with the engine's ownlistManifest+gatedListshelpers (so the view can't drift from real behavior) and a name-filter.App.jsx: Manage added as the 4th tab (after Single), gated on a local-mode capability probe (managerAvailable) rather than the build flag — locked with a hint online / on a static host, and only mounted when the backend is present. Header providers/gear stay hidden on this tab; the NSFW toggle stays.styles.css: Manage styling (tree rails by category/subfolder/special, kind-colored entry pills, badges, the detail pane).- Verified: model reproduces the real structure (Blocks: 7 categories incl. force-prefixed
prompt- nested
expansion/*groups; Lists: 11 categories with force-prefix on artist/danbooru-d/lore/name/ style), NSFW gating drops the 4 adult lists when off; SPA build green.
- nested
2026-06-28 — Manage tab, phase 1: runtime (disk-backed) catalog loader + local-mode API
(No version change yet; foundational plumbing on feature/manage-tab, no UI — the MINOR bump
lands when the tab ships.)
First slice of the in-app content Manage tab (plan: notes/plans/manage-tab.md). This phase adds
the runtime hot-apply foundation, no UI:
gui/server/manageFs.js(new): the local-mode content backend — traversal-guarded fs helpers overdata/lists+data/blocks: a full-catalog snapshot builder, a folder-tree builder, a safe path resolver, and an atomic file writer. Extracted as its own module so it's unit-testable in plain Node and reusable by a future production local build.gui/vite-plugin-api.js: new/api/manage/*endpoints —ping(capability probe → unlocks the tab in local mode),snapshot,tree, andfile(GET read / POST atomic write).gui/src/lib/runtimeLoader.js(new): a third engine loader implementing the same interface asbrowserLoader/nodeLoader. Until a snapshot is fetched it delegates to the build-time bundle (so first paint and the online build are unchanged); once a snapshot is installed it serves the catalog live from disk (lists,.dpltext compiled at runtime, groups, sidecars, markers), delegating only.js-module execution + presets to the bundle (running fetched JS would need eval).gui/src/lib/manageApi.js(new): client for/api/manage/*(snapshot, tree, file r/w,managerAvailableprobe).gui/src/lib/promptEngine.js: now drives the engine through the runtime loader;getBlocksrecomputes the catalog live; addedrefreshCatalog()(fetch snapshot →loadAll()→ notify) plus asubscribeCatalog/getCatalogVersionpub-sub for hot-apply.gui/src/core/browserLoader.js: exportdpJsModule(key)so the runtime loader can reach the bundled.jsgenerators/sidecars.App.jsxcallsrefreshCatalog()on load (no-op online);Home.jsxsubscribes so the palette re-renders on a hot-apply.- Verified: the snapshot reproduces the Node loader's catalog exactly (87 lists, 89 generators,
matching group/force-prefix dirs;
dpJs=0— every generator is.dpl, so the JS-reload boundary is effectively moot). SPA build +npm run smokegreen.
(PATCH; setup ergonomics.)
engine-v3/package.json: added"postinstall": "npm --prefix gui install", so a singlecd engine-v3 && npm installnow installs both the engine and thegui/SPA dependencies. Removes the fresh-clone footgun wherenpm run webfailed withCannot find package '@vitejs/plugin-react'because the gui deps were never installed.npm run web:installis kept for reinstalling just the gui deps. README updated to the single-install path. Harmless in CI (the existingguijob still installs/builds the SPA independently).
2026-06-28 — README: clearer "build & run from source" for fresh clones
(Docs only; no version change.)
README.md: a fresh clone that ran onlynpm installthennpm run webfailed withCannot find package '@vitejs/plugin-react'— the SPA ingui/is a separate npm package and its deps were never installed. The engine-v3 quick-start now lists the two installs explicitly (npm install→npm run web:install→npm run web), with a one-line note on why there are two. Production build (web:build) is its own block, and testing (npm test/test:e2e) moved to a separate Development subsection so the run-from-source path isn't cluttered with dev steps. Also fixed the staleweb-app/→gui/reference.
2026-06-28 — 2.11.1: open generated images reliably in a new tab (online build)
(PATCH; online-build bug.)
gui/src/lib/output.js+PromptResult.jsx: clicking a generated image in the online build navigated the<a href={img}>directly. Fordata:URLs (OpenAI/Gemini/Grok/Stability return base64) browsers block top-leveldata:navigation (intermittent "nothing happens"); for remote URLs it opened the remote address instead of the cached image. NewopenImageInNewTab()converts adata:URL to ablob:object URL synchronously (so thewindow.openstays in the click gesture and isn't popup-blocked) and opens that;blob:/ served / remote URLs open directly. The image<a>now intercepts the click and uses it when there's no single view (online). lint clean, build green, tests green.
2026-06-27 — 2.11.0: online build goes fully static (browser-direct generation, no functions)
(MINOR; makes the free online deploy actually viable.)
The Netlify free tier's function limits (125k invocations/mo and a 10s timeout) made the serverless BYOK proxy a poor fit for heavy/bulk generation. Fix: in the online build, call each provider's API straight from the browser with the visitor's own key — no backend, so no cap and no timeout. A live CORS-preflight check decided which providers can do this.
transport: "browser-direct"for OpenAI, Gemini, Grok, Stability, Leonardo, fal.ai (all send CORS headers). Theircode/generate.jsnow calls their owncode/server.jsdirectly instead of posting to/api/generate.src/lib/rewrite.jslikewise calls the provider's rewrite adapter directly (OpenAI/Gemini/Grok), so auto-fix/keyword rewrite is also backend-free online.- Replicate, Black Forest Labs (FLUX), Ideogram have no usable browser CORS → kept on
hosted-proxyand locked in the online build (greyed, with a tooltip explaining they can't be called from a browser + a link to the desktop version), alongside the local providers. availableProviders()online now excludeshosted-proxytoo; the App online-fallback picks the first browser-direct image provider.lockedHint()takes an optional reason; ProvidersMenu/ ProviderPicker pass a per-provider lock reason.- The deployed site now uses zero serverless functions. The functions +
server/dispatch.jsstay in the repo for the local dev proxy (desktop full version → non-CORS providers). Stays on Netlify (static hosting) at prompt.fairyfox.io. - Verify: lint clean, online build green (529 modules),
npm testgreen (100 Node + 51 jsdom).
2026-06-27 — 2.10.5: fix serverless BYOK proxy crash (adapter is not a function)
(PATCH; surfaced on the first live Netlify deploy.)
gui/server/dispatch.js— Netlify's function bundler hands an ESMexport defaultback as a namespace object ({ default: fn }), so the static default imports of the providercode/server.js/code/rewrite.jsadapters weren't callable in the deployed bundle —dispatchthrewadapter is not a functionand both/api/generateand/api/rewritereturned 502. (Worked locally under native ESM, which is why it wasn't caught.) Added anasFn()unwrap applied to everyserverAdapters/rewriteAdaptersentry, so both module shapes work. Verified on the live site: the dummy-key call now reaches OpenAI ("Incorrect API key") instead of crashing. lint clean, 51 jsdom tests green.
2026-06-27 — 2.10.4: online build shows local-only features disabled (with a link), not hidden
(PATCH; owner request — prep for the prompt.fairyfox.io Netlify demo.)
- New
gui/src/lib/online.js— sharedONLINEflag,FULL_VERSION_URL,lockedHint(feature), andopenFullVersion(). Single source for the online-build "this is in the full version" behaviour. App.jsx— the Gallery/Single tabs and the NSFW toggle are now always rendered; in the online build they show disabled (greyed, lock badge, tooltip; clicking opens the full version) instead of being omitted. Online also auto-switches a saved local provider (e.g. the default ComfyUI) to the first hosted provider so Generate works immediately.NsfwToggle.jsx— newlockedprop (greyed + tooltip + link; forced off).ProvidersMenu.jsx/ProviderPicker.jsx— local providers are shown disabled (🔒 "full version" badge) online rather than filtered out.styles.css—.is-lockedstyling for tabs, the NSFW switch, and provider rows.netlify.toml—[build.environment] VITE_ONLINE = "true", so a Netlify build is the online variant with no dashboard config.reference/deployment.mdupdated with theprompt.fairyfox.iosetup steps.- Verify: lint 0 errors, online build green,
npm testgreen (100 Node + 51 jsdom).
2026-06-27 — 2.10.3: add {#rays} back to the default wrapper end
(PATCH; owner request.)
gui/src/lib/wrapperStore.js—DEFAULT_WRAPPER_SEED.end:{#fx}, {#artists}→{#fx}, {#artists}\n- {#rays}. God rays return as the optional ~50% bullet (the only piece the owner wanted kept from the old end); the rest stays tossed. Comment updated. SPA build + gui tests green.
2026-06-27 — 2.10.2: strip quality-spam from the default wrapper
(PATCH.)
gui/src/lib/wrapperStore.js—DEFAULT_WRAPPER_SEED: removed the quality-booster trash. The START wasmasterpiece, best quality, highly detailed→ now empty; the END was{#fx}, {#artists}+intricate detail/sharp focus/wide shot/{#rays}/{#dap}→ trimmed to just{#fx}, {#artists}(random art-style + artist variety — the generator's actual feature — with nomasterpiece/best quality-class filler, no forced framing, no art-site tags). Comment updated.WrapperFab.jsx: the wrapper-start input placeholder no longer suggestsmasterpiece, best qualityas the example.- New/reset installs get the clean default; existing users keep their saved wrapper until they reset it
(it lives in
localStorage, which the seed change can't touch). SPA build + gui tests green.
2026-06-27 — 2.10.1: tail cleanup of the block refactor (coffecup typo, beach-merk de-scatter)
(PATCH.)
expansion/subject/coffecup: content typocoffecup→coffee cup(the token/filename stays).user/beach-merk(community submission): de-scattered like the corebeach— dropped the doubled{#city}pulls, deduped repeated synonyms (oceanside/sand/palm trees), and removed the off-theme{#color}crystal/gemstone; kept Merk's character (palm trees, tropical, oasis/vegetation extra), made waves{intensity}-aware, and preserved the credit.- Confirmed nothing auto-pulls the opt-in
{#legacy}/{#dap}packs (themasterpiece, 8K, hyper-detailedfiller now lives only in a deliberately-invoked expansion — no generator injects it). Closes out the catalog-wide sweep: no remaining inlineotherwise:colon bugs, typos, or dead bare-#refs.
2026-06-27 — engine: auto-appended fx/artists now re-resolves nested tokens (root-cause fix)
(On feature/dpl-intensity; part of the 2.10.0 work.)
src/core/stages/block.js: the auto-{#fx}/{#artists}append was moved to run before the{#…}resolver loop and now appends the tokens ({#fx},{#artists}) rather than their pre-rendered output. So any nested{#…}they emit resolves in the same passes. This is the root cause behind the literal{#rays}leak (thefx.dplcontent fix removed the symptom; this removes the cause — any future auto-appended generator with nested tokens is now safe). New engine integration test; 0/50 leaks with auto-fx + auto-artists on. Node suite 99 → 100 green.
2026-06-27 — prompt/ batch + fx leak fix (Phase 2 batch 5 — final category of the intensity overhaul)
(Content batch on feature/dpl-intensity; no version bump — part of the 2.10.0 work.)
fx.dpl: replaced the trailing nested{#rays}token with inlinegod rays, volumetric lighting. Auto-{#fx}is appended after the resolver loop, so that nested token used to survive in output as a literal{#rays}; inlining it removes the leak (0/40 leaks now with auto-fx + auto-artists on). The underlying engine auto-append quirk remains, but no shipped auto-appended generator emits a nested#-token anymore.- The rest of
prompt/(artists,d,random,random-words,simple-random,extra-random) are legitimate JS builtins (keyword/artist repeaters, the danbooru stream, the suggestion engine) — random by design — and were left as-is. - This completes the block content refactor across all five categories (scene, fragment,
subject, style, prompt);
expansion/is the deprecated tree. smoke + Vitest (99 + 51) + SPA build green.
2026-06-27 — style/ block refactor (Phase 2 batch 4 of the intensity overhaul)
(Content batch on feature/dpl-intensity; no version bump — part of the 2.10.0 work.)
- Renamed the flagged
anime-irl→anime-realism(clean break): the name now says what it is. Updated the dynprompt-meta description map and fixed the{#underwater-anime-irl}expansion that referenced it — it used the dead bare#anime-irlsyntax, so it had silently stopped resolving; it now reads{#anime-realism}and works again. De-scatteredanime-realism(dropped the{#expressive}/{#weather}/{instrument}pulls); kept themakoto shinkai / wlop / artgerm / rossdrawsartistAuto End— that artist set is the style (the owner's "some files use artists" case). retro-posterde-scattered (dropped{#weather}/{instrument}; kept its style-artist line). Fixed typos in the isometric set (blue blackground→background, doubledsoft colors).- Left the other style recipes intact — their render tags (
3d blender render,physically based rendering,Pixar render, etc.) genuinely define those publicprompts looks and are not gratuitous quality-spam. smoke + Vitest (99 + 51) + SPA build green.
2026-06-27 — subject/ block refactor (Phase 2 batch 3 of the intensity overhaul)
(Content batch on feature/dpl-intensity; no version bump — part of the 2.10.0 work.)
knightde-scattered (the flagged case): dropped{#landscape}and[[castle]], removed the render-farm Auto End (unreal engine 5,octane render,ray tracing,hdr, …) and thehyperrealistic/detailed face/detailed bodyfiller. Now framed with{#portrait}, a one-of armor pick,{emotion},{#general-state}, and[>70%]/[<25%]detail conditions.- Portrait family de-scattered + bug-fixed: removed the
{#expressive}/{#weather}/{instrument}pulls fromportrait-personandportrait-princess; fixed inlineotherwise:colon artifacts (portrait,portrait-princess, and the earlierbeach) andsceptor→scepter. Portraits now use{#portrait}framing and thelook/lists ({expression},{emotion}).furry/wildlifetidied. - The
entity*.jstype-system sidecars (animal,person,living-entity,entity-name,entity) are legitimately JS and were left as-is. smoke + Vitest (99 + 51) + SPA build green.
2026-06-27 — fragment/ block refactor (Phase 2 batch 2 of the intensity overhaul)
(Content batch on feature/dpl-intensity; no version bump — part of the 2.10.0 work.)
- Polished 9 of the 15
fragment/*.dplgarnishes. Typos fixed:accesories→accessories(room-state),mesmorizing→mesmerizing(mystical). Trimmed over-piles: room-state's giant damage list and general-state's filler (detailed,messy,holes); droppedunderwater paradise. - Intensity-aware:
{intensity}magnitude on clutter / crystal formations / snow drifts / embers / foliage / schools of fish / wear. A few sensible synonyms added (ethereal, otherworldly, ominous, foreboding, amethyst, quartz, icicles). The 6 already-clean tiny pools (color,glow,neon,water,weather,expressive) were left as-is. Descriptions unchanged, so no meta churn. - smoke + Vitest (99 + 51) + SPA build green; eyeballed low/high-intensity output per fragment.
2026-06-27 — scene/ block refactor (Phase 2 batch 1 of the intensity overhaul)
(Content batch on feature/dpl-intensity; no version bump — part of the 2.10.0 work.)
- All 26
scene/*.dplrewritten to be focused and filler-free. De-scattered:beachno longer pulls{#city};cave/mountains/landscapedrop{#settlement};futuristic/spacedrop the{#portrait-*}/{#ruins}/{#city}/{#animal}pulls;space's{#ship}(a sea ship) → the correct{#spaceship}. Synonym piles trimmed (ruins/ship/vehicle), typos fixed (interrior→interior). - Wired into the lists scenes were ignoring —
{view},{time},{mood},{style/building|general|construct}. - Intensity-aware: scenes use the new
{intensity}keyword for magnitude ({intensity}waves / stalagmites / crowd / sails / decay) and[>70%]/[<25%]conditions for sparse-vs-lush detail;great-tree/great-bridgebias large with{intensity +40%}/{+30%}. - 5 JS sidecars converted to pure DPL (
futuristic,great-bridge,great-tree,space,spaceship) and the.jsdeleted; the size-helper logic became the{intensity}keyword. Meta sidecars regenerated. smoke + Vitest (99 + 51) + SPA build all green.
2026-06-27 — 2.10.0: DPL intensity dial ({#name NN%}, conditions, auto-scaling, {intensity} keyword)
(MINOR — a notable new DPL language capability: a per-reference "how much" dial that flows into a generator and reshapes what it renders. Engine + tests + design note; the content refactor that uses it follows in batches.)
A {#name} reference can now carry an intensity percent, and generators react to it.
- Token argument —
{#great-bridge 25%}runs the generator at 25% intensity (1–100;0%→1%; unspecified → the default 50%, top-level and nested alike). Parsed insrc/core/stages/block.jsand handed to the generator as a 4th argument (mod.default(settings, imageSettings, upscaleSettings, intensity));.dplseesctx.intensity,.jssidecars read the 4th param (loaders updated to pass it). - Line conditions — a square-bracket slot now also takes an intensity condition:
[<10%] - grassrenders only when intensity < 10%. Operators< <= > >= = == !=; stackable with a weight, separated by a pipe or a space, either order ([100|<10%],[100 <10%],[<10% 100]). Non-spec brackets ([[castle]],[deemph],[a:b:0.5], the salt literal) still pass through as payload. - Auto-scaling — probability gates multiply by
intensity/100, andrepeat/one of/N ofcounts scale byround(count × intensity/100). Plain always-on lines and the bare-otherwisebranch are not scaled. (Because the default is 50%, an un-dialed generator now renders lighter than before — a deliberate, retunable choice viaDEFAULT_INTENSITY.) {intensity}keyword — text can react too:{intensity}→ a magnitude word (tiny/small/normal/large/huge/massive),{intensity%}→ the percent,{intensity-num}→ the number. e.g.{intensity} amount of grass.- Relative / derived intensity — any intensity reference accepts a signed modifier taken of the
value:
{intensity +25%},{intensity% -10%}, and relative refs{#weather +25%}/{#clouds -40%}(rewritten to an absolute percent inside the DPL renderer, which knows the base). - Tests — new DPL unit coverage (conditions, operators, weight/condition stacking, gate + count
scaling, the
{intensity}keyword + relative modifiers, clamping) and two engine-pipeline integration tests proving{#name NN%}threads end-to-end; the seeded DPL snapshot was updated for the 50% default. - Design —
notes/reference/intensity-design.md.
2026-06-27 — 2.9.0: provider header redesign (Providers dropdown, BYOK keys, Prompt/Negative switch)
(MINOR — a notable GUI feature set reworking how providers, keys, and the negative prompt are chosen. One cohesive change to the header + composer.)
Header is now a single Providers dropdown + a settings gear + the NSFW switch.
ProvidersMenu.jsx(new) — one header trigger opens a panel with two stacked rich pickers (ProviderPicker.jsx, new): Image (grouped Local — incl. Plain text — / Online) and Text (Off + the rewrite AIs: OpenAI / Gemini / Grok). Each row carries its BYOK key field on the right in a fixed two-column grid so the rows align and the key box keeps one width. Selecting the image provider still sets the engine dialect/mode; the text provider setsrewriteProvider.- BYOK key moved to the header (
ApiKeyField.jsx, new) — one compact pill per provider with the save / clear controls inside the field, plus the info tooltip and "Get a key" link. Keyed by provider id, so the same provider chosen for both rows shares one key (shown once with a "shares the image key" note). - Provider settings moved into a header gear (
ProviderGear.jsx, new) — the provider's own knobs (ProviderBox.jsx, now bare controls, no collapsible card) render in a popover next to the dropdown, out of the main prompt area; the popover header shows the provider label + tier (name ellipsizes, the tag never wraps). - Old
ProviderSelect.jsxremoved; the rewrite-provider select + its key were removed from the gearSettings.jsx(they live in the Providers dropdown now); auto-fix / keyword toggles stay on the prompt box.
Composer Prompt/Negative switch. The negative prompt left the provider settings; the composer's editor now
flips between Prompt and Negative via a segmented switch on the right of the insert bar, shown only when
the provider supports a negative prompt. Building-block inserts, clear, and the live preview follow the active
field; the negative is stored per provider (providerParams[id].negativePrompt, read by generation as before).
Tests. gui/tests/Settings.test.jsx updated for the moved key + bare ProviderBox; added ApiKeyField,
ProviderGear, and ProvidersMenu coverage (51 SPA tests). Verified: lint 0 errors, npm test green
(smoke + Node + SPA), gui build green, Playwright 8/8 (incl. a11y); Windows visual baselines refreshed for
the new top bar — the Linux set still needs the visual-baselines.yml workflow.
2026-06-27 — 2.8.0: keyword tooling + DPL insert toolbar
(MINOR — a notable two-part GUI feature set. Bundled into one release because the two strands share
Home.jsx + styles.css in the working tree and couldn't be cleanly split without breaking the build.)
Keyword tooling (single view + composer).
- Robust keyword parser (
gui/src/lib/keywords.js, new). Turns a real sent-to-model prompt into clean tags: strips SD/NovelAI weighting + attention syntax ((w:1.2),((w)),[a:b:0.4],<lora:…>,BREAK, pipes, quotes), splits on commas/newlines so multi-word tags stay whole, de-dupes. Keeps the accented display form (caféstayscafé) but computes a de-accented lowercased key for dedupe + gallery search — socafestill findscaféand they collapse to one chip. Replaces the single view's naive comma-split cloud. - Single view "Rebuild with AI" (
SingleView.jsx). Sends the sent prompt to the rewrite provider in a new keyword mode, alphabetizes the result, and saves it over the image's sidecar (meta.keywords) via a newPOST /api/image/metaendpoint (vite-plugin-api.js) +updateImageMeta()(lib/output.js). The cloud prefers the saved list ("Keywords · edited"); the gallery search haystack includes it. - Composer keyword-translate toggle (
Home.jsx,settings.jsautoKeyword). A tag button beside the auto-fix wand. Independent + chainable: auto-fix runs first (if on), then keyword-translate on its output. - Keyword rewrite mode threaded through the rewrite pipeline (
_shared/rewriteSystem.jsKEYWORD_SYSTEMsystemFor();dispatch.js; the openai/gemini/grok adapters take asystemoverride;lib/rewrite.js,vite-plugin-api.js,netlify/functions/rewrite.jspassmode).
- Details as a real
<table>(SingleView.jsx+styles.css), replacing the CSS-grid key/value divs.
DPL insert toolbar (gui/src/components/DplInsertBar.jsx + gui/src/lib/dpl/dplInserts.js, new). A slim
row of category buttons (Structure, Chance, Choose, Repeat, Flow, Emphasis, Code) above the prompt box; each
popover lists the non-text DPL constructs with name, description, literal syntax, and a live re-rolling
example, and drops a CodeMirror snippet (with ${…} tab stops) at the cursor. Backed by DplEditor's new
imperative insertSnippet() handle; minor supporting edits to dplLanguage.js, promptEngine.js,
ProviderBox.jsx, WrapperFab.jsx.
Tests. Fixed the stale E2E selectors (home.spec.js, accessibility.spec.js) that still targeted the
old <textarea> — they now drive the CodeMirror editor (.prompt-input .cm-content, click +
pressSequentially), which had left the Playwright job red since the 2.7.26 CodeMirror switch. Linux visual
baselines regenerated via the visual-baselines.yml workflow for the new toolbar + buttons. Verified: lint 0
errors, npm test green (smoke + Node + SPA), gui build green.
2026-06-27 — 2.7.29: online build is a stripped, Generate-only variant
(On feature/online-build; merged after 2.7.28's feature/gui-result-polish.)
The deployed (VITE_ONLINE=true) build is now a deliberately stripped variant (App.jsx):
- No tabbar. The header's Generate/Gallery/Single switch is hidden and the Gallery + Single views aren't rendered at all — the image feed needs the local dev server's filesystem, which the online build doesn't have. The view is pinned to Generate. Generated images open in a new tab (no single view) rather than into the gallery.
- NSFW off, no option. The header NSFW toggle is removed entirely, and adult content is forced off on
load (
includeAdultcoerced tofalse) so a previously-saved preference can't carry over. - Nothing stored to the browser. The online build skips the feed fetch and never persists images — they stay in-memory for the session only.
The local build is unchanged (full three-view app with the NSFW toggle). Verified: lint 0 errors, both
build modes green (vite build and VITE_ONLINE=true vite build), npm test green (smoke + Node 86 +
SPA 43).
2026-06-27 — 2.7.28: result/gallery polish — corner preview, click-to-copy, DPL example tooltip, gallery actions
(On feature/gui-result-polish; merged after 2.7.27's feature/dynprompt-sidecar.)
A pass of UI polish on the composer, the generated-prompt list, and the gallery:
- Preview moved to the prompt box's corner. The live-preview eye left the bottom action bar and now
sits in the upper-right corner of the prompt box as a bare icon (
.composer-corner/.preview-cornerinHome.jsx+styles.css); the clear-✕ shares that corner cluster. - Click-to-copy prompts (no copy button).
PromptResult.jsxdropped the per-row "copy" button — the prompt text is now click-to-copy (provider-aware, viaonCopy) with the full text in a hover tooltip, matching the already-click-to-copy DPL / original lines. - DPL hover shows a re-rolling example. Hovering the source-DPL line opens a tooltip with the full DPL
on top and a concrete example that re-rolls every second (new
DplHoverCode, the same live-preview idea the building-block chips use).PromptResultnow takessettingsfor this. - Gallery thumbnails gained the generate-thumbnail actions. Each gallery cell now reveals open in the
default app · reveal in explorer · delete on hover (
Gallery.jsxreworked from a single<button>to a.g-openbutton + a shared.img-actionsoverlay);Apppasses its existingdeleteItemthrough asonDelete.
Verified: lint 0 errors, npm test green (smoke + Node 86 + SPA 43), gui build green.
2026-06-27 — 2.7.27: block sidecars gain priority (pill order) + an nsfw flag
(On feature/dynprompt-sidecar; merged after 2.7.26's feature/dpl-codemirror.)
Two sidecar-driven changes to the block catalog:
- Category
priority. The block picker now orders the category/folder pills inside the Blocks tab by aprioritynumber in each category folder's.jsonsidecar — lower lifts it higher, default 1000.getBlocks()was refactored to build category groups ({ priority, name, pill, entries }), with the virtualanywildcard pinned to 0 (leads) andspecialto 9000 (trails). The metadata script (scripts/dynprompt-meta/write-dynprompt-meta.mjs) writes the curated order —prompt200,scene300,subject400,style500,fragment600,user700 — so the picker reads Any · Prompt · Scene · Subject · Style · Fragment · User · Special. (This reorders the pills within Blocks, not the Blocks/Lists tabs.) nsfwsidecar flag (hard-hide). A generator whose.jsonsidecar carriesnsfw: trueis treated as non-existent when the NSFW switch is off — hidden from the picker (getBlocks({ includeAdult })drops it, and a wholly-adult category vanishes) and never resolved by the engine: the{#name}stage (block.js) now gates onsidecar.nsfw === true || isGatedBlock(name), so it's excluded from{#any}and group picks and resolves to""if referenced. The automaticnsfw-name-token rule still applies on top; the flag is the escape hatch for an adult generator whose name has no token (the set is empty today — the catalog is SFW).Home.jsxre-derives the palette when the switch flips.
New SPA test asserts the pill order (any → … → special). Verified: lint 0 errors, npm test green (smoke +
Node 86 + SPA 44), gui build green. Sidecars regenerated (86 files).
2026-06-27 — 2.7.26: DPL boxes are CodeMirror editors (syntax highlighting + autocomplete)
The prompt, negative-prompt, and wrapper Start/End boxes are now CodeMirror 6 editors instead of plain
<textarea>s, sharing one new reusable component (gui/src/components/DplEditor.jsx — controlled, drop-in
value/onChange, line-wrapped). Two pieces back it:
- A DPL language (
gui/src/lib/dpl/dplLanguage.js): a line-orientedStreamLanguagetokenizer that mirrorssrc/core/dpl/dpl.jsand aHighlightStylemapping each token to a CSS class. It colors{#generator}refs and{list}refs distinctly, emphasis weighting( ) [ ] | :1.2,+refcalls,;comments, and the line-leading DPL structure (===headings,-bullets,[900]weights,NN%/maybe/otherwisegates,one of/N ofchoices,repeat … times,insert/go toflow). Structural keywords only fire at the start of a line, so prose ("a man of war", "maybe later") is never mis-highlighted. - Brace-aware autocomplete: inside a
{or{#, a dropdown offers every list and generator token from the engine catalog (getDplCompletions()inpromptEngine.js), each option replacing the partial brace so there's no double-brace; on a-bullet line it offers the DPL structural keywords.
Highlight colors are theme-aware — they live in styles.css as --dpl-* variables with a light-theme
override, not in a JS theme. New SPA deps: @codemirror/{state,view,language,autocomplete,commands} +
@lezer/highlight (recorded in reference/dependencies.md). Verified: lint 0 errors, smoke + Node suite
(86) + SPA suite (43) green, gui build green. The Playwright visual baselines for the prompt box change
still need a refresh (npm run test:e2e:update) before release.
2026-06-26 — 2.7.25 (cont.): three persistent views (Generate / Gallery / Single) with state + scroll
Reworked the SPA into three top-level views that keep their state across tab switches for the session.
The top-bar switch is now Generate · Gallery · Single (the single-image view is its own tab). All three
stay mounted the whole session and are shown one at a time via a CSS class (.view-pane / .on) — so
each view retains its full React state, form inputs, and scroll position with no manual save/restore
(scroll comes along for free precisely because the scroll containers are never unmounted — the only clean
way, as asked).
The shared coordination state moved up into App: the saved-image feed (items), the gallery search
query, and the image open in the single view (current). Gallery.jsx became a controlled grid-only
component; the single page was extracted to SingleView.jsx as a standalone view (keyboard nav gated on
active so a hidden view doesn't grab arrow keys). Flow: clicking a generated image in Home now opens it
in the Single view (Back → Generate, state intact) instead of a new browser tab; clicking a gallery
thumbnail opens it there too (Back → Gallery); keyword-cloud chips jump to the Gallery filtered by that term.
The Single tab shows the last image you looked at, or — the first time in a session with nothing loaded —
a random one from the feed. Delete lands on a neighbour image (or leaves the view when it was the last).
test:web 43 still green; lint 0 errors, smoke + gui build green.
2026-06-26 — 2.7.25 (cont.): proper single-image page, negative AI translation, ImageMagick export
Built out the gallery's single-image page into a real app view (migrated from the v1-2 /single) and
enriched the metadata it shows. Three strands:
- Prompt + negative are now recorded in layers. The sidecar moved to a nested shape —
prompt: { dpl, roll, ai, final }andnegative: { dpl, roll, ai, final }(DPL source → deterministic engine roll → AI translation → what was actually sent). Crucially, the negative prompt is now also AI-translated when auto-fix is on (it was only DPL-expanded before):Home.makeBatchrewrites the rolled negative through the same provider and sends/stores the translated negative. Both rewrites are cached on the prompt entry so re-generating a prompt doesn't re-bill the rewrite API.lib/gallery.jsexposespromptLayers/negativeLayers/promptTextthat read the nested shape and still accept the old flat one. - Rebuilt single page (
Gallery.jsx). Sticky image left, info right: a Prompt card and a Negative card each showing their layers as labeled, copy-able blocks (duplicates collapsed); a curated Details table (provider, model, sampler, steps, CFG, size, seed, saved, file) over a collapsible All settings and a Raw metadata (JSON) disclosure; a clickable keyword cloud (chips built from the sent prompt's tags that filter the gallery); prev/next + arrow-key nav; and an actions row. The obsolete v1-2 actions (variations, upscale, animate, rerolls, "select", parent, animation frames) were dropped — we don't use them in v3. - ImageMagick export. The dev server detects ImageMagick (
GET /api/magick) and lists the still (non-animated) formats it can write (magick -list format, filtered to a still-raster allowlist); the single page shows a Convert & download dropdown of those formats, each converting viaGET /api/image/convert(first frame only, streamed as a download). Hidden entirely when magick isn't on PATH — graceful, like the rest of the local-only gallery. Newlib/magick.js.
gallery.test.js updated for the nested schema (+ layer-helper tests); test:web 43 green. Lint 0 errors,
smoke, gui build all green. No VERSION bump (still the unreleased 2.7.25 gallery line on feature/gallery).
2026-06-26 — 2.7.25: photo gallery + per-image JSON metadata sidecars
Brought back the v1-2 image feed as a first-class v3 view. A Generate / Gallery switch in the
upper-left of the top-bar flips between the prompt/image composer and a new photo gallery that browses
everything saved to output/. To make that gallery meaningful, every generated image now gets a .json
metadata sidecar written next to it (same base name, like the v1-2 feed) capturing how it was made: the
prompt actually sent, the deterministic engine roll (promptOriginal), the AI translation (the auto-fix
rewrite, when on), the source DPL, the negative prompt, the provider, and a full settings snapshot
(API keys stripped — never written to disk). The gallery reads these back: a masonry grid of lazy-loaded
thumbnails (wide/tall by aspect), a keyword search over prompt/DPL/provider, and a dedicated
single-image page (replaces the grid — not a modal — with back + prev/next + keyboard nav) showing the
whole record with copy / open / reveal / delete (which removes the image and its sidecar). Implemented on the dev-server side as POST /api/image (now accepts meta and writes the
sidecar) + a new GET /api/feed (scans output/, pairs each image with its sidecar, newest first);
delete cleans up the sidecar too. Local-only by nature (needs the dev server's filesystem) — a static/
online build shows an empty gallery with an explanatory note rather than an error. New src/lib/gallery.js
(+ gallery.test.js, 5 tests); the previously-unused Gallery.jsx was repurposed as the gallery view.
Verified: lint 0 errors, format, smoke, gui build (browser-glob gate), test:unit 86 + test:web 40 green.
2026-06-26 — docs (no version bump): adopt fairyfox 0.9.3 delta (verification floor)
Scheduled fairyfox check-for-updates flow (hub 0.9.2 → 0.9.4), then owner-authorized adoption in the
same session. The only node-facing change is 0.9.3's clarification that the verification floor is
never skipped: an express-authorized (or any automated) adopt skips only the redundant confirmation
pause — never verification, which now must run the full floor (build/tests + the standards'
## Verify/compliance checks + project-constraint checks) before and after the apply, with a hard
fallback (if verification can't complete, do not auto-apply — fall back to check-report-wait). Reworded
the express-auth carve-out in CLAUDE.md and notes/reference/cross-project-sync.md to match. 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 (bookkeeping). Combined check→adopt report at
notes/fairyfox-reports/2026-06-26-adopting-updates-3.md. Notes/CLAUDE.md-only — no VERSION bump.
2026-06-26 — 2.7.24 (PATCH): auto-fix (AI prompt rewrite) before image generation
New opt-in auto-fix: a chosen text AI rewrites the mechanical DPL prompt into something a given image
model handles better, before the image request goes out. Configured in the gear → Auto-fix (rewrite)
group — a "Rewrite with" Select (None + any provider exposing loadRewrite: OpenAI, Gemini, Grok) plus its
own BYOK key row (in-memory until Save, Get-a-key link). The rewrite provider is separate from the active
image provider. When a provider is selected, a wand toggle appears on the prompt box (bottom-right, by the
preview eye); only then can it be turned on. With it on, each generated prompt is rewritten once (server-side
adapters via /api/rewrite → dispatchRewrite, or the Netlify rewrite function online), the cleaned text
is sent to the image provider, and the original is kept under the DPL line in the result (click to copy).
New: _shared/rewriteSystem.js, openai|gemini|grok/code/rewrite.js, lib/rewrite.js,
netlify/functions/rewrite.js, rewriteProviders() in the registry. Verified: web build, test:web 35.
Server changed (dispatch + dev middleware) — restart the dev server.
2026-06-26 — 2.7.23 (PATCH): live-preview eye buttons (prompt + negative) + bigger negative box
New reusable LivePreview — an eye button that, while hovered, shows a tooltip re-rolling the DPL into a
concrete example every ~1s (same idea as the building-block chip previews; reads the text fresh each tick).
Added to the prompt box field bar and next to the negative prompt (which is now a bigger textarea,
4 rows / 5rem min-height). Verified: web build, test:web 35. Client-only — a refresh suffices.
2026-06-26 — 2.7.22 (PATCH): grouped/explained provider dropdown + BYOK key links
Replaced the plain provider <select> with a custom dropdown grouped into Local and Online, each
entry showing a one-line description (and a "key" badge for BYOK). The provider box gained a "Get a key
↗" link next to the API-key field (per-provider URL). Descriptions/links live in
gui/src/lib/providerMeta.js. Retired the generic "Local Stable Diffusion WebUI" (A1111) entry — its
adapter + settings stay (reused by Forge/SD.Next); the default provider moved to ComfyUI and a settings
migration maps any saved local-webui → forge. Verified: web build, test:web 35 (provider/Settings
tests updated). Client-only — a refresh suffices.
2026-06-26 — 2.7.21 (PATCH): NovelAI + plain-text + Forge/SD.Next; catalog at 14 providers
Finished the catalog breadth. NovelAI (syntax tier — NovelAI {}/[] dialect, Copy-prompt) and a
generic Plain text target (plain dialect, Copy-prompt) — both no-API, just config. Forge and
SD.Next are A1111-sdapi-compatible, so they're thin providers reusing the Local WebUI adapter +
settings (distinct labels; set the URL to your server). The catalog is now 14: local-webui (=A1111),
forge, sdnext, comfyui, openai, replicate, fal, stability, gemini, grok, bfl, ideogram, leonardo,
midjourney, novelai, plain. Client-only — a refresh suffices. Verified: web build, test:web 35.
Deferred (bespoke/uncertain APIs, want careful verification): InvokeAI, SwarmUI, Fooocus.
2026-06-26 — 2.7.20 (PATCH): hosted providers — Gemini, Grok, FLUX/BFL, Ideogram, Leonardo
Second hosted batch. Gemini (Google generateContent "Nano Banana" image model → base64),
Grok/xAI (OpenAI-compatible api.x.ai/v1/images/generations), FLUX/Black Forest Labs (api.bfl.ai
submit → poll polling_url until Ready → result.sample, via the shared submitPoll), Ideogram
(v2 JSON /generate, Api-Key), Leonardo (create generation → poll until COMPLETE, model = UUID).
Each registered in server/dispatch.js. Dialect plain. Verified: web build, test:web 35. NOTE:
dispatch changed — restart the dev server. Confidence: Gemini/Grok/FLUX high; Ideogram (v3 API
shift) and Leonardo (model UUIDs drift) are best-effort and may need per-account tweaks — flagged in
their config comments. (FLUX/Ideogram are also reachable via the Replicate/fal providers.) Remaining:
NovelAI/plain, then local backends.
2026-06-26 — 2.7.19 (PATCH): hosted providers — Replicate, fal.ai, Stability
First catalog expansion on the provider framework: three hosted BYOK image APIs, all single-call
server.js adapters (no polling). Replicate (model endpoint + Prefer: wait, model = owner/name
slug, output URLs), fal.ai (fal.run/<model> sync, Key auth, named image_size presets),
Stability AI (v2beta stable-image/generate/{core,sd3,ultra}, multipart, base64 image, negative
prompt). Each is config + settings + code/{generate,server} + (model) data; registered in
server/dispatch.js; fieldInfo gained aspectRatio/imageSize. Dialect plain. Verified: web build,
test:web 35. NOTE: dispatch changed — restart the dev server for these to call out. API specifics
verified against live docs; exact per-model fields may still need refinement. Next batches: Gemini/Grok/
FLUX-BFL/Ideogram/Leonardo, then NovelAI/plain, then the local backends.
2026-06-26 — 2.7.18 (PATCH): full-width Reset button in the gear popover
The "Reset to defaults" button in the prompt-settings gear popover now spans the popover width
(.settings-actions stretch + button 100%) instead of being shoved to the right at half width.
CSS-only. Verified: web build.
2026-06-26 — 2.7.17 (PATCH): reversed result numbering + always show DPL line
With newest-first ordering, the result position number now counts down from the top (newest = highest
number, prompts.length - i, passed as number) instead of index + 1. The per-prompt DPL line now
always shows (dropped the "only if it differs from the expanded text" condition). Verified: web build,
test:web 35. Client-only — a refresh suffices.
2026-06-26 — 2.7.16 (PATCH): newest-first ordering + per-prompt DPL
Newest on top: new rolls prepend their prompts, and each new image batch prepends within its
prompt (batch numbers stay chronological — newest carries the highest number). Each prompt now remembers
the DPL it was rolled from (prompt.dpl) and shows it (monospace, 2-line clamp, click-to-copy) right
above its expanded text — replacing the single global "DPL" line, which was wrong once results from
different rolls coexisted. Verified: web build, test:web 35. Client-only — a refresh suffices.
2026-06-26 — 2.7.15 (PATCH): fix gallery width (specificity) + new roll appends
Root-caused the stubborn gallery layout: .prompts li (specificity 0,0,1,1) set align-items: flex-start,
overriding .prompt-result's align-items: stretch (0,0,1,0), so the gallery shrank to content width
— which forced ~3 columns and made the shimmer placeholders stack regardless of the earlier tile fixes.
Bumped the selector to .prompts li.prompt-result so column + stretch win; the gallery now fills the
row and images/placeholders flow into all available columns. Also: a new roll now APPENDS to the
results list instead of replacing it (removed the replace + disk-delete prompt; Clear all / per-prompt
clear handle removal). Verified: web build, test:web 35. Client-only — a refresh suffices.
2026-06-26 — 2.7.14 (PATCH): gear popover opens downward (no longer clips off the top)
The prompt-settings gear popover now opens downward from the gear (was upward) and caps at 60vh, so it no longer runs off the top of the page when the prompt box sits high. CSS-only. Verified: web build.
2026-06-26 — 2.7.13 (PATCH): results polish — skeletons, clear actions, source DPL, tighter bar
Round of results/composer polish. Skeletons now mirror the image tile structure (a figure per
placeholder) so they flow into columns like real images instead of stacking. Clear actions: a global
Clear all button in the results header, a per-prompt clear button in the action row, and a new
roll replaces the previous results — each offering to delete the corresponding image files from disk.
Source DPL: the DPL template a batch was rolled from now shows above the results (2-line clamp + copy),
and each result's expanded prompt is clamped to 2 lines (copy button beside it for the full text).
Composer bar: the prompts counter × became a "count" label and the gear sits tight beside it
(.prompt-tools). Blocks sidebar trimmed ~25% (minmax(320→240px, 400→300px)). Verified: web build,
test:web 35. Client-only — a refresh suffices.
2026-06-26 — 2.7.12 (PATCH): square thumbnail grid + move the gear to the prompt box
Two UI fixes. Gallery grid: images now render as uniform square thumbnails (object-fit: cover
inside an aspect-ratio tile) with min-width: 0 on the grid items, so they (and the shimmer placeholders)
flow into as many columns as fit instead of blowing the tracks out to the image's intrinsic width (which
caused premature wrapping / single-column placeholders). Settings gear moved: removed the header gear +
Settings drawer; the gear now sits next to the prompt-count on the prompt box and opens a compact,
single-column popover with the same prompt knobs (vocabulary, emphasis, editing/alternating, salt).
Verified: web build, test:web 35. Client-only — a refresh suffices.
2026-06-26 — 2.7.11 (PATCH): auto-render images + loading placeholders + fade-in
Images now auto-render with the prompts: generating prompts kicks off an image batch for each one
(api providers). While a batch renders, the grid shows shimmer placeholder tiles sized to the batch
count; each finished image fades in once it actually loads (ResultImage with an onLoad opacity
transition). makeBatch takes the prompt text directly (so auto-render works before state commits) and
records the expected count. Tightened the gallery grid. Verified: web build, test:web 35. Client-only
— a refresh suffices.
2026-06-26 — docs (no version bump): adopt fairyfox 0.9.2 delta + fix release tagging
Adopted the hub 0.9.0–0.9.1 standards delta (hub now at 0.9.2) — notes/CLAUDE.md only, so no
VERSION/package.json change. Headline fix: CLAUDE.md and git-workflow.md documented hand-tagging
(git tag … && git push --tags) for releases, but release.yml derives + creates the tag itself and
gates on the tag not already existing — a hand-pushed tag would make the release run skip itself
(silent no-op). Removed every hand-tag from the PATCH/MINOR/hotfix paths; added a "Who creates the tag —
CI, not by hand" section. Also adopted the express-authorization ledger carve-out (CLAUDE.md,
cross-project-sync.md): an active hub/authorizations.yml entry lets an interactive adopt skip only the
redundant confirmation pause (every other safety step stays; unattended checks still apply nothing);
plus the process-reports combined-report / real-hub_version-anchor rules and a compliance row note.
Combined adopt report at notes/fairyfox-reports/2026-06-26-adopting-updates-2.md.
2026-06-26 — 2.7.10 (PATCH): per-prompt image batches + image management actions
Image results are now per prompt: each prompt row carries its own image batches, and a clear
"Generate images" / "More images" button adds a batch beneath it (replacing the cryptic icon button);
copy stays. New PromptResult.jsx; Home prompts state became { id, text, batches: [{ id, busy, images }] }.
Per image: open in the OS default app, reveal in the file explorer, or remove (with a confirm to also
delete the file from disk). A batch and the whole prompt's images can be cleared too, each offering
disk deletion. Clicking an image still opens it in a new tab (galleries later). New dev-middleware endpoints
(/api/image/delete|reveal|open, localhost + output-folder restricted, Windows explorer/start) and
output.js helpers (isOutputFile, deleteImageFile, revealImageFile, openImageFile). Verified: web
build, test:web 35. NOTE: the dev middleware changed — restart the dev server for the file actions.
2026-06-26 — 2.7.9 (PATCH): provider box — collapse, tooltips, key save flow, negative DPL
Provider-box polish. The box now collapses (collapsed by default) via its header. Every control has
an info tooltip (a shared _shared/fieldInfo.js dictionary keyed by field key). The BYOK key is no
longer auto-saved — it's held in memory for the session (gui/src/lib/sessionKeys.js) and only persisted
when you click Save (with a confirm); a saved key shows a Clear saved button. Generation uses the
session key, else the saved one. Negative prompt is now a real textarea that accepts DPL —
rolled out via the engine (like the main prompt) right before sending — with a ⟳ random button that
materializes one roll. Verified: web build, test:web 35.
2026-06-26 — 2.7.8 (PATCH): UI redesign — provider in header, provider-controls box, slimmed gear
Owner-driven UI restructure (no "named slots" — kept simple). The provider picker moved to the
header (next to the gear); choosing a provider still sets the dialect/mode. A new capability-driven
provider-controls box (ProviderBox.jsx) renders in the main column showing only that provider's own
controls — URL / key / checkpoint / sampler / scheduler / steps / CFG / W·H / batch / seed / negative,
whatever it declares — stored per-provider (providerParams[id]). The gear Settings now holds only
the non-provider prompt knobs (vocabulary lists, emphasis, editing/alternating, salt). Removed from the
UI: keyword min/max and artist counts (better done in the prompt via DPL); auto-add fx/artists disabled
by default (includeArtist/autoAddArtists/autoAddFx → false; keys kept so the engine still has values).
Added a prompts-per-run counter on the prompt box (bottom-left of the field bar). New ProviderSelect.jsx;
base .field + new component styles. Verified: web build, test:web 35 (Settings/ProviderBox tests updated).
Client-only change — a browser refresh suffices (no dev-server restart).
2026-06-26 — 2.7.7 (PATCH): central output folder for generated images
Owner's call: every provider's images should land in one central output folder, not be loaded
piecemeal from each backend. (Comfy Desktop also 403'd the browser on direct /view loads, so the
images showed as broken.) Added to the dev middleware: POST /api/image ingests an image src
(a data: URL — decoded — or a localhost URL — fetched server-side, which sidesteps the 403) into
engine-v3/output/ and returns a served path; GET /api/output/<file> serves it. New
gui/src/lib/output.js ingestImage(); Home.makeImage now funnels every provider's results through
it before display, so the saved copies render (same-origin) and accumulate in one folder regardless of
provider. Replaced the earlier display-only /api/forward-image proxy. NOTE: changing the Vite plugin
requires a dev-server restart to take effect. Verified: web build, test:web 33.
2026-06-26 — 2.7.6 (PATCH): ComfyUI also auto-resolves sampler + scheduler
Follow-up to 2.7.5's checkpoint fix: with the checkpoint resolved, ComfyUI next rejected
sampler_name: 'Euler' (the global SD-style default leaked into the request; ComfyUI uses lowercase
euler). The adapter now also queries /object_info/KSampler and coerces sampler_name + scheduler
to a valid value (exact → case-insensitive → euler/normal → first available), via a shared pick()
helper, alongside the checkpoint. So stale/SD-style sampler names self-heal. Verified: web build,
test:web 33.
2026-06-26 — 2.7.5 (PATCH): ComfyUI checkpoint auto-resolve + readable local errors
Live-debug fixes after a real ComfyUI run. The app was sending a stale/blank checkpoint name
(model.safetensors), so ComfyUI returned a value_not_in_list validation error — and our transport
threw new Error(errorObject), which the UI showed as "[object Object]". Two fixes: (1) the ComfyUI
adapter (code/generate.js) now resolves the checkpoint against what ComfyUI actually has installed —
queries /object_info/CheckpointLoaderSimple, keeps the configured name if valid, else uses the first
available, else throws a clear "no checkpoints installed" message — so a missing/blank/stale name
self-heals; (2) localDirect.js gained readableError() which extracts ComfyUI's error.message +
node_errors details instead of stringifying the object, so failures show the real cause. Verified: web
build, test:web 33.
2026-06-26 — 2.7.4 (PATCH): route local-direct providers through the dev server (no browser CORS)
All provider calls now go through a server — no provider is called directly from the browser. The
local-direct transport (localDirect.js postJson/getJson) now posts to a new /api/forward
endpoint in the Vite dev-middleware (localhost-only, server-side fetch), so ComfyUI / A1111 work even
though they send no CORS headers (Comfy Desktop sends none and can't easily be given the
--enable-cors-header flag). Image URLs stay direct — an <img> tag isn't CORS-blocked for display, only
fetch() is. Hosted providers already routed through /api/generate, so the browser now makes zero
cross-origin provider calls. Also set the ComfyUI provider's default checkpoint to
v1-5-pruned-emaonly-fp16.safetensors. The local-webui contract test updated to assert the forwarded
target + body. Verified: web build, test:web 33. (Surfaced while debugging a live setup: Comfy Desktop
was running with no checkpoint model installed — separate user-side fix.)
2026-06-26 — 2.7.3 (PATCH): provider UI re-add — capability-driven settings, image gen, copy-prompt
Phase 2 of the provider work (feature/providers): re-added the image-generation UI (it was in the
"removed, pending re-add" bucket). Settings backend is now capability-driven — the provider <Select>
renders that provider's OWN settings.js fields (per optionsFrom data lists), stored under a
per-provider namespace providerParams[id] so switching providers never clobbers another's knobs; the
provider owns the dialect (engine mode is set from it — the standalone "Mode" dropdown and the
hardcoded global Image/Negative groups are gone). Re-added the Settings drawer + gear button in
App.jsx. Home now generates images via the active provider's adapter — a per-prompt image button
(api tier) feeding Gallery — and Copy-prompt runs the prompt through the provider's formatter for
the syntax tier (Midjourney --params), so the right thing is copied. New gui/src/lib/useProvider.js
(useProviderSettings lazy-loads schema + option data; providerMode; flattenForProvider merges
namespaced params + dialect for the adapters). providerParams added to default settings. Verified: lint
0 errors, web build (provider code/settings/format chunks code-split), test:web 33 (+ a Settings
capability-driven component test; +2 from 31). Note: the Playwright visual baselines will need a refresh
(the home UI gained a gear button + image results) — not part of the headless gate, do on a deliberate
test:e2e:update. Follow-ups: submit/poll hosted adapters; surface per-provider presets in the UI.
2026-06-26 — 2.7.2 (PATCH): provider framework foundation + plain dialect
First-class image-provider framework (on feature/providers), replacing the flat
gui/src/lib/providers/ (localWebui + a hostedProxy stub). Each provider is now a self-contained
folder gui/providers/<id>/ — config.js (manifest: tier · dialect · transport · capabilities),
provider-owned settings.js, presets/, code/, data/ — auto-discovered by gui/providers/index.js
via import.meta.glob (the block plugin pattern). The support ladder: api (we render),
syntax (emit the tool's grammar + params, Copy-prompt), plain (natural language). New native core
change: a plain dialect in src/helpers/randomEmphasis.js that keeps the engine's emphasis rolls
but renders them as natural-language words (intensifier/hedge ladder, provider-overridable) instead of
weighting syntax — so syntax-less targets still receive emphasis (the list stage already routes
non-NAI/MDJ modes through the SD function set, so no stage change). Transport: local-direct
(browser → user's server), hosted-proxy (browser → /api/generate → upstream BYOK, served by the
Netlify function online and a new Vite dev-middleware locally — both share gui/server/dispatch.js),
and none (syntax/plain). New storage subsystem gui/storage/ (pluggable by run mode: online =
stripped localStorage; local = real .json file via /api/storage, browser fallback) with a
per-provider presetStore. First providers: local-webui (ported, real), comfyui (real
local-direct workflow-graph adapter), openai (hosted, client + server adapter), midjourney
(syntax tier — data-driven --param catalog split into parameters.json + versions.json). The old
mode becomes provider-owned dialect (standalone Mode control to be removed when the UI re-adds image
generation). Verified: lint 0 errors, smoke, test:unit (86, +3 plain-dialect), test:web (31, +
registry/Midjourney contract), web build. Follow-ups: capability-driven settings UI + image-results +
Copy-prompt re-add in Home/Settings; submit/poll hosted providers (Stability/fal/Replicate/BFL/Ideogram/
Leonardo); live ComfyUI run once installed. Design: notes/plans/providers.md.
2026-06-26 — Release 2.7.1 (PATCH): flat blocks, data/sources, web-app → gui
Layout reorg completing the v3-only move. Blocks are now flat under
data/blocks/<category>/: the v3/ wrapper folder was removed and the leftover version
machinery stripped end-to-end — block.js is one flat catalog (auto-begin/end now
unconditional; {#v1/}/{#v2/}/{#any-ver} dropped), nodeLoader/browserLoader lost their
startsWith("v3/") filters, promptFilesAndSuggestions dropped v1Files/v2Files (v3/user/ →
user/), and the SPA promptEngine/Home dropped the per-generation browse + version superset switch
(this also fixed isExpansionKey, which would otherwise never match flat keys, and the Blocks picker that
would have rendered empty). The 5 .js generators' src imports went one level shallower
(../../../../src → ../../../src); dpl-validate/dpl-engine-check/write-dynprompt-meta and the
blocks README were de-versioned. Loose raw build inputs (artists.csv, danbooru.csv,
nai-tag-expirement.json) moved to data/sources/ (build scripts + .prettierignore updated). The
SPA folder was renamed web-app/ → gui/ — the name anticipates a planned CLI (the core engine is
already headless/isomorphic); every config reference updated (CI/pages/visual-baselines, eslint/vitest/
playwright, package.json scripts, build-docs, netlify.toml, the gui tree's own @module tags). The
duplicate root Upgrade-2-0.md was removed (it lives in engine-v1-2/). PATCH bump (2.7.0 → 2.7.1) — a
refactor/cleanup; v1/v2 addressing was already dead since 2.7.0, so nothing user-facing breaks. Verified:
smoke, web build, test:unit (83), test:web (28, +a Blocks-non-empty assertion), lint 0 errors. Known
follow-ups: a fuller notes sweep of v1/v2/web-app mentions in the deeper reference/ docs; the
netlify.toml engine-v3 path reconciliation (pre-existing); and the preset system rethink.
2026-06-26 — Adopt hub updates: process-reports + compliance audit
Ran the fairyfox check-for-updates flow, then adopted the new hub standards (notes/process-docs only —
no VERSION bump, no release). Refreshing the read-only hub clone needed a reset --hard origin/dev
because upstream dev was force-pushed (--ff-only aborts). Adopted: the process-reports feedback
loop — new notes/fairyfox-reports/ folder (README + this run's report) and reference/process-reports.md;
every fairyfox run now ends with a report, even a check-only one. The standards compliance audit
(reference/compliance.md) — the recurring whole-set check. Added ## Verify sections to
reference/git-workflow.md and reference/versioning.md; folded the "hub also reads
notes/fairyfox-reports/" model + a process-report step + the force-push refresh fallback into
reference/cross-project-sync.md; updated CLAUDE.md (fairyfox flow ends with a report; names the
compliance audit; new notes-maintenance trigger row) and notes/README.md. The git-flow release model
the hub finalized was already adopted here (2026-06-25), so no release-mechanics change.
2026-06-26 — Release 2.7.0 (MINOR): two-engine split + v3-only
Milestone release bundling this session's restructure. The repo is now two disconnected projects —
engine-v3/ (the active prompt engine + React SPA) and the frozen engine-v1-2/ (the literal
pre-revival CommonJS CLI + classic Express/Pug server, restored from 241a148, runnable, on its way out).
engine-v3 is v3-only (v1/v2 block generations deleted) and the legacy <expansion> mechanism
was removed end-to-end (engine stage, classifier, both loaders, SPA UI, data). Also: classic-only deps
pruned, CLAUDE.md + the notes reframed to the split, CI/release/pages workflows retargeted to engine-v3/
(CI uses npm install to dodge npm's cross-platform @emnapi lockfile bug). MINOR bump (2.6.1 → 2.7.0) —
a notable milestone, no breaking API for SPA users. Known follow-ups: the doc-site (npm run docs) +
GitHub Pages auto-deploy are paused pending build-docs.mjs path reconciliation to the new layout.
2026-06-25 — engine-v3 goes v3-only; drop the legacy expansion mechanism
Simplified/lightened engine-v3 to a single generation. Deleted the v1 + v2 block generations
(data/blocks/v1/ + v2/ + their .json metas) — engine-v3 now ships only v3 (89 .dpl + JS
sidecars), which stands alone. Removed the legacy <expansion> mechanism end to end: dropped the
expansion stage from the pipeline (engine.js + settings.js promptModules are now
block → prompt-salt → list → cleanup), deleted core/stages/expansion.js, removed expansion
loading from the classifier, deleted the SPA Expansions tab + the "Save as Expansion" feature
(button/panel/state/handler/icon) + the custom-expansion store, and deleted the data/expansions-obsolete/
content. The two v3 generators that still referenced expansions (v3/scene/futuristic.js: <dap>,
<detail/legacy>) were repointed to their migrated block equivalents ({#dap}, {#legacy} under
v3/expansion/, which remain as ordinary {#…} generators). Retired/updated the expansion-specific
integration + snapshot tests and the custom-expansion unit tests. Verified green: smoke OK, 83 Vitest +
28 SPA tests, SPA build; lint warnings dropped 140 → 18. Then removed the trivial dead-code remnants in a follow-up commit (the loaders' now-unreachable
expansionNames/readExpansion/globs + the orphaned namesUnder/expansionsRoot helpers, fakeLoader's
expansion support, and settings.expansionFiles) — engine-v3 now carries no expansion code at all. No
VERSION bump pending the release decision.
2026-06-25 — Split the repo: engine-v1-2 (frozen) + engine-v3 (the project)
Untangled the repo into two fully disconnected trees that share zero code. engine-v1-2/ is the
literal pre-revival snapshot (commit 241a148, 2023-04-07, CommonJS) restored as-is — its own
package.json/lockfile/webui.bat, web/ contained, runs standalone (verified: npm install + the CLI
generates a prompt). Frozen, unmaintained, on its way out. engine-v3/ is now the project: the
new core engine + SPA + data/ (the v1/v2/v3 block generations on the new lists) + tests +
scripts + package.json/configs, moved together so all cross-folder imports survived. Deleted the
transitional ESM legacy (classic server.js + web/, the CLI index.js/common.js/applyArgs,
image/upscale/animation, loadSettings/createMissingUserSettings/diffSettings, the legacy
prompt-modules stages, legacy helpers) since the old system is preserved by the snapshot. Rewrote
scripts/smoke-test.mjs to boot the core engine via nodeLoader + settings.js (no common.js).
Retargeted CI to run inside engine-v3/ and added a root README pointing to both engines. Verified from
engine-v3: lint 0 errors, smoke OK, 84 Vitest + 30 SPA tests, SPA build green. No VERSION bump (the
project moved, didn't change behavior). Follow-ups: drop the expansion stage (data still present), sync
CLAUDE.md/notes paths to engine-v3/, fix the doc-site (jsdoc/build-docs) paths, decide root
VERSION/fairyfox-node handling. Plan: notes/plans/engine-split.md.
2026-06-25 — Triage the old /generate page for SPA carry-over (notes only)
Catalogued every control on the legacy classic-server prompt page (src/web/views/generate.pug)
and decided where each one goes as the page is retired: image-AI settings → the future provider
abstraction; emphasis/editing/alternating → reworked to provider-dependent output (old (((x))) style is
dated — Flux barely responds to weights); keyword-count/chaos/auto-fx/artists/anime-words → replaced by
DPL + the v3 wrapper; all animation settings and the salt settings → dropped; per-image actions →
deferred to the future image viewer/editor; folder paths + keepers (promptCount, keyword/artist list
selectors) → port into the SPA UI. New plan doc notes/plans/generate-page-triage.md holds the full
disposition + a four-sweep order (prune first); notes/plans/next-steps.md gains Sweep 1 as the next
concrete item. No code touched (classic server is frozen/being deleted); no VERSION bump (notes only).
2026-06-25 — Adopt fairyfox git-flow standard; rename master → main
Adopted the fairyfox system's updated git standard, which replaces the lean dev → main fast-forward
model with full git-flow (long-lived main + dev; feature/*, release/*, hotfix/* support
branches; all --no-ff; every commit on main a tagged release). Renamed the stable branch
master → main (mandatory under the new standard) and repointed the CI/Pages/release workflows
(ci.yml, pages.yml, release.yml) from master to main. Rewrote notes/reference/git-workflow.md
to git-flow and updated the master/FF-only references in CLAUDE.md, deployment.md, versioning.md,
and status.md. GitHub-side default-branch flip and origin/master deletion are left for the owner.
No VERSION bump (process/CI/docs only). See notes/sessions/2026-06/2026-06-25.md.
2026-06-25 — SFW by default + a top-bar NSFW toggle (2.6.1)
The SPA now defaults to SFW: added includeAdult: false to web-app/src/lib/settings.js
defaults. Added a right-aligned NSFW switch to the top-bar
(web-app/src/components/NsfwToggle.jsx, wired in App.jsx, styled in styles.css) — a stopgap
"until we get an options screen". Enabling it pops a confirmation dialog (18+ / adult-content
warning) before flipping settings.includeAdult to true; disabling is immediate. The preference
rides in settings so it's remembered in the browser (localStorage). No engine change was needed —
the core already gates adult content on includeAdult (core/listStore.js, core/stages/*,
gatedLists.js); this exposes the switch. Bumped VERSION + package.json to 2.6.1 (ordinary
feature → PATCH).
2026-06-25 — Onboarded into the fairyfox system + themed the docs site
Folded this repo into the fairyfox hub mesh per the hub's onboarding-existing-project runbook,
project-side only (the hub repo was not touched). Added the "Cross-project standards & checking
the fairyfox system for updates" standing instruction to CLAUDE.md (the on-request check →
report → wait flow + guardrails, adapted to the dev→master model and the read-only
assets/references/fairyfox.io/ clone), and a new notes/reference/cross-project-sync.md recording
the sync model. Themed the JSDoc (docdash) doc-site toward fairyfox.io: new
assets/docs-theme/{fairyfox-docs.css,fairyfox-docs.js} reproduce the shared design tokens
(dark-first warm palette, Fraunces/Inter/JetBrains, theme-color metas) and inject the required
two-way links back to Fairy Fox (sidebar brand, breadcrumb locator, footer); wired through
jsdoc.config.json (docdash.scripts/menu/meta) with scripts/build-docs.mjs copying the
theme assets into the generated docs/jsdoc/. No VERSION bump (docs/notes/CI-tooling only).
2026-06-22 — Visual-regression now runs in CI too (Linux baselines)
Closed the last gap from the previous entry: visual-regression was the one suite skipped on CI because its
toHaveScreenshot baselines were committed for Windows only (*-chromium-win32.png). Generated matching
Linux baselines (*-chromium-linux.png) and committed them, so the CI e2e job now runs the visual
specs too. Mechanics: playwright.config.js now picks the browser by OS (Windows → system Chrome, Linux →
Playwright's bundled chromium — the same browser CI uses) and gates visual on PLAYWRIGHT_SKIP_VISUAL
(an escape hatch for a platform with no baselines yet) instead of CI. A new manual workflow,
.github/workflows/visual-baselines.yml ("Update visual baselines (Linux)"), regenerates the Linux PNGs on
the same runner the e2e job uses (ubuntu-latest + npx playwright install --with-deps chromium) and
uploads them as an artifact to download + commit — so baselines and CI render identically. (Local Docker
generation via mcr.microsoft.com/playwright was attempted first but the image pull crawled on this
connection; generating on the CI runner is both faster and a guaranteed match.) Test/CI only — no version
bump.
2026-06-22 — CI now runs the full test suite (Vitest + Playwright)
ci.yml previously ran only lint + format:check + smoke + the web-app build, so the 2.6.0 Vitest and
Playwright suites never ran in CI — green CI could miss real regressions. Expanded CI to mirror the local
gate: the check job now also runs npm run test:unit (Node Vitest); the web-app job now also runs
npm --prefix web-app run test (jsdom Vitest); and a new e2e job installs root + web-app deps and the
bundled Playwright chromium and runs npm run test:e2e (E2E + accessibility). Visual-regression is the
one deliberate exception — playwright.config.js now testIgnores visual.spec.js when process.env.CI
is set, because the toHaveScreenshot baselines are committed for Windows only (*-win32.png) and can't
match Linux rendering; the same guard switches CI from system Chrome to Playwright's bundled chromium. To
turn visual on in CI later, commit Linux baselines (test:e2e:update on Linux) and drop the guard. CI/test
config only — no version bump.
2026-06-22 — Unbreak Release + Pages workflows (JSDoc types + verify step)
The first-ever master push (the ship below) triggered release.yml and pages.yml for the first time
and both went red — pre-existing problems, unrelated to product code, that had never run before because
deployments were held. Two fixes: (1) npm run docs aborted because JSDoc's parser can't read
TypeScript-style type expressions — arrow types (n:string)=>(string[]|null) and optional record keys
{category?:string,…} in src/listManifest.js, src/promptFilesAndSuggestions.js, src/blockManifest.js
— rewrote them in JSDoc-native syntax (function(string): (string[]|null), explicit (T|undefined)); npm run docs now exits 0, fixing Pages and the Release docs-zip step. (2) release.yml's "Verify (lint +
smoke test)" step ran npm test, which 2.6.0 had redefined to include the web-app jsdom suite (deps not
installed in that job); realigned it to npm run lint + npm run smoke, matching the step's name and the
CI gate (master is FF-only from a CI-green dev, and CI checks lint/format/smoke/build). Build/CI only — no
version bump.
2026-06-22 — Unbreak CI: resync lockfiles + Prettier pass (ship to master)
CI had been red on every dev push since the test-suite landed, blocking the long-held first ship to
master. Two causes, both unrelated to product code: (1) npm ci failed in both jobs because
package-lock.json (root) and web-app/package-lock.json had drifted out of sync with package.json
after the 2.6.0 Vitest/Rolldown dependency additions (missing @emnapi/* + platform native bindings) —
regenerated both with Node 24 / npm 11; and (2) format:check was red because ~40 source/test files added
since the last green run were never Prettier-formatted (CI never got past install to catch them) — ran
prettier --write. lint, smoke, and the web-app build all pass locally; the CI gate is green again.
No version bump (build/style only). This green dev HEAD is the first commit fast-forwarded to master,
lifting the intentional deployment hold.
2026-06-22 — Full automated test suite: Vitest + Playwright (2.6.0)
Added a comprehensive, runnable test suite covering every standard test type — the project went from
"lint + import smoke" to real coverage. Vitest drives two suites: a Node-side suite under tests/
(environment: node, vitest.config.js) and a jsdom SPA suite under web-app/tests/
(web-app/vitest.config.js, reusing vite.config.js so import.meta.glob + the lodash alias resolve as
in the real build). Playwright (playwright.config.js, builds the SPA and serves dist/ via vite preview) drives E2E, visual-regression, and @axe-core/playwright accessibility specs under tests/e2e/.
Test types: unit (contentSafety, diffSettings, keywordRepeater, gatedLists, listManifest, the DPL
compiler, cleanup, prompt-salt; SPA share/settings/customStore) · component/UI (Field, TokenPicker via
React Testing Library) · integration (the full stage pipeline over a fake loader in Node, and over the
real bundled data in the browser facade) · contract/API (the SD WebUI txt2img request/response shape,
fetch mocked) · snapshot (seeded, reproducible DPL + pipeline output) · E2E (type → generate →
results) · visual regression (stable chrome screenshots, the random suggestion masked) ·
accessibility (WCAG 2 A/AA, fails on serious/critical) · smoke (the original import-graph gate,
retained) · bug regression (tests/regression/, one guard per fixed defect).
Scope deliberately excludes the legacy classic server (src/server.js, src/web/frontend/**,
src/prompt-modules/**) — it is being actively phased out; only the pure stages the active core engine still
imports (cleanup.js, prompt-salt.js) are tested. New npm scripts: test:unit, test:web, test:e2e,
test:e2e:update, test:all, *:coverage; npm test now runs lint + smoke + Node + SPA suites. New dev
deps: vitest 4, @vitest/coverage-v8, jsdom, @testing-library/{react,jest-dom,user-event}, @playwright/test,
@axe-core/playwright. Result: 118 Vitest tests green (88 Node + 30 SPA) plus 8 Playwright specs
green (E2E + visual-regression with committed baselines + axe a11y). The bundled Chrome-for-Testing
build hit a Windows side-by-side launch error here (even with the VC++ runtime present), so the Playwright
config uses channel: "chrome" (the version-matched system Google Chrome); CI can drop the channel to
use the bundled browser. Discovered + documented landmine:
lodash captures Math.random at import, so _.random/_.sample/_.shuffle can't be RNG-stubbed — tests
assert invariants or use single-entry lists, and only the DPL renderer is seeded.
2026-06-21 — prompt/ category rename + "Prompts" navbar grouping (2.5.0)
Naming + UI polish on the 2.5.0 work. Renamed the v2/engine/ category to v2/prompt/ and marked it
_force-prefix (so its generators show/insert as {#prompt/…}); within it danbooru→d,
random→random-words, and the -prompt suffix dropped (random-prompt→random,
simple-random-prompt→simple-random, extra-random-prompt→extra-random). So {#prompt/random} is the
composite suggestion and {#prompt/random-words} the keyword pile; the default settings.prompt (CLI +
SPA) is now {#random-words} to preserve the prior default behavior. Fixed the one sibling import
(extra-random → ./random.js) and regenerated sidecars. Reverted the forced {#user} group (the lone
v2/user/ folder relies on the normal auto-rule, so it isn't a group). Navbar: collapsed the two tabs
into a single "Prompts" heading with one v1/v2 superset switch (rendered v1 v2, v2 default) over
full / partial sub-tabs. Category descriptions rewritten to describe each category (no group
mechanics). lint (0 errors), smoke, web build (495 modules) green.
2026-06-21 — Pick-one groups + {#any} wildcard + Full/Partial navbar (2.5.0)
Reinstated and finished the "pick one" model for blocks and expansions, reframed (owner) as
"pick one GENERATOR/snippet, not one word." Groups: a category folder with 2+ generators is an implied
group — {#scene} runs one random scene generator; .group files + _enable/_disable-group-list markers
work too; the same for expansions (<lighting> splices one random expansion). Restored
blockGroupDirs/readBlockGroup and added expansionGroupDirs/readExpansionGroup in both
loaders; the dyn + expansion stages resolve folder/.group refs to one random member (gate-aware) and run/
splice it. Wildcard: {#any} / {#any-sfw} / {#any-nsfw} pick one generator from the whole v2
catalog with the lists' {keyword}-style mode variants. SPA navbar: split into "Blocks"
(full) and "Partial prompts" (partial) tabs with clickable folder-group pills + the {#any} family;
v1/v2 are superset links on the navbar (v2 default), replacing the inline category toggle. Every group/
wildcard resolves to ONE concrete generator/snippet (never a line union). lint (0 errors), smoke, and web
build (493 modules) all green.
2026-06-21 — Blocks: {#name} sigil + gating/wildcard + uniform SPA (2.4.0)
Round 2 of the block standards pass. Sigil: blocks are now written {#name}
(brace-delimited, uniform with {list}/<expansion>, and able to carry / paths like {#scene/beach});
the bare #name form is retired (it let stray # in plain text get eaten). Migrated the 204 internal
references across 54 v2 generators (scripts/migrate-dynprompt-sigil.mjs,
comment-safe + idempotent); v1 has no internal # refs so it was untouched; updated the engine default,
suggestion builder, settings, genImg, and SPA defaults. The list stage skips {#…} so the two {…} sigils
never collide. Gating: isGatedBlock now keys off the nsfw name token (parity with lists), so an
*-nsfw generator is hidden/empty when adult is off — no hardcoded list. Tags: src/blockManifest.js
adds a blockTags map. SPA: the four behavior blocks collapsed into one uniform Blocks block — category-folder
pills (plain labels) + a v1/v2 toggle on the header, mirroring Lists/Expansions. Per owner, dynamic
prompts get no group entry lists and no random-pick wildcard (a folder is organization, not a
random-member pool, and there is no {#any} either — a generator is a script with specific I/O, not a list
you pick an entry from); the implied-{#folder}-group machinery and the {#any} wildcard added mid-pass
were both removed. lint (0
errors), smoke, and web build (493 modules) all green.
2026-06-21 — Blocks: parity with lists/expansions + v2/ reorg (2.3.0)
Brought data/blocks/ up to the list/expansion standards, on both the file and UI sides.
File side: a scripted migration (scripts/reorg-dynprompts-v2.mjs)
moved the 79 v2 generators + the user-submitted one into category folders under a new v2/ root
(scene/subject/fragment/style/engine/user), v1/ frozen, rewriting every relative import by
resolving old→new absolute paths (v2 helpers now ../../../../src/…; cross-category siblings like
../fragment/nature.js). #name now resolves by path suffix (resolveName, splitting v1 vs v2 in
core/stages/block.js), so every existing reference still
works; #name-v1 and #user-name kept as aliases. Added <name>.json description sidecars for all 113
generators + 8 folders (scripts/dynprompt-meta/write-dynprompt-meta.mjs),
plus readBlockMeta / blockForcedPrefixDirs / _-internal skip / compareNames sort in both
loaders. UI side: the SPA token cloud keeps its Full/Partial/User/V1 sections but gained description
tooltips, natural-order sorting, and shortest-#token display (computeButtonNames). Only the new
engine was touched — the classic server and prompt-modules/block.js are read-only legacy
reference (per owner). Caught + fixed 10 v1/* files importing the moved entity.js (only the
vite build gate sees v1, not smoke). npm run lint (0 errors), npm run smoke, and
npm --prefix web-app run build (492 modules) all green. New design note:
reference/blocks-architecture.md.
2026-06-21 — Moved blocks/ from src/ to data/ (2.2.2)
The #name block generators now live under data/blocks/ instead of src/blocks/
— a deliberate, documented exception to "code lives in src/", since they're authored as prompt content like
lists/expansions. git mv preserved history (incl. v1/ and user-submitted/). Updated both loaders: the
legacy src/prompt-modules/block.js (require prefixed ../../data/), core/nodeLoader.js
(rootDir/data/blocks), and core/browserLoader.js (glob ../../data/blocks/**/*.js).
Rewrote the 18 cross-tree imports inside the generators that reach back into src/ (helpers +
promptFilesAndSuggestions.js). npm run smoke, npm --prefix web-app run build, and lint all green.
Recorded the exception in CLAUDE.md and decisions/architecture.md.
2026-06-21 — Expansions: rename detail/legacy* + port _force-prefix (2.2.1)
Renamed detail/legacy-detail→detail/legacy and detail/legacy-person-detail→detail/legacy-person (the
detail/ folder carries the meaning) and added a detail/_force-prefix marker so the editor shows/inserts
<detail/legacy> / <detail/legacy-person>. Ported _force-prefix to expansions: nodeLoader
markedDirs(marker, base) + expansionForcedPrefixDirs(), a **/_force-prefix glob + generalized
markerDirs(files, marker, seg) in browserLoader, fed to computeButtonNames in promptEngine. Updated the
one reference (futuristic.js) and the meta-script keys. Display-only like lists (suffix resolution still
works). npm test + vite build green.
2026-06-21 — Expansions brought to parity with the list system (2.2.0)
Ported the portable parts of the keyword-list modernization to data/expansions/. Nested the 9 expansions into
category folders (detail, style, lighting, subject, scene) with path-suffix resolution (shared
resolveName), so existing <rays>/<legacy-detail> references still resolve; both loaders walk the tree
recursively and skip _-prefixed files. Added per-expansion + per-folder <name>.json description sidecars (14
files, via the new scripts/expansion-meta/write-expansion-meta.mjs) and readExpansionMeta on both loaders;
the SPA "Expansions" cloud is now grouped by folder with category pills + tooltips, mirroring the Lists block.
Deliberately NOT ported (don't fit copy/paste snippets): random-union groups, clickable folder pills, SFW/NSFW
splitting, _force-prefix. New data/expansions/README.md + notes/reference/expansions-architecture.md.
Names/content left unchanged. npm test + vite build green.
2026-06-20 — SPA: Lists panel grouped by folder category (2.1.0)
The SPA "Lists" cloud is now grouped by folder, alphabetical, with an inline category pill before each
folder's entries (label = folder's last segment, tooltip = folder description). When the folder is an implied
group the pill is clickable and inserts the whole-folder group ({word}, {d}, …). Removed scene/_force-prefix
(scene names don't collide). npm test + vite build green.
2026-06-20 — Markers: dotfiles → _-prefixed regular files (2.1.0)
Renamed the folder markers from dotfiles (.force-prefix etc.) to _-prefixed regular files
(_force-prefix, _enable-group-list, _disable-group-list) so Vite's import.meta.glob sees them — and
dropped the workaround Vite plugin / virtual module. New convention: any _-prefixed file is internal/config,
never a list (loaders skip them). Markers are empty files. npm test + vite build green.
2026-06-20 — Fix: force-prefix / group markers ignored in the SPA (Vite skips dotfiles) (2.1.0)
import.meta.glob doesn't match dotfiles, so the browser saw no .force-prefix / .enable/.disable-group- list markers (buttons showed bare names). Added a list-markers Vite plugin that fs-scans data/lists and
exposes the marked folders as a virtual:list-markers module the browserLoader imports. Force-prefix and the
group overrides now work in the SPA. npm test + vite build green.
2026-06-20 — Lists: implied groups become automatic (folder with 2+ lists) (2.1.0)
A folder with 2+ direct list files is now automatically an implied group ({folder} = union of its own
lists); no marker needed. .force-group-list retired; .enable-group-list / .disable-group-list are the
overrides. Doesn't stack (own direct files only). autoGroupListDirs in listManifest; loaders compute from
.txt-only names + markers; impliedGroupMembers direct-children-only. Implied groups now include word,
look, place, nature, lore, scene, style (plus artist, d, name); brand (1 list) is not. npm test + vite build green.
2026-06-20 — Lists: optional <list>.json metadata sidecars (tooltips) (2.1.0)
Each list can have a <list>.json sidecar with a description for the editor button tooltip. Loaders read
them (readListMeta); the SPA cloud shows the description in the chip tooltip (falling back to the -sfw
file for mixed/implicit lists). Shipped descriptions for all 79 built-in lists/groups. npm test + vite build green.
2026-06-20 — Lists: .force-group-list implied groups + fix refs after folder-suffix rename (2.1.0)
Verified the owner's list refactor (dropped redundant folder-name suffixes like general-style→general,
added .force-prefix to artist/scene/style) and fixed the block token references it broke
({building-style}→{style/building}, {ship-type}→{scene/ship}, etc.) plus listTags keys. Added
.force-group-list: a folder with that marker is an implied group ({folder} = union of its lists,
mode-aware). Marked artist/, danbooru/d/, name/ and removed the redundant artist.group / d.group / name.group
(subset groups digipa, d/character, d/keyword kept). npm test + vite build green.
2026-06-20 — Lists: move d.group up to danbooru/d.group (buttons as {d}) (2.1.0)
Moved the whole-danbooru group out of the forced danbooru/d/ folder up to danbooru/d.group, so it
auto-names to {d} instead of {d/d}. listTags key updated. npm test + vite build green.
2026-06-20 — Editor: shortest-unambiguous button names + .force-prefix (2.1.0)
List buttons now show just the filename unless a conflict forces a longer path, or a folder is marked with an
empty .force-prefix file (which always shows its path from that folder down and is excluded from the
conflict check). Added computeButtonNames to listManifest + forcedPrefixDirs() to both loaders + the
danbooru/d/.force-prefix marker; the SPA's token cloud uses it (so danbooru shows {d/general}, everything
else a bare filename). Display-only; resolution unchanged. npm test + vite build green.
2026-06-20 — Lists: keyword becomes a reserved wildcard (any-loaded-word) (2.1.0)
keyword is now a reserved name, not a file: {keyword} draws a random word from all loaded vocabulary
(mode-aware), {keyword-sfw} = SFW always, {keyword-nsfw} = full set (gated). It supersedes any file named
keyword silently and excludes the artist/* and danbooru/* namespaces. Implemented via RESERVED_WILDCARD in
listManifest (resolveName short-circuit + resolveListLines union) and surfaced in the picker. The old
keyword/ files were relocated first (languages → word/language.txt; adult vocab → word/adult-nsfw.txt) and
the folder deleted. keyword stays the default keywordsFilename. npm test green.
2026-06-20 — Lists: second keyword pass — relocate leftovers, drop junk (593 -> 20) (2.1.0)
Hand-reclassified the keyword leftover tail: 354 entries relocated to their proper lists (animal, mythology,
astronomy, history, religion, place, person, art-movement, word/noun|adjective, look/time, etc.), 219 junk
dropped (chemical symbols, abbreviations, inflected artifacts, fragments), 20 languages kept. keyword-sfw
593 → 20. Lossless coverage-checked via scripts/list-cleanup/reclassify-keyword.mjs. npm test green. (Open:
keyword is the default keyword source and is now thin — needs repointing.)
2026-06-20 — Lists: fix incompatible base.txt+base-nsfw.txt pairs (keyword, clothes) (2.1.0)
Renamed keyword/keyword.txt→keyword-sfw.txt and look/clothes.txt→clothes-sfw.txt: a plain
base.txt beside a base-nsfw.txt is ignored by the safety rule, which hid the implicit {base} button
and made the default {keyword} alias mis-resolve to the danbooru keyword group. Now {keyword} and
{clothes} resolve to their own SFW lists and the picker shows the implicit base button. npm test green.
2026-06-20 — Lists: move danbooru groups into d/ folder ({d}, {d/keyword}, {d/character}) (2.1.0)
Moved the three danbooru group files inside the d/ folder to match the {d/...} typed convention:
d/d.group (ref {d}, was {danbooru}), d/keyword.group ({d/keyword}), d/character.group
({d/character}). Updated all references ({d-character}→{d/character} across danbooru/entity/v1 prompts;
anime keywordsFilename d-keyword→d/keyword; anime detection now startsWith("d/"); web-app migration;
listTags keys). npm test green.
2026-06-20 — Lists: SFW/NSFW safety rule — ignore plain name.txt when name-nsfw.txt exists (2.1.0)
Safety precaution: when a <name>-nsfw.txt exists, a plain <name>.txt is ignored entirely (not loaded,
not listed) — the SFW half must be <name>-sfw.txt, so a lone <name>.txt beside an NSFW file is treated
as NSFW-only and SFW content can't leak from a misnamed file. Enforced in readSfwBase and
logicalListNames. npm test green.
2026-06-20 — Lists: SFW/NSFW redesign — filename-token gating + mode-aware auto-combine (2.1.0)
Reworked SFW/NSFW into a filename-driven, mode-aware model (replaces the group-file scheme). Any name with
an nsfw token is adult and is fully hidden while adult mode is off (not listed/suggested, resolves to
nothing). A mixed list is two files <name>-sfw.txt + <name>-nsfw.txt (no <name>.txt); the bare
{name} is implicit and the resolver auto-combines by mode — {name} = SFW/both, {name-sfw} = SFW-only,
{name-nsfw} = both (SFW auto-tacked on), with the same suffixes working on groups. Gating is now automatic
by name token (no hardcoded list); the three split .group files are gone; the 4 standalone adult lists were
renamed to -nsfw. New logicalListNames + includeAdult-aware resolveListLines threaded through all
loaders; suggestion pool draws bare bases only; picker hides NSFW when off. npm test green.
2026-06-20 — Lists: SFW/NSFW convention FINAL — plain=SFW, name-nsfw-only, name-nsfw=both (2.1.0)
Reversed to the final, UX-first naming: plain <name>.txt is SFW (so {name} is safe by default with
no typing), <name>-nsfw-only.txt is NSFW-only, and <name>-nsfw.group imports both — so {name} = SFW,
{name-nsfw-only} = NSFW-only, {name-nsfw} = everything. Applied to danbooru general and rebuilt the
groups (danbooru/danbooru-nsfw, d-keyword/d-keyword-nsfw, d/general-nsfw). Gating now targets the
NSFW-bearing names; plain SFW names are ungated. Build script, listTags, README, and list-architecture
updated. npm test green.
2026-06-20 — Lists: SFW/NSFW convention name=both, name-sfw, name-nsfw (2.1.0)
Finalized the split naming: <name>-sfw + <name>-nsfw exclusive files plus <name>.group importing both,
so plain {name} = both, {name-sfw} = SFW-only, {name-nsfw} = NSFW-only. Applied to danbooru general
(general-sfw / general-nsfw / general.group). Rewired groups, #danbooru, gatedLists, and the build script.
npm test green.
2026-06-20 — Lists: exclusive SFW/NSFW + full-as-group (replaces d-sfw) (2.1.0)
Replaced the d/ + d-sfw/ duplication with the proper model: SFW and NSFW are exclusive lists and the full
version is a group importing both, only for files that genuinely mix. Only danbooru/d/general qualified:
split into d/general (SFW) + d/general-nsfw (NSFW) with d/general-all.group importing both. Rewired
the danbooru groups + #danbooru + gatedLists (general ungated, general-nsfw/general-all gated); build
script does the split on regenerate. Deleted d-sfw/. npm test green.
2026-06-20 — Lists: parallel audit + misfit cleanup (2.1.0)
Audited the curated descriptor lists and AI-classified proper-noun lists with 4 parallel review subagents,
then applied fixes (fix-audit.mjs): 17 typos fixed, 47 garbage/abbreviation entries removed, 229 misfits
relocated (surnames out of given-name into person, fictional names into work, culture-tagged gods out of
mythological-creature into mythology, etc.). npm test green; leak scan only the intentional keeps.
2026-06-20 — Lists: preprocess danbooru SFW, drop runtime @filter (2.1.0)
Replaced the runtime @filter sfw (which stripped adult words live, incl. in the browser) with
preprocessed SFW-only files: danbooru/d-sfw/* are split from the full danbooru/d/* at build time
(split-danbooru-nsfw.mjs + the CSV build script), and danbooru-sfw.group reads them directly as a pure
union. Removed @filter from the engine. Full danbooru stays gated; SFW is a real, separate, ungated list.
npm test green.
2026-06-20 — Lists: .group files replace hardcoded virtual lists (2.1.0)
Composites are now plain .group files (each line a list reference, resolved by the same path-suffix
lookup), not a hardcoded virtualLists object. resolveListLines reads <name>.group, unions its members
recursively (MAX_GROUP_DEPTH=3 + cycle guard, de-duped), with an optional @filter sfw|nsfw directive.
All three loaders walk .txt + .group. Group files (danbooru, danbooru-sfw, d-character, d-keyword,
artist, artist-digipa, name) organized into their folders; gatedLists updated to the new canonical paths.
npm test green.
2026-06-20 — Words: strict WordNet POS pass + one list per POS (2.1.0)
Validated the curated word lists against WordNet (reclassify-words.mjs): kept confirmed entries, routed
602 action gerunds to a new look/action, moved 450 cross-POS words to the correct list, kept
WordNet-unknown in place. Then collapsed the dictionary lists into the curated ones — dict-adjective→
adjective, dict-noun→noun, dict-verb→verb, dict-adverb→adverb, dict-misc→word/misc — deleting the dict-*
files and the redundant *-all virtuals (one list per POS; danbooru stays separate). npm test green;
full spot-check wave clean.
2026-06-20 — Lists: spot-check + fix NSFW/misfit leaks (2.1.0)
Reviewed every list for wrong content (scan-leaks.mjs). Relocated 93 NSFW terms into gated
look/clothes-adult and word/adult, removed 1 extreme term, pulled 26 danbooru expression/pose tags out
of word/adjective into a new look/expression, removed 3 typos, and moved a stray anime title out of
artist/anime. Gated lists added to gatedLists/listManifest. Legit false positives kept (real places,
"Naked mole rat", "X-ray", "presenting", and dictionary words breast/butt/sex/oral/facial/naked/...).
npm test green.
2026-06-20 — Lists: folder organization + path-suffix name resolution (2.1.0)
Organized all lists into folders (danbooru, artist, word, name, place, lore, nature, look,
style, scene, brand, keyword) with a data/lists/README.md. New resolveName() resolves a
reference by path suffix (bare filename / partial path / full path; shallowest wins, ties by a
guaranteed natural order via compareNames() — symbols, numeric numbers, letters). Loaders
walk data/lists recursively. Danbooru files nested under danbooru/d/ so old d-general
becomes {d/general}. Basenames kept unique so existing {name} refs still resolve; only the
danbooru d-* string refs updated. Updated gatedLists, listManifest unions/tags, and the CSV
build scripts. npm test green.
2026-06-20 — Lists: proper-noun categorization, keyword.txt 8,859 -> 593 (2.1.0)
Split the remaining proper-noun dump in keyword.txt into category lists. Automatic first pass
(split-proper.mjs: compromise + city.txt membership) extracted given-name, place, organization
and de-duplicated confirmed cities into city.txt; the remaining ~4,422 were hand-classified individually
(AI world-knowledge, 9 batch files under scripts/list-cleanup/cat/, distributed by an idempotent
build-categories.mjs with a coverage check) into person, place, organization, mythology,
astronomy, people-group, religion, history, work. keyword.txt 8,859 -> 593 (only the
uncategorizable tail remains; nothing deleted — unclassified stays). New lists tagged in listManifest,
plus a name virtual (given-name + person). Slurs found mid-pass (Jap/Negress/Negroid) added to
contentSafety.js and purged. npm test green; safety scan 0.
2026-06-20 — Keyword lists: content-safety purge + full reorganization (2.1.0)
Big data-cleanup milestone across data/lists/ and the CSV sources. (1) Content safety: new
browser-safe src/contentSafety.js defines a curated, list-type-aware blocklist (slurs, content
sexualizing minors, extreme shock/gore/non-consensual) with whole-word matching and a Scunthorpe-safe
exact mode for proper-noun lists. A one-time scan/purge removed 81 entries from the lists and 47
rows from danbooru.csv; the filter is wired into process-danbooru-csv.js / process-artists-csv.js
so regeneration stays clean. Ordinary adult/nudity terms are kept and handled via the NSFW lexicon, not
deleted. (2) Dictionary reorg: the 48,750-line keyword.txt SCOWL dump was sorted with compromise
into dict-adjective/noun/verb/adverb/misc; possessives + redundant inflections + junk were dropped
(2,250 lines), leaving keyword.txt as a 10,049-entry proper-noun list (48,750 = 46,500 sorted + 2,250
dropped, fully accounted). (3) Virtual lists: new src/listManifest.js adds composite lists computed
on demand with cross-member de-dup and optional sfw/nsfw filtering. The duplicated files the build
scripts used to emit (danbooru, d-keyword, d-character, artist, artist-digipa) are now virtual
(physical files deleted, 6 artist orphans preserved into artist-special), plus new danbooru-sfw and
adjective-all/noun-all/verb-all/adverb-all. Resolution wired into both engine loaders
(nodeLoader, browserLoader) and the runtime store (helpers/listFiles.js). npm test green (0 lint
errors; smoke loads the full graph and resolved a virtual {adverb-all}). Cleanup tooling lives under
scripts/list-cleanup/. Done on branch cleanup/list-reorg for review.
2026-06-20 — Lists: WordNet-authoritative POS sort (2.1.0)
Replaced the guess-from-spelling dictionary sort with WordNet lookups (wordpos/wordnet-db dev
dependency; scripts/list-cleanup/pos-dictionary.mjs). Each of the 48,750 SCOWL words is placed in the
dict-* list(s) for the part(s) of speech WordNet actually assigns it (bond → noun + verb); capitalized
noun-only words stay in keyword.txt as proper nouns (America/Paris/December), capitalized adjective/verb/
adverb words move out, demonyms → demonym.txt, and WordNet-unknown lowercase words → dict-misc. Final:
keyword.txt 8,859 proper nouns; dict-noun 22,748, dict-adjective 7,947, dict-verb 6,171, dict-adverb
1,050, dict-misc 5,451, demonym 124 (48,750 conserved). Deleted the four superseded heuristic scripts.
npm test green; safety re-scan 0.
2026-06-20 — Lists: keyword.txt second pass + dict-adverb fix (2.1.0)
Follow-up to the 2.1.0 reorg. Fixed a POS-sort bug (bare -ly was treated as an adverb, polluting
dict-adverb with -ly nouns/names — now requires the #Adverb tag; 63 entries re-routed). Ran a second
pass over keyword.txt: 1,232 entries whose lowercase form is a real dictionary word were moved into the
dict-* lists (precise set-membership test), and 127 demonyms split into a new demonym.txt.
keyword.txt is now 8,817 genuine proper nouns. demonym wired into listManifest (tags +
adjective-all/noun-all). npm test green.
2026-06-20 — Composer: four icon-only field actions (2.0.8)
Owner: Save and Share should be icon-only action buttons in the field next to the others, not a merged
text button — Save as a disk icon, Share as the standard share icon. Split the merged control back
into two and made all four field actions a cohesive set of round icon buttons: Save (floppy disk),
Share (three-node share glyph), Random (shuffle), Generate (sparkle, primary green) — all crisp inline
SVGs using currentColor. Each of Save/Share toggles its own inline panel (save = name row, share = link
row). State went back to panel: ""|"save"|"share". npm run lint 0 errors, web-app vite build green.
2026-06-20 — Composer: chat-style field with docked actions (2.0.7)
Owner: "still doesn't look like an app." Reworked the composer into a chat-style input field — a
bordered container that lights up on focus (:focus-within), with a borderless textarea on top and
an action bar docked along the bottom edge. The actions are now part of the field: Generate is an
icon-only primary (green) round button in the bottom-right (✦), Random is a matching round icon
button (🎲) right next to it, and Save + Share are merged into one "Save / Share" entry point on the
bottom-left that opens a single combined panel (name-to-save row + share-link row). Removed the standalone
toolbar, the separate Save/Share buttons, and the redundant close ✕ (the toggle closes the panel). CSS +
Home.jsx only. npm run lint 0 errors, web-app vite build green.
2026-06-20 — Composer: size the prompt box, don't fill the pane (2.0.6)
Quick follow-up to 2.0.5: the editor-fill layout over-stretched the textarea to the full column height,
which looked silly. Made the composer size to its content instead — a comfortable min-height: 8.5rem
prompt box that's vertically resizable (capped at 50vh), with the toolbar directly beneath it and
the results card flowing below; the column scrolls if it gets tall. CSS-only. web-app vite build green.
2026-06-20 — Composer redesign: editor-fill layout, compact toolbar, anime toggle removed (2.0.5)
Acted on owner feedback that the middle of the SPA still felt clunky and "not like an app." Rebuilt the
composer right pane as an editor that fills its space: the prompt textarea now stretches to fill the
card (no more big empty gap below a short box; a hover ✕ clears it), and a compact action toolbar
pins to the bottom — a prominent Generate, a 🎲 Random, and smaller secondary Save / Share
buttons. Save expansion moved off its own card into an inline panel triggered from the toolbar (it
shares the same inline-panel pattern as Share). When prompts are generated, the results card splits the
column with the composer and scrolls internally. Removed the Normal/Anime "Style" toggle: the "Anime"
lists (d-keyword.txt, a Danbooru tag dump) silently mix SFW with explicit adult tags, so there was no
way to get anime without adult — pulled pending a proper SFW/adult split of the word lists (see
plans/removed-pending-readd.md). loadSettings now migrates any browser stuck on d-keyword/d-artist
back to the safe keyword/artist defaults. Verified: npm run lint 0 errors, web-app vite build
green. (SPA-only; the separate in-progress list-gating work in the tree — src/gatedLists.js,
data/lists/keyword-adult.txt — was left untouched.)
2026-06-19 — App-frame layout + simpler prompt component (2.0.4)
Continued the home-page refinement from owner feedback ("feel more like an app", "the prompt component
is clunky/cluttered"). Turned the centered max-width page into a full-bleed app window (100vh:
title bar, full-height bordered panels, slim footer status bar) and widened the left panel. Replaced
the building-blocks accordion with an app-style category tab nav + chip area for the active
category (no <details>). Rebuilt the prompt component to be minimal: the textarea's rotating
placeholder now is the random suggestion (cycles every 5s); Random just drops the current
suggestion into the box; Generate uses whatever is typed (falling back to the suggestion when
empty). Removed Preview (owner: "needs to go" — it was a redundant expand-to-preview). Kept the
Normal/Anime word-list switch but relabeled it Style with a ? help tooltip explaining it.
Reworked Share link (owner: "make it visually and functionally work and better"): clicking it now
reveals the link in a selectable field with a Copy button and a clear "✓ Copied" state, and the link
stays visible so it works even when the clipboard API is blocked. Verified: npm run lint 0 errors,
npm run smoke green, web-app vite build green.
2026-06-19 — Refine the SPA home page (2.0.3): declutter + two-pane layout
Acted on owner feedback to make the home page look better and less cliché. Dropped the made-up tagline
and the centered hero (logo → title → tagline) — the logo + wordmark now live only in a slim top-bar.
Swapped the display font from Rokkitt (read as Impact-like) to Space Grotesk. Moved the
building-block cloud into a sticky left pane (Home.jsx is now a two-pane .workspace grid that
collapses to one column ≤860px) and re-added the rotating random suggestion (a fresh #random
prompt cycling every 5s, click to fill). Removed, for now, image generation, the chaos knob, presets
(apply + save), the Settings button/drawer, and the local/online mode badge — all tracked for re-add in
notes/plans/removed-pending-readd.md (presets to return richer:
full settings + auto-generation). Verified: npm run lint 0 errors, npm run smoke green, web-app
vite build green.
2026-06-19 — Redesign the SPA home page (2.0.2): unified composer + brand restyle
Reworked the React SPA from a cramped light-themed single "Build" tab into a polished home page
modeled on the pre-revival generate screen. Restyled styles.css to the brand: dark charcoal
canvas, mint-green accent (the pencil logo), Rokkitt display + Maven Pro body via Google
Fonts, pill inputs, rounded buttons, a centered hero with the app icon. Merged the separate Build and
Generate flows into one Home.jsx composer (prompt textarea + primary actions, Normal/Anime
segmented toggle, chaos, presets, live preview, generated-prompts list with copy, in-session gallery,
save expansion/preset, and a collapsible building-blocks cloud). Tucked the full settings form
into a new right-side SettingsDrawer.jsx (overlay + Esc to close) so the page stays uncluttered,
and reworked App.jsx into a brand top-bar (logo, local/online badge, Settings button) + hero +
Home + footer. Added web-app/public/ with the logo + favicons. Removed the superseded
Builder.jsx / Generate.jsx (folded into Home). Verified: npm run lint 0 errors, npm run smoke green, web-app vite build green, and the page rendered + screenshot-checked in a browser.
2026-06-18 — Fill out the notes + ignore the whole generated /docs/ tree
Completed the notes: wrote the historical changelog for the 2022–2023 original build (version/2022-12.md
→ 2023-04.md, reconstructed by theme from the full git log) and indexed it in version.md; closed the
remaining June-2026 changelog gaps (the src/+data/ reorg / 2.0.1, the Build-tab-only UI, Vite 6→8,
the lockfile + lodash-pin fixes, deployments-held, the DSL/history + block-catalog doc commits, the
smoke-test.mjs @file); and refreshed status.md, plans/next-steps.md, and decisions/architecture.md
to the current state (all four revival strands + three doc-tooling decisions). Also broadened the
.gitignore from /docs/jsdoc/ to all of /docs/ and removed the stale Doxygen docs/html/ output
left over from the retirement — the entire /docs/ tree is generated now.
2026-06-18 — Document the web-app React SPA (JSX wired into the JSDoc site)
Closed the last gap: the web-app/ React SPA. Added @module + per-function JSDoc to all 16 SPA files
(lib, providers, the 8 React components, App, main, the Netlify function). JSDoc can't parse JSX, so
build-docs.mjs now babel-transpiles web-app/src + netlify into a tmp/webapp-docs mirror (JSX
stripped, comments kept) that JSDoc reads; @module tags give clean nav names. Fixed the JSDoc
source.excludePattern (a generic /lib/ was excluding web-app/src/lib), added tmp/** to the ESLint
ignores, and simplified a tuple-type @returns. Every authored file in the repo is now documented in
one JSDoc site (~244 pages). npm run docs exit 0; lint 0 errors, smoke green, SPA build green.
2026-06-18 — Add @file to scripts/smoke-test.mjs
Closed the last file-header gap: added an @file header to scripts/smoke-test.mjs, so every authored
file under src/, data/, and scripts/ now has one. Comment-only.
2026-06-18 — Doc-site: replace Doxygen with a single JSDoc + docdash site (notes wired in)
Retired Doxygen here in favour of one generator. npm run docs now runs scripts/build-docs.mjs, which
wires the whole notes/ tree (+ list-credits / list-help / Upgrade-2-0) into JSDoc tutorials
with a hierarchy mirroring the old _nav.dox, and rewrites inter-note links to resolve — then runs JSDoc
with the docdash template. The result is one site (docs/jsdoc/) with the README home, the
per-function code API, and the living notes, plus a sidebar + search. Removed Doxyfile,
docs/doxygen-awesome/, and notes/_nav.dox; switched pages.yml + release.yml to build the JSDoc
site; updated documentation.md, deployment.md, fix-patterns.md, notes/README.md, and CLAUDE.md.
Builds clean; lint/format/smoke green.
2026-06-18 — Per-function JSDoc on the frontend scripts (docs now complete everywhere)
Closed the last gap: added per-function JSDoc to every top-level function in the 8 web/frontend/*
browser scripts (84 functions) via a UTF-8-safe, idempotent script (inferred param types, humanized
descriptions, value-return detection). With this, every named function in the codebase is documented
— server-side, all 113 blocks, and the frontend; only anonymous callbacks (route arrows, jQuery
closures) are left, which no doc generator can extract. JSDoc builds clean (170 pages); lint/format/smoke
green. Comment-only.
2026-06-18 — Per-function JSDoc: full server-side API + all 113 blocks
Took the JSDoc coverage from file-level to per-function across the whole server side: @param /
@returns / descriptions on every function in the prompt engine (prompt-modules, helpers, core/), the
variation/reroll/upscale/animation loaders, the settings loaders, common (batch loop), genImg,
promptFilesAndSuggestions, the server.js helpers, and the self-healing image index — plus a uniform
documented contract on all 113 block generators (parsed each one's real parameter list).
These now extract as a true per-function API in docs/jsdoc/. The legacy web/frontend/* browser
scripts stay file-level on purpose (jQuery client being retired). Comment-only; no runtime change.
(Also: restored 3 files' em-dashes that an earlier ANSI-vs-UTF-8 PowerShell round-trip had mojibake'd,
and fixed an invalid JSDoc optional-property type the parser rejected.)
2026-06-18 — Add JSDoc (the tool) for an ESM-native code API
Wired up JSDoc 4 (npm run docs:api → docs/jsdoc/, config jsdoc.config.json) alongside Doxygen,
reusing the @file headers already in the source. JSDoc parses ESM / export default where Doxygen
cannot, and renders a page per file with README.md as the homepage. Division of labour: Doxygen
hosts the living-notes site + GitHub Pages; JSDoc builds the code API. File-level today (every file
has an @file header); per-function @param/@returns on the named exports is a deliberate future
pass. See reference/documentation.md.
2026-06-18 — Comprehensive file-level Doxygen docs: @file on every authored JS
Added a /** @file @brief */ header to every authored JavaScript file — 165 under src/ (44 with
richer, notes-linked module headers; 113 blocks; 8 frontend scripts) plus the 3
data/process-*.js build scripts — so the Doxygen File List describes every file. Doxygen can't
extract this code's symbols (the generators are anonymous export default function), so coverage is
deliberately file-level plus the conceptual notes pages, not per-function — recorded in
reference/documentation.md. Also cut the Doxygen build warnings 7 → 3
(rest benign) by fixing escapes/anchors and an inline-code-span-wrapped-across-a-newline quirk in
esm-patterns.md. Comment/docs only — no runtime change.
2026-06-18 — Exclude assets/ from ESLint, Prettier, and Doxygen
The pinned assets/references/ source snapshot is gitignored, but the tools walk the filesystem and
were indexing it (569 phantom lint errors from the old CommonJS/jQuery clone; a polluted doc-site).
Excluded assets/ in eslint.config.js, .prettierignore, and Doxyfile. Gitignored ≠ tool-ignored —
see reference/fix-patterns.md.
2026-06-18 — Document the block catalog + the image deep-link graph
Notes-only. Added reference/blocks.md — a catalog of all 113
block generators (full vs partial, the _-core/user-/v1 conventions, how #random /
#extra-random-prompt discover and weight them) — and documented the image deep-link graph (how a
saved image's JSON encodes its command + original settings so re-rolls, variations, upscales, and
animation frames link back to their parent). No code change.
2026-06-18 — Verify original-vs-current intactness (session log)
Notes-only. Recorded in the session log the full verification that the 2022–2023 prompt logic survived the
CommonJS→ESM migration intact — an inventory diff of the original tree against src//data/, plus a
logic-parity spot-check beyond the mechanical require→import changes. Nothing was lost; the differences
are module-system and file-location only.
2026-06-18 — Document the original prompt DSL + enrich pre-revival history
Notes-only. Wrote reference/prompt-dsl.md (the sigils the engine
understands — <expansion>, #block, {list}, {salt}, AND-stacking, per-engine
emphasis/editing/alternating) and enriched context/history.md with the
2022–2023 build arc, so the project's own language and origins are written down rather than living only in
the code and the git log.
2026-06-18 — Record that deployments are intentionally held
Notes-only. Documented that master is deliberately held at the last pre-revival commit (241a148) and
that GitHub Releases / Pages deploys are paused on purpose — the rewrite is too early to ship. This is
a status decision, not a bug: work continues on dev. See ../status.md and
../reference/deployment.md.
2026-06-18 — Pin lodash to the SPA copy so the Vite 8 / Rolldown build resolves it
Build fix. The repo-root core/ engine and every block import _ from "lodash", but they live
outside web-app/, so resolution from them wouldn't find the SPA's lodash — and Vite 8 / Rolldown treats
an unresolved import as a hard error (Vite 6 / Rollup only warned). web-app/vite.config.js now aliases
lodash to the SPA's own installed copy (and dedupes it), so the build is self-contained and works on CI
and Netlify, neither of which installs the repo-root node_modules.
2026-06-18 — Regenerate the lockfile with cross-platform optional deps for CI npm ci
Build fix. The web-app lockfile was missing the platform-specific optional dependencies (Rolldown/SWC
native binaries) that npm ci needs on CI's Linux runners, so a clean install failed. Regenerated it to
include all platforms' optional deps.
2026-06-18 — Upgrade Vite 6 → 8 and @vitejs/plugin-react 4 → 6
Took the SPA's build tooling to current majors: Vite 8 (Rolldown-based) and @vitejs/plugin-react 6. This is what surfaced the stricter unresolved-import handling addressed by the lodash-pinning and lockfile fixes above.
2026-06-18 — Show only the Build tab while the UI is reworked
Temporarily hid the Generate and Settings tabs in the SPA, leaving only Build, while that part of the UI is reworked — so the in-progress tabs aren't exposed. A presentation-only change; the underlying code stays in place.
2026-06-18 — Reorganize into src/ + data/; modernize toolchain; add CI/docs/notes (2.0.1)
Tidied the repo's shape after the ESM migration: all code moved under src/ and all prompt content
(lists, expansions, presets, the CSV sources) under data/, with runtime/user data (output/,
user-settings.json, results.json) staying at the repo root. This is the layout the project keeps today
and the reason src/chdir.js pins the cwd to the repo root (its parent). Bumped to 2.0.1 (VERSION +
package.json in sync). Also rounded out the toolchain and CI/docs/notes scaffolding around the new layout.
2026-06-18 — Prompt Builder power tools: blocks cloud, share links, custom expansions/presets, chaos (phase 4b)
Brought the rest of the original generate screen's prompt-building power into the SPA after a closer
look at web/views/generate.pug + generate.js. The Builder now has a categorized building-blocks
cloud (Full / Partial blocks, Expansions, Lists, User, V1, and a Special section with
{salt}) with Title-Case labels and a single search — clicking inserts the token. Added Share Link
(encodes the current settings + prompt into a URL hash, base64, API keys excluded; a shared link seeds
settings on load), Save as Expansion and Save Preset stored in localStorage (the no-server
equivalents — custom expansions are usable as <name> and are merged into the engine via a composite
loader), a Chaos control (scales emphasis/alternating like the CLI's --chaos), and Anime /
Normal quick toggles (swap the danbooru vs normal keyword+artist lists).
Refactor: the lib facade (promptEngine.js) now owns the composite loader, the categorized blocks, and
the preset list (built-in + custom); catalog.js is a thin re-export. The core engine and CLI were
left untouched (chaos is applied in the web facade). Build verified.
2026-06-18 — Web SPA features: Settings editor, Prompt Builder, Generate gallery (phase 4)
Built out the SPA beyond the shell. Three tabs now: Generate (build prompt(s) client-side, run them
through the selected provider, in-session image gallery with per-image download), Build (a prompt
builder — searchable pickers to insert blocks #, lists {}, and expansions <>; apply a
preset; "surprise me" #random; and a live expansion preview), and a full Settings editor exposing
every prompt knob (counts, source lists, emphasis, editing, alternating, artists, auto-fx, salt, mode)
plus image params and the provider/key config — all persisted to localStorage.
Mechanics: browserLoader now also bundles presets/*.json; a new lib/catalog.js turns the loader's
catalog into insertable tokens; expandPrompt() drives the preview. The UI was restructured from one
file into focused components (Field, Settings, Builder, Generate, TokenPicker, Gallery) with
a small tab router. Build verified (268 modules).
Intentionally out of scope (they need server storage the app deliberately doesn't have): the old persistent image library/feed, search index, single-image stats, delete/download-from-disk, ImageMagick conversions, and animation file handling.
2026-06-18 — Browser-safe prompt-engine core (web migration, phase 3)
Ported the real prompt engine to run in the browser. Added a framework-agnostic core/ — an engine
(createEngine(loader)) that runs the full prompt-module pipeline (expansion, block with
danbooru/auto-fx, salt, list with emphasis/editing/alternating, cleanup) over a prompt, taking all data
through an injected loader. Two loaders implement it: nodeLoader (fs + createRequire) and
browserLoader (Vite import.meta.glob, which bundles all 113 blocks + the lists/expansions
text). The pure stages (prompt-salt, cleanup) and the random* helpers are reused directly, so there is
no duplicated prompt logic.
To make the chain browser-safe: the two built-in list aliases were split into a tiny dependency-free
helpers/aliases.js so keywordRepeater no longer imports the fs-backed listFiles; and
src/promptFilesAndSuggestions.js (the #random suggestion builder, reached via
extra-random-prompt → random-prompt) was refactored to be loader-injected instead of using
fs/createRequire, so it now runs in both Node and the browser (a single shared implementation). It's
configured once per host — Node in common.js (covers CLI + server), browser in the SPA's engine
wiring. A latent bug was fixed in passing: one accumulator (fullRegularExcluded) wasn't reset between
scans, so the suggestion pool grew on every #random call.
The SPA's placeholder engine is gone — it now runs the real one. Verified with a Node engine smoke
(#random, #simple-random-prompt, #extra-random-prompt, #beach all expand fully, no leftover
tokens), the existing Node CLI smoke, and a green web-app build (235 modules bundled). Known
follow-up: the eager glob bundles the list/expansion text into the JS (~712 KB gzipped); moving the
larger lists to runtime fetch is a size optimization for later.
2026-06-18 — Scaffold the React + Vite web app (online + local migration, phase 1)
Began the move to a React + Vite SPA usable online (bring-your-own-key) or locally. Added web-app/
(Vite + React 19) with a localStorage-backed settings layer, a modular image-provider interface
(a local-WebUI provider that calls the user's own WebUI directly, and a hosted-proxy provider), a
stateless Netlify proxy function stub (BYOK; stores/logs nothing), and netlify.toml. The prompt
engine is a placeholder pending the phase-3 browser-safe core port. Build verified (vite build green,
~63 KB gzipped). Design + phased plan in notes/plans/web-migration.md; rationale in
notes/decisions/architecture.md.
2026-06-18 — 2.0.0: ES-module + Node 24 modernization
Moved the entire project off CommonJS and onto ES modules ("type": "module"), targeting Node 24
LTS. About 130 files converted from require/module.exports to import/export:
- The ~113 block files and the simple helpers/prompt-modules were converted mechanically to
export default function(+export const full/suggestion_excludewhere present). helpers/keywordRepeater.jsbecame named exports;helpers/listFiles.jsstayed a default-export object (it's indexed dynamically). Settings files becameexport default { … }.- The entry points (
index.js,server.js,common.js) and thesrc/loaders were hand-converted. Dynamic JSONrequires becameJSON.parse(fs.readFileSync(...)); the config-driven plugin loaders (blocks, prompt modules) kept synchronous loading viacreateRequire(import.meta.url)— Node 24 canrequire()ES modules — calling.default(...)on the namespace. - New
chdir.jsis imported first bycommon.jssoprocess.chdir(import.meta.dirname)runs before any settings module reads a cwd-relative file (ES-module import ordering made this necessary; the old inlineprocess.chdir(__dirname)would otherwise have run after the settings import).
Dependencies were taken to current majors: Express 5, yargs 18, open 11, cli-progress 3, crc 4,
compromise 14, lodash 4, pug 3. node-fetch was removed — Node 24's global fetch replaces it in
genImg.js, imageUpscaler.js, and server.js.
Tooling added: ESLint 9 (flat config, separate Node-ESM and browser-script configs), Prettier 3,
.editorconfig, .nvmrc (24), engines.node >= 24, and npm scripts (start, server/webui,
lint, format). A repo-wide Prettier pass reformatted the code.
Verified with node --check (152 files, 0 syntax errors), npm run lint (0 errors), and an import
smoke test that loads the full module graph + all blocks and expands a prompt. Live image
generation still needs a running Stable Diffusion WebUI and was not exercised.
2026-06-18 — Set up the CLAUDE.md + notes AI-collaboration system
Added a root CLAUDE.md and a full notes/ tree (status, changelog, sessions, context, systems,
reference, decisions, plans), modeled on a sibling project, plus a VERSION single-source-of-truth.
This is documentation scaffolding so any AI or human can orient on the repo cold; it carries no code
change.