@@ -114,15 +114,19 @@ export interface InterlinearNav {
114114 rawScrRef : SerializedVerseRef ;
115115 /**
116116 * The active reference, equal to `rawScrRef` except that a verse-0 reference naming the chapter
117- * already shown is held sticky on the current verse. A verse-0 reference is otherwise passed
118- * through verbatim: when it names a chapter with a verse-0 superscription segment, that segment
119- * becomes the active verse. Whether a given chapter actually has verse-0 content is unknown here
120- * (the book is not loaded at this layer), so the loader resolves a verse-0 reference with no
121- * matching segment back to the chapter's first numbered verse before rendering.
117+ * already shown is held sticky on the current verse — unless it matches a fresh internal-nav
118+ * marker (the extension's own move to that chapter's superscription), which passes through. A
119+ * verse-0 reference is otherwise passed through verbatim: when it names a chapter with a verse-0
120+ * superscription segment, that segment becomes the active verse. Whether a given chapter actually
121+ * has verse-0 content is unknown here (the book is not loaded at this layer), so the loader
122+ * resolves a verse-0 reference with no matching segment back to the chapter's first numbered
123+ * verse before rendering.
122124 *
123125 * The sticky exception exists because, after a verse navigation, the host re-broadcasts the
124126 * chapter as a separate `verseNum: 0` reference (an echo of the current location, not a real
125- * move); treating that as a jump to verse 0 would yank the view off the verse the user is on.
127+ * move); treating that as a jump to verse 0 would yank the view off the verse the user is on. The
128+ * marker carve-out distinguishes that spurious echo from a deliberate verse-0 navigation the
129+ * extension itself just made (which is shaped identically).
126130 */
127131 liveScrRef : SerializedVerseRef ;
128132 /**
@@ -227,20 +231,42 @@ export function InterlinearNavProvider({
227231 */
228232 const liveScrRefRef = useRef < SerializedVerseRef > ( rawScrRef ) ;
229233
234+ /**
235+ * Verse keys of internal navigations still awaiting their host round-trip, each mapped to its
236+ * `Date.now()` stamp. `navigate(ref, 'internal')` records `verseKey(ref)`; `consumeInternalNav`
237+ * removes it on match. Keyed (not a single value) so that rapid successive clicks both stay
238+ * pending and neither host delivery is misread as external. The stamp gives each marker a TTL
239+ * ({@link INTERNAL_NAV_TTL_MS} — see its doc for why stranded markers must expire), honored by ALL
240+ * readers: `consumeInternalNav` (which also evicts expired markers), the verse-0 stickiness
241+ * exception below, and the render-phase mid-reveal guard (both pure reads — no eviction during
242+ * render).
243+ */
244+ const pendingInternalNavRef = useRef < Map < string , number > > ( new Map ( ) ) ;
245+
230246 // After a verse navigation the host re-broadcasts the *chapter* to the scroll group as a separate
231247 // `verseNum: 0` reference (an echo of the current location, not a real move). Treating that as a
232248 // jump to verse 0 would yank the views off the verse the user is actually on, so a verse-0
233249 // reference that names the book+chapter already shown is held sticky: keep the current `liveScrRef`
234- // (its real verse). Every other reference — including a verse-0 reference for a *different* chapter
235- // (a genuine chapter jump, which the loader resolves to the verse-0 superscription when one exists,
236- // else to verse 1) — passes through verbatim. The duplicate-delivery guard reuses the previously
237- // committed object when a re-send is value-equal, so a duplicate never reads as a fresh navigation.
250+ // (its real verse).
251+ //
252+ // The exception is a verse-0 reference the extension itself just navigated to — selecting a
253+ // chapter's verse-0 superscription segment writes `verseNum: 0` for the chapter already shown,
254+ // which is shaped exactly like the spurious echo. A fresh internal-nav marker for that verse-0 key
255+ // distinguishes the two: when one exists, this is our own deliberate move to the superscription, so
256+ // it passes through (and `consumeInternalNav` clears the marker downstream). A pure read here — no
257+ // eviction during render, matching the render-phase mid-reveal guard.
258+ //
259+ // Every other reference — including a verse-0 reference for a *different* chapter (a genuine chapter
260+ // jump, which the loader resolves to the verse-0 superscription when one exists, else to verse 1) —
261+ // passes through verbatim. The duplicate-delivery guard reuses the previously committed object when
262+ // a re-send is value-equal, so a duplicate never reads as a fresh navigation.
238263 const liveScrRef = useMemo ( ( ) => {
239264 const prev = liveScrRefRef . current ;
240265 if (
241266 rawScrRef . verseNum === 0 &&
242267 rawScrRef . book === prev . book &&
243- rawScrRef . chapterNum === prev . chapterNum
268+ rawScrRef . chapterNum === prev . chapterNum &&
269+ ! isInternalNavMarkerFresh ( pendingInternalNavRef . current . get ( verseKey ( rawScrRef ) ) )
244270 ) {
245271 return prev ;
246272 }
@@ -254,17 +280,6 @@ export function InterlinearNavProvider({
254280 const prevLiveScrRef = liveScrRefRef . current ;
255281 liveScrRefRef . current = liveScrRef ;
256282
257- /**
258- * Verse keys of internal navigations still awaiting their host round-trip, each mapped to its
259- * `Date.now()` stamp. `navigate(ref, 'internal')` records `verseKey(ref)`; `consumeInternalNav`
260- * removes it on match. Keyed (not a single value) so that rapid successive clicks both stay
261- * pending and neither host delivery is misread as external. The stamp gives each marker a TTL
262- * ({@link INTERNAL_NAV_TTL_MS} — see its doc for why stranded markers must expire), honored by
263- * BOTH readers: `consumeInternalNav` (which also evicts expired markers) and the render-phase
264- * mid-reveal guard (a pure read — no eviction during render).
265- */
266- const pendingInternalNavRef = useRef < Map < string , number > > ( new Map ( ) ) ;
267-
268283 const navigate = useCallback (
269284 ( newScrRef : SerializedVerseRef , origin : NavOrigin = 'external' ) => {
270285 if ( origin === 'internal' ) pendingInternalNavRef . current . set ( verseKey ( newScrRef ) , Date . now ( ) ) ;
0 commit comments