Newest entry on top.
Android target - Providers + API keys (BYOK image generation)
Third overflow control; the image-gen milestone. Read the shared provider registry (targets/web/shared): most providers are hosted-proxy (need the desktop backend) so can't run on mobile; only the browser-direct BYOK ones work (like the online web build) — OpenAI, fal, Stability, Gemini (+ text-only), plus copy-prompt tiers (midjourney/novelai).
- lib/imageProviders.js: ported the browser-direct adapters (openai / fal / stability / gemini server.js) —
direct fetch →
{images}(data:/https:). - lib/keys.js: expo-secure-store per-provider keys (web no-op).
- lib/storage.js
saveImageSrc:data:(base64 write) /https:(downloadAsync) /file:(copy) → Gallery. - theme.js context also carries the selected
provider(persisted). - OverflowMenu: Image-provider picker + secure key field (+ Get-a-key link) at the top, above Appearance.
- GenerateScreen.generate(): provider+key set → generate one image per rolled prompt, save each to the
Gallery, live status; else prompts-only. App bumps galleryKey via
onGenerated.
Started with OpenAI (owner's pick). Can't verify real output from here (needs the owner's key + phone network); UI + bundle verified via web render. Follow-ups: Leonardo (submit-poll transport), per-provider model/size "gear", and a batch-concurrency cap for large prompt counts.
Android target - theming: System/Dark/Light + accent picker (Appearance)
First of the three overflow controls the owner picked. Read the web theming source (tokens.css dark+light palettes, theme/presets.js + theme/themes/*.json, ThemePicker.jsx) and built the mobile counterpart:
- lib/theme.js: ThemeProvider + useTheme() → the token object
Tfor the resolved base (System viauseColorScheme/ Dark / Light) + accent; 9 accent presets mirrored; persists to the doc dir (expo-file-system legacy, web-safe no-op). - Refactored all screens/components from
import { T }toconst { T } = useTheme()+makeStyles(T)(memoized) — required becauseStyleSheet.createcaches at module load, so theme-dependent styles must be built per-render. Memoized rows (ResultRow / LineRow / palette SubTab) call the hook themselves so FlashList perf holds (context is stable unless the theme changes). - OverflowMenu: added the Appearance section (mode segmented + accent swatches) above the links — the web ThemePicker folded into the overflow.
Verified via web export + Playwright: switching to Dark + Violet recolored the whole app (tabs, editor, tools, generate, FAB). System mode follows the browser/OS scheme (light in the container, so the render looked light by default — on the phone it follows the phone). Next: Language picker, then Providers + API keys (image-gen milestone).
Android target - header rebuilt to match the web (read the source first)
Read the real web header before touching it: App.jsx <header className="topbar">, title-bar.css,
view-switch.css, topbar-responsive.css (the phone collapse behaviour), and LinksMenu.jsx. Then fixed the
mobile header:
- Brand = the real app logo (
assets/icon.png, 32px); the wordmark is hidden on phone, as the web does. - View-switch active tab =
accent-strong; switch bg = input tone (matches view-switch.css). - The ⋯ was a dead button →
components/OverflowMenu.js: a dropdown under the bar with the project links + "get it" group + legal pages + version, opened viaLinking— the web's compact LinksMenu. Added the Feather link icons tolib/icons.js.
Deploy origin for the legal links = prompt.fairyfox.io (found referenced in targets/web/public/legal/*.html).
Providers / NSFW / Theme overflow rows are deferred until those features land on mobile.
Android target - proper Generate pass (read the real web source, not the screenshot)
Owner (rightly) called out that I'd guessed from the page image: the Insert button wrongly opened the blocks menu, the preview didn't cycle, and the palette lacked the web's sorted categories. Read the actual web components (PromptComposer, DplInsertBar, LivePreview, home/BlockPalette, promptEngine.getBlocks, home/blockCategories, dplInserts) and rebuilt to match:
- Insert ▾ = the DPL SYNTAX menu (dplInserts catalog): category→constructs with live 1s examples; inserts a
snippet.
lib/dplInserts.js+components/InsertMenu.js. - eye = live preview re-rolling every 1s; click toggles.
- Building blocks = a faithful
getBlocksport over the metroLoader (lib/blockCatalog.js, reusingengine/nameOrder.jscomputeButtonNames/compareNames +engine/gatedLists.jsisGatedBlock) +foldersOf(lib/blockCategories.js);components/BlockPalette.jsrenders search + Blocks/Lists groups + All + folder sub-tabs + hints + group-pills + chips. - Added a DPL completion strip (
getDplCompletions) — tap-complete{…}/{#…}tokens as you type. lib/engine.jscentralizesrun+expandOnce(clean example expansion, autoAddFx/artists off).
Landmine: computeButtonNames/compareNames live in engine/nameOrder.js (the web re-exports them via
engine/listManifest.js) — import from nameOrder.js directly. Verified via expo export --platform web
- Playwright; no page errors. LAN URL is stable across Metro restarts (
exp://<lan-ip>:8081), so a restart to pick up new modules needs no new QR.
Android target - device-test fixes (owner testing round)
Owner tested on the phone and flagged: the status bar covering the tabs, the DPL editor rendering garbled,
tabs/buttons mis-tapping ("buttons click other buttons"), and keyword/artist settings that were asked
removed and aren't even on the web. Read the REAL web composer (components/PromptComposer.jsx) for true
parity, then fixed:
- App.js:
react-native-safe-area-contextSafeAreaProvider/SafeAreaView (edges top) - react-native's SafeAreaView is a no-op on Android, which is why the status bar overlapped the tabs. - GenerateScreen.js: dropped the highlight-overlay editor (garbled on-device) for a plain monospace input;
removed the invented settings; mirrored the web's editable Prompts count in the field bar; gear opens
a minimal prompt-settings sheet; auto-fix/keyword tools rendered disabled (no text provider on mobile);
removed
hitSlopthat overlapped adjacent tap targets.
ngrok tunnel is down today (@expo/ngrok "Cannot read properties of undefined (reading 'body')", points
to status.ngrok.com) - fell back to LAN (expo start, exp://<lan-ip>:8081; phone must share the PC's
Wi-Fi). Also: restart Metro after adding a native-bundled dep (safe-area-context) so its module map picks it
up (Metro was started before the install).
Android target - composer/nav fidelity pass + web⇄mobile parity rule
Owner: keep the web UI in parity with the mobile UI by default. Recorded as a standing rule in
notes/systems/mobile.md (new) - the analog of the CLI's engine+GUI parity rule.
Rendered the REAL web phone Home (expo-exported web + Playwright at 390px) and matched it natively:
- App.js: one-row top bar - brand mark + enclosed pill view-switch (active green-filled) +
⋯overflow. - GenerateScreen.js: the web
PromptComposernatively - Insert ▾ dropdown, code-editor prompt box (gutter1 ++{…}syntax highlight over a transparent input, ✓/eye/gear), PROMPTS count, tool toolbar + round green generate button, bottom-left green building-blocks FAB. - lib/icons.js (new): react-native-svg line-icons matched to the web set (Expo Go bundles svg - no rebuild).
Verify loop: edit on Windows → expo export --platform web --clear → stage dist-web/ into the cloud
container → serve + Playwright screenshot at 390px → compare to the web reference. Generate/Gallery/Manage
all match; no page errors. dist-web/ + run logs gitignored.
Landmine: each Windows-MCP PowerShell call is a SEPARATE session, so Start-Job / -NoNewWindow
children die with it. Launch long-lived processes (expo export, expo start --tunnel) via
Start-Process <npx.cmd> ... -RedirectStandardOutput <log> (detached, survives) and poll the log. The
exp:// tunnel URL isn't in the redirected log (expo's interactive UI only) - read it from the ngrok
inspector at http://127.0.0.1:4040/api/tunnels.
Android target - reworked the UI to match the web app (owner correction)
Correction. I'd assumed web = desktop-only and invented a fresh mobile design (blue theme, bottom tab bar, a top-right tab-name tag). Owner: the web app ALREADY has a complete responsive phone layout - stick close to it. Reworked to mirror it.
Read the web design first: notes/plans/responsive.md (nav pattern, drawers, master/detail), styles/foundation/tokens.css (the real palette), components/Home.jsx (composer + block-palette + results).
Changes:
- lib/theme.js: tokens mirrored from the web (mint #34e2a0 accent + dark ink, #1c1c1f canvas, #232328 panels, real text/border colors, 14px radius). Re-themed every screen off it.
- App.js: top-bar nav (brand + scrollable view switch), removed the bottom tab bar + the top-right tag.
- Generate: composer + a building-blocks bottom sheet (palette grouped from the engine catalog) + the virtualized results list - matching Home.jsx.
- Single/Manage: re-themed; Manage keeps master/detail + back.
Verified web render (screenshotted at 390px: mint theme, top-bar view-switch, blocks drawer works) + Android Hermes bundle (697 modules, 3.25 MB .hbc). The web:build for a live reference errored (a prerender/build issue in this env) so I matched from tokens + component structure; a live-reference pass can refine later.
Next. Image generation (targets/web/shared providers + SecureStore) to fill the Gallery; Manage overlay into runtime generation. Later: fonts (Maven Pro / Space Grotesk) + the secondary-controls overflow menu.
Android target - full 4-tab app built to the max-load performance bar
Scope correction. Earlier I wrongly scoped mobile to just Generate (mirroring the web online edition where Gallery/Single/Manage are disabled). Owner corrected: the phone HAS storage, so all four tabs apply, backed by the filesystem. Rebuilt as the real 4-tab app.
Read the perf bar first (owner-directed). notes/reference/performance.md + decisions/architecture.md: supported max simultaneous load = 100k-image gallery + 1000 prompts (~10k images) + 100k-line Manage file. Built each surface to it with RN-native tools:
- Generate: virtualized FlashList results (1000 rows), memoized ResultRow, stable seed:index identity.
- Gallery: recycling FlashList + expo-image (disk cache, downsampled, recyclingKey), uniform cells (100k).
- Manage: windowed editor - UNCONTROLLED per-line inputs commit into a ref so typing never re-renders the 100k-row FlashList; responsive filter; stable per-line ids for add/delete.
- Single: expo-image detail. Storage: lib/storage.js on expo-file-system (legacy API), web-safe no-op.
Shell: custom bottom tab bar, panes kept mounted (absoluteFill + display:none) so state persists on switch - the web SPA's pattern.
Verified: react-native-web render of all tabs (screenshotted at 412px; Generate/Gallery/Manage clean, tab nav works, web-safe storage guard shows "off in web preview"); Android Hermes bundle 696 modules -> 3.24 MB .hbc. Metro landmine: a non---clear export served a STALE App.js; always --clear after screen edits.
Next. Image generation (reuse targets/web/shared/ provider adapters + expo-secure-store keys) to fill the Gallery; wire the Manage user overlay into runtime generation.
Android target - runs on a real phone (Expo Go), SDK 54 pin, clean alias, emulator notes
On-device. The PoC now runs on the owner's Moto G Power (Android 16) via Expo Go. Two blockers solved:
- Expo Go SDK mismatch. create-expo-app gave SDK 57; the Play Store Expo Go is pinned to SDK 54 (Expo
policy, May 2026) and refused ("requires a newer Expo Go"). Downgraded the mobile package to SDK 54
(expo 54.0.35, RN 0.81.5, React 19.1.0) via
expo install --fix. Confirmed running on the phone. - Connection.
expo start --tunnel(@expo/ngrok) so the phone connects over any network (couldn't add a firewall rule for LAN without admin).
Engine linking - settled on a Metro alias (no copy, no symlink). Relative ../../engine never resolved
(repo root is an ESM type:module package Metro won't cross); a junction worked on SDK 57 but not SDK 54;
an interim copy worked but was inelegant (owner pushed back). Final: Metro resolver alias
(extraNodeModules: engine -> real repo path) + watchFolders + nodeModulesPaths. Single-source, verified.
Removed scripts/link-engine.mjs; setup is just catalog. Logged engine-as-workspace-package as the proper
follow-up.
Screenshots. Owner's phone Developer Options is carrier-blocked (no adb). Fixed the local emulator boot (hw.gpu.enabled=no + 2GB -> swiftshader_indirect + 4GB, ~47s) but headless screencap fails (hasReadColorBufferDma) and hardware-GPU crashes - a machine graphics-stack issue. Settled on the react-native-web render (identical RN tree, real engine) as the screenshot tool; owner can send manual phone screenshots for real-hardware checks.
Next. Port the tab UI to RN screens; then the engine-as-package refactor.
Android target - Expo scaffold + Hermes bundling proven
Scaffolded targets/mobile via create-expo-app --template blank (Expo SDK 57, RN 0.86, React 19; New
Architecture + Hermes are the defaults). Wrote a PoC App.js that runs the REAL engine via metroLoader
(generate a prompt + reroll). Pruned template cruft (its CLAUDE.md/AGENTS.md/LICENSE/.claude).
Metro monorepo fight (resolved). Metro would not resolve the engine across the monorepo boundary
(../../engine/...): even with canonical watchFolders + nodeModulesPaths and cleared caches it reported
the (existing) engine files as non-existent - the repo root being an ESM (type:module) package appears
to confuse Metro's resolver. Fix: link the engine IN-project via a Windows junction
(targets/mobile/engine -> ../../engine, scripts/link-engine.mjs / npm run link:engine), install the
engine's runtime deps (lodash, compromise) locally, and use the DEFAULT Metro config. All resolution now
stays inside the project root. (Separate earlier blocker: babel-preset-expo was not hoisted top-level;
installed it directly.)
Result. npx expo export --platform android -> Android Bundled 623 modules -> 2.7MB Hermes bytecode
(.hbc). The engine that parity-proved under Node now COMPILES for Android under Hermes/Metro. Compilation
proven; not yet run on an emulator.
Committed: targets/mobile scaffold (App.js, app.json newArchEnabled, babel/metro config, package.json with link:engine/catalog/setup scripts, scripts/link-engine.mjs, package-lock.json). node_modules, the engine junction, and dist/ are gitignored.
Next. Boot an Android emulator (SDK at C:\Users\juneh\AppData\Local\Android\Sdk) to see the PoC screen generate a prompt, then port the tab UI.
Android target - decision + Metro loader foundation (parity-proven)
Request. Owner wants an Android target (no iOS/Windows Phone). Asked whether Facebook's "React on the native layer" (React Native) still exists and is wise.
Decision. Rejected WebView wrappers (Tauri mobile / Capacitor) as fragile/heavy here, and a native
rewrite (Flutter/Kotlin) because it would abandon the isomorphic JS engine (the crown jewel). Chose
React Native via Expo (New Architecture: Hermes + Fabric) - native feel, light runtime, single JS
stack; the engine + fetch-based providers reuse unchanged, only the tab UI gets a one-time port. Mobile
drops NSFW + local providers (which conveniently removes the Node-dependent adapters). Android SDK to be
referenced via a git-ignored symlink under assets/references/ (owner's idea) with an ANDROID_HOME
fallback - wired at scaffolding.
Content rating. Play is hostile to BOTH adult apps and kids apps. Settled on: Play build = all-ages ("Everyone" IARC rating), NOT enrolled in Designed-for-Families/kids (avoids the heavy kids scrutiny), and no NSFW at all. NSFW stays GitHub-APK-only. Base corpus is already purged of slurs/minor-sexual/ extreme by contentSafety.js, so "SFW" starts from an already-clean base.
Built (de-risk foundation, no Expo install yet):
engine/core/metroLoader.js- third isomorphic loader (static catalog, synchronous).scripts/build-metro-catalog.mjs- codegen formetroCatalogData.js; tiers full|sfw; NSFW excluded by name token; nsfw group refs + nsfw preset stripped from sfw; all-ages scrub hook.scripts/metro-parity-check.mjs- parity gate. npmmetro:catalog/metro:parity; catalog gitignored.
Verified. npm run metro:parity -> 89 blocks + 88 lists identical to nodeLoader (built-in), 150 seeded
generations byte-identical. SFW catalog carries no adult content/refs (only the doc header + anti-nsfw
negative prompts remain as the literal token). Lint + smoke green.
Next. Scaffold targets/mobile/ (Expo, New Arch) consuming the shared engine + Metro catalog;
SecureStore for keys; two build flavors (play=sfw, full); then port the tabs to RN screens.
Maintenance sweep — branch cleanup, ship dev→main, docs reconcile, new runbook
Request. Owner asked for a full maintenance sweep: close all feature branches (local + GitHub),
ship dev→main, get both branches in sync, and fix any doc/notes/README drift — "full sweep type
thing." Mid-run they added: turn it into a reusable maintenance procedure and report it to the fairyfox
hub for adoption.
Git / branch cleanup.
- Audited first (read-only): all six local
feature/*branches were fully merged intodev; the two remote feature branches (gallery-composer-a11y-seo,user-overlay) were merged too; one Dependabot PR (#43) had unmerged content. - Surfaced PR #43 and asked — owner chose to adopt it. Merged it into
dev(vitest+@vitest/coverage-v84.1.9→4.1.10, a dev-dep patch; test-tooling only → no VERSION bump); its remote branch auto-deleted. - Deleted the 6 merged local branches and the 2 merged remote feature branches. End state: only
main+devon both the local checkout and GitHub, zero open PRs.
Docs reconcile. notes/status.md had drifted: Version: said 2.46.0 (actual 2.51.1), shipped
branches were still labeled "pending review," the structure para called the CLI "planned" (it shipped in
2.50.0, ComfyUI in 2.51.0), and the build-health/open-issues tables were stale (18 lint warnings, 128/60
tests). Corrected the version line + a dated "two branches only" note, marked CLI/ComfyUI as shipped
targets, retired the now-resolved lint-warning issues, and refreshed the health table with
freshly-run numbers (lint 0 problems; smoke ok; unit 319; web 419). README verified accurate.
New procedure (for reuse + hub proposal). Wrote ../../reference/maintenance-sweep.md
— the repeatable full-sweep runbook (audit → triage → close branches → ship → reconcile docs → verify),
composing the existing git-workflow / repo-hygiene / versioning standards with hard safety rules. Wrote
the fairyfox proposal report
../../fairyfox-reports/2026-07-09-propose-maintenance-sweep-standard.md
recommending the hub adopt the procedure cross-project (proposal-only — hub not touched, per the
stay-inside-this-repo guardrails; the owner carries it upstream).
Verify + ship. npm test green (check:docs 364 links · lint 0 · smoke · unit 319 · web 419);
npm run check:tidy clean. Then shipped dev→main via PR (no VERSION bump, so release.yml no-ops —
the v2.51.1 tag already exists) and back-merged so dev contains main.
DPL editor dark-mode chrome fix — white gutter + washed-out active line (2.51.1)
Symptom (owner-reported, verified visually at prompt.fairyfox.io in dark mode): the DPL prompt box's line-number column rendered as a bright-white slab, and the active line was washed out by a milky light-blue band.
Diagnosis. Inspected the live DOM's computed styles: .cm-gutters → rgb(245,245,245),
.cm-activeLineGutter → rgb(226,242,255), .cm-activeLine → rgba(204,238,255,0.267). Those are
CodeMirror 6's built-in light gutter baseTheme values. CM's gutter baseTheme ships both
&light .cm-gutters {#f5f5f5} and a &dark variant and switches on whether the EditorView theme
declares { dark: true }. Neither DplEditor.jsx nor CodeEditor.jsx did, so CM stayed light and
injected the light rules — at the SAME specificity as the existing .dpl-editor .cm-gutters CSS
(both one class + .cm-*), so CM's later-injected <style> won on source order. The plain-CSS
override (added back in 2.49.6) never actually applied; that's why the deployed 2.51.0 still looked
bad even though the source "had a fix".
Fix. Moved the chrome colors into a CodeMirror theme extension
(targets/web/frontend/lib/editorChrome.js) — a theme gets StyleModule priority over the
baseTheme, so it wins reliably. Values use the app's CSS variables (--faint, --muted, --fg,
--accent-soft) so it stays light/dark adaptive without hard-pinning a dark flag. Wired into both
editors. Removed the now-dead CSS rules and left a comment explaining the mechanism.
Verify. Ran the dev server, screenshotted the editor in dark mode (transparent gutter, dim
numbers, subtle neutral active-line lift) and confirmed light mode adapts (subtle dark lift,
transparent gutter). Added tests/e2e/editor-chrome.spec.js (dark scheme; asserts non-light gutter +
neutral active line) — passes against the production build (4.8s).
Note for release: committed to dev as 2.51.1 (PATCH). Not yet released to main/deployed — the
live site keeps showing the white gutter until a dev → main release goes out.
Landmine recorded: see notes/reference/esm-patterns.md / fix-patterns — CodeMirror chrome
(gutter, active line, selection, tooltips) must be themed via EditorView.theme, NOT plain CSS,
because CM's baseTheme is same-specificity and injected late. (The autocomplete-contrast fix hit the
same class of bug from the tooltip side.)