Skip to content

feat: let a capability package provide a whole Game mode - #4528

Merged
SpicyMarinara merged 20 commits into
Pasta-Devs:stagingfrom
SonaGat3:feat/game-mode-experience
Aug 4, 2026
Merged

SpicyMarinara merged 20 commits into
Pasta-Devs:stagingfrom
SonaGat3:feat/game-mode-experience

Conversation

@SonaGat3

@SonaGat3 SonaGat3 commented Aug 3, 2026 •

Copy link
Copy Markdown
Collaborator

Important

Contributions target staging. Only SpicyMarinara may promote this repository's staging branch or a same-repository hotfix/* branch to main.
Outside and first-time contributors also require an approving review from SpicyMarinara.

Linked issue

Closes #4526

Why this change

Marinara can already be extended a lot — a package can add a panel, a chat surface, a settings
section. What it cannot do is provide a Game mode of its own: one that draws its own HUD, menus and
combat, keeps its own state, and replaces the built-in systems it implements. Everything a package can
contribute today sits next to Game mode rather than being one.

This adds that seam, plus the three smaller pieces of host support it turned out to need. All of it is
generic: the engine never names a package, an experience DECLARES what it replaces, and anything left
undeclared stays built-in. A game without an experience behaves exactly as it does today.

It's four separable pieces, described below so they can be reviewed one at a time.

What changed

1. Re-scan the asset manifest after packages activate — packages/server/src/app.ts (+12)

Unrelated to game modes, and useful to any package: a package can install its own art during
activate(), which runs after the boot-time asset scan. Until now that art stayed invisible to
everything that reads the manifest — scene analysis, the asset pickers — until the next restart.
This re-runs the same idempotent scan the upload routes already re-run.

Guarded with try/catch + app.log.warn on purpose: this call runs over files a package has just
written, and scanDir uses bare readdirSync/statSync. A stale manifest costs that package its art
until the next scan; failing to boot costs the user everything.

2. A per-turn prompt-context hook — 3 files + 1 new

  • new server/services/capability-packages/capability-prompt-context.service.ts
  • server/services/capability-packages/capability-module-runtime.service.ts (+17)
  • server/routes/generate.routes.ts (+23)
  • server/services/game/gm-prompts.ts (+14/−3)

The prompt-context permission already existed in the manifest enum but had no consumer, so
declaring it did nothing. This gives it one: a package that owns live state can append a block to the
system prompt of each turn, so the model narrates in sync with what the player is looking at instead of
guessing.

Registration is gated on the permission. Contributors are keyed by package id, so re-activation replaces
rather than stacks. A contributor that returns nothing is skipped and one that throws is logged and
skipped — a package can never break a turn.

A contribution may also declare which built-in systems it replaces (provides). Today that's
{ inventory?: boolean }: an experience with its own item system takes [inventory: …] and the
PLAYER INVENTORY: block out of the GM format reminder. This has to be declarative rather than a line in
the package's own prompt, because the format reminder is injected as the LAST user message — a package
can't out-argue it from the system prompt. Anything not declared stays built-in.

3. Resource writes — 2 files (+97)

  • shared/types/capability-runtime.ts (+55)
  • server/services/capability-packages/capability-resources.service.ts (+42)

The resource facade exposed reads only. A package's setup flow needs to find-or-create the player persona
and its lorebook, so this adds six write methods as thin passthroughs to engine storage.

They are optional members of CapabilityResourceHost, so adding them is not a breaking type change
for any other implementation, and a host that only wants to expose reads doesn't have to stub six
throwers. The input types are declared narrowly enough to hand straight to storage (a closed
CapabilityLorebookCategory union, a required entry name), so nothing needs a cast — a wrong value is a
compile error in the package instead of a silent rejection at write time.

4. The game-mode experience seam — 7 files

  • shared/schemas/capability-package.schema.ts (+16) — the game-surface slot and its
    contributions.gameSurface.surfaceClass option
  • shared/types/game.ts (+9) and server/routes/game.routes.ts (+6) — gameExperienceId +
    experienceConfig on the setup config, copied to the chat metadata on create
  • client/components/game/GameSurface.tsx (+323/−36) — the mount and every seam
  • client/components/game/GameSetupWizard.tsx (+10/−1) — an optional experiencesSlot
  • new client/components/game/NewGameExperienceChooser.tsx — the Experiences block
  • client/components/game/GameNarration.tsx (+27/−6) — five inert theming hooks
  • client/src/localization/locales/en.json (+4)

The experience is chosen when the game is created, from an Experiences block in the setup wizard's
first step, and belongs to that game for its lifetime. It is deliberately not a per-chat toggle: an
experience owns the whole run — its own inventory, combat, calendar and state — so switching one on
midway through an ordinary game, or off midway through its own, leaves a half-built run either way.

Which package it is comes from the manifest (it declares the game-surface slot), so nothing here is
tied to a specific package id. grep for any package name across these files returns zero.

The surface mounts twice: the main layer above the narration for HUD and menus, and a presentation-only
underlay in the background band for what must render behind the dialogue box, like a standing
sprite. Same view, distinguished by constant props, so no new value was needed in the
CapabilityElement view union.

The seams are all declarative, and each is gated host-side so a stale value can never leak into an
ordinary game: scene background, speaker avatars, the live activeSpeaker, an in-flow choice slot above
the narration box, the host's sprite-scale setting, the resolved scene background handed to the underlay,
and a chrome declaration (providesInventory / providesCombat / providesPlayerInput) by which an
experience says which built-in chrome it replaces. The Inventory and Start-combat buttons are hidden by
simply not receiving their callbacks — withholding beats hiding, since there's no dead button and no way
to reach the built-in flow underneath.

The five hooks in GameNarration are class names and one data- attribute, with no logic. Nothing in the
engine styles them, so Classic renders identically; an experience needs them because that chrome renders
outside its own element and is otherwise unreachable from its stylesheet.

The first-game tutorial is suppressed for an experience — it spotlights built-in chrome that isn't on
screen. Suppressed, not consumed: the persisted "never show again" flag is left untouched, so a player
who runs an experience first still gets the tour on their first ordinary game.

5. Clear pending choices and QTEs on a scope change — packages/client/.../GameSurface.tsx (+3)

Pre-existing, and the only change here that touches ordinary games. Choices offered by one game stayed on
screen in the next one and were answerable there; an active QTE leaks the same way and carries a timer
that then fires in the new game. Every site clearing either one is driven by a player action or by the
session concluding, and the effect keyed on the scope resets about twenty other pieces of turn state
without touching these two.

It is fixed here rather than separately because it lives in the same effect this PR was asked to reset the
experience seam state in, and resetting ours while leaving these two leaking would be arbitrary. Say the
word and I will split it out.

Validation

  • pnpm check passes locally
  • Container (Docker / Podman) built and ran without issue
  • Ran the app, clicked through the changes manually
  • Checked edge cases (light + dark mode, mobile viewport, empty states, error paths)
  • Above manual verification completed (describe below)
  • Read and followed CONTRIBUTING.md

Manual verification notes

Ran against staging, with a package that provides an experience installed:

The experience path. The surface mounts, HUD and menus render over the narration, and the standing
sprite draws behind the dialogue box (the underlay mount). Speaker avatars resolve for a cast that has
no character cards. The experience's choice menu lands above the narration box rather than floating over
it. The built-in Inventory and Start-combat buttons are absent, and the text input disappears while a menu
is on screen and comes back when there is nothing to pick — the chrome declaration is live, not one-shot.

Switching between modes. Moved back and forth between an experience game and an ordinary one
repeatedly. No state leaks in either direction: no stuck background, no leftover avatars, no chrome from
one showing up in the other.

Ordinary Game mode. Checked on a fresh ordinary game: the setup wizard is unchanged apart from the
added block, and the Inventory / Start-combat buttons, text input, map, party bar, HUD widgets and
per-scene background and music all behave as before. The first-game tutorial still fires on the first
ordinary game even after playing an experience first, which is the point of suppressing it rather than
consuming the flag.

One deliberate change to ordinary games, described under part 5 below: pending choices and QTEs are now
cleared when the game scope changes.

Empty state. With no experience installed, the block renders its "none downloaded yet" state with an
affordance to open the catalog, and the built-in wizard is untouched.

Part 1 — the manifest is rebuilt after activation and indexes the package's art (114 entries for the
installed package) on the first restart after installing, with no second restart needed.

Part 2 — verified in a real game in both modes: an ordinary game still gets the [inventory: …]
command, and the experience — which declares provides: { inventory: true } — does not.

Part 3 — creating a game through the experience's setup creates the player persona and its lorebook
with the expected fields.

Docs and release impact

  • No docs changes needed
  • Updated docs (README / CONTRIBUTING / android/README / CHANGELOG) as needed
  • Translated docs on the docs-i18n branch updated to match, or a [docs-i18n] follow-up issue opened (see CONTRIBUTING.md § Translated documentation)
  • Version/release files updated (only if this PR includes a version bump)

docs/development/optional-agent-packages.md — extended "Package model" to cover full Game-mode
surfaces, per-turn prompt contributions, and the resource writes. No version heading, as suggested.
Nothing under docs/ proper changed, so no docs-i18n work is needed. No version bump.

UI evidence (if applicable)

Before:
image

After:
image

Summary by CodeRabbit

  • New Features
    • Added complete, package-provided Game experiences with custom HUDs, menus, combat, setup flows, avatars, backgrounds, and visual styling.
    • Added Game experience selection during new-game setup, including catalog access when none are installed.
    • Added support for saving experience-specific setup configuration.
    • Added package-provided turn context and support for replacing built-in systems.
    • Added persona and lorebook creation, editing, and entry management for supported experiences.
    • Newly installed package assets are available immediately after activation.
  • Bug Fixes
    • Improved resilience when asset rescanning fails during package activation.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds capability-package Game experiences with setup selection, package-owned surfaces, declarative Classic-system replacement, prompt-context contributions, resource writes, and post-activation asset rescanning.

Changes

Game Experiences

Layer / File(s) Summary
Experience contracts and package declarations
packages/shared/src/schemas/..., packages/shared/src/types/..., docs/development/optional-agent-packages.md, CHANGELOG.md, packages/client/src/localization/locales/en.json, scripts/regressions/...
Manifests support the game-surface slot and Capability API 1.8. Game setup stores an experience ID and opaque configuration. Capability resources expose persona and lorebook write contracts. Documentation, localization, release notes, and compatibility checks describe the experience flow.
Prompt context, resource, and server integration
packages/server/src/services/capability-packages/..., packages/server/src/routes/generate.routes.ts, packages/server/src/routes/game.routes.ts, packages/server/src/services/game/gm-prompts.ts, packages/server/src/app.ts
Authorized packages contribute per-turn prompt blocks and replacement declarations. Contributors are isolated on errors and timeouts. Resource writes delegate to storage. Experience setup data is validated and persisted. Asset manifests rescan after activation.
Experience setup and game creation
packages/client/src/components/game/NewGameExperienceChooser.tsx, packages/client/src/components/game/GameSetupWizard.tsx, packages/client/src/components/game/GameSurface.tsx, packages/client/src/hooks/use-capability-packages.ts
The setup flow discovers experiences, renders package or Classic setup, launches games with experience configuration, and returns the created chat identifier.
Experience surface mounting and Classic UI gating
packages/client/src/components/game/GameSurface.tsx, packages/client/src/components/game/GameNarration.tsx
Active experiences mount underlay and main surfaces, receive game seams, provide avatars and choice menus, and conditionally replace Classic controls, tutorials, widgets, backgrounds, and narration styling.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Player
  participant NewGameExperienceChooser
  participant GameRoute
  participant GameSurface
  participant ExperienceSurface
  Player->>NewGameExperienceChooser: Select experience and configure setup
  NewGameExperienceChooser->>GameRoute: Create game with experience metadata
  GameRoute-->>NewGameExperienceChooser: Return created chat ID
  GameSurface->>GameRoute: Load persisted experience ID
  GameSurface->>ExperienceSurface: Mount surface with game state and UI seams
  ExperienceSurface-->>GameSurface: Provide backgrounds, avatars, chrome, and choices
Loading

Possibly related PRs

Suggested reviewers: spicymarinara, gunterlie

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully addresses issue #4526 requirements: asset re-scan after activation, per-turn prompt-context contributions with deadlines, optional resource writes, game-surface slot, experience selection at creation, and declarative seams for backgrounds, avatars, and chrome replacement.
Out of Scope Changes check ✅ Passed All changes directly support the four core objectives: asset manifest re-scan, prompt-context hook implementation, resource write methods, and game-surface experience seam. One additional fix for clearing pending choices and QTEs on scope changes is justified as necessary for state integrity.
Title check ✅ Passed The title clearly summarizes the primary change: capability packages can provide complete Game modes.
Description check ✅ Passed The description follows the template, explains the changes, documents validation, and includes UI evidence; one container check remains unchecked.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@SpicyMarinara SpicyMarinara self-assigned this Aug 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/client/src/components/game/GameSurface.tsx`:
- Around line 3416-3451: Reset the experience seam state when the experience
scope changes: clear experienceAvatars, experienceChrome, and
experienceBackgroundUrl in the existing runtime reset logic keyed by
sceneRuntimeScopeKey and experienceSurfaceId. Update the relevant effects near
the activeChatId/sceneRuntimeScopeKey resets, ensuring stale avatar, background,
and providesPlayerInput declarations cannot persist across experience switches.
- Around line 6459-6467: Update pushExperienceBackground so
experienceBackgroundUrl is assigned only after validating the value as a
permitted game-asset path: resolve it through scopedAssetMap and accept
compatible game-assets entries, or validate package-owned paths with an explicit
scheme/hostname allowlist. Reject unsupported or untrusted values instead of
falling back to arbitrary value strings before CrossfadeBackground.

In `@packages/client/src/components/game/NewGameExperienceChooser.tsx`:
- Around line 224-252: Update the setup panel motion.div in
NewGameExperienceChooser to include role="dialog", aria-modal="true", and
aria-labelledby referencing the panel title. Add Escape-key handling that calls
onCancelSetup only when launching is false, while preserving the existing
backdrop and close-button behavior.

In `@packages/server/src/app.ts`:
- Around line 213-216: Update the post-activation asset rescan error handling
around buildAssetManifest to call the shared logger.warn instead of
app.log.warn, preserving the existing error object and warning message.

In `@packages/server/src/routes/game.routes.ts`:
- Around line 1687-1690: Update the game setup schema around gameExperienceId
and experienceConfig: require gameExperienceId to be a non-empty manifest-style
identifier using the existing package/manifest ID validation convention, and
constrain experienceConfig with explicit limits on key count, key size, and
value/payload size before persistence. Preserve both fields as optional and keep
the config opaque to the host.

In
`@packages/server/src/services/capability-packages/capability-prompt-context.service.ts`:
- Around line 95-98: Update the catch block in the capability prompt-context
contributor flow to pass the caught error object as the first argument to
logger.warn, preserving its stack and cause. Keep the Pino format string and
packageId as the subsequent context argument, and remove the String(error)
conversion.
- Around line 78-101: Update collectCapabilityPromptContext to invoke all
contributors concurrently and await them through Promise.allSettled, preserving
contributorsByPackage registration order when assembling blocks and provides.
Wrap each contribute(request) promise with a per-contributor timeout, and treat
timed-out or rejected contributors as non-fatal by logging the contributor
identifier and continuing without its context. Ensure the overall collection
completes within the timeout bound rather than being blocked by a contributor
that never settles.

In `@packages/shared/src/types/capability-runtime.ts`:
- Around line 99-105: Replace the open string index signature in
CapabilityLorebookEntryInput with an explicitly named nested extension bag for
forward-compatible fields. Preserve name, content, and keys as validated
top-level properties so misspelled fields produce compile-time errors, and
update any affected consumers to read additional fields through the new bag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 03f135c9-cc1c-4002-8550-56f439140688

📥 Commits

Reviewing files that changed from the base of the PR and between ce90b08 and d0cb9ab.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • docs/development/optional-agent-packages.md
  • packages/client/src/components/game/GameNarration.tsx
  • packages/client/src/components/game/GameSetupWizard.tsx
  • packages/client/src/components/game/GameSurface.tsx
  • packages/client/src/components/game/NewGameExperienceChooser.tsx
  • packages/client/src/localization/locales/en.json
  • packages/server/src/app.ts
  • packages/server/src/routes/game.routes.ts
  • packages/server/src/routes/generate.routes.ts
  • packages/server/src/services/capability-packages/capability-module-runtime.service.ts
  • packages/server/src/services/capability-packages/capability-prompt-context.service.ts
  • packages/server/src/services/capability-packages/capability-resources.service.ts
  • packages/server/src/services/game/gm-prompts.ts
  • packages/shared/src/schemas/capability-package.schema.ts
  • packages/shared/src/types/capability-runtime.ts
  • packages/shared/src/types/game.ts

Comment thread packages/client/src/components/game/GameSurface.tsx
Comment thread packages/client/src/components/game/GameSurface.tsx
Comment thread packages/client/src/components/game/NewGameExperienceChooser.tsx
Comment thread packages/server/src/app.ts
Comment thread packages/server/src/routes/game.routes.ts Outdated
Comment thread packages/shared/src/types/capability-runtime.ts
Reset the experience seam state when the game scope changes. experienceAvatars,
experienceChrome and experienceBackgroundUrl are component state on a component
that survives chat switches, so a switch between two experience games carried the
previous game's background, avatars and providesPlayerInput declaration into the
next one -- the last of which could leave the player with no way to send a turn.

Add dialog semantics to the experience setup panel: role, aria-modal and
aria-labelledby matching what GameSetupWizard already sets, plus Escape to
dismiss while a launch is not pending.

Constrain gameExperienceId to the manifest id shape, since it is matched against
one to mount the surface, and bound the opaque experienceConfig so it cannot grow
into a payload every later write of the setup config has to carry.

Give each prompt-context contributor a deadline. A throw was already non-fatal,
but a promise that never settled would hang the turn, which the documented
contract said could not happen. Pass the error object first to Pino so the stack
survives.

Trim the comments substantially and drop the bracketed tags left over from
development. The remaining ones explain why a seam exists or what breaks without
it, rather than restating the code.
@SonaGat3

SonaGat3 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough pass. Pushed a commit addressing five of the eight comments; notes on the
remaining three below.

Applied

Seam state now resets when the game scope changes. This was a real bug and the manual testing missed
it: I moved between an experience game and an ordinary one repeatedly, where experienceSurfaceActive
does go false and the host guards do clear everything — but never between two experience games, where it
stays true throughout. experienceAvatars, experienceChrome and experienceBackgroundUrl now clear on
a key combining sceneRuntimeScopeKey and experienceSurfaceId, following the same
prevScopeRef + effect shape the file already uses for the scene restore state. Good catch on
providesPlayerInput in particular: a stale true would have left the player with no way to send a turn.

Dialog semantics on the experience setup panel. Added role, aria-modal and aria-labelledby
pointing at the panel title, matching what GameSetupWizard sets on its own panel, plus an Escape
listener gated on launching so it can't tear down a run mid-launch.

gameExperienceId and experienceConfig are constrained. The id now uses the same
/^[a-z0-9]+(?:-[a-z0-9]+)*$/ + .max(80) the manifest schema applies, which is right since this value is
matched against a manifest id to mount the surface. experienceConfig keys are capped and the whole
record is bounded by a serialized-size limit.

Pino gets the error object first. Fixed — that one was a straightforward miss against the contributing
guidelines, and the stack is exactly what you'd want when a contributor misbehaves.

Per-contributor deadline. You were right that the try/catch isolates a throw but not a hang, and
the docblock claimed a guarantee the code didn't deliver. Each contributor now runs under a deadline and a
timeout is treated exactly like a throw: logged, skipped, turn proceeds.

I kept the calls sequential rather than moving to Promise.all. The contributors compose a single
prompt, so block order has to be stable across a run, and expressing that as "registration order" is
clearer about the intent than relying on Promise.all preserving array order. The latency argument is
also weak in practice — a contributor builds a string from state it already holds, and with the deadline
in place the worst case is now bounded either way. Happy to switch if you'd rather have the concurrency.

Verified by running the collector: a contributor that never settles is dropped at the deadline without
delaying the others, a throwing one is still skipped, registration order and the provides union are
unchanged, and no timer is left behind.

Not applied

Restricting pushExperienceBackground to game-asset paths. I don't think this is a boundary. The
value comes from an installed capability package, whose client bundle is loaded and executed in the page —
it registers the custom element this seam talks to. A package that wanted to render a remote image can
already do so inside its own element without going near this seam, so validating the string here
constrains nothing that isn't already unconstrained. The seam exists so the package's background renders
behind the shared narration, which it cannot reach on its own.

That said, I have no objection to adding a resolve-under-game-assets check as defence in depth if you'd
prefer packages to be held to it — say the word and I'll add it.

The open index signature on CapabilityLorebookEntryInput. The three named fields are the ones a
package needs to reason about; the signature is there because the entries go to
lorebooks.bulkCreateEntries, which accepts more than those three, and naming them all would couple this
published type to the engine's internal entry shape — every field added to storage would become a change
here. Your point about misspellings is fair, though, and a nested bag would trade one cost for the other.
This feels like your call on where you want that published surface to sit, so I'm happy to take it either
way.

app.log.warn in app.ts. I believe this one is a false positive. app.ts doesn't import the shared
logger at all, and all ten of its logging calls go through app.log.* — the line right below mine reads
app.log.warn({ err: error }, "sidecar bootstrap failed"), which is the same shape. CONTRIBUTING.md
lists app.log/req.log as acceptable, and the call sits inside buildApp where app is right there. I
kept it consistent with the file, but I'll switch it if you'd rather the shared logger be used
unconditionally.

Also in this commit

I cut the comments back substantially, which should address the redundant-comments flag on the walkthrough.
The bracketed tags were scaffolding from development and are gone, along with several explanations that
were repeated at three or four call sites. What's left explains why a seam exists or what breaks without
it, rather than restating the line below it.

pnpm check passes. pnpm regression:prompt is at 147 passing with the one failure that reproduces
identically on unmodified staging (Storyboard host consumes package-owned prompts without restoring Engine globals), unchanged from before this commit.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

@SonaGat3 Thanks for the detailed follow-up. I will re-review the complete PR, including the applied fixes and the rationale for the three unchanged items.

🐇🔍


✏️ Learnings added
Learnt from: SonaGat3
URL: https://github.com/Pasta-Devs/Marinara-Engine/pull/4528

Timestamp: 2026-08-03T16:53:28.549Z
Learning: In `packages/server/src/app.ts`, logging inside `buildApp` uses the Fastify `app.log.*` logger. The repository contribution guidance permits `app.log` and `req.log`, so this usage is valid when it follows the local logging pattern.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/server/src/services/capability-packages/capability-prompt-context.service.ts (1)

84-107: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Run contributors concurrently to bound total latency.

Each contributor now has a 2-second deadline, so a single hung contributor can no longer block the turn indefinitely. The for...of loop still awaits contributors one at a time, so total added latency for N registered contributors stays additive (up to N × 2000ms in the worst case) instead of being bounded by the slowest contributor.

Collect all contributions with Promise.all and process the resolved array afterward. Promise.all preserves array order regardless of completion order, so the documented "contributors run in registration order" guarantee for block composition still holds.

♻️ Proposed refactor for concurrent contributor collection
 export async function collectCapabilityPromptContext(
   request: CapabilityPromptContextRequest,
 ): Promise<CapabilityPromptContextResult> {
   if (contributorsByPackage.size === 0) return { blocks: [], provides: {} };
-  const blocks: string[] = [];
-  const provides: CapabilityProvidedGameSystems = {};
-  for (const [packageId, contribute] of contributorsByPackage) {
-    try {
-      const contribution = await withDeadline(contribute(request), packageId);
-      if (contribution === null || contribution === undefined) continue;
-      const text = typeof contribution === "string" ? contribution : contribution.text;
-      if (typeof text === "string" && text.trim().length > 0) blocks.push(text.trim());
-      if (typeof contribution === "object" && contribution.provides?.inventory === true) {
-        provides.inventory = true;
-      }
-    } catch (error) {
-      logger.warn(error, "[capability] prompt-context contributor failed for %s", packageId);
-    }
-  }
-  return { blocks, provides };
+  const settled = await Promise.all(
+    [...contributorsByPackage].map(async ([packageId, contribute]) => {
+      try {
+        return await withDeadline(contribute(request), packageId);
+      } catch (error) {
+        logger.warn(error, "[capability] prompt-context contributor failed for %s", packageId);
+        return null;
+      }
+    }),
+  );
+  const blocks: string[] = [];
+  const provides: CapabilityProvidedGameSystems = {};
+  for (const contribution of settled) {
+    if (contribution === null || contribution === undefined) continue;
+    const text = typeof contribution === "string" ? contribution : contribution.text;
+    if (typeof text === "string" && text.trim().length > 0) blocks.push(text.trim());
+    if (typeof contribution === "object" && contribution.provides?.inventory === true) {
+      provides.inventory = true;
+    }
+  }
+  return { blocks, provides };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/server/src/services/capability-packages/capability-prompt-context.service.ts`
around lines 84 - 107, Update collectCapabilityPromptContext to invoke all
contributors concurrently by mapping contributorsByPackage entries to promises
and awaiting them with Promise.all, while retaining each contribution’s
withDeadline and non-fatal error handling. Process the resolved results
afterward in registration order so blocks remain deterministically composed and
provides aggregation is unchanged.
packages/shared/src/schemas/capability-package.schema.ts (1)

46-64: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Update supportedCapabilityApi for the new capability surface.

supportedCapabilityApi still reports 1.7, but this release adds game-surface, prompt-context consumption, and optional resource write methods. A schemaVersion-2 package cannot declare the new capability API without getCapabilityApiCompatibilityIssue failing for a host that supports it, or it must under-declare and skip the guard. Bump the minor version to the next supported release.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/schemas/capability-package.schema.ts` around lines 46 -
64, Update the supportedCapabilityApi version from 1.7 to the next minor release
compatible with the added game-surface, prompt-context, and optional resource
write capabilities. Locate the supportedCapabilityApi declaration and preserve
the existing schema-version compatibility logic.
packages/client/src/components/game/NewGameExperienceChooser.tsx (1)

199-254: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the existing localization key for the fallback title.

Line 226 falls back to the hardcoded string "New Game". Every other string in this panel uses localizeUi. GameSetupWizard.tsx uses localizeUi("navigation.chatSidebar.new.game") for the identical fallback title in its own dialog header. Use the same key here for consistency and to keep the string translatable.

🌐 Proposed fix for the localization gap
-            <h3 id="game-experience-setup-title" className={NEUTRAL_PANEL_TITLE}>
-              {activeExperience?.manifest.name ?? "New Game"}
-            </h3>
+            <h3 id="game-experience-setup-title" className={NEUTRAL_PANEL_TITLE}>
+              {activeExperience?.manifest.name ?? localizeUi("navigation.chatSidebar.new.game")}
+            </h3>

The Escape handling and dialog semantics (role="dialog", aria-modal, aria-labelledby) that a past review requested are correctly implemented here. Based on learnings, packages/client/src/localization/locales/en.json is the authoritative source for UI localization keys, and navigation.chatSidebar.new.game is already the established key for this exact string.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/NewGameExperienceChooser.tsx` around
lines 199 - 254, Update the fallback title in the dialog header rendered by the
active experience branch to use localizeUi("navigation.chatSidebar.new.game")
instead of the hardcoded "New Game", matching GameSetupWizard.tsx while
preserving the existing activeExperience manifest name when available.

Source: Learnings

packages/client/src/components/game/GameSurface.tsx (1)

2161-2184: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restrict the cold-cache fallback for experienceOwnsGame.

installedCapabilityPackages exposes React Query state, but experienceOwnsGame uses gameExperienceId !== null for the whole installed-package query lifecycle. If the owning package later becomes inactive/missing after loading, experienceOwnsGame stays true while experienceSurfaceActive stays false, permanently suppressing the built-in tutorial and session-panel help. Track a dedicated “installed-packages still loading” flag so the broader check only bridges the initial fetch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/GameSurface.tsx` around lines 2161 -
2184, Update the experienceOwnsGame logic near useInstalledCapabilityPackages so
the gameExperienceId fallback applies only while the
installed-capability-packages query is still loading, using its React Query
loading state. Once loading completes, rely exclusively on
experienceSurfaceActive so inactive or missing packages no longer suppress
built-in help.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/client/src/components/game/GameSurface.tsx`:
- Around line 2161-2184: Update the experienceOwnsGame logic near
useInstalledCapabilityPackages so the gameExperienceId fallback applies only
while the installed-capability-packages query is still loading, using its React
Query loading state. Once loading completes, rely exclusively on
experienceSurfaceActive so inactive or missing packages no longer suppress
built-in help.

In `@packages/client/src/components/game/NewGameExperienceChooser.tsx`:
- Around line 199-254: Update the fallback title in the dialog header rendered
by the active experience branch to use
localizeUi("navigation.chatSidebar.new.game") instead of the hardcoded "New
Game", matching GameSetupWizard.tsx while preserving the existing
activeExperience manifest name when available.

In
`@packages/server/src/services/capability-packages/capability-prompt-context.service.ts`:
- Around line 84-107: Update collectCapabilityPromptContext to invoke all
contributors concurrently by mapping contributorsByPackage entries to promises
and awaiting them with Promise.all, while retaining each contribution’s
withDeadline and non-fatal error handling. Process the resolved results
afterward in registration order so blocks remain deterministically composed and
provides aggregation is unchanged.

In `@packages/shared/src/schemas/capability-package.schema.ts`:
- Around line 46-64: Update the supportedCapabilityApi version from 1.7 to the
next minor release compatible with the added game-surface, prompt-context, and
optional resource write capabilities. Locate the supportedCapabilityApi
declaration and preserve the existing schema-version compatibility logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1f1cff59-e2c2-4176-a4b2-3f9cf2f266f4

📥 Commits

Reviewing files that changed from the base of the PR and between d0cb9ab and 589135b.

📒 Files selected for processing (14)
  • packages/client/src/components/game/GameNarration.tsx
  • packages/client/src/components/game/GameSetupWizard.tsx
  • packages/client/src/components/game/GameSurface.tsx
  • packages/client/src/components/game/NewGameExperienceChooser.tsx
  • packages/server/src/app.ts
  • packages/server/src/routes/game.routes.ts
  • packages/server/src/routes/generate.routes.ts
  • packages/server/src/services/capability-packages/capability-module-runtime.service.ts
  • packages/server/src/services/capability-packages/capability-prompt-context.service.ts
  • packages/server/src/services/capability-packages/capability-resources.service.ts
  • packages/server/src/services/game/gm-prompts.ts
  • packages/shared/src/schemas/capability-package.schema.ts
  • packages/shared/src/types/capability-runtime.ts
  • packages/shared/src/types/game.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/development/optional-agent-packages.md`:
- Line 24: Update the prompt-context description to state that contributions are
added to the system prompt of each generated Game turn, not every turn type. In
the same description, state that contributors are logged and skipped when they
throw or fail to settle within the two-second deadline, while preserving the
existing behavior for contributors that return nothing.

In `@packages/client/src/components/game/GameSurface.tsx`:
- Around line 10277-10284: Update the setup flow around NewGameExperienceChooser
and its experience-owned gameSetup.mutateAsync handling so JSON-repairable
failures are forwarded to handleJsonRepairError, allowing GameJsonRepairModal to
render for experience setups. Ensure useGameSetup does not stop the repair
request at its info-toast handling path, while preserving the existing
classicSetup behavior.
- Around line 11725-11737: Update the choicesSlot ternary in GameSurface so
experienceSurfaceActive takes precedence over the activeChoices && narrationDone
Classic branch, keeping the ExperienceChoiceSlot anchor mounted whenever the
experience surface is active. Alternatively, add and propagate a providesChoices
declaration alongside providesPlayerInput in ExperienceChromeDeclaration, then
suppress Classic GameChoiceCards when that declaration is enabled; ensure
experience-owned menus retain a stable portal target even when shared narration
emits choices.

In `@packages/client/src/components/game/NewGameExperienceChooser.tsx`:
- Around line 24-32: Move the game-surface package eligibility helper from
GameSurface.tsx into the capability-package helpers module and export it for
reuse. Update NewGameExperienceChooser to use that shared helper with
useInstalledCapabilityPackages, removing the local InstalledPkg type and any
cast; preserve filtering for active packages whose manifest contributions
include the "game-surface" slot.

In
`@packages/server/src/services/capability-packages/capability-prompt-context.service.ts`:
- Around line 61-107: The optional improvement is to unref the timeout created
in withDeadline so it cannot keep the Node.js event loop alive during shutdown.
Update the timer setup in withDeadline while preserving its existing rejection
and finally-based cleanup behavior.

In `@packages/server/src/services/game/gm-prompts.ts`:
- Around line 629-630: Update the persona-stats inventory tracking flow to honor
experienceProvidedSystems.inventory (or the existing experienceOwnsInventory
helper) when an installed experience owns inventory. Skip the inventory prompt
tracking and playerStats.inventory updates in that case, while preserving
current behavior when inventory remains built-in.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: efa462a5-55c9-4a95-a78f-b8df430d6c26

📥 Commits

Reviewing files that changed from the base of the PR and between ce90b08 and 589135b.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • docs/development/optional-agent-packages.md
  • packages/client/src/components/game/GameNarration.tsx
  • packages/client/src/components/game/GameSetupWizard.tsx
  • packages/client/src/components/game/GameSurface.tsx
  • packages/client/src/components/game/NewGameExperienceChooser.tsx
  • packages/client/src/localization/locales/en.json
  • packages/server/src/app.ts
  • packages/server/src/routes/game.routes.ts
  • packages/server/src/routes/generate.routes.ts
  • packages/server/src/services/capability-packages/capability-module-runtime.service.ts
  • packages/server/src/services/capability-packages/capability-prompt-context.service.ts
  • packages/server/src/services/capability-packages/capability-resources.service.ts
  • packages/server/src/services/game/gm-prompts.ts
  • packages/shared/src/schemas/capability-package.schema.ts
  • packages/shared/src/types/capability-runtime.ts
  • packages/shared/src/types/game.ts

Comment thread docs/development/optional-agent-packages.md Outdated
Comment thread packages/client/src/components/game/GameSurface.tsx Outdated
Comment thread packages/client/src/components/game/GameSurface.tsx
Comment thread packages/client/src/components/game/NewGameExperienceChooser.tsx Outdated
Comment thread packages/server/src/services/game/gm-prompts.ts
experienceOwnsGame stayed true for the whole life of a game created with an
experience, because gameExperienceId lives in the chat metadata and never
clears. If the package was later uninstalled the surface stopped mounting but
the flag did not, so the game lost the experience UI and the built-in help
button and tutorial with it. The broad reading now applies only while the
installed-packages query is still in flight, which is the cold-cache flash it
was written for; once the query settles the flag follows the surface.

Use the existing navigation.chatSidebar.new.game key for the setup panel title
instead of a hardcoded English fallback, matching GameSetupWizard.

Document the exported capability input types, the contributor type and the new
deadline helper.
Add providesChoices to the chrome declaration. The choices slot gave the Classic
branch priority, so a turn whose narration emitted choices rendered the built-in
cards over an experience that has its own menu, and unmounted the in-flow anchor
that menu is portaled into. Avoiding that was left to the package's prompt
discipline, which the host should not depend on. Declaring it is opt-in: an
experience that says nothing keeps the Classic cards exactly as before.

Route setup failures through the JSON repair modal on the experience path. The
modal only mounted inside the built-in wizard, so a malformed opening response
was repairable in one setup path and a dead end in the other. It now mounts
outside the chooser, and a launch failure is offered to the host before it is
rethrown.

Share the game-experience eligibility filter between GameSurface and the chooser
instead of spelling it out twice, which also drops a local type and its cast.

Unref the contributor deadline so a pending guard cannot hold the event loop
open, and correct the prompt-context paragraph in the package docs: it applies
to Game turns, and a contributor that exceeds its deadline is skipped the same
way one that throws is.
Found while playtesting the experience surface: choices offered by one game were
still on screen in the next one, and answerable there, against a GM that never
asked them. An active QTE leaks the same way, and worse, since it carries a
timer that then fires in the new game.

Neither is caused by this PR. Every site that clears them is driven by a player
action or by the session concluding, and the effect keyed on the scope resets
about twenty other pieces of turn state without touching these two. Switching
between two ordinary games has always done this.

It surfaces here because an experience draws its own menu, so the stale cards
render beside it and read as the new mode double-listing its options. Fixed in
the effect that already owns this reset rather than filed separately, since
resetting the experience seam state there while leaving these two leaking would
be arbitrary. This does change Classic behaviour, which the PR description now
says explicitly.
@SonaGat3

SonaGat3 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Second round. Three more commits: the four remaining items from the first pass, everything from the full
review, and one bug the playtest turned up.

From the full review

providesChoices on the chrome declaration. You were right that the host was depending on the
package's prompt discipline. I took the declarative option rather than giving the surface precedence, and
it turned out to matter: the package I develop against deliberately relies on the Classic choice cards
for in-scene [choices:] and steps its own menu aside when they appear. Making the surface win
unconditionally would have broken it. Declared, the anchor stays mounted and the Classic cards stand down;
undeclared, nothing changes at all.

JSON repair on the experience path. Confirmed and fixed. The modal only mounted inside the built-in
wizard, so the same malformed opening was repairable in one setup path and a dead end in the other. It now
mounts outside the chooser, and a launch failure is offered to the host before being rethrown.

The experienceOwnsGame fallback is now scoped to the in-flight window of the installed-packages
query, so a game whose package was uninstalled gets the built-in chrome back instead of losing both. Also
applied: the shared eligibility helper (which drops a local type and its cast), unref on the deadline,
the localization key for the panel title, and the docs correction — it is Game turns specifically, and a
contributor that exceeds its deadline is skipped the same way one that throws is.

Found while playtesting — pre-existing, fixed here

Choices offered by one game were still on screen in the next one and answerable there, against a GM that
never asked them. An active QTE leaks the same way and is worse, since it carries a timer that then fires
in the new game.

This is not caused by this PR. Every site that clears either one is driven by a player action or by the
session concluding, and the effect keyed on the scope resets about twenty other pieces of turn state
without touching these two — switching between two ordinary games has always done this. It surfaces here
because an experience draws its own menu, so the stale cards render beside it and read as the new mode
double-listing its options.

I fixed it in the effect that already owns that reset rather than filing it separately, since resetting
the experience seam state there while leaving these two leaking would be arbitrary. This does change
Classic behaviour
, so I have said so explicitly in the description rather than leaving the earlier
"unchanged" claim standing. Happy to pull it into its own PR if you'd rather keep this one strictly to the
feature.

Still open for you

The capability API version. Still at 1.7, still deliberately not bumped.

The index signature on CapabilityLorebookEntryInput. Unchanged. These entries go to
lorebooks.bulkCreateEntries, which accepts more than the three named fields, so spelling them all out
would couple this published type to the engine's internal entry shape and make every field added to
storage a change here. Your point about misspellings is fair and a nested bag trades one cost for the
other — it is a call about where you want that published surface to sit, and I will take it either way.

Running contributors concurrently. Still sequential. They compose one prompt, so block order has to be
stable, and "registration order" says that more clearly than relying on Promise.all preserving array
order. Your new point is fair though — the deadline does make the sequential worst case additive at
N × 2s. With one contributor it is moot, and I would rather change it when a second one exists and the
ordering contract can be settled deliberately.

The client-side inventory parser. Left as is, and I checked the consequence rather than assuming it.
buildSessionSummaryPrompt takes only a language, so statsSnapshot is written by the model from the
transcript, not from engine state; across the server, playerInventory reaches the GM prompt in exactly
one place, buildGmFormatReminder, which the provides declaration already gates. The only other reader
is encounter generation, which an experience declaring providesCombat never starts. So a tag the GM is
no longer asked to emit would land in a panel the player cannot see and go nowhere else. Gating the parser
would mean another host-side gate for state with no path to the prompt.

Validation

pnpm check passes. pnpm regression:prompt is at 147 passing with the one failure that reproduces
identically on unmodified staging (Storyboard host consumes package-owned prompts without restoring Engine globals), unchanged throughout.

Beyond that I exercised the parts that types cannot cover: the contributor deadline against the built
service (a contributor that never settles is dropped at the deadline without delaying the others, a
throwing one is still skipped, registration order and the provides union hold, no timer is left behind),
and both gate changes as truth tables — the experienceOwnsGame change alters exactly one case out of
five, and providesChoices exactly one out of eight, in each instance the broken one. And the surface
itself was played against a real installed package, which is how the leak above turned up.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== PR metadata ==='
gh pr view 4528 --repo Pasta-Devs/Marinara-Engine --json baseRefName,headRefName,commits,files --jq '{baseRefName, headRefName, commitCount:(.commits|length), files:[.files[].path]}'

echo '=== Changed files and diff stat ==='
BASE="$(git merge-base "origin/$(gh pr view 4528 --repo Pasta-Devs/Marinara-Engine --json baseRefName --jq .baseRefName)" HEAD)"
git diff --stat "$BASE"...HEAD
git diff --name-only "$BASE"...HEAD

echo '=== Outlines for changed TypeScript/TSX files ==='
git diff --name-only "$BASE"...HEAD \
  | grep -E '\.(ts|tsx)$' \
  | while IFS= read -r file; do
      echo "--- $file ---"
      ast-grep outline "$file" --items all || true
    done

echo '=== Current diff ==='
git diff --find-renames --find-copies --unified=80 "$BASE"...HEAD

Length of output: 2301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Local refs ==='
git branch -a
git remote -v

echo '=== PR patch ==='
gh pr diff 4528 --repo Pasta-Devs/Marinara-Engine --patch

echo '=== Changed-file outlines ==='
for file in \
  packages/client/src/components/game/GameSurface.tsx \
  packages/client/src/components/game/NewGameExperienceChooser.tsx \
  packages/client/src/hooks/use-capability-packages.ts \
  packages/server/src/routes/generate.routes.ts \
  packages/server/src/services/capability-packages/capability-prompt-context.service.ts \
  packages/server/src/services/capability-packages/capability-module-runtime.service.ts \
  packages/server/src/services/game/gm-prompts.ts \
  packages/shared/src/schemas/capability-package.schema.ts \
  packages/shared/src/types/capability-runtime.ts \
  packages/server/src/routes/game.routes.ts; do
  echo "--- $file ---"
  ast-grep outline "$file" --items all || true
done

Length of output: 167171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Prompt-context implementation and registrations ==='
sed -n '1,150p' packages/server/src/services/capability-packages/capability-prompt-context.service.ts
rg -n -C 5 'registerPromptContext|collectCapabilityPromptContext|CapabilityPromptContextContributor' \
  packages/server packages/shared

echo '=== QTE and choice state: initialization, timer, cleanup, scope reset ==='
rg -n -C 8 'activeQte|queuedQte|setActiveChoices|setActiveQte|setQueuedQte|sceneRuntimeScopeKey' \
  packages/client/src/components/game/GameSurface.tsx

echo '=== Experience setup launch and JSON repair handler ==='
sed -n '1,310p' packages/client/src/components/game/NewGameExperienceChooser.tsx
rg -n -C 10 'handleJsonRepairError|jsonRepairRequest|GameJsonRepairModal|useGameSetup' \
  packages/client/src/components/game/GameSurface.tsx packages/client/src/hooks packages/client/src/lib

echo '=== Experience package query behavior ==='
sed -n '1,90p' packages/client/src/hooks/use-capability-packages.ts
rg -n -C 5 'useInstalledCapabilityPackages|capabilityPackageKeys\.installed|installed\(' \
  packages/client/src

echo '=== Capability runtime public API and manifest compatibility ==='
sed -n '35,195p' packages/server/src/services/capability-packages/capability-module-runtime.service.ts
sed -n '1,155p' packages/shared/src/schemas/capability-package.schema.ts
sed -n '1,145p' packages/shared/src/types/capability-runtime.ts

Length of output: 136299


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== CapabilityElement behavior and client-module loader ==='
ast-grep outline packages/client/src/components/capabilities/CapabilityElement.tsx --items all
sed -n '1,300p' packages/client/src/components/capabilities/CapabilityElement.tsx
sed -n '150,315p' packages/client/src/hooks/use-capability-packages.ts

echo '=== Capability package readiness and entrypoint validation ==='
rg -n -C 8 'entrypoints\.client|clientEntrypoint|runtimeBlockReason|markRuntimeStatus|isInstalledCapabilityReady' \
  packages/server/src/services/capability-packages packages/shared/src

echo '=== Prompt-context tests and deadline tests ==='
fd -t f . packages/server | grep -E '(test|spec)' | while IFS= read -r file; do
  rg -n -C 4 'collectCapabilityPromptContext|registerCapabilityPromptContext|prompt-context|CONTRIBUTOR_TIMEOUT' "$file" && echo "--- $file ---"
done || true

echo '=== Existing tests for game-surface selection ==='
fd -t f . packages/client packages/shared | grep -E '(test|spec)' | while IFS= read -r file; do
  rg -n -C 4 'game-surface|selectGameExperiencePackages|NewGameExperienceChooser' "$file" && echo "--- $file ---"
done || true

Length of output: 43393


@SonaGat3 I completed the full review. I found two issues.

  1. High — apply the contributor deadline to thenables.
    In packages/server/src/services/capability-packages/capability-prompt-context.service.ts, Line 72, withDeadline applies Promise.race only when value instanceof Promise. A capability can return a Promise-like thenable at runtime. The Promise.resolve(value) path assimilates that thenable without a timer. If it never settles, collectCapabilityPromptContext waits indefinitely and blocks the Game turn.

    Apply the deadline to Promise.resolve(value) for all returned values. Keep synchronous values fast by resolving them through the same race or by detecting only non-object values. Add a regression case for a non-settling thenable.

  2. Medium — require a client entry point for game-surface.
    In packages/client/src/hooks/use-capability-packages.ts, Line 59, selectGameExperiencePackages accepts any active package that declares game-surface. The manifest permits entrypoints.client to be absent. The client-module loader excludes such packages, so the chooser can let a user select an experience that cannot render either its setup view or its game surface.

    Require entrypoints.client when a manifest declares the game-surface slot. Enforce this in package manifest or installation validation. Also include it in selectGameExperiencePackages as defense in depth.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/shared/src/types/capability-runtime.ts (1)

116-125: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Scope capability resource writes to package-owned records.

updatePersona, updateLorebook, bulkCreateLorebookEntries, and removeLorebookEntry accept raw IDs and pass them directly to storage in packages/server/src/services/capability-packages/capability-resources.service.ts. A privileged package can update or delete unrelated user-created data. Enforce ownership for these writes; if any packages are intentionally trusted with global write access, document the privilege and add boundary tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/types/capability-runtime.ts` around lines 116 - 125,
Scope the write methods in CapabilityResourcesService—updatePersona,
updateLorebook, bulkCreateLorebookEntries, and removeLorebookEntry—to
package-owned records before invoking storage, rejecting or ignoring unrelated
user-owned IDs. Reuse the existing package ownership or authorization mechanism
where available, and document plus test any intentionally trusted package with
global write access.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/shared/src/types/capability-runtime.ts`:
- Around line 116-125: Scope the write methods in
CapabilityResourcesService—updatePersona, updateLorebook,
bulkCreateLorebookEntries, and removeLorebookEntry—to package-owned records
before invoking storage, rejecting or ignoring unrelated user-owned IDs. Reuse
the existing package ownership or authorization mechanism where available, and
document plus test any intentionally trusted package with global write access.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4d9d0b1c-0cbb-4e10-89f7-4274f91e9a22

📥 Commits

Reviewing files that changed from the base of the PR and between 589135b and c2a5d8f.

📒 Files selected for processing (6)
  • docs/development/optional-agent-packages.md
  • packages/client/src/components/game/GameSurface.tsx
  • packages/client/src/components/game/NewGameExperienceChooser.tsx
  • packages/client/src/hooks/use-capability-packages.ts
  • packages/server/src/services/capability-packages/capability-prompt-context.service.ts
  • packages/shared/src/types/capability-runtime.ts

…t entrypoint

The deadline only raced a native Promise. A contributor returning any other
thenable fell through to Promise.resolve, which ADOPTS it, so one that never
settled slipped past the guard and hung the turn -- the exact failure the
deadline exists to prevent. Anything that can defer now races it; only a
primitive is handed straight back, so the common synchronous return still costs
no timer.

Require entrypoints.client on a manifest declaring the game-surface slot. The
module loader skips a package without one, so it could be offered in the setup
wizard and then render neither its setup view nor its surface. Rejected at
manifest validation, with a matching check where the chooser and GameSurface
resolve experiences, for anything installed before the rule existed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/shared/src/schemas/capability-package.schema.ts (1)

39-64: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Require entrypoints.client when a package declares game-surface.

contributions.slots accepts "game-surface" independently of entrypoints.client, and .extend() is applied to the base schema before the exported discriminated union. Add the cross-field check to the exported capabilityPackageManifestSchema union so installed packages with a game surface cannot pass manifest validation without a client entry point. Also ensure client-side experience selection rejects or hides installed packages that declare game-surface without entrypoints.client.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/schemas/capability-package.schema.ts` around lines 39 -
64, Require an `entrypoints.client` value whenever `contributions.slots`
includes `"game-surface"` by adding the cross-field validation to the exported
`capabilityPackageManifestSchema` discriminated union, after the base schema
extension. Update client-side experience selection to reject or hide installed
manifests declaring `"game-surface"` without `entrypoints.client`, using the
existing selection logic and symbols.
packages/server/src/routes/game.routes.ts (1)

1676-1703: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make the classic game fields optional for experience-driven creation. createGameSchema still uses gameSetupConfigSchema, which requires genre, setting, tone, difficulty, gmMode, and partyCharacterIds; NewGameExperienceChooser only stamps gameExperienceId, so packages that do not populate those fields will fail at /game/create. Relax or provide defaults for those fields when gameExperienceId is present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/routes/game.routes.ts` around lines 1676 - 1703, The
gameSetupConfigSchema requires classic game fields even for experience-driven
creation. Update createGameSchema and its gameSetupConfigSchema integration so
genre, setting, tone, difficulty, gmMode, and partyCharacterIds are optional or
receive appropriate defaults when gameExperienceId is present, while preserving
required validation for classic creation;
packages/client/src/components/game/NewGameExperienceChooser.tsx requires no
direct change because it only supplies gameExperienceId.
packages/client/src/components/game/GameSurface.tsx (1)

11669-11681: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate the mobile HUD widget slot behind experienceSurfaceActive.

The desktop HUD widget rail excludes experienceSurfaceActive (Line 12247), but mobileWidgetSlot does not. It renders MobileWidgetPanel whenever hudWidgets.length > 0, with no check for an active experience surface. The compact "MobileChoiceStage" widget rails (Lines 11699-11719) have the same gap.

hudWidgets can stay populated for an experience-owned game, because [widget:] tags from the GM model are applied unconditionally (Line 4717-4723). As a result, a mobile player can see the built-in HUD widgets overlapping the package's own HUD, even though the package is supposed to fully replace it. Desktop players do not see this because the rail already excludes experienceSurfaceActive.

Add the same !experienceSurfaceActive condition to mobileWidgetSlot and to the compact widget rails.

🐛 Proposed fix
                   const mobileWidgetSlot =
-                    !combatUiActive && hudWidgets.length > 0 && !(compactHudWidgets && choicesVisible) ? (
+                    !combatUiActive &&
+                    !experienceSurfaceActive &&
+                    hudWidgets.length > 0 &&
+                    !(compactHudWidgets && choicesVisible) ? (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/GameSurface.tsx` around lines 11669 -
11681, Add !experienceSurfaceActive to the mobileWidgetSlot visibility condition
and to both compact MobileChoiceStage widget rail conditions. Preserve the
existing combat, widget-count, and choicesVisible checks while ensuring no
built-in mobile HUD widgets render when an experience surface is active.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/client/src/components/game/NewGameExperienceChooser.tsx`:
- Around line 85-96: Normalize setupConfig to a non-null object before accessing
promptPresetId in the onLaunch flow, rather than relying on the direct cast to {
promptPresetId?: string }. Preserve the existing gameExperienceId merge and pass
promptPresetId from the normalized object, ensuring null or other non-object
setupConfig values do not cause a property-access throw.

In `@packages/client/src/hooks/use-capability-packages.ts`:
- Around line 52-63: Update selectGameExperiencePackages to require a client
entry point in addition to active status and the game-surface slot. Check
manifest.entrypoints.client in the existing filter so only packages that
ExperienceOwnsGame can mount are returned.

In
`@packages/server/src/services/capability-packages/capability-prompt-context.service.ts`:
- Around line 71-84: Update withDeadline to detect promise-like values via a
callable .then property instead of instanceof Promise, so non-native thenables
are normalized and included in the timeout race. Preserve immediate resolution
for ordinary non-thenable values, and add a regression test covering a
non-native thenable that never settles and must reject after
CONTRIBUTOR_TIMEOUT_MS.

---

Outside diff comments:
In `@packages/client/src/components/game/GameSurface.tsx`:
- Around line 11669-11681: Add !experienceSurfaceActive to the mobileWidgetSlot
visibility condition and to both compact MobileChoiceStage widget rail
conditions. Preserve the existing combat, widget-count, and choicesVisible
checks while ensuring no built-in mobile HUD widgets render when an experience
surface is active.

In `@packages/server/src/routes/game.routes.ts`:
- Around line 1676-1703: The gameSetupConfigSchema requires classic game fields
even for experience-driven creation. Update createGameSchema and its
gameSetupConfigSchema integration so genre, setting, tone, difficulty, gmMode,
and partyCharacterIds are optional or receive appropriate defaults when
gameExperienceId is present, while preserving required validation for classic
creation; packages/client/src/components/game/NewGameExperienceChooser.tsx
requires no direct change because it only supplies gameExperienceId.

In `@packages/shared/src/schemas/capability-package.schema.ts`:
- Around line 39-64: Require an `entrypoints.client` value whenever
`contributions.slots` includes `"game-surface"` by adding the cross-field
validation to the exported `capabilityPackageManifestSchema` discriminated
union, after the base schema extension. Update client-side experience selection
to reject or hide installed manifests declaring `"game-surface"` without
`entrypoints.client`, using the existing selection logic and symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fabb6466-a3a7-41cb-93e2-388fa208f58b

📥 Commits

Reviewing files that changed from the base of the PR and between ce90b08 and c2a5d8f.

📒 Files selected for processing (18)
  • CHANGELOG.md
  • docs/development/optional-agent-packages.md
  • packages/client/src/components/game/GameNarration.tsx
  • packages/client/src/components/game/GameSetupWizard.tsx
  • packages/client/src/components/game/GameSurface.tsx
  • packages/client/src/components/game/NewGameExperienceChooser.tsx
  • packages/client/src/hooks/use-capability-packages.ts
  • packages/client/src/localization/locales/en.json
  • packages/server/src/app.ts
  • packages/server/src/routes/game.routes.ts
  • packages/server/src/routes/generate.routes.ts
  • packages/server/src/services/capability-packages/capability-module-runtime.service.ts
  • packages/server/src/services/capability-packages/capability-prompt-context.service.ts
  • packages/server/src/services/capability-packages/capability-resources.service.ts
  • packages/server/src/services/game/gm-prompts.ts
  • packages/shared/src/schemas/capability-package.schema.ts
  • packages/shared/src/types/capability-runtime.ts
  • packages/shared/src/types/game.ts

Comment thread packages/client/src/components/game/NewGameExperienceChooser.tsx Outdated
Comment thread packages/client/src/hooks/use-capability-packages.ts
…nfig defensively

The desktop widget rail was gated on the experience surface but the mobile slot
and the compact rails beside the choice stage were not, so a package that draws
its own HUD could still have the built-in widgets rendered over it on a narrow
viewport. All three render paths now share the same condition.

Read the setup config the package returns defensively as well: it is
package-supplied, so a null or non-object value threw on property access here
instead of failing validation with a message the player can act on.
@SonaGat3

SonaGat3 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Both rounds addressed. Five of the seven findings are fixed; notes on the other two below.

The deadline now covers thenables. This was the worst of the batch and worth saying plainly: the guard
looked like it closed the hole while leaving it open. withDeadline only raced a native Promise, so
anything else fell through to Promise.resolve, which adopts a thenable — one that never settles slipped
straight past and hung the turn, which is exactly what the deadline was added to prevent. Anything that can
defer now races it; only a primitive is handed straight back, so the ordinary synchronous return still
costs no timer.

Verified against the built service, since types cannot catch this: a thenable that never settles is dropped
at the deadline (2.0s, previously an indefinite hang), one that resolves is used, one that rejects is
skipped, and a plain-object return resolves in 0 ms — the race adds no latency to the normal path.
Registration order, the provides union and the existing string, promise and null paths are unchanged.

A game-surface package must declare a client entrypoint. Correct — the module loader skips a package
without one, so the wizard could offer an experience that then renders neither its setup view nor its
surface. Enforced as a cross-field rule on the manifest union, so it fails at install with a message naming
the reason, and the shared helper where the chooser and GameSurface both resolve experiences carries the
same check for anything installed before the rule existed. No catalog package declares game-surface
today, so nothing currently installable is affected.

The built-in HUD widgets are now hidden on mobile too. A real gap on my side: I gated the desktop rail
and left the mobile slot and the compact rails beside the choice stage ungated, so a package drawing its
own HUD could still have the built-in widgets rendered over it on a narrow viewport. All three render paths
now share the same condition.

The package's setup config is read defensively. Also fair — it is package-supplied, so a null or
non-object return threw on property access instead of failing validation with a message the player can act
on.

Not applied

Relaxing the classic fields in gameSetupConfigSchema for experience-driven creation. I think this one
rests on a misreading. The chooser does not supply only gameExperienceId — it spreads the entire config
the package produced and adds that one field:

setupConfig: { ...cfg, gameExperienceId: activeId }

An experience runs its own setup wizard and returns a complete GameSetupConfig; the host then creates a
real game from it, with a real GM prompt, party and world. Those fields are required because the game
genuinely needs them, and relaxing them would let a package create a half-formed game that fails later and
further from the cause. The current shape also keeps the contract honest: whatever an experience replaces
in the UI, the game it creates is still a Game-mode game.

Scoping the resource writes to package-owned records. The underlying concern is real, so it is worth
answering with specifics rather than waving it off.

A package's activate(context) receives context.app, the Fastify instance
(capability-module-runtime.service.ts), and app.db is the database itself — app.decorate("db", db) in
app.ts. Every package already has unrestricted read and write access to all storage, before this PR and
independently of it. Scoping these four methods would leave the sanctioned path narrower than the
unsanctioned one sitting beside it, which buys the user nothing.

There is also no ownership model to reuse: nothing associates a persona or a lorebook with the package that
created it, and the reads this facade already exposed are not scoped either.

So packages are broadly privileged by design today, and that is an engine-wide property rather than
something these six methods introduce. Changing it looks like real work to me — ownership on the records,
scoping reads as well as writes, and deciding what happens to a package's data when it is uninstalled — and
I would rather not bolt a partial version onto this PR. Happy to open a separate issue with what I found,
or to implement it here if you would prefer it land together.

pnpm check passes; pnpm regression:prompt is at 147 with the one failure that reproduces on unmodified
staging.

@coderabbitai the last two items are deliberate and explained above — please take them into account rather
than re-raising them. No need for another full review; the push already covers the changed files.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@SonaGat3 Acknowledged.

The thenable deadline and the game-surface client-entry-point validation address the two high-impact findings. The mobile HUD gating and defensive setup-config validation also address the reported UI and error-handling gaps.

I accept the two deliberate decisions for this PR:

  • An experience setup must return a complete GameSetupConfig. The required classic fields remain necessary for valid Game-mode creation.
  • Resource writes cannot be meaningfully scoped until the engine has a package ownership model and removes or scopes the existing unrestricted app.db access.

I will not re-raise either item in this PR.


✏️ Learnings added
Learnt from: SonaGat3
URL: https://github.com/Pasta-Devs/Marinara-Engine/pull/4528

Timestamp: 2026-08-03T19:22:26.488Z
Learning: For game experiences in `packages/client/src/components/game/NewGameExperienceChooser.tsx` and `packages/server/src/routes/game.routes.ts`, an experience setup must return a complete `GameSetupConfig`. The chooser spreads the package-provided config and adds `gameExperienceId`; the required Game setup fields must remain required because the created game still needs a GM prompt, party, and world.

Learnt from: SonaGat3
URL: https://github.com/Pasta-Devs/Marinara-Engine/pull/4528

Timestamp: 2026-08-03T19:22:26.488Z
Learning: Capability packages receive `context.app` in `packages/server/src/services/capability-packages/capability-module-runtime.service.ts`, and `app.db` provides unrestricted database access. Persona and lorebook resource-facade writes cannot be meaningfully package-scoped until the engine adds record ownership and scopes both direct database access and resource reads.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/client/src/components/game/NewGameExperienceChooser.tsx (2)

58-60: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fall back when the selected experience is no longer eligible.

activeId can outlive experiences. If the selected package becomes inactive or uninstalled, activeExperience becomes null, but the !activeId guard still selects package mode and mounts CapabilityElement with the stale package ID.

Derive a validated selected ID from experiences and use it for rendering and gameExperienceId, or clear activeId when the package disappears.

Proposed stale-selection guard
+  const selectedExperienceId =
+    activeId && experiences.some((experience) => experience.id === activeId) ? activeId : null;
-  const activeExperience = experiences.find((e) => e.id === activeId) ?? null;
+  const activeExperience = experiences.find((e) => e.id === selectedExperienceId) ?? null;

Use selectedExperienceId in the Classic/package branch, CapabilityElement, and launch payload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/NewGameExperienceChooser.tsx` around
lines 58 - 60, Derive a validated selected experience ID from `experiences` by
retaining `activeId` only when it matches an eligible package; otherwise fall
back to the non-package mode. Update the Classic/package rendering condition,
`CapabilityElement` props, and launch payload to use this validated ID instead
of the stale `activeId`, while preserving `activeExperience` lookup behavior.

117-120: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Refresh resource caches when setup fails.

These invalidations run only after gameSetup.mutateAsync succeeds. A package can write personas or lorebooks before calling onLaunch; if opening generation fails, the catch block rethrows before these calls run. The JSON repair path can then complete while the persona cache still contains old data.

Move the invalidations into a finally that covers the full launch. Await them without masking the original setup error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/NewGameExperienceChooser.tsx` around
lines 117 - 120, Update the launch flow around gameSetup.mutateAsync and the
existing persona/lorebook invalidateQueries calls so both cache invalidations
run in a finally covering the full launch, including setup and generation
failures. Await the invalidation promises while preserving and rethrowing the
original setup error if one occurs.
♻️ Duplicate comments (1)
packages/client/src/components/game/GameSurface.tsx (1)

6435-6439: 🔒 Security & Privacy | 🟡 Minor

Reject raw package background values.

The previous review flagged this fallback, and it is still present. When the asset-map lookup fails, the package-controlled value reaches displayedBackground, DirectionEngine.backgroundUrl, and the experience underlay unchanged.

This permits browser-side arbitrary resource loading and weakens the CSS URL trust boundary. It is not server-side SSRF, but it still exposes users to unvalidated package-controlled URLs. Resolve only manifest-backed assets or enforce a strict allowlist. Do not pass the raw fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/GameSurface.tsx` around lines 6435 -
6439, Update pushExperienceBackground so package-provided background values are
used only when they resolve through scopedAssetMap to a manifest-backed path;
remove the raw value fallback and set the background to null for unresolved
entries. Preserve null handling and ensure displayedBackground,
DirectionEngine.backgroundUrl, and the experience underlay receive no
unvalidated package-controlled URL.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/client/src/hooks/use-capability-packages.ts`:
- Around line 61-68: Update selectGameExperiencePackages to require
isInstalledCapabilityReady(pkg) in addition to the existing active status,
game-surface contribution, and client entrypoint checks, matching the
client-module loader’s eligibility criteria.

In `@packages/shared/src/schemas/capability-package.schema.ts`:
- Line 146: Update the manifest validation condition around the client
entrypoint check to reject missing or whitespace-only
`manifest.entrypoints.client` values when the `game-surface` slot is declared.
Trim the value before validating it while preserving acceptance of non-blank
client module identifiers.

---

Outside diff comments:
In `@packages/client/src/components/game/NewGameExperienceChooser.tsx`:
- Around line 58-60: Derive a validated selected experience ID from
`experiences` by retaining `activeId` only when it matches an eligible package;
otherwise fall back to the non-package mode. Update the Classic/package
rendering condition, `CapabilityElement` props, and launch payload to use this
validated ID instead of the stale `activeId`, while preserving
`activeExperience` lookup behavior.
- Around line 117-120: Update the launch flow around gameSetup.mutateAsync and
the existing persona/lorebook invalidateQueries calls so both cache
invalidations run in a finally covering the full launch, including setup and
generation failures. Await the invalidation promises while preserving and
rethrowing the original setup error if one occurs.

---

Duplicate comments:
In `@packages/client/src/components/game/GameSurface.tsx`:
- Around line 6435-6439: Update pushExperienceBackground so package-provided
background values are used only when they resolve through scopedAssetMap to a
manifest-backed path; remove the raw value fallback and set the background to
null for unresolved entries. Preserve null handling and ensure
displayedBackground, DirectionEngine.backgroundUrl, and the experience underlay
receive no unvalidated package-controlled URL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a537434a-da1e-4f2e-8195-efc07c41d884

📥 Commits

Reviewing files that changed from the base of the PR and between c2a5d8f and daf0e9e.

📒 Files selected for processing (5)
  • packages/client/src/components/game/GameSurface.tsx
  • packages/client/src/components/game/NewGameExperienceChooser.tsx
  • packages/client/src/hooks/use-capability-packages.ts
  • packages/server/src/services/capability-packages/capability-prompt-context.service.ts
  • packages/shared/src/schemas/capability-package.schema.ts

Comment thread packages/client/src/hooks/use-capability-packages.ts Outdated
Comment thread packages/shared/src/schemas/capability-package.schema.ts Outdated
…emembered id

Two findings from the last pass, both in the setup chooser.

The selected id outlived the package it named: uninstalling or deactivating an
experience while its setup panel is open left activeId set, so the panel still
mounted a capability element for a package that was no longer there. Rendering
and the id stamped onto the game now come from the resolved experience, so an
unresolvable selection falls back to the built-in wizard.

Move the persona and lorebook cache invalidations into a finally. A package can
write both before it ever calls the host launcher, so a failed opening
generation left those records in storage with nothing on the client aware of
them.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/client/src/components/game/NewGameExperienceChooser.tsx (1)

96-124: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Invalidate resource queries when game creation fails.

If createGame.mutateAsync rejects on Line 96, execution never enters the try block on Line 105. Persona and lorebook writes made before onLaunch then remain stale in the client cache.

Wrap game creation in the outer try/finally. Keep onSetupError scoped to the opening-generation request.

Proposed fix
-      const res = await createGame.mutateAsync({
-        name: gameName,
-        setupConfig: { ...cfg, gameExperienceId: selectedId } as unknown,
-        preferences: "",
-        chatId: activeChatId,
-        connectionId,
-        promptPresetId,
-      } as Parameters<typeof createGame.mutateAsync>[0]);
-      const chatId = res.sessionChat.id;
       try {
-        await gameSetup.mutateAsync({
-          chatId,
+        const res = await createGame.mutateAsync({
+          name: gameName,
+          setupConfig: { ...cfg, gameExperienceId: selectedId } as unknown,
+          preferences: "",
+          chatId: activeChatId,
           connectionId,
-          preferences: "",
-          promptPresetId: promptPresetId ?? null,
+          promptPresetId,
         } as Parameters<typeof gameSetup.mutateAsync>[0]);
-      } catch (error) {
-        onSetupError(error);
-        throw error;
+        const chatId = res.sessionChat.id;
+        try {
+          await gameSetup.mutateAsync({
+            chatId,
+            connectionId,
+            preferences: "",
+            promptPresetId: promptPresetId ?? null,
+          } as Parameters<typeof gameSetup.mutateAsync>[0]);
+        } catch (error) {
+          onSetupError(error);
+          throw error;
+        }
+        return chatId;
       } finally {
         queryClient.invalidateQueries({ queryKey: characterKeys.personas });
         queryClient.invalidateQueries({ queryKey: lorebookKeys.all });
       }
-      return chatId;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/NewGameExperienceChooser.tsx` around
lines 96 - 124, Wrap the createGame.mutateAsync call and subsequent setup flow
in an outer try/finally so persona and lorebook query invalidation always runs
when game creation or setup fails. Keep onSetupError confined to the
gameSetup.mutateAsync catch block, while preserving rethrow behavior and the
existing resource query keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/client/src/components/game/NewGameExperienceChooser.tsx`:
- Around line 96-124: Wrap the createGame.mutateAsync call and subsequent setup
flow in an outer try/finally so persona and lorebook query invalidation always
runs when game creation or setup fails. Keep onSetupError confined to the
gameSetup.mutateAsync catch block, while preserving rethrow behavior and the
existing resource query keys.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ddf8d53d-2ab1-4e0b-886f-44c005b49bdb

📥 Commits

Reviewing files that changed from the base of the PR and between daf0e9e and 00bfd86.

📒 Files selected for processing (1)
  • packages/client/src/components/game/NewGameExperienceChooser.tsx

…s too

The finally only wrapped the opening generation, so a rejection from game
creation skipped it entirely. A package can write the player persona and a
lorebook before it ever calls the host launcher, which means either step failing
left those records in storage with nothing on the client aware of them. The
block now spans both, with the repair routing still scoped to generation.

Document the named seam helpers while here.
It was mine, not the review's, and it was wrong. Game creation moves the active
chat to the new session, so keying the chooser on it remounted the component in
the middle of the launch it was driving, clearing the selection and unmounting
the setup mid-flight. That is worse than the stale selection it was meant to
prevent, which needs the player to switch chats during setup and is already
handled by resolving the id rather than remembering it.
The seam discarded the whole update when the speaker map was empty, so an
experience that has nothing to say about its speakers could not give the
player's own dialogue a face. Either half now counts as a real push.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/client/src/components/game/GameSurface.tsx (4)

3475-3480: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve a player-only avatar when no persona is selected.

effectivePersonaInfo injects playerAvatarUrl only when personaInfo exists. This component explicitly supports chats without a selected persona and creates a persona:default player. In that case, the retained player-only package update is not passed to GameNarration.

Provide a default player avatar entry or pass the fallback through the no-persona player contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/GameSurface.tsx` around lines 3475 -
3480, Update effectivePersonaInfo and the no-persona player contract used by
GameNarration so playerAvatarUrl is preserved when personaInfo is undefined.
Provide the fallback as the default player avatar entry or pass it through the
existing no-persona path, while retaining the current persona avatar override
behavior.

2164-2184: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Defer experience-sensitive work while package resolution is pending.

experienceOwnsGame covers the cold-cache state, but later gates still use experienceSurfaceActive. While useInstalledCapabilityPackages is pending, Classic map, party, widget, choice, and input UI can render before the package surface mounts. sceneAnalysisEnabled can also be false for an experience whose chatMeta.enableAgents is false.

If a new assistant turn is processed during this window, processSceneRef takes the inline path and sets lastProcessedMsgRef. Resolving the package later does not process that turn again.

Defer scene processing until experience resolution settles. Use the pending ownership state for Classic UI suppression, then restore Classic fallback only when the query settles without the selected package.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/GameSurface.tsx` around lines 2164 -
2184, Defer experience-sensitive scene processing until installed-package
resolution settles, using experienceOwnsGame rather than experienceSurfaceActive
for Classic map, party, widget, choice, and input UI suppression. Update
processSceneRef so pending experience resolution does not take the inline path
or set lastProcessedMsgRef, ensuring the turn is processed after the package
mounts. Also prevent scene processing when the selected experience has scene
analysis disabled by chatMeta.enableAgents, and restore Classic fallback only
once the query settles without the selected package.

8336-8340: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Apply inventory and combat declarations to every Classic path.

These lines remove only the narration open/start callbacks. GameCombatUI still receives onOpenInventory={() => setInventoryOpen(true)} at Line 11864, and the combatUiActive branch at Line 11806 still mounts Classic combat after a GM encounter or state tag.

A package that declares providesInventory or providesCombat can therefore still open or display the corresponding Classic system. Gate the Classic overlays, combat branch, queued encounter/generation flow, and every Classic callback when the declaration is true. Clear an already-open Classic inventory when ownership becomes true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/GameSurface.tsx` around lines 8336 -
8340, Apply activeExperienceChrome.providesInventory and providesCombat
consistently across every Classic path in GameSurface, not only
classicInventoryOpener and classicCombatStarter. Gate GameCombatUI callbacks,
the combatUiActive branch, Classic overlays, and queued encounter or generation
flows when the corresponding declaration is true, while preserving
custom-provided behavior. When inventory ownership becomes true, close any
already-open Classic inventory.

6443-6447: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Resolve pushed experience backgrounds lazily.

Store the raw pushed value from setExperienceBackgroundTag, then derive experienceBackgroundUrl from the latest scopedAssetMap before it is used by displayedBackground. A later manifest refresh can add the asset after pushExperienceBackground has already captured the unresolved raw value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/GameSurface.tsx` around lines 6443 -
6447, Update pushExperienceBackground and the surrounding GameSurface state so
setExperienceBackgroundTag stores the raw pushed value instead of resolving it
immediately. Derive experienceBackgroundUrl from that raw value and the latest
scopedAssetMap before displayedBackground consumes it, preserving direct asset
paths and null handling while allowing later manifest refreshes to resolve
previously unknown tags.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/client/src/components/game/GameSurface.tsx`:
- Around line 3405-3412: Update setExperienceSpeakerAvatars to validate
speakerAvatars as an iterable map-like collection whose entries are iterable
[string, { url: string }] pairs before storing it. Reject malformed collections
or entries, including objects that only expose size, by storing null; preserve
acceptance of valid playerAvatarUrl-only pushes.

---

Outside diff comments:
In `@packages/client/src/components/game/GameSurface.tsx`:
- Around line 3475-3480: Update effectivePersonaInfo and the no-persona player
contract used by GameNarration so playerAvatarUrl is preserved when personaInfo
is undefined. Provide the fallback as the default player avatar entry or pass it
through the existing no-persona path, while retaining the current persona avatar
override behavior.
- Around line 2164-2184: Defer experience-sensitive scene processing until
installed-package resolution settles, using experienceOwnsGame rather than
experienceSurfaceActive for Classic map, party, widget, choice, and input UI
suppression. Update processSceneRef so pending experience resolution does not
take the inline path or set lastProcessedMsgRef, ensuring the turn is processed
after the package mounts. Also prevent scene processing when the selected
experience has scene analysis disabled by chatMeta.enableAgents, and restore
Classic fallback only once the query settles without the selected package.
- Around line 8336-8340: Apply activeExperienceChrome.providesInventory and
providesCombat consistently across every Classic path in GameSurface, not only
classicInventoryOpener and classicCombatStarter. Gate GameCombatUI callbacks,
the combatUiActive branch, Classic overlays, and queued encounter or generation
flows when the corresponding declaration is true, while preserving
custom-provided behavior. When inventory ownership becomes true, close any
already-open Classic inventory.
- Around line 6443-6447: Update pushExperienceBackground and the surrounding
GameSurface state so setExperienceBackgroundTag stores the raw pushed value
instead of resolving it immediately. Derive experienceBackgroundUrl from that
raw value and the latest scopedAssetMap before displayedBackground consumes it,
preserving direct asset paths and null handling while allowing later manifest
refreshes to resolve previously unknown tags.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3a8984ad-84e9-4dad-8410-b1ca51fc65b0

📥 Commits

Reviewing files that changed from the base of the PR and between 9164248 and 62387c9.

📒 Files selected for processing (1)
  • packages/client/src/components/game/GameSurface.tsx

Comment thread packages/client/src/components/game/GameSurface.tsx Outdated
…size

The seam is iterated during render, and the guard only checked `.size` -- any
object carrying that property passed and then threw a TypeError mid-render,
taking the surface down with it. The stored value is now normalized: a real Map
or an empty one, so the consumer cannot be handed something it cannot iterate.

Normalizing rather than tightening the condition also keeps a valid
playerAvatarUrl when the map alongside it is malformed, which a condition change
alone would have dropped.
@SonaGat3

SonaGat3 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai the actionable one is fixed. Here is where I land on the four outside-diff items, so they do
not need to come back each round.

Speaker-avatar map — fixed, and normalized rather than just re-checked. The seam is iterated during
render, so a size check alone let any object carrying that property through and it threw a TypeError
mid-render. The stored value is now always a real Map or an empty one, which also keeps a valid
playerAvatarUrl when the map beside it is malformed — a tightened condition would have dropped both.

Player-only avatar with no persona — leaving as is. effectivePersonaInfo enriches an existing
persona, and with no persona there is no speaker identity in the narration to attach an avatar to. Making
one up to hang the image on would invent an entity the rest of the game does not know about.

Using experienceOwnsGame for the Classic suppression gates — leaving as is, and the distinction is
deliberate. experienceSurfaceActive means the surface is mounted and drawing. experienceOwnsGame is
broader: it is already true from the chat metadata before the installed-packages query resolves. Suppressing
built-in chrome on the broader flag would hide the built-in UI while nothing has replaced it yet — a blank
game area on a cold cache. So it gates the things that must never appear for such a game at all (the
first-game tutorial), and the surface flag gates the things that only make sense once something else is
drawing.

Lazy background resolution — leaving as is. A package pushes a ready asset path because it knows its own
bundled art; the scopedAssetMap lookup is a compatibility fallback, not the primary route. Deferring
resolution to display time would make the seam depend on a manifest the package deliberately does not need.
Happy to revisit if a concrete case turns up where a tag only becomes resolvable later.

Inventory and combat declarations on every Classic path — this one I think is a real point rather than a
nit, and that is exactly why I would rather not patch it here. The declaration currently withholds the entry
points, so an experience that ships its own systems hides the buttons; a turn that reaches built-in combat
another way, such as the GM emitting [state: combat], would still enter it. Where that boundary should sit
— whether a declaration suppresses a system everywhere or only its affordances — is a design decision about
the capability contract, and it touches the combat flow well beyond this seam. The maintainer has said he
wants to make the last calls on this PR himself, so I would rather he made that one with the whole thing in
front of him. I will implement whichever way he decides.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@SonaGat3 The Map normalization resolves the actionable speaker-avatar issue. It prevents render-time iteration failures and preserves playerAvatarUrl when the speaker-avatar value is malformed.

I accept the remaining decisions for this PR:

  • Do not create a narration speaker identity when no persona exists.
  • Use experienceSurfaceActive for Classic UI suppression. This prevents a blank game area while package resolution is pending.
  • Keep immediate package-provided background paths as the primary path. Keep scopedAssetMap as a compatibility fallback.
  • Defer full built-in inventory and combat suppression until the maintainer defines the capability contract.

I will not re-raise these items in this PR.

🐇📝


✏️ Learnings added
Learnt from: SonaGat3
URL: https://github.com/Pasta-Devs/Marinara-Engine/pull/4528

Timestamp: 2026-08-03T21:05:41.460Z
Learning: In `packages/client/src/components/game/GameSurface.tsx`, `experienceSurfaceActive` means that a game-surface experience is mounted and rendering, while `experienceOwnsGame` can be true before installed capability packages resolve. Use `experienceSurfaceActive` for Classic UI suppression to avoid a blank game area during package-query loading. Use `experienceOwnsGame` only for UI that must never appear for an experience game, such as the first-game tutorial.

Learnt from: SonaGat3
URL: https://github.com/Pasta-Devs/Marinara-Engine/pull/4528

Timestamp: 2026-08-03T21:05:41.460Z
Learning: In `packages/client/src/components/game/GameSurface.tsx`, normalize a package-provided speaker-avatar map to a real `Map` or an empty `Map` before render-time iteration. Do not reject a valid `playerAvatarUrl` merely because the speaker-avatar map is malformed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/client/src/components/game/GameSurface.tsx (1)

2180-2185: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Gate all Classic chrome with experienceOwnsGame while package discovery is pending.

While the installed-packages query is pending, experienceOwnsGame is true but experienceSurfaceActive is false. The map and party header at Line 11519, mobile widget tray at Line 11693, choice UI at Line 11713, and desktop widget rails at Line 12271 test only experienceSurfaceActive. Classic UI can flash and accept input before the experience resolves.

Keep experienceSurfaceActive for mounting the capability. Use experienceOwnsGame for Classic UI suppression until the query settles.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/GameSurface.tsx` around lines 2180 -
2185, Update the Classic UI suppression checks for the map and party header,
mobile widget tray, choice UI, and desktop widget rails to use
experienceOwnsGame instead of experienceSurfaceActive. Keep
experienceSurfaceActive unchanged for mounting the capability, while ensuring
all Classic chrome is gated by experienceOwnsGame during package discovery.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/client/src/components/game/GameSurface.tsx`:
- Around line 2180-2185: Update the Classic UI suppression checks for the map
and party header, mobile widget tray, choice UI, and desktop widget rails to use
experienceOwnsGame instead of experienceSurfaceActive. Keep
experienceSurfaceActive unchanged for mounting the capability, while ensuring
all Classic chrome is gated by experienceOwnsGame during package discovery.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 87340cfb-15fd-4cb6-8118-32fcccc6649e

📥 Commits

Reviewing files that changed from the base of the PR and between 62387c9 and f25049c.

📒 Files selected for processing (1)
  • packages/client/src/components/game/GameSurface.tsx

The map, party bar and the three widget rails tested experienceSurfaceActive,
which only turns true once the installed-packages query resolves. On a cold
cache that left the built-in HUD on screen, and clickable, for as long as
package discovery took, before the surface replaced it.

They now test experienceOwnsGame, which the chat already answers from its
metadata. The split is now consistent: ownership gates chrome that must not
appear for such a game, and the surface flag gates everything that needs the
package to actually be mounted -- the seams it pushes into and the two mounts
themselves. An ordinary game sets neither, so it is unaffected.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/client/src/components/game/GameSurface.tsx (1)

6480-6507: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear Classic choices before an experience sends a turn.

Line 6488 exposes sendMessage directly to the experience. This bypasses handleSendGameTurn, which clears activeChoices. If the experience submits a choice and later removes providesChoices, the prior turn's choices can render as Classic choice cards after narration completes.

Pass a wrapper that clears activeChoices before it calls sendMessage.

Proposed fix
+  const sendExperienceMessage = useCallback(
+    (...args: Parameters<typeof sendMessage>) => {
+      setActiveChoices(null);
+      return sendMessage(...args);
+    },
+    [sendMessage],
+  );
+
   const experienceSurfaceProps = useMemo(
     () => (!experienceSurfaceActive ? undefined : {
       chatId: activeChatId,
-      sendMessage,
+      sendMessage: sendExperienceMessage,
       setExperienceBackgroundTag: pushExperienceBackground,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/game/GameSurface.tsx` around lines 6480 -
6507, Update the experienceSurfaceProps construction to pass a wrapper for
sendMessage that clears activeChoices before delegating to the existing
sendMessage function. Keep the wrapper’s arguments and return behavior aligned
with sendMessage, and include any referenced state or callback in the useMemo
dependency list so Classic choices are cleared for every experience-submitted
turn.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/client/src/components/game/GameSurface.tsx`:
- Around line 6480-6507: Update the experienceSurfaceProps construction to pass
a wrapper for sendMessage that clears activeChoices before delegating to the
existing sendMessage function. Keep the wrapper’s arguments and return behavior
aligned with sendMessage, and include any referenced state or callback in the
useMemo dependency list so Classic choices are cleared for every
experience-submitted turn.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6b8feba0-f3aa-4a6a-9abf-9b856a9142dd

📥 Commits

Reviewing files that changed from the base of the PR and between f25049c and 85bbf85.

📒 Files selected for processing (1)
  • packages/client/src/components/game/GameSurface.tsx

The surface received sendMessage directly, so a turn it drove skipped the
cleanup the built-in send performs. Choices, a QTE or a queued encounter offered
by the previous turn survived into the next one and could render after its
narration finished.

It now goes through the same clearPendingInteractiveCommands the built-in send
uses, rather than clearing only the choices, so a turn driven by the experience
leaves exactly the state a typed one does.
@SonaGat3

SonaGat3 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

@SpicyMarinara I thinkg that I defeated the rabbit, it was... a learning experience.

@SpicyMarinara SpicyMarinara left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainer review complete on 95b6592. CodeRabbit has no unresolved threads; focused client/server/shared TypeScript, client ESLint, localization, version consistency, and capability lifecycle validation pass. UI uses theme accent tokens and responsive touch/focus states.

@SpicyMarinara
SpicyMarinara marked this pull request as draft August 4, 2026 09:22
@SpicyMarinara
SpicyMarinara marked this pull request as ready for review August 4, 2026 09:22

@SpicyMarinara SpicyMarinara left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconfirming maintainer approval on 6fd713b after the clean staging sync; the reviewed capability and UI changes are unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants