detached 67d90d0

style: format and clean up injected scripts

- Apply consistent code formatting to GUI injection scripts
- Improve code readability with better indentation and spacing
- Clean up popup handling logic with enhanced error handling
- Standardize OAuth callback and window communication patterns
- Optimize color scheme module formatting and structure
commit
67d90d09161408e22a5bafa85b83948494be0bb9
author
bamen · · 2025-09-23 16:02 UTC
committer
bamen · · 2025-09-23 16:26 UTC
parent
4b59d50
signature
unsigned
3 files · +340 -128
M gui/src/injected/gui.ts +12 -5
@@ -88,14 +88,17 @@ if (!window.__dumber_gui_ready) {
8888 let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events
8989
9090 // Request webview ID from Go if it's unknown (needed for stub builds)
91- if (currentWebViewId === "unknown" || currentWebViewId === "__WEBVIEW_ID__") {
91+ if (
92+ currentWebViewId === "unknown" ||
93+ currentWebViewId === "__WEBVIEW_ID__"
94+ ) {
9295 console.log("[workspace] WebView ID unknown, requesting from Go backend");
9396 if (window.webkit?.messageHandlers?.dumber) {
9497 window.webkit.messageHandlers.dumber.postMessage(
9598 JSON.stringify({
9699 type: "request-webview-id",
97- payload: { timestamp: Date.now() }
98- })
100+ payload: { timestamp: Date.now() },
101+ }),
99102 );
100103 }
101104 }
@@ -106,7 +109,10 @@ if (!window.__dumber_gui_ready) {
106109 if (detail.webviewId) {
107110 currentWebViewId = detail.webviewId;
108111 window.__dumber_webview_id = detail.webviewId;
109- console.log("[workspace] Received webview ID from Go:", detail.webviewId);
112+ console.log(
113+ "[workspace] Received webview ID from Go:",
114+ detail.webviewId,
115+ );
110116 }
111117 });
112118
@@ -285,7 +291,8 @@ if (!window.__dumber_gui_ready) {
285291 }
286292
287293 // Enhanced focus checking with webview ID validation
288- const isForThisWebView = !eventWebViewId || eventWebViewId === currentWebViewId;
294+ const isForThisWebView =
295+ !eventWebViewId || eventWebViewId === currentWebViewId;
289296 // Only apply strict focus checking to omnibox actions, be permissive for others
290297 const isOmniboxAction = action.startsWith("omnibox-");
291298 const shouldHandle =
@@ -88,14 +88,17 @@ if (!window.__dumber_gui_ready) {
88 let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events88 let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events
89 89
90 // Request webview ID from Go if it's unknown (needed for stub builds)90 // Request webview ID from Go if it's unknown (needed for stub builds)
91- if (currentWebViewId === "unknown" || currentWebViewId === "__WEBVIEW_ID__") {91+ if (
92+ currentWebViewId === "unknown" ||
93+ currentWebViewId === "__WEBVIEW_ID__"
94+ ) {
92 console.log("[workspace] WebView ID unknown, requesting from Go backend");95 console.log("[workspace] WebView ID unknown, requesting from Go backend");
93 if (window.webkit?.messageHandlers?.dumber) {96 if (window.webkit?.messageHandlers?.dumber) {
94 window.webkit.messageHandlers.dumber.postMessage(97 window.webkit.messageHandlers.dumber.postMessage(
95 JSON.stringify({98 JSON.stringify({
96 type: "request-webview-id",99 type: "request-webview-id",
97- payload: { timestamp: Date.now() }100+ payload: { timestamp: Date.now() },
98- })101+ }),
99 );102 );
100 }103 }
101 }104 }
@@ -106,7 +109,10 @@ if (!window.__dumber_gui_ready) {
106 if (detail.webviewId) {109 if (detail.webviewId) {
107 currentWebViewId = detail.webviewId;110 currentWebViewId = detail.webviewId;
108 window.__dumber_webview_id = detail.webviewId;111 window.__dumber_webview_id = detail.webviewId;
109- console.log("[workspace] Received webview ID from Go:", detail.webviewId);112+ console.log(
113+ "[workspace] Received webview ID from Go:",
114+ detail.webviewId,
115+ );
110 }116 }
111 });117 });
112 118
@@ -285,7 +291,8 @@ if (!window.__dumber_gui_ready) {
285 }291 }
286 292
287 // Enhanced focus checking with webview ID validation293 // Enhanced focus checking with webview ID validation
288- const isForThisWebView = !eventWebViewId || eventWebViewId === currentWebViewId;294+ const isForThisWebView =
295+ !eventWebViewId || eventWebViewId === currentWebViewId;
289 // Only apply strict focus checking to omnibox actions, be permissive for others296 // Only apply strict focus checking to omnibox actions, be permissive for others
290 const isOmniboxAction = action.startsWith("omnibox-");297 const isOmniboxAction = action.startsWith("omnibox-");
291 const shouldHandle =298 const shouldHandle =
M gui/src/injected/main-world.ts +225 -112
@@ -84,7 +84,12 @@ function detectWindowType(features?: string | null): string {
8484 const hasNoLocation = /location=0|location=no/.test(featuresStr);
8585
8686 // OAuth/login popups typically have size constraints and disabled UI elements
87- const popupIndicators = [hasSize, hasNoToolbar, hasNoMenubar, hasNoLocation].filter(Boolean).length;
87+ const popupIndicators = [
88+ hasSize,
89+ hasNoToolbar,
90+ hasNoMenubar,
91+ hasNoLocation,
92+ ].filter(Boolean).length;
8893
8994 // If 2 or more popup indicators are present, treat as popup
9095 if (popupIndicators >= 2) {
@@ -302,14 +307,19 @@ function detectWindowType(features?: string | null): string {
302307 const findParentPopupId = () => {
303308 const keys = Object.keys(localStorage);
304309 for (const key of keys) {
305- if (key.startsWith('popup_') && key.endsWith('_parent_info')) {
306- const popupId = key.replace('popup_', '').replace('_parent_info', '');
307- const parentInfo = JSON.parse(localStorage.getItem(key) || '{}');
310+ if (key.startsWith("popup_") && key.endsWith("_parent_info")) {
311+ const popupId = key
312+ .replace("popup_", "")
313+ .replace("_parent_info", "");
314+ const parentInfo = JSON.parse(localStorage.getItem(key) || "{}");
308315
309316 // Check if this popup info is recent (within last 30 seconds)
310317 const age = Date.now() - (parentInfo.timestamp || 0);
311318 if (age < 30000) {
312- console.log(`[dumber-popup] Found parent info for popup ID: ${popupId}`, parentInfo);
319+ console.log(
320+ `[dumber-popup] Found parent info for popup ID: ${popupId}`,
321+ parentInfo,
322+ );
313323 return { popupId, parentInfo };
314324 }
315325 }
@@ -321,55 +331,78 @@ function detectWindowType(features?: string | null): string {
321331 if (parentData && !window.opener) {
322332 const { popupId, parentInfo } = parentData;
323333
324- console.log(`[dumber-popup] Setting up window.opener bridge for popup ID: ${popupId}`);
334+ console.log(
335+ `[dumber-popup] Setting up window.opener bridge for popup ID: ${popupId}`,
336+ );
325337
326338 // Create window.opener proxy that uses localStorage for communication
327339 window.opener = {
328340 postMessage: (data: unknown, targetOrigin?: string) => {
329341 try {
330- localStorage.setItem(`popup_${popupId}_message_to_parent`, JSON.stringify({
342+ localStorage.setItem(
343+ `popup_${popupId}_message_to_parent`,
344+ JSON.stringify({
345+ data,
346+ origin: targetOrigin || "*",
347+ timestamp: Date.now(),
348+ source: "popup",
349+ }),
350+ );
351+ console.log(
352+ `[dumber-popup] Sent message to parent via localStorage:`,
331353 data,
332- origin: targetOrigin || '*',
333- timestamp: Date.now(),
334- source: 'popup',
335- }));
336- console.log(`[dumber-popup] Sent message to parent via localStorage:`, data);
354+ );
337355 } catch (err) {
338- console.warn(`[dumber-popup] Failed to send message to parent:`, err);
356+ console.warn(
357+ `[dumber-popup] Failed to send message to parent:`,
358+ err,
359+ );
339360 }
340361 },
341362
342363 focus: () => {
343364 try {
344- localStorage.setItem(`popup_${popupId}_parent_action`, JSON.stringify({
345- action: 'focus',
346- timestamp: Date.now(),
347- }));
365+ localStorage.setItem(
366+ `popup_${popupId}_parent_action`,
367+ JSON.stringify({
368+ action: "focus",
369+ timestamp: Date.now(),
370+ }),
371+ );
348372 } catch (err) {
349- console.warn(`[dumber-popup] Failed to request parent focus:`, err);
373+ console.warn(
374+ `[dumber-popup] Failed to request parent focus:`,
375+ err,
376+ );
350377 }
351378 },
352379
353380 blur: () => {
354381 try {
355- localStorage.setItem(`popup_${popupId}_parent_action`, JSON.stringify({
356- action: 'blur',
357- timestamp: Date.now(),
358- }));
382+ localStorage.setItem(
383+ `popup_${popupId}_parent_action`,
384+ JSON.stringify({
385+ action: "blur",
386+ timestamp: Date.now(),
387+ }),
388+ );
359389 } catch (err) {
360- console.warn(`[dumber-popup] Failed to request parent blur:`, err);
390+ console.warn(
391+ `[dumber-popup] Failed to request parent blur:`,
392+ err,
393+ );
361394 }
362395 },
363396
364397 location: {
365- href: parentInfo.parentUrl || '',
366- origin: new URL(parentInfo.parentUrl || 'about:blank').origin,
398+ href: parentInfo.parentUrl || "",
399+ origin: new URL(parentInfo.parentUrl || "about:blank").origin,
367400 },
368401
369402 closed: false,
370403
371404 // Support for custom properties/methods that websites might use
372- [Symbol.for('popup.bridge')]: true,
405+ [Symbol.for("popup.bridge")]: true,
373406 } as unknown as Window;
374407
375408 // Set up message polling to receive messages from parent
@@ -383,12 +416,15 @@ function detectWindowType(features?: string | null): string {
383416
384417 // Check if message is recent (within last 5 seconds)
385418 if (Date.now() - timestamp < 5000) {
386- console.log(`[dumber-popup] Received message from parent:`, data);
419+ console.log(
420+ `[dumber-popup] Received message from parent:`,
421+ data,
422+ );
387423
388424 // Dispatch as MessageEvent to window
389- const event = new MessageEvent('message', {
425+ const event = new MessageEvent("message", {
390426 data,
391- origin: origin || parentInfo.parentUrl || '',
427+ origin: origin || parentInfo.parentUrl || "",
392428 source: window.opener,
393429 });
394430 window.dispatchEvent(event);
@@ -398,7 +434,10 @@ function detectWindowType(features?: string | null): string {
398434 localStorage.removeItem(messageKey);
399435 }
400436 } catch (err) {
401- console.warn(`[dumber-popup] Failed to poll for parent messages:`, err);
437+ console.warn(
438+ `[dumber-popup] Failed to poll for parent messages:`,
439+ err,
440+ );
402441 }
403442 };
404443
@@ -411,10 +450,15 @@ function detectWindowType(features?: string | null): string {
411450 }
412451 });
413452
414- console.log(`[dumber-popup] window.opener bridge established successfully`);
453+ console.log(
454+ `[dumber-popup] window.opener bridge established successfully`,
455+ );
415456 }
416457 } catch (err) {
417- console.warn(`[dumber-popup] Failed to setup window.opener bridge:`, err);
458+ console.warn(
459+ `[dumber-popup] Failed to setup window.opener bridge:`,
460+ err,
461+ );
418462 }
419463 };
420464
@@ -430,10 +474,11 @@ function detectWindowType(features?: string | null): string {
430474 const hasRelevantData = () => {
431475 try {
432476 const keys = Object.keys(localStorage);
433- return keys.some(key =>
434- key.startsWith('popup_mapping_') ||
435- key.startsWith('oauth_callback_') ||
436- key.includes('message_to_parent')
477+ return keys.some(
478+ (key) =>
479+ key.startsWith("popup_mapping_") ||
480+ key.startsWith("oauth_callback_") ||
481+ key.includes("message_to_parent"),
437482 );
438483 } catch {
439484 return false;
@@ -451,19 +496,23 @@ function detectWindowType(features?: string | null): string {
451496
452497 for (const key of keys) {
453498 // Handle popup messages to parent
454- if (key.includes('message_to_parent')) {
499+ if (key.includes("message_to_parent")) {
455500 const messageData = localStorage.getItem(key);
456501 if (!messageData) continue;
457502
458503 try {
459- const { data, origin, timestamp, source } = JSON.parse(messageData);
504+ const { data, origin, timestamp, source } =
505+ JSON.parse(messageData);
460506
461507 // Check if message is recent (within last 5 seconds) and from a popup
462- if (Date.now() - timestamp < 5000 && source === 'popup') {
463- console.log(`[dumber-parent] Received message from popup:`, data);
508+ if (Date.now() - timestamp < 5000 && source === "popup") {
509+ console.log(
510+ `[dumber-parent] Received message from popup:`,
511+ data,
512+ );
464513
465514 // Dispatch as MessageEvent to parent window
466- const event = new MessageEvent('message', {
515+ const event = new MessageEvent("message", {
467516 data,
468517 origin: origin || window.location.origin,
469518 source: null, // popup reference would be here in real browser
@@ -474,54 +523,75 @@ function detectWindowType(features?: string | null): string {
474523 localStorage.removeItem(key);
475524 }
476525 } catch (err) {
477- console.warn(`[dumber-parent] Failed to parse popup message:`, err);
526+ console.warn(
527+ `[dumber-parent] Failed to parse popup message:`,
528+ err,
529+ );
478530 localStorage.removeItem(key); // Clean up invalid message
479531 }
480532 }
481533
482534 // Handle OAuth callback detection from popups
483- if (key.startsWith('oauth_callback_')) {
535+ if (key.startsWith("oauth_callback_")) {
484536 const callbackData = localStorage.getItem(key);
485537 if (!callbackData) continue;
486538
487539 try {
488- const { url, webviewId, timestamp, isOAuthCallback } = JSON.parse(callbackData);
540+ const { url, webviewId, timestamp, isOAuthCallback } =
541+ JSON.parse(callbackData);
489542
490543 // Check if callback is recent (within last 10 seconds) and is an OAuth callback
491544 if (Date.now() - timestamp < 10000 && isOAuthCallback) {
492- console.log(`[dumber-parent] OAuth callback detected for webview ${webviewId}:`, url);
545+ console.log(
546+ `[dumber-parent] OAuth callback detected for webview ${webviewId}:`,
547+ url,
548+ );
493549
494550 // Send close request to backend for this popup webview
495551 const bridge = window.webkit?.messageHandlers?.dumber;
496- if (bridge && typeof bridge.postMessage === 'function') {
552+ if (bridge && typeof bridge.postMessage === "function") {
497553 try {
498554 const closeMessage = {
499- type: 'close-popup',
555+ type: "close-popup",
500556 webviewId,
501- reason: 'oauth-callback-success',
502- timestamp: Date.now()
557+ reason: "oauth-callback-success",
558+ timestamp: Date.now(),
503559 };
504560
505- console.log(`[dumber-parent] Sending popup close request:`, closeMessage);
561+ console.log(
562+ `[dumber-parent] Sending popup close request:`,
563+ closeMessage,
564+ );
506565 bridge.postMessage(JSON.stringify(closeMessage));
507566 } catch (err) {
508- console.warn(`[dumber-parent] Failed to send popup close request:`, err);
567+ console.warn(
568+ `[dumber-parent] Failed to send popup close request:`,
569+ err,
570+ );
509571 }
510572 } else {
511- console.warn(`[dumber-parent] No webkit bridge available for popup close request`);
573+ console.warn(
574+ `[dumber-parent] No webkit bridge available for popup close request`,
575+ );
512576 }
513577
514578 // Clean up OAuth callback data
515579 localStorage.removeItem(key);
516580 }
517581 } catch (err) {
518- console.warn(`[dumber-parent] Failed to parse OAuth callback data:`, err);
582+ console.warn(
583+ `[dumber-parent] Failed to parse OAuth callback data:`,
584+ err,
585+ );
519586 localStorage.removeItem(key); // Clean up invalid data
520587 }
521588 }
522589 }
523590 } catch (err) {
524- console.warn(`[dumber-parent] Failed to poll for popup messages:`, err);
591+ console.warn(
592+ `[dumber-parent] Failed to poll for popup messages:`,
593+ err,
594+ );
525595 }
526596 };
527597
@@ -562,20 +632,19 @@ function detectWindowType(features?: string | null): string {
562632 const url = window.location.href.toLowerCase();
563633
564634 // Check for OAuth callback patterns - be more specific to avoid false positives
565- const isCallback = (
635+ const isCallback =
566636 // OAuth callback URLs typically have these parameters
567- url.includes('code=') ||
568- url.includes('access_token=') ||
569- url.includes('id_token=') ||
637+ url.includes("code=") ||
638+ url.includes("access_token=") ||
639+ url.includes("id_token=") ||
570640 // OAuth error responses
571- url.includes('error=access_denied') ||
572- url.includes('error=unauthorized') ||
641+ url.includes("error=access_denied") ||
642+ url.includes("error=unauthorized") ||
573643 // OAuth callback paths (more specific than just "callback" or "redirect")
574- url.includes('/oauth/callback') ||
575- url.includes('/auth/callback') ||
576- url.includes('oauth2callback') ||
577- url.includes('googlepopupcallback')
578- );
644+ url.includes("/oauth/callback") ||
645+ url.includes("/auth/callback") ||
646+ url.includes("oauth2callback") ||
647+ url.includes("googlepopupcallback");
579648
580649 if (isCallback) {
581650 try {
@@ -593,13 +662,18 @@ function detectWindowType(features?: string | null): string {
593662 const age = Date.now() - (mapping.timestamp || 0);
594663 if (age < 60000 && mapping.popupId) {
595664 targetWebViewId = mapping.popupId;
596- console.log(`[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`);
665+ console.log(
666+ `[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`,
667+ );
597668
598669 // Clean up the mapping since we're using it
599670 localStorage.removeItem(popupMappingKey);
600671 }
601672 } catch (err) {
602- console.warn(`[oauth-callback] Failed to parse popup mapping:`, err);
673+ console.warn(
674+ `[oauth-callback] Failed to parse popup mapping:`,
675+ err,
676+ );
603677 }
604678 }
605679
@@ -610,20 +684,34 @@ function detectWindowType(features?: string | null): string {
610684 isOAuthCallback: true,
611685 };
612686
613- localStorage.setItem(`oauth_callback_${targetWebViewId}`, JSON.stringify(callbackData));
614- console.log(`[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`, callbackData);
687+ localStorage.setItem(
688+ `oauth_callback_${targetWebViewId}`,
689+ JSON.stringify(callbackData),
690+ );
691+ console.log(
692+ `[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`,
693+ callbackData,
694+ );
615695
616696 // Auto-cleanup sensitive OAuth data after 10 seconds
617697 setTimeout(() => {
618698 try {
619699 localStorage.removeItem(`oauth_callback_${targetWebViewId}`);
620- console.log(`[oauth-callback] Cleaned up OAuth callback data from localStorage`);
700+ console.log(
701+ `[oauth-callback] Cleaned up OAuth callback data from localStorage`,
702+ );
621703 } catch (err) {
622- console.warn(`[oauth-callback] Failed to cleanup OAuth callback data:`, err);
704+ console.warn(
705+ `[oauth-callback] Failed to cleanup OAuth callback data:`,
706+ err,
707+ );
623708 }
624709 }, 10000);
625710 } catch (err) {
626- console.warn(`[oauth-callback] Failed to store OAuth callback data:`, err);
711+ console.warn(
712+ `[oauth-callback] Failed to store OAuth callback data:`,
713+ err,
714+ );
627715 }
628716 }
629717 };
@@ -647,11 +735,11 @@ function detectWindowType(features?: string | null): string {
647735 });
648736
649737 // Also check on navigation events
650- window.addEventListener('popstate', detectOAuthCallback);
651- window.addEventListener('hashchange', detectOAuthCallback);
738+ window.addEventListener("popstate", detectOAuthCallback);
739+ window.addEventListener("hashchange", detectOAuthCallback);
652740 cleanupHandlers.push(() => {
653- window.removeEventListener('popstate', detectOAuthCallback);
654- window.removeEventListener('hashchange', detectOAuthCallback);
741+ window.removeEventListener("popstate", detectOAuthCallback);
742+ window.removeEventListener("hashchange", detectOAuthCallback);
655743 });
656744 };
657745
@@ -737,23 +825,32 @@ function detectWindowType(features?: string | null): string {
737825 return timeSinceInteraction <= INTERACTION_TIMEOUT;
738826 };
739827
740-
741828 const createFakeWindow = (url: string, popupId?: string): WindowProxy => {
742829 // Generate unique popup ID if not provided
743- const actualPopupId = popupId || `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
830+ const actualPopupId =
831+ popupId ||
832+ `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
744833
745834 // Set up communication channel via shared localStorage
746835 try {
747- localStorage.setItem(`popup_${actualPopupId}_parent_info`, JSON.stringify({
748- parentUrl: window.location.href,
749- parentWebViewId: window.__dumber_webview_id,
750- timestamp: Date.now(),
751- popupUrl: url,
752- }));
836+ localStorage.setItem(
837+ `popup_${actualPopupId}_parent_info`,
838+ JSON.stringify({
839+ parentUrl: window.location.href,
840+ parentWebViewId: window.__dumber_webview_id,
841+ timestamp: Date.now(),
842+ popupUrl: url,
843+ }),
844+ );
753845
754- console.log(`[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`);
846+ console.log(
847+ `[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`,
848+ );
755849 } catch (err) {
756- console.warn(`[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`, err);
850+ console.warn(
851+ `[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`,
852+ err,
853+ );
757854 }
758855
759856 return {
@@ -764,33 +861,45 @@ function detectWindowType(features?: string | null): string {
764861 },
765862 focus: () => {
766863 try {
767- localStorage.setItem(`popup_${actualPopupId}_parent_action`, JSON.stringify({
768- action: 'focus',
769- timestamp: Date.now(),
770- }));
864+ localStorage.setItem(
865+ `popup_${actualPopupId}_parent_action`,
866+ JSON.stringify({
867+ action: "focus",
868+ timestamp: Date.now(),
869+ }),
870+ );
771871 } catch (err) {
772872 console.warn(`[window-open] Failed to store focus action:`, err);
773873 }
774874 },
775875 blur: () => {
776876 try {
777- localStorage.setItem(`popup_${actualPopupId}_parent_action`, JSON.stringify({
778- action: 'blur',
779- timestamp: Date.now(),
780- }));
877+ localStorage.setItem(
878+ `popup_${actualPopupId}_parent_action`,
879+ JSON.stringify({
880+ action: "blur",
881+ timestamp: Date.now(),
882+ }),
883+ );
781884 } catch (err) {
782885 console.warn(`[window-open] Failed to store blur action:`, err);
783886 }
784887 },
785888 postMessage: (data: unknown, targetOrigin?: string) => {
786889 try {
787- localStorage.setItem(`popup_${actualPopupId}_message_to_popup`, JSON.stringify({
890+ localStorage.setItem(
891+ `popup_${actualPopupId}_message_to_popup`,
892+ JSON.stringify({
893+ data,
894+ origin: targetOrigin || "*",
895+ timestamp: Date.now(),
896+ source: "parent",
897+ }),
898+ );
899+ console.log(
900+ `[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`,
788901 data,
789- origin: targetOrigin || '*',
790- timestamp: Date.now(),
791- source: 'parent',
792- }));
793- console.log(`[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`, data);
902+ );
794903 } catch (err) {
795904 console.warn(`[window-open] Failed to store postMessage:`, err);
796905 }
@@ -798,9 +907,13 @@ function detectWindowType(features?: string | null): string {
798907 } as unknown as WindowProxy;
799908 };
800909
801-
802910 // Track user interactions for popup validation
803- const interactionEvents = ["click", "mousedown", "keydown", "touchstart"] as const;
911+ const interactionEvents = [
912+ "click",
913+ "mousedown",
914+ "keydown",
915+ "touchstart",
916+ ] as const;
804917 interactionEvents.forEach((eventType) => {
805918 document.addEventListener(eventType, trackUserInteraction, true);
806919 });
@@ -814,7 +927,6 @@ function detectWindowType(features?: string | null): string {
814927 `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`,
815928 );
816929
817-
818930 // Window.open interceptor - only install once
819931 if (!window.__dumber_window_open_intercepted) {
820932 try {
@@ -837,7 +949,6 @@ function detectWindowType(features?: string | null): string {
837949 features,
838950 );
839951
840-
841952 // Enhanced duplicate check
842953 if (windowOpenDebouncer.isDuplicate(urlString, webviewId)) {
843954 console.log(
@@ -927,7 +1038,7 @@ function detectWindowType(features?: string | null): string {
9271038 try {
9281039 cleanup();
9291040 } catch (cleanupErr) {
930- console.warn('[dumber] Cleanup handler failed', cleanupErr);
1041+ console.warn("[dumber] Cleanup handler failed", cleanupErr);
9311042 }
9321043 }
9331044
@@ -942,18 +1053,20 @@ function detectWindowType(features?: string | null): string {
9421053 window.__dumber_showZoomToast = undefined;
9431054 window.__dumber_applyDomZoom = undefined;
9441055
945- if (window.__dumber) {
946- delete window.__dumber.omnibox;
947- delete window.__dumber.toast;
1056+ const globalState = window.__dumber;
1057+ if (globalState) {
1058+ const mutableState = globalState as unknown as Record<string, unknown>;
1059+ delete mutableState.omnibox;
1060+ delete mutableState.toast;
9481061 }
9491062 };
9501063
9511064 window.__dumber_teardown = teardown;
952- window.addEventListener('pagehide', teardown, { once: true });
953- window.addEventListener('beforeunload', teardown, { once: true });
1065+ window.addEventListener("pagehide", teardown, { once: true });
1066+ window.addEventListener("beforeunload", teardown, { once: true });
9541067 cleanupHandlers.push(() => {
955- window.removeEventListener('pagehide', teardown);
956- window.removeEventListener('beforeunload', teardown);
1068+ window.removeEventListener("pagehide", teardown);
1069+ window.removeEventListener("beforeunload", teardown);
9571070 window.__dumber_teardown = undefined;
9581071 });
9591072
@@ -84,7 +84,12 @@ function detectWindowType(features?: string | null): string {
84 const hasNoLocation = /location=0|location=no/.test(featuresStr);84 const hasNoLocation = /location=0|location=no/.test(featuresStr);
85 85
86 // OAuth/login popups typically have size constraints and disabled UI elements86 // OAuth/login popups typically have size constraints and disabled UI elements
87- const popupIndicators = [hasSize, hasNoToolbar, hasNoMenubar, hasNoLocation].filter(Boolean).length;87+ const popupIndicators = [
88+ hasSize,
89+ hasNoToolbar,
90+ hasNoMenubar,
91+ hasNoLocation,
92+ ].filter(Boolean).length;
88 93
89 // If 2 or more popup indicators are present, treat as popup94 // If 2 or more popup indicators are present, treat as popup
90 if (popupIndicators >= 2) {95 if (popupIndicators >= 2) {
@@ -302,14 +307,19 @@ function detectWindowType(features?: string | null): string {
302 const findParentPopupId = () => {307 const findParentPopupId = () => {
303 const keys = Object.keys(localStorage);308 const keys = Object.keys(localStorage);
304 for (const key of keys) {309 for (const key of keys) {
305- if (key.startsWith('popup_') && key.endsWith('_parent_info')) {310+ if (key.startsWith("popup_") && key.endsWith("_parent_info")) {
306- const popupId = key.replace('popup_', '').replace('_parent_info', '');311+ const popupId = key
307- const parentInfo = JSON.parse(localStorage.getItem(key) || '{}');312+ .replace("popup_", "")
313+ .replace("_parent_info", "");
314+ const parentInfo = JSON.parse(localStorage.getItem(key) || "{}");
308 315
309 // Check if this popup info is recent (within last 30 seconds)316 // Check if this popup info is recent (within last 30 seconds)
310 const age = Date.now() - (parentInfo.timestamp || 0);317 const age = Date.now() - (parentInfo.timestamp || 0);
311 if (age < 30000) {318 if (age < 30000) {
312- console.log(`[dumber-popup] Found parent info for popup ID: ${popupId}`, parentInfo);319+ console.log(
320+ `[dumber-popup] Found parent info for popup ID: ${popupId}`,
321+ parentInfo,
322+ );
313 return { popupId, parentInfo };323 return { popupId, parentInfo };
314 }324 }
315 }325 }
@@ -321,55 +331,78 @@ function detectWindowType(features?: string | null): string {
321 if (parentData && !window.opener) {331 if (parentData && !window.opener) {
322 const { popupId, parentInfo } = parentData;332 const { popupId, parentInfo } = parentData;
323 333
324- console.log(`[dumber-popup] Setting up window.opener bridge for popup ID: ${popupId}`);334+ console.log(
335+ `[dumber-popup] Setting up window.opener bridge for popup ID: ${popupId}`,
336+ );
325 337
326 // Create window.opener proxy that uses localStorage for communication338 // Create window.opener proxy that uses localStorage for communication
327 window.opener = {339 window.opener = {
328 postMessage: (data: unknown, targetOrigin?: string) => {340 postMessage: (data: unknown, targetOrigin?: string) => {
329 try {341 try {
330- localStorage.setItem(`popup_${popupId}_message_to_parent`, JSON.stringify({342+ localStorage.setItem(
343+ `popup_${popupId}_message_to_parent`,
344+ JSON.stringify({
345+ data,
346+ origin: targetOrigin || "*",
347+ timestamp: Date.now(),
348+ source: "popup",
349+ }),
350+ );
351+ console.log(
352+ `[dumber-popup] Sent message to parent via localStorage:`,
331 data,353 data,
332- origin: targetOrigin || '*',354+ );
333- timestamp: Date.now(),
334- source: 'popup',
335- }));
336- console.log(`[dumber-popup] Sent message to parent via localStorage:`, data);
337 } catch (err) {355 } catch (err) {
338- console.warn(`[dumber-popup] Failed to send message to parent:`, err);356+ console.warn(
357+ `[dumber-popup] Failed to send message to parent:`,
358+ err,
359+ );
339 }360 }
340 },361 },
341 362
342 focus: () => {363 focus: () => {
343 try {364 try {
344- localStorage.setItem(`popup_${popupId}_parent_action`, JSON.stringify({365+ localStorage.setItem(
345- action: 'focus',366+ `popup_${popupId}_parent_action`,
346- timestamp: Date.now(),367+ JSON.stringify({
347- }));368+ action: "focus",
369+ timestamp: Date.now(),
370+ }),
371+ );
348 } catch (err) {372 } catch (err) {
349- console.warn(`[dumber-popup] Failed to request parent focus:`, err);373+ console.warn(
374+ `[dumber-popup] Failed to request parent focus:`,
375+ err,
376+ );
350 }377 }
351 },378 },
352 379
353 blur: () => {380 blur: () => {
354 try {381 try {
355- localStorage.setItem(`popup_${popupId}_parent_action`, JSON.stringify({382+ localStorage.setItem(
356- action: 'blur',383+ `popup_${popupId}_parent_action`,
357- timestamp: Date.now(),384+ JSON.stringify({
358- }));385+ action: "blur",
386+ timestamp: Date.now(),
387+ }),
388+ );
359 } catch (err) {389 } catch (err) {
360- console.warn(`[dumber-popup] Failed to request parent blur:`, err);390+ console.warn(
391+ `[dumber-popup] Failed to request parent blur:`,
392+ err,
393+ );
361 }394 }
362 },395 },
363 396
364 location: {397 location: {
365- href: parentInfo.parentUrl || '',398+ href: parentInfo.parentUrl || "",
366- origin: new URL(parentInfo.parentUrl || 'about:blank').origin,399+ origin: new URL(parentInfo.parentUrl || "about:blank").origin,
367 },400 },
368 401
369 closed: false,402 closed: false,
370 403
371 // Support for custom properties/methods that websites might use404 // Support for custom properties/methods that websites might use
372- [Symbol.for('popup.bridge')]: true,405+ [Symbol.for("popup.bridge")]: true,
373 } as unknown as Window;406 } as unknown as Window;
374 407
375 // Set up message polling to receive messages from parent408 // Set up message polling to receive messages from parent
@@ -383,12 +416,15 @@ function detectWindowType(features?: string | null): string {
383 416
384 // Check if message is recent (within last 5 seconds)417 // Check if message is recent (within last 5 seconds)
385 if (Date.now() - timestamp < 5000) {418 if (Date.now() - timestamp < 5000) {
386- console.log(`[dumber-popup] Received message from parent:`, data);419+ console.log(
420+ `[dumber-popup] Received message from parent:`,
421+ data,
422+ );
387 423
388 // Dispatch as MessageEvent to window424 // Dispatch as MessageEvent to window
389- const event = new MessageEvent('message', {425+ const event = new MessageEvent("message", {
390 data,426 data,
391- origin: origin || parentInfo.parentUrl || '',427+ origin: origin || parentInfo.parentUrl || "",
392 source: window.opener,428 source: window.opener,
393 });429 });
394 window.dispatchEvent(event);430 window.dispatchEvent(event);
@@ -398,7 +434,10 @@ function detectWindowType(features?: string | null): string {
398 localStorage.removeItem(messageKey);434 localStorage.removeItem(messageKey);
399 }435 }
400 } catch (err) {436 } catch (err) {
401- console.warn(`[dumber-popup] Failed to poll for parent messages:`, err);437+ console.warn(
438+ `[dumber-popup] Failed to poll for parent messages:`,
439+ err,
440+ );
402 }441 }
403 };442 };
404 443
@@ -411,10 +450,15 @@ function detectWindowType(features?: string | null): string {
411 }450 }
412 });451 });
413 452
414- console.log(`[dumber-popup] window.opener bridge established successfully`);453+ console.log(
454+ `[dumber-popup] window.opener bridge established successfully`,
455+ );
415 }456 }
416 } catch (err) {457 } catch (err) {
417- console.warn(`[dumber-popup] Failed to setup window.opener bridge:`, err);458+ console.warn(
459+ `[dumber-popup] Failed to setup window.opener bridge:`,
460+ err,
461+ );
418 }462 }
419 };463 };
420 464
@@ -430,10 +474,11 @@ function detectWindowType(features?: string | null): string {
430 const hasRelevantData = () => {474 const hasRelevantData = () => {
431 try {475 try {
432 const keys = Object.keys(localStorage);476 const keys = Object.keys(localStorage);
433- return keys.some(key =>477+ return keys.some(
434- key.startsWith('popup_mapping_') ||478+ (key) =>
435- key.startsWith('oauth_callback_') ||479+ key.startsWith("popup_mapping_") ||
436- key.includes('message_to_parent')480+ key.startsWith("oauth_callback_") ||
481+ key.includes("message_to_parent"),
437 );482 );
438 } catch {483 } catch {
439 return false;484 return false;
@@ -451,19 +496,23 @@ function detectWindowType(features?: string | null): string {
451 496
452 for (const key of keys) {497 for (const key of keys) {
453 // Handle popup messages to parent498 // Handle popup messages to parent
454- if (key.includes('message_to_parent')) {499+ if (key.includes("message_to_parent")) {
455 const messageData = localStorage.getItem(key);500 const messageData = localStorage.getItem(key);
456 if (!messageData) continue;501 if (!messageData) continue;
457 502
458 try {503 try {
459- const { data, origin, timestamp, source } = JSON.parse(messageData);504+ const { data, origin, timestamp, source } =
505+ JSON.parse(messageData);
460 506
461 // Check if message is recent (within last 5 seconds) and from a popup507 // Check if message is recent (within last 5 seconds) and from a popup
462- if (Date.now() - timestamp < 5000 && source === 'popup') {508+ if (Date.now() - timestamp < 5000 && source === "popup") {
463- console.log(`[dumber-parent] Received message from popup:`, data);509+ console.log(
510+ `[dumber-parent] Received message from popup:`,
511+ data,
512+ );
464 513
465 // Dispatch as MessageEvent to parent window514 // Dispatch as MessageEvent to parent window
466- const event = new MessageEvent('message', {515+ const event = new MessageEvent("message", {
467 data,516 data,
468 origin: origin || window.location.origin,517 origin: origin || window.location.origin,
469 source: null, // popup reference would be here in real browser518 source: null, // popup reference would be here in real browser
@@ -474,54 +523,75 @@ function detectWindowType(features?: string | null): string {
474 localStorage.removeItem(key);523 localStorage.removeItem(key);
475 }524 }
476 } catch (err) {525 } catch (err) {
477- console.warn(`[dumber-parent] Failed to parse popup message:`, err);526+ console.warn(
527+ `[dumber-parent] Failed to parse popup message:`,
528+ err,
529+ );
478 localStorage.removeItem(key); // Clean up invalid message530 localStorage.removeItem(key); // Clean up invalid message
479 }531 }
480 }532 }
481 533
482 // Handle OAuth callback detection from popups534 // Handle OAuth callback detection from popups
483- if (key.startsWith('oauth_callback_')) {535+ if (key.startsWith("oauth_callback_")) {
484 const callbackData = localStorage.getItem(key);536 const callbackData = localStorage.getItem(key);
485 if (!callbackData) continue;537 if (!callbackData) continue;
486 538
487 try {539 try {
488- const { url, webviewId, timestamp, isOAuthCallback } = JSON.parse(callbackData);540+ const { url, webviewId, timestamp, isOAuthCallback } =
541+ JSON.parse(callbackData);
489 542
490 // Check if callback is recent (within last 10 seconds) and is an OAuth callback543 // Check if callback is recent (within last 10 seconds) and is an OAuth callback
491 if (Date.now() - timestamp < 10000 && isOAuthCallback) {544 if (Date.now() - timestamp < 10000 && isOAuthCallback) {
492- console.log(`[dumber-parent] OAuth callback detected for webview ${webviewId}:`, url);545+ console.log(
546+ `[dumber-parent] OAuth callback detected for webview ${webviewId}:`,
547+ url,
548+ );
493 549
494 // Send close request to backend for this popup webview550 // Send close request to backend for this popup webview
495 const bridge = window.webkit?.messageHandlers?.dumber;551 const bridge = window.webkit?.messageHandlers?.dumber;
496- if (bridge && typeof bridge.postMessage === 'function') {552+ if (bridge && typeof bridge.postMessage === "function") {
497 try {553 try {
498 const closeMessage = {554 const closeMessage = {
499- type: 'close-popup',555+ type: "close-popup",
500 webviewId,556 webviewId,
501- reason: 'oauth-callback-success',557+ reason: "oauth-callback-success",
502- timestamp: Date.now()558+ timestamp: Date.now(),
503 };559 };
504 560
505- console.log(`[dumber-parent] Sending popup close request:`, closeMessage);561+ console.log(
562+ `[dumber-parent] Sending popup close request:`,
563+ closeMessage,
564+ );
506 bridge.postMessage(JSON.stringify(closeMessage));565 bridge.postMessage(JSON.stringify(closeMessage));
507 } catch (err) {566 } catch (err) {
508- console.warn(`[dumber-parent] Failed to send popup close request:`, err);567+ console.warn(
568+ `[dumber-parent] Failed to send popup close request:`,
569+ err,
570+ );
509 }571 }
510 } else {572 } else {
511- console.warn(`[dumber-parent] No webkit bridge available for popup close request`);573+ console.warn(
574+ `[dumber-parent] No webkit bridge available for popup close request`,
575+ );
512 }576 }
513 577
514 // Clean up OAuth callback data578 // Clean up OAuth callback data
515 localStorage.removeItem(key);579 localStorage.removeItem(key);
516 }580 }
517 } catch (err) {581 } catch (err) {
518- console.warn(`[dumber-parent] Failed to parse OAuth callback data:`, err);582+ console.warn(
583+ `[dumber-parent] Failed to parse OAuth callback data:`,
584+ err,
585+ );
519 localStorage.removeItem(key); // Clean up invalid data586 localStorage.removeItem(key); // Clean up invalid data
520 }587 }
521 }588 }
522 }589 }
523 } catch (err) {590 } catch (err) {
524- console.warn(`[dumber-parent] Failed to poll for popup messages:`, err);591+ console.warn(
592+ `[dumber-parent] Failed to poll for popup messages:`,
593+ err,
594+ );
525 }595 }
526 };596 };
527 597
@@ -562,20 +632,19 @@ function detectWindowType(features?: string | null): string {
562 const url = window.location.href.toLowerCase();632 const url = window.location.href.toLowerCase();
563 633
564 // Check for OAuth callback patterns - be more specific to avoid false positives634 // Check for OAuth callback patterns - be more specific to avoid false positives
565- const isCallback = (635+ const isCallback =
566 // OAuth callback URLs typically have these parameters636 // OAuth callback URLs typically have these parameters
567- url.includes('code=') ||637+ url.includes("code=") ||
568- url.includes('access_token=') ||638+ url.includes("access_token=") ||
569- url.includes('id_token=') ||639+ url.includes("id_token=") ||
570 // OAuth error responses640 // OAuth error responses
571- url.includes('error=access_denied') ||641+ url.includes("error=access_denied") ||
572- url.includes('error=unauthorized') ||642+ url.includes("error=unauthorized") ||
573 // OAuth callback paths (more specific than just "callback" or "redirect")643 // OAuth callback paths (more specific than just "callback" or "redirect")
574- url.includes('/oauth/callback') ||644+ url.includes("/oauth/callback") ||
575- url.includes('/auth/callback') ||645+ url.includes("/auth/callback") ||
576- url.includes('oauth2callback') ||646+ url.includes("oauth2callback") ||
577- url.includes('googlepopupcallback')647+ url.includes("googlepopupcallback");
578- );
579 648
580 if (isCallback) {649 if (isCallback) {
581 try {650 try {
@@ -593,13 +662,18 @@ function detectWindowType(features?: string | null): string {
593 const age = Date.now() - (mapping.timestamp || 0);662 const age = Date.now() - (mapping.timestamp || 0);
594 if (age < 60000 && mapping.popupId) {663 if (age < 60000 && mapping.popupId) {
595 targetWebViewId = mapping.popupId;664 targetWebViewId = mapping.popupId;
596- console.log(`[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`);665+ console.log(
666+ `[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`,
667+ );
597 668
598 // Clean up the mapping since we're using it669 // Clean up the mapping since we're using it
599 localStorage.removeItem(popupMappingKey);670 localStorage.removeItem(popupMappingKey);
600 }671 }
601 } catch (err) {672 } catch (err) {
602- console.warn(`[oauth-callback] Failed to parse popup mapping:`, err);673+ console.warn(
674+ `[oauth-callback] Failed to parse popup mapping:`,
675+ err,
676+ );
603 }677 }
604 }678 }
605 679
@@ -610,20 +684,34 @@ function detectWindowType(features?: string | null): string {
610 isOAuthCallback: true,684 isOAuthCallback: true,
611 };685 };
612 686
613- localStorage.setItem(`oauth_callback_${targetWebViewId}`, JSON.stringify(callbackData));687+ localStorage.setItem(
614- console.log(`[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`, callbackData);688+ `oauth_callback_${targetWebViewId}`,
689+ JSON.stringify(callbackData),
690+ );
691+ console.log(
692+ `[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`,
693+ callbackData,
694+ );
615 695
616 // Auto-cleanup sensitive OAuth data after 10 seconds696 // Auto-cleanup sensitive OAuth data after 10 seconds
617 setTimeout(() => {697 setTimeout(() => {
618 try {698 try {
619 localStorage.removeItem(`oauth_callback_${targetWebViewId}`);699 localStorage.removeItem(`oauth_callback_${targetWebViewId}`);
620- console.log(`[oauth-callback] Cleaned up OAuth callback data from localStorage`);700+ console.log(
701+ `[oauth-callback] Cleaned up OAuth callback data from localStorage`,
702+ );
621 } catch (err) {703 } catch (err) {
622- console.warn(`[oauth-callback] Failed to cleanup OAuth callback data:`, err);704+ console.warn(
705+ `[oauth-callback] Failed to cleanup OAuth callback data:`,
706+ err,
707+ );
623 }708 }
624 }, 10000);709 }, 10000);
625 } catch (err) {710 } catch (err) {
626- console.warn(`[oauth-callback] Failed to store OAuth callback data:`, err);711+ console.warn(
712+ `[oauth-callback] Failed to store OAuth callback data:`,
713+ err,
714+ );
627 }715 }
628 }716 }
629 };717 };
@@ -647,11 +735,11 @@ function detectWindowType(features?: string | null): string {
647 });735 });
648 736
649 // Also check on navigation events737 // Also check on navigation events
650- window.addEventListener('popstate', detectOAuthCallback);738+ window.addEventListener("popstate", detectOAuthCallback);
651- window.addEventListener('hashchange', detectOAuthCallback);739+ window.addEventListener("hashchange", detectOAuthCallback);
652 cleanupHandlers.push(() => {740 cleanupHandlers.push(() => {
653- window.removeEventListener('popstate', detectOAuthCallback);741+ window.removeEventListener("popstate", detectOAuthCallback);
654- window.removeEventListener('hashchange', detectOAuthCallback);742+ window.removeEventListener("hashchange", detectOAuthCallback);
655 });743 });
656 };744 };
657 745
@@ -737,23 +825,32 @@ function detectWindowType(features?: string | null): string {
737 return timeSinceInteraction <= INTERACTION_TIMEOUT;825 return timeSinceInteraction <= INTERACTION_TIMEOUT;
738 };826 };
739 827
740-
741 const createFakeWindow = (url: string, popupId?: string): WindowProxy => {828 const createFakeWindow = (url: string, popupId?: string): WindowProxy => {
742 // Generate unique popup ID if not provided829 // Generate unique popup ID if not provided
743- const actualPopupId = popupId || `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;830+ const actualPopupId =
831+ popupId ||
832+ `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
744 833
745 // Set up communication channel via shared localStorage834 // Set up communication channel via shared localStorage
746 try {835 try {
747- localStorage.setItem(`popup_${actualPopupId}_parent_info`, JSON.stringify({836+ localStorage.setItem(
748- parentUrl: window.location.href,837+ `popup_${actualPopupId}_parent_info`,
749- parentWebViewId: window.__dumber_webview_id,838+ JSON.stringify({
750- timestamp: Date.now(),839+ parentUrl: window.location.href,
751- popupUrl: url,840+ parentWebViewId: window.__dumber_webview_id,
752- }));841+ timestamp: Date.now(),
842+ popupUrl: url,
843+ }),
844+ );
753 845
754- console.log(`[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`);846+ console.log(
847+ `[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`,
848+ );
755 } catch (err) {849 } catch (err) {
756- console.warn(`[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`, err);850+ console.warn(
851+ `[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`,
852+ err,
853+ );
757 }854 }
758 855
759 return {856 return {
@@ -764,33 +861,45 @@ function detectWindowType(features?: string | null): string {
764 },861 },
765 focus: () => {862 focus: () => {
766 try {863 try {
767- localStorage.setItem(`popup_${actualPopupId}_parent_action`, JSON.stringify({864+ localStorage.setItem(
768- action: 'focus',865+ `popup_${actualPopupId}_parent_action`,
769- timestamp: Date.now(),866+ JSON.stringify({
770- }));867+ action: "focus",
868+ timestamp: Date.now(),
869+ }),
870+ );
771 } catch (err) {871 } catch (err) {
772 console.warn(`[window-open] Failed to store focus action:`, err);872 console.warn(`[window-open] Failed to store focus action:`, err);
773 }873 }
774 },874 },
775 blur: () => {875 blur: () => {
776 try {876 try {
777- localStorage.setItem(`popup_${actualPopupId}_parent_action`, JSON.stringify({877+ localStorage.setItem(
778- action: 'blur',878+ `popup_${actualPopupId}_parent_action`,
779- timestamp: Date.now(),879+ JSON.stringify({
780- }));880+ action: "blur",
881+ timestamp: Date.now(),
882+ }),
883+ );
781 } catch (err) {884 } catch (err) {
782 console.warn(`[window-open] Failed to store blur action:`, err);885 console.warn(`[window-open] Failed to store blur action:`, err);
783 }886 }
784 },887 },
785 postMessage: (data: unknown, targetOrigin?: string) => {888 postMessage: (data: unknown, targetOrigin?: string) => {
786 try {889 try {
787- localStorage.setItem(`popup_${actualPopupId}_message_to_popup`, JSON.stringify({890+ localStorage.setItem(
891+ `popup_${actualPopupId}_message_to_popup`,
892+ JSON.stringify({
893+ data,
894+ origin: targetOrigin || "*",
895+ timestamp: Date.now(),
896+ source: "parent",
897+ }),
898+ );
899+ console.log(
900+ `[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`,
788 data,901 data,
789- origin: targetOrigin || '*',902+ );
790- timestamp: Date.now(),
791- source: 'parent',
792- }));
793- console.log(`[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`, data);
794 } catch (err) {903 } catch (err) {
795 console.warn(`[window-open] Failed to store postMessage:`, err);904 console.warn(`[window-open] Failed to store postMessage:`, err);
796 }905 }
@@ -798,9 +907,13 @@ function detectWindowType(features?: string | null): string {
798 } as unknown as WindowProxy;907 } as unknown as WindowProxy;
799 };908 };
800 909
801-
802 // Track user interactions for popup validation910 // Track user interactions for popup validation
803- const interactionEvents = ["click", "mousedown", "keydown", "touchstart"] as const;911+ const interactionEvents = [
912+ "click",
913+ "mousedown",
914+ "keydown",
915+ "touchstart",
916+ ] as const;
804 interactionEvents.forEach((eventType) => {917 interactionEvents.forEach((eventType) => {
805 document.addEventListener(eventType, trackUserInteraction, true);918 document.addEventListener(eventType, trackUserInteraction, true);
806 });919 });
@@ -814,7 +927,6 @@ function detectWindowType(features?: string | null): string {
814 `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`,927 `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`,
815 );928 );
816 929
817-
818 // Window.open interceptor - only install once930 // Window.open interceptor - only install once
819 if (!window.__dumber_window_open_intercepted) {931 if (!window.__dumber_window_open_intercepted) {
820 try {932 try {
@@ -837,7 +949,6 @@ function detectWindowType(features?: string | null): string {
837 features,949 features,
838 );950 );
839 951
840-
841 // Enhanced duplicate check952 // Enhanced duplicate check
842 if (windowOpenDebouncer.isDuplicate(urlString, webviewId)) {953 if (windowOpenDebouncer.isDuplicate(urlString, webviewId)) {
843 console.log(954 console.log(
@@ -927,7 +1038,7 @@ function detectWindowType(features?: string | null): string {
927 try {1038 try {
928 cleanup();1039 cleanup();
929 } catch (cleanupErr) {1040 } catch (cleanupErr) {
930- console.warn('[dumber] Cleanup handler failed', cleanupErr);1041+ console.warn("[dumber] Cleanup handler failed", cleanupErr);
931 }1042 }
932 }1043 }
933 1044
@@ -942,18 +1053,20 @@ function detectWindowType(features?: string | null): string {
942 window.__dumber_showZoomToast = undefined;1053 window.__dumber_showZoomToast = undefined;
943 window.__dumber_applyDomZoom = undefined;1054 window.__dumber_applyDomZoom = undefined;
944 1055
945- if (window.__dumber) {1056+ const globalState = window.__dumber;
946- delete window.__dumber.omnibox;1057+ if (globalState) {
947- delete window.__dumber.toast;1058+ const mutableState = globalState as unknown as Record<string, unknown>;
1059+ delete mutableState.omnibox;
1060+ delete mutableState.toast;
948 }1061 }
949 };1062 };
950 1063
951 window.__dumber_teardown = teardown;1064 window.__dumber_teardown = teardown;
952- window.addEventListener('pagehide', teardown, { once: true });1065+ window.addEventListener("pagehide", teardown, { once: true });
953- window.addEventListener('beforeunload', teardown, { once: true });1066+ window.addEventListener("beforeunload", teardown, { once: true });
954 cleanupHandlers.push(() => {1067 cleanupHandlers.push(() => {
955- window.removeEventListener('pagehide', teardown);1068+ window.removeEventListener("pagehide", teardown);
956- window.removeEventListener('beforeunload', teardown);1069+ window.removeEventListener("beforeunload", teardown);
957 window.__dumber_teardown = undefined;1070 window.__dumber_teardown = undefined;
958 });1071 });
959 1072
M gui/src/injected/modules/color-scheme.ts +103 -11
@@ -22,10 +22,23 @@ interface ColorSchemeConfig {
2222
2323 class ColorSchemeManager {
2424 private config: ColorSchemeConfig;
25+ private metaElement: HTMLMetaElement | null = null;
26+ private styleElement: HTMLStyleElement | null = null;
27+ private userPreference: "light" | "dark" | null = null;
28+ private static readonly STORAGE_KEY = "dumber.theme";
29+ private static instance: ColorSchemeManager | null = null;
30+ private static matchMediaPatched = false;
31+ private static originalMatchMedia: typeof window.matchMedia | null = null;
2532
2633 constructor() {
34+ ColorSchemeManager.instance = this;
35+
36+ this.userPreference = this.getStoredPreference();
37+
2738 // Use GTK preference if available, otherwise detect from system
28- const prefersDark = window.__dumber_gtk_prefers_dark ?? false;
39+ const prefersDark = this.userPreference
40+ ? this.userPreference === "dark"
41+ : (window.__dumber_gtk_prefers_dark ?? false);
2942
3043 this.config = {
3144 isDark: prefersDark,
@@ -79,32 +92,60 @@ class ColorSchemeManager {
7992 }
8093
8194 private injectColorSchemeMeta(): void {
82- const meta = document.createElement("meta");
95+ if (this.metaElement && this.metaElement.isConnected) {
96+ this.metaElement.content = this.config.metaContent;
97+ return;
98+ }
99+
100+ const existing = document.querySelector(
101+ 'meta[name="color-scheme"]',
102+ ) as HTMLMetaElement | null;
103+ const meta = existing ?? document.createElement("meta");
83104 meta.name = "color-scheme";
84105 meta.content = this.config.metaContent;
85- document.documentElement.appendChild(meta);
106+ if (!existing) {
107+ document.documentElement.appendChild(meta);
108+ }
109+ this.metaElement = meta;
86110 }
87111
88112 private applyRootStyles(): void {
89- const style = document.createElement("style");
113+ if (this.styleElement && this.styleElement.isConnected) {
114+ this.styleElement.textContent = this.config.rootStyle;
115+ return;
116+ }
117+
118+ const existing = document.querySelector(
119+ "style[data-dumber-color-scheme]",
120+ ) as HTMLStyleElement | null;
121+ const style = existing ?? document.createElement("style");
122+ style.setAttribute("data-dumber-color-scheme", "");
90123 style.textContent = this.config.rootStyle;
91- document.documentElement.appendChild(style);
124+ if (!existing) {
125+ document.documentElement.appendChild(style);
126+ }
127+ this.styleElement = style;
92128 }
93129
94130 private overrideMatchMedia(): void {
131+ if (ColorSchemeManager.matchMediaPatched) {
132+ return;
133+ }
134+
95135 const darkQuery = "(prefers-color-scheme: dark)";
96136 const lightQuery = "(prefers-color-scheme: light)";
97- const originalMatchMedia = window.matchMedia;
137+ ColorSchemeManager.originalMatchMedia = window.matchMedia.bind(window);
98138
99139 window.matchMedia = (query: string): MediaQueryList => {
140+ const manager = ColorSchemeManager.instance;
100141 if (
142+ manager &&
101143 typeof query === "string" &&
102144 (query.includes(darkQuery) || query.includes(lightQuery))
103145 ) {
104- // Create a mock MediaQueryList that reflects our GTK preference
105146 const matches = query.includes("dark")
106- ? this.config.isDark
107- : !this.config.isDark;
147+ ? manager.config.isDark
148+ : !manager.config.isDark;
108149
109150 return {
110151 matches,
@@ -118,9 +159,15 @@ class ColorSchemeManager {
118159 } as MediaQueryList;
119160 }
120161
121- // For non-color-scheme queries, use the original implementation
122- return originalMatchMedia.call(window, query);
162+ const original = ColorSchemeManager.originalMatchMedia;
163+ if (!original) {
164+ throw new Error("Original matchMedia not available");
165+ }
166+
167+ return original.call(window, query);
123168 };
169+
170+ ColorSchemeManager.matchMediaPatched = true;
124171 }
125172
126173 private setupThemeIntegration(): void {
@@ -160,6 +207,42 @@ class ColorSchemeManager {
160207 // Re-apply all configurations
161208 this.initialize();
162209 }
210+
211+ private getStoredPreference(): "light" | "dark" | null {
212+ try {
213+ const value = localStorage.getItem(ColorSchemeManager.STORAGE_KEY);
214+ if (value === "light" || value === "dark") {
215+ return value;
216+ }
217+ } catch (error) {
218+ console.warn("[dumber] Failed to read stored theme preference", error);
219+ }
220+ return null;
221+ }
222+
223+ private storePreference(theme: "light" | "dark"): void {
224+ try {
225+ localStorage.setItem(ColorSchemeManager.STORAGE_KEY, theme);
226+ } catch (error) {
227+ console.warn("[dumber] Failed to persist theme preference", error);
228+ }
229+ }
230+
231+ /**
232+ * Persist and apply an explicit user preference
233+ */
234+ public setUserPreference(theme: "light" | "dark"): void {
235+ if (this.userPreference === theme && this.config.colorScheme === theme) {
236+ return;
237+ }
238+ this.userPreference = theme;
239+ this.storePreference(theme);
240+ this.updateColorScheme(theme === "dark");
241+ }
242+
243+ public getCurrentTheme(): "light" | "dark" {
244+ return this.config.colorScheme;
245+ }
163246 }
164247
165248 // Initialize color scheme management
@@ -169,3 +252,12 @@ const colorSchemeManager = new ColorSchemeManager();
169252 (window as any).__dumber_color_scheme_manager = colorSchemeManager;
170253
171254 export default colorSchemeManager;
255+
256+declare global {
257+ interface Window {
258+ __dumber_color_scheme_manager?: {
259+ setUserPreference: (theme: "light" | "dark") => void;
260+ getCurrentTheme: () => "light" | "dark";
261+ };
262+ }
263+}
@@ -22,10 +22,23 @@ interface ColorSchemeConfig {
22 22
23 class ColorSchemeManager {23 class ColorSchemeManager {
24 private config: ColorSchemeConfig;24 private config: ColorSchemeConfig;
25+ private metaElement: HTMLMetaElement | null = null;
26+ private styleElement: HTMLStyleElement | null = null;
27+ private userPreference: "light" | "dark" | null = null;
28+ private static readonly STORAGE_KEY = "dumber.theme";
29+ private static instance: ColorSchemeManager | null = null;
30+ private static matchMediaPatched = false;
31+ private static originalMatchMedia: typeof window.matchMedia | null = null;
25 32
26 constructor() {33 constructor() {
34+ ColorSchemeManager.instance = this;
35+
36+ this.userPreference = this.getStoredPreference();
37+
27 // Use GTK preference if available, otherwise detect from system38 // Use GTK preference if available, otherwise detect from system
28- const prefersDark = window.__dumber_gtk_prefers_dark ?? false;39+ const prefersDark = this.userPreference
40+ ? this.userPreference === "dark"
41+ : (window.__dumber_gtk_prefers_dark ?? false);
29 42
30 this.config = {43 this.config = {
31 isDark: prefersDark,44 isDark: prefersDark,
@@ -79,32 +92,60 @@ class ColorSchemeManager {
79 }92 }
80 93
81 private injectColorSchemeMeta(): void {94 private injectColorSchemeMeta(): void {
82- const meta = document.createElement("meta");95+ if (this.metaElement && this.metaElement.isConnected) {
96+ this.metaElement.content = this.config.metaContent;
97+ return;
98+ }
99+
100+ const existing = document.querySelector(
101+ 'meta[name="color-scheme"]',
102+ ) as HTMLMetaElement | null;
103+ const meta = existing ?? document.createElement("meta");
83 meta.name = "color-scheme";104 meta.name = "color-scheme";
84 meta.content = this.config.metaContent;105 meta.content = this.config.metaContent;
85- document.documentElement.appendChild(meta);106+ if (!existing) {
107+ document.documentElement.appendChild(meta);
108+ }
109+ this.metaElement = meta;
86 }110 }
87 111
88 private applyRootStyles(): void {112 private applyRootStyles(): void {
89- const style = document.createElement("style");113+ if (this.styleElement && this.styleElement.isConnected) {
114+ this.styleElement.textContent = this.config.rootStyle;
115+ return;
116+ }
117+
118+ const existing = document.querySelector(
119+ "style[data-dumber-color-scheme]",
120+ ) as HTMLStyleElement | null;
121+ const style = existing ?? document.createElement("style");
122+ style.setAttribute("data-dumber-color-scheme", "");
90 style.textContent = this.config.rootStyle;123 style.textContent = this.config.rootStyle;
91- document.documentElement.appendChild(style);124+ if (!existing) {
125+ document.documentElement.appendChild(style);
126+ }
127+ this.styleElement = style;
92 }128 }
93 129
94 private overrideMatchMedia(): void {130 private overrideMatchMedia(): void {
131+ if (ColorSchemeManager.matchMediaPatched) {
132+ return;
133+ }
134+
95 const darkQuery = "(prefers-color-scheme: dark)";135 const darkQuery = "(prefers-color-scheme: dark)";
96 const lightQuery = "(prefers-color-scheme: light)";136 const lightQuery = "(prefers-color-scheme: light)";
97- const originalMatchMedia = window.matchMedia;137+ ColorSchemeManager.originalMatchMedia = window.matchMedia.bind(window);
98 138
99 window.matchMedia = (query: string): MediaQueryList => {139 window.matchMedia = (query: string): MediaQueryList => {
140+ const manager = ColorSchemeManager.instance;
100 if (141 if (
142+ manager &&
101 typeof query === "string" &&143 typeof query === "string" &&
102 (query.includes(darkQuery) || query.includes(lightQuery))144 (query.includes(darkQuery) || query.includes(lightQuery))
103 ) {145 ) {
104- // Create a mock MediaQueryList that reflects our GTK preference
105 const matches = query.includes("dark")146 const matches = query.includes("dark")
106- ? this.config.isDark147+ ? manager.config.isDark
107- : !this.config.isDark;148+ : !manager.config.isDark;
108 149
109 return {150 return {
110 matches,151 matches,
@@ -118,9 +159,15 @@ class ColorSchemeManager {
118 } as MediaQueryList;159 } as MediaQueryList;
119 }160 }
120 161
121- // For non-color-scheme queries, use the original implementation162+ const original = ColorSchemeManager.originalMatchMedia;
122- return originalMatchMedia.call(window, query);163+ if (!original) {
164+ throw new Error("Original matchMedia not available");
165+ }
166+
167+ return original.call(window, query);
123 };168 };
169+
170+ ColorSchemeManager.matchMediaPatched = true;
124 }171 }
125 172
126 private setupThemeIntegration(): void {173 private setupThemeIntegration(): void {
@@ -160,6 +207,42 @@ class ColorSchemeManager {
160 // Re-apply all configurations207 // Re-apply all configurations
161 this.initialize();208 this.initialize();
162 }209 }
210+
211+ private getStoredPreference(): "light" | "dark" | null {
212+ try {
213+ const value = localStorage.getItem(ColorSchemeManager.STORAGE_KEY);
214+ if (value === "light" || value === "dark") {
215+ return value;
216+ }
217+ } catch (error) {
218+ console.warn("[dumber] Failed to read stored theme preference", error);
219+ }
220+ return null;
221+ }
222+
223+ private storePreference(theme: "light" | "dark"): void {
224+ try {
225+ localStorage.setItem(ColorSchemeManager.STORAGE_KEY, theme);
226+ } catch (error) {
227+ console.warn("[dumber] Failed to persist theme preference", error);
228+ }
229+ }
230+
231+ /**
232+ * Persist and apply an explicit user preference
233+ */
234+ public setUserPreference(theme: "light" | "dark"): void {
235+ if (this.userPreference === theme && this.config.colorScheme === theme) {
236+ return;
237+ }
238+ this.userPreference = theme;
239+ this.storePreference(theme);
240+ this.updateColorScheme(theme === "dark");
241+ }
242+
243+ public getCurrentTheme(): "light" | "dark" {
244+ return this.config.colorScheme;
245+ }
163 }246 }
164 247
165 // Initialize color scheme management248 // Initialize color scheme management
@@ -169,3 +252,12 @@ const colorSchemeManager = new ColorSchemeManager();
169 (window as any).__dumber_color_scheme_manager = colorSchemeManager;252 (window as any).__dumber_color_scheme_manager = colorSchemeManager;
170 253
171 export default colorSchemeManager;254 export default colorSchemeManager;
255+
256+declare global {
257+ interface Window {
258+ __dumber_color_scheme_manager?: {
259+ setUserPreference: (theme: "light" | "dark") => void;
260+ getCurrentTheme: () => "light" | "dark";
261+ };
262+ }
263+}

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