Skip to content

Changelog

github-actions[bot] edited this page Aug 29, 2026 · 320 revisions

📝 Changelog

All notable changes to Formamorph. This fork's first line is 2.0.0 — a full TypeScript rebuild of the upstream JavaScript app (FieryLionite's Formamorph, ~v1.2) — with feature parity as the baseline plus new features on top.

2.0.0 – 2.15.2 are released (collapsed below). New work lands under 🚧 In Progress — an unnumbered section, so changes accumulate without pinning a version. When a batch earns a release its section is marked Released and collapsed, and a fresh In Progress opens. package.json reads 2.15.2 — the latest released version.

Each release groups changes as Major / Minor, then Added / Removed / Fixed, and within those by audience: 👤 user-facing · 🛠️ developer tooling · ⚙️ backend. Where two or more changes touch the same feature, they sit together under that feature's name.


🚧 In Progress

Unreleased — new work accumulates here until it earns a version bump. The next batch will pin its own version; package.json reads 2.15.2 (just released below).

Minor Changes

➕ Added

  • 👤 User-facing
    • The Request Anatomy now speaks in chips, the same ones you placed in the prompt editor, and flips between Chips and Preview the way an editor flips between Edit and Preview. The anatomy used to print an invented italic line above every block the app filled in — and it called all of them "world data from your chips", including the reply-length instruction, which is not your world at all. Those lines are gone everywhere, the in-game AI Context viewer included; in the hub, your text and the app's text are told apart by highlight and dimming. The hub now opens in Chips, where every filled-in block collapses to the chip behind it, so a request reads as your own template with the blanks marked. Chips from your template keep the editor's own name and variant — a Stats chip set to Range reads the same in both places — but wear a quiet neutral face here, so the view reads as your text with its blanks marked rather than a page of color; clicking one opens that editor and scrolls to that very chip, ringed so you can see which one you came for. Blocks the app assembled instead of a chip get a dashed chip with a short name and a plain-words tip: Memory Recap, Past Action, Turn Plan. Where another prompt wrote one, clicking it opens that prompt, so the hub reads as a map of the turn. Blocks nobody wrote — your own action, your notes — stay as labels with nothing to click. Preview shows the whole request resolved, every byte of it: the hub used to cut each assembled block to a single 110-character line, so the real request was never on screen. At full screen the hub gets the editors' side-by-side view too, template on the left and resolved request on the right, on the same split preference the prompt fields already use. Switching views keeps your place: the flip lands on the block you were reading rather than back at the top, side by side the two panes scroll together, and selecting a different prompt starts that one at its own top.
  • 🛠️ Developer tooling
    • The dev-router reaches the in-game AI Context popup directly (#dev?view=gameViewer&fixture=whiteRoom&modal=aiContext), so its layout is checkable without hand-navigating a live game.

🔧 Fixed

  • 👤 User-facing
    • Full Screen:
      • The dark sheet behind a full-screen editor now fades with the window instead of flashing. Pressing a full-screen toggle — on the markdown toolbar, a code field, or the Settings prompts — grows the editor out of its field, but the dimming sheet behind it snapped in ahead of the growth and stayed fully dark until well after the window had shrunk back. It now fades in with the opening window and fades out alongside the closing one.
      • The narration editor now settles in place after its full-screen toggle instead of blinking out and reopening. Growing the Edit Text window to full screen, or shrinking it back, ended with the whole window replaying its opening zoom-and-fade — it vanished for a beat and zoomed back in. The resize now ends where it lands.
      • The full-screen window now visibly grows out of its field and shrinks back into it as a solid, bordered panel, revealing the finished editor with a single fade. The window used to fade its live contents in and out mid-flight, which at real speed read as the editor vanishing at the click and popping back when the animation ended — and the traveling window itself was invisible, a background-colored box moving over background-colored panels. The window now wears a border and shadow while it moves and travels over the dimmed backdrop, its contents covered by a solid sheet. Opening, the sheet fades away once the window lands; closing, the docked view returns instantly underneath, the window shrinks onto it, and then fades away to reveal it.
    • AI Context:
      • Search now marks your term where it stands in the request and steps you through the mentions, instead of replacing the page with the matching lines. Typing a term used to rewrite the whole view: matching lines survived, everything else collapsed to ..., and the popup lost its own shape — the System Prompt and Messages regions, the chat stagger, and every dictionary highlight went with it, because those highlights point at the real text. The term itself was never marked, so you found the line and then re-read it by eye, and there was no way to see the sentences around a hit. Now nothing is rewritten. Each hit is marked in amber over the dictionary colors, and the one you are on is marked stronger and scrolled to the middle of the view. The search box carries a counter and up/down buttons on its right edge, and Enter and Shift+Enter step forward and back. A request with no hits folds shut and says · no matches, so the turn keeps its shape without the noise — click it to open it anyway, and clearing the search restores exactly the arrangement you had. Hits inside a collapsed section still count, and stepping to one opens it. The scroll bar becomes an overview ruler: one tick per hit on screen, the current one picked out, each clickable to jump straight there, and it stays visible while you search. Hits in the model's Raw Output are marked and navigable too, so a term can be traced from the request into the reply. Matching is unchanged — the current turn, case-insensitive, space-separated terms.
      • The popup now opens at the same near-fullscreen size as the in-game World Editor and shows every request word for word, in plain text, with the dictionary and hydration highlights as the only color. The popup used to be a step smaller than the editor, spent a full row on the words "AI context" and an Export button, and dressed each request in the anatomy hub's provenance coat — tinted template runs, dimmed context, label chips — which fought the highlights and got between you and the bytes. Export now sits beside the search box, the title and its icon share the row with the Dictionary and Hydrations buttons instead of holding one of their own, the freed space goes to the context itself, and reading who wrote what stays the Settings anatomy hub’s job. The raw model output now reads in the same face as the request instead of typewriter mono, and the request section is titled Raw Input to match Raw Output.
    • Model downloads now use your whole connection instead of one slow stream, so a model that took most of an hour can arrive in minutes. The desktop app pulled a model down over a single connection, and Hugging Face caps one connection at roughly 10 MB/s however fast your line is. The download now runs several connections at once and adds another only while the total keeps climbing, up to eight, so a fast line finishes fast and a modest line stops adding connections once it is full. There is no setting to tune — the right number is found on its own. Everything you see is unchanged: the same progress bar, the same toast, the same pause. Pausing still keeps what arrived, including across an app restart, and a part-downloaded model from an older version still counts toward the new download instead of starting over. A file only takes its final name once every piece of it is on disk, and a server that refuses parallel requests simply downloads the way it did before.
    • The desktop AI setup popup is rebuilt as one flow: a sorted model list that scrolls, a download you can close, and a clear finish. Show all models used to dump every model into one flat list that grew past the screen and clipped its own buttons. The list now sits in its own scroller and is grouped by fit against your graphics card — Best for Your GPU, Also Fits, and a dimmed Too Big for Your GPU you can still download from. Every row carries the model's one-line description beside its size, the recommended pick is badged in place instead of repeated above, and models that think before they answer are marked Reasoning. The popup also names the card and memory it detected, so you can see the recommendation is for your machine, and the expanded list folds back to the single recommendation. Starting a download no longer locks you in: close the window and a toast in the usual top-right corner keeps the model name and byte count updating. When the file lands, the popup says Loading your model through the minute the engine spends reading weights, rather than silently returning to the model list. Setup then ends on a short You're ready step with one button, or on a completion toast with Open Settings if you closed the window. The old line promising the game "starts on its own" is gone everywhere, because it never did.

✅ 2.15.2 — Released 2026-08-27 — The desktop engine loads models onto one graphics card instead of pooling every card's memory — so machines with an integrated GPU beside a discrete one work again — a GPU Device picker makes the choice yours; and every setting in the Display, Output, and Data tabs states its effect in one line, with a tip that defines the concept (click to expand)

Minor Changes

🔧 Fixed

  • 👤 User-facing
    • Models now load on a machine with a discrete graphics card beside an integrated one, and a new GPU Device row lets you say which card the engine uses. The engine counted every graphics device as one shared pool of memory. On these machines the pool reported tens of gigabytes total and zero free. Every model then failed to fit, at every setting, while the real card sat idle. A driver update can switch the integrated GPU on, which is why working machines broke "randomly" and why a reinstall never helped. The engine now uses one card and sizes each load against that card's real memory. Auto picks your discrete card and ignores integrated graphics. It shows the card it picked right in the dropdown. On a machine with one card, or with several real cards, Auto changes nothing, so a rig that splits a large model across two GPUs keeps doing that. The new GPU Device row in the Local Model settings lists every card the engine can see. Pick a card by name to overrule Auto, or pick All GPUs to keep every card available for splitting. The row says so plainly when there is no GPU to pick. Your choice is saved across launches. If your chosen card is later gone, the engine falls back to Auto and says so. The Engine device line above the row names the card in use and who chose it, so a wrong pick reads off a single screenshot. Changing the device reloads the model onto the new card without an app restart.
    • Every setting in the Display, Output, and Data tabs now states its effect in one line, with a ⓘ tip that defines the concept in detail. Tips that only repeated their own line are gone, and the System Prompts row gained one. Two Paragraph Limit notes claimed a capped reply can end mid-sentence; a capped reply ends at the last full sentence, and the notes now say so.

✅ 2.15.1 — Released 2026-08-26 — The embed-blocked guidance tells the truth about a popped-out tab's storage: it starts fresh, export/import carries worlds over, and the desktop app is the direct route for local models (click to expand)

Minor Changes

🔧 Fixed

  • 👤 User-facing
    • The embed-blocked guidance now says a popped-out tab starts with storage of its own — export and import carry your worlds over, and the desktop app is named as the direct route for local models. The 2.15.0 wording promised that saves and settings follow into the new tab because the address is the same; browsers partition an embedded page's storage under the site embedding it, so they don't — the tab starts fresh. The setup gate and the connection guide now say so plainly, point at export/import for anything worth keeping, and note that a cloud endpoint works inside the embed and the desktop app connects to local servers directly.

✅ 2.15.0 — Released 2026-08-26 — Six bundled worlds arrive rebuilt or brand new with connected maps, starting traits, and lore that changes every run, the Request Anatomy shows every request a turn sends and fronts every prompt in Settings, the desktop engine moves into its own process so the window stays smooth — with a model load bar and a graphics device readout, ==highlight== syntax colors prose everywhere it renders, playing embedded on another site names the browser permission blocking your local AI server and opens the tab that fixes it, and the Windows download no longer trips Defender (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing
    • Default Worlds:
      • Valentines Survival is rebuilt from a single room into a full game — a connected school to escape through, named characters, starting traits, and lore that changes every run. The premise is unchanged — escape school on Valentine's Day before the chocolate-armed crowd catches you — but the single room is now a school you can actually run through: seven connected places from Classroom 2-B to the Front Gate, named characters including the fan club president and a whistle-carrying PE teacher guarding the exits, ten starting traits across three groups that shape each run, a lorebook of school rules and rumors, and a readme that explains how to survive. Your speed now reads out live from your stamina and sugar, and each playthrough rolls a different best friend, front-page rumor, and guard post. Your existing copy updates itself on next launch — unless you've edited that world yourself, in which case your version is left alone as always.
      • City Rampage now razes a whole city instead of a single block — five districts to level, a military that visibly escalates, a rival mech held in reserve, and a different codename, origin, and secret weakness every run. The premise is unchanged — you are the colossal monster, and maxing out Destruction wins — but Neonspire is now five connected districts from the Harbor to Fort Bastion, defended by a military that escalates in tiers, with General Vasquez addressing you over the city PA, a news chopper narrating your rampage live, and the experimental mech Aegis-One waiting in its hangar for the moment the city is nearly lost. Four new mutations join the kaiju builder — a breath weapon, regeneration, wings, and a Berserk State you can toggle mid-rampage — alongside a lorebook of city doctrine and readmes that explain the monster picks and the rampage itself. Your existing copy updates itself on next launch — unless you've edited that world yourself, in which case your version is left alone as always.
      • Veilwood joins the bundled worlds — an eerie forest survival: forage glowing mushrooms, outwit two predators by learning their blind spots, and escape toward the one place the canopy breaks. Lost in a drowned forest under a closed canopy, you manage hunger, stress, and poison with five mushrooms whose effects are fixed and learnable — one of them a beautiful lie. Two hunters rule the wood, each with a weakness the wood itself hints at: a raptor that cannot climb, and a blind crawler that hunts by sound. Six connected places lead east to the Veilbreak, ten starting traits across three groups shape each run — including a Move Quietly toggle for slipping past what's listening — and every playthrough rolls how you got in, whose abandoned camp waits ahead, and one strange detail the wood won't explain. (A copy of the original Veilwood from long ago still in your library updates itself on next launch — unless you've edited it, in which case it's left alone as always.)
      • Sugarscape Survival joins the bundled worlds — a comedy survival where a hunger curse rides you through a realm made entirely of dessert, and overeating rather than injury is how you lose. Craving climbs on its own and only eating quiets it; Fullness is the price, because overstuffed means waddling and waddling means caught. Four connected regions lead from the ice cream hills of Sundaelora to the gray Stalefront where the sweetness ends and crossing out wins. The gummy bears, peppermint pouncers, sprinkle swarmers, and cookie brawlers are all edible with fixed effects, salt boulders offer craving relief at no cost but pride, mute gingerbread wayposts point the way out, and ten traits across three groups — including a Gritted Teeth toggle for white-knuckling the curse — shape each run, which also rolls how you were cursed, what the curse whispers about, and one odd landscape detail. (A copy of the original Sugarscape from long ago still in your library updates itself on next launch — unless you've edited it, in which case it's left alone as always.)
      • Slime Outbreak joins the bundled worlds — an eerie urban survival where you're already infected, the change is a countdown, and the cure waits at the derelict chemical works where the outbreak began. Assimilation climbs every turn and spikes when slime touches you; the middle of the change grants strange advantages while the far end erodes your grip, outline, and memory, and pharmacy remedies, salt, and a Fight It toggle push it back. Fog-bound Ashburn — the same city as Valentines Survival's school, whose gym now serves as an evacuation shelter — spans five connected districts patrolled by a quarantine that detains anyone who looks changed, populated by green slimes that recoil from salt, a serenely half-assimilated neighbor, and the vast calm Bloom at the source. Ten traits across three groups shape each run, which also rolls the neighbor, the outbreak rumor, and the broadcast's sign-off line. (A copy of the original Slime Outbreak from long ago still in your library updates itself on next launch — unless you've edited it, in which case it's left alone as always.)
      • Reincarnated Drone now starts each run in a region you choose or roll — and the chassis is a real either-or choice. Ashfall Capital, the Scavenger Wastes, and the Demon Frontier are all starting regions now, connected for travel, so where the drone boots up is where the story actually begins rather than a line in the prompt. Assault, Scout, and Siege frames sit in one pick-one Chassis group with their module sets nested beneath it, an introduction explains the builds before you choose, and the drone's story opens on its own boot sequence. Its cast also answer to their nicknames — the Archon, the broker, the captain, the dragon. Your existing copy updates itself on next launch — unless you've edited that world yourself, in which case your version is left alone as always.
    • Request Anatomy:
      • The AI Context viewer now shows every request a turn sends as an anatomy — your own prompt text highlighted, named, and clickable, everything the app assembled muted beneath it. The request splits into two headed regions, the System Prompt and the Messages, with the conversation staggered so a reply reads apart from the action it answers. Each stretch that came from one of your prompt fields is highlighted and labeled with the field it came from — System Prompt, User Message, Recap, Now, Recall, Direction — and clicking it opens Settings on that field, so noticing a problem in a real request and fixing it is one click. Every other stretch says in plain words what it is, from "world data from your chips" to "the narration that answered it, word-for-word". The planner, the director, each character, the storyboard, the choices, the stat updates, the location check, the summary, the diary, the clock and the scene tags are all labeled this way, not just the narration. A run that isn't there on a given turn is the condition demonstrating itself: no recap on a short game means no recap text to find. Dictionary and hydration highlighting keep working over the top, turns captured before this update read exactly as they did, and nothing about what is sent to the AI has changed.
      • Selecting any prompt in Settings → Prompts now opens on its Anatomy — the whole request that prompt belongs to, drawn on an example playthrough under your own settings — rather than on a wall of template text. Every prompt has one, so the planner, the director, a character, the storyboard, the choices, the stat updates, the location check, the summaries, a diary, both clock prompts and the scene tags each explain themselves the way the narration does. Context is folded down to a plain-words explanation plus a one-line excerpt, so the whole request reads in one scan and your own text is the only thing shown in full — and clicking your own text opens the field it came from, including the right one of the stacked narration messages. The editors are still listed under the prompt in the rail, and clicking the prompt itself brings the map back from any of them. What is drawn is what your settings really send: the location prompt shows the request your detection mode sends and not the other one, the character and diary prompts show one cast member's request and say one is sent per character present, your Thinking mode decides whether the narration carries the User Message and the Direction rider, and your section style, markdown output, paragraph limit, reply length and AI Language all read through into the system prompt. The example playthrough is shown at its fullest — the recap band, a Scene Recall hit, a bracketed action — with anything your settings rule out simply absent, and with the in-world clock on the condensed memories carry the times they happened at. Editing any field shows up here immediately, because the view runs the same code a turn does rather than a description of it.
    • The World Doctor now tells apart a placeholder you forgot to place from one you never used — a placeholder a trait pins but no text carries is its own warning, naming both the placeholder and the trait. A trait pinning a placeholder to a value is wired-in intent: that value was meant to show up somewhere. But when no text anywhere carries the placeholder's chip, the pinned value never appears — and until now the pin alone counted as "used", so the Bench said nothing about it. That case is now a warning of its own, with the placeholder and every trait pinning it each a click away on the row — each trait labeled with what it pins, so a row listing several placeholders still reads unambiguously — and no Fix button, because only you know where the chip belongs. The quieter "defined but never used" note now keeps to placeholders nothing references at all, so its one-click cleanup can never delete a placeholder a trait's pin still counts on.
    • Wrapping text in ==double equals== now highlights it, anywhere the app renders formatted prose — comments, feedback, world descriptions, readmes and the story's own narration. It is Obsidian's syntax, so notes pasted from a vault arrive already highlighted. The plain highlight is drawn from your active theme's own color, so it belongs to whichever of the eight palettes you play on in light mode or dark rather than being a fixed yellow, and a single letter between the first pair of signs picks one of nine colors instead — =r=red==, =g=green==, =b=blue== and so on, with a letter outside the set falling back to the themed highlight rather than breaking. A highlighter button joins the formatting toolbar above every prose field, with a chevron beside it that opens all ten colors as swatches — so the palette is there to be found rather than something you had to already know the syntax for. The button remembers the color you picked last, pressing a different color on an already-highlighted phrase recolors it in place, and pressing the color it already has takes the highlight off. A new Text Formatting page in the wiki lists the whole supported set — emphasis, strikethrough, superscript and subscript, lists, tables, code, links and highlighting with its color-key table.
    • The Narration prompt's Messages now say when each one is sent. Each message in Settings → Prompts → Narration → Messages carries a line above its editor stating the condition it rides under — the Recap and Now Messages once Memory Summaries has condensed older turns, the Recall Message when Scene Recall brings an old turn back, the Direction Message on actions with [square brackets] — and each ⓘ now explains what the message does in the exchange instead of assuming you had read the AI Context.
    • Built-In Engine:
      • Loading a model now shows a progress bar filling from 0 to 100%, instead of sitting on "loading…" until it finishes. A large model takes the better part of a minute to read in, and until now nothing on screen moved during it — a slow load and a hung one looked identical. The bar sits under the engine status line, which names the model and its percentage, both driven by the engine's own progress rather than a guess. Until the first percentage arrives the bar sweeps rather than sitting empty, and both disappear once the model is ready.
      • The desktop app's GPU memory box now names the graphics device the built-in engine actually chose, directly under the bars showing what your system reports. The line gives the backend it is running on — CUDA, Vulkan, Metal, or CPU — the device that backend picked, and how much video memory was free at the moment the model loaded. When that disagrees with the card listed above it, you have the answer to a model that says it will not fit while your graphics card sits nearly empty: the engine fell back to a different device, which commonly happens after a driver update leaves CUDA unusable. A load that fails keeps the line rather than clearing it, so the error and the device that produced it read together, and a fallback all the way to the CPU is called out in warning color.
  • 🛠️ Developer tooling
    • npm run desktop:debug builds the desktop app with the dev router left in, so window.__fmDev.goto(…) works there. The shell loads dist/, and every other desktop script builds it in production mode, which strips the DEV hooks — leaving the desktop build, the only one where the bundled engine exists, as the one place the dev router could not reach. Verifying anything engine-related meant driving raw CDP against a minified bundle. The script also turns the responsiveness meter on. Its output is unminified and carries DEV-gated code, so a packaging build still needs npm run build first.
    • npm run desktop:perf runs the desktop app with a responsiveness meter, turning "the app feels sluggish" into numbers. Every two seconds it prints one line covering the four things that can each make the app feel slow and which are not interchangeable: the main process's event loop (also the window message pump), dropped frames and the worst frame gap, the renderer's main thread blocking, and how long a real mouse or key event took to reach a JS handler — the last being what sluggishness actually is. Windows that breach a threshold are marked <-- SLOW, each line says whether the engine child was alive, and quitting prints the worst of the run. Off unless the flag is passed, and its renderer half is injected rather than bundled, so a normal build carries none of it.
    • An embed harness fakes the itch.io iframe locally, so the embed-blocked guidance can be tested before deploying. testing/embed-harness/index.html, served on 127.0.0.1, embeds the deployed build three ways — the itch-style iframe with no permission delegation, the fixed embed with allow="local-network-access", and a dev-server variant for UI-only checks. Pairs with Chromium's --ip-address-space-overrides switch to put any locally served build behind the real local-network permission gate.
    • A world-narration probe checks a bundled world's authored voice against a real model. testing/baseline/harness/world-narration-probe.mjs assembles the real narration system prompt from an actual src/defaultworlds/ JSON — resolved placeholders, default traits, rolled starting location, lore book — and chains scripted turns, auto-flagging per-world tone and mechanic violations (gore in the comedy worlds, premature Aegis-One, the drone speaking, the blind crawler credited with sight, dessert creatures speaking, intimate rather than clinical transformation prose) alongside the shared red flags.

🔧 Fixed

  • 👤 User-facing
    • Hovering a shortened item name in the World Doctor's findings now shows the full name. A name too long for its row used to cut off with an ellipsis and no way to read the rest; every clickable name on a finding now carries the whole text as its hover tooltip.
    • Playing the game embedded in another site — itch.io, say — now tells you the embed is what's blocking your own AI server, and offers a one-click Open in a New Tab that fixes it. Browsers now ask your permission before a page reaches a server on your machine or local network, and an embedded page is never allowed to ask, so LM Studio and Ollama connections simply failed there while the app said to check a server that was working fine. The setup window now names the real cause and opens the game in its own tab, where the browser asks you properly — the same address, so your saves, settings and endpoint come with you, and the game continues on its own the moment it connects. The connection guide gained the same step at the top of its checklist, and everyone pointed at a local server now gets a note about that permission prompt and how to re-allow it if it was dismissed.
    • The thinking section above the narration now stays collapsed once you collapse it, instead of reopening every turn. Closing the reasoning model's scratchpad used to last exactly one turn — the next turn mounted a fresh section that opened itself again while the model thought. Your last click on its header is now remembered across turns and across sessions: collapsed, future turns keep the pulsing "Thinking…" header with the text tucked away; expand any thinking section again and they open themselves as before.
    • The desktop app stays responsive while the built-in engine works — loading a model and playing a turn no longer freeze the window. The engine ran inside the same process that drives the app's window, so its work stalled the interface: loading a model froze it in bursts of up to four tenths of a second, and every AI request paused it again while the model's chat template was re-read, up to ten times in a single turn. The engine now runs in a process of its own and reads that template once when the model loads, so scrolling, typing your next action and opening panels stay smooth throughout — measured on an RTX 4090, the longest freeze during a model load went from 410ms to 2ms, with no change to how fast replies start. An engine that crashes now reports an error you can load past rather than taking the app down with it, and stopping the engine ends its process outright, which is what actually hands your graphics memory back.
    • The Windows download is no longer flagged as a virus by Windows Defender. Defender's machine-learning scanner was misreading the launcher in the portable Windows zip as a trojan and quarantining it on some machines. The launcher is now built in a way that no longer trips that detection — verified against Defender and the other major scanners on VirusTotal. If Defender already quarantined your copy, redownload the newest release; nothing on your machine was actually infected.
  • ⚙️ Backend
    • The desktop app's built-in engine is updated to node-llama-cpp 3.20.0. The update carries a newer llama.cpp, fixes a crash that could hit when unloading a model, and adds support for newer model families such as Muse Glimmer.

✅ 2.14.0 — Released 2026-08-24 — Community events arrive: contest banners and posters, a Contest tab with podiums and archives, and an admin calendar to run it all — community content can be reported to staff, published listings carry an update history you write, comments become markdown you can edit, a placeholder's values can be whole paragraphs, stats name the band their value sits in, a world's custom prompts show what the author changed, and NovelAI can draw your images (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing
    • Community Events:
      • A banner on the main menu and in Community Creations announces a running contest or announcement, naming it and the dates it runs between. Clicking the card takes you to the event — a contest's entries, an announcement's own card — as often as you like. Dismissing it collapses it to a small chip that folds into the top bar already there, keeping the event one click away at no cost in space. When a contest and an announcement are running at the same time both are shown, the contest first, each is dismissed on its own, and their chips stand side by side.
      • A poster appears once when an event starts and once when it ends, and closes only when you say you have read it. It waits for the first-run welcome animation to finish, so the menu is in front of you before it asks for your attention, and arrives immediately when there is no animation to wait for. The ending reaches you whenever you next open the app, so a contest that closed overnight still tells you judging has begun rather than passing you by, and a contest that has been judged names the world that took first place. Signed in, reading it also clears the matching message from your inbox; signed out, the device remembers on its own.
      • The organizer's own color and artwork carry the poster, on its opening and its ending alike, and the rules dialog opens under the same header band so the rules are visibly the same event. The text over the band follows the color, so a pale pick reads as clearly as a dark one, and an event styled with neither keeps the app's default band. What the organizer wrote in the details and the rules reaches you formatted, not as the symbols they typed.
      • A Contest tab in Community Creations gathers every entry in one place, with the contest's dates and a Rules button above the grid. It appears while a contest is running and stays afterwards for the archives. The contest's own banner card steps aside on that tab — its way in leads where you already are — while announcements keep showing.
      • Entries are freshly ordered on each visit while the contest runs, then stand by likes once judging starts, with the podium held at the front in the order it placed.
      • A contest awards first, second and third place. Once its results are announced the Contest tab carries a podium band above the entries naming every place that was awarded, in order, so results read like results. A small contest is not made to invent three winners — a contest can be decided on first place alone, or on first and second.
      • A place badge follows the world everywhere it is shown. It says which place and which contest — 1st, 2nd or 3rd — in gold, silver or bronze, so the placing is readable by color and spelled out in words both. The card wears it in the ordinary catalog as well as in the contest, the details window that card opens says it too, and a copy you have downloaded wears it in My Worlds — in either layout, in its details, on every copy you keep, and however much you have edited it since. A world that has placed in more than one contest names them all, one line each, newest first. Publishing a world now links your copy to its listing, so a contest it goes on to place in shows up in the library you actually work in without downloading your own work back. Nothing about a placing is stored on your device: it is read from the community server each time, so results announced long after you downloaded appear on their own, and offline your worlds simply look the way they always did.
      • A dropdown in the contest bar reaches past contests once more than one has been run; the running one is what opens by default. Past contests are grouped by the year they ran, newest year first, with anything still going on — the one taking entries and any waiting on its results — pinned in its own section above them, so the list reads as a calendar rather than a wall of titles however many contests there have been. Nothing is hidden or dropped: every contest ever run is still in there, and typing a title still jumps to it.
      • An Events tab in the admin panel lays the calendar out by where each event stands — a card for anything happening now, then what is scheduled, then what is over. One form authors both kinds: pick Contest or Announcement at the top of it, fill in the title, the banner line, the details players acknowledge and the window, and a contest also takes the rules entrants agree to. Starting it posts its pinned announcement by itself, so the broadcast composer has nothing new to do. Canceling asks first and says what it undoes; an event nobody was told about can be deleted outright instead. The Past group shows the ten most recent finished events with everything older one Show Older press away, so the section you check after a contest ends is at the top of the group rather than under years of cards.
      • The event form writes its details and rules in the same markdown editor world prose uses, with the same toolbar and the same preview, so an announcement can carry headings, lists and emphasis and can be read before it is saved.
      • The event form picks its start and end in a calendar that matches the app, with the day and the hour set separately, in place of the browser's own date popup. The month and the year are dropdowns rather than somewhere to page toward, each reaching ten years either side of this one, so a date a few years out is two clicks rather than twenty. The hour opens a picker of its own beside a field you can still type into: hours, five-minute marks, and morning or afternoon where the clock is written that way — every click landing straight away, so an hour, a minute and a half of the day are one visit rather than three. Naming an hour before choosing a day puts it on today.
      • An event's poster can be given a color and a picture, previewed live in the form as the band players will see. Both are optional, an event with neither keeps the default band, and removing the picture deletes it from the server with the event.
      • The poster's picture can be dragged and zoomed into place. Picking a picture opens a positioning window — the same flow as your profile picture — showing the real band: drag the artwork to choose what it shows, zoom with the wheel, a slider, or the plus and minus buttons, and Save to keep the framing or Cancel to leave it as it was. A Reposition button reopens that window any time, including when editing an event later, without choosing the file again. Your picture is never altered — the framing is remembered as a position, so the part you chose stays in the middle of the band at every shape it is drawn at, on a wide screen and a narrow one alike — and the artwork can never be dragged far enough to show blank space. Picking a different picture starts it centered again, and Reset recenters.
      • The podium is assembled by looking at the entries. A gallery of every entry in the contest opens from its card; clicking an entry gives it the next free place, clicking it again trades it with the place below, and clicking it on the bottom step takes it off. The three places fill from first down and can never be left with a hole in them. Announcing publishes the whole podium at once — one message naming every place, the contest decided, and entries unlocked — and the announcement that would go out is shown before anything is sent. Your own entries and anything quarantined stay in the grid wearing the reason they cannot be placed, rather than quietly going missing.
      • An announced podium can still be corrected. Reopening it from the contest's card loads the places already published, and saving a change quietly fixes the record without sending anyone a second message. Every correction is written to the audit log with who made it.
      • Moderators reach the Events tab as readers. Announcing a contest's results speaks to every player at once, so it stays an administrator's alongside scheduling, editing and canceling, and simply isn't shown to anyone else.
      • Publishing a world while a contest is running offers a switch that enters it, naming the contest, the day it closes, and a link to its rules. It appears only for a world you are publishing as new, since entering happens at publish time — and if you already have an entry, the card names it instead of offering the switch, so a publish is never spent on a request the server would refuse.
      • The listing that holds your contest entry is marked in the publish window's update list, and picking it keeps the contest card on screen saying the update keeps the entry — so updating an entered world never leaves you guessing which listing it is.
      • An entry can be withdrawn from the contest, from that card in the publish dialog and from your own entry's card in the Contest tab. It asks first, the listing stays published with its likes and comments, and the switch is armed again straight away so you can enter something else. A world that took first, second or third place can't be withdrawn, and says so.
    • World Comments:
      • A comment you left on a community listing can be edited, for as long as you want it edited. A pencil beside your own comment swaps it for the same editor you wrote it in; saving puts the new wording in place and marks the comment edited, so anyone who replied to the older version can see the words moved under them. Nobody else gets that pencil — not the world's author, not a moderator, not an administrator — because moderating a comment has never meant rewriting it.
      • A comment can be deleted by the person who left it, by the author of the world it sits on, or by staff. It asks before it goes, the count above the thread follows it down, and the rest of the thread is untouched. Staff deletions are written to the audit log with a snippet of what was removed, and the usual rule holds: staff moderate ordinary accounts, administrators also reach mods and devs, and nobody reaches an administrator.
      • Comments are written and read as markdown. The box you type in is the app's markdown editor — the same toolbar and the same preview tab as everywhere else — and comments render formatted rather than showing the symbols you typed. Every comment renders that way, including ones written before this. There is room for 4000 characters now instead of 1000, which is what a code block or a list needs to be worth posting.
      • A world under quarantine still lets the people who commented on it edit or delete their words. Nothing new can be posted while it is out of circulation, but quarantine was never meant to trap somebody else's comment there.
    • Listing Changelog:
      • A published listing can carry an update history you write yourself. Each entry has a title you choose, a date you set, and a body written in the app's markdown editor — the same toolbar and preview as everywhere else. Entries are shown newest first, under the day you gave them. Worlds, characters and dictionaries all take one.
      • The details window switches between Changelog and Comments. A listing with no changelog looks exactly as it always did — no switch, no empty tab — and its author still sees the switch, with an Add Entry button waiting in it, so the way to start a history is where the history will appear. Anyone whose downloaded copy has an update waiting arrives on the changelog instead of the comments, since "what changed?" is the question in front of them.
      • Publishing an update offers to say what changed. The ask appears only when you are updating a listing you already published, never on a first publish, and it is optional throughout — a one-line fix does not have to be written up before it can go out. What you write is held until the upload succeeds, so a refused publish costs you nothing you had typed.
      • Entries can be added, rewritten and deleted at any time, from the changelog itself. That includes backfilling a listing you published long ago: set each entry's date to the day the update actually happened and the history reads as the history it is. Fixing a typo in an old entry never resurfaces your listing as freshly updated, and never flashes Update Available at anyone holding a copy.
      • A listing whose contest is being judged still takes changelog entries, the way it still takes comments and likes. The judging lock holds the work still; a note about the work is not the work.
      • The changelog stays out of what you download. It is information about a listing, not part of the world, so a copy you download and anything you export from it carry only the world itself.
    • Placeholders:
      • A placeholder's values can be written as paragraphs, in the same markdown editor the readme uses. A Chips | Multiline switch sits at the top of the values section: Chips is the row of short values it has always been, and Multiline gives each value its own box with the formatting toolbar, an Edit/Preview pair that renders your markdown, a draw weight and its resulting roll chance in the box's header, a remove button, and an Add Value button under the lot. A placeholder holding a value with line breaks opens in Multiline on its own; one holding only short values opens in chips exactly as before, and switching between the two never changes a value. Every place placeholders are edited has it — the World Editor's Placeholders tab and the character and dictionary library editors alike.
      • A long value's box folds away to its first line. The chevron beside Value 1 collapses that box on its own, and a button in the values header takes every box down or back up at once, so a placeholder holding a dozen paragraphs is still something you can read down. A folded box keeps its weight, its chance and its remove button, so odds can be tuned without opening anything.
      • A value holding line breaks reads as its first line wherever one line is all there is room for — its chip in the chip row, the tooltip on a placeholder chip in world text, and the blurb on a library card. What the story and the AI receive is unchanged: the whole value, line breaks and all, exactly as you wrote it. A chip standing for such a value opens its draw weight as it always did, but is no longer renamed in place — a one-line field would quietly flatten the paragraph behind it, so its text is edited in the Multiline view.
    • Stats:
      • Each stat now says which band its value falls in, under its bar. If the world's author wrote descriptors for a stat — Winded, Flush, Freezing — the one your value currently sits in is named in a quiet line beneath the bar, so you can read the status the narrator is being told about you instead of inferring it from a number. It updates as the value moves, and brightens for a moment when a turn, or paging back through history, pushes a stat into a different band, so becoming Winded is visible without reading every row. A value the author left no band for shows nothing rather than a placeholder, and a long descriptor is trimmed to one line with the whole of it on hover. A world whose stats have no descriptors looks exactly as it did.
      • A stat's value can be typed in when you edit stats. The number beside the bar becomes a field: type the value you want and it applies as you type, so the bar, the band and your character's body follow each keystroke the way they follow the slider. Anything outside the stat's range is pulled back to the nearest end as you type, clearing the field leaves the stat where it was until you type something else, and the slider is still there underneath for a rough sweep. On mobile it opens the number keypad rather than the full keyboard.
    • Community content can be reported to staff. A Report button sits on a listing's details, beside every comment that isn't yours, and under a user's profile. Pick what's wrong with it — illegal content, hate or harassment, spam or scam, stolen content, malicious content or links, or something else — add a line if the reason alone doesn't say it, and it goes to staff. Your name never reaches the person you reported. Reporting the same thing twice while staff still have the first one says so plainly instead of filing a duplicate, and once a report is closed you can report that thing again if it breaks the rules afresh. When staff have looked at it you get a message in your inbox saying whether action was taken or nothing was found, with a note from them when one helps — the specific thing they did stays between them and the author.
    • A world's custom prompts now show what the author changed, not just the finished text. The View button on the custom-prompts notice opens each rewritten pass as a marked-up document: what this world added to the prompt Formamorph ships is tinted green, what it dropped is struck through in red right where it used to be, and everything untouched reads as normal. The dialog's own explanation shows you which color is which, and a small Raw switch beside the title still gives you the plain text exactly as authored. Placeholder chips are never cut in half — a chip that changed is swapped whole. Every opening starts on Changes, so a world that rewrote one line no longer looks like a world that rewrote everything.
    • Number fields across the app no longer carry the browser's gray up/down arrows. The tiny native spinners never matched the app's look and were too small to be worth clicking anyway; typing, the arrow keys, and scrolling the wheel over a focused field all still step the value.
    • NovelAI can now draw your images. Pick NovelAI as the image provider in Settings, paste the persistent API token from your NovelAI account, and scene pictures and editor artwork come from the model you already subscribe to — no local image server, no desktop app, straight from the browser. Choosing it sets the model, size and steps to what an Opus subscription draws for free (1024×1024 at 28 steps); going above either spends Anlas. Your prompt and negative prompt are translated into whatever the chosen model expects, so V3, V4, V4.5 and V5 all take the same text you already write. Stop frees the app up straight away, but NovelAI cannot call back a picture it has started, so that one may still cost Anlas. An expired token, an empty Anlas balance, and a second picture already being drawn each say so in words rather than failing as an unexplained error.
  • 🛠️ Developer tooling
    • The dev-router opens the event banner and acknowledge poster on a canned event#dev?view=mainMenu&modal=eventAck, with tab=start or tab=end picking the phase.
    • The dev-router opens the publish dialog on a canned world and a canned running contest#dev?view=mainMenu&modal=publish, so the dialog and the contest opt-in inside it are reachable with nothing published and no event running.
    • The dev-router opens the admin Events tab on a canned calendar#dev?view=mainMenu&modal=adminPanel&tab=events serves one event of every state, so all three groups and both role views are reachable without a live server.
    • The dev-router opens the Contest tab on canned contests#dev?view=mainMenu&modal=community&tab=contest serves one running contest and two archives, so the tab and its selector are reachable without a live event.
    • The browser suite covers the whole contest entry flow against a real server — sign in, publish a world with the entry switch on, find it in the Contest tab and the catalog, then have an admin announce a podium with that entry on it and check the place badge reaches the author's own library. It runs against a local FormamorphServer named by E2E_API_URL (which the runner also hands the dev server, so a test publish can never reach the live workshop), and skips rather than fails on a machine without one — the results half additionally needs an admin account and a contest nobody has decided yet.
    • The custom-prompts viewer is reachable from the dev router. #dev?view=mainMenu&modal=worldPrompts opens it on a canned world whose prompts carry every shape of edit — reworded lines, a dropped section, a new one — so the diff can be checked on a library holding no world that rewrites a prompt.
  • ⚙️ Backend
    • Community Events:
      • The events list is read once per session and shared by every surface that shows it, instead of being downloaded again each time Community Creations opens. It is refreshed in the background once what is held is a few minutes old, and dropped outright when staff create, edit, cancel or decide an event, so a podium announced mid-session reaches the archive and its badges without a restart. It is never written to disk: offline still means no archive and no badges.
      • A contest's places are stored one row each, on their own table, with the world's name and its author's snapshotted at the moment they are announced. One world per place and one place per world are held by the database itself, so no request can produce a podium with a world on two steps. A place keeps its snapshots after the listing behind it is deleted, so the archive still reads. Announcing is a single request carrying the whole podium — it validates every place against the same rules (an entry in this contest, not quarantined, not the judge's own), stores them, posts one message and stamps the contest as decided, so players never meet a contest that has announced first place and is still thinking about second. Correcting an announced podium is a separate admin-only request that posts nothing and writes one audit entry per place that actually moved. Any contest that had recorded a single winner before this becomes that world's first place, announced when it was picked.
      • The events list is fetched without the poster and rules text, which is around four fifths of what a contest row weighs, and the two screens that show that text — the end-of-contest poster and the Rules dialog — read the one event they are about in full when they open. The archive is permanent, so this is what keeps opening Community Creations as cheap after five years of monthly contests as it is today. Nothing depends on the community server having been updated: an older one answers with the whole rows it always did, and no further request is made.
    • World Comments:
      • A comment records when it was rewritten, on a column of its own. The row already carried an updated_at, but it is stamped when the comment is first written, so it could never tell an edited comment from an untouched one. The new column is added by the server as it starts, alongside every other schema step, and is never backfilled — nothing before this was editable, so an empty value is the truth rather than a gap.
      • The comment-edit route no longer answers to staff. It had always allowed a moderator through, and no shipped client ever asked it to, so closing it takes nothing away — it makes the server agree with the rule the rest of the app follows, that moderation removes a comment and never rewrites one. Deleting is unchanged: its author, the author of the listing, or staff who may reach the author.
    • Listing Changelog:
      • A listing's changelog entries are stored one row each, on their own table. Not columns on the listing, because the catalog sends the whole listing row for every card on a page — a changelog only one open listing ever reads would ride along with all of them. The table is created by the server as it starts, alongside every other schema step, and its entries go when the listing does.
      • A changelog write never marks the listing as updated. The listing's freshness date is what the catalog sorts by and what tells a downloader an update is waiting, so a typo fix in last month's notes must not move it — the same rule the spoiler toggle and the contest withdrawal already follow.
      • The changelog is fetched only when it is asked for. It rides on the single-listing request behind a flag, the way comments do, so browsing pages of cards costs nothing extra; and the download endpoint never serves it, so reading a changelog can never bump a listing's download count.
      • An older community server makes the whole feature invisible rather than broken. The client tells "this listing has no changelog" from "this server has never heard of changelogs" by whether the field arrives at all, so the app and the community server can be updated in either order.

🔧 Fixed

  • 👤 User-facing
    • The unread badge stays the theme's accent color for everything routine, including ordinary admin messages. Follows and feedback replies were already accent-colored, but any unread admin message — however mundane — turned the profile badge and the message row's unread mark blue. Now only a message sent as a warning or an urgent notice changes the color; everything else matches.
    • A notification's listing now opens even when the community browser hasn't caught up to it yet. The browser shows last visit's catalog first and refreshes it in the background — but a listing published since then, exactly what a "published a new world" notification points at, was looked up in that old list, declared "no longer in Community Creations", and dropped. The browser now waits for the refresh before giving a verdict: the listing opens the moment the fresh catalog lands, and only a listing genuinely gone still gets the notice. The same fix covers opening a listing from someone's profile, and closing the browser mid-lookup cancels the request instead of saving it up to pop open on a later visit.
    • Ctrl+Z answers right after a command from the locations map's right-click menu again. Picking Auto Arrange — or any other command — from the menu left the map deaf to the keyboard until you clicked it once more, so the arrangement you had just asked for couldn't be taken straight back. The menu now counts as working on the map, the way it did before it was rebuilt.
    • Renaming a placeholder's value carries every trait that pinned it. A trait holding Hair Color at "Red" used to keep forcing "Red" after you renamed that value to "Crimson" — a value the placeholder no longer offered, with nothing in the editor to say so, and the mismatch only showing up in play. The rename now reaches every trait in the world at once, and every pin within a trait. A pin you typed yourself rather than picking off the list is never rewritten, and reordering the values or swapping one out for a different one leaves all pins alone. Saved playthroughs pick the corrected pin up when you load them.
    • Your edits to a discovered character now survive re-generating and rolling back. Re-generating a turn or rolling back used to reset the story's discovered cast to how it looked when that turn ended — an edited description reverted, and the game would then spend a request re-describing a character it already knew. Both actions now keep the cast as you have it, dropping only characters introduced by the turns that were undone; re-rolling the very turn that introduced someone discards them, and if the fresh narration names them again they are discovered anew. A character you deleted stays deleted through it all.
  • 🛠️ Developer tooling
    • The test suite passes on a UTC runner. One test asserted the machine's own timezone sits west of Greenwich, one suite's service mock let an effect reject unhandled, and the first CodeMirror typing test could lose a keystroke under CI load — three failures CI hit that a westward dev machine never showed.

✅ 2.13.0 — Released 2026-08-19 — The Locations tab becomes a real map you arrange, wire and search — with connections, nesting by drag, auto-arrange, undo and a full-screen canvas; a Test Bench that finds what is wrong with your world and fixes what it safely can; traveling in-game shown as a nested tree or a clickable map; and a world can write the line its story opens on (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing

    • Locations:
      • Locations in the World Editor can be wired together with travel links that run one way or both ways. A location's editor now has a Connections section: pick a place, press Add Connection, and the story can move the player between the two. A new Connection is two-way; setting it Outgoing or Incoming makes it one-way in that direction, and an optional Travel Hint tells the AI how the trip is made ("through the shimmering portal"). One Connection is one link, so it shows on both locations' panels — flip it or delete it from whichever end you happen to be on. Connections used to be a list of other locations' names, stored on the location itself, editable only by hand-editing the world file: renaming a place silently broke every link pointing at it, a link was only two-way if both sides happened to name each other, and there was no way to build a drop, a chute or a portal that commits you — the free travel nesting hands out between a place, its sub-locations and their neighbors always won the trip back. A Connection between two places now replaces whatever free travel those two had, so a one-way link is genuinely one-way even between two sub-locations of the same place, and the story is never offered the trip back rather than being told not to take it. Worlds that never used connections navigate exactly as before. Existing ones convert on load and on import: two locations that named each other become one two-way link, a one-sided naming becomes a one-way link in that direction, and a name matching nothing is dropped — every location still reaches exactly where it reached before. Deleting a location deletes its Connections with it.
      • The Locations tab can show the world as a map instead of a list. A List / Canvas switch now sits above the locations, and the canvas draws every location as a box you can drag into whatever arrangement makes sense to you — a place holding sub-locations is drawn as a box around them, because being inside it is the free travel in and out, so no line is drawn for that. The free travel between neighbors inside the same place is drawn as thin dashed arrows, one per direction, and an authored Connection as a solid colored arrow per direction it can be traveled, with its Travel Hint written along it: a one-way link is read by counting one arrow instead of two. Any location no player can reach — nothing leads to it from any starting location, or a one-way link stranded it — is outlined in red and flagged. Clicking a box opens that location's full editor beside the map, and your arrangement is saved with the world, so it survives reloading, exporting and sharing. Nothing on the map ever moves unless you move it — a location you add lands at a clear spot below whatever its place already holds, and everything else stays exactly where you left it. Drag a location up or to the left past the edge of the box holding it and the box grows around it rather than pushing it back, with everything else inside staying visually still. Older worlds that were never arranged still open to a tidy layout, and it is the same layout every time. The list view is untouched and still does everything it did.
      • Dragging a location into a box on the map puts it inside that place. The canvas edited only where boxes sat; nesting a location under another still meant going back to the list and dragging a row sideways. Drop a location inside a place's box now and that place holds it — drop it out on open canvas and it goes back to being a place of its own. Which one a drag means is decided by where it came to rest, so there is nothing to hold down and nothing to switch: a location that lands in the box it started in has simply moved. Everything the map draws follows the drop as it happens — the free travel between the new neighbors appears as dashed arrows, the travel that nesting used to hand out disappears, and any location that just became reachable or stranded is re-flagged. Connections you authored are untouched by any of it: a link is between two places, not between two positions. The list view and the map are the same nesting seen twice, so a location dragged into a box on one is already indented under it on the other.
      • Connections are drawn on the map by hand. Drag from the dot on a location's edge to any other location and the two are linked, two-way, with nothing else to press. Click one of the thin dashed arrows and that free travel becomes a real Connection you can annotate — the same one gesture, from the other direction. Selecting a solid arrow opens a small panel on the map holding everything that link is: which way travel runs (both, or one way in either direction), its Travel Hint, and a button to delete it. Every change redraws the map as you make it, so narrowing a link to one way visibly removes the arrow coming back, and any location that just became unreachable is flagged the moment it happens. The map and the location editor's Connections list are two views of the same links — edit one and the other already agrees.
      • Locations on the map snap to a grid, so a hand-arranged layout lines up on its own. The canvas draws a faint grid and every drag comes to rest on it, which means two boxes placed by eye are actually level with each other instead of three pixels apart — no nudging, no zooming in to check. Right-click anywhere on the map for Snap To Grid and Show Grid: turn snapping off for freeform placement, hide the grid if you would rather read the map without it, and the two are independent, so nodes can keep snapping to a grid you cannot see. Both choices are yours rather than the world's — they are remembered on this device across sessions and go nowhere near the world you export, so a world you share carries your arrangement and never your working preferences.
      • Dragging a location on the map shows where it will land before you let go. Nesting was decided the instant you released the mouse, with nothing to look at while you aimed — over a stack of boxes inside boxes, the only way to find out which one caught it was to drop it and check the list. The box that would take the location now lights up while the drag is still in the air, and it is the innermost one under the cursor, so a place nested three deep is as easy to aim at as a place standing on its own. Drag a location out of the box holding it and the whole map is framed and labeled Top Level instead, because going back to being a place of your own is a real outcome and deserves a real target — while a location that already stands on its own is dragged around the map without either being announced, since nothing about it is changing. Boxes that can't take the drop — a location with nothing inside it, and anything already living inside the location you are dragging — never light up. What lights up is what catches it: the highlight and the drop are the same decision, asked twice. Dragging several locations at once lights up every box about to take one of them, so a move that carries one location into a place and another out of one says both things while it is still in the air.
      • The map is worked the way every other node editor is worked: drag a box to select several, move them as one, and right-click for a menu. Left-drag on empty canvas used to drag the whole map around, one location was all you could ever have selected, and right-clicking got you the browser's menu — so arranging a cluster meant moving each box in it separately, and there was nowhere for a per-location command to live. Left-drag over open canvas now draws a selection rectangle around everything it encloses, Shift or Ctrl with a click adds or removes one location at a time, and dragging any of the selected boxes carries the whole set — each one landing in whatever place it personally came to rest in, so a cluster dragged into a box nests all of it. Panning moved to the middle and right mouse buttons, which are free now that left-drag has a job. Right-clicking opens the canvas's own menu wherever you press: a location offers Edit Location, a multi-selection offers Clear Selection, open canvas offers Select All Locations, and the grid and snapping toggles are on all three. A right-drag pan never opens it on release. Esc clears the selection and Ctrl+A takes every location on the map. On a touch screen, where there is no Shift to hold, press and hold a location to add it to the selection or take it back out — a tap still opens its editor, and a hold that turns into a drag selects nothing. Panning works from anywhere, including with the pointer over a box. Clicking a single box still opens its editor beside the map exactly as before.
      • The map opens full screen for serious arranging, and the embedded view stays out of the way. A location canvas the size of a panel is fine for a glance and cramped for laying out a world, so the zoom controls now carry a full screen button that grows the map out to the whole window — the same window every other editor in the app opens into, with the same animation and the same Esc to come back. It is one canvas in two frames, not two canvases: everything picked, every arrow open for editing and every change already made travels with you both ways, so going full screen never costs you your place. The embedded map keeps only what a quick look needs — zoom, and the way to the big one — and the right-click menu is still there on both.
      • Right-click a place on the map and Auto Arrange tidies everything inside it into a compact block. Laying a world out by hand meant dragging every box into place one at a time, and a place holding a dozen sub-locations had no way back to a readable arrangement once it drifted. Auto Arrange now sits on the right-click menu of any box that holds something. Sub-locations you linked with Connections stand next to each other in the order they are traveled, left to right, with the arrows between them no longer crossing. Everything else — and each linked group as a whole — is packed into rows that wrap into a roughly screen-shaped block, so twelve plain rooms come out as a grid you can see at one zoom instead of a line running off the canvas. The packing follows the order of your locations list, and a linked group sits where the first of its places sat. Everything lands on the grid, so an arranged layout and a hand-dragged one line up. Only that box's own sub-locations move — a place nested inside it keeps whatever you arranged in it, and its frame simply resizes around it. Open canvas offers Auto Arrange All, which does the same for every place in the world and the top level together, working outward from the innermost so nothing ends up laid out around a box that then changed size. The result is yours like any other arrangement: it is saved with the world, and dragging a box afterwards is just a move. The same world always arranges the same way.
      • The arrows on the map can be drawn straight, curved or as right-angled elbows. A dense cluster of places is easier to follow as curves, and a tidy hierarchy reads better as elbows that turn at right angles, so right-click anywhere on the map and pick Straight Connections, Curved Connections or Elbow Connections. Straight is the default. The map redraws as you pick, and everything an arrow tells you survives the change: dashed for free travel and solid for a Connection you authored, one arrowhead per direction that can be traveled, the pair's two directions still side by side, and every Travel Hint still written on its line. The choice is yours rather than the world's — remembered on this device and kept out of anything you export. A Connection you author between a place and one of its own sub-locations is drawn frame to frame in every shape, from the sub-location's edge out to the surrounding box's edge on that same side, rather than aiming at either box's middle.
      • Ctrl+Z takes back the last thing you did on the map, and Ctrl+Y puts it back. Every edit made on the canvas was final the moment you made it: a drag that landed a location in the wrong box, an Auto Arrange that rearranged a place you had laid out by hand, a Connection deleted by mistake — all of them meant undoing the damage by hand, or reloading the world and losing everything else with it. The map now remembers what you did to it. Ctrl+Z walks back moves, nesting, arrangements, and Connections created, edited or deleted; Ctrl+Y or Ctrl+Shift+Z goes forward again. A drag that carried a whole selection comes back in one press rather than one per box, and so does an Auto Arrange, however many places it moved. Typing a Travel Hint is one edit too, not one per letter. The shortcuts only answer while you are working on the map, so they never fight the text field you are typing in. Saving does not end it — undoing past a save simply leaves the world with unsaved changes again — and the list view is reading the same world, so a nesting you undo on the map is already un-nested in the list. It lasts as long as you have the world open, remembers the last hundred steps, and starts fresh when you open a different world.
      • The full-screen map carries a minimap and a search box, so a big world stops being somewhere to get lost. A world of eighty places is a map you scroll blindly: nothing tells you which part of it you are looking at, and finding one location by name means panning until it appears. The full-screen canvas now draws a minimap in its corner showing the whole world at once with a frame around the part you are currently viewing — click anywhere on it to jump there, drag to slide the view around, and scroll on it to zoom. Beside it, a Find A Location box matches locations by name as you type: pick one and the map travels to it and marks it for a moment so you can see where you landed. Places nested inside other places are found just as easily as places standing on their own, and each match says which boxes hold it, so two rooms called Cellar are told apart at a glance. Arrow keys walk the matches and Enter takes the highlighted one. Both are drawn in the app's own colors in either theme, and both belong to the full-screen map only — the embedded view stays the quick glance it always was.
      • The full-screen map gets a toolbar, alignment tools and keyboard finishing moves, so a hand-made layout ends up as tidy as an arranged one. Laying places out by eye left every box a pixel or two off its neighbors, and the only way to true them up was more dragging. A toolbar now runs along the top of the full-screen map carrying Auto Arrange (the place your selection sits in, or the whole world when nothing is picked), the snap and grid switches and the connection style picker — the same choices the right-click menu offers, so changing one changes both. Pick two or more places and Align Left or Align Top brings their edges onto one line; pick three or more and Distribute Horizontally or Vertically spaces them evenly between the two on the ends. Both also sit in the right-click menu when you right-click a selection. On the keyboard, F frames what you have picked (or the whole world when you have picked nothing), and the arrow keys step the selection one grid cell at a time — a run of presses comes back in a single Ctrl+Z, and so does an alignment however many places it moved. The embedded map is unchanged: still just the zoom controls and the way to full screen.
      • Traveling shows the world as nested places or as a map you can click. The Change Location button now opens on two views instead of a dropdown. List puts every sub-location under the place holding it, indented by how deeply it sits, so a cellar inside a hall inside a castle reads as one — and clicking any row travels there and closes, a container location included. A place with a description carries it under its name, kept to a single clipped line so a wordy world still reads as a list, and the place you are standing in is colored apart from the row under your mouse. A place holding sub-locations carries a chevron that folds it away, and the list opens with only the branches that matter from where you are standing already open: the places holding you, and any branch holding somewhere you can reach in one step, whether by nesting or across a Connection from the far side of the world. Everything else opens folded, carrying a small badge counting the places it hides, so a world of eighty rooms opens as a short list of likely destinations with the rest one click away. The chevron is its own target, so opening a branch never travels you into it, and a branch you open stays open for as long as the dialog does. Where you are standing is always visible. Map is the world drawn the way its author arranged it on the Locations Canvas: the same boxes, the same nesting, the same travel arrows with their one-way directions, and the place you are standing in marked on both views. Click a box, or a containing place's name along the top of its frame, to travel there. The map pans, zooms and re-fits to the window, by fingertip as well as by mouse, and it is strictly something to read — nothing on it can be moved, connected or selected, and the author's own diagnostics (the starting-location star, the unreachable-place warning) stay out of it. A world whose author never arranged the canvas still gets a map, laid out the way the editor lays one out on first open. You can travel anywhere from anywhere: the arrows describe how the world is wired for the narrator, not where you are allowed to go. The dialog reopens on whichever of the two you traveled by last, and place names on both read through Placeholders like everywhere else in a playthrough.
      • The Locations picker on an entity lists places as a tree. Sub-locations sit under the place that holds them, indented by depth, in the same order and shape the game's own location list shows. The picker used to be the world's raw location array, where a cellar and a castle read as peers.
      • A location can be dropped onto another to nest it, and Undo and Redo are buttons now instead of a shortcut you had to know. Starting a hierarchy on the map used to be impossible: dragging a place into a box that already held sub-locations nested it, but dropping one onto a plain location did nothing, so the first sub-location under any place had to be made in the list view. Now hold a dragged place over another for a moment and the one underneath lights up; release while it is lit and it becomes a box holding what you dropped, right where you let go. Move away before releasing and nothing happens, so passing over places while tidying the map never nests them by accident, and a place can never be dropped into itself, into something it already holds, or into a place traveling along in the same drag. Drop a whole multi-selection and all of it nests together. Boxes that already hold sub-locations still take a drop the instant you cross into them, exactly as before, and any travel links between the two places are left alone — a nest is one press of Undo to put back. Undo and Redo now sit at the end of the full-screen toolbar and lead every right-click menu, grayed out when there is nothing to walk back, so the map's history is reachable without knowing Ctrl+Z works here — the embedded map, which has no toolbar, reaches it from the menu. The right-click menu is also ruled into groups now: history, then what you clicked on, then the grid switches, then the arrow shapes.
    • Test Bench:
      • The World Editor has a Test Bench that tells you what is wrong with your world while you are still editing it. A flask button in the editor header opens it — a third panel beside the list and detail on a desktop, a full-height sheet on mobile — and carries a count of everything currently wrong. The count is live: create a problem and it appears a moment later, fix it and it goes, and at zero the button says nothing at all. Inside, the Issues tab lists what was found, errors first, then warnings, then the merely untidy. Findings of the same kind are one row, not one row each, so fourteen bad aliases read as one problem naming the fourteen — and every name on a row is a button that takes you straight to that item, on its own tab, ready to edit. On mobile the sheet closes on the way so you land on the normal editor navigation. It checks alias hygiene — an alias beginning with the, a or an (alias matching is case-sensitive, so an articled alias misses wherever the sentence capitalizes it differently), two entities that can be matched by the same piece of text (plurals included, so the visitor and the visitors count as one), and an alias that just repeats its own entity's name (a lowercase alias of a one-word name is left alone — that one is doing real work, since a one-word name only matches where the sentence capitalizes it) — and everything that points at nothing or can never happen: an entity placed at a location that no longer exists, a trait that toggles a stat or pins a placeholder that isn't there, a placeholder chip whose placeholder was deleted, a chip sitting in a stat's description or descriptors — the one place chips never resolve, so they'd reach the AI as raw text — stat code that looks a stat up by a name no stat has, a dictionary entry that can never fire (no keywords and not constant, secondary keywords with no primary ones to gate, or a regex keyword that doesn't compile), a world where no location is flagged as starting (a new game would pick anywhere at random), the old isStartLocation flag from before 2.0 that the game no longer reads, an entity placed in no location at all, and a stat that starts disabled with no trait that ever enables it. A world with nothing wrong says No Problems Found and how many rules it checked, so silence reads as verified rather than broken. Nothing the Bench knows is written into your world, so a world you export or share is byte-for-byte what it was.
      • Findings whose repair is obvious now fix themselves. A finding that has exactly one right answer carries a Fix button on its row — Fix All when the row stands for several of the same problem — and pressing it repairs every instance at once: the article comes off an alias, an alias that merely repeats its own entity's name goes, a placement pointing at a deleted location is dropped, the dead pre-2.0 isStartLocation flag is cleared (carried onto the live starting-location flag first, when it was the only record of where the world begins), and a placeholder nothing anywhere uses is deleted. A fix is an ordinary edit made all at once — the world goes unsaved-changes just as if you had typed it, and Exit Without Saving still takes the whole thing back. The finding disappears on its own a moment later. Rows whose repair is a judgment call — which of two colliding entities should be renamed, which location should be the starting one, where an entity with no home belongs — never offer a Fix, only the way in to decide it yourself. There is no fix-everything button. The first fix on a world you downloaded and haven't edited says once that saving it marks your copy as edited.
      • The count on the Bench button tells you what is new, so a still badge is worth trusting. A number that only ever said "seventeen problems" is a number you stop reading: it looks the same whether you just broke something or have been ignoring the same seventeen for a week. The badge now reports what has appeared since you last looked, in the loud color; once you have seen the list it drops to a quiet gray total, so the loud badge always means something changed. Inside, anything you haven't been shown carries a New tag and sorts to the top of its severity, and a Mark All Seen button clears the marks without waiting. Closing the Bench counts as having seen it. Editing an item raises its finding again — a mark you left on "alias begins with an article" doesn't hide the next bad alias on the same entity. A finding you disagree with can be dismissed from its row: it drops out of the list and out of the count, and the count of dismissed rows sits at the bottom, one click from bringing any of them back. Marks and dismissals are per world, they survive closing the app, and they are stored on your machine — nothing goes into the world, so a world you export or share is byte-for-byte what it was, and a copy you publish carries none of your suppressions. Re-downloading a world you already had keeps them; downloading a world whose author has since updated it clears the seen marks, so a defect that arrived with the update can't turn up pre-read.
      • Issues now checks the stat numbers that quietly don't do what they say. Stats are where a world lies to its author most convincingly: every field is filled in, nothing looks wrong, and the numbers still don't reach play. Eight new checks read them. A stat whose starting value sits outside its own Min and Max is an error — the number in the box is not the number the game opens on. A stat that starts above every descriptor threshold is a warning, because the AI is told no status for it at all until the value drops; thresholds are read as the percentages they are, so a Min 0 / Max 200 stat starting at 80 is correctly seen as sitting at 40%. Two descriptors on the same threshold is flagged too — only the first can ever apply, so the second's wording never shows. Listing descriptors out of order is not flagged: the game sorts them before picking, so authored order decides nothing. A percentage stat that doesn't run from 0 to 100 carries a Fix that pins it to 0–100 and brings its starting value with it, exactly as re-picking the type in the editor would. A stat with code, in a world where no stat reads a clock variable, is flagged because that code only runs on turns the AI happened to report a stat change — one clock reference anywhere puts every coded stat back on the every-turn schedule. Two checks read traits against stats: a trait whose penalty lands on a stat already resting at its floor does nothing at all, because the clamp swallows the whole thing (the floor the trait itself raises counts), and a trait that moves a stat whose code recomputes the value from scratch is erased on the next run — unless that code reads the stat's own value, in which case the change is a real seed and nothing is said. Last, as information only: a stat locked against the AI in both directions with no code, no regen and no trait to move it can never change during play.
      • The Issues tab can run your stat code for real, on request. Everything else the Bench reports it works out by reading; code is the one thing that has to actually run to be judged, and running it means a fresh sandbox for every coded stat — far too slow to sit behind a live count. So it is a button: Check Stat Code, at the bottom of the Issues list, shown only when some stat has code and telling you how many it would run. Pressing it runs each one against the values your world actually opens on, and reports the ones that fail — whether the code throws, times out, or doesn't return a number — as ordinary findings naming the stat, so they list, group, open and dismiss like everything else. The results are cleared the moment you edit anything, so a stat you have just repaired can never keep showing its old failure.
      • Paste a scene into the Bench's Triggers tab and see exactly which entities and lore entries it sets off — and why the others stayed out. Matching rules were the one part of a world with no feedback at all before a playthrough: you wrote aliases and keywords, and found out whether they worked by playing until something failed to appear. Triggers takes prose — paste a paragraph, or your own draft — and answers as you type, with no button to press. Entities Present lists everyone the game would detect, each saying which written form earned it: the whole Name, one distinctive Part of Name, or an Alias, with the text it actually hit. It reads narration the way the game does, so a character named only inside quotation marks is correctly read as talked about rather than present. Below it, every dictionary entry in the world is listed with its verdict. The ones that fired show why — Keyword, Always On or Recursive — with the keyword, the literal text it matched and where it matched. The ones that didn't say what stopped them, in plain words: no keyword in the text; a secondary keyword present when the entry only fires without it; a secondary keyword the match still needed; a hit that landed further back than the entry's own scan depth; a keyword that appears only inside a longer word while whole-word matching is on; the entry switched off; its whole book switched off. A keyword written as a regular expression that doesn't compile is flagged on its own row rather than spoiling the run beside it. In the pasted text itself, every matched word is highlighted — click one and it takes you to the row that claimed it, and clicking again hands the same words to the other claimant when an entity and a lore entry both want them. Empty text is a result too: it lists the entries that inject on every turn whatever you write, and says the rest are waiting on keywords. When nothing fires at all it says so and counts what it checked, so an empty list reads as an answer rather than a broken tab.
      • Triggers now shows the lore block it would inject, word for word, with what it costs to send. Knowing that an entry fired never told you what the model is actually handed: entries render with their names attached, in book order, and an entry whose text is empty contributes nothing at all. Rendered Context shows the finished block exactly as the harness builds it — the same builder the game uses, so it is not a preview of the injection, it is the injection. A world that sorts its lore into the early and late blocks sees both, labeled Background Lore and Foreground Lore, each with its own cost, plus a total. Every count is written with a ~, because it is a rough estimate from the character count rather than a real tokenizer — a world can be pointed at any endpoint, and each one counts differently. When nothing fired it says so plainly instead of showing an empty box.
      • Triggers takes the messages behind your scene, fills them from your last played turn, and tells you how far back each match was. Entries with a scan depth read recent messages as well as the current scene, and there was no way to test that here: the box only ever held one paragraph, so a depth-limited entry could only ever say "no keyword found". A foldaway History box now takes earlier messages — oldest first, one per block with a --- line between them, since narration runs to several paragraphs and blank lines would cut one turn into several — and the header states how many are being traced even while it is folded. Every match found in them is labeled with its distance and the window that let it count: History, 2 messages back · inside its scan depth of 3 messages, or no scan depth, so all history is read. An entry whose keyword landed just outside its own window now says how far outside. And if the world has ever been played, Paste Last Turn fills both boxes from its most recent save — the narration that was on screen, with the messages behind it — so testing against real prose is one click rather than a copy-paste hunt. A world that has never been played simply has no button.
      • The matching mistakes the Bench can see are called out on the row they are about, with the same one-click fix. An articled alias, an alias that just repeats its entity's name, two entities that one word detects, an entry that can never fire — all of these were only ever listed on the Issues tab, a tab away from where you are thinking about matching. They now appear beside the row itself: the entity in Entities Present, or the entry in the dictionary list, in the same words Issues uses, with the same Fix button where the repair is unambiguous and none where it is a judgment call. It is the same rule engine either way — and the same list, so a warning dismissed on Issues stops nagging on both tabs. A warning about something the text didn't turn up — an articled alias is exactly why its entity went undetected — is listed on its own above the results rather than disappearing with the row it has no way to earn.
      • Triggers has a semantic switch that shows how close each entry's meaning came, and it is never quietly on. Semantic lore fires an entry whose meaning is near what just happened even when none of its keywords appear, and there was no way to see that here: every score the tab showed came from keywords, so a world relying on meaning looked like a world where nothing fires. A Semantic checkbox now sits under the History box. Turned on, every entry that the semantic pass can score gains a line reading its closeness against the threshold it is judged by — 0.62 vs the 0.39 threshold — close enough to fire on meaning — so a near miss is a number you can act on rather than a silence. An entry that fires on meaning alone is labeled Semantic and says so in as many words, and an entry that fired on a keyword keeps saying Keyword even when its meaning also scored, so no score can take credit for a match your keywords made. Only the closest few entries are ever injected, and one that clears the threshold and still misses that cut says which of the two stopped it. The switch is grayed out until the world has embeddings to score against — the Bench never builds them, so play the world once with Semantic Lore on in Settings and they are there. Left off, which is how it starts every time you open the editor, nothing semantic is computed at all: no model is loaded and nothing is embedded.
      • The Bench's lens bar works: pick the character you are testing as and the place you are standing in, and the instruments read them. The two selectors had been placeholders since the Bench arrived. They sit under the tab strip, on exactly the tabs that read them — Triggers, AI Context and Opening — and not on Issues, which checks the authored world as-is and has nothing for them to change. The at selector lists every location in the world and opens on whichever one you have selected in the editor — the starting location when you have none — so opening the Bench mid-edit lands where you were already looking. The Testing as selector lists the traits of every exclusive group, which is how a world says "pick one of these to be", and picking one applies exactly what picking it in a playthrough applies: the placeholder values that character fixes, and the stats they switch on or off, named under the selector. Triggers reads it at once — an entity whose name is a Wildcard is a name the tracer could never match before, and under the character who fixes it, it matches the prose exactly as it would in play. A chip nobody pins still reads as its options, because that value is a roll the playthrough makes and not something the editor can know. A pin naming a placeholder the world no longer has says so under the selector, since nothing it fixes can ever reach the text; a pin holding a placeholder at a value its own list doesn't carry is applied without comment, because naming a one-off value nobody else rolls is what pinning is for and a playthrough applies it the same way. Both selections hold across tab switches and across closing and re-opening the Bench for as long as the browser tab is open, and none of it is written into your world.
      • The Bench's AI Context tab shows everything the AI is told from a location, block by block, with what each one costs to send. A world's real size is invisible while you write it: nothing anywhere reported that standing in one room sends three thousand tokens of context every turn, or that a description you wrote never reaches the model at all. Pick a place in the lens and AI Context answers with A turn from here ≈ ~1,240 tokens, then every block behind that number — the world description, your stats and traits, the location itself, its sub-locations, the location that contains it, the places reachable from it, where you can travel to, who is here, and the lore. Each row carries its own estimate and the prompt chip that serves it, and opens to the finished text exactly as the model receives it — the same builders the running game uses, so it is not a preview of the context, it is the context. A block this location has nothing for says nothing here and costs zero. Every figure is written with a ~, because it is an estimate from the character count rather than a real tokenizer. Destinations lists everywhere a player can go from here, each with the travel wording you gave its connection, and states plainly that this is the whole set — anywhere not on it can never be traveled to from this place — while whether a given action counts as travel stays the model's judgment. Entities the AI Is Told About groups the cast into here, in sub-locations and in reachable locations, each name saying whether the AI gets its Full description, only its Summary, or Name Only because it has neither — so a summary quietly standing in for the description you wrote is visible at a glance. Everything resolves through the character in the lens, so a location whose text is built from placeholders reads as that character sees it.
      • The Bench's Opening tab shows what a fresh game as your chosen character actually looks like, before anyone plays it. Pick a PC in the lens bar (or none) and Opening lays out turn one. Every stat appears at the value the game would really start it at — trait changes applied, clamps included, with the moved amount called out beside it — together with the status word the AI would be told for that value. Each stat carries a slider that scrubs the whole range and live-updates that status, so descriptor coverage is checkable edge to edge: a start that sits above every band reads no status with a warning saying so, which is how a "starts at 1" vs "none yet" contradiction becomes visible before play instead of mid-playthrough. The traits in force are listed with everything they impose — placeholder pins, stat switches — and every Wildcard the game would roll shows the value it drew alongside each value's real chance from its weights, with a Reroll that draws again for everything no trait has pinned. Where several Unique chips share one pool, the tab states the odds that two of them land on the same value, because independent draws can repeat. At the top sits the ~token cost of the whole first prompt; at the bottom, the prompt itself — the system prompt and the opening user turn, expandable, assembled by the same builders a real turn uses, real turn-one lore scan included. Scrubbing and rerolling are test-only and never edit the world. With Opening built, all four instruments in the tab strip are live.
      • Issues now runs the whole World Doctor rule catalog — twenty more checks, finishing what the earlier packs started. The new rules catch a location filed under a parent that no longer exists (and can fix it by making the location top-level), a travel link pointing at a deleted location, a stat update targeting a stat that was renamed out from under it, entities whose names double as one of a Wildcard's values, lowercase multi-word aliases the text's own capitalization will never match (one that merely repeats the entity's own name is left to the repeat check instead, so the two never ask for opposite things), entities missing their player description, their AI description, or both — three separate checks, so each gap is its own counted row (an entity carrying an AI summary instead is told exactly which part of the prompt still gets only its name), an entity's or location's long AI description with no summary to shorten it, an AI summary that saves almost nothing over the description it hides (judged by the savings it buys: flagged when it runs to more than half of its description and the trade saves under a real threshold of tokens per turn — a summary longer than its description included — with the finding naming both sizes and what the trade saves; a summary just over half its description that still clears the threshold every turn is left alone, as is a description small enough that the savings are meaningless either way), locations that contain no entities, exclusive trait groups with two defaults or only one member, weights naming values that aren't in the pool (fixable — the dead weights are dropped), Wildcards whose weights bench every value but one, dictionary keywords that are substrings of another entry's keyword with whole-word matching off, disabled entries and books, an empty system prompt, a missing readme, and embedded images over their size budget, with a pointer at Optimize Images. Every new rule respects your existing seen marks and dismissals: what it finds on a world you've already triaged arrives as New exactly once, and the “N rules checked” line under a clean world counts them all.
      • The Bench reopens on the Instrument you were last using, per world. Checking a world is an edit-check-edit loop, and the Bench used to restart that loop on the Issues tab every time the editor rebuilt it — which on mobile meant every time the sheet closed. The open tab is now remembered the way the lens already is: for the session, per world, on your machine, and never in the world itself.
      • In Simple mode the Bench only reports what Simple mode can fix, and folds the rest away behind a count. Simple hides whole fields — aliases, AI summaries, stat code, chips — and the Bench used to report on them anyway, so the badge counted problems in fields you had never seen, described in words the editor never showed you. Every check now declares whether acting on it needs an Advanced field. In Simple, those are off the badge, off the new count and off the list, gathered instead into a “N findings need Advanced mode” line at the bottom that says the mode switch beside the World Editor title is the way in. Nothing there offers a Fix, so one click can never rewrite a field you can't see, and a downloaded world full of hidden defects still never reads as clean. Check Stat Code is gone from Simple too, since its verdicts are among the folded ones and pressing it would look like nothing happened. Switch to Advanced and the full list is exactly as it was — including the New marker on anything the fold was holding, since you haven't actually read it yet.
      • The flask opens a compact popover first, and the full Bench can sit in the editor's own list panel instead of a third column. Reading two findings used to cost the whole panel: the editor reflowed, a resize handle appeared, and four instruments loaded so you could click Fix twice. The flask's first stop is now a small popover under the button holding the complete World Doctor list — the same grouped rows, the same Fix and Fix All, dismissals and restores, Mark All Seen, the stat-code check and the Simple-mode fold — and clicking an item's name takes the editor to it without closing the popover, so a list can be worked all the way down in one go. Closing it quiets the count exactly the way closing the panel does. Open Test Bench at its foot goes on to the full panel when you want the lens, Triggers, AI Context or Opening. That panel now opens embedded in the editor's first panel by default, taking over the tab strip and the list while the detail panel beside it stays live — so a finding you click opens visibly next to the list you are triaging — and the editor's header and footer stay put, so Save, the mode switch and Find are still there mid-triage. Pop Out moves it to the side panel, Embed in Editor brings it back, and whichever you used last is where it opens next time. On mobile the popover works the same, and Open Test Bench still raises the full-height sheet.
      • The World Doctor spots images stored in a format that would be smaller as WebP, and converts them for you. A PNG, GIF or BMP pasted into a world stays that way forever, quietly carrying bytes a lossless WebP copy gives back for free — and nothing ever told you. The Issues tab now says so: one row naming every picture it applies to, each name a way straight to the entity, location or world overview that holds it, with a Fix button that runs the real conversion and writes the result back as an ordinary edit — so Exit Without Saving is still the whole undo. An animated GIF keeps its animation, and on a browser that cannot re-encode animation it is left alone rather than flattened. Photos are never nagged about: a JPEG's lossless WebP copy is bigger, so JPEGs and SVGs stay out of the findings entirely. And because some images genuinely come out larger as WebP, the conversion keeps those and tells you how many — so a row that doesn't fully clear reads as a fact about your pictures rather than a button that failed. A picture's format is judged by its actual bytes, never by the label its data carries — one of the bundled worlds ships a JPEG marked as PNG, which a label-trusting check kept promising a shrink that could never happen, so its row could never clear. An image like that isn't offered a conversion it can't have — instead the Issues tab names the disagreement on its own row ("labeled PNG but its bytes are JPEG") with a Fix that corrects the label to what the bytes really are, so nothing downstream is misled again; the WebP Fix corrects lying labels on its way through too. The footer's Optimize Images popup is honest about the same thing now: it only offers to convert when something actually converts, its size estimate counts the images it can convert and leaves the rest at their real size, and a run that kept images says so.
    • Stats:
      • A stat descriptor's threshold is a value of that stat now, and the World Editor draws what every band covers. A threshold was silently a percentage of the way from Min to Max, announced nowhere except a Threshold % placeholder that vanished the moment you typed — so a 0–10 stat banded 3 → low, 6 → stocked, 10 → full was really banded at 0.3, 0.6 and 1 rocket: 1 rocket read as full, and 2 through 10 got no status at all. Type 3 on a 0–10 stat and it now means 3. Above the rows, a coverage bar draws every band at its true width across Min→Max, with the range above your top band in red — the range where the AI is told no status — and an arrow pointing at exactly where the stat starts — its label slides aside near the bar's edges rather than dragging the arrow off its true spot — so a gap your world opens in is impossible to miss. The band a fresh game opens in is written in bold on the bar, so the word the AI is handed on turn one reads off it at a glance. Every threshold box wears its unit permanently (of 10, or %) and sizes itself so even a huge ceiling — worn compacted, of 101k — never crowds out the number you typed, and each row says what it covers underneath it (covers 0 – 3). Bands that should stay proportional still can: Thresholds in — Raw Unit | % of Max switches one stat over, and switching converts the numbers you already wrote, so no band moves at the moment you flip it. The difference shows up later — raise a 0–10 stat's Max to 20 and a Raw Unit band at 3 stays at 3 ("3 rockets is low"), while a % of Max band at 30% moves to 6 ("the bottom 30% is low"). A Percentage stat is pinned to 0–100, where both readings are the same number, so it just shows % and offers no switch. Nothing stored is rewritten and no world is migrated: every band in every bundled world runs 0–100, where the two readings are identical, so worlds behave exactly as they did — and a non-0-100 world that has been quietly mis-banding starts meaning what its author wrote. The Bench's Issues tab keeps up: a stat starting above every band now names the value and where coverage stops in matching units instead of comparing a raw number against a percentage, and a new check flags a threshold sitting outside the values its stat can ever hold — the leftover percentages, and the typos. A second new check flags bands that stop short of Max — every real case found was the same misreading, thresholds taken as a band's floor, leaving the top of the range silent — and carries a Fix that stretches the top band to Max, which is exactly what that author meant; a world that wants deliberate silence when the stat is full dismisses the row once.
      • A stat can be marked Hidden, so the AI uses it but the player never sees it. A new Hidden checkbox sits in the stat editor's Availability section, beside Enabled — and that whole section now shows only in the editor's advanced mode, since both switches are tools most worlds never touch. A hidden stat has no row in the stat panel — no bar, no value, no change chips — and trait cards at game start don't name it, while the AI keeps reading it and its regen and code keep running. It's for the bookkeeping a story needs but a player shouldn't watch: dice rolls, cooldowns, counters driving events behind the scenes. It is not the same as disabling a stat — a disabled stat is off for everyone and its code pauses; a hidden one is fully alive, just unseen. A hidden stat with body morphs still shapes the avatar, so a world can show consequences without showing numbers, and the game menu's AI Context view still tells the whole truth for anyone who goes looking. Worlds that never use it are untouched.
    • Readme:
      • A world can now open with an Introduction the player reads before choosing anything. The World Editor's Readme field is now a Readme section with two tabs, Introduction and Gameplay. The Introduction appears over the very first screen of setting up a new game, ahead of trait selection, so a world can introduce itself and explain the choices that are about to be made; the Gameplay tab holds the readme that already existed, still shown on entering the game. Both take markdown and placeholder chips, and find & replace reaches both. Players only ever see the one for the phase they are in, so nothing written for a finished character can be read before that character exists. Don't Show This Again on either covers both, as does the world's Show Readme on entry switch — one answer per world. Quick Start skips the Introduction along with every other setup step, and loading a save never shows it. A world that leaves the Introduction empty behaves exactly as it does today.
      • The Preview now tints each placeholder's resolved value in that chip's own color, and leaves a marker where a chip produced nothing. The Readme Preview rendered the resolved prose flat, so once a Town chip became Sedge Landing nothing told you which words came from a chip and which you typed — even though the plain Preview on every other chip field had been color-coding them all along. Both panes now read the same way, and a value still renders as markdown inside its highlight, so a Wildcard that rolls storm-lashed comes out italic and tinted. A chip resolving to an empty value used to vanish without a trace, indistinguishable from a placeholder you forgot to insert; in both Previews it now leaves a small marker in the chip's color — including an affixed prompt variable whose whole phrase drops out of what the model is sent. A value long enough to span a blank line renders exactly as before, just without the tint. What the player reads is untouched.
    • The in-game Traits tab is organized by the groups the world's author built, instead of being one long list. Each top-level trait group is now a section you can fold shut, headed by its name and a badge counting the traits you have switched on — a section with none reads a muted 0. Sections holding something active open on arrival and the rest start closed, so the tab opens on your build with nothing to press. Inside a section the active traits sit on top and everything switched off or not yet taken folds into a Disabled (N) block one click away, and a nested subgroup becomes a small heading (Mutations › Major) rather than another accordion — a quarter-width panel has no room for depth. Above it all, one line names every active trait, and a filter field narrows the whole tab to traits whose name or description matches, opening whatever sections and Disabled blocks hold a hit so a result is never left behind a closed header. A group the author marked exclusive now shows radio buttons rather than checkboxes, so the alternatives read as alternatives before you pick one — choosing a sibling still retires the one you had, which was already the rule and is now visible, and clicking the one you hold clears it, so an optional group can be left empty. A trait that changes a stat carries a small chevron, and tapping it reveals those changes with signs (Resolve: +10), hidden stats left out, and a trait that pins a placeholder reads its own value there. Traits the author never marked switchable appear with no control, as before; paging back through history shows only what you held on that turn, read-only. A world with no trait groups still gets a plain flat list with none of the section chrome, and traits sitting outside every group in a grouped world collect under General. How you leave the tab is how you find it — the filter text, the folds and any revealed stat list all survive a look at Stats or Location — until the trait list itself changes or you start a new session, when it opens on its defaults again.
    • Every font can be tuned to taste, and JetBrains Mono arrives already fixed. A Customize… button now sits beside both font pickers — the app-wide Font and the story's Narration Font — and opens sliders for size, bold weight, italic slant, line height and letter spacing, with regular, bold and italic sample lines that update as you drag. Nothing outside the dialog moves until you press Save, so a game in progress never reflows while you experiment, and Cancel throws the whole draft away. Each font remembers its own settings and carries its own Reset to Defaults, so switching fonts switches to that font's look and resetting returns to how the font shipped rather than to nothing. JetBrains Mono ships with its bold raised, because its stock bold was barely heavier than its regular text — nobody has to open the dialog for markdown bold to read as bold. The story pane's Reading Scale and Line Height still apply on top of whatever a font is tuned to.
    • A world can now carry its own Choices and Stats prompts, not just its own Narration prompt. The World Editor's custom narration field is now a Custom Prompts section with three buttons — Narration, Choices and Stats — each carrying its own switch, so which prompts a world supplies is readable without opening anything. None is open until you press one, and pressing the open one again closes it, so the section costs a single row until you are actually writing a prompt. Opening one shows the prompt the game would run right now, as a starting template rather than a blank box, and it keeps following that prompt until you edit it; the first edit is what makes the text the world's own, and Reset hands it back to following yours. A prompt can be written while its switch is off, and switching it off never discards what you wrote. Players see which passes a world rewrites before entering it, can read the authored text tab by tab, and still decline the lot with one checkbox. A world that customizes nothing, or only narration, behaves exactly as it does today.
    • The AI Language directive is now a chip you can place anywhere in the Narration and Choices prompts, and nothing is added to a prompt behind your back any more. A Language chip joins both palettes. It stands for the directive the language setting produces — Write all narration in Japanese. — and it renders where you put it, so a model that obeys an instruction better at the top can have it at the top. On English, or with the field cleared, it renders nothing at all and takes its own blank line with it, so an English prompt is unchanged to the byte. Both default prompts carry it in last place, which is exactly where the directive used to be added, so a default game is untouched. The narration prompt's Foreground Lore section now ends at the lore itself: the closing instruction that follows it — write only prose, never a question or a list — sits under its own Output heading instead of being filed as lore, which is what it looked like in the XML prompt style. Alongside it, three things the app used to slip in without saying so are gone: a prompt with no Notes chip no longer had a Player Notes section wedged in above the location, a prompt with no Dictionary chip no longer had the turn's lore stapled to the end, and no prompt gets a language line appended after the fact. The Stats pass also stops being told to answer in English, which was only ever an invitation to translate the stat names it has to echo back exactly. What this costs: a custom prompt written before those chips existed loses the injection it was silently getting — add the chip where you want it and it is back. The same goes for a downloaded world's prompts, since worlds carry their own. Lore is unaffected wherever a prompt kept even one Dictionary chip: entries filed for the block you removed flow into the block you kept.
    • A world can now write the line its story opens on, instead of every world opening on the same one. The World Editor's world panel gains an Opening Cue field in Advanced mode, beside Custom Prompts: switch it on — the field appears — write what the opening turn should ask for, and that text is what waits in the player's input box when they press Start Game. It stays a starting point rather than a script — the player reads it, tweaks it or replaces it before sending, exactly as they can today. The field opens showing the standard cue, guardrails and all, so you edit from something that works rather than writing blind, and it only becomes your world's once you actually change it; Reset hands it back to following the standard one, and switching the cue off keeps the text you wrote — the field goes away, your words don't. Placeholder chips work here too, so a world can open on a different detail each playthrough — the player sees the rolled words, never the chip. A world that writes no cue opens exactly as it does now, and a cue travels with the world when you export or publish it.
  • 🛠️ Developer tooling

    • Browser Tests:
      • The suite runs from a known chrome baseline, so a new tutorial or a hidden field can't fail a spec that was measuring something else. Nine tests had gone red for reasons none of them were about: the World Editor's first-run Simple mode hides the Custom Narration Prompt field the undo/redo specs type into, and the Settings tutorial popover sits over the prompt picker the dropdown specs click. openApp now seeds Advanced mode and every tutorial pre-dismissed — the dismissals read from the tutorial registry itself, so adding one can't reintroduce this — and a spec that wants either back overrides it. The focus-ring scan also skips controls that take no focus at all, since a disabled button draws no ring to clip.
      • The Locations canvas is proven to answer taps on a mobile-sized screen. The master-detail layout scrolls its list side, and that scroller sat over the canvas and swallowed every click aimed at the map — an overlap no rendered tree shows, so only real hit-testing could catch it. ListDetail now takes scrollList, which the World Editor turns off for the canvas on both its layouts, and e2e/location-canvas.spec.ts seeds a two-sibling world, clicks a travel arrow and clicks a direction in the panel that opens, failing if anything covers it.
    • Test Bench:
      • What the Bench reports is a pure rule engine over the world, not checks scattered through the editor. lib/testBench/rules holds the whole diagnosis: a rule is { id, severity, section, check(world) → Finding[], summary(count) }, runRules is one pass over an authored world, and groupFindings collapses a rule's findings into the single row the list shows and orders the rows by severity. Nothing in it touches React, storage or the world itself, so a rule is asserted by feeding it a defective world and a corrected one — and the same set can later be filtered to a matching-only subset for the Triggers instrument rather than reimplemented there. The three alias rules compare names and aliases through a new matchKey exported from lib/entityMatch, so the Bench's idea of "the same text" is the game's own case-folding and pluralize rule rather than a second one free to disagree with play; names and aliases are resolved through describePlaceholders first, so a chip is compared as the text it stands for. useDebouncedFindings re-runs the pass 400ms after the world stops changing — keyed on the editor's memoized payload, which is the only thing whose identity tracks a real edit — with the first pass synchronous so the badge is right the moment the editor opens. TestBench.tsx is presentational: findings in as props, navigation out as a callback, so the desktop panel and the mobile sheet render the same component. Every collection a rule reads is read as ?? [], fields the types call required included: world JSON is hand-editable and arrives from third-party tools, so a "required" array can simply be absent, and the pass runs inside the editor's render, where one throw is a blank editor rather than a finding — a rule diagnoses a malformed world instead of tripping over it. All of it is fixture-tested with each guard mutation-proved, and the panel is thin-tested against groups the real engine produced.
      • Issues catches every reference that points at nothing and every setup that can never happen. Thirteen rules join the engine in the same pure shape: reference integrity (entity placements naming dead locations, trait toggles and pins naming missing stats, placeholders or off-list values, chips referencing deleted placeholders, chips in stat descriptions/descriptors — flagged against the gameplay priming pass's own field list, so "where chips work" can't drift from the game — and stat-code s.name === "…" lookups against names no stat has, in either comparison direction), dictionary can-never-fire (keyword-less non-constant entries, secondaries with no primary to gate, regex keys compiled with the matcher's own flags so the check can't disagree with matching), and reachability (no isStarting location, the dead legacy isStartLocation field, entities placed nowhere, disabled stats no trait ever enables — the Centaur Breeder classes). FindingItem grew an optional per-item section so one rule's findings open on different editor tabs: the chip rule lives on Placeholders while each item opens on its owner's tab. Every rule ships as a defect/corrected fixture pair with a representative guard per pack mutation-proved. #dev?modal=worldEditor&bench=issues opens the editor with the Bench open on an instrument, through a bench slot on the dev-route — its own slot rather than subtab, since the Bench sits beside the editor's tabs instead of inside one, and the two combine (&tab=entities&bench=issues). The ledger lists only built instruments, so landing Triggers, AI Context or Opening fails the drift guard until it is made routable too.
      • A rule can carry its own repair, and the Issues row is where it is pressed. Rule gains an optional fix(world) → world beside its check, so a repair lives with the rule that diagnosed it rather than in the panel: applyRuleFix runs one by id, groupFindings marks a row fixable, and the editor writes the returned world back slice by slice through the same setters the panels use — only the slices the fix rebuilt, since a fix returns the world it was handed when there is nothing to repair. Five rules carry one (leading-article alias, self-duplicate alias, orphaned placement, legacy start flag, unused placeholder), and a sixth rule — a placeholder no chip and no trait pin anywhere reaches for — joins the engine to give the last of them something to find; its usage scan is deliberately wider than the resolver's field list, so a chip parked where it never resolves still counts as a mention. Every fix re-derives its targets from the world it is given, which is what makes fixing twice equal fixing once. A table in rules.test.ts round-trips each fix — the finding gone, no new rule raised, the second pass a no-op — and the registry test fails if a fix is added without a fixture.
      • What the author has already been shown is a stored set of finding identities, not a flag on the finding. lib/testBench/seenState is the pure half: a finding's identity is its rule id plus the sorted ids of the items it names, so the same problem about the same items survives a recompute and a reordered list, and partitionFindings splits a pass into live and dismissed rows with each marked new-or-known. The seen record stores a hash of what the finding said alongside the identity, which is what makes an edit re-raise it — nothing has to diff the world. withSeen merges rather than replaces, so a defect that vanishes mid-keystroke and returns isn't announced twice; dismissals key on identity alone, so renaming an item doesn't un-mute the author's own judgment. useBenchFindings binds that to one localStorage record per world id (FORMAMORPH_benchFindingState, the same store family as the download note) and hands the panel its grouped rows; groupFindings takes an isNew predicate and sorts new rows to the top of their severity, leaving the rules themselves ignorant of what anyone has read. A changed sourceUpdatedAt clears the seen set on load — proven with a fixture, along with the unchanged re-download that keeps it.
      • The stat rules read the engine's own arithmetic rather than restating it, and the one check that has to execute is split off from the live pass. Eight rules join lib/testBench/rules in the same pure shape, each derived from the code that actually decides the thing: the starting value resolves starting → value → min exactly as statBackfill seeds it; the descriptor band sorts ascending and takes the first threshold at or above the value, as buildStatContext does — which is also why unordered thresholds are not a rule, since authored order decides nothing (duplicates are, as the later one is unreachable); the clamp-swallow rule derives the floor as min + max(0, the trait's own min contribution), mirroring deriveEffectiveStats, and fires only when the delta moves nothing at all rather than when part of it lands; the every-turn gate reuses usesStatClock and applies it world-wide, as GameViewer's anyStatUsesClock does; and the percentage fix writes the same {min: 0, max: 100} plus clamp that StatManager.handleTypeChange writes. The code-overrides-trait rule excuses code that reads its own stat — by currentStatId, its own id, or its own name as a literal through the existing statNamesInCode — because that is the one shape where a trait's starting change is a seed rather than a doomed write. FindingItem's per-item section carries the trait half of a stat/trait finding onto the Traits tab. Execution lives apart in lib/testBench/statCodeCheck: checkStatCode(world) → Promise<Finding[]> marshals a turn-one snapshot, runs every coded stat in the real QuickJS sandbox, and reports the failures. Rule splits into a RuleHead (id, severity, section, summary, optional fix) that it extends with check, so the execution check can own a row — STAT_CODE_EXECUTION — that groups and sorts like any rule without pretending it can run in a debounced pass; ruleCount still counts only the live rules. executeStatCode gained a kind on its error results (timeout / non-number / throw) so the Bench sorts failures instead of pattern-matching prose. The editor holds the last run's findings and drops them on any world change. Every rule ships fixture-paired, the check is tested against the real VM rather than a stub, and ten mutations — the inverted range test, the dropped out-of-range guard, the uncollected duplicate, the no-op fix, the ignored clock gate, <= narrowed to <, the ignored self-read, the ignored regen and trait movers, the flattened failure kind and the live-value snapshot — were each reintroduced and each failed the test written for it.
      • The Triggers instrument reads the game's own activation and matching passes rather than running a second copy of them. lib/testBench/triggers is the whole tracer: buildTriggerReport(world, text, {history}) calls explainActivation for the dictionary and findEntityMatches for presence, and adds only the part play has no reason to compute — why an entry that didn't fire didn't. Each near-miss class is decided by re-asking the same matcher a narrower question: whole-word blocking by probing matchHits with matchWholeWords off, scan depth by probing the history slice the window dropped, a secondary gate by probing one secondary key at a time — so no near-miss reason can claim something the matcher would disagree with. Muted books are held back from the pass rather than filtered out of its results, because play never scans them and running them here would invent a verdict. Three seams were shared rather than copied: historyForEntry (the scan-depth window, now cut in one place and used by both explainActivation and the tracer), invalidRegexKeys (compiled through the matcher's own keyMatcher, primary and secondary keys alike) and matchRuleOf. stripQuotedSpeech now blanks quoted speech to spaces of the same width instead of collapsing it, keeping newlines — so a presence match's offsets still index the text as the author wrote it, which is what lets a highlight land on the original prose; whitespace is inert to everything downstream of it. describeNearMiss renders each class as its sentence, fixture-tested string by string, and useDebouncedTriggerReport re-traces 250ms after the typing (or the world) stops, first pass synchronous. TriggersInstrument.tsx is presentational like the rest of the Bench, with the scene text held above the tab strip so switching instruments doesn't discard it. #dev?modal=worldEditor&bench=triggers lands on it. Three mutations — the dropped whole-word probe, a scan-depth window that ignores its depth, and a collapsing strip — were each reintroduced and each failed the test written for it.
      • The evidence Triggers grew around its verdicts is the game's own renderer, the game's own history window and the same rule pass Issues runs — no second implementation anywhere. buildTriggerReport returns three more things and re-implements none of them. rendered is the injected lore itself: the fired entries split by position and rendered through buildDictionaryContext(entries, false) — the same call buildNarrationPrompt makes for a <DICTIONARY> chip, so the block is byte-identical to what would inject, and the test's oracle is the game's own pair (getActivatedDictionarybuildDictionaryContext) rather than a restatement of the tracer. Chips resolve through describePlaceholders, not a playthrough's rolls, since an author has none; ~ costs come from the existing estimateTokens. historyCount plus a historyDistance derived from the history:<i> region indices gives describeRegion a distance, and describeHitOrigin pairs it with the entry's own scanDepth — the same window historyForEntry cuts — so a hit's evidence line states both where it was and why it counted; TriggerEntry.scanDepth now carries the entry's window on every row rather than only on the entry that missed by it. splitHistory/joinHistory are the one place a ----separated box becomes the history array (a blank line cannot be the separator: narration is several paragraphs, so one turn would arrive as several messages and every distance measured from them would be wrong), and useDebouncedTriggerReport takes it as a string so the debounce key stays identity-stable. lib/testBench/lastTurn is the save half: pickLatestSave (by worldId, falling back to the stored world name for pre-folders saves) and lastTurnFrom (the last assistant message through scannableMessageText — the reading the activation pass gives a history message — with the ten before it), both pure, with loadLastTurn the thin IndexedDB wrapper that returns null rather than throwing. The inline warnings add no rules: Rule gains a matching flag, MATCHING_RULES derives from RULES, and selectMatchingFindings filters the pass the editor already ran — the finding object shown in Triggers is the same object Issues shows, taken from useBenchFindings' live groups so dismissals apply to both, and its Fix is applyRuleFix by rule id. Eight mutations — a heading-bearing block, a distance measured from the wrong end, an unfiltered subset, an unconditional paste button, a world-blind save pick, a dropped per-message dedupe, a blank-line separator and a suppressed loose-warning list — were each reintroduced and each failed the test written for it.
      • The semantic pass in Triggers is the game's own selector, and the toggle being off is what makes an embed impossible rather than merely unlikely. lib/testBench/semantic scores but never decides: traceSemantic(entries, {queryVec, vectors, threshold, cap}) hands activation to selectSemanticLore — the same call buildNarrationPrompt makes — and adds only what play has no reason to compute, a score for the entries that did not activate, split into activates / capped / below / unindexed so a cap-out reads differently from a miss. buildTriggerReport takes the input, folds it in with the game's applySemanticLore (a keyword reason always wins) and attaches the per-entry verdict, so report.semantic being absent is what makes a keyword-only run structurally unable to show a score anywhere. useTriggerSemantics owns the two asynchronous halves: a debounced getVectors probe over semanticIndexKeys(world) — a cache read, never a write and never an embed, which is why it can run with the toggle off and still be what enables it — and, guarded on the toggle, the model open plus the one embedTexts call for the pasted text. A vector is handed back only while it still belongs to the text on screen, so a keystroke drops the scores rather than aging them.
      • The lens is a pure module over the world, and both selections are Bench-local session state. lib/testBench/lens resolves { pcTraitId, locationId } against a world into the PC's placeholder pins, its stat-enabled map and the pins the world cannot honor — all of it through the game's own traitEffects, so the Bench cannot disagree with a turn. Its seedLens decides what a fresh lens points at (stored selection, then the editor's, then the starting location) and re-seeds only the half whose target the world has lost. describePlaceholders gained the pins argument the resolver already had, in the same order, so a pinned chip and a missing placeholder resolve identically on both paths. buildTriggerReport takes the pins and threads them through entity names, entry labels and the rendered block; the semantic pass deliberately keeps reading the unpinned text, since that is what the stored vectors were built from. useBenchLens holds the state, seeding on open and persisting to sessionStorage per world id, and the trigger hook compares pins by content rather than identity so an ordinary keystroke in the editor is not read as a change of character.
      • The Opening instrument is a pure view-model over the game's own start-of-game machinery, never a re-implementation of it. lib/testBench/opening settles stats through traitRuntime's real seeding and acquisition (the same clamps and derived bounds a new game runs), primes rolls over the exact field list the placeholder session primes, and assembles the first prompt with buildNarrationPrompt over authoredPreviewValues — which grew an options bag (traits, stats, location, resolver) instead of a second assembly path. The descriptor-by-value lookup now lives once as statContext.activeDescriptor, shared by the prompt builder, the stat rules and the Opening sliders, so no surface can band a value differently from play. Unique-collision odds are exact — 1 − n!·eₙ(p) over the pool's weighted chances — not a simulation. The shared Slider now passes its aria-label to the thumb, the element that actually carries the slider role.
      • Every piece of Bench-local browser state now rides one storage helper. The lens selections, the seen-and-dismissed record, the downloaded-copy note and the new remembered tab each hand-rolled the same read-guarded, write-swallowing storage shape; lib/keyedStorage now owns it once, parameterized by backing store and key. Stored keys and shapes are byte-identical, so nothing an author has marked or dismissed is touched.
      • The Bench's orchestration is one hook, and the panel takes one bundle per Instrument. The ~210 lines of Bench state that lived inline in the World Editor view — the rule pass, the stat-code check, the lens, the trigger report, both view-models, seen-state, quick-fix write-through — moved into useTestBench, so the view is wiring and nothing else. TestBench's 35 flat props collapsed into per-Instrument bundles, so adding an Instrument adds a bundle rather than another prop row.
      • Every Bench rule declares whether Simple mode can act on it, and the build fails until a new one decides. RuleHead gains an advanced marker beside matching, carried by the check-less stat-code execution head too, so every row the Issues list can show is classified. runRules stays mode-ignorant and pure: the fold is one predicate in useBenchFindings, applied where seen-state already partitions findings, so what Simple hides is also what it declines to mark seen. A registry test — the same shape as the fix-fixture one — asserts the rule heads and the classification table name exactly the same ids, so a rule added without a decision can't quietly default to Simple.
    • The canvas's leaf nesting and its menu grouping are both decided by pure functions, so neither is a drag handler's opinion. leafTarget extends the same center-point containment test that finds group boxes, reporting the childless location under a drag as an arming candidate with the same exclusions (self, descendants, anything carried by the drag) and the innermost winning; dropIntent and multiDropIntents take that armed leaf as an input and read the drop against it, which keeps the rules clock-free — the dwell timer lives in the component and hands over only which leaf. A nest is one applyCanvasDrops write, so it is one undo ticket however many locations it moved, and it edits parentId alone, leaving every authored Connection intact. lib/canvasMenu assembles the menu as sections — history, target actions, view switches, connection styles — from the target's rows, the settings and the history state, so the row model's new disabled notion and the separator boundaries are asserted without mounting a canvas. The armed leaf reaches the boxes through the drop-target context rather than a drag frame, because a dwell fires while the pointer is still and a highlight waiting on the next frame would never arrive. Guards for both live in locationCanvas.test.ts and canvasMenu.test.ts, mutation-tested.
    • Lining a canvas selection up is a pure function over the world, not geometry inside the renderer. lib/locationAlign takes the world and the picked ids and returns the world with those locations aligned, evenly spaced, or stepped by a fixed distance — all measured in canvas coordinates, so a location inside a Group and one standing on its own line up with each other on screen rather than each against its own frame. A location carried by a picked Group counts as part of it rather than as a box of its own, and a step past a frame grows the frame exactly as a drag does. The canvas component is left holding the buttons and the keys.
    • A probe measures whether the AI actually writes in the language the setting asked for, model by model. testing/baseline/harness/language-probe.mjs fires the real narration and choices prompts, chained the way a turn chains them, once per arm of a wording A/B, and scores each output by exclusive function-word counts — a word belongs to at most one language's set, so a token French and Spanish share can never tip the count, and a near-tie is left unclassified rather than guessed. The scorer places three known passages before any arm's number is printed, so a broken scorer reports itself instead of reporting percentages. The standing narration flags ride along, so a language win that costs the format contract shows up in the same table. docs-internal/ai-language-findings.md holds the numbers behind the directive the game currently sends.
    • A probe measures whether the narration prompt's closing contract is obeyed differently under its own heading than under the lore heading. testing/baseline/harness/output-header-probe.mjs fires the real narration prompt over the tracked planning cases with a lore block present, A/B on the one line that differs — the ## Output header — and scores the contract's own violations by pattern: a question or options menu put to the player, a trailing question, stat tabulation, and a heading or label in the reply. Each flag prints the text it matched, so a scorer bug can't pass for a finding, and a self-check has to catch four planted violations and clear two legitimate passages before any arm number prints. Shipped verdict: cloud identical at 108 runs per arm (100% clean both, 0% on every flag), Cydonia 81 runs per arm with no difference that replicated across three batches.
    • A probe measures whether deleting the stat pass's English rider costs a non-English world any stat tracking. testing/baseline/harness/stat-language-probe.mjs fires the real stat-updates prompt over a French world with French stat names (testing/baseline/stat-language-cases.json), A/B against the rider that used to be appended, and scores what the app would actually apply: did the stat the case says moved come back under a name parseStatUpdates can match, in the right direction. It also counts names the app could not match at all — the failure the rider invited — plus trap-stat moves and empty replies as the regression check. Shipped verdict: cloud 82% → 83% applied at 72 runs per arm, Cydonia 100% → 100% at 18, every other metric identical.
    • Which locations a canvas search names is a pure module, so nesting can't quietly become a filter. lib/locationSearch takes the world and a query and returns ranked matches — the whole name first, then a name starting with the query, then a word starting with it, then anywhere — each carrying the resolved label the author reads and the ancestor names holding it, so a location three boxes deep is as findable as a top-level one and two same-named rooms are told apart by their paths. Names are resolved through the canvas's own resolveName (placeholder chips included), which the map, the Connection inspector's header and the search box now share rather than each spelling out. The renderer keeps only the traveling: revealLocation centers xyflow on the node's measured box at the author's own zoom (opened to 0.8 if further out) and marks it for 1.4s through a FlashContext, so a flash never redraws the map to say something about one box, and the mark stands in place of the selection ring rather than beside it. The minimap and the search panel are gated on fullscreen, keeping the pane's chrome to zoom plus the way in; the minimap's click-to-jump is guarded by isStationaryClick, since xyflow hands on the trailing click of a pannable drag with no idea a pan just happened and taking it at face value threw the view to wherever the pointer came off. Ranking tiers, nesting, path building (cycles and missing parents included), the limit and the blank query are asserted in locationSearch.test.ts, each guard mutation-tested; e2e/location-canvas.spec.ts zooms a spread-out world until the target is off screen and proves the search brings it into the frame, that the mark appears and expires, and that the minimap moves the view — with the centering, the flash and the fullscreen gate each mutation-tested against it.
    • The Locations canvas's undo stack is a pure module of inverse patches, held for the open world rather than by the component. lib/canvasHistory records each canvas edit as the locations or connections array before and after it, and applies a step as the records whose identity differs between the two — written onto the world as it stands, through the same setter the edit used. That is what makes a multi-drag, a reparent and an autoArrange one step each regardless of how many records they touched, why the list view agrees the moment either is undone, and why an edit made in the list panel between an edit and its undo survives it rather than being carried away by a whole-array restore (the drop, arrange and intent functions all return untouched records by identity, which is what makes the changed set precise). Every write the canvas makes now goes through commitLocations/commitConnections rather than the context's per-record helpers, so nothing can edit the map without landing on the stack; applyCanvasIntent moved to locationCanvas beside the intents themselves, so what is recorded is the array the editor writes rather than one rebuilt to look like it. A mergeKey folds a run of hint keystrokes into one step. The stack lives in a module-scoped, world-keyed holder because the World Editor unmounts the canvas on the way to the list panel — a ref inside it would not survive the trip an author makes constantly — and it is dropped whole when another world opens, whose arrays these steps could not restore. Nothing clears it on save, so undoing past one re-dirties the world. Round-trips per edit kind (built from the real drop, arrange and intent functions), the cap's eviction end, forward-branch discard, keystroke folding, the chord reader and the session holder are asserted in canvasHistory.test.ts, each guard mutation-tested; e2e/location-canvas.spec.ts proves a real drag and a real Auto Arrange each come back in one press and that the list view agrees afterwards.
    • The Locations canvas's arrow geometry is a pure module holding all three connection styles and the Group border-anchoring rule. FloatingEdge used to compute its own line — two center-to-center border crossings and a straight M…L, with no way to assert any of it without mounting a canvas. lib/canvasEdgePath now takes two rects and a style and returns the anchors, the SVG path and the label point, so the three shapes — straight, bezier (control points leaning along the direction of travel), elbow (a mid-axis turn on whichever side the arrow leaves by) — differ only in what they build between two anchors every one of them shares. The nested case is anchored outward rather than at the container's center: when one rect holds the other's center, the ray runs from the Group's center through the child, so the arrow spans the child's border to the Group's border on that same side instead of crossing the whole frame. The perpendicular offset that keeps a pair's two directions apart is applied to the endpoints, which is what makes it hold in all three shapes. Style is a per-user pref beside snap and grid (useCanvasConnectionStyle, an unrecognized stored value falling back rather than reaching the renderer) and rides to the edge on edge.data, so picking one redraws through the same path every other canvas edit takes. Anchoring, per-style path shape, the offset, the label point and the concentric degenerate case are asserted in canvasEdgePath.test.ts, each guard mutation-tested; e2e/location-canvas.spec.ts proves the picked shape reaches the rendered d, outlives leaving the canvas, and that an authored Group↔child Connection touches both frames on screen.
    • The canvas's one automatic layout is a pure function over the world that lays out what has structure and packs what does not. lib/locationArrange takes the world and a group id and returns the world with that group's direct children repositioned; sizes come from buildLocationCanvas itself, so a nested group's frame is packed at its real measured size. The children split into connected components over authored Connections whose both ends are direct children of the group being arranged — a link crossing the frame, group-to-child or in from outside, ranks nothing inside the box and is dropped. A component of two or more goes to dagre (rankdir: LR, one edge per pair, the authored fromto even when the record is two-way) and its output becomes one composite box; a lone child is a box by the same path, with no special case. Every box is then shelf-packed in world order — a cluster sorting by its earliest member — into the wrap width whose resulting block comes closest to 16:10, chosen by trying the widths that fit exactly one, two, three… boxes on the first row and scoring each by log-distance from the target. Implicit sibling travel is never a layout edge in either half: it joins every pair of siblings, so a layered layout ranks each sibling in a column of its own (a measured four-child group came out 880px wide across four columns), while no edges at all put all four in one column — the packer exists because neither shape is what tidying means. Row and box gaps are the same two grid cells, and the block is rebased on the frame's near corner and snapped to CANVAS_GRID. autoArrangeAll folds every group deepest-first and then the top level, because a group's frame is measured around children that a later pass would still be moving. The canvas's menu handler is one setLocations call per invocation, which is what leaves the whole arrangement as a single world write for the undo ticket to invert. @dagrejs/dagre (the maintained fork, MIT) joins as a dependency. Scope, on-grid output, determinism, grid shape and aspect, cluster ordering and integrity, earliest-member slotting, boundary-edge exclusion and the deepest-first ordering are asserted in locationArrange.test.ts, each guard mutation-tested; e2e/location-canvas.spec.ts proves the menu entry only appears on a box that holds something and that what it draws is what the world kept.
    • The Locations canvas has no layout engine left to fight — placement is decided once, when a location is created. The map used to re-run a row-wrapping flow over every position-less location on every render, packing them beneath whatever the author had placed by hand, so writing a position onto one location visibly shuffled others and there was no way back. addLocation now stamps a canvasPosition from newLocationPosition wherever a location is created, the drawing pass reads stored positions and falls back to a fixed per-sibling slot that reads nothing an author wrote, and withCanvasPosition turns an overshoot past a group's frame into growth — the frame's origin moves out and every sibling is rebased by the same amount, recursing up the tree — instead of clamping the child back. Opening the canvas is a pure read and dirties nothing. The pure layer is asserted for determinism, for placed siblings not perturbing the fallback, and for absolute sibling positions surviving a rebase unchanged.
    • The full-screen Locations canvas is reachable in one dev-router jump, and one canvas serves both frames. #dev?modal=worldEditor&tab=locations&subtab=canvas&fullscreen=1 lands on the map in FullscreenShell, via a fullscreen slot on the dev-route that any surface with a full-screen shell can use — it is not a third view of the Locations tab and is not listed as one. The canvas component is mounted in the pane or in the window, never both, so what survives the trip survives by being held above it: the picked node ids, the last selection announced to the editor, and the open Connection live in the wrapper, and a selection report arriving while the canvas is between frames — which xyflow emits, empty, on teardown — is ignored rather than taken as the author letting go. e2e/location-canvas.spec.ts proves the window opens on the same four nodes with the selection intact, that Esc returns with it still intact, that the pane holds no second live canvas behind the window, and that the embedded chrome carries no toolbar or minimap.
    • The Locations canvas is reachable in one dev-router jump, and everything it draws is decided by a tested function. #dev?modal=worldEditor&tab=locations&subtab=canvas opens the World Editor straight onto the map, and the same drift guard that covers every other routable surface now covers its two views. What the map shows — which locations nest inside which box, where an unplaced one is laid out so it never lands on a neighbor, which pairs get dashed arrows, how many arrows a Connection gets, which travel hint is written where, and which locations are flagged unreachable — comes out of lib/locationCanvas as plain data, so it is asserted in unit tests rather than by looking at a canvas. Every gesture on the map goes back the same way: dragging between two locations, clicking a dashed arrow, changing a direction, editing a hint and deleting a link each produce an intent the canvas hands to the editor's existing write path, so what a gesture means is unit-tested without mounting a canvas, and both editing surfaces provably write the one record. A drop is the same: dropIntent turns the resting geometry into either a position move or a reparent — innermost containing box wins, a childless box is never a container, and a location can never be handed to itself or to what it already holds — so the move-versus-reparent ambiguity is decided by an asserted function rather than by a drag handler. The renderer holds no rules of its own.
    • A real play session's AI request sequence is recorded and tracked, so the turn refactor can be proven to change nothing. The Turn Pipeline work rewrote how a turn dispatches its dozen-odd AI calls, and the only honest check on it is what the old code actually sent. A recorder now observes the single request seam — type, system prompt, messages, requested token cap, dispatch order — without touching what is sent, and npm run baseline -- --parity <file> captures a scripted run through it. testing/parity/turn-pipeline-parity.json holds that capture: seven Sedge Landing turns, 86 requests, every settings-gated pass from staged planning through diaries and the measured clock, each paired with the answer it got so a replay can rebuild the same next-turn prompts. Its format, its ordering guarantees, and what it deliberately does not compare (sampler resolution, completion order, which turn a between-turn drainer landed in) are documented beside it, and a test validates the fixture's shape and pass coverage on every run.
  • ⚙️ Backend

    • A character now carries the list of places it belongs to, instead of each place carrying a list of characters. "Where does this character appear" was answered on every location that mentioned them, so one character standing in five places was five separate memberships scattered across the world — backwards from how an author thinks about a character, and the wrong footing for anything that describes a character's range. Membership is now one field on the entity, and every roster the AI is handed — who is here, who is in a sub-location, who is nearby, who the presence filter keeps — reads the same inversion of it. Both editing surfaces still work exactly as before: the entity's own Locations picker writes the field directly, and a location's Entities picker writes it from the other end. Existing worlds convert on load and on import, including the pre-2.0 spelling of the field; deleting a location now also removes it from everyone who was there rather than leaving a dead id in the exported world. Nothing plays differently.
    • A whole turn now runs through one tested module instead of an 880-line function inside the game view. Running a turn was a single contiguous block in the view: which of its dozen-odd AI calls fired depended on a tangle of settings, the concurrent and one-at-a-time dispatch paths were two hand-kept-identical copies free to drift apart, errors reached the turn by having a flag written onto the thrown exception, and none of it could be tested without mounting the game. It is now a Turn Pipeline in three parts. A Turn Plan answers, as plain data, which passes run this turn, in what order, with which prompts and token caps, and whether they go out together — each pass a self-contained record carrying its own eligibility rule, request assembly and answer parser, so one can be read, tested or added on its own. A runner executes that plan end to end — the up-front location router, staged planning, the narration, then everything that depends on it — through a single injected request function, with concurrent and sequential as the same code under one switch, and answers with a typed result: finished, stopped by the player, or failed under a named reason (unreachable server, wrong endpoint, rejected request, unreadable answer, empty narration), so guidance is chosen from the name rather than re-derived from an HTTP status. A Turn Commit then computes everything the turn changes — the stored turn, stat and cap deltas, the clock, discovered characters — as one value the view applies with plain setters, instead of merging it inline as the turn ran. Narration streaming, reveal pacing and read-aloud stay exactly where they were. Replaying the recorded parity run through the pipeline emits the same requests, in the same order, with the same caps and messages the old code sent. Nothing about a turn changed for the player.
    • Every AI call now runs through a tested request layer instead of a 350-line block inside the game view. Endpoint routing, sampler resolution, request-body construction, SSE parsing, reasoning accumulation, throttling and abort all lived in one function closed over twenty pieces of component state, where nothing could be tested without mounting the whole game view — so every model-compatibility fix landed in untested code with nowhere to put a regression test. That work now sits behind two seams: aiRequestSpec.ts turns a plain settings snapshot into a complete request body (the built-in engine's sampler trio, the reasoning budget-versus-effort split, both spellings of the repetition penalty, the single-paragraph stop), and aiStream.ts performs the fetch and yields typed delta / reasoning / debug / done events, buffering lines across network chunks and ending gracefully on a stop. The game view keeps only what it always owned — the settings snapshot, the event loop, and the screen updates — and holds no protocol knowledge at all. Nothing about a turn changed for the player.

🐛 Fixed

  • 👤 User-facing
    • Stat Code Editor:
      • Highlighting a suggestion in the autocomplete shows its description card again. Every completion the editor offers carries a one-line explanation — what regen holds, what deltaHours counts — meant to appear in a card beside the popup while its row is highlighted. The popup's styling clipped everything outside its own border, and the card hangs outside it by design, so the descriptions had never been visible at all. The card now escapes intact, and the popup's rounded corners stay tidy.
      • Typing a dot no longer suggests stat fields after things that are not stats. The editor offered the same eight fields — id, name, value, regen and the rest — after absolutely anything, so Math. and JSON. claimed to hold them too, which read as the editor telling you the sandbox works in a way it never has. What comes after a dot is now worked out first. Each built-in offers its own members with a line explaining each: Math. gives min, max, round, floor, abs, random and friends, JSON. gives parse and stringify, and Object., Number., Array., String. and Date. each give theirs. stats. gives what you actually do to the list — find, filter, map, some, every, reduce, at, length. The eight stat fields still appear where they belong, after a stats.find(…) result or a variable holding one, and now after the call itself rather than only after a variable — but not after stats.filter(…), which hands back another list rather than a stat. Anywhere the editor cannot tell what an expression is, it offers nothing at all, because a wrong suggestion costs more than no suggestion.
      • Tab now takes the highlighted suggestion, and indents at the caret instead of shifting the whole line. While the completion popup is open Tab accepts the highlighted row (Enter still does too); with no popup open it indents at the caret, rather than shoving the whole line sideways from wherever you happened to be standing. Selecting part of a line and pressing Tab replaces it, exactly as typing would; selecting whole or multiple lines still shifts them as a block, and Shift+Tab still takes them back. Tab on a blank line takes it to the depth the code around it asks for, so starting a nested line is one keypress. Escape followed by Tab still moves focus out of the field, so the field never traps a keyboard.
      • The completion popup can be scrolled with a wheel or a fingertip. The popup is drawn outside the dialog it belongs to so nothing can clip it, and the dialog's own scroll lock refused every scroll aimed at anything outside itself — so a list longer than the popup could only be moved with the arrow keys, and on mobile could not be moved at all. Scrolls over the popup now reach the popup.
    • Traits:
      • The pinned-value box suggests in the app's own dropdown, in whichever theme you use. Pinning a placeholder offered its values through the browser's own suggestion list — the only one left in the app — so it arrived in operating-system chrome that matched nothing around it, and glowed white in dark mode. The box is now the same autocomplete every tag and language field already uses: focusing it shows the placeholder's values, typing narrows them, clicking one fills the box, and the arrow keys walk the list with Enter or Tab taking the highlighted one. Free text still writes through as you type it, so a trait can still pin a shade the placeholder's own list doesn't carry. The list scrolls with a wheel or a fingertip inside the trait editor.
      • Every section trades its explanation paragraph for a ? button. Stat Availability and Placeholder Pins each opened with a block of text you had read a hundred times, pushing the actual controls down the panel, and Stat Changes explained itself nowhere at all. All three sections now carry the same ? button the alias and stat-code sections use, opening a fuller explanation than the old paragraphs held — how stat adjustments stack, what an off stat really stops, where an off-list pinned value is legal, and which trait wins a disagreement. The three Add buttons shrink to match. The ? glows until first opened, so the help is findable exactly once and quiet after that.
      • A conflict note names the winning trait outright, and clicking a rival's name opens it. When two traits set the same stat or pin the same placeholder, the note under the row used to end in a riddle — "This trait wins — the lower trait in the list does." It now reads plainly: Also set by X. The lowest in the trait list wins: followed by the winner's name — and every trait named in the note is a link that jumps straight to that trait, so checking what the rival does no longer means scrolling the list for it.
    • Autocomplete suggestions take Tab as well as Enter, and tabbing past a field never picks one for you. In every field that suggests as you type — tags, the AI language, the trait editor's pinned value — Tab now accepts the highlighted suggestion, the way Enter always has. A field you merely tab across on the way to somewhere else is left alone: until you have typed something, Tab just moves on, and it no longer stops on each suggestion row on its way out.
    • A world that leaves one of its lists out entirely — its stats, its entities, a stat's descriptors, a placeholder's values — now opens and plays like any other. The editor always writes every list out, so this only reaches you through a world hand-edited or exported by another tool. Several things that read a world took those lists for granted: a stat with no descriptors blanked the whole World Editor the moment you opened it and would have failed a turn the same way, a world missing a collection could blank it again on opening find & replace or on the check behind the Simple mode notice, and a placeholder without its list of values crashed anything that resolved one of its chips — entering a world included. In all of those places a list that isn't there now reads as an empty one, so everything the world does have still loads, plays and searches.
    • Moving your models to another folder no longer strands a stray .moving file when a move fails or is canceled. In the desktop app, a model copied between drives is written under a temporary .moving name and only takes its real name once it has landed. If the copy failed or was canceled in its first moments, the cleanup could run before the file it was cleaning had finished being created, so the leftover appeared afterwards and stayed — the move screen ignores .moving files, so nothing ever collected it again. Cleanup now waits for the file to be closed before removing it.
    • The help for stat descriptors now describes how bands are actually picked. Both the Stats help panel and the World Editor guide said the first descriptor in your list at or below the stat's level wins, and told you to list them low to high or the wrong one would match. Neither has been true for some time: the game sorts descriptors by threshold before picking, so the order you list them in decides nothing, and the rule reads the other way round — a threshold is the top of its band, and the lowest band the value fits in wins. Both now say that, and both point out the thing that does bite: unless your highest descriptor has a threshold of your Max, a value above it gets no descriptor at all.
    • Typing and narration no longer stutter in a location whose background is an SVG. The background image was painted behind the whole interface, so anything drawn over it — a caret in the Notes box, each chunk of a scene as it streams in — made the browser draw the image again underneath. Photographs are kept ready-drawn and cost nothing, but vector art is redrawn from its shapes every time, and a detailed drawing takes long enough that keystrokes fall behind and typos creep in. The background now sits on a layer of its own that the interface never disturbs, so what's on top of it redraws alone.
    • The language you set for the AI is now asked for as an instruction, and clearing the field reads as English. The setting used to reach the model as a bare label — Narration language: French — dropped in partway through the prompt, with a world's lore appended after it. Small models read that as a note about the story rather than something to do, and often kept writing in English. Narration and choices now ask outright ("Write all narration in French.", "Write all choices in French."), and narration's line is the very last thing in the prompt, where a model pays most attention. The field still takes any value, so a style like "pirate speak" reads as an instruction the same way. Clearing the field used to leave an empty value behind, which sent a dangling Narration language: line with nothing after it and told the stat pass to answer in English for no reason — a blank field now means exactly what picking English means, and adds nothing at all. On the built-in engine this took French narration from never to always across a dozen test turns. Some small local models still narrate in English whatever they are asked — they will write your choices in your language but not the story — so if that is what you are seeing, it is the model's limit rather than the setting's.
    • Worlds built in the original Formamorph now open where their author intended instead of somewhere at random. The pre-2.0 app marked its starting locations with a field named differently from the one the rebuild reads, and nothing ever carried the old name across — so a world from that era arrived with its start choice in a field the game ignores, and every new game began in whichever location it happened to land on. Loading such a world now moves every location its author marked onto the flag the game reads, so all of them are candidates again, and clears the dead field away. A world you have since flagged yourself is left exactly as you set it — your choice stands, and only the leftover field is removed. The Test Bench warning about that field goes quiet with it.
    • Two entities whose names share a word are told apart in the Entities list. When one entity's name sat inside another's — "Wolf" and "Direwolf", "Rose" and "Primrose" — the list matched them as the same entity and showed whichever was written first, so a scene holding both listed one name twice, and a scene holding only the longer name listed the shorter one instead. Clicking such a row opened the wrong entity's details, and the picture panel could show the wrong portrait for the same reason. A name now has to match a whole word to count, so the two stay separate, while a scene naming a character in part ("Emily" for Emily Foster) still finds them.
    • The optimize popup now offers lossless WebP conversion for every image it can shrink, not only the oversized ones. The popup that greets an arriving world, character or upload only ever looked at images over their size budget — so a world full of small PNGs sailed through with no offer at all, and then lit up the Test Bench's "would be smaller as lossless WebP" rows one screen later. Every PNG, GIF and BMP is now offered Optimize whatever its size, because lossless conversion is about total world size, while Downscale — the tool for large files — appears only when something is actually larger than recommended, and only ever touches those. The popup says which is which ("2 images worth optimizing — 1 larger than recommended"), and images that are already efficiently encoded still pass silently, so nothing opens a popup with nothing to offer. A GIF in a browser that can't re-encode its frames without flattening the animation is never offered conversion — the same guard the Test Bench's Fix has always had.
    • A location whose parent no longer exists still appears in the world. A sub-location pointing at a place that is not in the world — from a hand-edited or partially imported world file — was dropped from every list built off the location tree, leaving it in the world but nowhere to be seen. It now reads as a top-level location, which is where the map already drew it.
    • The world's opening cue now sits with the custom prompts, and the picker no longer runs off the edge of a narrow panel. The Opening Cue had a section to itself with its switch stranded out on the right-hand edge, directly above the Narration, Choices and Stats prompts sharing a segmented picker — four pieces of the same idea, arranged two different ways, and the picker was already wide enough to overflow the panel on a mobile screen. They are now one Custom Prompts section with a single four-part picker — Narration, Choices, Stats, Opening — each part carrying its own switch and divided from its neighbors by a short vertical rule, so it is plain which switch belongs to which prompt. The picker folds to two rows whenever the panel is too narrow to hold four across. Picking a part opens its text without switching it on, so a cue can be read and drafted before it ever applies, exactly as the three prompts always allowed. Switching one off still keeps everything written under it.
    • The Preview tab on plain text fields is grayed out until the text contains a chip. With no chip in the field there is nothing for Preview to swap in, so it showed a character-for-character identical text — a control that visibly did nothing. It now sits disabled until the first chip is inserted and disables again when the last one is removed, with the tab strip staying in place either way so the field never jumps under the cursor; fields that render markdown keep Preview available regardless, since theirs shows the formatted prose.
    • World Editor:
      • The map's right-click menu is no longer cut off by the edge of the panel it opens in. Right-clicking the Locations canvas near a panel edge used to open a menu with its lower half sliced away, because the menu was drawn inside the canvas rather than over the page. It is now drawn above everything and turns back on itself at the edge of the window, so every row is reachable wherever you click. It also walks with the arrow keys, closes with Escape, and opens on a long press on mobile.
      • Every add button in the editor is now the same compact square + button. The one in the add and search bar and the ones in the Connections and Stat Descriptors panels were three different shapes and sizes; all of them are now one small square button, sitting level with the box beside it, and hovering one names what it adds.
      • The editor's tab strip now fills the width of the panel rather than sitting centered in it. Overview through Placeholders share the row between them, so each tab is a wider target. On mobile the strip still scrolls sideways as it did.
      • On mobile the Locations List/Canvas switch is drawn as two icons. The two labels ate the room the add and search controls needed on a narrow screen. On a desktop-width window the labels are unchanged.
    • Divider lines are now drawn one screen pixel thick everywhere instead of coming out heavier in some places than others. On a display running at 125% or 150% scaling — the Windows default on most laptops — a one-pixel rule falls between two screen pixels, so the separators in a menu, the rules between settings and the small upright dividers in the prompt editor each rendered at whatever weight their position happened to land on. Every rule in the app now measures itself against the display it is on.
---
✅ 2.12.2 — Released 2026-08-12 — The desktop app can be told to stop loading models on its own; images made through Automatic1111 or Forge are kept on disk like any other generation; and the main menu's buttons, the What's New popout and the Manage Models labels all sit at the size and alignment everything around them uses (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing

    • The desktop app can be told to stop loading a model on its own. A downloaded model used to start itself twice over: once as soon as anything routed to the bundled engine, and again the moment a download finished — so the app could be holding several gigabytes of VRAM before you had asked it for anything. Manage Models → Options now opens with Auto-Load, on by default so nothing changes unless you turn it off. Off, a finished download just joins the list and the engine stays stopped until you press Load on the model you want. Turning it back on picks up a stopped engine straight away.
  • 🛠️ Developer tooling

    • Changelog entries that touch the same feature are grouped under a header, and the release notes ship that grouping. Two or more entries on one feature sit under a - **Feature:** bullet, which extractReleaseNotes.mjs emits in bold with its entries nested beneath — so a release that reworked one surface reads as one thing on the GitHub release page and in the update popout, rather than as a flat run of unrelated lines. scripts/changelogFormat.mjs holds the parse rules the generator and the guard share, and changelogFormat.test.mjs lints the unreleased section and the newest closed release for one-child groups, bullets with no bold lead, and nesting past one level. The retired sub-item convention still renders exactly as before, so regenerating an older release reproduces its published notes.
    • The What's New popout has a dev-router entry that opens it on a canned set of release notes. #dev?view=mainMenu&modal=changelog opens MainMenu's changelog dialog against devChangelogSample.ts rather than the live GitHub fetch, so the panel's typography can be checked offline and always against every shape the notes take — a minor group header, tinted current and newest captions, a category label, a feature group with nested entries, and a loose entry. The sample is behind a DEV-gated dynamic import.

🔧 Fixed

  • 👤 User-facing
    • The two folder names in Manage Models sit on the middle of the path they name. Both labels in Manage Models → Options were pinned to the top of their column, which lined them up with the first line of a stack that also held buttons and the occasional warning rather than with the folder path itself — so each label sat a little higher than the text it named, by a different amount depending on whether a warning was showing. They now center on the path line, matching how every other setting in the app lines up with its control.
    • Version headings in the What's New popout are sized to match the notes around them. The minor-version heading rendered at nearly double the surrounding text, because the markdown renderer sizes headings off its own scale rather than the app's. It now steps down to the patch version and the notes beneath it, so a popout listing several versions reads as one list.
    • The main menu's action buttons carry their labels at the same size as every other button. Community Creations, New World, Import World and Admin Panel had been set a step larger to make up for a size change elsewhere — buttons had just started following the app-wide rule that keeps every font looking the same size, and on JetBrains Mono that rule shrinks text slightly. Sizing these four up was the wrong place to fix it: it left them oversized on every other font. They go back to the standard control size and keep their bold weight, so they still lead the toolbar without shouting. The pastel buttons in a world's action column match, since they share the same style.
    • Images generated through Automatic1111 or Forge are now kept on disk like any other generation. The API leaves saving off unless asked, so pictures made from the game existed only inside the save file. They now land in the WebUI's usual output folder, named and stamped the way its own generations are.

✅ 2.12.1 — Released 2026-08-12 — Settings opens in a Simple mode that hides the sixty-odd advanced controls until you ask for them; any trait an author marked as yours to switch can now be taken mid-game; and trait stat changes stop drifting at their bounds, so toggling one can no longer be farmed for points (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing

    • Settings:

      • Settings opens in a Simple mode that shows the settings you'd actually change. Sixty-odd controls over five tabs meant a player wanting the theme, a bigger font, or a model connected had to walk past reasoning strategies, memory tuning and raw prompt editors to find them. A Simple / Advanced switch now sits in the Settings header, the same one the World Editor has, and it starts on Simple. Simple keeps appearance, the scene, how narration feels, the reading controls, the checkboxes that shape a turn, the Thinking picker, everything needed to connect a model, the everyday image controls, and autosave — nothing there is ever harder to reach. It puts away the Prompts tab, the image Tag Prompt editor, the Memory, Characters, Performance and Inspection sections, the reasoning details, the storage housekeeping, and the provider plumbing behind image generation. Everything hidden keeps working exactly as it was — the switch changes what you see and nothing else — and a small dot on Advanced appears when something out of sight isn't on its default, so you always know there's more through there. Your choice is remembered on that device, so setting Advanced once is the end of it.
      • Settings is organized by what each setting affects, across five tabs named Display, Output, Prompts, Endpoints and Data. Presentation, Generation, AI Endpoints and Accessibility divided the modal by where the code for a setting lived rather than by what moving it does, so Show Reasoning sat under Generation despite changing nothing about what the AI writes, the narration font sat on a tab of its own, and restoring a deleted world sat next to it. Display now holds everything you see or hear — theme, color, app font, music, background, narration reveal, and the reading controls as an Accessibility section that keeps its name. Output holds everything that changes what the AI produces, and every row left on it does. Prompts is unchanged but now sits beside Output, the same subject one level down — Output decides which passes run, Prompts is the text each one sends. Endpoints holds connections and their sampling, and Data holds autosave and the housekeeping that touches stored files. Describe New Characters, Character Diaries and Diary Recall move out of Memory into a Characters section of their own, and Scene Images — whether every turn gets a picture — moves to Display → Scene, leaving only the image server itself on Endpoints. Nothing was removed and nothing changed what it does — every setting keeps its value and its effect, at a new address.
      • Every setting says what it does, in one sentence, in the same voice as the one above it. Twenty-six rows — Theme, AI Language, Background Music, Endpoint URL, Model Name and more — had no description at all, and the rest were written at different times to no shared rule: some commanded the AI, some addressed you, some ran to four sentences and wrapped onto three lines while their neighbor fit on one. Every row now carries one sentence describing what the setting does. What used to pad those sentences out has moved to the beside the label, which is now reserved for what a setting costs you or how it actually works: Autosave's four sentences become one plus three bullets, the eight Native Reasoning levels no longer each end by repeating that they need a reasoning model, and the twelve local-engine rows no longer each end with "Applies on reload" — the panel says it once, at the top.
      • Experimental settings carry a mark instead of spending a third of their description saying so. Seven settings opened with the word "Experimental." before getting to what they do. The word is now a small flask beside the label, matched to the it sits next to, and the sentence is about the setting.
      • The rows you pick an option on say each thing once, explain the option you're on, and show which one is recommended before you pick it. Theme, Paragraph Limit, Thinking and Native Reasoning printed the row's description and the selected option's explanation one above the other in the same size and color — on Theme the two lines said the same thing twice. The row description now lives in the beside the label, leaving one line under the control that describes the option you're on. That follows your selection: on Staged it explains the director, character and storyboarder passes and what they cost; on Paragraph Limit's None it warns that a reply can be cut off mid-sentence. And the option a row recommends carries a small mark on the option itself, so you can see the recommendation without selecting it first.
      • Every setting's name lines up with the middle of the control it names. Dropdowns, text boxes, sliders, checkboxes and the System Prompts list each sat a few pixels off their label, by a different amount per row, because a label was centered against the control and the sentence beneath it rather than against the control alone. Names now sit on the control's center line throughout Settings, and a control that needs more than a line — the ComfyUI workflow editor, the tag fields — takes its name at the top line, where centering would strand it in the middle.
      • Memory is three sections instead of one long block. Seven settings sat under a single Memory heading on the Output tab, twice the length of anything around it, with two of them about the clock and one not about memory at all. Memory now holds what the AI carries forward, Time holds Time in Memory and Measured Clock, and Lore holds Semantic Lore, which activates dictionary entries rather than memories. Every setting keeps its value, its effect, and exactly when it appears.
      • Buttons and dialogs in Settings are capitalized the same way throughout. How to get this sat directly beside How to Set Up; Reset size & spacing, Restore default worlds, Clear cached images and Reset tutorials were sentence case while everything around them was not. All of them, and the confirmation titles they open, now match.
    • A trait the author marked as yours to switch can now be taken mid-game, not just switched off. Picking traits at character creation used to settle your build for the whole run: a trait you left unpicked was out of reach forever, even when the author had explicitly said the player controls it. Every switchable trait now sits in the Traits tab whether you took it or not, in the order the author arranged them, so related traits stay together — an unticked row is one you can take, and there is no separate list to learn. Switching one on does exactly what choosing it at creation would have: its stat changes land, the stats it hides or reveals follow, and its placeholder pins apply from that turn on. Switching it back off undoes it, and it stays available if you change your mind again. In an exclusive group — a species, an origin — taking a member retires the one you had, and you can leave the group with nothing switched on. Traits the author did not mark switchable stay exactly as they were: fixed, and invisible unless you chose them. Reading back through earlier turns still shows the traits you had on that turn, with the switches disabled.

  • 🛠️ Developer tooling

    • Settings:

      • Settings copy lives in one module with a test that enforces its rules. settingsCopy.ts holds every row label, description, ⓘ body and confirmation for the modal and the local-engine panel, and settingsCopy.test.ts asserts that a description exists, is a single sentence ending in a period, fits twelve words, and that the label is Title Case. The segmented rows' options live there too, held to the same rules, with recommended a data flag rather than a word in the text. Written against the shipped copy first, it found 98 violations across 79 rows. A new setting cannot land without a description or with a sentence-case label.
      • Every Settings row draws through Row/CheckRow. Around twenty hand-rolled copies of the same two-column grid, and eight hidden sm:block spacer divs, are gone; Row grew the info, top, muted and no-label variants that made them necessary, and RowLabel is now the single label renderer both delegate to. SettingsModal.alignment.test.tsx then walks every row on every tab and fails the ones whose label alignment disagrees with the control they hold, so a row can no longer be fixed at another row's expense.
      • SettingsTabId compile-checks anything that opens Settings at a tab. requestSettings, setSettingsTab and initialTab all take the union exported from settingsTabs.ts, and a dev-router tab= string passes through asSettingsTab, which falls back to the default tab rather than blanking the panel.
    • Trait acquisition, toggling, reversal and bounds derivation live in one pure module with tests. traitRuntime.ts owns the whole orchestration over a small state slice — stats, the player's traits, the switched-off ids and the applied-value record — and GameViewer's handlers are thin wrappers over it. traitRuntime.test.ts drives it through its public operations only: clamped reversal, repeated toggle cycles at a bound, exclusive-group retirement of a never-chosen member, order-independence, base recovery from a legacy save reproducing its saved numbers, and the no-record fallback. The clamped cases were confirmed red against the old negate-the-authored-change reversal before the fix landed. applyTraitStatChanges and invertStatChanges are gone, and with them the two reversal tests that passed only because their stat ranges were wide enough that no clamp ever fired.

🔧 Fixed

  • 👤 User-facing
    • Traits:
      • Switching a trait off gives back exactly what it took, so toggling one can no longer be farmed for stat points. A trait whose stat change was partly or wholly refused by a stat's floor or ceiling used to hand back the full authored amount on the way off: sitting at the bottom of a stat, you could take a penalty trait, drop it, and come out ahead — then repeat that indefinitely. The game now records what a trait's change actually moved and reverses that, so a change a bound swallowed gives nothing back and any number of on-off cycles leaves your stats exactly where they started. A save made before this change reverses its existing traits the old way once, then records properly from the next switch on.
      • A trait that writes two changes to the same stat now lands the same result whichever order the author wrote them in. The two used to be clamped one at a time, so a −80 followed by a +60 could stop at a floor the pair should have cleared. They are added together first and clamped once.
      • A stat's minimum, maximum and regeneration are now recomputed from the world's numbers rather than nudged up and down. Traits that raised and lowered the same bound could leave it slightly off after enough switching, and a maximum a trait had moved could never be returned to what the author wrote. Each bound is now the authored value plus whatever the active traits and the AI have contributed, so it cannot drift however many times a trait goes on and off. Loading an existing save reads every stat at exactly the number it read when you saved.
    • Clicking a suggestion in the stat code editor now inserts it. The completion list appeared and the arrow keys picked from it, but a click landed on nothing — the popup is drawn outside the World Editor window so that a long list isn't cut off by the panel it sits in, and everything outside that window ignores the mouse while it's open. Picking with the mouse, on a stat name or a snippet, now works the same as picking with Enter, and the error tooltips beside the line numbers are clickable for the same reason.

✅ 2.12.0 — Released 2026-08-11 — stat code gets a real editor with templates, completion and live error marks; Community Creations gains status filters, typed filters and a first-run tour; and full-screen editors grow out of the field you were typing in (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing
    • Community Creations can show only the worlds you've liked. Liking something was a way to say thanks to its author; it is now also a way to find it again. Add Filter opens a panel with a Status group: Liked, Downloaded, Not Downloaded, Has Update, and Mine. They stack — Liked plus Has Update is the shortlist of your favorites with a new version waiting. Liked and Mine need an account, so they aren't offered when you're signed out.
    • Every filter you've applied is now visible in one row, as chips you can remove. Authors and tags used to live in two separate boxes and status had nowhere to go at all; they are now one bar under the search box, each filter a chip with an ×, and a Clear that drops them all at once. Whether two tags mean any of or all of is written on the row itself rather than remembered from a panel.
    • Filters and sort stay put between visits, per tab. Worlds, Entities and Dictionaries each remember their own filters, so a tag that only exists on worlds no longer empties the Dictionaries tab when you switch. The search box is deliberately the exception and always starts empty — text left over from last time reads as an empty catalog rather than as a filter. An empty grid now says your filters are what emptied it.
    • Filters can be typed. author:, tag: and status: in the search box become filter chips as you finish each one, so tag:horror status:liked gets there without opening the panel. Quote a name with spaces — author:"Wren Hallow".
    • The generate window shows the picture at a size worth judging, beside the controls instead of under them. The image was a thumbnail at the bottom of a single narrow column, which is not enough to tell a good result from a nearly-good one without opening it full size. The window is now two panels: the prompt and its settings on the left, and the picture on the right at the full height of the window. It opens on an empty frame in the shape the picture will be, so nothing moves when the image lands, and the progress bar and the half-finished frames a local provider sends appear there too rather than pushing the controls around. Every picture is fitted to the panel whatever size it came out — the half-finished frames are drawn large enough to watch resolve rather than sitting in a corner as a stamp, and nothing on screen shifts as one replaces another. Clicking still opens it full size. On a phone the two panels stack, controls above the picture.
    • Community Creations introduces itself the first time you open it. Five short notes appear in turn beside the thing each one is about, and each one goes for good once you've read it: what the Worlds / Entities / Dictionaries tabs hold, what the filters can narrow, what the heart on a card does, that author:, tag: and status: can be typed straight into the search box, and what the Hidden panel is for. Each note says where you are — 2 / 5 — and carries Previous and Next, so a note read too quickly can be gone back to; the last one says Got It. Using the control explains it just as well as reading the note, so switching tabs or opening a filter moves the tour on without you having to press anything. The one about liking waits until there is something you can actually like — signed out, or on a page of your own listings, it holds rather than being spent. The main menu's two footer circles explain themselves too, whichever one applies: signed out, the profile circle says what an account gets you, and once you have one the feedback circle beside it points out that bugs and suggestions go straight to the developers and that replies come back to your profile. All of them come back from Settings → Reset Tutorials.
    • A stat's Dynamic Value Calculation comes with eight ready-made formulas you fill in rather than write. Writing one meant knowing JavaScript, knowing which variables the sandbox hands you, and spelling another stat's name exactly right — one typo and the stat silently sat at zero. A Templates button beside Test Code opens a picker: Weighted Blend, Inverse of a Stat, Threshold Flag, Per-Turn Change, Timer, Daypart Modifier, Random Per-Turn Roll and Regen Toward Target. Each one says what it does in a sentence and asks only for what it needs — which stat, how much per hour, which part of the day — and stats are chosen from a list of the ones your world actually has, so the name is always spelled the way the game will look for it. The code appears in a preview as you fill it in, and Insert drops it into the field as ordinary code you can then edit by hand. The formulas cover both directions of what they do: a negative rate drains where a positive one fills, and a timer counts down as readily as up, so hunger and pregnancy are the same template with a different number in it.
    • You can save your own formulas and share them as a file. New Template at the end of the list writes one from scratch and Duplicate starts from a built-in; anything you save is available in every world on that machine. Mark the parts that should become questions by writing {{name:type}} in the code — stat, number, daypart, choice(a|b) or text — and the picker builds the form for you. While you write one, an Edit and Preview pair sits above the code box — the same pair the prompt editors have: Preview shows the finished question form exactly as someone picking your formula will meet it, so you can tell it reads right before saving, and taking the code box full screen puts the two side by side so you watch the form change as you type, with a button beside it to go back to one pane whenever you'd rather have the width. Import and export are the two small buttons on the My Templates heading — export writes your whole library to a file, import reads one back in, and an imported formula never overwrites one of yours that happens to share a name. Worlds themselves are unchanged — templates live on your machine, not inside the world you're editing.
    • Boxes you write stat code in are proper code editors now. A stat's Dynamic Value Calculation and the template editor were both bare text boxes: no colors, no bracket matching, no way to take back an edit once a template had been inserted over it, no way to make one bigger than a few lines, and nothing on screen naming the variables the code is allowed to use. Both are now a real editor. Keywords, strings, numbers and comments each have their own color in whichever theme you're using, the bracket under the cursor lights up along with its partner, Tab indents and a new line starts where the block it's in does, and long lines wrap rather than running off the side. Lines are numbered, so a code error's line number means something. Above the box sits the same small toolbar as before: Undo and Redo, which matter on a phone where there's no Ctrl+Z and which keep their place in the story when you go full screen and come back. Full screen lifts just the code box to the size of the window, leaving the panel it came from where it was. Variable drops in a ready-made lookup for another stat's value or this one's, at the cursor. In the template editor a Slot menu sits beside it, writing the {{name:type}} forms and leaving the name selected for you to type over, and those slots are marked out from the code around them wherever they appear, including in the preview of what a formula will insert.
      • Stat code suggests what you're allowed to write as you type it. The sandbox hands your code a small, fixed set of things and nothing on screen named them, so using one meant remembering it. Start typing and a list appears with what fits, each entry saying in a line what it is — elapsedHours — total story hours at the end of this turn. After a dot on a stat you get its eight fields, value, min, max, regen and the rest; anything you declared yourself is offered alongside them. Escape closes the list and nothing else, so tabbing out of the field still works the way it did. In the template editor, typing {{ offers the slots that template already declares.
      • Quotes in stat code offer your own world's stat names, so a lookup can't quietly miss. stats.find(s => s.name === 'Helth') never complained — it found nothing and the stat sat where it was. The names come from the world you're editing, spelled the way the game will look for them.
      • Stat code is underlined where it won't work, as you type it. A name the sandbox doesn't have gets a red underline that names what you probably meant, so elapsedHrs points at elapsedHours; so does syntax that can't be read at all, and code that never returns a number gets a warning, which is the silent way a formula does nothing. Full screen shows the same marks in a margin beside the line numbers. They're advice and never block saving, and a template's {{slot}} parts are left out of it, since a template only becomes valid code once it's filled in.
      • Test Code says how many problems the editor found, alongside the number it produced. A script can run perfectly and still be wrong — a typo on a branch this run didn't take, or code that never returns at all — so Result: 42 on its own read as a clean bill of health. It now reads Result: 42 — 1 error, 1 warning in this code when there's something underlined. Test Code is still the ground truth: it runs whatever you wrote, and the count never stops it.
      • Dynamic Value Calculation explains itself in one place instead of five. The panel carried two paragraphs and a list of the clock variables, the Stats ? repeated all of it, and the Variable menu listed them a third time — three copies to keep in step, and the wiki had a fourth. The section is now one line with its own ? button beside it holding the full explanation, and the section no longer folds away like the odd one out among its neighbors. The Variable menu keeps only the two lookups that are more than a name; the clock variables come from typing now.
    • Every editor that opens full screen now grows out of the box you were typing in, and they all behave the same way. Going full screen used to make a window appear over the top of everything, with no sign of which field you'd opened — and the four places that offer it had drifted apart: some showed a heading, some didn't, some had a close cross in the corner that did the same job as the full-screen button beside it, and only one of them animated at all. The window now expands out of the field and shrinks back into it, so it always reads as the same box getting bigger. The close cross is gone everywhere: the full-screen button is the way back out, and Escape still works. Headings only appear where the editor doesn't already name itself — a prose field's own caption travels into full screen with it, so it no longer says the same thing twice and spends a row doing it, and on mobile the heading that does appear is centered like the rest of the window. Turning on your system's reduce motion setting opens them without the travel.
    • Every box you type image tags into is the same field now, with tag autocomplete and its own undo. The Prompt Prefix and Negative Prompt in Settings → AI Endpoints → Image were plain text boxes with no completion and no undo of their own, while the editor's Image Tags had both — so the two places you write the same kind of tag behaved differently. All of them — those Settings prefixes, an entity's or location's Image Tags, the prompt boxes in the generate window, and the scene tags in play — are one field: start typing and the Danbooru suggestions appear, and the undo and redo above the box step one tag at a time rather than one letter.

🔧 Fixed

  • 👤 User-facing

    • Code in a help popup or a world's Readme is colored again. Every code block the app renders as markdown — the examples in the ? popups, a world's Readme, the changelog — came out as one flat block of grey text, keywords and strings and comments all alike, which is exactly the text a code example is hardest to read as. The markdown renderer had lost its syntax highlighter in a library update; the highlighting is back, in both themes, and painted from the same colors the stat-code editor uses — so an example in a help popup and the box you retype it into no longer disagree about what a keyword looks like.
    • Import and Export buttons finally point the way the file is going. Export carried a download arrow and Import carried the same arrow again, left over from when they were called Download and Upload. Direction is now read from the app's side: a file arrives on Import (down) and leaves on Export (up). Every place that offers either now agrees — worlds, entities, dictionaries, avatars, saves, stat-code templates and Export Story. The stat-code template pair was the plainest case: its Import and Export sit side by side and pointed opposite to the arrows now used. Publish keeps a cloud so it never reads as a plain file save, Make Available Offline shows a picture coming down, and in Community Creations the three things the button can do are now three different marks rather than two: a cloud with a down arrow to fetch something new, a sync mark when a newer version is waiting, and a backup mark to pull down the version you already have. The details window shows the same mark as the card it opened from, where before it offered the words alone. Duplicate World carries the copy icon.
    • Community Creations gives two more rows of worlds back on mobile. Its header took four rows before the first card — the screen's own name, then the Worlds/Entities/Dictionaries tabs, then search, then the filter toggle. The title comes off (the header is the only thing on screen, so it named what you were already looking at), search and refresh take its row, and the tabs share the next row with a right-aligned Filters. Two rows instead of four, and the wide layout is untouched.
    • The Aliases field says its two rules and leaves the rest to the ? button. It explained itself three times over — a hint in the empty box, a paragraph underneath, and the ? popup that covers all of it properly with examples. The paragraph is gone and the hint is down to what you need while typing: press Enter after each, and they're case-sensitive.
    • A world's card shows your edits the moment you come back from the World Editor. The card stays open behind the editor, so it's the first thing you see on the way out — and it kept showing the name, description, author, thumbnail and edit date from before you started, until you closed it and clicked the world again. It now re-reads the world when the editor closes.
    • Save goes back to greyed out once you undo an edit. The editor decided whether a world had changed by comparing text, so two things it couldn't tell apart from real edits left Save lit and the "unsaved changes" warning showing over a world nobody had altered: adding something to an empty field and then taking it away again (an alias, a keyword, a tag), and any edit to a record that caused its fields to be written back in a different internal order. Both now read as what they are. The same applies to the standalone Character and Dictionary editors, and nothing about how worlds are stored has changed.
    • Keyword boxes take a click anywhere in them, and their hint sits where the typing starts. In a world that defines placeholders, the editor inside boxes like Aliases and dictionary Keywords was only about an eighth as wide as the box drawn around it — clicking the right-hand side of one landed on nothing, and the grey hint was indented a few pixels from where the text actually began. The editor now fills the box.
    • Picking a tag from the autocomplete leaves you ready to type the next one. Accepting a suggestion at the end of a line put the caret hard against the tag with no comma after it, so the next thing typed ran into the tag just picked. A , now comes with the completion anywhere one isn't already waiting.
    • A generated image goes into an empty slot instead of over the picture already there. Generating for an entity that already had a portrait wrote straight over it, so the only way to add a second was to upload one. It now fills the first free slot and frames it. When there is no free slot — an entity is allowed two images of its own — it asks which picture it should take the place of, showing each one to pick from, and starts on whichever you were looking at. Cancel leaves everything as it was and hands you back to the generate window with your image still there, so nothing is lost to a misclick. Slots holding a linked image aren't offered, since replacing a link with a generated picture would put the entity over its allowance.
  • 🛠️ Developer tooling

    • The icon set is 18 months newer, so around forty icons are redrawn. Formamorph was pinned to a version of its icon library from August 2024. Menus, hearts, stars, speaker and playback marks and about three dozen others are the same symbols drawn to the library's current proportions. The GitHub mark is now drawn in-app, since the library stopped shipping company logos.

✅ 2.11.1 — Released 2026-08-10 — find and replace across every field a world holds text in, placeholder chips renamed everywhere at once, and a round of editor polish (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing
    • The World Editor can search every field in a world at once, and replace what it finds — with text, or with a placeholder. Renaming a character, retiring a term, or noticing that the same phrase was typed into nine descriptions meant opening each tab, selecting each entity in turn, and reading. Press the magnifier beside the editor's title — or Ctrl+F — and a find bar opens over the editor. It searches everything the world holds text in: names, descriptions, summaries, system prompts and readmes, stat descriptors, dictionary entries and their keywords, entity aliases, location connections, image tags. Typing counts the hits as you go, and Next and Previous walk them: the editor switches tabs, opens the right entity or entry, scrolls both the list and the field to it, and rings the field with the matched words highlighted inside. Focus stays in the search box the whole time, so you can keep typing or keep stepping without clicking back. Aa makes the search case-sensitive and W matches whole words only, so fen stops finding fennel. Press Ctrl+H, or the chevron on the bar, for the replace row: Replace changes the one you're looking at and moves on, and Replace All tells you what it's about to do — Replace 23 matches across 9 fields? — before it does it. There's no separate undo; as everywhere else in the editor, Discard Changes is what takes it back.
    • Double-clicking a placeholder chip renames the placeholder, everywhere it is used. Renaming one meant leaving whatever you were writing, finding it in the Placeholders tab, and coming back. Double-click any chip — in a description, in the Placeholders strip above the panel, or a value in the placeholder's own list — and it becomes a text box in place; Enter commits, Escape abandons, and every other chip standing for the same placeholder takes the new name with it. Single-clicking still does what it did: opening a wildcard's World/Unique choice, or its draw weight.
    • Replace can put a placeholder chip in, not just text. In Advanced mode the replace row has a toggle that switches it from a text box to a placeholder picker, so every mention of a name can become the placeholder that stands for it in one pass — and if the placeholder doesn't exist yet, Create at the bottom of the picker makes one from what you searched for without leaving the bar. Each occurrence gets its own chip, so a wildcard set to roll per placement still rolls separately at each one. Fields that can't show a chip — a stat's description, an entity's type, a dictionary entry set to regex — are left alone rather than filled with a code that would only ever read as gibberish, and the bar says how many were skipped and where.

🔧 Fixed

  • 👤 User-facing
    • The Placeholders strip inserts into the field your cursor is in, and is greyed out when it isn't in one. It used to insert into whichever field you had typed in most recently, however long ago you left it, so a click meant for something else could drop a chip into a field elsewhere on the panel. Dragging a placeholder into a field is unchanged and still needs no cursor.
    • The sign that a world uses hidden advanced features now sits on the Advanced switch itself, as a dot. It was a separate ℹ️ beside the switch, which on mobile cost room the editor's own controls needed. Hovering the switch still explains it.
    • A chip keeps its width when you double-click it to edit. Placeholder values and world tags visibly shrank the moment they became a text box, so the row of chips shifted under the cursor mid-edit.
    • Clicking a placeholder value a second time closes its Draw Weight box. It reopened instead, leaving no way to dismiss it except clicking something else entirely.
    • Player notes survive Re-generate. Re-generating a turn you had stopped mid-generation wiped the Notes panel; notes now carry through Re-generate unchanged, and rolling back to a stopped turn restores its notes instead of clearing them.

✅ 2.11.0 — Released 2026-08-09 — the authoring release: the World Editor opens simple and grows on demand, images drag in and share one pane, and either description can be written from the other (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing

    • The World Editor introduces its Simple / Advanced switch the first time you see it. The switch decides how much of the editor is on screen, and it starts on Simple — so an author who never noticed it had no way of knowing that tabs and fields existed at all, and an author who'd used the editor before the switch arrived would simply find things missing. A short note now appears beside the switch shortly after the editor opens, pointing out that it's there and that Advanced shows everything. Press Got It, or just use the switch, and it doesn't come back; a stray click elsewhere leaves it alone, and it waits for you on your next visit if you never answered it. Settings → Accessibility → Help has a Reset tutorials button that brings back every explanation you've dismissed.

    • The World Editor opens in Simple mode, with everything the power-user features add tucked behind an Advanced switch. A new author's first look at the editor was seven tabs and a wall of fields — regex options, scan depths, stat scripts, placeholder pins — most of which only matter once you know you need them. The editor now has a Simple / Advanced switch beside its title, and it starts on Simple: the Placeholders tab is put away along with the chip strip that goes with it, and the fields that only a practiced author reaches for — an entity's aliases, summary, type, 3D model and booru image tags; a location's summary, ambient sound and image tags; a stat's descriptors, AI locks and calculation script; a trait's stat availability and placeholder pins; a world's custom narration prompt — stay out of sight until you ask for them. The Dictionary stays, thinned to what a lorebook actually needs: a name, its trigger keywords and its text, with the matching controls (always inject, regex, recursive, scan depth, secondary keywords), the Background/Foreground split and the enable toggles waiting in Advanced. Adding entities and traits is a single button rather than a menu offering folders, and the Entities and Dictionary footers offer Add — bringing a character or lorebook in from your library — while exporting one to hand to somebody else, and the bulk Optimize Images pass, wait for Advanced. An image that arrives too large still offers to shrink itself as you add it, in either mode. If the world you're editing uses anything Simple puts away, an ℹ️ marker beside the switch says so, so a world is never quietly missing pieces you can't find. The choice is remembered on your device, applies to every world, and is never part of a world file, so switching to Advanced gives you the editor exactly as it was.

    • The markdown toolbar covers the rest of markdown, grouped so the row stays short. It offered nine buttons — two heading levels, two list types, and no way to reach anything else — so a readme wanting a third-level heading, a checklist, a table or a horizontal rule had to be typed by hand, and there was nothing on screen to suggest the app even supported them. Headings, lists and inserts are now split buttons: press the face to apply what you used last from that group, press the chevron beside it to pick something else, and what you pick becomes the new face. Behind them: headings 1–4, bulleted, numbered and task lists, and an Insert menu holding link, image, table, code block, horizontal rule, and subscript/superscript. Strikethrough joins bold, italic, inline code and quote on the always-visible row.

    • An image being shrunk shows a bar over the slot it's landing in, with the image itself behind it. Adding a large image re-encodes it, and the only sign of that was a small toast in the corner reading "Optimizing images… 0/1" — which never said which image, so adding several meant guessing. The slot itself now shows the work: the image dimmed under a bar, and the slot ignores clicks until it's done rather than letting a second press re-open the file picker mid-encode. Adding several at once counts them — Converting 3 of 5… — while a single image gets a bar with no percentage, because the conversion happens in one step that can't report a position; claiming one would be made up. The bar appears only once you've chosen to shrink, so it never sits behind the question asking you. Nothing moves for anyone who's asked their system for reduced motion, and the corner toast still covers the whole-world optimize passes.

    • An entity or location's images share one pane: a framed image with a strip of thumbnails under it. Every extran image used to add another full-size upload box to the panel, so a character with four of them was four dashed frames tall and you scrolled past three to reach the tags underneath. There is now one framed image — portrait for a character, landscape for a place — with a row of thumbnails beneath it; press a thumbnail to bring that image into the frame, and the last tile in the row is the one that adds, taking a click, a drop, or a pasted link. Drag the thumbnails to reorder them, which is also how you choose the one that stands in wherever a single image is shown — it's simply the first, starred in the strip, so there's no separate "make primary" step to find. The order you set is the order the game shows them in. Dragging keeps the image you were looking at in the frame even when it moves, and a plain click still just brings an image into the frame. A subject that only holds one image is unchanged.

    • Drag an image onto any image slot to add it. Every image field — entity images, location backgrounds, the world cover, dictionary covers — took a file only through the picker or a link only through the URL box, so the gesture everyone tries first did nothing. Drop an image file onto the slot and it lands; while a drag it can take is overhead the frame is ringed and its prompt reads Drop to add, so it's obvious before you let go — and on an entity or location the pane brings the empty slot into view as the drag arrives, so what you're looking at is where the image is going rather than one it was never going to replace. Dragging an image straight out of a browser tab works too, and stores it as a link rather than as bytes — the same as pasting its address, so it costs a published world nothing. On an entity or location, which hold several images, dropping several files at once fills the empty slots in order and asks about shrinking them once for the whole batch instead of raising a window per file. A slot that already holds an image ignores a drop: replacing one still means removing it first, so a stray drag can't quietly overwrite the image standing in for a character.

    • Each of an entity's or location's two descriptions can be written from the other. The Player-Facing Description and the AI-Facing Description say the same thing to two different readers — one is prose the player reads, the other is reference the narrator draws on — so writing a character or a place meant writing it twice. Both fields now carry the ✨ button the summary field already had. On the player-facing field it turns your reference note into what a player sees, keeping the secrets, plans and private history out of it; on the AI-facing field it works the other way, expanding a short blurb into the fuller note, staying with the facts the blurb already states. Either one fills in whatever the other field holds, so the button is off until there's something to work from, and what it writes is a draft to edit rather than a final answer — undo puts your own text straight back. Placeholder chips in the source text don't survive the rewrite; add them back to the result.

  • 👤 User-facing

    • The main menu's colored buttons are set at reading size. Enter World, Quick Start, Edit World, Duplicate World, Export World, Make Available Offline and Publish World — the whole column that opens when you pick a world — were set at the size used for a checkbox label, which is smaller than the thing they are: the main way into the game. So were the buttons across the top of the menu, Community Creations, New, Import and Admin Panel, and Download World on a world in Community Creations. Every one of them now reads a step larger, so the colored buttons match each other wherever they turn up. The buttons themselves are the same size and in the same places.
    • The small print under each setting is bigger. Every description beneath a setting, and the ⓘ popovers behind them, were set two steps down from the label they explain — small enough that the sentence telling you what a toggle actually does read as fine print you were meant to skip. They now sit one step down instead, matching the size the rest of the app uses for secondary text. Same for the text on the first-run setup screen, where a new player is told to start their model server and wait — the one screen where nothing else is on the page to read. Counts, timestamps, download sizes and progress readouts are unchanged: those are values at a glance, not sentences.
  • 🛠️ Developer tooling

    • bridge-probe.mjs scores both directions of the World Editor's description generators. The two new generate buttons rewrite one description into the other, and the thing that can go wrong — a private note reaching the text a player reads — is invisible in a single good-looking sample. The probe runs four authored subjects, two characters and two places, whose reference notes carry three kinds of fact: what a player could see, what is genuinely private, and what is physically present but authorially withheld. It scores leakage against the private set only, keeps visible-fact recall as the guard against a rewrite that stays clean by saying nothing, and prints the words around every hit, because the first cut of the metric counted a missing bell as a spoiler. It compiles the prompts out of src/lib/bridgeDescription.ts in memory, so it always tests the shipped text; --promptfile runs an older snapshot for a paired baseline, and --dump writes every completion for re-scoring without another run.
    • A component handed to asChild has to forward its ref, and lint now says so. Radix renders an asChild trigger through Slot, which passes the child a ref — and a child that drops it leaves the control half-dead, working under a mouse while doing nothing on mobile. Nothing caught it: asChild puts no constraint on the child, so typecheck was happy, and the only signal was a React warning in the console. That is exactly how the main menu's hamburger broke. eslint.aschild-forwardref.js now walks every asChild element and errors on a component child that isn't on its verified list, naming the fix in the message. Native elements are skipped — they always take a ref — and so are namespaced Radix parts, so today's call sites pass untouched. The one gap it can't see through is a trigger that forwards {children} from its own caller.
    • Text sizes are named for what the text is, not how big it is. text-xs meant two unrelated things — "this is secondary text" and "this control is deliberately compact" — and nothing in the class told them apart, which is how three correct compact rows got resized during an unrelated font fix. There is now a role-keyed scale in tailwind.config.js (display, heading, title, body, label, helper, meta), each pinning size and line-height together and mapped onto the exact values already in use, so naming a role changes nothing on screen. Density moved off the type scale and onto the controls: Input, Textarea and SelectTrigger gained a size variant the way Button already had one, plus a new Button size="xs", so a compact row is <Input size="sm"> rather than text-xs h-8. The shadcn primitives consume the tokens, and the untemplated half of the app's text — hints, inline errors, section headers, counts — gets Hint, FieldError, SectionTitle and Meta as one variant table. All 549 existing call sites moved onto the tokens by codemod, one-for-one onto the size each already rendered at, so the sweep changed no pixel — and an eslint rule now rejects raw size utilities in className everywhere outside the primitives, so the mess can't come back with the next feature. Two exceptions stay raw and say why in a comment: an avatar's initial scales with its circle, not with the type scale.

🐛 Fixed

  • 👤 User-facing
    • Editing a turn's text uses the same editor the World Editor does, markdown toolbar and all. Edit Text was a plain box: rewriting a paragraph meant typing ** and > from memory, with no undo beyond what the browser happened to offer and no way to see how the result would read. It's now the app's markdown editor — bold, italic, headings, lists, quotes, links and tables on the toolbar, real undo and redo, a Preview tab that renders the turn exactly as the game will, and a full-screen button for a long rewrite that grows the window itself rather than stacking another one over the Save button. The window is bigger, and now opens at one size whatever the turn is — a long turn scrolls inside the editor instead of stretching the box — with Cancel and Save on their own row beneath it.
    • Your own action renders as markdown, like the narration under it. Anything you typed showed as literal text, so a *whispered* aside stayed asterisks while the same words in the narration below came out italic. The two now read as one page. Line breaks you type are still kept.
    • The Re-generate and Rollback buttons are set at reading size. Both sat a step below every other button in the game view — the smallest size the app has, on the control you press to redo a turn.
    • The image-optimization progress toast shows on top of the window you started it from. Kicking off a world's optimize pass from inside Community Creations — a window that covers the whole screen — put the toast reporting its progress behind that window, so the only sign the work was happening was hidden until you closed the thing you were working in. Toasts now sit above every full-screen window, wherever they're raised from.
    • Every list in the World Editor is built the same way. No two tabs quite matched: on Stats, Stat Updates and Placeholders the duplicate and delete buttons sat flush against the name where Entities, Locations, Traits and Dictionary had a gap; the flat lists left more air between rows than the trees, so the same row looked taller on one tab than another; a top-level row on a tree started further left than a row on a flat list; and a long name trimmed with an ellipsis on Dictionary but squashed the buttons everywhere else. Every list now draws through one row: the same height, padding, indent step and spacing on every tab, a name that doesn't fit trims instead of crowding the buttons, and the buttons keep their size no matter how long the name is.
    • The row of placeholder chips above a prompt fits the panel it sits in. The Insert: strip in Settings → Prompts kept the full width of its chips no matter how much room it had, so it ran past the edge of the panel: on a narrow window it hung off the side instead of scrolling sideways, and on a wide one it stayed a single overhanging row instead of wrapping onto two. It now fits the space it's given at both sizes.
    • Every tag box is the same box now. Image Tags changed shape depending on the world you opened it in: a world with placeholders got the chip editor, a world without got a plain text box, and the scene-image and image-generator tag boxes — the Prompt and Negative prompt both — were plain boxes too. They are all the chip editor now, three rows tall and draggable by the corner like every other multi-line field, so a placeholder tag works everywhere, the Danbooru autocomplete offers the same completions in each, and every one of them has undo and redo. The negative prompt gets no ✨ button: the AI writes what an image should contain, never what it shouldn't. Nothing about typing changes — tags stay comma-separated text, and only a {Placeholder} is ever drawn as a chip.
    • One set of undo/redo buttons per field, and generating from the AI is undoable. The AI-Facing Summary and Image Tags fields carried their own undo, redo and generate buttons, which sat in the same row as the field's own undo and redo — two identical pairs walking two different histories, in outlined boxes nothing else in that row wore. There is now one pair per field, plus the ✨ generate button, all styled as one toolbar. Undoing a generation works throughout: filling a field from the AI, resetting a prompt to its default, or switching prompt templates now steps back with a single press, where before an untouched field had nothing to undo at all. Image Tags and the image generator's Prompt box gained real undo for the first time, stepped by tag — adding or removing one tag is one press, typing its letters isn't, and a generated list comes back in one.
    • Pressing a formatting button a second time takes the formatting off. Every button only ever added, so bolding an already-bold word gave ****word**** and pressing Heading 1 twice produced # # Title — the only way back was to delete the markers by hand. Pressing a button now removes the formatting it applied, whether your selection sits inside the markers or covers them, and heading and list buttons replace each other rather than stacking, so a bulleted line becomes a numbered one in one press. Bulleting a line inside a quote keeps it in the quote.
    • Image generation is a single switch that puts away everything it turns off. The control read Disable Image Generation and sat halfway down Settings → AI Endpoints → Image, with a screen of endpoint, model and sampler settings still sitting beneath it after you'd switched the feature off — and Scene Images, the setting for drawing every turn automatically, lived a tab away under Generation, nowhere near the thing it depends on. It's now Enable Image Generation, ticked by default, sitting directly under the preset row with Scene Images beside it, and unticking it hides the whole panel below it — and the Tag Prompt tab with it — rather than leaving settings for a feature that isn't running. Your existing choice is carried over as-is.
    • The World Editor guide documents entity aliases, exclusive trait groups, unique-placeholder collisions, and which AI steps are sent a location's full description. Entity Aliases had shipped without a section anywhere in the guide, so the rules that decide whether one fires were undocumented: they match case-sensitively, meaning an alias written matron never catches "the Matron", and an alias beginning with "the" misses at the start of a sentence — exactly where narration puts a title. Both are written down now, with the two kinds of alias worth avoiding: a generic job title, which fires on any passing character of that trade, and a role the story uses for someone off-page, which marks that character present in scenes they aren't in. Exclusive trait groups had the same gap — the guide covered groups without mentioning the checkbox that turns one into a pick-one, or that an exclusive group wants a default so "nothing selected" never reaches the AI. The Locations page said the AI is given the current location in full, when only the narrator is; the choice writer, the continuity planner and the location router are all sent the summary, so anything living only in the full description reaches one step and no other. And Unique placeholders were described as rolling independently without noting that independent draws can land on the same value — which is how two "unique" towns end up sharing a name.
    • The stat code guide explains that Math.random() is reseeded on every run, so two stats in the same turn draw the same number. It used a random factor in a worked example without the caveat. A once-per-turn roll is unaffected in real play; the note covers when the repeat does bite, and how mixing in a clock variable gives an independent one.
    • The world format reference lists the entity, trait and trait-group fields the app actually writes. The entity table was missing aliases, groupId and order, and listed a singular image where the app writes an array of images — the singular survives only as a legacy fallback. Trait groups had no field table at all, leaving exclusive undocumented, and traits were missing playerToggle, statToggles and placeholderPins. isStarting claimed a start location is always picked at random, when flagging several lets the player choose between them.

✅ 2.10.0 — Released 2026-08-07 — the routing release: every prompt can send to its own AI endpoint, with linked images, placeholders in names, and a prompt editor you can actually work in (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing

    • The desktop app's built-in model is an endpoint like any other. It used to be a mode — a Use My Own Endpoint checkbox that swapped the whole app between the bundled model and everything else — which meant per-prompt routing couldn't see it: pinning a prompt to it while running on another endpoint fired at a model the app had just shut down. The checkbox is gone. Built-In Engine is now simply one of the presets in Settings → AI Endpoints, so you can keep the story on your own machine and send the summaries to a cloud model, or the other way round, and the engine starts and stops based on whether anything still points at it. Default now means the hosted endpoint on desktop too, so a desktop install can reach it for the first time. Nothing to do on upgrade: if you were running the built-in model you stay on it, and if you were pointed at your own API you stay there.

    • Send each prompt to a different endpoint. Formamorph asks the AI for a lot of things besides the story — the choices, the stat changes, the memory summaries, the image tags — and until now every one of them went to whichever endpoint preset you had selected, so a big expensive model was doing work a tiny one could do, or a small local one was writing your prose. Every prompt in Settings → Prompts now has an Endpoint dropdown under Options: leave it on Use Active Endpoint and it behaves exactly as before, or point that one prompt at any of your endpoint presets. Keep narration on a large model and hand the summaries to a fast local one; run the planning stage on a reasoning model and the writing on a prose model; whatever split you like. Each routed prompt uses its own endpoint's context window and output limit, detected the first time it's used, and the sampling and reasoning controls beneath it describe the endpoint it actually sends to. Routing belongs to the prompt preset you're using, alongside its per-prompt sampling and reasoning settings, so a preset tuned for a small local model can send its own prompts wherever suits it — and switching presets switches the routing with it. Like the rest of those settings it needs one of your own presets: the built-in Default, Simple and XML are read-only, so duplicate one first. It's never carried by a shared preset, since endpoint names mean nothing on someone else's machine, and deleting an endpoint preset simply returns the prompts that used it to Use Active Endpoint. A routed prompt also shows whether that endpoint is actually up, right under the dropdown — reachable, reachable but not serving the model you named, or not answering — with a Recheck link for when you've just started the server. Only routed prompts are checked, since an unpinned one uses the endpoint the app already tests on startup. And the AI context viewer now labels every captured request with the endpoint that served it — the preset name and model, with routed ones marked — so when one prompt in a turn comes back wrong you can see at a glance which machine wrote it. The endpoint's address is on hover, and the label travels with the Export file; your API key never does.

    • Placeholder names show as colored chips instead of raw {braces}. A name holding a placeholder used to spell itself out in the editor's lists as Keeper of {Sedge|Marrow}. It now draws a small coloured chip showing what the name will become — Keeper of ⟨Sedge|Marrow⟩ — in that placeholder's own colour, naming it on hover. Inside a field you're editing, the chip carries the placeholder's name instead, so it stays one word wide however long the list of values gets. A placeholder you've since deleted now shows a red ? instead of quietly vanishing. Places that can't draw a chip at all — dropdowns, the search box, file names, and anything shared or published — still spell the values out.

    • Placeholder chips now go in names, aliases, lore keywords, and booru image tags. A chip could go in a description but not in the thing being described — so a world with a randomly-named tavern keeper had to call her something fixed in the Name field and hope the description carried it. Chips now go in names: entities, locations, stats, traits, and dictionary entry names all take them, and the game resolves a name everywhere it matters — what the storyteller is told, what it matches when it writes that character into a scene, and what you read on screen — so the player and the AI never end up talking about two different people. A nickname or a lore trigger is a name like any other, so aliases and dictionary keywords take chips too: an alias can read "old {Town} keeper", and a dictionary entry can be triggered by whatever the world rolled its tavern keeper's name to be. So do the booru Image Tags on an entity or a location, so a character can be tagged 1girl, {HairColor} hair and the image generator draws a different one each playthrough — the Danbooru tag autocomplete works alongside it as before (type { for a placeholder, keep typing a tag for the usual suggestions), and this is the booru tag box only, not the Overview tab's Tags for finding a world in a list. Everywhere: type { to insert. A Wildcard in a name is rolled once per playthrough and frozen into the save like any other, and Unique still gives each placement its own roll, so one "Tavernkeeper Names" wildcard can name two different keepers in two different towns. A keyword tag holding a chip shows it as a chip rather than as raw code, and double-clicking it opens the same chip editor; keyword fields set to regex stay literal, since braces already mean something in a pattern. Sharing still works: a character card or lorebook now carries the placeholder definitions its name and aliases use, not just the ones in its descriptions, so a character whose name is a wildcard arrives intact.

    • Wildcards are rolled when you enter a world, so the setup screens show real names. A world's wildcards were rolled the moment the story began — which is one screen too late, because the screens before it are the ones asking you to choose. Picking a starting location in a world whose town name is a wildcard offered {Sedge|Marrow} Square and {Sedge|Marrow} Docks, two places that read as unrelated rather than as one town, and a trait called "Native of {Sedge|Marrow}" made you commit before knowing where you were from. The roll now happens when you enter the world, so every setup screen — traits, starting location, characters, lorebooks — reads the real names: Marrow Square and Marrow Docks, "Native of Marrow". A trait that pins a wildcard still wins, and now visibly: tick it and every value on the screen changes to match, untick it and the rolled one comes back. Backing out of setup entirely rolls again next time, so a world you haven't committed to isn't stuck with one draw. Loading a save is unaffected — it keeps the values it was saved with, exactly as before.

    • A trait's card always describes what picking it does, not what you've already picked. A trait can pin a wildcard — Redhead fixes hair to red — and any trait whose own name or description used that wildcard changed wording every time your selection did: tick "Native of Sedge" and the rival trait suddenly read "Sworn to Sedge" too. A trait's own card — its name, its description, and the stat names it lists — now always reads with its own pins, so it advertises what picking it does rather than echoing whatever is currently ticked. Everything outside the cards (the stat bars, locations, the story itself) still follows the traits actually in force, and the storyteller is told a trait in the same words its card shows you. If two active traits pin the same wildcard to different values, each card keeps its own word while the world follows the later one.

    • Your location's name follows the traits you picked, everywhere it's shown. Where you are was recorded the moment you arrived, wording and all — so in a world whose town name is a wildcard, a trait that fixes that name left the location behind: the setup screens and the map said "Marrow Square" while the Location tab, the log, and the storyteller were still told "Sedge Square". Your location is now looked up fresh wherever it's shown, so it always reads the same as everything else, and switching such a trait on or off mid-story moves it with everything else.

    • A single row of placeholder chips per panel, instead of one above every field. With the name fields now accepting chips too, the insert row that sat above each field would have repeated the same chips five or six times down a single panel. There is now one strip at the top of the panel, which stays put as you scroll and drops into the field you were last typing in — and greys out the moment your cursor lands somewhere a chip can't go, so a click can't drop one into a field you're not looking at. Fold it away with the Placeholders heading and it stays folded everywhere until you open it again. The faster way in is to just type { in any field that takes chips — the list appears at your cursor, narrows as you keep typing, and Enter drops the chip in. Empty fields say so, so there's something to find.

    • Point an image at a link instead of uploading it. Every image field — entity images, the world cover, location backgrounds, dictionary covers — now takes a web address as well as a file. Paste one into the Or paste an image URL box under the upload area and the image is used without its bytes being stored in the world, which keeps a published world small no matter how many images it carries. A linked slot is marked with a small link icon, and the editor flags a bad link where you paste it rather than letting it turn up blank mid-story: a Discord attachment link gets a plain warning that it will stop working, since Discord expires those (its permanent addresses — avatars, emojis, server icons — aren't flagged, because they're fine), and a link from a site that won't hand its images to other programs gets an amber marker explaining it'll show online but can't be saved for offline or put into a character card. Publishing a world whose images use expiring links warns you before it goes out, since that breakage lands on everyone who downloads it — it warns, it doesn't stop you. Linked images are downloaded to your device the first time you see them, so a world you've played stays viewable offline; Settings → Saves & Worlds shows how much space that's using and clears it. Exporting a world asks whether to keep the links (small file) or download the images into it (works anywhere), and exporting a character card always downloads the portrait, since a card is the image. A site that refuses to hand its images to other programs can still be shown, just not downloaded — the desktop app has no such limit.

    • Download a world's linked images in one press, before you lose your connection. A world that links its images gets a button in its details window that downloads them all at once, so it's ready before you lose your connection rather than only covering what you happened to look at. Already-saved images are skipped, so pressing it again after playing a while only fetches what's missing, and any that couldn't be downloaded are counted rather than failing the whole run. Only offered for worlds that actually link something.

    • Settings fills the screen on mobile. It was drawn as a floating panel with a margin all the way around — on mobile that spent well over a hundred pixels framing a window that was already too small, and the prompt editor inside it was the thing paying for it. Settings now fills the screen on small displays and keeps its window on larger ones. Two more space-savers came with it: the row of insertable chips scrolls sideways as a single line instead of stacking three rows deep, and the note explaining that a built-in preset can't be edited is now one line with the full explanation on hover. Together these roughly double the amount of a prompt you can see on mobile.

    • A built-in preset's Options tab explains it's locked, with a one-click editable copy. A built-in preset's prompt text explains itself — a lock, the reason, and a Duplicate & Edit button that makes an editable copy in one click — but the Options beside it had none of that, so its dead dropdown, checkboxes and sliders just looked broken. The same notice now sits at the top of Options, with the same one-click way out.

    • Each prompt's description sits above the editor, and four prompts got one for the first time. The one-line explanation of what a prompt does sat underneath the editor, which on a full-height prompt put it below the fold — the one place a description is no help. It now sits above the prompt. Four of them never had one at all (Narration, Planning, Choices, Stat Updates), and the rest have been rewritten shorter; the old "only used when such-and-such is on" endings are gone, since a prompt whose feature is switched off no longer appears in the list to begin with.

    • The preview scrolls to your cursor, and full screen brings the whole Prompts panel. Editing a long prompt with the preview beside it meant the two drifted apart — you'd be writing near the bottom while the preview showed the middle. Click into the text and the preview now moves to wherever your cursor is, so the resolved version of the line you're writing is always the one next to it; scroll normally and it behaves as before. Going full screen from Settings → Prompts now brings the whole panel — the prompt list and the preset row, not just the editor on its own — so you can move between prompts without leaving it. The values in the preview that come from your own settings, like the length and formatting guidance, now show your real settings rather than a stand-in; only the parts that need a world to exist are samples. And the group headings in the prompt list read as dividers instead of looking like buttons that don't do anything.

    • Prompt previews work from the main menu, and the prompt list is grouped down the side. The prompt editor's Preview swaps each chip for what it will really contain — but those values only existed while you were playing, so from the main menu the pane was empty and the side-by-side view never appeared. It now falls back to sample content: a small invented town, a couple of characters, some stats, clearly marked Sample data so it's never mistaken for one of your worlds. Preview and the side-by-side view work everywhere as a result. The thirteen prompt buttons that used to wrap across three rows are now a list down the side, grouped by what each prompt does — Story, Trackers, Memory, Images — with the open prompt showing its own parts beneath it, and the whole list becoming a single dropdown on mobile. And a built-in preset now says why it won't let you type instead of just refusing, with a button that makes an editable copy in one click.

    • Every prompt field opens full screen, with the preview beside it. The prompts are long — the narration one runs to several screens — and the box you edited them in showed about a quarter of that on a desktop and a tenth on mobile. Every prompt field now has a ⛶ full screen button, and inside that view Edit and Preview sit side by side instead of taking turns, scrolling together so the resolved text always lines up with the part you're writing; a button beside the full-screen one switches back to one-at-a-time whenever you'd rather have the width to yourself. Side by side lives in full screen only, because it's the only view with the width for it — a field sitting in a column of a form keeps the Edit / Preview buttons it always had rather than being chopped into two columns too narrow to read. Narrow windows and mobile keep the one-at-a-time view automatically, since two half-width columns help nobody. On mobile, tapping into a prompt opens it full screen straight away — roughly three times as much of it on screen — and you swipe between the writing and the preview, landing in the same place in both. The same buttons appear on every prompt-shaped field, including the world description, readme, and dictionary entries.

    • Run a world on its own prompt preset. If you keep more than one prompt preset — a terse one, a florid one, one tuned for a small local model — a world's details window now has a Prompts dropdown to pin that world to one of them. Every prompt, the section style, and the sampler and reasoning settings that preset carries all come along, exactly as if you'd switched presets by hand, and it happens automatically each time you enter that world whether you start fresh or load a save. The pin sticks to the world, not to your global choice: your usual preset is still there for everything else and is waiting for you the moment you leave. While you're in a pinned world, Settings → Prompts shows and edits the preset that's actually running and says so, and picking a different one there re-pins that world rather than changing your default. Pins live on your device only — never exported, never published, and a world you download never arrives with one. If you delete a preset a world was pinned to, that world quietly goes back to following your global choice.

    • A world can bring its own narration prompt. Until now a world could add to the storyteller's instructions but never change them, so a world wanting a different kind of storytelling — say one built around conversation rather than events — was stuck with whatever narration style the player had set. World Editor → Overview → Custom Narration Prompt now lets an author replace that prompt outright, written in the same chip editor as your own prompts — full screen, side-by-side preview and all — and starting from a copy of the standard one rather than a blank page. The preview is filled in with the world you're editing: your starting location, the characters you placed there, your stats at their starting values, and the traits you marked default, all built by the same code the running game uses, so what you read is what the AI will get. The parts only a turn can supply (the player's action, the previous narration) stay illustrative, which is what the Your world, sample turn marker on the pane means. Only the narration is replaceable: choices, planning, stat tracking, memory, and the clock always run on your prompts, so a world can change how the story reads without being able to break how the game works. A world that carries one says so in its details window, where you can read the exact text before playing and untick Use this world's narration prompt to keep your own — a choice remembered per world and never shared or exported. Switching the option off in the editor keeps what you wrote and hands it straight back when you switch it on again, so a stray click can't cost you the prompt.

    • "Download" is now "Export" for anything already on your machine. Saving a world, a character, a lorebook, a save file or a prompt preset to a file was called downloading it — but nothing is being fetched; the thing is already yours, and the file is going out, not coming in. All of those now read Export, matching the wording Export Story and Export Avatar already used. Download is kept where something genuinely arrives from elsewhere: Community Creations, model downloads, and app updates.

    • Undo and redo buttons on every prompt field, and Undo no longer does nothing. The buttons lived in the markdown formatting toolbar, so only the prose fields had them — the world description and readme. Every actual prompt, including the world editor's Custom Narration Prompt, had none: the keyboard shortcuts worked, but nothing on screen said so. They now sit beside the full-screen button on every prompt-shaped field. Pressing Undo also had no effect where the button did exist, because clicking it moved focus out of the editor and took the cursor with it; it holds focus now and undoes what you'd expect.

    • Export a world's player avatar from the World Editor. A .vrm added to a world in the World Editor could be previewed and replaced, but never saved back to your machine — so a world you'd been handed was the only copy of the avatar inside it, and rebuilding a lost original meant exporting the whole world and digging through the file. Preview now has Export Avatar at the bottom of its panel, exactly as the avatar library's details window does, and it saves the file under the name it was uploaded with. Nothing is re-encoded: the bytes that went in are the bytes that come out, so the model's own metadata and license travel with it untouched.

    • An unfinished bug report is kept when you close the window. Closing the report window threw away everything you'd typed, which made the two things you most need to do while writing one — go back and check what actually happened, or fetch the exact wording of an error — cost you the report. What you've written is now kept: close the window, look at whatever you needed to look at, reopen it and carry on from the same place. It survives a reload too, so reproducing a crash doesn't cost you the description of it. A reopened report says it picked up where you left off and comes back on the branch you were writing — bug or suggestion — whichever button you used to reopen it. Discard Draft throws it away deliberately, and sending it clears it as you'd expect. The old Cancel button is now Close, since it no longer cancels anything.

    • Fix a made-up character's description, or have it rewritten. When the story invents a character the world never defined, Formamorph writes them a description so they stay recognizable on later turns — but it writes it from the single passage they first walked into, before anyone knew who they'd turn out to be. Twenty turns later that description can be plain wrong, and a bad first attempt was permanent. Opening such a character now gives you Edit, to rewrite the description yourself, and Regenerate, to have it written again from everything the story has shown of them since. A regenerated description is shown to you first, with Keep and Discard, so a worse attempt costs you nothing. Regenerate waits until the story has finished generating, so it never competes with the turn you're waiting on. Authored characters are untouched: they belong to the world, and playing never rewrites it.

    • Export a world without opening the editor. Saving a copy of a world — to back it up, to hand it to someone, to move it to another device — meant opening the World Editor and finding the button in its footer, which is a strange detour for something that doesn't involve editing anything. A world's details window now has Export World beneath Duplicate World, and it does exactly what the editor's button does: the same offer to shrink oversized images first, the same choice between keeping links and downloading the images into the file, and the same file at the end.

    • An entity can carry any number of images, as long as only two are uploaded files. An entity may now carry as many images as you like, provided only two of them hold their own bytes — link the rest and the gallery keeps growing. The old cap was a flat two images however they were stored, which charged a linked image against an allowance it costs nothing to fill. Once both upload slots are spent the file picker steps aside and says so, leaving the paste a link box in its place; freeing one brings it back. Generate with AI follows the same rule, since a generated image is bytes like any other. A world already holding more images than that is left exactly as it is.

  • 🛠️ Developer tooling

    • An on-screen readout of the live viewport numbers, for debugging on a real phone. #dev?probe=viewport (or window.__fmDev.probe('viewport')) pins a live readout of every measurement the keyboard-aware sizing depends on — both viewports, the visual viewport's offset and scale, the published variables, and the app frame's own rect — and outlines that frame so a stray band can be told apart at a glance: inside the outline is a layout that isn't filling its box, outside it is a frame that isn't covering the screen. It exists because the case it diagnoses only happens on a real phone, in the browser's own full-screen mode, with a real keyboard up — none of which any local browser automation can produce. The dev-route hash gained a probe key, which survives a goto so it can be switched on and then navigated with. Dev builds only.
    • A probe that plays eight linked turns to test how [bracketed] directions accumulate. ooc-session-probe.mjs plays an eight-turn session where each turn's real output becomes the next turn's history, so bracketed directions accumulate the way they do in a real playthrough — the existing ooc-probe.mjs scores one bracket turn against a fixed prior passage and structurally cannot see anything that builds up. Every turn is scored, bracket-free ones included, because that is where the narrator's acknowledgment of a direction turns up; hits are reported with their turn position so late-session drift is visible. ooc-probe.mjs gains a matching arm for the rider placed before the action rather than after it, comma-separated --arm, and a metric for the narrator breaking frame to address the player. Its author-meta check was also narrowed — it had been matching the ordinary word "authority".
    • A Playwright suite (npm run test:e2e) drives the real app in a real browser. It runs at two viewports — desktop and mobile — and exists for the failures the unit tests structurally cannot see: the unit suite renders into a fake DOM with no layout engine, so every element it measures is zero pixels wide, nothing can overlap anything, and a click always lands. Three prompt-editor bugs shipped past a green suite for exactly those reasons. Two guards came with it. e2e/focus-ring.spec.ts checks that the focus ring is drawn inside the control and clears the 3:1 contrast bar on a solid-filled button in both themes — jsdom loads no stylesheet, so it can see neither a box-shadow nor the rule ordering the filled-button color depends on, and that ordering is the fragile part: the variant's ring color and the base ring color set the same property at the same specificity, so the override only wins on stylesheet order. e2e/prompt-history.spec.ts types with real keystrokes and drives the real undo/redo buttons across three hosts (a Settings prompt, the world editor's markdown description, and its custom narration prompt, whose value round-trips through GameDataContext on every keystroke) — Lexical's history keys off a real text selection, which jsdom has none of, so a programmatic edit never enters the history stack and the unit suite could not tell a working undo from a broken one. Every guard was confirmed by reintroducing its bug and watching the spec fail. It runs separately and deliberately: the four checks that gate a change stay as fast as they were. See e2e/README.md.
    • The dev router can read, write, and open stored worlds, so a world can be authored by script. The dev-only window.__fmDev helper gained listWorlds, getWorld, putWorld, and editWorld — enough to write an exported world straight into local storage, read one back out, and open the World Editor on a world that's already stored (the existing worldEditor route only ever opened a blank draft). Authoring a large world no longer means a manual export/import cycle per revision. Dev builds only; tree-shaken out of production like the rest of the dev router.

➖ Removed

  • ⚙️ Backend
    • The unreachable "list" stat type is gone. Inherited from the upstream codebase, a third stat type held a list of items instead of a number — but it was commented out of the editor's type dropdown before this fork began, so no world could ever contain one, and no part of gameplay knew how to read it: its items never reached the AI, no descriptor could describe it, and the first turn's arithmetic turned it into a broken value. Removed along with its editor. A world from an older upstream build that somehow carries one now loads it as an ordinary number stat starting at its minimum. Nothing you could see or author changes. Tracking a set of things — a herd, a crew, a collection — is still a real gap, but the answer isn't a stat holding a list; it's state that belongs to the entities themselves, which is being designed separately.

🔧 Fixed

  • 👤 User-facing
    • The on-screen keyboard stops sitting on top of what you're typing. On mobile, opening the keyboard with the app in full screen left the app drawn at the full height of the display — so the box you'd just tapped into was underneath the keyboard, with nothing to scroll to bring it back. Out of full screen the browser papered over it by shoving the page around. The app now measures the space the keyboard actually leaves and lays itself out in that, so the story, the buttons and the input all move up together. The growing action box was broken separately, and everywhere: it expanded over the story rather than pushing it up, so a multi-line action covered the text it was responding to. It now takes up real room, and the panel above it gives way. A full-screen editor — a prompt, a description, a lore entry — used to be centered on the screen, so every pixel the keyboard took cost it half off the top: the header slid down as you started typing and back up as you finished. Those now sit against the top edge and stay there. And when the app is put into full screen through the browser's own full-screen mode, any strip the keyboard leaves behind is drawn in the app's background color instead of the browser's default black, so it reads as part of the app rather than as the page having fallen apart.
    • Library cards stop showing the raw {{…}} text of a placeholder. A world, character, or lorebook whose description used one of your author-defined Variables or Wildcards showed the chip's underlying token text on its card in the main menu, instead of the value it stands for — so a character blurb could read as a line of code. Community Creations listings already resolved them; the local library didn't. All three now render chips the same way a listing does: a Variable shows its value, and a Wildcard shows its options as {a|b|…} rather than rolling one, so a card never says something different from one look to the next. Only what's drawn on the card changes — nothing stored, exported, or shown to the AI is affected.
    • The Admin Panel's bug and suggestion queues have an Unresolved filter, and open on it. They could be narrowed to one state at a time or shown in full, with nothing in between — so seeing the actual workload meant checking three states one after another and adding them up. Both queues now have an Unresolved filter (Still Open on suggestions): every report that isn't Resolved or Won't Fix, and every suggestion that isn't Done or Declined. The bug queue opens on it, where it used to open on Open alone — a report waiting on its reporter, or one already reproduced, is work nobody has finished either. Counts and paging are exact, so the pager says how much is really there.
    • A published world with an empty Author field now shows the uploader's name. A world published without anything typed in its Author field arrived on your machine unsigned, so the person who made it went unnamed everywhere the world's own details are shown. A downloaded world that names no author now takes the uploader's account name instead. Only a genuinely empty field is filled: a world signed with a pen name, or with anything else its author chose to type, is left exactly as written.
    • The keyboard focus outline isn't sliced off any more. Moving through the app with Tab draws an accent outline around whatever you're on — but it was drawn just outside the control, and anything in a scrolling panel sits flush against that panel's edge, so the outline got cut off on the left and top. It happened in roughly fifty places: settings rows, the world and character grids, save lists, admin tables, the tab strips that scroll sideways. The outline is now drawn just inside the control instead, where nothing can clip it, and on solid-colored buttons it switches to a shade that stays clearly visible against the fill. Every focus outline in the app now passes the standard contrast bar for interface elements, in both light and dark themes.
    • A made-up character no longer loses their description to a formatting slip. When the story invents a character, Formamorph asks the AI to write them a description. If the reply happened to open by repeating the label it was given — Character name: Mira, then the description — the whole thing was thrown away and the character was left with nothing, because the step that removes the AI parroting the prompt back ran before the step that strips that opening line. Smaller models do this often, so a character could go several turns undescribed for no visible reason. The opening echo is now removed first and the description survives; a label repeated after the description is still cut, as before.
    • A body slider keeps growing when the story raises the stat's maximum. A stat bound to a body shape — a belly, a bust, a build — drove the shape from empty bar to full bar, measuring "full" against whatever the stat's maximum happened to be right now. So when the story raised that maximum, the body went backwards: a stat at 50 of 50 filled the shape completely, and raising the cap to 100 halved the effect of every point below it, with the shape unable to go past what the old maximum already looked like. The shape is now measured against the maximum the world was authored with, so raising the ceiling extends the body past that point instead of rescaling it — 50 of an authored 50 looks the same as it always did, and climbing to 100 now pushes the shape twice as far. Percentage stats are unaffected, since 100 is their ceiling by definition.
    • An optimized image no longer shows as broken in the editor. Choosing Optimize (or Downscale) on an uploaded image could leave its slot reading "Couldn't load this image" even though the image itself was converted perfectly — the editor briefly tried to draw the slot before the new image's data reached it, took that blink for a load failure, and never looked again. The stored image was always fine; only the editor's verdict was wrong. It now waits for the data before drawing and re-checks whenever it changes, so the image appears the moment the conversion finishes.
    • The VRM library and its fields say "Avatar" instead of "Model". The library tab, its buttons, and every message about it said "model" — the same word Formamorph uses for the AI you're running and the image model that draws your scenes, so a screen about .vrm files read like a screen about language models. The tab is Avatars, the world editor's field is Player Avatar, and the in-game tab and Character Customization follow suit. An entity's uploaded .glb/.fbx is a different thing and keeps the name 3D Model — the button that opened it just said "View Model", which now reads View 3D Model so the two can't be confused. Nothing about your files, worlds, or saves changes; only what they're called on screen — your library and your saved characters carry over untouched, with the bundled default keeping the name, date, and file you already had.
    • A long world name no longer runs underneath the delete button. On a world card, the name and the trash icon shared the same space — the icon sat in the corner on top of the name, so a title long enough to wrap ran beneath it and became unreadable. The two now sit side by side, with the name wrapping in the room that's left. The delete button also announces itself to a screen reader, which it previously didn't.
    • A very tall image stays inside its panel in game. An entity image with an extreme portrait aspect ratio — much taller than wide — could spill out of its box in the side panel and the entity popup, drawing over the tabs and text below it. Only entities carrying more than one image were affected: the gallery's swipe frame lost track of how tall its box was allowed to be. The image now shrinks to fit like it always did with a single image.
    • Re-selecting the same image file works again. Picking a file the slot had already seen — say, re-uploading an image you'd just removed — did nothing at all: no error, no image. The file picker only announces a choice when it differs from the last one, and the editor never cleared its memory of the last one. It does now, so choosing the same file twice sets the image both times.
    • Your own profile shows the day you actually joined. "Member since" on your profile read today's date, every day, for everyone — while the same line on anybody else's profile was correct. Signing in hands the app your account without its join date (only a full profile fetch carries it), and the missing date quietly fell back to today rather than showing nothing, so it looked like a real answer. It now reads the date the rest of the app uses, and says nothing at all on the rare occasion it can't be reached.
    • Moving your downloaded AI models between folders no longer strands a half-copied file. In the desktop app, changing where models are downloaded offers to carry the existing ones across. When the new folder is on a different drive each model is copied under a temporary name and renamed into place at the end — but if that final step failed, the temporary file was left behind. It was invisible to the app: it isn't recognized as a model, so it never appeared in your library, was never counted, and would never be moved or cleaned up again, quietly holding on to several gigabytes. The leftover is now removed whenever any part of the move fails. Your original file was never at risk before and still isn't — it's only deleted once the copy has completely and verifiably landed.
    • Right-clicking a misspelled word in the desktop app now offers corrections. The desktop app underlined your typos but there was nothing behind the underline — right-clicking one did nothing, because the window had no right-click menu at all. It has one now: a misspelled word offers up to five suggestions and Add To Dictionary for the ones that aren't mistakes, and the menu also carries Cut / Copy / Paste / Select All, which the desktop app had never had anywhere outside the keyboard shortcuts. Right-clicking selected story text offers Copy. Unaffected in the browser, which has always had its own.
    • The update window stops mixing two versions' release notes together. When a new release came out while the update window was open — or its notes refreshed behind it — the freshly fetched changelog could keep pieces of the old one: a heading still reading the previous version number over the new version's items, an "Added" label stuck saying "Fixed". The renderer considered a replacement line "unchanged" whenever it happened to occupy the same position as the old one, the same fault fixed for story paging in 2.9.1 — now fixed for the changelog box, world Readmes, and the help pop-outs, which all share that panel.

✅ 2.9.2 — Released 2026-08-04 — the little-conveniences release: a "Continue the Story" button for the turns you'd rather watch than write, and switchable traits finally working in the saves you already had (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing
    • A "Continue the Story" button under the choices. Sometimes the answer to "what do you do?" is nothing in particular — you'd rather watch the scene keep moving than invent an action for it. A [Continue the Story] button now sits below the generated choices and fills the action box with that text, so the turn can be sent without writing anything. It costs no extra AI request of its own, and the story reads it as a nudge to carry on rather than as something your character did. It's offered even on a turn that came back with no choices at all, which is exactly when there's least to go on. Paging back through the story shows it again on the turns you actually used it for. Settings → Accessibility → Choices sets it to Off, On, or Always — where Always keeps the button even with the choices request itself switched off, so a playthrough that generates no choices at all still has one way to take a turn without typing.

🔧 Fixed

  • 👤 User-facing
    • Traits you can switch on and off now work in saves you already had. A playthrough kept its own copy of each trait you chose, frozen as the world stood when you started — so a trait the author made switchable after that never got its checkbox, and switching it was possible only in a brand-new game. A chosen trait now reads its author's latest version: the switch appears where it should, along with any renaming, rewritten description, stat it turns on, or placeholder it pins that has been added since. Your numbers are left exactly where they are. What a trait did to your stats was settled on turn 1, and re-running it against edited values would only make them drift, so switching a trait off still takes back precisely what it gave. A trait the author has since deleted stays in your playthrough rather than vanishing from it.

✅ 2.9.1 — Released 2026-08-04 — the housekeeping release: paging back through the story shows that turn's narration again, mods and devs can reach the tools built for them, every dialog announces itself to a screen reader, and the console stops filling with errors that were never problems (click to expand)

Minor Changes

🔧 Fixed

  • 👤 User-facing
    • Paging back through the story shows that turn's narration again. Stepping back with Previous moved the choices, the stats and the location to the turn you'd paged to, but the narration itself stayed on the turn you started from — so a past page read as the newest scene above an older set of choices. The narration now repaints with everything else, on every page and in both directions. Two separate faults were behind it: paging back also started an endless re-render that never settled on its own, and the markdown renderer decided a replacement paragraph was "unchanged" whenever it happened to occupy the same span as the one before it, so it left the old words on screen.
    • Mods and devs can reach the Admin Panel. The panel was built for the whole team — Users, Feedback and the Log are everyday moderation work, with only Broadcasts and Policies held back for an administrator — but the button that opens it asked for an administrator too, so a mod or a dev had no way in. It now shows for any staff account, and the two administrator-only tabs stay hidden as they were meant to.
    • Image servers are only asked for their model lists while Settings is open. With InvokeAI or ComfyUI selected as the image provider, the app used to fetch its model/sampler lists on every page load — the lists only fill fields inside Settings, so when the server wasn't running this just put connection errors in the console at startup. The fetch now waits until the Settings window is actually open, where a failure behaves as before (InvokeAI shows its message under the Model field; ComfyUI falls back to free text).
    • Every dialog now tells a screen reader what it is. An audit of all 55 dialogs found 29 with no accessible description wired up, which also printed a warning to the console each time one opened. Dialogs whose body text was already the explanation (duplicate-save, world-not-installed, custom-code) now announce that text properly; viewers like the 3D model and theme preview gained short hidden descriptions; the rest declare explicitly that their title says it all. Two dialogs (the in-game World Editor and the command palette) were also missing their accessible titles and got hidden ones.
    • Expected endpoint probes stop repeating their console errors. Finding out whether an AI server is LM Studio means asking for lists only LM Studio has, and on any other endpoint the browser prints each of those 404s as though something broke — several times per session, since reachability, context length, and reasoning detection each asked on their own. A 404 is now remembered for the session the first time any of them sees it, so the question is asked once per endpoint and the console shows at most one such line per list instead of a stream. Pointing at an actual LM Studio is unaffected.
    • The Community Creations refresh arrow spins the way it points. The sync button's arrow is drawn counterclockwise but rotated clockwise while a refresh ran, so the icon appeared to spin against itself. It now rotates in the direction of its own arrow. The app's only other spinners are direction-neutral loading arcs, checked in the same pass.
    • A mod or dev answering feedback from their own profile is treated as staff. Opening a thread from Profile → Feedback gave the team's reply box to administrators only, so a mod or a dev reading the same thread there was told replies were somebody else's business — even though the same account gets the controls everywhere else. All three staff roles now answer from wherever they found the thread. Triage stays in the Admin Panel, as before.

✅ 2.9.0 — Released 2026-08-03 — the community release: bugs and suggestions become a board you can read, vote on and reply to, profiles gain faces, follows, likes and a record of what you've made, a mod team with badges and an audit log behind it, quarantine instead of deletion, admin messages that reach you in app, traits that switch on and off mid-story with exclusive groups and hidden stats, weighted placeholder values, an entity's 3D model reachable in game, and a desktop app that no longer needs CORS on your AI server (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing

    • Traits can be switched on and off during play. A trait used to be a decision you made once, at the character screen, and lived with for the rest of the story. An author can now mark a trait switchable, and it gets a checkbox in the in-game Traits panel. Switching one off takes back everything it gave: its stat changes reverse exactly — minimum, maximum, regen and starting value all return to where they stood — its description stops being sent to the AI, and anything else it was doing stops with it. Switching it back on reapplies the same changes, so a trait can go back and forth as often as you like without the numbers drifting. The switch positions are part of the turn, so rewinding the story rewinds them too. Traits the author didn't mark switchable read exactly as they always have.

    • A trait group can be exclusive. Some sets of traits are choices between options rather than a list to tick — a species, an origin, a starting class. Marking a group Exclusive in the World Editor turns it into radio buttons on the character screen and holds it to at most one trait: picking one drops the one you had. Clicking the one you've already chosen clears it, so "none of these" stays reachable, and it works the same way in-game for switchable traits — turning one on retires its siblings.

    • A trait can hide or reveal a stat. Stats used to be all-or-nothing: every stat in the world showed on every playthrough. A stat can now start disabled, and a trait can switch it on — so a Bloodthirst stat can exist in a world and appear only for the players who took the trait that gives it. It works the other way too: a trait can switch off a stat that would normally be there. A disabled stat is genuinely inert — invisible in the panel, absent from everything the AI is told, its regen paused, its code not run, and its body-morph link inactive — but it keeps its value, so switching it back on resumes where it left off.

    • Trait descriptions accept placeholders. Traits were the last authored text that couldn't take placeholder chips; both the player-facing and AI-facing descriptions now can, and so can a trait group's. On the character screen — before anything has been rolled — a chip reads as its options, {red|brown|black}, rather than committing to a value.

    • A trait can pin a placeholder. A trait can hold a placeholder at a fixed value for as long as it's active, so a Redhead trait can make every mention of hair color across the whole world read the same. The pin is free text with the placeholder's own values suggested, so it can name a shade the list doesn't carry. The playthrough's own roll is kept underneath and comes back the moment the trait is switched off. Where two active traits pin the same placeholder — or one turns a stat on and another turns it off — the one further down the trait list wins, so precedence is set by dragging rows rather than by learning a rule.

    • The trait editor names the other trait when two set the same stat or placeholder, and says which wins. Where a trait switches a stat on or pins a placeholder, and another trait does the same, a line under that row names the other one and says which of the two wins — so the precedence rule is learned where it matters rather than discovered months later. Traits in an exclusive group are left alone, since they can never both be active: a set of hair traits all pinning Hair Color is the intended shape, not a mistake. It is a note, not a warning — nothing is blocked, and reordering the trait list is how you change the answer.

    • The image viewer lets you drag the image around. Full-screen images were pinned to the middle of the window: zooming in let you look closer but the image stayed put, so a detail near an edge couldn't be brought anywhere useful. Now a left-click drag moves it freely, at any zoom, and double-clicking puts it back — centered and fitted to the window, the same thing the ⛶ button does. Double-click used to toggle the zoom instead. This is every full-screen image in the app: world covers, character art, and the entity images below.

    • An entity's 3D model is reachable in game. A model uploaded for an entity used to be findable only by opening that entity and scrolling past the description to a View 3D Model button — so most players never learned there was one. It sits with the image now, wherever the image is shown: the entity popup and the side panel both carry a small cube button in the corner of the image, and pressing it opens the model to be spun and zoomed. An entity that has a model but no image shows the model itself in the image's place, turnable where it stands, rather than showing nothing at all — and such an entity now appears in the side panel's entity view like any other, where before only an image counted. The viewer fills whatever space it's given instead of drawing into a fixed square, so it's bigger in the dialog and fits the panel. And you can make the model the one you see first. A Display by Default button at the bottom of the model viewer switches that entity over for this playthrough: from then on it opens on the model wherever its image appears, with the corner button now holding the artwork instead. The full-screen image viewer carries the same button to send it back. It's remembered per entity, so a statue can lead with its model while a portrait keeps its art, and it's a viewing preference rather than part of the story — rewinding turns leaves it alone. It's saved with the playthrough and comes back when you load.

    • Optimizing images always shows its progress. Choosing Optimize or Downscale used to give feedback only from the World Editor's own button — everywhere else the app just went quiet while it re-encoded, which on a big world could read as a hang. Every place the choice is offered now shows a corner notification with a bar and an X/Y image count while the work runs, and since the encoding happens in the background you can keep using the app rather than watching it. That covers downloading a world from Community Creations, exporting one from the editor, importing worlds or character cards from files, shrinking an uploaded or generated image, and restoring a backup.

    • An entity can carry more than one image. A character used to have exactly one portrait, so a second look — a different outfit, a later state, another angle — had nowhere to live. The Image field in the character editor is a short gallery now: fill a slot and another appears, up to two, and any slot can be made the primary with one button. The primary is the one image that stands in wherever only one fits — the library grid, a published listing, the face of an exported character card — so choosing it is a matter of reordering rather than re-uploading. In game, an entity with more than one shows arrows at the edges of its image that fade in as you point at it, with a faded 1/2 underneath, and it wraps around in both directions. On mobile there are no arrows to point at: swipe the image sideways and it follows your finger, letting go past a quarter of the width to turn the page. Opening an image full-screen keeps the arrows and stays on whichever one you were looking at. Which image you're on is not part of your save — loading a game starts every character back at its primary.

    • Placeholder values can be weighted. A wildcard used to pick evenly between its values, which made a rare option as likely as a common one. Click a value in the Values field and a small popup sets its draw weight — a whole number, 1 by default, where 2 is twice as likely as 1. Setting a weight to 0 benches that value: it stays in the list and stops being rolled. Once any value carries a weight other than 1, an eye icon appears beside Values; press it and every chip shows the chance it will actually roll, as (45%). With everything left at 1 there's nothing to show and the icon stays out of the way. The Preview tab rolls with the weights too, so the distribution can be felt rather than calculated.

    • Administrators can send you messages. Clicking your profile circle used to open a box with nothing in it but a password field; it now has two tabs, and Messages opens first. Notices from the administrators land there — an announcement to everyone, or something meant only for you — and the profile circle carries a count until you've read them, with a nudge when you start the game if any are waiting. Each one is signed either by the administrator who wrote it or simply by the Formamorph Team, carries a severity that colors it (Info, Warning, or Urgent — how loud it is, not anything it does to your account), and is written in the same formatting the rest of the game uses, so links and lists come through properly. Open one to read it; clear the ones you're done with and they leave your list for good — except the ones an administrator has pinned, which stay until they say otherwise. There is deliberately no reply: this is a channel to you, not a conversation, which keeps it from becoming somewhere messages can be sent that nobody is watching. Your suspension now comes with an explanation. Being suspended used to be a wall with no reason attached — the notice simply told you to contact an administrator, with nowhere to do it. Suspending someone now offers to send them a message saying why, and a suspended account can still read and clear its messages even though it can write nothing else.

    • A suspended account can sign in again. Being suspended used to mean the login screen simply refused you, which protected nothing — every browsing and download route is open to signed-out visitors anyway, so all the refusal really cost was the sight of your own account and, now, the message explaining what happened. Suspension is enforced on each individual action instead: you can sign in and read, and the server turns down everything you could write — publishing or editing or deleting a world, posting or editing a comment, and changing your password. The password fields say so plainly rather than failing when you press the button.

    • Manage Users became the Admin Panel. The old single-purpose dialog is now two tabs. Users keeps the account table and gains messaging: every row has Message and History, and checkboxes let several people be written to at once. Broadcasts is new, and is where messages to everyone are written and reviewed — each one showing how many of the accounts that could see it have read it. Anything sent can be recalled, which takes it out of the reader's list while keeping it on yours, so there's still a record of what went out.

    • The Admin Panel's user table has a Terms column reading Not Seen, Declined, or Accepted. A Terms column reads Not Seen, Declined or Accepted, with a reset icon beside it that asks that one person to answer again — greyed out for anyone who hasn't answered, and greying itself out the moment it is used. Someone whose answer a change to the terms invalidated reads as Not Seen, since that is what the gate will treat them as. Message and History were two buttons per row competing for width with Activate and Suspend; they are one split button now — Message on the left, History behind the caret — and Account Type is just Type. Every data column sorts: click Username, Email, Type, Status or Terms to order by it, click again to reverse. The sort runs across the whole userbase rather than the ten rows on screen, so page one really is the top of the list. History says how many messages are behind it, and Sent Messages is now All Messages, with the total alongside it. Sorting or paging no longer makes the table jump: the rows dim in place while the new ones load instead of being swapped for a loading placeholder that was a fixed five rows tall and half the height of a real one, which collapsed a full page and sprang it back. The page controls underneath were disappearing and reappearing for the same reason, and now dim alongside the rows instead. The placeholder is still used for the very first load, when there is nothing on screen to dim. The sent-message lists behind All Messages, a user's History and the Broadcasts tab did the same thing and got the same fix.

    • The profile dialog is Admin Panel-sized with a fixed header, and gains a Terms tab you can accept from. It opens at the same size as the Admin Panel with the identity, the account buttons and the tab strip frozen in place, so only the panel below them scrolls. Manage is gone as a tab: Change Password and Logout are buttons in the header now, and changing a password opens its own small popup with the same form. A Terms tab shows the contributor terms whenever an administrator has written any — read them, and accept or decline right there rather than waiting to be stopped mid-publish. Declining costs nothing but publishing and can be undone from the same place; accepting leaves the tab in place, since you should be able to re-read what you agreed to.

    • You can report a bug, and get an answer. A Report a Bug button sits in the main menu footer and in the in-game menu, so a problem can be filed where it happened rather than after backing out. A report is a title, a category (crash, AI output, World Editor, community, visuals, or something else) and a description written in the same markdown editor the rest of the app uses. Your app version, whether you're on the desktop build, and your OS and browser go with it — shown to you in the form before you send, and nothing about your worlds or saves is included. Filing needs an account, because the point is that we can write back: every report gets a comment thread, and a new Bugs tab in your profile lists the ones you've filed with replies from the team. The profile circle's badge counts a bug reply the same as a message, so there's one place to check for anything waiting on you. Administrators get their own Bugs tab in the Admin Panel: the whole queue, filtered by state, where each report can be answered, moved through triage (Open, Need Info, Confirmed, Resolved, Won't Fix) or deleted.

    • A published world, entity, or dictionary can be hidden for a set number of days instead of deleted. When a published world, entity or dictionary needs changes rather than removal, an administrator can hide it for a set number of days — seven by default — instead of taking it down for good. A quarantined listing leaves Community Creations entirely: nobody else can find it, open it, download it or comment on it, and its existing comments go with it. Its author still sees it, badged, with the date it will be deleted, and can keep working on it — and the administrator writes to them saying what needs to change. Updating it buys another week, once, so a listing fixed on the sixth day isn't deleted on the seventh. An administrator can release it at any point, which puts it back exactly as it was; if the deadline passes first, it's deleted. Quarantining, updating, releasing and expiring all appear in the Log, and administrators get a Quarantined filter in Community Creations to see what's currently hidden.

    • A Log tab in the Admin Panel records suspensions, terms resets, and community deletions. A new Log tab in the Admin Panel records suspensions and reinstatements, terms resets (one person or everyone), and anything deleted from Community Creations — a published world, entity or dictionary, or a comment — whether an administrator removed it or its owner did. Each entry names who did it, who it was done to, and what it was, and a deleted comment keeps a short excerpt of what it said: the whole point is that the record still reads after the thing it describes is gone, so the names are kept as they were rather than looked up later. An entry outlives the account it names. Filter by kind of event, or search a username. Nothing can edit or delete an entry, including administrators — a record somebody can rewrite isn't one.

    • Characters and dictionaries can be tagged, and a dictionary can have a cover. Publishing a character or a lorebook used to send it untagged no matter what — only worlds carried tags, so the two other kinds were invisible to every tag filter in Community Creations. Both editors now have an Overview tab with the same tag field the World Editor uses, drawing on the same suggestions. A dictionary's Overview also holds a new cover image, so a published lorebook looks like a listing rather than a blank card; its name, description and enabled toggle stay where they were, beside the entry tree. Characters and dictionaries get the detailed card view too — the grid/detailed toggle in the top right now works on all four library tabs, and remembers its setting per tab, since wanting worlds as big cards says nothing about wanting a hundred characters that way. The detailed card shows the tags and the description, and anchors character art to the top so the crop takes the face. Tags and the cover also travel with the file: a character card and a dictionary export carry them now, where before they would have been dropped on the way out.

    • A profile shows followers, total likes, and total downloads across everything they've published. Under somebody's name there's now a row of three numbers — followers, total likes, total downloads — counted across everything they've published. It's the same row on your own account dialog from the main menu footer, reading the same figures a stranger sees, so there's one answer rather than two that can drift apart. The totals count what's in the catalog: your own quarantined work still appears in your list with its own numbers, but sits out of the totals until it's released, so the profile you look at is the profile you're handing out.

    • A report filed by the team is badged like their replies are. A bug or suggestion showed its reporter as plain text, so a mod's own report read exactly like anybody else's — and opening it gave you an unbadged post above a column of badged replies by the same people. The name now carries an Admin, Dev or Mod badge in the queue and on the thread. Like a reply, the badge records what they were when they filed it: promoting someone doesn't turn their old reports into official statements, and demoting them doesn't erase that a mod wrote what they wrote.

    • The vote button on a suggestion you've already voted for is legible in the light theme. The vote button only changed color once it was yours, and on a light theme that color is a pale mint on a near-white card — so a suggestion you'd already voted for was indistinguishable from one you hadn't until you opened it. The arrow fills in solid and the count goes bold, neither of which depends on the tint being legible.

    • You can like something in Community Creations. Every listing now carries a heart beside its download count, and pressing it turns it pink. Downloads only ever said how many people tried a world; likes say how many were glad they did, so a world lots of people downloaded and quietly abandoned no longer looks the same as one people loved. The heart sits where the download count used to be on a card, with downloads moving to the middle, and it's in the details of an open listing too. Sort by Likes joins Last Updated, Creation Date and Downloads. Liking needs an account, you can take a like back at any time, and you can't like your own work — the number is about what other people thought of it. Fair warning: for a while yet a world's likes will say more about how long it has been up than how good it is, until everyone has had a chance to go back and heart the ones they enjoyed.

    • A profile shows what that person has made. Somebody's profile card now lists everything they've published, right under their name — worlds, entities and dictionaries, newest first, with a small image and how many times each has been downloaded and commented on. The three kinds sit behind a filter that says how many of each they have, so an account that only makes entities opens on entities rather than on an empty list of worlds. Click one and it opens in Community Creations, the same way a notification does; if the profile was opened from inside Community Creations it goes straight to the details rather than reopening the browser behind it. Your own quarantined work is listed to you, marked Hidden — the staff see it too, and nobody else is shown it at all.

    • You can follow people. Open somebody's profile and there's a Follow button. Their profile shows how many follow them — only the number, never the names, including on your own. A new Notifications tab in your profile lists what everyone you follow has published or updated since you started following them, whichever kind it is: worlds, entities and dictionaries all count. Opening the tab is what marks it read, and the count joins messages and feedback replies in the same badge on your profile circle. Following somebody shows you what they do next rather than dumping everything they've ever made, and however many times an author revises one listing it stays a single row — so nobody can flood your feed by pressing publish repeatedly. Clicking the name of a listing in a notification takes you straight to it in Community Creations, opening its details on the right tab. A Following strip at the top of the tab expands to show who you follow, with a button to stop. Quarantined and deleted listings drop out of the feed.

    • The profile dialog's Bugs and Suggestions tabs became one Feedback tab. The same change the Admin Panel got, for the same reason: two tabs for one thing left no room, and Notifications needed a slot. Both branches are sub-tabs under Feedback now.

    • The Author line on a downloaded world opens the profile of whoever published it. Open a world you downloaded from Community Creations and the Author line now opens the profile of whoever published it. Only downloads carry that link: the Author field on a world you made or imported is free text somebody typed in the World Editor, and a name is not an account — so those stay plain text rather than opening somebody else's profile by coincidence. Worlds already in your library were downloaded before this and don't know who published them; re-downloading one links it up.

    • Clicking a name opens a card with that person's picture, name, and join date. Names are clickable now — on a listing you've opened, on its comments, and on a bug or suggestion and its replies. A small card shows their profile picture at full size, their name, and when they joined. It's the same card everywhere, and it works signed out, since browsing Community Creations always has. One deliberate exception: the author line on a card in the Community Creations grid still hides that author's work, the way it always has — the profile only opens once you've clicked into something. A name with no account behind it, like a comment whose author has since been deleted, stays plain text rather than offering a card with nothing in it.

    • A bug or suggestion can be edited after it's filed. Until now the opening post was the one thing nobody could change — you could reply to it, but not fix the title you rushed or add the step you left out. There's an edit control on the thread now, and it only ever shows what you're allowed to change. You can rewrite your own report, title and description, for as long as it isn't locked. The team can rewrite a bug — a bug is a work item, and one that reads "IT BROKE" is no use to anybody — but not a suggestion, which stays in the words of whoever thought of it. Filing is the team's either way: they can move a report to a different category, or move it between Bugs and Suggestions entirely. A move like that says what it costs before you commit: the status goes back to Open, and a bug's version and platform line is deleted, since a suggestion is a public board post and never collects one. Replies and votes come along. Anything edited says edited next to its date, the same as a comment does, and an edit by anyone other than the reporter appears in the Log.

    • There's a mod team now. An administrator can make any account a Mod or a Dev from the Admin Panel's Users tab, and put them back to normal the same way. The two are the same job under different names, and both carry the everyday moderation work — the user table, the Log, quarantining and removing listings, triaging bugs and suggestions, clearing a profile image, resetting one person's answer to the terms, and writing to somebody to say why. What stays with administrators: changing what an account is, Broadcasts, and Policies — those two tabs simply aren't there for a mod or a dev. Administrators themselves are still made on the server by hand and can't be created or removed from inside the app. Staff moderate the room, not each other: a mod or a dev can act on ordinary accounts only, an administrator can also act on mods and devs, and nobody can act on an administrator — so one compromised moderator account can't take the team down with it. Every promotion and demotion appears in the Log.

    • Replies from the team are signed by name. A reply on a bug or a suggestion used to read Formamorph Team with no indication of who wrote it — and because that was worked out from the account's current type, demoting someone rewrote the signature on every reply they'd ever left, while promoting someone turned their old replies into official statements. Now every reply carries its author's own name and picture with an Admin, Dev or Mod badge beside it, and what they were is recorded when they write it, so the thread reads the same a year later whatever has changed since. Replies written before this keep working from the old rule, since there's no way to know what anybody was at the time.

    • Upload a profile picture, with a crop step, and it appears beside your name everywhere. Upload a picture from your profile and it becomes a circle beside your name everywhere you appear — on what you've published in Community Creations, on your comments, and on the bugs and suggestions you file. Picking a file opens a crop step first: drag to move the picture, scroll or use the slider to zoom, and the circle you're looking at is exactly what everyone else will see. It's saved at a fixed size in a lossless format, so a photo straight off a phone arrives as a few tens of kilobytes rather than several megabytes. Anyone without a picture gets a colored circle with their initial in it, and the color is worked out from the name — so the same person is the same color for everyone, with nothing to load. Remove yours at any time and you go straight back to the initial. Administrators can clear somebody else's picture from the Users tab, which is recorded in the Log and offers to send them a message saying why, the way a takedown does.

    • The Tag Notice's tag field is a chip editor instead of a comma-separated text box. It was one text box holding a comma-separated line, which meant a tag containing a comma couldn't be written at all and nothing checked what you typed against the tags authors actually use. It's the same field the World Editor tags a world with: type to autocomplete, Enter makes a chip, drag to reorder, double-click to fix one. What an administrator names here and what an author tags with now come from one list.

    • The Admin Panel's Bugs and Suggestions tabs became one Feedback tab. Both queues sat on the top strip and cost it two of six slots, which left every label cramped. They're sub-tabs under Feedback now — same two queues, one click deeper, and the tabs above them have room to breathe.

    • Bugs and suggestions filter by category. Both the Admin Panel's queues and the Bugs and Suggestions tabs in your profile gained a category dropdown beside the ones already there — so a list can be narrowed to just the crashes, or just the interface ideas. The filtering runs across everything rather than the page on screen. The sentence describing each tab is gone to make room; the tab's own label already said it.

    • You can suggest things, not just report bugs. The bug form is now Send Feedback, with a Bug | Suggestion tab at the top of it — same entry points, same markdown editor, one more thing you can do with them. A suggestion has its own categories (Gameplay, AI & writing, World Editor, Community, Interface, Something else) and its own states (Open, Considering, Planned, Declined, Done), because "confirmed" and "planned" are different promises. Nothing about your machine is sent with a suggestion — the version, platform and browser line that goes with a bug report isn't collected here, because a suggestion is about the game rather than the computer it was written on. A Suggestions tab joins Bugs in your profile and in the Admin Panel.

    • Suggestions are a board, not an inbox. Everyone can read every suggestion, vote for the ones they want, and reply to any of them — a bug thread stays between the reporter and the team, but a suggestion is a conversation worth having in the open. Filing counts as your own vote, so nothing starts at zero, and you can take a vote back any time. The board opens on everyone's suggestions sorted newest-first, with Most voted a click away; your own Bugs tab still opens on yours, where your replies are. Administrators can lock a thread — it stays readable and still takes votes, but nobody can add to it — and can remove any comment on it, though never rewrite one.

    • You can read every bug that's been reported, not just your own. A dropdown beside Report a Bug switches the tab between My Reports and All Reports, so you can check whether what you hit is already known — and see where it stands — before filing it again. It opens on your own, since that's where your replies are. Somebody else's report is read-only: the thread is there to read, but replying stays between the reporter and the team, which keeps a bug from turning into a comment section. Only the threads you're part of are ever marked as having new replies — a report you're merely reading never carries a dot you can't clear. Filing needs an account and so does reading, so none of this is on the open web.

    • The footer's Send Feedback button is now Feedback, and opens both queues instead of the form. The Send Feedback button in the main menu footer is just Feedback, and it opens everything to do with bugs and suggestions rather than jumping straight to the form: both queues, the filters, and a button to file a new one. The Feedback tab is gone from your profile, which keeps Messages, Notifications and Terms. Replies you haven't read are counted on the Feedback button itself instead of in the profile circle's total — the count sits on the thing it's about, so the circle is now messages and follows. In-game is deliberately unchanged: Send Feedback in the menu still opens the form directly, because filing what just happened shouldn't mean browsing a queue first. Administrators' triage queue stays where it was, in the Admin Panel.

    • A bug or suggestion says how many replies it has. Every list of them — your own tabs, the public suggestion board and the staff queues — now shows a reply count under the status, so a thread people are actually talking in reads as one from the list rather than only after opening it. Nothing is shown on a thread nobody has replied to.

    • The message list says "Showing 50 of 63" instead of silently stopping at fifty. The message list loads fifty at a time and used to stop there without a word, so anything older simply wasn't in it. It now says Showing 50 of 63, and dismissing one pulls the next in behind it.

    • Edit and Delete on your own comments in a bug thread. Every comment in a bug thread now carries Edit and Delete — on your own words only, whichever side of the conversation you're on. An administrator can move a report through triage and delete the whole thing, but cannot rewrite what the reporter said. An edited comment is marked edited next to its date, since the other person may already have read the earlier wording; a deleted one is simply gone, and the rest of the thread is untouched. The reply box and the edit box are also tall enough for their own contents now — the editor was overflowing the comment it sat in.

    • Admin messages get the world editor's markdown editor. The message body — when writing to one person, to everyone, or editing something already sent — and both publish-policy bodies were plain text boxes that happened to be rendered as markdown for the reader. They are now the same editor the World Editor uses for a world's description and readme: a formatting toolbar for bold, italics, headings, lists, quotes, links and code, and an Edit | Preview pair that shows exactly what the reader will see before it goes out. What gets stored is unchanged — the same markdown text as before.

    • Removing someone's published listing offers to message the author with a reason. Removing somebody else's published world, entity or dictionary from Community Creations now asks whether to send the author a message, the way suspending an account does. The draft names what was removed and leaves the reason blank for you to fill in — the item is already gone by then, so there is nothing left to point at. Declining leaves it removed and simply unexplained; the takedown never waits on the message. Deleting your own work asks nothing.

    • A message can reach new accounts, and can be made permanent. Sending to everyone now asks who exactly: Existing accounts only, Existing + New so that everyone who signs up later gets it too, or Pinned. A pinned message goes to everyone including later signups, sits at the top of their inbox, and cannot be dismissed until an administrator unpins or recalls it — which is what a rules or guidelines notice wants, while a dated announcement or a welcome note stays clearable. Direct messages can be pinned the same way, and a suspension notice is pinned by default. Pinning something a reader had already cleared brings it back for them; unpinning puts it back exactly as they left it.

    • Sent messages can be edited. Anything already delivered can be rewritten in place — from the Broadcasts tab, from a user's message history, or from All Messages — subject, body, severity, who it reaches, and the signature — instead of being recalled and sent again. Readers see an Edited date next to it, since they may have read the earlier wording. Most edits are typo fixes and pass silently, but Mark unread again re-badges everyone, restarts the read count against the new text, and returns the message to anyone who had dismissed it. A recalled message can't be edited — that record stays closed.

    • The desktop app can use models you already downloaded. Local models had to live in the app's own models folder, which meant anyone already running LM Studio or something like it had to copy the same multi-gigabyte file twice to use it here. The Installed tab now has a Search Locations box: the app folder is still there and still where downloads land, and underneath it you can point at one more folder to look in. Search Subfolders is on by default, because LM Studio files models away under a publisher and a repository rather than leaving them loose. If LM Studio is installed, a Use LM Studio button fills its library path in for you rather than making you go and find it. Models found in your other folder sit in the same list as everything else and load the same way, marked with the folder they came from so two files with the same name are still telling apart — but they have no delete button, because they belong to whichever program downloaded them and this one has no business removing them. Downloads from the Recommended tab still always land in the app's own folder. Point it somewhere that isn't there right now — an external drive that's unplugged — and it says so and simply hides those models until it's back, rather than forgetting the setting.

    • You can choose where models download to. Models landed beside the app and nowhere else, which is a problem when the app is on a system drive with no room on it. Change… next to the download folder points it anywhere you like, and Use Default puts it back. The folder shows how much space is free on its drive, and starting a download that won't fit says so — with both numbers — rather than failing seven gigabytes in; you can go ahead anyway, since that figure isn't always right for a network drive. If the folder isn't reachable when you go to download — an external drive that's unplugged — it says so and stops, rather than quietly filling the drive you moved off of. Changing the folder offers to bring your models with it. Moving is per-file with a progress bar and a Cancel button, and nothing is deleted until its copy is finished and safely written, so canceling or running out of room part way costs you nothing but time — the original is still there. Anything that couldn't be moved, such as a file another program is holding open, is listed by name with the reason and the folder it's still in, and the list can be copied. Choosing not to move them is fine too, but the old folder isn't searched afterward either way, so whatever stays there won't be in your model list. One thing worth knowing: if you put models outside the app folder, copying the app folder to another machine no longer brings them along — the panel says so while that's the case.

  • ⚙️ Backend

    • Changing your password now signs you out everywhere else. A sign-in token used to be good until it expired no matter what happened afterwards, so a token taken from a shared or stolen machine survived the two things anybody actually does about it — changing the password, and an administrator suspending the account. Each account carries a session generation now: changing your password ends every other session while keeping the one you're using, and suspending or demoting somebody takes effect on their next action rather than whenever their token happened to run out. Sessions signed in before this change keep working; the first password change or moderation action is what retires them.
    • The first administrator can no longer be seeded with a default password. The setup step used to fall back to a well-known password when none was configured, and the setup instructions prescribed exactly that one — so a by-the-book install shipped with a guessable owner and nothing to indicate it. There is no default now: setup refuses to run without a password of its own, and rejects anything short or already known. Existing installations are untouched, since the step has always skipped an administrator that already exists.
    • The community server can quarantine a listing. Three columns on worlds, added by a migration that runs at boot — the first change to a table that already exists in production, since creating tables can never add a column to one. Visibility is decided per request: the catalog routes gained optional authentication, so a quarantined listing is absent for the room, present for its author and for administrators, and answers the same 404 a deleted one does rather than admitting it exists. The deadline is enforced three ways — at boot, on an hourly timer, and before any catalog read — so a server that was down over a deadline cannot serve the listing again. The one-time extension is per quarantine rather than per listing, and is reset by a release or a fresh quarantine.
    • The community server keeps an audit log. An append-only audit_log table with no foreign keys and nothing cascading: every name is a snapshot taken at the time, so an entry survives the world, the comment or the account it describes. Writing an entry can never fail the action it records — the suspension or takedown has already happened by then, so a failed write is logged loudly to the console and the request still succeeds. There is no route that writes, edits or removes an entry; recording happens inside the four actions themselves. Reading is administrators only.
    • The community server carries bug reports and suggestions. One feedback tree with a type branch rather than two parallel systems — a thread, a status and a read-marker mean the same thing on both, and only the vocabulary and who may write differ. Four tables — the threads, their comments, one read-marker per person per thread, and one row per vote — plus endpoints for filing, listing, reading a thread, commenting, triaging, deleting, and counting what's unread. A thread is readable by any signed-in account and writable only by the person who filed it and by administrators, so a reader who is neither is refused the reply rather than told the report does not exist. Reading somebody else's report leaves no read-marker behind, so a curious look doesn't accumulate a row per report. Filing is capped at ten an hour per account rather than per address, so a shared connection isn't throttled as one person. Deleting an account takes its own reports with it, but not the replies it left on other people's — those stay in place, unattributed, so deleting an administrator can't tear the team's half out of a conversation somebody is still reading. A comment can be rewritten by its author only — administrators included, whose moderation stops short of rewriting what somebody said — and a rewrite stamps an edited_at the thread reads back. Removing one is the author's or an administrator's, which is the only lever there is when an open thread goes bad. Statuses and categories are checked per branch by the database itself, so a suggestion cannot be marked "confirmed" even by hand. A suggestion's vote count is joined into the list query rather than counted per row, since the board is paged and sortable by it — ordering a page would rank ten rows against each other instead of the whole board. Deploying this needs npm run init-db run once — the server does not create tables on boot.
    • The community server carries messages. Two new tables (the messages themselves, and one read/dismiss row per reader) plus endpoints for reading an inbox, counting what's unread, marking read, dismissing, sending, editing, listing what was sent, and recalling it. A deleted administrator's messages stay in every inbox that had them, unlinked from the account rather than deleted with it — otherwise deleting the administrator who suspended someone would take the notice explaining it away while the suspension stood. A broadcast is stored once rather than copied per account, so its audience is worked out fresh on every read — which is what lets raising a message's reach later pull in everyone who has joined since. Marking a message read checks the reader can actually see it first, so a read receipt can only ever be moved by someone the message was genuinely delivered to, and the refusal to dismiss a pinned message lives on the server rather than in a hidden button. Deploying this needs npm run init-db run once — the server does not create tables on boot.
    • Administrators can require terms before anyone uploads. A popup can now be written that appears the first time someone publishes a world, entity or dictionary. Accepting it is remembered and it never appears again; declining leaves publishing unavailable, with a short note and a link to read the terms again whenever they change their mind. An administrator can ask one person, or everyone, to accept again — useful when the terms actually change rather than when a typo is fixed. Declining is recorded rather than only remembered on your own machine, so an administrator can tell someone who read the terms and said no from someone who has simply never tried to publish; it changes nothing about what you can do, and accepting later replaces it. The two popups sit on their own sub-tabs — Upload Gate and Tag Notice — rather than stacked down one long scroll, with Reset Everyone kept alongside the gate it resets. A second popup warns about tags: list the tags that deserve a word of caution and anyone publishing with one of them sees the notice first. That one never blocks anything — read it and continue, or back out of this upload and change your mind about the tags. Both are written in the same formatting the rest of the game uses, both have an on/off switch, and neither exists until an administrator writes one, so nothing changes for anyone until they do.

🐛 Fixed

  • 👤 User-facing

    • The Hidden panel in Community Creations opens with nothing focused, so its list isn't buried under the tag dropdown. Opening it put the cursor straight into the Tags field, which shows every tag in the catalog the moment it's focused — so the list of what you've hidden was buried under a dropdown before you'd typed anything. The panel now opens with nothing focused; click into either field and the suggestions work as before.

    • The Log records the role a mod or dev actually held when they acted. Each entry kept a yes/no for "was this an administrator", written before mods and devs existed — so every moderation by a mod or a dev was stored the same way an ordinary account's would be. It now records what they actually were at the time. Entries written before this keep reading as they did: an administrator's still says so, and nothing else can be worked out after the fact, so those stay blank rather than guessed at.

    • The Log and Backup & Restore say "entity" instead of "character". The Log described a removed entity as a "character", and Backup & Restore listed Characters. An entity doesn't have to be a person — a plant, an object or a fixture is one too — so calling the type "character" made it read narrower than it is. Both say entity now, matching the rest of the app.

    • Each Log entry carries the Admin, Dev, or Mod badge the person held at the time. An entry named who did it but not what they were, so a mod's suspension and an administrator's read identically. Each line now carries an Admin, Dev or Mod badge beside the name — the role recorded at the time, so a promotion or demotion since doesn't rewrite the record. Entries from before this is recorded, and anything an ordinary account did to its own work, carry no badge.

    • The desktop app no longer needs CORS enabled on your AI server. The desktop build was always meant to handle CORS for you, but a browser rule slipped through: before sending a real request, the app first asks the server for permission, and that permission check must get a "success" answer. LM Studio with CORS off answers it with an error — so every chat request failed with a generic message until you turned CORS on in LM Studio's server settings. The desktop app now answers that check itself, so LM Studio works out of the box with CORS off, and a server that's off or misconfigured shows its real error instead of a dead end. (The web version still needs CORS — that's the browser's rule, not ours.)

    • The desktop app now offers the connection troubleshooting guide. When the app couldn't reach your AI server — LM Studio not running, a wrong port, CORS off — the web version offered a Fix connection → walkthrough while the desktop version showed only a generic error, on the theory that the desktop build had already handled CORS for you. It hadn't, at least not for everyone. The desktop app now shows the same guide, both from the failed-request message and from a Trouble connecting? link under the Endpoint field in Settings.

    • Community comment and world dates read in your own timezone instead of being off by your UTC offset. Comment times, and a world's created and updated dates, are recorded by the server in UTC but written without saying so — the app read them as though they were your own local time, so everything was off by your offset (five hours, for anyone on US Eastern). A comment posted a minute ago could read as though it were posted this morning. All of them now say what they mean, and the app's own local dates are untouched.

    • Suspending or activating a user updates that row immediately, instead of waiting for a reload. The row still offered Activate after they'd been activated (and Suspend after suspending), and only corrected itself once a search reloaded the table — the row was being matched by an ID field this endpoint doesn't return, so the update quietly landed on nobody. The row updates immediately now.

    • The unread dot is visible in every light theme, instead of a pale accent on white. The dot marking an unread message or a bug thread with new replies was painted in the theme's accent color — which in this app is a pale pastel meant to sit behind dark text, not to be a mark of its own. On white it came out at about a seventh of the contrast a small indicator needs, so in the default, purple, rose, forest and bubblegum light themes it was barely there; the grey and high-contrast themes and every dark theme happened to be fine, which is why it went unnoticed. It's larger now, and colored by what's actually waiting rather than one color for everything: an admin message takes its own severity (blue for Info, amber for Warning, red for Urgent), while bug replies, suggestions and new work from people you follow use the theme's accent color. A suspension notice and a reply on a suggestion are both unread, and only one of them should look alarming. The count on your profile circle follows the same rule — it takes the color of the loudest thing behind it, so a red circle means something from the team needs you, and an accent-colored one means the community has been busy. An unread bug or suggestion is also in bold now, the way an unread message already was: the dot was the only thing marking those rows, and a mark made of color alone isn't one for everybody. Unread rows also carry a short red bar down their left edge, so a full inbox or queue can be scanned down the side instead of reading to the end of every title.

    • The Follow button remembers you already followed someone after reopening their profile. Following somebody, closing their profile and opening it again offered Follow a second time — the request that fetches a profile was the one place that didn't identify you, so the server had no way to know whose follow state to answer with. It looked right immediately after clicking only because the button was going by what it had just done.

    • Mod and Dev badges appear on published work, comments, profiles, and notifications, not just feedback replies. Someone on the team was badged on their replies to bugs and suggestions and nowhere else — not on the worlds, entities and dictionaries they published, not on their comments, and not on their own profile card. The badge now appears beside their name in all of those places, and on the Notifications tab — both on the rows themselves and on the list of who you follow. It reads from the account as it stands today, so a promotion or a demotion is reflected everywhere at once; a reply on a bug still says what they were when they wrote it, which is the one place that should not change.

    • A location's entity picker lists entities in the order the Entities tab does, folders and all. The Entities picker in a location was listing them in the order they happened to be created, so a world whose entities had been dragged into folders and sorted in the Entities tab showed an unrelated jumble here. It now reads in the same order the tab does — folders, nesting and all.

    • ComfyUI is the image provider you're offered first. The provider dropdown in Settings listed Automatic1111 / Forge at the top and started there on a fresh install, which pointed newcomers at the least actively developed of the three. The order is ComfyUI, InvokeAI, then Automatic1111 / Forge, and a fresh install starts on ComfyUI. A1111 / Forge still works exactly as it did — it's kept for anyone already running it, just no longer the first suggestion. If you've already picked a provider, nothing changes for you.

  • 🛠️ Developer tooling

    • Fourteen option switchers announce themselves correctly to screen readers. Several of the game's segmented controls were built out of tab components, but they don't switch a tab panel — they pick a value, and whatever they change is rendered somewhere else entirely. A screen reader was told otherwise: each button claimed to be a tab controlling a panel that was never on the page, so it announced "tab, 1 of 4" and pointed at an element that didn't exist. Fourteen of them are now segmented controls (a radio group), which is what they always were: the main menu's card-type switcher and its grid/detailed view toggle, Theme and Paragraph Limit, Thinking mode and Native Reasoning, the local model panel's Simple/Advanced and GPU Layers rows, the model manager's Installed/Recommended and VRAM tier rows, the variable chip pop-out's format and scope options, the in-game Player/Entities model swap, the System Prompt / User Message / Messages / Options row under each prompt, and the group rows in Select Starting Traits. Nothing moves, resizes or recolors — the replacement is styled to match to the pixel, right down to the ones that lay their options out in even columns or wrap them onto a second row. The switchers that really do drive a panel (the Settings sections, the World Editor, the prompt list) stay tabs.
    • Tab buttons now point at their own panels, so screen readers announce them correctly. The library's entity and dictionary editors, Community Creations' Worlds / Characters / Dictionaries switcher, and the World Editor's seven tabs are genuine tabs — but their contents were rendered outside the tab set, so each button still pointed at a panel the page didn't have. The panel is now part of the tab set proper, which is what a screen reader needs to follow the switch and announce what it landed on. Nothing about the layout changes, and the tabs behave exactly as before. In the World Editor's side-by-side layout the tab owns its list; the detail pane beside it is the other half of a master-detail split, so it stays its own region.

✅ 2.8.0 — Released 2026-07-30 — the images release: every turn can be drawn from your own character and location tags, InvokeAI gains Anima models, a Face Fix that needs nothing installed, live progress and board filing, your character finally speaks their own words when your action says something, characters who were only talked about stop moving into the room, stat code can read the story clock, and a Stop button for anything rendering (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing

    • Draw the scene you're reading. A new button above the story sends the turn to your image server and puts the image under the narration. It isn't one prompt guessed from scratch: your characters' own image tags describe who is in frame and your location's describe where it is, both used exactly as you wrote them, and a small silent pass reads the turn's prose and adds only what is happening — the action, the pose, how the shot is framed, the light. That split is the point: the look of a place and a person stays the same from turn to turn, while the image still changes with the story. Whoever actually took part in the turn is who gets drawn, at most two of them, because image models blur people together past that; a character your world never tagged is tagged on the fly for this session only, so an untagged world still draws something. There are two buttons: one writes the tags for the turn and stops there, one draws it. Tags cost a single small text request and no render, so you can re-roll them and read what the turn would be drawn as before spending an image on it. A re-roll really is a re-roll: any tags that had to be guessed for an untagged character or place are guessed again, rather than the first guess being reused for the rest of the session — otherwise only a third of the line could ever change. Drawing keeps the guesses, so a place still looks the same from turn to turn — and re-roll again from under the image once you have one. You watch it draw: the half-finished frame your image server streams appears in place of the image and sharpens as it renders, the same as the editor already did. The tag field has the same Danbooru autocomplete as the editor’s Image Tags box, since hand-fixing a tag is exactly where completion earns its keep. Each turn keeps every image you make for it — arrow through them, delete the ones that missed — and the tag line is right there to read and edit, so a bad draw can be fixed by hand and redrawn without touching the story. Settings → Generation → Turn Extras → Scene Images draws every turn automatically; it's off by default, because an image renders after the turn's writing is done and holds your next action until it finishes. That wait is deliberate: one graphics card cannot run the artist and the writer at once without spilling both to system memory, so the game takes its turn, then draws. The Stop button is always there. The tag pass has its own editable prompt under Settings → Prompts → Scene Tags, and everything else about the image — the model, the quality tags, the negatives, the size — stays where it already was in your image settings, since those depend on which model you're generating with. Neither character names nor place names reach the image model: a place is matched by its full name only — a world with a location called Classroom A loses that, while classroom and a are untouched — and a name that is a real tag in its own right (a location called Kitchen) is kept, since that one does mean something to an image model. Character names go the same way: they are taken back out of the tags the AI writes (aliases too), including the tags derived for a character or place your world never tagged, and a character with no tags of their own is described from their description alone — a name is not a tag any image model knows, and left in it drags the image nowhere. Drawing a scene no longer slows the story down. The images are held beside the turns rather than inside them: every part of the game that reads back over your history had been re-reading a megabyte of image with it, on every sentence of every turn that followed — which made the narration crawl until you refreshed. Saving asks: scene images are big — around a megabyte each — so the Save dialog offers to include them and tells you what they weigh, unchecked by default. Left off, the story keeps each scene's tags (so it can be drawn again) and the images are gone. Autosave never carries them.
    • InvokeAI can draw with Anima models. Anima checkpoints are their own architecture — anime-focused, built on Cosmos Predict2 — and InvokeAI has supported them for a while, but Formamorph didn't: an Anima model wasn't even offered in the Model list, so a library full of them looked empty. They now appear alongside your SDXL and Z-Image checkpoints and generate the same way, with nothing extra to configure. Anima loads two companion pieces next to the checkpoint — a Qwen3 text encoder and a VAE — and the right ones are picked for you from what you have installed, with overrides in Settings → AI Endpoints → Image if you'd rather choose them yourself. One quirk is handled quietly: Anima accepts only six samplers, so a preset carrying one it doesn't know falls back to Euler instead of being rejected by the server.
    • Face Fix works on InvokeAI now, with nothing to install. Faces are what image models get wrong first, and they get worse the smaller the face is in frame — a character standing at the far end of a room is drawn at a fraction of the resolution the model was trained on, so the eyes go crooked and the mouth smears. A1111 users had a fix for this: the ADetailer toggle, which quietly re-renders the face on its own. InvokeAI has no such extension, so the toggle wasn't even shown there. Formamorph now builds the same thing itself out of InvokeAI's own parts — the toggle is simply called Face Fix, it sits in Settings → AI Endpoints → Image, and it works on both servers. It finds the face, cuts a generous margin around it, re-renders that at the resolution your model actually wants, and lays the result back over the image through a soft edge, so the fix blends instead of sitting on top as a patch. The margin is the part that matters: cropping tight to a face and re-rendering it is how you get a beautifully detailed head that belongs to a different painting — giving the model the shoulders and the background around it keeps the new face in the same style, lighting and angle as the rest. It's off by default, because a second render roughly doubles the time an image takes, and one graphics card can't do both at once. Two things are worth knowing before you turn it on. The first image you generate with it will pause noticeably: InvokeAI downloads the two small models that find the face, once, and there's no progress shown for that. And if there's no face in the shot — a landscape, an empty room, a figure turned away — nothing happens and you get your image as normal, rather than an error; the same is true if the fix itself fails for any reason. Anything found that covers most of the canvas is thrown out rather than trusted, since a face detector that thinks the whole image is a face would otherwise redraw your entire image. Available for SDXL and SD1.5 models; Z-Image and Anima render through a different path that has no way to mask a region, so the toggle is skipped for them.
    • Stat code can read the story clock. A stat's Dynamic Value Calculation could see every other stat but had no idea how much time had just passed — so it silently assumed every turn was an hour, which stopped being true the moment Measured Clock arrived. Regen already scaled with the real duration; code didn't, and there was no way to make it. Six values are now available inside the code: deltaHours (how long this turn took), elapsedHours (the story's running total), day and daypart for where the story stands at the end of the turn, and startDay / startDaypart for where it stood at the start — because a turn spans time, and an eight-hour sleep that begins in the afternoon ends at night. Dayparts are the same six words the rest of the game uses: night, dawn, morning, midday, afternoon, evening. That makes hourly drain (return current - 2 * deltaHours), conditional regen, and time-of-day effects (a stat that only climbs while it's dark) writable for the first time. With the clock off, deltaHours is simply 1 and everything behaves as it always has. One scheduling change comes with it: code that mentions any of these re-runs every turn, since time passes on every turn — previously stat code only re-ran when the AI moved some stat, which would have made a time-based stat tick only on the turns the narration happened to mention stats. Code that doesn't mention them keeps the old schedule exactly, so existing worlds are untouched. The editor's Test Code button runs your code as a one-hour turn on day one.
  • 🛠️ Developer tooling

    • A test harness for the three gameplay panels. The panels players actually read the story through had no tests, because there was nowhere to put one: they read three separate contexts and take every callback from GameViewer, which is far too large to mount in a test. src/test/gamePanels.tsx now stands the real Settings / GameData / Gameplay providers up around a single panel, stages a playthrough into them (committed turns, stats, which page is being viewed, the authored world), and hands back the panel's props as spies so a test can assert what the panel asked for. The two dependencies jsdom genuinely can't run — the VRM viewer (WebGL) and the TTS engine (Web Audio) — have reusable stubs beside it, and the TTS one is what lets a test put a panel into its "audio exists" state at all. Landed with regression tests for two shipped fixes that had no home: the partial re-generate items being held while a scene image renders, and the scene panel being remounted per turn so an edited tag draft can't follow the player to another turn and get drawn onto it. Both were verified by putting each bug back and watching the test fail.
    • Editing a turn's text is under test now. Hand-editing the narration does five things at once — it rewrites that turn's stored text, re-reads who took part from the new prose, throws away the turn's memory digest and its characters' diary entries so both rebuild from what's actually written, throws away the player's own rewrite of that memory (it described prose that no longer exists), and does all of it to the turn being viewed rather than the newest one. Every one of those had to be right for a save to stay coherent, and none was covered. Five tests now pin them, each proven to fail when its own piece is broken.

🐛 Fixed

  • 🛠️ Developer tooling

    • stat-numeral-probe.mjs — asks whether a stat value in the narration context leaks into the prose. With Measured Clock on, a stat's value is routinely fractional, so the context used to carry a numeral like 0.5833333333333333/100 — precise enough to look parrotable, and pure token waste. Both arms render through the real buildStatContext and the pre-fix arm substitutes the float back into the finished line, so heading, ordering, descriptors and meanings are byte-identical and the numeral is the only variable. Measures long decimals in the prose, echoes of each case's exact stat numerals, bare n/m ratios, stat tabulation, and prompt_tokens — the token cost measured off the endpoint rather than estimated. Its negative control is a whole-number stat block, where the two arms render identical context, so any metric that moves there is read as endpoint noise rather than signal.
    • monologue-probe.mjs — the counterpart to the dialogue-hold gate. That one asks whether a character speaks at all; this one asks whether they ever stop, which is the failure players actually report: on a turn with room to run, one character holds the floor for paragraphs while the player's half of the scene never arrives. Measures the shape of the speech rather than its presence — how many separate times someone speaks, how long the largest single block runs, how much of the narration sits inside quote marks, and how many turns in a row the same character dominates — over a 20-turn session that can be extended to any length. Its false-positive guard is the dialogue-hold participation bar scored on the same run's baited turns, so a fix that cures rambling by muting everyone fails rather than passes. Built on the committed dialogue-corpus.mjs.
    • scene-tags-probe.mjs — grades the scene-image tag pass on the one thing it exists to do: write the action layer and nothing else. Leakage is measured against each case’s own authored character/location tags (the real collision, not a guess at one), vocabulary against the tag list the app actually ships, and the headline number is cross-scene overlap — every other metric can look fine while a model emits the same five tags for every turn. Cases live in testing/baseline/scene-tags-cases.json, each carrying the action it should land and the contradictions it must not, with a nobody-in-frame case as the negative control.
    • rider-ablation-probe.mjs — asks whether the narration prompt's rider block still earns its place over a long session. Each of those guidelines was validated one turn at a time; none was ever measured at depth, where it competes for attention with an accumulating history. The probe replays one frozen 40-turn action script — generated once by freeze-script.mjs from the real choices stage, so the actions read like play and are byte-identical across arms — under the shipped prompt and under a stripped prompt that reproduces a real player session with the riders, the closing output contract and the voice rider all removed. Results are bucketed by turn index, so a guideline that helps early and fades late shows up as a slope rather than an average. Measures parroting the player's action back, self-repetition against earlier turns, resolve-vetting endings, handbacks, dialogue volume, stall motifs and re-describing the player. --drop <rider> runs leave-one-out on a single guideline once the two-arm result says which direction to dig.
    • presence-probe.mjs — measures whether a character who was only ever talked about gets written into the scene, over chained turns rather than one. Two arms built by the real shipped parses (bundled from src, so it also fails if the call sites stop using them), across real test worlds, with the bring-them-over path wired in so the probe reproduces the actual mechanism. Carries both no-call controls: a second present character acting in prose must survive both arms, and a present character named only inside dialogue is reported as the measured cost rather than hidden. Cases live in testing/baseline/presence-cases.json.
  • 👤 User-facing

    • When you say something, you actually say it. An action like "I tell him what I think" used to come back with your character silent and the other person doing all the talking — usually restating what you'd just said back at you ("So you're telling me what you think? I appreciate the honesty…"), which is as flat as it sounds. The narrator was being told your words were already spoken and its job was only the world's reply, so it had no way to put them on the page. Now your action is the turn's first beat: it gets written as it happens, and when it speaks, your character speaks — in real quoted words, carrying the temper the action named — before anyone answers. Say it angry and it lands angry. The other half is unchanged and deliberately so: whoever you spoke to still answers in their own voice with something of their own, which measured as the single biggest thing keeping characters talking at all. Measured across both reference models, characters answering you did not drop at all, while your character went from rarely speaking to speaking on most turns. Worlds that had customized the narration prompt keep their own wording.
    • You can stop an image generation. The only way out of a running generation was to close the dialog — which threw away your prompt and, on A1111, left the render cooking on the GPU anyway. While a generation is in flight the Generate button is now a Stop button: press it and the run ends, the dialog stays open with your prompt intact, and you can adjust and try again. Where the provider supports it, Stop also interrupts the server rather than just walking away from it — ComfyUI and InvokeAI already did, and A1111 now does too, so a canceled render actually frees the graphics card instead of finishing into the void. Cloud generations can't be called back once sent, so there Stop hands you the dialog back and discards the result. Providers that report no progress also show a plain Generating… line now, so the dialog no longer looks idle while it works.
    • Generated image tags stop containing the character’s name. Asking the AI to turn a description into image tags sent it the subject’s name, and the name came straight back as a tag — “dean wolfram” sitting at the front of a prompt no image model can do anything with, quietly dragging the image nowhere. The generator now works from the description alone, in the editor and in the game. If you want a name in there, type it into the tags yourself — which is the only case where it means something.
    • The Generate Image dialog reaches its own settings. The dialog told you it "uses your configured image provider (Settings → AI Endpoints → Image)" and left you to go find it — closing the dialog, backing out of whatever editor you were in, and losing your prompt on the way. There's now an Open Settings button beside that line which lands directly on the Image endpoint tab, with the generate dialog still open behind it and your prompt intact.
    • The generate dialog stops making the shared negative prompt look like your problem. It pre-filled the field with your preset's whole negative prompt — a wall of worst quality, bad anatomy, jpeg artifacts… you had to scroll past or edit around every time, while the matching positive prefix was applied silently and never shown. Both now behave the same way: the fields hold only what you're adding for this subject, and each shows the preset's own text as its placeholder, so an empty box reads as this is what you already get rather than nothing is being sent. One consequence worth knowing: the preset's negative prompt is now always applied, where before you could delete it out of the field for a one-off generation. Change it in the preset if you want it gone.
    • The Prompt Prefix and Negative Prompt boxes are big enough to read. Both were single-line inputs in Settings → AI Endpoints → Image, so a realistic negative prompt scrolled past in a one-line slot you had to arrow through to edit. They're three-line text areas now, like the prompt fields everywhere else.
    • Pick which InvokeAI board your images are filed under. Everything Formamorph generated landed in InvokeAI's Uncategorized pile, mixed in with whatever else you'd been making. Settings → AI Endpoints → Image now has a Board dropdown listing your actual boards, read live from InvokeAI — pick one and generated images are filed there. It defaults to Uncategorized, so nothing changes until you choose. Archived boards are left out, and if a board you'd picked has since been deleted the image quietly goes to Uncategorized rather than failing the generation.
    • InvokeAI shows the image forming, and its progress bar means something. Generating through InvokeAI, the bar jumped straight to 50% and sat there for the whole render — 50% was a placeholder, not a measurement — and unlike the other providers you never saw the image take shape. Formamorph now listens to InvokeAI's live event stream: the bar tracks the real denoising step, and the blurry in-progress frame appears and sharpens as it renders, the same as ComfyUI and A1111. The bar only ever moves forward, and if the stream is unavailable it falls back to the old status-based reading — a small nudge when the render starts rather than a made-up halfway point.
    • Characters the dialogue talks about are no longer treated as standing in the room. Whoever was named in a turn's narration counted as present — including someone another character merely mentioned. In a real 45-turn session that meant a professor who was never in the scene was reported present for 29 straight turns, purely because the dialogue kept referring to her; the story then wrote her in again, which named her again, which kept her present. Presence is now read from the narration's prose — what someone is shown doing — rather than from quoted speech, and a character who lives somewhere else is dropped from the scene line unless they've actually come over. Characters the story invents on the spot, and visitors who genuinely walked in, are unaffected — but a character who was only ever talked about no longer walks into the room and takes up residence there, which is how a passing mention used to become permanent. This also feeds the choices you're offered and which characters write diary entries, so those stop including people who aren't there. Stories running with Staged thinking already had this guard, since the planner decided the cast; it now holds in every mode.
    • The buttons over the story stop sitting on top of it. The row above the narration had grown to six icons — write tags, draw, regenerate audio, text to speech, export, edit — nearly three hundred pixels of buttons pinned over the text, which the story then scrolled underneath. Edit stays where it was, since it's the one action about the text you're reading; everything else moved into a menu beside it, where each action gets its name spelled out instead of a guessable icon. The pair is about a quarter the width it was, and adding a feature later can't push it back across the page. Each fades to the background while you read and comes back when you reach for it — on a touch screen, where there's nothing to reach with, they simply stay visible. If something is working — tags being written, an image drawing, audio regenerating — the shows the spinner itself and stays lit, so a job you started from the menu is still visible after it closes.
    • Z-Image stopped being handed the wrong text encoder. If you had more than one Qwen3 encoder installed in InvokeAI — likely if you also run Anima or FLUX.2, since each architecture needs its own size — Formamorph picked whichever one InvokeAI happened to list first rather than the one Z-Image needs. They're indistinguishable by the field the app was looking at, so it was effectively a coin toss decided by install order. The right encoder is now identified properly, and the encoder and VAE dropdowns only offer models the selected checkpoint can actually use instead of listing every one you own. If you'd worked around this by setting the Qwen3 Encoder override by hand, your setting is still honored and you can now safely clear it.
    • Repetition Penalty works on LM Studio now. The setting was sent under the name repetition_penalty, which is what vLLM-style servers and Formamorph's own built-in engine read — but LM Studio reads repeat_penalty and silently ignores the other, so the slider did nothing there no matter where you set it. That also meant the planning prompt's built-in penalty of 1 (deliberate: a penalty above 1 suppresses the near-identical cast lines and quietly drops characters from a scene) never reached an LM Studio model. The value now goes out under both names; a server ignores the one it doesn't know, so every endpoint gets the setting it understands. If you had already compensated by turning the penalty up in LM Studio's own model settings, it will now actually apply on top of that — worth a look at your numbers.
    • Stat numbers stop printing sixteen decimal places. With Measured Clock on a turn can take any amount of time — thirty-five minutes, three hours — and both regen and stat code scale by it, so a stat gaining 1 per hour gained 0.5833333333333333 on a thirty-five-minute turn and the panel printed exactly that, wrapping the row onto two lines. Values and their maximums now read as whole numbers, and the +/− change beside them keeps a single decimal when it has one (+0.6, −4.5) — rounding a real sub-point gain to +0 would have read as nothing having happened. The starvation log line reads to a tenth for the same reason. The stat keeps its full precision underneath, so fractions still accumulate into whole points across turns rather than being thrown away each turn. The AI is handed whole numbers too — a value that long is roughly seventeen wasted tokens on every single turn.
    • The scroll wheel changes a number field in both directions now. Click into a numeric box and the wheel nudges its value up and down — a quick way to dial in a stat's starting value, its regen, a threshold. It only half-worked: near the ends of a long list the value would go one way and refuse the other. At the top of the Stats page you could only count down, at the bottom only up, and it behaved properly solely in the middle, because the browser only let the wheel reach the field while the page still had somewhere left to scroll. The wheel now steps the field the same way wherever you are on the page. Nothing else changes: you still have to click into a box first, so scrolling past one you haven't touched can't alter it, and the field's own limits and increment are respected — a stat capped at 100 stops at 100, and a field that counts in halves still counts in halves. This applies to every numeric field in the app, not just the Stats screen.

✅ 2.7.0-beta — Released 2026-07-26 — the memory and time release: memories you can rewrite, pin, delete or write yourself from a full Memory Manager, a measured story clock that reads how long each turn really takes and dates every memory by it, semantic memory that ranks what the story recalls against what you just did, characters the narrator invents becoming real cast members you can open and remove, prompt chips that carry their own wording, and a regrouped Settings with swappable text-endpoint presets — plus [square-bracket] authorial direction and a long tail of editor, save and 3D-model fixes (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing

    • Stories no longer all begin at eight in the morning. Every world opened at 08:00 regardless of what it was written to be — a midnight vigil, a supper table, a pre-dawn launch, all reported as morning, and every later reading inherited that error. With Measured Clock on, one small silent request now reads your opening scene and works out what time of day the story actually starts at, then sets the clock there. It answers with a time of day rather than a number, because that's what prose can support: night, dawn, morning, midday, afternoon, evening. Measured against the bundled test worlds, it reads each one differently — a river landing at evening, a college town in the afternoon, a forest at morning — rather than defaulting them all to the same answer. It costs one request per new game, runs alongside the rest of the opening turn, and falls back to the old 08:00 if it can't tell. Existing saves are never re-dated: a story already underway keeps the clock it has, since a retroactive answer would shift every memory stamp you've already collected. Re-rolling your opening scene re-reads it, so a rewritten opening can't contradict its own clock. The prompt is yours to edit under Settings → Prompts → Opening, beside Clock.
  • 👤 User-facing

    • The Log reads as a diary, not a stopwatch. Entries were stamped [0d 0h], [1d 5h] — time elapsed since you pressed New Game, which is a fact about your session rather than about the story. They now carry the moment they happened: [Day 2, 19:15]. With Measured Clock on the minutes are real, so a five-minute conversation and a night's sleep no longer land on the same stamp. The Log also stopped pretending your save button happened in the story: entries about the game rather than the world — saving, loading, a failed request, an aborted generation, rolling back a turn — now appear dimmed and without a timestamp, because dating "Game saved" to a Tuesday evening in the fiction was simply untrue. Story events — moving somewhere, a trait landing, going hungry — keep their stamp. Logs written before this change are all treated as story events, exactly as they were shown before.
  • 👤 User-facing

    • See which memories the story actually used. A memory being kept and a memory being sent aren't the same thing: with Semantic Memory on, the kept pool is ranked against what you just did and only the most relevant handful rides each turn. The Memory tab and the Memory Manager now mark the ones that reached the story on the last turn with a left accent bar — hover for confirmation. Everything unmarked is still remembered, just quiet this turn; struck-through lines are the ones the story let go, unchanged. A memory Scene Recall sent back as full prose rather than a summary is marked too, and tagged Scene in the Manager. Filter chips go with them, naming the form the story holds each memory in: Verbatim (it has the real text — a recent turn, or one Scene Recall brought back whole), Summary (it has the compressed line, sent last turn), and Held (it has nothing this turn — still remembered, waiting to matter again). No memory is ever two of the three. The Memory tab gains its own short row of these, and the Manager's Mine chip is now Custom. Nothing is marked until a turn has actually run, so a freshly loaded save shows no marks rather than replaying an old turn's selection.
    • Memories show when they happened. With Measured Clock on, every entry in the Memory tab and the Memory Manager carries its place in the story's clock — "Day 3, evening — two days ago" — the same stamp the AI reads. Both readings sit together because "Day 3" alone makes you do the subtraction and "two days ago" alone loses where you were. Player-written memories are stamped at the moment you anchored them, and recent memories say "moments ago" rather than nothing. Nothing appears while Measured Clock is off: without it every turn costs a flat hour, so a date would be a turn count in disguise. Turning it on mid-story dates the older turns at that flat hour — rough in scale, but still in the right order.
    • The story's clock reads as a date, not a stopwatch. The line under the character panel showed "0 days, 0 hours"; it now reads "Day 1, morning" — where you are in the story rather than how long you've been playing, and the same wording the memories and the AI use.
  • 👤 User-facing

    • The "where things stand" line is editable. Every recap ends with a sentence stating the present — "Now you are at Sarah's Place with Sarah Jones present; the scene is already underway." — which was built in code and couldn't be touched. It's now a Now Message under Settings → Prompts → Narration → Messages, sitting directly beneath the Recap Message it closes. It is built from ordinary chips — Location (Content: Name), the containing Location (Scope: Parent), Entities (Scope: In Scene), Time and Notes — each carrying its own wording in its Prepend/Append fields. It also now names the place your location sits inside: "Now you are at the Kitchen, inside the Old Mill with Mira present…", which quietly disappears at a top-level location. Each optional one carries its own full clause and vanishes when it has nothing to say, so a scene with nobody around still reads as a clean sentence — reorder them, reword the frame around them, or drop the ones you don't want. Your notes no longer repeat here by default: they were being sent twice each turn, once in the prompt's own Player Notes section and again in this line, and measuring both models showed the second copy changed nothing about whether the story respected them. The chip is still there if you want it back.
  • 👤 User-facing

    • Measured Clock (experimental). Settings → Generation → Memory, off by default. The game has always charged a flat one hour per action, whether you traded three words or slept through the night — so the Log's clock, stat regeneration, and hunger drifted further from the story with every turn. A small silent pass now reads what actually happened and measures it: minutes for a few words, hours for a meal or a night's rest, and when the story says "three weeks pass", three weeks. It rides alongside choices and stat updates, so it costs no extra waiting on a parallel-capable endpoint. Anything it can't read falls back to the old flat hour, so a missed measurement plays exactly like before. Turns that were never measured — every turn in an existing save — keep counting as an hour each. The pass has its own Clock tab in Settings → Prompts (system and user message, like every other prompt) so you can retune how it judges a scene; presets saved before it existed simply pick up the shipped default.
  • 👤 User-facing

    • Chips can carry their own wording. Location, Entities and Notes chips gain Before and After fields in their pop-out — text that sits either side of the value, like "…at the mill with Mira present." The trick is that both disappear along with the value whenever the chip has nothing to show, so a sentence never ends up reading "…at the mill with present." or, worse, "…with N/A present." Spaces matter in these fields and are usually the whole point, so the field echoes what you typed with spaces shown as ·. Chips that render a whole block — World, Stats, Traits, Lore — don't offer the fields, since there's no sentence to fit into.
  • 👤 User-facing

    • Characters introduced by their hands and faces are found now. When the story invents someone your world never defined, the game watches the prose to work out that a real person has entered the scene. It needed the name to appear mid-sentence twice — a rule that keeps ordinary sentence openers like "But" and "That" from being mistaken for people, but that quietly stranded any character whose name happens to start every sentence it appears in: "Lyria's hand is warm as it closes around yours… Lyria glances back with a playful smile." She speaks, she's touched, she sits down, and she was invisible. Owning a body or an expression — her hand, his voice, their gaze — now identifies a person on its own, the same way a title like Doctor always has. Checked against 47 recorded sessions: it finds two more genuine characters and nothing that isn't one.
  • 👤 User-facing

    • A Parent scope for the Location chip. Its Scope setting now offers Parent — the single location that contains this one, and nothing else. Reachable already covered the parent, but bundled in every sibling alongside it, which is too much for a sentence that just wants to say where you are: "the kitchen, inside the Old Mill." Nothing at the top level, where an affixed chip simply disappears. The scope buttons now sit on two rows — Current · Sub · Parent, then Reachable · Destinations — so the labels stay readable.
    • An In scene scope for the Entities chip. Lists whoever has actually taken part over the last few turns, wherever they came from — including characters the story invented that your world never defined. Different from Here, which lists whoever the location is authored to contain.
    • A Time chip. The story's own clock — "Day 3, evening" — usable in any prompt. It shows nothing while Time in Memory is off, so a chip that carries wording around it disappears cleanly rather than needing its own switch.
  • 👤 User-facing

    • Location and Entities chips can render just names. Their Content setting was Full or Summary; it now offers Name — the bare name, or a comma-separated list of names, with no description and no field labels. It's what lets a chip sit inside a sentence rather than in its own section. The Sub-locations scope option is now labeled just Sub, so the chip's label stays readable when several options are combined.
  • 🛠️ Developer tooling

    • notes-duplication-probe.mjs — measures whether the player's notes need to appear twice in a narration request (system-prompt section, recap now-line clause, or both), judging each generated passage against the note itself.
    • time-delta-probe.mjs + time-delta-cases.json — scores the clock pass against eight scenes with known duration ranges, including stated time in both directions (a three-week skip that must be honored, a stated ten minutes that must not be read as one). Prints the flat-hour baseline alongside, so the comparison is explicit.
    • time-stamp-probe.mjs — A/Bs the digest band with and without in-world time stamps over a real session dump. Scores temporal recall ("how many days ago did this happen?"), story span, event ordering, and the narration itself (length, dialogue rate, and whether calendar words leak into the prose). Samples the clock two ways — the flat hour-per-turn that ships today and a plausible measured spread — so the value of stamping can be read separately from the value of measuring. Pulls its formatting from the real gameClock.ts at runtime, so the module is the only thing to edit between runs.
  • 👤 User-facing

    • Time in Memory (experimental). Settings → Generation → Memory, off by default. The AI has never known when anything happened — memories reached it as an undated list, so "three weeks ago" and "an hour ago" read the same and it guessed. Each remembered moment now carries its place in the story's own clock — "Day 3, evening — two days ago" — and the recap closes by stating the present moment. Both readings ride together because models are unreliable at working out one from the other. Time of day stays coarse (dawn, morning, evening) rather than a clock reading, which keeps exact numerals out of the prose. It uses the same game clock the Log has always shown, so nothing about how time passes changes yet.
  • 👤 User-facing

    • Edit the story's memories. The Memory tab gains a Manage Memories button opening a full memory manager, where every remembered moment can be rewritten in your own words, regenerated if the story's first summary missed the point, deleted (recoverable — switch the filter to Deleted to restore it), or written from scratch: add a memory for something the story should know that never happened in a turn. Anything you write by hand is always kept — the story doesn't get to let go of your own words — and your own memories are never judged by the memory selector at all. They still ride even with Memory Summaries switched off. Search and filter chips (Kept · Let Go · Edited · Mine · Deleted) make a long story's memory list navigable, each edited memory shows the story's original underneath with a one-click Revert, and Reset All My Changes puts everything back the way the story had it. Nothing is destructive: the story's own summary is always preserved underneath, so every change is reversible.
  • 👤 User-facing

    • Keywords can contain any character now — commas included. Trigger keywords, secondary keywords, entity aliases, placeholder values and tags used to be split apart on every comma, which made a regex keyword like \d{2,3} impossible to write and quietly broke an alias like Elizabeth, Queen of Grayhold. Each chip is now stored on its own, so a comma is just a comma. Press Enter to add a keyword — the comma key types a comma. Pasting several lines at once still adds one keyword per line, and if a keyword you just added looks like a list, a small "Split into 3?" link appears under the field — one click if you meant a list, ignore it if you didn't (it never appears on regex entries).
    • ⚙️ Semantic Lore matches on the entry, not on its keywords twice. Because the editor used to copy an entry's name from its keyword list, a matching entry embedded those keywords twice — weighting them against the lore text the entry actually contains. The repeat is dropped, and the activation threshold was retuned against a purpose-built 45-entry test world (0.30 → 0.39). The old value was tuned on too small a sample and fired far too eagerly — on a realistically-sized world it was right less than half the time. The new setting roughly doubles how often the right lore surfaces while cutting the wrong lore sharply. Semantic Lore users re-embed their entries once, in the background, the next time a world loads.
    • Dictionary entries have their own name. An entry's name used to be overwritten with its keyword list every time you touched the keywords, so the sidebar read threat, enemy, evil and the AI saw that same list labelling the lore. The entry editor now has a Name field you control: call it Hostile Forces and it stays that, however you edit the keywords. Leave it blank and it falls back to the first keyword, so existing entries read exactly as before until you rename them.
    • Chips can be edited on mobile. Editing a keyword chip needed a double-click, which doesn't exist on a touchscreen — so on mobile you could add and delete keywords but never fix one. Tap a chip to edit it; double-click still works with a mouse, and dragging to reorder is unchanged.
  • 👤 User-facing

    • Preview the world's player model in the World Editor. Overview → Custom Player Model (VRM) gains a Preview button beside Change and Remove. It opens the attached model in the same details view the model library uses: the 3D viewer to orbit and zoom, the customization sliders to check its morphs and colors actually respond, and what the file says about its author, format, size and license — the last of which is worth reading before you publish a world that ships someone else's model. No need to start a game to find out the model loads.
  • 👤 User-facing

    • Turn image generation off entirely. Settings → AI Endpoints → Image gains a Disable Image Generation checkbox right under the preset picker. Tick it and every "Generate with AI" sparkle button disappears — in the World Editor, entity and location images, and anywhere else they appear — without touching your saved image presets. Untick it and they're all back.
    • Prompts sits before AI Endpoints. The Settings tab order now reads Presentation · Generation · Prompts · AI Endpoints · Accessibility.
  • 👤 User-facing

    • A broken AI connection no longer strands you on the main menu. The "can't reach your endpoint" warning used to fire when you tried to enter a world, and it refused to close — so a server that wasn't running yet meant you couldn't get in at all. It now appears once the world has loaded, and you can dismiss it: read the world, look around, open Settings and fix the endpoint from inside the game. Nothing is lost by continuing — only the turn itself needs the AI, and the warning still disappears on its own the moment your server answers. The first-run "set up your AI" prompt is unchanged.
  • 👤 User-facing

    • Swap between text endpoints with presets. Settings → AI Endpoints → Text now has a preset picker, matching the one the Image tab already has. Save several endpoint configs — a cloud model, a local server, a friend's API — and switch between them from a dropdown instead of retyping the URL, token, model, context window, and output cap each time. An immutable Default entry always sits at the top: it's the shared built-in endpoint, read-only, and can't be deleted or renamed — pick it to go back to "just use ours". On the web the picker replaces the old Use Custom Endpoint checkbox (selecting a preset other than Default is using your own endpoint); on the desktop app it appears only when Use My Own Endpoint is checked, since a bundled local model has no endpoint to swap. Your existing endpoint carries over into a preset automatically.
  • 👤 User-facing

    • Direct the story with [square brackets]. Bracketed text in your action now speaks to the AI as the author, not your character — I climb on behind her. [She agrees, and they ride off.] steers what happens, its pace, or its tone, without your character saying a word of it. The direction never appears in the story's prose, and the turn's memory records only what actually happened. The action box's placeholder hints at it, and a ? beside the box opens a How to Play popup (it glows until first opened, like the World Editor's) with four short tabs: writing actions, choices (including Ctrl+click — press-and-hold on mobile — to stack a choice onto your action instead of replacing it), directing with brackets, and how the story's memory and Notes work — with the Choices and Memory Summaries settings right there in their tabs.
    • A Messages view for the narration prompt. Settings → Prompts → Narration's growing row of message editors (Recap Message, Recall Message) is now one Messages view: the conditional lines that ride the story exchange, stacked with their own reset buttons, each appearing only while its feature is on. It also gains the new Direction Message — the line sent alongside a [bracketed] action, now editable per preset like the rest (older preset files simply keep the default).
  • 👤 User-facing

    • Semantic Lore (experimental). A companion toggle to Semantic Memory, off by default. Dictionary entries can now also activate when your action's meaning matches them, even with none of their keywords in the text — "I climb the ruined tower on the headland" wakes an Old Beacon entry whose keywords are only "beacon, lighthouse". Keyword activation is completely unchanged; meaning-matches only ever add entries (at most three per turn, above a tuned similarity bar). The AI-context legend marks them with ≈ and shows the similarity, since there's no keyword hit to highlight. Uses the same on-device model as Semantic Memory. For authors: a clear, concrete entry description now doubles as its activation net — sprawling synonym keyword lists become optional rather than necessary.
  • 👤 User-facing

    • Scene Recall (experimental). The third semantic toggle, under Semantic Memory, off by default. When your action clearly returns to an old moment — pulling out the keepsake a dead friend carved you — the game brings that turn's full original narration back for the AI, word for word, framed explicitly as a remembered scene ("this already happened; everything since still stands"). Never presented as recent history, at most two scenes per turn, and never near-duplicates of each other or of recent turns — the guard against the repetition freeze that got the old keyword version of this feature removed. Measured on both test models: a planted scene detail no summary carried (a carved gull whistle) surfaced in 0–1 of 3 runs without recall and 3 of 3 with it, with zero cases of the dead friend being written back alive.
  • 👤 User-facing

    • Memory Cap (experimental). An always-on companion to Semantic Memory, off by default. Normally every surviving memory rides along each turn, growing with the story; with a cap set, only the N most relevant to your current action ride — even when more would fit — keeping long-story prompts small and sharp. The story's opening and the newest memories always stay, and without relevance data the cap simply does nothing (it never trims blindly by age). Measured on both test models with a 28-memory story capped to 12: recall of a planted old fact held or improved (25%→33% cloud, 83%→92% local) while carrying 57% fewer memories.
    • The Scene Recall prompt is editable. Settings → Prompts → Narration gains a Recall Message view beside the Recap Message (shown while Scene Recall is on): the line that frames a recalled scene as the past is now yours to edit per preset, with the same reset-to-default. Saved and shared presets carry it like any other prompt text; older preset files simply keep the default.
    • Diary Recall (experimental). The fourth semantic toggle — under Semantic Memory, shown with Character Diaries in Staged mode, off by default. A character's motivation pass used to carry only their five newest diary entries; now it carries their three newest plus up to two relevant older ones — she remembers the last time you put a hand on your knife, even if it was thirty turns ago. Same total entry count, so it costs no extra context, and a near-duplicate guard stops a brooding character's five identical "I still don't trust them" lines from crowding out the one distinct memory. Retrieval is deliberately conservative (short diary lines are the hardest texts to match): it stays quiet unless the match is strong.
  • 🛠️ Developer tooling

    • Diary-retrieve probe. diary-retrieve-probe.mjs + semantic-diary-cases.json: threshold sweep on diary-shaped first-person entries, shared-word guard asserted, precision/recall per threshold with the shipped value marked.
    • Rehydrate probe. rehydrate-probe.mjs + semantic-rehydrate-cases.json: the dead-Jim temporal fixture — three arms (no recall / bare-pair splice / framed exchange), alive-write and detail-recall metrics, including a bait case the scene itself sets up.
    • Semantic-lore probe. semantic-lore-probe.mjs + semantic-lore-cases.json: threshold sweep for meaning-based dictionary activation — paraphrase actions (asserted keyword-free) vs unrelated actions, precision/recall per candidate threshold, shipped value marked.
    • Semantic-band probe. semantic-band-probe.mjs + semantic-band-cases.json: A/Bs the two digest-band trim strategies on a 28-digest story with planted old facts — a deterministic trim-survival stage (embeddings in Node, no LLM) and a narration-recall stage on both test targets.
    • Recall-margin probe. recall-margin-probe.mjs + semantic-recall-margin-cases.json: a same-cast one-house fixture where every scene clears the absolute floor — margin-over-median sweep (return-hit and charged-clean rates per margin, deterministic, embeddings only) plus a bare-vs-enriched query arm that settled the recall-query question.
    • Sem A/B scorer + rerank probe. sem-ab-score.mjs (objective scoring of the 50-turn semantic-memory A/B runs: planted-fact recall, 8-gram repetition, request size, digests riding) and t6-rerank-probe.mjs (cross-encoder rerankers vs cosine on the A/B's real failure cases — three browser-size families, raw-logit scoring).
    • Freeze-replay probe. freeze-replay-probe.mjs: replays a real failed session's scene-recall selection from its AI-context dump, old stack vs shipped stack — the validation that closed the charged-scene freeze gate (26/34 turns firing → 6/34, zero consecutive repeats).
    • Repetition-probe harness. format-arms-probe.mjs gains --prefill N (seed history with a recorded session's own narrations, then generate from there — repetition collapse is an in-context feedback loop and can't be replayed into from a clean start), a callback guard (callback x/n, so an anti-echo edit that also eats deliberate callbacks is visible), and sampler variants that ride the same batch as prompt arms. New arms-aggregate.mjs reports per-arm mean±sd with a Welch t against control.
    • Un-baited dialogue probe hardening. Real narration user template instead of the bare action (--bare keeps the ablation), in-batch no-speaker negative controls, and a per-case framing axis (withdrawn / neutral / engaged / expectant).
    • OOC-channel probe. ooc-probe.mjs: three-arm A/B (no convention / system-prompt line / line + user-slot rider) over bracket-direction cases built from the real T27/28 stall, with compliance/defiance/leak metrics and bracket-free control cases as the false-positive guard.
    • Incremental milestone probe mode. milestone-select-probe.mjs --mode incremental: batch-of-1 replay of the labeled fixture through the write-time selector — end-state scoring, must-forget tracking, and the prompt-arm history that produced the shipped paired-forget protocol.
    • Dialogue probes match the production message shape. dialogue-probe.mjs and dialogue-unbaited-probe.mjs now send the player action bare, without the Player action: wrapper the app dropped — their NPC-spoke rates measure the same request the game sends.
  • 👤 User-facing

    • Semantic Memory (experimental). A new toggle under Settings → Gameplay, off by default. When the story's remembered moments no longer fit the context budget, the game normally lets the oldest go first; with Semantic Memory on it instead keeps the ones most relevant to what you're doing right now — an old promise resurfaces when you return to the person you made it to. Relevance is judged by a small AI model running entirely on your device (a one-time ~23 MB download when you enable it); nothing about your story leaves your machine, and if the model isn't ready the game simply falls back to the old behavior. The milestone keep/forget system and your pins are unchanged — this only reorders what gets trimmed under pressure.
  • 👤 User-facing

    • Setting descriptions moved behind an info icon. Every setting's explanation used to sit inline as a paragraph of gray text, so a tab full of settings read as a wall. Each row now shows the control with a short one-line summary, and its full description opens from a small next to the label — so the tab stays scannable and the detail is one click away. Nothing was hidden that you can't reach in a tap, and the icons line up in a column down each tab.
    • Settings tabs are grouped instead of one long list. Presentation, Generation, Accessibility, and Image Gen now break into labeled sections — Appearance / Scene / Narration, Turn Extras / Reasoning / Memory / Performance / Inspection, Reading / Saves & Worlds, and Connection / Image — so a tab reads as a few named blocks rather than a wall of rows. Nothing was added, removed, or renamed — only regrouped, with Narration Reveal and AI Language moved down beside the other narration settings.
    • The Endpoint and Image Gen tabs are now one — AI Endpoints. The two endpoint tabs merged into a single tab with three sub-tabs: Text (your language-model endpoint), Image (the image-generation provider and its presets), and Tag Prompt (the booru-tag prompt). Every field and preset is exactly where it was, just under one roof — Settings now reads as five tabs instead of six, which also fits mobile better.
  • ⚙️ Backend

    • Scene Recall cooldown. A scene that just rode as a recalled memory sits out the next 2 turns before it can be recalled again, so the same moment no longer rides turn after turn while the story stalls. Re-rolling a turn reproduces its original recall exactly.
    • Memory Cap on by default. With Semantic Memory on, the story's long-term recap now keeps the 12 most relevant memories per turn by default instead of everything that fits (the number stays editable in Settings, 0 = uncapped). A 50-turn A/B showed uncapped recaps growing to ~48 memories with markedly more repeated narration; the cap cut request size ~40% on the local tier with recall of planted facts largely held. Fresh installs only — existing installs keep their stored value.
    • Sticky memory verdicts. The story's keep/let-go memory decisions are now made once, when a moment ages into long-term memory, instead of re-voting the whole list every turn — so memories no longer flip between kept and forgotten as the story grows. A kept memory changes only when a newly kept moment explicitly replaces it (a promise fulfilled supersedes the promise) or when you pin it. Verdicts are saved with your game, and the recurring memory-selection request shrinks to just the new moments.
    • Scene Recall margin threshold. A scene is only recalled when it stands out from the story's own baseline relatedness (beats the median similarity by a margin), not merely when it clears a fixed score — in a one-house same-cast story everything clears a fixed score, which had let one scene fire ten times during unrelated moments. Early game (fewer than 5 candidate scenes) keeps the old fixed-score rule.
    • 👤 Characters the story invents now show up on their own. A doctor, a shopkeeper, a stranger the AI writes into a scene used to be invisible to everything except the prose itself: no entry in the Entities panel, absent from the list of who's present, and never considered when working out what you can do next. It only ever worked with Staged thinking on, because it relied on the planner naming the cast. Names are now read straight out of the narration, so it works on plain narration too — and it costs nothing, so it's always on. In one real session a doctor who spoke across 23 turns was invisible on every one of them; she now appears the moment she's named.
    • 👤 Describe New Characters. New setting under Settings → Generation, available in every thinking mode. Turn it on and each invented character also gets a written description, so you can open them like any authored character. That's the only part that costs a request — one the first time each new character is named — which is why it's the setting and the rest isn't. If you already had Character Diaries on, it's on for you already.
    • 👤 Story-invented characters can be opened like any other. They appeared in the Entities panel as greyed-out rows that couldn't be clicked and never showed a portrait, because that panel only ever received the world's authored cast. They're now full entries — click one to open it and read their description, same as an authored character. They also join the Entities list on the turn they first appear rather than the turn after.
    • 👤 Help for the Entities panel. A ? sits at the top of the Entities tab during play, explaining who the list tracks, why a name nobody wrote can appear on it, and how to remove one the story got wrong. Like the other help buttons it's tinted until you've opened it once. Learn more now links to the wiki page that actually covers the topic rather than always the World Editor page.
    • 👤 Only actual people become characters. A repeated capitalized name isn't necessarily someone — across 16 test sessions on four real worlds, two thirds of the names picked up this way were talent agencies, cafés, TV shows, songs, wings of a building, weekdays and lore items. A name now has to behave like a person somewhere before it's remembered: speak, gesture, be introduced, or be described with a face or a voice. Names carrying a title (Doctor, Professor, Mr) still count on sight. On the same sessions this removed 26 of 35 wrong entries and kept every character who actually appeared on stage. One deliberate consequence: someone only ever talked about and never present — an off-screen relative, a shopkeeper's absent baker — is no longer added to your cast.
    • 👤 Remove characters the story got wrong. Because names are read out of the prose, now and then something that isn't a person — a tavern, a street — gets mistaken for one. Story-invented characters carry a remove button in the Entities panel. Removing one takes them out of the scene and remembers the decision, so they aren't picked up again later. Authored characters can't be removed this way; they belong to the world.
    • 👤 No more "Sarah, Sarah, Sarah present". The line telling the AI where things stand listed everyone once per recent turn instead of once each, so a character who'd been in the scene a while was repeated up to three times. Names are listed once now, most recent first.
    • ⚙️ No wasted location request when there's nowhere to go. The auto location change asked the AI where you were heading whenever a world had more than one location — but the reply is only accepted if it names somewhere actually reachable from where you stand. In a world whose locations aren't linked to each other, nothing is reachable, so every one of those requests was discarded on arrival: in one real 50-turn session, all 50. The request is now skipped unless there's somewhere to go, which removes a request per turn from affected worlds and changes nothing for worlds that link their locations. If your locations aren't connecting, check that each has its sub-locations or connections set in the World Editor.
    • ⚙️ The story's turning points hold their place in the recap. Which memories the recap kept was decided purely by how closely each one matched your latest action — so on a quiet turn a long-ago conversation about tea could outrank the birth of your child, simply because the words matched better. The memory keeper now also judges how much the story turns on each moment as it records it, and pivotal moments keep their slot against merely topical ones. Because different AI models use such a rating scale very differently, only the ordering is used, never the raw number — so the behavior is the same whichever model you run. Moments the model doesn't rate sit in the middle and are never penalized for it.
    • ⚙️ The recap stops reshuffling every turn. With Semantic Memory on, which memories the Memory Cap kept was decided fresh each turn by how closely each one matched your latest action — so the story's long-term memory was re-drawn from scratch every time you typed. Measured on a real 90-turn session: over half the contested slots changed every single turn, meaning a memory could drop out and return repeatedly while nothing about the story had changed. A memory already in last turn's recap now needs to be genuinely out-scored to lose its place, rather than merely out-matched by a hair — so the recap drifts as the story moves instead of churning. Memories at the two protected ends (the story's opening and the two most recent) are unaffected, and with Semantic Memory off nothing changes.

🐛 Fixed

  • 👤 User-facing

    • An entity's 3D model now actually appears in play. Opening View 3D Model on a character during a game gave you an empty box — the in-game viewer worked out the file's format from its MIME type, which browsers report inconsistently (and often not at all for .fbx and .obj), so it gave up before loading anything. The World Editor's preview was unaffected, which is why models looked fine while authoring and vanished in play. The format is now worked out from the filename first, then the MIME type, and failing both, from the file's own opening bytes — so models attached before this fix work too, without re-uploading.
    • "Exit Without Saving" now actually discards your changes. Leaving the World Editor without saving only closed the editor — every edit stayed live in the background. Go straight back in via Edit World and your discarded changes were still sitting there, with Save waiting to write them to disk. It looked like it worked if you closed the world's card first and re-opened it from the library, because that path reloads the world from storage; going straight back in did not. Exiting without saving now restores the world as it was last saved.
    • Animated 3D models now play their animation. An entity's model preview only ever showed the first frame — any animation baked into a .glb, .gltf, or .fbx sat frozen, so an idle loop or a flapping wing looked like a broken export. Every animation the file carries now plays on a loop while the preview is open. Models without animation are unchanged.
    • 3D models are no longer rendered flat and dark. The model preview's lighting used intensity values from an older version of the 3D engine, so the light barely registered and models were lit by an even grey wash — a bright red model rendered a muddy brown with no visible shading at all. Models now read at roughly twice the brightness, with shading that shows their shape.
    • Rotating a 3D model no longer turns it dark. A second, softer light from the opposite side now lifts the model's shaded half, so spinning it around no longer leaves you looking at an unlit back — while the main light still shades the model enough to show its shape.
    • Opening lots of model previews no longer breaks 3D rendering. Each View 3D Model left its graphics resources behind on close, so after enough previews in one sitting the browser started dropping the oldest — at which point models (and the character viewer) would silently stop drawing until a reload. Closing the preview now releases everything it allocated.
    • A 3D model or ambient sound can be removed again. Once attached, an entity's 3D model and a location's ambient sound were permanent — there was no way to clear either short of deleting the entity or location. Both now have an beside them, matching images.
    • The Entities tab no longer lists half the cast every turn. Without a Thinking mode, who's in a scene is read straight out of the narration — and that reading was far too generous. A two-word name matched whenever both words turned up anywhere in the text, so "The old woman is gone; a young man tends the mill" marked Old Man present, and any name built on an everyday word ("Blade Dancer" ← "Blades flash in the dark") fired on nearly every turn. Names are now matched as phrases, and a lone word only counts for a character if it's distinctive enough to identify them — so Emily still brings up Emily Foster, while Guards no longer summons Captain Vashti. Names made entirely of ordinary words (Rose Wolf, Iron Gate) keep matching as loosely as before, on purpose: missing someone who is there is the worse mistake. Separately, the rule that walks a character over from a neighboring location now demands their full name in the narration, so a passing mention can't relocate them.
    • Published listings no longer show placeholder gibberish. A character, world, or dictionary whose description used a placeholder chip published with a raw internal id in its blurb — a long string of letters and numbers where the text should be — on the Community Creations page. Listings now show the placeholder's actual content instead: a Variable reads as its value, and a Wildcard reads as its options, like {gray|green|amber} (long lists trim to three plus ). The item you download is untouched — its chips still roll normally in play. Existing listings pick this up when they're published again.
    • Optimizing images no longer freezes the app. The World Editor's Optimize Images button (and the same optimize step on world import, export, and backup restore) now does its image crunching off the main thread, so the interface stays responsive instead of locking up while large images re-encode. The button also shows a spinner and a live Optimizing 3/12… count while it works, so it's clear something is happening.
    • Markdown headings, blockquotes, and code render properly. # and ### headings came out at body-text size (a # heading looked identical to a ##### one), and blockquotes had no indent or quote bar at all, so nesting them showed nothing. Tables, inline code, and code blocks were missing their styling too. All of it now renders as intended — headings step down cleanly from # to ######, and nested blockquotes indent per level.
    • Subscript and superscript now work in Markdown. Write H~2~O and x^2^. They nest, and they leave ordinary prose alone — "about ~5 minutes and ~10 more" stays exactly as typed, since the shorthand never spans a space. ~~Strikethrough~~ is unchanged, and writing <sub>/<sup> HTML directly still works as before.
    • Help popup tabs are centered. The How to Play tabs sat left-aligned, so on mobile — where they wrap onto two rows — the rows lined up raggedly against the left edge. They now center, on both rows and on desktop's single row.
    • Worlds with no locations yet start properly. A world that hadn't had any locations authored — which every newly created world starts as — skipped its entire game setup on launch: the opening story prompt wasn't placed in the action box, and stat starting values, traits, and dictionaries were never applied for that playthrough either. Existing worlds were unaffected, since they all have at least one location.
    • Closing the World Editor cancels an in-progress image optimization. Exiting mid-optimize used to leave the run going invisibly: re-entering made the Optimize Images button feel dead for seconds (the abandoned run still owned the encoder), and when the orphaned run eventually finished it silently wrote its stale snapshot back over the world — undoing any edits made since. The run now stops within one image of exiting, a canceled run never writes anything, and the button shows "Scanning…" from the moment it's clicked so there's no feedback gap before the choice dialog.
    • Pasting a local server's address just works. LM Studio's "Reachable at" chip copies http://127.0.0.1:1234, and most guides stop at .../v1 — but the Endpoint URL field wanted the full .../v1/chat/completions, so the obvious paste failed with nothing explaining why. An address that's only a host and port, or that ends at /v1, now gets the rest filled in automatically, with a line under the field showing exactly where requests will go. Endpoints you've already set up are untouched, and an address with its own custom path is left exactly as you typed it.
    • The Diary prompt is hidden unless Staged thinking is on. Settings → Prompts only showed the Diary tab based on the Character Diaries toggle — but diaries are only used by Staged mode, so the tab could linger after switching thinking modes. It now appears only when both are active, matching the toggle that governs it.
  • ⚙️ Backend

    • Image encoding runs in a Web Worker. The decode → downscale → WebP re-encode pipeline (including the lossless WASM path and the image measuring/scanning step) moved into a dedicated worker with a main-thread fallback, and world-optimize reports per-image progress. Public imageOptim API is unchanged. Verified live: optimizing three ~30 MB images produced zero main-thread long tasks (previously 7–15 stutters of ~100 ms each).
    • Workers now load correctly in dev. The image-encode and save conversion/download workers are created as module workers — in dev, Vite serves their source with bare ESM imports, which a classic worker rejects, so they silently fell back to the main thread (or failed). Production builds were unaffected.
    • One worker now handles every JSON file. Save downloads used a worker of their own that built a base64 data URL; worlds, backups, and saves all share one JSON worker that hands back the file directly, so the extra encode step is gone. Downloaded files are byte-identical.
    • World and backup files are written and read off the main thread. Exporting a world, saving a backup, and importing a world file each ran one long blocking call over the file's whole text — with images embedded as base64, that froze the UI for seconds on a large world. All three now run in a Web Worker.
    • Model previews no longer stall on load. The model viewer decoded a model's data by hand, one character at a time, before three.js ever saw it — a multi-second freeze and a large memory spike for a big VRM. It now hands the browser the decode, and cleans up correctly if the viewer closes mid-load.
✅ 2.6.0 — Released 2026-07-23 — the memory release: a milestone system that keeps what actually matters with a Memory tab to pin or forget moments, digests that preserve names, places, and deadlines and travel as a single story recap ending with where things now stand, characters that engage you in dialogue several times more often, editable narration User and Recap Messages — plus a long tail of save, stat, model-viewer, and editor fixes (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing
    • Milestone memory: the story now remembers what matters. With Memory Digests on, older turns used to ride along as an ever-growing list of one-line summaries — most of them noise ("you walk the bridge road…") that crowded the story's context and, measurably, dragged characters toward silence. Now a silent AI pass reviews those older summaries each turn and keeps only real milestones — promises made, debts owed, wounds taken, things gained, how characters came to see you — while recent turns always stay. In testing this held dialogue as well as sending the full history at roughly a quarter of the context size, and it means long sessions stop growing their prompt every turn.
    • A Memory tab in the game side panel. See exactly what the story remembers long-term: kept moments in full, let-go moments faded. Pin a faded memory to bring it back for good, or forget a kept one — your pins always override the AI's choice, and they're saved with your game. Memories appear the turn they form — the newest sit under a subtle "Recent" divider (those turns are still in the story's full view regardless; the keep/let-go verdict takes effect once they age out of it).
    • Characters engage in conversation several times more often (thinking off). The character's-own-voice rule now also rides directly beside your action in the narration request — the position the AI weighs most. Measured on the default endpoint across four test batches: roughly 3× more turns where characters genuinely engage you in dialogue, including the first-ever 50-turn session that never lost its dialogue; on a strong local model, a perfect score with no side effects. Applies when Thinking Mode is off (other modes already carry the rule in their prompt); if you've customized the narration User Message, your version is kept.
    • The narration prompt's User Message is now editable. Settings → Prompts → Narration gains the same System / User Message toggle the other prompts have. By default it is exactly the Player Action chip — byte-identical to what the game always sent — but you can now add your own standing text around it. A word of care: this text rides directly next to the AI's reply, the most influential position in the entire prompt, and our testing consistently shows extra wrapping here can mute character dialogue. Your story history always records the plain action regardless, so experiments never rewrite the past.
    • Memory Digests is now on by default for new installs — with milestone memory it is simply the better way to play; existing installs keep whatever they had.
    • Narration now keeps 4 recent turns in full (was 3). Paired long-session sweeps of the verbatim window (2 vs 3 vs 4 vs 5, six 50-turn runs each) put the dialogue-holding peak at 4: fewer full turns starve the AI of dialogue examples to imitate, more just adds weight that drags it toward silence. Applies only if you haven't set the Narration verbatim-turns value yourself.

🐛 Fixed

  • 👤 User-facing

    • Characters speak up a little more often. Your action now reaches the AI as plain text instead of behind a Player action: label — matching how past turns already appear — which measurably nudges the narrator toward spoken dialogue on charged scenes, with no change to anything you see or type.
    • Characters confirm less and carry the scene more. Two vague narrator guidelines rewritten to say what they actually mean: "stay consistent with the story so far" (which quietly encouraged repeating the story's question habit back at you) is now a concrete continuity rule — positions, objects, and what's been said carry forward unless the action changes them — and the undefined "your reply is complete once the events have been told" clause is simply gone. Measured together on a real stuck session: character turns that end by re-asking something you already answered dropped ~58%, with slightly more spoken dialogue, on both test models.
    • Characters say what they want instead of asking if they may. One word in the narrator's dialogue guidance — characters "asking for what they want next" — read to the AI as a license to seek permission, feeding the confirm-everything habit on charged scenes. It now reads "voicing what they want next": on the default endpoint that nearly doubled turns where characters genuinely engage you (with charged-scene dialogue holding steady across a whole session instead of fading), and on a strong local model it added ~27% more spoken dialogue with no change to its confirmation habits. Credit where due: found by chasing the theory that the AI wrongly believes it needs the player's explicit permission for things to happen.
    • Characters answer you in their own words. When you speak to a character, the narrator could voice your question aloud and then have the character "answer" with only body language. The narration prompt now closes with an explicit contract: the character's spoken, quoted reply is the scene's answer — measured to roughly double the rate of turns where a character genuinely engages you in dialogue.
    • Reordering keyword/value chips works across multiple rows. When a keyword or placeholder-value list wrapped onto more than one line, dragging a chip on a lower row dropped it in the wrong spot. Chip reordering now tracks correctly regardless of how the chips wrap.
    • Dragging a dictionary entry drops it where you aimed. Dragging an entry down past others in the same zone landed it one slot higher than the drop indicator showed, and picking an entry up and dropping it without moving would banish it to the bottom of the list. Both now land exactly where the preview indicates. Upward drags and moves between zones/dictionaries were already correct.
    • Exported world files always end in .json. Downloading a world whose name was set produced a file with no extension, which the import picker then filtered out. The download now always carries the .json extension.
    • Character cards export reliably, even for small portraits. Exporting a character whose portrait was already small/well-compressed could fail with "Could not encode the portrait as WebP" — the export now always produces the WebP the card format needs.
    • A corrupt saved setting can't stop the app from starting. A single malformed value in browser storage (from a bad write, a half-finished migration, or manual tinkering) could throw while the app read it during startup and leave you on a blank screen. Settings now fall back to their default when a stored value can't be read, and quietly rewrite the good value back.
    • The Edit Text box no longer keeps edits you canceled. Making changes in the narration Edit Text box, cancelling, then reopening the same page could leave your abandoned changes sitting in the box. It now starts fresh from the saved text each time you open it.
    • Clearing a numeric setting no longer saves a zero. Emptying Max Output Tokens or an image width/height/steps/CFG field used to store 0 immediately — which then sent a broken request. Cleared fields now fall back to their minimum instead.
    • Editing a character or dictionary no longer discards your work if the save fails. "Save & Exit" on the unsaved-changes prompt used to close the editor even when the save errored — so the edits were gone despite the error message. It now stays open on failure so you can try again; same fix for the world editor's exit prompt.
    • A failed or empty AI turn no longer quietly breaks the rest of your session. When the AI returned nothing (or the request errored) partway through a turn, that turn's action was left stranded in the story history — which then paired every following turn against the wrong message, silently dropping older turns from what the AI could see. Failed turns now clean up after themselves and hand your previous choices back, so you can simply try again.
    • Switching worlds no longer shows the wrong world's comments. Opening one community world's details and quickly opening another could drop the first world's comments into the second's panel if its response arrived late. Each world's comments now stay with that world.
    • Re-rolling a turn's stat changes keeps the turn's regen. Re-rolling only the stat updates for a turn recomputed your stats from the pre-turn baseline plus the new changes — quietly dropping that turn's regeneration/starvation tick, and leaving stale colored deltas on the stat bars. The re-roll now reproduces the full turn (changes + regen), without advancing the clock a second time, so your stats and the bar match a normal turn.
    • Turning off narration read-aloud mid-turn no longer bleeds into the next. Stopping speech while a sentence was still being generated could play that leftover audio into the following turn and shift the word highlighting. Canceled speech now stays canceled.
    • Stop behaves correctly at every point in a turn. Pressing Stop while the narration was still fading in used to let the turn finish anyway (choices and stat changes landed as if you hadn't stopped); pressing Stop while the game was working out an automatic location move could corrupt the previous turn's saved state and blank its choices. Both now stop cleanly. And exiting to the menu mid-turn now cancels the AI request instead of leaving a model generating a reply into nothing.
    • The in-game character model appears as soon as it's ready. The model panel used to wait a fixed 5 seconds before showing your character, regardless of how fast it loaded. It now appears the moment it's ready — instantly for the default model, and after just the real load time for a custom one — while still avoiding the brief flash of the default model that the wait was there to hide. A deleted custom model also no longer leaves the panel spinning forever.
    • Your in-game body matches the customization screen. Body Weight and Breasts Size were applied at double strength once you were in-game — so the model looked heavier/curvier than the preview you set it to. The in-game model now renders exactly what the customization screen shows.
    • Customization now shows a slider for every body shape your model supports. The body sliders used to be a fixed pair (Body Weight, Breasts) — so any other body morph a model exposed was unreachable. Customization now reads the loaded model and gives you one slider per body morph it actually has, labeled by name. Sliders run from -25% to 125% of the model's authored shape (0% = off, 100% = the shape as sculpted). Existing characters carry over unchanged.
  • 👤 User-facing

    • Long sessions no longer starve the narration. With Memory Digests on, older turns used to ride the story's context as many one-line exchanges — and after enough of them, some models started imitating that brevity, collapsing narration from full paragraphs to a sentence or two that could even drop a character's reply mid-scene. Those digests now travel as a single recap exchange — the story asked "Recap the story so far." and answering in one block — which reads as a recap, not as how narration should look. Measured on a real collapsed turn: ~40 words and the NPC's answer lost 4 runs in 5 before, ~125 words and the answer present 5 for 5 after, with no change on models that never collapsed. The recap question itself is editable: Settings → Prompts → Narration → Recap Message.
  • ⚙️ Backend

    • Prerelease updates past .9 are offered correctly. The update check compared prerelease versions as text, so a build like beta.10 looked older than beta.9 and was never offered. Numeric parts of a prerelease tag now compare numerically.

    • Imported prompt-preset share codes are validated more strictly. A hand-crafted share code could smuggle a non-numeric sampler value (e.g. a text temperature) into the AI request, making every call fail. Sampler settings from a share code are now validated and dropped if malformed, like the other tuning fields already were.

    • Desktop app: tighter update and download hardening. The auto-updater now refuses a payload it can't checksum (instead of trusting the connection alone) and the Windows launcher re-verifies the staged download's checksum before installing it; a disk-full or write error during a model/update download now surfaces a clean error instead of crashing the app; and the app window is pinned to its own origin so rendered content can't navigate it elsewhere. The app:// file-serving path also got a stricter containment check.

    • Loading a save from another installed world migrates that world first. Cold-loading a save whose world was an older format could restore against the un-migrated world data, bypassing the compatibility net. The save now restores against the migrated world, and the world details/duplicate paths cache the migrated copy too.

    • New and duplicated worlds get collision-proof IDs. Their IDs were minted from the current timestamp, so creating or duplicating two worlds within the same millisecond could collide and overwrite. They now use a random UUID like every other generated ID.

    • The choices prompt's "who's in the scene" filter now covers every Entities chip format. Choices are shown only the characters actually present this turn, not everyone at the location — but if you'd edited the Choices prompt to use an XML-format Entities chip, that filter was bypassed and the full roster leaked in. All chip formats (plain, markdown, XML; full and summary) now get the scene-only roster, hoisted to one shared path so the list can't drift again.

    • Image generation and downloads can't hang forever anymore. A ComfyUI run that failed server-side (bad graph, out-of-memory) used to leave the app polling for an image that would never arrive; it now detects the failed run and surfaces the error. A community download that stalled mid-transfer is now cut off after 30s of silence instead of hanging, and a failed desktop update download now shows an error you can retry from rather than sticking on "downloading".

    • Context-window auto-detection can't save the wrong endpoint's value. Editing the custom endpoint while a detection probe was still running could persist the old endpoint's context size against the new one; the probe result is now discarded if you've moved on. The admin user search was likewise hardened against a stale page of results landing after a new search, and its query is now URL-encoded.

    • Trait stat effects apply once, not once per rule. A trait that both raised a stat's floor and shifted its cap could over-adjust the stat's value — the pull-to-floor correction was re-applied for every rule the trait carried instead of once. Now applied a single time per stat.

    • The 3D character viewer no longer leaks when you switch models. Swapping the previewed VRM while its model was still loading left the old model's animation loop running forever against a discarded scene, quietly piling up over a session. In-flight loads are now cancelled on switch.

    • The save database opens one connection instead of one per operation. Every save read/write used to open a fresh IndexedDB connection and never close it, so autosave leaked handles each turn — enough of them would have blocked a future save-format upgrade. The connection is now opened once and reused.

    • The story recap now ends with where things stand. The memory recap read as pure backstory — a chronicle with no now — and on some models that caused real harm: a live mid-scene conversation re-opened as a fresh arrival scene, and a standing roleplay agreement lost out to the recap's older framing, inverting who was playing whom. The recap block now closes with a code-built line stating the present: current location, who is present, and your Player Notes verbatim ("the player's own notes hold true: …"). Replayed against the real failing turns, the scene-reset and identity-inversion failures disappeared from every run; no AI call is added — the line is assembled from state the game already tracks. This also gives Player Notes a much stronger seat: they now ride right beside the story instead of only sitting mid-prompt.

    • Long-term memory now keeps the facts that govern the story, not just its events. The milestone selector judged memories by what happened — promises, debts, things gained — so standing state fell through: in real sessions it forgot the player's own stated goal and an in-effect roleplay agreement, leaving later scenes misread. Two fixes: the selector's keep-criteria and worked example now cover a role or pretense being played and the player's stated errand (probed: the goal digest went from dropped 3/3 to kept on both test models), with the example rewritten in placeholder form — generic <entry> slots instead of a concrete mini-story, so the model learns the reasoning rather than pattern-matching real play against example content; and the story's opening memory is always kept as an anchor — with it gone, models wrote a fresh arrival scene over the live one. A player's drop-pin still overrides the anchor.

    • Memory digests keep the facts, not just the shape of them. The turn-summary prompt compressed specifics into categories — "you state your destination and deadline" with the place and date gone for good, since a digest is all that survives of an old turn. Three rules rewritten: specifics stay specific (names, places, objects, amounts, deadlines go in as themselves, from the player's action or the narration), speech's upshot carries what was actually named, and a fact-dense turn is what the allowed second sentence is for. Probed on both test models: fact retention on the default endpoint rose from 8/21 to 18/21 planted facts (both runs), and the strong local model — already retaining everything — cut its over-long two-paragraph digests by two-thirds as a side effect.

    • Memory digests no longer teach the narrator past tense. With Memory Digests enabled, the one-line turn summaries were written in the past tense, and a history full of them could drag the whole story's narration into past tense (and mute dialogue) on some models. Summaries are now written in the story's own present tense — measured to eliminate the tense drift entirely and to make repeated-phrasing echo drop rather than rise.

    • Narration prompt trimmed. The Player Stats section's preamble line duplicated the stats guideline bullet; a section-by-section ablation on both test models measured it inert, so it's removed to spare prompt tokens.

  • 🛠️ Developer tooling

    • Now-line probe. New now-line-probe.mjs: replays a real session turn's narration context with and without a proposed recap closing line, for judging mid-scene anchoring and frame-fact fixes against the exact contexts that failed.
    • Summary probe measures fact retention. summary-cases.json gains three planted-fact cases (a stated goal with name/place/deadline, an introduced personal object, a named price) with per-case require regexes; summary-probe.mjs scores them, honors per-model endpoint overrides, counts the player's action as legitimate name material, and sends reasoning_effort: "none".
    • Quality baseline profile + scorer. New harness Profile Q: a 25-turn scripted Sedge Landing session with planted facts, question turns, action-outcome turns, and an agency trap, paced so memory digests drain like real play; qualityScore.mjs scores the countable side (planted-fact recall, cross-turn 8-gram repetition, sentence shape, format violations).
    • Digest-framing probe. New digest-framing-probe.mjs: replays a real collapsed turn's context in three message shapes (paired one-line digests vs one grouped block vs a "Recap the story so far" exchange) to isolate why narration length collapses once digests dominate the history.
    • Strict dialogue-hold probe. New dialogue-hold-probe.mjs + charged-interview-corpus.mjs: 25 consecutive baited turns (every action directly asks a willing character to speak) scored on a strict bar — a character must speak at least two quoted sentences that engage the ask, player-voiced lines excluded — with per-turn trend reporting, so a steadily decreasing dialogue rate registers as a failure even when the average looks fine.
    • Milestone-memory harness. New milestone-select-probe.mjs (hand-labeled 57-entry fixture across four story genres; must-keep recall and noise-keep metrics; prompt variants including the shipping few-shot selector) and carryforward-check.mjs (re-derives each chain's kept/dropped split and judges every dropped fact against late narration for contradictions), plus a selection-driven arm M in the dialogue-hold probe.
    • Format-arms probe: hybrid arm + edit crossing. format-arms-probe.mjs gains arm H (bare history with the label on the current turn only — byte-faithful to the app's real message assembly) and --edit variants now cross with --arms, so e.g. --arms H,B --edit fuse runs both cells fused in one paired batch.
✅ 2.5.2 — Released 2026-07-21 — entity aliases and smarter plural detection, a new Percentage stat type, an action box that grows into a popover, an editable opening scene, and self-sorting stat descriptors — plus narration fixes that keep characters talking through intimate scenes and stop them re-asking questions you already answered, dictionary triggers that cover everything the AI actually reads, and an end to reasoning errors on models that don't support reasoning (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing
    • The action box grows as you type — without shoving the game around. Writing a longer action now expands the input upward into a floating popover instead of pushing the narration and buttons out of place. It grows to a comfortable maximum and then scrolls inside itself, and drops back to a single line when you click away (your text is kept). Enter still sends; Shift+Enter adds a new line for multi-line actions.
    • You can edit the opening scene before it begins. The old Start Game button (which fired a hidden, uneditable opening prompt) is gone. When you enter a world the action box is now pre-filled with the editable opening cue — reword it, add a detail, or write your own — and pressing Send begins the story with exactly that text. Re-generating the opening turn drops it back into the box so you can change your starting action and try again.
    • Stat descriptors stay in order automatically. In the Stats editor, a stat's descriptors now keep themselves sorted by threshold (low to high) — the order the AI reads them in — so you no longer have to add them in the right sequence or delete-and-re-add to fix the order. Editing a threshold snaps the row into place when you click away. Existing worlds are read in the correct order too, even if their descriptors were saved out of order.
    • New "Percentage" stat type. Stats can now be a Percentage — a value pinned to 0–100% and shown to you and the AI as N% (e.g. 40%) instead of 40 / 100. It keeps everything a number stat has (descriptors, regen, dynamic code, body-slider bindings, don't-increase/don't-decrease locks); only the min/max range is fixed and the cap can never be moved. Switching an existing stat to Percentage snaps its value into range.
    • Help & changelog pop-outs read better. The in-app help, world Readme, and changelog pop-outs are taller (so long topics need less scrolling) while short ones still size to their content, and bullet lists now hang their wrapped text under the text instead of back under the bullet.
    • Smarter plural detection for entity names. The story parser now recognizes irregular plurals, so an entity named "Wolf" is detected when the narration says "Wolves" (likewise City→Cities, Child→Children, Person→People) — previously only a simple trailing "s" was caught. Applies to entity names and aliases alike.
    • Entity aliases. Entities now have an Aliases field — other names a character or thing goes by (e.g. "Synthia" also called "Matron" or "Em"). Aliases are detected in the story text just like the real name, so an entity is still recognized when the narration uses a nickname, and they're shared with the AI as "also known as" so it can use those nicknames naturally. Aliases travel with a world and with exported character cards.

🐛 Fixed

  • 👤 User-facing

    • Quick Start now honors the author's default traits. The world modal's quick-start button (renamed from Skip Customize to Quick Start) skips the setup steps but no longer drops the world's Enabled by Default traits — it now starts you with exactly the defaults the author intended, instead of no traits at all.
    • Characters keep talking during intimate and physical scenes. On charged scenes — closeness, touch, intimacy — characters could fall silent for the rest of the session, the story collapsing into wordless description. The narrator's dialogue guidance now frames speech as part of physical action (words woven into movement) instead of a separate activity, and turn endings favor a spoken line. Measured on the default endpoint: roughly 3× more turns with dialogue and the silence spiral pushed much deeper into a session, with no stray chatter added to scenes where no one is present.
    • Characters stop re-asking questions you already answered. On charged scenes, characters could stall the story by re-posing the same question ("are you sure?", "tell me true") turn after turn even once you'd committed. The story planner and narrator now treat an answered moment as settled — characters act on what you said and the scene moves forward — while still asking genuine questions when you haven't answered yet. Validated against a real stuck session: turn-ending re-asks dropped ~64%, with dialogue volume and legitimate hesitation intact.
    • Fewer turns cut off mid-sentence. The automatic paragraph guidance overestimated how many paragraphs fit the response budget, so intense scenes could hit the token cap and lose their final sentences — often exactly at the payoff moment. The per-paragraph estimate is now calibrated from real play sessions, so the AI is asked for a length it can actually finish.
    • Player Notes now trigger dictionary entries. Keywords written into your Player Notes now activate matching dictionary/lorebook entries, just like the current scene and your action already did — previously notes were sent to the AI but never scanned for triggers.
    • Dictionary entries now trigger on everything the AI is actually told. The rule is now simply: if the AI can read it, it can fire a trigger. Previously only the current location and the characters present were scanned, so a keyword in a nearby or sub-location description — text the AI was being given every turn — silently did nothing. Those blocks now trigger too, and they trigger on exactly the wording the AI receives: where a block is sent as a summary, the summary is what's matched. Two quiet oddities are gone with it — entries could fire on the story's internal save-file structure, and could fire on full descriptions in cases where the AI was only sent the short version. Always-present text (the world description, your stats and traits, and formatting guidance) is still deliberately not scanned, since it repeats every turn and would fire constantly. Authors: a keyword that appears in a neighboring area's summary will now activate — which is the intended control, but worth a pass over entries with very common trigger words.
    • No more reasoning errors on non-reasoning models. When a model doesn't support reasoning (e.g. many local models in LM Studio), Formamorph now stops sending it reasoning settings — which previously made those backends error on every turn — and the Native Reasoning control is replaced with a short "this model doesn't support reasoning" note. Reasoning-capable models are unaffected.
  • 🛠️ Developer tooling

    • AI Context viewer highlights only real dictionary matches. The Dictionary view in the AI-context pop-out used to run a plain case-insensitive keyword sweep over the whole prompt — highlighting text that never actually triggered anything and ignoring each entry's match settings. It now marks only the genuine activation hits for the turn, honoring regex / whole-word / case-sensitive / secondary-key rules, and only inside the text that was actually scanned (scene, history, and — for recursive entries — other entries' lore). Every highlight is a clickable chip whose popover explains why it fired (which keyword, where, which rule, secondary status), the entry legend dims entries that didn't activate this turn, and the match data is captured per turn so past turns stay accurate.
    • Stat editor tidy-up + shared collapsible. The Stats editor's lower half (Prevent AI Changes, Stat Descriptors, Dynamic Value Calculation) now matches the labeled-block rhythm of the other editor tabs instead of heavy section headings, and the code section uses a new reusable CollapsibleSection component.
    • Section-style regression probe. regression-style-probe.mjs A/B-tests the Default (Markdown) vs XML prompt preset on the committed regression corpus (planner continuation + digest), assembling each style faithfully — chip bodies through the real context builders, headings through the real restyle — so section style is the only variable. Runs under vite-node.
    • Fact-adherence style probe. factstyle-probe.mjs A/B-tests Markdown vs XML on a fact-dense, comprehension-trap scenario (fixed counts, positions, a name the player hasn't heard) across planner + narration surfaces at matched seeds, dumping every paired output for human judging. Also vite-node.
    • Dynamic baseline mode. The baseline harness can now play a full session live instead of a fixed script: a dynamic profile gives a turn count and a goal policy, and a chooser model (on the cloud default endpoint, so it never competes with the game model for VRAM) reads each turn's narration and choices and writes the player's next action through the real app pipeline. Falls back to the first offered choice if the chooser balks. Also handles worlds with embedded images (the "Optimize imported images?" dialog) on import.
    • Message-format arms + over-fire probes. format-arms-probe.mjs is a standalone loop harness that replays a recorded 30-turn session with self-managed history, so the shape of the conversation itself becomes testable: three message formats (the app's Player action: turns, bare first-person turns, and a single-message story rebuild), plus named prompt ablations and candidate edits, scored per turn for dialogue, freeze register, repetition, and the point a session goes quiet. It measured the narration dialogue fix in this release. overfire-probe.mjs is its false-positive guard — empty scenes and a mute animal companion, where any quoted dialogue at all means the rule is over-firing.
    • Narration re-ask probe. narration-reask-probe.mjs replays a real AI-context export's plan-mode narration requests and scores the re-ask loop (vetting questions ending a turn the player already answered), with dialogue-volume and legitimate-vetting guards; --arm B substitutes the current prompt edits into the recorded messages. The planner probe library also gained a PROMPTS_SRC override so an A/B baseline can run against a snapshot of GamePrompts.ts.

✅ 2.5.1 — Released 2026-07-19 — a new built-in XML prompt preset (section wrapping + nested-tag chip bodies), InvokeAI as a supported image generator, plus fixes for world export dropping entity folders/placeholders and missing-group items vanishing from the editor (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing

    • New built-in "XML" prompt preset. Alongside Default (Markdown) and Simple, you can now switch the whole prompt set to an XML style — a format some models follow more reliably. It wraps each context section in tags (<game_world>…</game_world>) and renders each chip's contents as nested XML (stats, traits, location, and cast become <stat>, <trait>, <entity> elements with child tags). The chips' new XML format option is also selectable individually on any preset. Like the others the built-in is read-only; duplicate it to a preset of your own to edit.
    • InvokeAI is now a supported image generator. Alongside Automatic1111/Forge and ComfyUI, you can point Formamorph at a local InvokeAI server to generate entity, location, and world images. Pick an installed SDXL, SD1.5, or Z-Image model from a list that loads straight from the server — no filenames to type. Z-Image needs a Qwen3 text encoder and a FLUX-type VAE too; Formamorph finds installed ones for you, with an override in Settings if you'd rather choose. The How to Set Up button shows the one line to add to your invokeai.yaml so the app is allowed to reach it.
  • 🛠️ Developer tooling

    • The dev router can set Image Gen settings directly. window.__fmDev.setImage({ provider, model, … }) patches the active image-endpoint preset in one call, so preview verification can reach a provider/model state without driving the Settings dropdowns by hand. DEV-only, tree-shaken from production.

🐛 Fixed

  • 👤 User-facing
    • Entities and traits filed under a missing group no longer vanish from the World Editor. If a world's group list was lost but its entities or traits still remembered which group they belonged to, those items simply didn't show up in the editor — only the ungrouped ones loaded. They now appear at the top level instead of disappearing.
    • Exporting a world no longer drops your entity folders or placeholders. The Download World file left out entity groups and placeholders, so importing that file elsewhere lost your folder organization and any author-defined variables/wildcards. Exports now carry the complete world (saving to your library was already fine — only the downloaded .json was affected).

✅ 2.5.0 — Released 2026-07-18 — a local Models library with its own tab — add, preview in 3D, inspect license, reorder, delete — plus first-run model onboarding sized to your GPU, body sliders that bind from every model grouped by name, two more local models and a new top-tier pick, dead-endpoint and unloaded-LM-Studio detection, World Editor tab help, and a batch of placeholder, text-box, and Community Creations fixes (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing

    • Two more local models added, one for mid-range cards and one for the top end. Gemma-4 12B Uncensored joins the ≤8GB tier as its strongest option yet — it edged out what was there on our tests, and fits comfortably on an 8GB card. Gemma-4 26B StyleTune V2 joins the No-Limit tier as the second-strongest model overall, just behind MeroMero. Both are additions, not replacements — everything already in the list stays.
    • A new top-tier local model: G4 MeroMero 31B. For machines that can run a large model, it's the strongest option Formamorph now offers — it scored highest of every model we tested, and it's the one that best knows when to leave your stats alone: it doesn't nudge your health or nerve for glancing around a room or retying a bootlace, the way most models do. It joins the No-Limit tier of the download list. Nothing changes for existing setups — it's simply there to pick if your graphics card has the room (about 19 GB).
    • On mobile, switching library sections is now a bottom tab bar. Worlds, Entities, Dictionaries, and Models sit across the bottom of the screen — one tap each, always visible, with the current section highlighted — instead of crowding the top row (which the fourth section had made too tight in portrait). The desktop layout is unchanged.
    • Your VRM models now have a library of their own. A new Models tab sits alongside Worlds, Entities, and Dictionaries, showing every avatar you've added as a card with its own portrait — read from the model's own thumbnail where it has one, and drawn for it where it doesn't. Models are named after the title the artist gave them rather than whatever the file was called, they can be reordered by dragging like every other card, and importing a file you already have asks first instead of quietly keeping a second copy of something tens of megabytes large. Files that aren't really avatars (a plain .glb) are marked as such, since they carry no license information and aren't guaranteed to pose or morph. Deleting is free except for your last model — you always keep something to be — and if a save is currently wearing the model you're removing, Formamorph names the saves before you commit rather than letting you find out later.
    • The model you start with is now yours to manage. The bundled avatar appears in your Models library as Default Model, like anything else you'd add — preview it, export it, reorder it, or delete it once you've added another. It's placed there once, so a delete sticks rather than reappearing next launch, and Formamorph keeps at least one model at all times so there's always someone to be. Existing characters carry over untouched.
    • Click a model to look it over, test it, and see who made it. Opening a card gives you the avatar in 3D — turn it, look closer — beside everything its file says about itself: the artist, the license, and whether they've allowed redistribution, commercial use, or asked to be credited. Where a model simply doesn't say, it reads Unknown rather than guessing on the artist's behalf. Underneath, the same body and color sliders you'd use in-game appear for whichever ones the model actually supports — so a creator can confirm an exported avatar responds the way they intended. Export Model saves it back out exactly as it came in, so the artist's own details travel with the file rather than being stripped by the round trip.
    • New installs now point you at the model you need, instead of failing on the first turn. The desktop app runs its AI on your own machine, but a fresh install has no model downloaded yet — so the first thing you tried to play simply didn't work, with nothing to explain why or where to go. Formamorph now offers you a model up front, sized to the graphics card it detects, with one click to download it (the full list is a click away if you'd rather choose). You can dismiss that and look around; if you then start a world before it's ready, it asks again — and the download keeps running while you browse, starting your world by itself the moment it's ready.
    • The Readme can now use placeholders. A world's Readme is shown on entry, once a playthrough's placeholders have been rolled — so it can now insert them like the other world text fields, and its Preview shows the rolled values in the formatted Readme the player will actually read. The World Description deliberately doesn't offer them: it's read in the library before anything is rolled, so a placeholder there would never resolve.
    • A dead endpoint now says so — and notices when you fix it. If you point Formamorph at your own server and it isn't answering, starting a world used to fail with no explanation. It now tells you it can't reach the endpoint and takes you to the setting to fix it. Better still, it keeps checking while it waits: start your server and the prompt clears itself and carries on into your world, with no need to reload the page. There's a Try again button if you'd rather not wait for the next check. Nothing changes if your endpoint is working, and this prompt never appears while your own endpoint is set up and reachable.
    • LM Studio with nothing loaded is now caught before you play, not after. If you point Formamorph at LM Studio while no model is loaded and the model name is still the stock one, every turn fails with "No models loaded" — but the server answers, so nothing looked wrong until you tried to play. Formamorph now spots that exact combination up front and says so. Everything else is left alone on purpose: a model that's listed but idle loads on demand, and once anything is loaded LM Studio answers to any name — so neither is treated as a problem.
    • World Editor tabs can now explain themselves. A ? beside the search box opens a short write-up of what the tab does and why it's built that way — including the parts you couldn't otherwise discover, like exactly which text the Dictionary searches for your keywords. Until you've read a tab's page its ? is tinted, then it settles down for good. Every tab is covered now — Stats, Entities, Locations, Traits, Dictionary and Placeholders. There's a fuller World Editor guide on the wiki.
  • 🛠️ Developer tooling

    • The dev router reaches the library's character and dictionary editors. #dev?modal=entityEditor and #dev?modal=dictionaryEditor open each on a blank draft, so both are reachable on a fresh profile without stored data. They were the only editing surfaces the router couldn't land on.

➖ Removed

  • 👤 User-facing
    • Three of the smallest downloadable models are gone, because they couldn't actually play. Qwen3-4B RPG Roleplay v2, Gemmasutra Small 4B and Gemmasutra Mini 2B are no longer offered in the model list. Every model Formamorph offers has to do more than write — it also has to quietly answer "did you just walk somewhere?" after each turn, and these three got that wrong a third of the time or worse, sending you to the stables while you stood still reading a notice. The Qwen3 one was worse still: now and then it returned nothing at all, which stopped a story dead on its first turn. The ≤4GB tab now offers Impish LLAMA 4B alone while we look for small models that hold up; if you've already downloaded any of the three, they still work — Formamorph simply won't recommend them to anyone new.

🐛 Fixed

  • 👤 User-facing

    • The "nothing here yet" message in Community Creations is now centered. When a section had no listings (or a search matched none), the "Be the first to publish one!" line sat off to the left instead of centered across the grid.
    • Selected multi-select chips are never invisible. A chosen value whose option hadn't loaded yet (for lists filled on demand, like the body-slider picker) counted toward the selection but drew a blank, unremovable chip. Such a chip now shows its own value as its label and can be removed like any other.
    • Binding a stat to a body slider now sees every model, not just the default. When you tie a stat to a body shape in the World Editor, the slider list used to offer only the bundled default model's shapes — so a shape that exists on another model you'd added was simply missing. It now gathers sliders from every model in your library (plus the world's own custom model), grouped under each model's name so you can tell where each comes from. A shape shared by several models appears under each; picking it drives that shape on whichever model is worn. The list loads the first time you open the picker.
    • Community Creations dialogs now name the right kind of thing. Deleting one of your published listings always read "Delete Published World", and the download button always said "Download World" — whatever you were actually looking at. Both now match the item: World, Entity, or Dictionary. The middle browser tab also read "Characters"; it now reads "Entities", matching the library tab.
    • Entity thumbnails in Community Creations no longer crop faces off. Entity art is almost always a portrait, but the cards centered the image — so a tall portrait showed its middle and cut the head off. Entity cards — and the larger preview when you open one — now anchor the thumbnail to the top. World and dictionary art is unchanged.
    • Starting a game no longer wrongly blocks you when your AI server is actually ready. If Formamorph checked your endpoint before you'd loaded a model in it (say, at startup), it remembered that "not ready" answer and would throw up the setup prompt the moment you pressed Start — even after you'd since loaded a model, so "Try again" fixed it instantly. It now re-checks the moment you start a game rather than trusting the old answer, so a working setup launches straight into play. The prompt only appears if a fresh check still can't reach your AI.
    • The model Formamorph suggests for your card is now the best one, not the biggest. The download list used to recommend whichever model was largest for your graphics card — which isn't the same as best, and in the top tier it meant suggesting a huge 70B model that most cards can't even run, ahead of the model that actually scored highest in our testing. Each tier now leads with its top performer and lists the rest in ranked order, so the first thing you're offered is the one we'd actually pick.
    • Large models no longer refuse to load on a graphics card that has room for them. Loading a big model at a large context size could fail with "context size is too large for the available VRAM" — on a card that could fit it. Formamorph loaded the model first and asked for the context second, so it filled the card with the model and left the context nowhere to go. It now reserves room for the context up front and fits the model around it. The strangest part, and the reason this hid for so long: the more graphics memory you had free, the more likely it was to fail — a busy card accidentally left just enough room to work. If you'd been lowering your context size to get a model to load, you can try putting it back.
    • A turn that comes back empty now tells you, instead of doing nothing. Once in a while a model returns no story text at all — nothing to read, nothing to choose from. Formamorph treated that exactly like you pressing Stop: it quietly gave up and sat there, so the only thing you saw was a submit that seemingly did nothing, with no hint whether the game was thinking, broken, or ignoring you. An empty turn now says so and suggests trying again or switching models. Pressing Stop yourself is unchanged — that's still silent, because you already know you did it.
    • World editor text boxes can be dragged taller or shorter again. Grab the bottom-right corner of any long text field — description, readme, entity and location descriptions, dictionary entries — and drag. They still start at the size they always have, so nothing moves until you decide to change it, and the size you pick sticks while you switch between Edit and Preview. The prompt editors in Settings are deliberately unchanged; they're built to fill their panel.
    • Trait descriptions read as words, not raw code. A trait whose player-facing description used a Variable or Wildcard showed the underlying placeholder token in the traits panel instead of the value. It now resolves like every other piece of player-facing text — matching how the location panel already behaved.
    • Traits can now lower a stat's minimum, within limits. A trait that lowered a stat's Min silently did nothing — Min could only ever be raised. Now one trait can lower a floor that another trait raised, back down toward the value you gave the stat, though never below it. A trait that only lowers a Min still does nothing on its own; this only matters when two traits touch the same stat. Max was already free to move both ways and is unchanged.
    • Deleting a character cleans up after itself. Characters are attached to locations by the location keeping a list of who's there — and deleting a character left its entry behind in every location it belonged to. Nothing broke, since the game skips names it can't find, but the leftovers stayed in your world file and built up over time. They're now removed along with the character. Worlds saved before this keep their existing leftovers; they remain harmless.
    • Image tags no longer leak into the story. Image Tags — the booru tags used only for image generation — were being sent to the narrator along with the description, on both characters and locations, so the AI read 1girl, blue_hair, smiling as if it were part of who someone is. They were always meant to stay out of the story, and now do. Your location and the characters in it are described to the AI on every turn, so this was quietly spending context and muddying every scene.
    • Locations no longer describe themselves twice. A location written before descriptions were split into player-facing and AI-facing versions could send the AI two different description lines at once — the current one, then the older text underneath it — leaving smaller models to guess which to believe. The AI now gets one description, and the older text is only used when there's no newer one to send.
    • The Readme popup is easier to read. The Readme, the help pages and the "What's new" popup all show their text on the same inset panel now, so the writing sits in its own framed area instead of floating on the dialog background — and it's still selectable if you want to copy a line out. The Readme is a little wider than it was to match; "What's new" is unchanged.
    • Text boxes can no longer be dragged sideways out of their column. The plain text fields that were resizable (trait, stat, group and dictionary notes) could be dragged in any direction, which pulled them wider than the space they sit in and skewed the layout around them. They now stretch vertically only, matching every other field.
    • The Reincarnated Drone world's description no longer shows raw placeholder codes. Its description tried to roll a random past life and threat, but a description is read in the library before a playthrough exists — so there was nothing to roll yet and the codes showed through as gibberish. It now describes the same idea in plain words, and your existing copy updates itself on next launch — unless you've edited that world yourself, in which case your version is left alone as always.
    • Desktop updates on Windows can no longer get permanently stuck. If an update was interrupted partway through — the app killed, the machine shut down, antivirus stepping in — every update after it would silently fail and quietly reopen the old version, with no error and no way back. The updater used to test whether the old app had let go of its files by briefly renaming its folder aside and back; an interruption in that split second left the spare folder behind, and its mere presence made every future attempt fail. That step is gone entirely — the update now simply retries the real swap until the old app releases its files, which can't leave anything behind. If you're already stuck, this fix reaches you only by downloading the full app again (the updater can't replace the piece that was broken), or by deleting the leftover app.locktest folder next to Formamorph.exe.
    • A missing launcher now explains itself instead of crashing. If Formamorph.exe had been removed or quarantined by antivirus, choosing Update & Restart closed the app with a raw JavaScript error dialog and no explanation. It now tells you the launcher is missing, names the likely cause, and leaves the app open so you don't lose your place.
    • Placeholders in a world's Readme now read as words, not raw code. A Readme using Variables or Wildcards showed the underlying token text instead of the value, so the first thing you read on entering a world was gibberish. Readme text now resolves its placeholders like every other piece of authored writing. Wildcards in a Readme are also frozen into your save now, so the Readme names the same value the story goes on to use, rather than picking a fresh one each time the popup opened.
    • Deleting a bundled world now sticks. City Rampage, Valentines Survival, and Reincarnated Drone came straight back the next time you returned to the main menu — Formamorph couldn't tell a world you'd deleted apart from one you'd never been given, so it helpfully re-created it. It now remembers that you deleted it and leaves it alone, including when that world's bundled copy is later updated. Changed your mind? Settings → Accessibility → Restore default worlds brings back everything you've deleted, at its latest version. Worlds you made or imported yourself were never affected.
    • Auto response length no longer cuts turns short. With Response Length set to Auto, Formamorph sizes each narration to your Max Output Tokens — but it had badly overestimated how much a paragraph costs, so it was telling the AI to stop after about a third of the room you'd actually given it. It now estimates paragraph length from real narration, so Auto turns fill the budget you set instead of ending early. Nothing changes if you pick a fixed paragraph length yourself; this only affects Auto.
  • ⚙️ Backend

    • The world store no longer opens its database at import. It opened IndexedDB in its constructor, which every operation already does lazily anyway — harmless in the app, but it surfaced as an unhandled error (and a red CI run) in test files that import the store without a database. Opening is now fully lazy.

✅ 2.4.2 — Released 2026-07-16 — a one-fix release: each world shows its own Readme again, instead of every world showing the last one you edited (click to expand)

Minor Changes

🐛 Fixed

  • 👤 User-facing
    • Each world shows its own Readme again. Once you'd written or edited a Readme, that same text kept appearing when you entered every other world, and a world's own Readme never showed at all. Loading a world now clears the previous world's details instead of carrying them over. This also means editing a world can no longer save someone else's Readme into it — if a world picked up the wrong Readme this way, open it in the editor and correct the text, then save.

✅ 2.4.1 — Released 2026-07-16 — a fix-only follow-up to 2.4.0: sub-locations you can walk back out of and a narrator that knows what place you're inside, dropdowns that stay on screen, and desktop publishing that can update and delete again (click to expand)

Minor Changes

🐛 Fixed

  • 👤 User-facing
    • The AI now knows what place your location is inside, and you can walk back out of it. The Reachable scope on the Location and Entities prompt chips now sends the location that contains your current one — listed first — alongside its neighbors, so the narrator knows what building the room is in and who's out there. Previously nothing you could put in a prompt could mention it at all. It's also somewhere you can now travel to: moving into a sub-location with no sub-locations of its own and no neighbors used to strand you there permanently, because the game worked out where you could go from a place without ever considering the one containing it. No prompt edits needed — if you use the Reachable chip, this arrives on its own.
    • Long dropdown lists no longer run off the top of the screen. In a world with a lot of locations, the Change Location list was tall enough that it opened upward and had its top cut off by the edge of the screen, putting the first locations out of reach. Dropdowns now size themselves to the space actually available and scroll inside it. Affects every dropdown in the app, not just Change Location — most noticeable on shorter screens.
    • Updating and deleting your published worlds works again in the desktop app. Publishing an update over an existing world, or deleting one you'd already posted, failed in the desktop app with a "Failed to fetch" error — while both worked fine in the browser. The desktop app rewrites the community server's permissions so a native app can talk to it, and that rewrite was accidentally dropping the permission to update or delete. Publishing a brand-new world and downloading were never affected, and nothing about your worlds or the server needed changing.

Minor Changes

➕ Added

  • 👤 User-facing
    • Browse and download community characters and dictionaries. Community Creations gains Characters and Dictionaries tabs, with the same search, filters, sorting, and paging worlds already had — each tab scoped to its own kind. Downloading one adds it straight to your library, and the button tells you what you're getting: nothing yet, a re-download of the copy you hold, or an update when the author has published a newer version. Each listing keeps a single local copy that a re-download refreshes in place — so if you've edited yours, it asks before replacing it. An oversized portrait offers the same Optimize / Downscale / Keep choice worlds do.
    • Publish a character or dictionary. When you're signed in, the character and dictionary editors gain a Publish button, working the same way worlds already do: publish as new, or pick one of your existing listings to update. A character with no portrait and a dictionary (which has no art at all) both get stand-in cover art on the listing, so every card looks right in the grid. Only listings of the same type are offered as update targets — a world can't be replaced by a character.
  • ⚙️ Backend
    • Characters and dictionaries can remember where they came from. Their local library records now carry the same community link worlds already had — which listing a copy was downloaded from, the version held, when it was downloaded, and whether you've since edited it. The link survives an editor save, and editing a downloaded copy marks it as yours so a later re-download can't quietly overwrite your changes. It's device-local: it never travels in an exported character card or dictionary file, though a backup does carry it, the same way backups already carry this for worlds.

🐛 Fixed

  • 👤 User-facing
    • Publishing works when you've published a lot. The publish dialog only ever listed your first 10 published worlds, so an 11th could never be picked to update — it simply wasn't there, with nothing to say so. It now lists every world you've published, and the list scrolls instead of growing the dialog off the top and bottom of the screen (which could put the Publish button out of reach).

✅ 2.4.0 — Released 2026-07-16 — a safety net for your library: full Backup & Restore to one portable file, per-turn Autosave, and persistent-storage marking — plus multi-file imports, a browsable Save menu, jump-to-page, data-backed reasoning-model recommendations, and built-in worlds that refresh themselves (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing
    • Import many files at once. The Import buttons for worlds, characters, dictionaries, and saves now let you select multiple files in one go. Bad files are skipped and you get a summary ("Imported 5 worlds (1 skipped)"). When a world or character import includes oversized images, a single Optimize / Downscale / Keep prompt covers the whole batch. Lorebooks embedded in imported character cards are added to your dictionary library automatically.
    • Jump to a page. In any paged list (the in-game transcript, the AI-context view, Community Creations), click the or the current page number to open a small “go to page” box — type a page and press Enter. Out-of-range numbers snap to the nearest valid page. On mobile, tap the current page number.
    • Autosave. Your game now saves itself after every turn, starting once the opening scene finishes. Each world keeps a single Autosave slot (overwritten each turn, tagged Auto in Load) that's separate from your manual saves — it never overwrites them, and you can't accidentally clobber it from the Save dialog. It also re-saves after a Regenerate, but not after an Undo. On by default; turn it off in Settings → Accessibility. A downloaded autosave re-imports as an ordinary manual save.
    • The Save menu now lists your existing saves. Saving a game shows the same browsable save list as Load — your worlds' save folders, with download, delete, and drag-to-reorder. Click a save to drop its name into the box, so overwriting an existing save is one click (then Save confirms the overwrite as before). Typing a fresh name still makes a new save. The box also prefills with the name of the save you're currently in (the one you last loaded or saved this session), so re-saving over your slot is quick.
    • Back up and restore your library in one file. A new Backup & Restore option in the main menu exports your worlds, saves, characters, and dictionaries to a single portable .json, and restores them on any device or after an update. You pick exactly which worlds, saves, characters, and dictionaries to include — each is its own checkbox under a per-type group toggle. Restore works the same way; items that already exist are tagged so you can choose, per type, whether the backup overwrites them. Restoring worlds or characters can also optimize or downscale their images on the way in. Especially useful in the itch app, where an update can otherwise hide your local data — back up first.
    • Optimize a character's portrait when you import it. Importing a character with an oversized image now offers the same Optimize / Downscale choice worlds already had, so a huge portrait doesn't bloat your library.
    • The desktop model catalog now includes reasoning roleplay models, backed by data. The Recommended list (desktop) was refreshed with current community roleplay finetunes, and each now shows its release date and download count so you can weigh the recommendation yourself (counts refresh from Hugging Face and fall back to a snapshot when you're offline). The reasoning-capable ones are flagged with a Reasoning badge (a compact 4B and the Qwen3.6 mixture-of-experts finetune); reasoning is controlled per-prompt from Settings, so these pair with the thinking-budget control out of the box.
    • Built-in worlds refresh themselves. When a built-in world's content changes in a new build, your copy updates to it automatically on launch — as long as you haven't edited that world (edited copies are always left alone). Any worlds updated this way are named in a notification so you know what changed. It compares the actual content, so an update lands exactly once and a world can be improved without waiting for an app version bump.
    • Old saves pick up newly added stats. If a world gains a new stat after you started playing it, loading an existing save now shows that stat (at its starting value) instead of leaving it out. Your existing stats and progress are untouched.
    • Built-in worlds no longer warn about custom code. The "this world contains custom code, trust the source" warning is meant for worlds from elsewhere — it no longer shows for the unmodified built-in example worlds, since they're ours. If you edit a built-in world, the warning returns.
  • 🛠️ Developer tooling
    • A repeatable prompt-quality gate. The baseline harness gained a scripted gate (GATE-PROBE.md) with a neutral prompt preset and a second reference world, so prompt changes can be measured the same way every time instead of judged by eye.
    • Model leaderboard. Probe runs now record to a tracked leaderboard (testing/baseline/leaderboard.md), making it easy to see how the reference models compare across prompts — with the research writeup kept alongside it.
    • Screenshot capture in the harness. The baseline runner can grab screens (harness/screen.mjs) and its profile example covers the current model tiers, so a run's output can be eyeballed after the fact.

🐛 Fixed

  • 👤 User-facing
    • Long lists scroll again on mobile. On mobile, the Traits and Dictionary lists in the World Editor and the save list in Load/Save wouldn't scroll once they grew past a screenful — the overflow was simply cut off and unreachable. All three now scroll normally.
    • Your library is now marked as persistent storage. The app asks the browser to keep its saved data (worlds, saves, characters, dictionaries) exempt from automatic cleanup, so it's far less likely to be evicted under storage pressure — a common cause of "everything disappeared" reports on the web and itch builds. Pairs with the new Backup & Restore for a belt-and-suspenders safeguard.
    • No more endpoint errors when you're not using reasoning. The app used to probe your endpoint's reasoning support (and tag requests with a reasoning setting) on every connect — which made plain servers like LM Studio log a burst of 'messages' field is required errors and could wedge a just-loaded model. That reasoning machinery now runs only when you've actually turned reasoning on (a Thinking mode, a native effort level, or a per-prompt reasoning level). With reasoning off, requests look exactly like before — no probe, no reasoning fields.
    • Popups no longer flash their empty state while closing. Many dialogs briefly reverted to a blank/initial state for a frame or two during their fade-out — form dialogs (Save game, Backup & Restore, login/profile, preset sharing) and confirmation prompts alike (e.g. a delete confirm losing the item's name as it closed). Form dialogs now reset when opening rather than closing, and confirmations keep the content they were showing until they finish fading out.
    • Saving a backup is reliable everywhere. An earlier approach used the system "save as" dialog, which errored when overwriting an existing file and, in the itch app, could pop the save dialog twice. Backups now download directly — one step, and it works the same in every browser and in the itch app.
    • Downloading community worlds quickly no longer loses one. Two worlds downloaded within the same instant could be assigned the same internal id, so the second silently overwrote the first in your library. Each download now gets a unique id.
    • Clicking a character opens the right one. When two characters shared a name prefix (e.g. "Guard" and "Guard Captain"), clicking one could open the other's details. It now matches the exact character.
    • Dragging a chip and dropping it outside a text box no longer misplaces one later. Cancelling a chip drag (dropping it on empty space or pressing Escape) could leave a stray reference that made your next unrelated drag move a chip you didn't touch. Cancelled drags now clear cleanly.
    • Stat descriptors accept a 0% threshold. In the World Editor, adding a stat descriptor with a threshold of 0 did nothing; it now saves like any other value.
    • Starving no longer wipes out that turn's other stat changes. On a turn where you were starving, the health penalty overwrote Health with a value read before the turn's other changes landed — so any Health the AI awarded, and any Health recovery you'd earned, silently vanished. The penalty is now subtracted from your current Health, so it stacks with the rest of the turn instead of replacing it.
    • Loading a save can't hang on a worker failure. If a background helper (save conversion/download) failed to start, the "Processing save file…" notice could spin forever; such failures now surface as an error and release the dialog.
    • The oldest saves restore their traits and body changes. Very old saves (the deep-nested pre-2.0 format) loaded with their character traits stripped of description (so the AI saw only bare names) and their body stats no longer driving the 3D model. Loading one now brings those up to the current format, the same as newer saves already did.
    • A clearer message when the web app can't reach your AI server. On the web/itch build, a connection failure used to show only a generic "Failed to process AI request." It now offers a Fix connection guide covering the three causes — server not running, wrong Endpoint URL, or CORS turned off on the server — with the exact toggle for LM Studio and Ollama. The same guide is reachable any time from Settings → Endpoint → "Trouble connecting?". (The desktop app already handles this automatically, so it's web-only.)
  • ⚙️ Backend
    • Built-in worlds seed with their full content. The first-run seeding path dropped a world's lorebook (dictionaries) and placeholders; they're now carried over intact.
    • Built-in worlds stop re-saving themselves on every launch. The refresh check compared a world's shipped version against a field that actually records the app version that last touched it — so a world could look permanently "newer", rewriting itself (and showing an "Updated" notice) every single launch, while a genuine content-only fix could never reach you at all. It now compares a hash of the shipped content, which settles after one update and can't loop.
    • A background helper that fails to start no longer wedges the rest of the session. If a worker's script failed to load, the broken instance was kept and reused, so every later save load hung on a spinner instead of reporting the error. The failed worker is now discarded and the next attempt starts fresh.

✅ 2.3.0 — Released 2026-07-14 — Formamorph goes mobile: the game view, World Editor, character customization, Settings, and Community Creations all adapt to mobile and portrait — one-panel push layouts, bottom sheets, collapsing toolbars, and viewport-correct popups — plus a version-aware "What's new" popup and a configurable Staged-thinking cast (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing
    • The "What's new" popup highlights your version. The changelog popout is wider and laid out as a cleaner outline — each version flush-left with its Added/Fixed groups and items stepped in beneath it. Your installed version is tinted blue and tagged (Current), and the newest available version is tinted green and tagged (New!), so you can tell at a glance what you have versus what's new.
    • The "What's new" popup surfaces the versions that matter to you. Instead of a flat "newest three," it now picks what to show based on the version you're on: the full current X.Y series, the couple of versions just before yours, and — when you're several releases behind — the newest few as well. A ... line marks any stretch that's been skipped, and an available update is always shown. You see more of what's relevant around your version without scrolling the whole history.
    • The library's top bar collapses to one menu sooner. The Community Creations / New / Import buttons now fold into a single hamburger on narrower windows (below 1000px), not just in portrait. When they collapse, the separate right-side Load Game / Settings menu folds into that same hamburger too — so there's one menu instead of two, and the grid/detailed view toggle sits flush to the right.
    • Skip Customize is clearer when entering a world. The button gained a skip-forward icon, and in portrait it collapses to just the icon so it fits alongside Enter World without crowding.
    • Choose how many characters the director stages (Staged thinking). Staged mode used to hard-cap the cast at three, so editing the Director prompt to allow more had no effect. Settings → System Prompts now has a Limit Active Characters control (under Thinking, shown for Staged): turn it off for as many characters as the scene calls for, or set your own number (default 5). A new Active Character Guidance chip in the Director prompt keeps its wording in sync with your choice.
    • Toggle the character's animation while customizing. A new Animate character checkbox (above Initial Body Features) freezes the model in place when unchecked — handy for lining up body and color changes on a still pose — and resumes the idle motion when checked. The scene stays live either way, so you can still orbit and see your edits.
    • Character customization works in portrait now. On mobile or a narrow window, the customization controls no longer get squeezed into a sliver that cuts off their labels. They move into a bottom sheet that slides up over the character — tap Customize to open it, adjust anything, then close it to see the full model. The sheet opens at a shorter height that keeps the character in view, with an expand button in its header to grow it when you want more controls at once; it scrolls its full contents at either size. Desktop is unchanged.

🐛 Fixed

  • 👤 User-facing
    • Manual stat edits survive a re-generate. Nudging a stat with the in-game slider, then playing on and re-generating a later turn, no longer reverts that stat to its pre-edit value. The edit now becomes the turn's baseline, matching how edited narration and notes already carry forward.
    • Chip pop-outs stay open while you adjust options. Picking a mode on a placeholder or prompt chip (e.g. a Wildcard's World/Unique, or a Location chip's scope/content/format) no longer closes the pop-out, so you can change several settings in a row. Click elsewhere or press Escape to close it as before.
    • The placeholder Edit/Preview toggle only shows when a world has placeholders. It used to appear on every placeholder-capable field even when no placeholders were defined; now it shows up only once you've added at least one.
    • Popups fit the visible screen on mobile. Dialogs (Settings, world details, entity/dictionary editors, load game, and more) were sized to the full device height including the browser's address-bar area, so on mobile a centered popup's title and close button could sit above the visible screen. Every popup now sizes to the actually-visible area, so its header and controls stay on-screen.
    • Turn pager is tidier on mobile. The Previous / Next buttons show just their arrows on mobile (the labels return on wider screens), the numbered strip collapses to only the three pages closest to your current one (no ellipsis), and in the story the pager left-aligns on mobile instead of centering — so the row reads cleanly next to Re-generate.
    • Re-generate is icon-only on mobile. The Re-generate button under the story drops its text label on mobile and shows just the refresh icon, saving room in the tight mobile toolbar. Its keyboard/screen-reader label is preserved.
    • The in-game header is tidier on mobile. The floating buttons over the story crowded the top of the screen, so on mobile the menu (⋯) now sits on the Character/Game/Status tab row, with Edit World and AI Context folded inside it. The background-music toggle and the Hide-UI button are dropped on mobile (not needed), and the three tabs shrink to make room. Desktop keeps all the header buttons as before.
    • The World Editor works on mobile. It used to warn "not optimized for mobile, use a desktop" and hand you the cramped desktop split. It now adapts: on mobile each tab shows one panel at a time — the list of stats/entities/locations/traits/dictionary entries/placeholders, and tapping one slides in its editor with a back button — while desktop keeps the resizable two-column view. The tab strip scrolls sideways, the toolbar wraps, and the mobile warning is gone.
    • The placeholder editor works on mobile. Its two-column list-and-editor layout was unusably cramped on mobile; it now shows one panel at a time — the list, and tapping a placeholder slides in its editor with a back button. Desktop keeps the side-by-side view.
    • The character model fills its panel on mobile. In the Model tab, the 3D character was boxed to a fixed aspect and left dead space; it now uses the panel's full height and width. Landscape/desktop keep the aspect-boxed viewer above the panel.
    • The game view uses the full screen on mobile. The play screen no longer has an outer margin on mobile, so the panels run edge-to-edge and reclaim that space. Desktop is unchanged.
    • The main-menu footer is less crowded on mobile. On mobile the footer keeps your profile/login button and the copyright, and tucks the version/what's-new control, the Patreon and GitHub links, and the "Based on…" credit into a single menu. Desktop shows them all inline as before.
    • Community Creations filters tuck away on mobile. On mobile, the sort, author/tag filters, hidden list, and "updates first" toggle now collapse behind a Filters button (with a count of what's active) so the search bar and results have room; tap to expand them in a panel. Refresh now sits beside the search box everywhere. Desktop keeps all the controls inline. Their label-and-control rows used a fixed two-column layout that stayed side-by-side on mobile (unlike Presentation and Generation, which already stacked), squeezing the controls. Those rows now drop to a single stacked column below a small breakpoint, matching the other tabs.
    • Settings tab widgets fit narrow screens. The six-tab row along the top of Settings, plus the Thinking and Native Reasoning mode pickers, were too wide for mobile; below a small breakpoint each now collapses into a dropdown of the current choice, and stays a full tab row on wider screens.
    • Character customization's Back button becomes Abort on the first step. When avatar customization is the first (or only) step of entering a world, its Back button was disabled — a dead end. It now shows Abort and cancels the enter-world flow, matching the other steps.
    • The enter-world steps dim the menu behind them again. The trait / location / character / dictionary selection popups render as plain cards without a dialog backdrop, so the main menu showed through undimmed. A black overlay now sits behind them like every other popup.
    • Location Change is skipped in single-location worlds. With Location Change on, a world that has only one location no longer fires the (pointless) move request each turn — there's nowhere to go — so you don't have to toggle the setting off per world.
    • Community world thumbnails zoom again. Clicking a world's image in the Community Creations details view opens the pan/zoom viewer as before; it had stopped loading because the zoom used the raw server image URL (which the server blocks from cross-origin <img> loads), while the card thumbnail already loaded through the local image cache. The zoom now uses that same cached copy.
    • The world details popup no longer scrolls sideways on small screens. On a narrow portrait viewport, the panel that opens when you pick a world could be cut off on the right with a stray horizontal scroll; its content now stays within the screen.
    • The character model renders cleanly in its viewer. In character customization the 3D model could draw outside the rounded edges of the viewer card, and it could stay cut off at the wrong size after rotating the device or resizing between landscape and portrait. It's now clipped to the panel and re-fits itself whenever the panel changes size, in both the desktop and portrait layouts.
    • The app no longer shows a blank screen over plain HTTP. Opening Formamorph from a non-secure origin — for example loading the dev build on your phone at http://<your-pc-ip> over the local network, or self-hosting without HTTPS — used to render a white screen, because the browser withholds its built-in ID generator outside a secure context. It now falls back to a compatible generator, so it loads and runs the same everywhere.
    • Scrolling the main-menu card grids on touch no longer drags cards around. On mobile, swiping to scroll the worlds, entities, or dictionaries could accidentally pick up and reorder a card. Reordering on touch now needs a short press-and-hold first, so a plain swipe scrolls as expected. Mouse drag is unchanged.
    • The World Editor's + button always adds an item. Adding a stat, entity, location, or stat update used to do nothing unless you'd first typed a name in the search box. It now creates a new item with a default name (e.g. "New Stat") and selects it for editing — matching how traits, dictionaries, and placeholders already worked. Typing a name first still uses that name.
    • The World Editor runs edge-to-edge on mobile. The mobile layout had an outer margin and extra card padding that ate into the little screen space there is; those are gone, so the list and editor use the full width. Desktop is unchanged.
    • Popup buttons don't crowd together when they stack on mobile. When a dialog's action buttons wrap from a row into a vertical stack on a narrow screen, they had no space between them and nearly touched. They now keep standard spacing whether stacked or side-by-side.
  • 🛠️ Developer tooling
    • Reusable ListDetail master-detail layout. New components/ui/list-detail.tsx renders a list + detail side-by-side on desktop and a single-panel push (detail slides in over the list with a back header, parallaxed, reduced-motion aware) on mobile — branching on useIsMobile. The caller owns selection and passes showDetail + onBack. PlaceholderEditor is the first adopter (drops its hand-rolled two-column split), so the entity/dictionary modals and the World Editor placeholders tab all get the mobile push for free. Unit-tested (desktop-both-panels vs mobile-back-bar). The list's dnd is unaffected — at rest the list panel's transform is none, so no containing-block interference. Adopted by PlaceholderEditor, DictionaryEditorModal (dictionary tree ⇄ book/entry editor), and the WorldEditor (its per-tab list + detail extracted to shared listContent/detailContent fragments, branched on useIsMobile: desktop resizable split vs mobile push; the < 1024px "not optimized for mobile" warning was removed).
    • Configurable staged cast cap + guidance chip. matchCastToEntities's hardcoded 3 is now runStagedPlanning's activeCharacterCap (Infinity = unbounded), fed from two new settings — limitActiveCharacters (bool) + activeCharacterLimit (int, default 5). A new <ACTIVE CHARACTER GUIDANCE> prompt variable (registered in promptVariables, offered on the Director prompt, resolved via activeCharacterGuidance() in buildContextValues/preview) replaces the Director prompt's cast-size sentence so the prompt tracks the setting. Settings UI row is gated to Staged. Unit tests cover the guidance text and the pinned Director chip list. Default staged cast size shifts 3 → 5.
    • Modal/overlay heights use dvh, not vh. Every arbitrary [Nvh] height on a dialog, card modal, bottom drawer, or scroll region (plus the two full-screen dialogs' h-screen) was converted to the dynamic-viewport unit dvh, so centered overlays no longer overflow the mobile visible viewport and clip their own chrome. Page-root h-screen scaffolds were intentionally left as-is. Verified no [Nvh] heights remain.
    • Changelog popout formatting. UpdateService.foldRelease now emits each version as its own ### <tag> heading (no more version·category merge); ChangelogBody tags the current / newest-uninstalled version headings (cl-current / cl-update) so .changelog-body CSS can tint them (info / success via color-mix, kept muted) and append the (Current) / (New!) labels; indentation is CSS too. Both changelog popouts widened to max-w-3xl. Tests updated.
    • Version-aware changelog selection. buildRecentChangelog now takes the running version and picks releases by rule instead of slicing the newest three: always the newest release (rule 0), the full current minor including newer patches (rule 1), the newest RECENT_CHANGELOG_COUNT when behind by more than one release (rule 2), and the two releases just below the running version (rule 3). Renders ... gap markers wherever hidden releases sit between shown ones (or below the last). currentVersion threaded through parseReleases; unit-tested across the up-to-date, behind-by-one-into-new-minor, and behind-by-many cases.
    • Character customization responsive layout + bottom drawer. CharacterCustomization extracts the viewer and the controls into shared fragments and branches on useIsMobile: desktop keeps the two-card split; portrait renders the viewer full-height with the controls in a vaul bottom drawer (new dep) toggled by a floating Customize button. New components/ui/drawer.tsx follows the shadcn recipe but keeps the overlay separate from DrawerContent. The drawer is modal with a faint overlay so its gestures don't contend with the model's OrbitControls (this was the first-interact lag); it toggles between two fixed, fully-scrollable heights (DRAWER_HEIGHTS, short/tall) via a header expand button rather than vaul snapPoints — a partial snap can't scroll its inner content, so snap points were dropped. The viewer Card gained overflow-hidden to clip the WebGL canvas, and VRMViewer now re-fits via a ResizeObserver on its mount (not just window.resize, which measured stale dimensions mid-layout-swap) so orientation/landscape↔portrait changes no longer leave the model cut off. Single mount of the controls (no duplicate DOM ids). Reachable for verification via the dev-router (#dev?modal=avatar, added to DEV_MODALS). No export-shape or behavior change.
    • VRMViewer cancels its render loop and animation timers on unmount. The requestAnimationFrame loop and the self-scheduling playNextAnimation setTimeout chain were never cancelled in the effect cleanup (only THREE resources were disposed), so every remount (model swap, layout change) leaked another loop + timer driving a disposed scene. Cleanup now tracks and cancels both. Also: the Animate-character toggle feeds dt=0 to freeze in place, and playNextAnimation skips the clip swap while paused so the pose no longer jumps between keyframes.
    • Touch-friendly DnD sensors on the library grids. MainMenu swapped the shared worldSensors from a single PointerSensor (distance: 8, which fired on any touch swipe) to a MouseSensor (distance: 8) + TouchSensor (delay: 200, tolerance: 5) pair — a touch swipe scrolls, a press-and-hold reorders; mouse behavior is unchanged. Fixes worlds/entities/dictionaries in one place. Paired with this, SortableWorldCard's whole-card draggables changed touch-nonetouch-pan-y so a vertical swipe pans the grid's ScrollArea natively (with touch-none the browser couldn't start a scroll on a card, so the grid was unscrollable on touch); the delay sensor still takes over both axes once a drag activates. Small grip-handle draggables keep touch-none (their card bodies already scroll).
    • randomUUID() helper with a non-secure-context fallback. crypto.randomUUID is only defined in secure contexts (HTTPS/localhost), so any startup path that mints an id — e.g. migrateWorld on load — threw a blank screen when the app was served over plain-HTTP LAN (phone testing the dev server) or self-hosted without TLS. New lib/uuid.ts uses the native API when present and otherwise builds an RFC-4122 v4 from crypto.getRandomValues (which isn't secure-context-gated); all 34 crypto.randomUUID() call sites now route through it. Unit-tested (native path, fallback path, uniqueness). Still v4 UUIDs — no export-shape or version impact.

✅ 2.2.2 — Released — author-defined **Placeholders**: reusable Variables / random Wildcards dropped into world text as chips, with a live Edit/Preview toggle, portability when you share characters and dictionaries, and one shared editor everywhere — plus downloaded worlds now keep their own thumbnail and work fully offline (click to expand)

Minor Changes

➕ Added

  • 👤 User-facing
    • Placeholders — reusable values and randomness in your worlds. A new Placeholders tab in the World Editor lets you define named values and drop them into your world's text. Add one value and it's a Variable — reuse it everywhere and change it in one place. Add two or more and it's a Wildcard — each playthrough picks a random one, so worlds feel varied without you writing every case. Insert them as chips into any entity, location, world, or dictionary description (both the player-facing and AI-facing text), and each Wildcard chip chooses World (the same value everywhere this playthrough) or Unique (its own value at that spot). The player and the AI always see the same rolled value, frozen for the save.
    • Preview placeholders while editing. Any field that accepts placeholder chips now has an Edit / Preview toggle (like the prompt fields). Preview swaps each chip for a value — a Variable shows its value, a Wildcard shows a random one of its values (World chips share a value, Unique chips roll separately) — highlighted in the chip's own color. Re-rolls each time you open Preview, so you can see the range.
    • Edit a shared item's placeholders in the library. The library character and dictionary editors get a Character/Dictionary ↔ Placeholders tab switch at the top; the Placeholders tab is the same list-and-editor as the World Editor's, so you can add, edit, and reorder the placeholders a standalone item carries. Its chips now display and resolve correctly in the library editor too.
    • Shared entities and dictionaries keep their placeholders. When you export a character card or a dictionary that uses placeholders, the placeholder definitions it needs travel with it. Importing it into another world adds them to that world (reusing an exact same-name/values match if one already exists, so you don't get duplicates), and its chips keep working. Held in the library, an imported item carries its placeholders until it lands in a world.
  • 🛠️ Developer tooling
    • Placeholders — scoped store, shared split UI. The placeholder editor now follows the same pattern as the dictionary editor: a scoped PlaceholderStore context (contexts/PlaceholderStoreContext.tsx, with a placeholderStore(value, setValue) adapter). GameDataContext binds it to the world's placeholders; the library editors bind an isolated adapter over the item's own placeholders field. The split UI is now genuinely the same components everywhere — PlaceholderList (left) + PlaceholderManager (right), reading/writing through the store — instead of the library reimplementing the layout. PlaceholderEditor is the context-bound combined wrapper the modals use; the World Editor's Placeholders tab renders the same pieces (dropped from the generic tab machinery, like the Dictionary tab already was). No behavior change; verified in the World Editor.
    • Placeholders — export/import portability (slice 6). Entity + Dictionary gain an optional placeholders? (one representation for the export file and the in-app library item). Export (buildEntityCardData/buildDictionaryFile) bundles just the used defs via collectUsedPlaceholders; import parses them onto the object; adding a card/book to a world runs absorbPlaceholders (perfect name+values match → reuse id, else fresh-id add) + remapPlaceholderIds to rewrite the item's tokens, then clears the carried section. All pure + unit-tested; entity round-trip test added. Additive export-shape change to the entity card + dictionary .json.
    • Placeholders — field rollout (slice 5). PlaceholderField (the placeholder chip editor) replaced the plain textareas on the accepting fields: entity player/AI descriptions + summary (EntityFields), location player/AI descriptions + summary (LocationManager), world System Prompt (WorldDetailsManager), and dictionary entry value (DictionaryManager). Shared editors (EntityFields, DictionaryManager) take placeholders as a prop so the world-editor callers pass the world's list while the isolated library editors pass none. Extracted KeywordChips reused for values. Completes the feature (author → insert chip → resolve at runtime), verified end-to-end.
    • Placeholders — resolution wiring (slice 4). Placeholder chips now resolve to their frozen values at runtime. Rolls are eagerly primed when a save activates (primeRolls scans the world's authored text once, rolls every Wildcard placement, keeps a loaded save's existing rolls), so resolution is a pure lookup everywhere — no render-time side effects. SaveObject.placeholderRolls persists them (GameplayContext). usePlaceholderResolver + GameViewer's resolvePH apply resolution at the AI-context boundaries (buildContextValues, scene entities, dictionary) and at player-display (entity modal, location panel). The same roll feeds both, so the player and the AI always see the same value. Reverses the earlier "lazy" plan for the render-safety win.
    • Placeholders — chip editor (slice 3). The Lexical chip editor was generalized behind a ChipVocabulary abstraction (lib/chipVocabulary.ts) so one editor serves both token families: the static prompt variables (<…>) and dynamic per-world placeholders ({{ph…}}). PromptField/VariableNode now read their vocabulary from context (prompt path unchanged — verified), and a new PlaceholderField wraps the same editor with the placeholder vocabulary: the toolbar inserts the world's placeholders (fresh placement id per insert), and a Wildcard chip's pop-out offers World | Unique once it has 2+ values. Vocabulary logic unit-tested; not wired into world fields yet (slice 5).
    • Placeholders — foundation (slices 1–2). New authored-world concept: a named value dropped into world text, its behavior inferred from value count (1 = a fixed Variable, 2+ = a random Wildcard). Slice 1: Placeholder type + World.placeholders, save-side SaveObject.placeholderRolls, and the pure resolvePlaceholders core (lib/placeholders.ts) with the token codec, World-shared/Unique-per-spot rolls, lazy roll+freeze, and missing/empty → "" (unit-tested). Slice 2: a per-world Placeholders editor tab (list dnd/duplicate/delete + PlaceholderManager name/values panel), reusing the extracted shared KeywordChips input. Additive world + save export-shape change (nothing consumes it at runtime yet — the chip editor + resolution wiring are later slices).

🐛 Fixed

  • 👤 User-facing
    • Downloaded worlds now show their thumbnail in the library — and work fully offline. A world downloaded from Community Creations was storing a link to the server's thumbnail image, which the browser refuses to display on the library card (the server marks those images as same-origin only) and which wouldn't work offline anyway. Downloads now save the world's own cover image directly into the local copy, so the card shows it and the world is fully self-contained. Worlds you already downloaded are healed automatically — no need to re-download.
  • 🛠️ Developer tooling
    • World download stores the embedded thumbnail, not the catalog URL. useDownloadCoordinator.fetchWorldContent now prefers migrated.worldOverview.thumbnail (base64, already in the downloaded content) over the ${API_URL}/thumbnails/... URL — the server's Cross-Origin-Resource-Policy: same-origin (Helmet default) blocks the raw URL from <img> embedding cross-origin, and a URL can't render offline. getWorldMetadata also falls back to the embedded thumbnail when a stored one is an http(s) URL, healing pre-fix downloads at read time (it already loads data for tags, so no extra cost). Unit-tested. Separately, the FormamorphServer thumbnails route now sets Cross-Origin-Resource-Policy: cross-origin (overriding Helmet's same-origin default) so those public images are embeddable cross-origin

Clone this wiki locally