// workspace_pane_mode.go - Backend-driven pane mode implementation package browser import ( "fmt" "github.com/bnema/dumber/internal/logging" ) // EnterPaneMode activates pane mode on the currently focused pane // This is called directly from the keyboard bridge, not from JavaScript func (wm *WorkspaceManager) EnterPaneMode() { // Note: Caller (RegisterPaneModeHandler) ensures this is called on app.workspace (active tab) wm.paneMutex.Lock() defer wm.paneMutex.Unlock() // Get the currently focused pane activeNode := wm.GetActiveNode() if activeNode == nil { logging.Error(fmt.Sprintf("[pane-mode] Cannot enter pane mode: no active pane")) return } // Already in pane mode on this pane? if wm.paneModeActive && wm.paneModeActivePane == activeNode { logging.Info(fmt.Sprintf("[pane-mode] Pane mode already active on this pane")) return } logging.Debug(fmt.Sprintf("[pane-mode] Entering pane mode on pane %p (workspace=%p)", activeNode, wm)) wm.paneModeActive = true wm.paneModeActivePane = activeNode if activeNode.pane != nil && activeNode.pane.webView != nil { wm.paneModeSource = activeNode.pane.webView } // Apply visual border to workspace root wm.applyPaneModeBorder() // Notify JavaScript for UI feedback (visual indicators) wm.dispatchPaneModeEvent("entered", "") } // ExitPaneMode deactivates pane mode func (wm *WorkspaceManager) ExitPaneMode(reason string) { wm.paneMutex.Lock() defer wm.paneMutex.Unlock() if !wm.paneModeActive { return } logging.Info(fmt.Sprintf("[pane-mode] Exiting pane mode: %s", reason)) wm.paneModeActive = false wm.paneModeActivePane = nil wm.paneModeSource = nil // Remove visual border from workspace root wm.removePaneModeBorder() // Notify JavaScript wm.dispatchPaneModeEvent("exited", reason) } // HandlePaneAction processes pane mode actions (close, split, etc.) // Called from keyboard bridge when user presses action keys in pane mode func (wm *WorkspaceManager) HandlePaneAction(action string) { // Note: No IsInActiveTab check here - caller (RegisterPaneModeHandler) ensures // this is called on app.workspace (active tab's workspace) wm.paneMutex.Lock() if !wm.paneModeActive { wm.paneMutex.Unlock() logging.Info(fmt.Sprintf("[pane-mode] Ignoring action '%s': pane mode not active", action)) return } activePane := wm.paneModeActivePane wm.paneMutex.Unlock() if activePane == nil { logging.Info(fmt.Sprintf("[pane-mode] Ignoring action '%s': no active pane", action)) wm.ExitPaneMode("no-active-pane") return } logging.Debug(fmt.Sprintf("[pane-mode] Handling action '%s' on pane %p", action, activePane)) switch action { case "close": wm.handlePaneClose(activePane) case "split-right", "split-left", "split-up", "split-down": wm.handlePaneSplit(activePane, action) case "stack": wm.handlePaneStack(activePane) default: logging.Warn(fmt.Sprintf("[pane-mode] Unknown action: %s", action)) } // Exit pane mode after action wm.ExitPaneMode(action) } // handlePaneClose closes the pane that has pane mode active func (wm *WorkspaceManager) handlePaneClose(node *paneNode) { if node == nil { return } logging.Debug(fmt.Sprintf("[pane-mode] Closing pane %p", node)) // Notify JavaScript first so it can show toast wm.dispatchPaneModeEvent("action", "close") // Close the pane if err := wm.ClosePane(node); err != nil { logging.Error(fmt.Sprintf("[pane-mode] Failed to close pane: %v", err)) } } // handlePaneSplit splits the pane in the specified direction func (wm *WorkspaceManager) handlePaneSplit(node *paneNode, action string) { if node == nil { return } // Extract direction from action (e.g., "split-right" -> DirectionRight) var direction string switch action { case "split-right": direction = DirectionRight case "split-left": direction = DirectionLeft case "split-up": direction = DirectionUp case "split-down": direction = DirectionDown default: logging.Error(fmt.Sprintf("[pane-mode] Invalid split action: %s", action)) return } logging.Debug(fmt.Sprintf("[pane-mode] Splitting pane %p in direction %s", node, direction)) // Notify JavaScript first so it can show toast wm.dispatchPaneModeEvent("action", action) // Perform the split (pass nil so it creates a fresh pane, not popup mode) newNode, err := wm.splitNode(node, direction, nil) if err != nil { logging.Error(fmt.Sprintf("[pane-mode] Failed to split pane: %v", err)) return } // Load initial URL in the new pane so scripts execute wm.clonePaneState(node, newNode) logging.Debug(fmt.Sprintf("[pane-mode] Split successful: direction=%s", direction)) } // handlePaneStack creates a stacked pane func (wm *WorkspaceManager) handlePaneStack(node *paneNode) { if node == nil { return } logging.Debug(fmt.Sprintf("[pane-mode] Stacking pane %p", node)) // Notify JavaScript first so it can show toast wm.dispatchPaneModeEvent("action", "stack") // Perform the stack operation newNode, err := wm.stackedPaneManager.StackPane(node) if err != nil { logging.Error(fmt.Sprintf("[pane-mode] Failed to stack pane: %v", err)) return } // Load initial URL in the new pane so scripts execute wm.clonePaneState(node, newNode) logging.Info(fmt.Sprintf("[pane-mode] Stack successful")) } // dispatchPaneModeEvent sends a pane mode event to JavaScript for UI updates func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) { if wm.paneModeActivePane == nil || wm.paneModeActivePane.pane == nil || wm.paneModeActivePane.pane.webView == nil { return } view := wm.paneModeActivePane.pane.webView data := map[string]interface{}{ "event": event, "detail": detail, } if err := view.DispatchCustomEvent("dumber:pane-mode", data); err != nil { logging.Error(fmt.Sprintf("[pane-mode] Failed to dispatch event: %v", err)) } } // applyPaneModeBorder applies pane mode visual indicator using overlay func (wm *WorkspaceManager) applyPaneModeBorder() { if wm.app != nil && wm.app.tabManager != nil { wm.app.tabManager.showPaneModeBorder() } } // removePaneModeBorder removes the pane mode visual indicator func (wm *WorkspaceManager) removePaneModeBorder() { if wm.app != nil && wm.app.tabManager != nil { wm.app.tabManager.hidePaneModeBorder() } }