From ab44f39cda8145db4b4790a9170f328209070198 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Wed, 22 Apr 2026 22:58:19 +0800 Subject: [PATCH 1/3] fix(ui): skip MacOSPref entries with empty domain or key in snapshot editor A corrupted or old-format snapshot file can contain a MacOSPref entry with an empty Key (e.g. Domain="cirruslabs/cli", Key=""), which would render as "cirruslabs/cli." in the macOS Prefs tab instead of in Taps. Filter out any MacOSPref entry where Domain or Key is empty in NewSnapshotEditor, since every valid macOS preference requires both. The tap itself is already correctly shown in the Taps tab. --- internal/ui/snapshot_editor.go | 11 +++++++---- internal/ui/snapshot_editor_test.go | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/internal/ui/snapshot_editor.go b/internal/ui/snapshot_editor.go index 8825246..4b66d04 100644 --- a/internal/ui/snapshot_editor.go +++ b/internal/ui/snapshot_editor.go @@ -114,14 +114,17 @@ func NewSnapshotEditor(snap *snapshot.Snapshot) SnapshotEditorModel { } tabs[3] = editorTab{name: "Taps", icon: "🔌", items: tapItems, itemType: editorItemTap} - prefItems := make([]editorItem, len(snap.MacOSPrefs)) - for i, p := range snap.MacOSPrefs { - prefItems[i] = editorItem{ + var prefItems []editorItem + for _, p := range snap.MacOSPrefs { + if p.Domain == "" || p.Key == "" { + continue + } + prefItems = append(prefItems, editorItem{ name: fmt.Sprintf("%s.%s", p.Domain, p.Key), description: fmt.Sprintf("= %s (%s)", p.Value, p.Desc), selected: true, itemType: editorItemMacOSPref, - } + }) } tabs[4] = editorTab{name: "macOS Prefs", icon: "⚙️ ", items: prefItems, itemType: editorItemMacOSPref} diff --git a/internal/ui/snapshot_editor_test.go b/internal/ui/snapshot_editor_test.go index f95d49e..c29e24e 100644 --- a/internal/ui/snapshot_editor_test.go +++ b/internal/ui/snapshot_editor_test.go @@ -68,6 +68,25 @@ func TestNewSnapshotEditorItems(t *testing.T) { assert.Equal(t, "homebrew/core", m.tabs[3].items[0].name) } +func TestNewSnapshotEditorSkipsInvalidMacOSPrefs(t *testing.T) { + snap := makeTestSnapshot() + snap.MacOSPrefs = append(snap.MacOSPrefs, + snapshot.MacOSPref{Domain: "cirruslabs/cli", Key: ""}, // tap misclassified as pref + snapshot.MacOSPref{Domain: "", Key: "SomeKey"}, // empty domain + snapshot.MacOSPref{Domain: "com.apple.dock", Key: "tilesize", Value: "48"}, // valid + ) + m := NewSnapshotEditor(snap) + + // Only valid prefs (non-empty domain AND key) should appear in the macOS Prefs tab. + prefTab := m.tabs[4] + for _, item := range prefTab.items { + assert.NotEmpty(t, item.name, "pref item name must not be empty") + assert.Contains(t, item.name, ".", "pref item name must contain a dot separator") + } + // 2 from makeTestSnapshot + 1 valid new pref = 3 + assert.Equal(t, 3, len(prefTab.items)) +} + func TestNewSnapshotEditorAllItemsSelected(t *testing.T) { snap := makeTestSnapshot() m := NewSnapshotEditor(snap) From 69d602af687f64efefcffda1f708c12d1c2c1f4b Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Wed, 22 Apr 2026 23:05:28 +0800 Subject: [PATCH 2/3] style: gofmt snapshot_editor_test.go --- internal/ui/snapshot_editor_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ui/snapshot_editor_test.go b/internal/ui/snapshot_editor_test.go index c29e24e..b11b2b5 100644 --- a/internal/ui/snapshot_editor_test.go +++ b/internal/ui/snapshot_editor_test.go @@ -71,8 +71,8 @@ func TestNewSnapshotEditorItems(t *testing.T) { func TestNewSnapshotEditorSkipsInvalidMacOSPrefs(t *testing.T) { snap := makeTestSnapshot() snap.MacOSPrefs = append(snap.MacOSPrefs, - snapshot.MacOSPref{Domain: "cirruslabs/cli", Key: ""}, // tap misclassified as pref - snapshot.MacOSPref{Domain: "", Key: "SomeKey"}, // empty domain + snapshot.MacOSPref{Domain: "cirruslabs/cli", Key: ""}, // tap misclassified as pref + snapshot.MacOSPref{Domain: "", Key: "SomeKey"}, // empty domain snapshot.MacOSPref{Domain: "com.apple.dock", Key: "tilesize", Value: "48"}, // valid ) m := NewSnapshotEditor(snap) From 33e6651bc2abe1357901c35d181a1b7e6d444781 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Wed, 22 Apr 2026 23:28:48 +0800 Subject: [PATCH 3/3] fix(ui): restore tea.ClearScreen on tab switch to eliminate ghost text bubbletea v1.3.0 + x/ansi v0.8.0 made tea.ClearScreen fully functional again (it was effectively a no-op in v1.1.0 because x/ansi v0.4.2 had silently changed EraseEntireDisplay to clear only the scrollback buffer rather than the visible frame). PR #47 removed the tea.ClearScreen workaround under the assumption that v1.3.0's incremental diff renderer would clear rows correctly on its own, but the ghost-text symptom (cirruslabs/cli appearing at the top of the macOS Prefs tab after switching from the Taps tab) persists in practice across tested terminals. Restoring the explicit clear is the safest and most portable fix: tea.ClearScreen forces bubbletea to erase the alt-screen buffer and do a full repaint before rendering the new tab, guaranteeing no stale rows survive the transition. Also adds two tests: - TestSnapshotEditorTabSwitchReturnsClearScreenCmd: asserts Tab and ShiftTab return a non-nil cmd (the ClearScreen cmd). - TestSnapshotEditorViewTabIsolation: asserts that View() for the macOS Prefs tab never contains tap-item text, and vice-versa, locking in cross-tab isolation at the logic level. --- internal/ui/snapshot_editor.go | 2 ++ internal/ui/snapshot_editor_test.go | 33 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/internal/ui/snapshot_editor.go b/internal/ui/snapshot_editor.go index 4b66d04..85e6441 100644 --- a/internal/ui/snapshot_editor.go +++ b/internal/ui/snapshot_editor.go @@ -211,11 +211,13 @@ func (m SnapshotEditorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint m.activeTab = (m.activeTab + 1) % len(m.tabs) m.cursor = 0 m.scrollOffset = 0 + return m, tea.ClearScreen case key.Matches(msg, keys.ShiftTab), key.Matches(msg, keys.Left): m.activeTab = (m.activeTab - 1 + len(m.tabs)) % len(m.tabs) m.cursor = 0 m.scrollOffset = 0 + return m, tea.ClearScreen case key.Matches(msg, keys.Up): if m.cursor > 0 { diff --git a/internal/ui/snapshot_editor_test.go b/internal/ui/snapshot_editor_test.go index b11b2b5..6327aff 100644 --- a/internal/ui/snapshot_editor_test.go +++ b/internal/ui/snapshot_editor_test.go @@ -770,6 +770,39 @@ func TestBuildEditedSnapshotAddedMacOSPrefSplitsOnLastDot(t *testing.T) { assert.Equal(t, "48", added.Value) } +func TestSnapshotEditorTabSwitchReturnsClearScreenCmd(t *testing.T) { + m := NewSnapshotEditor(makeTestSnapshot()) + + _, cmd := m.Update(tea.KeyMsg{Type: tea.KeyTab}) + assert.NotNil(t, cmd, "Tab should return tea.ClearScreen cmd") + + _, cmd = m.Update(tea.KeyMsg{Type: tea.KeyShiftTab}) + assert.NotNil(t, cmd, "ShiftTab should return tea.ClearScreen cmd") +} + +func TestSnapshotEditorViewTabIsolation(t *testing.T) { + snap := makeTestSnapshot() + snap.Packages.Taps = []string{"cirruslabs/cli", "hashicorp/tap"} + snap.MacOSPrefs = []snapshot.MacOSPref{ + {Domain: "com.apple.dock", Key: "tilesize", Value: "48", Desc: "Dock tile size"}, + } + m := NewSnapshotEditor(snap) + m.width = 80 + m.height = 30 + + // macOS Prefs tab must not show any tap items + m.activeTab = 4 + view := m.View() + assert.NotContains(t, view, "cirruslabs/cli", "tap item must not appear on macOS Prefs tab") + assert.Contains(t, view, "com.apple.dock.tilesize", "macOS pref must appear on macOS Prefs tab") + + // Taps tab must not show any macOS pref items + m.activeTab = 3 + view = m.View() + assert.Contains(t, view, "cirruslabs/cli", "tap must appear on Taps tab") + assert.NotContains(t, view, "com.apple.dock.tilesize", "macOS pref must not appear on Taps tab") +} + func TestSnapshotEditorAddedItemVisualBadge(t *testing.T) { m := NewSnapshotEditor(makeTestSnapshot()) m.width = 80