detached 1147e01

fix: resync tab bar after fresh window rollback

commit
1147e01b16522cb27a9d12a989360125657a4f6d
author
brice · · 2026-04-14 16:10 UTC
parent
7ef5ca7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +40 -9
M internal/ui/app.go +3 -2
@@ -546,8 +546,9 @@ func (a *App) openFreshWindow(ctx context.Context, url string) error {
546546 if a.tabs != nil {
547547 a.tabs.Remove(tab.ID)
548548 }
549- if created.mainWindow != nil && created.mainWindow.TabBar() != nil {
550- created.mainWindow.TabBar().RemoveTab(tab.ID)
549+ if a.mainWindow != nil && a.mainWindow.TabBar() != nil {
550+ a.mainWindow.TabBar().RemoveTab(tab.ID)
551+ a.mainWindow.TabBar().SetActive(a.tabs.ActiveTabID)
551552 }
552553 a.removeBrowserWindow(created.id)
553554 if created.mainWindow != nil {
@@ -546,8 +546,9 @@ func (a *App) openFreshWindow(ctx context.Context, url string) error {
546 if a.tabs != nil {546 if a.tabs != nil {
547 a.tabs.Remove(tab.ID)547 a.tabs.Remove(tab.ID)
548 }548 }
549- if created.mainWindow != nil && created.mainWindow.TabBar() != nil {549+ if a.mainWindow != nil && a.mainWindow.TabBar() != nil {
550- created.mainWindow.TabBar().RemoveTab(tab.ID)550+ a.mainWindow.TabBar().RemoveTab(tab.ID)
551+ a.mainWindow.TabBar().SetActive(a.tabs.ActiveTabID)
551 }552 }
552 a.removeBrowserWindow(created.id)553 a.removeBrowserWindow(created.id)
553 if created.mainWindow != nil {554 if created.mainWindow != nil {
M internal/ui/app_browser_launch_test.go +37 -7
@@ -5,10 +5,12 @@ import (
55 "io"
66 "reflect"
77 "testing"
8+ "unsafe"
89
910 "github.com/bnema/dumber/internal/application/port"
1011 "github.com/bnema/dumber/internal/application/usecase"
1112 "github.com/bnema/dumber/internal/domain/entity"
13+ "github.com/bnema/dumber/internal/ui/component"
1214 "github.com/bnema/dumber/internal/ui/coordinator"
1315 "github.com/bnema/dumber/internal/ui/layout"
1416 "github.com/bnema/dumber/internal/ui/window"
@@ -72,6 +74,25 @@ func tabCoordinatorMainWindowPtr(t *testing.T, tc *coordinator.TabCoordinator) u
7274 return fv.Pointer()
7375 }
7476
77+func setWindowTabBar(t *testing.T, mw *window.MainWindow, tabBar *component.TabBar) {
78+ t.Helper()
79+
80+ rv := reflect.ValueOf(mw).Elem()
81+ fv := rv.FieldByName("tabBar")
82+ if !fv.IsValid() {
83+ t.Fatalf("MainWindow missing tabBar field")
84+ }
85+ reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().Set(reflect.ValueOf(tabBar))
86+}
87+
88+func windowTabBarActiveID(t *testing.T, mw *window.MainWindow) entity.TabID {
89+ t.Helper()
90+ if mw == nil || mw.TabBar() == nil {
91+ return ""
92+ }
93+ return mw.TabBar().ActiveTabID()
94+}
95+
7596 type fakeWidgetFactory struct{}
7697
7798 func (fakeWidgetFactory) NewPaned(layout.Orientation) layout.PanedWidget { return nil }
@@ -149,6 +170,12 @@ func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T
149170
150171 func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) {
151172 created := &browserWindow{id: "window-1"}
173+ originalWindow := &window.MainWindow{}
174+ tabBar := &component.TabBar{}
175+ setWindowTabBar(t, originalWindow, tabBar)
176+ existingTabID := entity.TabID("existing-tab")
177+ staleTabID := entity.TabID("stale-tab")
178+ tabBar.SetActive(staleTabID)
152179 app := &App{
153180 tabs: entity.NewTabList(),
154181 tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }),
@@ -156,14 +183,14 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) {
156183 browserWindowFactory: func(context.Context, string) (*browserWindow, error) {
157184 return created, nil
158185 },
159- tabCoord: coordinator.NewTabCoordinator(context.Background(), coordinator.TabCoordinatorConfig{
160- TabsUC: usecase.NewManageTabsUseCase(func() string { return "id-2" }),
161- Tabs: nil,
162- MainWindow: &window.MainWindow{},
163- }),
164- widgetFactory: fakeWidgetFactory{},
186+ mainWindow: originalWindow,
165187 }
166- app.tabs.Add(entity.NewTab(entity.TabID("existing-tab"), entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane"))))
188+ app.tabs.Add(entity.NewTab(existingTabID, entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane"))))
189+ app.tabCoord = coordinator.NewTabCoordinator(context.Background(), coordinator.TabCoordinatorConfig{
190+ TabsUC: app.tabsUC,
191+ Tabs: app.tabs,
192+ MainWindow: &window.MainWindow{},
193+ })
167194
168195 if err := app.OpenFreshWindow(context.Background(), "https://example.com/fail"); err == nil {
169196 t.Fatalf("OpenFreshWindow = nil error, want failure")
@@ -174,4 +201,7 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) {
174201 if got := windowForTabCount(t, app); got != 0 {
175202 t.Fatalf("windowForTab length = %d, want 0", got)
176203 }
204+ if got := windowTabBarActiveID(t, originalWindow); got != existingTabID {
205+ t.Fatalf("tab bar active tab = %q, want %q", got, existingTabID)
206+ }
177207 }
@@ -5,10 +5,12 @@ import (
5 "io"5 "io"
6 "reflect"6 "reflect"
7 "testing"7 "testing"
8+ "unsafe"
8 9
9 "github.com/bnema/dumber/internal/application/port"10 "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/application/usecase"11 "github.com/bnema/dumber/internal/application/usecase"
11 "github.com/bnema/dumber/internal/domain/entity"12 "github.com/bnema/dumber/internal/domain/entity"
13+ "github.com/bnema/dumber/internal/ui/component"
12 "github.com/bnema/dumber/internal/ui/coordinator"14 "github.com/bnema/dumber/internal/ui/coordinator"
13 "github.com/bnema/dumber/internal/ui/layout"15 "github.com/bnema/dumber/internal/ui/layout"
14 "github.com/bnema/dumber/internal/ui/window"16 "github.com/bnema/dumber/internal/ui/window"
@@ -72,6 +74,25 @@ func tabCoordinatorMainWindowPtr(t *testing.T, tc *coordinator.TabCoordinator) u
72 return fv.Pointer()74 return fv.Pointer()
73 }75 }
74 76
77+func setWindowTabBar(t *testing.T, mw *window.MainWindow, tabBar *component.TabBar) {
78+ t.Helper()
79+
80+ rv := reflect.ValueOf(mw).Elem()
81+ fv := rv.FieldByName("tabBar")
82+ if !fv.IsValid() {
83+ t.Fatalf("MainWindow missing tabBar field")
84+ }
85+ reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().Set(reflect.ValueOf(tabBar))
86+}
87+
88+func windowTabBarActiveID(t *testing.T, mw *window.MainWindow) entity.TabID {
89+ t.Helper()
90+ if mw == nil || mw.TabBar() == nil {
91+ return ""
92+ }
93+ return mw.TabBar().ActiveTabID()
94+}
95+
75 type fakeWidgetFactory struct{}96 type fakeWidgetFactory struct{}
76 97
77 func (fakeWidgetFactory) NewPaned(layout.Orientation) layout.PanedWidget { return nil }98 func (fakeWidgetFactory) NewPaned(layout.Orientation) layout.PanedWidget { return nil }
@@ -149,6 +170,12 @@ func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T
149 170
150 func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) {171 func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) {
151 created := &browserWindow{id: "window-1"}172 created := &browserWindow{id: "window-1"}
173+ originalWindow := &window.MainWindow{}
174+ tabBar := &component.TabBar{}
175+ setWindowTabBar(t, originalWindow, tabBar)
176+ existingTabID := entity.TabID("existing-tab")
177+ staleTabID := entity.TabID("stale-tab")
178+ tabBar.SetActive(staleTabID)
152 app := &App{179 app := &App{
153 tabs: entity.NewTabList(),180 tabs: entity.NewTabList(),
154 tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }),181 tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }),
@@ -156,14 +183,14 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) {
156 browserWindowFactory: func(context.Context, string) (*browserWindow, error) {183 browserWindowFactory: func(context.Context, string) (*browserWindow, error) {
157 return created, nil184 return created, nil
158 },185 },
159- tabCoord: coordinator.NewTabCoordinator(context.Background(), coordinator.TabCoordinatorConfig{186+ mainWindow: originalWindow,
160- TabsUC: usecase.NewManageTabsUseCase(func() string { return "id-2" }),
161- Tabs: nil,
162- MainWindow: &window.MainWindow{},
163- }),
164- widgetFactory: fakeWidgetFactory{},
165 }187 }
166- app.tabs.Add(entity.NewTab(entity.TabID("existing-tab"), entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane"))))188+ app.tabs.Add(entity.NewTab(existingTabID, entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane"))))
189+ app.tabCoord = coordinator.NewTabCoordinator(context.Background(), coordinator.TabCoordinatorConfig{
190+ TabsUC: app.tabsUC,
191+ Tabs: app.tabs,
192+ MainWindow: &window.MainWindow{},
193+ })
167 194
168 if err := app.OpenFreshWindow(context.Background(), "https://example.com/fail"); err == nil {195 if err := app.OpenFreshWindow(context.Background(), "https://example.com/fail"); err == nil {
169 t.Fatalf("OpenFreshWindow = nil error, want failure")196 t.Fatalf("OpenFreshWindow = nil error, want failure")
@@ -174,4 +201,7 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) {
174 if got := windowForTabCount(t, app); got != 0 {201 if got := windowForTabCount(t, app); got != 0 {
175 t.Fatalf("windowForTab length = %d, want 0", got)202 t.Fatalf("windowForTab length = %d, want 0", got)
176 }203 }
204+ if got := windowTabBarActiveID(t, originalWindow); got != existingTabID {
205+ t.Fatalf("tab bar active tab = %q, want %q", got, existingTabID)
206+ }
177 }207 }

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