You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Parent epic:#1446 (Amicus — AI Study Partner v1) Phase: 3 · Size: S · Depends on:#1463 (peek conversation — closed), #1457 (persistence — closed), #1454 (Amicus tab — closed)
Promotes an ephemeral peek conversation into a persistent thread in the Amicus tab. The handoff preserves all messages and navigates the user into the full conversation view.
The underlying crash mechanism was subsequently fixed by app/patches/react-native+0.81.5.patch — which patches exactly that file/line to re-throw void-method exceptions cleanly instead of corrupting jsi::Runtime. The specific symptom from (12) cannot reoccur with that patch in place.
app/src/components/amicus/AmicusFab.tsx — forwards onContinueInTab to PeekSheet
app/App.tsx — provides the onContinueInTab callback via navigationRef
Architecture — callback-based navigation (changed from original #1500)
Do not use getParent().navigate() in the service layer. The original PR #1500 did this:
// ❌ original pattern — leaf component coupled to tab name + tree shapenavigation.getParent()?.navigate('AmicusTab',{screen: 'Thread',params: { threadId }});
That's the exact anti-pattern #1562 / #1563 were trying to address. This re-land fixes it up front: the service returns the new threadId, and the parent chain (PeekSheet → FAB → AppShell) decides how to navigate.
// ✅ new pattern — service is navigation-agnosticexportasyncfunctionpromotePeekToThread(params: {peekMessages: PeekMessage[];chapterRef?: ChapterRef|null;}): Promise<string>;// returns threadId// AmicusPeekSheet receives a callback prop:interfaceAmicusPeekSheetProps{onContinueInTab?: (threadId: string)=>void;// ... existing props}// App.tsx provides the concrete navigation:<AmicusFabonContinueInTab={(threadId)=>navigationRef.navigate('AmicusTab',{screen: 'Thread',params: { threadId }})}/>
This means:
promotePeekToThread has zero React Navigation imports
Unit tests don't mock useNavigation or getParent
If navigators are ever restructured, one call site in App.tsx changes — not a service file
exportasyncfunctionpromotePeekToThread(params: {peekMessages: PeekMessage[];// from #1463 snapshotForPromotion()chapterRef?: ChapterRef|null;// optional — becomes thread's chapter_ref}): Promise<string>;// returns new threadId
Flow:
Generate thread title from first user message (truncated to ~50 chars at word boundary; add … if longer; fallback "New Amicus conversation" if < 10 chars)
Tap creates thread with generated title, writes all messages
Navigation lands on AmicusTab/Thread with the new threadId scrolled to bottom
Peek sheet closes after navigation
Underlying screen state preserved (scroll, focus)
Double-tap prevented; CTA shows "Saving…" during save
DB failure → toast + peek stays open; messages preserved
Unit tests cover: title generation (5 cases), happy path, DB failure path, double-tap
Service has zero @react-navigation/native imports
No any types; lint clean
Pre-merge validation (new — added from #1504's re-land plan)
This card was previously burned by a TestFlight crash that unit tests did not catch. Before merging:
Rentamac local build with this branch applied → install on iOS sim → walk the peek → tap CTA → confirm it actually navigates into the Amicus tab thread view without crash
Release-mode build (eas build --profile production on rentamac OR Windows) → on-device or sim → same flow. Release-mode bundling + Hermes + New Arch often behave differently from Jest.
Post-DB-download hot path — reproduce the first-launch scenario that crashed build (12): fresh install → DB downloads → immediately open peek and tap CTA. The RN patch at app/patches/react-native+0.81.5.patch should now prevent the rethrow abort, but verify directly.
Refactoring AmicusFab's paywall routing — separate concern, do in this PR only if it falls out of the navigationRef-in-App.tsx pattern naturally; otherwise leave for a followup
Parent epic: #1446 (Amicus — AI Study Partner v1)
Phase: 3 · Size: S · Depends on: #1463 (peek conversation — closed), #1457 (persistence — closed), #1454 (Amicus tab — closed)
Promotes an ephemeral peek conversation into a persistent thread in the Amicus tab. The handoff preserves all messages and navigates the user into the full conversation view.
Status
ObjCTurboModule::performVoidMethodInvocationatRCTTurboModule.mm:441→__cxa_rethrow→ abort.app/patches/react-native+0.81.5.patch— which patches exactly that file/line to re-throw void-method exceptions cleanly instead of corruptingjsi::Runtime. The specific symptom from (12) cannot reoccur with that patch in place.Files to create
app/src/services/amicus/promotePeekToThread.ts— the handoff functionapp/src/services/amicus/__tests__/promotePeekToThread.test.tsFiles to modify
app/src/components/amicus/PeekMiniConversation.tsx— CTA button calls the handoff serviceapp/src/components/amicus/AmicusPeekSheet.tsx— acceptsonContinueInTabcallback prop, closes peek after handoff completesapp/src/components/amicus/AmicusFab.tsx— forwardsonContinueInTabto PeekSheetapp/App.tsx— provides theonContinueInTabcallback vianavigationRefArchitecture — callback-based navigation (changed from original #1500)
Do not use
getParent().navigate()in the service layer. The original PR #1500 did this:That's the exact anti-pattern #1562 / #1563 were trying to address. This re-land fixes it up front: the service returns the new
threadId, and the parent chain (PeekSheet → FAB → AppShell) decides how to navigate.This means:
promotePeekToThreadhas zero React Navigation importsuseNavigationorgetParentApp.tsxchanges — not a service fileHandoff function
Flow:
…if longer; fallback"New Amicus conversation"if < 10 chars)createAmicusThread(ai-partner: conversation persistence (user.db) #1457 mutation) withthreadId,title,chapterRefpeekMessages, callappendAmicusMessagewith role, content, citations, follow_upsthreadIdNavigation + peek close are the caller's responsibility (AppShell provides navigation, PeekSheet closes itself on success).
Idempotency + UX
"Couldn't save conversation — try again", peek stays open, messages preserved in hook statenavigationRef.navigate(...), then PeekSheet animates closed. Scroll position on underlying screen preserved.Acceptance criteria
AmicusTab/Threadwith the new threadId scrolled to bottom"Saving…"during save@react-navigation/nativeimportsanytypes; lint cleanPre-merge validation (new — added from #1504's re-land plan)
This card was previously burned by a TestFlight crash that unit tests did not catch. Before merging:
eas build --profile productionon rentamac OR Windows) → on-device or sim → same flow. Release-mode bundling + Hermes + New Arch often behave differently from Jest.app/patches/react-native+0.81.5.patchshould now prevent the rethrow abort, but verify directly.Out of scope
useAmicusThreadshook reactivitynavigationRef-in-App.tsx pattern naturally; otherwise leave for a followupReferences
d1ce3030). Reviewing that code is fine but do not cherry-pick — rewrite the navigation layer per the pattern above.app/patches/react-native+0.81.5.patch