feat(tui): interactive diff view with unified/side-by-side/plain modes#27
feat(tui): interactive diff view with unified/side-by-side/plain modes#27engalar wants to merge 5 commits intomendixlabs:mainfrom
Conversation
Add a reusable TUI diff viewer supporting: - Unified and side-by-side views (Tab to toggle) - Word-level inline diff highlighting (Lipgloss segments) - Sticky line numbers during horizontal scroll - Mouse wheel vertical and horizontal scrolling - Vim-style navigation (j/k, h/l, g/G, ]/[ hunk jump) - Search with live matching (/, n/N) - Chroma syntax highlighting for unchanged lines Uses go-difflib SequenceMatcher for high-quality line pairing and sergi/go-diff for word-level segments within paired lines. Integrated via CompareView 'D' key: pick two objects in compare mode, press D to diff their content with word-level precision. Closes #17
Add third DiffView mode (Tab cycles: Unified → Side-by-Side → Plain Diff): - Plain Diff renders standard unified diff format (--- a/, +++ b/, @@ @@) - No ANSI colors — tmux capture-pane produces LLM-readable output - y key copies unified diff to clipboard for pasting to LLM LLM agents can see the "Tab mode" hint, press Tab to switch to Plain Diff, then tmux capture the standard diff format.
Replace ad-hoc view routing (bool/pointer priority chain) with a unified View interface and ViewStack pattern. All views (Browser, Overlay, Compare, Diff, Jumper) implement the same interface, enabling declarative chrome rendering and eliminating 30+ manual sync calls. Key changes: - View interface + ViewStack for composable view management - BrowserView wraps MillerView, absorbs action keys from app.go - OverlayView handles NDSL/MDL tab switching self-contained - Semantic color system (AdaptiveColor) replacing monochrome styles - Focus indicators: blue title + edge bar on focused column, faint unfocused - Global fuzzy jump (Space key) to navigate to any project node - Preview debounce (150ms) to prevent subprocess flooding during fast scroll - LLM anchor lines ([mxcli:mode]) for tmux capture-pane parsing - Clickable breadcrumb in status bar for mouse navigation - Enhanced chrome: mode badge, context summary, compact hint bar Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…efixed search - Fix preview not auto-triggering on cursor move: previewDebounceMsg was silently discarded by BrowserView.Update() type switch - Fix Miller columns scroll bug: value-semantics caused SetSize() to operate on copies, leaving height=0 and maxVisible()=1 - Fix hunk navigation in Plain Diff mode: compute separate hunk indices from @@ headers, use activeHunkStarts() per view mode - Fix ]c/[c keybinding: implement Vim-style two-key sequence with pendingKey state instead of single-key ]/[ - Fix Plain Diff UTF-8 truncation: use hslice instead of byte-based len() for multi-byte character safety - Extract shared FuzzyList component from CompareView and JumperView - Add type-prefixed fuzzy search (e.g. mf:query filters by Microflow) using dynamic fuzzyScore matching against NodeType, no hardcoded map - Add unit tests for DiffEngine (14), DiffRenderer (16), FuzzyList (10)
- Remove dead leftOffset/rightOffset/syncScroll/focus fields from DiffView - Replace time.Sleep with tea.Tick in CompareView flash clear - Consolidate duplicate stripANSI/stripAnsi into single stripAnsi (clipboard.go) - Add OSC sequence limitation comment to hslice - Add y:yank hint to DiffViewHints - Extract scrollbarGeometry/scrollbarChar helpers, remove 3x duplicated scrollbar code - Move diff color palette from diffrender.go to theme.go as AdaptiveColor pairs - Run go mod tidy (promote sergi/go-diff to direct dependency)
ako
left a comment
There was a problem hiding this comment.
Code Review: feat(tui): interactive diff view with unified/side-by-side/plain modes
30 files, +4880/-629, 5 commits. Adds DiffView component with three display modes, introduces View interface/ViewStack pattern, adds type-prefixed fuzzy search.
Critical Issues
1. Overlay Tab-switching is broken (browserview.go / app.go)
BrowserView.handleKey for "b" and "m" keys sets bv.overlayQName, bv.overlayNodeType, bv.overlayIsNDSL on the value receiver, then emits OpenOverlayMsg. But App handles OpenOverlayMsg with:
ov := NewOverlayView(msg.Title, msg.Content, a.width, a.height, OverlayViewOpts{})The opts are always empty — Switchable is false, QName is empty. NDSL/MDL Tab-switching in the overlay is dead.
2. DiffView search broken in PlainDiff mode (diffview.go)
buildMatchLines() searches dv.result.Lines (structural diff lines), but PlainDiff mode renders dv.plainLines which have different indices (includes @@ headers, ---/+++ lines). Search match indices don't correspond to visible lines, causing scroll-to-match to jump to wrong positions.
3. Key hint mismatch: d shown but D required (hintbar.go / compare.go)
CompareHints shows {Key: "d", Label: "diff"} but the handler checks case "D":. Users will press d and nothing happens.
Moderate Issues
4. hslice/truncateToWidth don't handle wide characters (diffrender.go)
hslice increments visW by 1 per rune. East Asian characters take 2 columns. mattn/go-runewidth is added as a dependency but not used in these functions. Diff view will misalign with CJK text.
5. handleBrowserAppKeys returns nil-cmd goroutines (app.go)
Many cases return func() tea.Msg { return nil } as a "handled" sentinel. Each spawns a goroutine that does nothing. Should use a no-op constant or change the return signature.
6. CompareView visible field redundant with ViewStack (compare.go)
updateInternal starts with if !c.visible { return c, nil } — all updates are no-ops unless Show() was called. This is redundant with being on the ViewStack and fragile if someone forgets to call Show().
Dead Code
7. BrowserView.overlayQName, overlayNodeType, overlayIsNDSL fields — set but never read (browserview.go)
These are set in handleKey but the OpenOverlayMsg handler creates overlays with empty opts. Remove these fields.
8. BrowserView.handleKey "c" and "r" cases unreachable (browserview.go)
Both keys are intercepted by App.handleBrowserAppKeys before reaching BrowserView.Update. The BrowserView cases are dead.
9. BrowserView.compareItems field — set but never read (browserview.go)
Assigned in syncBrowserView() and LoadTreeMsg handler but never used by BrowserView.
Minor Issues
10. Magic numbers
150msdebounce delay (miller.go)3for divider width in side-by-side (diffrender.go)8for horizontal scroll step (diffview.go)
11. renderContextSummary naive pluralization (app.go)
strings.ToLower(k)+"s" produces wrong plurals for some types (e.g., "Index" → "indexs").
12. Duplicated loadBsonNDSL across BrowserView and CompareView
Identical implementations in both views.
What Looks Good
- The View interface + ViewStack pattern is a solid architectural improvement over the scattered boolean flags — makes view routing declarative and composable
- DiffEngine with word-level inline highlighting using SequenceMatcher + sergi/go-diff is well-implemented
- Type-prefixed fuzzy search (
e:Customer,mf:query) is a nice UX touch using dynamicfuzzyScorematching - 40 unit tests for DiffEngine, DiffRenderer, and FuzzyList
- The post-review cleanup commit addressing dead fields,
time.Sleep→tea.Tick, scrollbar dedup, and theme extraction shows good self-review discipline - Plain Diff mode for LLM-friendly output is a thoughtful addition
Workflow Note
The AI review workflow ran but returned an empty response from models.github.ai — likely the 6k-line diff exceeded the request size limit. The pull-requests: write permission was also not granted (only Contents: read, Metadata: read, Models: read appeared in the token). Will fix the workflow separately.
Recommendation
Request changes — fix the critical issues (#1-3) before merging. The overlay Tab-switching being completely broken and the search/hint mismatches are user-facing bugs. The dead code (#7-9) should also be cleaned up since it was identified during the ViewStack refactor.
🤖 Generated with Claude Code
Summary
DiffViewcomponent with sticky line numbers, hunk navigation (]c/[c), horizontal scroll, and searchViewinterface andViewStackto replace scattered boolean visibility flags inapp.goFuzzyList(e.g.e:Customerto filter entities)Code Review Fixes (post-review commit)
leftOffset/rightOffset/syncScroll/focusscaffold fieldstime.Sleepwithtea.Tickin flash-clear cmdstripANSI/stripAnsiinto single functionscrollbarGeometry/scrollbarCharhelpers (3× duplication removed)theme.goasAdaptiveColorpairs for light/dark supporty:yanktoDiffViewHints; addhsliceOSC limitation commentgo mod tidy— promotesergi/go-diffto direct dependencyTest Plan
go test ./cmd/mxcli/tui/...passesTabcycles through all three modesycopies plain diff to clipboarde:,mf:) filters correctly in FuzzyList