detached dbf9169

refactor(gui): integrate shadow dom host and webkit message bridge in injected modules

commit
dbf9169b1b0a1469465a663295e3f8d51ab0e9cd
author
bamen · · 2025-09-15 17:20 UTC
committer
bamen · · 2025-09-15 16:29 UTC
parent
0675302
signature
unsigned
3 files · +84 -52
M gui/src/injected/gui.ts +17 -8
@@ -18,6 +18,8 @@ interface DumberGUI {
1818 isReady: boolean;
1919 }
2020
21+// (legacy suggestions normalization removed; omnibox now fetches suggestions directly via API)
22+
2123 // DOM readiness utility - handles all document ready states
2224 function whenDOMReady(callback: () => void) {
2325 if (document.readyState === 'loading') {
@@ -31,6 +33,9 @@ function whenDOMReady(callback: () => void) {
3133 }
3234 }
3335
36+// Install a page-world bridge so Go can call window.__dumber_omnibox_suggestions()
37+// Page-world bridge is injected by WebKit at document-start; no-op here to avoid duplicates.
38+
3439 // Create global namespace
3540 declare global {
3641 interface Window {
@@ -43,7 +48,6 @@ declare global {
4348 __dumber_toggle?: () => void;
4449 __dumber_find_open?: (query?: string) => void;
4550 __dumber_find_close?: () => void;
46- __dumber_setSuggestions?: (suggestions: any[]) => void;
4751 __dumber_find_query?: (query: string) => void;
4852 }
4953 }
@@ -59,6 +63,8 @@ if (!window.__dumber_gui_ready) {
5963 // Initialize Svelte GUI systems immediately
6064 whenDOMReady(async () => {
6165 try {
66+ // Page-world bridge is injected by WebKit; avoid duplicate injection here
67+
6268 // Initialize toast system first
6369 await initializeToast();
6470
@@ -151,6 +157,16 @@ if (!window.__dumber_gui_ready) {
151157 }, true);
152158
153159 console.log('✅ KeyboardService initialized with global listeners');
160+
161+ // Listen to bridge keyboard events from page-world and forward to KeyboardService
162+ document.addEventListener('dumber:key', (e: Event) => {
163+ const detail = (e as CustomEvent).detail || {};
164+ if (detail && typeof detail.shortcut === 'string') {
165+ keyboardService.handleNativeShortcut(detail.shortcut);
166+ }
167+ });
168+
169+ // Suggestions are now fetched via API directly by the omnibox component; no page-bridge handlers needed
154170 });
155171
156172 // Legacy compatibility functions for existing Go code
@@ -190,13 +206,6 @@ if (!window.__dumber_gui_ready) {
190206 }
191207 };
192208
193- // Key function that Go uses to send search suggestions
194- window.__dumber_setSuggestions = (suggestions: any[]) => {
195- if (window.__dumber_omnibox?.setSuggestions) {
196- window.__dumber_omnibox.setSuggestions(suggestions);
197- }
198- };
199-
200209 // Find query function that Go uses to set find query
201210 window.__dumber_find_query = (query: string) => {
202211 if (window.__dumber_omnibox?.findQuery) {
@@ -18,6 +18,8 @@ interface DumberGUI {
18 isReady: boolean;18 isReady: boolean;
19 }19 }
20 20
21+// (legacy suggestions normalization removed; omnibox now fetches suggestions directly via API)
22+
21 // DOM readiness utility - handles all document ready states23 // DOM readiness utility - handles all document ready states
22 function whenDOMReady(callback: () => void) {24 function whenDOMReady(callback: () => void) {
23 if (document.readyState === 'loading') {25 if (document.readyState === 'loading') {
@@ -31,6 +33,9 @@ function whenDOMReady(callback: () => void) {
31 }33 }
32 }34 }
33 35
36+// Install a page-world bridge so Go can call window.__dumber_omnibox_suggestions()
37+// Page-world bridge is injected by WebKit at document-start; no-op here to avoid duplicates.
38+
34 // Create global namespace39 // Create global namespace
35 declare global {40 declare global {
36 interface Window {41 interface Window {
@@ -43,7 +48,6 @@ declare global {
43 __dumber_toggle?: () => void;48 __dumber_toggle?: () => void;
44 __dumber_find_open?: (query?: string) => void;49 __dumber_find_open?: (query?: string) => void;
45 __dumber_find_close?: () => void;50 __dumber_find_close?: () => void;
46- __dumber_setSuggestions?: (suggestions: any[]) => void;
47 __dumber_find_query?: (query: string) => void;51 __dumber_find_query?: (query: string) => void;
48 }52 }
49 }53 }
@@ -59,6 +63,8 @@ if (!window.__dumber_gui_ready) {
59 // Initialize Svelte GUI systems immediately63 // Initialize Svelte GUI systems immediately
60 whenDOMReady(async () => {64 whenDOMReady(async () => {
61 try {65 try {
66+ // Page-world bridge is injected by WebKit; avoid duplicate injection here
67+
62 // Initialize toast system first68 // Initialize toast system first
63 await initializeToast();69 await initializeToast();
64 70
@@ -151,6 +157,16 @@ if (!window.__dumber_gui_ready) {
151 }, true);157 }, true);
152 158
153 console.log('✅ KeyboardService initialized with global listeners');159 console.log('✅ KeyboardService initialized with global listeners');
160+
161+ // Listen to bridge keyboard events from page-world and forward to KeyboardService
162+ document.addEventListener('dumber:key', (e: Event) => {
163+ const detail = (e as CustomEvent).detail || {};
164+ if (detail && typeof detail.shortcut === 'string') {
165+ keyboardService.handleNativeShortcut(detail.shortcut);
166+ }
167+ });
168+
169+ // Suggestions are now fetched via API directly by the omnibox component; no page-bridge handlers needed
154 });170 });
155 171
156 // Legacy compatibility functions for existing Go code172 // Legacy compatibility functions for existing Go code
@@ -190,13 +206,6 @@ if (!window.__dumber_gui_ready) {
190 }206 }
191 };207 };
192 208
193- // Key function that Go uses to send search suggestions
194- window.__dumber_setSuggestions = (suggestions: any[]) => {
195- if (window.__dumber_omnibox?.setSuggestions) {
196- window.__dumber_omnibox.setSuggestions(suggestions);
197- }
198- };
199-
200 // Find query function that Go uses to set find query209 // Find query function that Go uses to set find query
201 window.__dumber_find_query = (query: string) => {210 window.__dumber_find_query = (query: string) => {
202 if (window.__dumber_omnibox?.findQuery) {211 if (window.__dumber_omnibox?.findQuery) {
M gui/src/injected/modules/omnibox.ts +36 -32
@@ -6,6 +6,7 @@
66
77 import { mount } from 'svelte';
88 import { Omnibox } from '../../components/omnibox';
9+import { ensureShadowMount } from './shadowHost';
910 import type { OmniboxConfig } from '../../components/omnibox/types';
1011
1112 let omniboxComponent: ReturnType<typeof mount> | null = null;
@@ -19,7 +20,7 @@ export interface OmniboxInitConfig extends OmniboxConfig {
1920 /**
2021 * Initialize the omnibox component system
2122 */
22-export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise<void> {
23+export async function initializeOmnibox(_config: OmniboxInitConfig = {}): Promise<void> {
2324 if (isInitialized) {
2425 console.warn('Omnibox already initialized');
2526 return;
@@ -38,34 +39,10 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise
3839 return;
3940 }
4041
41- // Create container element
42- const containerId = config.containerId || 'dumber-omnibox-root';
43- let container = document.getElementById(containerId);
44-
45- if (!container) {
46- container = document.createElement('div');
47- container.id = containerId;
48- container.style.cssText = `
49- position: fixed !important;
50- top: 0 !important;
51- left: 0 !important;
52- right: 0 !important;
53- bottom: 0 !important;
54- z-index: 2147483647 !important;
55- pointer-events: none !important;
56- margin: 0 !important;
57- padding: 0 !important;
58- border: none !important;
59- background: none !important;
60- isolation: isolate !important;
61- contain: layout style !important;
62- `;
63- document.documentElement.appendChild(container);
64- }
65-
66- // Mount the Svelte component
42+ // Mount omnibox into the shared global Shadow DOM host
43+ const mountEl = ensureShadowMount('dumber-omnibox');
6744 omniboxComponent = mount(Omnibox, {
68- target: container
45+ target: mountEl as unknown as Element
6946 });
7047
7148 // Wait for the component to mount and set up the global API
@@ -75,6 +52,12 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise
7552 console.log('✅ Omnibox component system initialized');
7653 console.log('🔧 Global API is available:', Object.keys(window.__dumber_omnibox));
7754 isInitialized = true;
55+ // Notify isolated bundle listeners that omnibox is ready
56+ try {
57+ document.dispatchEvent(new CustomEvent('dumber:omnibox-ready'));
58+ } catch (e) {
59+ console.warn('Failed to dispatch omnibox-ready event', e);
60+ }
7861 resolve();
7962 } else {
8063 setTimeout(checkAPI, 10); // Check again in 10ms
@@ -83,6 +66,30 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise
8366 checkAPI();
8467 });
8568
69+ // Bridge: listen for page-world events carrying suggestions and forward to API
70+ const handleSuggestionsEvent = (e: Event) => {
71+ try {
72+ const detail = (e as CustomEvent).detail;
73+ const suggestions = detail?.suggestions ?? detail;
74+ if (Array.isArray(suggestions) && window.__dumber_omnibox?.setSuggestions) {
75+ console.log('🔗 [Bridge] Received page-world suggestions event:', suggestions.length);
76+ window.__dumber_omnibox.setSuggestions(suggestions);
77+ } else {
78+ console.warn('🔗 [Bridge] Suggestions event received but API not ready or invalid payload');
79+ }
80+ } catch (err) {
81+ console.warn('🔗 [Bridge] Failed handling suggestions event:', err);
82+ }
83+ };
84+
85+ document.addEventListener(
86+ 'dumber:omnibox-suggestions',
87+ handleSuggestionsEvent,
88+ false
89+ );
90+ // Unified event name
91+ document.addEventListener('dumber:omnibox:suggestions', handleSuggestionsEvent, false);
92+
8693 } catch (error) {
8794 console.error('❌ Failed to initialize omnibox component:', error);
8895 throw error;
@@ -109,10 +116,7 @@ export function cleanupOmnibox(): void {
109116 omniboxComponent = null;
110117 }
111118
112- const container = document.getElementById('dumber-omnibox-root');
113- if (container) {
114- container.remove();
115- }
119+ // The shared shadow host remains persistent; just log cleanup
116120
117121 isInitialized = false;
118122 console.log('🧹 Omnibox component cleaned up');
@@ -6,6 +6,7 @@
6 6
7 import { mount } from 'svelte';7 import { mount } from 'svelte';
8 import { Omnibox } from '../../components/omnibox';8 import { Omnibox } from '../../components/omnibox';
9+import { ensureShadowMount } from './shadowHost';
9 import type { OmniboxConfig } from '../../components/omnibox/types';10 import type { OmniboxConfig } from '../../components/omnibox/types';
10 11
11 let omniboxComponent: ReturnType<typeof mount> | null = null;12 let omniboxComponent: ReturnType<typeof mount> | null = null;
@@ -19,7 +20,7 @@ export interface OmniboxInitConfig extends OmniboxConfig {
19 /**20 /**
20 * Initialize the omnibox component system21 * Initialize the omnibox component system
21 */22 */
22-export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise<void> {23+export async function initializeOmnibox(_config: OmniboxInitConfig = {}): Promise<void> {
23 if (isInitialized) {24 if (isInitialized) {
24 console.warn('Omnibox already initialized');25 console.warn('Omnibox already initialized');
25 return;26 return;
@@ -38,34 +39,10 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise
38 return;39 return;
39 }40 }
40 41
41- // Create container element42+ // Mount omnibox into the shared global Shadow DOM host
42- const containerId = config.containerId || 'dumber-omnibox-root';43+ const mountEl = ensureShadowMount('dumber-omnibox');
43- let container = document.getElementById(containerId);
44-
45- if (!container) {
46- container = document.createElement('div');
47- container.id = containerId;
48- container.style.cssText = `
49- position: fixed !important;
50- top: 0 !important;
51- left: 0 !important;
52- right: 0 !important;
53- bottom: 0 !important;
54- z-index: 2147483647 !important;
55- pointer-events: none !important;
56- margin: 0 !important;
57- padding: 0 !important;
58- border: none !important;
59- background: none !important;
60- isolation: isolate !important;
61- contain: layout style !important;
62- `;
63- document.documentElement.appendChild(container);
64- }
65-
66- // Mount the Svelte component
67 omniboxComponent = mount(Omnibox, {44 omniboxComponent = mount(Omnibox, {
68- target: container45+ target: mountEl as unknown as Element
69 });46 });
70 47
71 // Wait for the component to mount and set up the global API48 // Wait for the component to mount and set up the global API
@@ -75,6 +52,12 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise
75 console.log('✅ Omnibox component system initialized');52 console.log('✅ Omnibox component system initialized');
76 console.log('🔧 Global API is available:', Object.keys(window.__dumber_omnibox));53 console.log('🔧 Global API is available:', Object.keys(window.__dumber_omnibox));
77 isInitialized = true;54 isInitialized = true;
55+ // Notify isolated bundle listeners that omnibox is ready
56+ try {
57+ document.dispatchEvent(new CustomEvent('dumber:omnibox-ready'));
58+ } catch (e) {
59+ console.warn('Failed to dispatch omnibox-ready event', e);
60+ }
78 resolve();61 resolve();
79 } else {62 } else {
80 setTimeout(checkAPI, 10); // Check again in 10ms63 setTimeout(checkAPI, 10); // Check again in 10ms
@@ -83,6 +66,30 @@ export async function initializeOmnibox(config: OmniboxInitConfig = {}): Promise
83 checkAPI();66 checkAPI();
84 });67 });
85 68
69+ // Bridge: listen for page-world events carrying suggestions and forward to API
70+ const handleSuggestionsEvent = (e: Event) => {
71+ try {
72+ const detail = (e as CustomEvent).detail;
73+ const suggestions = detail?.suggestions ?? detail;
74+ if (Array.isArray(suggestions) && window.__dumber_omnibox?.setSuggestions) {
75+ console.log('🔗 [Bridge] Received page-world suggestions event:', suggestions.length);
76+ window.__dumber_omnibox.setSuggestions(suggestions);
77+ } else {
78+ console.warn('🔗 [Bridge] Suggestions event received but API not ready or invalid payload');
79+ }
80+ } catch (err) {
81+ console.warn('🔗 [Bridge] Failed handling suggestions event:', err);
82+ }
83+ };
84+
85+ document.addEventListener(
86+ 'dumber:omnibox-suggestions',
87+ handleSuggestionsEvent,
88+ false
89+ );
90+ // Unified event name
91+ document.addEventListener('dumber:omnibox:suggestions', handleSuggestionsEvent, false);
92+
86 } catch (error) {93 } catch (error) {
87 console.error('❌ Failed to initialize omnibox component:', error);94 console.error('❌ Failed to initialize omnibox component:', error);
88 throw error;95 throw error;
@@ -109,10 +116,7 @@ export function cleanupOmnibox(): void {
109 omniboxComponent = null;116 omniboxComponent = null;
110 }117 }
111 118
112- const container = document.getElementById('dumber-omnibox-root');119+ // The shared shadow host remains persistent; just log cleanup
113- if (container) {
114- container.remove();
115- }
116 120
117 isInitialized = false;121 isInitialized = false;
118 console.log('🧹 Omnibox component cleaned up');122 console.log('🧹 Omnibox component cleaned up');
M gui/src/injected/modules/toast.ts +31 -12
@@ -7,6 +7,7 @@
77
88 import { mount, flushSync } from 'svelte';
99 import ToastContainer from '$components/toast/ToastContainer.svelte';
10+import { ensureShadowMount } from './shadowHost';
1011 import '$lib/styles.css';
1112
1213 export interface ToastConfig {
@@ -78,17 +79,8 @@ export async function initializeToast(config?: ToastConfig): Promise<void> {
7879 }
7980
8081 try {
81- // Find or create browser component root
82- let rootElement = document.querySelector('.browser-component-root') as HTMLDivElement;
83- if (!rootElement) {
84- console.log('🔧 Creating browser-component-root');
85- rootElement = document.createElement('div');
86- rootElement.className = 'browser-component-root';
87- rootElement.style.cssText = 'position: fixed; top: 0; left: 0; pointer-events: none; z-index: 2147483647;';
88- document.documentElement.appendChild(rootElement);
89- } else {
90- console.log('✅ Found existing browser-component-root');
91- }
82+ // Use the global Shadow DOM host for isolation
83+ const rootElement = ensureShadowMount('dumber-toast');
9284
9385 // Initialize theme
9486 const initialTheme = config?.theme || window.__dumber_initial_theme;
@@ -98,9 +90,36 @@ export async function initializeToast(config?: ToastConfig): Promise<void> {
9890
9991 // Mount the Svelte toast container (Svelte 5 syntax)
10092 mount(ToastContainer, {
101- target: rootElement
93+ target: rootElement as unknown as Element
10294 });
10395
96+ // Signal isolated GUI readiness for page-world bridge
97+ try {
98+ document.documentElement.setAttribute('data-dumber-gui', 'ready');
99+ document.dispatchEvent(new CustomEvent('dumber:gui-ready'));
100+ } catch {
101+ // Ignore errors in GUI readiness signaling
102+ }
103+
104+ // Listen for page-world bridge events and forward to internal toast functions
105+ const handleShowToast = (e: Event) => {
106+ const detail = (e as CustomEvent).detail || {};
107+ if (typeof window.__dumber_showToast === 'function') {
108+ window.__dumber_showToast(detail.message, detail.duration, detail.type);
109+ }
110+ };
111+ document.addEventListener('dumber:showToast', handleShowToast);
112+ document.addEventListener('dumber:toast:show', handleShowToast);
113+
114+ const handleZoomToast = (e: Event) => {
115+ const detail = (e as CustomEvent).detail || {};
116+ if (typeof window.__dumber_showZoomToast === 'function') {
117+ window.__dumber_showZoomToast(detail.level);
118+ }
119+ };
120+ document.addEventListener('dumber:showZoomToast', handleZoomToast);
121+ document.addEventListener('dumber:toast:zoom', handleZoomToast);
122+
104123 // Force immediate effect execution to ensure onMount callbacks run
105124 flushSync();
106125
@@ -7,6 +7,7 @@
7 7
8 import { mount, flushSync } from 'svelte';8 import { mount, flushSync } from 'svelte';
9 import ToastContainer from '$components/toast/ToastContainer.svelte';9 import ToastContainer from '$components/toast/ToastContainer.svelte';
10+import { ensureShadowMount } from './shadowHost';
10 import '$lib/styles.css';11 import '$lib/styles.css';
11 12
12 export interface ToastConfig {13 export interface ToastConfig {
@@ -78,17 +79,8 @@ export async function initializeToast(config?: ToastConfig): Promise<void> {
78 }79 }
79 80
80 try {81 try {
81- // Find or create browser component root82+ // Use the global Shadow DOM host for isolation
82- let rootElement = document.querySelector('.browser-component-root') as HTMLDivElement;83+ const rootElement = ensureShadowMount('dumber-toast');
83- if (!rootElement) {
84- console.log('🔧 Creating browser-component-root');
85- rootElement = document.createElement('div');
86- rootElement.className = 'browser-component-root';
87- rootElement.style.cssText = 'position: fixed; top: 0; left: 0; pointer-events: none; z-index: 2147483647;';
88- document.documentElement.appendChild(rootElement);
89- } else {
90- console.log('✅ Found existing browser-component-root');
91- }
92 84
93 // Initialize theme85 // Initialize theme
94 const initialTheme = config?.theme || window.__dumber_initial_theme;86 const initialTheme = config?.theme || window.__dumber_initial_theme;
@@ -98,9 +90,36 @@ export async function initializeToast(config?: ToastConfig): Promise<void> {
98 90
99 // Mount the Svelte toast container (Svelte 5 syntax)91 // Mount the Svelte toast container (Svelte 5 syntax)
100 mount(ToastContainer, {92 mount(ToastContainer, {
101- target: rootElement93+ target: rootElement as unknown as Element
102 });94 });
103 95
96+ // Signal isolated GUI readiness for page-world bridge
97+ try {
98+ document.documentElement.setAttribute('data-dumber-gui', 'ready');
99+ document.dispatchEvent(new CustomEvent('dumber:gui-ready'));
100+ } catch {
101+ // Ignore errors in GUI readiness signaling
102+ }
103+
104+ // Listen for page-world bridge events and forward to internal toast functions
105+ const handleShowToast = (e: Event) => {
106+ const detail = (e as CustomEvent).detail || {};
107+ if (typeof window.__dumber_showToast === 'function') {
108+ window.__dumber_showToast(detail.message, detail.duration, detail.type);
109+ }
110+ };
111+ document.addEventListener('dumber:showToast', handleShowToast);
112+ document.addEventListener('dumber:toast:show', handleShowToast);
113+
114+ const handleZoomToast = (e: Event) => {
115+ const detail = (e as CustomEvent).detail || {};
116+ if (typeof window.__dumber_showZoomToast === 'function') {
117+ window.__dumber_showZoomToast(detail.level);
118+ }
119+ };
120+ document.addEventListener('dumber:showZoomToast', handleZoomToast);
121+ document.addEventListener('dumber:toast:zoom', handleZoomToast);
122+
104 // Force immediate effect execution to ensure onMount callbacks run123 // Force immediate effect execution to ensure onMount callbacks run
105 flushSync();124 flushSync();
106 125

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