v0.14.0 76d1892

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

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