Skip to content

feat(chat): merge the Human page into chat as a docked, expandable mascot - #5419

Merged
graycyrus merged 9 commits into
tinyhumansai:mainfrom
graycyrus:feat/merge-human-into-chat
Aug 7, 2026
Merged

feat(chat): merge the Human page into chat as a docked, expandable mascot#5419
graycyrus merged 9 commits into
tinyhumansai:mainfrom
graycyrus:feat/merge-human-into-chat

Conversation

@graycyrus

@graycyrus graycyrus commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Merges the Human page into /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.
  • Human stays its own tab. /human keeps the dedicated full-bleed mascot stage; /chat carries the same mascot docked on the composer. Both read one set of mascot preferences from mascotSlice, so they cannot drift apart. (An earlier revision of this PR removed the tab; that was reverted on request.)
  • Adds app/src/features/human/chatMascot/ — one Rive instance moved between two anchors with a requestAnimationFrame travel.
  • Moves speakReplies out of ad-hoc localStorage into mascotSlice (joined by chatMascotExpanded), with a redux-persist migrate hook for the legacy key.
  • Wires MicComposer.onRecordingChange, so the mascot holds its listening pose while the mic is hot — useHumanMascot has always supported this and no caller ever passed it.

Problem

/chat and /human were 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.tsx already carried a dormant FaceModePanel split (chat left / mascot right), permanently disabled with faceMode hard-coded false — a half-finished earlier attempt at exactly this merge (IA "Phase 6", later reverted; see the comment in config/navConfig.ts).

Solution

One surface, Claude/Gemini-style. pages/Accounts.tsx animates a right-hand stage column open; ChatMascotOverlay flies the mascot onto it.

Module Role
ChatMascotContext.tsx Shared dock/stage refs + the chat send binding
ChatMascotDock.tsx The slot standing on the composer — anchor rect + hit area, draws nothing
ChatMascotStage.tsx The voice surface: MicComposer, device selector, speak-replies switch, collapse
ChatMascotOverlay.tsx The one Rive instance, moved between anchors by transform
geometry.ts Pure dock ⇄ stage transform maths

Three 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 .riv twice 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 ChatMascotContext is stable (refs, dispatch-bound callbacks); reactive state lives in Redux or in the send-binding external store read via useSyncExternalStore. 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: transform is 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:

  • The overlay only mounts while the agent account is selected. HTML paints behind the native CEF provider webviews, so a fixed overlay left alive under WhatsApp/Slack would be an invisible canvas still burning frames.
  • The legacy speakReplies migration is a migrate hook, 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. migrate runs before REHYDRATE, so the value simply arrives in the payload.

Merged with main: realtime voice agents carried over

Upstream #5407 landed ElevenLabs realtime voice agents on the Human page while this PR was replacing that page. The feature is ported, not dropped:

  • RealtimeVoiceControls now renders on ChatMascotStage behind the same gate (VOICE_MODE_FLAG_ENABLED + persisted voiceMode === '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's voiceMode alongside this branch's chatMascotExpanded / speakReplies.
  • HumanPage.realtimeMode.test.tsx targeted the deleted page → replaced by ChatMascotStage.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

/human and /chat both 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: HumanPage owned localStorage['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. HumanPage now reads the shared mascotSlice value — as do colour, voice and dismissal.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80%pnpm test:coverage. Changed-file line coverage: ChatMascotOverlay.tsx 100%, Accounts.tsx 100%, ChatComposer.tsx 96.7%, mascotSlice.ts 95.3%, geometry.ts 92.9%, MicComposer.tsx 92.5%, ChatMascotContext.tsx 88.9%.

    Correction: when this PR was first opened ChatMascotOverlay.tsx was at 77.3%, below the gate — the rAF travel loop had no test at all (the existing tests only hit the snap and reduced-motion paths, which skip it). Fixed in e1b6761eb; the box was wrong when first ticked.

  • Coverage matrix updated — the mascot row in docs/TEST-COVERAGE-MATRIX.md now points at chatMascot/ChatMascotOverlay.test.tsx instead of the deleted HumanPage.test.tsx
  • All affected feature IDs from the matrix are listed in the PR description under ## Related
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: no new release-cut surface; /chat and the mascot are both already covered.
  • Linked issue closed via Closes #NNNN/A: requested directly, no tracking issue.

Impact

Desktop + mobile. The Human tab disappears from the sidebar and the iOS tab bar. /human redirects, 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. With idlePoseRotation on, the Rive state machine keeps animating even when idle. For scale: the old Human page rendered the mascot at min(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 real width/height at 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.speakReplies is folded into the persisted mascot blob once, then deleted. Users who turned TTS off keep it off.

Compatibility. The composer / projectThreadList props on Conversations lose their only production caller (HumanPage) but are kept — they are still exercised by three test files, and the mic-cloud mode stays reachable through the composer's mic button.

Related

  • Coverage matrix feature IDs: 4.3.4 (Subagent Mascot Visualization) — test-path reference updated only.
  • Closes: N/A — requested directly, no tracking issue.
  • Fixed here (pre-existing, surfaced by this PR): the chat's four fixed inset-0 z-50 overlays were trapped inside Conversations' relative z-10 stacking context, so any sibling with a higher z-index painted over them. Portaled to document.body per the existing ModalShell pattern.
  • Follow-up PR(s)/TODOs:
    • features/human/SubMascotLayer.tsx is dead code (only its own test imports it). Pre-existing — verified against d9d03af3e, not introduced here. Worth a separate cleanup.
    • Optional perf follow-up: exact-size layout at rest (see ## Impact).

AI Authored PR Metadata

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/merge-human-into-chat
  • Commit SHA: ef3d1d572 (feature), e1b6761eb (z-index + coverage), 3d6d6d5a7 (merge + realtime port), 67af700bd (review fixes)

Validation Run

  • pnpm format:check (prettier + cargo fmt --check) — clean
  • pnpm typecheck — clean
  • Focused tests: src/features/human/chatMascot (43), mascotSlice.chatMascot, Accounts.mascotStage, Accounts.webviewSelection, config, components/layout/shell, components/ios, components/walkthrough, AppRoutes*
  • Full suite: pnpm test792 files, 9341 tests passed, 0 failures
  • pnpm lint — 0 errors (98 pre-existing warnings)
  • pnpm i18n:check + pnpm i18n:english:check — 0 missing / 0 extra / 0 untranslated across 14 locales
  • pnpm docs:check — generated docs up to date
  • Rust: GGML_NATIVE=OFF cargo check — clean (doc-comment change only)
  • Tauri fmt/check: N/A — no app/src-tauri changes
  • Ran the real app (pnpm dev:app) and exercised dock → stage → dock

Validation Blocked

  • command: WDIO / Playwright E2E
  • error: not run — they need a built app bundle
  • impact: route tables and specs are updated in this PR (shared-flows HASH_REDIRECTS, both navigation.specs, navigation-smoothness, voice-mode, settings-feature-preferences) but were not executed locally. CI Full covers them.

Behavior Changes

  • Intended behavior change: Human and chat become one surface; the Human tab is removed.
  • User-visible effect: mascot docks on the composer and expands into a voice stage in place; /human redirects to /chat; speak-replies is now scoped to the expanded stage, so a docked mascot never starts talking over a text conversation.

Parity Contract

  • Legacy behavior preserved: same useHumanMascot, same mascot renderers/manifest/palette, same MicComposer, same send path. /human deep links still resolve. speakReplies migrates rather than resetting.
  • Guard/fallback/dispatch parity checks: useChatMascotOptional() returns null outside the merged surface, so the embedded sidebar (Workflow Copilot) and iOS render exactly as before; the mic-cloud composer override path is untouched.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this one
  • Resolution: N/A

Summary by CodeRabbit

  • New Features

    • Integrated the interactive mascot into Chat with docked and expanded views.
    • Added microphone input, recording feedback, spoken-reply preferences, and mascot controls.
    • Added a setting to show or hide the chat mascot.
    • Preserved mascot preferences across sessions and migrated existing spoken-reply settings.
    • Improved overlay behavior for panels and dialogs.
    • Stopping spoken replies now immediately ends active playback.
  • Navigation

    • Merged Human into Chat and removed the Human navigation tab.
    • Added a backward-compatible /human redirect to /chat.
    • Updated mobile navigation and onboarding walkthroughs.
  • Documentation

    • Updated architecture and test coverage documentation.
    • Added translations for mascot controls and spoken replies.

…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.
@graycyrus
graycyrus requested a review from a team August 6, 2026 06:27

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The Human page and navigation tab are removed. Chat now hosts the mascot dock, stage, overlay, voice controls, and persisted mascot state. /human redirects to /chat, with related tests, translations, portals, and documentation updated.

Changes

Unified Chat mascot and Human route merge

Layer / File(s) Summary
Chat routing and navigation
app/src/AppRoutes*.tsx, app/src/config/*, app/src/components/ios/*, app/src/components/walkthrough/*
Chat is the primary surface. /human redirects to /chat. Mobile navigation and walkthrough steps no longer include Human.
Mascot state and integration contracts
app/src/features/human/chatMascot/*, app/src/features/human/MicComposer.tsx, app/src/store/*
Adds mascot context, send bindings, geometry utilities, recording callbacks, Redux state, persistence, and legacy speech-preference migration.
Mascot dock, stage, and overlay
app/src/features/conversations/Conversations.tsx, app/src/components/chat/ChatComposer.tsx, app/src/features/human/chatMascot/*
Adds mascot expansion, microphone submission, error handling, animation, anchor measurement, and speech controls.
Accounts integration
app/src/pages/Accounts.tsx, app/src/pages/__tests__/*
Replaces the legacy Human panel with an agent-gated mascot provider, stage column, overlay, and preserved conversation transcript.
Conversation overlay portals
app/src/features/conversations/components/*, app/src/features/conversations/components/__tests__/*
Moves conversation panels and dialogs into document.body through React portals and updates portal-aware tests.
Compatibility updates
app/src/lib/i18n/*, app/test/*, docs/*, gitbooks/*, AGENTS.md, src/openhuman/inference/voice/cloud_transcribe.rs
Updates translations, navigation coverage, architecture references, route documentation, and voice-module documentation.

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

Possibly related issues

Possibly related PRs

  • tinyhumansai/openhuman#5407: Both PRs modify the Human voice experience. This PR replaces that experience with the unified Chat mascot flow.

Suggested labels: feature

Suggested reviewers: senamakel

Poem

A rabbit hops into Chat,
The mascot docks beside its hat.
Voice and stage share one clear view,
/human redirects as routes renew.
State persists through every hop—
The carrot-powered tests all stop!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes merging the Human page into chat with a docked, expandable mascot.

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

@coderabbitai coderabbitai Bot added the feature Net-new user-facing capability or product behavior. label Aug 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +89 to +91
const speakReplies = speakRepliesPref && expanded;

const { face, visemeCode } = useHumanMascot({ speakReplies, listening });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@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

🧹 Nitpick comments (3)
app/src/lib/i18n/ar.ts (1)

2665-2670: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Arabic resolution coverage for the mascot keys. The existing coverage test checks key presence only. It does not verify that I18nProvider resolves 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 win

Add navigateViaHash('/human') coverage.

waitForHashRouteReady already resolves /human to #/chat, so passing hash does 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 win

Dispatch the action creator instead of a hand-written action type.

rerenderWithState builds the action literal { type: 'mascot/setChatMascotExpanded', payload }. If the reducer is renamed, this dispatch becomes an unknown action and expanded never flips. The reduced-motion test at Lines 127-138 then passes vacuously, because requestAnimationFrame is never called for a transition that never started.

Import setChatMascotExpanded from ../../../store/mascotSlice and 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

📥 Commits

Reviewing files that changed from the base of the PR and between e29bfc6 and ef3d1d5.

📒 Files selected for processing (61)
  • AGENTS.md
  • app/src/AppRoutes.auth.test.tsx
  • app/src/AppRoutes.tsx
  • app/src/AppRoutesIOS.test.tsx
  • app/src/AppRoutesIOS.tsx
  • app/src/components/chat/ChatComposer.tsx
  • app/src/components/ios/MobileTabBar.test.tsx
  • app/src/components/ios/MobileTabBar.tsx
  • app/src/components/layout/shell/CollapsedNavRail.test.tsx
  • app/src/components/layout/shell/SidebarNav.test.tsx
  • app/src/components/walkthrough/__tests__/AppWalkthrough.test.tsx
  • app/src/components/walkthrough/walkthroughSteps.ts
  • app/src/config/__tests__/navConfig.test.ts
  • app/src/config/navConfig.ts
  • app/src/features/conversations/Conversations.tsx
  • app/src/features/human/HumanPage.test.tsx
  • app/src/features/human/HumanPage.tsx
  • app/src/features/human/Mascot/manifest/useMascotManifest.test.tsx
  • app/src/features/human/MicComposer.tsx
  • app/src/features/human/chatMascot/ChatMascotContext.tsx
  • app/src/features/human/chatMascot/ChatMascotDock.test.tsx
  • app/src/features/human/chatMascot/ChatMascotDock.tsx
  • app/src/features/human/chatMascot/ChatMascotOverlay.test.tsx
  • app/src/features/human/chatMascot/ChatMascotOverlay.tsx
  • app/src/features/human/chatMascot/ChatMascotStage.test.tsx
  • app/src/features/human/chatMascot/ChatMascotStage.tsx
  • app/src/features/human/chatMascot/geometry.test.ts
  • app/src/features/human/chatMascot/geometry.ts
  • app/src/features/human/chatMascot/index.ts
  • app/src/features/human/chatMascot/sendBinding.test.ts
  • app/src/features/human/chatMascot/sendBinding.ts
  • app/src/features/meet/MascotFrameProducer.tsx
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/pages/Accounts.tsx
  • app/src/pages/__tests__/Accounts.mascotStage.test.tsx
  • app/src/pages/__tests__/Accounts.webviewSelection.test.tsx
  • app/src/store/__tests__/mascotSlice.chatMascot.test.ts
  • app/src/store/index.ts
  • app/src/store/mascotSlice.ts
  • app/test/e2e/helpers/shared-flows.ts
  • app/test/e2e/specs/navigation-smoothness.spec.ts
  • app/test/e2e/specs/navigation.spec.ts
  • app/test/e2e/specs/voice-mode.spec.ts
  • app/test/playwright/specs/navigation.spec.ts
  • app/test/playwright/specs/settings-feature-preferences.spec.ts
  • docs/TEST-COVERAGE-MATRIX.md
  • gitbooks/developing/architecture/frontend.md
  • src/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

Comment thread app/src/AppRoutes.tsx Outdated
Comment thread app/src/features/human/chatMascot/ChatMascotOverlay.tsx Outdated
Comment thread app/src/features/human/chatMascot/ChatMascotStage.tsx
Comment thread app/src/features/human/MicComposer.tsx
Comment thread app/src/lib/i18n/hi.ts
Comment thread app/src/pages/Accounts.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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@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

🧹 Nitpick comments (1)
app/src/features/human/chatMascot/ChatMascotOverlay.test.tsx (1)

81-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the animation-frame fake honor cancellation.

The fake keeps canceled callbacks in queue. The unmount test can only prove that cancelAnimationFrame was 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 that frames.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

📥 Commits

Reviewing files that changed from the base of the PR and between ef3d1d5 and e1b6761.

📒 Files selected for processing (10)
  • app/src/features/conversations/components/AgentProcessSourcePanel.tsx
  • app/src/features/conversations/components/BackgroundProcessesPanel.tsx
  • app/src/features/conversations/components/SubagentDrawer.tsx
  • app/src/features/conversations/components/TaskKanbanBoard.tsx
  • app/src/features/conversations/components/__tests__/AgentProcessSourcePanel.test.tsx
  • app/src/features/conversations/components/__tests__/BackgroundProcessesPanel.test.tsx
  • app/src/features/human/chatMascot/ChatMascotContext.tsx
  • app/src/features/human/chatMascot/ChatMascotOverlay.test.tsx
  • app/src/store/__tests__/mascotSlice.chatMascot.test.ts
  • app/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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@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 (1)
app/src/features/human/useHumanMascot.ts (1)

466-467: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Close streamed TTS when full_response is absent.

If text deltas started a TTS turn and onDone has no full_response, Lines 462-465 return before finalizeTtsTurn. 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

📥 Commits

Reviewing files that changed from the base of the PR and between e1b6761 and 67af700.

📒 Files selected for processing (32)
  • app/src/components/chat/__tests__/ChatComposer.test.tsx
  • app/src/features/conversations/components/__tests__/AgentProcessSourcePanel.test.tsx
  • app/src/features/conversations/components/__tests__/BackgroundProcessesPanel.test.tsx
  • app/src/features/human/MicComposer.test.tsx
  • app/src/features/human/chatMascot/ChatMascotContext.tsx
  • app/src/features/human/chatMascot/ChatMascotDock.tsx
  • app/src/features/human/chatMascot/ChatMascotOverlay.tsx
  • app/src/features/human/chatMascot/ChatMascotStage.tsx
  • app/src/features/human/chatMascot/index.ts
  • app/src/features/human/useHumanMascot.test.ts
  • app/src/features/human/useHumanMascot.ts
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/pages/Accounts.tsx
  • app/src/pages/__tests__/Accounts.mascotStage.test.tsx
  • app/src/pages/__tests__/Accounts.webviewSelection.test.tsx
  • app/src/pages/__tests__/AppRoutes.humanRedirect.test.tsx
  • app/src/pages/__tests__/AppRoutes.phase6.test.tsx
  • app/src/store/index.ts
  • app/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

Comment thread app/src/pages/__tests__/AppRoutes.humanRedirect.test.tsx Outdated
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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 6, 2026
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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@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: 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 win

Update the remaining Human-tab copy.

voice.mode.desc at Line 2479 still says pestañ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

📥 Commits

Reviewing files that changed from the base of the PR and between b8630df and e3bcc13.

📒 Files selected for processing (24)
  • app/src/components/settings/panels/AppearancePanel.test.tsx
  • app/src/components/settings/panels/AppearancePanel.tsx
  • app/src/features/human/chatMascot/ChatMascotDock.test.tsx
  • app/src/features/human/chatMascot/ChatMascotDock.tsx
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/pages/Accounts.tsx
  • app/src/pages/__tests__/Accounts.mascotStage.test.tsx
  • app/src/pages/__tests__/Accounts.webviewSelection.test.tsx
  • app/src/store/__tests__/mascotSlice.chatMascot.test.ts
  • app/src/store/index.ts
  • app/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

Comment thread app/src/features/human/chatMascot/ChatMascotDock.tsx Outdated
Comment thread app/src/lib/i18n/bn.ts Outdated
Comment thread app/src/lib/i18n/de.ts Outdated
Comment thread app/src/lib/i18n/es.ts
Comment thread app/src/lib/i18n/id.ts Outdated
Comment thread app/src/lib/i18n/pl.ts Outdated
Comment on lines +536 to +539
state.chatMascotDismissed =
typeof rehydrateAction.payload?.chatMascotDismissed === 'boolean'
? rehydrateAction.payload.chatMascotDismissed
: initialState.chatMascotDismissed;

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.

🎯 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.

Suggested change
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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.
@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown

Too many files changed for review (146 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

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".
@graycyrus
graycyrus merged commit fd6e8c6 into tinyhumansai:main Aug 7, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant