v0.10.0 0f4e9eb

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 { 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}

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