1/**
2 * Unified GUI Bundle Entry Point
3 *
4 * Single entry point for all WebKit GUI functionality.
5 * Loads immediately and provides modular initialization system.
6 */
7
8// Import all GUI modules
9import { initializeToast, type ToastConfig } from "./modules/toast";
10import { initializeOmnibox, type OmniboxInitConfig } from "./modules/omnibox";
11import {
12 initializeWorkspace,
13 type WorkspaceConfigPayload,
14 type WorkspaceRuntime,
15} from "./modules/workspace";
16import { initializeWindowOpenInterceptor } from "./modules/window-open";
17import { keyboardService, type KeyboardService } from "$lib/keyboard";
18import type { Suggestion } from "../components/omnibox/types";
19// Note: color-scheme module is loaded separately at document-start by WebKit
20
21// Global interface for the unified GUI system
22interface DumberGUI {
23 initializeToast: (config?: ToastConfig) => Promise<void>;
24 initializeOmnibox: (config?: OmniboxInitConfig) => Promise<void>;
25 keyboard: KeyboardService;
26 isReady: boolean;
27}
28
29// (legacy suggestions normalization removed; omnibox now fetches suggestions directly via API)
30
31// DOM readiness utility - handles all document ready states
32function whenDOMReady(callback: () => void) {
33 if (document.readyState === "loading") {
34 document.addEventListener("DOMContentLoaded", callback, { once: true });
35 } else if (document.readyState === "interactive" && !document.body) {
36 // Interactive but no body yet, wait a bit more
37 setTimeout(() => whenDOMReady(callback), 10);
38 } else {
39 // DOM ready and body exists
40 callback();
41 }
42}
43
44// Install a page-world bridge so Go can call window.__dumber_omnibox_suggestions()
45// Page-world bridge is injected by WebKit at document-start; no-op here to avoid duplicates.
46
47// Create global namespace
48declare global {
49 interface Window {
50 __dumber_gui?: DumberGUI;
51 __dumber_gui_ready?: boolean;
52 __dumber_showToast?: (
53 message: string,
54 duration?: number,
55 type?: "info" | "success" | "error",
56 ) => number | void;
57 __dumber_showZoomToast?: (level: number) => void;
58 __dumber_keyboard?: KeyboardService;
59 __dumber_workspace_config?: WorkspaceConfigPayload;
60 __dumber_workspace?: WorkspaceRuntime;
61 __dumber_omnibox?: {
62 setSuggestions: (suggestions: Suggestion[]) => void;
63 toggle: () => void;
64 open: (mode?: string, query?: string) => void;
65 close: () => void;
66 findQuery: (query: string) => void;
67 setActive: (active: boolean) => void;
68 };
69 __dumber_gui_bootstrap?: () => void;
70 // Legacy compatibility functions for Go bridge
71 __dumber_toggle?: () => void;
72 __dumber_find_open?: (query?: string) => void;
73 __dumber_find_close?: () => void;
74 __dumber_find_query?: (query: string) => void;
75 }
76}
77
78// Prevent multiple initialization with enhanced logging
79if (!window.__dumber_gui_ready) {
80 // Skip initialization in iframes - GUI only needed in main frame
81 if (window.self !== window.top) {
82 console.log("🚫 Dumber GUI skipped in iframe");
83 } else {
84 window.__dumber_gui_ready = true;
85
86 let workspaceHasFocus = true; // Assume focus initially, will be corrected by focus events
87 let currentWebViewId = window.__dumber_webview_id || "unknown";
88 let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events
89
90 // Request webview ID from Go if it's unknown (needed for stub builds)
91 if (
92 currentWebViewId === "unknown" ||
93 currentWebViewId === "__WEBVIEW_ID__"
94 ) {
95 console.log("[workspace] WebView ID unknown, requesting from Go backend");
96 if (window.webkit?.messageHandlers?.dumber) {
97 window.webkit.messageHandlers.dumber.postMessage(
98 JSON.stringify({
99 type: "request-webview-id",
100 payload: { timestamp: Date.now() },
101 }),
102 );
103 }
104 }
105
106 // Listen for webview ID from Go backend
107 document.addEventListener("dumber:webview-id", (event: Event) => {
108 const detail = (event as CustomEvent).detail || {};
109 if (detail.webviewId) {
110 currentWebViewId = detail.webviewId;
111 window.__dumber_webview_id = detail.webviewId;
112 console.log(
113 "[workspace] Received webview ID from Go:",
114 detail.webviewId,
115 );
116 }
117 });
118
119 document.addEventListener("dumber:workspace-focus", (event: Event) => {
120 const detail = (event as CustomEvent).detail || {};
121 console.log(
122 "[workspace focus event]",
123 detail,
124 "prev focus=",
125 workspaceHasFocus,
126 );
127
128 hasReceivedFocusEvent = true;
129
130 // Update focus state
131 if (typeof detail?.active === "boolean") {
132 workspaceHasFocus = detail.active;
133 } else {
134 workspaceHasFocus = detail !== false;
135 }
136
137 console.log(
138 "[workspace focus updated] focus=",
139 workspaceHasFocus,
140 "webviewId=",
141 currentWebViewId,
142 );
143
144 // Handle GUI visibility based on focus
145 if (window.__dumber_omnibox) {
146 if (workspaceHasFocus) {
147 window.__dumber_omnibox.setActive(true);
148 } else {
149 window.__dumber_omnibox.setActive(false);
150 window.__dumber_omnibox.close();
151 }
152 }
153 });
154
155 // Initialize Svelte GUI systems immediately
156 whenDOMReady(async () => {
157 try {
158 // Page-world bridge is injected by WebKit; avoid duplicate injection here
159
160 // Initialize window.open interceptor first (must be early)
161 initializeWindowOpenInterceptor();
162
163 // Initialize toast system first
164 await initializeToast();
165
166 // Verify toast functions are available
167 if (typeof window.__dumber_showToast !== "function") {
168 throw new Error("Toast functions not exposed after initialization");
169 }
170
171 // Initialize omnibox system
172 await initializeOmnibox();
173
174 // Initialize workspace controls (pane/tab scaffolding)
175 initializeWorkspace(window.__dumber_workspace_config);
176
177 console.log("✅ All GUI systems initialized successfully");
178 } catch (e) {
179 console.error("❌ Failed to initialize Svelte toast system:", e);
180
181 // Enhanced fallback with better error recovery
182 window.__dumber_showToast = (
183 message: string,
184 duration: number = 2500,
185 ) => {
186 // Remove existing toasts to prevent overlap
187 document
188 .querySelectorAll(".dumber-fallback-toast")
189 .forEach((el) => el.remove());
190
191 const toast = document.createElement("div");
192 toast.className = "dumber-fallback-toast";
193 toast.textContent = message;
194 toast.style.cssText = `
195 position: fixed;
196 bottom: 20px;
197 right: 20px;
198 background: rgba(0,0,0,0.9);
199 color: white;
200 padding: 12px 16px;
201 border-radius: 6px;
202 z-index: 2147483647;
203 font-family: system-ui, -apple-system, sans-serif;
204 font-size: 14px;
205 opacity: 0;
206 transform: translateX(100%);
207 transition: all 0.3s ease;
208 max-width: 300px;
209 word-wrap: break-word;
210 `;
211
212 if (document.body) {
213 document.body.appendChild(toast);
214
215 // Animate in
216 requestAnimationFrame(() => {
217 toast.style.opacity = "1";
218 toast.style.transform = "translateX(0)";
219 });
220
221 // Animate out and remove
222 setTimeout(() => {
223 toast.style.opacity = "0";
224 toast.style.transform = "translateX(100%)";
225 setTimeout(() => toast.remove(), 300);
226 }, duration);
227 }
228 };
229
230 window.__dumber_showZoomToast = (level: number) => {
231 const percentage = Math.round(level * 100);
232 window.__dumber_showToast!(`Zoom: ${percentage}%`, 1500);
233 };
234
235 // Expose other required functions
236 window.__dumber_dismissToast = () => {
237 document
238 .querySelectorAll(".dumber-fallback-toast")
239 .forEach((el) => el.remove());
240 };
241
242 window.__dumber_clearToasts = () => {
243 document
244 .querySelectorAll(".dumber-fallback-toast")
245 .forEach((el) => el.remove());
246 };
247 }
248 });
249
250 // Expose keyboard service globally for Go bridge
251 window.__dumber_keyboard = keyboardService;
252
253 // Initialize global keyboard and mouse event listeners for the keyboard service
254 whenDOMReady(() => {
255 // Global keyboard event listener (capture phase for high priority)
256 document.addEventListener(
257 "keydown",
258 (event) => {
259 keyboardService.handleKeyboardEvent(event);
260 },
261 true,
262 );
263
264 // Global mouse event listener for navigation buttons
265 document.addEventListener(
266 "mousedown",
267 (event) => {
268 keyboardService.handleMouseEvent(event);
269 },
270 true,
271 );
272
273 console.log("✅ KeyboardService initialized with global listeners");
274
275 // Listen to bridge keyboard events from page-world and forward to KeyboardService
276 document.addEventListener("dumber:key", (e: Event) => {
277 const detail = (e as CustomEvent).detail || {};
278 if (detail && typeof detail.shortcut === "string") {
279 keyboardService.handleNativeShortcut(detail.shortcut);
280 }
281 });
282
283 document.addEventListener("dumber:ui:shortcut", (e: Event) => {
284 const detail = (e as CustomEvent).detail || {};
285 const action = detail?.action;
286 const eventWebViewId = detail?.webviewId;
287 const source = detail?.source || "unknown";
288
289 if (typeof action !== "string") {
290 return;
291 }
292
293 // Enhanced focus checking with webview ID validation
294 const isForThisWebView =
295 !eventWebViewId || eventWebViewId === currentWebViewId;
296 // Only apply strict focus checking to omnibox actions, be permissive for others
297 const isOmniboxAction = action.startsWith("omnibox-");
298 const shouldHandle =
299 isForThisWebView &&
300 (isOmniboxAction
301 ? hasReceivedFocusEvent
302 ? workspaceHasFocus
303 : true
304 : true);
305
306 if (!shouldHandle) {
307 console.log("[dumber shortcuts] ignored action", {
308 action,
309 workspaceHasFocus,
310 isForThisWebView,
311 eventWebViewId,
312 currentWebViewId,
313 source,
314 detail,
315 hasReceivedFocusEvent,
316 });
317 return;
318 }
319
320 console.log("[dumber shortcuts] handling action", {
321 action,
322 source,
323 webviewId: currentWebViewId,
324 });
325
326 const omnibox = window.__dumber_omnibox;
327
328 switch (action) {
329 case "omnibox-nav-toggle":
330 omnibox?.open?.("omnibox", detail?.query);
331 break;
332 case "omnibox-open":
333 omnibox?.open?.("omnibox", detail?.query);
334 break;
335 case "omnibox-find-toggle":
336 omnibox?.open?.("find", detail?.query);
337 break;
338 case "omnibox-close":
339 omnibox?.close?.();
340 break;
341 default:
342 console.warn("[dumber] Unknown UI shortcut action:", action);
343 }
344 });
345
346 // Suggestions are now fetched via API directly by the omnibox component; no page-bridge handlers needed
347 });
348
349 // Legacy compatibility functions for existing Go code
350 window.__dumber_toggle = () => {
351 try {
352 console.log("🎯 __dumber_toggle called");
353 console.log("🔧 Omnibox API available:", !!window.__dumber_omnibox);
354 if (window.__dumber_omnibox?.toggle) {
355 console.log("✅ Using Svelte omnibox toggle");
356 window.__dumber_omnibox.toggle();
357 return;
358 }
359
360 throw new Error("Omnibox toggle requested but API is unavailable");
361 } catch (error) {
362 console.error("❌ Error in __dumber_toggle:", error);
363 }
364 };
365
366 window.__dumber_find_open = (query?: string) => {
367 try {
368 if (window.__dumber_omnibox?.open) {
369 window.__dumber_omnibox.open("find", query);
370 return;
371 }
372
373 throw new Error("Omnibox find requested but API is unavailable");
374 } catch (error) {
375 console.error("❌ Error in __dumber_find_open:", error);
376 }
377 };
378
379 window.__dumber_find_close = () => {
380 if (window.__dumber_omnibox?.close) {
381 window.__dumber_omnibox.close();
382 return;
383 }
384
385 const error = new Error("Omnibox close requested but API is unavailable");
386 console.error("❌ Error in __dumber_find_close:", error);
387 };
388
389 // Find query function that Go uses to set find query
390 window.__dumber_find_query = (query: string) => {
391 if (window.__dumber_omnibox?.findQuery) {
392 window.__dumber_omnibox.findQuery(query);
393 }
394 };
395
396 // Create the global GUI object
397 window.__dumber_gui = {
398 initializeToast: async (config?: ToastConfig) => {
399 return await initializeToast(config);
400 },
401
402 initializeOmnibox: async (config?: OmniboxInitConfig) => {
403 return await initializeOmnibox(config);
404 },
405
406 keyboard: keyboardService,
407
408 isReady: true,
409 };
410
411 // Bootstrap function for lazy GUI initialization by workspace manager
412 window.__dumber_gui_bootstrap = function () {
413 console.log("[gui-bootstrap] Initializing GUI for workspace");
414
415 // Pane ID will be updated via focus events
416
417 // Ensure omnibox is available for this pane
418 if (!window.__dumber_omnibox) {
419 console.log(
420 `[gui-bootstrap] Omnibox not yet available for webview ${currentWebViewId}, will be loaded when needed`,
421 );
422 } else {
423 console.log(
424 `[gui-bootstrap] Omnibox already available for webview ${currentWebViewId}`,
425 );
426 // Ensure omnibox is aware of the current pane
427 window.__dumber_omnibox.setActive(workspaceHasFocus);
428 }
429
430 console.log(
431 `[gui-bootstrap] GUI bootstrap complete for webview ${currentWebViewId}`,
432 );
433 };
434 }
435}