detached 2f5a6d2

fix: restore stacked pane callbacks

Rewire stacked-pane activation handlers when rebuilding a session so restored stack title bars remain clickable.
commit
2f5a6d256fbaeabeff94aa46e1879ebf3383360e
author
brice · · 2026-04-15 09:53 UTC
parent
f439770
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +93 -0
M internal/ui/app.go +4 -0
@@ -1692,6 +1692,10 @@ func (a *App) restoreSession(ctx context.Context, sessionID entity.SessionID) er
16921692 tabBar := a.mainWindow.TabBar()
16931693 for _, tab := range a.tabs.Tabs {
16941694 a.createWorkspaceViewWithoutAttach(ctx, tab)
1695+ wsView := a.workspaceViews[tab.ID]
1696+ if a.wsCoord != nil && wsView != nil {
1697+ a.wsCoord.SetupStackedPaneCallbacks(ctx, tab.Workspace, wsView)
1698+ }
16951699 if tabBar != nil {
16961700 tabBar.AddTab(tab)
16971701 }
@@ -1692,6 +1692,10 @@ func (a *App) restoreSession(ctx context.Context, sessionID entity.SessionID) er
1692 tabBar := a.mainWindow.TabBar()1692 tabBar := a.mainWindow.TabBar()
1693 for _, tab := range a.tabs.Tabs {1693 for _, tab := range a.tabs.Tabs {
1694 a.createWorkspaceViewWithoutAttach(ctx, tab)1694 a.createWorkspaceViewWithoutAttach(ctx, tab)
1695+ wsView := a.workspaceViews[tab.ID]
1696+ if a.wsCoord != nil && wsView != nil {
1697+ a.wsCoord.SetupStackedPaneCallbacks(ctx, tab.Workspace, wsView)
1698+ }
1695 if tabBar != nil {1699 if tabBar != nil {
1696 tabBar.AddTab(tab)1700 tabBar.AddTab(tab)
1697 }1701 }
M internal/ui/app_browser_launch_test.go +89 -0
@@ -162,6 +162,20 @@ func windowTabBarVisible(t *testing.T, mw *window.MainWindow) bool {
162162 return mw.TabBar().Box().GetVisible()
163163 }
164164
165+func stackedViewOnActivateIsNil(t *testing.T, sv *layout.StackedView) bool {
166+ t.Helper()
167+ if sv == nil {
168+ return true
169+ }
170+
171+ rv := reflect.ValueOf(sv).Elem()
172+ fv := rv.FieldByName("onActivate")
173+ if !fv.IsValid() {
174+ t.Fatalf("StackedView missing onActivate field")
175+ }
176+ return reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().IsNil()
177+}
178+
165179 func newTestShellToaster(t *testing.T) (*component.Toaster, *layoutmocks.MockBoxWidget, *layoutmocks.MockLabelWidget) {
166180 t.Helper()
167181
@@ -329,6 +343,81 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi
329343 }
330344 }
331345
346+func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) {
347+ EnsureAdwaitaInitialized()
348+ appID := AppID
349+ gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
350+ if gtkApp == nil {
351+ t.Fatal("gtk application creation failed")
352+ }
353+ defer gtkApp.Unref()
354+
355+ mainWindow, err := window.New(context.Background(), gtkApp, "top")
356+ if err != nil {
357+ t.Fatalf("window creation failed: %v", err)
358+ }
359+ defer mainWindow.Destroy()
360+
361+ stackedSessionID := entity.SessionID("session-stacked-restore")
362+ stackedTabs := entity.NewTabList()
363+ stackedTabs.Add(entity.NewTab(entity.TabID("restored-tab"), entity.WorkspaceID("restored-workspace"), entity.NewPane(entity.PaneID("restored-pane-1"))))
364+ stackedTabs.Tabs[0].Workspace = &entity.Workspace{
365+ ID: entity.WorkspaceID("restored-workspace"),
366+ Root: &entity.PaneNode{
367+ ID: "restored-stack-root",
368+ IsStacked: true,
369+ Children: []*entity.PaneNode{
370+ {ID: "restored-child-1", Pane: entity.NewPane(entity.PaneID("restored-pane-1"))},
371+ {ID: "restored-child-2", Pane: entity.NewPane(entity.PaneID("restored-pane-2"))},
372+ },
373+ ActiveStackIndex: 0,
374+ },
375+ ActivePaneID: entity.PaneID("restored-pane-1"),
376+ }
377+
378+ app := &App{
379+ deps: &Dependencies{
380+ Config: &config.Config{},
381+ SessionStateRepo: &fakeSessionStateRepo{state: entity.SnapshotFromTabList(stackedSessionID, stackedTabs)},
382+ },
383+ mainWindow: mainWindow,
384+ widgetFactory: layout.NewGtkWidgetFactory(),
385+ contentCoord: &contentcoord.Coordinator{},
386+ wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}),
387+ tabs: entity.NewTabList(),
388+ browserWindows: map[string]*browserWindow{},
389+ workspaceViews: map[entity.TabID]*component.WorkspaceView{},
390+ windowForTab: map[entity.TabID]*browserWindow{},
391+ }
392+
393+ if err := app.restoreSession(context.Background(), stackedSessionID); err != nil {
394+ t.Fatalf("restoreSession returned error: %v", err)
395+ }
396+
397+ restoredTab := app.tabs.ActiveTab()
398+ if restoredTab == nil {
399+ t.Fatal("restored active tab missing")
400+ }
401+ wv := app.workspaceViews[restoredTab.ID]
402+ if wv == nil {
403+ t.Fatal("restored workspace view missing")
404+ }
405+ tr := wv.TreeRenderer()
406+ if tr == nil {
407+ t.Fatal("restored tree renderer missing")
408+ }
409+ if restoredTab.Workspace == nil || restoredTab.Workspace.Root == nil || len(restoredTab.Workspace.Root.Children) == 0 {
410+ t.Fatal("restored stacked workspace missing children")
411+ }
412+ stackedView := tr.GetStackedViewForPane(string(restoredTab.Workspace.Root.Children[0].Pane.ID))
413+ if stackedView == nil {
414+ t.Fatal("restored stacked view missing")
415+ }
416+ if stackedViewOnActivateIsNil(t, stackedView) {
417+ t.Fatal("restored stacked view onActivate callback is nil")
418+ }
419+}
420+
332421 func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T) {
333422 oldWindow := &browserWindow{id: "window-1", mainWindow: &window.MainWindow{}}
334423 newWindow := &browserWindow{id: "window-2", mainWindow: &window.MainWindow{}}
@@ -162,6 +162,20 @@ func windowTabBarVisible(t *testing.T, mw *window.MainWindow) bool {
162 return mw.TabBar().Box().GetVisible()162 return mw.TabBar().Box().GetVisible()
163 }163 }
164 164
165+func stackedViewOnActivateIsNil(t *testing.T, sv *layout.StackedView) bool {
166+ t.Helper()
167+ if sv == nil {
168+ return true
169+ }
170+
171+ rv := reflect.ValueOf(sv).Elem()
172+ fv := rv.FieldByName("onActivate")
173+ if !fv.IsValid() {
174+ t.Fatalf("StackedView missing onActivate field")
175+ }
176+ return reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().IsNil()
177+}
178+
165 func newTestShellToaster(t *testing.T) (*component.Toaster, *layoutmocks.MockBoxWidget, *layoutmocks.MockLabelWidget) {179 func newTestShellToaster(t *testing.T) (*component.Toaster, *layoutmocks.MockBoxWidget, *layoutmocks.MockLabelWidget) {
166 t.Helper()180 t.Helper()
167 181
@@ -329,6 +343,81 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi
329 }343 }
330 }344 }
331 345
346+func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) {
347+ EnsureAdwaitaInitialized()
348+ appID := AppID
349+ gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
350+ if gtkApp == nil {
351+ t.Fatal("gtk application creation failed")
352+ }
353+ defer gtkApp.Unref()
354+
355+ mainWindow, err := window.New(context.Background(), gtkApp, "top")
356+ if err != nil {
357+ t.Fatalf("window creation failed: %v", err)
358+ }
359+ defer mainWindow.Destroy()
360+
361+ stackedSessionID := entity.SessionID("session-stacked-restore")
362+ stackedTabs := entity.NewTabList()
363+ stackedTabs.Add(entity.NewTab(entity.TabID("restored-tab"), entity.WorkspaceID("restored-workspace"), entity.NewPane(entity.PaneID("restored-pane-1"))))
364+ stackedTabs.Tabs[0].Workspace = &entity.Workspace{
365+ ID: entity.WorkspaceID("restored-workspace"),
366+ Root: &entity.PaneNode{
367+ ID: "restored-stack-root",
368+ IsStacked: true,
369+ Children: []*entity.PaneNode{
370+ {ID: "restored-child-1", Pane: entity.NewPane(entity.PaneID("restored-pane-1"))},
371+ {ID: "restored-child-2", Pane: entity.NewPane(entity.PaneID("restored-pane-2"))},
372+ },
373+ ActiveStackIndex: 0,
374+ },
375+ ActivePaneID: entity.PaneID("restored-pane-1"),
376+ }
377+
378+ app := &App{
379+ deps: &Dependencies{
380+ Config: &config.Config{},
381+ SessionStateRepo: &fakeSessionStateRepo{state: entity.SnapshotFromTabList(stackedSessionID, stackedTabs)},
382+ },
383+ mainWindow: mainWindow,
384+ widgetFactory: layout.NewGtkWidgetFactory(),
385+ contentCoord: &contentcoord.Coordinator{},
386+ wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}),
387+ tabs: entity.NewTabList(),
388+ browserWindows: map[string]*browserWindow{},
389+ workspaceViews: map[entity.TabID]*component.WorkspaceView{},
390+ windowForTab: map[entity.TabID]*browserWindow{},
391+ }
392+
393+ if err := app.restoreSession(context.Background(), stackedSessionID); err != nil {
394+ t.Fatalf("restoreSession returned error: %v", err)
395+ }
396+
397+ restoredTab := app.tabs.ActiveTab()
398+ if restoredTab == nil {
399+ t.Fatal("restored active tab missing")
400+ }
401+ wv := app.workspaceViews[restoredTab.ID]
402+ if wv == nil {
403+ t.Fatal("restored workspace view missing")
404+ }
405+ tr := wv.TreeRenderer()
406+ if tr == nil {
407+ t.Fatal("restored tree renderer missing")
408+ }
409+ if restoredTab.Workspace == nil || restoredTab.Workspace.Root == nil || len(restoredTab.Workspace.Root.Children) == 0 {
410+ t.Fatal("restored stacked workspace missing children")
411+ }
412+ stackedView := tr.GetStackedViewForPane(string(restoredTab.Workspace.Root.Children[0].Pane.ID))
413+ if stackedView == nil {
414+ t.Fatal("restored stacked view missing")
415+ }
416+ if stackedViewOnActivateIsNil(t, stackedView) {
417+ t.Fatal("restored stacked view onActivate callback is nil")
418+ }
419+}
420+
332 func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T) {421 func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T) {
333 oldWindow := &browserWindow{id: "window-1", mainWindow: &window.MainWindow{}}422 oldWindow := &browserWindow{id: "window-1", mainWindow: &window.MainWindow{}}
334 newWindow := &browserWindow{id: "window-2", mainWindow: &window.MainWindow{}}423 newWindow := &browserWindow{id: "window-2", mainWindow: &window.MainWindow{}}

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