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