Viewer: detail-pane UX parity with deployed ara-hub (#55-#59) - #65
Conversation
- #55: render exhibit captions (Exhibit.description) above exhibit bodies - #56: mono node id + isolated-subtree pill in the detail header - #57: DEPENDS ON block with jump chips that select the target node - #58: collapsible evidence/built-on/deps/result/sources blocks with counts - #59: ignore global replay arrows while a modal is open or in a textarea Bump 0.1.12, regenerate embedded viewer bundle, update stage-3-viewer and hub-parity docs.
`.chip-jump` inherited `display: inline-flex` from the base `.chip` class, on which `text-overflow: ellipsis` silently has no effect, so long labels were hard-clipped mid-word. Override to `display: inline-block` and regenerate the embedded viewer bundle via scripts/embed-viewer.sh.
fenfenai
left a comment
There was a problem hiding this comment.
Review Summary
Reviewed the functional diff in crates/ara-viewer/src/detail.rs, crates/ara-viewer/src/replay.rs, and crates/ara-viewer/public/styles.css across bug detection, error handling, type design, test coverage, comment quality, and guidelines-compliance dimensions (parallel specialized reviewers, findings filtered to confidence ≥75 and cross-checked against the actual PR source).
Solid PR overall: the pure detail_model() transform is thoroughly unit-tested (7 new cases covering isolated-subtree inheritance, cycle guards, and depends-on resolution in both directions), docs/CHANGELOG/version bump all follow this repo's CLAUDE.md conventions, and the plans/ → docs/ workflow was followed correctly. Error handling, comment accuracy, and guidelines compliance reviewers found nothing to report.
One likely UI-state bug and a few smaller design/coverage gaps below.
Important Issues (confidence 75-89)
crates/ara-viewer/src/detail.rs:511 — Collapsed block state may leak across node selection (confidence: 78)
CollapsibleBlock sets <details open=true> as a static (non-reactive) attribute. DetailPane's single top-level move || {...} re-invokes render_detail(model, selected).into_any() on every selected change, but since every Some(node) branch produces the same concrete view type, Leptos's AnyView reconciliation patches the existing <details> elements in place rather than remounting them — static attributes set once at initial build() aren't reapplied on rebuild(). If a user manually collapses e.g. the "evidence" block (native <details> toggle) and then selects a different node, that block stays collapsed for the new node's data with no visual cue it's hidden.
Suggestion: make open reactive per node id (e.g. open=move || ... keyed off m.id), or force a full remount on node change (<Show>/keyed wrapper) so collapse state doesn't persist across node switches.
crates/ara-viewer/src/detail.rs:68-74 — DepView.known: bool + label: String can drift out of sync (confidence: 78)
known is meant to indicate whether label came from a resolved node vs. a raw-id fallback, but nothing in the type ties the two together — both fields are pub and nothing prevents constructing an inconsistent DepView (e.g. known: true with a raw-id label). Today dep_view() is the only constructor so it's not exploited, but the render layer trusts known alone to decide whether to emit a clickable jump chip.
Suggestion: enum DepTarget { Resolved { id: NodeId, label: String }, Unresolved(NodeId) }, or keep the struct but make fields private behind DepView::resolved()/DepView::unresolved() constructors so known is derived, not independently settable.
crates/ara-viewer/src/detail.rs:93 — DetailModel.id: String duplicates the NodeId newtype (confidence: 76)
DepView.id (added in this same PR, line 70) correctly uses NodeId, but DetailModel.id is typed String and populated via node.id.as_str().to_string() — discarding the type-level guarantee NodeId already provides, inconsistent with the sibling field added in the same diff.
Suggestion: type as pub id: NodeId and convert to a display string at the Leptos render call site instead of at model-construction time.
Test gap: multi-parent Child links to the same node are untested (confidence: 82)
is_in_isolated_subtree's upward walk picks the first matching Child link via .find() over manifest.links (i.e., insertion/iteration order) when a node has more than one incoming Child link. The viewer deserializes Manifest directly from JSON without routing through ara-core's parse.rs tree-building validation (which would normally enforce single-parent trees), so a hand-edited or malformed manifest reaching the viewer is a real, reachable input — the isolated pill could flip depending on link order.
Suggestion: add a test with two Child links pointing at the same child from an isolated and a non-isolated root, and assert/pin the "first parent wins" contract explicitly (or reject/warn on the ambiguity if that's preferred).
Test gap: duplicate DependsOn links are untested (confidence: 76)
ara-core::parse.rs::dedupe_links normally strips duplicate links at compile time, but the viewer bypasses that path (see above) — a duplicate DependsOn link between the same pair would render as two identical duplicate chips with no dedup in detail_model.
Suggestion: add a test asserting the current (duplicate-preserving) behavior is intentional, or dedupe in detail_model if not.
Positive Observations
is_in_isolated_subtree's cycle guard (HashSet-based, matchestree_model's existing guard pattern) and thedepends_on/depended_on_bylink-direction filtering are correct and well covered by tests.- The CSS fix comment on
button.chip-jump(whyinline-flexbreakstext-overflow: ellipsis) is technically accurate and a good "why" comment. docs/hub-parity.mdanddocs/stage-3-viewer.mdupdates accurately describe the shipped behavior, not just touched files.- Version bump (0.1.11 → 0.1.12) and
CHANGELOG.mdentries correctly follow this repo'sCLAUDE.mdconventions, including theplans/→docs/rewrite workflow (plans/viewer-detail-pane-ux.mdadded then folded into docs within this PR's commit lineage).
… types - CollapsibleBlock: force-apply `open` via NodeRef effect keyed on `selected` — Leptos patches <details> in place and skips unchanged attribute writes, so a user-collapsed block used to stay collapsed for the next node (web test: collapsible_block_reopens_on_node_switch) - DepView: private fields behind resolved()/unresolved() constructors so `known` can never drift out of sync with `label` - DetailModel.id: String -> NodeId (matches DepView.id, keeps the newtype guarantee to the render boundary) - Pin two malformed-manifest contracts with tests: multi-parent Child links are first-parent-wins; duplicate DependsOn links render as-is - Fix web tests querying div.built-on-block/div.result-block — blocks are <details> since #58
|
Thanks for the thorough review — all five findings addressed in fc91ee7. Replies inline below. 1. Collapsed block state leaking across node selection (detail.rs:511) — Confirmed and fixed, with one correction to the suggested approach: a reactive 2. 3. 4. Multi-parent 5. Duplicate Failing |
fenfenai
left a comment
There was a problem hiding this comment.
Re-Review: Comment Resolution
Resolved (5 threads)
detail.rs:511— Collapsed block state leak ✓ Fixed.CollapsibleBlocknow takesselectedand forcesset_open(true)on the live<details>element via aNodeRef+Effectkeyed onselected, working around Leptos's in-place patching. Verified by a newwasm_bindgen_test(collapsible_block_reopens_on_node_switch) that collapses a block on N01, switches to N02, and asserts the block re-opened — this directly exercises the failure mode I flagged.detail.rs:68-74—DepView.known/labeldrift ✓ Fixed. Fields are now private; construction goes throughDepView::resolved()/DepView::unresolved(), so the invariant is enforced by the type, not just by convention.detail.rs:93—DetailModel.id: String✓ Fixed. NowNodeId, matchingDepView.id; converted to a display string only at the render call site (m.id.to_string()).detail.rs:280(multi-parentChildlinks) ✓ Fixed. New testisolated_multi_parent_first_child_link_winspins the "first Child link wins" contract in both link orderings.detail.rs:239(duplicateDependsOnlinks) ✓ Fixed. New testduplicate_depends_on_links_rendered_as_ispins the current preserve-duplicates behavior as intentional.
Verification
cargo test -p ara-viewer --lib: 172 passed (up from 170; +2 native tests for the two pinned contracts above).- Viewer bundle (
index.html, hashed.js/.wasm) was correctly regenerated to match the new source. - No regressions spotted in the rest of the diff; the
tests/web.rsselector fixes (div.built-on-block→.built-on-blocketc.) are a correct follow-on from blocks becoming<details>in #58, not new scope creep.
All prior findings addressed. No new issues found.
Summary
display: inline-flexfrom the base.chipclass, on whichtext-overflow: ellipsissilently has no effect — long labels were hard-clipped mid-word instead of showing an ellipsis. Fixed by overridingchip-jumptodisplay: inline-block, and regenerated the embedded viewer bundle viascripts/embed-viewer.sh.Test plan
cargo test -p ara-viewer— 170 passedscripts/embed-viewer.sh --check— embedded bundle up to date