detached 2e0dabf

feat(pane-mode): add 's' key to create stacked panes

Implement Ctrl+P then 's' keyboard shortcut to stack a new pane on top
of the active pane. This completes the pane mode action set alongside
existing split and close actions.

- Add keyS and actionStack constants to keyboard bridge
- Map 's'/'S' keys to stack action
- Implement handlePaneStack() to create stacked panes via StackedPaneManager
- Clone pane state to new stacked pane for script execution
commit
2e0dabfe3a3d0c10a4443cd73c78bc13ddad2946
author
bamen · · 2025-10-14 18:10 UTC
parent
b171bac
signature
unsigned
2 files · +31 -0
M internal/app/browser/workspace_pane_mode.go +26 -0
@@ -91,6 +91,8 @@ func (wm *WorkspaceManager) HandlePaneAction(action string) {
9191 wm.handlePaneClose(activePane)
9292 case "split-right", "split-left", "split-up", "split-down":
9393 wm.handlePaneSplit(activePane, action)
94+ case "stack":
95+ wm.handlePaneStack(activePane)
9496 default:
9597 log.Printf("[pane-mode] Unknown action: %s", action)
9698 }
@@ -156,6 +158,30 @@ func (wm *WorkspaceManager) handlePaneSplit(node *paneNode, action string) {
156158 log.Printf("[pane-mode] Split successful: direction=%s", direction)
157159 }
158160
161+// handlePaneStack creates a stacked pane
162+func (wm *WorkspaceManager) handlePaneStack(node *paneNode) {
163+ if node == nil {
164+ return
165+ }
166+
167+ log.Printf("[pane-mode] Stacking pane %p", node)
168+
169+ // Notify JavaScript first so it can show toast
170+ wm.dispatchPaneModeEvent("action", "stack")
171+
172+ // Perform the stack operation
173+ newNode, err := wm.stackedPaneManager.StackPane(node)
174+ if err != nil {
175+ log.Printf("[pane-mode] Failed to stack pane: %v", err)
176+ return
177+ }
178+
179+ // Load initial URL in the new pane so scripts execute
180+ wm.clonePaneState(node, newNode)
181+
182+ log.Printf("[pane-mode] Stack successful")
183+}
184+
159185 // dispatchPaneModeEvent sends a pane mode event to JavaScript for UI updates
160186 func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) {
161187 if wm.paneModeActivePane == nil || wm.paneModeActivePane.pane == nil || wm.paneModeActivePane.pane.webView == nil {
@@ -91,6 +91,8 @@ func (wm *WorkspaceManager) HandlePaneAction(action string) {
91 wm.handlePaneClose(activePane)91 wm.handlePaneClose(activePane)
92 case "split-right", "split-left", "split-up", "split-down":92 case "split-right", "split-left", "split-up", "split-down":
93 wm.handlePaneSplit(activePane, action)93 wm.handlePaneSplit(activePane, action)
94+ case "stack":
95+ wm.handlePaneStack(activePane)
94 default:96 default:
95 log.Printf("[pane-mode] Unknown action: %s", action)97 log.Printf("[pane-mode] Unknown action: %s", action)
96 }98 }
@@ -156,6 +158,30 @@ func (wm *WorkspaceManager) handlePaneSplit(node *paneNode, action string) {
156 log.Printf("[pane-mode] Split successful: direction=%s", direction)158 log.Printf("[pane-mode] Split successful: direction=%s", direction)
157 }159 }
158 160
161+// handlePaneStack creates a stacked pane
162+func (wm *WorkspaceManager) handlePaneStack(node *paneNode) {
163+ if node == nil {
164+ return
165+ }
166+
167+ log.Printf("[pane-mode] Stacking pane %p", node)
168+
169+ // Notify JavaScript first so it can show toast
170+ wm.dispatchPaneModeEvent("action", "stack")
171+
172+ // Perform the stack operation
173+ newNode, err := wm.stackedPaneManager.StackPane(node)
174+ if err != nil {
175+ log.Printf("[pane-mode] Failed to stack pane: %v", err)
176+ return
177+ }
178+
179+ // Load initial URL in the new pane so scripts execute
180+ wm.clonePaneState(node, newNode)
181+
182+ log.Printf("[pane-mode] Stack successful")
183+}
184+
159 // dispatchPaneModeEvent sends a pane mode event to JavaScript for UI updates185 // dispatchPaneModeEvent sends a pane mode event to JavaScript for UI updates
160 func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) {186 func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) {
161 if wm.paneModeActivePane == nil || wm.paneModeActivePane.pane == nil || wm.paneModeActivePane.pane.webView == nil {187 if wm.paneModeActivePane == nil || wm.paneModeActivePane.pane == nil || wm.paneModeActivePane.pane.webView == nil {
M pkg/webkit/keyboard_bridge.go +5 -0
@@ -39,6 +39,7 @@ const (
3939 keyC = "c"
4040 keyW = "w"
4141 keyX = "x"
42+ keyS = "s"
4243 keyEscape = "escape"
4344 keyArrowLeft = "arrowleft"
4445 keyArrowRight = "arrowright"
@@ -58,6 +59,7 @@ const (
5859 actionSplitRight = "split-right"
5960 actionSplitDown = "split-down"
6061 actionSplitUp = "split-up"
62+ actionStack = "stack"
6163 actionExit = "exit"
6264 actionEnter = "enter"
6365 )
@@ -74,6 +76,7 @@ const (
7476 var shortcutActions = map[string]string{
7577 // Pane mode actions (no modifier)
7678 keyX: actionClose,
79+ keyS: actionStack,
7780 keyL: actionSplitLeft,
7881 keyR: actionSplitRight,
7982 keyD: actionSplitDown,
@@ -155,6 +158,8 @@ func (w *WebView) AttachKeyboardBridge() {
155158 keyName = keyW
156159 case 'x', 'X':
157160 keyName = keyX
161+ case 's', 'S':
162+ keyName = keyS
158163 case gdkKeyLeft:
159164 keyName = keyArrowLeft
160165 case gdkKeyRight:
@@ -39,6 +39,7 @@ const (
39 keyC = "c"39 keyC = "c"
40 keyW = "w"40 keyW = "w"
41 keyX = "x"41 keyX = "x"
42+ keyS = "s"
42 keyEscape = "escape"43 keyEscape = "escape"
43 keyArrowLeft = "arrowleft"44 keyArrowLeft = "arrowleft"
44 keyArrowRight = "arrowright"45 keyArrowRight = "arrowright"
@@ -58,6 +59,7 @@ const (
58 actionSplitRight = "split-right"59 actionSplitRight = "split-right"
59 actionSplitDown = "split-down"60 actionSplitDown = "split-down"
60 actionSplitUp = "split-up"61 actionSplitUp = "split-up"
62+ actionStack = "stack"
61 actionExit = "exit"63 actionExit = "exit"
62 actionEnter = "enter"64 actionEnter = "enter"
63 )65 )
@@ -74,6 +76,7 @@ const (
74 var shortcutActions = map[string]string{76 var shortcutActions = map[string]string{
75 // Pane mode actions (no modifier)77 // Pane mode actions (no modifier)
76 keyX: actionClose,78 keyX: actionClose,
79+ keyS: actionStack,
77 keyL: actionSplitLeft,80 keyL: actionSplitLeft,
78 keyR: actionSplitRight,81 keyR: actionSplitRight,
79 keyD: actionSplitDown,82 keyD: actionSplitDown,
@@ -155,6 +158,8 @@ func (w *WebView) AttachKeyboardBridge() {
155 keyName = keyW158 keyName = keyW
156 case 'x', 'X':159 case 'x', 'X':
157 keyName = keyX160 keyName = keyX
161+ case 's', 'S':
162+ keyName = keyS
158 case gdkKeyLeft:163 case gdkKeyLeft:
159 keyName = keyArrowLeft164 keyName = keyArrowLeft
160 case gdkKeyRight:165 case gdkKeyRight:

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