detached 965849a

feat(tabs): integrate tab system with browser architecture

- Initialize TabManager in browser.go during createWebView
- Set tab manager root container as window content
- Add window-level shortcuts for tab mode (Ctrl+T) and actions
- Add direct tab switching with Alt+1-9 (tabs 1-9) and Alt+0 (tab 10)
- Update workspace manager with helper methods:
  - GetAllPanes(), GetActivePane(), RestoreFocus(), GetRootWidget()
- Maintain app-level workspace/panes compatibility references
- Tab manager delegates to active tab's workspace
- Each tab creates embedded WebView (CreateWindow=false)
commit
965849af0ece22cab4ae58d42fc2808f12bf3136
author
bnema · · 2025-11-17 19:41 UTC
parent
1518120
signature
unsigned
4 files · +166 -43
M internal/app/browser/browser.go +23 -20
@@ -48,7 +48,11 @@ type BrowserApp struct {
4848 navigationController *control.NavigationController
4949 clipboardController *control.ClipboardController
5050
51- // Pane workspace management
51+ // Tab and workspace management
52+ tabManager *TabManager
53+
54+ // Convenience accessors (delegate to active tab's workspace)
55+ // These maintain compatibility with existing code
5256 panes []*BrowserPane
5357 activePane *BrowserPane
5458 workspace *WorkspaceManager
@@ -248,8 +252,19 @@ func (app *BrowserApp) Run() {
248252 // Continue without content blocking
249253 }
250254
251- // Handle browse command if present
252- app.navigationController.HandleBrowseCommand()
255+ // Handle browse command if present (must use active tab's navigation controller)
256+ if app.tabManager != nil {
257+ activeTab := app.tabManager.GetActiveTab()
258+ if activeTab != nil && activeTab.workspace != nil {
259+ activePane := activeTab.workspace.GetActivePane()
260+ if activePane != nil && activePane.navigationController != nil {
261+ activePane.navigationController.HandleBrowseCommand()
262+ }
263+ }
264+ } else {
265+ // Fallback to legacy behavior if no tab manager
266+ app.navigationController.HandleBrowseCommand()
267+ }
253268
254269 // Setup signal handling
255270 app.setupSignalHandling()
@@ -269,23 +284,11 @@ func (app *BrowserApp) cleanup() {
269284 app.windowShortcutHandler = nil
270285 }
271286
272- // Cleanup all panes
273- if app.panes != nil {
274- log.Printf("Cleaning up %d panes", len(app.panes))
275- for i, pane := range app.panes {
276- if pane != nil {
277- log.Printf("Cleaning up pane %d (%s)", i, pane.ID())
278- pane.Cleanup()
279- }
280- }
281- app.panes = nil
282- }
283-
284- // Cleanup workspace manager
285- if app.workspace != nil {
286- log.Printf("Cleaning up workspace manager")
287- // WorkspaceManager doesn't have explicit cleanup yet, but we clear the reference
288- app.workspace = nil
287+ // Cleanup tab manager (which cleans up all tabs and their workspaces)
288+ if app.tabManager != nil {
289+ log.Printf("Cleaning up tab manager")
290+ app.tabManager.Cleanup()
291+ app.tabManager = nil
289292 }
290293
291294 // Flush all caches to ensure pending writes complete before database closes
@@ -48,7 +48,11 @@ type BrowserApp struct {
48 navigationController *control.NavigationController48 navigationController *control.NavigationController
49 clipboardController *control.ClipboardController49 clipboardController *control.ClipboardController
50 50
51- // Pane workspace management51+ // Tab and workspace management
52+ tabManager *TabManager
53+
54+ // Convenience accessors (delegate to active tab's workspace)
55+ // These maintain compatibility with existing code
52 panes []*BrowserPane56 panes []*BrowserPane
53 activePane *BrowserPane57 activePane *BrowserPane
54 workspace *WorkspaceManager58 workspace *WorkspaceManager
@@ -248,8 +252,19 @@ func (app *BrowserApp) Run() {
248 // Continue without content blocking252 // Continue without content blocking
249 }253 }
250 254
251- // Handle browse command if present255+ // Handle browse command if present (must use active tab's navigation controller)
252- app.navigationController.HandleBrowseCommand()256+ if app.tabManager != nil {
257+ activeTab := app.tabManager.GetActiveTab()
258+ if activeTab != nil && activeTab.workspace != nil {
259+ activePane := activeTab.workspace.GetActivePane()
260+ if activePane != nil && activePane.navigationController != nil {
261+ activePane.navigationController.HandleBrowseCommand()
262+ }
263+ }
264+ } else {
265+ // Fallback to legacy behavior if no tab manager
266+ app.navigationController.HandleBrowseCommand()
267+ }
253 268
254 // Setup signal handling269 // Setup signal handling
255 app.setupSignalHandling()270 app.setupSignalHandling()
@@ -269,23 +284,11 @@ func (app *BrowserApp) cleanup() {
269 app.windowShortcutHandler = nil284 app.windowShortcutHandler = nil
270 }285 }
271 286
272- // Cleanup all panes287+ // Cleanup tab manager (which cleans up all tabs and their workspaces)
273- if app.panes != nil {288+ if app.tabManager != nil {
274- log.Printf("Cleaning up %d panes", len(app.panes))289+ log.Printf("Cleaning up tab manager")
275- for i, pane := range app.panes {290+ app.tabManager.Cleanup()
276- if pane != nil {291+ app.tabManager = nil
277- log.Printf("Cleaning up pane %d (%s)", i, pane.ID())
278- pane.Cleanup()
279- }
280- }
281- app.panes = nil
282- }
283-
284- // Cleanup workspace manager
285- if app.workspace != nil {
286- log.Printf("Cleaning up workspace manager")
287- // WorkspaceManager doesn't have explicit cleanup yet, but we clear the reference
288- app.workspace = nil
289 }292 }
290 293
291 // Flush all caches to ensure pending writes complete before database closes294 // Flush all caches to ensure pending writes complete before database closes
M internal/app/browser/webview.go +22 -16
@@ -249,28 +249,34 @@ func (app *BrowserApp) createWebView() error {
249249 app.clipboardController = pane.clipboardController
250250 app.messageHandler = pane.messageHandler
251251 app.shortcutHandler = pane.shortcutHandler
252- app.panes = []*BrowserPane{pane}
253- app.activePane = pane
254252
255- if app.workspace == nil {
256- app.workspace = NewWorkspaceManager(app, pane)
257- // CRITICAL: Register navigation handler for the root webview
258- // This is needed because buildPane() couldn't register it when workspace was nil
259- app.workspace.RegisterNavigationHandler(view)
253+ // Initialize tab manager (which manages workspaces)
254+ window := view.Window()
255+ if window != nil && app.tabManager == nil {
256+ app.tabManager = NewTabManager(app, window)
260257
261- // CRITICAL: Setup popup handling for the root webview
262- // This must be done after workspace is created so WebKit's create signal is connected
263- node := app.workspace.GetNodeForWebView(view)
264- if node != nil {
265- app.workspace.setupPopupHandling(view, node)
266- log.Printf("[webview] Setup native popup handling for root WebView ID: %d", view.ID())
258+ // Determine initial URL
259+ initialURL := "about:blank"
260+
261+ // Initialize tab system (creates root container with tab bar and first tab)
262+ if err := app.tabManager.Initialize(initialURL); err != nil {
263+ log.Printf("ERROR: failed to initialize tab manager: %v", err)
264+ return err
265+ }
266+
267+ // Set tab manager's root container as window content
268+ rootContainer := app.tabManager.GetRootContainer()
269+ if rootContainer != nil {
270+ window.SetChild(rootContainer)
271+ log.Printf("[tabs] Tab system initialized and set as window content")
267272 } else {
268- log.Printf("[webview] WARNING: Could not find node for root WebView ID: %d", view.ID())
273+ log.Printf("ERROR: tab manager root container is nil")
274+ return fmt.Errorf("failed to get tab manager root container")
269275 }
270276 }
271277
272- // Initialize window-level global shortcuts AFTER workspace is set up
273- if window := view.Window(); window != nil {
278+ // Initialize window-level global shortcuts AFTER tab manager is set up
279+ if window != nil {
274280 app.windowShortcutHandler = NewWindowShortcutHandler(window, app)
275281 if app.windowShortcutHandler != nil {
276282 log.Printf("Window-level global shortcuts initialized")
@@ -249,28 +249,34 @@ func (app *BrowserApp) createWebView() error {
249 app.clipboardController = pane.clipboardController249 app.clipboardController = pane.clipboardController
250 app.messageHandler = pane.messageHandler250 app.messageHandler = pane.messageHandler
251 app.shortcutHandler = pane.shortcutHandler251 app.shortcutHandler = pane.shortcutHandler
252- app.panes = []*BrowserPane{pane}
253- app.activePane = pane
254 252
255- if app.workspace == nil {253+ // Initialize tab manager (which manages workspaces)
256- app.workspace = NewWorkspaceManager(app, pane)254+ window := view.Window()
257- // CRITICAL: Register navigation handler for the root webview255+ if window != nil && app.tabManager == nil {
258- // This is needed because buildPane() couldn't register it when workspace was nil256+ app.tabManager = NewTabManager(app, window)
259- app.workspace.RegisterNavigationHandler(view)
260 257
261- // CRITICAL: Setup popup handling for the root webview258+ // Determine initial URL
262- // This must be done after workspace is created so WebKit's create signal is connected259+ initialURL := "about:blank"
263- node := app.workspace.GetNodeForWebView(view)260+
264- if node != nil {261+ // Initialize tab system (creates root container with tab bar and first tab)
265- app.workspace.setupPopupHandling(view, node)262+ if err := app.tabManager.Initialize(initialURL); err != nil {
266- log.Printf("[webview] Setup native popup handling for root WebView ID: %d", view.ID())263+ log.Printf("ERROR: failed to initialize tab manager: %v", err)
264+ return err
265+ }
266+
267+ // Set tab manager's root container as window content
268+ rootContainer := app.tabManager.GetRootContainer()
269+ if rootContainer != nil {
270+ window.SetChild(rootContainer)
271+ log.Printf("[tabs] Tab system initialized and set as window content")
267 } else {272 } else {
268- log.Printf("[webview] WARNING: Could not find node for root WebView ID: %d", view.ID())273+ log.Printf("ERROR: tab manager root container is nil")
274+ return fmt.Errorf("failed to get tab manager root container")
269 }275 }
270 }276 }
271 277
272- // Initialize window-level global shortcuts AFTER workspace is set up278+ // Initialize window-level global shortcuts AFTER tab manager is set up
273- if window := view.Window(); window != nil {279+ if window != nil {
274 app.windowShortcutHandler = NewWindowShortcutHandler(window, app)280 app.windowShortcutHandler = NewWindowShortcutHandler(window, app)
275 if app.windowShortcutHandler != nil {281 if app.windowShortcutHandler != nil {
276 log.Printf("Window-level global shortcuts initialized")282 log.Printf("Window-level global shortcuts initialized")
M internal/app/browser/window_shortcuts.go +76 -7
@@ -60,9 +60,33 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error {
6060 {"ctrl+f", h.handleFindToggle, "Find in page"},
6161 {"ctrl+shift+c", h.handleCopyURL, "Copy URL"},
6262 {"ctrl+shift+p", h.handlePrint, "Print page"},
63- {"ctrl+t", h.handleNewTab, "New tab (no-op)"},
64- {"ctrl+w", h.handleClosePane, "Close current pane"},
6563 {"F12", h.handleDevTools, "Developer tools"},
64+ // Pane management
65+ {"ctrl+p", h.handlePaneMode, "Enter pane mode"},
66+ // Tab management
67+ {"ctrl+t", h.handleTabMode, "Enter tab mode"},
68+ // Tab mode actions (only active when tab mode is enabled)
69+ {"n", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"},
70+ {"c", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"},
71+ {"x", func() { h.handleTabModeAction("close-tab") }, "Close tab (tab mode)"},
72+ {"l", func() { h.handleTabModeAction("next-tab") }, "Next tab (tab mode)"},
73+ {"h", func() { h.handleTabModeAction("previous-tab") }, "Previous tab (tab mode)"},
74+ {"r", func() { h.handleTabModeAction("rename-tab") }, "Rename tab (tab mode)"},
75+ {"Tab", func() { h.handleTabModeAction("next-tab") }, "Next tab (tab mode)"},
76+ {"shift+Tab", func() { h.handleTabModeAction("previous-tab") }, "Previous tab (tab mode)"},
77+ {"Return", func() { h.handleTabModeAction("confirm") }, "Confirm (tab mode)"},
78+ {"Escape", func() { h.handleTabModeAction("cancel") }, "Cancel (tab mode)"},
79+ // Direct tab switching
80+ {"alt+1", func() { h.handleDirectTabSwitch(0) }, "Switch to tab 1"},
81+ {"alt+2", func() { h.handleDirectTabSwitch(1) }, "Switch to tab 2"},
82+ {"alt+3", func() { h.handleDirectTabSwitch(2) }, "Switch to tab 3"},
83+ {"alt+4", func() { h.handleDirectTabSwitch(3) }, "Switch to tab 4"},
84+ {"alt+5", func() { h.handleDirectTabSwitch(4) }, "Switch to tab 5"},
85+ {"alt+6", func() { h.handleDirectTabSwitch(5) }, "Switch to tab 6"},
86+ {"alt+7", func() { h.handleDirectTabSwitch(6) }, "Switch to tab 7"},
87+ {"alt+8", func() { h.handleDirectTabSwitch(7) }, "Switch to tab 8"},
88+ {"alt+9", func() { h.handleDirectTabSwitch(8) }, "Switch to tab 9"},
89+ {"alt+0", func() { h.handleDirectTabSwitch(9) }, "Switch to tab 10"},
6690 // Page reload shortcuts
6791 {"ctrl+r", h.handleReload, "Reload page"},
6892 {"ctrl+shift+r", h.handleHardReload, "Hard reload (bypass cache)"},
@@ -409,11 +433,56 @@ func (h *WindowShortcutHandler) handleClosePane() {
409433 }
410434 }
411435
412-// handleNewTab is a no-op handler for Ctrl+T
413-// This prevents the default browser behavior and allows for future implementation
414-func (h *WindowShortcutHandler) handleNewTab() {
415- log.Printf("[window-shortcuts] Ctrl+T pressed (no-op - tabs not yet implemented)")
416- // TODO: Implement new tab functionality
436+// handlePaneMode enters pane mode for modal pane management.
437+func (h *WindowShortcutHandler) handlePaneMode() {
438+ if h.app == nil || h.app.workspace == nil {
439+ log.Printf("[window-shortcuts] Cannot enter pane mode: workspace not available")
440+ return
441+ }
442+
443+ log.Printf("[window-shortcuts] Entering pane mode")
444+ h.app.workspace.EnterPaneMode()
445+}
446+
447+// handleTabMode enters tab mode for modal tab management.
448+func (h *WindowShortcutHandler) handleTabMode() {
449+ if h.app == nil || h.app.tabManager == nil {
450+ log.Printf("[window-shortcuts] Cannot enter tab mode: tab manager not available")
451+ return
452+ }
453+
454+ log.Printf("[window-shortcuts] Entering tab mode")
455+ h.app.tabManager.EnterTabMode()
456+}
457+
458+// handleDirectTabSwitch switches to a specific tab by index (0-based).
459+func (h *WindowShortcutHandler) handleDirectTabSwitch(index int) {
460+ if h.app == nil || h.app.tabManager == nil {
461+ log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available")
462+ return
463+ }
464+
465+ log.Printf("[window-shortcuts] Direct tab switch to index %d", index)
466+ if err := h.app.tabManager.SwitchToTab(index); err != nil {
467+ log.Printf("[window-shortcuts] Failed to switch to tab %d: %v", index, err)
468+ }
469+}
470+
471+// handleTabModeAction handles tab mode action keys (n, x, l, h, etc.)
472+// Only processes actions when tab mode is active, otherwise lets keys pass through to WebView
473+func (h *WindowShortcutHandler) handleTabModeAction(action string) {
474+ if h.app == nil || h.app.tabManager == nil {
475+ return // Tab manager not available, let key pass through
476+ }
477+
478+ // Check if tab mode is active
479+ if !h.app.tabManager.IsTabModeActive() {
480+ return // Tab mode not active, let key pass through to WebView
481+ }
482+
483+ // Tab mode is active, handle the action
484+ log.Printf("[window-shortcuts] Tab mode action: %s", action)
485+ h.app.tabManager.HandleTabAction(action)
417486 }
418487
419488 // Cleanup releases resources
@@ -60,9 +60,33 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error {
60 {"ctrl+f", h.handleFindToggle, "Find in page"},60 {"ctrl+f", h.handleFindToggle, "Find in page"},
61 {"ctrl+shift+c", h.handleCopyURL, "Copy URL"},61 {"ctrl+shift+c", h.handleCopyURL, "Copy URL"},
62 {"ctrl+shift+p", h.handlePrint, "Print page"},62 {"ctrl+shift+p", h.handlePrint, "Print page"},
63- {"ctrl+t", h.handleNewTab, "New tab (no-op)"},
64- {"ctrl+w", h.handleClosePane, "Close current pane"},
65 {"F12", h.handleDevTools, "Developer tools"},63 {"F12", h.handleDevTools, "Developer tools"},
64+ // Pane management
65+ {"ctrl+p", h.handlePaneMode, "Enter pane mode"},
66+ // Tab management
67+ {"ctrl+t", h.handleTabMode, "Enter tab mode"},
68+ // Tab mode actions (only active when tab mode is enabled)
69+ {"n", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"},
70+ {"c", func() { h.handleTabModeAction("new-tab") }, "New tab (tab mode)"},
71+ {"x", func() { h.handleTabModeAction("close-tab") }, "Close tab (tab mode)"},
72+ {"l", func() { h.handleTabModeAction("next-tab") }, "Next tab (tab mode)"},
73+ {"h", func() { h.handleTabModeAction("previous-tab") }, "Previous tab (tab mode)"},
74+ {"r", func() { h.handleTabModeAction("rename-tab") }, "Rename tab (tab mode)"},
75+ {"Tab", func() { h.handleTabModeAction("next-tab") }, "Next tab (tab mode)"},
76+ {"shift+Tab", func() { h.handleTabModeAction("previous-tab") }, "Previous tab (tab mode)"},
77+ {"Return", func() { h.handleTabModeAction("confirm") }, "Confirm (tab mode)"},
78+ {"Escape", func() { h.handleTabModeAction("cancel") }, "Cancel (tab mode)"},
79+ // Direct tab switching
80+ {"alt+1", func() { h.handleDirectTabSwitch(0) }, "Switch to tab 1"},
81+ {"alt+2", func() { h.handleDirectTabSwitch(1) }, "Switch to tab 2"},
82+ {"alt+3", func() { h.handleDirectTabSwitch(2) }, "Switch to tab 3"},
83+ {"alt+4", func() { h.handleDirectTabSwitch(3) }, "Switch to tab 4"},
84+ {"alt+5", func() { h.handleDirectTabSwitch(4) }, "Switch to tab 5"},
85+ {"alt+6", func() { h.handleDirectTabSwitch(5) }, "Switch to tab 6"},
86+ {"alt+7", func() { h.handleDirectTabSwitch(6) }, "Switch to tab 7"},
87+ {"alt+8", func() { h.handleDirectTabSwitch(7) }, "Switch to tab 8"},
88+ {"alt+9", func() { h.handleDirectTabSwitch(8) }, "Switch to tab 9"},
89+ {"alt+0", func() { h.handleDirectTabSwitch(9) }, "Switch to tab 10"},
66 // Page reload shortcuts90 // Page reload shortcuts
67 {"ctrl+r", h.handleReload, "Reload page"},91 {"ctrl+r", h.handleReload, "Reload page"},
68 {"ctrl+shift+r", h.handleHardReload, "Hard reload (bypass cache)"},92 {"ctrl+shift+r", h.handleHardReload, "Hard reload (bypass cache)"},
@@ -409,11 +433,56 @@ func (h *WindowShortcutHandler) handleClosePane() {
409 }433 }
410 }434 }
411 435
412-// handleNewTab is a no-op handler for Ctrl+T436+// handlePaneMode enters pane mode for modal pane management.
413-// This prevents the default browser behavior and allows for future implementation437+func (h *WindowShortcutHandler) handlePaneMode() {
414-func (h *WindowShortcutHandler) handleNewTab() {438+ if h.app == nil || h.app.workspace == nil {
415- log.Printf("[window-shortcuts] Ctrl+T pressed (no-op - tabs not yet implemented)")439+ log.Printf("[window-shortcuts] Cannot enter pane mode: workspace not available")
416- // TODO: Implement new tab functionality440+ return
441+ }
442+
443+ log.Printf("[window-shortcuts] Entering pane mode")
444+ h.app.workspace.EnterPaneMode()
445+}
446+
447+// handleTabMode enters tab mode for modal tab management.
448+func (h *WindowShortcutHandler) handleTabMode() {
449+ if h.app == nil || h.app.tabManager == nil {
450+ log.Printf("[window-shortcuts] Cannot enter tab mode: tab manager not available")
451+ return
452+ }
453+
454+ log.Printf("[window-shortcuts] Entering tab mode")
455+ h.app.tabManager.EnterTabMode()
456+}
457+
458+// handleDirectTabSwitch switches to a specific tab by index (0-based).
459+func (h *WindowShortcutHandler) handleDirectTabSwitch(index int) {
460+ if h.app == nil || h.app.tabManager == nil {
461+ log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available")
462+ return
463+ }
464+
465+ log.Printf("[window-shortcuts] Direct tab switch to index %d", index)
466+ if err := h.app.tabManager.SwitchToTab(index); err != nil {
467+ log.Printf("[window-shortcuts] Failed to switch to tab %d: %v", index, err)
468+ }
469+}
470+
471+// handleTabModeAction handles tab mode action keys (n, x, l, h, etc.)
472+// Only processes actions when tab mode is active, otherwise lets keys pass through to WebView
473+func (h *WindowShortcutHandler) handleTabModeAction(action string) {
474+ if h.app == nil || h.app.tabManager == nil {
475+ return // Tab manager not available, let key pass through
476+ }
477+
478+ // Check if tab mode is active
479+ if !h.app.tabManager.IsTabModeActive() {
480+ return // Tab mode not active, let key pass through to WebView
481+ }
482+
483+ // Tab mode is active, handle the action
484+ log.Printf("[window-shortcuts] Tab mode action: %s", action)
485+ h.app.tabManager.HandleTabAction(action)
417 }486 }
418 487
419 // Cleanup releases resources488 // Cleanup releases resources
M internal/app/browser/workspace_manager.go +45 -0
@@ -542,3 +542,48 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) {
542542
543543 log.Printf("[workspace] Registered navigation handler for webview: %d", webView.ID())
544544 }
545+
546+// GetAllPanes returns all BrowserPanes in this workspace.
547+// This is used by TabManager to update app-level panes reference.
548+func (wm *WorkspaceManager) GetAllPanes() []*BrowserPane {
549+ panes := make([]*BrowserPane, 0, len(wm.viewToNode))
550+ for _, node := range wm.viewToNode {
551+ if node.pane != nil {
552+ panes = append(panes, node.pane)
553+ }
554+ }
555+ return panes
556+}
557+
558+// GetActivePane returns the currently active BrowserPane in this workspace.
559+// This is used by TabManager to update app-level activePane reference.
560+func (wm *WorkspaceManager) GetActivePane() *BrowserPane {
561+ node := wm.GetActiveNode()
562+ if node != nil && node.pane != nil {
563+ return node.pane
564+ }
565+ return nil
566+}
567+
568+// RestoreFocus restores focus to the active pane in this workspace.
569+// Called when switching to a tab to ensure the correct pane has focus.
570+func (wm *WorkspaceManager) RestoreFocus() {
571+ // Focus is managed automatically by the focus state machine
572+ // when the workspace becomes visible
573+ activeNode := wm.GetActiveNode()
574+ if activeNode != nil && activeNode.pane != nil && activeNode.pane.webView != nil {
575+ // Request focus on the active pane
576+ if wm.focusStateMachine != nil {
577+ wm.focusStateMachine.RequestFocus(activeNode, SourceProgrammatic)
578+ }
579+ }
580+}
581+
582+// GetRootWidget returns the root GTK widget for this workspace.
583+// This is the container that should be added to the tab's content area.
584+func (wm *WorkspaceManager) GetRootWidget() gtk.Widgetter {
585+ if wm.root != nil && wm.root.container != nil {
586+ return wm.root.container
587+ }
588+ return nil
589+}
@@ -542,3 +542,48 @@ func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) {
542 542
543 log.Printf("[workspace] Registered navigation handler for webview: %d", webView.ID())543 log.Printf("[workspace] Registered navigation handler for webview: %d", webView.ID())
544 }544 }
545+
546+// GetAllPanes returns all BrowserPanes in this workspace.
547+// This is used by TabManager to update app-level panes reference.
548+func (wm *WorkspaceManager) GetAllPanes() []*BrowserPane {
549+ panes := make([]*BrowserPane, 0, len(wm.viewToNode))
550+ for _, node := range wm.viewToNode {
551+ if node.pane != nil {
552+ panes = append(panes, node.pane)
553+ }
554+ }
555+ return panes
556+}
557+
558+// GetActivePane returns the currently active BrowserPane in this workspace.
559+// This is used by TabManager to update app-level activePane reference.
560+func (wm *WorkspaceManager) GetActivePane() *BrowserPane {
561+ node := wm.GetActiveNode()
562+ if node != nil && node.pane != nil {
563+ return node.pane
564+ }
565+ return nil
566+}
567+
568+// RestoreFocus restores focus to the active pane in this workspace.
569+// Called when switching to a tab to ensure the correct pane has focus.
570+func (wm *WorkspaceManager) RestoreFocus() {
571+ // Focus is managed automatically by the focus state machine
572+ // when the workspace becomes visible
573+ activeNode := wm.GetActiveNode()
574+ if activeNode != nil && activeNode.pane != nil && activeNode.pane.webView != nil {
575+ // Request focus on the active pane
576+ if wm.focusStateMachine != nil {
577+ wm.focusStateMachine.RequestFocus(activeNode, SourceProgrammatic)
578+ }
579+ }
580+}
581+
582+// GetRootWidget returns the root GTK widget for this workspace.
583+// This is the container that should be added to the tab's content area.
584+func (wm *WorkspaceManager) GetRootWidget() gtk.Widgetter {
585+ if wm.root != nil && wm.root.container != nil {
586+ return wm.root.container
587+ }
588+ return nil
589+}

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