fix(workspace): keep stacked panes visible during popup splits
- commit
- a36e3e712b89519668ce318eb14048b40ea01444
- author
- bamen · · 2025-10-05 18:37 UTC
- committer
- bamen · · 2025-10-05 18:09 UTC
- parent
- 81824f0
- signature
- unsigned
3 files · +46 -36
M gui/src/injected/main-world.ts +38 -34
| @@ -628,7 +628,13 @@ function detectWindowType(features?: string | null): string { | |||
| 628 | 628 | ||
| 629 | 629 | // OAuth callback detection for auto-close | |
| 630 | 630 | const setupOAuthCallbackDetection = () => { | |
| 631 | + | let oauthCallbackHandled = false; | |
| 632 | + | ||
| 631 | 633 | const detectOAuthCallback = () => { | |
| 634 | + | if (oauthCallbackHandled) { | |
| 635 | + | return; | |
| 636 | + | } | |
| 637 | + | ||
| 632 | 638 | const url = window.location.href.toLowerCase(); | |
| 633 | 639 | ||
| 634 | 640 | // Check for OAuth callback patterns - be more specific to avoid false positives | |
| @@ -647,69 +653,67 @@ function detectWindowType(features?: string | null): string { | |||
| 647 | 653 | url.includes("googlepopupcallback"); | |
| 648 | 654 | ||
| 649 | 655 | if (isCallback) { | |
| 656 | + | oauthCallbackHandled = true; | |
| 650 | 657 | try { | |
| 651 | - | // Determine which webview should be closed | |
| 658 | + | // Determine which WebView should close as part of this callback | |
| 652 | 659 | let targetWebViewId = window.__dumber_webview_id; | |
| 653 | 660 | ||
| 654 | - | // If this is a parent window detecting OAuth callback, find the associated popup | |
| 661 | + | // When the parent window detects the callback, look up its popup mapping | |
| 655 | 662 | const popupMappingKey = `popup_mapping_${window.__dumber_webview_id}`; | |
| 656 | - | const popupMappingData = localStorage.getItem(popupMappingKey); | |
| 657 | - | ||
| 658 | - | if (popupMappingData) { | |
| 659 | - | try { | |
| 663 | + | try { | |
| 664 | + | const popupMappingData = localStorage.getItem(popupMappingKey); | |
| 665 | + | if (popupMappingData) { | |
| 660 | 666 | const mapping = JSON.parse(popupMappingData); | |
| 661 | - | // Check if mapping is recent (within last 60 seconds) | |
| 662 | 667 | const age = Date.now() - (mapping.timestamp || 0); | |
| 663 | 668 | if (age < 60000 && mapping.popupId) { | |
| 664 | 669 | targetWebViewId = mapping.popupId; | |
| 665 | 670 | console.log( | |
| 666 | 671 | `[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`, | |
| 667 | 672 | ); | |
| 668 | - | ||
| 669 | - | // Clean up the mapping since we're using it | |
| 670 | 673 | localStorage.removeItem(popupMappingKey); | |
| 671 | 674 | } | |
| 672 | - | } catch (err) { | |
| 673 | - | console.warn( | |
| 674 | - | `[oauth-callback] Failed to parse popup mapping:`, | |
| 675 | - | err, | |
| 676 | - | ); | |
| 677 | 675 | } | |
| 676 | + | } catch (err) { | |
| 677 | + | console.warn( | |
| 678 | + | `[oauth-callback] Failed to resolve popup mapping:`, | |
| 679 | + | err, | |
| 680 | + | ); | |
| 678 | 681 | } | |
| 679 | 682 | ||
| 680 | - | const callbackData = { | |
| 681 | - | url: window.location.href, | |
| 682 | - | webviewId: targetWebViewId, | |
| 683 | - | timestamp: Date.now(), | |
| 684 | - | isOAuthCallback: true, | |
| 685 | - | }; | |
| 686 | - | ||
| 687 | - | localStorage.setItem( | |
| 688 | - | `oauth_callback_${targetWebViewId}`, | |
| 689 | - | JSON.stringify(callbackData), | |
| 690 | - | ); | |
| 691 | 683 | console.log( | |
| 692 | - | `[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`, | |
| 693 | - | callbackData, | |
| 684 | + | `[oauth-callback] Detected OAuth callback, closing popup webview: ${targetWebViewId}`, | |
| 694 | 685 | ); | |
| 695 | 686 | ||
| 696 | - | // Auto-cleanup sensitive OAuth data after 10 seconds | |
| 697 | - | setTimeout(() => { | |
| 687 | + | // Send close request to backend for this popup webview | |
| 688 | + | const bridge = window.webkit?.messageHandlers?.dumber; | |
| 689 | + | if (bridge && typeof bridge.postMessage === "function") { | |
| 698 | 690 | try { | |
| 699 | - | localStorage.removeItem(`oauth_callback_${targetWebViewId}`); | |
| 691 | + | const closeMessage = { | |
| 692 | + | type: "close-popup", | |
| 693 | + | webviewId: targetWebViewId, | |
| 694 | + | reason: "oauth-callback-success", | |
| 695 | + | timestamp: Date.now(), | |
| 696 | + | }; | |
| 697 | + | ||
| 700 | 698 | console.log( | |
| 701 | - | `[oauth-callback] Cleaned up OAuth callback data from localStorage`, | |
| 699 | + | `[oauth-callback] Sending popup close request:`, | |
| 700 | + | closeMessage, | |
| 702 | 701 | ); | |
| 702 | + | bridge.postMessage(JSON.stringify(closeMessage)); | |
| 703 | 703 | } catch (err) { | |
| 704 | 704 | console.warn( | |
| 705 | - | `[oauth-callback] Failed to cleanup OAuth callback data:`, | |
| 705 | + | `[oauth-callback] Failed to send popup close request:`, | |
| 706 | 706 | err, | |
| 707 | 707 | ); | |
| 708 | 708 | } | |
| 709 | - | }, 10000); | |
| 709 | + | } else { | |
| 710 | + | console.warn( | |
| 711 | + | `[oauth-callback] No webkit bridge available for popup close request`, | |
| 712 | + | ); | |
| 713 | + | } | |
| 710 | 714 | } catch (err) { | |
| 711 | 715 | console.warn( | |
| 712 | - | `[oauth-callback] Failed to store OAuth callback data:`, | |
| 716 | + | `[oauth-callback] Failed to handle OAuth callback:`, | |
| 713 | 717 | err, | |
| 714 | 718 | ); | |
| 715 | 719 | } | |
| @@ -628,7 +628,13 @@ function detectWindowType(features?: string | null): string { | |||||
| 628 | 628 | ||||
| 629 | // OAuth callback detection for auto-close | 629 | // OAuth callback detection for auto-close | ||
| 630 | const setupOAuthCallbackDetection = () => { | 630 | const setupOAuthCallbackDetection = () => { | ||
| 631 | + | let oauthCallbackHandled = false; | |||
| 632 | + | ||||
| 631 | const detectOAuthCallback = () => { | 633 | const detectOAuthCallback = () => { | ||
| 634 | + | if (oauthCallbackHandled) { | |||
| 635 | + | return; | |||
| 636 | + | } | |||
| 637 | + | ||||
| 632 | const url = window.location.href.toLowerCase(); | 638 | const url = window.location.href.toLowerCase(); | ||
| 633 | 639 | ||||
| 634 | // Check for OAuth callback patterns - be more specific to avoid false positives | 640 | // Check for OAuth callback patterns - be more specific to avoid false positives | ||
| @@ -647,69 +653,67 @@ function detectWindowType(features?: string | null): string { | |||||
| 647 | url.includes("googlepopupcallback"); | 653 | url.includes("googlepopupcallback"); | ||
| 648 | 654 | ||||
| 649 | if (isCallback) { | 655 | if (isCallback) { | ||
| 656 | + | oauthCallbackHandled = true; | |||
| 650 | try { | 657 | try { | ||
| 651 | - | // Determine which webview should be closed | 658 | + | // Determine which WebView should close as part of this callback |
| 652 | let targetWebViewId = window.__dumber_webview_id; | 659 | let targetWebViewId = window.__dumber_webview_id; | ||
| 653 | 660 | ||||
| 654 | - | // If this is a parent window detecting OAuth callback, find the associated popup | 661 | + | // When the parent window detects the callback, look up its popup mapping |
| 655 | const popupMappingKey = `popup_mapping_${window.__dumber_webview_id}`; | 662 | const popupMappingKey = `popup_mapping_${window.__dumber_webview_id}`; | ||
| 656 | - | const popupMappingData = localStorage.getItem(popupMappingKey); | 663 | + | try { |
| 657 | - | 664 | + | const popupMappingData = localStorage.getItem(popupMappingKey); | |
| 658 | - | if (popupMappingData) { | 665 | + | if (popupMappingData) { |
| 659 | - | try { | |||
| 660 | const mapping = JSON.parse(popupMappingData); | 666 | const mapping = JSON.parse(popupMappingData); | ||
| 661 | - | // Check if mapping is recent (within last 60 seconds) | |||
| 662 | const age = Date.now() - (mapping.timestamp || 0); | 667 | const age = Date.now() - (mapping.timestamp || 0); | ||
| 663 | if (age < 60000 && mapping.popupId) { | 668 | if (age < 60000 && mapping.popupId) { | ||
| 664 | targetWebViewId = mapping.popupId; | 669 | targetWebViewId = mapping.popupId; | ||
| 665 | console.log( | 670 | console.log( | ||
| 666 | `[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`, | 671 | `[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`, | ||
| 667 | ); | 672 | ); | ||
| 668 | - | ||||
| 669 | - | // Clean up the mapping since we're using it | |||
| 670 | localStorage.removeItem(popupMappingKey); | 673 | localStorage.removeItem(popupMappingKey); | ||
| 671 | } | 674 | } | ||
| 672 | - | } catch (err) { | |||
| 673 | - | console.warn( | |||
| 674 | - | `[oauth-callback] Failed to parse popup mapping:`, | |||
| 675 | - | err, | |||
| 676 | - | ); | |||
| 677 | } | 675 | } | ||
| 676 | + | } catch (err) { | |||
| 677 | + | console.warn( | |||
| 678 | + | `[oauth-callback] Failed to resolve popup mapping:`, | |||
| 679 | + | err, | |||
| 680 | + | ); | |||
| 678 | } | 681 | } | ||
| 679 | 682 | ||||
| 680 | - | const callbackData = { | |||
| 681 | - | url: window.location.href, | |||
| 682 | - | webviewId: targetWebViewId, | |||
| 683 | - | timestamp: Date.now(), | |||
| 684 | - | isOAuthCallback: true, | |||
| 685 | - | }; | |||
| 686 | - | ||||
| 687 | - | localStorage.setItem( | |||
| 688 | - | `oauth_callback_${targetWebViewId}`, | |||
| 689 | - | JSON.stringify(callbackData), | |||
| 690 | - | ); | |||
| 691 | console.log( | 683 | console.log( | ||
| 692 | - | `[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`, | 684 | + | `[oauth-callback] Detected OAuth callback, closing popup webview: ${targetWebViewId}`, |
| 693 | - | callbackData, | |||
| 694 | ); | 685 | ); | ||
| 695 | 686 | ||||
| 696 | - | // Auto-cleanup sensitive OAuth data after 10 seconds | 687 | + | // Send close request to backend for this popup webview |
| 697 | - | setTimeout(() => { | 688 | + | const bridge = window.webkit?.messageHandlers?.dumber; |
| 689 | + | if (bridge && typeof bridge.postMessage === "function") { | |||
| 698 | try { | 690 | try { | ||
| 699 | - | localStorage.removeItem(`oauth_callback_${targetWebViewId}`); | 691 | + | const closeMessage = { |
| 692 | + | type: "close-popup", | |||
| 693 | + | webviewId: targetWebViewId, | |||
| 694 | + | reason: "oauth-callback-success", | |||
| 695 | + | timestamp: Date.now(), | |||
| 696 | + | }; | |||
| 697 | + | ||||
| 700 | console.log( | 698 | console.log( | ||
| 701 | - | `[oauth-callback] Cleaned up OAuth callback data from localStorage`, | 699 | + | `[oauth-callback] Sending popup close request:`, |
| 700 | + | closeMessage, | |||
| 702 | ); | 701 | ); | ||
| 702 | + | bridge.postMessage(JSON.stringify(closeMessage)); | |||
| 703 | } catch (err) { | 703 | } catch (err) { | ||
| 704 | console.warn( | 704 | console.warn( | ||
| 705 | - | `[oauth-callback] Failed to cleanup OAuth callback data:`, | 705 | + | `[oauth-callback] Failed to send popup close request:`, |
| 706 | err, | 706 | err, | ||
| 707 | ); | 707 | ); | ||
| 708 | } | 708 | } | ||
| 709 | - | }, 10000); | 709 | + | } else { |
| 710 | + | console.warn( | |||
| 711 | + | `[oauth-callback] No webkit bridge available for popup close request`, | |||
| 712 | + | ); | |||
| 713 | + | } | |||
| 710 | } catch (err) { | 714 | } catch (err) { | ||
| 711 | console.warn( | 715 | console.warn( | ||
| 712 | - | `[oauth-callback] Failed to store OAuth callback data:`, | 716 | + | `[oauth-callback] Failed to handle OAuth callback:`, |
| 713 | err, | 717 | err, | ||
| 714 | ); | 718 | ); | ||
| 715 | } | 719 | } | ||
M internal/app/browser/workspace_utils.go +7 -0
| @@ -73,6 +73,13 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u | |||
| 73 | 73 | func (wm *WorkspaceManager) setContainer(node *paneNode, ptr uintptr, typeInfo string) { | |
| 74 | 74 | _ = typeInfo // Type info kept for API compatibility but not used | |
| 75 | 75 | node.container = ptr | |
| 76 | + | ||
| 77 | + | // Mark widget validity based on container presence so idle guards run for stacks too | |
| 78 | + | if ptr != 0 { | |
| 79 | + | node.widgetValid = true | |
| 80 | + | } else { | |
| 81 | + | node.widgetValid = false | |
| 82 | + | } | |
| 76 | 83 | } | |
| 77 | 84 | ||
| 78 | 85 | // setTitleBar sets the titleBar widget pointer | |
| @@ -73,6 +73,13 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u | |||||
| 73 | func (wm *WorkspaceManager) setContainer(node *paneNode, ptr uintptr, typeInfo string) { | 73 | func (wm *WorkspaceManager) setContainer(node *paneNode, ptr uintptr, typeInfo string) { | ||
| 74 | _ = typeInfo // Type info kept for API compatibility but not used | 74 | _ = typeInfo // Type info kept for API compatibility but not used | ||
| 75 | node.container = ptr | 75 | node.container = ptr | ||
| 76 | + | ||||
| 77 | + | // Mark widget validity based on container presence so idle guards run for stacks too | |||
| 78 | + | if ptr != 0 { | |||
| 79 | + | node.widgetValid = true | |||
| 80 | + | } else { | |||
| 81 | + | node.widgetValid = false | |||
| 82 | + | } | |||
| 76 | } | 83 | } | ||
| 77 | 84 | ||||
| 78 | // setTitleBar sets the titleBar widget pointer | 85 | // setTitleBar sets the titleBar widget pointer | ||
M pkg/webkit/workspace_widgets_cgo.go +1 -2
| @@ -184,9 +184,8 @@ static void widget_remove_focus_controller(GtkWidget* widget, GtkEventController | |||
| 184 | 184 | ||
| 185 | 185 | tracker->is_attached = FALSE; | |
| 186 | 186 | ||
| 187 | - | // Remove from registry and unref controller to avoid leaks | |
| 187 | + | // Remove from registry; GTK drops the final reference when removing controller | |
| 188 | 188 | g_hash_table_remove(controller_registry, controller); | |
| 189 | - | g_object_unref(controller); | |
| 190 | 189 | } | |
| 191 | 190 | ||
| 192 | 191 | static GtkWidget* paned_get_start_child(GtkWidget* paned) { | |
| @@ -184,9 +184,8 @@ static void widget_remove_focus_controller(GtkWidget* widget, GtkEventController | |||||
| 184 | 184 | ||||
| 185 | tracker->is_attached = FALSE; | 185 | tracker->is_attached = FALSE; | ||
| 186 | 186 | ||||
| 187 | - | // Remove from registry and unref controller to avoid leaks | 187 | + | // Remove from registry; GTK drops the final reference when removing controller |
| 188 | g_hash_table_remove(controller_registry, controller); | 188 | g_hash_table_remove(controller_registry, controller); | ||
| 189 | - | g_object_unref(controller); | |||
| 190 | } | 189 | } | ||
| 191 | 190 | ||||
| 192 | static GtkWidget* paned_get_start_child(GtkWidget* paned) { | 191 | static GtkWidget* paned_get_start_child(GtkWidget* paned) { | ||