v0.13.0 976176a

bootstrap.ts RawHistoryFolder

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    if (typeof window.__dumber_showToast === "function" && typeof window.__dumber_showZoomToast === "function") {
185      console.log("[GUI] Toast already initialized, skipping");
186      toastInitialized = true;
187    } else {
188      try {
189        console.log("[GUI] Attempting toast initialization");
190        await initializeToast();
191        if (typeof window.__dumber_showToast !== "function") {
192          throw new Error("Toast functions not exposed after initialization");
193        }
194        toastInitialized = true;
195        console.log("[GUI] Toast initialized successfully");
196      } catch (e) {
197        console.error("❌ Failed to initialize Svelte toast system:", e);
198        throw e;
199      }
200    }
201
202    try {
203      await initializeOmnibox();
204      console.log("✅ Omnibox system initialized successfully");
205
206      // Auto-open omnibox on about:blank pages
207      if (window.location.href === "about:blank" && window.__dumber_omnibox) {
208        console.log("[about:blank] Auto-opening omnibox");
209        setTimeout(() => {
210          window.__dumber_omnibox?.open?.("omnibox", "");
211        }, 100);
212      }
213    } catch (omniboxError) {
214      console.error("❌ Failed to initialize omnibox system:", omniboxError);
215    }
216
217    console.log("✅ GUI systems initialization complete", {
218      toast: toastInitialized ? "Svelte" : "Fallback",
219      omnibox: !!window.__dumber_omnibox,
220    });
221  });
222
223  window.__dumber_keyboard = keyboardService;
224
225  whenDOMReady(() => {
226    document.addEventListener(
227      "keydown",
228      (event) => {
229        keyboardService.handleKeyboardEvent(event);
230      },
231      true,
232    );
233
234    document.addEventListener(
235      "mousedown",
236      (event) => {
237        keyboardService.handleMouseEvent(event);
238      },
239      true,
240    );
241
242    console.log("✅ KeyboardService initialized with global listeners");
243
244    document.addEventListener("dumber:key", (e: Event) => {
245      const detail = (e as CustomEvent).detail || {};
246      if (detail && typeof detail.shortcut === "string") {
247        keyboardService.handleNativeShortcut(detail.shortcut);
248      }
249    });
250
251    document.addEventListener("dumber:ui:shortcut", (e: Event) => {
252      const detail = (e as CustomEvent).detail || {};
253      const action = detail?.action;
254      const eventWebViewId = normalizeWebviewId(detail?.webviewId);
255      const source = detail?.source || "unknown";
256
257      if (eventWebViewId !== null && currentWebViewId === null) {
258        currentWebViewId = eventWebViewId;
259        window.__dumber_webview_id = eventWebViewId;
260        console.log(
261          "[dumber shortcuts] Learned webview ID from shortcut event:",
262          eventWebViewId,
263        );
264      }
265
266      console.log("[dumber shortcuts] Event received", {
267        action,
268        eventWebViewId,
269        currentWebViewId,
270        detail,
271      });
272
273      if (typeof action !== "string") {
274        console.log("[dumber shortcuts] No action in event, ignoring");
275        return;
276      }
277
278      const isForThisWebView =
279        eventWebViewId === null || eventWebViewId === currentWebViewId;
280      const isOmniboxAction = action.startsWith("omnibox-");
281      const shouldHandle =
282        isForThisWebView &&
283        (isOmniboxAction
284          ? hasReceivedFocusEvent
285            ? workspaceHasFocus
286            : true
287          : true);
288
289      if (!shouldHandle) {
290        console.log("[dumber shortcuts] ignored action", {
291          action,
292          workspaceHasFocus,
293          isForThisWebView,
294          eventWebViewId,
295          currentWebViewId,
296          source,
297          detail,
298          hasReceivedFocusEvent,
299        });
300        return;
301      }
302
303      console.log("[dumber shortcuts] handling action", {
304        action,
305        source,
306        webviewId: currentWebViewId,
307      });
308
309      const omnibox = window.__dumber_omnibox;
310
311      switch (action) {
312        case "omnibox-nav-toggle":
313          omnibox?.open?.("omnibox", detail?.query);
314          break;
315        case "omnibox-open":
316          omnibox?.open?.("omnibox", detail?.query);
317          break;
318        case "omnibox-find-toggle":
319          omnibox?.open?.("find", detail?.query);
320          break;
321        case "omnibox-close":
322          omnibox?.close?.();
323          break;
324        default:
325          console.warn("[dumber] Unknown UI shortcut action:", action);
326      }
327    });
328  });
329
330  window.__dumber_toggle = () => {
331    try {
332      console.log("🎯 __dumber_toggle called");
333      console.log("🔧 Omnibox API available:", !!window.__dumber_omnibox);
334      if (window.__dumber_omnibox?.toggle) {
335        console.log("✅ Using Svelte omnibox toggle");
336        window.__dumber_omnibox.toggle();
337        return;
338      }
339
340      throw new Error("Omnibox toggle requested but API is unavailable");
341    } catch (error) {
342      console.error("❌ Error in __dumber_toggle:", error);
343    }
344  };
345
346  window.__dumber_find_open = (query?: string) => {
347    try {
348      if (window.__dumber_omnibox?.open) {
349        window.__dumber_omnibox.open("find", query);
350        return;
351      }
352
353      throw new Error("Omnibox find requested but API is unavailable");
354    } catch (error) {
355      console.error("❌ Error in __dumber_find_open:", error);
356    }
357  };
358
359  window.__dumber_find_close = () => {
360    if (window.__dumber_omnibox?.close) {
361      window.__dumber_omnibox.close();
362      return;
363    }
364
365    const error = new Error("Omnibox close requested but API is unavailable");
366    console.error("❌ Error in __dumber_find_close:", error);
367  };
368
369  window.__dumber_find_query = (query: string) => {
370    if (window.__dumber_omnibox?.findQuery) {
371      window.__dumber_omnibox.findQuery(query);
372    }
373  };
374
375  window.__dumber_gui = {
376    initializeToast: async (config?: ToastConfig) => {
377      return await initializeToast(config);
378    },
379    initializeOmnibox: async (config?: OmniboxInitConfig) => {
380      return await initializeOmnibox(config);
381    },
382    keyboard: keyboardService,
383    isReady: true,
384  };
385
386  window.__dumber_gui_bootstrap = function () {
387    console.log("[gui-bootstrap] Initializing GUI for workspace");
388
389    if (!window.__dumber_omnibox) {
390      console.log(
391        `[gui-bootstrap] Omnibox not yet available for webview ${currentWebViewId}, will be loaded when needed`,
392      );
393    } else {
394      console.log(
395        `[gui-bootstrap] Omnibox already available for webview ${currentWebViewId}`,
396      );
397      window.__dumber_omnibox.setActive(workspaceHasFocus);
398    }
399
400    console.log(
401      `[gui-bootstrap] GUI bootstrap complete for webview ${currentWebViewId}`,
402    );
403  };
404}

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