detached 9ba1d18

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) {
6464 } else {
6565 window.__dumber_gui_ready = true;
6666
67- let workspaceHasFocus = document.hasFocus();
67+ let workspaceHasFocus = true; // Assume focus initially, will be corrected by focus events
6868 let currentPaneId = window.__dumber_pane?.id || 'unknown';
69+ let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events
6970
7071 document.addEventListener('dumber:workspace-focus', (event: Event) => {
7172 const detail = (event as CustomEvent).detail || {};
7273 console.log('[workspace focus event]', detail, 'prev focus=', workspaceHasFocus);
74+
75+ hasReceivedFocusEvent = true;
7376
7477 // Update focus state
7578 if (typeof detail?.active === 'boolean') {
@@ -222,7 +225,9 @@ if (!window.__dumber_gui_ready) {
222225
223226 // Enhanced focus checking with pane ID validation
224227 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);
226231
227232 if (!shouldHandle) {
228233 console.log('[dumber shortcuts] ignored action', {
@@ -232,7 +237,8 @@ if (!window.__dumber_gui_ready) {
232237 eventPaneId,
233238 currentPaneId,
234239 source,
235- detail
240+ detail,
241+ hasReceivedFocusEvent
236242 });
237243 return;
238244 }
@@ -242,13 +248,13 @@ if (!window.__dumber_gui_ready) {
242248 const omnibox = window.__dumber_omnibox;
243249
244250 switch (action) {
245- case 'omnibox-toggle':
246- omnibox?.toggle?.();
251+ case 'omnibox-nav-toggle':
252+ omnibox?.open?.('omnibox', detail?.query);
247253 break;
248254 case 'omnibox-open':
249255 omnibox?.open?.('omnibox', detail?.query);
250256 break;
251- case 'omnibox-find':
257+ case 'omnibox-find-toggle':
252258 omnibox?.open?.('find', detail?.query);
253259 break;
254260 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 state77 // 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 validation226 // 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- detail240+ 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 {
7474 }
7575
7676 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")
10678 }
10779
10880 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")
13582 }
13683
13784 func (h *WindowShortcutHandler) handleCopyURL() {
@@ -224,6 +171,36 @@ func (h *WindowShortcutHandler) getPaneId(pane *BrowserPane) string {
224171 return pane.ID()
225172 }
226173
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+
227204 // Cleanup releases resources
228205 func (h *WindowShortcutHandler) Cleanup() {
229206 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 resources204 // Cleanup releases resources
228 func (h *WindowShortcutHandler) Cleanup() {205 func (h *WindowShortcutHandler) Cleanup() {
229 if h.shortcuts != nil {206 if h.shortcuts != nil {

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