Skip to content

feat(mobile): persistent Book Chat parity — history, markdown, quote-and-ask, spoiler toggle#443

Open
mrviduus wants to merge 3 commits into
mainfrom
feat/mobile-book-chat
Open

feat(mobile): persistent Book Chat parity — history, markdown, quote-and-ask, spoiler toggle#443
mrviduus wants to merge 3 commits into
mainfrom
feat/mobile-book-chat

Conversation

@mrviduus

Copy link
Copy Markdown
Owner

Closes the last big mobile tail: the Ask sheet moves onto the persistent /me/chat endpoints — the same thread as web, surviving app restarts, inheriting gpt-4.1 tutor answers, rolling-summary memory, highlights-as-context, and chapter-summary retrieval for free.

Key engineering

  • First real SSE on mobile: postSse over expo/fetch's ReadableStream (RN fetch buffers), byte-faithful port of the web parser (same wire as legacy ask), one-shot 401 refresh-retry via the existing single-flight (duplicate-safe — auth precedes any persist), JSON fallback on any can't-stream runtime shape.
  • Custom ~180-line RN markdown renderer (headings/bold/lists/code/fenced/blockquote; tables → scrollable monospace) — react-native-markdown-display is unmaintained vs RN 0.83/new-arch; no raw-HTML path.
  • Quote-and-ask on the selection bar ("Ask about this" → detachable quote card); chatQuote codec moved to packages/shared (web re-exports; web tests pass through it).
  • Spoiler-safe switch + Clear chat (optimistic + revert); skeleton history load; starters only on empty thread; stream aborts on close. Citation chips/jump unchanged; legacy /ask untouched.

Adversarial QA (core verified sound; fixed)

Narrow stream-fallback → all shapes; per-token re-render/scroll jank (memo + animate-only-on-new-turn); stale quote/draft after close; ghost blank-answer turns on hard failures; starters dropping an attached quote; table-detector false positive (+6 pure tests).

Accepted: inline [n] plain text on mobile (chips = jump surface); no-done stream settles as truncated-but-finished (web parity).

Verify

mobile tsc + 70 vitest · shared 209 (8 new codec) · web tsc + 730 vitest (re-export exercised). JS-only → ships via OTA. On-device musts listed in CHANGELOG (multi-byte chunk splits, abort-on-close, long-answer jank, streams-less degrade).

🤖 Generated with Claude Code

mrviduus and others added 3 commits July 17, 2026 13:00
…r toggle, quote-and-ask

- AskSheet rewritten onto /me/chat: server-owned history loads on open (skeleton),
  optimistic send + SSE stream (expo/fetch ReadableStream — RN fetch buffers; wire
  identical to legacy ask), JSON fallback, one-shot 401 refresh-retry
- custom ~180-line RN markdown renderer (headings/bold/lists/code/fenced/blockquote;
  tables degrade to scrollable monospace) — no unmaintained dep, no Data-Safety delta
- Spoiler-safe switch (subtle on user books) + Clear chat w/ confirm, optimistic+revert
- quote-and-ask: selection bar 'Ask about this' -> detachable quote card -> shared codec
- chatQuote codec moved to packages/shared (web re-exports; imports unchanged)
- stream aborts when the sheet closes; citation chips + jump preserved
- legacy /ask endpoints untouched (evals/fallback)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eanup

- SseUnsupportedError on ANY can't-stream shape (missing getReader/TextDecoder) -> JSON fallback
- memo(AskMarkdown) + scroll animates only on new turns (no per-token re-animation)
- close clears attached quote + typed draft (sheet stays mounted)
- hard send failure reverts the optimistic turn (no ghost blank-answer question)
- starters hidden while a quote is attached (can't silently drop it)
- table separator requires '|' (pure isTableSeparator helper + 6 tests); stale comment fixed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant