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