v0.79.0 — the generators reach the models you already chose
Two phases with one root cause between them: a capability existed and the surface
that wanted it had no way to ask. Phase M gives genai a headless half so any
platform surface can generate an image without rendering the module's panel.
Phase G moves System 8 monster generation out of the platform and into the module
that owns the rules.
The three larger features that were listed here as bullets — Retouch upsampling,
background-token resize, and a second image-search provider — are not in this
release. Each was written up as its own spec under docs/committed/ and is
carried by v0.80.0.
Phase M - the generators reach the models the user already chose
The monster generators have an image problem and a naming problem, and both come from the
same place: genai can generate an image from a prompt, and nothing outside genai can ask
it to. So MonsterArtworkPicker auto-fetches art the only way it can - a Google Image search
on "${name} ${type}" - and a level 7 orc skirmisher with Arcane Archer levels comes back as
whatever the open web has under "orc humanoid". The generator knows far more about the monster
than the query it is allowed to send.
The AI tab has the mirror of it. "Generate with AI" produces a stat block and no art at all, even though the user picked an image model minutes earlier in the artwork editor, and even though the prompt they typed is a better image prompt than anything the algorithm can derive.
M.1 comes first because everything after it wants the answer to "which model did the user last pick?" and today nothing records it. M.2 is the extension point the rest depends on.
-
[x] M.1 - A model chosen by hand becomes the default for its kind. Every model dropdown outside Settings is per-surface and forgets on close, so a user who has settled on Nano Banana Pro re-picks it in the artwork editor, in the encounter generator, and in the monster generator, every time - and Settings never hears about it. Choosing a model by hand records it as the default for that domain, image or language, and every dropdown of that domain opens on it.
It writes into Phase I's own `imagePreference` rather than beside it, and adds the symmetric `languagePreference`. Two stores would have been two competing answers to "which image model?" - the one Settings shows and the one the generators use - and the failure mode is a user changing it in Settings and watching nothing change. There is one preference; this is a second way to set it. `chooseInitialModel` handles the case that makes it not a lookup: a remembered model disappears when a provider deprecates it, when its key is removed, or when it is disabled in Settings. -
[x] M.2 - Modules can contribute an image generator, not just an artwork panel.
registerArtworkSourcehands the platform a component, which is right for the token artwork editor and useless everywhere else: the monster and encounter generators want an image for a monster they just rolled, with no UI of the module's own and no interaction.ImageGeneratorExtensionis the headless half -listModels()andgenerate()- so any platform surface can ask. MODULE_API_VERSION 1.34.0.`generate()` is a function call rather than a platform POST to `/api/modules/genai/generate`, and that is the load-bearing decision: genai v0.6.0 moved generation direct-to-provider because returning image bytes as base64 through the server OOM-killed a 512 MB container. So the request carries `authFetch` and `getImageGenKey` and the module keeps deciding, per call, whose key it is spending. `ArtworkSourceProps.onPreferredImageModelChange` rides along, finishing M.1: I.3's `preferredImageModel` could only be set in Settings, which left a module's own dropdown a dead end - switch model, generate, come back tomorrow, get the old default. A host with no generator registered is exactly 1.33.0 behaviour. -
[x] M.3 - Auto-artwork has three states, and the third one is the good one. The
Autoswitch becomes off / Search / Generate, defaulting to Generate whenever a module has registered a generator that offered models, and to Search when none has - which is exactly today's behaviour, and what every deployment without genai keeps. Generate is omitted rather than disabled where it is unavailable; a disabled option advertises a feature nothing in the modal can enable.The prompt is the difference. `buildMonsterArtPrompt` describes the monster the generator actually produced - level, ancestry, role, classes, and up to four powers or feats - where `searchGoogleImages(`${name} ${type}`)` sent two words. Search keeps the short query, because a search engine matches documents and a descriptive sentence matches nothing; the two modes want different strings, which is why both live in one file rather than one being reused for the other. A model dropdown sits beside the control. Changing it re-queries, the refresh button re-queries the selected model, and the pick is remembered as the user's default (M.1). -
[x] M.4 - The encounter generator gets the same control.
MonsterArtworkPickeris a near-copy ofrenderArtworkCellhere, by a deliberate v0.25.0 decision, and the copy has been drifting since. M.3's hook and control are the pieces worth sharing, so the encounter generator uses them rather than growing its own third state. Its ownsearchGoogleImagescall and itsautoSelectArtboolean are gone, with a test asserting both stay gone.A group's subject is thinner than a monster's - no ancestry, no level - but the CR and the combat role are real, and size and type come from a monster the group already generated. The all-empty guard on re-acquisition stays: without it, opening the mode control would discard art the user had already curated in the drawers. -
[x] M.5 - The algorithmic generator suggests a name worth keeping.
Custom Monster (Level 7)is what a template can produce, and it is the one part of a generated stat block a GM always rewrites by hand.POST /api/ai/suggest-token-namesends the race, level, role, classes, powers and feats and gets backGrok-nar, Battle-Hardened Chief of the Iron-Skull Clan.The suggestion fills an editable field with a sparkle button beside it; it is never applied silently, and what the user types always wins at insert. The field is separate from the Configure tab's Monster Name, which is an *input* to generation - sharing one would have made a re-roll on the preview quietly change the next generation. Gated on the genai module being loaded, which is a product decision rather than a technical need (the LLM plumbing is the platform's) and is confined to `nameSuggestionAvailable` so it is one line to revisit. Also hidden on the in-browser path, where there is no server route to call. Temperature is 1.0, not the parsing routes' 0.1: a re-roll button that returns the same name twice is a broken button. -
[x] M.6 - Generate with AI generates the image too. The AI tab gains an image-model dropdown beside the language model, and one click fires both: the stat block and the portrait, concurrently, from the one prompt the user already wrote. The image is started before the stat block and awaited after it - sequential would make the tab take as long as both, and the image is usually the slower half.
Neither failure blocks the other. `generateAiArtwork` resolves to `null` rather than rejecting, because a stat block with no art is exactly what this tab produced before M.6. The image dropdown carries its own "No image" option instead of sitting beside a checkbox - two controls for one decision, and a greyed dropdown next to an unticked box reads as broken. Both dropdowns record the pick as the user's default (M.1), except Automatic, which is the absence of a choice and would clobber a real one. The language choice rides on `x-ai-model`, overriding the task preference for this request only. -
[x] M.7 - Bump
modules.hosted.jsonto@purple-cat-games/genai0.9.0. Last, and only once the module is published, for the same reason as G.4: the pin names an exact version, so bumping it ahead of the publish points the host at a package that does not exist.Worth noting while doing it - **the pin is at 0.6.1, and 0.7.0 and 0.8.0 both shipped.** A deployed host has therefore never run the registered image operations (0.7.0) or the preferred-model picker (0.8.0), and I.4's "genai opens on the preferred model" is inert in production today for that reason rather than for anything wrong with either release. Until this lands, a deployed v0.79.0 runs a genai with no image generator registered, which M.2 was written to survive: the artwork controls offer Search and Off, and no Generate.
Phase G — Generate Monster asks the module
The System 8 monster generator does not live in the System 8 module. It lives here, in eight
apps/web/src/utils/system8*.ts files and four System8*Editor.tsx components, and it is the
larger of the two implementations — the module's own generateCharacter assigns an ability
array and stops, because when it was written the module had no way to read System 8's powers
and feats. Module v0.13.0 removed that reason: the content ships inside the package now.
So the app fetches /api/modules/system8/{powers,feats} in a useEffect, holds the rows in
component state, picks the class, rolls the array, fills the power and feat slots and assembles
the Character — all of it System 8 rules, none of it in System 8. The leakage guard has the
count baselined; this is the ratchet.
Three requested behaviour changes ride along, and all three are reasons the split hurts: the role dropdown is labelled "class" in the one place that still shows the legacy favor table, the generator cannot be told which class to build toward, and it re-rolls the subclass on every power pick so a "Death Knight" comes out holding one power from each of Fighter's five subclasses. Every one of those is a rules decision, which is to say the module's.
-
[x] G.1 — The seed can carry what was asked for. Module API 1.32.0 adds
MonsterSeed.name,.classPreferenceand.subclassPreference. M-2's seed has been "level + template + rngSeed" since it was written, which is enough to generate A monster and not the one the GM asked for — so every choice past the template necessarily stayed in the app. All three optional; absent is exactly 1.31.0 behaviour.classPreferenceis a string rather than an enum because class names are the system's vocabulary; only'auto'and'monster'are the platform's. AddsTemplate.castingStatsandSystemClass.castingStatsalongside — the plural of the long-standingspellcastingAbility, for systems where the class picks (a Witch keys off Int, Wis or Cha) — so the app can decide whether to show the Casting Stat control without knowing which roles are casters. Artillery Flavor needs no such field: it only ever narrowed the class roster, which is what Class Preference now does. -
[x] G.2 — System 8 owns generation end to end (module v0.15.0, released separately). The roles, the ability array, the power and feat auto-fill, the derived stats and the Character assembly move into
@purple-cat-games/system8, which fetches its own content rather than being handed it. Adds Class Preference and Subclass Preference, and drops theclass→rolemisnaming. Bumpmodules.hosted.jsononce it is published. -
[x] G.3 — Generate Monster becomes a form, and what moved is deleted. The System 8 branch of
MonsterGenerator.tsxloses the Template/Manual toggle, the favor-weight sliders, the Monster Preset and Race Name, and gains Class Preference and Subclass Preference. It callsgenerateCharacter(seed)on the campaign's own module and renders the result; it decides nothing. Artillery Flavor goes too — it only ever narrowed the class roster, which is what Class Preference does now.Manual mode goes rather than moves. Create Token ▸ **New Monster** already opens a blank character sheet, which is a better hand-editor than four modal sub-editors reimplementing one against a second copy of the rules. The deletions are the same commit because they are what makes the rewrite compile: eight `system8*` utils, `seededRandom.ts`, four `System8*Editor` components and their tests — 3,000-odd lines — with the `systemLiterals` guard baseline ratcheted down beside them. Size / Type / Alignment are hidden for System 8, where they were collected and discarded: size comes from the generated Large/Huge powers, and the system has no monster type or alignment. Found on the way: `hashCode` was **called and never defined**. Every System 8 generation with a non-numeric seed threw `hashCode is not defined`, and the missing identifier sat among `apps/web`'s ~200 known type errors where it looked like all the rest. -
[x] G.4 — Bump
modules.hosted.jsonto@purple-cat-games/system80.15.0. Last, and only once the module is published: the pin names an exact version, so bumping it ahead of the publish points the host at a package that does not exist. Until it lands, a deployed v0.79.0 runs the 0.14.1 module, which ignores the new seed fields and generates as it did before — the form collects preferences nothing acts on, which is the right way round for the window between the two releases.Two corrections made when it landed. The branch was cut before v0.78.1 and still pinned system8 **0.14.1**, so merging it would have walked the pin backwards past 0.14.3 — the Express-5 route-string fix v0.78.1 was released to pin — and 404'd every `/api/modules/system8` route in production for the third time. And **`modules.dev.json` pins `genai` separately**, at 0.6.1; M.7 in one file would have left the dev environment with no image generator while prod had one. Both files move together now. Both module branches also had to be merged forward before they could ship: `genai` `version/0.9.0` was cut from v0.8.0 and lacked v0.8.1's `vitest.setup.ts` (51 client tests failing under Node 26), and `version/system8-0.15.0` was cut from 0.14.1 and lacked the Express-5 fix it is the whole point of this task not to regress.