detached fece6d9

fix(ui): reduce browser window cleanup complexity

commit
fece6d9b9b720bbeddfd7fe2deeb533e14066530
author
bnema · · 2026-06-17 20:15 UTC
parent
52415e5
signature
unsigned
1 file · +90 -53
M internal/ui/browser_window.go +90 -53
@@ -325,81 +325,118 @@ func (a *App) removeBrowserWindow(id string) {
325325 return
326326 }
327327 removed := a.browserWindows[id]
328+ wasMainWindow := removed != nil && removed.mainWindow == a.mainWindow
329+
330+ a.removeBrowserWindowOrder(id)
331+ a.releaseBrowserWindowTabs(context.Background(), id, removed)
332+ if removed != nil {
333+ removed.teardownForDestroy()
334+ }
335+ if a.contentCoord != nil {
336+ a.contentCoord.ClearPopupNamedContextsForWindow(id)
337+ }
338+ delete(a.browserWindows, id)
328339
329- // Remove from registration order
340+ fallback := a.deterministicBrowserWindowFallback()
341+ a.updateLastFocusedWindowAfterRemoval(id, fallback)
342+ if wasMainWindow {
343+ a.clearMainBrowserWindowAfterRemoval(fallback)
344+ }
345+}
346+
347+func (a *App) removeBrowserWindowOrder(id string) {
330348 for i, wid := range a.browserWindowOrder {
331349 if wid == id {
332350 a.browserWindowOrder = append(a.browserWindowOrder[:i], a.browserWindowOrder[i+1:]...)
333- break
351+ return
334352 }
335353 }
336- wasMainWindow := removed != nil && removed.mainWindow == a.mainWindow
354+}
355+
356+func (a *App) releaseBrowserWindowTabs(ctx context.Context, windowID string, bw *browserWindow) {
357+ ownedTabs := a.collectBrowserWindowTabs(windowID, bw)
358+ for _, tabID := range sortedTabIDs(ownedTabs) {
359+ a.releaseTabWorkspace(ctx, ownedTabs[tabID])
360+ }
361+ a.cleanupStaleTabMappingsForWindow(windowID)
362+}
363+
364+func (a *App) collectBrowserWindowTabs(windowID string, bw *browserWindow) map[entity.TabID]*entity.Tab {
337365 ownedTabs := make(map[entity.TabID]*entity.Tab)
338- if removed != nil && removed.tabs != nil {
339- for _, tab := range removed.tabs.Tabs {
366+ if bw != nil && bw.tabs != nil {
367+ for _, tab := range bw.tabs.Tabs {
340368 if tab != nil {
341369 ownedTabs[tab.ID] = tab
342370 }
343371 }
344372 }
345- for tabID, bw := range a.windowForTab {
346- if bw != nil && bw.id == id {
347- if bw.tabs != nil {
348- if tab := bw.tabs.Find(tabID); tab != nil {
349- ownedTabs[tabID] = tab
350- continue
351- }
352- }
353- if a.tabs != nil {
354- ownedTabs[tabID] = a.tabs.Find(tabID)
355- }
373+ for tabID, owner := range a.windowForTab {
374+ if owner == nil || owner.id != windowID {
375+ continue
376+ }
377+ if tab := tabFromBrowserWindow(owner, tabID); tab != nil {
378+ ownedTabs[tabID] = tab
379+ continue
380+ }
381+ if a.tabs != nil {
382+ ownedTabs[tabID] = a.tabs.Find(tabID)
356383 }
357384 }
358- ownedTabIDs := make([]entity.TabID, 0, len(ownedTabs))
359- for tabID := range ownedTabs {
360- ownedTabIDs = append(ownedTabIDs, tabID)
385+ return ownedTabs
386+}
387+
388+func tabFromBrowserWindow(bw *browserWindow, tabID entity.TabID) *entity.Tab {
389+ if bw == nil || bw.tabs == nil {
390+ return nil
361391 }
362- sort.Slice(ownedTabIDs, func(i, j int) bool { return ownedTabIDs[i] < ownedTabIDs[j] })
363- for _, tabID := range ownedTabIDs {
364- a.releaseTabWorkspace(context.Background(), ownedTabs[tabID])
392+ return bw.tabs.Find(tabID)
393+}
394+
395+func sortedTabIDs(tabs map[entity.TabID]*entity.Tab) []entity.TabID {
396+ ids := make([]entity.TabID, 0, len(tabs))
397+ for tabID := range tabs {
398+ ids = append(ids, tabID)
365399 }
400+ sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
401+ return ids
402+}
403+
404+func (a *App) cleanupStaleTabMappingsForWindow(windowID string) {
366405 for tabID, bw := range a.windowForTab {
367- if bw != nil && bw.id == id {
368- delete(a.workspaceViews, tabID)
369- delete(a.windowForTab, tabID)
370- if a.tabs != nil && a.tabs.Find(tabID) != nil {
371- a.tabs.Remove(tabID)
372- }
406+ if bw == nil || bw.id != windowID {
407+ continue
408+ }
409+ delete(a.workspaceViews, tabID)
410+ delete(a.windowForTab, tabID)
411+ if a.tabs != nil && a.tabs.Find(tabID) != nil {
412+ a.tabs.Remove(tabID)
373413 }
374414 }
375- if removed != nil {
376- removed.teardownForDestroy()
415+}
416+
417+func (a *App) updateLastFocusedWindowAfterRemoval(removedID string, fallback *browserWindow) {
418+ if a.lastFocusedWindowID != removedID {
419+ return
377420 }
378- if a.contentCoord != nil {
379- a.contentCoord.ClearPopupNamedContextsForWindow(id)
421+ if fallback != nil {
422+ a.lastFocusedWindowID = fallback.id
423+ return
380424 }
381- delete(a.browserWindows, id)
382- fallback := a.deterministicBrowserWindowFallback()
383- if a.lastFocusedWindowID == id {
384- if fallback != nil {
385- a.lastFocusedWindowID = fallback.id
386- } else {
387- a.lastFocusedWindowID = ""
388- }
425+ a.lastFocusedWindowID = ""
426+}
427+
428+func (a *App) clearMainBrowserWindowAfterRemoval(fallback *browserWindow) {
429+ a.clearResizeModeBorder()
430+ if fallback != nil {
431+ a.activateBrowserWindow(fallback)
432+ return
389433 }
390- if wasMainWindow {
391- a.clearResizeModeBorder()
392- if fallback != nil {
393- a.activateBrowserWindow(fallback)
394- return
395- }
396- a.lastFocusedWindowID = ""
397- a.mainWindow = nil
398- a.keyboardHandler = nil
399- a.globalShortcutHandler = nil
400- if a.tabCoord != nil {
401- a.tabCoord.SetCurrentTarget(coordinator.TabTarget{})
402- }
434+ a.lastFocusedWindowID = ""
435+ a.mainWindow = nil
436+ a.keyboardHandler = nil
437+ a.globalShortcutHandler = nil
438+ if a.tabCoord != nil {
439+ a.tabCoord.SetCurrentTarget(coordinator.TabTarget{})
403440 }
404441 }
405442
@@ -325,81 +325,118 @@ func (a *App) removeBrowserWindow(id string) {
325 return325 return
326 }326 }
327 removed := a.browserWindows[id]327 removed := a.browserWindows[id]
328+ wasMainWindow := removed != nil && removed.mainWindow == a.mainWindow
329+
330+ a.removeBrowserWindowOrder(id)
331+ a.releaseBrowserWindowTabs(context.Background(), id, removed)
332+ if removed != nil {
333+ removed.teardownForDestroy()
334+ }
335+ if a.contentCoord != nil {
336+ a.contentCoord.ClearPopupNamedContextsForWindow(id)
337+ }
338+ delete(a.browserWindows, id)
328 339
329- // Remove from registration order340+ fallback := a.deterministicBrowserWindowFallback()
341+ a.updateLastFocusedWindowAfterRemoval(id, fallback)
342+ if wasMainWindow {
343+ a.clearMainBrowserWindowAfterRemoval(fallback)
344+ }
345+}
346+
347+func (a *App) removeBrowserWindowOrder(id string) {
330 for i, wid := range a.browserWindowOrder {348 for i, wid := range a.browserWindowOrder {
331 if wid == id {349 if wid == id {
332 a.browserWindowOrder = append(a.browserWindowOrder[:i], a.browserWindowOrder[i+1:]...)350 a.browserWindowOrder = append(a.browserWindowOrder[:i], a.browserWindowOrder[i+1:]...)
333- break351+ return
334 }352 }
335 }353 }
336- wasMainWindow := removed != nil && removed.mainWindow == a.mainWindow354+}
355+
356+func (a *App) releaseBrowserWindowTabs(ctx context.Context, windowID string, bw *browserWindow) {
357+ ownedTabs := a.collectBrowserWindowTabs(windowID, bw)
358+ for _, tabID := range sortedTabIDs(ownedTabs) {
359+ a.releaseTabWorkspace(ctx, ownedTabs[tabID])
360+ }
361+ a.cleanupStaleTabMappingsForWindow(windowID)
362+}
363+
364+func (a *App) collectBrowserWindowTabs(windowID string, bw *browserWindow) map[entity.TabID]*entity.Tab {
337 ownedTabs := make(map[entity.TabID]*entity.Tab)365 ownedTabs := make(map[entity.TabID]*entity.Tab)
338- if removed != nil && removed.tabs != nil {366+ if bw != nil && bw.tabs != nil {
339- for _, tab := range removed.tabs.Tabs {367+ for _, tab := range bw.tabs.Tabs {
340 if tab != nil {368 if tab != nil {
341 ownedTabs[tab.ID] = tab369 ownedTabs[tab.ID] = tab
342 }370 }
343 }371 }
344 }372 }
345- for tabID, bw := range a.windowForTab {373+ for tabID, owner := range a.windowForTab {
346- if bw != nil && bw.id == id {374+ if owner == nil || owner.id != windowID {
347- if bw.tabs != nil {375+ continue
348- if tab := bw.tabs.Find(tabID); tab != nil {376+ }
349- ownedTabs[tabID] = tab377+ if tab := tabFromBrowserWindow(owner, tabID); tab != nil {
350- continue378+ ownedTabs[tabID] = tab
351- }379+ continue
352- }380+ }
353- if a.tabs != nil {381+ if a.tabs != nil {
354- ownedTabs[tabID] = a.tabs.Find(tabID)382+ ownedTabs[tabID] = a.tabs.Find(tabID)
355- }
356 }383 }
357 }384 }
358- ownedTabIDs := make([]entity.TabID, 0, len(ownedTabs))385+ return ownedTabs
359- for tabID := range ownedTabs {386+}
360- ownedTabIDs = append(ownedTabIDs, tabID)387+
388+func tabFromBrowserWindow(bw *browserWindow, tabID entity.TabID) *entity.Tab {
389+ if bw == nil || bw.tabs == nil {
390+ return nil
361 }391 }
362- sort.Slice(ownedTabIDs, func(i, j int) bool { return ownedTabIDs[i] < ownedTabIDs[j] })392+ return bw.tabs.Find(tabID)
363- for _, tabID := range ownedTabIDs {393+}
364- a.releaseTabWorkspace(context.Background(), ownedTabs[tabID])394+
395+func sortedTabIDs(tabs map[entity.TabID]*entity.Tab) []entity.TabID {
396+ ids := make([]entity.TabID, 0, len(tabs))
397+ for tabID := range tabs {
398+ ids = append(ids, tabID)
365 }399 }
400+ sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
401+ return ids
402+}
403+
404+func (a *App) cleanupStaleTabMappingsForWindow(windowID string) {
366 for tabID, bw := range a.windowForTab {405 for tabID, bw := range a.windowForTab {
367- if bw != nil && bw.id == id {406+ if bw == nil || bw.id != windowID {
368- delete(a.workspaceViews, tabID)407+ continue
369- delete(a.windowForTab, tabID)408+ }
370- if a.tabs != nil && a.tabs.Find(tabID) != nil {409+ delete(a.workspaceViews, tabID)
371- a.tabs.Remove(tabID)410+ delete(a.windowForTab, tabID)
372- }411+ if a.tabs != nil && a.tabs.Find(tabID) != nil {
412+ a.tabs.Remove(tabID)
373 }413 }
374 }414 }
375- if removed != nil {415+}
376- removed.teardownForDestroy()416+
417+func (a *App) updateLastFocusedWindowAfterRemoval(removedID string, fallback *browserWindow) {
418+ if a.lastFocusedWindowID != removedID {
419+ return
377 }420 }
378- if a.contentCoord != nil {421+ if fallback != nil {
379- a.contentCoord.ClearPopupNamedContextsForWindow(id)422+ a.lastFocusedWindowID = fallback.id
423+ return
380 }424 }
381- delete(a.browserWindows, id)425+ a.lastFocusedWindowID = ""
382- fallback := a.deterministicBrowserWindowFallback()426+}
383- if a.lastFocusedWindowID == id {427+
384- if fallback != nil {428+func (a *App) clearMainBrowserWindowAfterRemoval(fallback *browserWindow) {
385- a.lastFocusedWindowID = fallback.id429+ a.clearResizeModeBorder()
386- } else {430+ if fallback != nil {
387- a.lastFocusedWindowID = ""431+ a.activateBrowserWindow(fallback)
388- }432+ return
389 }433 }
390- if wasMainWindow {434+ a.lastFocusedWindowID = ""
391- a.clearResizeModeBorder()435+ a.mainWindow = nil
392- if fallback != nil {436+ a.keyboardHandler = nil
393- a.activateBrowserWindow(fallback)437+ a.globalShortcutHandler = nil
394- return438+ if a.tabCoord != nil {
395- }439+ a.tabCoord.SetCurrentTarget(coordinator.TabTarget{})
396- a.lastFocusedWindowID = ""
397- a.mainWindow = nil
398- a.keyboardHandler = nil
399- a.globalShortcutHandler = nil
400- if a.tabCoord != nil {
401- a.tabCoord.SetCurrentTarget(coordinator.TabTarget{})
402- }
403 }440 }
404 }441 }
405 442

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