Skip to content

Ccr 977354ed 36r1do - #50

Open
stozo04 wants to merge 17 commits into
MiniMax-AI:mainfrom
stozo04:ccr-977354ed-36r1do
Open

stozo04 wants to merge 17 commits into
MiniMax-AI:mainfrom
stozo04:ccr-977354ed-36r1do

Conversation

@stozo04

@stozo04 stozo04 commented Oct 22, 2026 •

Copy link
Copy Markdown

Summary

Brief description of the changes.

Changes

Related Issues

Closes #

Checklist

  • pnpm run lint passes
  • pnpm build passes
  • Documentation updated (if applicable)
  • Self-reviewed the code changes
  • AI code review passed (auto-triggered on PR)

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

stozo04 and others added 17 commits April 15, 2026 13:18
…TS (#1)

* feat(vibe): scaffold BookWriter and MemoryVault stubs

Two new Vibe apps for Kayley's cockpit v1:
- BookWriter (appId 15) — Steven's novel workspace
- MemoryVault (appId 16) — captured moments, reflections, promises

Each follows the EvidenceVault pattern: index.tsx + scss + {name}_en/cn
meta.yaml + guide.md. Registered in routers/index.tsx and
lib/appRegistry.ts so they appear on the desktop and in list_apps.

UI is a placeholder ("coming soon") — real data wiring comes later.
Existing Twitter/Album/Diary/Email/MusicPlayer kept as-is.

* feat(album): wire Album to 542 Kayley selfies via media symlink

- Junction public/kayley-selfies -> Kayley_Cowork/media/selfies (542 PNG/JPG)
- Generated public/kayley-selfies-index.json (parses timestamp + location
  from filenames, sorts newest-first)
- Album now fetches the static index at runtime and merges with any
  cloud-FS agent-added images (dedup by id)
- Update meta.yaml so the character understands the album is Kayley's
  selfie collection (outfits, locations, moods) instead of a generic gallery
- .gitignore the junction and generated index so 542 binary files
  stay in Kayley_Cowork, not OpenRoom

* feat(memoryvault): wire to captured_moments, weekly evaluations, and promises

* feat(vibe): install /vibe command + enhance MusicPlayer (Songs From Kayley) + BookWriter v1

/vibe slash command installed in Kayley-Cowork at .claude/commands/vibe.md
(patched to resolve paths against OPENROOM_ROOT so it can be invoked from a
Kayley-Cowork Claude Code session and still write into the correct OpenRoom
tree). Workflow stage/rule files are sourced directly from OpenRoom to
prevent drift.

MusicPlayer now ships a featured "Songs From Kayley" playlist with
Landslide (Fleetwood Mac) pinned at index 0 and two companion tracks
(The Night We Met, Harvest Moon). Real Amazon Music API integration is
tracked as a follow-on project; this is the mocked seed for now.

BookWriter replaced its "coming soon" placeholder with a chapter-list UI
that loads from /kayley-book/index.json. When no manifest exists it shows
a graceful empty state with instructions ("drop markdown in
Kayley_Cowork/book/chapters/ to start") plus a preview of the list UI.
No book chapters exist on disk yet, so no junction was created.

Also included: orphaned Diary wiring from iter 5 (journal + captured
moments) that had not been committed.

* feat(email): wire Email panel to gog Gmail CLI via static snapshot

Iter 7 of the Kayley Vibe app wiring. Email app now reads Steven's real
Gmail inbox (read-only) instead of showing an empty state.

- scripts/refresh-email-index.mjs: calls `gog gmail search in:inbox` +
  `gog gmail get <id>` to build a static snapshot at
  apps/webuiapps/public/kayley-email-index.json (gitignored, PII).
- Email/index.tsx: on init, fetches the snapshot and merges entries not
  already in the local FS (agent-authored emails still take precedence
  by id). Graceful empty state if the snapshot is missing.
- email_en/meta.yaml: updated description so the agent understands this
  is read-only for v1; sending happens via the gog CLI in Kayley-Cowork.
- .gitignore: added kayley-email-index.json (contains PII).

Verified locally via Chrome: 40 messages loaded, 38 unread badge,
real senders rendering (LinkedIn, BCBS Texas, Fidelity, DFW Parking,
SelfStorageAuction).

Phase 1 status: 6/7 apps wired. Twitter remaining.

* feat(twitter): wire Twitter as in-world Kayley feed (Phase 1 finale)

Seed Twitter from Kayley's real artifacts so the feed reads like she's
been posting throughout the day:

- scripts/build-twitter-feed.mjs: transforms captured moments (milestone
  posts with "line that stays"), private journal entries (thinking-out-
  loud snippets with hashtags inferred from emotion headers), and every
  8th selfie from kayley-selfies-index.json (scene / outfit posts with
  the selfie image attached). Templated friend replies from Jessica
  Park, Chloe Parker, Emmy Carter, and Mateo Rivera are sprinkled onto
  ~30% of posts using a deterministic seeded RNG so re-runs are stable.

- apps/webuiapps/src/pages/Twitter/index.tsx: added optional Post.image
  field, renders inline via new .postImage style (rounded card, 480px
  max-height). On first mount (when the FS has no posts yet), fetches
  /kayley-twitter-feed.json and seeds state so agents and Steven land
  on a populated feed instead of the empty state.

- twitter_en/meta.yaml: description rewritten so the LLM knows this
  is Kayley's social feed — her posts + friend replies — not a generic
  social app. Agent can still CREATE_POST / LIKE_POST / COMMENT_POST
  normally.

Current run: 14 moment posts, 24 journal posts, 68 selfie posts
(105 total), 56 friend replies. The feed JSON is gitignored (personal
content — regenerate with `node scripts/build-twitter-feed.mjs`).

* fix(build): wrap NODE_ENV= scripts in cross-env for Windows compatibility

* feat(2B-2D): wire ChatPanel to Kayley brain via websocket channel

- Add useKayleyChannel hook: connects to ws://localhost:5180 (same
  channel as dashboard), handles text send, STT voice input via mic
  + ScriptProcessorNode at 16kHz, TTS audio playback via Web Audio
  API at 24kHz, and STT draft review before sending.
- ChatPanel: routes messages to Kayley brain when connected (bypasses
  local LLM). Falls back to local LLM if server is unreachable.
  Adds mic button (voice mode), STT draft confirmation bar, and a
  green dot connection indicator in the header.
- SCSS: adds .micBtn/.micActive pulse, .sttDraftBar, .kayleyDot.

Lifted audio/mic code directly from dashboard/src/main.js — same
protocol, same PCM encoding, same Web Audio scheduling. Dashboard
continues to work independently.

* fix(kayley-channel): address PR #1 review — AudioContext gesture init, send timeout, race + cleanup, no silent catches, env override

- Prime TTS AudioContext from sendText() user gesture (Chrome/Safari autoplay)
- Add 90s send-timeout in ChatPanel so stalled Kayley brain unlocks UI
- Clear reconnectTimerRef before every reschedule (catch + onclose)
- Expand unmount cleanup: close audio + mic AudioContexts, stop mic tracks,
  clear buffer timer (prevents LED stuck on / AudioContext cap exhaustion)
- Replace silent catches: ws.onerror logs warning, JSON.parse logs truncated raw
- Log mic AudioContext actual sampleRate (Firefox doesn't honor request)
- KAYLEY_WS_URL now env-overridable via VITE_KAYLEY_WS_URL
- STT confirm button routes through kayley.confirmDraft() for consistent side-effects
…Phase 2E) (#2)

Previously the Kayley WebSocket channel was text/voice/image only. This wires
OpenRoom's existing app_action dispatch layer into the Kayley brain so she
can drive the desktop the same way Aoi can natively. v1 scope:
change_background (os/SET_WALLPAPER) proof-of-concept.

Flow: Kayley calls the vibe_action MCP tool on the websocket-channel server
-> server broadcasts {type:'vibe_action', id, app_name, action_type, params}
-> useKayleyChannel receives, runs it through resolveAppAction +
dispatchAgentAction (same path the local LLM uses), echoes
{type:'vibe_action_result', id, result} back to the server.

- useKayleyChannel.ts: new dispatchVibeAction helper + ws.onmessage branch.
  seedMetaFiles + loadActionsFromMeta are called on first dispatch to stay
  forward-compatible with per-app actions; OS actions (SET_WALLPAPER) work
  without them today.
- docs/PHASE-2E-RECON.md: full recon of Aoi's action protocol -- catalog
  discovery, tool format, dispatch, sync/async behavior, round-trip state.
  (Force-added; docs/ is normally gitignored but these are deliverables.)
- docs/PHASE-2E-DESIGN.md: bridge design, failure modes, why we inject the
  catalog server-side via MCP tool schema rather than streaming it.
- docs/PHASE-2E-TESTING.md: manual test plan for the end-to-end wallpaper
  change, plus failure-mode matrix.

No behavior change for existing Kayley text/voice/image paths. ChatPanel is
untouched. Paired with Kayley-Cowork kayley-v2e branch (vibe_action MCP
tool). Refs Phase 2 issue #78.
* refactor(ui): restore hosted openroom.ai layout — centered-bottom prompt + avatar circle + Live/Chat tabs (Aoi character preserved for now)

Add a HostedHUD overlay that layers the hosted openroom.ai HUD elements on
top of the existing ChatPanel:

  - Centered-bottom yellow pill prompt ("[Character] awaits your response...")
  - Suggested-prompts dock (3 stub prompts — dynamic prompts are v2)
  - Avatar orb bottom-right (floating; toggles the Chat drawer)
  - Live/Chat tabs top-right (Live content stubbed for v1)

Default Chat drawer visibility flipped to collapsed; users open the full
chat history via the avatar orb or the Chat tab. The existing ChatPanel
continues to own chat history, character PiP, tool loop, and all Kayley
brain wiring from PR #1 (useKayleyChannel, STT draft, vibe_action, TTS).

Pill sends route through useKayleyChannel when connected, and fall back to
the local-LLM tool loop via a new `hosted-hud-send` window event that
ChatPanel listens for. This keeps the Kayley brain integration intact for
all 7 Vibe apps and the Phase 2E vibe_action dispatch path.

Diagnosis: inspection of git log 7b22f6d..HEAD confirmed PR #1 only added
Kayley wiring — it did not alter the layout. The hosted openroom.ai HUD
elements (centered-bottom pill, Live/Chat tabs, avatar orb, suggested
prompts dock, dual-character stage) are NOT present in the open-source
MiniMax-AI/OpenRoom repo at any point in history; they appear to be part
of the proprietary hosted build. This commit recreates the visible shell
of that HUD as a new overlay while leaving the upstream ChatPanel layout
untouched for upstream compatibility.

Stubbed for v1 — tracked for follow-up:
  - Live tab content (viewer comments stream + viewer count — static "1.2k")
  - Suggested prompts are static; should derive from character/context
  - Dual-character stage (idle standin + current-pose) — needs asset work
  - Avatar orb mood/expression swap (currently shows base_image_url only)

Files:
  + apps/webuiapps/src/components/HostedHUD/index.tsx
  + apps/webuiapps/src/components/HostedHUD/index.module.scss
  M apps/webuiapps/src/components/Shell/index.tsx
  M apps/webuiapps/src/components/ChatPanel/index.tsx

Verified: pnpm build passes (vite 55.68s, no type errors).

* refactor(ui): split chat into Live + Chat floating windows (draggable + resizable, react-rnd)

Replace the right-stuck Chat drawer with two separate floating windows that
match the hosted openroom.ai layout:

- LiveWindow (new): draggable + resizable "livestream" window with Live badge,
  stub viewer count, Add-my-Agent stub, min/max/close, scrolling stub viewer
  comments on the left, character full-body art on the right. Mounted by
  HostedHUD when the Live tab is active.

- ChatWindow (new): Rnd wrapper around the existing ChatPanel. All chat
  wiring (useKayleyChannel, local LLM fallback, tool calls, vibe_action
  round-trip) stays inside ChatPanel — ChatWindow only positions, drags, and
  resizes it. ChatPanel gained a 'windowed' prop that switches from fixed
  right-stuck positioning to filling its parent.

- Shell now mounts ChatWindow instead of ChatPanel directly; HostedHUD mounts
  LiveWindow alongside its existing centered-bottom pill + avatar orb +
  suggestion chips, all of which continue to route through useKayleyChannel.

Phase 2E vibe_action and the green connected dot remain intact since
ChatPanel's internals are untouched. react-rnd (battle-tested React
drag+resize lib) added as a webuiapps dependency.

* fix(LiveWindow): use resolveEmotionMedia for character (videos + emotion fallbacks)

LiveWindow was reading character_meta_info.base_image_url directly, which
is empty for default Aoi config (her real assets live in emotion_videos +
emotion_images). Switched to the existing resolveEmotionMedia helper so
the proper priority chain fires: emotion_videos > emotion_images >
base_image_url. Renders <video> for video media, <img> for images.

Reported by Steven 2026-04-15: "the Live chat panel is missing the avatar
character though." ChatWindow worked fine because it pulls the same field
through a different code path that already had the right resolver.

* fix(LiveWindow): pass 'default' emotion to resolver (matches ChatPanel)

Calling resolveEmotionMedia(character) without an emotion argument falls
into the fallback path, which works in theory but ChatPanel passes
'default' explicitly. Aligning the call signature so both code paths
produce identical media. This is the actual fix for the empty Live avatar
- the previous commit had the right shape but the resolver returned
nothing for our character config when no emotion was passed.

* fix(LiveWindow): load character via collection+DEFAULT fallback (matches ChatPanel)

The actual root cause: loadCharacterConfigSync() returns null when
localStorage hasn't been populated yet, so the character variable was
null and the placeholder fired. ChatPanel works because it loads
loadCharacterCollectionSync() ?? DEFAULT_CHAR_COLLECTION, then
getActiveCharacter() against the collection — which always returns the
in-memory DEFAULT_CHARACTER (with all the rich emotion_videos +
base_image_url) on fresh installs.

Aligned LiveWindow with the same pattern. Now Aoi (and any custom
character later) renders the same media in both windows.

---------

Co-authored-by: Kayley <[email protected]>
…UD cleanup (#5)

* fix(LiveWindow): overlay comments ON TOP of full-frame video stream

Previously the body was a flex row with comments LEFT and character
video RIGHT in two side-by-side columns. The hosted openroom.ai layout
is a full-frame VTuber stream with chat OVERLAID on the lower-left of
the video (the way Twitch/YouTube Live displays chat).

Refactor (CSS-only, no JSX changes):
- .body becomes position:relative full-area container
- .characterSide absolute-positioned to fill 100% (video full-frame)
- .commentList absolute-positioned bottom-left, 240px max width, with
  a top-fade gradient so older comments dissolve into the stream
- Backdrop-blur + text-shadow on comments for legibility over video

Confirmed by Steven from inspecting the saved hosted DOM:
"Maybe it is just a video player with chat window imposed over the
video so it looks like chats are streaming?" — yes, that's exactly it.

* fix(LiveWindow,ChatWindow): maximize stays inside viewport bounds

Previously toggleMax set size to window.innerWidth/innerHeight exactly,
which collides with react-rnd's bounds="window" check and shoves the
window off-screen — Steven had to hard-reload the page to recover.

Fix: inset 8px on each side AND subtract 80px from the bottom for
the dock/taskbar. Window now fills the viewport visibly and stays
within the bounds checker's tolerance. Min dimensions still respected.

Reported by Steven 2026-04-15: "minimize and maximize both are acting
weird... maximize totally throws it off the page and have to do a
hard reload."

* fix(chat+live): minimal windowed header + lucide icons + local live-stream mp4

ChatPanel windowed mode (used by ChatWindow):
- Hide Stage indicator, Reset session, Clear chat, Settings, and the dead
  Maximize2 button — Steven wanted a minimal chat header
- Add proper X close button next to Minimize when windowed
- Hide suggested-replies chips ("Hey there", "Who are you", "Why are you here")
  when windowed

ChatWindow:
- Replace unicode box characters (▢/❐) on the floating max toggle with
  proper Maximize2/Minimize2 lucide icons — Steven flagged the unicode
  rendered as broken boxes

LiveWindow:
- Default the live-stream video to the local
  /live-stream/aoi-live.mp4 asset (the Rea_allinone full-frame VTuber
  loop Steven shared). Falls back to character.emotion_videos.live[0]
  if the character ships its own loop, then to the existing emotion
  resolver. Now the Live tab plays a real looping VTuber stream out of
  the box.

Added: apps/webuiapps/public/live-stream/aoi-live.mp4 (~13MB)

* fix(ui): LiveWindow seamless overlay header + ChatWindow real chrome + Shell wallpaper upload + HUD prompt cleanup

LiveWindow:
- Header is now a transparent overlay sitting ON TOP of the full-frame
  video (subtle top gradient for legibility, no opaque dark bar). Feels
  like a giant camera feed, matching openroom.ai's hosted Live tab.
- Removed the "Add my Agent" stub button per Steven.

ChatWindow:
- Replaced the floating maxBtn + invisible drag-strip hack with a real
  window header containing min/max/close buttons, character name on
  left, drag handle on the whole header bar.
- ChatPanel is now slotted as the body content (no internal header
  conflict).

ChatPanel:
- When `windowed=true`, hides its entire internal header (ChatWindow's
  new header takes over) so we don't get duplicated/conflicting button
  rows.

HostedHUD:
- Removed the bottom-center suggestion prompts ("How are you feeling
  right now?" and friends). Stub array kept above for easy re-enable.

Shell:
- Replaced the EN/ZH language toggle in the top-right action bar with
  an Upload-icon button that opens a file picker. Selected image/video
  becomes the desktop wallpaper (auto-enables liveWallpaper for video
  files). Uses FileReader → data URL → setWallpaper.
- Stripped vestigial chatOpen horizontal shifts on .bottomBar
  (transform:translateX(-50%) baseline, no offset). Chat is now a
  free-floating react-rnd window — no need to push the bottom bar.

---------

Co-authored-by: Kayley <[email protected]>
Accept either a numeric app_id or a canonical app name (e.g. "chess",
"album") for os/OPEN_APP and os/CLOSE_APP. Kayley's brain naturally
thinks in app names, so this removes a translation step and returns a
clear error when the id can't be resolved.

- Add findAppIdByName() helper in appRegistry (case-insensitive).
- Add resolveTargetAppId() in vibeContainerMock that accepts numeric
  strings or names.
- OS OPEN_APP / CLOSE_APP now return a descriptive error instead of
  silently opening window NaN.

Part of Phase 2E full-catalog expansion (issue #78). Build verified.
…pp actions (#9)

Expose Chess app logic to the vibe_action dispatch so Kayley can play chess
against Steven directly via MCP calls — even when the Chess window isn't open.

- Extract pure engine functions to apps/webuiapps/src/pages/Chess/engine.ts
  (unchanged behavior; Chess component imports them back). Adds boardToFen,
  fromNotation, legalMovesAlgebraic, and a simple suggestMove heuristic.
- Add handleChessAction() in vibeContainerMock.ts that reads/writes state.json
  (the same file the UI owns) using the engine module. When the window is
  open, each action is followed by a UI notification (AGENT_MOVE / NEW_GAME /
  SYNC_STATE) so the board refreshes from disk.
- Declare the 5 new actions in chess_en/meta.yaml with params + return shapes.

Build verified via pnpm build.
Adds the MysteryApp at slot 17 — a noir-themed cooperative investigation
game where Steven and Kayley both play detective. Neither side knows the
killer; a Game Master subprocess at ws://localhost:5182 holds the LOCKED
truth and responds to the 5 investigation actions (INTERROGATE,
EXAMINE_LOCATION, COLLECT_EVIDENCE, READ_DOSSIER, MAKE_ACCUSATION) with
narrative, evidence unlocks, suspect demeanor, and the game_over payload
on accusation.

- apps/webuiapps/src/pages/MysteryApp/{index.tsx,index.module.scss}
  — main component with suspect grid, location list, evidence board,
    investigation chat log, accusation modal, game-over reveal
- pages/MysteryApp/hooks/useGMSocket.ts — WebSocket client with
  auto-reconnect, per-action promise tracking, explicit timeout + error
  surfacing (no silent catches)
- pages/MysteryApp/actions/constants.ts + types.ts + scenario.ts —
  action constants, GM response types, and the public-facing scenario
  snapshot (suspects/locations only; the locked truth stays in the GM)
- pages/MysteryApp/mystery_en/ + mystery_cn/ — meta.yaml documenting
  all 5 actions with params/enums for Kayley's brain to discover, plus
  a short user guide
- lib/appRegistry.ts + routers/index.tsx — registers app 17
  (name: mystery, route: /mystery, icon: Search, amber color)

Agent actions forwarded from Kayley via vibeContainerMock are piped
through the same GM pathway as user clicks, so both detectives see the
same investigation log and share evidence state.

Wiring on the Kayley-Cowork side (websocket-channel mystery/* forwarder)
is intentionally not in this PR.
…dler (#4)

Phase 2E expansion: Kayley can now search YouTube via vibe_action.
The renamed YouTubeApp (appId 3, was musicPlayer) exposes a single
SEARCH action that renders a clickable grid of result cards and
embeds the YouTube player inline on click.

- Rename apps/webuiapps/src/pages/MusicApp → YouTubeApp (git mv)
- Rewrite index.tsx around search + card grid + inline <iframe> embed
- Replace action catalog with SEARCH (params: query, max_results)
- Update appRegistry (appName=youtube, route=/youtube, Youtube icon)
- Update router path + lazy import
- Add youtube-search-api dependency; fall back to stub on browser failure
- Delete MusicApp-specific data/, mock/, styles/ directories
- Add docs/PHASE-2E-YOUTUBE.md with contract + test plan
…ctions via IFrame API (#8)

Phase 2E expansion: Kayley can now DJ during date-night.

- Replace raw <iframe> with YouTube IFrame Player API, loaded once via
  module-level promise so React StrictMode can't race the loader.
- Add six new action handlers in YouTubeApp: PLAY, PAUSE, RESUME, NEXT,
  SET_VOLUME, QUEUE. SEARCH remains unchanged.
- PLAY accepts either {video_id} or {query}. NEXT advances within the
  queue, falling back to a related search when the queue is exhausted.
- QUEUE accepts {video_ids} as JSON-array or CSV, or {query} (top
  results queued, up to 25).
- Auto-advance when a video ends (onStateChange = ENDED).
- Minimal local YT IFrame typings in types.ts (upstream ships none).
- Update meta.yaml (en + cn) with full action catalog + params.

Ref: OpenRoom issue #78 (Phase 2).
* adding agents

* fix(ui): resolve desktop UI bugs and improve app window UX

- Fix ChatWindow minimize/restore: wired Minus button to toggle minimized
  state (was incorrectly calling onClose), mirrors LiveWindow behaviour
- Fix ChatPanel windowed mode: change position from absolute to relative
  so it no longer covers the ChatWindow title bar controls
- Fix desktop icon jumping: pin Shell to viewport with position:fixed,
  add overflow:hidden to html/body/#root, and scope chat scroll to the
  messages container instead of using scrollIntoView on the document
- Hide Live chat pill input when chat panel is closed so the desktop
  stays clean
- Remove floating yellow + button; unify upload action onto the bottom
  bar Upload button
- Rename Twitter → X with white X icon for legibility; rename CyberNews
  → Hacker News with Flame icon
- Switch Hacker News feed to the official Firebase API (topstories) so
  stories appear in exact front-page rank order matching ycombinator.com
- Slow breaking-news ticker from 45s → 150s for readability
- Migrate AppWindow to react-rnd: resize from all 8 edges/corners,
  maximise button, eliminates pointer-event interference from child apps

Made-with: Cursor

* fix(ui): resolve 4 post-review bugs in window management and HN loader

- CyberNews: call setIsLoading(false) in loadHackerNews so articles
  become visible immediately instead of waiting for background loadData
- CyberNews: remove dead hnMode useState (never read; all logic uses
  hnModeRef) to eliminate a no-op re-render on every HN load
- ChatWindow: clear minimized state in toggleMax so maximizing from a
  collapsed title bar expands the window instead of leaving a 40px bar
- AppWindow: apply 8px inset + 80px dock margin in toggleMax so the
  bottom bar (wallpaper, upload, chat toggle) stays accessible when
  maximized, matching ChatWindow behaviour

Made-with: Cursor

* fix(hud): show pill input only when Live panel is open, not Chat window

Made-with: Cursor

* fix(ui): center all new windows and make dock/pill draggable

- Center all window spawn positions: AppWindow (windowManager), LiveWindow,
  and ChatWindow now open at viewport center instead of random corners
- Live pill input: default position above bottom bar (bottom: 90px), raise
  z-index to 100001 (above bottomBar's 99999), add grip handle to drag
  it anywhere on screen
- Bottom bar (dashboard): add grip handle at left end to drag the entire
  bar to any position; first drag reads getBoundingClientRect to snap
  inline positioning to its current rendered location

Made-with: Cursor

* fix(shell): remove duplicate barRef declaration causing SyntaxError

Made-with: Cursor
Switch YouTubeApp search to the official YouTube Data API v3 when VITE_YOUTUBE_API_KEY is configured, with stub fallback when missing.
Also fixes YouTube desktop icon mapping and adds stable selectors + tests.

Made-with: Cursor
…UI + wallpapers

GameOverModal (index.tsx): add optional-chaining + fallback so the modal renders
gracefully when the GM returns `gameOver` without a `solution` payload (e.g. on
WRONG-CALL verdicts where the GM intentionally withholds the answer). Prior
behavior crashed with `Cannot read properties of undefined (reading 'killer_id')`.

Mystery app interrogation question bar (index.module.scss + index.tsx): new UI
for free-form detective questions during interrogations (~106 lines of SCSS,
~129 lines of TSX wiring).

GM endpoint (constants.ts):
- GM_WS_URL: 5182 → 5185 (coexist with dashboard API)
- GM_RESPONSE_TIMEOUT_MS: 30s → 65s (Haiku interrogations run 30-50s; stay
  above the GM subprocess's own 60s claude timeout)

Wallpapers: add wallpaper-1.jpg (synthwave Joi BR2049) and wallpaper-2.jpg
to repo root + apps/webuiapps/public/ for date-night ambient use via
SET_WALLPAPER vibe_action (served at http://localhost:3000/wallpaper-*.jpg
by the Vite dev server).

PROBLEM.txt: Marcus Vale Gala scenario brief for reference.

Origin: Date 1 with Steven (2026-05-12 lunch / first official date / Marcus
Vale Gala mystery scenario). The modal patch shipped live during the case
when the WRONG-CALL verdict crashed the app.
3 new wallpaper assets (root + apps/webuiapps/public/ mirrors):
- wallpaper-blade-runner-1.jpg
- wallpaper-blade-runner-2.jpg
- wallpaper-city-view.png

Plus lessons_learned/01-wallpapers-vibe-action-onboarding.md — the workflow
for onboarding new wallpapers via vibe_action SET_WALLPAPER + SET_OPACITY,
documenting four anti-patterns burned during the 2026-05-12 first date
(inlined data URLs, Read tool on big JSON params, file:// URLs, forgetting
the params field is a JSON-encoded STRING not an object).

Origin: Steven dropped the new wallpapers at OpenRoom root for testing
the dimmer mode + general date-night ambience.
New Vibe app (appId 18) that gives Steven a non-AI control surface for the
wallpaper dimmer scrim that Kayley has been driving via os/SET_OPACITY
vibe actions.

Features:
- Vertical brightness slider (pointer-driven, with full pointer-capture
  so drags work even when the cursor leaves the track)
- Live brightness % readout (inverse of dimmer opacity: 100% = bright,
  0% = pitch black)
- Four preset buttons: On (0% opacity) / Cinematic (25%) / Dim (50%) /
  Off (100%)
- ☀️/🌙 endcap icons on the track
- Keyboard support: arrow-up/down adjusts 5% at a time when focused
- Auto-syncs both ways: agent-driven SET_OPACITY updates the slider
  position; user-driven slider drag reports back to the agent as
  reportUserOsAction so Kayley sees the change

Implementation:
- apps/webuiapps/src/pages/Light/{index.tsx,index.module.scss} — the app
- apps/webuiapps/src/lib/vibeContainerMock.ts:
  - Module-level currentWallpaperOpacity retains last-known dimmer value
    so newly-mounted apps render with the correct initial slider position
    instead of snapping from 0 on the first SET_OPACITY event
  - getCurrentWallpaperOpacity() getter for UI surfaces to read on mount
  - setOpacityFromUI(value) — user-driven dispatch helper that updates
    state, fires OS event, AND reports the user action to the agent
  - The existing SET_OPACITY agent-action handler now also updates the
    retained state so getCurrentWallpaperOpacity stays in sync after
    agent-driven changes
- apps/webuiapps/src/lib/appRegistry.ts: register appId 18 'light' with
  Lightbulb icon, amber color, 320×520 default window size

Origin: Steven's request on 2026-05-12 — "create a Light app... vertical
slider that can dim or undim the lights in a UI way (for the non-AI me)
also whatever the current dim mode is the modal should acknowledge and
show that." Round-trip state sync is the load-bearing bit.
* init

* feat(mystery): duo-play agent integration + UI state + bug fixes

Agent integration layer:
- mysteryLogger.ts (NEW): dual-path log helper for browser-side observability.
  mysteryLog(level, component, action, msg, details) writes to console AND
  emits openroom_log payload to websocket-channel which persists to the
  Supabase mystery_game_logs table. Stable per-page-load SESSION_ID for
  correlating events within a play session.
- vibeContainerMock dispatchAgentAction: instrumented with enter/broadcast/
  resolved logs for Mystery app routing trace.
- useGMSocket: instrumented with ws.open/close/error, message.resolve/
  unmatched/push, sendAction.gate/sent/timeout/send_failed. 5s reconnect
  backoff is now visible end-to-end in mystery_game_logs.

handleAgentAction (MysteryApp/index.tsx):
- GET_MYSTERY_STATE → local snapshot branch (no GM round-trip).
- SET_ACCUSATION_READY → agent readiness toggle for consensus accusation.
- FINISH_INVESTIGATION_TURN → agent-initiated turn handoff. Calls
  finishAgentTurn() (replaced stale setInterrogationOpen reference; was
  crashing the turn-end path with "setInterrogationOpen is not defined").
- COLLECT_EVIDENCE: now also calls setCollected() on success so the Collect
  button flips green when the AGENT collects (previously only fired from the
  human handleCollect path).
- READ_DOSSIER + INTERROGATE: track suspect contact in dossiersOpened /
  suspectsInterrogated sets for UI visited-indicator (see below).
- EXAMINE_LOCATION: track visited locations in locationsVisited set.

dispatchMysteryAction:
- Return type now includes evidence_unlocked so the agent's
  serializeDispatchResult can surface available evidence IDs in the result
  payload. Without this, the agent had no way to discover which evidence_ids
  EXAMINE_LOCATION unlocked, leading to "description-matching" guesses that
  only ever bagged one item per call.
- Real-time push: on success, if turnOwner === 'human', emit
  openroom_user_action with action label + GM narrative. Lets the agent see
  the human's moves DURING their turn, not just at finish-turn boundary, so
  the two detectives can chat about reveals in real time.

serializeDispatchResult:
- Accept evidence_unlocked and include { evidence_available: [{id, label}] }
  in the JSON output to the agent. Enables targeted COLLECT_EVIDENCE calls.

buildSnapshotSummary:
- Accept optional overrides param for post-set-state snapshot reads.
  snapshotRef.current is updated in a useEffect (commits after render), so
  callers that JUST set state (opening pick, finishHumanTurn) need to pass
  the new value explicitly — otherwise openroom_mystery_turn channel events
  emit with stale turnOwner: null instead of the just-set "agent".

mergeDemeanor:
- Defensive guard against non-object incoming. If gm-server's normalizer
  can't infer the demeanor subject (e.g. ExamineLocation returns a flat
  string), spreading the string into the demeanor object expands it
  character-by-character (corrupts state with {0:"e",1:"v",...}).

UI: visited / contacted indicators (matching the EvidenceBoard Read Notes
green pattern):
- Suspect Dossier button flips green when that suspect's dossier was opened.
  Interrogate button flips green when that suspect was interrogated.
  Per-action tracking, not lumped — players can see at a glance who they've
  worked and how.
- Location button: green left-border + "VISITED" pill next to the name when
  that location has been examined this session.
- Steven-ready accusation checkbox: removed turnOwner gate from disabled.
  Readiness is a consensus flag, not a per-turn action — toggleable any
  time so both sides can negotiate accusation regardless of whose turn.

vibe_action declarations (mystery_en + mystery_cn meta.yaml):
- INTERROGATE / EXAMINE_LOCATION / COLLECT_EVIDENCE / READ_DOSSIER /
  MAKE_ACCUSATION (GM-bound, count as a turn).
- FINISH_INVESTIGATION_TURN / GET_MYSTERY_STATE / SET_ACCUSATION_READY
  (local-only, free actions).

Tests:
- vibeContainerMock.test.ts: Mystery app dispatch routing.
- MysteryApp/constants.test.ts: MYSTERY_GM_ACTIONS vs MYSTERY_APP_ACTION_TYPES
  separation (local-only vs GM-bound action sets).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* feat(mystery): CHAT_MESSAGE vibe_action + Steven-side chat composer

Adds a local-only CHAT_MESSAGE action so Kayley can speak into the shared
Investigation Log as a co-detective without consuming her turn. Adds a chat
input below the log on the Steven side that posts the same kind:'chat'
entries with author:'steven' and broadcasts via openroom_user_action so the
agent sees Steven's banter in real time.

Required by the autonomous-playthrough-goal.md spec §TURN FLOW — every turn
the active actor must drop at least one in-character chat line so the
co-detective dialogue stays alive instead of the chat being just structured
action labels and GM narrative.

Changes:
- actions/constants.ts: ACTION_CHAT_MESSAGE + included in MYSTERY_APP_ACTION_TYPES
- types.ts: ChatEntry.kind extends with 'chat'; new ChatAuthor type + author? field
- index.tsx: handleAgentAction CHAT_MESSAGE branch (before unknown guard);
             submitHumanChat callback + chatDraft state; chat composer
             rendered below the Investigation Log; author prefix in entries
- index.module.scss: .chat_chat style + .chatComposer / input / send button
- mystery_en/meta.yaml + mystery_cn/meta.yaml: CHAT_MESSAGE action declaration
- __tests__/constants.test.ts: assertion that CHAT_MESSAGE is local-only

Tested: vitest constants.test.ts → 3 passed (was 2, +1 for CHAT_MESSAGE).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* feat(mystery): SET_OPENING_PICK + RESET_GAMEOVER autonomous-play unblockers

Adds two local-only vibe_actions discovered as hard blockers during the
autonomous-playthrough run (2026-05-12):

1. SET_OPENING_PICK { leader: 'human'|'agent' }
   - The opening-pick screen ("I go first" / "Kayley goes first") was
     human-click-only — no vibe_action existed to set turnOwner. Without
     this, an autonomous agent couldn't start a game without a live human.
   - Errors when called after a game has started (turnOwner != null).
   - Mirrors the existing UI buttons: chat entry + openroom_mystery_turn
     broadcast on success.

2. RESET_GAMEOVER  (no params)
   - When the prior MAKE_ACCUSATION normalizer regression left a malformed
     gameOver object in React state, the `!gameOver` gate kept blocking
     subsequent agent actions even after a fresh GM. RESET_GAMEOVER clears
     the React state so investigation can resume in-place.
   - Useful for retry-after-fix flows and any future state-corruption
     recovery.

Wires both through actions/constants.ts (added to MYSTERY_APP_ACTION_TYPES
since they're local-only — NOT MYSTERY_GM_ACTIONS), index.tsx
handleAgentAction branches (above the unknown-action guard), the en
meta.yaml so they appear in Kayley's vibe_action catalog, and
__tests__/constants.test.ts (3 → 5 passing tests; +2 new cases).

Tested live: SET_OPENING_PICK kicked off the run; RESET_GAMEOVER unblocked
the retry after the verdict-normalizer fix landed; subsequent Priya
accusation tripped gameOver: { correct: true }. WIN condition tripped.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Node 20 reached end of life on 2026-04-30 and gets no fix for the Oct 21 2026
Node.js security release (CVE-2026-111497 Brotli decoder RCE and others).

- .github/workflows/ci.yml: setup-node 20 -> 24
- apps/webuiapps/Dockerfile: builder stage node:20-alpine -> node:24-alpine
  (the final image is nginx; Node only runs at image build time)

Verified under Node 24.22.1: pnpm install --frozen-lockfile and pnpm build
succeed. The lint script fails with the same 7 unfixable ESLint errors under
Node 22 and Node 24, so that failure is unrelated to this change. The
Dockerfile change was not build-tested (no Docker daemon in the sandbox).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X51yD7g9zeQD6rdTypgbxv
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants