-
-
Notifications
You must be signed in to change notification settings - Fork 2
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.9.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.jsonreads 2.9.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.
Unreleased โ new work accumulates here until it earns a version bump. The next batch will pin its own version; package.json reads 2.9.2 (just released below).
-
๐ค User-facing
-
Placeholders in image tags. The booru Image Tags on an entity or a location now take chips, so a character can be tagged
1girl, {HairColor} hairand the picture generator draws a different one each playthrough โ rolled once and frozen into the save like every other wildcard. The Danbooru tag autocomplete still works exactly as before alongside it; type{for a placeholder, keep typing a tag for the usual suggestions. This is the booru tag box only โ the Tags on the Overview tab, which are for finding a world in a list, are unchanged. -
The placeholder strip dims when there's nowhere to put a chip. Its chips stayed lit after you clicked into an ordinary text box, and clicking one would drop it back into whichever chip field you'd used last โ somewhere you weren't looking. The strip now goes grey the moment focus lands somewhere a chip can't go, and lights up again when you're back in a field that takes them.
-
Aliases and lore keywords take placeholders too. A nickname or a lore trigger is a name like any other, so both now accept chips: 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. Type
{in the box where you add them. A tag holding a chip shows it by name rather than as raw code, and is changed by removing and re-adding it rather than typed over. 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. -
Placeholders work in names. A placeholder 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 too: 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 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 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; 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 pictures, 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 picture is used without its bytes being stored in the world, which keeps a published world small no matter how many pictures it carries. A linked slot is marked with a small link icon, and a link that doesn't resolve says so right there in the editor rather than turning up blank mid-story. Linked pictures 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 pictures into it (works anywhere), and exporting a character card always downloads the portrait, since a card is the picture. 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.
-
Linked images say what's wrong with them, in the editor. A pasted link can fail in ways that don't show up until much later โ so the editor now flags them where you paste them. A Discord attachment link is marked immediately with a plain warning that it will stop working: Discord expires those, so a picture you check today can be gone in a week. Discord's permanent addresses (avatars, emojis, server icons) aren't flagged, because they're fine. A link from a site that won't hand its pictures 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.
-
Make Available Offline. A world that links its pictures 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 pictures 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 a phone. It was drawn as a floating panel with a margin all the way around โ on a phone 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 a phone.
-
Every prompt says what it's for, before you read it. 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 follows your cursor, and the prompt list comes with you. 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.
-
Write a prompt without loading a world first, and find it without hunting. 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 a phone. 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.
-
Side by side is a full-screen thing now. Showing the prompt and its preview at once needs real width, and only the full-screen view has it. Everywhere else โ the Settings panel, and the world editor's description, readme and dictionary fields โ an editor keeps the Edit / Preview buttons it always had, so a field sitting in a column of a form is never chopped into two columns too narrow to read. The โถ button is on every one of those fields, and side by side is waiting inside it. Settings is also back to one width for every tab, the same as the Feedback window; it was only ever widened to make room for the panes that have now moved.
-
Room to actually edit a prompt. 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 a phone. Every prompt field now has a full screen button, and where there's width to spare 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. The Settings window widens itself while you're on the Prompts tab to make room, and a button beside the full-screen one switches between side-by-side and one-at-a-time whenever you'd rather have the width to yourself. Narrow windows and phones keep the one-at-a-time view automatically, since two half-width columns help nobody. On a phone, 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 and starting from a copy of the standard one rather than a blank page. 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.
-
The world's own narration prompt gets the same editor as every other prompt โ previewing your world. It was the one prompt field with no Preview, so there was no way to see what the chips you'd inserted would actually become, and no side-by-side view in full screen, since that view is built on the preview. It has both now, and 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.
-
"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 on every prompt field, and undo that works. 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.
-
Get a world's player avatar back out of it. A
.vrmadded 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. -
A half-written bug report waits for you. 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 pictures first, the same choice between keeping links and downloading the pictures into the file, and the same file at the end.
-
Entity galleries are limited by weight, not by count. An entity may now carry as many pictures 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 pictures however they were stored, which charged a linked picture 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 picture is bytes like any other. A world already holding more pictures than that is left exactly as it is.
-
-
๐ ๏ธ Developer tooling
-
A multi-turn probe for the stage-direction channel.
ooc-session-probe.mjsplays 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 existingooc-probe.mjsscores 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.mjsgains 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". -
The focus ring has a guard now.
e2e/focus-ring.spec.tschecks the two things that keep it visible: that the ring is drawn inside the control, and that it clears the 3:1 contrast bar on a solid-filled button in both themes. Neither is checkable in the unit suite โ jsdom loads no stylesheet, so it can't see a box-shadow, a CSS variable, or the rule ordering the filled-button color depends on. 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, and reordering the Tailwind color config would silently undo it. Both guards were confirmed by reintroducing each bug and watching the spec fail. -
Undo/redo is covered in the browser suite now. Lexical's history keys off a real text selection, which jsdom has none of โ a programmatic edit there never enters the history stack at all, so the unit suite could not tell a working undo from a broken one.
e2e/prompt-history.spec.tstypes with real keystrokes and drives the real buttons across three hosts: a Settings prompt, the world editor's markdown description, and the world editor's custom narration prompt (whose value round-trips through GameDataContext on every keystroke). Desktop only for now โ on a phone a tap opens the editor full screen, which is a different spec's subject. -
Browser tests, alongside the unit suite. A small Playwright suite (
npm run test:e2e) now drives the real app in a real browser at two viewports โ desktop and phone. It 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, and each now has a guard that was checked by putting the bug back and watching the test fail. It runs separately and deliberately: the four checks that gate a change stay as fast as they were. Seee2e/README.md. -
World authoring from outside the app. The dev-only
window.__fmDevhelper gainedlistWorlds,getWorld,putWorld, andeditWorldโ 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 existingworldEditorroute 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.
-
A multi-turn probe for the stage-direction channel.
-
โ๏ธ 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.
-
๐ค User-facing
-
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. - See everything still waiting on you, in one filter. The Admin Panel's bug and suggestion queues 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.
- An author who forgot to sign their world still gets the credit. 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 its stat's ceiling is raised. 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 picture 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 picture itself was converted perfectly โ the editor briefly tried to draw the slot before the new picture'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 picture appears the moment the conversion finishes.
-
Your 3D characters are called avatars now, not models. 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
.vrmfiles 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/.fbxis 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 picture stays inside its panel in game. An entity picture 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 picture were affected: the gallery's swipe frame lost track of how tall its box was allowed to be. The picture 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 a picture 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 picture 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 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.
-
Library cards stop showing the raw
โ 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)
-
๐ค 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.
-
๐ค 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)
-
๐ค 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)
-
๐ค 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 says when two traits fight over the same thing. 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 picture around. Full-screen images were pinned to the middle of the window: zooming in let you look closer but the picture 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 pictures 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 picture now, wherever the picture 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 picture shows the model itself in the picture'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 a picture 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 picture 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 picture. 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 picture 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 picture that fade in as you point at it, with a faded 1/2 underneath, and it wraps around in both directions. On a phone or tablet there are no arrows to point at: swipe the picture sideways and it follows your finger, letting go past a quarter of the width to turn the page. Opening a picture full-screen keeps the arrows and stays on whichever one you were looking at. Which picture 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 shows where each account stands on the terms. 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 got roomier, and gained the terms. 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.
-
Something can be quarantined 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.
-
Administrators can see what was done. 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 says what that person's work has earned. 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.
-
A suggestion you've voted for now looks like it on the board. 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 picture 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 of a downloaded world is clickable too. 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.
-
Click somebody's name to see who they are. 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.
-
You can put a face to your name. 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 tags are chips now. 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.
-
Feedback has its own place now. 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.
-
A long inbox says how long it is. 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.
-
You can fix or take back what you wrote on a bug. 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.
-
Taking something down offers to say why. 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
modelsfolder, 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_logtable 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
feedbacktree 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 anedited_atthe 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 needsnpm run init-dbrun 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-dbrun 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.
-
๐ค User-facing
-
The Hidden panel in Community Creations opened covered by its own autocomplete. 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 recorded a mod's action as if they weren't staff. 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.
-
An entity was called a character in a few places. 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.
-
The Log says what the person was when they acted. 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 dates were shown in the wrong timezone. 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.
-
Activating a suspended user left the button showing the old state. 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 dots were nearly invisible in half the themes. 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 forgot you already followed someone. 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 were missing everywhere except feedback threads. 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 list ignored how you'd arranged your entities. 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.
- The real tab sets finally point at their own contents. 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 pictures 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)
-
๐ค User-facing
- Draw the scene you're reading. A new button above the story sends the turn to your image server and puts the picture 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 picture 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 a picture 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 picture 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 picture 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 a picture 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 picture โ 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 picture nowhere. Drawing a scene no longer slows the story down. The pictures 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 pictures 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 picture 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 a picture 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 picture 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 picture. 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),dayanddaypartfor where the story stands at the end of the turn, andstartDay/startDaypartfor 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,deltaHoursis 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.tsxnow 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.
-
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
-
๐ ๏ธ 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 like0.5833333333333333/100โ precise enough to look parrotable, and pure token waste. Both arms render through the realbuildStatContextand 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, baren/mratios, stat tabulation, andprompt_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 committeddialogue-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 intesting/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 byfreeze-script.mjsfrom 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 fromsrc, 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 intesting/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 picture 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 picture 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, a picture 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 readsrepeat_penaltyand 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.5833333333333333on 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+0would 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)
-
๐ค 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.
-
The Log reads as a diary, not a stopwatch. Entries were stamped
-
๐ค 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.
-
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
-
๐ค 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 labelled 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 realgameClock.tsat 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 likeElizabeth, 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, eviland 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 a phone 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.
-
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
-
๐ค 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).
-
Direct the story with [square brackets]. Bracketed text in your action now speaks to the AI as the author, not your character โ
-
๐ค 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) andt6-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.mjsgains--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. Newarms-aggregate.mjsreports per-arm meanยฑsd with a Welch t against control. -
Un-baited dialogue probe hardening. Real narration user template instead of the bare action (
--barekeeps 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.mjsanddialogue-unbaited-probe.mjsnow send the player action bare, without thePlayer action:wrapper the app dropped โ their NPC-spoke rates measure the same request the game sends.
-
Diary-retrieve probe.
-
๐ค 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 a phone better.
-
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
-
โ๏ธ 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.
-
๐ค 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
.fbxand.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.fbxsat 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~Oandx^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 a phone โ 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.
-
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
-
โ๏ธ 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
imageOptimAPI 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.
-
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
โ 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)
-
๐ค 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.
-
๐ค 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.jsonextension. - 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
0immediately โ 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.
-
Characters speak up a little more often. Your action now reaches the AI as plain text instead of behind a
-
๐ค 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
.9are offered correctly. The update check compared prerelease versions as text, so a build likebeta.10looked older thanbeta.9and 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.jsongains three planted-fact cases (a stated goal with name/place/deadline, an introduced personal object, a named price) with per-caserequireregexes;summary-probe.mjsscores them, honors per-model endpoint overrides, counts the player's action as legitimate name material, and sendsreasoning_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.mjsscores 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) andcarryforward-check.mjs(re-derives each chain's kept/dropped split and judges every dropped fact against late narration for contradictions), plus a selection-driven armMin the dialogue-hold probe. -
Format-arms probe: hybrid arm + edit crossing.
format-arms-probe.mjsgains armH(bare history with the label on the current turn only โ byte-faithful to the app's real message assembly) and--editvariants now cross with--arms, so e.g.--arms H,B --edit fuseruns both cells fused in one paired batch.
-
Now-line probe. New
โ 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)
-
๐ค 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 of40 / 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.
-
๐ค 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
CollapsibleSectioncomponent. -
Section-style regression probe.
regression-style-probe.mjsA/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 realrestyleโ so section style is the only variable. Runs undervite-node. -
Fact-adherence style probe.
factstyle-probe.mjsA/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. Alsovite-node. -
Dynamic baseline mode. The baseline harness can now play a full session live instead of a fixed script: a
dynamicprofile 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.mjsis 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'sPlayer 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.mjsis 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.mjsreplays 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 Bsubstitutes the current prompt edits into the recorded messages. The planner probe library also gained aPROMPTS_SRCoverride so an A/B baseline can run against a snapshot ofGamePrompts.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)
-
๐ค 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.yamlso the app is allowed to reach it.
-
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 (
-
๐ ๏ธ 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.
-
The dev router can set Image Gen settings directly.
-
๐ค 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
.jsonwas 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)
-
๐ค 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=entityEditorand#dev?modal=dictionaryEditoropen 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.
-
The dev router reaches the library's character and dictionary editors.
-
๐ค 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.
-
๐ค 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 picture generation โ were being sent to the narrator along with the description, on both characters and locations, so the AI read
1girl, blue_hair, smilingas 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
descriptionlines 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.locktestfolder next toFormamorph.exe. -
A missing launcher now explains itself instead of crashing. If
Formamorph.exehad 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)
-
๐ค 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)
-
๐ค 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.
-
๐ค 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.
-
๐ค 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)
-
๐ค 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.
-
A repeatable prompt-quality gate. The baseline harness gained a scripted gate (
-
๐ค User-facing
- Long lists scroll again on mobile. On a phone, 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 requirederrors 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)
-
๐ค 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.
-
๐ค 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
ListDetailmaster-detail layout. Newcomponents/ui/list-detail.tsxrenders 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 onuseIsMobile. The caller owns selection and passesshowDetail+onBack.PlaceholderEditoris 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 isnone, so no containing-block interference. Adopted byPlaceholderEditor,DictionaryEditorModal(dictionary tree โ book/entry editor), and theWorldEditor(its per-tab list + detail extracted to sharedlistContent/detailContentfragments, branched onuseIsMobile: desktop resizable split vs mobile push; the< 1024px"not optimized for mobile" warning was removed). -
Configurable staged cast cap + guidance chip.
matchCastToEntities's hardcoded3is nowrunStagedPlanning'sactiveCharacterCap(Infinity = unbounded), fed from two new settings โlimitActiveCharacters(bool) +activeCharacterLimit(int, default 5). A new<ACTIVE CHARACTER GUIDANCE>prompt variable (registered inpromptVariables, offered on the Director prompt, resolved viaactiveCharacterGuidance()inbuildContextValues/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, notvh. 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 unitdvh, so centered overlays no longer overflow the mobile visible viewport and clip their own chrome. Page-rooth-screenscaffolds were intentionally left as-is. Verified no[Nvh]heights remain. -
Changelog popout formatting.
UpdateService.foldReleasenow emits each version as its own### <tag>heading (no more versionยทcategory merge);ChangelogBodytags the current / newest-uninstalled version headings (cl-current/cl-update) so.changelog-bodyCSS can tint them (info / success viacolor-mix, kept muted) and append the (Current) / (New!) labels; indentation is CSS too. Both changelog popouts widened tomax-w-3xl. Tests updated. -
Version-aware changelog selection.
buildRecentChangelognow 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 newestRECENT_CHANGELOG_COUNTwhen 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).currentVersionthreaded throughparseReleases; unit-tested across the up-to-date, behind-by-one-into-new-minor, and behind-by-many cases. -
Character customization responsive layout + bottom drawer.
CharacterCustomizationextracts the viewer and the controls into shared fragments and branches onuseIsMobile: desktop keeps the two-card split; portrait renders the viewer full-height with the controls in avaulbottom drawer (new dep) toggled by a floating Customize button. Newcomponents/ui/drawer.tsxfollows the shadcn recipe but keeps the overlay separate fromDrawerContent. 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 vaulsnapPointsโ a partial snap can't scroll its inner content, so snap points were dropped. The viewerCardgainedoverflow-hiddento clip the WebGL canvas, andVRMViewernow re-fits via aResizeObserveron its mount (not justwindow.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 toDEV_MODALS). No export-shape or behavior change. -
VRMViewercancels its render loop and animation timers on unmount. TherequestAnimationFrameloop and the self-schedulingplayNextAnimationsetTimeoutchain 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 feedsdt=0to freeze in place, andplayNextAnimationskips the clip swap while paused so the pose no longer jumps between keyframes. -
Touch-friendly DnD sensors on the library grids.
MainMenuswapped the sharedworldSensorsfrom a singlePointerSensor(distance: 8, which fired on any touch swipe) to aMouseSensor(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 changedtouch-noneโtouch-pan-yso a vertical swipe pans the grid's ScrollArea natively (withtouch-nonethe 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 keeptouch-none(their card bodies already scroll). -
randomUUID()helper with a non-secure-context fallback.crypto.randomUUIDis only defined in secure contexts (HTTPS/localhost), so any startup path that mints an id โ e.g.migrateWorldon load โ threw a blank screen when the app was served over plain-HTTP LAN (phone testing the dev server) or self-hosted without TLS. Newlib/uuid.tsuses the native API when present and otherwise builds an RFC-4122 v4 fromcrypto.getRandomValues(which isn't secure-context-gated); all 34crypto.randomUUID()call sites now route through it. Unit-tested (native path, fallback path, uniqueness). Still v4 UUIDs โ no export-shape or version impact.
-
Reusable
โ 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)
-
๐ค 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
PlaceholderStorecontext (contexts/PlaceholderStoreContext.tsx, with aplaceholderStore(value, setValue)adapter).GameDataContextbinds it to the world's placeholders; the library editors bind an isolated adapter over the item's ownplaceholdersfield. 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.PlaceholderEditoris 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+Dictionarygain an optionalplaceholders?(one representation for the export file and the in-app library item). Export (buildEntityCardData/buildDictionaryFile) bundles just the used defs viacollectUsedPlaceholders; import parses them onto the object; adding a card/book to a world runsabsorbPlaceholders(perfect name+values match โ reuse id, else fresh-id add) +remapPlaceholderIdsto 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) takeplaceholdersas a prop so the world-editor callers pass the world's list while the isolated library editors pass none. ExtractedKeywordChipsreused 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 (
primeRollsscans 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.placeholderRollspersists them (GameplayContext).usePlaceholderResolver+ GameViewer'sresolvePHapply 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
ChipVocabularyabstraction (lib/chipVocabulary.ts) so one editor serves both token families: the static prompt variables (<โฆ>) and dynamic per-world placeholders ({{phโฆ}}).PromptField/VariableNodenow read their vocabulary from context (prompt path unchanged โ verified), and a newPlaceholderFieldwraps 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:
Placeholdertype +World.placeholders, save-sideSaveObject.placeholderRolls, and the pureresolvePlaceholderscore (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 +PlaceholderManagername/values panel), reusing the extracted sharedKeywordChipsinput. Additive world + save export-shape change (nothing consumes it at runtime yet โ the chip editor + resolution wiring are later slices).
-
Placeholders โ scoped store, shared split UI. The placeholder editor now follows the same pattern as the dictionary editor: a scoped
-
๐ค 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.fetchWorldContentnow prefersmigrated.worldOverview.thumbnail(base64, already in the downloaded content) over the${API_URL}/thumbnails/...URL โ the server'sCross-Origin-Resource-Policy: same-origin(Helmet default) blocks the raw URL from<img>embedding cross-origin, and a URL can't render offline.getWorldMetadataalso falls back to the embedded thumbnail when a stored one is anhttp(s)URL, healing pre-fix downloads at read time (it already loadsdatafor tags, so no extra cost). Unit-tested. Separately, the FormamorphServer thumbnails route now setsCross-Origin-Resource-Policy: cross-origin(overriding Helmet'ssame-origindefault) so those public images are embeddable cross-origin โ not needed by the app anymore, but correct for any other embedder (needs a server deploy).
-
World download stores the embedded thumbnail, not the catalog URL.
โ 2.2.1 โ Released โ turns run their follow-up requests (choices, stats, summaries, diaries) concurrently for faster, steadier pacing, and the bundled desktop engine gains parallel requests, Flash Attention on by default, big-model / multi-GPU offload, and a VRAM-share readout (click to expand)
-
๐ค User-facing
- Turns finish faster with concurrent requests. After the story text, the app asks the model for your choices, stat changes, and any location change โ previously one after another, now all at once by default. On endpoints that handle parallel requests (e.g. LM Studio's Parallel setting) this cuts the wait between the narration and your choices by roughly a third, and makes turn times steadier. When memory summaries (and, in Staged mode, character diaries and new-character notes) are on, those run in the same batch too, so they finish with the turn instead of trickling in afterward. In Staged mode the per-character motivation passes also run at the same time instead of one by one, which noticeably speeds up busy scenes. A new Concurrent Requests toggle under Settings โ Generation lets you turn it off if a memory-tight local model slows down under the extra load; endpoints that don't run requests in parallel are unaffected either way.
- The GPU memory widget now shows Formamorph's own share (Desktop). The VRAM bars in Settings โ Endpoint get a highlighted segment marking how much the bundled model is using, with a "Formamorph: โฆ GB" readout. It uses the exact per-process figure when your GPU reports one, and otherwise falls back to the engine's own estimate (shown with a ~) โ so it works even on the many Windows GeForce cards that don't expose per-process VRAM.
- Big models and multi-GPU rigs are now supported (Desktop). The GPU offload control used to cap at 64 layers, so a large downloaded model (or a multi-GPU setup with room to spare) couldn't be fully offloaded. It now has Auto (fits as many layers as your VRAM allows โ the new default, never runs out of memory), Max (offloads the whole model, splitting across all your GPUs), and Custom (a fixed layer count) under Settings โ Endpoint (Advanced). The Context Size slider now also tops out at whatever the loaded model was trained for, instead of a fixed limit.
- Flash Attention is now on by default (Desktop). The bundled local engine's Flash Attention โ which lowers the model's memory use and is often faster โ now defaults on instead of off. It's widely supported on modern GPUs, and turning it on by default leaves more headroom for the parallel requests above. If an older GPU won't run it, you can still turn it off under Settings โ Endpoint (Advanced).
- The bundled desktop engine now handles parallel requests. The built-in local model (Desktop app) used to answer one request at a time and reject the rest, which meant the concurrent-requests speedup above didn't apply to it. It now runs up to a configurable number of requests at once. A new Parallel Requests slider under Settings โ Endpoint (Advanced) sets how many โ the default is 2, and it shows the per-slot context each setting leaves you. Higher is faster but splits the context window between slots and uses more VRAM, so raise it only if your GPU has room.
-
๐ ๏ธ Developer tooling
-
Local engine request pooling (
electron/llmEngine.cjs). The single-sequence +busy/429 design was replaced with a pool of N node-llama-cpp context sequences (createContext({ sequences: N })) plus an acquire/release wait-queue: concurrent requests each take a free sequence and decode in parallel (llama.cpp batching), and overflow requests queue instead of erroring. Sequences areclearHistory()-reset between uses; a client disconnect aborts the in-flight generation (via anAbortControlleron the responseclose) so a canceled request frees its slot. Slot count threads throughengineOptions(llm-set-options) and a newlocalParallelRequestssetting; changing it reloads the model.
-
Local engine request pooling (
โ 2.2.0 โ Released โ the desktop app self-updates, paging back shows a whole past turn (stats, scene, and choices travel back with the story text), entity groups, stacked choices, and a batch of drag-and-drop, scrollbar, stat-bar, and theming polish (click to expand)
-
๐ค User-facing
-
The desktop app can update itself โ The version number in the menu footer now checks for new releases (on launch, every few hours, and whenever you click it) and shows a " โ Update Available!" tag when there's a newer version. Click it for a dialog with the new version's release notes and a Download button; the download runs in the background with a progress bar under the version number, then turns into an Update & Restart button. A release channel picker in that dialog lets you choose Stable (default) or Pre-release builds. On Windows and Linux the update installs itself; on macOS the Download button opens the new
.dmgfor you to install. (The welcome-animation replay moved off the version number onto the copyright text to make room.)
-
The desktop app can update itself โ The version number in the menu footer now checks for new releases (on launch, every few hours, and whenever you click it) and shows a " โ Update Available!" tag when there's a newer version. Click it for a dialog with the new version's release notes and a Download button; the download runs in the background with a progress bar under the version number, then turns into an Update & Restart button. A release channel picker in that dialog lets you choose Stable (default) or Pre-release builds. On Windows and Linux the update installs itself; on macOS the Download button opens the new
-
๐ ๏ธ Developer tooling
-
Auto-update architecture (desktop). Detection is renderer-side and uniform across platforms:
UpdateServicereads the GitHub Releases API through the existingnet-fetchbridge, filters by theupdateChannelsetting (GitHub'sprereleaseflag), semver-compares againstAPP_VERSION, and caches the last result for an offline fallback. State is a pure reducer (src/lib/updates/updateState.ts) driven byuseUpdateChecker; the footer UI (UpdateVersionControl+UpdateDialog) is mounted only underisDesktop()so the web build never checks. Download/apply are per-platform in the main process (electron/updater.cjs): Linux useselectron-updaterAppImage self-update (addedelectron-updaterdep;publish: githubin electron-builder.yml; the Linux CI step now--publish alwayssolatest-linux.ymlreaches the release, which thepublishjob finalizes); macOS is a notifier (shell.openExternalthe.dmg); Windows uses a new Discord-style launcher. Pure logic is unit-tested (semver, reducer, channel filtering, cache fallback); a render test covers the footer tag + dialog. -
Windows launcher +
/appswap. A tiny Go stub (launcher/) is the stable root exe (the only binary SmartScreen judges); the real Electron app lives in<root>/appand is swapped in place on update, withuserdata/+models/beside the root untouched (FORMAMORPH_ROOTnow takes precedence inportableProfile.cjs/main.cjs).electron/winUpdate.cjsdownloads the/apppayload zip, verifies its sha512, and recordspending.json; the launcher waits for the app to exit, extracts + swaps (appโapp.backup,app.newโapp), relaunches with--just-updated, and rolls back if the new build crashes before writing alaunch-okmarker. The Windows target changed fromportabletodir; CI builds the Go stub and assembles the full-root zip + theapp-<ver>-win.zippayload + its.sha512. -
No world/save export-shape change. The only new persisted state is the
FORMAMORPH_updateChannelsetting; save/world.jsonshapes are untouched.
-
Auto-update architecture (desktop). Detection is renderer-side and uniform across platforms:
-
๐ค User-facing
- Multi-select dropdowns now match the theme like the regular dropdowns. The closed multi-select control used a transparent/off-theme surface and its selected-item chips used hardcoded colors that washed out in light mode. The control now uses the same background, border, and focus ring as a normal dropdown, and the chips and their remove buttons follow the theme.
- Stat-change bars animate consistently everywhere, and respect "reduce motion." The green/red change band and the accent fill now use one shared animation whether the change came from the AI or from paging back through turns, so the two no longer disagree. Turning on your OS "reduce motion" setting makes the bars snap to their final state instead of sliding.
- Ctrl+click (or long-press) a choice to add it to your action instead of replacing it. Clicking a suggested choice still replaces whatever's in the input box, but Ctrl/Cmd+click โ or a touch long-press โ appends the choice as a new sentence, so you can stack a couple of choices into one compound action (e.g. "Draw your sword. Attack the goblin."). Every choice whose text is currently staged in the box stays highlighted, and when you page back to a past turn, every choice you stacked that turn is highlighted (not just one).
-
Entities can be organized into groups (folders). The Entities tab now works like the Traits tab: add a group (a named folder), then drag entities into it, and nest groups inside groups to any depth. Groups are purely for keeping the editor tidy โ they are never sent to the AI and change nothing about gameplay or how entities read in a scene. Imported character cards land ungrouped. (Adds
entityGroups+ an entitygroupId/orderto world exports.) - Paging back to an earlier turn now shows that whole turn, not just its text. Flipping to a past turn used to change only the story text while the stats, location, scene, and character stayed on the latest turn. Now the entire view travels back with you: that turn's stats, traits, location (background and ambient sound included), who was in the scene, its choices, and the character's appearance all reflect how things were on that turn. A "Viewing turn N of M" banner marks that you're browsing history, and the controls that only make sense on the current turn (editing stats, changing location, picking a choice) are disabled while you look back โ narration and Player Notes stay editable on any turn. Return to the latest turn and everything is live again. On a past turn, the choice you took is highlighted โ inferred by matching your next action against that turn's options, so it still lights up even if you reworded the choice before sending it (nothing about your pick is stored). The stat changes for that turn show too โ the green/red +/โ text and the growing bar segment โ and they animate in as you scroll to the turn rather than snapping into place.
-
๐ ๏ธ Developer tooling
-
Dragged list/card items no longer resize while dragging. When rows differed in height (e.g. a save folder with a two-line name), the item being dragged snapped to the size of the row it was over. Cause: every sortable rendered its drag offset with
CSS.Transform.toString, which bakes in dnd-kit's slot-fit scale. Switched all of them (CSS.Translate.toString, translation only) so a dragged row/card/chip keeps its own size โ acrossSortableTree,LoadGameDialog,LocalModelModal,DictionaryTree,DictionarySelectionModal,WorldEditor,SortableWorldCard,Chip/EditableChip. -
Dragging an item to the bottom of a World Editor list no longer jitters/runs away. Inside the editor's Radix
<ScrollArea>, dragging a location/entity/trait to the bottom made the list judder and the view scroll into empty space. Two causes: at the bottom edgepointerWithinreturned no target, so the drop gap flipped on/off every frame (jitter); and the in-flow dragged row extended the scroll height, so auto-scroll chased it downward forever (runaway). Fixed inSortableTreewith aclosestCenterfallback for collisions (never empty) and a Y-only bounding modifier (restrictYToScrollAncestor) that clamps the drag's vertical position to the viewport but leaves X free โ so auto-scroll stays bounded while horizontal depth-nesting still works. Audited every otherDndContext: none need it (they're vertical-only/grids bounded byrestrictToFirstScrollableAncestor, or have auto-scroll off). -
Scrollbars: converting native
overflow-*containers to Radix<ScrollArea>to drop the OS arrow buttons. On Windows, Chromium renders up/down stepper arrows on native scrollbars that CSS cannot remove (::-webkit-scrollbar-button { display: none }andscrollbar-widthare both ignored for the buttons). The only button-less scrollbar is Radix<ScrollArea>, which hides the native bar and draws its own thumb. Converted so far: the Community Creations results list, the changelog body (ChangelogUi), the local-model recommended list + settings panel (LocalModelModal/LocalModelPanel), all sixSettingsModaltab/section scrollers (presentation, generation, endpoint, image, prompt-options, accessibility), and the Character Customization card. Also converted (harder cases): the drag-and-drop lists inLoadGameDialogand the installed-models list (LocalModelModal) โ the sortable rows now live in the ScrollArea viewport, whichrestrictToFirstScrollableAncestorstill finds;EntityModal(its short-content vertical centering degrades to top-aligned inside the overlay); and bothThemePreviewDialogcolumns. Still deferred:<textarea>s / editable-div editors (MarkdownField,PromptField, the prompt boxes โ need auto-grow + wrapper, an app-wide UX call), the responsive dual-columnWorldDetails(splitColumnClassesscrolls one column on mobile, two on desktop โ needs a per-breakpoint restructure), horizontal-scroll code blocks (the shared<ScrollArea>is vertical-only by design), the 3D-model viewer tab (canvas owns the wheel), and the popover-hosted lists (command/multi-select/SuggestionListโ scroll-lock interactions). No behavior change beyond the scrollbar visual. E2E coverage for these (which jsdom can't provide) is a tracked follow-up. -
Stat bar animation unified onto one tested geometry (
statBar.ts). The live and history paths had diverged โ different delta sources (held/drainvsviewStatChanges) and different fill mechanisms (plain width transition vs keyedstat-fill-slide) โ which is why the bars kept breaking with each change. Both now feed the same purestatBarFrame(prevValue, curValue, min, max)โ{ curPct, prevPct, bandLeftPct, bandWidthPct, hasBand, gain }plusbandOrigin(gain, draining), and the accent always slides prevโcur viastat-fill-slide(drain leaves the accent put and collapses the band toward the current value).StatBarnow takes(value, delta, draining, animKey)instead of(delta, drainDelta, animKey). Reduced-motion snaps to final (no slide/grow, no drain band) viausePrefersReducedMotion. Geometry is unit-tested (statBar.test.ts: gain/loss/no-change/clamping/sub-pixel/origins). No save- or world-shape change. -
Entity groups (editor-only folders; the simplest version). Replaced the earlier entity-nesting model with distinct
EntityGroupfolders, mirroringTraitGroup/traitTreeexactly.EntityGroup { id, name, parentId, order }(nestable folders);Entityswaps its oldparentIdfor a singlegroupId+order. A newentityGroupTree.ts(a 1:1 port oftraitTree.tsminus the AI-context builder โ groups never feed the AI) backs a rewrittenEntityTreeon the sharedSortableTree; the World Editor's Entities tab gains an Add-Group/Add-Entity popover.GameDataContextgainsentityGroups+addEntityGroup/updateEntityGroup/removeEntityGroup(deleting a group reparents its children). Selecting a group opens a name-onlyEntityGroupManager(mirrors the traitGroupManager).buildEntityContextreverted to the flat pre-grouping output (groupId/orderdestructured out) โ the entity context is byte-for-byte what it was before grouping. Card imports resetgroupIdto null. Deleted the oldparentIdTree.tsand the nesting tests; addedentityGroupTree.test.ts. World-export-shape change (entityGroupsadded,Entity.parentIdโgroupId+order); no save-shape change. Since 2.2.0 is unreleased, no migration is shipped โ old entity-nesting silently drops. -
Per-page "viewed" state in
GameplayContext(isViewingPast+view*selectors). Instead of mutating live state on page change (which would re-introduce the edit-clobbering bug), the context derives read-onlyviewStats/viewTraits/viewCharacterData/viewVisibleEntities/viewGameTime/viewLocationId/viewChoices/viewStatChangesfrom the paged snapshot โ each aliases the live value on the latest page, so normal play is provably unchanged and only past pages read the snapshot. Panels consume them via destructure-aliases (minimal churn) and gate their mechanical controls onisViewingPast. The body-morph effect now derives fromviewStats(avatar re-morphs to the paged turn); background reads the resolved viewed location; a new ambient effect keyed onviewLocationIdfollows the viewed location and fixes a pre-existing gap whereloadGameStateset the location without its ambient sound. No save-shape change. Verified in the preview via thewhiteRoom3-turn fixture: cue/narration/choices/edit-disabling all track the paged turn; the latest page stays live. -
Inferred choice highlight on past turns (
matchChoiceToAction,choices.ts). A past page marks which choice the player took by fuzzy-matching the next turn's action against that turn'sviewChoicesโ word-set SรธrensenโDice similarity with a 0.5 confidence threshold (returns-1for a custom action), tolerant of reworded/reordered choices, glue words dropped, markdown/punctuation stripped. Derived, never stored. Exposed asviewSelectedChoice; the choice render uses it forisSelectedon past pages (the live page keeps itschoice === playerInputselection). Pure + unit-tested (choices.test.ts), and preview-confirmed on the fixture (the highlighted choice matched the actual action taken). -
Animated stat deltas on past pages. Reviewing a past turn feeds that turn's derived change (
viewStatChanges, computed vs the previous page's stats) into the sameStatBargrow band and delta text as a live turn, so the green/red bar ++/โtext render for the reviewed turn. The delta is computed by a purepageStatDeltas(stats, prevStats)(statChanges.ts): each stat's value minus its previous-turn value, or โ on the opening turn, which has no predecessor โ itsstartingvalue (elsemin), so turn 1 shows its deltas too instead of nothing. Newstat-delta-text-inCSS class (fade/slide-in and hold, unlike the live 10s fade-out);StatBargained ananimKey(the page number) so the band re-animates on each scroll-back even when a stat's value/delta coincide between turns; the delta text span re-keys oncurrentPagefor the same reason. While reviewing history the base fill also slides via a keyedstat-fill-slideCSS-var animation (--fill-from/--fill-to= the turn's previous โ current value) instead of a plainwidthtransition โ so it always starts from the correct origin and stays in step with the grow band, rather than sliding from whatever page you scrolled from (which left a gap on backward navigation). Live keeps the plain transition (you always arrive from the previous turn there). The DEVwhiteRoomfixture now carries two stats with per-turn changes so the stats view + deltas are actually exercised (preview-verified:+2green andโ10red on turn 2, both with animate-in text and matching grow bands; the latest page stays on live deltas). โ Starting Formamorph briefly popped up a black terminal window before the app appeared; it no longer does, and the launcher now shows the Formamorph icon instead of a generic one - Updates no longer leave a full-size backup copy behind (Windows) โ After a successful update the previous version was kept on disk, roughly doubling the install size; it's now removed once the new version is confirmed healthy
- Re-generate and Rollback no longer undo your edits to the previous turn โ Editing a turn's narration or your Player Notes and then re-generating (or rolling back to that turn) reverted it to the original, unedited text. Both now keep your edits โ they restore the turn's game state without throwing away what you changed.
-
Stat change bars show the real change and match between the current turn and history โ The current turn's green/red delta came from the amount the AI asked to change a stat, while a past turn showed what actually changed; when a change was capped (stat already at max/min) or otherwise limited these disagreed โ a maxed stat could still flash a "+N" bar. The live bar/text now use the actual applied movement (
appliedStatDeltas, computed from the clamped result), so they line up with the history view everywhere. Unit-tested incl. the capped-stat case. - Editing a turn's narration now refreshes what the AI remembers of it โ A turn's memory summary and character diary entries are built from its narration; after you edited that narration they were left describing the old text. Editing now clears them so they quietly rebuild from what you wrote.
- Page footers' Previous/Next buttons no longer jump around as you flip, and the current page is clearly highlighted. As you paged, the strip of page numbers changed how many were shown (more in the middle, fewer near the ends), shifting the Previous and Next buttons left and right. Every pager โ the story turns, the AI-context viewer, and the Community Creations browser โ now reserves its widest layout at all times, so the buttons stay put. The low page numbers hug the left, the high ones anchor to the right, and a single "โฆ" sits centered between them. The page you're on now shows as a filled button so it's obvious at a glance.
- You can recover after a local model fails to load (desktop) โ If the built-in engine reported a context size too large for VRAM, the panel got stuck: lowering Context Size (or GPU Layers) did nothing because the Save & Reload button only worked while a model was already loaded. Now, after a failed load, the button re-enables so you can lower the setting and retry, and a hint points you at what to change. (The GPU Layers slider under Advanced already lets you partially offload a model that doesn't fully fit VRAM.)
-
Dragged list/card items no longer resize while dragging. When rows differed in height (e.g. a save folder with a two-line name), the item being dragged snapped to the size of the row it was over. Cause: every sortable rendered its drag offset with
-
๐ ๏ธ Developer tooling
-
Local-engine reload isn't gated on
status === 'ready'anymore.LocalModelPanel's Save & Reload was disabled unless the engine wasready, so astatus: 'error'load (e.g. node-llama-cpp's memory-safety check rejecting a too-large context) trapped the user โ the lowered Context Size never reachedengineOptionsinmain.cjs. The enable check is now "settings differ on a ready model, or the last load errored"; the error branch also relabels the button "Retry with these settings" and shows an out-of-VRAM hint. Renderer-only; the underlying node-llama-cpp VRAM estimate is unchanged (its rejection was legitimate). -
Rollback/Re-generate restore mechanical state only; narration + notes come from the live history. Each
gameStates[i]snapshot froze a full copy offullMessageHistory+playerNotesat turn-end, andloadGameStatere-injected those stale copies on restore โ clobbering any edit the player made afterward.loadGameStategained akeepLiveHistoryoption;handleRollback/handleRegeneratenow restore the snapshot's mechanical fields but rewind narration by slicing the live flat history (sliceHistoryToPage,turnHistory.ts) and leave notes live. Restore logic only โ no save-file shape change. Unit-tested (turnHistory.test.ts), incl. a regression that a kept turn's edit survives the slice. -
Narration edits invalidate the turn's derived memory (
clearTurnDerived).EditTextModal onSavedrops the edited turn'ssummary+diaries(turnDigest.ts, keyed byturnId) so the digest/diary drainers re-select and rebuild the turn. Scoped to narration edits only โ Notes edits and the choices/stat partial-regens don't feed the digest or diaries, so they don't force a rebuild. Unit-tested (turnDigest.test.ts). - A past page with no saved snapshot no longer claims to show history. If a save's per-turn snapshots were short of its page count (e.g. a converted deep-nested legacy save), paging back flagged history mode โ banner, disabled controls โ while the stats/location/notes silently fell back to the current turn. A page now counts as history only when it actually has a snapshot; otherwise the panels stay live (matching the pre-page-view behavior) instead of contradicting themselves. Verified: removing a page's snapshot flips it out of history mode with the view state equal to live.
- Code-derived stats now animate their change live, like every other stat. A stat computed by code (e.g. a level derived from XP) updated its value but showed no green/red delta band on the turn it changed โ while paging back to that turn did show it. The live feedback now folds in the code-recompute delta, so live and history agree. (Only affects worlds using stat code.)
- Editing an unparseable past turn's notes no longer wipes its narration. If a turn's stored JSON couldn't be parsed, saving a notes edit on that turn wrote a blank stub โ destroying its narration, choices, and turn id. The notes write now bails when the turn doesn't parse (leaving it untouched), matching the digest/diary paths. Verified: a corrupt turn's content is left intact on a notes edit, while a normal turn still takes the note with narration/choices preserved.
-
A fresh world (or an aborted opening turn) no longer falsely shows "viewing history". With an empty message history the page counter landed on 0 while the total was floored to 1, so
currentPage < totalPagesflagged history mode โ showing a "Viewing turn 0 of 1" banner, disabling choices/location/edit, and dropping Player Notes edits (the write targeted message index โ1) before the game even started. The current-page follow now floors at 1. Verified: empty history โ page 1, not viewing past, no banner. -
Stat bars no longer animate away twice when you submit an action. On submit the green/red band drained once, then instantly re-grew and drained again. Cause:
currentPagewas synced to the latest turn by a post-render effect, so appending your action grew the page count a frame beforecurrentPagecaught up โ flipping "viewing history" true for one render and remounting the stat band.currentPageis now derived (GameplayContext): it follows the latest turn automatically unless you've paged back, so it can't lag and the flip is gone at the source โ fixing every panel, not just the bars. Verified by counting band mounts across a real turn:grow โ drain(one), previouslygrow โ grow โ drain. Side effect (intended): paging back during generation is now sticky โ a new turn no longer yanks you to the latest until you navigate or take an action. - Loading a save shows the real stats, not the world's starting values. After loading a mid-game save, the live stats panel could briefly (or persistently) show the world's default values instead of the saved ones, because a reactive effect re-seeded stats from the world whenever its definition loaded โ racing with the save restore. Seeding now happens once, only when starting a fresh game (the load path is separate), so a loaded save's stats are never overwritten. Verified: loading a save shows its saved stat values, with each stat's game-start baseline intact.
- Code-driven stats no longer wipe a turn's regen. After a turn, applying stat code re-set the whole stats array from a snapshot taken before the code ran โ so if regen (or a re-generate) landed in between, it was overwritten. The code result now updates only the stats the code actually moved, layered onto the latest values, so regen and concurrent changes survive.
-
Turn/page internals cleanup. The paged turn's assistant message was JSON-parsed twice per render (once for choices, once for notes) โ now parsed once. The pageโmessage index math (
pageยท2โ1for the turn's narration,pageยท2for the next action) is centralized in testedpageAssistantIndex/pageNextActionIndexhelpers instead of being re-encoded across the view selectors, rollback, and re-generate. No behavior change. -
Stat bars don't double-grow / lose regen from a redundant recompute. Separately, after a turn
processStatCodealways handed back a fresh array even when no stat had code, soapplyStatChangesre-set the stats a second time โ from a pre-regen baseline โ replaying the grow and wiping that turn's regen.processStatCodenow returns the original array when nothing changed, so the redundant update is skipped. Unit-tested (same reference on no-op, new reference on a real code change). -
The opening turn's stat changes read from the world's starting values, not zero. Page 1 always showed green "+full value" bars because it had no earlier turn to compare against and fell back to 0. Each stat's game-start value is now recorded (
starting, populated at game init from the world default), so the first turn's green/red deltas reflect what actually changed from the world's starting stats. Additive save-shape (startingon saved stats); old saves without it fall back to the previous behavior on their opening turn. Preview-verified: turn 1 shows +2/+5 from the starting values instead of +5/+30 from zero. -
Rollback/Re-generate now carry the target turn's own notes, not the latest turn's. With per-turn notes,
handleRollback/handleRegeneraterestore withkeepLiveHistory(which skips the snapshot's notes), so the live scratchpad still held the pre-rollback latest turn's notes โ a re-generate then froze those onto the fresh turn. Both handlers now seedplayerNotesfrom the target turn's messagenotes(parseTurnContent(...).notes ?? '') after the restore. Preview-verified: rolling back to a turn seeds the scratchpad with that turn's notes. -
Per-turn Player Notes (
AITurnResult.notes, additive save-shape). Notes moved from the single globalplayerNotesscratchpad to a per-turn field frozen onto each turn's assistant-message JSON when the turn commits (omitted when empty).GameplayContextgainedviewNotes(current page โ live scratchpad; past page โ that turn's messagenotes, falling back to the snapshot's globalplayerNotesfor old saves) and a page-awaresetViewNotes(edits on the current page write the live scratchpad; on a past page they patch that turn's message only). The Notes tab consumes both; notes stay editable on any page. The live scratchpad is still what the AI receives each turn, so send behavior is unchanged. No migration needed โ the snapshot-playerNotesfallback covers pre-existing saves. Preview-verified on thewhiteRoomfixture: a note typed on the current page doesn't bleed to past pages, a note typed on a past page persists to that turn only (confirmed on the message JSON) and not its neighbors, and the live page keeps its own note. -
Paging back mid-generation no longer drags the streaming narration onto the history page. While a turn was still generating, flipping to an earlier turn kept printing the new narration onto the page you were viewing. Generation now continues in the background while you browse history โ the past page shows that turn's finished text โ and the live narration reappears when you return to the current turn. Fix was render-only (
MiddlePanel): the live reveal (narration + reasoning) is now gated on being on the current page (!isViewingPast), not just on a page-local "last message" flag. - Player Notes are now per-turn โ each turn keeps its own notes as you page back. Notes used to be one shared box for the whole game; paging to an earlier turn still showed (and edited) that single global note. Now every turn carries the notes it had, so flipping back shows that turn's notes and you can edit them there without touching any other turn. On the current turn the box works exactly as before (a running scratchpad the AI sees), and Re-generate carries your notes onto the fresh turn. Old saves still load โ a turn with no saved per-turn note falls back to the save's previous global note.
-
Save files no longer store the chat history N times over (storage cleanup + migration). Each per-turn snapshot (
currentState+ everystateHistoryentry) embedded its own copy of the whole flat message history โ O(Nยฒ) growth on disk. The envelope now carries one canonical top-levelmessageHistory; snapshots are stripped of theirfullMessageHistorycopies (stripSnapshotHistory,version.ts), andsaveGamewrites the new shape. Loading reconstitutes the live current state from the canonical history, so narration/rollback are unchanged. Save-file shape change with a presence-based migration in the sharedmigrateSavepath (import + load): any old save โ legacy numericversion: 2or a string-version save written before this change โ has its history hoisted and copies stripped on load, idempotently; old saves still load. The import boundary now runsmigrateSavefor every envelope (not just numeric-legacy). Unit-tested (saveCompat.test.ts: hoist + strip on legacy and string-version saves, idempotency, canonical-history correctness); preview-verified thewhiteRoomfixture (an old-shape save) still loads all 8 turns through the migration with the live history intact. -
Shared, non-reflowing page footer (
Pager). Three pagers โ game turns, the AI-context debug viewer, and the community browser โ each carried their own copy of the same item loop (first, last, ยฑ1 window, ellipsis gaps), so the centered strip's width, and thus Previous/Next, shifted while flipping. Extracted onePagercomponent (components/ui/pagination.tsx) over a purepaginationSlots(currentPage, totalPages)(pagination.ts) that returns a constant number of cells (min(totalPages, 7)), padding short/near-edge layouts with invisible spacers; every cell is normalized to the same width (linksw-10, spacersh-10 w-10, ellipsis overridden tow-10). Constant count ร equal width โ invariant strip โ the buttons never reflow. All three call sites now render<Pager page pageCount onPageChange />โ the three duplicate loops (~60 lines) are gone. For near-edge layouts the spacers are distributed, not tail-padded: low pages left, high pages right-anchored.Pagercoalesces each contiguous spacer+ellipsis run into one flex cell (width = the cells it replaces, incl. inter-cell gaps, so the strip width is unchanged) with the ellipsis centered โ so a lone ellipsis sits dead-center of the gap even when the groups are lopsided (1 2 3 4 โฆ 8), not jammed against a cluster. The active page uses the filleddefaultbutton variant (was the near-invisibleoutline). Pure logic unit-tested (pagination.test.ts); the DEVwhiteRoomfixture extended to 8 turns to exercise it โ preview-measured: ellipsis 0px off gap-center, Previous/Next pixel-identical across pages, active pagebg-primary.
-
Local-engine reload isn't gated on
โ 2.1.0 โ Released โ reasoning-model support (Native mode, endpoint-aware effort, per-prompt reasoning, desktop thought budget, live reasoning block), shareable prompt presets, a first-run welcome animation, and a halved Vulkan-only desktop build (click to expand)
-
๐ค User-facing
-
Share prompt presets โ Settings โ Prompts now has an Export button and an Import Presetโฆ entry in the preset dropdown. Export gives you a copy-paste share code (for Discord/chat) or a
.jsonfile; the whole preset travels โ all prompt text, its section style, and its per-prompt tuning (samplers, reasoning, verbatim turns). You can export any preset, including the built-in Default/Simple as starting points. Import lets you pick a file or paste a code, previews the name and any "made for a different version" note, and lets you choose prompt text only vs text + tuning; if the name already exists you can overwrite it or keep both. Presets made in another version import as-is โ anything the app doesn't recognize is skipped. -
A first-run welcome animation โ The first time you ever launch Formamorph, a "Welcome Toโฆ" title types in and a field of liquid "goo" pops up, gathers together, and merges into the Formamorph title before handing off to the menu. It plays once (never again automatically) and respects your reduced-motion setting (shows a still title instead). Click the tiny version number in the footer to replay it any time (snappier the second time around).
-
Native Reasoning control for reasoning models โ The Thinking mode formerly called Off is now Native โ it adds nothing to the prompt, so a reasoning model thinks as it normally would and everything else answers immediately. When Native is selected, a Native Reasoning control appears with Default (send no hint โ the endpoint decides), None (stop a reasoning model from thinking so it answers directly), and effort levels. The levels shown are detected from your endpoint โ the app quietly asks it which it accepts on connect, so you only see options that actually work (e.g. some endpoints offer Min, others Max). While it's still probing (or your endpoint is offline) it shows the universally-safe set. All of it is ignored by models without native reasoning.
Reasoning is no longer an all-or-nothing switch: it's per-prompt. The Narration and Choices prompts each get their own Native Reasoning control in their Options tab (Settings โ Prompts โ pick prompt โ Options) โ narration follows the endpoint-wide level (Global) by default, choices is None. Every other prompt (stat updates, location, summaries, images, the planning stages) never reasons, so a reasoning model won't waste time thinking on bookkeeping. And the Inline / Planning / Staged thinking modes now turn a reasoning model's own thinking off entirely, so it doesn't fight Formamorph's planning step.
-
Reasoning Budget on the built-in desktop engine โ When you run Formamorph's own bundled model (desktop app, no custom endpoint), the reasoning control in Narration/Choices Options becomes a Reasoning Budget slider โ the share of Max Output Tokens the model may spend thinking before it writes (0% = no reasoning). It's an exact, graceful cap: the model wraps up its thought and gets on with the scene, so you trade a little planning for a lot less waiting. Narration defaults to 40%, Choices to 0%. (This precise budget isn't available over most external endpoints โ those keep the coarse effort control instead.)
-
See a reasoning model's thinking โ When a reasoning model (or the Inline thinking mode) thinks before it writes, that scratchpad now shows as a collapsible "Thinkingโฆ" note above the turn's narration โ it streams live while the model thinks, then folds up to "Thought for Ns" once the story begins. Click to re-open it any time; it's saved with the turn, so it's there on old turns and in scrollback too. A new Show Reasoning toggle (Settings โ Generation, on by default) hides it if you'd rather not see it โ the reasoning is still captured, so turning it back on reveals it on past turns.
-
-
๐ ๏ธ Developer tooling
-
Inline thinking reliably opens the
<think>block now (placement fix). The Inline-mode reasoning directive was buried at the end of the narration system prompt, where small models mostly ignored it โ so the new "See a reasoning model's thinking" block often stayed empty in Inline mode. RewroteINLINE_THINKING_DIRECTIVEto a tight 3โ4-bullet continuity structure and moved it onto the final player-action turn (adjacent to where the model writes, same trick the Planning plan uses). A new A/B probe (testing/baseline/harness/inline-probe.mjs, 9 gold scenarios ร 2 runs on the reference tiers) isolated the cause: the reworded directive still in the system prompt emitted<think>on only Silver-Siren 2/18 ยท MeroMero 5/18, but the same text on the user turn hit 18/18 on both, every one followed by clean narration with no bullet-leak into the prose. Recency โ not wording โ was the lever. Prompt/plumbing only; no version/save/export impact. -
Desktop build roughly halved โ Vulkan-only GPU backend (CUDA dropped). electron-builder now excludes node-llama-cpp's CUDA backends (
@node-llama-cpp/*cuda*, ~537MB), the unused Windows arm64 backend, and the from-sourcegitRelease.bundle. Reason (investigated, not assumed): the CUDA prebuilts need CUDA redistributables that don't ship with node-llama-cpp and aren't on normal systems, and even with the exact-matching redist the prebuilt initializes 0 devices on a reference RTX 4090 + CUDA-13.3 driver โ a liability that only balloons the download while showing a "CUDA not compatible, falling back" warning every launch. Vulkan runs on every GPU including NVIDIA (it's what the reference machine used all along), so it's the shipped GPU backend with CPU fallback. A clean Windows--dirbuild measured 548MB, down from 1134MB (โ52%), and boots. A dedicated CUDA build (bundling the redistributables) can come later. Config only โ no code/version/save impact. -
Desktop package ~350MB lighter (renderer deps no longer double-shipped). Every renderer library (react, three, kokoro, onnxruntime, Radix, fontsโฆ) was listed in
dependencies, so electron-builder auto-packed its rawnode_modulescopy into the app โ dead weight, since Vite already bakes them intodist/. All renderer-only packages moved todevDependencies;dependenciesnow holds justnode-llama-cpp(the one package the Electron main process loads at runtime โ verified by grepping everyrequire/importinelectron/*.cjs). Verified on a real--dirbuild:app.asaris 52MB (โ dist + app code), its 92 packages are node-llama-cpp + transitive deps only, zero renderer packages remain, and the built exe boots. Faster launch for the portable exe (that much less to extract + virus-scan every start) and a smaller download. -
reasoningEffortsetting + Native rename.ThinkingMode'soffvalue is relabeled Native in the UI (the stored value stays'off', so no settings migration); addedReasoningEffort = 'none' | 'auto' | 'low' | 'medium' | 'high'(SettingsContext,FORMAMORPH_reasoningEffort, defaultauto;nonewill sendreasoning_effort: "none"to suppress native thinking). The Settings โ Generation โ Thinking block gains a Native Reasoning tabs widget rendered only under Native (thinkingMode === 'off'), mirroring the Thinking widget's stacked-help layout. All of the reasoning-effort logic is pure + unit-tested insrc/lib/reasoningEffort.ts:-
Endpoint capability detection.
detectSupportedReasoningEfforts(url, token, model)probes each candidate (none/minimal/low/medium/high/xhigh/max) with a 1-token request and keeps the 200s (400 = rejected; any network/auth error โnull= inconclusive, keep fallback).SettingsContextruns it debounced on connect and remembers eachendpoint|model's result in a bounded map (FORMAMORPH_reasoningSupport, cap 30, oldest evicted), so flipping between endpoints or swapping the model on one reads from cache instead of re-probing; it exposessupportedReasoningEffortsfor the active pair.reasoningTabs(supported)builds the tabs โ always Default (omit) first, then the supported levels in canonical order โ falling back to the universalnone/low/medium/highwhen the set is unknown. Verified live: cloud accepts all seven; Ollama acceptsnone/low/medium/high/max(nominimal); an offline endpoint falls back to the safe set. -
Request wiring.
reasoningEffortBody(mode, effort, supported)is spread intomakeAIRequest's body: under Native it sendsreasoning_effortverbatim and omits it forauto(send nothing โ endpoint default; the literalautoisn't a valid value anywhere โ every backend 400s on it); under the guided modes (Inline/Planning/Staged) it forcesnoneso a native model's own thinking doesn't fight the guided step. In every case a value the detected set doesn't include is omitted, sononecan't 400 a non-reasoning endpoint and a stale selection can't 400 a turn after switching endpoints. -
Per-prompt override.
resolvePromptReasoning(kind, prefs, globalEffort)picks each request's effort under Native mode:REASONING_CONTROL_KINDS(narration, choices) read their stored choice (now preset-scoped viaactiveReasoning, defaults narration=global, choices=none), every other prompt is forced tonone;globalfolds in the Generation-levelreasoningEffort(which no longer applies to requests on its own). The control renders in the prompt's Options tab (PromptReasoningField) only under Native mode for the two controlled kinds, disabled under the built-in Default preset.makeAIRequestfeeds the resolved value throughreasoningEffortBody. -
Reasoning block (capture + persist + UI, additive save-shape). The narration stream now captures reasoning from both sources โ a native model's separate
reasoning/reasoning_contentdelta field and inline<think>blocks in content (extractReasoning/extractReasoningLive, unit-tested) โ with think-duration timing (first token โ first narration token). It's persisted into the per-turn assistant-message JSON asreasoning: { text, ms }(AITurnResult) โ an additive save-shape change (absent when the model didn't reason / on pre-2.1.0 saves; presence-based, old saves load fine). Live streaming rides an external store (reasoningStreamStore, mirroringgameplayTextStoreso per-token updates don't re-render gameplay).ReasoningBlockrenders it above the narration (MiddlePanel): expanded + "Thinkingโฆ" whileactive, auto-collapsing to "Thought for Ns" when narration begins, full markdown in a de-emphasized container. Gated by theshowReasoningsetting (default on; captured regardless, so toggling on reveals past turns). Verified: model emits<think>/reasoning (node-confirmed), helpers parse it (unit tests), the app correctly captures nothing when the model emits nothing, and the block renders/collapses/styles correctly (preview, injected data). -
Reasoning Budget (local engine โ thought-token cap). The reasoning control is engine-split in the Options tab: external endpoints show the effort level; the bundled desktop engine (
localModelActive) shows a Reasoning Budget slider (0โ100% of max output).reasoningBudgetBody(mode, kind, budgets, maxTokens)โ{ thinking_budget_tokens: round(pct% ร maxTokens) }, spread intomakeAIRequest's body only whenlocalModelActive; guided modes and uncontrolled prompts resolve to0.electron/llmEngine.cjsmapsbody.thinking_budget_tokens โ session.prompt({ budgets: { thoughtTokens } })(node-llama-cpp; verified exact โ capping to 200/100/40/0 landed on the cap, narration stayed coherent, latency scaled). This also fixes a gap: the local engine ignoresreasoning_effort, so guided-mode/None suppression is now enforced there viathinking_budget_tokens: 0. Per-prompt %, preset-scoped (reasoningBudgetonPromptPreset, defaults narration 40% / choices 0%), travels with preset export/import. Preset-share shape change (additive): the shared preset.json/code gains an optionalreasoningBudgetmap (older imports drop it;FORMAT_VERSIONunchanged). Pure logic unit-tested; UI (engine swap + defaults) verified in preview by forcing the local branch. Desktop end-to-end confirmed by driving the realllmEngine.cjsserver standalone:thinking_budget_tokensof omit/200/40/0 capped the<think>block to 1785/873/161/0 chars, narration stayed coherent at every budget (incl. 0).
-
Endpoint capability detection.
-
Intro sequence (
src/components/IntroSequence.tsx) โ a self-contained Canvas 2D + SVG-metaball-filter animation. Two layers (themed background + a blob layer behind afeGaussianBlur+feColorMatrixgoo threshold), with the real letterforms drawn onto the filtered layer so they read crisply while keeping liquid edges; the word's alpha ramps through the threshold across the whole hold so it appears to set out of the goo rather than fade in. Colors are pulled live from the app's CSS theme tokens (--primary/--card/--background), so it adapts to the active skin. The puffy title face is a ~2KB Baloo 2 subset embedded as base64 (src/lib/introFont.ts), loaded viaFontFacebefore canvas sampling. First-run gating is alocalStorageflag inApp.tsx(cinematic first, snappy on replay); the overlay mounts over MainMenu and fades its backdrop out on hand-off to reveal the menu underneath. Reachable for verification via the dev-router (#dev?modal=intro).
-
Inline thinking reliably opens the
-
๐ ๏ธ Developer tooling
-
Cross-version chip compat locked (import safety). Confirmed + regression-tested that the prompt tokenizer already treats any chip it doesn't recognize as literal text โ so a preset imported from a different app version can't blank, drop text, or crash the editor/preview/runtime; a chip added in a later version just shows as its raw
<โฆ>token until that version is installed.parsePromptTemplate's registry-onlyTOKEN_REis the single chokepoint (editor, preview,renderPromptTemplateall route through it); new tests inpromptTemplate.test.tscover a foreign base and a foreign variant across parse/serialize/render. No behavior change. -
Prompt-preset export/import UI.
PresetShareDialogs.tsx(ExportPresetDialog+ImportPresetDialog) wired into Settings โ Prompts: an Export button (materializes the selected preset โ built-ins included โ viaexportActivePreset(APP_VERSION)) and anImport PresetโฆSelect sentinel. Import parses a file or pasted code (parseSharedAny), previews name + non-blocking warnings, offers texts-only vs +tuning and overwrite-vs-keep-both on a name clash, thenimportPreset(imported, opts)adds (addFullPreset) or overwrites (replacePreset) and selects it. Export/import round-trip verified live (code decodes to the full stamped preset with tuning; import created a distinct copy, showed the version warning, handled the collision). Not an export-shape change to worlds/saves โ a new shareable format (like character cards). -
Prompt-preset share codec (
src/lib/promptPresetShare.ts). Serializes a preset โ 15 texts + section style + preset-scoped tuning (samplers/reasoning/verbatim) โ to a pretty.jsonfile and a copy-paste share code (UTF-8-safe base64 behind anFMPRESET1:prefix). Stampskind+formatVersion(1) + the sourceappVersion. Parsing sanitizes: keeps only recognized text keys and well-typed tuning, silently drops the rest (with a warning), and a source-version/newer-format mismatch warns but still imports as-is (the agreed "import as-is + warn" contract). Groundwork for the import UI; not wired to any surface yet. Unit-tested (round-trip, unicode, junk rejection, unknown-key drop, version/format warnings). Part of the shareable-preset feature (seedocs-internal/prompt-preset-sharing.md). -
Per-prompt tuning is now preset-scoped (settings-store change). Samplers, per-prompt reasoning, and verbatim-turns moved from three global stores (
FORMAMORPH_promptSamplers/_promptReasoning/ the six_*VerbatimTurnskeys) onto eachPromptPreset(samplers/reasoning/verbatimfields insrc/lib/promptPresets.ts) โ groundwork for shareable "prompt packs". Built-in presets carry no tuning (resolve to shipped defaults; their tuning setters no-op like their text).SettingsContextderives the three viaactiveSamplers/activeReasoning/activeVerbatim(presetStore)and routes setters throughupdateSamplers/updateReasoning/updateVerbatim(no-op under a built-in) โ the public context field/setter names are unchanged, so GameViewer/SettingsModal consumers are untouched. A one-timemigratePromptTuning()folds the old global tuning onto every user preset (viafoldTuningIntoUserPresets, never overwriting an existing per-preset value, only carrying non-default verbatim), then retires the old keys behind a_promptTuningMigratedmarker. Behavior change: switching presets now swaps tuning too (previously text-only); a user on a built-in with custom tuning reverts to defaults there (by design). Pure logic unit-tested; migration verified live. Settings-store only โ not world/save export shape.
-
Cross-version chip compat locked (import safety). Confirmed + regression-tested that the prompt tokenizer already treats any chip it doesn't recognize as literal text โ so a preset imported from a different app version can't blank, drop text, or crash the editor/preview/runtime; a chip added in a later version just shows as its raw
-
๐ค User-facing
- The welcome animation now matches your theme โ On a dark theme the first-run intro rendered on a white background with the "Welcome Toโฆ" text invisible (white on white). It read the theme's colors a moment too early โ before your theme was applied โ so it always grabbed the light-theme palette. It now tracks the active theme, so the intro shows on the correct background with readable text in light or dark.
- The desktop app now works with local AI servers that don't send CORS headers โ On the desktop build, pointing at a custom endpoint whose server has CORS turned off (e.g. some LM Studio setups) failed every request with a generic "Failed to process AI request" โ the browser layer blocked the call before it left the app, so no error detail ever surfaced. The desktop app now handles this itself, so a custom endpoint works regardless of the server's CORS setting. (The browser build still needs the server's CORS enabled โ that's a browser rule the web version can't bypass.)
-
๐ ๏ธ Developer tooling
-
Intro sequence samples theme tokens inside the rAF loop, not once at mount.
IntroSequenceread--background/--card/--primaryviagetComputedStylein its mount effect โ which fires before the parentThemeProvider's effect adds the.dark/.lightclass, so it always caught the classless:root(light) defaults, while the kicker's livetext-foregroundflipped to the applied theme โ white-on-white on a dark theme.readColors()now runs at the top of eachframe()(andrenderStatic(), whose reduced-motion call is deferred one rAF), so the canvas tracks whatever theme CSS currently resolves โ in lockstep with the CSS-driven kicker, and self-healing if the OS theme flips mid-intro. Verified deterministically (rAF is throttled in a hidden preview tab, so live capture is unreliable): reproducing the token math showed the classless read yields--background: hsl(0 0% 100%)(white) vshsl(220 12% 10%)(dark) with.darkapplied. -
Desktop CORS shim. The renderer's
app://localorigin made every external fetch (custom LLM endpoint, community server, Hugging Face) browser-CORS-gated; a server with CORS off failed the preflight (noAccess-Control-Allow-Origin) and surfaced only as a network-levelTypeError: Failed to fetchโ the generic toast.electron/main.cjsnow rewrites external http(s) responses (including theOPTIONSpreflight) viasession.defaultSession.webRequest.onHeadersReceivedto carry permissive CORS headers โ keeping the normal streaming fetch and leavingwebSecurityon (notwebSecurity: false, which would drop cross-origin protections app-wide given the app loads community-authored worlds). The pure header transform lives inelectron/corsShim.cjs(Authorizationis listed explicitly becauseAccess-Control-Allow-Headers: *excludes it per spec); Node-verified. The web build is unaffected โ browsers enforce CORS, so hosted users still need the server to send the headers.
-
Intro sequence samples theme tokens inside the rAF loop, not once at mount.
โ 2.0.3 โ Released โ per-world save folders, per-prompt sampling & rebuilt Planning mode, portable-build data folder, smoother narration reveal, and a batch of editor/menu UX fixes (click to expand)
-
๐ค User-facing
-
Saves are now organized into per-world folders โ Opening Load Game drops you straight into the current world's folder, listing only its saves โ the common case is now one step. A Back button (under Import) and a
Saves / <world>breadcrumb take you to the root, where every world with saves has a folder (the current world is always first, even when empty), each showing its save count and when it was last played. Saves and folders can be dragged into any order by a handle on the left, and new ones always appear on top. On a save row, Download and a โ delete (which now asks before deleting) sit on the right. Loading a save from a different world now actually switches to that world (after a confirmation) instead of loading in place โ or, if that world isn't installed, warns you it'll load into your current world and lets you decide. Importing a save file drops it into its own world's folder and takes you there. Saving a name that already exists in the current world lets you overwrite it or keep both. You can also load a save straight from the main menu โ its cog is now a hamburger with Load Game and Settings; picking a save loads its world and drops you into the game (a save whose world you don't have installed explains that and offers download/delete). - Per-prompt sampling controls โ each prompt's editor (Settings โ System Prompts) gains an Options tab with Custom Temperature and Custom Repetition Penalty controls: tick either to override that one prompt's sampling on a slider, or leave it off to use the smart default. The behind-the-scenes helper prompts default to tuned values so their bookkeeping and scene-planning stay consistent โ Stat Updates, Location Change, and Summary run low/near-deterministic, while the Planning step runs at a moderate temperature with the repetition penalty relaxed (which it needs to keep every character in a scene). Creative prompts (narration, choices, โฆ) follow your endpoint's own settings. Like the custom-endpoint fields, a value you dial in is kept (just unused) when you switch the override back off. The editor's Verbatim turns field moved onto this Options tab too.
- Steadier scenes in Planning mode โ the optional pre-turn Planning step (Settings โ Generation โ Thinking โ Planning) was rebuilt as a lightweight scene-continuity planner. It now carries the previous scene forward โ who was present, what they were holding, where they stood โ so the narration doesn't quietly drop a character mid-conversation or swap an established object for a new one, and it names the cast up front. A single call that does the job of the heavier Staged mode's first stages, tuned and verified on both a 12B and a 24B local model.
- Planning mode fills the scene list โ the cast the Planning step names now drives the Entities panel (who's present this turn), the same as Staged mode already did. And a character whose name the player hasn't heard yet is kept out of the narrator's hidden plan โ the planner refers to them by how you currently know them ("the silver-haired woman") until the name is actually spoken in the story, so the plan can't accidentally spoil a reveal.
- The Entities tab is now a live scene list โ in Planning and Staged modes it shows exactly who is physically present this turn (sourced from the planner's cast, so someone merely mentioned โ "my father, King Luke" โ no longer wrongly appears), with their portrait. A character you haven't been introduced to yet shows by how you currently know them ("the hooded woman") and their picture, and their real name appears the instant the narration first speaks it. Off/Inline modes keep the previous behavior (the tab is filled from names found in the narration). Opening a not-yet-named character's detail card is held back until the name is revealed, so the list itself can't spoil it.
-
Portable desktop builds now keep everything in their own folder โ The Windows portable
.exeand the Linux AppImage store all your data โ saves, settings, worlds, and your character library โ in auserdata/folder right beside the app, instead of tucked away in your OS profile. Copy that folder (or the whole portable directory to a USB stick) and your entire setup moves with it. The first time you run the new build, existing data is migrated across automatically. The macOS build installs normally and is unchanged. The version in the corner also now shows the build type โ portable, installed, or dev โ so you can tell at a glance which build you're on (the browser build stays unlabeled).
-
Saves are now organized into per-world folders โ Opening Load Game drops you straight into the current world's folder, listing only its saves โ the common case is now one step. A Back button (under Import) and a
-
๐ ๏ธ Developer tooling
-
Save store re-keyed to a stable id + per-world folders (save-shape change). The IndexedDB
savesstore (keyed byname) is replaced bysaveRecords(keyed by a uuidid), so identical names can coexist across and within worlds; a one-time upgrade migrates existing rows, minting anidand best-effort stampingworldIdby matching each save'sworldNameto an installed world (unmatched saves stay name-only and group by name). New saves carrySaveObject.worldId(fromWorldOverview.id's owningWorld) โ an additive save-file shape change (the exported.jsongainsworldId; the device-localidis stripped on export, re-minted on import). This additive shape change is what 2.0.3 carries โ the version bump lands with it. Grouping/ordering logic is pure + unit-tested inlib/saveOrdering.ts(groupSaves/folderRefFor/mergeOrder); ordering is device-local (a separatesaveOrderstore keyed by__folders__/ folder key), so it never touches exported saves.saveGame/loadGamenow key by id;MenuModalrebuilt around folder/root views with dnd-kit reorder (mirrors the DictionarySelectionModal pattern). Cross-world load is a real world switch:onLoad(id, worldId)โGameViewercallsgetWorldData(worldId)โloadWorldDataโloadGameagainst the target world's locations (previously the save loaded into the current world's shell); an uninstalled world can't be switched to, so it warns and loads in place (current world's rules) โ harmless (the location lookup is guarded; foreign stats/entities degrade to no-ops), so it's left to the player. The load UI is extracted to a sharedLoadGameDialogused by both the in-game menu and the main menu;groupSavestakes an optionalcurrent(omitted in the main menu โ no pinned folder). The main menu's cog became a[Load Game, Settings]hamburger; a cold-load there resolves the save's world (getWorldDataโloadWorldData) and hands the save id toApp(initialSaveId), whichGameViewerrestores on mount instead of starting fresh โ an orphaned save is blocked there (no world to run it in). -
Precall planning wired into participation โ the Planning step now parses its
Scene/Cast/Beatsoutput through the sharedclassifyCast(extracted fromrunStagedPlanningso both paths bucket identically) into defined-entity (loose-match) + ad-hoc (strict-match) candidates that the narration confirms, exactly like the staged director. AndsanitizePlanForRevealrewrites any cast name absent from past narration to its parenthetical alias โ or a neutral fallback when the model gave none โ across the whole plan before it's injected, a code backstop for the prompt's name-spoiler rule that larger models honor unevenly.lib/stagedPlanning.ts. -
Scene list re-sourced from the planner cast (
SceneEntity, save-shape change) โGameState.visibleEntitiesgrows fromstring[]toSceneEntity[]({ name, alias?, revealed }). A newbuildSceneList(lib/stagedPlanning.ts) maps the turn's planner cast to the tab: canonical entity name for the portrait tie, the alias carried through, andrevealedcomputed against the narration (prior turns + this turn so far, so it flips mid-stream); with no planner it falls back tofindEntityNames.runStagedPlanningnow returns its NPCcast;GameViewerdrives the list at stream-start, per-sentence, and finalize (via a ref, since the narration stream runs at component scope). Old saves load fine โ a barestring[]normalizes to{ name, revealed: true }on load (normalizeVisibleEntities), non-destructive, no migration of stored files. New saves write the richer shape. -
Per-prompt sampler resolution (
lib/promptSamplers.ts) โ resolves each request's temperature and repetition penalty: a pinned or custom value goes to every endpoint; an unpinned prompt sends the global slider value on the built-in engine but omits the field on a custom endpoint, so LM Studio / Ollama apply the model's own configured value (see the verified endpoint-temperature behavior). Every pin was set from measured probe sweeps against a 12B + 24B local model (testing/baseline/harness/*-probe.mjs), not guessed:statUpdates0.2,locationChange0.15,summarygreedy 0 (small models echo dialogue verbatim above it), and โ the odd one out โthinking(Planning) at temp 0.4 with repetition penalty 1, because that prompt inverts the usual rule: its greedy default is to drop cast members the action doesn't engage (so it needs entropy to keep the scene), and a repetition penalty on the list-shaped cast roster suppresses completeness and drives invented characters. Overrides persist as onepromptSamplerslocalStorage record. -
Portable profile + build-type indicator. Electron redirects
userDatato<portableRoot>/userdatafor relocatable builds (runtimePORTABLE_EXECUTABLE_DIR/APPIMAGE), moving the whole Chromium profile beside the exe; on first run it copies the persistent stores (IndexedDB, Local/Session Storage โ caches skipped) out of the old default location once. Pure, electron-free logic lives inelectron/portableProfile.cjs(unit-testable). The build type is baked via a Vite__BUILD_TARGET__define fed byFORMAMORPH_BUILD(set per target in the release workflow:portablefor the Windows portable + Linux AppImage,installedfor the mac dmg,webfor the browser build), surfaced asBUILD_TAGinlib/buildInfo.tsand shown in the footer (empty โdev). The portable artifact is renamedFormamorph-${version}-portable.exeso a future NSIS installer (-setup.exe) can't collide. Release workflow's official actions bumped to current majors (checkout 7, setup-node 6, upload-artifact 7, download-artifact 8).
-
Save store re-keyed to a stable id + per-world folders (save-shape change). The IndexedDB
-
๐ค User-facing
- Load Game menu no longer clips or overflows โ Save rows were fixed-height buttons, so a long save name or the "different world" warning spilled out of the row instead of growing it, and a long list of saves ran off the bottom of the dialog with no way to scroll. Rows now expand to fit their contents, the list scrolls when it's taller than the dialog, and the per-save Delete (left) and Download (right) controls sit on opposite ends for less mis-clicking.
- Long names in dictionary and picker lists now clip with "โฆ" instead of overflowing โ A long entry/dictionary name (or entity/theme label) stretched its row past the panel's edge, pushing the duplicate and delete buttons out of reach. Those rows now truncate the name with an ellipsis and keep their controls in view.
- Opening the World Editor from the menu now animates and no longer reloads the whole menu โ It opened as a hard screen swap and, on close, rebuilt the entire main menu (a visible flash plus a redundant re-fetch and profile re-check). It now opens with the same full-screen zoom-from-center animation as Community Creations, and closing it (via its back arrow) just refreshes the world grid, keeping the rest of the menu untouched. The distracting "New world ready" toast on creating a world was also removed.
- New worlds no longer start with a broken thumbnail โ Creating a world seeded a placeholder image from an external host that no longer resolves, so the new world showed a broken image. New worlds now start with no thumbnail (the same clean fallback used everywhere else) until you set one.
- Every World Editor tab now shows a hint when it's empty โ Only Locations did before โ Stats, Entities, Traits, and Dictionaries were just blank, leaving you unsure whether to add something or whether it failed to load. They now all show the same "No โฆ yet โ use the + button to add one." hint (and a "no matches" note when a search comes up empty).
- Prompt editor's "Thinking" tab renamed to "Planning" โ to match the Thinking-mode option it edits (Off / Inline / Planning / Staged), which was already labeled Planning.
- Re-generate no longer flashes the old narration โ Pressing Re-generate briefly animated the entire previous narration (every paragraph at once) before clearing and streaming the new text. The reveal now only animates while the new narration is actually streaming โ during the setup/thinking phase it shows the settled text โ so the flash is gone.
-
Smoother narration reveal โ a cluster of long-standing glitches in the animated text reveal traced to several compounding causes, now all fixed. The core one: the sentence detector didn't understand markdown, so a sentence ending inside emphasis (
*screech.*), a paragraph ending on an em-dash, or a bare**Power: 88%**line never counted as a boundary โ whole paragraphs fused into one giant "sentence" that starved the reveal (stutter), then animated across the paragraph break in one burst. Sentence boundaries now recognize closing emphasis markers, and a blank line always ends a sentence (this also fixes streamed text-to-speech chunking at the same spots). On top of that: the reveal now lets each paragraph finish landing before the next begins, no longer has a speed floor faster than slow models can generate (the old cap forced stuttering on any model under ~11 words/s), and uses a tighter per-word fade so a sentence finishes fading within its own span instead of being cut off by the next one. Most importantly, the reveal no longer guesses at a tokens-per-second rate (which a server's initial burst of text could throw off for the whole turn) โ it now measures the real time between sentences as they arrive and paces to that, keeping a small buffer so it never runs dry (stutter) or lags far behind (text still revealing long after the choices appear). It also remembers the measured speed between turns, so after the first turn it already knows roughly how fast your model is and starts at the right pace instead of ramping up from a cautious default.
-
๐ ๏ธ Developer tooling
-
World Editor is now an in-place modal, not a top-level view. It moved from an
AppcurrentViewbranch into a MainMenu-hosted full-screen<Dialog>(reusing Community Creations' classes) so the menu stays mounted and animates; a newrefreshWorldscallback (mirroringrefreshDictionaries/refreshEntities) refreshes just the world grid on close instead of remounting the menu. WorldEditor gained abackButtonprop that decouples the header back arrow fromembedded(GameViewer's embedded popup keeps its X; the menu modal shows the back arrow with no X, Esc/overlay blocked so the guarded back arrow is the sole exit). In the DEV dev-router,worldEditormoved fromDEV_VIEWStoDEV_MODALS(reached via#dev?modal=worldEditor), and the drift-guard test was updated to match. -
List rows clip instead of overflowing (root cause + shared empty-state). Two causes: a
flex-grow/flex-1truncating child needsmin-w-0to shrink, and Radix's ScrollArea wraps its viewport content in an inlinedisplay:tablediv that shrink-wraps to content width โ overridden with[&>div]:!blockon the shadcnScrollAreaviewport (safe: no horizontal ScrollArea exists anywhere). Dictionary row controls gotshrink-0so the name (not the buttons) absorbs the truncation. The per-tab "nothing here yet" hint is unified into one sharedEmptyListHintcomponent reused by every list/tree tab.
-
World Editor is now an in-place modal, not a top-level view. It moved from an
-
โ๏ธ Backend
-
Imported multi-turn saves from v1.2 no longer corrupt their page history. Converting an old (v1.2) save duplicated the opening page and dropped the current page from the turn history, so on a 3-turn save Re-generate and Rollback landed one turn too early (a page effectively vanished). Old saves stored their history as the prior pages only, with the current turn kept separately; the converter now appends the current turn as the final page (instead of a stale re-alignment that assumed the opposite) and stamps the newer
discoveredEntitiesfield onto every converted snapshot. Single-turn saves were unaffected and stay so. Already-imported broken saves aren't auto-repaired โ re-import the original v1.2 file to fix them. -
Phantom "empty scene" cast bullets no longer become participants.
isEmptyCastNamenow also recognizes multi-word declarations like "no other characters present" / "no one else" (not only single-word sentinels such as "none"), so a planner or director that writes the empty scene as a cast bullet doesn't spawn a bogus participant. Affects both Planning and Staged. -
Cast names with a role/alias parenthetical now classify correctly. Small models habitually gloss a cast line as
Bram (ferryman)orMaela (the hooded woman); the cast parser kept the whole"Bram (ferryman"as the name, so it no longer matched the "Bram" entity and misfiled as an invented character (losing the entity tie).parseDirectorCastnow splits the name from the parenthetical (shared with the reveal sanitizer viasplitNameAlias) and captures the alias on the cast member. Confirmed on a live 12B via a new real-parser probe (harness/planning-parser-probe.test.ts): misclassification dropped from 5/9 cases to 0. Affects Planning and Staged participation. - Silent background requests no longer overlap. With Memory Summaries and Character Diaries both on, a turn-summary request could fire while a diary or character-discovery request was already running, doubling up on a single-GPU local endpoint; the three background jobs now strictly take turns.
- Rollback snapshots no longer change after the fact. Repeating log entries incremented a counter on an entry object shared with already-saved game-state snapshots, retroactively editing them; the increment now produces a fresh entry.
-
Desktop IPC surface hardened โ the desktop app's internal fetch bridge now accepts only
http/httpsURLs (it could previously be asked to readfile:paths), and an unused bridge that could load a model from an arbitrary absolute path was removed (models load by filename from the models folder only). - Duplication pass across the codebase โ extracted shared helpers for the five copy-pasted blob-download flows, the three location-list serializers, the image providers' HTTP plumbing, the main menu's three drag-reorder handlers, the editor managers' draft-editing pattern, the settings-form row layouts, and the Location/Trait drag-tree scaffold (~480 duplicated lines removed, no behavior change).
-
Imported multi-turn saves from v1.2 no longer corrupt their page history. Converting an old (v1.2) save duplicated the opening page and dropped the current page from the turn history, so on a 3-turn save Re-generate and Rollback landed one turn too early (a page effectively vanished). Old saves stored their history as the prior pages only, with the current turn kept separately; the converter now appends the current turn as the final page (instead of a stale re-alignment that assumed the opposite) and stamps the newer
โ 2.0.2 โ Released โ old-save rollback/import fixes + summary-prompt hardening (click to expand)
-
๐ค User-facing
- The opening turn of an old (1.2) save can now be rolled back and re-generated โ The 2.0.1 import fix handled later turns but left the very first turn dead โ rolling back to it did nothing and its Re-generate button was inert. The opening turn now restores and re-rolls. (Its exact pre-turn snapshot was never stored, so a rolled-back opening lands one turn later, and re-generating it rebuilds a fresh start.) This also fixed re-generating the opening on any loaded save, not just 1.2 ones.
-
Importing an old (1.2) save now upgrades it properly โ Bringing in a 1.2 save file used to stamp it as current without actually converting it โ so its traits reached the AI as bare names (descriptions dropped) and its body-shape stats (belly / weight / bust) stopped driving the character model. Old saves are now migrated the same way whether you import a file or load one already in your library, and the two paths run identical conversion so they can't drift.
โ ๏ธ If you imported a 1.2 save before this fix, re-import the original file to upgrade it โ a save that was already brought in stays in its old state. - More consistent turn summaries โ the memory-digest "summary" prompt (which condenses each turn for long-game memory) was hardened so its retellings come out uniformly short (one or two sentences, single line), lead with what you did, and never quote dialogue verbatim or invent a character name the narration didn't give. Verified on both a small (12B) and large (24B) local model.
-
๐ ๏ธ Developer tooling
- The Sedge Landing baseline harness gained a
summaryprofile (exercises the digest prompt) and per-profilesettleMs, so a profile with async drainers can wait longer without slowing the others.
- The Sedge Landing baseline harness gained a
โ 2.0.1 โ Released โ local LLM engine, AI image generation, dictionaries & character cards, themes, and more (click to expand)
-
๐ค User-facing
- Run a model locally โ no endpoint needed (desktop) โ the desktop app can now run an LLM entirely on your own machine, so no LM Studio / Ollama / cloud API is required. A bundled engine (node-llama-cpp) loads a GGUF model and serves it on a local OpenAI-compatible endpoint the app targets by default; flip Use My Own Endpoint on to point at a custom/cloud API instead. A model manager (Settings โ Endpoint โ Manage modelsโฆ) has two tabs: Recommended โ a curated catalog of community roleplay models grouped by the VRAM tier they fit (โค4 GB ยท โค8 GB ยท โค16 GB ยท No Limit, auto-selected from your detected GPU), each downloadable straight from Hugging Face with a live progress bar and pause / resume (a paused download survives closing the app); and Installed โ every GGUF in your models folder, reorderable, with one-click Load / Unload and a confirm-first Delete. Runtime controls sit in the Endpoint tab behind a Simple / Advanced toggle: Context Size, GPU (a simple on/off, or an exact layer count in Advanced), Flash Attention, and sampling โ Temperature, Max Output Tokens, Top-p, Top-K, Min-P, Repetition Penalty โ beside a live GPU memory readout and engine status. Models are saved next to the app (portable โ not buried in AppData), and it's cross-platform (Windows / macOS / Linux).
-
AI image generation โ generate images for entities, locations, and world thumbnails right in the World Editor. A Generate with AI button drafts an SD-style prompt from the subject's description (via your text model), lets you tweak it, and renders through your configured image provider. Providers: a local A1111 / Forge server, and โ in the desktop app โ an OpenAI-compatible cloud image API (proxied through Electron to sidestep browser CORS). Prompts are LLM-refined into stripped danbooru / booru tags via an editable Tag Prompt (with a
<SUBJECT>chip, Settings โ Image Gen), plus a positive-prompt prefix for quality/style tags. Characters render at portrait dimensions, locations & thumbnails at landscape. Settings live in named, editable endpoint presets (a "Default" plus your own), switchable in Settings and from the generate popup. For A1111: an ADetailer face/hand-fix toggle and a live progress bar + in-progress preview while it renders.
-
๐ ๏ธ Developer tooling
-
Local LLM engine โ
electron/llmEngine.cjsdynamic-imports node-llama-cpp (ESM) from the CommonJS main process, loads a GGUF, and serves a minimal OpenAI-compatible chat-completions endpoint (streaming + non-streaming) on127.0.0.1. It reconstructs the model's raw output โ re-wrapping reasoning<think>segments viaonResponseChunkโ so structured aux requests (choices / stats / location) don't come back empty on models that segment their scratchpad. The renderer drives it over an IPC bridge (window.formamorphDesktop.llm): start/stop/load, status pushes, installed-model + partial-download listing, engine options (context / GPU layers / flash attention) with reload-on-change, and a Hugging-Face-only downloader (electron/modelDownload.cjs) using.parttemp files, HTTP Range resume, and cancel. The catalog + VRAM tiers live insrc/lib/localModels.ts(filenames/sizes verified against the HF API; reasoning models deliberately excluded for now). All of it is gated onisDesktop()and tree-shaken from the web build. -
Linux & macOS desktop builds โ electron-builder now targets AppImage (Linux) and dmg (macOS) beside the Windows portable exe (
desktop:build:linux/desktop:build:mac). A release workflow builds all three from onev*tag (a Windows / Linux / macOS matrix) and attaches them to the GitHub Release. -
Hosted browser build on GitHub Pages โ deploys on every push to
main, with the community-server features (Discover, login, publish) disabled in that build (VITE_ENABLE_COMMUNITY=false) so nothing unmoderated is surfaced by default. Play it at jakejamesdev.github.io/formamorph. -
Web build release artifact โ tagging a
v*release also produces aformamorph-web-<tag>.zip(a full, community-enabled browser build) attached to the GitHub Release, ready to drop onto itch.io or your own host.
-
Local LLM engine โ
-
๐ ๏ธ Developer tooling
- Tagged desktop builds no longer fail on the Linux/macOS runners โ electron-builder no longer tries to self-publish (missing
GH_TOKEN); the workflow's own publish step handles the upload.
- Tagged desktop builds no longer fail on the Linux/macOS runners โ electron-builder no longer tries to self-publish (missing
-
๐ค User-facing
- Import tags from an image's metadata โ upload a PNG that embeds its generation prompt (A1111/Forge, ComfyUI, InvokeAI, NovelAI, and other JSON-metadata tools) to an entity or location, and Formamorph offers to use that positive prompt as the Image Tags โ no retyping. Declined images and plain photos are untouched.
- Authored image tags โ entities and locations now have an Image Tags field (under the image, above Generate with AI) that persists your booru tags. A shared โจ toolbar with Undo / Redo writes tags from the description (and now also generates the AI-Facing Summary the same way). The Generate with AI popup edits those same tags two-way โ tweak, generate, adjust, regenerate โ and no longer auto-overwrites what you authored.
- Danbooru tag autocomplete โ the Image Tags fields, the Generate with AI prompt, and the world Tags editor suggest real Danbooru tags as you type, ranked by how commonly they're used, so you complete a tag with a keystroke instead of guessing spellings. Start typing to filter (an empty field shows nothing); the list loads on demand and works offline. The world Tags editor previously suggested tags scraped from the community browser (with a Refresh button) โ it now uses this standardized list, so that button is gone.
- Editable tag chips โ double-click a chip in the world Tags editor or a dictionary entry's Keywords to fix it in place (Enter saves, Esc cancels, clearing it removes the chip) instead of deleting and retyping. Editing a world tag still shows the Danbooru autocomplete; chips keep their position, and drag-to-reorder is unchanged.
-
ComfyUI image provider โ generate images through a local ComfyUI server alongside the existing A1111/Forge and cloud options. Ships a working SDXL txt2img workflow that's fully editable per preset: paste your own Save (API Format) graph and mark inject points with tokens (
%prompt%,%negative%,%ckpt%,%width%,%height%,%steps%,%cfg%,%seed%,%sampler%). Live progress bar + preview frames stream over ComfyUI's WebSocket, matching the A1111 experience. Launch ComfyUI with--enable-cors-headerso the browser can reach it (default port 8188). - World README โ authors can add a markdown Readme (in the World Editor, below System Prompt Addition, using the same editor as the World Description) that greets the player in a popup when they enter the world. Dismiss it with the X, click-outside, or Esc. A per-world Show Readme toggle lives in the main-menu world popup and is mirrored by a Don't Show This Again checkbox in the popup itself โ the two stay in sync (one setting, on by default).
- Consent-based image optimization โ before an oversized image is re-encoded (on import, on generate "Use image", or via the editor's Optimize Images), a prompt offers Optimize (convert to lossless WebP at the same resolution โ no visible quality loss) or Downscale (shrink to fit), with a clear Keep original to do nothing. Already-WebP images skip the redundant Optimize option.
- The world-browser Hidden popover now uses editable tag / author autocomplete boxes, matching the search filters.
-
Dictionaries are books โ the World Editor's Dictionary tab now groups lorebook entries into named dictionaries ("books") you add, reorder, collapse, and toggle on/off as a whole, instead of one flat pile. Each book keeps its own Background and Foreground lists, and entries drag between zones or across books; book order sets injection order, and disabling a book mutes all its entries at once. New worlds start with a Default book. A single book exports to its own
.json(Download <name> on a selected dictionary) and imports back as a new book (Import Dictionary) โ handy for sharing lore between worlds; importing a full world or save file is rejected with a clear message. -
Dictionary library + lorebook import โ the main menu's top-left switcher gains a Dictionaries tab: a library of standalone dictionaries you Import and manage as cards (persisted locally), separate from any world. Import isn't limited to Formamorph files โ it also reads and converts open lorebooks: SillyTavern World Info exports and Character Card V2/V3
character_bookdata (keywords, secondary keywords, content, enabled/constant, case-sensitivity, regex, placement, scan depth). Cards with no image (and worlds without a thumbnail) now show a neutral grey placeholder instead of a broken image. Click a dictionary card to open it in a full editor โ the same Background/Foreground tree, entry editor, and drag-to-reorder as the World Editor โ rename it, edit entries, Download it, and Save back to the library (with an unsaved-changes prompt). Dictionaries also gain a Description (author note, not sent to the AI), and entries gain Whole words matching (keyword boundaries, so "art" no longer fires on "cart") and richer secondary-keyword logic โ require any or all secondaries, and an Exclude mode so an entry fires only when certain words are absent. In the World Editor, Add Dictionary pulls copies of one or more library dictionaries into the current world (multi-select), each as an independent copy. - Theme + theme color โ Settings โ Presentation has a Theme control (Light / Dark / System, as segmented tab buttons; System is the new default and follows your OS) and a Theme Color dropdown below it that recolors the whole app. Eight presets: Graphite (the default โ a soft monochrome), Purple, Blue, Rose, Bubble Gum, Forest, Monochrome (crisp greyscale with a true-black dark mode), and High Contrast (an accessibility theme โ maximal text contrast, everything outlined, yellow accents in dark). Each is a full hand-tuned palette for both light and dark, so switching restyles every surface โ backgrounds, cards, borders, buttons, focus rings โ not just a button tint, while keeping text legible. Status colors (error / success / warning) stay constant across all themes. On a fresh install, if your OS requests higher contrast and you're on System mode, it starts on High Contrast automatically โ as the initial default only; whatever you pick afterward always sticks.
- Location background controls โ Settings โ Presentation adds a Location Background toggle (on by default) that shows or hides the current location's image behind the game; turn it off for a clean, blank themed background. When it's on, a Background Fade slider (0โ100%) fades the location image toward the background color for readability โ slide it up to calm a busy scene toward the plain background. Pressing the hide-UI (eye) button drops the fade too, so you always see the untouched image underneath.
- AI Language is now a combobox โ the language setting (Settings โ Presentation, and the in-game side panel โ renamed AI Language) suggests common languages the moment you focus it (no typing needed), while still accepting any free text. A short note explains what it does: it sets the language the AI writes narration and choices in โ and you can type your own value, or even a style like "formal English" or "pirate speak", instead of picking from the list.
- Back button in the enter-world flow โ every step of the start-a-world flow (Traits โ Location โ Characters โ Dictionaries โ Avatar) now has a Back button, always just to the left of the next/confirm button, that returns to the previous step. On the very first step it's shown but faded/disabled. On the Traits step it also pages back through trait sections before it reaches the flow's start.
- Bring characters into a world when you start it โ a new step in the enter-world flow (after location, before dictionaries) lists your library characters with their portraits; check the ones you want and they're placed into your starting location for that playthrough. Only appears when you have saved characters. The chosen characters are copied in with fresh ids and live entirely in the save โ they're never written into the authored world.
- Choose dictionaries when you start a world โ a new step in the enter-world flow (after location, before avatar customization) lists the world's own dictionaries on top and your downloaded library below, each with a drag handle, an enable/disable checkbox, its description, and an enabled/total entry count. Reorder and toggle any of them โ world-authored included โ to set exactly which lore applies and in what order for this playthrough; enabling a library dictionary pulls in a fresh copy. Your choice is saved with the game, so reloading keeps it. The step is skipped when there's nothing to choose (a single-book world with an empty library).
-
Character cards โ share a character as an image โ a single character now exports to a shareable WebP whose picture is the character's portrait and whose text (name, descriptions, summary, type, image tags) rides along invisibly inside the image, the way other games do character cards. In the World Editor, Download <name> on a selected entity saves the card; drop that
.webpback in via the new main-menu Entities tab and the character returns with its portrait and every field intact (the image itself becomes the portrait โ no duplicate stored). The Entities tab is a local library of characters you New, Import, edit in a full editor, reorder, and delete โ separate from any world; Add Character in the World Editor pulls copies of one or more of them into the world (multi-select), each an independent copy. A character with no picture still exports, using a generated placeholder. Importing a world, save, dictionary, or a plain non-card image shows a clear message. -
Import SillyTavern characters โ the Entities Import also reads SillyTavern / Character-Card PNGs (the
ccv3/charaembedded-card format). It brings across the picture and the name plus description + personality + scenario (folded into the AI-facing description, with{{char}}โ the character's name and{{user}}โ "the player"); the chat-only fields (greeting, example dialogue, jailbreak prompts) are skipped as they have no place in a world character. If the card carries its own lorebook, you're asked whether to add it to your dictionary library too. - Sign in & publish from the desktop app โ the desktop build no longer blocks logging into the community server or publishing a world; that guard is gone now that v2 is the standard.
- Selectable app font โ Settings โ Presentation gains a Font picker that restyles the whole app's typeface.
- Live theme preview โ a Preview popup (Settings โ Presentation) shows every theme color token with a label and color picker, laid out across the real widgets the app uses โ checkboxes, tabs, draggable tags, a multiselect, a reorderable list, circle buttons, a sample card โ so you can eyeball a palette on actual UI before committing. It's a viewer; nothing is saved.
- Cards stand out โ each theme now gives the card surface its own shade distinct from the page background, so world / entity / dictionary cards read as raised panels instead of blending in. Community & main-menu tags also recolor with the theme (they were stuck blue).
- Animated stat changes โ in-game stat bars now show a change instead of only printing a number: a gain grows a green band and a loss a red band spanning the previous โ current value, the ยฑdelta text fades in and out, and the colors drain away at the start of the next turn.
- "Move Automatically" โ the auto-move setting (Settings โ Generation) is renamed from Location Moves to Move Automatically, and now resolves the move up front so the narration is written already in the new place rather than describing a move that hasn't happened yet.
- Narration reveal animation โ the streamed-text reveal is a configurable animation (fade / scale / blur / slide, its pace tracking how fast the model generates), tuned in Settings โ Presentation with a live demo.
-
๐ ๏ธ Developer tooling
-
Env-seeded image defaults โ the whole Image Gen setup (provider, endpoint, dimensions, ADetailer, and multiple named presets via
VITE_DEFAULT_IMAGE_PRESETS) can be seeded from.env.local. -
Unified app icon โ one source icon (
public/icon.png) is now both the browser-tab favicon and the desktop app icon (electron-builder converts it per-platform), replacing the default Vite / Electron icons. -
VRAM readout built into the desktop app โ the Hardware tab's live GPU readout no longer needs a separately-run
npm run vram-helperin the packaged desktop build: the Electron main process runsnvidia-smiitself and feeds the renderer over an IPC bridge (window.formamorphDesktop.vramStats). The nvidia-smi logic was factored into a sharedelectron/vramCollect.cjsused by both the desktop main process and the standalone dev helper.useVramStatsprefers IPC when in the desktop build, else polls the helper URL. The Hardware tab is now hidden in the production web build (where no VRAM source exists) and shown only innpm run devor the desktop app; on desktop it drops the "run the helper" instructions for a built-in note. - The main-menu GitHub button and the README / wiki now link to this fork's repo and the live web build.
-
API doc comments + tooling โ the high-value TypeScript surface (domain types, contexts, services, and the core
libmodules) now carries prose TSDoc comments, so VS Code shows a plain-English summary on hover next to the types. The lint gate validates comment syntax (eslint-plugin-tsdoc), andnpm run docsgenerates a browsable TypeDoc API reference into the gitignoreddocs-api/. -
World shape:
dictionaryโdictionariesโ the flatdictionary: DictionaryEntry[]becomes an ordereddictionaries: Dictionary[](books of entries). An idempotent load-time migration folds any legacy world (including the bundled defaults) into one Default book, positions preserved โ no world is re-persisted until the user saves. Injection stays entry-based via aflattenEnabledBookEntriesbridge, so a single-book world's prompt output is unchanged. New standalone dictionary file format carries aformamorphKind: 'dictionary'discriminator; world and save exports now also carry an optionalformamorphKind: 'world'/'save'label (additive โ old files without it still import). Foreign lorebook parsing (lorebookImport.ts) is grounded in the MIT Character Card V3 spec (credited inTHIRD-PARTY-NOTICES.md). The dictionary editing widgets were decoupled from the world store into a scopedDictionaryStorecontext (useDictionaryStoreState), so the World Editor binds them to the current world's books while the library editor binds them to an isolated single book โ the same widgets, two hosts, no shared-state leakage.Dictionarygains an optionaldescription;DictionaryEntrygains optionalmatchWholeWords,secondaryAll, andsecondaryExclude(all additive โ absent = prior behavior). The importer now maps STmatchWholeWords,selectiveLogic(AND-ANY/ALL, NOT-ANY/ALL), and lorebookdescription. -
Save shape: optional per-playthrough
dictionariesโ the save envelope (SaveObject) gains an optionaldictionaries: Dictionary[]written by the new world-entry dictionary step; older saves lack it and fall back to the world's current books (never clobbered). The per-run set is owned byGameplayContext(runtimeDictionaries+ the flattenedruntimeDictionaryfed to injection);saveGame/loadGamestamp and restore it internally, soGameDataContextholds only the authored books. Pure selection helpers live inlib/dictionarySelection.ts(shouldShowDictionaryStep,buildInitialSelection,finalizeSelection). Additive โ old saves import unchanged. -
New file format: character cards (
.webp) โ a single entity slices out to a shareable WebP with its text fields embedded in the image, mirroring the dictionary slice.lib/entityCard.tsis a pure RIFF/WebP metadata codec (embedEntityCard/readEntityCard): it promotes a simple VP8/VP8L file to extended VP8X and writes the JSON into anXMPchunk (idempotent โ re-embedding replaces, never appends), modeled on the existing PNG-chunk reader insdMetadata.ts.lib/entityFile.tsadds theformamorphKind: 'entity'discriminator,buildEntityCardData(dropsimage/model/sound),parseEntityCardData(fresh id, rejects world/save/dictionary payloads), and theexportEntityCard/importEntityCardorchestrators โ export omits the portrait from the JSON and import re-sources it from the card's own pixels, so the image is never stored twice. A local library mirrors the dictionary one:EntityStorageService(IndexedDBentitiesDB), a main-menu Entities grid (reusingSortableWorldCard),EntityEditorModal, andAddEntityModal.EntityManager's fields were extracted into a sharedEntityFieldsbody (locations picker optional) so the World Editor and the library editor reuse it. NewEntityMetadatatype. This is a new shareable artifact type; it does not change the world or save.jsonshape โ no version bump. -
Entry-step character placement (runtime-only) โ the new enter-world character step is gated by
shouldShowCharacterStep(lib/characterSelection.ts) and picked inCharacterSelectionModal. The chosen library characters thread MainMenu โAppโGameVieweras aninitialCharactersprop (mirroringinitialDictionaries), and the init effect seeds them intoGameplayContext.discoveredEntitiesanchored to the resolved starting location โ reusing the existing runtime-character path (allEntities+mergeDiscoveredIntoLocation). So they show as present, persist in the save, and never touchGameData(the authored-world-immutable invariant). Fresh ids on selection;loadGameoverrides on load. -
SillyTavern character PNG import โ
lib/tavernCard.tsreads a Character-Card PNG (base64 JSON in theccv3/charatext chunk, V1 flat / V2-V3dataenvelope), reusing the now-exportedreadPngTextChunksfromsdMetadata.ts. It maps only name + description/personality/scenario โaiDescription(macro substitution{{char}}/{{user}}), drops the chat-runtime fields, and hands anycharacter_bookto the existingconvertLorebook.entityFile.importCharacterFile(file)is the unified import entry โ WebP card or ST PNG โ returning{ entity, book }; the main menu offers the book via a confirm dialog. The PNG's pixels become the portrait (re-encoded to WebP). -
Authored world is immutable during play โ the running session no longer writes runtime state back into the authored world (
GameDataContext). The stat-change handler updates onlyplayerStats(dropping the oldupdateStatwrite-back), and the per-run dictionary set lives inGameplayContext, threaded in from world entry as aGameViewerprop (mirroringinitialTraits/initialLocationId). The mid-game Edit World tool edits the authored world directly; it persists only on Save World, and no longer opens pre-marked dirty. The now-unused flatteneddictionaryderivation was removed fromGameDataContext.
-
Env-seeded image defaults โ the whole Image Gen setup (provider, endpoint, dimensions, ADetailer, and multiple named presets via
-
โ๏ธ Backend
-
Design-token color pass โ the app's colors were migrated off hardcoded Tailwind palette classes (grays, plus inline reds / greens / ambers / blues for status) and stray hex onto CSS design tokens in
index.css, so every surface now flips correctly between light/dark and swaps wholesale with the selected theme color with no per-component overrides. Preset themes ride on this: adata-themeattribute on<html>swaps a full token set, so a theme is authored purely as.light[data-theme="โฆ"]/.dark[data-theme="โฆ"]blocks inindex.cssโ no per-component work. Eight ship (Blue, Purple, Rose, Bubble Gum, Forest, Graphite, Monochrome, High Contrast), each built by one recipe: pick a hue, preserve each token's lightness (so contrast holds), scale saturation by role, and leave the semantic status/chart tokens constant. The bare base and the first-run default are decoupled โbluelives in:rootand applies with no attribute, whilegraphiteis what a fresh install starts on. Monochrome and High Contrast communicate depth by an elevation-by-lightness surface ramp (true-black canvas โ progressively lighter panels) rather than shadows, which don't read on black; High Contrast targets WCAG AAA (near-maximal text contrast, borders on every element, a yellow accent in dark / monochrome in light) and auto-selects on first run when the OS setsprefers-contrast: more(System mode only, and default-only โ it never overrides a chosen theme). To make that outline-heavy theme complete, the button variants now reserve a token-colored border (border border-transparentin the base,border-borderon secondary/ghost) โ invisible-to-faint in the color themes, maximal in High Contrast, with no layout shift. This whole system replaces the earlier single-color accent picker (itslib/color.tshexโHSL helper is removed). Added semantic--success/--warning/--infotokens (styled to match the existing--destructive), an--overlayscrim token for image and modal backdrops, and a fill-vs-ink split for destructive red:--destructiveis tuned bright for text/icons/indicator bars (readable as its own color on a dark surface) while--destructive-fillstays deep for solid button/badge fills (where white text sits on top) โ one token per role so red reads right in both. react-toastify's per-type accent variables now derive from these tokens too. Duplicated widgets were factored into a sharedWorldCardShell(main-menu card frame) andDropzone(dashed upload frame) so a shared color changes in one place. Decorative gradient buttons, scrim contrast text, and runtime/data-driven colors (entity chips, VRM appearance defaults, the categorical highlight palette) are intentionally left literal.
-
Design-token color pass โ the app's colors were migrated off hardcoded Tailwind palette classes (grays, plus inline reds / greens / ambers / blues for status) and stray hex onto CSS design tokens in
-
๐ค User-facing
- New World / Entity / Dictionary no longer leave a blank behind โ Clicking New used to immediately save an empty entry, so backing out of the editor without saving left a stray "New World"/"New Character"/"New Dictionary" in your library. Now New opens the editor on an in-memory draft and nothing is stored until you Save โ cancel and it's gone.
- Gameplay no longer edits your world โ Playing a world used to quietly drift its authored stat values (and, briefly, its dictionary order/toggles) into the saved world โ so opening Edit World mid-run showed changed values and a "dirty" state you hadn't touched, and saving there baked playthrough state into the world. Now the world changes only when you edit it in the World Editor and Save; per-run state stays with the save file.
- Re-generate & rollback work on imported v1.2 saves โ Old (1.2) saves stored their per-turn history one slot short, so after loading one, rolling back or re-generating landed on the wrong turn. Loading a 1.2 save now realigns that history. (The very first turn of a 1.2 save still can't be rolled back โ its snapshot was never stored โ but every later turn works.)
- The world-browser autocomplete dropdowns no longer close when you click their scrollbar, and the Hidden popover's dropdown now scrolls with the mouse wheel.
- Image Gen polish: the booru tag prompt no longer emits PascalCase / underscored tags the model can't use; the Settings โ System Prompts tab is no longer pushed out of place by the Image Gen tab; and generated / imported images are no longer silently optimized without asking first.
- Toasts follow the theme โ pop-up notifications were hardcoded to the dark theme, so they stayed dark-on-light in the new Light / System modes; they now track the resolved light/dark theme, and their success / error / warning accents come from the app's color tokens.
- Streaming narration reveal fixes โ the character-by-character reveal no longer re-animates every paragraph at the end of a response, no longer stalls at the start, and its pacing now tracks the true generation rate (bugs that surfaced with reasoning models and the local engine). A stray "0" could also flash where a stat change of exactly zero was rendered โ zero-changes now show nothing at all.
โ 2.0.0 โ Released โ full TypeScript rebuild + feature parity (click to expand)
Everything below is relative to the point this fork diverged from upstream.
-
๐ค User-facing
-
Standalone Windows app โ run Formamorph as a desktop
.exe(Electron) with no browser needed. It bundles its own engine, so WebGPU TTS, 3D rendering, and saved-world storage all work the same as in-browser. -
Custom 3D avatars โ bring your own
.vrm, or set a per-world custom player model. - In-app media player โ a fully themed audio widget for TTS narration, with auto-generated TTS when the model is loaded and a one-click unload of the TTS model to free VRAM.
- Redesigned world browser ("Discover") โ card-grid layout, tag & author chip search/filtering, and world-preview image zoom.
- AI output controls โ automatic paragraph limits and length-based truncation, an optional "thinking / planning" step before the reply, and a smoothed character-by-character reveal of streamed text.
- Re-generate & rollback โ page back through the turn history to re-generate the latest AI response, or roll back to an earlier turn (discarding the turns after it). The per-turn AI-context viewer flags the regenerated and rolled-back turns.
- Markdown narration โ the AI can format story text with bold/italic, bullet & numbered lists, tables (e.g. inventory), headings, blockquotes, and code; it renders live as the text streams in.
- Token-based Context Window, auto-detected from your endpoint (LM Studio, OpenRouter, โฆ) with a Detect button and manual override โ conversation history now fills the model's real context instead of a fixed character cap, and an over-limit warning flags a value set above the detected maximum.
- Portrait / mobile layout โ a single-panel responsive mode alongside the three-panel desktop view.
- Lore Dictionary โ a keyword-triggered world-info editor; matching keywords inject lore into the AI prompt.
- Full per-turn AI context viewer โ see exactly what's sent to the AI on each turn, plus the raw response before sanitation โ useful both for debugging and for authoring/tuning worlds. Each request is its own collapsible section with the prompt and raw output nested inside, and Collapse/Expand all folds every level at once.
- Stat-driven body sliders โ in the world editor, bind any stat to one or more of the model's body morph sliders; the stat's value (minโmax) drives the morph live in-game. Legacy weight/breasts/stomach stats are auto-bound on import, so existing worlds keep working.
-
Prompt variables are now chips โ the AI prompt editors (Settings โ System Prompts) no longer use raw
<โฆ>placeholder text. A toolbar inserts each variable as a color-coded chip you can drag to reposition, remove with ร, or use more than once. Clicking a chip opens its options: the Location chip switches between Full / Summary / List (the list of all location names, usable in any prompt โ no separate chip needed), and Entities switches between Full / Summary. During a game, an Edit / Preview toggle shows each chip swapped for its live value โ color-matched to its chip โ so you can see the real assembled prompt. - Entities are their own prompt section โ the characters and things at a location are no longer dumped inside the location block (which made the AI treat the whole cast as all-present). They're now a separate Entities chip โ a roster of who or what could appear โ so the model is less likely to crowd every NPC into the scene.
-
Standalone Windows app โ run Formamorph as a desktop
-
๐ ๏ธ Developer tooling
-
Automated test suite built from scratch (Vitest + Testing Library) covering the new libraries, services, and components, plus a coverage script (
npm run coverage). - GitHub Actions CI โ typecheck, lint, and tests run on every push and pull request.
-
Optional VRAM monitor โ a local helper (
npm run vram-helper) and in-app readout that warns before loading TTS would exhaust GPU memory. -
Desktop packaging โ a thin Electron shell (
electron/) plus electron-builder config;npm run desktop:buildproduces a portable Windows exe from the same web build. - Project scaffolding:
.env.example,.nvmrc, and a rewritten README. - This wiki, published automatically from
docs/by a GitHub Action.
-
Automated test suite built from scratch (Vitest + Testing Library) covering the new libraries, services, and components, plus a coverage script (
-
โ๏ธ Backend
-
Full JavaScript โ TypeScript conversion of the entire codebase (10 phases), finishing on strict mode with shared domain types under
src/types. -
World & save versioning โ an
APP_VERSIONderived frompackage.json, idempotent import migration (migrateWorld) that upgrades legacy worlds at every import boundary, presence-based save-envelope detection, and version stamps on saves, exports, and the bundled worlds. (See the World Format versioning notes.) - Heavy save conversion and serialization moved to web workers so the UI never freezes on large files.
- Reusable utilities extracted: world import/catalog, output length, AI-response parsing, tag/dictionary/highlight helpers, and a thumbnail cache.
-
Full JavaScript โ TypeScript conversion of the entire codebase (10 phases), finishing on strict mode with shared domain types under
-
๐ค User-facing
- The fullscreen menu popup, replaced by the redesigned world browser.
- The previous bundled example worlds (
slime,sugarscape,veilwood), swapped for the current set.
-
๐ ๏ธ Developer tooling
- In-repo planning notes and server stubs under
src/docs/(publish-feature plan, server README, tags-implementation notes, server-worlds stub).
- In-repo planning notes and server stubs under
-
โ๏ธ Backend
- All legacy
.jsx/.jssources, superseded by.tsx/.tsequivalents (views, contexts, workers, prompts, db & utils, UI primitives). -
jsconfig.json(replaced bytsconfig.json) and a strayIdle.fbxanimation file.
- All legacy
-
๐ค User-facing
- Custom VRM models exported from v1.2 were silently stripped on import โ now migrated and preserved.
- Body-morph sliders now resize clothing meshes instead of clipping through them.
- VRM outline color rendering issue.
- AI response parsing errors that could break choices and stat updates.
- The Settings panel no longer jumps vertically when its contents change.
- Unsaved world edits are detected on exit, with a confirmation prompt to prevent data loss.
-
โ๏ธ Backend
- Server-Sent Events streaming now handles chunk boundaries correctly โ no dropped or garbled tokens mid-stream.
- Removed side effects from the
setPlayerStatsstate updater. - The recent-stat-change highlight timer is cleared properly, and streaming guards against a missing
response.body.
-
๐ค User-facing
- Markdown formatting โ toggle (Settings โ Presentation, default on) โ when on, the narration prompt asks the AI to format with bold/italics, lists, and tables (e.g. inventory) using a floor-based rule (one bold + at least one italic per turn) that small models follow reliably; off keeps plain prose. (Reset Prompts to pick it up if you've customized the system prompt.)
- Hide stat numbers from the narrator (Settings โ Generation, default on) โ the narration, planning, and choices requests receive stat descriptors (e.g. "severely injured") instead of raw values, so the model writes to how a stat feels rather than fixating on the number. Stat-updates still get the numbers; falls back to the number when a stat has no descriptor.
-
Slash commands โ in the action box (input starting with
/) that bypass the AI; the first,/markdown test, types a rich sample through the real narration renderer to preview markdown formatting. - Use Custom Endpoint โ toggle in Settings โ Endpoint โ off uses the built-in default endpoint (fields read-only); on lets you enter and edit your own. Custom values are preserved when toggling off and back on.
- Hide-UI toggle โ an eye button in the bottom-left of the game view hides all panels to reveal the background image; it fades out over the background until hovered.
- Drag-to-reorder for worlds in the main menu.
- Additional avatar color options.
- Unified, consistently styled scroll areas, checkboxes, radios, and chips across the app.
- Tag truncation and a centered download control in the world browser.
- LLM request status surfaced next to the status bar.
- Update-aware world browser โ downloaded worlds show their status (Downloaded / Update available / out of date) with one-click re-download to pull the latest version.
- World provenance on cards โ local world cards show how each world arrived (Created / Downloaded / Imported) with timestamps.
- Main-menu layout toggle โ switch the world grid between Grid and Detailed layouts.
- Markdown in world descriptions โ world descriptions render markdown, matching the in-game narration.
- In-game entity viewer โ the avatar panel gained Player / Entities tabs; the Entities view shows the first detected entity's image, clicking an entity in the list swaps to it, and clicking the image opens a full zoom view (works even for worlds without a player model). The tab fills from the characters that actually took part in the turn (once the narration finishes, so there's no mid-stream flicker); a Staged-mode character the planner invented shows as a plain, non-clickable entry.
- Entity popup โ viewing an entity now supports the same pan/zoom image view as world thumbnails, sizes the image to most of the popup with its description below, and shows a muted "No description provided." when there's no text.
- AI-context "current context only" filter โ the per-turn context viewer can hide re-generated, rolled-back, and aborted turns so you see only what the AI currently sees (on by default).
- System Prompts editor โ Settings โ System Prompts has a tab per prompt type (Narration, Thinking, Choices, Stat Updates, Location Change, Summary, plus the Staged director/character/storyboard), and Reset affects only the selected prompt. Each helper request (Choices, Stat Updates, Location Change, Summary) has a System | User toggle, so you can edit the user message it sends โ the framing and the "just output X, no story" cue โ not just the system prompt, with Player Action and Narration chips to drop the turn's values into your template. A checkbox group in Settings โ Generation turns the optional prompts (Choices, Stat Updates, Location Change) on or off โ replacing the old "type DISABLED into the prompt" trick โ and a disabled prompt hides its editor tab (worlds that used the DISABLED trick migrate to off automatically). A Verbatim turns field in the editor footer sets how many recent turns each prompt sends in full before older ones collapse into the summary recap.
- Organized Settings โ gameplay settings are grouped into Presentation (language, music, output length, auto-scroll, markdown, narration audio) and Generation (hide stat numbers, thinking, memory summaries, silent requests), so related settings sit together.
- Staged (director-led) thinking โ a new Thinking mode beside Off / Planning / Inline that plans a turn in stages for higher-quality, more consistent scenes: a director stages the scene (a brief description of where you are and what's visible) and casts who's present with each one's placement โ where they stand and what they're physically doing right now, always leading with your own position as "Player Character". The director casts only living, acting beings (people, creatures, threats โ places and objects stay in the scene description), and gives any character it invents a concrete, reusable name so they stay the same character turn to turn. Each chosen character (up to three) plans its own motivation in a separate pass, speaking in the first person ("I wantโฆ / I intend toโฆ"); and a storyboarder combines those into the plan (reacting to what you did, never scripting your next move). When no other characters are present, the motivation and storyboard passes are skipped โ the plan is just the scene and your placement โ so a solo moment stays fast and doesn't invent filler. The director's scene and cast placements are handed to the narrator alongside the storyboard beats, so the story is grounded in concrete staging and physical interactions stay consistent. The trade-off is several extra requests per turn, so it's best with a fast endpoint. Each stage is visible in the AI-context viewer and never leaks into the story.
- Full-length, progressive TTS narration โ narration is split into sentence chunks so audio is no longer cut off at ~26 s (Kokoro's per-call token cap). Playback is progressive: speech starts after the first sentence and plays gaplessly (Web Audio) as the rest generate, with a determinate progress bar (sentence X of N); the seek bar is driven by the same engine, so the clip is scrubbable/replayable with no hand-off.
- Stream Narration Audio (in the Text-to-Speech popup, default off) โ when on, text-to-speech begins synthesizing each sentence the moment it finishes streaming from the model, instead of waiting for the whole story; much lower audio latency. Off by default since TTS then runs alongside the model and can compete for the GPU on a single-machine setup.
- Narration speed โ a persisted Speed slider (0.5รโ2.0ร) in the Text-to-Speech popup controls how fast the voice reads. Applies to newly generated audio, so change it and hit the regenerate (โป) button to re-speak the current text at the new pace.
- Karaoke sentence highlighting โ while narration audio plays, the sentence currently being spoken is highlighted in place with a soft accent tint, and the highlight follows the playhead when you scrub the seek bar โ so you always know where you are in the text. Works across bold/italic/linked words within a sentence and while narration is still streaming in. On by default (toggle "Highlight while speaking" in the Text-to-Speech popup); silently does nothing on browsers without highlight support.
- Frozen audio bar โ once TTS audio exists, the play/scrub seek bar is pinned to the top of the story panel (instead of scrolling away with the text) and carries the audio-specific buttons (play, regenerate) on its row; the export and edit buttons stay in the top-right corner.
-
Export the story โ a download button beside the narration's edit pencil saves the whole playthrough's narration to a file, one turn per block, with a choice of plain text (
.txt) or Markdown (.md). - Audio settings live with the audio โ the TTS controls (voice, speed, Stream Narration Audio, Highlight while speaking) all sit together in the Text-to-Speech popup rather than the main Settings menu, since they only matter once a TTS model is loaded โ which only happens from that popup, in-game.
- Non-streaming narration generates before the rest of the turn โ with a TTS model loaded and Stream Narration Audio off, the narration's audio is now generated right after the story text and the turn's remaining requests (choices, stat updates, location change) wait for it to finish, rather than racing alongside it โ avoiding GPU contention when the model and TTS share one machine.
- Memory Summaries (Settings โ Generation, default off) โ long stories stay coherent without bloating each request. Older turns are summarized into short fact lines as they age out of recent history and fed back to the model: recent turns stay word-for-word, older turns collapse into a compact "story so far" recap, and any past turn your action references is pulled back to full detail. Recall works two ways: by keyword, and by participant โ when your action mentions a character, an older turn that character actually took part in is resurfaced even if its summary shares no words with what you typed (so returning to "talk to Mira" after many turns finds the right scene). The memory meter breaks out how much of the window the recap and rehydrated turns use. Runs an extra request per turn, so it can compete for the GPU on a single-machine setup.
- Link entities to locations from either side โ in the World Editor a location's entities are now chosen from a searchable multiselect dropdown (replacing the long checkbox list), and each entity gained a matching Locations dropdown. The two stay in sync, so the relationship can be edited from whichever side you're on.
- Steadier World Editor tabs โ the add/search row now sits below the editor tabs instead of above them, so the tab strip no longer shifts when you switch to or from the Overview tab.
-
Audience-labeled description fields โ an entity's and location's descriptions are now split by audience: Player-Facing Description (shown in-game) and AI-Facing Description (full text sent to the model), plus a new AI-Facing Summary โ a short version for use where the full description is too long, which a โจ button generates from the AI-Facing Description using your connected LLM (with undo). Existing v1.2 worlds are upgraded automatically (their old
inGameDescription/detailedDescriptionfields are renamed on import). - Planning step uses AI-Facing Summaries โ the optional pre-reply planning request now feeds the model each location's and entity's short AI-Facing Summary (falling back to the full description where none is authored), keeping the planning pass lightweight. Narration still gets the full descriptions. The plan also lists each present character with their placement โ where they are and what they're physically doing right now โ giving the narration concrete staging to work from.
- Smarter next-action choices โ the choices request sees the player's most recent action (not just the resulting narration), so suggestions track what you were actually trying to do, and it uses the short AI-Facing Summaries instead of full descriptions. It's scene-aware: it only sees the characters actually present (named in the recent narration, within a short rolling window), not the whole location roster, so it won't suggest actions for people who haven't appeared yet or already left. And the story no longer duplicates the menu โ the narrator prompt is a positive contract that writes only prose while the separate Choices step presents the options, so smaller models stop tacking a "What do you do next?" list onto the end of the narration.
- AI-context "Hydrations" view โ the AI Context viewer (the per-turn debug popup) gained a Dictionary / Hydrations toggle. "Hydrations" shows the exact words and characters used to decide what older context gets recalled for that turn, as color-coded, clickable chips that highlight their matches โ and only within the game-text request, so the choices/stats requests stay clean.
- Duplicate button in the World Editor โ every list item (stats, entities, locations, traits, groups, dictionary entries) gains a duplicate button beside delete that makes a deep copy placed right below the original. Duplicating a trait keeps it in the same group; duplicating a group copies the whole nested subtree (subgroups and traits) with fresh ids in the exact same nesting. The copy's name gets a " (Copy)" suffix.
- Markdown toolbar for the world description โ the Edit tab gained a formatting toolbar (bold, italic, headings, lists, link, quote, inline code) plus undo/redo (buttons and Ctrl+Z / Ctrl+Y); the Preview still renders through the same engine as in-game narration.
- Trait groups (folders) โ the World Editor's Traits tab is now a foldering tree: the + opens a popout to Add Group or Add Trait, and traits/groups drag to reorder and nest โ drag a row right to tuck it under the group above, left to pull it back out, with the row indenting live as you drag. New worlds start with World and Player groups. Traits and groups gain Player-Facing and AI-Facing descriptions, and traits gain an Enabled by Default flag. In play, the trait-selection popup is now tabbed by group (an implicit General tab holds ungrouped traits, nested groups add a tab row); default traits start checked, and the buttons are Next (walk the groups), Skip (start with the current selection), and Abort (cancel loading the world). A group's AI description is sent to the model as a header above its selected traits.
-
โ๏ธ Backend
- Shared hooks and helpers: a
createWorkerClientworker factory, a single IndexedDB open/promisify helper (idb),usePersistentState,useIsMobile, and a cached-thumbnail hook. - The Community Creations browser was split out of
MainMenuinto its ownCommunityCreationsBrowserview, with shared world-card/detail presentation helpers (WorldDetails). -
Trait group data model โ the world format gains a
traitGroupsarray ({ id, name, playerDescription?, aiDescription?, parentId, order }); traits move from a singledescriptiontoplayerDescription/aiDescriptionand gaingroupId,isDefault, andorder. The tree, reordering, and AI trait-context builder live in pure, unit-tested helpers (lib/traitTree.ts); legacy v1.2 worlds rename their traitdescriptionโplayerDescriptionon import. -
Character diaries โ each character present in a turn quietly records a short first-person diary entry about it, from their own point of view, generated as turns age out (opt-in via Settings โ Generation โ Character Diaries; prompt editable under System Prompts โ Diary; covers ad-hoc walk-ons too). With "Show Silent Requests" on, each entry is inspectable per turn in the AI-context viewer. In staged planning, a character's own recent diary is now fed back into its motivation pass as private memory, so it acts with continuity and only knows what it recorded โ the start of real information asymmetry between characters. Entries are stored per-turn (
AITurnResult.diaries), an additive save-shape change that rolls back with the turn; old saves read as absent.
- Shared hooks and helpers: a
-
โ๏ธ Backend
- Dead code and noisy logging, including a leftover
bufferToSentencehelper and an auth-token console log.
- Dead code and noisy logging, including a leftover
-
๐ค User-facing
- Mislabeled UI text and icons corrected.
- The memory meter is now an accurate token gauge of the model's context window โ prompt + (actually-sent) history + reserved output as a % of the window โ fixing the old bar that mixed characters and tokens and counted the full untrimmed log.
-
โ๏ธ Backend
- Entity IDs now use
crypto.randomUUID()instead of timestamp-based IDs, avoiding collisions. -
Cleaner AI prompts โ blank or unset author fields (an empty entity
type, a missing description, etc.) are no longer padded into the location/entity/trait data sent to the model, and the editor-only starting-location flag is dropped from it. Empty fields previously leaked as blank lines or the literal textundefined, which could confuse smaller models. -
Stat code runs in a real sandbox โ world-authored stat formulas (which ride inside downloaded worlds) now execute in an isolated QuickJS WebAssembly VM instead of
new Function, so they can no longer reach the page (fetch,localStorage, the DOM) โ closing an untrusted-code hole. A runtime interrupt enforces the 1-second timeout, so awhile(true)loop is actually killed instead of hanging the tab (the old after-the-fact check never fired), with memory and stack caps on top. Only whitelisted stat data crosses into the VM; the numeric result is read back and clamped exactly as before. The engine is lazily loaded on first use, so it doesn't affect page load. - Lint cleanups across the converted codebase.
- Entity IDs now use
Playing
Reference
Project