fix(ui): improve focus handling and refactor shortcut dispatch
- commit
- 9ba1d1878358e75558237b82d28aa1a09b6e9691
- author
- bamen · · 2025-09-20 05:21 UTC
- parent
- 8bcc885
- signature
- unsigned
2 files · +44 -61
M gui/src/injected/gui.ts +12 -6
| @@ -64,12 +64,15 @@ if (!window.__dumber_gui_ready) { | |||
| 64 | 64 | } else { | |
| 65 | 65 | window.__dumber_gui_ready = true; | |
| 66 | 66 | ||
| 67 | - | let workspaceHasFocus = document.hasFocus(); | |
| 67 | + | let workspaceHasFocus = true; // Assume focus initially, will be corrected by focus events | |
| 68 | 68 | let currentPaneId = window.__dumber_pane?.id || 'unknown'; | |
| 69 | + | let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events | |
| 69 | 70 | ||
| 70 | 71 | document.addEventListener('dumber:workspace-focus', (event: Event) => { | |
| 71 | 72 | const detail = (event as CustomEvent).detail || {}; | |
| 72 | 73 | console.log('[workspace focus event]', detail, 'prev focus=', workspaceHasFocus); | |
| 74 | + | ||
| 75 | + | hasReceivedFocusEvent = true; | |
| 73 | 76 | ||
| 74 | 77 | // Update focus state | |
| 75 | 78 | if (typeof detail?.active === 'boolean') { | |
| @@ -222,7 +225,9 @@ if (!window.__dumber_gui_ready) { | |||
| 222 | 225 | ||
| 223 | 226 | // Enhanced focus checking with pane ID validation | |
| 224 | 227 | const isForThisPane = !eventPaneId || eventPaneId === currentPaneId; | |
| 225 | - | const shouldHandle = workspaceHasFocus && isForThisPane; | |
| 228 | + | // Only apply strict focus checking to omnibox actions, be permissive for others | |
| 229 | + | const isOmniboxAction = action.startsWith('omnibox-'); | |
| 230 | + | const shouldHandle = isForThisPane && (isOmniboxAction ? (hasReceivedFocusEvent ? workspaceHasFocus : true) : true); | |
| 226 | 231 | ||
| 227 | 232 | if (!shouldHandle) { | |
| 228 | 233 | console.log('[dumber shortcuts] ignored action', { | |
| @@ -232,7 +237,8 @@ if (!window.__dumber_gui_ready) { | |||
| 232 | 237 | eventPaneId, | |
| 233 | 238 | currentPaneId, | |
| 234 | 239 | source, | |
| 235 | - | detail | |
| 240 | + | detail, | |
| 241 | + | hasReceivedFocusEvent | |
| 236 | 242 | }); | |
| 237 | 243 | return; | |
| 238 | 244 | } | |
| @@ -242,13 +248,13 @@ if (!window.__dumber_gui_ready) { | |||
| 242 | 248 | const omnibox = window.__dumber_omnibox; | |
| 243 | 249 | ||
| 244 | 250 | switch (action) { | |
| 245 | - | case 'omnibox-toggle': | |
| 246 | - | omnibox?.toggle?.(); | |
| 251 | + | case 'omnibox-nav-toggle': | |
| 252 | + | omnibox?.open?.('omnibox', detail?.query); | |
| 247 | 253 | break; | |
| 248 | 254 | case 'omnibox-open': | |
| 249 | 255 | omnibox?.open?.('omnibox', detail?.query); | |
| 250 | 256 | break; | |
| 251 | - | case 'omnibox-find': | |
| 257 | + | case 'omnibox-find-toggle': | |
| 252 | 258 | omnibox?.open?.('find', detail?.query); | |
| 253 | 259 | break; | |
| 254 | 260 | case 'omnibox-close': | |
| @@ -64,12 +64,15 @@ if (!window.__dumber_gui_ready) { | |||||
| 64 | } else { | 64 | } else { | ||
| 65 | window.__dumber_gui_ready = true; | 65 | window.__dumber_gui_ready = true; | ||
| 66 | 66 | ||||
| 67 | - | let workspaceHasFocus = document.hasFocus(); | 67 | + | let workspaceHasFocus = true; // Assume focus initially, will be corrected by focus events |
| 68 | let currentPaneId = window.__dumber_pane?.id || 'unknown'; | 68 | let currentPaneId = window.__dumber_pane?.id || 'unknown'; | ||
| 69 | + | let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events | |||
| 69 | 70 | ||||
| 70 | document.addEventListener('dumber:workspace-focus', (event: Event) => { | 71 | document.addEventListener('dumber:workspace-focus', (event: Event) => { | ||
| 71 | const detail = (event as CustomEvent).detail || {}; | 72 | const detail = (event as CustomEvent).detail || {}; | ||
| 72 | console.log('[workspace focus event]', detail, 'prev focus=', workspaceHasFocus); | 73 | console.log('[workspace focus event]', detail, 'prev focus=', workspaceHasFocus); | ||
| 74 | + | ||||
| 75 | + | hasReceivedFocusEvent = true; | |||
| 73 | 76 | ||||
| 74 | // Update focus state | 77 | // Update focus state | ||
| 75 | if (typeof detail?.active === 'boolean') { | 78 | if (typeof detail?.active === 'boolean') { | ||
| @@ -222,7 +225,9 @@ if (!window.__dumber_gui_ready) { | |||||
| 222 | 225 | ||||
| 223 | // Enhanced focus checking with pane ID validation | 226 | // Enhanced focus checking with pane ID validation | ||
| 224 | const isForThisPane = !eventPaneId || eventPaneId === currentPaneId; | 227 | const isForThisPane = !eventPaneId || eventPaneId === currentPaneId; | ||
| 225 | - | const shouldHandle = workspaceHasFocus && isForThisPane; | 228 | + | // Only apply strict focus checking to omnibox actions, be permissive for others |
| 229 | + | const isOmniboxAction = action.startsWith('omnibox-'); | |||
| 230 | + | const shouldHandle = isForThisPane && (isOmniboxAction ? (hasReceivedFocusEvent ? workspaceHasFocus : true) : true); | |||
| 226 | 231 | ||||
| 227 | if (!shouldHandle) { | 232 | if (!shouldHandle) { | ||
| 228 | console.log('[dumber shortcuts] ignored action', { | 233 | console.log('[dumber shortcuts] ignored action', { | ||
| @@ -232,7 +237,8 @@ if (!window.__dumber_gui_ready) { | |||||
| 232 | eventPaneId, | 237 | eventPaneId, | ||
| 233 | currentPaneId, | 238 | currentPaneId, | ||
| 234 | source, | 239 | source, | ||
| 235 | - | detail | 240 | + | detail, |
| 241 | + | hasReceivedFocusEvent | |||
| 236 | }); | 242 | }); | ||
| 237 | return; | 243 | return; | ||
| 238 | } | 244 | } | ||
| @@ -242,13 +248,13 @@ if (!window.__dumber_gui_ready) { | |||||
| 242 | const omnibox = window.__dumber_omnibox; | 248 | const omnibox = window.__dumber_omnibox; | ||
| 243 | 249 | ||||
| 244 | switch (action) { | 250 | switch (action) { | ||
| 245 | - | case 'omnibox-toggle': | 251 | + | case 'omnibox-nav-toggle': |
| 246 | - | omnibox?.toggle?.(); | 252 | + | omnibox?.open?.('omnibox', detail?.query); |
| 247 | break; | 253 | break; | ||
| 248 | case 'omnibox-open': | 254 | case 'omnibox-open': | ||
| 249 | omnibox?.open?.('omnibox', detail?.query); | 255 | omnibox?.open?.('omnibox', detail?.query); | ||
| 250 | break; | 256 | break; | ||
| 251 | - | case 'omnibox-find': | 257 | + | case 'omnibox-find-toggle': |
| 252 | omnibox?.open?.('find', detail?.query); | 258 | omnibox?.open?.('find', detail?.query); | ||
| 253 | break; | 259 | break; | ||
| 254 | case 'omnibox-close': | 260 | case 'omnibox-close': | ||
M internal/app/browser/window_shortcuts.go +32 -55
| @@ -74,64 +74,11 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||
| 74 | 74 | } | |
| 75 | 75 | ||
| 76 | 76 | func (h *WindowShortcutHandler) handleOmniboxToggle() { | |
| 77 | - | h.mu.Lock() | |
| 78 | - | defer h.mu.Unlock() | |
| 79 | - | ||
| 80 | - | // Debounce: 50ms minimum between toggles | |
| 81 | - | if time.Since(h.lastOmniboxToggle) < 50*time.Millisecond { | |
| 82 | - | log.Printf("[window-shortcuts] Omnibox toggle debounced") | |
| 83 | - | return | |
| 84 | - | } | |
| 85 | - | h.lastOmniboxToggle = time.Now() | |
| 86 | - | ||
| 87 | - | if h.app.activePane == nil || h.app.activePane.webView == nil { | |
| 88 | - | log.Printf("[window-shortcuts] No active pane for omnibox") | |
| 89 | - | return | |
| 90 | - | } | |
| 91 | - | ||
| 92 | - | log.Printf("[window-shortcuts] Omnibox toggle -> pane %p", h.app.activePane.webView) | |
| 93 | - | ||
| 94 | - | // Ensure GUI is available in active pane | |
| 95 | - | h.ensureGUIInActivePane("omnibox") | |
| 96 | - | ||
| 97 | - | // Dispatch to active pane only | |
| 98 | - | if err := h.app.activePane.webView.DispatchCustomEvent("dumber:ui:shortcut", map[string]any{ | |
| 99 | - | "action": "omnibox-toggle", | |
| 100 | - | "paneId": h.getPaneId(h.app.activePane), | |
| 101 | - | "timestamp": time.Now().UnixMilli(), | |
| 102 | - | "source": "window-global", | |
| 103 | - | }); err != nil { | |
| 104 | - | log.Printf("[window-shortcuts] Failed to dispatch omnibox toggle: %v", err) | |
| 105 | - | } | |
| 77 | + | h.handleUIToggle(&h.lastOmniboxToggle, "omnibox", "omnibox-nav-toggle") | |
| 106 | 78 | } | |
| 107 | 79 | ||
| 108 | 80 | func (h *WindowShortcutHandler) handleFindToggle() { | |
| 109 | - | h.mu.Lock() | |
| 110 | - | defer h.mu.Unlock() | |
| 111 | - | ||
| 112 | - | if time.Since(h.lastFindToggle) < 50*time.Millisecond { | |
| 113 | - | log.Printf("[window-shortcuts] Find toggle debounced") | |
| 114 | - | return | |
| 115 | - | } | |
| 116 | - | h.lastFindToggle = time.Now() | |
| 117 | - | ||
| 118 | - | if h.app.activePane == nil || h.app.activePane.webView == nil { | |
| 119 | - | log.Printf("[window-shortcuts] No active pane for find") | |
| 120 | - | return | |
| 121 | - | } | |
| 122 | - | ||
| 123 | - | log.Printf("[window-shortcuts] Find toggle -> pane %p", h.app.activePane.webView) | |
| 124 | - | ||
| 125 | - | h.ensureGUIInActivePane("omnibox") | |
| 126 | - | ||
| 127 | - | if err := h.app.activePane.webView.DispatchCustomEvent("dumber:ui:shortcut", map[string]any{ | |
| 128 | - | "action": "omnibox-find", | |
| 129 | - | "paneId": h.getPaneId(h.app.activePane), | |
| 130 | - | "timestamp": time.Now().UnixMilli(), | |
| 131 | - | "source": "window-global", | |
| 132 | - | }); err != nil { | |
| 133 | - | log.Printf("[window-shortcuts] Failed to dispatch find toggle: %v", err) | |
| 134 | - | } | |
| 81 | + | h.handleUIToggle(&h.lastFindToggle, "find", "omnibox-find-toggle") | |
| 135 | 82 | } | |
| 136 | 83 | ||
| 137 | 84 | func (h *WindowShortcutHandler) handleCopyURL() { | |
| @@ -224,6 +171,36 @@ func (h *WindowShortcutHandler) getPaneId(pane *BrowserPane) string { | |||
| 224 | 171 | return pane.ID() | |
| 225 | 172 | } | |
| 226 | 173 | ||
| 174 | + | func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureName, action string) { | |
| 175 | + | h.mu.Lock() | |
| 176 | + | defer h.mu.Unlock() | |
| 177 | + | ||
| 178 | + | if time.Since(*lastToggle) < 50*time.Millisecond { | |
| 179 | + | log.Printf("[window-shortcuts] %s toggle debounced", featureName) | |
| 180 | + | return | |
| 181 | + | } | |
| 182 | + | *lastToggle = time.Now() | |
| 183 | + | ||
| 184 | + | if h.app.activePane == nil || h.app.activePane.webView == nil { | |
| 185 | + | log.Printf("[window-shortcuts] No active pane for %s", featureName) | |
| 186 | + | return | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | log.Printf("[window-shortcuts] %s toggle -> pane %p", featureName, h.app.activePane.webView) | |
| 190 | + | ||
| 191 | + | h.ensureGUIInActivePane("omnibox") | |
| 192 | + | ||
| 193 | + | if err := h.app.activePane.webView.DispatchCustomEvent("dumber:ui:shortcut", map[string]any{ | |
| 194 | + | "action": action, | |
| 195 | + | "paneId": h.getPaneId(h.app.activePane), | |
| 196 | + | "timestamp": time.Now().UnixMilli(), | |
| 197 | + | "source": "window-global", | |
| 198 | + | }); err != nil { | |
| 199 | + | log.Printf("[window-shortcuts] Failed to dispatch %s toggle: %v", featureName, err) | |
| 200 | + | } | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | ||
| 227 | 204 | // Cleanup releases resources | |
| 228 | 205 | func (h *WindowShortcutHandler) Cleanup() { | |
| 229 | 206 | if h.shortcuts != nil { | |
| @@ -74,64 +74,11 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||||
| 74 | } | 74 | } | ||
| 75 | 75 | ||||
| 76 | func (h *WindowShortcutHandler) handleOmniboxToggle() { | 76 | func (h *WindowShortcutHandler) handleOmniboxToggle() { | ||
| 77 | - | h.mu.Lock() | 77 | + | h.handleUIToggle(&h.lastOmniboxToggle, "omnibox", "omnibox-nav-toggle") |
| 78 | - | defer h.mu.Unlock() | |||
| 79 | - | ||||
| 80 | - | // Debounce: 50ms minimum between toggles | |||
| 81 | - | if time.Since(h.lastOmniboxToggle) < 50*time.Millisecond { | |||
| 82 | - | log.Printf("[window-shortcuts] Omnibox toggle debounced") | |||
| 83 | - | return | |||
| 84 | - | } | |||
| 85 | - | h.lastOmniboxToggle = time.Now() | |||
| 86 | - | ||||
| 87 | - | if h.app.activePane == nil || h.app.activePane.webView == nil { | |||
| 88 | - | log.Printf("[window-shortcuts] No active pane for omnibox") | |||
| 89 | - | return | |||
| 90 | - | } | |||
| 91 | - | ||||
| 92 | - | log.Printf("[window-shortcuts] Omnibox toggle -> pane %p", h.app.activePane.webView) | |||
| 93 | - | ||||
| 94 | - | // Ensure GUI is available in active pane | |||
| 95 | - | h.ensureGUIInActivePane("omnibox") | |||
| 96 | - | ||||
| 97 | - | // Dispatch to active pane only | |||
| 98 | - | if err := h.app.activePane.webView.DispatchCustomEvent("dumber:ui:shortcut", map[string]any{ | |||
| 99 | - | "action": "omnibox-toggle", | |||
| 100 | - | "paneId": h.getPaneId(h.app.activePane), | |||
| 101 | - | "timestamp": time.Now().UnixMilli(), | |||
| 102 | - | "source": "window-global", | |||
| 103 | - | }); err != nil { | |||
| 104 | - | log.Printf("[window-shortcuts] Failed to dispatch omnibox toggle: %v", err) | |||
| 105 | - | } | |||
| 106 | } | 78 | } | ||
| 107 | 79 | ||||
| 108 | func (h *WindowShortcutHandler) handleFindToggle() { | 80 | func (h *WindowShortcutHandler) handleFindToggle() { | ||
| 109 | - | h.mu.Lock() | 81 | + | h.handleUIToggle(&h.lastFindToggle, "find", "omnibox-find-toggle") |
| 110 | - | defer h.mu.Unlock() | |||
| 111 | - | ||||
| 112 | - | if time.Since(h.lastFindToggle) < 50*time.Millisecond { | |||
| 113 | - | log.Printf("[window-shortcuts] Find toggle debounced") | |||
| 114 | - | return | |||
| 115 | - | } | |||
| 116 | - | h.lastFindToggle = time.Now() | |||
| 117 | - | ||||
| 118 | - | if h.app.activePane == nil || h.app.activePane.webView == nil { | |||
| 119 | - | log.Printf("[window-shortcuts] No active pane for find") | |||
| 120 | - | return | |||
| 121 | - | } | |||
| 122 | - | ||||
| 123 | - | log.Printf("[window-shortcuts] Find toggle -> pane %p", h.app.activePane.webView) | |||
| 124 | - | ||||
| 125 | - | h.ensureGUIInActivePane("omnibox") | |||
| 126 | - | ||||
| 127 | - | if err := h.app.activePane.webView.DispatchCustomEvent("dumber:ui:shortcut", map[string]any{ | |||
| 128 | - | "action": "omnibox-find", | |||
| 129 | - | "paneId": h.getPaneId(h.app.activePane), | |||
| 130 | - | "timestamp": time.Now().UnixMilli(), | |||
| 131 | - | "source": "window-global", | |||
| 132 | - | }); err != nil { | |||
| 133 | - | log.Printf("[window-shortcuts] Failed to dispatch find toggle: %v", err) | |||
| 134 | - | } | |||
| 135 | } | 82 | } | ||
| 136 | 83 | ||||
| 137 | func (h *WindowShortcutHandler) handleCopyURL() { | 84 | func (h *WindowShortcutHandler) handleCopyURL() { | ||
| @@ -224,6 +171,36 @@ func (h *WindowShortcutHandler) getPaneId(pane *BrowserPane) string { | |||||
| 224 | return pane.ID() | 171 | return pane.ID() | ||
| 225 | } | 172 | } | ||
| 226 | 173 | ||||
| 174 | + | func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureName, action string) { | |||
| 175 | + | h.mu.Lock() | |||
| 176 | + | defer h.mu.Unlock() | |||
| 177 | + | ||||
| 178 | + | if time.Since(*lastToggle) < 50*time.Millisecond { | |||
| 179 | + | log.Printf("[window-shortcuts] %s toggle debounced", featureName) | |||
| 180 | + | return | |||
| 181 | + | } | |||
| 182 | + | *lastToggle = time.Now() | |||
| 183 | + | ||||
| 184 | + | if h.app.activePane == nil || h.app.activePane.webView == nil { | |||
| 185 | + | log.Printf("[window-shortcuts] No active pane for %s", featureName) | |||
| 186 | + | return | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | log.Printf("[window-shortcuts] %s toggle -> pane %p", featureName, h.app.activePane.webView) | |||
| 190 | + | ||||
| 191 | + | h.ensureGUIInActivePane("omnibox") | |||
| 192 | + | ||||
| 193 | + | if err := h.app.activePane.webView.DispatchCustomEvent("dumber:ui:shortcut", map[string]any{ | |||
| 194 | + | "action": action, | |||
| 195 | + | "paneId": h.getPaneId(h.app.activePane), | |||
| 196 | + | "timestamp": time.Now().UnixMilli(), | |||
| 197 | + | "source": "window-global", | |||
| 198 | + | }); err != nil { | |||
| 199 | + | log.Printf("[window-shortcuts] Failed to dispatch %s toggle: %v", featureName, err) | |||
| 200 | + | } | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | ||||
| 227 | // Cleanup releases resources | 204 | // Cleanup releases resources | ||
| 228 | func (h *WindowShortcutHandler) Cleanup() { | 205 | func (h *WindowShortcutHandler) Cleanup() { | ||
| 229 | if h.shortcuts != nil { | 206 | if h.shortcuts != nil { | ||