detached 42e6c82

refactor(ui): move runtime config state out of app

commit
42e6c82f8ebacebbfd415df053ad469ac02b6580
author
brice · · 2026-06-23 16:32 UTC
committer
brice · · 2026-06-23 20:01 UTC
parent
a0ed55e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +334 -230
M internal/ui/app.go +57 -66
@@ -78,9 +78,7 @@ type App struct {
7878 gtkApp *gtk.Application
7979 mainWindow *window.MainWindow
8080
81- runtimeConfig port.RuntimeConfigSnapshot
82- runtimeConfigLoaded bool
83- popupBrowsingContexts entity.BrowsingContextConfig
81+ runtimeConfig *runtimeConfigState
8482
8583 browserWindows map[string]*browserWindow
8684 browserWindowOrder []string // registration order of window IDs
@@ -185,20 +183,17 @@ func New(deps *Dependencies) (*App, error) {
185183 return nil, err
186184 }
187185 ctx, cancel := context.WithCancelCause(deps.Ctx)
188- initialRuntimeConfig := deps.RuntimeConfig.Current()
189186
190187 app := &App{
191- deps: deps,
192- runtimeConfig: initialRuntimeConfig,
193- runtimeConfigLoaded: true,
194- popupBrowsingContexts: initialRuntimeConfig.UI.Workspace.BrowsingContexts,
195- tabs: entity.NewTabList(),
196- tabsUC: deps.TabsUC,
197- panesUC: deps.PanesUC,
198- workspaceViews: make(map[entity.TabID]*component.WorkspaceView),
199- windowForTab: make(map[entity.TabID]*browserWindow),
200- floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),
201- browserWindows: make(map[string]*browserWindow),
188+ deps: deps,
189+ runtimeConfig: newRuntimeConfigState(deps.RuntimeConfig),
190+ tabs: entity.NewTabList(),
191+ tabsUC: deps.TabsUC,
192+ panesUC: deps.PanesUC,
193+ workspaceViews: make(map[entity.TabID]*component.WorkspaceView),
194+ windowForTab: make(map[entity.TabID]*browserWindow),
195+ floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),
196+ browserWindows: make(map[string]*browserWindow),
202197 dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
203198 if fn != nil {
204199 fn()
@@ -264,8 +259,8 @@ func (a *App) runtimeConfigSnapshot() port.RuntimeConfigSnapshot {
264259 if a == nil {
265260 return port.RuntimeConfigSnapshot{}
266261 }
267- if a.runtimeConfigLoaded {
268- return a.runtimeConfig
262+ if a.runtimeConfig != nil {
263+ return a.runtimeConfig.Current()
269264 }
270265 if a.deps != nil && a.deps.RuntimeConfig != nil {
271266 return a.deps.RuntimeConfig.Current()
@@ -273,30 +268,6 @@ func (a *App) runtimeConfigSnapshot() port.RuntimeConfigSnapshot {
273268 return port.RuntimeConfigSnapshot{}
274269 }
275270
276-func (a *App) updateRuntimeConfig(snapshot port.RuntimeConfigSnapshot) {
277- if a != nil {
278- a.runtimeConfig = snapshot
279- a.runtimeConfigLoaded = true
280- a.updatePopupRuntimeConfig(snapshot.UI.Workspace.BrowsingContexts)
281- }
282-}
283-
284-func (a *App) updatePopupRuntimeConfig(cfg entity.BrowsingContextConfig) {
285- if a != nil {
286- a.popupBrowsingContexts = cfg
287- }
288-}
289-
290-func (a *App) popupBrowsingContextConfig() *entity.BrowsingContextConfig {
291- if a == nil {
292- return nil
293- }
294- if !a.runtimeConfigLoaded {
295- a.updatePopupRuntimeConfig(a.runtimeConfigSnapshot().UI.Workspace.BrowsingContexts)
296- }
297- return &a.popupBrowsingContexts
298-}
299-
300271 // Run starts the GTK application and blocks until it exits.
301272 // Returns the exit code.
302273 func (a *App) Run(ctx context.Context, args []string) int {
@@ -3394,7 +3365,7 @@ func (a *App) initCoordinators(ctx context.Context) {
33943365 }
33953366 a.contentCoord.SetPopupConfig(
33963367 a.engine.Factory(),
3397- a.popupBrowsingContextConfig(),
3368+ &runtimeCfg.Workspace.BrowsingContexts,
33983369 a.generateID,
33993370 )
34003371 a.contentCoord.SetPopupWindowIDResolver(func(paneID entity.PaneID) (string, bool) {
@@ -5115,35 +5086,55 @@ func (a *App) initConfigWatcher(ctx context.Context) {
51155086
51165087 // Hot-reload appearance and keybindings on config change.
51175088 a.deps.RuntimeConfig.OnChange(func(snapshot port.RuntimeConfigSnapshot) {
5118- cb := glib.SourceFunc(func(_ uintptr) bool {
5119- a.updateRuntimeConfig(snapshot)
5120- runtimeCfg := snapshot.UI
5121- workspaceCfg := runtimeCfg.Workspace
5122- sessionCfg := runtimeCfg.Session
5123- a.syncExternalThemeWatcher(ctx)
5124- a.applyAppearanceConfig(ctx)
5125- for _, bw := range a.browserWindows {
5126- if bw == nil {
5127- continue
5128- }
5129- // Reapply sidebar width from live config (reloads after sidebar_width
5130- // changes in the config file).
5131- bw.applySidebarWidthConfig(a)
5132- if bw.keyboardHandler != nil {
5133- bw.keyboardHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
5134- }
5135- if bw.globalShortcutHandler != nil {
5136- bw.globalShortcutHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
5137- }
5138- }
5139- return false
5140- })
5141- glib.IdleAdd(&cb, 0)
5089+ a.dispatchRuntimeConfigChange(ctx, snapshot)
51425090 })
51435091
51445092 log.Debug().Msg("config watcher initialized")
51455093 }
51465094
5095+func (a *App) dispatchRuntimeConfigChange(ctx context.Context, snapshot port.RuntimeConfigSnapshot) {
5096+ run := func() {
5097+ a.applyRuntimeConfigChange(ctx, snapshot)
5098+ }
5099+ if a != nil && a.dispatchOnMainThread != nil {
5100+ a.dispatchOnMainThread("ui.runtime_config_reload", run)
5101+ return
5102+ }
5103+ run()
5104+}
5105+
5106+func (a *App) applyRuntimeConfigChange(ctx context.Context, snapshot port.RuntimeConfigSnapshot) {
5107+ if a == nil {
5108+ return
5109+ }
5110+ if a.runtimeConfig == nil {
5111+ a.runtimeConfig = newRuntimeConfigState(nil)
5112+ }
5113+ a.runtimeConfig.Update(snapshot)
5114+ if a.contentCoord != nil {
5115+ a.contentCoord.UpdatePopupConfig(snapshot.UI.Workspace.BrowsingContexts)
5116+ }
5117+ runtimeCfg := snapshot.UI
5118+ workspaceCfg := runtimeCfg.Workspace
5119+ sessionCfg := runtimeCfg.Session
5120+ a.syncExternalThemeWatcher(ctx)
5121+ a.applyAppearanceConfig(ctx)
5122+ for _, bw := range a.browserWindows {
5123+ if bw == nil {
5124+ continue
5125+ }
5126+ // Reapply sidebar width from live config (reloads after sidebar_width
5127+ // changes in the config file).
5128+ bw.applySidebarWidthConfig(a)
5129+ if bw.keyboardHandler != nil {
5130+ bw.keyboardHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
5131+ }
5132+ if bw.globalShortcutHandler != nil {
5133+ bw.globalShortcutHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
5134+ }
5135+ }
5136+}
5137+
51475138 func (a *App) syncExternalThemeWatcher(ctx context.Context) {
51485139 if a == nil || a.deps == nil || a.deps.ExternalThemeWatcher == nil {
51495140 return
@@ -78,9 +78,7 @@ type App struct {
78 gtkApp *gtk.Application78 gtkApp *gtk.Application
79 mainWindow *window.MainWindow79 mainWindow *window.MainWindow
80 80
81- runtimeConfig port.RuntimeConfigSnapshot81+ runtimeConfig *runtimeConfigState
82- runtimeConfigLoaded bool
83- popupBrowsingContexts entity.BrowsingContextConfig
84 82
85 browserWindows map[string]*browserWindow83 browserWindows map[string]*browserWindow
86 browserWindowOrder []string // registration order of window IDs84 browserWindowOrder []string // registration order of window IDs
@@ -185,20 +183,17 @@ func New(deps *Dependencies) (*App, error) {
185 return nil, err183 return nil, err
186 }184 }
187 ctx, cancel := context.WithCancelCause(deps.Ctx)185 ctx, cancel := context.WithCancelCause(deps.Ctx)
188- initialRuntimeConfig := deps.RuntimeConfig.Current()
189 186
190 app := &App{187 app := &App{
191- deps: deps,188+ deps: deps,
192- runtimeConfig: initialRuntimeConfig,189+ runtimeConfig: newRuntimeConfigState(deps.RuntimeConfig),
193- runtimeConfigLoaded: true,190+ tabs: entity.NewTabList(),
194- popupBrowsingContexts: initialRuntimeConfig.UI.Workspace.BrowsingContexts,191+ tabsUC: deps.TabsUC,
195- tabs: entity.NewTabList(),192+ panesUC: deps.PanesUC,
196- tabsUC: deps.TabsUC,193+ workspaceViews: make(map[entity.TabID]*component.WorkspaceView),
197- panesUC: deps.PanesUC,194+ windowForTab: make(map[entity.TabID]*browserWindow),
198- workspaceViews: make(map[entity.TabID]*component.WorkspaceView),195+ floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),
199- windowForTab: make(map[entity.TabID]*browserWindow),196+ browserWindows: make(map[string]*browserWindow),
200- floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),
201- browserWindows: make(map[string]*browserWindow),
202 dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {197 dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
203 if fn != nil {198 if fn != nil {
204 fn()199 fn()
@@ -264,8 +259,8 @@ func (a *App) runtimeConfigSnapshot() port.RuntimeConfigSnapshot {
264 if a == nil {259 if a == nil {
265 return port.RuntimeConfigSnapshot{}260 return port.RuntimeConfigSnapshot{}
266 }261 }
267- if a.runtimeConfigLoaded {262+ if a.runtimeConfig != nil {
268- return a.runtimeConfig263+ return a.runtimeConfig.Current()
269 }264 }
270 if a.deps != nil && a.deps.RuntimeConfig != nil {265 if a.deps != nil && a.deps.RuntimeConfig != nil {
271 return a.deps.RuntimeConfig.Current()266 return a.deps.RuntimeConfig.Current()
@@ -273,30 +268,6 @@ func (a *App) runtimeConfigSnapshot() port.RuntimeConfigSnapshot {
273 return port.RuntimeConfigSnapshot{}268 return port.RuntimeConfigSnapshot{}
274 }269 }
275 270
276-func (a *App) updateRuntimeConfig(snapshot port.RuntimeConfigSnapshot) {
277- if a != nil {
278- a.runtimeConfig = snapshot
279- a.runtimeConfigLoaded = true
280- a.updatePopupRuntimeConfig(snapshot.UI.Workspace.BrowsingContexts)
281- }
282-}
283-
284-func (a *App) updatePopupRuntimeConfig(cfg entity.BrowsingContextConfig) {
285- if a != nil {
286- a.popupBrowsingContexts = cfg
287- }
288-}
289-
290-func (a *App) popupBrowsingContextConfig() *entity.BrowsingContextConfig {
291- if a == nil {
292- return nil
293- }
294- if !a.runtimeConfigLoaded {
295- a.updatePopupRuntimeConfig(a.runtimeConfigSnapshot().UI.Workspace.BrowsingContexts)
296- }
297- return &a.popupBrowsingContexts
298-}
299-
300 // Run starts the GTK application and blocks until it exits.271 // Run starts the GTK application and blocks until it exits.
301 // Returns the exit code.272 // Returns the exit code.
302 func (a *App) Run(ctx context.Context, args []string) int {273 func (a *App) Run(ctx context.Context, args []string) int {
@@ -3394,7 +3365,7 @@ func (a *App) initCoordinators(ctx context.Context) {
3394 }3365 }
3395 a.contentCoord.SetPopupConfig(3366 a.contentCoord.SetPopupConfig(
3396 a.engine.Factory(),3367 a.engine.Factory(),
3397- a.popupBrowsingContextConfig(),3368+ &runtimeCfg.Workspace.BrowsingContexts,
3398 a.generateID,3369 a.generateID,
3399 )3370 )
3400 a.contentCoord.SetPopupWindowIDResolver(func(paneID entity.PaneID) (string, bool) {3371 a.contentCoord.SetPopupWindowIDResolver(func(paneID entity.PaneID) (string, bool) {
@@ -5115,35 +5086,55 @@ func (a *App) initConfigWatcher(ctx context.Context) {
5115 5086
5116 // Hot-reload appearance and keybindings on config change.5087 // Hot-reload appearance and keybindings on config change.
5117 a.deps.RuntimeConfig.OnChange(func(snapshot port.RuntimeConfigSnapshot) {5088 a.deps.RuntimeConfig.OnChange(func(snapshot port.RuntimeConfigSnapshot) {
5118- cb := glib.SourceFunc(func(_ uintptr) bool {5089+ a.dispatchRuntimeConfigChange(ctx, snapshot)
5119- a.updateRuntimeConfig(snapshot)
5120- runtimeCfg := snapshot.UI
5121- workspaceCfg := runtimeCfg.Workspace
5122- sessionCfg := runtimeCfg.Session
5123- a.syncExternalThemeWatcher(ctx)
5124- a.applyAppearanceConfig(ctx)
5125- for _, bw := range a.browserWindows {
5126- if bw == nil {
5127- continue
5128- }
5129- // Reapply sidebar width from live config (reloads after sidebar_width
5130- // changes in the config file).
5131- bw.applySidebarWidthConfig(a)
5132- if bw.keyboardHandler != nil {
5133- bw.keyboardHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
5134- }
5135- if bw.globalShortcutHandler != nil {
5136- bw.globalShortcutHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
5137- }
5138- }
5139- return false
5140- })
5141- glib.IdleAdd(&cb, 0)
5142 })5090 })
5143 5091
5144 log.Debug().Msg("config watcher initialized")5092 log.Debug().Msg("config watcher initialized")
5145 }5093 }
5146 5094
5095+func (a *App) dispatchRuntimeConfigChange(ctx context.Context, snapshot port.RuntimeConfigSnapshot) {
5096+ run := func() {
5097+ a.applyRuntimeConfigChange(ctx, snapshot)
5098+ }
5099+ if a != nil && a.dispatchOnMainThread != nil {
5100+ a.dispatchOnMainThread("ui.runtime_config_reload", run)
5101+ return
5102+ }
5103+ run()
5104+}
5105+
5106+func (a *App) applyRuntimeConfigChange(ctx context.Context, snapshot port.RuntimeConfigSnapshot) {
5107+ if a == nil {
5108+ return
5109+ }
5110+ if a.runtimeConfig == nil {
5111+ a.runtimeConfig = newRuntimeConfigState(nil)
5112+ }
5113+ a.runtimeConfig.Update(snapshot)
5114+ if a.contentCoord != nil {
5115+ a.contentCoord.UpdatePopupConfig(snapshot.UI.Workspace.BrowsingContexts)
5116+ }
5117+ runtimeCfg := snapshot.UI
5118+ workspaceCfg := runtimeCfg.Workspace
5119+ sessionCfg := runtimeCfg.Session
5120+ a.syncExternalThemeWatcher(ctx)
5121+ a.applyAppearanceConfig(ctx)
5122+ for _, bw := range a.browserWindows {
5123+ if bw == nil {
5124+ continue
5125+ }
5126+ // Reapply sidebar width from live config (reloads after sidebar_width
5127+ // changes in the config file).
5128+ bw.applySidebarWidthConfig(a)
5129+ if bw.keyboardHandler != nil {
5130+ bw.keyboardHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
5131+ }
5132+ if bw.globalShortcutHandler != nil {
5133+ bw.globalShortcutHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
5134+ }
5135+ }
5136+}
5137+
5147 func (a *App) syncExternalThemeWatcher(ctx context.Context) {5138 func (a *App) syncExternalThemeWatcher(ctx context.Context) {
5148 if a == nil || a.deps == nil || a.deps.ExternalThemeWatcher == nil {5139 if a == nil || a.deps == nil || a.deps.ExternalThemeWatcher == nil {
5149 return5140 return
M internal/ui/app_browser_launch_test.go +65 -90
@@ -1023,8 +1023,7 @@ func TestApp_CheckConfigMigrationUsesLastFocusedBrowserWindowToaster(t *testing.
10231023 deps: &Dependencies{
10241024 MigrationChecker: migrator,
10251025 },
1026- runtimeConfig: runtimeConfigSnapshotForTest(cfg),
1027- runtimeConfigLoaded: true,
1026+ runtimeConfig: runtimeConfigStateForTest(cfg),
10281027 browserWindows: map[string]*browserWindow{bw.id: bw},
10291028 lastFocusedWindowID: bw.id,
10301029 }
@@ -1136,8 +1135,7 @@ func TestApp_RestoreSessionHandlesEmptyWindowSnapshotAsSuccess(t *testing.T) {
11361135 deps: &Dependencies{
11371136 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
11381137 },
1139- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1140- runtimeConfigLoaded: true,
1138+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
11411139 tabs: entity.NewTabList(),
11421140 browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
11431141 lastFocusedWindowID: staleWindow.id,
@@ -1178,8 +1176,7 @@ func TestApp_CreateInitialTabNoFallbackAfterEmptyWindowRestore(t *testing.T) {
11781176 InitialURL: "https://fallback.example",
11791177 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
11801178 },
1181- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1182- runtimeConfigLoaded: true,
1179+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
11831180 tabs: entity.NewTabList(),
11841181 tabsUC: tabsUC,
11851182 tabCoord: coordinator.NewTabCoordinator(ctx, coordinator.TabCoordinatorConfig{TabsUC: tabsUC, Tabs: entity.NewTabList()}),
@@ -1219,14 +1216,13 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi
12191216 deps: &Dependencies{
12201217 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, restoredSessionID, entity.SnapshotFromTabList(restoredSessionID, restoredTabs)),
12211218 },
1222- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1223- runtimeConfigLoaded: true,
1224- mainWindow: mainWindow,
1225- widgetFactory: layout.NewGtkWidgetFactory(),
1226- tabs: entity.NewTabList(),
1227- browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
1228- workspaceViews: map[entity.TabID]*component.WorkspaceView{staleTab.ID: &component.WorkspaceView{}},
1229- windowForTab: map[entity.TabID]*browserWindow{staleTab.ID: staleWindow},
1219+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
1220+ mainWindow: mainWindow,
1221+ widgetFactory: layout.NewGtkWidgetFactory(),
1222+ tabs: entity.NewTabList(),
1223+ browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
1224+ workspaceViews: map[entity.TabID]*component.WorkspaceView{staleTab.ID: &component.WorkspaceView{}},
1225+ windowForTab: map[entity.TabID]*browserWindow{staleTab.ID: staleWindow},
12301226 floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{
12311227 staleSessionKey: {},
12321228 },
@@ -1287,16 +1283,15 @@ func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) {
12871283 deps: &Dependencies{
12881284 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, stackedSessionID, entity.SnapshotFromTabList(stackedSessionID, stackedTabs)),
12891285 },
1290- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1291- runtimeConfigLoaded: true,
1292- mainWindow: mainWindow,
1293- widgetFactory: layout.NewGtkWidgetFactory(),
1294- contentCoord: &contentcoord.Coordinator{},
1295- wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}),
1296- tabs: entity.NewTabList(),
1297- browserWindows: map[string]*browserWindow{},
1298- workspaceViews: map[entity.TabID]*component.WorkspaceView{},
1299- windowForTab: map[entity.TabID]*browserWindow{},
1286+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
1287+ mainWindow: mainWindow,
1288+ widgetFactory: layout.NewGtkWidgetFactory(),
1289+ contentCoord: &contentcoord.Coordinator{},
1290+ wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}),
1291+ tabs: entity.NewTabList(),
1292+ browserWindows: map[string]*browserWindow{},
1293+ workspaceViews: map[entity.TabID]*component.WorkspaceView{},
1294+ windowForTab: map[entity.TabID]*browserWindow{},
13001295 }
13011296
13021297 if err := app.restoreSession(context.Background(), stackedSessionID); err != nil {
@@ -1774,8 +1769,7 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowAnchorsOwningWindow(t *testing.
17741769 tabs: tabs,
17751770 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "new-tab" }),
17761771 deps: &Dependencies{},
1777- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1778- runtimeConfigLoaded: true,
1772+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
17791773 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
17801774 lastFocusedWindowID: second.id,
17811775 }
@@ -1813,8 +1807,7 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowActivatesTargetOwnerForCrossWin
18131807 tabs: tabs,
18141808 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),
18151809 deps: &Dependencies{},
1816- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1817- runtimeConfigLoaded: true,
1810+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
18181811 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
18191812 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second, tab3.ID: second},
18201813 lastFocusedWindowID: first.id,
@@ -1870,8 +1863,7 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowCrossWindowDerivedMirror(t *tes
18701863 tabs: tabs,
18711864 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),
18721865 deps: &Dependencies{},
1873- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1874- runtimeConfigLoaded: true,
1866+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
18751867 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
18761868 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab3.ID: second},
18771869 lastFocusedWindowID: first.id,
@@ -1921,12 +1913,11 @@ func TestApp_InitKeyboardHandlerDoesNotReattachExistingWindowInput(t *testing.T)
19211913 globalShortcutHandler: sentinelShortcutHandler,
19221914 }
19231915 app := &App{
1924- deps: &Dependencies{},
1925- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1926- runtimeConfigLoaded: true,
1927- mainWindow: mainWindow,
1928- browserWindows: map[string]*browserWindow{bw.id: bw},
1929- kbDispatcher: dispatcher.NewKeyboardDispatcher(context.Background(), nil, nil, nil, nil, dispatcher.KeyboardActions{}, func(context.Context) entity.PaneID { return "" }),
1916+ deps: &Dependencies{},
1917+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
1918+ mainWindow: mainWindow,
1919+ browserWindows: map[string]*browserWindow{bw.id: bw},
1920+ kbDispatcher: dispatcher.NewKeyboardDispatcher(context.Background(), nil, nil, nil, nil, dispatcher.KeyboardActions{}, func(context.Context) entity.PaneID { return "" }),
19301921 }
19311922
19321923 app.initKeyboardHandler(context.Background())
@@ -1978,9 +1969,8 @@ func TestApp_UpdateBrowserWindowTabBarVisibilityAutoHidesSingleTabButPreservesAl
19781969 cfg := &config.Config{}
19791970 cfg.Workspace.HideTabBarWhenSingleTab = true
19801971 app := &App{
1981- deps: &Dependencies{},
1982- runtimeConfig: runtimeConfigSnapshotForTest(cfg),
1983- runtimeConfigLoaded: true,
1972+ deps: &Dependencies{},
1973+ runtimeConfig: runtimeConfigStateForTest(cfg),
19841974 }
19851975
19861976 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
@@ -2051,9 +2041,8 @@ func TestApp_UpdateBrowserWindowTabBarVisibilityHonorsHideWhenSingleTabDisabled(
20512041 cfg := &config.Config{}
20522042 cfg.Workspace.HideTabBarWhenSingleTab = false
20532043 app := &App{
2054- deps: &Dependencies{},
2055- runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2056- runtimeConfigLoaded: true,
2044+ deps: &Dependencies{},
2045+ runtimeConfig: runtimeConfigStateForTest(cfg),
20572046 }
20582047
20592048 app.updateBrowserWindowTabBarVisibility(bw)
@@ -2111,9 +2100,8 @@ func TestApp_UpdateBrowserWindowTabBarVisibility_BottomInset(t *testing.T) {
21112100 cfg := &config.Config{}
21122101 cfg.Workspace.HideTabBarWhenSingleTab = true
21132102 app := &App{
2114- deps: &Dependencies{},
2115- runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2116- runtimeConfigLoaded: true,
2103+ deps: &Dependencies{},
2104+ runtimeConfig: runtimeConfigStateForTest(cfg),
21172105 }
21182106
21192107 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
@@ -2171,8 +2159,7 @@ func TestApp_HandlePaneFullscreenChanged_ClearsBottomInset(t *testing.T) {
21712159 app := &App{
21722160 tabs: tabs,
21732161 deps: &Dependencies{},
2174- runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2175- runtimeConfigLoaded: true,
2162+ runtimeConfig: runtimeConfigStateForTest(cfg),
21762163 browserWindows: map[string]*browserWindow{bw.id: bw},
21772164 lastFocusedWindowID: bw.id,
21782165 }
@@ -2291,8 +2278,7 @@ func TestApp_CreatePopupTabUsesParentPaneOwnerWhenFocusIsStale(t *testing.T) {
22912278 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}
22922279 app := &App{
22932280 deps: &Dependencies{},
2294- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
2295- runtimeConfigLoaded: true,
2281+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
22962282 tabs: globalTabs,
22972283 tabsUC: usecase.NewManageTabsUseCase(func() string { return "popup-tab" }),
22982284 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
@@ -2353,8 +2339,7 @@ func TestApp_TabCoordinatorCallbacksUseTargetWindowWhenFocusIsStale(t *testing.T
23532339 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}
23542340 app := &App{
23552341 deps: &Dependencies{},
2356- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
2357- runtimeConfigLoaded: true,
2342+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
23582343 tabs: entity.NewTabList(),
23592344 tabsUC: usecase.NewManageTabsUseCase(func() string { return "created-tab" }),
23602345 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
@@ -2987,8 +2972,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexUsesSourceWindow(t *testin
29872972 }
29882973 app := &App{
29892974 deps: &Dependencies{},
2990- runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2991- runtimeConfigLoaded: true,
2975+ runtimeConfig: runtimeConfigStateForTest(cfg),
29922976 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
29932977 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
29942978 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab1.ID: second, secondTab2.ID: second},
@@ -3031,8 +3015,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexCreatesOnlyOneMissingTab(t
30313015 }
30323016 app := &App{
30333017 deps: &Dependencies{},
3034- runtimeConfig: runtimeConfigSnapshotForTest(cfg),
3035- runtimeConfigLoaded: true,
3018+ runtimeConfig: runtimeConfigStateForTest(cfg),
30363019 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
30373020 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
30383021 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},
@@ -3077,12 +3060,11 @@ func TestApp_SwitchBrowserWindowTabIndexNegativeIndexIsIgnored(t *testing.T) {
30773060
30783061 cfg := &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}}
30793062 app := &App{
3080- deps: &Dependencies{},
3081- runtimeConfig: runtimeConfigSnapshotForTest(cfg),
3082- runtimeConfigLoaded: true,
3083- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3084- browserWindows: map[string]*browserWindow{second.id: second},
3085- windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3063+ deps: &Dependencies{},
3064+ runtimeConfig: runtimeConfigStateForTest(cfg),
3065+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3066+ browserWindows: map[string]*browserWindow{second.id: second},
3067+ windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
30863068 }
30873069 app.initTabCoordinator(ctx)
30883070
@@ -3106,8 +3088,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexNegativeDoesNotMutateWindo
31063088 cfg := &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}}
31073089 app := &App{
31083090 deps: &Dependencies{},
3109- runtimeConfig: runtimeConfigSnapshotForTest(cfg),
3110- runtimeConfigLoaded: true,
3091+ runtimeConfig: runtimeConfigStateForTest(cfg),
31113092 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
31123093 browserWindows: map[string]*browserWindow{second.id: second},
31133094 lastFocusedWindowID: "window-1",
@@ -3136,12 +3117,11 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexMissingURLDoesNotCreateTab
31363117 second := &browserWindow{id: "window-2", tabs: secondTabs}
31373118
31383119 app := &App{
3139- deps: &Dependencies{},
3140- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3141- runtimeConfigLoaded: true,
3142- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3143- browserWindows: map[string]*browserWindow{second.id: second},
3144- windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3120+ deps: &Dependencies{},
3121+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3122+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3123+ browserWindows: map[string]*browserWindow{second.id: second},
3124+ windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
31453125 }
31463126 app.initTabCoordinator(ctx)
31473127
@@ -3164,8 +3144,7 @@ func TestApp_SwitchBrowserWindowTabIndexMissingURLDoesNotMutateWindowState(t *te
31643144
31653145 app := &App{
31663146 deps: &Dependencies{},
3167- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3168- runtimeConfigLoaded: true,
3147+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
31693148 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
31703149 browserWindows: map[string]*browserWindow{second.id: second},
31713150 lastFocusedWindowID: "window-1",
@@ -3208,8 +3187,7 @@ func TestApp_WorkspaceOmniboxNavigateUsesOwnerWindow(t *testing.T) {
32083187
32093188 app := &App{
32103189 deps: &Dependencies{},
3211- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3212- runtimeConfigLoaded: true,
3190+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
32133191 widgetFactory: layout.NewGtkWidgetFactory(),
32143192 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
32153193 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second},
@@ -3306,9 +3284,8 @@ func TestApp_RestoreSessionDoesNotLeakStaleWindowsIntoTabMerge(t *testing.T) {
33063284 deps: &Dependencies{
33073285 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
33083286 },
3309- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3310- runtimeConfigLoaded: true,
3311- mainWindow: mainWindow,
3287+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3288+ mainWindow: mainWindow,
33123289 browserWindows: map[string]*browserWindow{
33133290 staleBW1.id: staleBW1,
33143291 staleBW2.id: staleBW2,
@@ -3387,14 +3364,13 @@ func TestApp_RestoreSessionHonorsActiveWindowIndex(t *testing.T) {
33873364 deps: &Dependencies{
33883365 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
33893366 },
3390- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3391- runtimeConfigLoaded: true,
3392- mainWindow: mainWindow,
3393- widgetFactory: layout.NewGtkWidgetFactory(),
3394- browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3395- tabs: entity.NewTabList(),
3396- windowForTab: map[entity.TabID]*browserWindow{},
3397- workspaceViews: map[entity.TabID]*component.WorkspaceView{},
3367+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3368+ mainWindow: mainWindow,
3369+ widgetFactory: layout.NewGtkWidgetFactory(),
3370+ browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3371+ tabs: entity.NewTabList(),
3372+ windowForTab: map[entity.TabID]*browserWindow{},
3373+ workspaceViews: map[entity.TabID]*component.WorkspaceView{},
33983374 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {
33993375 runtimeW1 = &browserWindow{id: "runtime-w1", mainWindow: mainWindow}
34003376 return runtimeW1, nil
@@ -3455,12 +3431,11 @@ func TestApp_RestoreSessionFailsOnAdditionalWindowCreationError(t *testing.T) {
34553431 deps: &Dependencies{
34563432 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
34573433 },
3458- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3459- runtimeConfigLoaded: true,
3460- mainWindow: mainWindow,
3461- browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3462- tabs: entity.NewTabList(),
3463- windowForTab: map[entity.TabID]*browserWindow{},
3434+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3435+ mainWindow: mainWindow,
3436+ browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3437+ tabs: entity.NewTabList(),
3438+ windowForTab: map[entity.TabID]*browserWindow{},
34643439 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {
34653440 return nil, factoryErr
34663441 },
@@ -1023,8 +1023,7 @@ func TestApp_CheckConfigMigrationUsesLastFocusedBrowserWindowToaster(t *testing.
1023 deps: &Dependencies{1023 deps: &Dependencies{
1024 MigrationChecker: migrator,1024 MigrationChecker: migrator,
1025 },1025 },
1026- runtimeConfig: runtimeConfigSnapshotForTest(cfg),1026+ runtimeConfig: runtimeConfigStateForTest(cfg),
1027- runtimeConfigLoaded: true,
1028 browserWindows: map[string]*browserWindow{bw.id: bw},1027 browserWindows: map[string]*browserWindow{bw.id: bw},
1029 lastFocusedWindowID: bw.id,1028 lastFocusedWindowID: bw.id,
1030 }1029 }
@@ -1136,8 +1135,7 @@ func TestApp_RestoreSessionHandlesEmptyWindowSnapshotAsSuccess(t *testing.T) {
1136 deps: &Dependencies{1135 deps: &Dependencies{
1137 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),1136 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
1138 },1137 },
1139- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),1138+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
1140- runtimeConfigLoaded: true,
1141 tabs: entity.NewTabList(),1139 tabs: entity.NewTabList(),
1142 browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},1140 browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
1143 lastFocusedWindowID: staleWindow.id,1141 lastFocusedWindowID: staleWindow.id,
@@ -1178,8 +1176,7 @@ func TestApp_CreateInitialTabNoFallbackAfterEmptyWindowRestore(t *testing.T) {
1178 InitialURL: "https://fallback.example",1176 InitialURL: "https://fallback.example",
1179 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),1177 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
1180 },1178 },
1181- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),1179+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
1182- runtimeConfigLoaded: true,
1183 tabs: entity.NewTabList(),1180 tabs: entity.NewTabList(),
1184 tabsUC: tabsUC,1181 tabsUC: tabsUC,
1185 tabCoord: coordinator.NewTabCoordinator(ctx, coordinator.TabCoordinatorConfig{TabsUC: tabsUC, Tabs: entity.NewTabList()}),1182 tabCoord: coordinator.NewTabCoordinator(ctx, coordinator.TabCoordinatorConfig{TabsUC: tabsUC, Tabs: entity.NewTabList()}),
@@ -1219,14 +1216,13 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi
1219 deps: &Dependencies{1216 deps: &Dependencies{
1220 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, restoredSessionID, entity.SnapshotFromTabList(restoredSessionID, restoredTabs)),1217 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, restoredSessionID, entity.SnapshotFromTabList(restoredSessionID, restoredTabs)),
1221 },1218 },
1222- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),1219+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
1223- runtimeConfigLoaded: true,1220+ mainWindow: mainWindow,
1224- mainWindow: mainWindow,1221+ widgetFactory: layout.NewGtkWidgetFactory(),
1225- widgetFactory: layout.NewGtkWidgetFactory(),1222+ tabs: entity.NewTabList(),
1226- tabs: entity.NewTabList(),1223+ browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
1227- browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},1224+ workspaceViews: map[entity.TabID]*component.WorkspaceView{staleTab.ID: &component.WorkspaceView{}},
1228- workspaceViews: map[entity.TabID]*component.WorkspaceView{staleTab.ID: &component.WorkspaceView{}},1225+ windowForTab: map[entity.TabID]*browserWindow{staleTab.ID: staleWindow},
1229- windowForTab: map[entity.TabID]*browserWindow{staleTab.ID: staleWindow},
1230 floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{1226 floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{
1231 staleSessionKey: {},1227 staleSessionKey: {},
1232 },1228 },
@@ -1287,16 +1283,15 @@ func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) {
1287 deps: &Dependencies{1283 deps: &Dependencies{
1288 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, stackedSessionID, entity.SnapshotFromTabList(stackedSessionID, stackedTabs)),1284 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, stackedSessionID, entity.SnapshotFromTabList(stackedSessionID, stackedTabs)),
1289 },1285 },
1290- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),1286+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
1291- runtimeConfigLoaded: true,1287+ mainWindow: mainWindow,
1292- mainWindow: mainWindow,1288+ widgetFactory: layout.NewGtkWidgetFactory(),
1293- widgetFactory: layout.NewGtkWidgetFactory(),1289+ contentCoord: &contentcoord.Coordinator{},
1294- contentCoord: &contentcoord.Coordinator{},1290+ wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}),
1295- wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}),1291+ tabs: entity.NewTabList(),
1296- tabs: entity.NewTabList(),1292+ browserWindows: map[string]*browserWindow{},
1297- browserWindows: map[string]*browserWindow{},1293+ workspaceViews: map[entity.TabID]*component.WorkspaceView{},
1298- workspaceViews: map[entity.TabID]*component.WorkspaceView{},1294+ windowForTab: map[entity.TabID]*browserWindow{},
1299- windowForTab: map[entity.TabID]*browserWindow{},
1300 }1295 }
1301 1296
1302 if err := app.restoreSession(context.Background(), stackedSessionID); err != nil {1297 if err := app.restoreSession(context.Background(), stackedSessionID); err != nil {
@@ -1774,8 +1769,7 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowAnchorsOwningWindow(t *testing.
1774 tabs: tabs,1769 tabs: tabs,
1775 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "new-tab" }),1770 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "new-tab" }),
1776 deps: &Dependencies{},1771 deps: &Dependencies{},
1777- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),1772+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
1778- runtimeConfigLoaded: true,
1779 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},1773 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
1780 lastFocusedWindowID: second.id,1774 lastFocusedWindowID: second.id,
1781 }1775 }
@@ -1813,8 +1807,7 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowActivatesTargetOwnerForCrossWin
1813 tabs: tabs,1807 tabs: tabs,
1814 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),1808 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),
1815 deps: &Dependencies{},1809 deps: &Dependencies{},
1816- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),1810+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
1817- runtimeConfigLoaded: true,
1818 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},1811 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
1819 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second, tab3.ID: second},1812 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second, tab3.ID: second},
1820 lastFocusedWindowID: first.id,1813 lastFocusedWindowID: first.id,
@@ -1870,8 +1863,7 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowCrossWindowDerivedMirror(t *tes
1870 tabs: tabs,1863 tabs: tabs,
1871 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),1864 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),
1872 deps: &Dependencies{},1865 deps: &Dependencies{},
1873- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),1866+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
1874- runtimeConfigLoaded: true,
1875 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},1867 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
1876 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab3.ID: second},1868 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab3.ID: second},
1877 lastFocusedWindowID: first.id,1869 lastFocusedWindowID: first.id,
@@ -1921,12 +1913,11 @@ func TestApp_InitKeyboardHandlerDoesNotReattachExistingWindowInput(t *testing.T)
1921 globalShortcutHandler: sentinelShortcutHandler,1913 globalShortcutHandler: sentinelShortcutHandler,
1922 }1914 }
1923 app := &App{1915 app := &App{
1924- deps: &Dependencies{},1916+ deps: &Dependencies{},
1925- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),1917+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
1926- runtimeConfigLoaded: true,1918+ mainWindow: mainWindow,
1927- mainWindow: mainWindow,1919+ browserWindows: map[string]*browserWindow{bw.id: bw},
1928- browserWindows: map[string]*browserWindow{bw.id: bw},1920+ kbDispatcher: dispatcher.NewKeyboardDispatcher(context.Background(), nil, nil, nil, nil, dispatcher.KeyboardActions{}, func(context.Context) entity.PaneID { return "" }),
1929- kbDispatcher: dispatcher.NewKeyboardDispatcher(context.Background(), nil, nil, nil, nil, dispatcher.KeyboardActions{}, func(context.Context) entity.PaneID { return "" }),
1930 }1921 }
1931 1922
1932 app.initKeyboardHandler(context.Background())1923 app.initKeyboardHandler(context.Background())
@@ -1978,9 +1969,8 @@ func TestApp_UpdateBrowserWindowTabBarVisibilityAutoHidesSingleTabButPreservesAl
1978 cfg := &config.Config{}1969 cfg := &config.Config{}
1979 cfg.Workspace.HideTabBarWhenSingleTab = true1970 cfg.Workspace.HideTabBarWhenSingleTab = true
1980 app := &App{1971 app := &App{
1981- deps: &Dependencies{},1972+ deps: &Dependencies{},
1982- runtimeConfig: runtimeConfigSnapshotForTest(cfg),1973+ runtimeConfig: runtimeConfigStateForTest(cfg),
1983- runtimeConfigLoaded: true,
1984 }1974 }
1985 1975
1986 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))1976 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
@@ -2051,9 +2041,8 @@ func TestApp_UpdateBrowserWindowTabBarVisibilityHonorsHideWhenSingleTabDisabled(
2051 cfg := &config.Config{}2041 cfg := &config.Config{}
2052 cfg.Workspace.HideTabBarWhenSingleTab = false2042 cfg.Workspace.HideTabBarWhenSingleTab = false
2053 app := &App{2043 app := &App{
2054- deps: &Dependencies{},2044+ deps: &Dependencies{},
2055- runtimeConfig: runtimeConfigSnapshotForTest(cfg),2045+ runtimeConfig: runtimeConfigStateForTest(cfg),
2056- runtimeConfigLoaded: true,
2057 }2046 }
2058 2047
2059 app.updateBrowserWindowTabBarVisibility(bw)2048 app.updateBrowserWindowTabBarVisibility(bw)
@@ -2111,9 +2100,8 @@ func TestApp_UpdateBrowserWindowTabBarVisibility_BottomInset(t *testing.T) {
2111 cfg := &config.Config{}2100 cfg := &config.Config{}
2112 cfg.Workspace.HideTabBarWhenSingleTab = true2101 cfg.Workspace.HideTabBarWhenSingleTab = true
2113 app := &App{2102 app := &App{
2114- deps: &Dependencies{},2103+ deps: &Dependencies{},
2115- runtimeConfig: runtimeConfigSnapshotForTest(cfg),2104+ runtimeConfig: runtimeConfigStateForTest(cfg),
2116- runtimeConfigLoaded: true,
2117 }2105 }
2118 2106
2119 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))2107 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
@@ -2171,8 +2159,7 @@ func TestApp_HandlePaneFullscreenChanged_ClearsBottomInset(t *testing.T) {
2171 app := &App{2159 app := &App{
2172 tabs: tabs,2160 tabs: tabs,
2173 deps: &Dependencies{},2161 deps: &Dependencies{},
2174- runtimeConfig: runtimeConfigSnapshotForTest(cfg),2162+ runtimeConfig: runtimeConfigStateForTest(cfg),
2175- runtimeConfigLoaded: true,
2176 browserWindows: map[string]*browserWindow{bw.id: bw},2163 browserWindows: map[string]*browserWindow{bw.id: bw},
2177 lastFocusedWindowID: bw.id,2164 lastFocusedWindowID: bw.id,
2178 }2165 }
@@ -2291,8 +2278,7 @@ func TestApp_CreatePopupTabUsesParentPaneOwnerWhenFocusIsStale(t *testing.T) {
2291 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}2278 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}
2292 app := &App{2279 app := &App{
2293 deps: &Dependencies{},2280 deps: &Dependencies{},
2294- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),2281+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
2295- runtimeConfigLoaded: true,
2296 tabs: globalTabs,2282 tabs: globalTabs,
2297 tabsUC: usecase.NewManageTabsUseCase(func() string { return "popup-tab" }),2283 tabsUC: usecase.NewManageTabsUseCase(func() string { return "popup-tab" }),
2298 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},2284 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
@@ -2353,8 +2339,7 @@ func TestApp_TabCoordinatorCallbacksUseTargetWindowWhenFocusIsStale(t *testing.T
2353 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}2339 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}
2354 app := &App{2340 app := &App{
2355 deps: &Dependencies{},2341 deps: &Dependencies{},
2356- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),2342+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
2357- runtimeConfigLoaded: true,
2358 tabs: entity.NewTabList(),2343 tabs: entity.NewTabList(),
2359 tabsUC: usecase.NewManageTabsUseCase(func() string { return "created-tab" }),2344 tabsUC: usecase.NewManageTabsUseCase(func() string { return "created-tab" }),
2360 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},2345 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
@@ -2987,8 +2972,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexUsesSourceWindow(t *testin
2987 }2972 }
2988 app := &App{2973 app := &App{
2989 deps: &Dependencies{},2974 deps: &Dependencies{},
2990- runtimeConfig: runtimeConfigSnapshotForTest(cfg),2975+ runtimeConfig: runtimeConfigStateForTest(cfg),
2991- runtimeConfigLoaded: true,
2992 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),2976 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
2993 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},2977 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
2994 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab1.ID: second, secondTab2.ID: second},2978 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab1.ID: second, secondTab2.ID: second},
@@ -3031,8 +3015,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexCreatesOnlyOneMissingTab(t
3031 }3015 }
3032 app := &App{3016 app := &App{
3033 deps: &Dependencies{},3017 deps: &Dependencies{},
3034- runtimeConfig: runtimeConfigSnapshotForTest(cfg),3018+ runtimeConfig: runtimeConfigStateForTest(cfg),
3035- runtimeConfigLoaded: true,
3036 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3019 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3037 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},3020 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
3038 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},3021 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},
@@ -3077,12 +3060,11 @@ func TestApp_SwitchBrowserWindowTabIndexNegativeIndexIsIgnored(t *testing.T) {
3077 3060
3078 cfg := &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}}3061 cfg := &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}}
3079 app := &App{3062 app := &App{
3080- deps: &Dependencies{},3063+ deps: &Dependencies{},
3081- runtimeConfig: runtimeConfigSnapshotForTest(cfg),3064+ runtimeConfig: runtimeConfigStateForTest(cfg),
3082- runtimeConfigLoaded: true,3065+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3083- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3066+ browserWindows: map[string]*browserWindow{second.id: second},
3084- browserWindows: map[string]*browserWindow{second.id: second},3067+ windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3085- windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3086 }3068 }
3087 app.initTabCoordinator(ctx)3069 app.initTabCoordinator(ctx)
3088 3070
@@ -3106,8 +3088,7 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexNegativeDoesNotMutateWindo
3106 cfg := &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}}3088 cfg := &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}}
3107 app := &App{3089 app := &App{
3108 deps: &Dependencies{},3090 deps: &Dependencies{},
3109- runtimeConfig: runtimeConfigSnapshotForTest(cfg),3091+ runtimeConfig: runtimeConfigStateForTest(cfg),
3110- runtimeConfigLoaded: true,
3111 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3092 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3112 browserWindows: map[string]*browserWindow{second.id: second},3093 browserWindows: map[string]*browserWindow{second.id: second},
3113 lastFocusedWindowID: "window-1",3094 lastFocusedWindowID: "window-1",
@@ -3136,12 +3117,11 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexMissingURLDoesNotCreateTab
3136 second := &browserWindow{id: "window-2", tabs: secondTabs}3117 second := &browserWindow{id: "window-2", tabs: secondTabs}
3137 3118
3138 app := &App{3119 app := &App{
3139- deps: &Dependencies{},3120+ deps: &Dependencies{},
3140- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),3121+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3141- runtimeConfigLoaded: true,3122+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3142- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3123+ browserWindows: map[string]*browserWindow{second.id: second},
3143- browserWindows: map[string]*browserWindow{second.id: second},3124+ windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3144- windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3145 }3125 }
3146 app.initTabCoordinator(ctx)3126 app.initTabCoordinator(ctx)
3147 3127
@@ -3164,8 +3144,7 @@ func TestApp_SwitchBrowserWindowTabIndexMissingURLDoesNotMutateWindowState(t *te
3164 3144
3165 app := &App{3145 app := &App{
3166 deps: &Dependencies{},3146 deps: &Dependencies{},
3167- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),3147+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3168- runtimeConfigLoaded: true,
3169 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3148 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3170 browserWindows: map[string]*browserWindow{second.id: second},3149 browserWindows: map[string]*browserWindow{second.id: second},
3171 lastFocusedWindowID: "window-1",3150 lastFocusedWindowID: "window-1",
@@ -3208,8 +3187,7 @@ func TestApp_WorkspaceOmniboxNavigateUsesOwnerWindow(t *testing.T) {
3208 3187
3209 app := &App{3188 app := &App{
3210 deps: &Dependencies{},3189 deps: &Dependencies{},
3211- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),3190+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3212- runtimeConfigLoaded: true,
3213 widgetFactory: layout.NewGtkWidgetFactory(),3191 widgetFactory: layout.NewGtkWidgetFactory(),
3214 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},3192 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
3215 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second},3193 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second},
@@ -3306,9 +3284,8 @@ func TestApp_RestoreSessionDoesNotLeakStaleWindowsIntoTabMerge(t *testing.T) {
3306 deps: &Dependencies{3284 deps: &Dependencies{
3307 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),3285 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
3308 },3286 },
3309- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),3287+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3310- runtimeConfigLoaded: true,3288+ mainWindow: mainWindow,
3311- mainWindow: mainWindow,
3312 browserWindows: map[string]*browserWindow{3289 browserWindows: map[string]*browserWindow{
3313 staleBW1.id: staleBW1,3290 staleBW1.id: staleBW1,
3314 staleBW2.id: staleBW2,3291 staleBW2.id: staleBW2,
@@ -3387,14 +3364,13 @@ func TestApp_RestoreSessionHonorsActiveWindowIndex(t *testing.T) {
3387 deps: &Dependencies{3364 deps: &Dependencies{
3388 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),3365 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
3389 },3366 },
3390- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),3367+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3391- runtimeConfigLoaded: true,3368+ mainWindow: mainWindow,
3392- mainWindow: mainWindow,3369+ widgetFactory: layout.NewGtkWidgetFactory(),
3393- widgetFactory: layout.NewGtkWidgetFactory(),3370+ browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3394- browserWindows: map[string]*browserWindow{firstBW.id: firstBW},3371+ tabs: entity.NewTabList(),
3395- tabs: entity.NewTabList(),3372+ windowForTab: map[entity.TabID]*browserWindow{},
3396- windowForTab: map[entity.TabID]*browserWindow{},3373+ workspaceViews: map[entity.TabID]*component.WorkspaceView{},
3397- workspaceViews: map[entity.TabID]*component.WorkspaceView{},
3398 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {3374 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {
3399 runtimeW1 = &browserWindow{id: "runtime-w1", mainWindow: mainWindow}3375 runtimeW1 = &browserWindow{id: "runtime-w1", mainWindow: mainWindow}
3400 return runtimeW1, nil3376 return runtimeW1, nil
@@ -3455,12 +3431,11 @@ func TestApp_RestoreSessionFailsOnAdditionalWindowCreationError(t *testing.T) {
3455 deps: &Dependencies{3431 deps: &Dependencies{
3456 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),3432 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
3457 },3433 },
3458- runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),3434+ runtimeConfig: runtimeConfigStateForTest(&config.Config{}),
3459- runtimeConfigLoaded: true,3435+ mainWindow: mainWindow,
3460- mainWindow: mainWindow,3436+ browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3461- browserWindows: map[string]*browserWindow{firstBW.id: firstBW},3437+ tabs: entity.NewTabList(),
3462- tabs: entity.NewTabList(),3438+ windowForTab: map[entity.TabID]*browserWindow{},
3463- windowForTab: map[entity.TabID]*browserWindow{},
3464 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {3439 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {
3465 return nil, factoryErr3440 return nil, factoryErr
3466 },3441 },
M internal/ui/browser_window_test.go +27 -30
@@ -137,16 +137,15 @@ func TestTabCoordinator_CloseReleasesClosedTabWorkspaceWebViews(t *testing.T) {
137137
138138 bw := &browserWindow{id: "window-1", tabs: tabs}
139139 app := &App{
140- deps: &Dependencies{},
141- runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),
142- runtimeConfigLoaded: true,
143- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
144- contentCoord: contentCoord,
145- browserWindows: map[string]*browserWindow{bw.id: bw},
146- tabs: entity.NewTabList(),
147- workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}, survivingTab.ID: {}},
148- windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw, survivingTab.ID: bw},
149- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
140+ deps: &Dependencies{},
141+ runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
142+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
143+ contentCoord: contentCoord,
144+ browserWindows: map[string]*browserWindow{bw.id: bw},
145+ tabs: entity.NewTabList(),
146+ workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}, survivingTab.ID: {}},
147+ windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw, survivingTab.ID: bw},
148+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
150149 }
151150 app.tabs.Add(closedTab)
152151 app.tabs.Add(survivingTab)
@@ -182,16 +181,15 @@ func TestTabCoordinator_SwitchDoesNotReleaseWorkspaceWebViews(t *testing.T) {
182181
183182 bw := &browserWindow{id: "window-1", tabs: tabs}
184183 app := &App{
185- deps: &Dependencies{},
186- runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),
187- runtimeConfigLoaded: true,
188- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
189- contentCoord: contentCoord,
190- browserWindows: map[string]*browserWindow{bw.id: bw},
191- tabs: entity.NewTabList(),
192- workspaceViews: map[entity.TabID]*component.WorkspaceView{firstTab.ID: {}, secondTab.ID: {}},
193- windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: bw, secondTab.ID: bw},
194- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
184+ deps: &Dependencies{},
185+ runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
186+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
187+ contentCoord: contentCoord,
188+ browserWindows: map[string]*browserWindow{bw.id: bw},
189+ tabs: entity.NewTabList(),
190+ workspaceViews: map[entity.TabID]*component.WorkspaceView{firstTab.ID: {}, secondTab.ID: {}},
191+ windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: bw, secondTab.ID: bw},
192+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
195193 }
196194 app.tabs.Add(firstTab)
197195 app.tabs.Add(secondTab)
@@ -218,16 +216,15 @@ func TestTabCoordinator_CloseLastTabReleasesWorkspaceBeforeWindowRemoval(t *test
218216
219217 bw := &browserWindow{id: "window-1", tabs: tabs}
220218 app := &App{
221- deps: &Dependencies{},
222- runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),
223- runtimeConfigLoaded: true,
224- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
225- contentCoord: contentCoord,
226- browserWindows: map[string]*browserWindow{bw.id: bw},
227- tabs: entity.NewTabList(),
228- workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}},
229- windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw},
230- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
219+ deps: &Dependencies{},
220+ runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
221+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
222+ contentCoord: contentCoord,
223+ browserWindows: map[string]*browserWindow{bw.id: bw},
224+ tabs: entity.NewTabList(),
225+ workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}},
226+ windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw},
227+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
231228 }
232229 app.tabs.Add(closedTab)
233230 app.initTabCoordinator(ctx)
@@ -137,16 +137,15 @@ func TestTabCoordinator_CloseReleasesClosedTabWorkspaceWebViews(t *testing.T) {
137 137
138 bw := &browserWindow{id: "window-1", tabs: tabs}138 bw := &browserWindow{id: "window-1", tabs: tabs}
139 app := &App{139 app := &App{
140- deps: &Dependencies{},140+ deps: &Dependencies{},
141- runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),141+ runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
142- runtimeConfigLoaded: true,142+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
143- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),143+ contentCoord: contentCoord,
144- contentCoord: contentCoord,144+ browserWindows: map[string]*browserWindow{bw.id: bw},
145- browserWindows: map[string]*browserWindow{bw.id: bw},145+ tabs: entity.NewTabList(),
146- tabs: entity.NewTabList(),146+ workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}, survivingTab.ID: {}},
147- workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}, survivingTab.ID: {}},147+ windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw, survivingTab.ID: bw},
148- windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw, survivingTab.ID: bw},148+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
149- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
150 }149 }
151 app.tabs.Add(closedTab)150 app.tabs.Add(closedTab)
152 app.tabs.Add(survivingTab)151 app.tabs.Add(survivingTab)
@@ -182,16 +181,15 @@ func TestTabCoordinator_SwitchDoesNotReleaseWorkspaceWebViews(t *testing.T) {
182 181
183 bw := &browserWindow{id: "window-1", tabs: tabs}182 bw := &browserWindow{id: "window-1", tabs: tabs}
184 app := &App{183 app := &App{
185- deps: &Dependencies{},184+ deps: &Dependencies{},
186- runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),185+ runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
187- runtimeConfigLoaded: true,186+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
188- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),187+ contentCoord: contentCoord,
189- contentCoord: contentCoord,188+ browserWindows: map[string]*browserWindow{bw.id: bw},
190- browserWindows: map[string]*browserWindow{bw.id: bw},189+ tabs: entity.NewTabList(),
191- tabs: entity.NewTabList(),190+ workspaceViews: map[entity.TabID]*component.WorkspaceView{firstTab.ID: {}, secondTab.ID: {}},
192- workspaceViews: map[entity.TabID]*component.WorkspaceView{firstTab.ID: {}, secondTab.ID: {}},191+ windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: bw, secondTab.ID: bw},
193- windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: bw, secondTab.ID: bw},192+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
194- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
195 }193 }
196 app.tabs.Add(firstTab)194 app.tabs.Add(firstTab)
197 app.tabs.Add(secondTab)195 app.tabs.Add(secondTab)
@@ -218,16 +216,15 @@ func TestTabCoordinator_CloseLastTabReleasesWorkspaceBeforeWindowRemoval(t *test
218 216
219 bw := &browserWindow{id: "window-1", tabs: tabs}217 bw := &browserWindow{id: "window-1", tabs: tabs}
220 app := &App{218 app := &App{
221- deps: &Dependencies{},219+ deps: &Dependencies{},
222- runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),220+ runtimeConfig: runtimeConfigStateForTest(config.DefaultConfig()),
223- runtimeConfigLoaded: true,221+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
224- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),222+ contentCoord: contentCoord,
225- contentCoord: contentCoord,223+ browserWindows: map[string]*browserWindow{bw.id: bw},
226- browserWindows: map[string]*browserWindow{bw.id: bw},224+ tabs: entity.NewTabList(),
227- tabs: entity.NewTabList(),225+ workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}},
228- workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}},226+ windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw},
229- windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw},227+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
230- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
231 }228 }
232 app.tabs.Add(closedTab)229 app.tabs.Add(closedTab)
233 app.initTabCoordinator(ctx)230 app.initTabCoordinator(ctx)
M internal/ui/coordinator/content/popup.go +4 -0
@@ -151,6 +151,10 @@ func (c *Coordinator) SetPopupConfig(
151151 c.ensurePopupManager().setConfig(factory, popupConfig, generateID)
152152 }
153153
154+func (c *Coordinator) UpdatePopupConfig(popupConfig entity.BrowsingContextConfig) {
155+ c.ensurePopupManager().updatePopupConfig(popupConfig)
156+}
157+
154158 func (c *Coordinator) SetPopupWindowIDResolver(fn func(entity.PaneID) (string, bool)) {
155159 c.ensurePopupManager().setWindowIDResolver(fn)
156160 }
@@ -151,6 +151,10 @@ func (c *Coordinator) SetPopupConfig(
151 c.ensurePopupManager().setConfig(factory, popupConfig, generateID)151 c.ensurePopupManager().setConfig(factory, popupConfig, generateID)
152 }152 }
153 153
154+func (c *Coordinator) UpdatePopupConfig(popupConfig entity.BrowsingContextConfig) {
155+ c.ensurePopupManager().updatePopupConfig(popupConfig)
156+}
157+
154 func (c *Coordinator) SetPopupWindowIDResolver(fn func(entity.PaneID) (string, bool)) {158 func (c *Coordinator) SetPopupWindowIDResolver(fn func(entity.PaneID) (string, bool)) {
155 c.ensurePopupManager().setWindowIDResolver(fn)159 c.ensurePopupManager().setWindowIDResolver(fn)
156 }160 }
M internal/ui/coordinator/content/popup_manager.go +46 -12
@@ -18,7 +18,8 @@ import (
1818 // leak into the application/usecase or domain layers.
1919 type popupManager struct {
2020 factory port.WebViewFactory
21- popupConfig *entity.BrowsingContextConfig
21+ popupConfig entity.BrowsingContextConfig
22+ hasPopupConfig bool
2223 onInsertPopup func(ctx context.Context, input InsertPopupInput) error
2324 onOpenNativePopup func(ctx context.Context, input NativePopupInput) error
2425 onClosePane func(ctx context.Context, paneID entity.PaneID) error
@@ -103,10 +104,38 @@ func (pm *popupManager) setConfig(
103104 }
104105 pm.ensureInitialized()
105106 pm.factory = factory
106- pm.popupConfig = popupConfig
107+ pm.setPopupConfig(popupConfig)
107108 pm.generatePaneID = generateID
108109 }
109110
111+func (pm *popupManager) setPopupConfig(popupConfig *entity.BrowsingContextConfig) {
112+ if pm == nil {
113+ return
114+ }
115+ if popupConfig == nil {
116+ pm.popupConfig = entity.BrowsingContextConfig{}
117+ pm.hasPopupConfig = false
118+ return
119+ }
120+ pm.popupConfig = *popupConfig
121+ pm.hasPopupConfig = true
122+}
123+
124+func (pm *popupManager) updatePopupConfig(popupConfig entity.BrowsingContextConfig) {
125+ if pm == nil {
126+ return
127+ }
128+ pm.popupConfig = popupConfig
129+ pm.hasPopupConfig = true
130+}
131+
132+func (pm *popupManager) currentPopupConfig() *entity.BrowsingContextConfig {
133+ if pm == nil || !pm.hasPopupConfig {
134+ return nil
135+ }
136+ return &pm.popupConfig
137+}
138+
110139 func (pm *popupManager) setWindowIDResolver(fn func(entity.PaneID) (string, bool)) {
111140 if pm == nil {
112141 return
@@ -409,7 +438,8 @@ func (pm *popupManager) popupParentURIAtOpen(
409438 hooks popupCoordinatorHooks,
410439 targetURI string,
411440 ) string {
412- if pm.popupConfig == nil || !pm.popupConfig.OAuthAutoClose || !IsOAuthURL(targetURI) {
441+ cfg := pm.currentPopupConfig()
442+ if cfg == nil || !cfg.OAuthAutoClose || !IsOAuthURL(targetURI) {
413443 return ""
414444 }
415445 parentURIAtOpen := parentWV.URI()
@@ -439,6 +469,7 @@ func (pm *popupManager) openNativePopup(
439469 log.Error().Err(err).Msg("failed to create webview for native popup")
440470 return nil
441471 }
472+ cfg := pm.currentPopupConfig()
442473 pm.setBrowsingContextDecision(popupWV, decision)
443474 if err := pm.onOpenNativePopup(ctx, NativePopupInput{
444475 ParentPaneID: parentPaneID,
@@ -447,7 +478,7 @@ func (pm *popupManager) openNativePopup(
447478 PopupWebView: popupWV,
448479 TargetURI: req.TargetURI,
449480 Request: req,
450- ObserveOAuthAutoClose: pm.popupConfig != nil && pm.popupConfig.OAuthAutoClose && IsOAuthURL(req.TargetURI),
481+ ObserveOAuthAutoClose: cfg != nil && cfg.OAuthAutoClose && IsOAuthURL(req.TargetURI),
451482 }); err != nil {
452483 log.Error().Err(err).Msg("failed to open native popup host")
453484 popupWV.Destroy()
@@ -476,7 +507,8 @@ func (pm *popupManager) handlePopupCreate(
476507 Bool("no_javascript_access", req.NoJavaScriptAccess).
477508 Msg("popup create request")
478509
479- if pm.popupConfig != nil && !pm.popupConfig.OpenInNewPane {
510+ cfg := pm.currentPopupConfig()
511+ if cfg != nil && !cfg.OpenInNewPane {
480512 log.Debug().Msg("popups disabled by config, blocking")
481513 return nil
482514 }
@@ -534,10 +566,10 @@ func (pm *popupManager) handlePopupCreate(
534566
535567 popupType := DetectPopupType(req.FrameName)
536568 popupID := popupWV.ID()
537- behavior := GetBehavior(popupType, pm.popupConfig)
569+ behavior := GetBehavior(popupType, cfg)
538570 placement := "right"
539- if pm.popupConfig != nil {
540- placement = pm.popupConfig.Placement
571+ if cfg != nil {
572+ placement = cfg.Placement
541573 }
542574 paneID, popupPane := pm.createPopupPane(popupID, parentPaneID, req.TargetURI)
543575
@@ -562,8 +594,9 @@ func (pm *popupManager) finishPopupCreate(
562594 create popupCreateContext,
563595 ) port.WebView {
564596 log := logging.FromContext(ctx)
565- hasConfig := pm.popupConfig != nil
566- oauthEnabled := hasConfig && pm.popupConfig.OAuthAutoClose
597+ cfg := pm.currentPopupConfig()
598+ hasConfig := cfg != nil
599+ oauthEnabled := hasConfig && cfg.OAuthAutoClose
567600 isOAuth := IsOAuthURL(create.Request.TargetURI)
568601
569602 log.Debug().
@@ -792,7 +825,8 @@ func (pm *popupManager) handleLinkMiddleClick(
792825 Str("uri", logging.TruncateURL(uri, logURLMaxLen)).
793826 Msg("middle-click/ctrl+click on link")
794827
795- if pm.popupConfig != nil && !pm.popupConfig.OpenInNewPane {
828+ cfg := pm.currentPopupConfig()
829+ if cfg != nil && !cfg.OpenInNewPane {
796830 log.Debug().Msg("popups disabled by config, ignoring middle-click")
797831 return false
798832 }
@@ -845,7 +879,7 @@ func (pm *popupManager) handleLinkMiddleClick(
845879 hooks.setupWebViewCallbacks(ctx, paneID, newWV)
846880 }
847881
848- behavior, placement := popupTabInsertionConfig(pm.popupConfig)
882+ behavior, placement := popupTabInsertionConfig(pm.currentPopupConfig())
849883
850884 if pm.onInsertPopup != nil {
851885 popupInput := InsertPopupInput{
@@ -18,7 +18,8 @@ import (
18 // leak into the application/usecase or domain layers.18 // leak into the application/usecase or domain layers.
19 type popupManager struct {19 type popupManager struct {
20 factory port.WebViewFactory20 factory port.WebViewFactory
21- popupConfig *entity.BrowsingContextConfig21+ popupConfig entity.BrowsingContextConfig
22+ hasPopupConfig bool
22 onInsertPopup func(ctx context.Context, input InsertPopupInput) error23 onInsertPopup func(ctx context.Context, input InsertPopupInput) error
23 onOpenNativePopup func(ctx context.Context, input NativePopupInput) error24 onOpenNativePopup func(ctx context.Context, input NativePopupInput) error
24 onClosePane func(ctx context.Context, paneID entity.PaneID) error25 onClosePane func(ctx context.Context, paneID entity.PaneID) error
@@ -103,10 +104,38 @@ func (pm *popupManager) setConfig(
103 }104 }
104 pm.ensureInitialized()105 pm.ensureInitialized()
105 pm.factory = factory106 pm.factory = factory
106- pm.popupConfig = popupConfig107+ pm.setPopupConfig(popupConfig)
107 pm.generatePaneID = generateID108 pm.generatePaneID = generateID
108 }109 }
109 110
111+func (pm *popupManager) setPopupConfig(popupConfig *entity.BrowsingContextConfig) {
112+ if pm == nil {
113+ return
114+ }
115+ if popupConfig == nil {
116+ pm.popupConfig = entity.BrowsingContextConfig{}
117+ pm.hasPopupConfig = false
118+ return
119+ }
120+ pm.popupConfig = *popupConfig
121+ pm.hasPopupConfig = true
122+}
123+
124+func (pm *popupManager) updatePopupConfig(popupConfig entity.BrowsingContextConfig) {
125+ if pm == nil {
126+ return
127+ }
128+ pm.popupConfig = popupConfig
129+ pm.hasPopupConfig = true
130+}
131+
132+func (pm *popupManager) currentPopupConfig() *entity.BrowsingContextConfig {
133+ if pm == nil || !pm.hasPopupConfig {
134+ return nil
135+ }
136+ return &pm.popupConfig
137+}
138+
110 func (pm *popupManager) setWindowIDResolver(fn func(entity.PaneID) (string, bool)) {139 func (pm *popupManager) setWindowIDResolver(fn func(entity.PaneID) (string, bool)) {
111 if pm == nil {140 if pm == nil {
112 return141 return
@@ -409,7 +438,8 @@ func (pm *popupManager) popupParentURIAtOpen(
409 hooks popupCoordinatorHooks,438 hooks popupCoordinatorHooks,
410 targetURI string,439 targetURI string,
411 ) string {440 ) string {
412- if pm.popupConfig == nil || !pm.popupConfig.OAuthAutoClose || !IsOAuthURL(targetURI) {441+ cfg := pm.currentPopupConfig()
442+ if cfg == nil || !cfg.OAuthAutoClose || !IsOAuthURL(targetURI) {
413 return ""443 return ""
414 }444 }
415 parentURIAtOpen := parentWV.URI()445 parentURIAtOpen := parentWV.URI()
@@ -439,6 +469,7 @@ func (pm *popupManager) openNativePopup(
439 log.Error().Err(err).Msg("failed to create webview for native popup")469 log.Error().Err(err).Msg("failed to create webview for native popup")
440 return nil470 return nil
441 }471 }
472+ cfg := pm.currentPopupConfig()
442 pm.setBrowsingContextDecision(popupWV, decision)473 pm.setBrowsingContextDecision(popupWV, decision)
443 if err := pm.onOpenNativePopup(ctx, NativePopupInput{474 if err := pm.onOpenNativePopup(ctx, NativePopupInput{
444 ParentPaneID: parentPaneID,475 ParentPaneID: parentPaneID,
@@ -447,7 +478,7 @@ func (pm *popupManager) openNativePopup(
447 PopupWebView: popupWV,478 PopupWebView: popupWV,
448 TargetURI: req.TargetURI,479 TargetURI: req.TargetURI,
449 Request: req,480 Request: req,
450- ObserveOAuthAutoClose: pm.popupConfig != nil && pm.popupConfig.OAuthAutoClose && IsOAuthURL(req.TargetURI),481+ ObserveOAuthAutoClose: cfg != nil && cfg.OAuthAutoClose && IsOAuthURL(req.TargetURI),
451 }); err != nil {482 }); err != nil {
452 log.Error().Err(err).Msg("failed to open native popup host")483 log.Error().Err(err).Msg("failed to open native popup host")
453 popupWV.Destroy()484 popupWV.Destroy()
@@ -476,7 +507,8 @@ func (pm *popupManager) handlePopupCreate(
476 Bool("no_javascript_access", req.NoJavaScriptAccess).507 Bool("no_javascript_access", req.NoJavaScriptAccess).
477 Msg("popup create request")508 Msg("popup create request")
478 509
479- if pm.popupConfig != nil && !pm.popupConfig.OpenInNewPane {510+ cfg := pm.currentPopupConfig()
511+ if cfg != nil && !cfg.OpenInNewPane {
480 log.Debug().Msg("popups disabled by config, blocking")512 log.Debug().Msg("popups disabled by config, blocking")
481 return nil513 return nil
482 }514 }
@@ -534,10 +566,10 @@ func (pm *popupManager) handlePopupCreate(
534 566
535 popupType := DetectPopupType(req.FrameName)567 popupType := DetectPopupType(req.FrameName)
536 popupID := popupWV.ID()568 popupID := popupWV.ID()
537- behavior := GetBehavior(popupType, pm.popupConfig)569+ behavior := GetBehavior(popupType, cfg)
538 placement := "right"570 placement := "right"
539- if pm.popupConfig != nil {571+ if cfg != nil {
540- placement = pm.popupConfig.Placement572+ placement = cfg.Placement
541 }573 }
542 paneID, popupPane := pm.createPopupPane(popupID, parentPaneID, req.TargetURI)574 paneID, popupPane := pm.createPopupPane(popupID, parentPaneID, req.TargetURI)
543 575
@@ -562,8 +594,9 @@ func (pm *popupManager) finishPopupCreate(
562 create popupCreateContext,594 create popupCreateContext,
563 ) port.WebView {595 ) port.WebView {
564 log := logging.FromContext(ctx)596 log := logging.FromContext(ctx)
565- hasConfig := pm.popupConfig != nil597+ cfg := pm.currentPopupConfig()
566- oauthEnabled := hasConfig && pm.popupConfig.OAuthAutoClose598+ hasConfig := cfg != nil
599+ oauthEnabled := hasConfig && cfg.OAuthAutoClose
567 isOAuth := IsOAuthURL(create.Request.TargetURI)600 isOAuth := IsOAuthURL(create.Request.TargetURI)
568 601
569 log.Debug().602 log.Debug().
@@ -792,7 +825,8 @@ func (pm *popupManager) handleLinkMiddleClick(
792 Str("uri", logging.TruncateURL(uri, logURLMaxLen)).825 Str("uri", logging.TruncateURL(uri, logURLMaxLen)).
793 Msg("middle-click/ctrl+click on link")826 Msg("middle-click/ctrl+click on link")
794 827
795- if pm.popupConfig != nil && !pm.popupConfig.OpenInNewPane {828+ cfg := pm.currentPopupConfig()
829+ if cfg != nil && !cfg.OpenInNewPane {
796 log.Debug().Msg("popups disabled by config, ignoring middle-click")830 log.Debug().Msg("popups disabled by config, ignoring middle-click")
797 return false831 return false
798 }832 }
@@ -845,7 +879,7 @@ func (pm *popupManager) handleLinkMiddleClick(
845 hooks.setupWebViewCallbacks(ctx, paneID, newWV)879 hooks.setupWebViewCallbacks(ctx, paneID, newWV)
846 }880 }
847 881
848- behavior, placement := popupTabInsertionConfig(pm.popupConfig)882+ behavior, placement := popupTabInsertionConfig(pm.currentPopupConfig())
849 883
850 if pm.onInsertPopup != nil {884 if pm.onInsertPopup != nil {
851 popupInput := InsertPopupInput{885 popupInput := InsertPopupInput{
M internal/ui/coordinator/content/popup_test.go +22 -2
@@ -719,7 +719,10 @@ func TestSetPopupConfig_SetsFields(t *testing.T) {
719719 c.SetPopupConfig(factory, cfg, genID)
720720
721721 assert.Equal(t, factory, c.popups.factory)
722- assert.Equal(t, cfg, c.popups.popupConfig)
722+ assert.True(t, c.popups.hasPopupConfig)
723+ assert.Equal(t, *cfg, c.popups.popupConfig)
724+ cfg.Behavior = entity.PopupBehaviorWindowed
725+ assert.Equal(t, entity.PopupBehaviorSplit, c.popups.popupConfig.Behavior)
723726 assert.NotNil(t, c.popups.generatePaneID)
724727 assert.Equal(t, "test-id", c.popups.generatePaneID())
725728 }
@@ -731,10 +734,27 @@ func TestSetPopupConfig_NilConfigAllowed(t *testing.T) {
731734 c.SetPopupConfig(nil, nil, nil)
732735
733736 assert.Nil(t, c.popups.factory)
734- assert.Nil(t, c.popups.popupConfig)
737+ assert.False(t, c.popups.hasPopupConfig)
738+ assert.Equal(t, entity.BrowsingContextConfig{}, c.popups.popupConfig)
735739 assert.Nil(t, c.popups.generatePaneID)
736740 }
737741
742+func TestUpdatePopupConfig_CopiesLatestValue(t *testing.T) {
743+ t.Parallel()
744+
745+ c := &Coordinator{}
746+ initial := &entity.BrowsingContextConfig{OpenInNewPane: true, OAuthAutoClose: false}
747+ c.SetPopupConfig(nil, initial, nil)
748+
749+ c.UpdatePopupConfig(entity.BrowsingContextConfig{OpenInNewPane: false, OAuthAutoClose: true})
750+
751+ assert.True(t, c.popups.hasPopupConfig)
752+ assert.False(t, c.popups.popupConfig.OpenInNewPane)
753+ assert.True(t, c.popups.popupConfig.OAuthAutoClose)
754+ assert.True(t, initial.OpenInNewPane)
755+ assert.False(t, initial.OAuthAutoClose)
756+}
757+
738758 type popupNavigationWebViewStub struct {
739759 *mocks.MockWebView
740760 primed []string
@@ -719,7 +719,10 @@ func TestSetPopupConfig_SetsFields(t *testing.T) {
719 c.SetPopupConfig(factory, cfg, genID)719 c.SetPopupConfig(factory, cfg, genID)
720 720
721 assert.Equal(t, factory, c.popups.factory)721 assert.Equal(t, factory, c.popups.factory)
722- assert.Equal(t, cfg, c.popups.popupConfig)722+ assert.True(t, c.popups.hasPopupConfig)
723+ assert.Equal(t, *cfg, c.popups.popupConfig)
724+ cfg.Behavior = entity.PopupBehaviorWindowed
725+ assert.Equal(t, entity.PopupBehaviorSplit, c.popups.popupConfig.Behavior)
723 assert.NotNil(t, c.popups.generatePaneID)726 assert.NotNil(t, c.popups.generatePaneID)
724 assert.Equal(t, "test-id", c.popups.generatePaneID())727 assert.Equal(t, "test-id", c.popups.generatePaneID())
725 }728 }
@@ -731,10 +734,27 @@ func TestSetPopupConfig_NilConfigAllowed(t *testing.T) {
731 c.SetPopupConfig(nil, nil, nil)734 c.SetPopupConfig(nil, nil, nil)
732 735
733 assert.Nil(t, c.popups.factory)736 assert.Nil(t, c.popups.factory)
734- assert.Nil(t, c.popups.popupConfig)737+ assert.False(t, c.popups.hasPopupConfig)
738+ assert.Equal(t, entity.BrowsingContextConfig{}, c.popups.popupConfig)
735 assert.Nil(t, c.popups.generatePaneID)739 assert.Nil(t, c.popups.generatePaneID)
736 }740 }
737 741
742+func TestUpdatePopupConfig_CopiesLatestValue(t *testing.T) {
743+ t.Parallel()
744+
745+ c := &Coordinator{}
746+ initial := &entity.BrowsingContextConfig{OpenInNewPane: true, OAuthAutoClose: false}
747+ c.SetPopupConfig(nil, initial, nil)
748+
749+ c.UpdatePopupConfig(entity.BrowsingContextConfig{OpenInNewPane: false, OAuthAutoClose: true})
750+
751+ assert.True(t, c.popups.hasPopupConfig)
752+ assert.False(t, c.popups.popupConfig.OpenInNewPane)
753+ assert.True(t, c.popups.popupConfig.OAuthAutoClose)
754+ assert.True(t, initial.OpenInNewPane)
755+ assert.False(t, initial.OAuthAutoClose)
756+}
757+
738 type popupNavigationWebViewStub struct {758 type popupNavigationWebViewStub struct {
739 *mocks.MockWebView759 *mocks.MockWebView
740 primed []string760 primed []string
M internal/ui/external_theme_reload_test.go +8 -13
@@ -106,10 +106,9 @@ func TestApplyAppearanceConfigSendsCompleteEngineSettingsPayload(t *testing.T) {
106106 Return(nil).
107107 Once()
108108 app := &App{
109- deps: &Dependencies{},
110- runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
111- runtimeConfigLoaded: true,
112- engine: engine,
109+ deps: &Dependencies{},
110+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(bootstrap.RuntimeConfigSnapshotFromConfig(cfg)),
111+ engine: engine,
113112 }
114113
115114 app.applyAppearanceConfig(ctx)
@@ -151,8 +150,7 @@ func TestExternalThemeWatcherCallbackAppliesThemeThroughSharedPath(t *testing.T)
151150 ExternalThemeSource: source,
152151 ExternalThemeWatcher: watcher,
153152 },
154- runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
155- runtimeConfigLoaded: true,
153+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(bootstrap.RuntimeConfigSnapshotFromConfig(cfg)),
156154 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
157155 engine: engine,
158156 }
@@ -186,8 +184,7 @@ func TestExternalThemeReloadKeepsLastGoodAndDisablingClearsIt(t *testing.T) {
186184 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
187185 ExternalThemeSource: source,
188186 },
189- runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
190- runtimeConfigLoaded: true,
187+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(bootstrap.RuntimeConfigSnapshotFromConfig(cfg)),
191188 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
192189 }
193190
@@ -204,7 +201,7 @@ func TestExternalThemeReloadKeepsLastGoodAndDisablingClearsIt(t *testing.T) {
204201 require.Equal(t, "#000000", app.deps.Theme.GetCurrentPalette().Background)
205202
206203 cfg.Appearance.ExternalTheme.Enabled = false
207- app.updateRuntimeConfig(bootstrap.RuntimeConfigSnapshotFromConfig(cfg))
204+ setRuntimeConfigSnapshotForTest(app, bootstrap.RuntimeConfigSnapshotFromConfig(cfg))
208205 app.applyAppearanceConfig(ctx)
209206 require.Equal(t, cfg.Appearance.DarkPalette.Background, app.deps.Theme.GetCurrentPalette().Background)
210207 }
@@ -227,8 +224,7 @@ func TestExternalThemeReloadKeepsLastGoodOnReadError(t *testing.T) {
227224 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
228225 ExternalThemeSource: source,
229226 },
230- runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
231- runtimeConfigLoaded: true,
227+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(bootstrap.RuntimeConfigSnapshotFromConfig(cfg)),
232228 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
233229 }
234230
@@ -259,8 +255,7 @@ func TestExternalThemeReloadRecoversAfterMalformedUpdate(t *testing.T) {
259255 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
260256 ExternalThemeSource: source,
261257 },
262- runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
263- runtimeConfigLoaded: true,
258+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(bootstrap.RuntimeConfigSnapshotFromConfig(cfg)),
264259 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
265260 }
266261
@@ -106,10 +106,9 @@ func TestApplyAppearanceConfigSendsCompleteEngineSettingsPayload(t *testing.T) {
106 Return(nil).106 Return(nil).
107 Once()107 Once()
108 app := &App{108 app := &App{
109- deps: &Dependencies{},109+ deps: &Dependencies{},
110- runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),110+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(bootstrap.RuntimeConfigSnapshotFromConfig(cfg)),
111- runtimeConfigLoaded: true,111+ engine: engine,
112- engine: engine,
113 }112 }
114 113
115 app.applyAppearanceConfig(ctx)114 app.applyAppearanceConfig(ctx)
@@ -151,8 +150,7 @@ func TestExternalThemeWatcherCallbackAppliesThemeThroughSharedPath(t *testing.T)
151 ExternalThemeSource: source,150 ExternalThemeSource: source,
152 ExternalThemeWatcher: watcher,151 ExternalThemeWatcher: watcher,
153 },152 },
154- runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),153+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(bootstrap.RuntimeConfigSnapshotFromConfig(cfg)),
155- runtimeConfigLoaded: true,
156 dispatchOnMainThread: immediateDispatchForExternalThemeTest,154 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
157 engine: engine,155 engine: engine,
158 }156 }
@@ -186,8 +184,7 @@ func TestExternalThemeReloadKeepsLastGoodAndDisablingClearsIt(t *testing.T) {
186 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),184 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
187 ExternalThemeSource: source,185 ExternalThemeSource: source,
188 },186 },
189- runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),187+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(bootstrap.RuntimeConfigSnapshotFromConfig(cfg)),
190- runtimeConfigLoaded: true,
191 dispatchOnMainThread: immediateDispatchForExternalThemeTest,188 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
192 }189 }
193 190
@@ -204,7 +201,7 @@ func TestExternalThemeReloadKeepsLastGoodAndDisablingClearsIt(t *testing.T) {
204 require.Equal(t, "#000000", app.deps.Theme.GetCurrentPalette().Background)201 require.Equal(t, "#000000", app.deps.Theme.GetCurrentPalette().Background)
205 202
206 cfg.Appearance.ExternalTheme.Enabled = false203 cfg.Appearance.ExternalTheme.Enabled = false
207- app.updateRuntimeConfig(bootstrap.RuntimeConfigSnapshotFromConfig(cfg))204+ setRuntimeConfigSnapshotForTest(app, bootstrap.RuntimeConfigSnapshotFromConfig(cfg))
208 app.applyAppearanceConfig(ctx)205 app.applyAppearanceConfig(ctx)
209 require.Equal(t, cfg.Appearance.DarkPalette.Background, app.deps.Theme.GetCurrentPalette().Background)206 require.Equal(t, cfg.Appearance.DarkPalette.Background, app.deps.Theme.GetCurrentPalette().Background)
210 }207 }
@@ -227,8 +224,7 @@ func TestExternalThemeReloadKeepsLastGoodOnReadError(t *testing.T) {
227 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),224 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
228 ExternalThemeSource: source,225 ExternalThemeSource: source,
229 },226 },
230- runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),227+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(bootstrap.RuntimeConfigSnapshotFromConfig(cfg)),
231- runtimeConfigLoaded: true,
232 dispatchOnMainThread: immediateDispatchForExternalThemeTest,228 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
233 }229 }
234 230
@@ -259,8 +255,7 @@ func TestExternalThemeReloadRecoversAfterMalformedUpdate(t *testing.T) {
259 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),255 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
260 ExternalThemeSource: source,256 ExternalThemeSource: source,
261 },257 },
262- runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),258+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(bootstrap.RuntimeConfigSnapshotFromConfig(cfg)),
263- runtimeConfigLoaded: true,
264 dispatchOnMainThread: immediateDispatchForExternalThemeTest,259 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
265 }260 }
266 261
A internal/ui/runtime_config_state.go +38 -0
@@ -0,0 +1,38 @@
1+package ui
2+
3+import "github.com/bnema/dumber/internal/application/port"
4+
5+type runtimeConfigState struct {
6+ provider port.RuntimeConfigProvider
7+ snapshot port.RuntimeConfigSnapshot
8+ loaded bool
9+}
10+
11+func newRuntimeConfigState(provider port.RuntimeConfigProvider) *runtimeConfigState {
12+ state := &runtimeConfigState{provider: provider}
13+ if provider != nil {
14+ state.Update(provider.Current())
15+ }
16+ return state
17+}
18+
19+func (s *runtimeConfigState) Current() port.RuntimeConfigSnapshot {
20+ if s == nil {
21+ return port.RuntimeConfigSnapshot{}
22+ }
23+ if s.loaded {
24+ return s.snapshot
25+ }
26+ if s.provider != nil {
27+ return s.provider.Current()
28+ }
29+ return port.RuntimeConfigSnapshot{}
30+}
31+
32+func (s *runtimeConfigState) Update(snapshot port.RuntimeConfigSnapshot) {
33+ if s == nil {
34+ return
35+ }
36+ s.snapshot = snapshot
37+ s.loaded = true
38+}
@@ -0,0 +1,38 @@
1+package ui
2+
3+import "github.com/bnema/dumber/internal/application/port"
4+
5+type runtimeConfigState struct {
6+ provider port.RuntimeConfigProvider
7+ snapshot port.RuntimeConfigSnapshot
8+ loaded bool
9+}
10+
11+func newRuntimeConfigState(provider port.RuntimeConfigProvider) *runtimeConfigState {
12+ state := &runtimeConfigState{provider: provider}
13+ if provider != nil {
14+ state.Update(provider.Current())
15+ }
16+ return state
17+}
18+
19+func (s *runtimeConfigState) Current() port.RuntimeConfigSnapshot {
20+ if s == nil {
21+ return port.RuntimeConfigSnapshot{}
22+ }
23+ if s.loaded {
24+ return s.snapshot
25+ }
26+ if s.provider != nil {
27+ return s.provider.Current()
28+ }
29+ return port.RuntimeConfigSnapshot{}
30+}
31+
32+func (s *runtimeConfigState) Update(snapshot port.RuntimeConfigSnapshot) {
33+ if s == nil {
34+ return
35+ }
36+ s.snapshot = snapshot
37+ s.loaded = true
38+}
M internal/ui/runtime_config_test.go +48 -14
@@ -4,10 +4,14 @@ import (
44 "context"
55 "testing"
66
7+ "github.com/stretchr/testify/mock"
8+
79 "github.com/bnema/dumber/internal/application/dto"
810 "github.com/bnema/dumber/internal/application/port"
911 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
1012 "github.com/bnema/dumber/internal/domain/entity"
13+ "github.com/bnema/dumber/internal/shared/syncdispatch"
14+ "github.com/bnema/dumber/internal/ui/coordinator/content"
1115 )
1216
1317 func TestDependenciesValidateAcceptsRuntimeConfigOnly(t *testing.T) {
@@ -49,9 +53,9 @@ func TestAppRuntimeConfigReturnsProviderSnapshot(t *testing.T) {
4953 }
5054 }
5155
52-func TestAppUpdateRuntimeConfigStoresLatestSnapshot(t *testing.T) {
56+func TestAppRuntimeConfigStateStoresLatestSnapshot(t *testing.T) {
5357 app := &App{}
54- app.updateRuntimeConfig(port.RuntimeConfigSnapshot{
58+ setRuntimeConfigSnapshotForTest(app, port.RuntimeConfigSnapshot{
5559 UI: port.RuntimeUIConfig{SidebarWidth: 340},
5660 })
5761
@@ -61,32 +65,62 @@ func TestAppUpdateRuntimeConfigStoresLatestSnapshot(t *testing.T) {
6165 }
6266 }
6367
64-func TestAppUpdateRuntimeConfigRefreshesStablePopupConfigPointer(t *testing.T) {
65- app := &App{}
66- app.updateRuntimeConfig(port.RuntimeConfigSnapshot{
68+func TestConfigWatcherOnChangeUpdatesRuntimeConfigState(t *testing.T) {
69+ ctx := context.Background()
70+ provider := portmocks.NewMockRuntimeConfigProvider(t)
71+ provider.EXPECT().Watch().Return(nil).Once()
72+ var onChange func(port.RuntimeConfigSnapshot)
73+ provider.EXPECT().
74+ OnChange(mock.Anything).
75+ Run(func(fn func(port.RuntimeConfigSnapshot)) {
76+ onChange = fn
77+ }).
78+ Once()
79+
80+ initial := port.RuntimeConfigSnapshot{
6781 UI: port.RuntimeUIConfig{
6882 Workspace: entity.WorkspaceConfig{
6983 BrowsingContexts: entity.BrowsingContextConfig{OpenInNewPane: true},
7084 },
7185 },
72- })
73- ptr := app.popupBrowsingContextConfig()
86+ }
87+ app := &App{
88+ deps: &Dependencies{RuntimeConfig: provider},
89+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(initial),
90+ contentCoord: &content.Coordinator{},
91+ browserWindows: map[string]*browserWindow{},
92+ dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
93+ if label != "ui.runtime_config_reload" {
94+ t.Fatalf("dispatch label=%q, want ui.runtime_config_reload", label)
95+ }
96+ fn()
97+ return syncdispatch.SyncDispatchResult{Label: label, Status: syncdispatch.SyncDispatchCompleted}
98+ },
99+ }
100+ app.contentCoord.SetPopupConfig(nil, &initial.UI.Workspace.BrowsingContexts, nil)
74101
75- app.updateRuntimeConfig(port.RuntimeConfigSnapshot{
102+ app.initConfigWatcher(ctx)
103+ if onChange == nil {
104+ t.Fatal("runtime config OnChange callback was not registered")
105+ }
106+
107+ onChange(port.RuntimeConfigSnapshot{
76108 UI: port.RuntimeUIConfig{
109+ SidebarWidth: 360,
77110 Workspace: entity.WorkspaceConfig{
78111 BrowsingContexts: entity.BrowsingContextConfig{OpenInNewPane: false, OAuthAutoClose: true},
79112 },
80113 },
81114 })
82115
83- if ptr != app.popupBrowsingContextConfig() {
84- t.Fatal("popup config pointer must remain stable for content coordinator")
116+ got := app.runtimeConfigSnapshot()
117+ if got.UI.SidebarWidth != 360 {
118+ t.Fatalf("SidebarWidth=%d, want 360", got.UI.SidebarWidth)
85119 }
86- if ptr.OpenInNewPane {
87- t.Fatal("popup config pointer did not observe updated OpenInNewPane")
120+ if got.UI.Workspace.BrowsingContexts.OpenInNewPane {
121+ t.Fatal("runtime config state did not observe updated popup OpenInNewPane")
88122 }
89- if !ptr.OAuthAutoClose {
90- t.Fatal("popup config pointer did not observe updated OAuthAutoClose")
123+ if !got.UI.Workspace.BrowsingContexts.OAuthAutoClose {
124+ t.Fatal("runtime config state did not observe updated popup OAuthAutoClose")
91125 }
92126 }
@@ -4,10 +4,14 @@ import (
4 "context"4 "context"
5 "testing"5 "testing"
6 6
7+ "github.com/stretchr/testify/mock"
8+
7 "github.com/bnema/dumber/internal/application/dto"9 "github.com/bnema/dumber/internal/application/dto"
8 "github.com/bnema/dumber/internal/application/port"10 "github.com/bnema/dumber/internal/application/port"
9 portmocks "github.com/bnema/dumber/internal/application/port/mocks"11 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
10 "github.com/bnema/dumber/internal/domain/entity"12 "github.com/bnema/dumber/internal/domain/entity"
13+ "github.com/bnema/dumber/internal/shared/syncdispatch"
14+ "github.com/bnema/dumber/internal/ui/coordinator/content"
11 )15 )
12 16
13 func TestDependenciesValidateAcceptsRuntimeConfigOnly(t *testing.T) {17 func TestDependenciesValidateAcceptsRuntimeConfigOnly(t *testing.T) {
@@ -49,9 +53,9 @@ func TestAppRuntimeConfigReturnsProviderSnapshot(t *testing.T) {
49 }53 }
50 }54 }
51 55
52-func TestAppUpdateRuntimeConfigStoresLatestSnapshot(t *testing.T) {56+func TestAppRuntimeConfigStateStoresLatestSnapshot(t *testing.T) {
53 app := &App{}57 app := &App{}
54- app.updateRuntimeConfig(port.RuntimeConfigSnapshot{58+ setRuntimeConfigSnapshotForTest(app, port.RuntimeConfigSnapshot{
55 UI: port.RuntimeUIConfig{SidebarWidth: 340},59 UI: port.RuntimeUIConfig{SidebarWidth: 340},
56 })60 })
57 61
@@ -61,32 +65,62 @@ func TestAppUpdateRuntimeConfigStoresLatestSnapshot(t *testing.T) {
61 }65 }
62 }66 }
63 67
64-func TestAppUpdateRuntimeConfigRefreshesStablePopupConfigPointer(t *testing.T) {68+func TestConfigWatcherOnChangeUpdatesRuntimeConfigState(t *testing.T) {
65- app := &App{}69+ ctx := context.Background()
66- app.updateRuntimeConfig(port.RuntimeConfigSnapshot{70+ provider := portmocks.NewMockRuntimeConfigProvider(t)
71+ provider.EXPECT().Watch().Return(nil).Once()
72+ var onChange func(port.RuntimeConfigSnapshot)
73+ provider.EXPECT().
74+ OnChange(mock.Anything).
75+ Run(func(fn func(port.RuntimeConfigSnapshot)) {
76+ onChange = fn
77+ }).
78+ Once()
79+
80+ initial := port.RuntimeConfigSnapshot{
67 UI: port.RuntimeUIConfig{81 UI: port.RuntimeUIConfig{
68 Workspace: entity.WorkspaceConfig{82 Workspace: entity.WorkspaceConfig{
69 BrowsingContexts: entity.BrowsingContextConfig{OpenInNewPane: true},83 BrowsingContexts: entity.BrowsingContextConfig{OpenInNewPane: true},
70 },84 },
71 },85 },
72- })86+ }
73- ptr := app.popupBrowsingContextConfig()87+ app := &App{
88+ deps: &Dependencies{RuntimeConfig: provider},
89+ runtimeConfig: runtimeConfigStateFromSnapshotForTest(initial),
90+ contentCoord: &content.Coordinator{},
91+ browserWindows: map[string]*browserWindow{},
92+ dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
93+ if label != "ui.runtime_config_reload" {
94+ t.Fatalf("dispatch label=%q, want ui.runtime_config_reload", label)
95+ }
96+ fn()
97+ return syncdispatch.SyncDispatchResult{Label: label, Status: syncdispatch.SyncDispatchCompleted}
98+ },
99+ }
100+ app.contentCoord.SetPopupConfig(nil, &initial.UI.Workspace.BrowsingContexts, nil)
74 101
75- app.updateRuntimeConfig(port.RuntimeConfigSnapshot{102+ app.initConfigWatcher(ctx)
103+ if onChange == nil {
104+ t.Fatal("runtime config OnChange callback was not registered")
105+ }
106+
107+ onChange(port.RuntimeConfigSnapshot{
76 UI: port.RuntimeUIConfig{108 UI: port.RuntimeUIConfig{
109+ SidebarWidth: 360,
77 Workspace: entity.WorkspaceConfig{110 Workspace: entity.WorkspaceConfig{
78 BrowsingContexts: entity.BrowsingContextConfig{OpenInNewPane: false, OAuthAutoClose: true},111 BrowsingContexts: entity.BrowsingContextConfig{OpenInNewPane: false, OAuthAutoClose: true},
79 },112 },
80 },113 },
81 })114 })
82 115
83- if ptr != app.popupBrowsingContextConfig() {116+ got := app.runtimeConfigSnapshot()
84- t.Fatal("popup config pointer must remain stable for content coordinator")117+ if got.UI.SidebarWidth != 360 {
118+ t.Fatalf("SidebarWidth=%d, want 360", got.UI.SidebarWidth)
85 }119 }
86- if ptr.OpenInNewPane {120+ if got.UI.Workspace.BrowsingContexts.OpenInNewPane {
87- t.Fatal("popup config pointer did not observe updated OpenInNewPane")121+ t.Fatal("runtime config state did not observe updated popup OpenInNewPane")
88 }122 }
89- if !ptr.OAuthAutoClose {123+ if !got.UI.Workspace.BrowsingContexts.OAuthAutoClose {
90- t.Fatal("popup config pointer did not observe updated OAuthAutoClose")124+ t.Fatal("runtime config state did not observe updated popup OAuthAutoClose")
91 }125 }
92 }126 }
M internal/ui/runtime_config_test_helpers_test.go +19 -3
@@ -10,10 +10,26 @@ func runtimeConfigSnapshotForTest(cfg *config.Config) port.RuntimeConfigSnapshot
1010 return bootstrap.RuntimeConfigSnapshotFromConfig(cfg)
1111 }
1212
13+func runtimeConfigStateFromSnapshotForTest(snapshot port.RuntimeConfigSnapshot) *runtimeConfigState {
14+ state := newRuntimeConfigState(nil)
15+ state.Update(snapshot)
16+ return state
17+}
18+
19+func runtimeConfigStateForTest(cfg *config.Config) *runtimeConfigState {
20+ return runtimeConfigStateFromSnapshotForTest(runtimeConfigSnapshotForTest(cfg))
21+}
22+
23+func setRuntimeConfigSnapshotForTest(app *App, snapshot port.RuntimeConfigSnapshot) {
24+ if app.runtimeConfig == nil {
25+ app.runtimeConfig = newRuntimeConfigState(nil)
26+ }
27+ app.runtimeConfig.Update(snapshot)
28+}
29+
1330 func appWithRuntimeConfigForTest(cfg *config.Config) *App {
1431 return &App{
15- deps: &Dependencies{},
16- runtimeConfig: runtimeConfigSnapshotForTest(cfg),
17- runtimeConfigLoaded: true,
32+ deps: &Dependencies{},
33+ runtimeConfig: runtimeConfigStateForTest(cfg),
1834 }
1935 }
@@ -10,10 +10,26 @@ func runtimeConfigSnapshotForTest(cfg *config.Config) port.RuntimeConfigSnapshot
10 return bootstrap.RuntimeConfigSnapshotFromConfig(cfg)10 return bootstrap.RuntimeConfigSnapshotFromConfig(cfg)
11 }11 }
12 12
13+func runtimeConfigStateFromSnapshotForTest(snapshot port.RuntimeConfigSnapshot) *runtimeConfigState {
14+ state := newRuntimeConfigState(nil)
15+ state.Update(snapshot)
16+ return state
17+}
18+
19+func runtimeConfigStateForTest(cfg *config.Config) *runtimeConfigState {
20+ return runtimeConfigStateFromSnapshotForTest(runtimeConfigSnapshotForTest(cfg))
21+}
22+
23+func setRuntimeConfigSnapshotForTest(app *App, snapshot port.RuntimeConfigSnapshot) {
24+ if app.runtimeConfig == nil {
25+ app.runtimeConfig = newRuntimeConfigState(nil)
26+ }
27+ app.runtimeConfig.Update(snapshot)
28+}
29+
13 func appWithRuntimeConfigForTest(cfg *config.Config) *App {30 func appWithRuntimeConfigForTest(cfg *config.Config) *App {
14 return &App{31 return &App{
15- deps: &Dependencies{},32+ deps: &Dependencies{},
16- runtimeConfig: runtimeConfigSnapshotForTest(cfg),33+ runtimeConfig: runtimeConfigStateForTest(cfg),
17- runtimeConfigLoaded: true,
18 }34 }
19 }35 }

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