From 77fcb32e7f2d43d97b2a46bdea1fad1fc237b191 Mon Sep 17 00:00:00 2001 From: Pier-Jean Malandrino Date: Mon, 27 Apr 2026 11:48:32 +0200 Subject: [PATCH] fix(reasoning): re-scroll PDF when re-clicking the active iteration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The watch-based plumbing from iteration click to PDF scroll relied on a "flip via null" pattern (assign null then the value) to coerce Vue into re-firing the watcher. Vue 3 collapses synchronous mutations of the same ref and only delivers the final value, so the trick was a no-op: a second click on the same iteration left the document view stuck on the previous page. The bug only showed when the trace had a single iteration — with several, the user naturally clicks different ones and the value really changes. Replace the watch chain with imperative dispatch. ReasoningPanel now just emits iterationFocus; ReasoningWorkspace handles it by calling the graph focus and the new StructureViewer.scrollToFocused method directly. Both side effects fire on every click regardless of state. --- .../features/analysis/ui/StructureViewer.vue | 46 ++++++++++--------- frontend/src/features/reasoning/store.ts | 9 ++-- .../features/reasoning/ui/DocumentView.vue | 14 +++++- .../features/reasoning/ui/ReasoningPanel.vue | 17 +++---- .../reasoning/ui/ReasoningWorkspace.vue | 38 +++++++-------- 5 files changed, 67 insertions(+), 57 deletions(-) diff --git a/frontend/src/features/analysis/ui/StructureViewer.vue b/frontend/src/features/analysis/ui/StructureViewer.vue index 018841c..43efb3d 100644 --- a/frontend/src/features/analysis/ui/StructureViewer.vue +++ b/frontend/src/features/analysis/ui/StructureViewer.vue @@ -284,29 +284,33 @@ watch( // When the caller sets a focused self_ref (e.g. the user clicked a node in // the graph), find which page that element lives on and jump to it. The // overlay redraw will then show the dashed focus ring around its bbox. -watch( - () => props.focusedSelfRef, - (ref) => { - if (!ref) { - nextTick(drawOverlay) +function scrollToFocused(ref: string | null): void { + if (!ref) { + nextTick(drawOverlay) + return + } + for (const page of props.pages) { + if (page.elements.some((e) => e.self_ref === ref)) { + if (selectedPage.value !== page.page_number) { + selectedPage.value = page.page_number + // Let reload before drawing — drawOverlay runs on @load. + } else { + nextTick(drawOverlay) + } return } - for (const page of props.pages) { - if (page.elements.some((e) => e.self_ref === ref)) { - if (selectedPage.value !== page.page_number) { - selectedPage.value = page.page_number - // Let reload before drawing — drawOverlay runs on @load. - } else { - nextTick(drawOverlay) - } - return - } - } - // Ref not on any page (e.g. a #/body node) — just redraw to clear the - // previous focus ring. - nextTick(drawOverlay) - }, -) + } + // Ref not on any page (e.g. a #/body node) — just redraw to clear the + // previous focus ring. + nextTick(drawOverlay) +} + +watch(() => props.focusedSelfRef, scrollToFocused) + +// Imperative entry point so callers can re-trigger a scroll on the same +// self_ref (the watch above only fires on value change). Used by the +// reasoning workspace when the user re-clicks the active iteration card. +defineExpose({ scrollToFocused })