Tutorial: 2026-06-27

2026-06-27

Newest entry on top.

online build fully static — browser-direct generation (2.11.0)

Owner's call after the function-limit discussion: don't restrict users, keep it free, and don't let the 10s timeout / 125k-invocation cap make it unusable. Answer: stop using a backend for the online build — call providers straight from the browser (BYOK key → provider, never our infra). Ran a live CORS preflight against all hosted endpoints from prompt.fairyfox.io:

  • CORS OK → browser-direct: OpenAI, Gemini, Grok, Stability, Leonardo, fal.ai.
  • No usable CORS → must stay proxied (locked online): Replicate, BFL/FLUX, Ideogram.

Implemented: the 6 get transport: "browser-direct"; their generate.js calls their own server.js directly (dropped callProxy). src/lib/rewrite.js calls the rewrite adapter directly for browser-direct providers (OpenAI/Gemini/Grok). The 3 non-CORS stay hosted-proxy and are locked in the online build like local providers — greyed, tooltip "can't be called directly from a browser… use the full version", click → GitHub. availableProviders() online excludes hosted-proxy; App online-fallback picks the first browser-direct image provider; lockedHint(feature, reason) gained a reason arg.

Net: the deployed site uses no serverless functions → no cap, no 10s timeout, $0, overnight bulk runs work (cost on the visitor's key). Functions + dispatch.js kept for the local dev proxy (desktop full version uses them for the 3 non-CORS providers). Staying on Netlify (owner likes it + the domain). MINOR 2.11.0. lint clean, online build green (529 modules), npm test green (100 + 51). Next: redeploy.

serverless proxy fix: adapter is not a function (2.10.5)

First real online test run: image gen + auto-fix both failed — /api/generate and /api/rewrite returned 502 with {"error":"adapter is not a function"} (confirmed by curling the live endpoint). Root cause: Netlify's function bundler returns an ESM export default as a namespace ({default: fn}), so dispatch.js's static default imports of the provider server/rewrite adapters weren't callable in the bundle (fine locally under native ESM — why the test suite + dev server never caught it). Fix: an asFn() helper that accepts a function or {default} and unwraps it, applied to every entry in serverAdapters and rewriteAdapters. Redeployed; re-tested the live endpoints with a dummy key — now returns OpenAI's "Incorrect API key" (i.e. the adapter runs and the request reaches OpenAI). So a real BYOK key generates. /api/magick 404 is expected/benign (local-only). PATCH 2.10.5. lint clean, 51 jsdom tests green. (Possible follow-up: the proxy maps every upstream error to 502 — a bad-key error is arguably a 400/401; could pass the upstream status through. Cosmetic, left for later.)

Netlify deploy live at prompt-fairyfox.netlify.app + netlify.toml path fix

Stood up the online demo on Netlify (owner gave the go-ahead, connected the Netlify connector, OK'd installing the CLI). Created site prompt-fairyfox (team junebug12851), installed netlify-cli 26.1.0 locally, netlify login, linked the repo folder, deployed --prod --build.

Found + fixed a latent netlify.toml bug on the first real deploy: the toml still pointed at a root-level gui/ (pre-engine-split), so npm --prefix gui → ENOENT. Tried base = "engine-v3" — the build command then ran in engine-v3 (good) but publish/functions still resolved from the repo root (the toml's location), so the deploy dir wasn't found. Settled on no base, explicit engine-v3/-prefixed paths (npm --prefix engine-v3/gui ..., publish = engine-v3/gui/dist, functions = engine-v3/gui/netlify/functions) — unambiguous for both CLI and any future git-CD. Deploy then went green: 45 files + 2 functions, online build (VITE_ONLINE in the resolved config).

Attached custom domain prompt.fairyfox.io via netlify api updateSite. fairyfox.io is not on Netlify DNS (only darkprincess638.com / popupmc.com zones exist on the account), so the owner adds one CNAME promptprompt-fairyfox.netlify.app at the fairyfox.io DNS host; Netlify auto-SSLs after it resolves. Current setup is a manual CLI deploy (not git-connected) — noted continuous-deploy options in reference/deployment.md. No VERSION bump (deploy config only). app itself unchanged since 2.10.4.

online build: disable-with-link instead of hide (2.10.4)

Owner wants a public online demo at prompt.fairyfox.io (Netlify), and wants the local-only features shown disabled rather than hidden so visitors can see what the full version adds. The VITE_ONLINE build already existed but it hid Gallery/Single, the NSFW toggle, and local providers. Flipped that to greyed-out-with-tooltip + a click-through to the GitHub full version:

  • New gui/src/lib/online.js — single home for ONLINE, FULL_VERSION_URL, lockedHint(feature), openFullVersion().
  • App.jsx — tabbar always renders; Gallery/Single get .is-locked (lock badge + tooltip, click opens the full version) when online; NsfwToggle always renders with locked={ONLINE}. Also: if a local provider is the saved default (ComfyUI is), online falls back to the first hosted provider so Generate works out of the box.
  • NsfwToggle.jsxlocked prop: greyed, tooltip, click → full version; never reports on when locked.
  • ProvidersMenu.jsx / ProviderPicker.jsx — show all providers; local ones render .is-locked (🔒 "full version" badge, greyed, non-selectable, click → full version) when online. availableProviders() kept as-is for the default-fallback logic.
  • styles.css.vs-tab.is-locked / .lock-badge, .switch.is-locked, .ps-item.is-locked / .ps-lock.
  • netlify.toml[build.environment] VITE_ONLINE = "true" so a Netlify build is the online variant with no dashboard config.

Verify: lint 0 errors, prettier --check clean on touched files, online build green (VITE_ONLINE=true npm --prefix gui run build), npm test green (100 Node + 51 jsdom). Wrote the prompt.fairyfox.io setup steps (connect repo → branch main → add prompt CNAME → HTTPS) into reference/deployment.md. PATCH 2.10.4. The Netlify connect + DNS are owner-side dashboard actions.

default wrapper: rays back in (2.10.3)

Owner liked god rays for the default end, fine tossing the rest. Restored {#rays} as the ~50% bullet on DEFAULT_WRAPPER_SEED.end ({#fx}, {#artists}\n- {#rays}). PATCH, released. SPA build + gui tests green.

default wrapper de-spammed (2.10.2)

Owner: "remove the default wrapper trash too like masterpiece and best quality." Found it in gui/src/lib/wrapperStore.jsDEFAULT_WRAPPER_SEED.start was literally masterpiece, best quality, highly detailed and the END piled on intricate detail/sharp focus/wide shot/{#rays}/{#dap}. Emptied the START; trimmed the END to just {#fx}, {#artists} (kept the random art-style/artist variety — that's the generator's actual feature — dropped the quality words, forced framing, and art-site tags). Also fixed the WrapperFab placeholder that suggested masterpiece, best quality. No test pinned the default, so safe; SPA build + gui tests green. Released 2.10.2 (PATCH). (If the owner wants the default fully neutral, dropping {#fx}/{#artists} too is a one-line follow-up.)

tail cleanup (2.10.1) — catalog-wide sweep

After shipping 2.10.0, swept the whole .dpl catalog for leftovers: grepped for inline otherwise: colon bugs, the known typos, and dead bare-# refs — all clean. Reviewed the two untouched categories: fixed the coffecup content typo (expansion/), and de-scattered the user/beach-merk community submission (doubled {#city} + duplicated synonyms removed, off-theme {#color} crystals dropped, waves made intensity-aware, credit preserved). Confirmed the {#legacy}/{#dap} quality-filler packs are opt-in only (no auto-pull). Bumped to 2.10.1 (PATCH) and released dev → main. The block refactor is now complete top to bottom.

engine: auto-append re-resolution (root-cause fix for the {#rays}-style leak)

Fixed the underlying engine quirk: block.js now appends auto-{#fx}/{#artists} as tokens before the resolver loop (instead of pre-rendered output after it), so nested {#…} inside them resolve. This is the cause behind the literal {#rays} leak the fx.dpl content fix had only symptom-patched — any future auto-appended generator with nested tokens is now safe. New integration test; 0/50 leaks with auto-fx + auto-artists on; Node suite 99 → 100 green; SPA build green. (Self-audit note: earlier in the session a too-broad git checkout -- <dir> reverted my own uncommitted edits to 6 files mid-batch; I rewrote + verified them and confirmed via git fsck / git diff HEAD / local==origin that nothing committed was ever lost. Tightened my own rule: never run a directory-wide checkout while uncommitted work exists, and stage with one explicit path per git add.)

prompt/ batch — Phase 2 batch 5 (final category)

Last category. prompt/ is mostly JS builtins (artist/keyword repeaters, danbooru d, the random suggestion engine) — random by design, left alone. The one real fix: fx.dpl's trailing nested {#rays} token → inlined as god rays, volumetric lighting, killing the literal-{#rays} leak that auto-{#fx} caused (it's appended after the resolver loop, so nested tokens didn't re-resolve). Verified 0/40 leaks with auto-fx + auto-artists on. Phase 2 is complete across all five categories (scene · fragment · subject · style · prompt). Possible follow-ups: make the engine re-resolve auto-appended fx/artists (root cause of the quirk), and decide whether to fold the whole feature/dpl-intensity branch toward main as 2.10.0.

style/ refactor — Phase 2 batch 4 (the intensity overhaul, content)

Renamed the owner-flagged anime-irlanime-realism (clean break): file moved, meta description map key updated, stale anime-irl.json removed, new anime-realism.json generated, and the {#underwater-anime-irl} expansion that pointed at it fixed (it used the dead bare #anime-irl form, so it had been a no-op — now {#anime-realism}, resolving again). De-scattered anime-realism + retro-poster (dropped {#expressive}/{#weather}/{instrument}) but kept the artist lines — those define the look (the owner's "some files use artists, anime-irl is one"). Fixed isometric typos (blackground, doubled soft colors). Deliberately left the rest of the style recipes alone — their render tags are functional style definitions, not filler. smoke + Vitest (99+51) + SPA build green. Remaining: prompt/ (the whole-prompt builders) is the last category; then a sweep for the auto-{#fx} literal-token quirk noted earlier.

subject/ refactor — Phase 2 batch 3 (the intensity overhaul, content)

The knight→hill/castle scatter the owner flagged is fixed: knight no longer pulls {#landscape} or [[castle]], and its render-farm Auto End (unreal engine 5 / octane / ray tracing / hdr / …) + hyperrealistic/detailed face/detailed body filler are gone — it's now a focused character ({#portrait} framing, one-of armor, {emotion}, {#general-state}, intensity detail conditions). Portrait family de-scattered (portrait-person/portrait-princess dropped {#expressive}/{#weather}/ {instrument}) and bug-fixed: the inline otherwise: colon artifact (which also hit beach/portrait) and sceptorscepter. Left the entity*.js polymorphic type-system sidecars alone — they're legit JS. smoke + Vitest (99+51) + SPA build green. Remaining batches: style/ (anime-irl etc. — the non-descriptive-name + artist-list cases) and prompt/ (the whole-prompt builders).

fragment/ refactor — Phase 2 batch 2 (the intensity overhaul, content)

Polished the garnish pool (fragment/). Fixed the two typos surfaced last batch (accesories in room-state, mesmorizing in mystical), trimmed room-state/general-state's over-long damage piles, dropped filler, and made the magnitude-y fragments intensity-aware ({intensity} clutter / crystal formations / snow drifts / embers / foliage / schools of fish / wear). Left the 6 already-minimal pools (color, glow, neon, water, weather, expressive) untouched — they're correctly small. Descriptions unchanged → no meta regen. smoke + Vitest (99+51) + SPA build green. Next: subject/ (person, animal, knight, the portrait-* family — the knight→hill scatter you flagged).

scene/ refactor — Phase 2 batch 1 (the intensity overhaul, content)

Started the content refactor with the scene/ category (owner's pick). Decisions confirmed: default intensity stays 50% everywhere; scene first. Kept scene filenames (they're already clear — the rename "clean break" applies where a name is genuinely unclear, e.g. anime-irl in style/, not here), so no cross-ref/SPA/preset breakage — the value this batch is content focus, not renames.

All 26 scene/*.dpl rewritten: de-scatter (no scene/subject pulling another whole scene/subject — fragments like {#weather}/{#nature} stay as garnish, but {#city}/{#settlement}/{#portrait-*}/ {#ruins} cross-pulls removed), trimmed synonym piles, fixed interrior/accesories typos, wired in the ignored lists ({view},{time},{mood},{style/*}), and made them intensity-aware ({intensity} magnitude words + [>70%]/[<25%] detail conditions; great-tree/great-bridge bias up with {intensity +40/30%}). Converted the 5 JS sidecars (futuristic, great-bridge, great-tree, space, spaceship) to pure DPL — the giant/huge/massive size helpers became the {intensity} keyword — and deleted the .js. Regenerated meta sidecars. Eyeballed low/high-intensity output per scene; smoke + Vitest (99+51) + SPA build green.

Found (pre-existing, deferred): {#room-state} carries an accesories typo (fragment batch); and generate()'s auto-{#fx}/{#artists} is appended after the resolver loop, so a nested token inside fx (e.g. {#rays}) can survive as a literal — an engine/auto-append quirk, not a scene issue. Next batch: fragment/ (color, glow, weather, nature, eerie, mystical, room-state, …) — the garnishes every scene leans on.

DPL intensity dial — engine + tests (2.10.0, Phase 1 of the block refactor)

Kicked off a large block overhaul (owner request: strip "masterpiece/best quality" filler, stop generators dragging in scattered keywords like knight→hill / anime-irl→portrait, rename/split/focus files, lean on the expanded lists, and add an intensity dial). Agreed scope decisions up front: auto-scale and explicit conditions; default 50% when unspecified (top-level and nested); rename with a clean break (no aliases) for the content phase; engine first, then content in batches.

Phase 1 (this entry) — the engine feature, on feature/dpl-intensity:

  • src/core/dpl/dpl.jsctx.intensity (default 50, 01, clamp 1–100); [weight|cond]/[cond] bracket parsing before and after the bullet, tolerant of | or space and either order, and refusing non-spec brackets so [[castle]]/[deemph]/[a:b:0.5] stay payload; conditions as hard pre-gate include/exclude; auto-scaling of probability gates (scaleGate, not the bare-otherwise) and repeat/choice counts; {intensity}/{intensity%}/{intensity-num} inline tokens with a word ladder; relative ±NN% modifiers on the keyword and on nested {#name ±NN%} refs (rewritten to absolute).
  • src/core/stages/block.js{#name NN%} parsing threaded through expandGen/pickFrom/run into mod.default(...,intensity); loaders (nodeLoader, browserLoader) pass intensity to JS sidecars.
  • Tests — DPL unit block + 2 engine-pipeline integration tests; seeded snapshot updated for the 50% default. npm test green (lint 0 errors, 99 Node + 51 web), SPA build clean.
  • Design note: notes/reference/intensity-design.md.

Open for the owner: confirm the 50% default feels right at the top level (vs 100%); it's one constant (DEFAULT_INTENSITY) to retune. Next: Phase 2 — refactor the .dpl catalog in category batches (fragment → scene → subject → style → prompt), using intensity + the lists, renaming for clarity.

Provider header redesign shipped (2.9.0)

Owner asked (in Cowork) to "move the API key to the header," and over a long live-iteration loop that grew into a full header/composer rework — corrected my course several times (the early "regroup one combo" answer was superseded mid-session by "a Providers dropdown with two stacked combos"). Landed as one MINOR.

  • Header → one Providers dropdown + a gear + NSFW. ProvidersMenu.jsx (new) holds two rich pickers (ProviderPicker.jsx, new, recreated from the deleted ProviderSelect): Image (Local incl. Plain / Online) and Text (Off + OpenAI/Gemini/Grok). Two-column grid (picker + fixed-width key) so rows align.
  • BYOK key → header (ApiKeyField.jsx, new): compact pill, save/clear icons inside the field, info tip
    • Get-a-key kept. Keyed by provider id → same provider on both rows shares one key (shown once).
  • Provider settings → header gear (ProviderGear.jsx, new) wrapping a now-bare ProviderBox (card + collapse removed). Negative-prompt field pulled out of the gear.
  • Composer Prompt/Negative switch (Home.jsx): segmented switch on the insert bar (only when the provider supports negatives) flips the editor between prompt and the per-provider negative; inserts/clear/preview follow the active field. DplEditor already syncs external value, so the swap just works.
  • Removed ProviderSelect.jsx; stripped the rewrite select + key from Settings.jsx (now in the dropdown). Auto-fix / keyword toggles unchanged on the prompt box.

Process notes (rough edges):

  • Long, fast feedback loop — many of the messages were live CSS nits (save icon offset, grid reflow, name cutoff, the green tier tag wrapping). Each was a small fix; the AskUserQuestion on the combo grouping was worth it but its answer evolved a message later, so I re-cut the design.
  • Deferred: a per-provider preset manager + migrating the old v1–v2 flat presets — flagged as the next scoped piece, not built here (it revives the removed preset system and needs its own design pass).
  • Windows visual baselines regenerated locally (test:e2e:update); the Linux set still needs the visual-baselines.yml workflow before the main release CI is green.
  • Bumped root VERSION + engine-v3 package.json 2.8.0 → 2.9.0 (MINOR).

Keyword tooling + DPL insert toolbar shipped (2.8.0)

Owner asked (in Cowork) for keyword features on the single view + composer, then "ship it … normal procedures including testing." Built the keyword strand, found the composer/single-view files already carried an uncommitted in-flight DPL insert toolbar, and — on the owner's call — shipped both together as one MINOR release.

  • Keyword parser gui/src/lib/keywords.js (new): strips SD/NovelAI weighting + attention syntax, splits on commas/newlines (multi-word tags intact), de-dupes. Display keeps accents; a de-accented lowercased key drives dedupe + gallery search (owner asked whether to keep accents — answer: keep for display, fold only for matching).
  • Single view SingleView.jsx: KeywordsCard prefers a saved meta.keywords list, else parses; new "Rebuild with AI" button → keyword-mode rewrite → alphabetize → persist to sidecar. Details block became a real <table>. New props settings + onMetaUpdate threaded from App.jsx.
  • Sidecar write-back: POST /api/image/meta (vite-plugin-api.js) shallow-merges a patch into the <base>.json sidecar; updateImageMeta() in lib/output.js. Gallery searchHaystack now includes saved keywords.
  • Composer Home.jsx: autoKeyword toggle (tag icon) beside the wand; makeBatch chains fix → keyword-translate on prompt + negative, independently, cached on the entry.
  • Rewrite pipeline: KEYWORD_SYSTEM + systemFor(mode) (_shared/rewriteSystem.js); adapters take a system override; mode threaded through lib/rewrite.js → both proxies → dispatchRewrite.

Process notes (rough edges):

  • The working tree arrived dirty: the in-flight DPL insert bar (untracked DplInsertBar.jsx, dplInserts.js + edits to DplEditor/dplLanguage/promptEngine/ProviderBox/WrapperFab), an untracked fairyfox report, and ~140 files showing as modified that were CRLF-only noise (git diff empty; only ~20 had real content diffs). Staged specific real-diff files only — never git add -A; left the CRLF noise alone.
  • CI on dev HEAD was already red before this work: the Playwright job had 5 failures — home.spec / accessibility.spec still selecting <textarea> (gone since the 2.7.26 CodeMirror switch) and stale Linux visual baselines. Fixed the selectors (.prompt-input .cm-content, click + pressSequentially) and regenerated baselines via the visual-baselines.yml workflow as part of getting the release green.
  • engine-v3/VERSION doesn't exist — the canonical VERSION is at the repo root (CI reads it). Bumped root VERSION + package.json to 2.8.0.

DPL boxes → CodeMirror editors (feature/dpl-codemirror, 2.7.26)

First of a planned four-branch GUI/DPL UX pass the owner asked for (the others, not yet started: sidecar category priority + an nsfw sidecar flag; result/gallery polish — preview icon, click-to-copy, tooltips, gallery thumbnail actions; an online-build variant). Owner chose CodeMirror 6 (over a custom highlighter) and separate feature/* branches grouped by coupling.

Built a reusable controlled editor and a DPL language for it:

  • gui/src/components/DplEditor.jsx — a CodeMirror 6 EditorView wrapper behaving like a <textarea> (value + onChange). Echo-safe (tracks the latest value in a ref, only re-dispatches on a real external change) and reconfigures its placeholder via a Compartment so the prompt box's rotating suggestion still shows without a remount. No closeBrackets — it would fight the brace-completion (double }).
  • gui/src/lib/dpl/dplLanguage.js — a StreamLanguage tokenizer + HighlightStyle (tag → CSS class) + a brace-aware completion source. Mirrors src/core/dpl/dpl.js; line-leading keywords only.
  • getDplCompletions() added to promptEngine.js (flattens getBlocks() into de-duped token entries).
  • Wired into the prompt box (Home.jsx), the negative box (ProviderBox.jsx), and the wrapper Start/End boxes (WrapperFab.jsx).
  • styles.css: a "DPL editor (CodeMirror)" section dressing .cm-* to match the old textareas, the autocomplete dropdown themed to the app, and --dpl-* token colors in :root + the light override.

Deps added to gui/package.json: @codemirror/{state,view,language,autocomplete,commands} + @lezer/highlight (12 packages). Recorded in reference/dependencies.md.

Verification: npm run lint 0 errors (18 pre-existing warnings, none in new files); npm test green (smoke + Node 86 + SPA 43); npm --prefix gui run build green. Not yet done: Playwright visual baselines shift with the prompt box and need npm run test:e2e:update; branch not yet merged to dev (awaiting owner go-ahead). VERSION + engine-v3 package.json bumped 2.7.25 → 2.7.26 (PATCH, per the versioning guide — ordinary feature).

Dynamic-prompt sidecars: priority + nsfw flag (feature/dynprompt-sidecar, 2.7.27)

Second of the four-branch GUI/DPL UX pass (branched off dev, independent of feature/dpl-codemirror). Two sidecar-driven catalog changes:

  • Category priority orders the category/folder pills within the Blocks tab (lower = higher, default 1000). Refactored getBlocks() (gui/src/lib/promptEngine.js) to assemble category groups (anyGroup/dynCatGroups/specialGroup, each { priority, name, pill, entries }) and sort them; any is pinned to 0, special to 9000. The metadata script writes the curated priorities (prompt 200 … user 700), giving Any · Prompt · Scene · Subject · Style · Fragment · User · Special. Per the owner's clarification, this reorders the pills inside Blocks, not the Blocks/Lists top tabs.
  • nsfw sidecar flag = hard-hide when the NSFW switch is off. getBlocks({ includeAdult }) drops flagged generators (and empty categories), and the engine's {#name} stage (src/core/stages/block.js) gates on loader.readBlockMeta(key)?.nsfw === true || isGatedBlock(key) — so flagged generators are unpickable, excluded from {#any}/groups, and resolve to "". The name-token rule still applies; the flag is the no-token escape hatch (empty set today). Home.jsx re-memoizes the palette on settings.includeAdult.

Touched: scripts/dynprompt-meta/write-dynprompt-meta.mjs (PRIORITY map + NSFW set; sidecar now { description, priority?, nsfw? }), regenerated 86 sidecars; block.js (dropped the hasNsfwToken import for the unified isNsfw); promptEngine.js; Home.jsx. New SPA test asserts the pill order.

Verification: lint 0 errors; npm test green (smoke + Node 86 + SPA 44); gui build green. VERSION + engine-v3 package.json2.7.27 (assumes it lands after 2.7.26). Not merged to dev (awaiting owner go-ahead). Branches 3–4 (result/gallery polish; online-build) still to come.

Result/gallery polish (feature/gui-result-polish, 2.7.28)

Third of the four-branch GUI/DPL UX pass (off dev, independent of the earlier two). Four small UX changes the owner asked for:

  • Corner preview. Moved the LivePreview eye out of the .field-bar into a new .composer-corner cluster pinned to the prompt box's upper-right, styled as a bare icon (.preview-corner). The clear-✕ joined the same cluster (no longer absolutely positioned on its own).
  • Click-to-copy prompt text. PromptResult.jsx lost the "copy" button; the prompt text is now click-to-copy via the existing provider-aware onCopy, with the full text shown on hover (title).
  • DPL hover example. New DplHoverCode subcomponent: hovering the source-DPL <code> shows the full DPL plus an example re-rolled every 1s (expandPrompt, fx/artists off — like the chip tooltips). PromptResult now receives settings (passed from Home).
  • Gallery thumbnail actions. Reworked Gallery.jsx's Thumb from one <button> into a .g-open button + a shared .img-actions overlay (open / reveal / delete), revealed on .g-cell:hover. App.jsx passes its existing deleteItem as onDelete; open/reveal call the output.js helpers directly.

Touched: Home.jsx, PromptResult.jsx, Gallery.jsx, App.jsx, styles.css. No engine/test changes.

Verification: lint 0 errors; smoke OK; SPA suite 43 green; gui build green. VERSION + engine-v3 package.json2.7.28 (assumes it lands after 2.7.27). Not merged to dev. Branch 4 (online-build) still to come.

Online build = stripped Generate-only variant (feature/online-build, 2.7.29)

Fourth and last of the GUI/DPL UX pass (off dev, independent). All in App.jsx, gated on const ONLINE = import.meta.env.VITE_ONLINE === "true":

  • No tabbar online. The .view-switch header tabs and the Gallery + Single view-panes are only rendered when !ONLINE; the view stays "generate". Home gets onOpenImage={ONLINE ? undefined : …}, so a generated image opens in a new tab (the existing <a target=_blank>) instead of the (absent) single view.
  • NSFW off, no toggle. <NsfwToggle> is only rendered when !ONLINE, and the mount effect coerces settings.includeAdult to false online so a stored preference can't leak adult content through.
  • No browser image storage / feed. The mount effect skips loadFeed() online (sets loadingItems false); images are shown in-memory only, never persisted.

Verified both build modes: npm --prefix gui run build and VITE_ONLINE=true … build both green; lint 0 errors; SPA suite 43 green; smoke green. VERSION + engine-v3 package.json2.7.29 (assumes it lands after 2.7.28). Not merged to dev.

Pass complete — four feature branches pushed, none merged

All four branches of the UX pass are committed + pushed, each verified, none merged to dev (awaiting the owner): feature/dpl-codemirror (2.7.26), feature/dynprompt-sidecar (2.7.27), feature/gui-result-polish (2.7.28), feature/online-build (2.7.29). They fork from dev independently, so they share trivial doc/VERSION conflicts on merge — intended to land in version order. The Playwright visual baselines will need the usual Linux refresh after the GUI branches merge.