1/**
2 * Dumber GUI bootstrap routine shared between injected pages and special schemes.
3 *
4 * Sets up toast, omnibox, keyboard service, and workspace event listeners.
5 */
6
7import { initializeToast, type ToastConfig } from "./modules/toast";
8import {
9 initializeOmnibox,
10 type OmniboxInitConfig,
11} from "./modules/omnibox";
12import "./modules/workspace"; // Auto-initializes pane mode UI notifications
13import { keyboardService, type KeyboardService } from "$lib/keyboard";
14import type { Suggestion } from "../components/omnibox/types";
15
16declare global {
17 interface Window {
18 __dumber_gui?: DumberGUI;
19 __dumber_gui_ready?: boo
20 __dumber_gui_ready_for?: Document | null;
21 __dumber_showToast?: (
22 message: string,
23 duration?: number,
24 type?: "info" | "success" | "error",
25 ) => number | void;
26 __dumber_showZoomToast?: (level: number) => void;
27 __dumber_keyboard?: KeyboardService;
28 __dumber_omnibox?: {
29 setSuggestions: (suggestions: Suggestion[]) => void;
30 toggle: () => void;
31 open: (mode?: string, query?: string) => void;
32 close: () => void;
33 findQuery: (query: string) => void;
34 setActive: (active: boolean) => void;
35 };
36 __dumber_gui_bootstrap?: () => void;
37 __dumber_toggle?: () => void;
38 __dumber_find_open?: (query?: string) => void;
39 __dumber_find_close?: () => void;
40 __dumber_find_query?: (query: string) => void;
41 __dumber_dismissToast?: (id: number) => void;
42 __dumber_clearToasts?: () => void;
43 }
44}
45
46interface DumberGUI {
47 initializeToast: (config?: ToastConfig) => Promise<void>;
48 initializeOmnibox: (config?: OmniboxInitConfig) => Promise<void>;
49 keyboard: KeyboardService;
50 isReady: boolean;
51}
52
53function whenDOMReady(callback: () => void) {
54 if (document.readyState === "loading") {
55 document.addEventListener("DOMContentLoaded", callback, { once: true });
56 } else if (document.readyState === "interactive" && !document.body) {
57 setTimeout(() => whenDOMReady(callback), 10);
58 } else {
59 callback();
60 }
61}
62
63function normalizeWebviewId(value: unknown): number | null {
64 if (typeof value === "number" && Number.isFinite(value)) {
65 return value;
66 }
67 if (typeof value === "string") {
68 const trimmed = value.trim();
69 if (!trimmed || trimmed === "__WEBVIEW_ID__") {
70 return null;
71 }
72 const parsed = Number.parseInt(trimmed, 10);
73 return Number.isNaN(parsed) ? null : parsed;
74 }
75
76 return null;
77}
78
79export function bootstrapGUI(): void {
80 if (window.__dumber_gui_ready_for === document) {
81 return;
82 }
83
84 if (window.self !== window.top) {
85 console.log("🚫 Dumber GUI skipped in iframe");
86 return;
87 }
88
89 window.__dumber_gui_ready_for = document;
90 window.__dumber_gui_ready = true;
91
92 try {
93 window.addEventListener("pagehide", () => {
94 window.__dumber_gui_ready = false;
95 window.__dumber_gui_ready_for = null;
96 }, { once: true });
97 } catch {
98 // Ignore environments without pagehide support
99 }
100
101 let workspaceHasFocus = false;
102 const initialRawWebViewId = window.__dumber_webview_id;
103 let currentWebViewId = normalizeWebviewId(initialRawWebViewId);
104 if (currentWebViewId !== null) {
105 window.__dumber_webview_id = currentWebViewId;
106 }
107 let hasReceivedFocusEvent = false;
108
109 if (currentWebViewId === null) {
110 console.log("[workspace] WebView ID unknown, requesting from Go backend");
111 if (window.webkit?.messageHandlers?.dumber) {
112 window.webkit.messageHandlers.dumber.postMessage(
113 JSON.stringify({
114 type: "request-webview-id",
115 payload: { timestamp: Date.now() },
116 }),
117 );
118 }
119 }
120
121 document.addEventListener("dumber:webview-id", (event: Event) => {
122 const detail = (event as CustomEvent).detail || {};
123 const incomingId = normalizeWebviewId(detail.webviewId);
124 if (incomingId !== null && incomingId !== currentWebViewId) {
125 currentWebViewId = incomingId;
126 window.__dumber_webview_id = incomingId;
127 console.log("[workspace] Received webview ID from Go:", incomingId);
128 }
129 });
130
131 document.addEventListener("dumber:workspace-focus", (event: Event) => {
132 const detail = (event as CustomEvent).detail || {};
133 const eventWebviewId = normalizeWebviewId(detail?.webviewId);
134
135 if (eventWebviewId !== null) {
136 if (currentWebViewId === null) {
137 currentWebViewId = eventWebviewId;
138 window.__dumber_webview_id = eventWebviewId;
139 console.log(
140 "[workspace] Learned webview ID from focus event:",
141 eventWebviewId,
142 );
143 } else if (eventWebviewId !== currentWebViewId) {
144 console.log("[workspace] Ignoring focus event for other webview", {
145 eventWebviewId,
146 self: currentWebViewId,
147 detail,
148 });
149 return;
150 }
151 }
152
153 console.log("[workspace focus event]", detail, "prev focus=", workspaceHasFocus);
154
155 hasReceivedFocusEvent = true;
156
157 if (typeof detail?.active === "boolean") {
158 workspaceHasFocus = detail.active;
159 } else {
160 workspaceHasFocus = detail !== false;
161 }
162
163 console.log(
164 "[workspace focus updated] focus=",
165 workspaceHasFocus,
166 "webviewId=",
167 currentWebViewId,
168 );
169
170 if (window.__dumber_omnibox) {
171 if (workspaceHasFocus) {
172 window.__dumber_omnibox.setActive(true);
173 } else {
174 window.__dumber_omnibox.setActive(false);
175 window.__dumber_omnibox.close();
176 }
177 }
178 });
179
180 whenDOMReady(async () => {
181 console.log("[GUI] DOM ready, starting initialization");
182 let toastInitialized = false;
183
184 try {
185 console.log("[GUI] Attempting toast initialization");
186 await initializeToast();
187 toastInitialized = true;
188 console.log("[GUI] Toast initialized successfully");
189 } catch (e) {
190 console.error("❌ Failed to initialize Svelte toast system:", e);
191 throw e;
192 }
193
194 try {
195 await initializeOmnibox();
196 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) {
206 console.error("❌ Failed to initialize omnibox system:", omniboxError);
207 }
208
209 console.log("✅ GUI systems initialization complete", {
210 toast: toastInitialized ? "Svelte" : "Fallback",
211 omnibox: !!window.__dumber_omnibox,
212 });
213 });
214
215 window.__dumber_keyboard = keyboardService;
216
217 whenDOMReady(() => {
218 document.addEventListener(
219 "keydown",
220 (event) => {
221 keyboardService.handleKeyboardEvent(event);
222 },
223 true,
224 );
225
226 document.addEventListener(
227 "mousedown",
228 (event) => {
229 keyboardService.handleMouseEvent(event);
230 },
231 true,
232 );
233
234 console.log("✅ KeyboardService initialized with global listeners");
235
236 document.addEventListener("dumber:key", (e: Event) => {
237 const detail = (e as CustomEvent).detail || {};
238 if (detail && typeof detail.shortcut === "string") {
239 keyboardService.handleNativeShortcut(detail.shortcut);
240 }
241 });
242
243 document.addEventListener("dumber:ui:shortcut", (e: Event) => {
244 const detail = (e as CustomEvent).detail || {};
245 const action = detail?.action;
246 const eventWebViewId = normalizeWebviewId(detail?.webviewId);
247 const source = detail?.source || "unknown";
248
249 if (eventWebViewId !== null && currentWebViewId === null) {
250 currentWebViewId = eventWebViewId;
251 window.__dumber_webview_id = eventWebViewId;
252 console.log(
253 "[dumber shortcuts] Learned webview ID from shortcut event:",
254 eventWebViewId,
255 );
256 }
257
258 console.log("[dumber shortcuts] Event received", {
259 action,
260 eventWebViewId,
261 currentWebViewId,
262 detail,
263 });
264
265 if (typeof action !== "string") {
266 console.log("[dumber shortcuts] No action in event, ignoring");
267 return;
268 }
269
270 const isForThisWebView =
271 eventWebViewId === null || eventWebViewId === currentWebViewId;
272 const isOmniboxAction = action.startsWith("omnibox-");
273 const shouldHandle =
274 isForThisWebView &&
275 (isOmniboxAction
276 ? hasReceivedFocusEvent
277 ? workspaceHasFocus
278 : true
279 : true);
280
281 if (!shouldHandle) {
282 console.log("[dumber shortcuts] ignored action", {
283 action,
284 workspaceHasFocus,
285 isForThisWebView,
286 eventWebViewId,
287 currentWebViewId,
288 source,
289 detail,
290 hasReceivedFocusEvent,
291 });
292 return;
293 }
294
295 console.log("[dumber shortcuts] handling action", {
296 action,
297 source,
298 webviewId: currentWebViewId,
299 });
300
301 const omnibox = window.__dumber_omnibox;
302
303 switch (action) {
304 case "omnibox-nav-toggle":
305 omnibox?.open?.("omnibox", detail?.query);
306 break;
307 case "omnibox-open":
308 omnibox?.open?.("omnibox", detail?.query);
309 break;
310 case "omnibox-find-toggle":
311 omnibox?.open?.("find", detail?.query);
312 break;
313 case "omnibox-close":
314 omnibox?.close?.();
315 break;
316 default:
317 console.warn("[dumber] Unknown UI shortcut action:", action);
318 }
319 });
320 });
321
322 window.__dumber_toggle = () => {
323 try {
324 console.log("🎯 __dumber_toggle called");
325 console.log("🔧 Omnibox API available:", !!window.__dumber_omnibox);
326 if (window.__dumber_omnibox?.toggle) {
327 console.log("✅ Using Svelte omnibox toggle");
328 window.__dumber_omnibox.toggle();
329 return;
330 }
331
332 throw new Error("Omnibox toggle requested but API is unavailable");
333 } catch (error) {
334 console.error("❌ Error in __dumber_toggle:", error);
335 }
336 };
337
338 window.__dumber_find_open = (query?: string) => {
339 try {
340 if (window.__dumber_omnibox?.open) {
341 window.__dumber_omnibox.open("find", query);
342 return;
343 }
344
345 throw new Error("Omnibox find requested but API is unavailable");
346 } catch (error) {
347 console.error("❌ Error in __dumber_find_open:", error);
348 }
349 };
350
351 window.__dumber_find_close = () => {
352 if (window.__dumber_omnibox?.close) {
353 window.__dumber_omnibox.close();
354 return;
355 }
356
357 const error = new Error("Omnibox close requested but API is unavailable");
358 console.error("❌ Error in __dumber_find_close:", error);
359 };
360
361 window.__dumber_find_query = (query: string) => {
362 if (window.__dumber_omnibox?.findQuery) {
363 window.__dumber_omnibox.findQuery(query);
364 }
365 };
366
367 window.__dumber_gui = {
368 initializeToast: async (config?: ToastConfig) => {
369 return await initializeToast(config);
370 },
371 initializeOmnibox: async (config?: OmniboxInitConfig) => {
372 return await initializeOmnibox(config);
373 },
374 keyboard: keyboardService,
375 isReady: true,
376 };
377
378 window.__dumber_gui_bootstrap = function () {
379 console.log("[gui-bootstrap] Initializing GUI for workspace");
380
381 if (!window.__dumber_omnibox) {
382 console.log(
383 `[gui-bootstrap] Omnibox not yet available for webview ${currentWebViewId}, will be loaded when needed`,
384 );
385 } else {
386 console.log(
387 `[gui-bootstrap] Omnibox already available for webview ${currentWebViewId}`,
388 );
389 window.__dumber_omnibox.setActive(workspaceHasFocus);
390 }
391
392 console.log(
393 `[gui-bootstrap] GUI bootstrap complete for webview ${currentWebViewId}`,
394 );
395 };
396}