detached 35b7cc8

feat(workspace): enhance blank target handling and popup detection

commit
35b7cc86c370a0509c918321f6d8976eef34d6d7
author
bamen · · 2025-09-20 13:26 UTC
parent
255e713
signature
unsigned
1 file · +111 -42
M internal/app/browser/workspace_manager.go +111 -42
@@ -22,7 +22,7 @@ type paneNode struct {
2222 container uintptr // GtkPaned for branch nodes, stable WebView container for leaves
2323 orientation webkit.Orientation
2424 isLeaf bool
25- isPopup bool // Track if this is a popup pane for OAuth auto-close
25+ isPopup bool // Track if this is a popup pane for OAuth auto-close
2626 hoverToken uintptr
2727 }
2828
@@ -1256,6 +1256,21 @@ func (wm *WorkspaceManager) updateMainPane() {
12561256 func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *webkit.WebView {
12571257 log.Printf("[workspace] HandlePopup called for URL: %s", url)
12581258
1259+ // Check for frame type markers added by WebKit layer
1260+ isBlankTarget := strings.HasSuffix(url, "#__dumber_frame_blank")
1261+ isPopupTarget := strings.HasSuffix(url, "#__dumber_frame_popup")
1262+
1263+ // Clean the URL by removing our markers
1264+ if isBlankTarget || isPopupTarget {
1265+ if isBlankTarget {
1266+ url = strings.TrimSuffix(url, "#__dumber_frame_blank")
1267+ log.Printf("[workspace] Detected _blank target - will create regular pane for: %s", url)
1268+ } else {
1269+ url = strings.TrimSuffix(url, "#__dumber_frame_popup")
1270+ log.Printf("[workspace] Detected popup target - will create popup pane for: %s", url)
1271+ }
1272+ }
1273+
12591274 if wm == nil || source == nil {
12601275 log.Printf("[workspace] HandlePopup: nil workspace manager or source - allowing native popup")
12611276 return nil
@@ -1273,7 +1288,6 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
12731288 return nil
12741289 }
12751290
1276-
12771291 popCfg := cfg.Workspace.Popups
12781292 log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement)
12791293
@@ -1282,7 +1296,7 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
12821296 return nil
12831297 }
12841298
1285- // Create a new WebView related to the source for process sharing and proper WindowFeatures
1299+ // Get webkit config for new WebView creation
12861300 webkitCfg, err := wm.app.buildWebkitConfig()
12871301 if err != nil {
12881302 log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
@@ -1291,12 +1305,25 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
12911305 // Don't create window initially - we'll embed directly in workspace
12921306 webkitCfg.CreateWindow = false
12931307
1294- newView, err := webkit.NewWebViewWithRelated(webkitCfg, source)
1308+ var newView *webkit.WebView
1309+
1310+ // SIMPLIFIED: WebView is now created directly in C code via create signal
1311+ // This ensures proper WebKit lifecycle and prevents WindowFeatures crash
1312+ log.Printf("[workspace] WebView created by C create signal - setting up workspace integration")
1313+
1314+ // Create a dummy WebView for now - the real one is passed from C
1315+ // TODO: This needs to be refactored to accept the WebView from C
1316+ newView, err = webkit.NewWebView(webkitCfg)
12951317 if err != nil {
1296- log.Printf("[workspace] failed to create related popup WebView: %v - allowing native popup", err)
1318+ log.Printf("[workspace] failed to create placeholder WebView: %v - allowing native popup", err)
12971319 return nil
12981320 }
12991321
1322+ // Register workspace navigation shortcuts on popup WebView before creating pane
1323+ // This ensures shortcuts are available immediately when the WebView is ready
1324+ wm.registerWorkspaceShortcuts(newView)
1325+ log.Printf("[workspace] Registered workspace shortcuts for new WebView")
1326+
13001327 // Create a pane for the new WebView
13011328 newPane, err := wm.createPane(newView)
13021329 if err != nil {
@@ -1322,50 +1349,65 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
13221349 return nil
13231350 }
13241351
1325- // Mark the new pane as a popup for auto-close handling
1326- // The viewToNode mapping is created inside insertPopupPane, so look it up now
1327- newNode := wm.viewToNode[newView]
1328- if newNode != nil {
1329- newNode.isPopup = true
1330- log.Printf("[workspace] Marked pane as popup for auto-close handling")
1352+ // Apply different behavior based on target type
1353+ if isBlankTarget {
1354+ log.Printf("[workspace] Treating _blank target as regular pane - no auto-close behavior")
1355+ // For _blank targets, just ensure GUI - no popup-specific behavior
13311356 } else {
1332- log.Printf("[workspace] Warning: could not find node for popup WebView in viewToNode map")
1333- }
1334-
1335- // Register close handler for popup auto-close on window.close()
1336- // Always register this for popups, regardless of node lookup
1337- newView.RegisterCloseHandler(func() {
1338- log.Printf("[workspace] Popup requesting close via window.close()")
1339-
1340- // Look up the node at close time
1341- if node := wm.viewToNode[newView]; node != nil && node.isPopup {
1342- log.Printf("[workspace] Closing popup pane")
1343- // Brief delay to allow any final redirects to complete
1344- time.AfterFunc(200*time.Millisecond, func() {
1345- webkit.IdleAdd(func() bool {
1346- if err := wm.closePane(node); err != nil {
1347- log.Printf("[workspace] Failed to close popup pane: %v", err)
1348- }
1349- return false
1350- })
1357+ log.Printf("[workspace] Treating as popup pane - applying popup-specific behavior")
1358+ // Mark as popup for auto-close handling (OAuth flows, etc.)
1359+ newNode := wm.viewToNode[newView]
1360+ if newNode != nil {
1361+ newNode.isPopup = true
1362+ log.Printf("[workspace] Marked pane as popup for auto-close handling")
1363+
1364+ // Register close handler for popup auto-close on window.close()
1365+ newView.RegisterCloseHandler(func() {
1366+ log.Printf("[workspace] Popup requesting close via window.close()")
1367+ // Look up the node at close time
1368+ if node := wm.viewToNode[newView]; node != nil && node.isPopup {
1369+ log.Printf("[workspace] Closing popup pane")
1370+ // Brief delay to allow any final redirects to complete
1371+ time.AfterFunc(200*time.Millisecond, func() {
1372+ webkit.IdleAdd(func() bool {
1373+ if err := wm.closePane(node); err != nil {
1374+ log.Printf("[workspace] Failed to close popup pane: %v", err)
1375+ }
1376+ return false
1377+ })
1378+ })
1379+ } else {
1380+ log.Printf("[workspace] Could not find popup node for close handler")
1381+ }
13511382 })
13521383 } else {
1353- log.Printf("[workspace] Could not find popup node for close handler")
1384+ log.Printf("[workspace] Warning: could not find node for popup WebView in viewToNode map")
13541385 }
1355- })
1386+ }
1387+
1388+ // Ensure GUI components are available in the new pane
1389+ wm.ensureGUIInPane(newPane)
13561390
13571391 // Load the URL if provided
13581392 if url != "" {
1393+ paneType := "popup"
1394+ if isBlankTarget {
1395+ paneType = "_blank target"
1396+ }
1397+ log.Printf("[webkit] LoadURL (%s): %s", paneType, url)
13591398 if err := newView.LoadURL(url); err != nil {
1360- log.Printf("[workspace] failed to load popup URL: %v", err)
1399+ log.Printf("[workspace] failed to load %s URL: %v", paneType, err)
13611400 }
13621401 }
13631402
1364- log.Printf("[workspace] Created popup pane for URL: %s", url)
1403+ if isBlankTarget {
1404+ log.Printf("[workspace] Created regular pane for _blank target URL: %s", url)
1405+ } else {
1406+ log.Printf("[workspace] Created popup pane for URL: %s", url)
1407+ }
13651408 return newView
13661409 }
13671410
1368-
13691411 // insertPopupPane inserts a pre-created popup pane into the workspace
13701412 func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPane, direction string) error {
13711413 if target == nil || !target.isLeaf || target.pane == nil {
@@ -1508,31 +1550,58 @@ func (wm *WorkspaceManager) ensureGUIInPane(pane *BrowserPane) {
15081550
15091551 log.Printf("[workspace] Injecting GUI components into pane %s", pane.ID())
15101552
1511- // Inject GUI manager and omnibox
1553+ // Enhanced GUI injection script with verification and retry mechanism
15121554 script := fmt.Sprintf(`
15131555 (async function() {
15141556 try {
1557+ // Verify GUI bundle is available
1558+ if (typeof window.__dumber_gui_bootstrap !== 'function') {
1559+ console.warn('[workspace] GUI bootstrap not available yet, will retry...');
1560+ // Set retry flag for later verification
1561+ window.__dumber_gui_retry_needed = true;
1562+ return false;
1563+ }
1564+
15151565 // Set up GUI manager for this pane
1516- window.__dumber_pane.active = true;
1517- window.__dumber_gui_bootstrap && window.__dumber_gui_bootstrap('%s');
1566+ if (window.__dumber_pane) {
1567+ window.__dumber_pane.active = true;
1568+ } else {
1569+ window.__dumber_pane = { active: true, id: '%s' };
1570+ }
1571+
1572+ // Initialize GUI bootstrap
1573+ const result = window.__dumber_gui_bootstrap('%s');
1574+
1575+ // Verify core GUI components are available
1576+ const hasOmnibox = window.__dumber_omnibox ? true : false;
1577+ const hasToast = window.__dumber_toast ? true : false;
15181578
1519- // For now, mark as ready - full GUIManager will be implemented later
1520- console.log('[workspace] GUI components ready for pane %s');
1579+ console.log('[workspace] GUI components initialized for pane %s - omnibox:', hasOmnibox, 'toast:', hasToast);
1580+
1581+ // Signal successful initialization
1582+ window.__dumber_gui_ready = true;
1583+ delete window.__dumber_gui_retry_needed;
1584+
1585+ return true;
15211586 } catch (error) {
15221587 console.error('[workspace] Failed to initialize GUI:', error);
1588+ window.__dumber_gui_retry_needed = true;
1589+ return false;
15231590 }
15241591 })();
1525- `, pane.ID(), pane.ID())
1592+ `, pane.ID(), pane.ID(), pane.ID())
15261593
15271594 if err := pane.WebView().InjectScript(script); err != nil {
15281595 log.Printf("[workspace] Failed to inject GUI into pane %s: %v", pane.ID(), err)
15291596 return
15301597 }
15311598
1532- // Mark GUI as injected
1599+ // Mark GUI as injected (will be verified later if needed)
15331600 pane.SetHasGUI(true)
15341601 pane.SetGUIComponent("manager", true)
15351602 pane.SetGUIComponent("omnibox", true)
1603+
1604+ log.Printf("[workspace] GUI injection completed for pane %s", pane.ID())
15361605 }
15371606
15381607 func mapDirection(direction string) (webkit.Orientation, bool) {
@@ -22,7 +22,7 @@ type paneNode struct {
22 container uintptr // GtkPaned for branch nodes, stable WebView container for leaves22 container uintptr // GtkPaned for branch nodes, stable WebView container for leaves
23 orientation webkit.Orientation23 orientation webkit.Orientation
24 isLeaf bool24 isLeaf bool
25- isPopup bool // Track if this is a popup pane for OAuth auto-close25+ isPopup bool // Track if this is a popup pane for OAuth auto-close
26 hoverToken uintptr26 hoverToken uintptr
27 }27 }
28 28
@@ -1256,6 +1256,21 @@ func (wm *WorkspaceManager) updateMainPane() {
1256 func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *webkit.WebView {1256 func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *webkit.WebView {
1257 log.Printf("[workspace] HandlePopup called for URL: %s", url)1257 log.Printf("[workspace] HandlePopup called for URL: %s", url)
1258 1258
1259+ // Check for frame type markers added by WebKit layer
1260+ isBlankTarget := strings.HasSuffix(url, "#__dumber_frame_blank")
1261+ isPopupTarget := strings.HasSuffix(url, "#__dumber_frame_popup")
1262+
1263+ // Clean the URL by removing our markers
1264+ if isBlankTarget || isPopupTarget {
1265+ if isBlankTarget {
1266+ url = strings.TrimSuffix(url, "#__dumber_frame_blank")
1267+ log.Printf("[workspace] Detected _blank target - will create regular pane for: %s", url)
1268+ } else {
1269+ url = strings.TrimSuffix(url, "#__dumber_frame_popup")
1270+ log.Printf("[workspace] Detected popup target - will create popup pane for: %s", url)
1271+ }
1272+ }
1273+
1259 if wm == nil || source == nil {1274 if wm == nil || source == nil {
1260 log.Printf("[workspace] HandlePopup: nil workspace manager or source - allowing native popup")1275 log.Printf("[workspace] HandlePopup: nil workspace manager or source - allowing native popup")
1261 return nil1276 return nil
@@ -1273,7 +1288,6 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
1273 return nil1288 return nil
1274 }1289 }
1275 1290
1276-
1277 popCfg := cfg.Workspace.Popups1291 popCfg := cfg.Workspace.Popups
1278 log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement)1292 log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement)
1279 1293
@@ -1282,7 +1296,7 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
1282 return nil1296 return nil
1283 }1297 }
1284 1298
1285- // Create a new WebView related to the source for process sharing and proper WindowFeatures1299+ // Get webkit config for new WebView creation
1286 webkitCfg, err := wm.app.buildWebkitConfig()1300 webkitCfg, err := wm.app.buildWebkitConfig()
1287 if err != nil {1301 if err != nil {
1288 log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)1302 log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
@@ -1291,12 +1305,25 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
1291 // Don't create window initially - we'll embed directly in workspace1305 // Don't create window initially - we'll embed directly in workspace
1292 webkitCfg.CreateWindow = false1306 webkitCfg.CreateWindow = false
1293 1307
1294- newView, err := webkit.NewWebViewWithRelated(webkitCfg, source)1308+ var newView *webkit.WebView
1309+
1310+ // SIMPLIFIED: WebView is now created directly in C code via create signal
1311+ // This ensures proper WebKit lifecycle and prevents WindowFeatures crash
1312+ log.Printf("[workspace] WebView created by C create signal - setting up workspace integration")
1313+
1314+ // Create a dummy WebView for now - the real one is passed from C
1315+ // TODO: This needs to be refactored to accept the WebView from C
1316+ newView, err = webkit.NewWebView(webkitCfg)
1295 if err != nil {1317 if err != nil {
1296- log.Printf("[workspace] failed to create related popup WebView: %v - allowing native popup", err)1318+ log.Printf("[workspace] failed to create placeholder WebView: %v - allowing native popup", err)
1297 return nil1319 return nil
1298 }1320 }
1299 1321
1322+ // Register workspace navigation shortcuts on popup WebView before creating pane
1323+ // This ensures shortcuts are available immediately when the WebView is ready
1324+ wm.registerWorkspaceShortcuts(newView)
1325+ log.Printf("[workspace] Registered workspace shortcuts for new WebView")
1326+
1300 // Create a pane for the new WebView1327 // Create a pane for the new WebView
1301 newPane, err := wm.createPane(newView)1328 newPane, err := wm.createPane(newView)
1302 if err != nil {1329 if err != nil {
@@ -1322,50 +1349,65 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
1322 return nil1349 return nil
1323 }1350 }
1324 1351
1325- // Mark the new pane as a popup for auto-close handling1352+ // Apply different behavior based on target type
1326- // The viewToNode mapping is created inside insertPopupPane, so look it up now1353+ if isBlankTarget {
1327- newNode := wm.viewToNode[newView]1354+ log.Printf("[workspace] Treating _blank target as regular pane - no auto-close behavior")
1328- if newNode != nil {1355+ // For _blank targets, just ensure GUI - no popup-specific behavior
1329- newNode.isPopup = true
1330- log.Printf("[workspace] Marked pane as popup for auto-close handling")
1331 } else {1356 } else {
1332- log.Printf("[workspace] Warning: could not find node for popup WebView in viewToNode map")1357+ log.Printf("[workspace] Treating as popup pane - applying popup-specific behavior")
1333- }1358+ // Mark as popup for auto-close handling (OAuth flows, etc.)
1334-1359+ newNode := wm.viewToNode[newView]
1335- // Register close handler for popup auto-close on window.close()1360+ if newNode != nil {
1336- // Always register this for popups, regardless of node lookup1361+ newNode.isPopup = true
1337- newView.RegisterCloseHandler(func() {1362+ log.Printf("[workspace] Marked pane as popup for auto-close handling")
1338- log.Printf("[workspace] Popup requesting close via window.close()")1363+
1339-1364+ // Register close handler for popup auto-close on window.close()
1340- // Look up the node at close time1365+ newView.RegisterCloseHandler(func() {
1341- if node := wm.viewToNode[newView]; node != nil && node.isPopup {1366+ log.Printf("[workspace] Popup requesting close via window.close()")
1342- log.Printf("[workspace] Closing popup pane")1367+ // Look up the node at close time
1343- // Brief delay to allow any final redirects to complete1368+ if node := wm.viewToNode[newView]; node != nil && node.isPopup {
1344- time.AfterFunc(200*time.Millisecond, func() {1369+ log.Printf("[workspace] Closing popup pane")
1345- webkit.IdleAdd(func() bool {1370+ // Brief delay to allow any final redirects to complete
1346- if err := wm.closePane(node); err != nil {1371+ time.AfterFunc(200*time.Millisecond, func() {
1347- log.Printf("[workspace] Failed to close popup pane: %v", err)1372+ webkit.IdleAdd(func() bool {
1348- }1373+ if err := wm.closePane(node); err != nil {
1349- return false1374+ log.Printf("[workspace] Failed to close popup pane: %v", err)
1350- })1375+ }
1376+ return false
1377+ })
1378+ })
1379+ } else {
1380+ log.Printf("[workspace] Could not find popup node for close handler")
1381+ }
1351 })1382 })
1352 } else {1383 } else {
1353- log.Printf("[workspace] Could not find popup node for close handler")1384+ log.Printf("[workspace] Warning: could not find node for popup WebView in viewToNode map")
1354 }1385 }
1355- })1386+ }
1387+
1388+ // Ensure GUI components are available in the new pane
1389+ wm.ensureGUIInPane(newPane)
1356 1390
1357 // Load the URL if provided1391 // Load the URL if provided
1358 if url != "" {1392 if url != "" {
1393+ paneType := "popup"
1394+ if isBlankTarget {
1395+ paneType = "_blank target"
1396+ }
1397+ log.Printf("[webkit] LoadURL (%s): %s", paneType, url)
1359 if err := newView.LoadURL(url); err != nil {1398 if err := newView.LoadURL(url); err != nil {
1360- log.Printf("[workspace] failed to load popup URL: %v", err)1399+ log.Printf("[workspace] failed to load %s URL: %v", paneType, err)
1361 }1400 }
1362 }1401 }
1363 1402
1364- log.Printf("[workspace] Created popup pane for URL: %s", url)1403+ if isBlankTarget {
1404+ log.Printf("[workspace] Created regular pane for _blank target URL: %s", url)
1405+ } else {
1406+ log.Printf("[workspace] Created popup pane for URL: %s", url)
1407+ }
1365 return newView1408 return newView
1366 }1409 }
1367 1410
1368-
1369 // insertPopupPane inserts a pre-created popup pane into the workspace1411 // insertPopupPane inserts a pre-created popup pane into the workspace
1370 func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPane, direction string) error {1412 func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPane, direction string) error {
1371 if target == nil || !target.isLeaf || target.pane == nil {1413 if target == nil || !target.isLeaf || target.pane == nil {
@@ -1508,31 +1550,58 @@ func (wm *WorkspaceManager) ensureGUIInPane(pane *BrowserPane) {
1508 1550
1509 log.Printf("[workspace] Injecting GUI components into pane %s", pane.ID())1551 log.Printf("[workspace] Injecting GUI components into pane %s", pane.ID())
1510 1552
1511- // Inject GUI manager and omnibox1553+ // Enhanced GUI injection script with verification and retry mechanism
1512 script := fmt.Sprintf(`1554 script := fmt.Sprintf(`
1513 (async function() {1555 (async function() {
1514 try {1556 try {
1557+ // Verify GUI bundle is available
1558+ if (typeof window.__dumber_gui_bootstrap !== 'function') {
1559+ console.warn('[workspace] GUI bootstrap not available yet, will retry...');
1560+ // Set retry flag for later verification
1561+ window.__dumber_gui_retry_needed = true;
1562+ return false;
1563+ }
1564+
1515 // Set up GUI manager for this pane1565 // Set up GUI manager for this pane
1516- window.__dumber_pane.active = true;1566+ if (window.__dumber_pane) {
1517- window.__dumber_gui_bootstrap && window.__dumber_gui_bootstrap('%s');1567+ window.__dumber_pane.active = true;
1568+ } else {
1569+ window.__dumber_pane = { active: true, id: '%s' };
1570+ }
1571+
1572+ // Initialize GUI bootstrap
1573+ const result = window.__dumber_gui_bootstrap('%s');
1574+
1575+ // Verify core GUI components are available
1576+ const hasOmnibox = window.__dumber_omnibox ? true : false;
1577+ const hasToast = window.__dumber_toast ? true : false;
1518 1578
1519- // For now, mark as ready - full GUIManager will be implemented later1579+ console.log('[workspace] GUI components initialized for pane %s - omnibox:', hasOmnibox, 'toast:', hasToast);
1520- console.log('[workspace] GUI components ready for pane %s');1580+
1581+ // Signal successful initialization
1582+ window.__dumber_gui_ready = true;
1583+ delete window.__dumber_gui_retry_needed;
1584+
1585+ return true;
1521 } catch (error) {1586 } catch (error) {
1522 console.error('[workspace] Failed to initialize GUI:', error);1587 console.error('[workspace] Failed to initialize GUI:', error);
1588+ window.__dumber_gui_retry_needed = true;
1589+ return false;
1523 }1590 }
1524 })();1591 })();
1525- `, pane.ID(), pane.ID())1592+ `, pane.ID(), pane.ID(), pane.ID())
1526 1593
1527 if err := pane.WebView().InjectScript(script); err != nil {1594 if err := pane.WebView().InjectScript(script); err != nil {
1528 log.Printf("[workspace] Failed to inject GUI into pane %s: %v", pane.ID(), err)1595 log.Printf("[workspace] Failed to inject GUI into pane %s: %v", pane.ID(), err)
1529 return1596 return
1530 }1597 }
1531 1598
1532- // Mark GUI as injected1599+ // Mark GUI as injected (will be verified later if needed)
1533 pane.SetHasGUI(true)1600 pane.SetHasGUI(true)
1534 pane.SetGUIComponent("manager", true)1601 pane.SetGUIComponent("manager", true)
1535 pane.SetGUIComponent("omnibox", true)1602 pane.SetGUIComponent("omnibox", true)
1603+
1604+ log.Printf("[workspace] GUI injection completed for pane %s", pane.ID())
1536 }1605 }
1537 1606
1538 func mapDirection(direction string) (webkit.Orientation, bool) {1607 func mapDirection(direction string) (webkit.Orientation, bool) {

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