detached 72a6840

Fix omnibox initialization flash

commit
72a6840c31a30a79c770957073e7136a0b6aa6a6
author
bnema · · 2025-11-18 22:20 UTC
parent
f83a7ce
signature
unsigned
5 files · +68 -53
M CHANGELOG.md +3 -0
@@ -7,6 +7,9 @@ All notable changes to this project will be documented in this file.
77 ### Added
88 - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits.
99
10+### Fixed
11+- **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation.
12+
1013 ## [0.14.1] - 2025-11-18
1114
1215 ### Added
@@ -7,6 +7,9 @@ All notable changes to this project will be documented in this file.
7 ### Added7 ### Added
8 - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits.8 - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits.
9 9
10+### Fixed
11+- **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation.
12+
10 ## [0.14.1] - 2025-11-1813 ## [0.14.1] - 2025-11-18
11 14
12 ### Added15 ### Added
M gui/src/components/omnibox/Omnibox.svelte +25 -26
@@ -16,6 +16,9 @@
1616 let mode = $derived(omniboxStore.mode);
1717 let faded = $derived(omniboxStore.faded);
1818
19+ // Ready state - prevents flash before initialization
20+ let isReady = $state(false);
21+
1922 // Component refs
2023 let boxElement: HTMLDivElement;
2124 let blurLayerElement: HTMLDivElement;
@@ -136,29 +139,27 @@
136139 // Functions to control main-world event blocking
137140 function enablePageEventBlocking() {
138141 try {
139- // Send message to Go backend to enable keyboard blocking
140- if (window.webkit?.messageHandlers?.dumber) {
141- window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({
142- type: 'keyboard_blocking',
143- action: 'enable'
144- }));
145- }
142+ // Send message to Go backend via bridge (isolated world → main world → Go)
143+ omniboxBridge.postMessage({
144+ type: 'keyboard_blocking',
145+ action: 'enable'
146+ });
147+ console.log('[omnibox] Sent enable keyboard blocking message');
146148 } catch (error) {
147- console.warn('[omnibox] Failed to enable page event blocking:', error);
149+ console.error('[omnibox] Failed to enable page event blocking:', error);
148150 }
149151 }
150152
151153 function disablePageEventBlocking() {
152154 try {
153- // Send message to Go backend to disable keyboard blocking
154- if (window.webkit?.messageHandlers?.dumber) {
155- window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({
156- type: 'keyboard_blocking',
157- action: 'disable'
158- }));
159- }
155+ // Send message to Go backend via bridge (isolated world → main world → Go)
156+ omniboxBridge.postMessage({
157+ type: 'keyboard_blocking',
158+ action: 'disable'
159+ });
160+ console.log('[omnibox] Sent disable keyboard blocking message');
160161 } catch (error) {
161- console.warn('[omnibox] Failed to disable page event blocking:', error);
162+ console.error('[omnibox] Failed to disable page event blocking:', error);
162163 }
163164 }
164165
@@ -369,17 +370,13 @@
369370 omniboxBridge.setSuggestions(pending);
370371 delete (window as any).__dumber_omnibox_pending_suggestions;
371372 }
372-
373- // Auto-open omnibox on about:blank pages
374- if (window.location.href === 'about:blank') {
375- console.log('Auto-opening omnibox for about:blank');
376- omniboxStore.open('omnibox');
377- // Fetch initial history for empty omnibox
378- omniboxBridge.fetchInitialHistory();
379- }
380373 } catch (error) {
381374 console.error('❌ Failed to set up omnibox global API:', error);
382375 }
376+
377+ // Mark as ready - removes initial hiding styles
378+ isReady = true;
379+ console.log('✅ Omnibox fully initialized and ready');
383380 });
384381
385382 onDestroy(() => {
@@ -413,8 +410,10 @@
413410 left: 0 !important;
414411 right: 0 !important;
415412 bottom: 0 !important;
416- z-index: 2147483647 !important;
417- pointer-events: none !important;
413+ z-index: {isReady ? '2147483647' : '-9999'} !important;
414+ visibility: {isReady ? 'visible' : 'hidden'} !important;
415+ opacity: {isReady ? '1' : '0'} !important;
416+ pointer-events: {isReady ? 'none' : 'none'} !important;
418417 margin: 0 !important;
419418 padding: 0 !important;
420419 isolation: isolate;"
@@ -16,6 +16,9 @@
16 let mode = $derived(omniboxStore.mode);16 let mode = $derived(omniboxStore.mode);
17 let faded = $derived(omniboxStore.faded);17 let faded = $derived(omniboxStore.faded);
18 18
19+ // Ready state - prevents flash before initialization
20+ let isReady = $state(false);
21+
19 // Component refs22 // Component refs
20 let boxElement: HTMLDivElement;23 let boxElement: HTMLDivElement;
21 let blurLayerElement: HTMLDivElement;24 let blurLayerElement: HTMLDivElement;
@@ -136,29 +139,27 @@
136 // Functions to control main-world event blocking139 // Functions to control main-world event blocking
137 function enablePageEventBlocking() {140 function enablePageEventBlocking() {
138 try {141 try {
139- // Send message to Go backend to enable keyboard blocking142+ // Send message to Go backend via bridge (isolated world → main world → Go)
140- if (window.webkit?.messageHandlers?.dumber) {143+ omniboxBridge.postMessage({
141- window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({144+ type: 'keyboard_blocking',
142- type: 'keyboard_blocking',145+ action: 'enable'
143- action: 'enable'146+ });
144- }));147+ console.log('[omnibox] Sent enable keyboard blocking message');
145- }
146 } catch (error) {148 } catch (error) {
147- console.warn('[omnibox] Failed to enable page event blocking:', error);149+ console.error('[omnibox] Failed to enable page event blocking:', error);
148 }150 }
149 }151 }
150 152
151 function disablePageEventBlocking() {153 function disablePageEventBlocking() {
152 try {154 try {
153- // Send message to Go backend to disable keyboard blocking155+ // Send message to Go backend via bridge (isolated world → main world → Go)
154- if (window.webkit?.messageHandlers?.dumber) {156+ omniboxBridge.postMessage({
155- window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({157+ type: 'keyboard_blocking',
156- type: 'keyboard_blocking',158+ action: 'disable'
157- action: 'disable'159+ });
158- }));160+ console.log('[omnibox] Sent disable keyboard blocking message');
159- }
160 } catch (error) {161 } catch (error) {
161- console.warn('[omnibox] Failed to disable page event blocking:', error);162+ console.error('[omnibox] Failed to disable page event blocking:', error);
162 }163 }
163 }164 }
164 165
@@ -369,17 +370,13 @@
369 omniboxBridge.setSuggestions(pending);370 omniboxBridge.setSuggestions(pending);
370 delete (window as any).__dumber_omnibox_pending_suggestions;371 delete (window as any).__dumber_omnibox_pending_suggestions;
371 }372 }
372-
373- // Auto-open omnibox on about:blank pages
374- if (window.location.href === 'about:blank') {
375- console.log('Auto-opening omnibox for about:blank');
376- omniboxStore.open('omnibox');
377- // Fetch initial history for empty omnibox
378- omniboxBridge.fetchInitialHistory();
379- }
380 } catch (error) {373 } catch (error) {
381 console.error('❌ Failed to set up omnibox global API:', error);374 console.error('❌ Failed to set up omnibox global API:', error);
382 }375 }
376+
377+ // Mark as ready - removes initial hiding styles
378+ isReady = true;
379+ console.log('✅ Omnibox fully initialized and ready');
383 });380 });
384 381
385 onDestroy(() => {382 onDestroy(() => {
@@ -413,8 +410,10 @@
413 left: 0 !important;410 left: 0 !important;
414 right: 0 !important;411 right: 0 !important;
415 bottom: 0 !important;412 bottom: 0 !important;
416- z-index: 2147483647 !important;413+ z-index: {isReady ? '2147483647' : '-9999'} !important;
417- pointer-events: none !important;414+ visibility: {isReady ? 'visible' : 'hidden'} !important;
415+ opacity: {isReady ? '1' : '0'} !important;
416+ pointer-events: {isReady ? 'none' : 'none'} !important;
418 margin: 0 !important;417 margin: 0 !important;
419 padding: 0 !important;418 padding: 0 !important;
420 isolation: isolate;"419 isolation: isolate;"
M gui/src/injected/bootstrap.ts +15 -20
@@ -60,6 +60,14 @@ function whenDOMReady(callback: () => void) {
6060 }
6161 }
6262
63+function whenPageLoaded(callback: () => void) {
64+ if (document.readyState === "complete") {
65+ callback();
66+ } else {
67+ window.addEventListener("load", callback, { once: true });
68+ }
69+}
70+
6371 function normalizeWebviewId(value: unknown): number | null {
6472 if (typeof value === "number" && Number.isFinite(value)) {
6573 return value;
@@ -177,39 +185,26 @@ export function bootstrapGUI(): void {
177185 }
178186 });
179187
188+ // Initialize toast early (DOM ready is enough)
180189 whenDOMReady(async () => {
181- console.log("[GUI] DOM ready, starting initialization");
182- let toastInitialized = false;
183-
190+ console.log("[GUI] DOM ready, initializing toast");
184191 try {
185- console.log("[GUI] Attempting toast initialization");
186192 await initializeToast();
187- toastInitialized = true;
188193 console.log("[GUI] Toast initialized successfully");
189194 } catch (e) {
190- console.error("❌ Failed to initialize Svelte toast system:", e);
191- throw e;
195+ console.error("❌ Failed to initialize toast system:", e);
192196 }
197+ });
193198
199+ // Initialize omnibox as soon as the DOM is ready (no need to wait for full load)
200+ whenDOMReady(async () => {
201+ console.log("[GUI] DOM ready, initializing omnibox");
194202 try {
195203 await initializeOmnibox();
196204 console.log("✅ Omnibox system initialized successfully");
197-
198- // Auto-open omnibox on about:blank pages
199- if (window.location.href === "about:blank" && window.__dumber_omnibox) {
200- console.log("[about:blank] Auto-opening omnibox");
201- setTimeout(() => {
202- window.__dumber_omnibox?.open?.("omnibox", "");
203- }, 100);
204- }
205205 } catch (omniboxError) {
206206 console.error("❌ Failed to initialize omnibox system:", omniboxError);
207207 }
208-
209- console.log("✅ GUI systems initialization complete", {
210- toast: toastInitialized ? "Svelte" : "Fallback",
211- omnibox: !!window.__dumber_omnibox,
212- });
213208 });
214209
215210 window.__dumber_keyboard = keyboardService;
@@ -60,6 +60,14 @@ function whenDOMReady(callback: () => void) {
60 }60 }
61 }61 }
62 62
63+function whenPageLoaded(callback: () => void) {
64+ if (document.readyState === "complete") {
65+ callback();
66+ } else {
67+ window.addEventListener("load", callback, { once: true });
68+ }
69+}
70+
63 function normalizeWebviewId(value: unknown): number | null {71 function normalizeWebviewId(value: unknown): number | null {
64 if (typeof value === "number" && Number.isFinite(value)) {72 if (typeof value === "number" && Number.isFinite(value)) {
65 return value;73 return value;
@@ -177,39 +185,26 @@ export function bootstrapGUI(): void {
177 }185 }
178 });186 });
179 187
188+ // Initialize toast early (DOM ready is enough)
180 whenDOMReady(async () => {189 whenDOMReady(async () => {
181- console.log("[GUI] DOM ready, starting initialization");190+ console.log("[GUI] DOM ready, initializing toast");
182- let toastInitialized = false;
183-
184 try {191 try {
185- console.log("[GUI] Attempting toast initialization");
186 await initializeToast();192 await initializeToast();
187- toastInitialized = true;
188 console.log("[GUI] Toast initialized successfully");193 console.log("[GUI] Toast initialized successfully");
189 } catch (e) {194 } catch (e) {
190- console.error("❌ Failed to initialize Svelte toast system:", e);195+ console.error("❌ Failed to initialize toast system:", e);
191- throw e;
192 }196 }
197+ });
193 198
199+ // Initialize omnibox as soon as the DOM is ready (no need to wait for full load)
200+ whenDOMReady(async () => {
201+ console.log("[GUI] DOM ready, initializing omnibox");
194 try {202 try {
195 await initializeOmnibox();203 await initializeOmnibox();
196 console.log("✅ Omnibox system initialized successfully");204 console.log("✅ Omnibox system initialized successfully");
197-
198- // Auto-open omnibox on about:blank pages
199- if (window.location.href === "about:blank" && window.__dumber_omnibox) {
200- console.log("[about:blank] Auto-opening omnibox");
201- setTimeout(() => {
202- window.__dumber_omnibox?.open?.("omnibox", "");
203- }, 100);
204- }
205 } catch (omniboxError) {205 } catch (omniboxError) {
206 console.error("❌ Failed to initialize omnibox system:", omniboxError);206 console.error("❌ Failed to initialize omnibox system:", omniboxError);
207 }207 }
208-
209- console.log("✅ GUI systems initialization complete", {
210- toast: toastInitialized ? "Svelte" : "Fallback",
211- omnibox: !!window.__dumber_omnibox,
212- });
213 });208 });
214 209
215 window.__dumber_keyboard = keyboardService;210 window.__dumber_keyboard = keyboardService;
M gui/src/injected/modules/omnibox.ts +19 -3
@@ -36,10 +36,26 @@ export async function initializeOmnibox(
3636 return;
3737 }
3838
39- // Check if DOM is ready
39+ // Ensure a <body> exists before mounting (document-start injection can run before it's available)
4040 if (!document.body) {
41- console.warn("DOM body not ready for omnibox initialization");
42- return;
41+ await new Promise<void>((resolve) => {
42+ let resolved = false;
43+ const finish = () => {
44+ if (!resolved) {
45+ resolved = true;
46+ resolve();
47+ }
48+ };
49+ const check = () => {
50+ if (document.body) {
51+ finish();
52+ } else {
53+ setTimeout(check, 10);
54+ }
55+ };
56+ document.addEventListener("DOMContentLoaded", finish, { once: true });
57+ check();
58+ });
4359 }
4460
4561 // Mount omnibox into the shared global Shadow DOM host
@@ -36,10 +36,26 @@ export async function initializeOmnibox(
36 return;36 return;
37 }37 }
38 38
39- // Check if DOM is ready39+ // Ensure a <body> exists before mounting (document-start injection can run before it's available)
40 if (!document.body) {40 if (!document.body) {
41- console.warn("DOM body not ready for omnibox initialization");41+ await new Promise<void>((resolve) => {
42- return;42+ let resolved = false;
43+ const finish = () => {
44+ if (!resolved) {
45+ resolved = true;
46+ resolve();
47+ }
48+ };
49+ const check = () => {
50+ if (document.body) {
51+ finish();
52+ } else {
53+ setTimeout(check, 10);
54+ }
55+ };
56+ document.addEventListener("DOMContentLoaded", finish, { once: true });
57+ check();
58+ });
43 }59 }
44 60
45 // Mount omnibox into the shared global Shadow DOM host61 // Mount omnibox into the shared global Shadow DOM host
M pkg/webkit/user_content.go +6 -4
@@ -163,8 +163,9 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
163163 log.Printf("[webkit] Injected main-world script (%d bytes, main world)", len(assets.MainWorldScript))
164164 }
165165
166- // Inject GUI controls script in ISOLATED world only
166+ // Inject GUI controls script in ISOLATED world at document-start
167167 // This contains Svelte components and must be protected from page interference
168+ // Loading at document-start ensures the omnibox is available as early as possible
168169 if assets.GUIScript != "" {
169170 ucm.AddScript(webkit.NewUserScriptForWorld(
170171 assets.GUIScript,
@@ -174,12 +175,13 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
174175 nil,
175176 nil,
176177 ))
177- log.Printf("[webkit] Injected GUI controls script (%d bytes, isolated world)", len(assets.GUIScript))
178+ log.Printf("[webkit] Injected GUI controls script (%d bytes, isolated world, document-start)", len(assets.GUIScript))
178179 }
179180
180- // Inject component CSS styles as a JavaScript variable in ISOLATED world
181+ // Inject component CSS styles as a JavaScript variable in ISOLATED world at document-start
181182 // These styles need to be injected into the shadow root by shadowHost.ts
182183 // because Shadow DOM has style encapsulation - external stylesheets don't penetrate it
184+ // Keep at document-start to prevent flash of unstyled content
183185 if assets.ComponentStyles != "" {
184186 // Use fmt.Sprintf with %q to properly escape the CSS string for JavaScript
185187 componentStylesScript := fmt.Sprintf(`window.__dumber_component_styles = %q;`, assets.ComponentStyles)
@@ -192,7 +194,7 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
192194 nil,
193195 nil,
194196 ))
195- log.Printf("[webkit] Injected component styles string (%d bytes, isolated world)", len(assets.ComponentStyles))
197+ log.Printf("[webkit] Injected component styles string (%d bytes, isolated world, document-start)", len(assets.ComponentStyles))
196198 }
197199
198200 // Register script message handler "dumber" in the MAIN world
@@ -163,8 +163,9 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
163 log.Printf("[webkit] Injected main-world script (%d bytes, main world)", len(assets.MainWorldScript))163 log.Printf("[webkit] Injected main-world script (%d bytes, main world)", len(assets.MainWorldScript))
164 }164 }
165 165
166- // Inject GUI controls script in ISOLATED world only166+ // Inject GUI controls script in ISOLATED world at document-start
167 // This contains Svelte components and must be protected from page interference167 // This contains Svelte components and must be protected from page interference
168+ // Loading at document-start ensures the omnibox is available as early as possible
168 if assets.GUIScript != "" {169 if assets.GUIScript != "" {
169 ucm.AddScript(webkit.NewUserScriptForWorld(170 ucm.AddScript(webkit.NewUserScriptForWorld(
170 assets.GUIScript,171 assets.GUIScript,
@@ -174,12 +175,13 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
174 nil,175 nil,
175 nil,176 nil,
176 ))177 ))
177- log.Printf("[webkit] Injected GUI controls script (%d bytes, isolated world)", len(assets.GUIScript))178+ log.Printf("[webkit] Injected GUI controls script (%d bytes, isolated world, document-start)", len(assets.GUIScript))
178 }179 }
179 180
180- // Inject component CSS styles as a JavaScript variable in ISOLATED world181+ // Inject component CSS styles as a JavaScript variable in ISOLATED world at document-start
181 // These styles need to be injected into the shadow root by shadowHost.ts182 // These styles need to be injected into the shadow root by shadowHost.ts
182 // because Shadow DOM has style encapsulation - external stylesheets don't penetrate it183 // because Shadow DOM has style encapsulation - external stylesheets don't penetrate it
184+ // Keep at document-start to prevent flash of unstyled content
183 if assets.ComponentStyles != "" {185 if assets.ComponentStyles != "" {
184 // Use fmt.Sprintf with %q to properly escape the CSS string for JavaScript186 // Use fmt.Sprintf with %q to properly escape the CSS string for JavaScript
185 componentStylesScript := fmt.Sprintf(`window.__dumber_component_styles = %q;`, assets.ComponentStyles)187 componentStylesScript := fmt.Sprintf(`window.__dumber_component_styles = %q;`, assets.ComponentStyles)
@@ -192,7 +194,7 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
192 nil,194 nil,
193 nil,195 nil,
194 ))196 ))
195- log.Printf("[webkit] Injected component styles string (%d bytes, isolated world)", len(assets.ComponentStyles))197+ log.Printf("[webkit] Injected component styles string (%d bytes, isolated world, document-start)", len(assets.ComponentStyles))
196 }198 }
197 199
198 // Register script message handler "dumber" in the MAIN world200 // Register script message handler "dumber" in the MAIN world

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