detached 0acad67

fix(history): address final review feedback

commit
0acad67716b0ab9bcf010eea3338ef2f6bbeaad3
author
brice · · 2026-06-13 07:55 UTC
parent
1ed6020
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +24 -17
M internal/ui/browser_window_history_sidebar_test.go +8 -3
@@ -94,7 +94,13 @@ func TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing(t *testi
9494 bwTabs.Add(tab)
9595 bwTabs.SetActive(tab.ID)
9696
97- bw := &browserWindow{id: "window-1", tabs: bwTabs}
97+ bw := &browserWindow{
98+ id: "window-1",
99+ tabs: bwTabs,
100+ mainWindow: &window.MainWindow{},
101+ historySidebar: &component.HistorySidebar{},
102+ sidebarVisible: true,
103+ }
98104
99105 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
100106 fakeWv := &fakeRecordingWebView{id: 1}
@@ -118,6 +124,7 @@ func TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing(t *testi
118124 require.NoError(t, err)
119125 assert.True(t, fakeWv.loadURICalled)
120126 assert.Equal(t, navigateURL, fakeWv.loadURILastURI, "Ctrl+Enter navigation should go to the URL")
127+ assert.True(t, bw.sidebarVisible, "Ctrl+Enter navigation should keep the sidebar visible")
121128 }
122129
123130 // TestHistorySidebar_OwnershipOnMultiWindowNavigation verifies that when
@@ -844,8 +851,6 @@ func TestApp_HideAndRestoreFocusForBrowserWindow_HidesSidebar(t *testing.T) {
844851
845852 // Sidebar must be hidden.
846853 assert.False(t, bw.sidebarVisible, "sidebar must be hidden")
847-
848- _ = wsView
849854 }
850855
851856 // TestApp_HideAndRestoreFocusForBrowserWindow_NilBWIsSafe verifies that
@@ -94,7 +94,13 @@ func TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing(t *testi
94 bwTabs.Add(tab)94 bwTabs.Add(tab)
95 bwTabs.SetActive(tab.ID)95 bwTabs.SetActive(tab.ID)
96 96
97- bw := &browserWindow{id: "window-1", tabs: bwTabs}97+ bw := &browserWindow{
98+ id: "window-1",
99+ tabs: bwTabs,
100+ mainWindow: &window.MainWindow{},
101+ historySidebar: &component.HistorySidebar{},
102+ sidebarVisible: true,
103+ }
98 104
99 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)105 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
100 fakeWv := &fakeRecordingWebView{id: 1}106 fakeWv := &fakeRecordingWebView{id: 1}
@@ -118,6 +124,7 @@ func TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing(t *testi
118 require.NoError(t, err)124 require.NoError(t, err)
119 assert.True(t, fakeWv.loadURICalled)125 assert.True(t, fakeWv.loadURICalled)
120 assert.Equal(t, navigateURL, fakeWv.loadURILastURI, "Ctrl+Enter navigation should go to the URL")126 assert.Equal(t, navigateURL, fakeWv.loadURILastURI, "Ctrl+Enter navigation should go to the URL")
127+ assert.True(t, bw.sidebarVisible, "Ctrl+Enter navigation should keep the sidebar visible")
121 }128 }
122 129
123 // TestHistorySidebar_OwnershipOnMultiWindowNavigation verifies that when130 // TestHistorySidebar_OwnershipOnMultiWindowNavigation verifies that when
@@ -844,8 +851,6 @@ func TestApp_HideAndRestoreFocusForBrowserWindow_HidesSidebar(t *testing.T) {
844 851
845 // Sidebar must be hidden.852 // Sidebar must be hidden.
846 assert.False(t, bw.sidebarVisible, "sidebar must be hidden")853 assert.False(t, bw.sidebarVisible, "sidebar must be hidden")
847-
848- _ = wsView
849 }854 }
850 855
851 // TestApp_HideAndRestoreFocusForBrowserWindow_NilBWIsSafe verifies that856 // TestApp_HideAndRestoreFocusForBrowserWindow_NilBWIsSafe verifies that
M internal/ui/component/history_sidebar.go +2 -2
@@ -255,7 +255,7 @@ func (hs *HistorySidebar) Show() {
255255 hs.Reload()
256256 return false
257257 })
258- glib.IdleAdd(&reloadCb, 0)
258+ hs.scheduleIdle(reloadCb)
259259
260260 // Focus search entry via idle callback to ensure layout is stable.
261261 cb := glib.SourceFunc(func(uintptr) bool {
@@ -269,7 +269,7 @@ func (hs *HistorySidebar) Show() {
269269 entry.GrabFocus()
270270 return false
271271 })
272- glib.IdleAdd(&cb, 0)
272+ hs.scheduleIdle(cb)
273273 }
274274
275275 // Hide hides the sidebar.
@@ -255,7 +255,7 @@ func (hs *HistorySidebar) Show() {
255 hs.Reload()255 hs.Reload()
256 return false256 return false
257 })257 })
258- glib.IdleAdd(&reloadCb, 0)258+ hs.scheduleIdle(reloadCb)
259 259
260 // Focus search entry via idle callback to ensure layout is stable.260 // Focus search entry via idle callback to ensure layout is stable.
261 cb := glib.SourceFunc(func(uintptr) bool {261 cb := glib.SourceFunc(func(uintptr) bool {
@@ -269,7 +269,7 @@ func (hs *HistorySidebar) Show() {
269 entry.GrabFocus()269 entry.GrabFocus()
270 return false270 return false
271 })271 })
272- glib.IdleAdd(&cb, 0)272+ hs.scheduleIdle(cb)
273 }273 }
274 274
275 // Hide hides the sidebar.275 // Hide hides the sidebar.
M internal/ui/component/history_sidebar_keyboard.go +4 -2
@@ -229,10 +229,10 @@ func (hs *HistorySidebar) rebuildLocalGroups() {
229229
230230 // removeFromAllEntries removes all history entries matching the given URL or ID
231231 // from hs.allEntries. Must be called with hs.mu write lock held.
232-func (hs *HistorySidebar) removeFromAllEntries(url string, id int64) {
232+func (hs *HistorySidebar) removeFromAllEntries(_ string, id int64) {
233233 filtered := make([]*entity.HistoryEntry, 0, len(hs.allEntries))
234234 for _, e := range hs.allEntries {
235- if e != nil && (e.URL == url || e.ID == id) {
235+ if e != nil && e.ID == id {
236236 continue
237237 }
238238 filtered = append(filtered, e)
@@ -377,6 +377,8 @@ func (hs *HistorySidebar) jumpToLastSelectable() {
377377 lastRow = row
378378 }
379379 }
380+ // GetRowAtIndex returns nil once the index is past the end of the list,
381+ // so this scan terminates naturally at the first missing row.
380382 // If we found a selectable row, try it. Otherwise fall back to last row.
381383 if lastRow != nil {
382384 hs.listBox.SelectRow(lastRow)
@@ -229,10 +229,10 @@ func (hs *HistorySidebar) rebuildLocalGroups() {
229 229
230 // removeFromAllEntries removes all history entries matching the given URL or ID230 // removeFromAllEntries removes all history entries matching the given URL or ID
231 // from hs.allEntries. Must be called with hs.mu write lock held.231 // from hs.allEntries. Must be called with hs.mu write lock held.
232-func (hs *HistorySidebar) removeFromAllEntries(url string, id int64) {232+func (hs *HistorySidebar) removeFromAllEntries(_ string, id int64) {
233 filtered := make([]*entity.HistoryEntry, 0, len(hs.allEntries))233 filtered := make([]*entity.HistoryEntry, 0, len(hs.allEntries))
234 for _, e := range hs.allEntries {234 for _, e := range hs.allEntries {
235- if e != nil && (e.URL == url || e.ID == id) {235+ if e != nil && e.ID == id {
236 continue236 continue
237 }237 }
238 filtered = append(filtered, e)238 filtered = append(filtered, e)
@@ -377,6 +377,8 @@ func (hs *HistorySidebar) jumpToLastSelectable() {
377 lastRow = row377 lastRow = row
378 }378 }
379 }379 }
380+ // GetRowAtIndex returns nil once the index is past the end of the list,
381+ // so this scan terminates naturally at the first missing row.
380 // If we found a selectable row, try it. Otherwise fall back to last row.382 // If we found a selectable row, try it. Otherwise fall back to last row.
381 if lastRow != nil {383 if lastRow != nil {
382 hs.listBox.SelectRow(lastRow)384 hs.listBox.SelectRow(lastRow)
M internal/ui/component/history_sidebar_loading_search.go +4 -5
@@ -353,14 +353,15 @@ func (hs *HistorySidebar) applyFilter() {
353353 func (hs *HistorySidebar) doFTSearch(query string, gen uint64) {
354354 hs.mu.RLock()
355355 uc := hs.historyUC
356+ ctx := hs.ctx
356357 hs.mu.RUnlock()
357358
358- if uc == nil {
359+ if uc == nil || ctx == nil {
359360 return
360361 }
361362
362363 go func() {
363- out, err := uc.Search(hs.ctx, dto.HistorySearchInput{Query: query, Limit: sidebarSearchLimit})
364+ out, err := uc.Search(ctx, dto.HistorySearchInput{Query: query, Limit: sidebarSearchLimit})
364365 var entries []*entity.HistoryEntry
365366 if out != nil {
366367 entries = make([]*entity.HistoryEntry, len(out.Matches))
@@ -397,9 +398,7 @@ func (hs *HistorySidebar) applySearchResults(entries []*entity.HistoryEntry, gen
397398 }
398399 hs.searchResults = entries
399400 hs.searchDone = true
400- if err != nil {
401- hs.searchErr = err
402- }
401+ hs.searchErr = err
403402 hs.setDisplayGroupsLocked(groupHistoryByDay(entries))
404403 return true
405404 }
@@ -353,14 +353,15 @@ func (hs *HistorySidebar) applyFilter() {
353 func (hs *HistorySidebar) doFTSearch(query string, gen uint64) {353 func (hs *HistorySidebar) doFTSearch(query string, gen uint64) {
354 hs.mu.RLock()354 hs.mu.RLock()
355 uc := hs.historyUC355 uc := hs.historyUC
356+ ctx := hs.ctx
356 hs.mu.RUnlock()357 hs.mu.RUnlock()
357 358
358- if uc == nil {359+ if uc == nil || ctx == nil {
359 return360 return
360 }361 }
361 362
362 go func() {363 go func() {
363- out, err := uc.Search(hs.ctx, dto.HistorySearchInput{Query: query, Limit: sidebarSearchLimit})364+ out, err := uc.Search(ctx, dto.HistorySearchInput{Query: query, Limit: sidebarSearchLimit})
364 var entries []*entity.HistoryEntry365 var entries []*entity.HistoryEntry
365 if out != nil {366 if out != nil {
366 entries = make([]*entity.HistoryEntry, len(out.Matches))367 entries = make([]*entity.HistoryEntry, len(out.Matches))
@@ -397,9 +398,7 @@ func (hs *HistorySidebar) applySearchResults(entries []*entity.HistoryEntry, gen
397 }398 }
398 hs.searchResults = entries399 hs.searchResults = entries
399 hs.searchDone = true400 hs.searchDone = true
400- if err != nil {401+ hs.searchErr = err
401- hs.searchErr = err
402- }
403 hs.setDisplayGroupsLocked(groupHistoryByDay(entries))402 hs.setDisplayGroupsLocked(groupHistoryByDay(entries))
404 return true403 return true
405 }404 }
M internal/ui/component/history_sidebar_rendering.go +5 -3
@@ -24,6 +24,7 @@ func (hs *HistorySidebar) rebuildList() {
2424 rows := hs.displayRows
2525 query := hs.currentQuery
2626 hasSearchResults := hs.searchResults != nil
27+ searchDone := hs.searchDone
2728 totalLoaded := hs.totalLoaded
2829 hs.mu.RUnlock()
2930
@@ -33,7 +34,7 @@ func (hs *HistorySidebar) rebuildList() {
3334 if len(rows) == 0 {
3435 if !hasSearchResults && totalLoaded == 0 {
3536 // Browse has not loaded yet AND no search has completed.
36- hs.showLoadingOrEmpty()
37+ hs.showLoadingOrEmpty(query, searchDone)
3738 return
3839 }
3940 // Search completed with 0 results, or browse loaded but empty (no history).
@@ -60,7 +61,7 @@ func (hs *HistorySidebar) rebuildList() {
6061 hs.ensureAtLeastOneSelection()
6162 }
6263
63-func (hs *HistorySidebar) showLoadingOrEmpty() {
64+func (hs *HistorySidebar) showLoadingOrEmpty(query string, searchDone bool) {
6465 label := gtk.NewLabel(nil)
6566 if label == nil {
6667 return
@@ -69,12 +70,13 @@ func (hs *HistorySidebar) showLoadingOrEmpty() {
6970
7071 hs.mu.RLock()
7172 isLoading := hs.isLoading
72- query := hs.currentQuery
7373 hs.mu.RUnlock()
7474
7575 switch {
7676 case isLoading && query == "":
7777 label.SetText("Loading history...")
78+ case query != "" && !searchDone:
79+ label.SetText("Searching...")
7880 case query != "":
7981 label.SetText(fmt.Sprintf("No results for \"%s\"", query))
8082 default:
@@ -24,6 +24,7 @@ func (hs *HistorySidebar) rebuildList() {
24 rows := hs.displayRows24 rows := hs.displayRows
25 query := hs.currentQuery25 query := hs.currentQuery
26 hasSearchResults := hs.searchResults != nil26 hasSearchResults := hs.searchResults != nil
27+ searchDone := hs.searchDone
27 totalLoaded := hs.totalLoaded28 totalLoaded := hs.totalLoaded
28 hs.mu.RUnlock()29 hs.mu.RUnlock()
29 30
@@ -33,7 +34,7 @@ func (hs *HistorySidebar) rebuildList() {
33 if len(rows) == 0 {34 if len(rows) == 0 {
34 if !hasSearchResults && totalLoaded == 0 {35 if !hasSearchResults && totalLoaded == 0 {
35 // Browse has not loaded yet AND no search has completed.36 // Browse has not loaded yet AND no search has completed.
36- hs.showLoadingOrEmpty()37+ hs.showLoadingOrEmpty(query, searchDone)
37 return38 return
38 }39 }
39 // Search completed with 0 results, or browse loaded but empty (no history).40 // Search completed with 0 results, or browse loaded but empty (no history).
@@ -60,7 +61,7 @@ func (hs *HistorySidebar) rebuildList() {
60 hs.ensureAtLeastOneSelection()61 hs.ensureAtLeastOneSelection()
61 }62 }
62 63
63-func (hs *HistorySidebar) showLoadingOrEmpty() {64+func (hs *HistorySidebar) showLoadingOrEmpty(query string, searchDone bool) {
64 label := gtk.NewLabel(nil)65 label := gtk.NewLabel(nil)
65 if label == nil {66 if label == nil {
66 return67 return
@@ -69,12 +70,13 @@ func (hs *HistorySidebar) showLoadingOrEmpty() {
69 70
70 hs.mu.RLock()71 hs.mu.RLock()
71 isLoading := hs.isLoading72 isLoading := hs.isLoading
72- query := hs.currentQuery
73 hs.mu.RUnlock()73 hs.mu.RUnlock()
74 74
75 switch {75 switch {
76 case isLoading && query == "":76 case isLoading && query == "":
77 label.SetText("Loading history...")77 label.SetText("Loading history...")
78+ case query != "" && !searchDone:
79+ label.SetText("Searching...")
78 case query != "":80 case query != "":
79 label.SetText(fmt.Sprintf("No results for \"%s\"", query))81 label.SetText(fmt.Sprintf("No results for \"%s\"", query))
80 default:82 default:
M internal/ui/component/history_sidebar_search_test.go +1 -1
@@ -271,7 +271,7 @@ func TestApplySearchResults_CurrentGenerationAppliedAfterSearch(t *testing.T) {
271271 }
272272
273273 hs.mu.RLock()
274- assert.NotNil(t, hs.searchResults)
274+ require.NotNil(t, hs.searchResults)
275275 assert.True(t, hs.searchDone)
276276 assert.Len(t, hs.searchResults, 1)
277277 assert.Equal(t, "https://live.com", hs.searchResults[0].URL)
@@ -271,7 +271,7 @@ func TestApplySearchResults_CurrentGenerationAppliedAfterSearch(t *testing.T) {
271 }271 }
272 272
273 hs.mu.RLock()273 hs.mu.RLock()
274- assert.NotNil(t, hs.searchResults)274+ require.NotNil(t, hs.searchResults)
275 assert.True(t, hs.searchDone)275 assert.True(t, hs.searchDone)
276 assert.Len(t, hs.searchResults, 1)276 assert.Len(t, hs.searchResults, 1)
277 assert.Equal(t, "https://live.com", hs.searchResults[0].URL)277 assert.Equal(t, "https://live.com", hs.searchResults[0].URL)
M internal/ui/window/main_window.go +0 -1
@@ -114,7 +114,6 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
114114 mw.mainContentBox.SetHexpand(true)
115115 mw.mainContentBox.SetVexpand(true)
116116 mw.mainContentBox.SetVisible(true)
117- mw.mainContentBox.AddCssClass("content-area")
118117
119118 // Sidebar box (hidden by default).
120119 mw.sidebarBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
@@ -114,7 +114,6 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
114 mw.mainContentBox.SetHexpand(true)114 mw.mainContentBox.SetHexpand(true)
115 mw.mainContentBox.SetVexpand(true)115 mw.mainContentBox.SetVexpand(true)
116 mw.mainContentBox.SetVisible(true)116 mw.mainContentBox.SetVisible(true)
117- mw.mainContentBox.AddCssClass("content-area")
118 117
119 // Sidebar box (hidden by default).118 // Sidebar box (hidden by default).
120 mw.sidebarBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)119 mw.sidebarBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close