v2.60.0
Random AI prompt + image generator for the Stable Diffusion WebUI -- a CLI and a local web UI.
Pre-release software. Provided as-is under Apache-2.0.
What's new in this release
2026-07-12 — 2.60.0 — The verdict: 1000 prompts render in 253 ms on a phone, with flat memory
The number the whole exercise existed to produce, from the release APK on a real Android runtime:
| Roll | engine | render | memory | jank |
|---|---|---|---|---|
| 20 | 1497 ms | 383 ms | 112 MB | 95.7% |
| 200 | 5447 ms | 196 ms | 122 MB | 92.7% |
| 1000 | 23366 ms | 253 ms | 110 MB | 89.8% |
| The list virtualizes exactly as promised: 50× the rows costs no more memory (110 MB vs 112 MB) and | ||||
| no more render time (253 ms vs 383 ms), and the cost per prompt falls with N (94 → 28 → 24 ms). The | ||||
| jank is the emulator's software GPU — identical across all three rolls, which is precisely why the gate | ||||
| judges each roll against the same device's own baseline rather than an absolute frame budget. |
2026-07-12 — 2.60.0 — …and the "defect" it first reported was my test
The on-device gate's first runs said the app missed its headline promise: 1000 prompts, no
"1000 generated" inside ten minutes. For a few hours the notes said so. It was wrong, and what
proved it was the instrumentation added to diagnose it:
[rap-perf] roll 1000 prompts: 13044ms (engine only)
[rap-perf] committed 1220 result rows ← 34 ms later
The engine produced all 1000 prompts — 13.0 s on a software-rendered CI emulator, linear across 20 → 200
→ 1000 — and React committed every row 34 ms later. Nothing was slow.
The bug was in the test: results accumulate across rolls (each batch is prepended, by design), so
after the 20-prompt baseline the label reads "220 generated", and a test waiting for "200 generated"
waits forever. It then burned a ten-minute timeout per roll and reported that as the app failing. The
suite now clears the list before each roll (new clear-all testID).
Two lessons, both now in the notes:
- The test's verdict was louder than the app's evidence, and I believed the test. The
[rap-perf]
lines were sitting in the log the whole time. Read the instrument you built before you distrust the
thing it measures. - It retires the 2026-07-11 conclusion that FlashList's web renderer was to blame for the same
symptom in the proxy.scripts/probe-mobile-list.mjs: the export renders 1000 prompts in 165 ms with
16 rows mounted in the DOM. That explanation was comfortable and false.
2026-07-12 — 2.60.0 — CI gets two tiers: fast on every push, expensive on the release path
Owner: "only use CI for things that don't take a lot of time; things that take a lot of time should be
gated for release/ship/deploy only."
The android-device job compiles the Android app from source and boots an emulator — 30–45 minutes. Run
on every push to dev, it made each one-line iteration a 40-minute wait (and, this session, several of
them). It now runs where it actually protects something: the pull request into main, pushes to
main, and workflow_dispatch. main still cannot take a build it rejects — the release PR must be
green — while dev stays fast. A gate that makes iteration miserable is a gate people learn to ignore.
2026-07-12 — 2.60.0 — The harness itself (Detox on a real Android runtime)
The mobile testing mandate had one honest exception, and this closes it. The app promises 1000
prompts in a single roll with no performance loss — a claim about a phone. The react-native-web
proxy could never check it: it runs FlashList's web renderer, which doesn't recycle like the native
one, so any number it produces describes react-native-web. The test was kept skipped, with the
measurements inline, rather than deleted (pretending) or left failing (crying wolf).
Now there's a real harness: targets/mobile/e2e/ + .detoxrc.js — Detox driving the release APK
(built from the Expo-CNG native project, which stays gitignored) on an emulator; local via the
rap_phone AVD, in CI via a new android-device job (KVM-accelerated Ubuntu runner).
What it asserts, and why it can't be gamed. Not "1000 prompts in under N ms" — that benchmarks
whatever box CI got. It rolls 20, then 1000, on the same device in the same session, and
compares: a virtualized list holds a window of rows, so 50× the data must not mean 50× the memory or a
collapsed frame rate. The evidence is the platform's own accounting — dumpsys gfxinfo (janky-frame
%, p50/p90/p95/p99) and dumpsys meminfo (real PSS) — not a stopwatch inside the test. Emulator noise
is identical in both rolls and cancels. It also pins that the app produced all 1000 (the wait is on
the literal "1000 generated", so a re-introduced cap times out rather than passing quietly) and that
the app is still interactive afterwards.
Also: gradle/ndk-override.init.gradle — an opt-in escape hatch (ANDROID_NDK_VERSION) so a local
build uses an NDK you already have instead of stalling for hours on a 700 MB download of the exact
pinned one. No-op in CI.
2026-07-12 — 2.60.0 — De-dup, the last one: engine/dplInsertCatalog.js
The DPL insert catalog describes what engine/core/dpl/dpl.js compiles — the constructs, their
syntax, their templates. That is the engine's grammar; it was never the web target's to own, and the
phone's 262-line hand-port existed only because the web owned it (guarded by checkDplInserts).
The plan called this one "entangled", because the web localizes the menu's labels through react-intl
while mobile inlines English — sharing the module naively would have dragged react-intl into React
Native. The split that resolves it generalizes: grammar is shared, presentation is not. The engine
holds ids / syntax / templates / examples / materializeTemplate; each target attaches its own label
layer via buildInsertMenu({category, item}). Label keys are derived from the catalog ids, so a
construct added to the grammar with no string in a target fails tests/unit/dplInsertCatalog.test.js
instead of rendering undefined on a phone.
Mobile 262 → a label table; web 264 → 24. checkDplInserts deleted — six of the seven drift checks
are now gone, each because the thing it guarded can no longer differ.
2026-07-12 — 2.59.1 — The local gate was a SUBSET of the CI gate (CI had been red, unseen)
gh run list on dev: CI failing since the previous session's last push — on Format check.
Nine files that session wrote were never Prettier'd, and npm test ran lint but not format:check,
which CI does. So the session ended "green" on a commit CI rejects. A gate you don't run is not a gate
— and a local gate that is a subset of the CI gate is a lie about what green means. format:check now
runs inside npm test.
Two of the nine were generated files, and that exposed a deadlock: hand-formatting them made
check:registry call them STALE (it compares byte-for-byte against the generator's output), while
leaving them made format:check red — two gates each demanding the other be broken. Fixed at the
source: scripts/build-provider-registry.mjs runs its rendered output through Prettier before writing
and before comparing. Never hand-format a generated file.
2026-07-12 — docs — The notes are the system of record (working-agreements §A0)
Owner: "look in the notes please — use them by default and grow accustomed to them by default."
Written down as a hard rule, in both places an assistant actually reads: CLAUDE.md's Start Here
is now an ordered session-start ritual (status.md → the latest session log → the plans//systems/
page for the area being touched), and working-agreements.md gains §A0 — notes read first, used as
the default source of truth over any private/AI memory, written back in the same change. Also refreshed
the CLAUDE.md notes index (it still said "there is no automated suite yet").
2026-07-12 — 2.59.0 — De-dup: the building-block catalog → engine/blockCatalog.js
The token cloud + the DPL autocomplete are engine domain: they describe the engine's own content
pools, their folder categories, the virtual {#any} / {keyword} wildcards, the NSFW gate, and the
naming rules. Nothing about that is a UI concern — the UI just renders what it returns. And it's a pure
function of a loader, which is precisely why it could be shared: each target passes its own
(runtimeLoader in the browser, metroLoader on the phone).
targets/web/frontend/lib/promptEngine.js: 411 → 219 lines.targets/mobile/lib/blockCatalog.js: 218 → 33 lines (a hand-port with no drift check at all —
the worst case, because nothing would have noticed the phone's palette falling behind the web's).
Replaced with a real test (tests/unit/blockCatalog.test.js, 9): the catalog's rules ({#any}leads,
{salt}trails,{keyword}exists, NSFW hidden by default and additive when adult is on,
expansion/never listed), the completion flattening, and — the one a hand-port could never give you —
that the phone never invents content the engine doesn't have.
Two things that test taught me, both worth keeping:- I first asserted a byte-identical catalog across the two loaders. It failed on
{#beach-merk}, and my
instinct was "mobile is missing a block". It wasn't: that's the repo-rootuser/overlay, a
desktop-only content pool (the phone's user content is its on-device Manage overlay). The test
was wrong, not the app — so I asserted the real invariant rather than weakening it until it passed. - The comparison then failed intermittently because
metroLoadercarries a module-level runtime overlay
and a sibling suite left one installed. Reset it inbeforeAll; run the suite twice to prove it's
order-stable.
Also: 43 coverage artifacts had been committed in 2.58.0 (targets/mobile/coverage/wasn't
gitignored, though the Node and web ones were). Untracked and ignored.
2026-07-12 — 2.58.0 — The mobile app was never in CI. Now it is (with press tests + coverage gates).
The biggest finding is what wasn't there. The mobile jest suite, the web⇄mobile parity gate, the
capability-gating check, the no-caps check, the engine/metro parity check — none of them ran in CI.
Every gate built during this campaign was enforced only on my machine, by me, when I remembered.
A gate you don't run is not a gate. There is now a mobile CI job: metro parity → mobile parity
(surface + gating + no-caps) → jest with a coverage gate → Codecov upload under its own flag. The check
job also gained check:registry (a provider or theme added without regenerating would silently vanish
from every target).
Press tests (working-agreements §B2). The Single view had 25 interactive controls and 3 presses;
Gallery had 9 and 2. Render-only assertions cannot catch a dead control. Now: 122 tests (was 110), with
Single's back/prev/next/delete/viewer/share/save all pressed and asserted on their real effect, and
Gallery's multi-select → select-all → delete flow pressed end-to-end and asserted on the storage
call — a Delete that clears the UI but never touches disk would pass any render test and lose nothing…
until the user reopens the app and finds everything still there.
Two real defects fell out of writing them:
- Gallery cells had no accessible name. A screen-reader user heard "button, button, button" and
could not tell one image from another, or select the right one. (axe missed it because the test
device's gallery is empty — no cells to scan.) Cells are now named by the prompt that made them, and
announce their selected state. - Single's action buttons were bare glyphs (
⤢ ⤴ ⤓ ✕), so a screen reader announced "⤢". Named.
Coverage gates, set as a floor just below the measured truth (58.8% stmts / 49.4% branches / 54.9%
functions / 61.9% lines over all mobile source, not just the files tests happen to touch) so a real
regression fails CI without flaking on churn. Raise as coverage grows.
50 visual baselines committed (5 surfaces × 5 sizes × both colour schemes), verified green on a
clean re-run. They're-win32only, so CI runs a11y + perf and visual stays a local gate until Linux
baselines exist — the same convention the web suite already follows. Stated in the workflow rather than
quietly skipped.
2026-07-11 — 2.57.1 — The web build was BROKEN on dev for four commits. Verify what you commit.
engine/listEditorOps.js was moved out of the web target in 2.55.0 and the old file deleted — but
ManageListEditor.jsx still imported the deleted path. dev did not compile:
[UNRESOLVED_IMPORT] Could not resolve '../lib/manage/listEditorOps.js'
in frontend/components/ManageListEditor.jsx
Four commits (2.55.0 … 2.57.0) shipped that way, and every gate I ran reported green. Of course it
did: a multi-path git add silently staged 13 of the 17 files I listed, so the importer fixes stayed on
disk — and npm test, the build and the parity checks all read the working tree. The committed
tree, which is what CI and every other clone sees, was broken. Caught by checking out HEAD into a
clean git worktree and building it: it failed on the first try.
A working tree is not evidence. New gate: npm run check:committed fails when any tracked
source file still differs from HEAD, so "the suite is green" can no longer be claimed over an
uncommitted fix. Recorded as working-agreements §B3a and two fix-pattern rows. (Second row earned the
hard way: I'd junctioned node_modules into the throwaway worktree to skip a reinstall, and
git worktree remove --force followed the junction and emptied the real one — nothing lost, it's
derived, but both packages had to be reinstalled.)
Also lands the rename sweep: GitHub owner junebug12851 → 1fairyfox across every current-state file
(workflows, README, CONTRIBUTING, credits, doc-site theme, sonar config, ComfyUI target, engine
copyright lines). Dated history — sessions, changelog, fairyfox reports — is deliberately left intact:
it records what was true on the day.
2026-07-11 — 2.57.0 — The app was capping the user. It shouldn't. Caps removed.
The owner's correction, and it's the important kind — a design principle I'd quietly inverted:
"Nowhere in my app does it limit the user… It only officially supports with no performance loss
those numbers, and beyond that hope for the best. If it supports numbers that high then it'll support
much higher numbers before problems increase."
1000 prompts / 100k gallery / 100k-line editor are levels the app supports without degradation — a
promise about behaviour, not permission. The app never tells the user "no." The code had
forgotten that in three places, and my new count field had just made it worse:
| Where | The bug |
|---|---|
|lib/home/buildRoll.js(web) |MAX_PROMPTS = 50— every web roll silently truncated to 50. The app could not produce the 1000 prompts it advertises; ask for 200, get 50, no explanation. |
|GenerateScreen.js(mobile) | Clamped to 1000 in five places — a mobile-only limit neither the engine nor the web had. |
| The web prompt-count<input>|max={50}— capped the spinner and made the browser mark anything higher as invalid. |
All gone. The floor stays (≥ 1, whole prompts) because that's validity, not a limit.
And the tests were defending the bug.expect(len(999)).toBe(50); // capped— an assertion that
enshrines a defect as a specification and makes the fix look like a regression. That's how it survived.
They now assert the opposite, and the engine backs it up: measured 1000 → 257 ms, 5000 → 869 ms,
25 000 → 3.9 s, all produced in full and linear (0.16 ms/prompt — cheaper per prompt at 25k than at
1k). Exactly as the owner said: if it does 1000, it does far more.
New fix-pattern row: a test that asserts a bug is the bug's best defender.
2026-07-11 — 2.56.0 — The shuffle control was missing on mobile (found by LOOKING)
Ran the visual harness and actually looked at the screenshots — the standing rule
(working-agreements §B3) that had never been honored for the mobile
app. It found, in one glance, a defect that every automated check had passed:
The Generate toolbar's 5th slot held a second building-blocks button — same icon, same
setPaletteOpen(true) handler as the green FAB right below it. So mobile had a redundant control and
was missing the web's SHUFFLE control entirely (drop the rotating random suggestion into the box). That's
a feature loss, which the parity mandate forbids outright.
Why nothing caught it. The surface-parity marker for it was /suggestion/ — which matched the caret
completion strip's suggestions array, a completely different feature that happens to share the word. A
marker satisfiable by an unrelated identifier is worse than no marker: it buys false confidence. (My first
fix, /ShuffleIcon/, was wrong the same way — the import line satisfies it even with the button
deleted. The marker now matches the wiring: /onPress=\{useSuggestion\}/, and I proved it by deleting the
feature and watching the gate go red.)
Restored to full web parity: the rotating random suggestion (re-rolls every 5s), the Try: … placeholder
advertising it, and a shuffle button that appends it — locked (not error-on-press) while no suggestion
exists, like every other capability-gated control. The FAB is now the one and only blocks control.
Regression-tested (3 tests) and proven by re-introducing the bug (2 fail → restore → 10 pass).
And the harness itself was broken in two ways, which is why the shots went unreviewed:
spawnSync("npx.cmd")can't exec on Windows withoutshell: true, so every run silently degraded to
"can't capture visual parity". A broken tool must never look like a skipped step.- It only ever captured the light scheme. Theme mode defaults to
"system"(web and mobile — that's
parity, not a bug) and a headless browser reports light, so the app's PRIMARY look — the dark canvas the
design tokens are built around — was literally never rendered. Now both schemes are shot
(--scheme=light|dark), and apageerrorlistener reports a crashed render instead of quietly saving a
white PNG.
That listener earned its keep immediately: my first cut of the fix putuseRef(settings)above
const settings = useMemo(...), a temporal-dead-zone ReferenceError that blanked the entire app — and
all 108 unit tests still passed, because they mock the engine and never exercise the real declaration
order. Only the screenshot saw it. Both landmines are in
fix-patterns.md.
2026-07-11 — 2.55.0 — De-dup Phase E: list ops → engine, accent themes → shared
Two more mobile hand-ports promoted, and two more drift checks deleted with them (five of the seven
are now gone).
listOps.js → engine/listEditorOps.js. Sort / dedupe / AI-candidate parse operate on list content,
which is engine domain — "what counts as a duplicate entry" is a property of the engine's lists, not of any
one UI. The web owned the file and mobile carried a byte-for-byte copy, policed by checkListOps. Both
Manage editors now import the engine module; the copy, its duplicate unit test, and the check are deleted.
Accent themes → targets/shared/theme/. The nine theme JSONs moved out of the web target, and the
generator (scripts/build-provider-registry.mjs) now also emits theme/presets.generated.js — the theme
DATA inlined as plain JS (a bare JSON import needs an import attribute in Node but not in Vite/Metro;
inlining sidesteps the disagreement entirely). Same root cause as the providers, third time: the web
discovered the themes with a Vite import.meta.glob Metro can't run, so mobile transcribed all nine
accents by hand. Now both read one source — themeData.js went 82 → 24 lines and checkAccents is gone.
gen-accents.mjs still emits a byte-identical accents.css (only its source path moved), and the web build
dropped 698 → 690 modules as the nine JSON modules collapsed into one.
checkLocales no longer imports the mobile module (it reads the source text) — plain Node can't resolve
Metro's bare shared/ alias. The parity script's header now lists exactly which copies the two surviving
drift checks guard, so the next promotion is obvious.
Verified: mobile 105/105 (19 suites) · web 430 · unit 352 · surface parity 22/22 · metro parity PASS ·
build 690 modules · accents.css unchanged · lint 0 errors.
2026-07-11 — 2.54.0 — De-dup Phase C/D: mobile now IMPORTS the providers (the hand-port is gone)
The 892-line mobile provider hand-port (targets/mobile/lib/imageProviders.js) — which re-declared all
~40 providers and re-implemented their transports (submitPoll, localPostJson, fetchWithTimeout,
a proxy shim) — is deleted. In its place: a 268-line adapter over the shared registry, with zero
provider or transport logic of its own. It derives the three role lists from the same manifests the web
uses, applying the web's rules verbatim (image = copy-prompt or tier: "api" + loadGenerate; text =
rewrite-capable; upscale = capabilities.upscale + loadUpscale), and dispatches generate/rewrite/upscale
straight into the shared provider code.
Three things had to be true first, and all three are now:
- The registry is importable under Metro (2.53.0's generated static index).
- The transport is injectable (2.53.0). Mobile calls
configureMobileTransport(backendUrl)at boot and
whenever the Backend URL changes: absolute base for our own/api/…(a phone has no origin), local
servers called directly (RN has no CORS, so the web's/api/forwardhop would wrongly demand a
backend), and a fetch timeout (RN's fetch has none). AddedcallRewriteProxyso/api/rewrite— the
last hardcoded relative fetch, in the web'slib/rewrite.js— goes through the same seam. - The settings-schema mismatch is resolved by preloading, not by an async UI. The manifests expose
settings asynchronously (loadSettings(), code-split for the web's gear). Mobile preloads every schema
once at boot and resolves the async option sources (samplers/schedulers/models) into concrete arrays, so
the UI stays synchronous — on a phone the bundle already ships everything, so lazy-loading buys
nothing and an async gear would be strictly worse.
cleanDplOutput(DPL reply cleanup — engine-domain) moved toshared/_shared/rewriteSystem.jsbeside
systemFor; the web re-exports it. ProviderkeyHintjoineddescription/keyUrlon the manifests.
Phase D — retired the drift checks the duplication had made necessary.checkProviders,
checkRewriteSystemsandcheckLocalSettingsare deleted fromscripts/mobile-parity-check.mjs: they
compared mobile's copy against the web source, and you cannot drift from yourself.checkSurfaces
STAYS — it asserts the mobile UI exposes every web feature (the full-parity mandate), which no amount of
code-sharing guarantees. The script's header now spells out the difference so nobody re-adds them.
Replaced by a real contract test (targets/mobile/lib/__tests__/imageProviders.test.js, 17 tests): the
derivation rules, labels/descriptions coming from the manifest, real schemas + resolved sampler options,
serverKeyderived from the provider's own URL field, the transport config, and the rewrite dispatch
(browser-direct → its own API; proxied → the Backend URL). One visible behavior change, and it's a parity
FIX: local providers now show the web's label ("ComfyUI"), not the mobile-only "ComfyUI (local server)" —
the picker already groups Local/Online.
Test infrastructure: Jest neededmoduleNameMapperforshared/*(mirroring Metro's alias) and
babel-plugin-dynamic-import-nodein the test env only — Jest's CJS VM throws on a nativeimport(),
which is exactly how the manifests lazy-load. Root Vitest got the matchingsharedalias.
Verified: mobile 110/110 (20 suites) against the REAL shared registry · web 430 · unit 352 · surface
parity (Manage 22/22) · metro parity PASS · build 698 modules · lint 0 errors.
2026-07-11 — 2.53.0 — De-dup Phase B: ONE provider registry for every target
The provider pool (targets/shared/<id>/) is a drop-a-folder-in plugin pool, which needs discovery —
and each runtime discovers differently: Vite has import.meta.glob, Node has fs.readdirSync, and Metro
has neither (it resolves a static module graph, with no filesystem at runtime). So the same 40 provider
folders had grown three registries: the Vite glob in shared/index.js, an fs-discovery re-port in the
CLI (which also re-implemented applySharedSettings), and — because Metro can do neither — an 892-line
hand-port in the mobile target that re-declared every provider and re-implemented the transports. Three
copies of one truth, free to drift; the parity checks existed to detect that drift rather than remove it.
Replaced all three with a generated static index — targets/shared/registry.generated.js, plain
import statements, the one form Vite, Node and Metro all understand (scripts/build-provider-registry.mjs;
npm run registry to regenerate, npm run check:registry in npm test fails if it's stale, so
drop-a-folder-in still needs no central edit). shared/index.js is now runtime-agnostic — no
import.meta.glob, no import.meta.env, no node: imports — which is precisely what let the other targets
import it instead of forking it. The one web-only concept, online-build gating, moved out to the web shim
(lib/providers/index.js), which binds providersFor(online) to VITE_ONLINE; the SPA's zero-arg
availableProviders() / getProvider() API is unchanged. The CLI's registry dropped from 145 lines to a
thin async facade over the shared one (−111 lines, and its duplicate applySharedSettings is gone).
Verified on all three runtimes: local web build 697 modules, online build + SSR prerender green, prompt list providers 40 (settings fold intact through the CLI's JSON loader hook), web 419 tests, unit 352, lint 0 errors.
2026-07-10 — Mobile parity Phase 1b: Single view two-pane on tablet
The Single (image detail) view now goes two-pane on tablet (web parity): the image becomes a ~44%
left column beside the metadata (prompt layers, details, keywords, derived strips) instead of a full-bleed
image stacked over it; phones keep the single stacked column (the wrappers are layout-neutral there — no
phone regression). Uses useResponsive().twoPane; the media/meta split is a plain flex row. This
completes the per-screen tablet layouts (Generate/Manage/Single centered or two-pane; Gallery full-width
grid) — no feature is hidden at any size. test:mobile 80/80, lint clean.
2026-07-10 — Mobile parity Phase 4: strict Manage surface gate ON
With the Manage port complete, the Manage surface is now in the strict parity gate — 22 markers, one
per web Manage capability (two roots, folder tree, block editor DPL/Insert/Refine/Modify-Draft/Cleanup/JS/
NSFW/description/rename/delete, list Entries-Raw/Sort/Dedupe/AI-Expand/description, built-in browse +
override, runtime overlay). A missing feature now fails the build (no per-feature ignores), exactly like
the other surfaces. mobile:parity green (Manage: all 22 present), test:mobile 80/80, metro:parity
PASS. The mobile Manage is now at feature parity with the web to the platform-allowed extent (on-device
user overlay + read-only built-ins; the SFW build strips NSFW as before).
2026-07-10 — Mobile parity Phase 3h: built-in catalog browse + override
The Manage screen gains a Built-in catalog section — a searchable, read-only browser of the baked
catalog (components/BuiltinBrowser.js, reading metroLoader directly). Since built-ins can't be edited
in place on device, each entry offers Override: copy its source into the editable user overlay
(readBlockSource for blocks, readListLines joined for lists) where it wins over the built-in, then open
it in the editor — the web's "Create override". Search-gated so the 89-block/88-list catalog renders
nothing until queried. New component test (virtual-mocked engine). test:mobile 80/80, parity green.
2026-07-10 — Mobile parity Phase 3g: block editor Refine + Modify/Draft (AI DPL modes)
The Manage block editor gains the web's AI DPL controls: a Refine bar (Detail / Complexity / Focus /
Intensity / Variety −/+ steppers + Cleanup) and Modify (dpl-custom) / Draft (dpl-create) with a
free-text input — each runs the selected Text provider on the DPL and replaces the source (busy state,
markdown-fence stripping via cleanDplOutput). Ported DPL_PRIMER + DPL_TASKS (13 modes) VERBATIM into
the mobile systemFor, plus a new parity-gate step (checkRewriteSystems) asserting systemFor() is
byte-identical to the web rewriteSystem.js for all 17 modes — so the prompts can't drift. New tests
(Refine + Draft). test:mobile 77/77, parity + metro green.
2026-07-10 — Mobile parity Phase 3f: list editor AI Expand
The Manage list editor gains AI Expand — samples up to 25 existing entries, asks the selected Text
provider for 25 fresh ones in the same vein, and merges in only the net-new (via the parity-locked
listOps parse/merge). Reuses the mobile rewrite provider wiring (getTextProvider + getKey +
per-provider settings/backend), and systemFor("expand") now returns an EXPAND_SYSTEM ported verbatim
from the web rewriteSystem.js. Handles no-provider / no-key / no-entries / only-dupes with clear status.
New test (mocked provider). test:mobile 75/75, parity green.
2026-07-10 — Mobile parity Phase 3e: block editor Insert menu
The Manage block editor gains the web block editor's Insert control by reusing the existing mobile
InsertMenu (the DPL-syntax bottom sheet — structure/chance/choose/repeat/flow/emphasis/code with live
re-rolling examples): picking a construct appends its snippet into the DPL source. Component tests stub
InsertMenu (it pulls the engine, not jest-resolvable). test:mobile 74/74.
2026-07-10 — Mobile parity Phase 3d: runtime overlay (custom content feeds generation)
The Manage overlay is now live — custom lists + block generators actually feed prompt generation, not
just persist. metroLoader gained a runtime user overlay (setMetroOverlay) consulted user-wins across
readListLines / listNames / readListMeta / loadBlock / blockNames / readBlockMeta (+ a
readBlockSource for future override); it defaults EMPTY so metro-parity-check (which never populates
it) stays identical — metro:parity still PASS (89 blocks / 88 lists / 150 seeded gens identical). New
mobile lib/overlay.js reads the whole on-device overlay (nested) from storage and installs it via
setMetroOverlay; App.js calls it at startup and ManageScreen after every edit, so {name} /
{#name} draw from the user's own content immediately. (Platform limit: an overlay block's .js sidecar
body can't execute on device — no runtime require/eval — so insert js: yields "" there; the .dpl
runs.) New unit test (virtual-mocked engine); the two component tests mock the overlay away (engine isn't
jest-resolvable). test:mobile 74/74.
2026-07-10 — Mobile parity Phase 3c: list editor description + Entries/Raw tabs
The mobile Manage list editor gains two more web-parity pieces: a description field (persisted to the
.json sidecar via the Phase-2 readUserSidecar/writeUserSidecar) and an Entries ⇄ Raw tab pair —
Raw edits the whole file as text, re-parsing back into rows on switch/save. Save now writes both the file
(entries or raw) and the description sidecar. Tests cover the description load + the Raw switch.
test:mobile 72/72, parity + metro green.
2026-07-10 — Mobile parity Phase 3b: Manage folder tree
The two-root Manage master is now a nested folder tree (the web Manage's tree, RN form). New
components/ManageTree.js renders readUserTree output — collapsible folder nodes with entry counts + a
delete action, and tap-to-open entries with a kind dot / JS badge / delete. ManageScreen feeds it
readUserTree("blocks") + readUserTree("lists"); folders are created implicitly by naming an entry
folder/name (sanitized per segment; storage makes the parent dirs), and folder headers delete the
folder (recursive). New ManageTree component tests + updated screen tests. test:mobile 71/71, parity +
metro green.
2026-07-10 — Mobile parity Phase 3a: Manage gains block/generator editing (two-root)
The mobile Manage was lists-only; it now has the web's second root — Blocks (custom DPL generators).
New components/DplMiniEditor.js (a reusable line-numbered monospace DPL editor, same shape as the Generate
composer's box) and components/ManageBlockEditor.js (name + rename, description, NSFW flag, DPL source,
optional JS sidecar view/edit/create, save, delete) — built on the Phase-2 storage overlay. ManageScreen
is now a two-root master/detail (Blocks + Lists) that creates/opens/deletes both and routes to the block or
list editor. New component tests for the block editor + the block flow; test:mobile 66/66, parity + metro
green. Remaining Manage layers (folder tree + folder editor, built-in browse, override/restore, Insert/Refine/
Modify-Draft, list Raw tab + AI-Expand, runtime overlay, strict gate) are tracked in
notes/plans/mobile-parity.md — sequenced, not dropped.
2026-07-10 — Mobile parity Phase 2: Manage data layer + list-editor Sort/Dedupe
Foundation for the full RN Manage port. lib/storage.js gains the on-device user-content overlay (RN
counterpart to web user/lists + user/blocks): user blocks CRUD (.dpl), .js/.json sidecars,
nested folders (recursive walk), a readUserTree builder mirroring the web Manage tree model, and
folder create/delete + entry move/rename (10 unit tests, in-memory FS). The list editor's Sort and
Dedupe land via a new lib/listOps.js — a faithful port of the web listEditorOps.js (sort, dedupe,
AI-candidate parse/merge) — wired into the Manage list editor with a status line. A new parity-gate step
(checkListOps in mobile-parity-check.mjs) asserts the ported ops stay behaviorally identical to
the web source (5/5 cases), so they can't drift. test:mobile 60/60, parity + metro gates green.
2026-07-10 — Mobile parity Phase 1a: tablet content layouts (Gallery grid + centered columns)
First slice of the responsive/tablet work (no size-based feature loss): the Gallery now fills the full
width on tablet/wide with a larger cell target (was capped at 900px, leaving dead space) — phone sizing is
byte-identical, so no phone regression. Reading/editing surfaces (Generate, Single, Manage) get the web's
centered max-width reading column on tablet via a new components/ContentColumn.js (RN counterpart to the
web's .main-col > * { max-width: 960px }), wired in App.js; the Gallery grid opts out and uses full
width. ContentColumn renders its children at both phone and tablet sizes (component test guards the
"same content at every size" invariant). test:mobile 46/46.
2026-07-10 — Mobile⇄web full-parity campaign: Phase 0 (audit + responsive foundation)
Kicked off the mandated push to complete mobile↔web parity (no exceptions, no size-based feature loss,
gate-enforced; the SFW/NSFW split stays a build variant, not a feature drop). Combed every mobile surface
against the web and wrote the authoritative gap audit + phased plan (notes/plans/mobile-parity.md;
standing instruction strengthened in notes/systems/mobile.md). Phase-0 code, all green: the visual-parity
harness (scripts/mobile-visual-parity.mjs) now captures the full phone→tablet size matrix (360/390/430 +
tablet 834/1112) into per-size folders with a --size= filter; a new lib/responsive.js
(useResponsive(), breakpoints mirroring the web tiers, two-pane + capped reading column) with unit tests
lays the foundation for per-screen tablet layouts; and the jest FlashList mock now keys its empty element
(kills the ManageScreen "unique key" test warning). test:mobile 44/44, parity gates green. The big items
(full RN Manage port, per-screen tablet two-pane, strict Manage gate) are the following phases.
2026-07-10 — Fix the Manage tab's phone editors (right pane)
A web-vs-mobile audit of the Manage tab (code + live screenshots at 360/390/768) turned up three
right-pane editor bugs on the phone layout, all fixed in one mobile-only @media (width <= 768px) block
in manage-responsive.css (scoped to .workspace.manage, so desktop + the Generate composer are
untouched): (1) the block editor's absolutely-pinned Modify / Draft combo overlapped the DPL code —
its .cm-content top-gutter is silently overridden by CodeMirror's own padding, and the narrow pane let
wrapping code run under it; on phone it now un-pins to a right-aligned row above the editor, with its
popover floated to a full-width bottom sheet (reusing the mobile-sheets.css pattern) so
overflow: hidden can't clip it. (2) The editor head was a nowrap row that squeezed the name
field to ~21px — it now wraps, name field full-width. (3) The list tools row squeezed the entries
search to ~24px — same wrap fix, search full-width. The master tree, list rows, and folder editor were
already fine. Added two Playwright regression tests in tests/e2e/responsive.spec.js. CSS + test only,
mobile-scoped (no desktop visual-baseline churn). PATCH.
How to get it
Prefer not to build? Download a pre-built edition:
- Desktop app -- Windows
.msi/.exe(or a portable.zip), macOS.dmg, Linux.AppImage/.deb(attached below). Self-contained; nothing to install first. random-ai-prompt-2.60.0-online.zip-- the online edition as static files; host it anywhere, or just use https://prompt.fairyfox.io.random-ai-prompt-2.60.0.tar.gz-- source tarball (run with Node 24:npm installthennpm start).random-ai-prompt-2.60.0-docs.zip-- the generated documentation site.
What's Changed
- Release v2.60.0 by @1fairyfox in #51
Full Changelog: v2.52.0...v2.60.0