1// workspace_pane_mode.go - Backend-driven pane mode implementation
2package browser
3
4import (
5 "fmt"
6
7 "github.com/bnema/dumber/internal/logging"
8)
9
10// EnterPaneMode activates pane mode on the currently focused pane
11// This is called directly from the keyboard bridge, not from JavaScript
12func (wm *WorkspaceManager) EnterPaneMode() {
13 // Note: Caller (RegisterPaneModeHandler) ensures this is called on app.workspace (active tab)
14
15 wm.paneMutex.Lock()
16 defer wm.paneMutex.Unlock()
17
18 // Get the currently focused pane
19 activeNode := wm.GetActiveNode()
20 if activeNode == nil {
21 logging.Error(fmt.Sprintf("[pane-mode] Cannot enter pane mode: no active pane"))
22 return
23 }
24
25 // Already in pane mode on this pane?
26 if wm.paneModeActive && wm.paneModeActivePane == activeNode {
27 logging.Info(fmt.Sprintf("[pane-mode] Pane mode already active on this pane"))
28 return
29 }
30
31 logging.Debug(fmt.Sprintf("[pane-mode] Entering pane mode on pane %p (workspace=%p)", activeNode, wm))
32
33 wm.paneModeActive = true
34 wm.paneModeActivePane = activeNode
35
36 if activeNode.pane != nil && activeNode.pane.webView != nil {
37 wm.paneModeSource = activeNode.pane.webView
38 }
39
40 // Apply visual border to workspace root
41 wm.applyPaneModeBorder()
42
43 // Notify JavaScript for UI feedback (visual indicators)
44 wm.dispatchPaneModeEvent("entered", "")
45}
46
47// ExitPaneMode deactivates pane mode
48func (wm *WorkspaceManager) ExitPaneMode(reason string) {
49 wm.paneMutex.Lock()
50 defer wm.paneMutex.Unlock()
51
52 if !wm.paneModeActive {
53 return
54 }
55
56 logging.Info(fmt.Sprintf("[pane-mode] Exiting pane mode: %s", reason))
57
58 wm.paneModeActive = false
59 wm.paneModeActivePane = nil
60 wm.paneModeSource = nil
61
62 // Remove visual border from workspace root
63 wm.removePaneModeBorder()
64
65 // Notify JavaScript
66 wm.dispatchPaneModeEvent("exited", reason)
67}
68
69// HandlePaneAction processes pane mode actions (close, split, etc.)
70// Called from keyboard bridge when user presses action keys in pane mode
71func (wm *WorkspaceManager) HandlePaneAction(action string) {
72 // Note: No IsInActiveTab check here - caller (RegisterPaneModeHandler) ensures
73 // this is called on app.workspace (active tab's workspace)
74
75 wm.paneMutex.Lock()
76
77 if !wm.paneModeActive {
78 wm.paneMutex.Unlock()
79 logging.Info(fmt.Sprintf("[pane-mode] Ignoring action '%s': pane mode not active", action))
80 return
81 }
82
83 activePane := wm.paneModeActivePane
84 wm.paneMutex.Unlock()
85
86 if activePane == nil {
87 logging.Info(fmt.Sprintf("[pane-mode] Ignoring action '%s': no active pane", action))
88 wm.ExitPaneMode("no-active-pane")
89 return
90 }
91
92 logging.Debug(fmt.Sprintf("[pane-mode] Handling action '%s' on pane %p", action, activePane))
93
94 switch action {
95 case "close":
96 wm.handlePaneClose(activePane)
97 case "split-right", "split-left", "split-up", "split-down":
98 wm.handlePaneSplit(activePane, action)
99 case "stack":
100 wm.handlePaneStack(activePane)
101 default:
102 logging.Warn(fmt.Sprintf("[pane-mode] Unknown action: %s", action))
103 }
104
105 // Exit pane mode after action
106 wm.ExitPaneMode(action)
107}
108
109// handlePaneClose closes the pane that has pane mode active
110func (wm *WorkspaceManager) handlePaneClose(node *paneNode) {
111 if node == nil {
112 return
113 }
114
115 logging.Debug(fmt.Sprintf("[pane-mode] Closing pane %p", node))
116
117 // Notify JavaScript first so it can show toast
118 wm.dispatchPaneModeEvent("action", "close")
119
120 // Close the pane
121 if err := wm.ClosePane(node); err != nil {
122 logging.Error(fmt.Sprintf("[pane-mode] Failed to close pane: %v", err))
123 }
124}
125
126// handlePaneSplit splits the pane in the specified direction
127func (wm *WorkspaceManager) handlePaneSplit(node *paneNode, action string) {
128 if node == nil {
129 return
130 }
131
132 // Extract direction from action (e.g., "split-right" -> DirectionRight)
133 var direction string
134 switch action {
135 case "split-right":
136 direction = DirectionRight
137 case "split-left":
138 direction = DirectionLeft
139 case "split-up":
140 direction = DirectionUp
141 case "split-down":
142 direction = DirectionDown
143 default:
144 logging.Error(fmt.Sprintf("[pane-mode] Invalid split action: %s", action))
145 return
146 }
147
148 logging.Debug(fmt.Sprintf("[pane-mode] Splitting pane %p in direction %s", node, direction))
149
150 // Notify JavaScript first so it can show toast
151 wm.dispatchPaneModeEvent("action", action)
152
153 // Perform the split (pass nil so it creates a fresh pane, not popup mode)
154 newNode, err := wm.splitNode(node, direction, nil)
155 if err != nil {
156 logging.Error(fmt.Sprintf("[pane-mode] Failed to split pane: %v", err))
157 return
158 }
159
160 // Load initial URL in the new pane so scripts execute
161 wm.clonePaneState(node, newNode)
162
163 logging.Debug(fmt.Sprintf("[pane-mode] Split successful: direction=%s", direction))
164}
165
166// handlePaneStack creates a stacked pane
167func (wm *WorkspaceManager) handlePaneStack(node *paneNode) {
168 if node == nil {
169 return
170 }
171
172 logging.Debug(fmt.Sprintf("[pane-mode] Stacking pane %p", node))
173
174 // Notify JavaScript first so it can show toast
175 wm.dispatchPaneModeEvent("action", "stack")
176
177 // Perform the stack operation
178 newNode, err := wm.stackedPaneManager.StackPane(node)
179 if err != nil {
180 logging.Error(fmt.Sprintf("[pane-mode] Failed to stack pane: %v", err))
181 return
182 }
183
184 // Load initial URL in the new pane so scripts execute
185 wm.clonePaneState(node, newNode)
186
187 logging.Info(fmt.Sprintf("[pane-mode] Stack successful"))
188}
189
190// dispatchPaneModeEvent sends a pane mode event to JavaScript for UI updates
191func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) {
192 if wm.paneModeActivePane == nil || wm.paneModeActivePane.pane == nil || wm.paneModeActivePane.pane.webView == nil {
193 return
194 }
195
196 view := wm.paneModeActivePane.pane.webView
197 data := map[string]interface{}{
198 "event": event,
199 "detail": detail,
200 }
201
202 if err := view.DispatchCustomEvent("dumber:pane-mode", data); err != nil {
203 logging.Error(fmt.Sprintf("[pane-mode] Failed to dispatch event: %v", err))
204 }
205}
206
207// applyPaneModeBorder applies pane mode visual indicator using overlay
208func (wm *WorkspaceManager) applyPaneModeBorder() {
209 if wm.app != nil && wm.app.tabManager != nil {
210 wm.app.tabManager.showPaneModeBorder()
211 }
212}
213
214// removePaneModeBorder removes the pane mode visual indicator
215func (wm *WorkspaceManager) removePaneModeBorder() {
216 if wm.app != nil && wm.app.tabManager != nil {
217 wm.app.tabManager.hidePaneModeBorder()
218 }
219}