feat(chat): merge the Human page into chat as a docked, expandable mascot - #5419
Conversation
…scot The Human page and chat were two tabs driving the same agent over the same threads — one with a text composer, one with a big mascot and a mic. Talking to the mascot meant leaving the conversation. They are now one surface. A small mascot stands on the composer's input box; clicking it scales the mascot up into a right-hand voice stage while the transcript and text composer reflow left and stay live, so voice and text are the same conversation. `/human` becomes a back-compat redirect and the tab is gone (desktop + mobile). New module `app/src/features/human/chatMascot/`: ChatMascotContext shared dock/stage refs + the chat send binding ChatMascotDock the slot standing on the composer (anchor + hit area) ChatMascotStage the voice surface: MicComposer, device selector, TTS switch ChatMascotOverlay the one Rive instance, moved between anchors geometry pure dock <-> stage transform maths Three invariants the implementation is built around: - One Rive instance. A second mascot for the expanded state would load the `.riv` twice and turn the travel into a crossfade. - The mascot re-renders at ~60fps during TTS lipsync, so it is a leaf and the mascot context value is deliberately non-reactive — reactive state lives in Redux or the send-binding external store. A reactive context value would reconcile the whole chat tree every frame (the stall fixed in tinyhumansai#5357). - The canvas is laid out big and only ever scaled DOWN onto an anchor. Scaling a canvas up is a raster stretch, which reads as a blurry mascot. `speakReplies` moves out of ad-hoc localStorage into `mascotSlice`, joined by `chatMascotExpanded`. The legacy `human.speakReplies` key is folded in by a redux-persist `migrate` hook rather than the reducer, so the reducer stays a pure function of (state, action). Also wires `MicComposer.onRecordingChange`, so the mascot finally holds its `listening` pose while the mic is hot — `useHumanMascot` has always supported it and no caller ever passed it.
There was a problem hiding this comment.
graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe Human page and navigation tab are removed. Chat now hosts the mascot dock, stage, overlay, voice controls, and persisted mascot state. ChangesUnified Chat mascot and Human route merge
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ef3d1d5724
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const speakReplies = speakRepliesPref && expanded; | ||
|
|
||
| const { face, visemeCode } = useHumanMascot({ speakReplies, listening }); |
There was a problem hiding this comment.
Cancel TTS playback when the stage collapses
When a reply finishes while the mascot stage is expanded, useHumanMascot starts TTS playback and keeps it running until it ends, unmounts, sees listening, or receives an error; collapsing the stage only changes this computed speakReplies value, which the hook stores in speakRef for future onDone events and does not use to stop an active playbackRef. In that scenario, clicking Back to the chat leaves the docked mascot/audio speaking over the text conversation, so collapse should explicitly cancel current TTS (or unmount the speech driver).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid — fixed in 67af700bd.
Confirmed against the code: cancelTtsPlayback() is called on unmount, chat_error, and barge-in (listening going true), but nothing cancels on speakReplies flipping false. speakRef is only consulted when a new reply arrives, so collapsing the stage mid-reply left the docked mascot talking over the text conversation exactly as described.
Added an effect in useHumanMascot that mirrors the barge-in path — same cancelTtsPlayback() drain (active clip + queued sentences), reset to REST/idle. Put it in the hook rather than the stage so every caller benefits, not just this surface. It no-ops when nothing is playing, so the mount-time speakReplies: false case does no work.
Covered by two tests in useHumanMascot.test.ts: turning speech off mid-reply stops the active clip and drops the queue, and it doesn't disturb an already-silent mascot.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
app/src/lib/i18n/ar.ts (1)
2665-2670: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd Arabic resolution coverage for the mascot keys. The existing coverage test checks key presence only. It does not verify that
I18nProviderresolves the four mascot keys to Arabic values instead of English fallbacks.🤖 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 `@app/src/lib/i18n/ar.ts` around lines 2665 - 2670, Extend the Arabic i18n coverage test for I18nProvider to resolve and assert Arabic values for chat.mascot.expand, chat.mascot.collapse, chat.mascot.speakReplies, and chat.mascot.speakRepliesHint, rather than checking key presence only. Preserve the existing key-presence assertions and verify these resolutions do not fall back to English.Source: Coding guidelines
app/test/e2e/helpers/shared-flows.ts (1)
148-148: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd
navigateViaHash('/human')coverage.
waitForHashRouteReadyalready resolves/humanto#/chat, so passinghashdoes not cause a timeout. Existing route tests do not exercise the helper with/human.🤖 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 `@app/test/e2e/helpers/shared-flows.ts` at line 148, Add coverage in the existing route tests for navigateViaHash using '/human' and verify it resolves through the expected '`#/chat`' route. Reuse the existing waitForHashRouteReady setup and assertions rather than changing the route mapping.app/src/features/human/chatMascot/ChatMascotOverlay.test.tsx (1)
237-246: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDispatch the action creator instead of a hand-written action type.
rerenderWithStatebuilds the action literal{ type: 'mascot/setChatMascotExpanded', payload }. If the reducer is renamed, this dispatch becomes an unknown action andexpandednever flips. The reduced-motion test at Lines 127-138 then passes vacuously, becauserequestAnimationFrameis never called for a transition that never started.Import
setChatMascotExpandedfrom../../../store/mascotSliceand dispatch it.♻️ Proposed refactor
rerenderWithState: (expanded: boolean) => { - utils.store.dispatch({ type: 'mascot/setChatMascotExpanded', payload: expanded }); + utils.store.dispatch(setChatMascotExpanded(expanded)); },Add the import:
import { renderWithProviders } from '../../../test/test-utils'; +import { setChatMascotExpanded } from '../../../store/mascotSlice';🤖 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 `@app/src/features/human/chatMascot/ChatMascotOverlay.test.tsx` around lines 237 - 246, Update renderOverlayWithToggle’s rerenderWithState helper to import and dispatch the setChatMascotExpanded action creator from the mascot slice instead of constructing a literal action object, preserving the existing expanded payload behavior.
🤖 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 `@app/src/AppRoutes.tsx`:
- Around line 123-126: Update app/src/pages/__tests__/AppRoutes.phase6.test.tsx
to render the real AppRoutes component instead of a placeholder, and replace the
Human-page expectation with an assertion that /human redirects to /chat. Add
coverage for the ProtectedRoute authentication boundary, verifying
unauthenticated access is redirected appropriately while authenticated access
reaches the protected route.
In `@app/src/features/human/chatMascot/ChatMascotOverlay.tsx`:
- Around line 215-245: Track the current dock anchor element in state and update
it when the dock mounts or is replaced, then include that state in the
ResizeObserver effect dependencies. Use the resolved anchor state in the
ancestor-observation setup so the observer disconnects from detached nodes and
subscribes to the new dock and its ancestors; ensure the existing polling path
updates this state while resolving the dock.
In `@app/src/features/human/chatMascot/ChatMascotStage.tsx`:
- Around line 63-71: Update the Button in ChatMascotStage’s collapse control to
use the shared analytics API by replacing the data-analytics-id attribute with
the analyticsId prop set to "chat-mascot-collapse"; keep the existing test
identifier unchanged.
In `@app/src/features/human/MicComposer.tsx`:
- Around line 124-126: Add tests for the new callback and dock behavior: in
app/src/features/human/MicComposer.tsx lines 124-126 and 195-207, cover
onRecordingChange notifications with true on recording start and false on stop
and unmount; in app/src/components/chat/ChatComposer.tsx lines 77-83 and
232-235, cover the optional mascotDock prop and verify it renders inside the
input-box anchor container. Update ChatMascotStage.test.tsx or relevant
component tests so MicComposer is not mocked where lifecycle behavior must be
verified.
In `@app/src/lib/i18n/hi.ts`:
- Around line 2728-2729: Update the chat.mascot.expand and chat.mascot.collapse
translations in the Hindi locale to use action-oriented labels describing the
mascot control: “मैस्कॉट खोलें” for expanding and “मैस्कॉट छोटा करें” for
collapsing.
In `@app/src/pages/Accounts.tsx`:
- Around line 99-103: Update the stage column transition in the Accounts
component around the mascot stage div to use prefersReducedMotion() when setting
the inline style: disable the inline transition when reduced motion is
preferred, otherwise retain STAGE_TRANSITION. Import or re-export
prefersReducedMotion through the existing ChatMascotOverlay-related path as
needed, while preserving the current width behavior.
---
Nitpick comments:
In `@app/src/features/human/chatMascot/ChatMascotOverlay.test.tsx`:
- Around line 237-246: Update renderOverlayWithToggle’s rerenderWithState helper
to import and dispatch the setChatMascotExpanded action creator from the mascot
slice instead of constructing a literal action object, preserving the existing
expanded payload behavior.
In `@app/src/lib/i18n/ar.ts`:
- Around line 2665-2670: Extend the Arabic i18n coverage test for I18nProvider
to resolve and assert Arabic values for chat.mascot.expand,
chat.mascot.collapse, chat.mascot.speakReplies, and
chat.mascot.speakRepliesHint, rather than checking key presence only. Preserve
the existing key-presence assertions and verify these resolutions do not fall
back to English.
In `@app/test/e2e/helpers/shared-flows.ts`:
- Line 148: Add coverage in the existing route tests for navigateViaHash using
'/human' and verify it resolves through the expected '`#/chat`' route. Reuse the
existing waitForHashRouteReady setup and assertions rather than changing the
route mapping.
🪄 Autofix
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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3f979ecf-1d89-4c77-9479-24c87bdbb8f9
📒 Files selected for processing (61)
AGENTS.mdapp/src/AppRoutes.auth.test.tsxapp/src/AppRoutes.tsxapp/src/AppRoutesIOS.test.tsxapp/src/AppRoutesIOS.tsxapp/src/components/chat/ChatComposer.tsxapp/src/components/ios/MobileTabBar.test.tsxapp/src/components/ios/MobileTabBar.tsxapp/src/components/layout/shell/CollapsedNavRail.test.tsxapp/src/components/layout/shell/SidebarNav.test.tsxapp/src/components/walkthrough/__tests__/AppWalkthrough.test.tsxapp/src/components/walkthrough/walkthroughSteps.tsapp/src/config/__tests__/navConfig.test.tsapp/src/config/navConfig.tsapp/src/features/conversations/Conversations.tsxapp/src/features/human/HumanPage.test.tsxapp/src/features/human/HumanPage.tsxapp/src/features/human/Mascot/manifest/useMascotManifest.test.tsxapp/src/features/human/MicComposer.tsxapp/src/features/human/chatMascot/ChatMascotContext.tsxapp/src/features/human/chatMascot/ChatMascotDock.test.tsxapp/src/features/human/chatMascot/ChatMascotDock.tsxapp/src/features/human/chatMascot/ChatMascotOverlay.test.tsxapp/src/features/human/chatMascot/ChatMascotOverlay.tsxapp/src/features/human/chatMascot/ChatMascotStage.test.tsxapp/src/features/human/chatMascot/ChatMascotStage.tsxapp/src/features/human/chatMascot/geometry.test.tsapp/src/features/human/chatMascot/geometry.tsapp/src/features/human/chatMascot/index.tsapp/src/features/human/chatMascot/sendBinding.test.tsapp/src/features/human/chatMascot/sendBinding.tsapp/src/features/meet/MascotFrameProducer.tsxapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.tsapp/src/pages/Accounts.tsxapp/src/pages/__tests__/Accounts.mascotStage.test.tsxapp/src/pages/__tests__/Accounts.webviewSelection.test.tsxapp/src/store/__tests__/mascotSlice.chatMascot.test.tsapp/src/store/index.tsapp/src/store/mascotSlice.tsapp/test/e2e/helpers/shared-flows.tsapp/test/e2e/specs/navigation-smoothness.spec.tsapp/test/e2e/specs/navigation.spec.tsapp/test/e2e/specs/voice-mode.spec.tsapp/test/playwright/specs/navigation.spec.tsapp/test/playwright/specs/settings-feature-preferences.spec.tsdocs/TEST-COVERAGE-MATRIX.mdgitbooks/developing/architecture/frontend.mdsrc/openhuman/inference/voice/cloud_transcribe.rs
💤 Files with no reviewable changes (4)
- app/src/features/human/HumanPage.test.tsx
- app/src/AppRoutes.auth.test.tsx
- app/src/features/human/HumanPage.tsx
- app/src/components/layout/shell/CollapsedNavRail.test.tsx
…ng context `Conversations`' root is `relative z-10`, which opens a stacking context. Four overlays live inside it — the agent-process source panel, the background processes panel, the subagent drawer, and the task-board modal — each `fixed inset-0 z-50`. That `z-50` only orders them against their own siblings; from outside, the whole chat subtree is just "z-10", so any sibling with a higher z-index paints straight over them. The chat mascot (z-30) is the first thing to actually sit there and expose it. No z-index on the mascot can resolve this: it has to be above the composer (inside the chat) and below the panels (also inside the chat), and while the chat is one sealed stacking context an outside element is either above everything in it or below everything in it. So the overlays escape instead, via the pattern the rest of the app already uses (`components/ui/ModalShell`, `features/privacy/WhatLeavesMyComputerSheet`): portal to `document.body`, where `z-50` means what it reads like. Two panel tests queried `container.querySelector(...)`; portaled content is not in the render container, so they now assert against `document.body`. The "renders nothing when closed" case was strengthened while it was open — it asserted the container was empty, which after portaling would pass even if the panel HAD rendered; it now checks the panel's testid is absent from the body. Also in this commit: - Cover the mascot's rAF travel. `ChatMascotOverlay` was at 77% lines, under the 80% diff gate, with the whole travel loop untested — the earlier tests only hit the snap and reduced-motion paths, which skip it. Driving rAF by hand with controlled timestamps takes it to 100%: interpolation between anchors, the compositor hint being released on landing, cancellation on unmount, re-settle on layout change, and not fighting the loop mid-travel. - Drop the dead `toggle`. Nothing called it once the dock moved to `expand` and the stage to `collapse`; the unused `toggleChatMascotExpanded` slice action goes with it rather than shipping a third way to change the same state.
There was a problem hiding this comment.
graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
app/src/features/human/chatMascot/ChatMascotOverlay.test.tsx (1)
81-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake the animation-frame fake honor cancellation.
The fake keeps canceled callbacks in
queue. The unmount test can only prove thatcancelAnimationFramewas called. It cannot prove that the canceled frame will not run.Track callbacks by frame ID. Remove the callback from the queue in a mocked
cancelAnimationFrame. After unmount, assert thatframes.pending()is zero.🤖 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 `@app/src/features/human/chatMascot/ChatMascotOverlay.test.tsx` around lines 81 - 84, Update the animation-frame mocks in the ChatMascotOverlay tests to track callbacks by their returned frame IDs, and mock cancelAnimationFrame to remove canceled callbacks from the pending queue. Extend the frame helper with pending() and assert after unmount that frames.pending() is zero, proving cancellation prevents execution.
🤖 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
`@app/src/features/conversations/components/__tests__/AgentProcessSourcePanel.test.tsx`:
- Around line 242-245: Scope both portaled-panel tests to their respective
roots: in
app/src/features/conversations/components/__tests__/AgentProcessSourcePanel.test.tsx
lines 242-245, query data-testid="agent-process-source-panel" and find the
backdrop within it; in
app/src/features/conversations/components/__tests__/BackgroundProcessesPanel.test.tsx
lines 162-167, query data-testid="background-processes-panel" and assert the
text on that root instead of document.body.
---
Nitpick comments:
In `@app/src/features/human/chatMascot/ChatMascotOverlay.test.tsx`:
- Around line 81-84: Update the animation-frame mocks in the ChatMascotOverlay
tests to track callbacks by their returned frame IDs, and mock
cancelAnimationFrame to remove canceled callbacks from the pending queue. Extend
the frame helper with pending() and assert after unmount that frames.pending()
is zero, proving cancellation prevents execution.
🪄 Autofix
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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b4db8d99-012b-42a1-a260-1943e5db6ad8
📒 Files selected for processing (10)
app/src/features/conversations/components/AgentProcessSourcePanel.tsxapp/src/features/conversations/components/BackgroundProcessesPanel.tsxapp/src/features/conversations/components/SubagentDrawer.tsxapp/src/features/conversations/components/TaskKanbanBoard.tsxapp/src/features/conversations/components/__tests__/AgentProcessSourcePanel.test.tsxapp/src/features/conversations/components/__tests__/BackgroundProcessesPanel.test.tsxapp/src/features/human/chatMascot/ChatMascotContext.tsxapp/src/features/human/chatMascot/ChatMascotOverlay.test.tsxapp/src/store/__tests__/mascotSlice.chatMascot.test.tsapp/src/store/mascotSlice.ts
💤 Files with no reviewable changes (1)
- app/src/store/mascotSlice.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/store/tests/mascotSlice.chatMascot.test.ts
Upstream tinyhumansai#5407 added ElevenLabs realtime voice agents to the Human page while this branch was replacing that page with the chat's voice stage. The feature is ported rather than dropped: the gate (build flag + persisted `realtime` mode) now lives on ChatMascotStage, which IS the voice surface, and replaces the turn-based mic when active. - mascotSlice / store persist: union of both sides — upstream's `voiceMode` alongside this branch's `chatMascotExpanded` / `speakReplies`. - HumanPage.realtimeMode.test.tsx targeted the deleted page; replaced by ChatMascotStage.realtimeMode.test.tsx, which pins the same contract plus the classic-mic fallback.
…y motion Seven of the eight review findings were valid; each was verified against the code before changing anything. - **In-flight TTS survived collapsing the stage.** `cancelTtsPlayback()` fires on unmount, `chat_error` and barge-in, but never when `speakReplies` flips false — `speakRef` is only consulted when a NEW reply arrives. Collapsing the voice stage mid-reply therefore left the mascot talking over the text conversation. Cancels now, with the same queue drain as barge-in. - **The ResizeObserver never saw a late-mounting dock.** Its effect keyed on stable refs, so it ran once and read whatever `dockRef.current` was at that moment — `null` in exactly the case the anchor poll exists for, and a detached node after a composer remount. The dock element is now published as state so the subscription re-runs. It lives in its OWN context: putting it on the main value would re-render `Conversations` on every dock mount, which is the non-reactive-context guarantee this module is built on. - **Reduced motion was ignored by the stage column.** `motion-reduce:transition-none` is a class; the column's `transition` is inline (it shares a duration with the mascot's travel), and an inline declaration wins. Users who ask for reduced motion still got the slide while the mascot snapped. Applied in JS instead, so both halves agree. - **`AppRoutes.phase6.test.tsx` could not fail.** It declared its own local route tree, so it asserted a hardcoded fixture rather than the app — and kept passing while its docblock described the `/human` behaviour this PR removed. Replaced with `AppRoutes.humanRedirect.test.tsx`, which renders the real router. - Coverage for the two new contracts flagged as untested: `MicComposer.onRecordingChange` (start, stop, unmount-mid-recording, never-started, and absent-callback) and `ChatComposer.mascotDock` (anchored to the input box, not the header stack). - `Button` collapse control moved to the `analyticsId` API per AGENTS.md. - Portal test queries scoped to each panel root, so unrelated body content cannot satisfy them. Not taken: relabelling the mascot controls to "Open/Minimize mascot". "Talk to your assistant" and "Back to the chat" already describe what the controls do and read better; replied on the thread with the reasoning.
There was a problem hiding this comment.
graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
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 (1)
app/src/features/human/useHumanMascot.ts (1)
466-467: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winClose streamed TTS when
full_responseis absent.If text deltas started a TTS turn and
onDonehas nofull_response, Lines 462-465 return beforefinalizeTtsTurn. The queue stays open, so pending audio can continue after the acknowledgement face changes.Finalize the active streamed turn with an empty fallback. Keep the immediate acknowledgement path only when no streamed turn exists.
Proposed fix
- if (!speakRef.current || !e.full_response?.trim()) { + if ( + !speakRef.current || + (!e.full_response?.trim() && !sawDeltaThisTurnRef.current) + ) { holdThenIdle(ackFace); return; } - finalizeTtsTurn(e.full_response, ackFace); + finalizeTtsTurn(e.full_response ?? '', ackFace);🤖 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 `@app/src/features/human/useHumanMascot.ts` around lines 466 - 467, Update the onDone handling in useHumanMascot so a missing full_response still finalizes any active streamed TTS turn by passing an empty fallback to finalizeTtsTurn. Retain the immediate acknowledgement path only when no streamed turn was started.
🤖 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 `@app/src/pages/__tests__/AppRoutes.humanRedirect.test.tsx`:
- Around line 35-39: Add a useLocation() probe to the Desktop /human route test
and assert that the rendered location pathname is /chat, while retaining the
existing chat-page rendering assertion.
---
Outside diff comments:
In `@app/src/features/human/useHumanMascot.ts`:
- Around line 466-467: Update the onDone handling in useHumanMascot so a missing
full_response still finalizes any active streamed TTS turn by passing an empty
fallback to finalizeTtsTurn. Retain the immediate acknowledgement path only when
no streamed turn was started.
🪄 Autofix
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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 79daf9c9-5ea5-41ac-b1ac-0598946f9f1c
📒 Files selected for processing (32)
app/src/components/chat/__tests__/ChatComposer.test.tsxapp/src/features/conversations/components/__tests__/AgentProcessSourcePanel.test.tsxapp/src/features/conversations/components/__tests__/BackgroundProcessesPanel.test.tsxapp/src/features/human/MicComposer.test.tsxapp/src/features/human/chatMascot/ChatMascotContext.tsxapp/src/features/human/chatMascot/ChatMascotDock.tsxapp/src/features/human/chatMascot/ChatMascotOverlay.tsxapp/src/features/human/chatMascot/ChatMascotStage.tsxapp/src/features/human/chatMascot/index.tsapp/src/features/human/useHumanMascot.test.tsapp/src/features/human/useHumanMascot.tsapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.tsapp/src/pages/Accounts.tsxapp/src/pages/__tests__/Accounts.mascotStage.test.tsxapp/src/pages/__tests__/Accounts.webviewSelection.test.tsxapp/src/pages/__tests__/AppRoutes.humanRedirect.test.tsxapp/src/pages/__tests__/AppRoutes.phase6.test.tsxapp/src/store/index.tsapp/src/store/mascotSlice.ts
💤 Files with no reviewable changes (1)
- app/src/pages/tests/AppRoutes.phase6.test.tsx
🚧 Files skipped from review as they are similar to previous changes (22)
- app/src/features/human/chatMascot/ChatMascotDock.tsx
- app/src/features/conversations/components/tests/AgentProcessSourcePanel.test.tsx
- app/src/lib/i18n/hi.ts
- app/src/features/human/chatMascot/index.ts
- app/src/features/human/chatMascot/ChatMascotOverlay.tsx
- app/src/lib/i18n/ru.ts
- app/src/features/conversations/components/tests/BackgroundProcessesPanel.test.tsx
- app/src/lib/i18n/en.ts
- app/src/lib/i18n/zh-CN.ts
- app/src/lib/i18n/de.ts
- app/src/pages/tests/Accounts.webviewSelection.test.tsx
- app/src/lib/i18n/pt.ts
- app/src/lib/i18n/bn.ts
- app/src/store/index.ts
- app/src/lib/i18n/it.ts
- app/src/lib/i18n/id.ts
- app/src/lib/i18n/ar.ts
- app/src/lib/i18n/ko.ts
- app/src/lib/i18n/fr.ts
- app/src/lib/i18n/pl.ts
- app/src/lib/i18n/es.ts
- app/src/store/mascotSlice.ts
The previous assertion only proved the chat page mounted — but `Accounts` is the element for `/chat`, so it would have passed just as happily if `/human` rendered it directly instead of redirecting. That is the same weakness that let the test this file replaced pass while describing removed behaviour. Adds a `useLocation()` probe and pins the resolved pathname. Verified by sabotage: pointing `/human` at `<Accounts />` makes it fail, and restoring the redirect makes it pass.
There was a problem hiding this comment.
graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
A character standing on your message box is charming until it isn't, and until
now there was no way to say no. Hovering the mascot now reveals a small dismiss
control; clicking it asks first, in the same voice the mascot has everywhere
else, rather than silently deleting it:
Send Tiny away?
No hard feelings if you'd rather have the message box to yourself. You can
bring Tiny back any time from Settings › Appearance › Chat.
[ Keep Tiny ] [ Hide Tiny ]
Three things this gets right on purpose:
- **The dialog names the exact route back.** A control that removes itself and
leaves no visible way to undo is a trap. The copy spells out the settings path
instead of a vague "you can re-enable this later".
- **Dismissing also collapses the voice stage.** Otherwise `chatMascotDismissed`
and `chatMascotExpanded` disagree, and restoring the mascot would pop its stage
open without the user asking for it. The reducer enforces it.
- **Dismissing unmounts the overlay, it does not just hide it.** With no dock to
anchor to, the overlay would park itself off-screen at opacity 0 — an invisible
Rive canvas still re-rendering on every lipsync frame, plus a poll hunting an
anchor that will never mount.
The preference is persisted: someone who does not want a mascot should not have
to dismiss it again on every launch. Restored from the new
Settings → Appearance → Chat switch, framed as "show" so it reads the same
direction as its neighbours even though the stored flag is "dismissed".
Reuses the existing `ConfirmDialog`. Copy added in all 14 locales.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/lib/i18n/es.ts (1)
341-341: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUpdate the remaining Human-tab copy.
voice.mode.descat Line 2479 still sayspestaña Human. This directs users to a tab that no longer exists. Refer to Chat or the mascot voice stage instead.Proposed copy update
- 'voice.mode.desc': 'Elige cómo habla el asistente en la pestaña Human.', + 'voice.mode.desc': 'Elige cómo habla el asistente en el chat.',🤖 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 `@app/src/lib/i18n/es.ts` at line 341, Update the Spanish voice.mode.desc translation in es.ts to remove the obsolete “pestaña Human” reference and instead direct users to Chat or the mascot voice stage, while preserving the rest of the translation.
🤖 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 `@app/src/features/human/chatMascot/ChatMascotDock.tsx`:
- Around line 71-99: Add fixed grep-friendly diagnostic events to the
ChatMascotDock dismissal flow: log when the confirmation dialog opens, when
cancellation closes it, and when confirmation is initiated, using only fixed
event names or boolean state and no identifying data. Update the relevant
onClick, onConfirm, and onCancel handlers while preserving their existing state
updates and dispatch behavior.
In `@app/src/lib/i18n/bn.ts`:
- Around line 2747-2751: Update the Bengali dismissal-dialog translations for
chat.mascot.dismissTitle and chat.mascot.dismissCancel: use wording equivalent
to “Hide Tiny?” in the title and a natural keep action such as “Tiny-কে রাখুন”
for the cancel label, while leaving dismissBody and dismissConfirm unchanged.
In `@app/src/lib/i18n/de.ts`:
- Around line 2825-2826: Update the German translation value for
chat.mascot.dismissBody to replace “Darstellung” with the visible settings label
“Aussehen,” keeping the rest of the restore-path message unchanged.
In `@app/src/lib/i18n/es.ts`:
- Around line 2795-2800: Update the chat.mascot.dismissTitle translation in the
Spanish locale to use “Ocultar” instead of “Despedir,” matching
chat.mascot.dismiss, chat.mascot.dismissConfirm, chat.mascot.dismissCancel, and
the Appearance setting wording.
In `@app/src/lib/i18n/id.ts`:
- Around line 2753-2755: Update the Indonesian translations for
chat.mascot.dismissTitle and chat.mascot.dismissBody to describe hiding Tiny
rather than permanently dismissing it, and reference the correct Indonesian
Settings path for showing Tiny again. Preserve the existing reversible hide/show
behavior and keep both strings consistent.
In `@app/src/lib/i18n/pl.ts`:
- Line 2778: Update the chat.mascot.dismissTitle translation to use “Ukryć
Tiny?” so it matches the reversible hide action wording used by
chat.mascot.dismiss and chat.mascot.dismissConfirm.
In `@app/src/store/mascotSlice.ts`:
- Around line 536-539: Update the rehydration logic in the mascot slice to clear
chatMascotExpanded whenever the restored chatMascotDismissed value is true,
preventing a later setChatMascotDismissed(false) from reopening the voice stage.
Preserve existing fallback behavior for invalid or missing persisted values, and
add a rehydration test covering both persisted flags set to true.
---
Outside diff comments:
In `@app/src/lib/i18n/es.ts`:
- Line 341: Update the Spanish voice.mode.desc translation in es.ts to remove
the obsolete “pestaña Human” reference and instead direct users to Chat or the
mascot voice stage, while preserving the rest of the translation.
🪄 Autofix
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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7c2be9e2-3a22-428a-88c2-a85ada485cf1
📒 Files selected for processing (24)
app/src/components/settings/panels/AppearancePanel.test.tsxapp/src/components/settings/panels/AppearancePanel.tsxapp/src/features/human/chatMascot/ChatMascotDock.test.tsxapp/src/features/human/chatMascot/ChatMascotDock.tsxapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.tsapp/src/pages/Accounts.tsxapp/src/pages/__tests__/Accounts.mascotStage.test.tsxapp/src/pages/__tests__/Accounts.webviewSelection.test.tsxapp/src/store/__tests__/mascotSlice.chatMascot.test.tsapp/src/store/index.tsapp/src/store/mascotSlice.ts
🚧 Files skipped from review as they are similar to previous changes (12)
- app/src/lib/i18n/zh-CN.ts
- app/src/lib/i18n/pt.ts
- app/src/lib/i18n/fr.ts
- app/src/store/tests/mascotSlice.chatMascot.test.ts
- app/src/store/index.ts
- app/src/lib/i18n/hi.ts
- app/src/pages/tests/Accounts.webviewSelection.test.tsx
- app/src/lib/i18n/en.ts
- app/src/lib/i18n/ru.ts
- app/src/lib/i18n/it.ts
- app/src/lib/i18n/ar.ts
- app/src/lib/i18n/ko.ts
| state.chatMascotDismissed = | ||
| typeof rehydrateAction.payload?.chatMascotDismissed === 'boolean' | ||
| ? rehydrateAction.payload.chatMascotDismissed | ||
| : initialState.chatMascotDismissed; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Preserve the dismissed-state invariant during rehydration.
A persisted payload can restore both chatMascotDismissed and chatMascotExpanded as true. If Settings later dispatches setChatMascotDismissed(false), the stale expanded value reopens the voice stage.
Clear chatMascotExpanded after restoring a dismissed mascot. Add a rehydration test for the combined payload.
Proposed fix
state.chatMascotDismissed =
typeof rehydrateAction.payload?.chatMascotDismissed === 'boolean'
? rehydrateAction.payload.chatMascotDismissed
: initialState.chatMascotDismissed;
+ if (state.chatMascotDismissed) {
+ state.chatMascotExpanded = false;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| state.chatMascotDismissed = | |
| typeof rehydrateAction.payload?.chatMascotDismissed === 'boolean' | |
| ? rehydrateAction.payload.chatMascotDismissed | |
| : initialState.chatMascotDismissed; | |
| state.chatMascotDismissed = | |
| typeof rehydrateAction.payload?.chatMascotDismissed === 'boolean' | |
| ? rehydrateAction.payload.chatMascotDismissed | |
| : initialState.chatMascotDismissed; | |
| if (state.chatMascotDismissed) { | |
| state.chatMascotExpanded = false; | |
| } |
🤖 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 `@app/src/store/mascotSlice.ts` around lines 536 - 539, Update the rehydration
logic in the mascot slice to clear chatMascotExpanded whenever the restored
chatMascotDismissed value is true, preventing a later
setChatMascotDismissed(false) from reopening the voice stage. Preserve existing
fallback behavior for invalid or missing persisted values, and add a rehydration
test covering both persisted flags set to true.
The dismiss dialog is the only thing telling a user how to get the mascot back, and in eight languages it named menu items that are not in their UI. The paths were translated from the English string instead of assembled from each locale's own labels: de Darstellung → Aussehen bn চেহারা → উপস্থিতি hi रूप → दिखावट ko 화면 → 외관 fr/pt/id/ar Discussion / Conversa / Obrolan / المحادثة → the real Chat label A dismissed mascot plus a path that goes nowhere is a one-way door, so all 14 now quote the live `nav.settings`, `settings.appearance.title` and `settings.appearance.chatHeading` values. `mascotDismissPath.test.ts` pins that: every locale's body must contain those three label values. Sabotage-checked — restoring `Darstellung` fails it with a message naming the label it should have used. Also from review: - Verbose diagnostics on the new flow, per AGENTS.md. The reducer logged only the final persisted state; the dialog itself logged nothing. Added grep-friendly `[chat-mascot][dismiss]` events for open, confirm and cancel. - One verb per dialog. The title said "Send Tiny away" while every button and the Appearance switch said hide, which reads like two different outcomes for one control. Now "Hide Tiny?" everywhere. - bn: `Tiny থাক` → `Tiny-কে রাখুন` for a natural keep action.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
Both surfaces show the mascot on purpose now. `/human` is the dedicated full-bleed stage with a right-rail chat; `/chat` carries the same mascot docked on the composer, expanding into a voice stage in place. Neither is a redirect. The one thing that could not be restored verbatim is the speak-replies preference. `HumanPage` owned `localStorage['human.speakReplies']`, and this branch's persist migration consumes and deletes that key — so bringing the page back unchanged would have given the Human tab and the chat mascot two toggles for the same setting that silently disagree, on top of dropping whatever the user had already chosen. It reads the shared `mascotSlice` value instead, which is also what makes "both surfaces" coherent rather than two half-features: colour, voice, speak-replies and dismissal are one set of preferences. Restored: HumanPage + its realtime-voice test, the nav tab, the `/human` route, the iOS route and mobile tab entry, and the walkthrough's Human step. `nav.human` and the walkthrough copy are recovered verbatim from d9d03af across all 14 locales rather than re-translated. `AppRoutes.humanRedirect.test.tsx` becomes `AppRoutes.humanRoute.test.tsx` and asserts the opposite of what it did two commits ago — each route serves its own page. It keeps the `useLocation()` probe, so a silent redirect between the two still cannot pass unnoticed.
|
Too many files changed for review (146 files, 100 file limit). Bypass the limit by tagging |
The staged mascot rendered visibly soft. Three earlier attempts in this branch
(dropping a persistent `will-change`, supersampling the render box to 768px,
snapping to whole pixels) all failed, because they shared a wrong assumption:
that the canvas stayed at my layout size and the browser merely scaled the
result. Instrumenting the live canvas showed the opposite:
dpr=2 backing=127x127 css=768px x 768px client=768x768
A 768px box backed by 127 pixels. Two facts explain it:
1. Rive's WebGL2 renderer sizes its backing store from
`getBoundingClientRect()`, which INCLUDES ancestor transforms. Docked at
`scale(0.083)`, the 768px box measures ~64px on screen, so it allocated a
~127px surface (64 x dpr).
2. `ResizeObserver` does not fire on transform changes. Expanding altered only
the transform, so Rive never re-measured — and that 127px texture was
stretched across ~420 CSS px. A 6.6x upscale, permanently.
This also explains why "render big, only ever scale down" made things worse
rather than better: the larger the layout box, the harsher the ratio between it
and the scaled-down surface Rive actually allocated.
So the resting states now use their real layout size with a translate-only
transform. Changing the layout size is precisely what makes Rive's observer fire
and reallocate at the right resolution — the one thing a transform-only approach
can never do:
docked 64px layout -> 128 device px backing, shown at 128 (1:1)
expanded 400px layout -> 800 device px backing, shown at 800 (1:1)
travelling -> fixed 768px box + scale, composited
Transform-scaling is kept for the 320ms travel, where a canvas reallocation per
frame would be the wrong trade and any softness is invisible in motion. The cost
is two reallocations per toggle, which is fine for a user-initiated action.
The regression test records the 127x127 measurement, so the transform-only
approach cannot be reintroduced as an "optimisation".
Summary
/chat. A small mascot stands on the composer's input box; clicking it scales the mascot up into a right-hand voice stage while the transcript and text composer reflow left and stay live./humankeeps the dedicated full-bleed mascot stage;/chatcarries the same mascot docked on the composer. Both read one set of mascot preferences frommascotSlice, so they cannot drift apart. (An earlier revision of this PR removed the tab; that was reverted on request.)app/src/features/human/chatMascot/— one Rive instance moved between two anchors with arequestAnimationFrametravel.speakRepliesout of ad-hoclocalStorageintomascotSlice(joined bychatMascotExpanded), with a redux-persistmigratehook for the legacy key.MicComposer.onRecordingChange, so the mascot holds itslisteningpose while the mic is hot —useHumanMascothas always supported this and no caller ever passed it.Problem
/chatand/humanwere two top-level tabs driving the same agent over the same threads — one with a text composer, one with a big mascot and a mic. Talking to the mascot meant leaving the conversation, and the mascot never appeared in chat at all.pages/Accounts.tsxalready carried a dormantFaceModePanelsplit (chat left / mascot right), permanently disabled withfaceModehard-codedfalse— a half-finished earlier attempt at exactly this merge (IA "Phase 6", later reverted; see the comment inconfig/navConfig.ts).Solution
One surface, Claude/Gemini-style.
pages/Accounts.tsxanimates a right-hand stage column open;ChatMascotOverlayflies the mascot onto it.ChatMascotContext.tsxChatMascotDock.tsxChatMascotStage.tsxMicComposer, device selector, speak-replies switch, collapseChatMascotOverlay.tsxtransformgeometry.tsThree invariants the implementation is built around, each worth knowing before changing it:
One Rive instance. Mounting a second mascot for the expanded state would load the
.rivtwice and turn the travel into a crossfade rather than the mascot actually moving and scaling.The mascot is a leaf, and its context is deliberately non-reactive. It re-renders at ~60fps during TTS lipsync. Every value on
ChatMascotContextis stable (refs, dispatch-bound callbacks); reactive state lives in Redux or in the send-binding external store read viauseSyncExternalStore. A reactive context value would reconcile the whole chat tree every frame — the stall #5357 had to fix on the Human page.The canvas is laid out big and only ever scaled DOWN. Scaling a canvas up is a raster stretch, which reads as a blurry mascot.
STAGE_RENDER_PX(768) clears the largest display size by a real margin,will-change: transformis applied only during the travel (a permanently-hinted layer is rasterized once at the scale it first sees), and the landing position is snapped to whole pixels so the canvas is not resampled across the pixel grid.Two smaller design notes:
speakRepliesmigration is amigratehook, not reducer code. Reading and deleting a localStorage key inside the REHYDRATE reducer would make it impure — replaying the action log would take the other branch the second time.migrateruns before REHYDRATE, so the value simply arrives in the payload.Merged with
main: realtime voice agents carried overUpstream #5407 landed ElevenLabs realtime voice agents on the Human page while this PR was replacing that page. The feature is ported, not dropped:
RealtimeVoiceControlsnow renders onChatMascotStagebehind the same gate (VOICE_MODE_FLAG_ENABLED+ persistedvoiceMode === 'realtime'), replacing the turn-based mic when active. The stage is the voice surface now, so the gate belongs there.mascotSlice+ the persist whitelist are a genuine union: upstream'svoiceModealongside this branch'schatMascotExpanded/speakReplies.HumanPage.realtimeMode.test.tsxtargeted the deleted page → replaced byChatMascotStage.realtimeMode.test.tsx, pinning the same contract plus the classic-mic fallback.Worth a reviewer's eye: git reports a clean resolution whether or not the feature survives, so this is the part of the merge most worth checking.
Both surfaces, one set of preferences
/humanand/chatboth show the mascot by design. The only thing that could not come back verbatim when the Human tab was restored is the speak-replies preference:HumanPageownedlocalStorage['human.speakReplies'], and this branch's persist migration consumes and deletes that key. Restored unchanged it would have produced two toggles for one setting that silently disagree, plus a dropped user preference.HumanPagenow reads the sharedmascotSlicevalue — as do colour, voice and dismissal.Submission Checklist
pnpm test:coverage. Changed-file line coverage:ChatMascotOverlay.tsx100%,Accounts.tsx100%,ChatComposer.tsx96.7%,mascotSlice.ts95.3%,geometry.ts92.9%,MicComposer.tsx92.5%,ChatMascotContext.tsx88.9%.docs/TEST-COVERAGE-MATRIX.mdnow points atchatMascot/ChatMascotOverlay.test.tsxinstead of the deletedHumanPage.test.tsx## RelatedN/A: no new release-cut surface; /chat and the mascot are both already covered.Closes #NNN—N/A: requested directly, no tracking issue.Impact
Desktop + mobile. The Human tab disappears from the sidebar and the iOS tab bar.
/humanredirects, so saved deep links keep working.Performance — one regression worth reviewing. The mascot now renders continuously on
/chat, the default landing surface, where it previously only ran on a tab you opted into. WithidlePoseRotationon, the Rive state machine keeps animating even when idle. For scale: the old Human page rendered the mascot atmin(80vh, 90%)(~860px on a tall window), so this is less work than that surface — but it is new always-on cost on chat. If it bites, the fix is to give the overlay its realwidth/heightat rest with a translate-only transform, so the docked mascot renders a 64px canvas instead of 768px; I deliberately left that out to keep the travel a single code path.Migration.
human.speakRepliesis folded into the persisted mascot blob once, then deleted. Users who turned TTS off keep it off.Compatibility. The
composer/projectThreadListprops onConversationslose their only production caller (HumanPage) but are kept — they are still exercised by three test files, and themic-cloudmode stays reachable through the composer's mic button.Related
fixed inset-0 z-50overlays were trapped insideConversations'relative z-10stacking context, so any sibling with a higher z-index painted over them. Portaled todocument.bodyper the existingModalShellpattern.features/human/SubMascotLayer.tsxis dead code (only its own test imports it). Pre-existing — verified againstd9d03af3e, not introduced here. Worth a separate cleanup.## Impact).AI Authored PR Metadata
Linear Issue
Commit & Branch
feat/merge-human-into-chatef3d1d572(feature),e1b6761eb(z-index + coverage),3d6d6d5a7(merge + realtime port),67af700bd(review fixes)Validation Run
pnpm format:check(prettier +cargo fmt --check) — cleanpnpm typecheck— cleansrc/features/human/chatMascot(43),mascotSlice.chatMascot,Accounts.mascotStage,Accounts.webviewSelection,config,components/layout/shell,components/ios,components/walkthrough,AppRoutes*pnpm test— 792 files, 9341 tests passed, 0 failurespnpm lint— 0 errors (98 pre-existing warnings)pnpm i18n:check+pnpm i18n:english:check— 0 missing / 0 extra / 0 untranslated across 14 localespnpm docs:check— generated docs up to dateGGML_NATIVE=OFF cargo check— clean (doc-comment change only)N/A— noapp/src-taurichangespnpm dev:app) and exercised dock → stage → dockValidation Blocked
command:WDIO / Playwright E2Eerror:not run — they need a built app bundleimpact:route tables and specs are updated in this PR (shared-flowsHASH_REDIRECTS, bothnavigation.specs,navigation-smoothness,voice-mode,settings-feature-preferences) but were not executed locally. CI Full covers them.Behavior Changes
/humanredirects to/chat; speak-replies is now scoped to the expanded stage, so a docked mascot never starts talking over a text conversation.Parity Contract
useHumanMascot, same mascot renderers/manifest/palette, sameMicComposer, same send path./humandeep links still resolve.speakRepliesmigrates rather than resetting.useChatMascotOptional()returnsnulloutside the merged surface, so the embedded sidebar (Workflow Copilot) and iOS render exactly as before; themic-cloudcomposer override path is untouched.Duplicate / Superseded PR Handling
Summary by CodeRabbit
New Features
Navigation
/humanredirect to/chat.Documentation