feat(tui): aggregated layer view (A keybind)#57
Merged
Conversation
Implements Dive's CompareAllLayers parity — the most-cited migration gap from Dive (P1 / Tier 1.3). Pressing A in the file-tree pane switches between two views: - Default (Current Layer Contents): unchanged, today's CompareSingleLayer view. result[i] shows only what layer i changed against the cumulative state of layers 0..i-1. - Aggregated (Aggregated Layer Contents): cumulative provenance from L0 baseline. result[i] overlays every layer 1..i in order, preserving DiffType labels across iterations — a file Modified in L1 stays Modified at L7 even when L2..L7 didn't touch it. Implementation: - image/stack.go: factor mergeLayer into mergeLayerWith(carry) so Stack and the new BuildAggregatedTrees walker share the bulk of the logic. Stack's observable behaviour is bit-identical (TestStack_StillCompareSingleLayer locks this). The aggregated walker uses cloneWithDiffType as its carry-forward, propagating DiffType labels and Removed tombstones through untouched-by-this-layer paths, including recursively into directory subtrees. - image/analysis.go: Analysis gains AggregatedTrees []*FileTree; AnalyzeWithOptions populates it via BuildAggregatedTrees(layers). Pure addition — ci/, compare.go, efficiency.go, JSON export, and the cache schema are all untouched. - tui/keymap.go, tui/model.go, tui/filetree.go, tui/help.go: A toggle with reset-tree-cursor semantics; currentTreeRoot routes on the bool; [agg] status badge alongside [diff]; bottom-bar hint in both focus modes; help row under Layers; panel title flips between Dive's exact strings. Tests: - 7 new tests in image/stack_test.go covering length, L0 equivalence, the spec's Worked Example (carry-forward across untouched layers), Removed propagation, opaque-whiteout-at-mid-layer (the recursion fix), empty-layer snapshotting, and a Stack regression guard. - 6 new tests in tui/model_test.go covering toggle reset, root routing, viewer/filter precedence, filter+diff+sort survival across toggle, and nil-AggregatedTrees fallback. - 2 new title assertions in tui/filetree_test.go. - analysis_test.go gains a wiring assertion (length matches Layers). No cache schema bump (AggregatedTrees is derived). No public API removed. ci/json/compare subcommands unchanged.
00ac958 to
593740c
Compare
…-side The previous A toggle swapped which tree the file panel pointed at and relabelled the title — useful but forced flip-flopping to compare what the current layer changed vs. the cumulative carried-forward state. Replace the toggle with a real split: A halves the file-tree panel into two stacked sub-panels at the same layer cursor. Top shows per-layer Δ (StackedTrees); bottom shows cumulative provenance (AggregatedTrees) with Modified/Removed labels carried forward across untouched layers. A labelled divider (▾ Cumulative N/M) sits between them and accents when the bottom pane has focus. Each sub-pane has its own cursor, scroll offset, and collapse state, so inspecting one does not lose your place in the other — the whole point of seeing both at once. Tab cycles layers → top → bottom → layers only when the split is on; with it off, Tab is the original two-state cycle. Filter, sort, diff-only, and Enter act on the focused pane. Status bar shows [split]; help and bottom-bar hints describe the new flow. Refactor: extract renderTreeBody from renderFileTree so the same body assembly serves both single-pane and split-pane layouts. Add focus-aware helpers (rootFor / displayTreeFor / treeCursorFor / treeOffsetFor / adjustTreeScrollFor / clampTreeCursor) and a third focus state focusTreeAgg. Navigation, file open, extract, and copy-path all operate on the active pane. Tests: updated currentTreeRoot/displayTree expectations to follow active-pane semantics; added coverage for the three-state Tab cycle, focus snap-back when toggling out, independent pane cursors, focus-driven displayTree, filter applying to both panes, and split renderer titles/divider.
Two CI failures on the previous commit: 1. TestSplitMode_DisplayTreeFollowsFocus — expected the per-layer Δ slice and the cumulative slice to differ in length. They don't: Stack also produces a cumulative tree, just with DiffType labels stripped to Unchanged for paths the current layer didn't touch. Switch the assertion to compare DiffType labels at a probe path (/etc/passwd: Unchanged in the Δ view, Added in the cumulative view) — that is the actual observable difference between the two panes and what the user reads off the screen. 2. TestOverlayHelp_WideTerminal_UsesHorizontalLayout — the new help footer line about the split view was wide enough to dominate lipgloss.Width(content), clamping boxWidth to maxBox and forcing renderPanel to truncate the three-column header row, which dropped the third column's 'Wasted Files' title from the rendered output. The bottom-bar hint and the panel title already advertise the split, so drop the extra footer line. Also collapse the new Tab+A help rows into a single A row so column 0 width stays at the prior maximum.
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.
Summary
Adds an aggregated layer view to the TUI as a togglable alternate to the default per-layer view. Default behaviour is unchanged; users who never press
Asee only a cosmetic relabel of the file-tree panel title.What's new
Atoggle in the TUI flips the file tree between two views:A): cumulative provenance from the base layer — DiffType labels carry forward across iterations (a fileModifiedin L1 staysModifiedat L7 even when L2..L7 didn't touch it).[agg]next to[diff]; bottom-bar hint in both focus modes; help row under "Layers".Implementation
image/stack.goBuildAggregatedTrees,aggregateMerge,cloneWithDiffType.mergeLayerrefactored to delegate tomergeLayerWith(carry)—Stack's observable behaviour is bit-identical (regression-tested).image/analysis.goAnalysis.AggregatedTreesfield; populated alongsideStackedTreesinAnalyzeWithOptions.tui/{keymap,model,filetree,help}.goAggregatebinding,aggregatedfield, toggle handler afterSort,currentTreeRootrouting,[agg]badge, bottom-bar hints, help row, title strings.No cache-schema bump —
AggregatedTreesis a pure function ofLayersand re-derives on cache load. No changes toci/,compare.go,efficiency.go, JSON export, or the cache envelope.Testing
image/stack_test.gotests, including:TestBuildAggregatedTrees_PreservesPriorDiffTypeAcrossUntouchedLayers— central correctness test for label carry-forward.TestBuildAggregatedTrees_OpaqueWhiteoutAtMidLayer— verifies that opaque whiteouts propagateRemovedchildren through untouched directory merges.TestStack_StillCompareSingleLayer— regression guard lockingStack's existing semantics.tui/model_test.gotests covering toggle reset, root routing, viewer/filter precedence, state survival across toggle, and nil-fallback.tui/filetree_test.go.Analysis{...}literals continue to compile via named-field syntax.A,[agg]badge, label carry-forward, filter-input swallowing, filter/diff/sort survival across toggle,Enterextractor, archive-mode parity, cache cold/hot parity, and byte-identical output fromlayerx ciandlayerx --json(those subcommands consumeStackedTreesonly).Risks addressed
Stack→TestStack_StillCompareSingleLayerplus the existingStacktest suite.TestBuildAggregatedTrees_OpaqueWhiteoutAtMidLayeris designed to fail in exactly that scenario.BuildAggregatedTreespeak ≈ 2× final tree + N snapshots, comparable toStack. Doc-comment states the bound.Analysis{...}literals → new field zero-values tonil;currentTreeRoothandles nil gracefully.