Tree lens: collapse path-shaped query results into a forest#20
Conversation
New catalog lens (10th): domain → concept → related in one collapsible
view. LEVELS mode: one query row = one root→leaf path (exactly what a
multi-hop .gq match returns); `levels` names {key, label} column pairs
root→leaf; shared prefixes group client-side.
- core: buildForest — the pure grouping helper shared by both
renderers (sparse paths truncate at the first empty level, children
dedupe per parent by key value, first-seen order, path-joined ids as
stable collapse identity). Author/runtime Zod schemas follow the
Table pattern; the exhaustive buildRuntimeProps switch forced the
wiring at compile time.
- web: disclosure triangles, child-count badges, indent guides;
clicking ANY node writes its key value to select_state (Table's
setState dispatch), so a domain, concept, or leaf click drives
dependent cells. expand_depth controls the initially-open levels.
- tui: box-drawing flatten of the expanded forest (├─/└─/│), ↑/↓
cursor, ←/→ fold/unfold, Enter selects; 14-line sliding viewport.
Tests 122 core (buildForest matrix, schema round-trip, <2 levels
rejected) + 76 web (real-registry render, collapse, expand_depth: 0,
select highlight, empty state). Verified live on the concept graph:
248 domain→concept→related paths, leaf click drove /selected across
tabs; TUI render confirmed against the live server.
Known data caveat (documented, not a lens bug): .gq traversals are
inner joins, so nodes with no leaf-level edge drop out of the path
query entirely — the fix is server-side optional match; buildForest
already handles the sparse rows it would produce.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TrhtF1SiJghJASMWECzBh
|
|
||
| const renderNode = (node: TreeNode): React.ReactElement => { | ||
| const hasChildren = node.children.length > 0; | ||
| const isOpen = expanded.has(node.path); |
There was a problem hiding this comment.
buildForest gives duplicate key values under different parents distinct path identities, but selection compares only node.key. If two visible nodes share the same slug at different levels or parents, clicking either one writes the same value and highlights every matching node, so the clicked node is not uniquely represented.
There was a problem hiding this comment.
Intended: selection is ENTITY-level. The key value is what select_state consumers read (e.g. /selected = a concept slug), and duplicate keys in the forest are the SAME graph entity appearing under multiple parents — highlighting every occurrence of the selected entity is the consistent rendering. Now stated explicitly in the lens description. Tree-position selection would write a path no downstream cell can consume.
| {window.map((line, i) => { | ||
| const index = start + i; | ||
| const isCursor = isFocused && index === cur; | ||
| const isSelected = selectable && line.node.key === selected; |
There was a problem hiding this comment.
The terminal renderer also highlights selection by line.node.key instead of the tree node identity. With duplicate slugs in different branches, selecting one visible node marks all nodes with that key as selected and downstream state cannot distinguish which branch the user chose.
There was a problem hiding this comment.
Same rationale as the web finding — entity-level selection by design (the state pointer carries the entity key that dependent cells consume); documented in the lens description.
Greptile: both renderers materialized the expanded set ONCE in a useState initializer, so a state-driven query re-read that grew the forest left new branch paths missing from the set — default-open nodes rendered collapsed. Disclosure is now derived per node (expand_depth policy) overlaid with an explicit user-toggle map keyed by path: new paths get the default, the user's opens/closes survive re-reads. Regression test: re-read adds a domain (renders open) while a user's explicit collapse survives. Selection-by-key findings: intended — selection is ENTITY-level (the key value is what select_state consumers read; duplicate keys in the forest ARE the same graph entity), now stated in the lens description. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011TrhtF1SiJghJASMWECzBh
The catalog had no hierarchical lens — you could see a domain's concepts or one concept's neighbors as tables, but not the containment picture. The new Tree lens renders
domain → concept → related(or any fixed-depth hierarchy) as a collapsible forest, in both renderers.Levels mode
One query row = one root→leaf path — exactly the shape a multi-hop
.gqmatch returns:buildForest(core, pure, shared by web + TUI) groups shared prefixes: sparse paths truncate at the first empty level, children dedupe per parent by key value, first-seen order, path-joined ids give stable collapse identity. The exhaustivebuildRuntimePropsswitch made the compiler force every wiring point.Renderers
select_statevia the samesetStatedispatch as Table — the tree drives dependent cells at every level.expand_depthsets the initially-open depth (default: all).├─/└─/│), ↑/↓ cursor over visible nodes, ←/→ fold/unfold, Enter/space selects, sliding 14-line viewport.Verification
expand_depth: 0, select-highlight on the clicked node only, empty state). Registry smokes cover the TUI.domain → concept → relatedpaths grouped into a 10-domain forest; a leaf click focused/selectedand every dependent cell followed. TUI verified against the same live server (▶ ▾ Cognitive Science (19) ├─ ▸ Affordances (1) …)..gqtraversals are inner joins, so nodes with no leaf-level edge drop out of a path query entirely.buildForestalready handles the sparse rows an eventual server-side optional-match would emit — the fix is purely engine-side.Follow-up noted: edge-pairs mode (
parent_column/child_column) for arbitrary-depth self-referential hierarchies.🤖 Generated with Claude Code
https://claude.ai/code/session_011TrhtF1SiJghJASMWECzBh
Greptile Summary
This PR adds a Tree lens for rendering hierarchical query results. The main changes are:
buildForestsupport for path-shaped rows.Confidence Score: 5/5
This looks safe to merge.
Important Files Changed
Comments Outside Diff (1)
packages/core/src/catalog/lenses/tree.ts, line 78 (link)This separator is stored as a literal NUL character in the TypeScript source, which makes the new file appear binary to normal diff/search tooling. Keeping the same runtime separator via an escaped string would preserve behavior without breaking text-based source tools.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (2): Last reviewed commit: "Tree disclosure = default policy + expli..." | Re-trigger Greptile
Context used: