426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
315 lines
18 KiB
Markdown
315 lines
18 KiB
Markdown
# Chat window UX: scrolling, gestures, search, mic
|
||
|
||
## Summary
|
||
|
||
Workstream research for the LifeOps Personal Assistant MVP (GitHub project 15).
|
||
Chat is the MVP's primary surface: every LifeOps flow (reminders, goals, todos,
|
||
scheduling, coordination) is driven through the continuous chat overlay, so its
|
||
scrolling, gesture, search, and voice affordances must be solid for every kind
|
||
of user — children, adults with ADHD/ADD/Asperger's/autism, neurotypical
|
||
adults, and elderly people — with no therapy language and no special rails,
|
||
just a chat window that never fights the hand that drives it.
|
||
|
||
The audit's headline: **the gesture/detent system asked for in the brief is
|
||
already built, well-documented, and CI-gated** (pull-to-maximize landed in
|
||
#13531; the maximize button is already gone; detent magnetism, springs, and
|
||
haptics all exist). What is actually missing or broken is narrower: **no
|
||
history pagination at all** (hard 80-row render cap over a 200-message server
|
||
window), **a transcript that can silently become horizontally scrollable**,
|
||
**message search that exists end-to-end but is unreachable from the primary
|
||
surface**, **a mic button that does not pulse while recording** on the primary
|
||
surface, and **a stale Playwright suite still asserting buttons #13531
|
||
removed**. The MVP plan is fix-and-verify, not build: six issues, five of
|
||
which delete, wire, or repair what already exists.
|
||
|
||
## Current state
|
||
|
||
### Render paths (three, not two)
|
||
|
||
1. **`ContinuousChatOverlay`** — the primary surface, always mounted over every
|
||
view ([packages/ui/src/App.tsx:2513](../../../ui/src/App.tsx),
|
||
[packages/ui/src/components/shell/ContinuousChatOverlay.tsx:128-157](../../../ui/src/components/shell/ContinuousChatOverlay.tsx)).
|
||
One infinite thread since #13531 — no conversation switcher, no clear/new-chat.
|
||
2. **`ChatSurface` + `AssistantOverlay` + `HomePill`** — mounted only by
|
||
`ShellFoundationMount` for the desktop OS chat-overlay window and the kiosk
|
||
shell ([App.tsx:496](../../../ui/src/App.tsx), [App.tsx:539](../../../ui/src/App.tsx)).
|
||
3. **`ChatView`** (1,536 lines) — desktop *detached* chat windows only, via
|
||
[packages/app-core/src/runtime/desktop/AppWindowRenderer.tsx:44](../../../app-core/src/runtime/desktop/AppWindowRenderer.tsx)
|
||
and `DetachedShellRoot.tsx:71`. Not routed anywhere in the web/mobile shell.
|
||
|
||
Message *text* additionally renders through two independent code paths (overlay
|
||
body renderer vs `MessageContent.tsx`), pinned by a render-parity contract test
|
||
(`parser-parity.contract.test.ts`, `render-parity.contract.tsx`). Any formatting
|
||
change must land in both.
|
||
|
||
### Sheet state machine, detents, gestures — built and tested
|
||
|
||
Single source of truth: `ChatMode = "pill" | "input" | "half" | "full"` with
|
||
derived `ChatState` (`CLOSED | INPUT | OPEN_UNDER_HALF | OPEN_HALF_OR_OVER |
|
||
MAXIMIZED`) — [ContinuousChatOverlay.tsx:177-193](../../../ui/src/components/shell/ContinuousChatOverlay.tsx).
|
||
The interaction constants ARE the spec (all verified in code):
|
||
|
||
| Parameter | Value | Ref (ContinuousChatOverlay.tsx) |
|
||
|---|---|---|
|
||
| HALF detent | 0.46 × viewport height | `:197` |
|
||
| FULL detent | ≈0.9 × viewport (inset, under status bar) | `:198-206` |
|
||
| Pull-to-maximize | peak raw pull ≥ max(0.8×vh, FULL) + 56px over-pull | `:207`, `:3228-3237` |
|
||
| Restore-from-maximized | downward pull starting in top 20% of panel | `:211`, `:3878-3892` |
|
||
| Detent magnet | release within 64px snaps to detent, else free-rest | `:219`, `:3385-3410` |
|
||
| Pill→input morph distance | 120px of finger travel; excess flows into thread height | `:265`, `:3158-3183` |
|
||
| Sheet spring | stiffness 320 / damping 34 / mass 0.9 | `:247-252` |
|
||
| Pill-open spring | stiffness 300 / damping 26 / mass 0.85 | `:256-261` |
|
||
| Detent haptic | Capacitor light impact per detent cross | `:228-246` |
|
||
|
||
The live drag writes the `flexBasis` MotionValue directly (no React re-render
|
||
per frame, `:3985-3995`); flicks step one detent; slow drags free-rest with
|
||
magnetism; pull-down through input→pill reverses the morph under the finger
|
||
(`:3171-3183`). **The maximize button is already removed** — header carries
|
||
only a Launcher button (`:3895-3901`), and
|
||
`ContinuousChatOverlay.test.tsx:2308-2309` pins `chat-full-maximize` /
|
||
`chat-full-clear` absent. Pull-to-maximize + top-20% restore are e2e-covered
|
||
with real CDP touch (`__e2e__/run-chat-sheet-e2e.mjs:256-354`).
|
||
|
||
Gesture engine: `use-pull-gesture.ts` — axis lock at 8px slop, widened
|
||
0.8-dominance cone for swipes, rAF-coalesced drag, commit-on-`pointercancel`
|
||
for Android (#9943), capture-loss handling for rotation. **Weakness:** its
|
||
`verticalScrollPriority` option (`:87-102`, built for the transcript,
|
||
#chat-scroll-web) has **zero consumers** — dead code since #13531 removed the
|
||
transcript swipe binding (grep: only definition sites match).
|
||
|
||
### Scrolling — what works and what is missing
|
||
|
||
Works: the transcript scroller (`#continuous-thread`,
|
||
[ContinuousChatOverlay.tsx:4025](../../../ui/src/components/shell/ContinuousChatOverlay.tsx))
|
||
is `touch-pan-y overflow-y-auto overscroll-contain` with the iOS
|
||
`-webkit-overflow-scrolling:touch` fix and a WebKit-safe bounded-height flex
|
||
chain (`:3966-3995`, guarded by `run-chat-scroll-web-e2e.mjs` on Chromium AND
|
||
WebKit). Tail-follow / jump-to-latest is the shared `useThreadAutoScroll`
|
||
engine ([packages/ui/src/hooks/useThreadAutoScroll.ts:1-23](../../../ui/src/hooks/useThreadAutoScroll.ts),
|
||
wired at `:4126`).
|
||
|
||
**Missing — history pagination (req 1).** `GET
|
||
/api/conversations/:id/messages` returns only the newest 200
|
||
(`CONVERSATION_MESSAGE_WINDOW`,
|
||
[packages/agent/src/api/conversation-routes.ts:1050](../../../agent/src/api/conversation-routes.ts),
|
||
`:1684-1688`); there is no `before` cursor — only `?around=<id>` for search
|
||
jumps (`:1052-1118`). The client fetches the window as a full replace
|
||
([packages/ui/src/state/useDataLoaders.ts:336-368](../../../ui/src/state/useDataLoaders.ts))
|
||
and the overlay renders at most the last **80** rows
|
||
(`MAX_RENDERED_SHELL_MESSAGES`,
|
||
[packages/ui/src/components/shell/shell-state.ts:60](../../../ui/src/components/shell/shell-state.ts)).
|
||
Scrolling up simply ends. `runtime.getMemories` already supports
|
||
`end`+`limit`+`orderBy desc` (used by the around-window), so a cursor is a
|
||
small, store-pushed addition.
|
||
|
||
**Broken — horizontal-scroll "unlock" (req 1).** Root cause found statically:
|
||
the scroller sets `overflow-y-auto` but **not** `overflow-x-hidden` (`:4025`).
|
||
Per CSS Overflow, specifying one axis coerces the other from `visible` to
|
||
`auto` — so the moment any child overflows horizontally, the transcript itself
|
||
becomes a horizontal scroll container. `touch-pan-y` blocks touch panning, but
|
||
**trackpad/wheel deltas ignore `touch-action`** — on desktop a diagonal
|
||
two-finger scroll pans the thread sideways. Overflowing children exist today:
|
||
text/code attachment previews use both-axes `overflow-auto` with no
|
||
`overscroll-x-contain` ([MessageAttachments.tsx:831](../../../ui/src/components/chat/MessageAttachments.tsx));
|
||
inline widgets are unaudited for min-width. The other chat path already does
|
||
this right: `chat-thread-layout.tsx:106-107` pairs `overflow-x-hidden
|
||
overflow-y-auto`. Designed horizontal scrollers inside the thread
|
||
(code blocks `MessageContent.tsx:572,1242`, chips bar `TopicChipsBar.tsx:30`)
|
||
are fine — they carry `overscroll-x-contain`.
|
||
|
||
### Search (req 2) — exists end-to-end, unreachable on the primary surface
|
||
|
||
- Server: `GET /api/conversations/messages/search` — SQL `ILIKE` pushdown
|
||
scoped to accessible rooms, BM25 ranking, snippets, limit clamp 50
|
||
([conversation-routes.ts:1470-1545](../../../agent/src/api/conversation-routes.ts)).
|
||
- Client API: `searchConversationMessages()`
|
||
([packages/ui/src/api/client-chat.ts:334-337](../../../ui/src/api/client-chat.ts)).
|
||
- Jump plumbing: `?around=` window load
|
||
(`useDataLoaders.ts:451-481`, #9955).
|
||
- UI: `MessageSearchPanel`
|
||
([packages/ui/src/components/chat/message-search/MessageSearchPanel.tsx:33](../../../ui/src/components/chat/message-search/MessageSearchPanel.tsx))
|
||
— mounted **only** inside `ConversationsSidebar:971`, which is mounted
|
||
**only** in the desktop detached shell (`DetachedShellRoot.tsx:157`). On
|
||
web/mobile — the MVP surfaces — search is unreachable (an architecture
|
||
rule-10 violation: an endpoint without a client trigger on the shipping
|
||
surface).
|
||
|
||
### Mic + '+' buttons (req 5)
|
||
|
||
`SoftButton` ([ContinuousChatOverlay.tsx:309-370](../../../ui/src/components/shell/ContinuousChatOverlay.tsx)):
|
||
borderless 44×44 hit target (WCAG 2.5.5), lucide icons at 26px, hand-drawn
|
||
glyphs at 30px — the icons were already sized up when chrome was removed; the
|
||
26px-vs-30px mix is a minor optical inconsistency (plus glyph renders larger
|
||
than mic/send). Row inset `px-2 py-2 gap-1.5` (`:4233`).
|
||
|
||
Pulse matrix (verified):
|
||
|
||
| Surface | While mic hot | Ref |
|
||
|---|---|---|
|
||
| Grabber bar | ✅ `animate-pulse bg-accent` | `:509-513`, glow at `:3703` |
|
||
| Collapsed pill | ✅ same pulse (`glow={listening \|\| responding}`) | `:582-586`, `:4445` |
|
||
| **Overlay mic button** | ❌ static `text-accent` only | `:349-355`, `:4400-4427` |
|
||
| ChatSurface mic (kiosk path) | ✅ `active && "animate-pulse"` | glass-composer.tsx:108 |
|
||
| HomePill (foundation path) | ✅ `animate-pulse bg-warn/70` on listening | HomePill.tsx:66-68 |
|
||
|
||
So req 5's pill-pulses-when-collapsed is **done**; the mic button itself not
|
||
pulsing on the primary surface is the gap, and it is inconsistent with the
|
||
sibling path.
|
||
|
||
### Jank sources + perf gating
|
||
|
||
- The height animation is `flexBasis` — layout runs every frame by design (the
|
||
pointer-driven MotionValue avoids re-renders but not reflow; `:3985-3995`).
|
||
Rows are `React.memo`'d with a comparator (`chat-message.tsx:379,409`);
|
||
token streaming is context-isolated (`useShellController.ts:317-325`). No
|
||
`content-visibility`/containment anywhere (grep: zero hits) — every open
|
||
sheet re-lays-out all ≤80 rows per animation frame.
|
||
- Hard perf gates exist and run in CI: `run-chat-perf-gate.mjs` (dropped-frame
|
||
ratio, p95 frame time, CLS ≤ 0.1 on the REAL overlay) in
|
||
`chat-shell-gestures.yml:160`; frame-glitch outlier detection
|
||
(`run-chat-sheet-frame-glitch-e2e.mjs`) at `:169-175`.
|
||
- **Known red:** `run-perf-gate-e2e.mjs` has a pre-existing CLS failure — the
|
||
fixture layout-shifts **0.80** during scroll+swipe, reproducing on develop
|
||
([chat-shell-gestures.yml:18-22](../../../../.github/workflows/chat-shell-gestures.yml))
|
||
— yet `test.yml:333` still runs it as a hard-fail leg. Untriaged.
|
||
|
||
### Test infrastructure (req 6) — strong core, stale edges
|
||
|
||
Strong: 11 real-browser gesture runners (`packages/ui/src/components/shell/__e2e__/`),
|
||
a CHAT_GESTURE_COVERAGE gate that fails CI when a gesture-handler site lacks a
|
||
matrix row ([packages/app/test/chat-gesture-coverage.test.ts](../../../app/test/chat-gesture-coverage.test.ts),
|
||
docs in `packages/app/docs/CHAT_GESTURE_COVERAGE.md`), WebKit engine coverage
|
||
for the scroll chain, and a canary mode proving the glitch detectors fire.
|
||
|
||
Stale (post-#13531): `chat-clear-swipe.spec.ts:386,424,497` and
|
||
`chat-send-voice-newchat-fuzz.spec.ts:325,382` still drive the removed
|
||
`chat-full-clear` button; `walkthrough/journey.ts:1108-1121` still expects
|
||
`chat-full-maximize`; `chat-clear-swipe.spec.ts` is still in a CI lane
|
||
(`scenario-pr.yml:341`). The unit suite already pins those testids as ABSENT
|
||
(`ContinuousChatOverlay.test.tsx:2308-2309`) — the repo is testing both sides
|
||
of the same contradiction. Minor slop: literal `" "` filler class strings in
|
||
`cn()` calls (`:497`, `:570`, HomePill.tsx:54).
|
||
|
||
## Design considerations
|
||
|
||
- **Chat is load-bearing for LifeOps.** A parent checking a child's reminder
|
||
history, or an ADHD user scrolling back to "what did I agree to yesterday",
|
||
hits the pagination wall immediately. History reachability (scroll-up +
|
||
search) is MVP-critical, not polish.
|
||
- **Never fight the scroll.** The one thing that most degrades trust in a chat
|
||
surface is an axis surprise. The fix must be at the scroller (one line +
|
||
child audit), not per-widget whack-a-mole.
|
||
- **The gesture system is done — protect it, don't touch it.** The detent
|
||
physics match the brief (lerp on drag, springs on release, half detent,
|
||
pull-to-top maximize). New work must ride the existing perf gates so nothing
|
||
slows the drag path.
|
||
- **Pagination must not regress the drag path.** Prepending hundreds of rows
|
||
into a scroller whose height animates per-frame is the main risk; render
|
||
windowing and scroll-anchor math must be part of the same change.
|
||
- **Doctrine:** no side panels, no suggestion chips, uniform top bar. Search
|
||
therefore lives *inside the sheet* (header affordance at half+), not as a
|
||
sidebar. Orange accent only; pulse animations use the existing
|
||
`animate-pulse` + `motion-reduce:animate-none` pattern.
|
||
|
||
## Open questions → answers
|
||
|
||
**Q1. Pagination: cursor design and interaction with the 80-row render cap?**
|
||
A: Add `?before=<messageId>` to GET messages (pivot's `createdAt` pushed into
|
||
the store as `end` + `orderBy desc` + `limit 100` — identical mechanics to the
|
||
existing around-window, `conversation-routes.ts:1088-1098`). Client keeps a
|
||
`hasOlder` flag (last page full ⇒ true). Overlay: top sentinel
|
||
(IntersectionObserver inside `#continuous-thread`) triggers the fetch; prepend
|
||
with manual scroll-anchor compensation (record `scrollHeight` before, restore
|
||
`scrollTop + delta` after — `overflow-anchor` is not implemented in WebKit).
|
||
The render cap becomes a *sliding window* over loaded messages (grow by page,
|
||
bounded ~400) rather than a hard `slice(-80)`; verify with the existing
|
||
`chat-perf-gate` thresholds. Rationale: reuses every mechanism already in the
|
||
file; no virtualizer dependency.
|
||
|
||
**Q2. Where does search live under the no-side-panels doctrine, and what
|
||
scope?** A: A search icon in the sheet header's intentionally-empty left slot
|
||
(`:3933-3935`), visible at half+ like the launcher button, opening an in-sheet
|
||
search row that reuses `MessageSearchPanel` (it is already
|
||
presentation-only/injectable). Scope: keep the endpoint's cross-conversation
|
||
reach; a hit in the active thread jumps via the existing around-window +
|
||
scroll-into-view; a hit in another conversation switches the active
|
||
conversation through the existing `handleSelectConversation` handoff — search
|
||
becomes the one sanctioned way to reach an older thread now that the switcher
|
||
is gone. Undecidable-without-owner: whether cross-thread jump violates the
|
||
one-infinite-thread doctrine. **Default: ship active-thread jump first; gate
|
||
cross-thread jump behind the same PR only if the owner confirms.**
|
||
|
||
**Q3. Should the maximize button come back?** A: No. #13531's pull-to-maximize
|
||
is implemented, unit-pinned, and e2e-covered. The remaining work is deleting
|
||
the stale specs that still look for the button (Q6/issue 5).
|
||
|
||
**Q4. Which states pulse the mic button?** A: Pulse exactly when the grabber/
|
||
pill glow (`listening || responding` drives those); for the button itself,
|
||
pulse on `recording || handsFree || transcriptionMode` — the same predicate
|
||
that sets `active` today (`:4420`) — so color and motion never disagree.
|
||
Respect `motion-reduce:animate-none` (pattern at `:512`).
|
||
|
||
**Q5. Hide or design horizontal scrolling for wide content?** A: Both, split
|
||
by ownership: the transcript gets `overflow-x-hidden` (parity with
|
||
`chat-thread-layout.tsx:106`); wide content scrolls *inside its own row*
|
||
(`overflow-x-auto overscroll-x-contain`, the `MessageContent.tsx:572` pattern)
|
||
— per repo doctrine "wide content must scroll inside its own container".
|
||
`MessageAttachments.tsx:831` additionally needs `overscroll-x-contain`.
|
||
|
||
**Q6. Bring ChatSurface/kiosk and detached-window paths to parity?** A: Not
|
||
for MVP. They are desktop/kiosk-only surfaces; the MVP surfaces are web +
|
||
mobile where `ContinuousChatOverlay` is the only path. Keep the render-parity
|
||
contract green; take no feature work there.
|
||
|
||
**Q7. Virtualize long threads?** A: No virtualizer for MVP. Measured-first:
|
||
add per-row `content-visibility: auto` + `contain-intrinsic-size` only if the
|
||
`chat-perf-gate` numbers regress after pagination lands (issue 6 pairs the
|
||
CLS triage with this). A virtualizer would fight the `mt-auto` bottom-anchored
|
||
layout and the detent height animation for little MVP gain.
|
||
|
||
## Recommendation (minimal-scope MVP plan, ordered)
|
||
|
||
1. **P0 — Lock the transcript to vertical scrolling** (1-line scroller fix +
|
||
child audit + wheel/deltaX regression e2e). Smallest change, biggest feel
|
||
improvement.
|
||
2. **P0 — Infinite scroll up with `before`-cursor pagination** (server cursor,
|
||
client prepend + anchor, sliding render window, perf-gate verification).
|
||
3. **P1 — Wire message search into the overlay** (header affordance reusing
|
||
`MessageSearchPanel` + around-window jump; active-thread scope first).
|
||
4. **P1 — Mic pulse + composer icon optics** (pulse on the primary surface,
|
||
normalize 26/30px optical sizing, keep 44×44 targets; pill pulse already
|
||
done — add the regression test).
|
||
5. **P1 — Reconcile stale gesture specs with #13531** (delete/rewrite
|
||
`chat-full-clear`/`chat-full-maximize` consumers; delete the dead
|
||
`verticalScrollPriority` option and filler class strings).
|
||
6. **P2 — Triage the perf-gate CLS 0.80 failure** and, only if pagination
|
||
regresses frame budgets, add row containment.
|
||
|
||
Everything else in the brief (detents, springs, pull-to-maximize, pill morph,
|
||
haptics, pill pulse, gesture CI) is already implemented and gated — the job is
|
||
to not break it, which the existing `chat-shell-gestures.yml` lane enforces.
|
||
|
||
## Out of scope (explicit non-goals for MVP)
|
||
|
||
- Feature work on `ChatSurface`/kiosk/detached-window paths (parity contract
|
||
only). No consolidation of the three render paths.
|
||
- Thread virtualization; framer-motion replacement; transform-based sheet
|
||
animation rework.
|
||
- Cross-conversation UI beyond search-jump (no switcher/swipe resurrection).
|
||
- Semantic/vector message search (keyword ILIKE + BM25 is enough).
|
||
- New gesture surfaces (e.g. pull-from-transcript detent control — the grabber
|
||
stays the single drag authority; note: any future scroller-attached pull
|
||
gesture must use touch events + non-passive `preventDefault`, since
|
||
`touch-action: pan-y` compositor scrolling `pointercancel`s pointer-based
|
||
gestures).
|
||
- Suggestion strips (`SHOW_PROMPT_SUGGESTIONS` stays off, `:223`).
|
||
|
||
## Proposed issues
|
||
|
||
1. `[chat-ux]` Lock the chat transcript to vertical-only scrolling (P0)
|
||
2. `[chat-ux]` Infinite scroll up: async history pagination in the continuous chat overlay (P0)
|
||
3. `[chat-ux]` Wire message search into the primary chat surface (P1)
|
||
4. `[chat-ux]` Mic button pulses while recording; normalize composer icon optics (P1)
|
||
5. `[chat-ux]` Remove stale gesture specs and dead gesture options left behind by the one-infinite-thread change (P1)
|
||
6. `[chat-ux]` Triage the perf-gate CLS failure and guard long-thread scroll performance (P2)
|