v0.11.0 dc50343

gui.ts RawHistoryFolder

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}

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