detached b6be884

fix(workspace): replace pane ID abstraction with persistent webview IDs for reliable active pane detection

- Remove complex pane ID injection and URI change handlers
- Simplify focus tracking using webview IDs instead of pane IDs
- Add webview ID request/response mechanism for stub builds
- Fix omnibox responsiveness after navigation without mouse movement
- Clean up dead code and TypeScript errors
- Reduce complexity from 91 to 126 lines (+35 net, -85 deletions)
commit
b6be884200add120aaee591249c7e1d325996109
author
Brice · · 2025-09-22 20:40 UTC
parent
e09e4cf
signature
unsigned
10 files · +126 -91
M gui/package-lock.json +0 -15 generated file: collapsed
M gui/src/injected/gui.ts +37 -25
@@ -58,7 +58,6 @@ declare global {
5858 __dumber_keyboard?: KeyboardService;
5959 __dumber_workspace_config?: WorkspaceConfigPayload;
6060 __dumber_workspace?: WorkspaceRuntime;
61- __dumber_pane?: { id: string; active: boolean };
6261 __dumber_omnibox?: {
6362 setSuggestions: (suggestions: Suggestion[]) => void;
6463 toggle: () => void;
@@ -85,9 +84,32 @@ if (!window.__dumber_gui_ready) {
8584 window.__dumber_gui_ready = true;
8685
8786 let workspaceHasFocus = true; // Assume focus initially, will be corrected by focus events
88- let currentPaneId = window.__dumber_pane?.id || "unknown";
87+ let currentWebViewId = window.__dumber_webview_id || "unknown";
8988 let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events
9089
90+ // Request webview ID from Go if it's unknown (needed for stub builds)
91+ if (currentWebViewId === "unknown" || currentWebViewId === "__WEBVIEW_ID__") {
92+ console.log("[workspace] WebView ID unknown, requesting from Go backend");
93+ if (window.webkit?.messageHandlers?.dumber) {
94+ window.webkit.messageHandlers.dumber.postMessage(
95+ JSON.stringify({
96+ type: "request-webview-id",
97+ payload: { timestamp: Date.now() }
98+ })
99+ );
100+ }
101+ }
102+
103+ // Listen for webview ID from Go backend
104+ document.addEventListener("dumber:webview-id", (event: Event) => {
105+ const detail = (event as CustomEvent).detail || {};
106+ if (detail.webviewId) {
107+ currentWebViewId = detail.webviewId;
108+ window.__dumber_webview_id = detail.webviewId;
109+ console.log("[workspace] Received webview ID from Go:", detail.webviewId);
110+ }
111+ });
112+
91113 document.addEventListener("dumber:workspace-focus", (event: Event) => {
92114 const detail = (event as CustomEvent).detail || {};
93115 console.log(
@@ -106,21 +128,11 @@ if (!window.__dumber_gui_ready) {
106128 workspaceHasFocus = detail !== false;
107129 }
108130
109- // Update pane tracking
110- if (detail?.paneId) {
111- currentPaneId = detail.paneId;
112- }
113-
114- // Update global pane state
115- if (window.__dumber_pane) {
116- window.__dumber_pane.active = workspaceHasFocus;
117- }
118-
119131 console.log(
120132 "[workspace focus updated] focus=",
121133 workspaceHasFocus,
122- "paneId=",
123- currentPaneId,
134+ "webviewId=",
135+ currentWebViewId,
124136 );
125137
126138 // Handle GUI visibility based on focus
@@ -265,19 +277,19 @@ if (!window.__dumber_gui_ready) {
265277 document.addEventListener("dumber:ui:shortcut", (e: Event) => {
266278 const detail = (e as CustomEvent).detail || {};
267279 const action = detail?.action;
268- const eventPaneId = detail?.paneId;
280+ const eventWebViewId = detail?.webviewId;
269281 const source = detail?.source || "unknown";
270282
271283 if (typeof action !== "string") {
272284 return;
273285 }
274286
275- // Enhanced focus checking with pane ID validation
276- const isForThisPane = !eventPaneId || eventPaneId === currentPaneId;
287+ // Enhanced focus checking with webview ID validation
288+ const isForThisWebView = !eventWebViewId || eventWebViewId === currentWebViewId;
277289 // Only apply strict focus checking to omnibox actions, be permissive for others
278290 const isOmniboxAction = action.startsWith("omnibox-");
279291 const shouldHandle =
280- isForThisPane &&
292+ isForThisWebView &&
281293 (isOmniboxAction
282294 ? hasReceivedFocusEvent
283295 ? workspaceHasFocus
@@ -288,9 +300,9 @@ if (!window.__dumber_gui_ready) {
288300 console.log("[dumber shortcuts] ignored action", {
289301 action,
290302 workspaceHasFocus,
291- isForThisPane,
292- eventPaneId,
293- currentPaneId,
303+ isForThisWebView,
304+ eventWebViewId,
305+ currentWebViewId,
294306 source,
295307 detail,
296308 hasReceivedFocusEvent,
@@ -301,7 +313,7 @@ if (!window.__dumber_gui_ready) {
301313 console.log("[dumber shortcuts] handling action", {
302314 action,
303315 source,
304- paneId: currentPaneId,
316+ webviewId: currentWebViewId,
305317 });
306318
307319 const omnibox = window.__dumber_omnibox;
@@ -398,18 +410,18 @@ if (!window.__dumber_gui_ready) {
398410 // Ensure omnibox is available for this pane
399411 if (!window.__dumber_omnibox) {
400412 console.log(
401- `[gui-bootstrap] Omnibox not yet available for pane ${currentPaneId}, will be loaded when needed`,
413+ `[gui-bootstrap] Omnibox not yet available for webview ${currentWebViewId}, will be loaded when needed`,
402414 );
403415 } else {
404416 console.log(
405- `[gui-bootstrap] Omnibox already available for pane ${currentPaneId}`,
417+ `[gui-bootstrap] Omnibox already available for webview ${currentWebViewId}`,
406418 );
407419 // Ensure omnibox is aware of the current pane
408420 window.__dumber_omnibox.setActive(workspaceHasFocus);
409421 }
410422
411423 console.log(
412- `[gui-bootstrap] GUI bootstrap complete for pane ${currentPaneId}`,
424+ `[gui-bootstrap] GUI bootstrap complete for webview ${currentWebViewId}`,
413425 );
414426 };
415427 }
@@ -58,7 +58,6 @@ declare global {
58 __dumber_keyboard?: KeyboardService;58 __dumber_keyboard?: KeyboardService;
59 __dumber_workspace_config?: WorkspaceConfigPayload;59 __dumber_workspace_config?: WorkspaceConfigPayload;
60 __dumber_workspace?: WorkspaceRuntime;60 __dumber_workspace?: WorkspaceRuntime;
61- __dumber_pane?: { id: string; active: boolean };
62 __dumber_omnibox?: {61 __dumber_omnibox?: {
63 setSuggestions: (suggestions: Suggestion[]) => void;62 setSuggestions: (suggestions: Suggestion[]) => void;
64 toggle: () => void;63 toggle: () => void;
@@ -85,9 +84,32 @@ if (!window.__dumber_gui_ready) {
85 window.__dumber_gui_ready = true;84 window.__dumber_gui_ready = true;
86 85
87 let workspaceHasFocus = true; // Assume focus initially, will be corrected by focus events86 let workspaceHasFocus = true; // Assume focus initially, will be corrected by focus events
88- let currentPaneId = window.__dumber_pane?.id || "unknown";87+ let currentWebViewId = window.__dumber_webview_id || "unknown";
89 let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events88 let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events
90 89
90+ // Request webview ID from Go if it's unknown (needed for stub builds)
91+ if (currentWebViewId === "unknown" || currentWebViewId === "__WEBVIEW_ID__") {
92+ console.log("[workspace] WebView ID unknown, requesting from Go backend");
93+ if (window.webkit?.messageHandlers?.dumber) {
94+ window.webkit.messageHandlers.dumber.postMessage(
95+ JSON.stringify({
96+ type: "request-webview-id",
97+ payload: { timestamp: Date.now() }
98+ })
99+ );
100+ }
101+ }
102+
103+ // Listen for webview ID from Go backend
104+ document.addEventListener("dumber:webview-id", (event: Event) => {
105+ const detail = (event as CustomEvent).detail || {};
106+ if (detail.webviewId) {
107+ currentWebViewId = detail.webviewId;
108+ window.__dumber_webview_id = detail.webviewId;
109+ console.log("[workspace] Received webview ID from Go:", detail.webviewId);
110+ }
111+ });
112+
91 document.addEventListener("dumber:workspace-focus", (event: Event) => {113 document.addEventListener("dumber:workspace-focus", (event: Event) => {
92 const detail = (event as CustomEvent).detail || {};114 const detail = (event as CustomEvent).detail || {};
93 console.log(115 console.log(
@@ -106,21 +128,11 @@ if (!window.__dumber_gui_ready) {
106 workspaceHasFocus = detail !== false;128 workspaceHasFocus = detail !== false;
107 }129 }
108 130
109- // Update pane tracking
110- if (detail?.paneId) {
111- currentPaneId = detail.paneId;
112- }
113-
114- // Update global pane state
115- if (window.__dumber_pane) {
116- window.__dumber_pane.active = workspaceHasFocus;
117- }
118-
119 console.log(131 console.log(
120 "[workspace focus updated] focus=",132 "[workspace focus updated] focus=",
121 workspaceHasFocus,133 workspaceHasFocus,
122- "paneId=",134+ "webviewId=",
123- currentPaneId,135+ currentWebViewId,
124 );136 );
125 137
126 // Handle GUI visibility based on focus138 // Handle GUI visibility based on focus
@@ -265,19 +277,19 @@ if (!window.__dumber_gui_ready) {
265 document.addEventListener("dumber:ui:shortcut", (e: Event) => {277 document.addEventListener("dumber:ui:shortcut", (e: Event) => {
266 const detail = (e as CustomEvent).detail || {};278 const detail = (e as CustomEvent).detail || {};
267 const action = detail?.action;279 const action = detail?.action;
268- const eventPaneId = detail?.paneId;280+ const eventWebViewId = detail?.webviewId;
269 const source = detail?.source || "unknown";281 const source = detail?.source || "unknown";
270 282
271 if (typeof action !== "string") {283 if (typeof action !== "string") {
272 return;284 return;
273 }285 }
274 286
275- // Enhanced focus checking with pane ID validation287+ // Enhanced focus checking with webview ID validation
276- const isForThisPane = !eventPaneId || eventPaneId === currentPaneId;288+ const isForThisWebView = !eventWebViewId || eventWebViewId === currentWebViewId;
277 // Only apply strict focus checking to omnibox actions, be permissive for others289 // Only apply strict focus checking to omnibox actions, be permissive for others
278 const isOmniboxAction = action.startsWith("omnibox-");290 const isOmniboxAction = action.startsWith("omnibox-");
279 const shouldHandle =291 const shouldHandle =
280- isForThisPane &&292+ isForThisWebView &&
281 (isOmniboxAction293 (isOmniboxAction
282 ? hasReceivedFocusEvent294 ? hasReceivedFocusEvent
283 ? workspaceHasFocus295 ? workspaceHasFocus
@@ -288,9 +300,9 @@ if (!window.__dumber_gui_ready) {
288 console.log("[dumber shortcuts] ignored action", {300 console.log("[dumber shortcuts] ignored action", {
289 action,301 action,
290 workspaceHasFocus,302 workspaceHasFocus,
291- isForThisPane,303+ isForThisWebView,
292- eventPaneId,304+ eventWebViewId,
293- currentPaneId,305+ currentWebViewId,
294 source,306 source,
295 detail,307 detail,
296 hasReceivedFocusEvent,308 hasReceivedFocusEvent,
@@ -301,7 +313,7 @@ if (!window.__dumber_gui_ready) {
301 console.log("[dumber shortcuts] handling action", {313 console.log("[dumber shortcuts] handling action", {
302 action,314 action,
303 source,315 source,
304- paneId: currentPaneId,316+ webviewId: currentWebViewId,
305 });317 });
306 318
307 const omnibox = window.__dumber_omnibox;319 const omnibox = window.__dumber_omnibox;
@@ -398,18 +410,18 @@ if (!window.__dumber_gui_ready) {
398 // Ensure omnibox is available for this pane410 // Ensure omnibox is available for this pane
399 if (!window.__dumber_omnibox) {411 if (!window.__dumber_omnibox) {
400 console.log(412 console.log(
401- `[gui-bootstrap] Omnibox not yet available for pane ${currentPaneId}, will be loaded when needed`,413+ `[gui-bootstrap] Omnibox not yet available for webview ${currentWebViewId}, will be loaded when needed`,
402 );414 );
403 } else {415 } else {
404 console.log(416 console.log(
405- `[gui-bootstrap] Omnibox already available for pane ${currentPaneId}`,417+ `[gui-bootstrap] Omnibox already available for webview ${currentWebViewId}`,
406 );418 );
407 // Ensure omnibox is aware of the current pane419 // Ensure omnibox is aware of the current pane
408 window.__dumber_omnibox.setActive(workspaceHasFocus);420 window.__dumber_omnibox.setActive(workspaceHasFocus);
409 }421 }
410 422
411 console.log(423 console.log(
412- `[gui-bootstrap] GUI bootstrap complete for pane ${currentPaneId}`,424+ `[gui-bootstrap] GUI bootstrap complete for webview ${currentWebViewId}`,
413 );425 );
414 };426 };
415 }427 }
M gui/src/injected/main-world.ts +1 -1
@@ -404,7 +404,7 @@ function detectWindowType(features?: string | null): string {
404404 setupPopupOpenerBridge();
405405
406406 // Parent window message polling - only for OAuth scenarios
407- let parentPollingInterval = null;
407+ let parentPollingInterval: ReturnType<typeof setInterval> | null = null;
408408
409409 const setupParentMessagePolling = () => {
410410 // Only start polling if we have popup mappings or OAuth callbacks
@@ -404,7 +404,7 @@ function detectWindowType(features?: string | null): string {
404 setupPopupOpenerBridge();404 setupPopupOpenerBridge();
405 405
406 // Parent window message polling - only for OAuth scenarios406 // Parent window message polling - only for OAuth scenarios
407- let parentPollingInterval = null;407+ let parentPollingInterval: ReturnType<typeof setInterval> | null = null;
408 408
409 const setupParentMessagePolling = () => {409 const setupParentMessagePolling = () => {
410 // Only start polling if we have popup mappings or OAuth callbacks410 // Only start polling if we have popup mappings or OAuth callbacks
M gui/src/services/app.ts +3 -0
@@ -54,6 +54,7 @@ export class AppService {
5454 visit_count: 3,
5555 last_visited: new Date().toISOString(),
5656 created_at: new Date().toISOString(),
57+ favicon_url: null,
5758 },
5859 {
5960 id: 2,
@@ -62,6 +63,7 @@ export class AppService {
6263 visit_count: 2,
6364 last_visited: new Date().toISOString(),
6465 created_at: new Date().toISOString(),
66+ favicon_url: null,
6567 },
6668 {
6769 id: 3,
@@ -70,6 +72,7 @@ export class AppService {
7072 visit_count: 1,
7173 last_visited: new Date().toISOString(),
7274 created_at: new Date().toISOString(),
75+ favicon_url: null,
7376 },
7477 ];
7578 }
@@ -54,6 +54,7 @@ export class AppService {
54 visit_count: 3,54 visit_count: 3,
55 last_visited: new Date().toISOString(),55 last_visited: new Date().toISOString(),
56 created_at: new Date().toISOString(),56 created_at: new Date().toISOString(),
57+ favicon_url: null,
57 },58 },
58 {59 {
59 id: 2,60 id: 2,
@@ -62,6 +63,7 @@ export class AppService {
62 visit_count: 2,63 visit_count: 2,
63 last_visited: new Date().toISOString(),64 last_visited: new Date().toISOString(),
64 created_at: new Date().toISOString(),65 created_at: new Date().toISOString(),
66+ favicon_url: null,
65 },67 },
66 {68 {
67 id: 3,69 id: 3,
@@ -70,6 +72,7 @@ export class AppService {
70 visit_count: 1,72 visit_count: 1,
71 last_visited: new Date().toISOString(),73 last_visited: new Date().toISOString(),
72 created_at: new Date().toISOString(),74 created_at: new Date().toISOString(),
75+ favicon_url: null,
73 },76 },
74 ];77 ];
75 }78 }
M gui/src/types/generated.ts +1 -1
@@ -86,4 +86,4 @@ export enum InputType {
8686 // Type aliases for frontend usage
8787 export type HistoryEntry = History;
8888 export type SearchShortcut = Shortcut;
89-export type ZoomEntry = ZoomSetting;
89+export type ZoomEntry = ZoomLevel;
@@ -86,4 +86,4 @@ export enum InputType {
86 // Type aliases for frontend usage86 // Type aliases for frontend usage
87 export type HistoryEntry = History;87 export type HistoryEntry = History;
88 export type SearchShortcut = Shortcut;88 export type SearchShortcut = Shortcut;
89-export type ZoomEntry = ZoomSetting;89+export type ZoomEntry = ZoomLevel;
M internal/app/browser/webview.go +7 -17
@@ -119,6 +119,11 @@ func (app *BrowserApp) buildPane(view *webkit.WebView) (*BrowserPane, error) {
119119 pane.zoomController.RegisterHandlers()
120120 pane.shortcutHandler.RegisterShortcuts()
121121
122+ // Register navigation handler for workspace focus events if workspace exists
123+ if app.workspace != nil {
124+ app.workspace.RegisterNavigationHandler(view)
125+ }
126+
122127 return pane, nil
123128 }
124129
@@ -132,23 +137,8 @@ func (app *BrowserApp) createPaneForView(view *webkit.WebView) (*BrowserPane, er
132137 pane.SetID(fmt.Sprintf("pane-%d-%p", time.Now().Unix(), view))
133138 pane.initializeGUITracking()
134139
135- // Inject minimal bootstrap script for pane initialization
136- bootstrapScript := fmt.Sprintf(`
137- window.__dumber_pane = {
138- id: '%s',
139- created: %d,
140- active: false
141- };
142-
143- // GUI manager will be loaded on-demand
144- window.__dumber_gui_ready = true;
145-
146- console.log('[pane] Initialized pane %s');
147- `, pane.ID(), time.Now().UnixMilli(), pane.ID())
148-
149- if err := view.InjectScript(bootstrapScript); err != nil {
150- log.Printf("[pane-%s] Failed to inject bootstrap: %v", pane.ID(), err)
151- }
140+ // GUI manager will be loaded on-demand when needed
141+ log.Printf("[webview] Created pane for webview: %s", view.ID())
152142
153143 app.attachPaneHandlers(pane)
154144 return pane, nil
@@ -119,6 +119,11 @@ func (app *BrowserApp) buildPane(view *webkit.WebView) (*BrowserPane, error) {
119 pane.zoomController.RegisterHandlers()119 pane.zoomController.RegisterHandlers()
120 pane.shortcutHandler.RegisterShortcuts()120 pane.shortcutHandler.RegisterShortcuts()
121 121
122+ // Register navigation handler for workspace focus events if workspace exists
123+ if app.workspace != nil {
124+ app.workspace.RegisterNavigationHandler(view)
125+ }
126+
122 return pane, nil127 return pane, nil
123 }128 }
124 129
@@ -132,23 +137,8 @@ func (app *BrowserApp) createPaneForView(view *webkit.WebView) (*BrowserPane, er
132 pane.SetID(fmt.Sprintf("pane-%d-%p", time.Now().Unix(), view))137 pane.SetID(fmt.Sprintf("pane-%d-%p", time.Now().Unix(), view))
133 pane.initializeGUITracking()138 pane.initializeGUITracking()
134 139
135- // Inject minimal bootstrap script for pane initialization140+ // GUI manager will be loaded on-demand when needed
136- bootstrapScript := fmt.Sprintf(`141+ log.Printf("[webview] Created pane for webview: %s", view.ID())
137- window.__dumber_pane = {
138- id: '%s',
139- created: %d,
140- active: false
141- };
142-
143- // GUI manager will be loaded on-demand
144- window.__dumber_gui_ready = true;
145-
146- console.log('[pane] Initialized pane %s');
147- `, pane.ID(), time.Now().UnixMilli(), pane.ID())
148-
149- if err := view.InjectScript(bootstrapScript); err != nil {
150- log.Printf("[pane-%s] Failed to inject bootstrap: %v", pane.ID(), err)
151- }
152 142
153 app.attachPaneHandlers(pane)143 app.attachPaneHandlers(pane)
154 return pane, nil144 return pane, nil
M internal/app/browser/window_shortcuts.go +4 -4
@@ -227,11 +227,11 @@ func (h *WindowShortcutHandler) ensureGUIInActivePane(component string) {
227227 }
228228 }
229229
230-func (h *WindowShortcutHandler) getPaneId(pane *BrowserPane) string {
231- if pane == nil {
230+func (h *WindowShortcutHandler) getWebViewId(pane *BrowserPane) string {
231+ if pane == nil || pane.webView == nil {
232232 return "unknown"
233233 }
234- return pane.ID()
234+ return pane.webView.ID()
235235 }
236236
237237 func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureName, action string) {
@@ -255,7 +255,7 @@ func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureNam
255255
256256 if err := h.app.activePane.webView.DispatchCustomEvent("dumber:ui:shortcut", map[string]any{
257257 "action": action,
258- "paneId": h.getPaneId(h.app.activePane),
258+ "webviewId": h.getWebViewId(h.app.activePane),
259259 "timestamp": time.Now().UnixMilli(),
260260 "source": "window-global",
261261 }); err != nil {
@@ -227,11 +227,11 @@ func (h *WindowShortcutHandler) ensureGUIInActivePane(component string) {
227 }227 }
228 }228 }
229 229
230-func (h *WindowShortcutHandler) getPaneId(pane *BrowserPane) string {230+func (h *WindowShortcutHandler) getWebViewId(pane *BrowserPane) string {
231- if pane == nil {231+ if pane == nil || pane.webView == nil {
232 return "unknown"232 return "unknown"
233 }233 }
234- return pane.ID()234+ return pane.webView.ID()
235 }235 }
236 236
237 func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureName, action string) {237 func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureName, action string) {
@@ -255,7 +255,7 @@ func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureNam
255 255
256 if err := h.app.activePane.webView.DispatchCustomEvent("dumber:ui:shortcut", map[string]any{256 if err := h.app.activePane.webView.DispatchCustomEvent("dumber:ui:shortcut", map[string]any{
257 "action": action,257 "action": action,
258- "paneId": h.getPaneId(h.app.activePane),258+ "webviewId": h.getWebViewId(h.app.activePane),
259 "timestamp": time.Now().UnixMilli(),259 "timestamp": time.Now().UnixMilli(),
260 "source": "window-global",260 "source": "window-global",
261 }); err != nil {261 }); err != nil {
M internal/app/browser/workspace_manager.go +46 -27
@@ -354,6 +354,47 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa
354354 }
355355 }
356356
357+// GetActiveNode returns the currently active pane node
358+func (wm *WorkspaceManager) GetActiveNode() *paneNode {
359+ return wm.active
360+}
361+
362+// GetNodeForWebView returns the pane node associated with a WebView
363+func (wm *WorkspaceManager) GetNodeForWebView(webView *webkit.WebView) *paneNode {
364+ return wm.viewToNode[webView]
365+}
366+
367+// RegisterNavigationHandler sets up navigation handling for a webview (simplified)
368+func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) {
369+ if webView == nil {
370+ return
371+ }
372+
373+ log.Printf("[workspace] Registered navigation handler for webview: %s", webView.ID())
374+}
375+
376+// DispatchPaneFocusEvent sends a workspace focus event to a pane's webview
377+func (wm *WorkspaceManager) DispatchPaneFocusEvent(node *paneNode, active bool) {
378+ if node == nil || node.pane == nil || node.pane.webView == nil {
379+ return
380+ }
381+
382+ detail := map[string]any{
383+ "active": active,
384+ "webview": fmt.Sprintf("%p", node.pane.webView),
385+ "webviewId": node.pane.webView.ID(),
386+ "hasGUI": node.pane.HasGUI(),
387+ "timestamp": time.Now().UnixMilli(),
388+ }
389+
390+ if err := node.pane.webView.DispatchCustomEvent("dumber:workspace-focus", detail); err != nil {
391+ log.Printf("[workspace] failed to dispatch focus event: %v", err)
392+ } else if wm.app.config != nil && wm.app.config.Debug.EnableWorkspaceDebug {
393+ log.Printf("[workspace] dispatched focus event for webview %s (active=%v)", node.pane.webView.ID(), active)
394+ }
395+}
396+
397+
357398 func (wm *WorkspaceManager) focusNode(node *paneNode) {
358399 if node == nil || !node.isLeaf || node.pane == nil || node.pane.webView == nil {
359400 return
@@ -371,17 +412,7 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
371412 if previous != nil && previous != node && previous.pane != nil && previous.pane.webView != nil {
372413 previousContainer = previous.container
373414 previousWebView = previous.pane.webView
374- detail := map[string]any{
375- "active": false,
376- "webview": fmt.Sprintf("%p", previous.pane.webView),
377- "paneId": previous.pane.ID(),
378- "timestamp": time.Now().UnixMilli(),
379- }
380- if err := previous.pane.webView.DispatchCustomEvent("dumber:workspace-focus", detail); err != nil {
381- log.Printf("[workspace] failed to notify focus loss: %v", err)
382- } else if wm.app.config != nil && wm.app.config.Debug.EnableWorkspaceDebug {
383- log.Printf("[workspace] notified focus loss for pane %s", previous.pane.ID())
384- }
415+ wm.DispatchPaneFocusEvent(previous, false)
385416 }
386417
387418 // Set previously active WebView as inactive
@@ -453,25 +484,13 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
453484 })
454485 }
455486
456- if node.pane != nil && node.pane.webView != nil && node.pane.webView != previousWebView {
487+ if node.pane != nil && node.pane.webView != nil {
457488 // Update pane focus time
458489 node.pane.UpdateLastFocus()
459490
460- // Enhanced focus event with pane ID and GUI status
461- detail := map[string]any{
462- "active": true,
463- "webview": fmt.Sprintf("%p", node.pane.webView),
464- "paneId": node.pane.ID(),
465- "hasGUI": node.pane.HasGUI(),
466- "timestamp": time.Now().UnixMilli(),
467- }
468-
469- // Dispatch focus event to new active pane
470- if err := node.pane.webView.DispatchCustomEvent("dumber:workspace-focus", detail); err != nil {
471- log.Printf("[workspace] failed to notify focus gain: %v", err)
472- } else if wm.app.config != nil && wm.app.config.Debug.EnableWorkspaceDebug {
473- log.Printf("[workspace] notified focus gain for pane %s", node.pane.ID())
474- }
491+ // Always dispatch focus event to ensure JavaScript side knows the active pane
492+ // This fixes the issue where navigation within the same pane doesn't update JS state
493+ wm.DispatchPaneFocusEvent(node, true)
475494
476495 // Lazy-load GUI components if first focus
477496 if !node.pane.HasGUI() {
@@ -354,6 +354,47 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa
354 }354 }
355 }355 }
356 356
357+// GetActiveNode returns the currently active pane node
358+func (wm *WorkspaceManager) GetActiveNode() *paneNode {
359+ return wm.active
360+}
361+
362+// GetNodeForWebView returns the pane node associated with a WebView
363+func (wm *WorkspaceManager) GetNodeForWebView(webView *webkit.WebView) *paneNode {
364+ return wm.viewToNode[webView]
365+}
366+
367+// RegisterNavigationHandler sets up navigation handling for a webview (simplified)
368+func (wm *WorkspaceManager) RegisterNavigationHandler(webView *webkit.WebView) {
369+ if webView == nil {
370+ return
371+ }
372+
373+ log.Printf("[workspace] Registered navigation handler for webview: %s", webView.ID())
374+}
375+
376+// DispatchPaneFocusEvent sends a workspace focus event to a pane's webview
377+func (wm *WorkspaceManager) DispatchPaneFocusEvent(node *paneNode, active bool) {
378+ if node == nil || node.pane == nil || node.pane.webView == nil {
379+ return
380+ }
381+
382+ detail := map[string]any{
383+ "active": active,
384+ "webview": fmt.Sprintf("%p", node.pane.webView),
385+ "webviewId": node.pane.webView.ID(),
386+ "hasGUI": node.pane.HasGUI(),
387+ "timestamp": time.Now().UnixMilli(),
388+ }
389+
390+ if err := node.pane.webView.DispatchCustomEvent("dumber:workspace-focus", detail); err != nil {
391+ log.Printf("[workspace] failed to dispatch focus event: %v", err)
392+ } else if wm.app.config != nil && wm.app.config.Debug.EnableWorkspaceDebug {
393+ log.Printf("[workspace] dispatched focus event for webview %s (active=%v)", node.pane.webView.ID(), active)
394+ }
395+}
396+
397+
357 func (wm *WorkspaceManager) focusNode(node *paneNode) {398 func (wm *WorkspaceManager) focusNode(node *paneNode) {
358 if node == nil || !node.isLeaf || node.pane == nil || node.pane.webView == nil {399 if node == nil || !node.isLeaf || node.pane == nil || node.pane.webView == nil {
359 return400 return
@@ -371,17 +412,7 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
371 if previous != nil && previous != node && previous.pane != nil && previous.pane.webView != nil {412 if previous != nil && previous != node && previous.pane != nil && previous.pane.webView != nil {
372 previousContainer = previous.container413 previousContainer = previous.container
373 previousWebView = previous.pane.webView414 previousWebView = previous.pane.webView
374- detail := map[string]any{415+ wm.DispatchPaneFocusEvent(previous, false)
375- "active": false,
376- "webview": fmt.Sprintf("%p", previous.pane.webView),
377- "paneId": previous.pane.ID(),
378- "timestamp": time.Now().UnixMilli(),
379- }
380- if err := previous.pane.webView.DispatchCustomEvent("dumber:workspace-focus", detail); err != nil {
381- log.Printf("[workspace] failed to notify focus loss: %v", err)
382- } else if wm.app.config != nil && wm.app.config.Debug.EnableWorkspaceDebug {
383- log.Printf("[workspace] notified focus loss for pane %s", previous.pane.ID())
384- }
385 }416 }
386 417
387 // Set previously active WebView as inactive418 // Set previously active WebView as inactive
@@ -453,25 +484,13 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
453 })484 })
454 }485 }
455 486
456- if node.pane != nil && node.pane.webView != nil && node.pane.webView != previousWebView {487+ if node.pane != nil && node.pane.webView != nil {
457 // Update pane focus time488 // Update pane focus time
458 node.pane.UpdateLastFocus()489 node.pane.UpdateLastFocus()
459 490
460- // Enhanced focus event with pane ID and GUI status491+ // Always dispatch focus event to ensure JavaScript side knows the active pane
461- detail := map[string]any{492+ // This fixes the issue where navigation within the same pane doesn't update JS state
462- "active": true,493+ wm.DispatchPaneFocusEvent(node, true)
463- "webview": fmt.Sprintf("%p", node.pane.webView),
464- "paneId": node.pane.ID(),
465- "hasGUI": node.pane.HasGUI(),
466- "timestamp": time.Now().UnixMilli(),
467- }
468-
469- // Dispatch focus event to new active pane
470- if err := node.pane.webView.DispatchCustomEvent("dumber:workspace-focus", detail); err != nil {
471- log.Printf("[workspace] failed to notify focus gain: %v", err)
472- } else if wm.app.config != nil && wm.app.config.Debug.EnableWorkspaceDebug {
473- log.Printf("[workspace] notified focus gain for pane %s", node.pane.ID())
474- }
475 494
476 // Lazy-load GUI components if first focus495 // Lazy-load GUI components if first focus
477 if !node.pane.HasGUI() {496 if !node.pane.HasGUI() {
M internal/app/messaging/handler.go +22 -1
@@ -8,6 +8,7 @@ import (
88 "sort"
99 "strconv"
1010 "strings"
11+ "time"
1112
1213 "github.com/bnema/dumber/internal/app/constants"
1314 "github.com/bnema/dumber/internal/app/control"
@@ -42,7 +43,6 @@ type Message struct {
4243 Event string `json:"event"`
4344 Action string `json:"action"`
4445 Direction string `json:"direction"`
45- PaneID string `json:"paneId"`
4646 // History operations
4747 HistoryID string `json:"historyId"`
4848 // Request tracking
@@ -129,6 +129,8 @@ func (h *Handler) Handle(payload string) {
129129 h.handleClosePopup(msg)
130130 case "console-message":
131131 h.handleConsoleMessage(msg)
132+ case "request-webview-id":
133+ h.handleWebViewIDRequest(msg)
132134 }
133135 }
134136
@@ -602,3 +604,22 @@ func (h *Handler) handleConsoleMessage(msg Message) {
602604 // Send to logging system with [CONSOLE] tag
603605 logging.CaptureWebKitLog(consolePayload.Message)
604606 }
607+
608+// handleWebViewIDRequest responds to JavaScript requests for the webview ID
609+func (h *Handler) handleWebViewIDRequest(msg Message) {
610+ if h.webView == nil {
611+ log.Printf("[messaging] Cannot provide webview ID - no webview available")
612+ return
613+ }
614+
615+ webViewID := h.webView.ID()
616+ log.Printf("[messaging] Sending webview ID %s to JavaScript", webViewID)
617+
618+ // Send the webview ID back to JavaScript via custom event
619+ if err := h.webView.DispatchCustomEvent("dumber:webview-id", map[string]any{
620+ "webviewId": webViewID,
621+ "timestamp": time.Now().UnixMilli(),
622+ }); err != nil {
623+ log.Printf("[messaging] Failed to send webview ID: %v", err)
624+ }
625+}
@@ -8,6 +8,7 @@ import (
8 "sort"8 "sort"
9 "strconv"9 "strconv"
10 "strings"10 "strings"
11+ "time"
11 12
12 "github.com/bnema/dumber/internal/app/constants"13 "github.com/bnema/dumber/internal/app/constants"
13 "github.com/bnema/dumber/internal/app/control"14 "github.com/bnema/dumber/internal/app/control"
@@ -42,7 +43,6 @@ type Message struct {
42 Event string `json:"event"`43 Event string `json:"event"`
43 Action string `json:"action"`44 Action string `json:"action"`
44 Direction string `json:"direction"`45 Direction string `json:"direction"`
45- PaneID string `json:"paneId"`
46 // History operations46 // History operations
47 HistoryID string `json:"historyId"`47 HistoryID string `json:"historyId"`
48 // Request tracking48 // Request tracking
@@ -129,6 +129,8 @@ func (h *Handler) Handle(payload string) {
129 h.handleClosePopup(msg)129 h.handleClosePopup(msg)
130 case "console-message":130 case "console-message":
131 h.handleConsoleMessage(msg)131 h.handleConsoleMessage(msg)
132+ case "request-webview-id":
133+ h.handleWebViewIDRequest(msg)
132 }134 }
133 }135 }
134 136
@@ -602,3 +604,22 @@ func (h *Handler) handleConsoleMessage(msg Message) {
602 // Send to logging system with [CONSOLE] tag604 // Send to logging system with [CONSOLE] tag
603 logging.CaptureWebKitLog(consolePayload.Message)605 logging.CaptureWebKitLog(consolePayload.Message)
604 }606 }
607+
608+// handleWebViewIDRequest responds to JavaScript requests for the webview ID
609+func (h *Handler) handleWebViewIDRequest(msg Message) {
610+ if h.webView == nil {
611+ log.Printf("[messaging] Cannot provide webview ID - no webview available")
612+ return
613+ }
614+
615+ webViewID := h.webView.ID()
616+ log.Printf("[messaging] Sending webview ID %s to JavaScript", webViewID)
617+
618+ // Send the webview ID back to JavaScript via custom event
619+ if err := h.webView.DispatchCustomEvent("dumber:webview-id", map[string]any{
620+ "webviewId": webViewID,
621+ "timestamp": time.Now().UnixMilli(),
622+ }); err != nil {
623+ log.Printf("[messaging] Failed to send webview ID: %v", err)
624+ }
625+}
M pkg/webkit/webview.go +5 -0
@@ -278,3 +278,8 @@ func (w *WebView) ID() string {
278278 }
279279 return strconv.FormatUint(uint64(w.id), 10)
280280 }
281+
282+// PrefersDarkTheme returns true if GTK is configured to prefer dark theme (stub)
283+func PrefersDarkTheme() bool {
284+ return false // Fallback to light theme when not using CGO
285+}
@@ -278,3 +278,8 @@ func (w *WebView) ID() string {
278 }278 }
279 return strconv.FormatUint(uint64(w.id), 10)279 return strconv.FormatUint(uint64(w.id), 10)
280 }280 }
281+
282+// PrefersDarkTheme returns true if GTK is configured to prefer dark theme (stub)
283+func PrefersDarkTheme() bool {
284+ return false // Fallback to light theme when not using CGO
285+}

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