detached 95fab16

refactor(gui): simplify toast system to single source of truth

Fixes regression where zoom keyboard shortcuts (Ctrl+minus, Ctrl+equal,
Ctrl+0) were not showing toast notifications.

Changes:
- Remove all fallback toast implementations following KISS/DRY principles
- Eliminate toast bridge layer from main-world.ts that was overwriting functions
- Simplify toast.ts to only mount ToastContainer.svelte
- Make ToastContainer.svelte the single source of truth for toast functions
- Fix bootstrap initialization check to verify both showToast and showZoomToast
- Add ensureGUIInActivePane("toast") before showing zoom notifications
- Clean up verbose debug logging from zoom handler

The refactored architecture has ToastContainer.svelte expose
window.__dumber_showToast and window.__dumber_showZoomToast directly
when it mounts. No bridges, no events, no fallbacks.
commit
95fab16d83c17627a0137eb7a777961137daa984
author
bamen · · 2025-10-13 17:49 UTC
committer
bamen · · 2025-10-13 19:00 UTC
parent
8c61237
signature
unsigned
4 files · +12 -194
M gui/src/injected/bootstrap.ts +2 -62
@@ -181,7 +181,7 @@ export function bootstrapGUI(): void {
181181 console.log("[GUI] DOM ready, starting initialization");
182182 let toastInitialized = false;
183183
184- if (typeof window.__dumber_showToast === "function") {
184+ if (typeof window.__dumber_showToast === "function" && typeof window.__dumber_showZoomToast === "function") {
185185 console.log("[GUI] Toast already initialized, skipping");
186186 toastInitialized = true;
187187 } else {
@@ -195,67 +195,7 @@ export function bootstrapGUI(): void {
195195 console.log("[GUI] Toast initialized successfully");
196196 } catch (e) {
197197 console.error("❌ Failed to initialize Svelte toast system:", e);
198- window.__dumber_showToast = (
199- message: string,
200- duration: number = 2500,
201- ) => {
202- document
203- .querySelectorAll(".dumber-fallback-toast")
204- .forEach((el) => el.remove());
205-
206- const toast = document.createElement("div");
207- toast.className = "dumber-fallback-toast";
208- toast.textContent = message;
209- toast.style.cssText = `
210- position: fixed;
211- bottom: 20px;
212- right: 20px;
213- background: rgba(0,0,0,0.9);
214- color: white;
215- padding: 12px 16px;
216- border-radius: 6px;
217- z-index: 2147483647;
218- font-family: system-ui, -apple-system, sans-serif;
219- font-size: 14px;
220- opacity: 0;
221- transform: translateX(100%);
222- transition: all 0.3s ease;
223- max-width: 300px;
224- word-wrap: break-word;
225- `;
226-
227- if (document.body) {
228- document.body.appendChild(toast);
229-
230- requestAnimationFrame(() => {
231- toast.style.opacity = "1";
232- toast.style.transform = "translateX(0)";
233- });
234-
235- setTimeout(() => {
236- toast.style.opacity = "0";
237- toast.style.transform = "translateX(100%)";
238- setTimeout(() => toast.remove(), 300);
239- }, duration);
240- }
241- };
242-
243- window.__dumber_showZoomToast = (level: number) => {
244- const percentage = Math.round(level * 100);
245- window.__dumber_showToast!(`Zoom: ${percentage}%`, 1500);
246- };
247-
248- window.__dumber_dismissToast = (_id?: number) => {
249- document
250- .querySelectorAll(".dumber-fallback-toast")
251- .forEach((el) => el.remove());
252- };
253-
254- window.__dumber_clearToasts = () => {
255- document
256- .querySelectorAll(".dumber-fallback-toast")
257- .forEach((el) => el.remove());
258- };
198+ throw e;
259199 }
260200 }
261201
@@ -181,7 +181,7 @@ export function bootstrapGUI(): void {
181 console.log("[GUI] DOM ready, starting initialization");181 console.log("[GUI] DOM ready, starting initialization");
182 let toastInitialized = false;182 let toastInitialized = false;
183 183
184- if (typeof window.__dumber_showToast === "function") {184+ if (typeof window.__dumber_showToast === "function" && typeof window.__dumber_showZoomToast === "function") {
185 console.log("[GUI] Toast already initialized, skipping");185 console.log("[GUI] Toast already initialized, skipping");
186 toastInitialized = true;186 toastInitialized = true;
187 } else {187 } else {
@@ -195,67 +195,7 @@ export function bootstrapGUI(): void {
195 console.log("[GUI] Toast initialized successfully");195 console.log("[GUI] Toast initialized successfully");
196 } catch (e) {196 } catch (e) {
197 console.error("❌ Failed to initialize Svelte toast system:", e);197 console.error("❌ Failed to initialize Svelte toast system:", e);
198- window.__dumber_showToast = (198+ throw e;
199- message: string,
200- duration: number = 2500,
201- ) => {
202- document
203- .querySelectorAll(".dumber-fallback-toast")
204- .forEach((el) => el.remove());
205-
206- const toast = document.createElement("div");
207- toast.className = "dumber-fallback-toast";
208- toast.textContent = message;
209- toast.style.cssText = `
210- position: fixed;
211- bottom: 20px;
212- right: 20px;
213- background: rgba(0,0,0,0.9);
214- color: white;
215- padding: 12px 16px;
216- border-radius: 6px;
217- z-index: 2147483647;
218- font-family: system-ui, -apple-system, sans-serif;
219- font-size: 14px;
220- opacity: 0;
221- transform: translateX(100%);
222- transition: all 0.3s ease;
223- max-width: 300px;
224- word-wrap: break-word;
225- `;
226-
227- if (document.body) {
228- document.body.appendChild(toast);
229-
230- requestAnimationFrame(() => {
231- toast.style.opacity = "1";
232- toast.style.transform = "translateX(0)";
233- });
234-
235- setTimeout(() => {
236- toast.style.opacity = "0";
237- toast.style.transform = "translateX(100%)";
238- setTimeout(() => toast.remove(), 300);
239- }, duration);
240- }
241- };
242-
243- window.__dumber_showZoomToast = (level: number) => {
244- const percentage = Math.round(level * 100);
245- window.__dumber_showToast!(`Zoom: ${percentage}%`, 1500);
246- };
247-
248- window.__dumber_dismissToast = (_id?: number) => {
249- document
250- .querySelectorAll(".dumber-fallback-toast")
251- .forEach((el) => el.remove());
252- };
253-
254- window.__dumber_clearToasts = () => {
255- document
256- .querySelectorAll(".dumber-fallback-toast")
257- .forEach((el) => el.remove());
258- };
259 }199 }
260 }200 }
261 201
M gui/src/injected/main-world.ts +1 -60
@@ -51,14 +51,6 @@ interface Suggestion {
5151 }
5252
5353 interface DumberAPI {
54- toast: {
55- show: (
56- message: string,
57- duration?: number,
58- type?: "info" | "success" | "error",
59- ) => void;
60- zoom: (level: number) => void;
61- };
6254 omnibox: {
6355 suggestions: (suggestions: Suggestion[]) => void;
6456 };
@@ -237,59 +229,9 @@ interface DumberAPI {
237229 }
238230 };
239231
240- // Initialize unified API object
232+ // Initialize unified API object (toast functions are exposed by ToastContainer.svelte)
241233 window.__dumber = window.__dumber || ({} as DumberAPI);
242234
243- // Toast bridge
244- window.__dumber.toast = window.__dumber.toast || {
245- show: (message: string, duration?: number, type?: string) => {
246- try {
247- document.dispatchEvent(
248- new CustomEvent("dumber:toast:show", {
249- detail: { message, duration, type },
250- }),
251- );
252- // Legacy compatibility
253- document.dispatchEvent(
254- new CustomEvent("dumber:showToast", {
255- detail: { message, duration, type },
256- }),
257- );
258- } catch {
259- // ignore
260- }
261- },
262- zoom: (level: number) => {
263- try {
264- document.dispatchEvent(
265- new CustomEvent("dumber:toast:zoom", {
266- detail: { level },
267- }),
268- );
269- // Legacy compatibility
270- document.dispatchEvent(
271- new CustomEvent("dumber:showZoomToast", {
272- detail: { level },
273- }),
274- );
275- } catch {
276- // ignore
277- }
278- },
279- };
280-
281- // Legacy toast helpers
282- window.__dumber_showToast = (
283- message: string,
284- duration?: number,
285- type?: "info" | "success" | "error",
286- ) => {
287- window.__dumber.toast.show(message, duration, type);
288- };
289- window.__dumber_showZoomToast = (level: number) => {
290- window.__dumber.toast.zoom(level);
291- };
292-
293235 // DOM zoom functionality
294236 if (typeof window.__dumber_dom_zoom_level !== "number") {
295237 window.__dumber_dom_zoom_level = initialZoom;
@@ -890,7 +832,6 @@ interface DumberAPI {
890832 if (globalState) {
891833 const mutableState = globalState as unknown as Record<string, unknown>;
892834 delete mutableState.omnibox;
893- delete mutableState.toast;
894835 }
895836 };
896837
@@ -51,14 +51,6 @@ interface Suggestion {
51 }51 }
52 52
53 interface DumberAPI {53 interface DumberAPI {
54- toast: {
55- show: (
56- message: string,
57- duration?: number,
58- type?: "info" | "success" | "error",
59- ) => void;
60- zoom: (level: number) => void;
61- };
62 omnibox: {54 omnibox: {
63 suggestions: (suggestions: Suggestion[]) => void;55 suggestions: (suggestions: Suggestion[]) => void;
64 };56 };
@@ -237,59 +229,9 @@ interface DumberAPI {
237 }229 }
238 };230 };
239 231
240- // Initialize unified API object232+ // Initialize unified API object (toast functions are exposed by ToastContainer.svelte)
241 window.__dumber = window.__dumber || ({} as DumberAPI);233 window.__dumber = window.__dumber || ({} as DumberAPI);
242 234
243- // Toast bridge
244- window.__dumber.toast = window.__dumber.toast || {
245- show: (message: string, duration?: number, type?: string) => {
246- try {
247- document.dispatchEvent(
248- new CustomEvent("dumber:toast:show", {
249- detail: { message, duration, type },
250- }),
251- );
252- // Legacy compatibility
253- document.dispatchEvent(
254- new CustomEvent("dumber:showToast", {
255- detail: { message, duration, type },
256- }),
257- );
258- } catch {
259- // ignore
260- }
261- },
262- zoom: (level: number) => {
263- try {
264- document.dispatchEvent(
265- new CustomEvent("dumber:toast:zoom", {
266- detail: { level },
267- }),
268- );
269- // Legacy compatibility
270- document.dispatchEvent(
271- new CustomEvent("dumber:showZoomToast", {
272- detail: { level },
273- }),
274- );
275- } catch {
276- // ignore
277- }
278- },
279- };
280-
281- // Legacy toast helpers
282- window.__dumber_showToast = (
283- message: string,
284- duration?: number,
285- type?: "info" | "success" | "error",
286- ) => {
287- window.__dumber.toast.show(message, duration, type);
288- };
289- window.__dumber_showZoomToast = (level: number) => {
290- window.__dumber.toast.zoom(level);
291- };
292-
293 // DOM zoom functionality235 // DOM zoom functionality
294 if (typeof window.__dumber_dom_zoom_level !== "number") {236 if (typeof window.__dumber_dom_zoom_level !== "number") {
295 window.__dumber_dom_zoom_level = initialZoom;237 window.__dumber_dom_zoom_level = initialZoom;
@@ -890,7 +832,6 @@ interface DumberAPI {
890 if (globalState) {832 if (globalState) {
891 const mutableState = globalState as unknown as Record<string, unknown>;833 const mutableState = globalState as unknown as Record<string, unknown>;
892 delete mutableState.omnibox;834 delete mutableState.omnibox;
893- delete mutableState.toast;
894 }835 }
895 };836 };
896 837
M gui/src/injected/modules/toast.ts +4 -64
@@ -96,78 +96,18 @@ export async function initializeToast(config?: ToastConfig): Promise<void> {
9696 updateTheme(initialTheme === "dark");
9797 }
9898
99- // Mount the Svelte toast container (Svelte 5 syntax)
99+ // Mount the Svelte toast container - this exposes all toast functions to window
100100 mount(ToastContainer, {
101101 target: rootElement as unknown as Element,
102102 });
103103
104- // Signal isolated GUI readiness for page-world bridge
105- try {
106- document.documentElement.setAttribute("data-dumber-gui", "ready");
107- document.dispatchEvent(new CustomEvent("dumber:gui-ready"));
108- } catch {
109- // Ignore errors in GUI readiness signaling
110- }
111-
112- // Listen for page-world bridge events and forward to internal toast functions
113- const handleShowToast = (e: Event) => {
114- const detail = (e as CustomEvent).detail || {};
115- if (typeof window.__dumber_showToast === "function") {
116- window.__dumber_showToast(detail.message, detail.duration, detail.type);
117- }
118- };
119- document.addEventListener("dumber:showToast", handleShowToast);
120- document.addEventListener("dumber:toast:show", handleShowToast);
121-
122- const handleZoomToast = (e: Event) => {
123- const detail = (e as CustomEvent).detail || {};
124- if (typeof window.__dumber_showZoomToast === "function") {
125- window.__dumber_showZoomToast(detail.level);
126- }
127- };
128- document.addEventListener("dumber:showZoomToast", handleZoomToast);
129- document.addEventListener("dumber:toast:zoom", handleZoomToast);
130-
131- // Force immediate effect execution to ensure onMount callbacks run
104+ // Force immediate effect execution to ensure component functions are exposed
132105 flushSync();
133106
134- // Toast functions are now exposed by the component itself
135- console.log("✅ Toast functions exposed via component instantiation");
136-
137107 toastInitialized = true;
138- console.log("✅ Toast system initialized with Svelte 5");
108+ console.log("✅ Toast system initialized - functions exposed by ToastContainer");
139109 } catch (error) {
140110 console.error("❌ Failed to initialize toast system:", error);
141-
142- // Fallback to basic toast system if Svelte fails
143- initializeFallbackToast();
111+ throw error;
144112 }
145113 }
146-
147-function exposeToastFunctions(): void {
148- // The Svelte ToastContainer component already exposes these functions
149- // We just need to make sure they exist, they're handled by the component
150- console.log("✅ Toast functions will be exposed by ToastContainer component");
151-}
152-
153-function initializeFallbackToast(): void {
154- console.warn("🔄 Falling back to basic toast system");
155-
156- // Create basic toast container for fallback
157- const fallbackContainer = document.createElement("div");
158- fallbackContainer.id = "dumber-fallback-toast";
159- fallbackContainer.style.cssText = `
160- position: fixed;
161- top: 20px;
162- right: 20px;
163- z-index: 10000;
164- pointer-events: none;
165- `;
166- document.documentElement.appendChild(fallbackContainer);
167-
168- // Expose toast functions for fallback too
169- exposeToastFunctions();
170-
171- toastInitialized = true;
172- console.log("✅ Fallback toast system ready");
173-}
@@ -96,78 +96,18 @@ export async function initializeToast(config?: ToastConfig): Promise<void> {
96 updateTheme(initialTheme === "dark");96 updateTheme(initialTheme === "dark");
97 }97 }
98 98
99- // Mount the Svelte toast container (Svelte 5 syntax)99+ // Mount the Svelte toast container - this exposes all toast functions to window
100 mount(ToastContainer, {100 mount(ToastContainer, {
101 target: rootElement as unknown as Element,101 target: rootElement as unknown as Element,
102 });102 });
103 103
104- // Signal isolated GUI readiness for page-world bridge104+ // Force immediate effect execution to ensure component functions are exposed
105- try {
106- document.documentElement.setAttribute("data-dumber-gui", "ready");
107- document.dispatchEvent(new CustomEvent("dumber:gui-ready"));
108- } catch {
109- // Ignore errors in GUI readiness signaling
110- }
111-
112- // Listen for page-world bridge events and forward to internal toast functions
113- const handleShowToast = (e: Event) => {
114- const detail = (e as CustomEvent).detail || {};
115- if (typeof window.__dumber_showToast === "function") {
116- window.__dumber_showToast(detail.message, detail.duration, detail.type);
117- }
118- };
119- document.addEventListener("dumber:showToast", handleShowToast);
120- document.addEventListener("dumber:toast:show", handleShowToast);
121-
122- const handleZoomToast = (e: Event) => {
123- const detail = (e as CustomEvent).detail || {};
124- if (typeof window.__dumber_showZoomToast === "function") {
125- window.__dumber_showZoomToast(detail.level);
126- }
127- };
128- document.addEventListener("dumber:showZoomToast", handleZoomToast);
129- document.addEventListener("dumber:toast:zoom", handleZoomToast);
130-
131- // Force immediate effect execution to ensure onMount callbacks run
132 flushSync();105 flushSync();
133 106
134- // Toast functions are now exposed by the component itself
135- console.log("✅ Toast functions exposed via component instantiation");
136-
137 toastInitialized = true;107 toastInitialized = true;
138- console.log("✅ Toast system initialized with Svelte 5");108+ console.log("✅ Toast system initialized - functions exposed by ToastContainer");
139 } catch (error) {109 } catch (error) {
140 console.error("❌ Failed to initialize toast system:", error);110 console.error("❌ Failed to initialize toast system:", error);
141-111+ throw error;
142- // Fallback to basic toast system if Svelte fails
143- initializeFallbackToast();
144 }112 }
145 }113 }
146-
147-function exposeToastFunctions(): void {
148- // The Svelte ToastContainer component already exposes these functions
149- // We just need to make sure they exist, they're handled by the component
150- console.log("✅ Toast functions will be exposed by ToastContainer component");
151-}
152-
153-function initializeFallbackToast(): void {
154- console.warn("🔄 Falling back to basic toast system");
155-
156- // Create basic toast container for fallback
157- const fallbackContainer = document.createElement("div");
158- fallbackContainer.id = "dumber-fallback-toast";
159- fallbackContainer.style.cssText = `
160- position: fixed;
161- top: 20px;
162- right: 20px;
163- z-index: 10000;
164- pointer-events: none;
165- `;
166- document.documentElement.appendChild(fallbackContainer);
167-
168- // Expose toast functions for fallback too
169- exposeToastFunctions();
170-
171- toastInitialized = true;
172- console.log("✅ Fallback toast system ready");
173-}
M internal/app/browser/window_shortcuts.go +5 -8
@@ -230,24 +230,21 @@ func (h *WindowShortcutHandler) handleZoom(action string, multiplier float64) {
230230 return
231231 }
232232
233+ // Ensure GUI/toast component is loaded before showing toast
234+ h.ensureGUIInActivePane("toast")
235+
233236 // Show zoom toast notification
234- zoomPercent := int(newZoom * 100)
235237 toastScript := fmt.Sprintf(`
236238 (function() {
237239 try {
238240 if (typeof window.__dumber_showZoomToast === 'function') {
239241 window.__dumber_showZoomToast(%f);
240- } else {
241- // Fallback: dispatch toast event directly
242- document.dispatchEvent(new CustomEvent('dumber:showToast', {
243- detail: { message: 'Zoom: %d%%', duration: 1500, type: 'info' }
244- }));
245242 }
246243 } catch (e) {
247- console.error('[zoom] Failed to show toast:', e);
244+ console.error('[window-shortcuts] Failed to show zoom toast:', e);
248245 }
249246 })();
250- `, newZoom, zoomPercent)
247+ `, newZoom)
251248
252249 if err := activeWebView.InjectScript(toastScript); err != nil {
253250 log.Printf("[window-shortcuts] Failed to show zoom toast: %v", err)
@@ -230,24 +230,21 @@ func (h *WindowShortcutHandler) handleZoom(action string, multiplier float64) {
230 return230 return
231 }231 }
232 232
233+ // Ensure GUI/toast component is loaded before showing toast
234+ h.ensureGUIInActivePane("toast")
235+
233 // Show zoom toast notification236 // Show zoom toast notification
234- zoomPercent := int(newZoom * 100)
235 toastScript := fmt.Sprintf(`237 toastScript := fmt.Sprintf(`
236 (function() {238 (function() {
237 try {239 try {
238 if (typeof window.__dumber_showZoomToast === 'function') {240 if (typeof window.__dumber_showZoomToast === 'function') {
239 window.__dumber_showZoomToast(%f);241 window.__dumber_showZoomToast(%f);
240- } else {
241- // Fallback: dispatch toast event directly
242- document.dispatchEvent(new CustomEvent('dumber:showToast', {
243- detail: { message: 'Zoom: %d%%', duration: 1500, type: 'info' }
244- }));
245 }242 }
246 } catch (e) {243 } catch (e) {
247- console.error('[zoom] Failed to show toast:', e);244+ console.error('[window-shortcuts] Failed to show zoom toast:', e);
248 }245 }
249 })();246 })();
250- `, newZoom, zoomPercent)247+ `, newZoom)
251 248
252 if err := activeWebView.InjectScript(toastScript); err != nil {249 if err := activeWebView.InjectScript(toastScript); err != nil {
253 log.Printf("[window-shortcuts] Failed to show zoom toast: %v", err)250 log.Printf("[window-shortcuts] Failed to show zoom toast: %v", err)

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