Repository navigation
Release: FusionLab task-mode IA redesign (Direction C) - #50
Merged
obvious-autobuild[bot] merged 11 commits intoOct 12, 2026
Merged
Conversation
* feat(router): three workspaces behind one hash grammar
One route() owns which workspace is visible and the URL grammar —
#/replay/{shot}?t= · #/compare/{ids}?ref= · #/explore — with hashchange
and popstate handling, a persisted landing (last visited, else the
welcome-modal pick, else Replay), legacy '#sandbox' and ?shot=/?compare=
deep links kept working, and data-guide anchors plus the window.FusionLab
/ fusionlab:* contracts untouched (showTab is now a hash-writing alias).
The compare chip machine's public hook (FusionLab.replay.compare) lets
the router mount #/compare/{ids}; catalog shot clicks route through the
grammar; the legacy srchSetUrl/cmpUrl URL writers preserve the hash they
used to strip; and a welcome-modal pick no longer stomps a deep-linked
route (it only navigates off the bare default landing).
Mirrors the section-id renames in the UI-contract tests and serves the
new module from the static-assets test.
Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
* style(header): task-mode tabs with ARIA, focus ring, mobile legend collapse
Header tabs become Replay / Compare / Explore with role=tablist/tab,
aria-selected, aria-controls and arrow-key roving focus; the sections
become tab-replay / tab-compare / tab-explore. A --focus-ring token adds
:focus-visible styling app-wide, the disabled ⌘K button stakes the
palette's header slot, and the provenance legend (its tags repeat on
every panel) collapses below 760px so the title stops clipping on
mobile — the audit defect captured in fl_AmO2E2fS. The welcome modal
gains an "Explore the model" pick that persists its landing.
Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
* fix(welcome): keep the welcome pick working across reloads of a bare landing
Reloading a bare landing stranded the welcome-modal pick on Replay: the
boot's own canonicalizing replaceState wrote '#/replay' into the URL, so
on the next boot route() read that app-written hash as an explicit route
and defaultLanding stayed false — the pick stored its landing but never
navigated. Root cause, not symptom: a hash the app itself wrote must not
look like one the user asked for. Leave the bare landing bare; the hash
carries state only once the user navigates (deep links, #sandbox and the
legacy ?shot=/?compare= entry points are unchanged).
Verified in-browser: bare boot -> pick 'Explore the model' -> '#/explore'
applied; reload -> '#/explore' restored, modal suppressed; reload BEFORE
picking -> pick still navigates. pytest 352 passed + 1 skipped, ruff and
mypy clean.
Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
---------
Co-authored-by: Obvious <[email protected]>
Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
The router PR (#58) already re-homed the what-if sandbox into #tab-explore with its data-guide anchors intact; this completes the Explore workspace task (spec What-we-build section 2, L1): - Explore gains the visible path into the anchored editor: an "Edit a real shot instead" link that deep-links to #/replay/30166?edit=1 (a real anchor, so Back/Forward and Share work through the router's grammar, not around it). - The router honors edit=1 on #/replay/{shot}: until the drawer change owns ?edit=1 (L2), it opens today's anchored what-if on the replayed shot via the existing F.replay.whatIf hook - no new app path. - Label-driven lesson/glossary copy: user-facing prose that named the old "sandbox" surface now says Explore; the lesson routing fields ("tab": "sandbox") and every data-guide anchor name are untouched (L3 contract). Co-authored-by: Obvious <[email protected]> Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
7 tasks done
…ip (#63) * feat(shotload): shared shot-load state machine with a visible load chip Generalize the compare chip fetch machine into web/shotload.js as THE one shot-load lifecycle (spec L4): queued -> fetching -> ready | error per shot, AbortController supersede by generation, honest per-shot errors. A superseded fetch lands back in 'queued' — an abort is not a failure. Single-shot loading now runs through the machine and renders its states as a load chip in Replay's status cluster ("loading shot #30192..."; on error the reason plus Retry/Drop), closing the silent cold-load gap the audit found. getReplay stores each cached promise with the signal it was created under so a new-signal fetch never adopts an in-flight promise an older signal is about to abort, and a failed fetch only evicts its own cache entry. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> * refactor(compare): read the compare chips from the shared shot-load machine The compare fan-out's hand-rolled state Maps (cmp.st/cmp.err/cmp.ctrls) are gone: per-shot fetch state now lives in web/shotload.js, the same machine a single pick uses. cmpFetch becomes a thin payload router — supersede and honest errors are the machine's to record — and Cancel returns in-flight chips to 'queued' instead of marking them 'failed: fetch cancelled', matching the spec's state table (an abort is a supersede, not a failure). Selection changes drop the previous batch's machine state, so re-entering compare refetches exactly as before; payload ownership (cmp.data, replayCache) is unchanged. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> --------- Co-authored-by: Obvious <[email protected]> Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
…ons, glossary (#65) Native <dialog> palette (web/palette.js): case-insensitive substring search over the three workspaces, the shot catalog (/shots, fetched once and cached), the 43 lesson titles and 47 glossary terms, plus actions — share this view, OpenUSD export, start/resume the guided study, open the edit drawer, open the catalog. Keyboard-first (arrows/Enter/Esc), focus trapped by native showModal and restored to the opener on close; runs every action through existing public surfaces (router hash grammar, window.FusionLab hooks, replay/app handlers) — no new app paths, no dependencies, ES5-compatible IIFE. Companion edits: header ⌘K stub enabled with aria-haspopup/aria-expanded; palette styles from the existing token set (no new palette); a small additive window.FusionLab.guided.start(lessonId) hook in guide.js so a lesson item opens that exact lesson through the same jump the per-panel "?" buttons use. Co-authored-by: Obvious <[email protected]> Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
…r Replay (#66) * feat(edit drawer): anchored what-if becomes a modal drawer over Replay Replace the cell swap (setWhatIf hiding the state cell's children) with a right-side <dialog> drawer (~420px, role=dialog, aria-modal): the shot state, limit gauges and logbook underneath stay live and follow every edit, because scrub() repaints them from the what-if result without any hiding. - Entry: the toolbar button is now "Edit this shot" (aria-haspopup=dialog, aria-expanded mirrors state); Esc and the drawer's close button both close natively, and focus returns to the opening control (wi.opener). - Sliders start from the measured programme (data-zero), persist across close/reopen (persistent DOM + memoized gate), and "Reset edits" restores. - Gate failure opens the drawer just enough to carry a one-line error with a working Retry button, instead of an unopenable hidden panel. - Drawer styling composes from the token system: --drawer token, anchored to #tab-replay, single-column .wi-grid inside. - ?edit=1 now owns the drawer (workspaces.js comment); lesson 8.1 copy names the new label. data-guide anchors and the window.FusionLab / fusionlab:* contracts unchanged. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> * style(toolbar clusters): three labelled groups — transport, shot actions, status The toolbar row mixed transport, document actions and infrastructure status in one flat line (audit H1). It now reads in three labelled clusters: - transport: shot select, play, speed, time scrubber - shot actions: Edit this shot, OpenUSD, Share (copy this view's URL), Field lines (toggle — off stops line fetches and clears them from the 3D panel, q95 chips go quiet), Compare… (navigates to the Compare workspace) - status: load chip, transient flash chip (role=status), compute chip Share reproduces the current view (explicit hash routes pass through; the bare landing builds #/replay/{id}?t=SECONDS). The 3D panel gains a visible note that magnetic-surface reconstruction is lossy — field lines are indicative. Also restores the drawer CSS block lost from the previous commit's style.css (the --drawer token had survived, the rules had not). Clusters wrap as units in the ≤1100px container query. data-guide anchors unchanged. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> * fix(usd error surfacing): failed OpenUSD exports reach the banner and status chip, not console only A failed export previously did console.error and nothing else (audit state-handling gap): the button's aria-busy was the sole signal, and only while the request was in flight. Now renderFallback is the single painter for the notice row — a compute fallback outranks a failed export, which outranks the onboarding hint; one notice per grid row, with the reason escaped. The banner takes an error severity (red edge) when it carries the export failure; the status chip flashes wait → ok/bad through the new role=status flash element. Dismiss is per-visit; a clean /compute or a successful export re-arms the row. Recovery paths unchanged. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> * fix(units): stored energy in MJ on every Replay surface The W strip, its live value, the shot-state readout, the compare traces and the ΔW difference strip all multiplied the MJ data by 1e3 and labelled it kJ, while the catalog table and the search filters already used MJ — adjacent surfaces flipped the scale of the same quantity (audit consistency finding). One unit now: the arrays render directly, the ΔW difference drops the ×1e3, and the guided-study lessons read 0.01 MJ / 0.13 MJ. The sliceState contract field W_kJ is unchanged (guided-study surface, L3). Also: the SHOT ACTIONS cluster could overflow its box and paint over the status cluster when the toolbar ran tight — the bar and cluster bodies now wrap as whole units at any width, and the time label stays on one line. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> * fix(drawer): anchor the edit drawer to the right edge at full height The UA stylesheet gives modal dialogs inset:0 and height:fit-content; with a fixed width, LTR resolution takes the UA's left:0 over the rule's right:0 and the fixed height defeats the top/bottom stretch — the drawer rendered as a shrink-wrapped panel at the room's top-left instead of a 420px right sheet. left:auto and height:auto restore the intended anchor (verified in-browser: x=1005, w=420, h=900 at 1440x900). Co-authored-by: Tapan Ganatma Nakkina <[email protected]> --------- Co-authored-by: Obvious <[email protected]> Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
* feat(compare): the comparison reads in the Compare workspace Fold the comparison read into #tab-compare: the compare bar, the chips and a new "Side by side" panel (W, worst limit, dW difference, compared Thomson profiles) render in the Compare workspace, above the catalog search and clouds; the Replay room keeps its four single-shot strips and loses the in-grid compare chrome (bar row, dW strip, cmp-on grid). Replay's "Compare..." action navigates to #/compare with the shot on screen preloaded as the reference chip; one-id compare routes mount the reference and prompt for more; legacy ?compare= links canonicalize into the hash grammar and land here. The bar gains Share (copies the URL - the route, filters and selection) and "Retry failed" now refetches every failed chip; re-pointing the reference to an already-ready shot repaints the bar so a stale failure header cannot survive the move. Single-shot picking no longer re-points a live compare - the two workspaces are independent. The /db failure catch now blanks only the catalog-clouds panel so the bar, search and side-by-side read survive a /db outage (JS-07 assertion updated to the targeted handler). NO SEMANTIC CHANGES to the compare flow: per-shot chip states, abort- on-supersede, reference semantics, 2-6 shot limit and the shared time cursor are unchanged; the cursor now spans both workspaces' time panels. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> * refactor(catalog): remove the last "database" label from lesson copy The guided-study prose still said "the machines in that database" for the scaling-law fits' source catalog; the surface itself is already the catalog. Lesson tab values and data-guide anchors (db, db-tau, db-ops, holdout) are untouched - the router normalizes them and the anchors are the guided-study compatibility contract. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> * fix(compare): a one-shot compare shows the reference's curves startCompare's sub-two-shot early return painted the bar but never drew the read panel, so a "Compare..." preload or a one-id deep link showed three empty labelled boxes. Below two shots the read now shows the reference's own measured curves (W, worst limit, Thomson at the cursor); the dW difference strip stays hidden until a second shot makes a difference to read. Verified in the browser against this head. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> --------- Co-authored-by: Obvious <[email protected]> Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
* chore(copy): finish the sandbox → Explore label sweep The sibling PRs renamed the surfaces in the lessons themselves (0.2, 0.3, 4.8, 7.1, 8.1) but left three "sandbox" stragglers in web/: two comments (figures.js preset note, guide.js lab-help note) and one user-facing string (the agent's protocol-accept confirmation). Part of the V9 final sweep; no behavior change. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> * feat(a11y): one focus token, reduced-motion collapses panel motion - style.css: repair the tail left by a sibling merge (a stray "auto; }" fragment and duplicated .srch-rows/.bad/Explore/mobile-header rules); drop the one hardcoded :focus-visible override so every interactive element shares the --focus-ring token; add @media (prefers-reduced-motion: reduce) collapsing the two orientation tweens (gauge bars, spotlight dim). - guide.js, replay.js: the two smooth scrolls (spotlight, catalog panel jump) jump instantly under the same query — the preference otherwise could not reach them from CSS. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> * fix(redundancy): the insight paragraph references the limits, not their numbers Limit proximity was rendered four times on one screen: the three gauges, the limits strip, the strip value's color, and the insight prose — the prose being the third restatement of the same numbers. Keep the gauges (the instant) and the limits strip (the run); the prose now names which limit is closest and points at those two. Lesson 5.5, which described the panel's prose, drops "and when" to match. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> --------- Co-authored-by: Obvious <[email protected]> Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
…redesign-20261011-170103 # Conflicts: # web/replay.js
The merge brought main's CSV export with a console-only failure path — the exact pattern the release fixed for OpenUSD. dlExport now takes an onErr callback; the Replay caller renders the fallback banner + status flash, the Compare caller uses the bar's button-text idiom. usdErr/ usdSeen renamed to exportErr/exportSeen since the banner now carries both export kinds. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> Human author: Tapan Ganatma Nakkina ([email protected])
…dths At 375px the compare bar (.cr-hint) and its action buttons could not wrap, pushing the page 200px past the viewport (the catalog table scrolls in .srch-rows, so it was never the offender). flex-wrap on the bar and on .cmp-actions keeps every compare control inside the container-query column — the V10 acceptance criterion. Co-authored-by: Tapan Ganatma Nakkina <[email protected]> Human author: Tapan Ganatma Nakkina ([email protected])
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release: FusionLab task-mode IA redesign (Direction C)
Release intent: reorganize the FusionLab web UI around the three jobs it serves — Replay (watch a real shot), Compare (shots side by side), Explore (the model) — per the approved spec (Obvious Blueprint
art_ZVpZN0l7), folding in the audit's hygiene fixes: one name per concept ("catalog", not "database"), MJ units everywhere, honest loading/error states, an edit drawer that keeps shot context visible, a Ctrl/Cmd+K command palette, and a hash-backed router (Back/Forward + shareable deep links). No backend changes — all work lands inweb/andlessons.json.Verification: complete — V1–V10 from the spec, executed on the integrated branch at the release tip
73b4048(see the sweep table below). Capability parity (V8): PASS, 72 controls, all reachable at depth ≤ 2 — V8 capability-parity checklist.Branch:
release/task-mode-ia-redesign-20261011-170103Progress: 8/8 PRs complete (7 child lanes merged; this promotion audited and ready)
Planned Work
Review bots should verify that the release branch implements each planned subtask below, not just that the code is generally safe. For each row, compare the final diff and linked child PR against the expected outcome.
web/workspaces.jshash router; tabs Replay/Compare/Explore; welcome pick persists landingweb/shotload.jsgeneralizes the compare chip machine; load chip on Replayweb/palette.jsnative dialog, Ctrl/Cmd+K, all registries indexedMerged PRs
#/replay/{shot}?t=·#/compare/{ids}?ref=·#/explore, legacy#db/#sandbox/query deep links preserved); ARIA tabs Replay/Compare/Explore; welcome-pick landing persisted incl. reload-regression fix (bcf8301); provenance legend collapses <760px fixing the 375px title clip;--focus-ringtoken. pytest 352 passed + 1 skipped, ruff/mypy clean, CI 3/3 atbcf8301; QA evidence tc-1..6 uploaded.#/replay/30166?edit=1; router honors?edit=1by opening today's anchored what-if (drawer task will own it, call site commented); label-driven sandbox→Explore copy in lessons.json/glossary.json, tab:sandbox routing fields and anchors untouched (L3). pytest 352 passed + 1 skipped, CI 3/3 ate226638; QA records tc-1..4 attached.web/shotload.js: one fetch lifecycle for every shot (queued → fetching → ready | error, AbortController supersede lands back in queued,errorOf()/onChange()); single-shot loads render a status-cluster chip with Retry · Drop on failure (V6); compare chips migrated onto the machine. pytest 352 passed + 1 skipped, CI 3/3 at946687b; QA tc-1/tc-2/tc-4 incl. a genuine both-chips-fetching capture. Pre-existing stale compare failure-header behavior documented for the compare lane.web/palette.js: native<dialog>over every workspace, opened with Ctrl/Cmd+K or the header stub; case-insensitive search over workspaces, the/shotscatalog (fetched once, cached), 43 lesson titles, 47 glossary terms (definitions shown in place), and actions running existing public surfaces only; navigation writes the URL grammar via workspaces.js; focus scoped to the dialog, restored to the opener (Esc and cancel). pytest 352 passed + 1 skipped, CI 3/3 at02c5775; QA tc-1..3 (shot load, focus restore, lesson + glossary jumps).role=dialog,aria-modal, opener focus return) replaces the what-if cell swap; state cell, limit gauges and logbook stay live underneath; edits persist across close/reopen, reset restores the measured programme,?edit=1auto-opens; toolbar regrouped into Transport / Shot actions / Status clusters, wrap-safe at any width, all data-guide anchors kept; USD export failures surface in the fallback banner + transient status chip; stored energy in MJ on every Replay surface incl. compare traces and ΔW strip. pytest 352 passed + 1 skipped, ruff/mypy clean, CI 3/3 at8035ea7; QA tc-1..3 (drawer flow via screenshots + DOM assertions — recordings wrote sub-second stubs this session, documented in the PR body; USD-error banner evidence; V7 lesson-1 walkthrough passed).#tab-compare, with the side-by-side read (stored energy in MJ, worst limit, ΔW vs reference, Thomson at the shared cursor); Replay preloads its on-screen shot as the reference via#/compare/{id}; Share copies the view URL; the stale-reference header bug (handed off from PR #63) is fixed; Retry re-fetches all failed chips; a/dboutage degrades only the clouds panel. Zero "database" labels remain inweb/(verified at tip). pytest 352 passed + 1 skipped, ruff/mypy clean, CI 3/3 atf994688; QA tc-1..4 (deep-link boot, Share recording, Replay preload, error path).:focus-visibleoverride (one shared--focus-ringrule);prefers-reduced-motioncollapses gauge/spotlight tweens and gates the two JS smooth scrolls; insight() caps limit proximity at two encodings (names the closest limit, references gauges + limits strip instead of restating numbers). ARIA tablist/tab/tabpanel with roving tabindex + arrow keys verified in the accessibility tree. pytest 352 passed + 1 skipped, ruff/mypy clean, CI 3/3 at9cef3e0; QA tc-1..4 (walkthrough, panel help, insight copy, focus ring). Known follow-ups: 3D auto-orbit and figure-jitter render-loop motion (outside the CSS-token brief), tracked in the release audit.Release audit (this PR) — what landed after the child lanes
7ecf77a, 12web/replay.jsconflicts resolved). Import-bundle, CSV-export, benchmark/progress and archive-abort capabilities were carried forward with string shot-ID routing for imported bundles; a wording regression caught in validation ("a distance, never a forecast") was restored pre-push.02c16c3— CSV-export failures surface in the UI (they had inherited main's console-only handler):dlExporttakes anonErrcallback; the Replay caller renders the fallback banner + status flash, the Compare caller uses the bar's own button-text idiom;usdErr/usdSeenrenamedexportErr/exportSeensince the banner now carries both export kinds.73b4048— mobile wrap: at 375px the compare bar and its action cluster could not wrap and pushed the page 200px past the viewport;flex-wrapon.cr-hintand.cmp-actionskeeps every compare control inside the container-query column.73b4048unless noted):7ecf77a,02c16c3,73b404802c16c3+ banner re-verified at tip)73b4048— no page-level horizontal scrollprefers-reduced-motiontoken coverage — render-loop work, not CSS tokens; and the agent panel remains code-only untilFUSIONLAB_AGENT_ENABLED+ an API key are set (unchanged from the audit).Human author: Tapan Ganatma Nakkina ([email protected])
🔗 Obvious Project · 🧵 Obvious Thread · 🔀 Obvious PR Artifact
Test Evidence
V10 mobile 375px: header title unclipped, all three workspaces usable in the container-query column, no page-level horizontal scroll after the compare-bar wrap fix


