Newest entry on top.
Automated release screenshots + GIF walkthroughs (scripts/screenshots/)
Owner asked for auto-updating screenshots that refresh with each release, published to the Pages
site (explicitly not committed to the repo), plus manually-authored GIF walkthroughs. They
recalled a Qt/CI pattern of stitching captured frames into a GIF via an external script and asked if the
same works for JS — it does: Playwright (already in the repo) screenshots any viewport, and gifenc
stitches frames.
Built scripts/screenshots/ — capture.mjs (build local edition → static-serve gui/dist →
Playwright), seed.mjs (route-mock /api/* for the local-only Gallery/Single/Manage with sample data +
pngjs gradient thumbnails), shots.mjs (Generate + block palette, Gallery, Single, Manage at
1025×768 / 769 / 345, 2×), frames.mjs (Recorder + diff-encoded GIF), gifs.mjs + scenarios/
(authored-GIF registry), and cursor.mjs (synthetic pointer for future mouse GIFs). Wired into
pages.yml (Chromium install + capture into docs/jsdoc/screenshots before upload). npm run screenshots
for local runs (screenshots-preview/, git-ignored).
Requirement iteration during the session:
- Framing vs resolution. Owner asked whether cropping the GIF meant abandoning 1025×768. Clarified the statics are captured at 1025×768; the GIF renders at that viewport too but was cropped for size. Owner: full frame or no GIF if it can't be both full-frame and small. Solved with inter-frame differencing in the encoder (store only changed pixels, rest transparent over the retained prior frame) → the full 1025×768 GIF is ~220 KB. Owner OK'd.
- Smooth typing / future cursor GIFs. Switched typing to one char per frame; added the reusable virtual-cursor overlay for the mouse-movement GIFs they want later.
- README. Owner will add the images to the README manually — toolkit README documents the Pages URLs instead.
- Second pass (owner follow-ups): (1) dark mode — captures now run in a dark Playwright context
(
colorScheme: "dark"; the app's defaultthemeModeissystem, so it renders dark); (2) never crop — every tab shot is full-page, and the GIF is the full 1025×768 frame; (3) autocomplete visible — the GIF typing now pauses ~140ms per keystroke so CodeMirror's token autocomplete popup renders and is captured. The full-frame diff encode was too slow first pass (pngjs decode + re-mapping every pixel of 72 full-HD frames), so the encoder now maps only the changed pixels per frame (nearestColorIndex), the rest transparent. - Third pass (corrections after I mis-scoped twice): the spec is each tab AND the block menu, at
all applicable device sizes; only the GIF is desktop-only. GIF trimmed to end after
{#futuristic}(shorter string over the fixed 5s → slower, steadier typing). Block menu → phone + tablet (the desktop Generate tab already shows the palette inline, so a desktop block-menu shot just duplicates it), and it's captured with the palette expanded to full height (composer hidden,.cat-tabs/.picker-listmax-height caps removed, drawer taken out of fixed positioning) so the WHOLE block list is shown — the earlier shots were clipped by the palette's internal 40% scroll cap, which read as "cropped". Tab shots (Generate/Gallery/Single/Manage) stay at all three sizes. - Fourth pass: tablet width set to 770; block menu → phone only (tablet, like desktop,
shows the palette inline on Generate, so a separate shot duplicates it); added a phone-only
manage-editorshot that opens a block so the DPL editor (hidden behind the tree in the phone master/detail) is captured. - Real sample images for Gallery/Single: owner generated 25 images and asked to use them. Committed
as optimized JPEG fixtures (
scripts/screenshots/assets/gallery/, ~6.5 MB, downscaled ~1024px via ImageMagick) + a sanitizedmanifest.jsoncarrying the real prompt/metadata (scrubbed of personal info / absolute paths — the manifest IS the committed sidecar text).seed.mjsserves these instead of the old gradient placeholders; the Single shot opensmanifest.single(…u99ko0) via a gallery click. Committed (not local-only) so the published Pages gallery shows them too (CI has nooutput/). - Release CodeQL gate: on the
dev → mainPR (#27), CodeQL flagged the two throwaway static file servers in the screenshot toolkit — "uncontrolled data in path expression" (path built from the request URL) + check-then-use file races. Hardened both:capture.mjsresolves withindistand rejects any..escape;seed.mjsbasenames + whitelists the image filename; both read-then-catch (noexistsSync/statSyncrace). Tooling-only, no version change. - App bug found via the screenshots (2.42.1): the DPL autocomplete popup + info panel were
light-on-light (unreadable) in dark mode — CodeMirror injects its default light tooltip theme
UNLAYERED, which beats our
@layer componentsoverrides; fixed with!importanton the surface colours (generic-code-highlighting.css). Verified in dark + light; regression guardtests/e2e/autocomplete-contrast.spec.js. See../../version/2026-07.md@ 2.42.1 and the newfix-patterns.mdrow (unlayered-third-party-CSS-wins-over-layered).
Verified locally: all 15 statics + the GIF render correctly (eyeballed Generate/Gallery/Manage/phone-drawer
- the GIF first frame). Lint + format clean. Tooling/CI only → no
VERSIONbump; legal docs unaffected. Notes: this session log,version/2026-07.md,reference/dependencies.md,list-credits.md.
Large-scale performance: virtualization, chunked generation, per-provider concurrency + a perf suite (2.42.0)
Owner set the officially supported maximum load: 100k gallery + 1000 prompts / 10k images + a 100k Manage file, all at once, with no perf loss, no bugs, no crashing, no instability, no API thrashing. The requirements evolved over the session (auto-image gen at scale, placeholder-first, tab round-trip quality, online-build limits), and the concurrency knob landed as a new shared-settings system after the owner explicitly asked for a proper, DRY, folder-based design (not a shortcut) — chosen via an AskUserQuestion when they were deliberating the structure.
What shipped (details in ../../version/2026-07.md @ 2.42.0):
- Gallery virtualized — windowed uniform grid over the new pure
lib/virtual/windowRange.js(bounded DOM at 100k). Verified visually at 1280px + 390px. - Results list bounded —
content-visibilityon.prompt-result+ memoizedPromptResult. - Placeholder-first chunked image gen —
useImageBatchessplits the instant placeholder from the queued generate; a per-provider image limiter + a separate text (rewrite) limiter. - Shared-settings system —
gui/providers/_shared/settings/(auto-discovered,applySharedSettingsinjects into every provider's schema at the registry). First entry: per-provider Batch chunk size with metadata-derived defaults (local 6 / hosted 3 / poll 4). Reverted an earlier global-gear attempt. - Perf suite + profiler —
tests/perf/*.perf.spec.js(real release server viaplaywright.perf.config.js),npm run test:perf:scenarios(intest:all+ a CI job), andnpm run profile→scripts/profile-scenarios.mjs(DevTools traces + metrics toperf-profile/).
Verification: npm test green (363 SPA + Node suites, incl. new windowRange/sharedSettings/extended
useImageBatches); npm --prefix gui run build green; all 5 perf specs green (100k gallery, 1000
prompts, 100k Manage, hot-reload, combined max-load) with round-trip scroll quality asserted; lint +
stylelint clean; gallery screenshotted at both breakpoints. No legal/credits impact (concurrency is
client-side, stored in the existing per-provider params; no new network/storage/analytics).
Seed follow-up: preview independence + coverage + zero Sonar debt (2.41.3)
Owner raised the quality bar for the release: SonarCloud (coverage ≥85%, zero tech debt), Codecov, and CodeRabbit are all required gates, nothing outstanding, and fix any bugs/hacks/debt even if off-topic. Also flagged: mouseover previews must keep working and stay independent of the seed (a hidden preview seed that constantly re-rolls).
Pulled the SonarCloud issue list via the public API (Chrome, since web_fetch returned empty): exactly 6
open issues, all CODE_SMELL (0 bugs/vulns/hotspots) — nameOrder.js:243 (S7735) and parser.js
227/228/231/234/295 (S7780 ×3, S3358, S6557). Fixed all 6 (String.raw, un-nested ternary, endsWith,
positive condition); DPL suites confirm behaviour preserved.
Preview fix: added previewPrompt (always-random, seed-independent) and routed every preview surface
(LivePreview, DplEditor, DplInsertBar, PromptResult, Home tip + suggestion) through it; expandPrompt
stays seed-honouring for the real negative-prompt path. Extracted the roll-seed logic to a pure
lib/home/seed.js (100% covered) and dropped the unused newRollSeed export. New unit suites:
homeSeed, random (ambient accessors), SettingsSeed, plus previewPrompt facade tests. Node 270 +
web 335 green; full npm test gate green. Version → 2.41.3. Landing via feature/seed-coverage →
dev → release PR so the Sonar gate + CodeRabbit re-run.
Fix: frozen "random" reroll + explicit Random-seed control (2.41.2)
Owner: reroll rolls a new seed but the preview doesn't change — suspected the async/deterministic engine overhaul. Diagnosed two independent defects:
- Seed conflation. GUI default settings carry the image-provider
seed: -1(SD "random" sentinel).engine.jshasSeedtreated any presentseedas an explicit prompt seed → every roll wascreateRng(-1)→ identical prompt. The preview was fine; the RNG was pinned. - Leftover list-stage state.
list.jskept its emphasis/editing/alternating rotation bag (promptFuncsTmp) as closure state, never reset per generation. The SPA engine is a singleton, so residue from the prior roll desynced the next roll — a pinned seed didn't reproduce once emphasis fired. Node tests missed it (fresh engine per call → bag always empty). Found it by isolating in the jsdom env: list-only pinned reproduced, emphasis-on didn't;randomEmphasiswas reproducible in isolation, so the culprit was un-reset state, not the RNG source.
Design (owner-directed, mid-session): drop magic numbers; use an explicit Random seed toggle. Random on → fresh seed each roll, shown greyed/read-only in the box (copyable); off → box un-greys for a manual seed. Seeds may be any text (cyrb128 hashes letters/spaces/emoji), incl. 0 / negatives.
Changes: list.js resets the bag per generation; promptEngine.js facade routes the engine seed from a
dedicated promptSeed (never the image seed) gated by randomSeed, adds newRollSeed(), forks base#i
per prompt; Home.jsx establishes one roll seed and writes it back for display; Settings.jsx new Seed
group (toggle + read-only-when-random box + contextual hint); settings.js adds randomSeed/promptSeed;
random.js moves the ambient RNG to a global Symbol.for slot (defensive vs. module duplication);
reverted the engine -1 special-case. Tests: reused-engine reproducibility (emphasis on), any-integer/
no-seed, random-on/off through emphasis, forced fork seed, negative/zero, free-text. Node 263 + web 321
green; lint + smoke green; verified live in-browser (toggle greys/un-greys, free-text accepted, used seed
shown after a roll). Version 2.41.1 → 2.41.2.
Fix: mobile composer settings-gear backdrop over its own menu + regression-test standing rule (2.41.1)
Owner reported (with a screenshot arrow) that on mobile/tablet the gear in the composer's top-right corner opened a settings menu with the dark backdrop painted on top of it — couldn't scroll/tap.
Root cause (traced through the stacking chain): the composer prompt-settings gear (Home.jsx
.field-menu-wrap.prompt-settings-gear → plain .gear-pop + shared .gear-pop-scrim) is a different
popover from the topbar ProviderGear (.provider-gear .gear-pop.provider-gear-pop). On <=1024px,
mobile-sheets.css gives the generic .gear-pop-scrim the dark z-90 backdrop, but its bottom-sheet rule
only lifted the provider gear's pop to z-91. The composer's plain .gear-pop kept desktop z-42, so the
scrim covered it. .composer-corner (z-index:2) further trapped the fixed scrim+sheet in its stacking
context (would pin a bottom sheet under the Home FAB at z-50).
Fix (CSS-only):
mobile-sheets.css: added.prompt-settings-gear .gear-popto the bottom-sheet selector group (z-91, above the scrim) and theoverflow-y:autorule.composer-corner.css:.composer-corner { z-index: auto }at<=1024pxso the fixed backdrop + sheet escape to the root context and cover the whole page (above the FAB).
Verified live in Chrome at a real 455px viewport: computed z-index (scrim 90 / pop 91), .composer-corner
z-index:auto, elementFromPoint over the menu body → the menu (not the scrim), and a screenshot of the
sheet correctly over the dimmed page. Added a regression guard in tests/e2e/responsive.spec.js
("composer prompt-settings gear — phone bottom sheet") that hit-tests the stacking; proved it fails with
the CSS reverted, passes with it. eslint + stylelint + prettier green.
Owner set a standing rule: every bug fix ships a regression test by default (saved to agent memory
regression-test-every-fix), asked for a full retroactive audit to backfill guards for past fixes
(skipping trivial one-off tweaks), and asked to propose regression testing as a default standard for all
fairyfox projects — written up as notes/fairyfox-reports/2026-07-04-propose-regression-testing-standard.md
(proposal only; hub untouched). Backfill audit tracked as ongoing.