detached c18691d

feat(omnibox): enhance accessibility with keyboard navigation and blur animation improvements

commit
c18691d8668d45ba8d61a076ab6f8989d50a6b50
author
bamen · · 2025-09-16 08:13 UTC
parent
57beb4a
signature
unsigned
4 files · +129 -44
M gui/src/components/omnibox/Omnibox.svelte +87 -19
@@ -18,6 +18,7 @@
1818
1919 // Component refs
2020 let boxElement: HTMLDivElement;
21+ let blurLayerElement: HTMLDivElement;
2122 let inputElement = $state<HTMLInputElement>();
2223
2324 // Responsive styling state
@@ -78,23 +79,52 @@
7879
7980
8081 // Apply faded styling effect (matching original JS implementation)
82+ // NOTE: to avoid text becoming blurry while animating, we animate the
83+ // blur via an absolutely-positioned layer's opacity instead of
84+ // animating `backdrop-filter` on the container. This keeps the text
85+ // on a separate compositing layer and prevents browser antialiasing
86+ // artifacts during transitions.
8187 $effect(() => {
82- if (!boxElement || !inputElement) return;
88+ if (!boxElement || !inputElement || !blurLayerElement) return;
89+
90+ // Ensure subtle transitions are present on the container so
91+ // background-color (and other cheap properties) animate smoothly.
92+ try {
93+ // container transitions (background & color)
94+ if (!boxElement.style.transition) {
95+ boxElement.style.transition = 'background-color 120ms ease';
96+ }
97+ } catch (e) {
98+ // defensive: ignore if setting styles fails for some reason
99+ }
83100
84101 if (faded) {
85- // Faded state - semi-transparent with backdrop blur
86- boxElement.style.background = 'rgba(27,27,27,0.25)';
87- boxElement.style.backdropFilter = 'blur(2px) saturate(110%)';
88- (boxElement.style as any).webkitBackdropFilter = 'blur(2px) saturate(110%)';
102+ // Faded state - semi-transparent backgrounds. The blur itself
103+ // is provided by the blur layer whose opacity we animate.
104+ boxElement.style.background = 'rgba(27,27,27,0.35)';
89105 inputElement.style.background = 'rgba(18,18,18,0.35)';
90106 inputElement.style.color = '#eee';
107+
108+ // Small black text border/shadow (2px, 75% opacity) when faded.
109+ // Keep this immediate to avoid animating text-shadow (which can
110+ // trigger repaints that blur text).
111+ const textShadow = '1px 1px 1px rgba(0,0,0,0.75)';
112+ inputElement.style.textShadow = textShadow;
113+ boxElement.style.textShadow = textShadow;
114+
115+ // Show the blur layer (opacity animation defined in CSS)
116+ blurLayerElement.style.opacity = '1';
91117 } else {
92- // Normal state - solid background
118+ // Normal state - solid background, hide blur layer.
93119 boxElement.style.background = '#1b1b1b';
94- boxElement.style.backdropFilter = '';
95- (boxElement.style as any).webkitBackdropFilter = '';
96120 inputElement.style.background = '#121212';
97121 inputElement.style.color = '#eee';
122+
123+ // Remove text shadow in normal state
124+ inputElement.style.textShadow = '';
125+ boxElement.style.textShadow = '';
126+
127+ blurLayerElement.style.opacity = '0';
98128 }
99129 });
100130
@@ -178,6 +208,14 @@
178208 };
179209
180210 console.log('✅ Omnibox API exposed:', Object.keys(window.__dumber_omnibox));
211+
212+ // Check for pending suggestions that arrived before API was ready
213+ if ((window as any).__dumber_omnibox_pending_suggestions) {
214+ console.log('🔄 [OMNIBOX] Processing pending suggestions');
215+ const pending = (window as any).__dumber_omnibox_pending_suggestions;
216+ omniboxBridge.setSuggestions(pending);
217+ delete (window as any).__dumber_omnibox_pending_suggestions;
218+ }
181219 } catch (error) {
182220 console.error('❌ Failed to set up omnibox global API:', error);
183221 }
@@ -220,7 +258,7 @@
220258 <!-- Main omnibox container -->
221259 <div
222260 bind:this={boxElement}
223- class="bg-[#1b1b1b] border border-[#444] rounded-lg shadow-[0_10px_30px_rgba(0,0,0,0.6)] text-[#eee]"
261+ class="bg-[#1b1b1b] border border-[#121212] rounded shadow-[0_10px_30px_rgba(0,0,0,0.6)] text-[#eee]"
224262 style="position: relative !important;
225263 left: 50% !important;
226264 transform: translateX(-50%) !important;
@@ -229,7 +267,7 @@
229267 margin-right: 0 !important;
230268 width: {responsiveStyles.width};
231269 padding: {responsiveStyles.padding};
232- font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif;
270+ font-family: 'Fira sans', system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif;
233271 pointer-events: auto !important;
234272 box-sizing: border-box !important;"
235273 onmousedown={handleBoxClick}
@@ -239,18 +277,48 @@
239277 aria-modal="true"
240278 aria-label={mode === 'find' ? 'Find in page' : 'Omnibox search'}
241279 >
242- <!-- Input component -->
243- <OmniboxInput bind:inputElement {responsiveStyles} />
244-
245- <!-- Content based on mode -->
246- {#if mode === 'omnibox'}
247- <OmniboxSuggestions />
248- {:else if mode === 'find'}
249- <OmniboxFind />
250- {/if}
280+ <!-- Blur layer: absolute full-cover element providing backdrop blur.
281+ We animate its opacity rather than the container's backdrop-filter
282+ to avoid causing text to blur during transitions. -->
283+ <div
284+ bind:this={blurLayerElement}
285+ class="dumber-omnibox-blur-layer"
286+ aria-hidden="true"
287+ style="position: absolute; inset: 0; pointer-events: none; border-radius: inherit; opacity: 0;"
288+ ></div>
289+ <div class="dumber-omnibox-content">
290+ <!-- Input component -->
291+ <OmniboxInput bind:inputElement {responsiveStyles} />
292+
293+ <!-- Content based on mode -->
294+ {#if mode === 'omnibox'}
295+ <OmniboxSuggestions />
296+ {:else if mode === 'find'}
297+ <OmniboxFind />
298+ {/if}
299+ </div>
251300 </div>
252301 </div>
253302
254303 <style>
304+ /* Blur layer styles: use `backdrop-filter` but animate opacity only. */
305+ .dumber-omnibox-blur-layer {
306+ backdrop-filter: blur(2px) saturate(110%);
307+ -webkit-backdrop-filter: blur(2px) saturate(110%);
308+ background: rgba(0,0,0,0.12); /* slight tint so blur looks natural */
309+ transition: opacity 160ms ease;
310+ will-change: opacity;
311+ z-index: 0;
312+ mix-blend-mode: normal;
313+ }
314+
315+ /* Ensure the actual content (input, suggestions) sits above the blur layer */
316+ .dumber-omnibox-content {
317+ position: relative;
318+ z-index: 1;
319+ /* promote to its own layer to avoid being rasterized with the blur */
320+ transform: translateZ(0);
321+ backface-visibility: hidden;
322+ }
255323 /* Additional custom styles if needed */
256324 </style>⏎̸
@@ -18,6 +18,7 @@
18 18
19 // Component refs19 // Component refs
20 let boxElement: HTMLDivElement;20 let boxElement: HTMLDivElement;
21+ let blurLayerElement: HTMLDivElement;
21 let inputElement = $state<HTMLInputElement>();22 let inputElement = $state<HTMLInputElement>();
22 23
23 // Responsive styling state24 // Responsive styling state
@@ -78,23 +79,52 @@
78 79
79 80
80 // Apply faded styling effect (matching original JS implementation)81 // Apply faded styling effect (matching original JS implementation)
82+ // NOTE: to avoid text becoming blurry while animating, we animate the
83+ // blur via an absolutely-positioned layer's opacity instead of
84+ // animating `backdrop-filter` on the container. This keeps the text
85+ // on a separate compositing layer and prevents browser antialiasing
86+ // artifacts during transitions.
81 $effect(() => {87 $effect(() => {
82- if (!boxElement || !inputElement) return;88+ if (!boxElement || !inputElement || !blurLayerElement) return;
89+
90+ // Ensure subtle transitions are present on the container so
91+ // background-color (and other cheap properties) animate smoothly.
92+ try {
93+ // container transitions (background & color)
94+ if (!boxElement.style.transition) {
95+ boxElement.style.transition = 'background-color 120ms ease';
96+ }
97+ } catch (e) {
98+ // defensive: ignore if setting styles fails for some reason
99+ }
83 100
84 if (faded) {101 if (faded) {
85- // Faded state - semi-transparent with backdrop blur102+ // Faded state - semi-transparent backgrounds. The blur itself
86- boxElement.style.background = 'rgba(27,27,27,0.25)';103+ // is provided by the blur layer whose opacity we animate.
87- boxElement.style.backdropFilter = 'blur(2px) saturate(110%)';104+ boxElement.style.background = 'rgba(27,27,27,0.35)';
88- (boxElement.style as any).webkitBackdropFilter = 'blur(2px) saturate(110%)';
89 inputElement.style.background = 'rgba(18,18,18,0.35)';105 inputElement.style.background = 'rgba(18,18,18,0.35)';
90 inputElement.style.color = '#eee';106 inputElement.style.color = '#eee';
107+
108+ // Small black text border/shadow (2px, 75% opacity) when faded.
109+ // Keep this immediate to avoid animating text-shadow (which can
110+ // trigger repaints that blur text).
111+ const textShadow = '1px 1px 1px rgba(0,0,0,0.75)';
112+ inputElement.style.textShadow = textShadow;
113+ boxElement.style.textShadow = textShadow;
114+
115+ // Show the blur layer (opacity animation defined in CSS)
116+ blurLayerElement.style.opacity = '1';
91 } else {117 } else {
92- // Normal state - solid background118+ // Normal state - solid background, hide blur layer.
93 boxElement.style.background = '#1b1b1b';119 boxElement.style.background = '#1b1b1b';
94- boxElement.style.backdropFilter = '';
95- (boxElement.style as any).webkitBackdropFilter = '';
96 inputElement.style.background = '#121212';120 inputElement.style.background = '#121212';
97 inputElement.style.color = '#eee';121 inputElement.style.color = '#eee';
122+
123+ // Remove text shadow in normal state
124+ inputElement.style.textShadow = '';
125+ boxElement.style.textShadow = '';
126+
127+ blurLayerElement.style.opacity = '0';
98 }128 }
99 });129 });
100 130
@@ -178,6 +208,14 @@
178 };208 };
179 209
180 console.log('✅ Omnibox API exposed:', Object.keys(window.__dumber_omnibox));210 console.log('✅ Omnibox API exposed:', Object.keys(window.__dumber_omnibox));
211+
212+ // Check for pending suggestions that arrived before API was ready
213+ if ((window as any).__dumber_omnibox_pending_suggestions) {
214+ console.log('🔄 [OMNIBOX] Processing pending suggestions');
215+ const pending = (window as any).__dumber_omnibox_pending_suggestions;
216+ omniboxBridge.setSuggestions(pending);
217+ delete (window as any).__dumber_omnibox_pending_suggestions;
218+ }
181 } catch (error) {219 } catch (error) {
182 console.error('❌ Failed to set up omnibox global API:', error);220 console.error('❌ Failed to set up omnibox global API:', error);
183 }221 }
@@ -220,7 +258,7 @@
220 <!-- Main omnibox container -->258 <!-- Main omnibox container -->
221 <div259 <div
222 bind:this={boxElement}260 bind:this={boxElement}
223- class="bg-[#1b1b1b] border border-[#444] rounded-lg shadow-[0_10px_30px_rgba(0,0,0,0.6)] text-[#eee]"261+ class="bg-[#1b1b1b] border border-[#121212] rounded shadow-[0_10px_30px_rgba(0,0,0,0.6)] text-[#eee]"
224 style="position: relative !important;262 style="position: relative !important;
225 left: 50% !important;263 left: 50% !important;
226 transform: translateX(-50%) !important;264 transform: translateX(-50%) !important;
@@ -229,7 +267,7 @@
229 margin-right: 0 !important;267 margin-right: 0 !important;
230 width: {responsiveStyles.width};268 width: {responsiveStyles.width};
231 padding: {responsiveStyles.padding};269 padding: {responsiveStyles.padding};
232- font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif;270+ font-family: 'Fira sans', system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Arial, sans-serif;
233 pointer-events: auto !important;271 pointer-events: auto !important;
234 box-sizing: border-box !important;"272 box-sizing: border-box !important;"
235 onmousedown={handleBoxClick}273 onmousedown={handleBoxClick}
@@ -239,18 +277,48 @@
239 aria-modal="true"277 aria-modal="true"
240 aria-label={mode === 'find' ? 'Find in page' : 'Omnibox search'}278 aria-label={mode === 'find' ? 'Find in page' : 'Omnibox search'}
241 >279 >
242- <!-- Input component -->280+ <!-- Blur layer: absolute full-cover element providing backdrop blur.
243- <OmniboxInput bind:inputElement {responsiveStyles} />281+ We animate its opacity rather than the container's backdrop-filter
244-282+ to avoid causing text to blur during transitions. -->
245- <!-- Content based on mode -->283+ <div
246- {#if mode === 'omnibox'}284+ bind:this={blurLayerElement}
247- <OmniboxSuggestions />285+ class="dumber-omnibox-blur-layer"
248- {:else if mode === 'find'}286+ aria-hidden="true"
249- <OmniboxFind />287+ style="position: absolute; inset: 0; pointer-events: none; border-radius: inherit; opacity: 0;"
250- {/if}288+ ></div>
289+ <div class="dumber-omnibox-content">
290+ <!-- Input component -->
291+ <OmniboxInput bind:inputElement {responsiveStyles} />
292+
293+ <!-- Content based on mode -->
294+ {#if mode === 'omnibox'}
295+ <OmniboxSuggestions />
296+ {:else if mode === 'find'}
297+ <OmniboxFind />
298+ {/if}
299+ </div>
251 </div>300 </div>
252 </div>301 </div>
253 302
254 <style>303 <style>
304+ /* Blur layer styles: use `backdrop-filter` but animate opacity only. */
305+ .dumber-omnibox-blur-layer {
306+ backdrop-filter: blur(2px) saturate(110%);
307+ -webkit-backdrop-filter: blur(2px) saturate(110%);
308+ background: rgba(0,0,0,0.12); /* slight tint so blur looks natural */
309+ transition: opacity 160ms ease;
310+ will-change: opacity;
311+ z-index: 0;
312+ mix-blend-mode: normal;
313+ }
314+
315+ /* Ensure the actual content (input, suggestions) sits above the blur layer */
316+ .dumber-omnibox-content {
317+ position: relative;
318+ z-index: 1;
319+ /* promote to its own layer to avoid being rasterized with the blur */
320+ transform: translateZ(0);
321+ backface-visibility: hidden;
322+ }
255 /* Additional custom styles if needed */323 /* Additional custom styles if needed */
256 </style>⏎̸324 </style>⏎̸
M gui/src/components/omnibox/OmniboxInput.svelte +1 -1
@@ -189,7 +189,7 @@
189189 type="text"
190190 {placeholder}
191191 value={inputValue}
192- class="w-full box-border bg-[#121212] text-[#eee] border border-[#555] rounded-md focus:outline-none"
192+ class="w-full box-border bg-[#121212] text-[#eee] border border-[#333] rounded focus:outline-none"
193193 style="padding: {responsiveStyles?.inputPadding || '10px 12px'};
194194 font-size: {responsiveStyles?.fontSize || '16px'};
195195 box-sizing: border-box"
@@ -189,7 +189,7 @@
189 type="text"189 type="text"
190 {placeholder}190 {placeholder}
191 value={inputValue}191 value={inputValue}
192- class="w-full box-border bg-[#121212] text-[#eee] border border-[#555] rounded-md focus:outline-none"192+ class="w-full box-border bg-[#121212] text-[#eee] border border-[#333] rounded focus:outline-none"
193 style="padding: {responsiveStyles?.inputPadding || '10px 12px'};193 style="padding: {responsiveStyles?.inputPadding || '10px 12px'};
194 font-size: {responsiveStyles?.fontSize || '16px'};194 font-size: {responsiveStyles?.fontSize || '16px'};
195 box-sizing: border-box"195 box-sizing: border-box"
M gui/src/components/omnibox/OmniboxSuggestions.svelte +10 -1
@@ -30,6 +30,14 @@
3030 omniboxStore.close();
3131 }
3232
33+ // Handle keyboard activation on suggestion item
34+ function handleItemKeyDown(event: KeyboardEvent, suggestion: any) {
35+ if (event.key === 'Enter' || event.key === ' ') {
36+ event.preventDefault();
37+ handleItemClick(suggestion);
38+ }
39+ }
40+
3341 // Scroll list to show selected item
3442 function scrollToSelection() {
3543 const selectedItem = document.getElementById(`omnibox-item-${selectedIndex}`);
@@ -92,10 +100,11 @@
92100 border-b border-[#2a2a2a] last:border-b-0
93101 {isSelected ? 'bg-[#0a0a0a]' : ''}"
94102 role="option"
95- tabindex="-1"
103+ tabindex={isSelected ? 0 : -1}
96104 aria-selected={isSelected}
97105 onmouseenter={() => handleItemMouseEnter(index)}
98106 onclick={() => handleItemClick(suggestion)}
107+ onkeydown={(e) => handleItemKeyDown(e, suggestion)}
99108 >
100109 <!-- Favicon chip -->
101110 {#if suggestion.favicon}
@@ -30,6 +30,14 @@
30 omniboxStore.close();30 omniboxStore.close();
31 }31 }
32 32
33+ // Handle keyboard activation on suggestion item
34+ function handleItemKeyDown(event: KeyboardEvent, suggestion: any) {
35+ if (event.key === 'Enter' || event.key === ' ') {
36+ event.preventDefault();
37+ handleItemClick(suggestion);
38+ }
39+ }
40+
33 // Scroll list to show selected item41 // Scroll list to show selected item
34 function scrollToSelection() {42 function scrollToSelection() {
35 const selectedItem = document.getElementById(`omnibox-item-${selectedIndex}`);43 const selectedItem = document.getElementById(`omnibox-item-${selectedIndex}`);
@@ -92,10 +100,11 @@
92 border-b border-[#2a2a2a] last:border-b-0100 border-b border-[#2a2a2a] last:border-b-0
93 {isSelected ? 'bg-[#0a0a0a]' : ''}"101 {isSelected ? 'bg-[#0a0a0a]' : ''}"
94 role="option"102 role="option"
95- tabindex="-1"103+ tabindex={isSelected ? 0 : -1}
96 aria-selected={isSelected}104 aria-selected={isSelected}
97 onmouseenter={() => handleItemMouseEnter(index)}105 onmouseenter={() => handleItemMouseEnter(index)}
98 onclick={() => handleItemClick(suggestion)}106 onclick={() => handleItemClick(suggestion)}
107+ onkeydown={(e) => handleItemKeyDown(e, suggestion)}
99 >108 >
100 <!-- Favicon chip -->109 <!-- Favicon chip -->
101 {#if suggestion.favicon}110 {#if suggestion.favicon}
M gui/src/components/omnibox/messaging.ts +31 -23
@@ -8,22 +8,30 @@ import type { OmniboxMessage, OmniboxMessageBridge, Suggestion } from './types';
88 import { omniboxStore } from './stores.svelte.ts';
99
1010 export class OmniboxBridge implements OmniboxMessageBridge {
11- /**
12- * Send message to Go backend via WebKit message handler
13- */
11+ // Forward messages to the native WebKit message handler when available
12+ // Fallback: for navigate, perform direct location change
1413 postMessage(msg: OmniboxMessage): void {
15- try {
16- console.log('📨 Sending message to Go backend:', msg);
17- const messageHandler = window.webkit?.messageHandlers?.dumber;
18- if (messageHandler && messageHandler.postMessage) {
19- const jsonMsg = JSON.stringify(msg);
20- console.log('📨 JSON message:', jsonMsg);
21- messageHandler.postMessage(jsonMsg);
22- } else {
23- console.warn('WebKit message handler not available');
14+ console.log('📤 [DEBUG] Posting message to backend:', msg);
15+ // NOTE: Do NOT detach postMessage from its receiver. In WKWebView,
16+ // postMessage must be called on the UserMessageHandler instance.
17+ const bridge = window.webkit?.messageHandlers?.dumber;
18+ if (bridge && typeof bridge.postMessage === 'function') {
19+ try {
20+ console.log('📱 [DEBUG] Using webkit message handler');
21+ bridge.postMessage(JSON.stringify(msg));
22+ return;
23+ } catch (e) {
24+ console.warn('postMessage to native handler failed, using fallback:', e);
25+ }
26+ }
27+ console.log('⚠️ [DEBUG] No webkit bridge available');
28+ // Fallback navigation if no native bridge is available
29+ if (msg.type === 'navigate' && typeof msg.url === 'string' && msg.url) {
30+ try {
31+ window.location.href = msg.url;
32+ } catch (e) {
33+ console.error('Fallback navigation failed:', e);
2434 }
25- } catch (error) {
26- console.error('Failed to send message to Go backend:', error);
2735 }
2836 }
2937
@@ -31,6 +39,7 @@ export class OmniboxBridge implements OmniboxMessageBridge {
3139 * Update suggestions from Go backend
3240 */
3341 setSuggestions(suggestions: Suggestion[]): void {
42+ console.log('📝 [DEBUG] Received suggestions from backend:', suggestions);
3443 omniboxStore.updateSuggestions(suggestions);
3544 }
3645
@@ -39,22 +48,21 @@ export class OmniboxBridge implements OmniboxMessageBridge {
3948 */
4049 navigate(url: string): void {
4150 console.log('🚀 Omnibox navigate called with:', url);
42- this.postMessage({
43- type: 'navigate',
44- url
45- });
51+ this.postMessage({ type: 'navigate', url });
4652 }
4753
4854 /**
4955 * Handle search query
5056 */
5157 query(searchTerm: string, limit?: number): void {
52- this.postMessage({
53- type: 'query',
54- q: searchTerm,
55- limit: limit || omniboxStore.config.defaultLimit
56- });
58+ const q = searchTerm ?? '';
59+ const lim = limit || omniboxStore.config.defaultLimit;
60+ console.log('🔍 [DEBUG] Sending query to backend:', { q, limit: lim });
61+ // Send to native handler; Go will compute suggestions and call setSuggestions
62+ this.postMessage({ type: 'query', q, limit: lim });
5763 }
64+
65+ // Suggestions are returned via setSuggestions() when native handler responds
5866 }
5967
6068 // Singleton instance
@@ -8,22 +8,30 @@ import type { OmniboxMessage, OmniboxMessageBridge, Suggestion } from './types';
8 import { omniboxStore } from './stores.svelte.ts';8 import { omniboxStore } from './stores.svelte.ts';
9 9
10 export class OmniboxBridge implements OmniboxMessageBridge {10 export class OmniboxBridge implements OmniboxMessageBridge {
11- /**11+ // Forward messages to the native WebKit message handler when available
12- * Send message to Go backend via WebKit message handler12+ // Fallback: for navigate, perform direct location change
13- */
14 postMessage(msg: OmniboxMessage): void {13 postMessage(msg: OmniboxMessage): void {
15- try {14+ console.log('📤 [DEBUG] Posting message to backend:', msg);
16- console.log('📨 Sending message to Go backend:', msg);15+ // NOTE: Do NOT detach postMessage from its receiver. In WKWebView,
17- const messageHandler = window.webkit?.messageHandlers?.dumber;16+ // postMessage must be called on the UserMessageHandler instance.
18- if (messageHandler && messageHandler.postMessage) {17+ const bridge = window.webkit?.messageHandlers?.dumber;
19- const jsonMsg = JSON.stringify(msg);18+ if (bridge && typeof bridge.postMessage === 'function') {
20- console.log('📨 JSON message:', jsonMsg);19+ try {
21- messageHandler.postMessage(jsonMsg);20+ console.log('📱 [DEBUG] Using webkit message handler');
22- } else {21+ bridge.postMessage(JSON.stringify(msg));
23- console.warn('WebKit message handler not available');22+ return;
23+ } catch (e) {
24+ console.warn('postMessage to native handler failed, using fallback:', e);
25+ }
26+ }
27+ console.log('⚠️ [DEBUG] No webkit bridge available');
28+ // Fallback navigation if no native bridge is available
29+ if (msg.type === 'navigate' && typeof msg.url === 'string' && msg.url) {
30+ try {
31+ window.location.href = msg.url;
32+ } catch (e) {
33+ console.error('Fallback navigation failed:', e);
24 }34 }
25- } catch (error) {
26- console.error('Failed to send message to Go backend:', error);
27 }35 }
28 }36 }
29 37
@@ -31,6 +39,7 @@ export class OmniboxBridge implements OmniboxMessageBridge {
31 * Update suggestions from Go backend39 * Update suggestions from Go backend
32 */40 */
33 setSuggestions(suggestions: Suggestion[]): void {41 setSuggestions(suggestions: Suggestion[]): void {
42+ console.log('📝 [DEBUG] Received suggestions from backend:', suggestions);
34 omniboxStore.updateSuggestions(suggestions);43 omniboxStore.updateSuggestions(suggestions);
35 }44 }
36 45
@@ -39,22 +48,21 @@ export class OmniboxBridge implements OmniboxMessageBridge {
39 */48 */
40 navigate(url: string): void {49 navigate(url: string): void {
41 console.log('🚀 Omnibox navigate called with:', url);50 console.log('🚀 Omnibox navigate called with:', url);
42- this.postMessage({51+ this.postMessage({ type: 'navigate', url });
43- type: 'navigate',
44- url
45- });
46 }52 }
47 53
48 /**54 /**
49 * Handle search query55 * Handle search query
50 */56 */
51 query(searchTerm: string, limit?: number): void {57 query(searchTerm: string, limit?: number): void {
52- this.postMessage({58+ const q = searchTerm ?? '';
53- type: 'query',59+ const lim = limit || omniboxStore.config.defaultLimit;
54- q: searchTerm,60+ console.log('🔍 [DEBUG] Sending query to backend:', { q, limit: lim });
55- limit: limit || omniboxStore.config.defaultLimit61+ // Send to native handler; Go will compute suggestions and call setSuggestions
56- });62+ this.postMessage({ type: 'query', q, limit: lim });
57 }63 }
64+
65+ // Suggestions are returned via setSuggestions() when native handler responds
58 }66 }
59 67
60 // Singleton instance68 // Singleton instance

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