From d14036d1b5985e719197da5d342c5f4f9ee39cf2 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 19 Sep 2026 09:08:01 -0400 Subject: [PATCH] Keep the view across a tab switch Perspective's auto-pause is on by default, and pausing deletes the view: AutoPauseState::apply() fires on the viewer's own IntersectionObserver and on the document's visibilitychange, and set_pause(true) does view_sub.take().delete(). Coming back is therefore not a redraw but restore_and_render() -- a fresh view and a fresh traversal of the whole grain -- which is the multi-second chug on every tab switch, and takes any per-node expansion with it. Nothing updates the table while the tab is hidden; every operation is driven from this page. So the pause bought nothing and cost a rebuild. Also drops a comment that still described the old set_depth re-apply on refocus, whose machinery is long gone. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 15 +++++++++++++++ ui/src/views/Forecast.jsx | 22 ++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4761478..535f36c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -272,6 +272,21 @@ gone: `split_full` is still *read* on load, for layouts saved under the old scheme. +### Auto-pause is off + +`` auto-pauses by default: an `IntersectionObserver` on +itself plus the document's `visibilitychange` drive `AutoPauseState::apply()`, +and pausing **deletes the view** (`session.set_pause(true)` → +`view_sub.take().delete()`). Returning to the tab is therefore not a redraw but +`restore_and_render()` — a fresh view and a fresh traversal of the whole grain, +which on a large one is a multi-second chug on every tab switch, and takes any +per-node expansion with it. + +`initViewer()` calls `viewer.setAutoPause(false)` right after `viewer.load()`. +Nothing updates the table while the tab is hidden — every operation is driven +from this page — so the pause bought nothing and cost a rebuild. The view is now +held while the tab is backgrounded. + ### Still not solved: per-node expansion Expanding one specific branch is view state with no config representation, and diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 24847a7..c7434bb 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -259,12 +259,6 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // the current selection, readable from event handlers that were registered once const slicesRef = useRef([]) - // Perspective's ROLLUP view contains every level of the hierarchy; view.set_depth() - // is the only thing hiding the deeper ones, and it lives on the view rather than in - // the saved config. The viewer rebuilds its view when it redraws — which its - // Intersection/ResizeObserver triggers when you switch back to the tab — and the - // fresh view has no depth set, so the whole tree appears expanded. Re-apply the - // depth we last set once the redraw has settled. // perspective-click fires as a CustomEvent with no modifier state of its own, // and carries no cell coordinates either, so record both from the mousedown that // precedes it. The grid lives in a shadow root, so the real target is the head of @@ -822,6 +816,22 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // that occurs when viewer.load(worker) + restore({ table: name }) can't resolve // the named table in time. await viewer.load(tableRef.current) + + // Auto-pause is on by default, and pausing *deletes the view*: + // AutoPauseState::apply() fires on visibilitychange and on the viewer's own + // IntersectionObserver, and set_pause(true) does + // `view_sub.take().delete()`. Coming back is therefore not a redraw but + // `restore_and_render()` -- a fresh view, a fresh traversal of the whole + // grain -- which is the multi-second chug on every tab switch, and why any + // per-node expansion is gone when you return. + // + // Nothing updates the table while the tab is hidden (operations are driven + // from this page), so the pause buys nothing here and costs a rebuild. + // Guarded: it is a method on the custom element, absent on older builds. + try { if (viewer.setAutoPause) await viewer.setAutoPause(false) } catch (err) { + console.error('[setAutoPause]', err) + } + viewer.setAttribute('theme', dark ? 'Pro Dark' : 'Pro Light') if (!hideSplitTotal()) setTimeout(hideSplitTotal, 400)