Newest entry on top.
SPA internationalization with react-intl (2.15.0, branch feature/i18n-react-intl)
Internationalized the entire React SPA with react-intl + the full FormatJS tooling pipeline, on a
feature/i18n-react-intl branch off dev. Owner chose: ship other languages only if quality is
genuinely good (else English only), the full FormatJS pipeline, and a feature branch + commit.
Shipped-locale decision. The app is dense with domain jargon (DPL, prompt "salt", wrappers,
expansions, block tokens, NSFW gating) that machine translation handles poorly and I can't
verify — so per the owner's quality bar, English is the only real locale shipped. The pipeline makes a
real language a one-file drop-in, and a generated en-XA pseudo-locale (accented/expanded English)
ships as a coverage aid that exposes any missed string.
Core (gui/src/i18n/): config.js (locale registry + resolveLocale over navigator.languages),
loadMessages.js (eager import.meta.glob over compiled/*.json; source en uses inline
defaultMessage), I18nProvider.jsx (IntlProvider wrapper, <html lang/dir> sync, quiet
MISSING_TRANSLATION fallback). App.jsx split into a thin root (settings + i18n boundary) + AppShell.
A Display language select added to Settings.jsx, persisted in settings.locale (default "auto").
Extraction: every user-facing string across all ~28 components wrapped with defineMessages +
intl.formatMessage / <FormattedMessage> — text, title/placeholder/aria-label, confirm/prompt
dialogs, ICU plurals/number formatting. ~407 messages. Explicit namespaced IDs (app.*, home.*,
manage.*, …). English renders byte-identically, so the Playwright visual/text baselines are untouched.
ICU gotchas handled: literal braces ({#fx} → '{#fx}') and an apostrophe-laden tooltip (reworded).
Tooling: babel-plugin-formatjs wired into @vitejs/plugin-react (auto IDs matching the extractor);
@formatjs/cli scripts i18n:extract / i18n:pseudo (needs --ast) / i18n / i18n:check; a focused
gui/eslint.config.js (formatjs-only, no-op react-hooks stub so existing disable directives resolve)
runs enforce-default-message via npm run lint:i18n. Added react-intl, @formatjs/cli,
babel-plugin-formatjs, eslint-plugin-formatjs, eslint to gui/package.json.
Tests: new gui/tests/testUtils.jsx exports a render wrapped in IntlProvider; Settings.test +
TokenPicker.test switched to it (Field tests untouched — presentational).
Deliberately deferred (English-only): gui/src/lib/dpl/validateDpl.js (editor lint diagnostics) and
gui/src/lib/dpl/dplInserts.js (DPL syntax teaching catalog) — isomorphic non-React modules shared with
the core engine + a test suite keyed on English messages; threading i18n through them is a separate,
riskier refactor noted for follow-up.
Verification (all green): repeated npm --prefix gui run build; npm test at engine-v3 root — lint
(0 errors, 18 pre-existing warnings), smoke OK, test:unit 128 passed, test:web 60 passed; and
npm run lint:i18n clean. VERSION + package.json → 2.15.0 (MINOR). Not yet released — awaiting the
owner's go-ahead to merge to main.
DPL focus dial + global layer auto-merge + $ keyword sigil (2.13.0)
Implemented three linked DPL features (owner request), engine-first (content fluff-revival deferred to a follow-up pass):
-
Focus dial — a sibling of intensity in
dpl.js+block.js: token arg{#name fNN%},[f<NN%]line conditions,$focus/$focus%/$focus-wordkeyword (word ladder loose→pure), relative±, threaded to generators as the 5th arg (ctx.focus; both loader bridges pass it to JS sidecars). Per the owner's clarification, focus is author-judged (no auto-scaling) — an AI/human tags each line's fluff threshold ([f<10%] a nearby cityin a cave, etc.). Low focus fills the frame; high focus keeps only essentials and stacks cleanly. -
Mandatory
i/fprefixes +$sigil. Conditions/token-args must prefix the dial ([i<10%],{#name i25% f80%}) since the two percents look identical; an unprefixed25%/<10%is not dial syntax (owner was explicit: no unprefixed default). Keyword tokens moved off{intensity}(collides with{list}) to the$sigil — and since the dial is a percent,$intensityrenders50%(no$var%form, per owner) and$intensity-wordthe word. Migrated all in-tree content losslessly via byte/EOL-preserving PowerShell passes:{intensity…}→$intensity-word(28 files) and bare[<NN%]→[i<NN%](18 files). -
Global layer auto-merge (dedup). A generator renders once per prompt; a nested re-import of a singular generator drops out (user-typed duplicates always render).
stacking: true(aliasmulti) front-matter exempts a generator — set on the chained decoratorscolor/glow/neon/crystalso dedup doesn't regress them. State is per-block()call; keyed on the resolved generator key; pass-0 = user tokens, pass≥1 = imports. -
Editor (CodeMirror DPL language,
gui/src/lib/dpl/dplLanguage.js+styles.css). Added highlighting for the dials (i25%/f80%args,[i<..]/[f<..]conditions,$intensity/$focus), front matter (fences/keys/values), section heading names + specialStart/Auto Begin/Auto End(via aViewPluginmark-decoration — a stream tokenizer can't see the next===line, so the heading-name line is decorated cross-line instead), andgo to/insert/+name targets. Added context-aware autocompletes:i/fdial popup (intensity-vs-focus descriptions) on a space inside{#name …}, front-matter keys,go tosection names, andinsert/+generator+section names. Verified the completion logic with a throwaway node harness (9 cases) since there's no jsdom test for the language module. Also normalized a blank line after the front-matter fence across 89.dplfiles (owner preference). -
Content pass — focus-tagging + fluff revival (started). Reworked 9 core place/nature scenes (
cave,beach,city,landscape,mountains,ruins,house,park,castle) onto a consistent focus ladder: essentials unconditioned;[f<70%]atmosphere ({#nature}/{#weather}/…);[f<65..40%]extra depth;[f<35..15%]out-of-subject fluff revived from v1/v2 (distant settlements, a city beyond the cave, a dragon over the castle, …). Verified the gradient end-to-end with a node harness: at f95 a cave is just "cave, cave walls, cavern, …"; at f10 it fills with every layer + fluff. Remaining scenes (interiors, vehicles, space, micro-*) + subjects still to do. -
100-step word scales + size list.
$intensity-wordand$focus-wordwent from 6-bucket ladders to 100-word scales (one word per percent, owner-curated):INTENSITY_WORDS(size/amount/scale, 50≈normal) andFOCUS_WORDS(broad→pure, 50≈balanced) insrc/core/dpl/dpl.js. Also expanded thelook/sizelist (5→~128) with the deduped union of the owner's good size words (suffix-variants collapsed; the intentional ultra/extra/very-tiny sequence kept). Word-scale test assertions updated.
Tests: extended tests/unit/dpl.test.js (dial conditions both dials, $-keywords, ref normalization,
legacy back-compat) and tests/integration/enginePipeline.test.js (focus dial, dedup, stacking, focus as
5th JS arg). Green: npm run smoke, test:unit (119), test:web (51), lint (0 errors), gui build.
Notes: added reference/focus-design.md + reference/layering-design.md, updated
reference/intensity-design.md to the $/i/f syntax and data/blocks/README.md. VERSION +
package.json → 2.13.0 (MINOR). Not yet committed/merged at time of writing.
Manage tab — phase 6 + merge to dev + CI fix (2.12.0)
Added Node contract tests (tests/integration/manageFs.test.js, 11 — snapshot↔loader equivalence,
ops round-trips, traversal guard, ghost detection); updated dependencies.md
(@codemirror/lang-javascript) and status.md; bumped VERSION+package.json to 2.12.0 (MINOR).
Full npm test green (Node 111 + SPA 51). Merged feature/manage-tab → dev --no-ff, deleted the
branch.
CI caught a real bug the local build hid: the merge run went red — SPA "Build gui" failed with
UNRESOLVED_IMPORT ./InlineImageControls.jsx, and Playwright failed as a cascade. Cause: Home.jsx
edits for the catalog subscription swept in pre-existing uncommitted WIP — an InlineImageControls
import+usage whose file was untracked — plus several styles.css hunks (provider-redesign WIP). It
built locally (file on disk) but CI checks out only committed files. Fix: committed the untracked
InlineImageControls.jsx (completing the sweep rather than ripping the WIP back out — less
destructive, and it's the owner's own work; builds + SPA tests pass). dev CI now green. Lesson:
when editing a file that already shows uncommitted changes, check whether those changes are mine before
staging the whole file (the working tree had genuine WIP mixed with CRLF noise). 2.12.0 is on dev,
CI green — awaiting the owner's go-ahead to release to main. Follow-up: jsdom component + Playwright
visual baselines for the Manage tab.
Manage tab — phase 5 (add/delete, drag-move, external watch)
Interactive structure ops: always-visible "+" on folders/roots → menu (New block/list → creates + opens for edit; New folder → empty folders now render); entry pill Delete (content+sidecars, confirmed)
- folder editor "Delete folder" (recursive); drag entry pills onto folders/roots to move (content +
sidecars, drop highlight). External-edit watch via SSE (
/api/manage/watch, fs.watch both roots) → debounced client refresh; manual Refresh stays as fallback. Tweaked manageTree folder filter to keep empty folders (hide only nsfw-named when adult off). Build green. Reused the already-verified fsOp primitives (mkfile/move/delete/traversal). Next: phase 6 (tests, docs, dep/credits, version bump, merge to dev, ask before main).
Manage tab — phase 4b (ghost entries + published manifest)
Owner wanted deleted-locally-but-online files shown as restore-only "ghost" pills. First drafted it
against the GitHub git-tree API; owner suggested a simpler published manifest instead — pivoted to
scripts/build-data-manifest.mjs → data/manifest.json (npm run manifest, regenerate at release).
remoteManifest fetches it from main and disk-caches in the OS temp dir for a day (hash-named file,
checked on boot, ~once/day refresh, stale-cache offline fallback) per owner's caching request.
computeGhosts/injectGhosts do the set difference + place ghosts (synthesizing ghost folders);
Manage renders faded dashed restore-only pills. Verified ghost detection headlessly (exact), manifest
gen (76/89), build green. Note: ghosts only light up once manifest.json is on main (after release);
until then getRemoteManifest 404s → null → no ghosts (graceful). Next: phase 5 (add buttons, drag-drop,
external watch).
Manage tab — phase 4 (list editor + restore-default)
ManageListEditor: entry mode (virtualized rows, search, add/inline-edit/delete — smooth at 27k lines,
no size warnings) + raw mode (CodeEditor); fetch-once, in-memory line array, trailing-newline
preserved; rename + description sidecar + Restore default. Added /api/manage/restore +
manageFs.restoreFromRepo. Branch surprise: owner said restore from master, but master is a
stale OLD-layout branch (root data/, no engine-v3/) — restore 404'd. Confirmed with owner to use
main (has the engine-v3 layout, "Release 2.11.2"); verified main raw 200 + 404→delete path. Wired
list/group routing in Manage. Build green. Next: phase 5 (add buttons, drag-drop, external watch).
Manage tab — phase 3 (block + folder editors, save + hot-apply)
Built the editors: ManageBlockEditor (DPL via DplEditor + JS sidecar with DPL⇄JS tabs / create-from-
boilerplate / JS CodeMirror, name/description/NSFW-gated, save hot-applies, JS-runs-on-reload note),
ManageFolderEditor (rename, sidecar priority/description/forceList, force-prefix + group-mode markers
as plain controls), and a reusable CodeEditor. Added /api/manage/{sidecar,marker,fs} + manageFs
helpers (mergeSidecar/setMarker/fsOp) + client wrappers; installed @codemirror/lang-javascript. Wired
selection routing + handleChanged (hot-apply + reselect-on-rename) in Manage; exposed
enableGroup/disableGroup on tree nodes. Verified the whole data round-trip headlessly (create/sidecar/
marker/rename+sidecar/delete + traversal guard + cleanup, 9/9); SPA build green. Next: phase 4 (list
editor — entry + raw — and restore-defaults).
Manage tab — phase 2 (the tab + read-only folder tree)
Built the Manage tab UI (Manage.jsx + manageTree.js + styles + App wiring). Left pane is the real
nested folder tree of both roots with categories vs subfolders color-coded, force-prefix/group folders
badged (the _-markers abstracted away), per-folder gear, entry pills with a hover Edit action (no
insert), search, and a Refresh button; right pane is a detail/preview (editors next). Tab is the 4th
(after Single), gated on the local-mode capability probe (not the build flag) — locked online. Verified
the model headlessly: Blocks 7 categories (prompt force-prefixed, expansion/* nested groups), Lists 11
categories with the expected force-prefix dirs, NSFW gating drops 4 adult lists when off. SPA build
green. Next: phase 3 (block + folder editors with save/hot-apply).
Manage tab — plan + phase 1 (runtime loader + local-mode API)
Owner asked for a new Manage tab (4th, after Single): an in-app content manager for the
catalog — edit generators/lists, the folder/category structure, sidecar options, with NSFW gating,
big editor pane, real folder tree, pill hover Edit/Delete, drag-drop, restore-from-master. Wrote
the full plan to notes/plans/manage-tab.md and iterated on it with the owner. Settled decisions:
runtime/hot-apply like v1–v2 (clean only — no eval); edits the real data/ files; local mode vs
online are runtime modes (not dev-vs-prod), Manage gated on a capability probe; restore fetches from
the stable branch master; no JS hot-reload but full JS-sidecar editing (boilerplate +
DPL⇄JS toggle, save-but-reload-to-run); seamless large lists (no size warnings); drag just nudges
priority (ride on existing sort).
Built phase 1 (plumbing, no UI) on feature/manage-tab: gui/server/manageFs.js (snapshot/tree/
path-guard/atomic-write), /api/manage/{ping,snapshot,tree,file}, gui/src/lib/runtimeLoader.js
(snapshot-backed loader, bundle fallback for .js/presets), manageApi.js, a rebuildable
promptEngine.js with refreshCatalog() + catalog pub-sub, dpJsModule export on browserLoader,
and bootstrap/subscribe wiring in App/Home. Verified a snapshot reproduces the Node loader's
catalog exactly (87 lists / 89 generators, dpJs=0); SPA build + smoke green. Next: phase 2 (the tab
- read-only folder tree).
fix stale web-app/ → gui/ refs in architecture.md
Cleaned up the three remaining current-tense web-app/ references in notes/decisions/architecture.md
(the SPA folder is gui/): the npm --prefix web-app run build verify command and the JSDoc-SPA
decision's web-app//web-app/src mentions. Left the tmp/webapp-docs build-artifact name alone
(separate token, unverified). Notes-only, no version change.
postinstall hook: one npm install covers the gui deps (2.11.2)
Follow-up to the README clarity fix below. Owner approved making the gui install automatic so the docs
become a backstop instead of a tripwire. Added "postinstall": "npm --prefix gui install" to
engine-v3/package.json; now cd engine-v3 && npm install installs both packages, and the
@vitejs/plugin-react fresh-clone failure can't happen from a single install. Kept web:install for
reinstalling just the gui deps; simplified the README run-from-source block back to one install. CI
unaffected (the gui job still installs+builds the SPA on its own). PATCH 2.11.2 (VERSION +
package.json). Verified: npm install re-run fires postinstall and installs the gui deps; lint clean.
Tooling note: did all of this with PowerShell + the file tools — no bash sandbox (it had failed
the previous commit with .git/objects permission errors and CRLF mangling). Hardened the
no-bash-use-powershell memory accordingly.
README clarity for fresh clones (build & run from source)
A user cloning the repo hit Cannot find package '@vitejs/plugin-react' after npm install +
npm run web: they'd installed only the engine-v3 deps, not the SPA's. The SPA in gui/ is a separate
npm package (npm run web is just npm --prefix gui run dev), so its deps need their own install.
Root cause in the docs: the README quick-start showed npm install → npm run web with no gui install,
and still referred to the SPA as web-app/ (renamed to gui/).
Fix (README.md, docs only — no version bump): rewrote the engine-v3 Build & run from source
block to list both installs (npm install → npm run web:install → npm run web) with a one-line
"separate dependencies" note; split the production build (web:build) into its own block; moved
testing (npm test / test:e2e) into a separate Development subsection pointing at notes/.
First pass over-emphasized the step (callout quote + caps) — toned down to a plain step per owner
feedback. Verified the README working-tree diff was CRLF-only before editing (git diff --ignore-all-space empty); prettier clean.
Still offered, not done: a postinstall hook on engine-v3 so a single npm install also installs the
gui deps (makes the docs a backstop instead of a tripwire), and fixing the remaining current-tense
web-app/ references in notes/decisions/architecture.md.
open generated images reliably in a new tab — online build (2.11.1)
Owner: clicking a generated image online "sometimes works, sometimes doesn't" and tries to open a
website / pull online instead of the cached image. Cause: PromptResult renders each image as
<a href={img} target="_blank">, and online there's no single view so onImageClick is undefined →
the browser just navigates href. For data: URLs (OpenAI/Gemini/Grok/Stability return base64)
browsers block top-level data: navigation (the intermittent failures); for remote URLs
(fal/Leonardo/DALL·E-3) it opens the remote address, not the cached image.
Fix: openImageInNewTab() in lib/output.js — converts a data: URL to a blob: object URL
synchronously (via atob, no await, so window.open stays inside the click gesture and dodges
the popup blocker) and opens that; blob: / served / remote URLs open directly. PromptResult's image
link now always preventDefault()s and routes to the single view (local) or openImageInNewTab
(online). PATCH 2.11.1. lint clean, online build green, npm test green.