fix(cleanup): implement comprehensive WebView teardown and memory management
- Add JavaScript teardown functionality with cleanup handlers for all event listeners, intervals, and global state - Implement controller detachment methods to prevent post-destruction usage - Enhance pane cleanup with proper GTK widget detachment sequencing - Add WebView validity checks before JavaScript evaluation - Improve workspace manager pane closing logic with better popup handling - Add console capture cleanup and OAuth polling teardown
- commit
- 75a1db0e145bfc13bd2a5fd9a1299c10bd533c1b
- author
- bamen · · 2025-09-23 19:21 UTC
- committer
- bamen · · 2025-09-23 16:12 UTC
- parent
- 3d7a462
- signature
- unsigned
10 files · +319 -23
M gui/src/injected/main-world.ts +104 -5
| @@ -29,6 +29,7 @@ declare global { | |||
| 29 | 29 | __dumber_omnibox_suggestions?: (suggestions: Suggestion[]) => void; | |
| 30 | 30 | __dumber_webview_id?: string; | |
| 31 | 31 | __dumber_is_active?: boolean; | |
| 32 | + | __dumber_teardown?: () => void; | |
| 32 | 33 | webkit?: { | |
| 33 | 34 | messageHandlers?: { | |
| 34 | 35 | dumber?: { | |
| @@ -101,6 +102,9 @@ function detectWindowType(features?: string | null): string { | |||
| 101 | 102 | } | |
| 102 | 103 | window.__dumber_page_bridge_installed = true; | |
| 103 | 104 | ||
| 105 | + | const cleanupHandlers: Array<() => void> = []; | |
| 106 | + | let teardownExecuted = false; | |
| 107 | + | ||
| 104 | 108 | // Initialize zoom level placeholder (will be replaced by Go) | |
| 105 | 109 | const initialZoom = 1.0; // __DOM_ZOOM_DEFAULT__ will be replaced by Go | |
| 106 | 110 | window.__dumber_dom_zoom_seed = initialZoom; | |
| @@ -267,13 +271,18 @@ function detectWindowType(features?: string | null): string { | |||
| 267 | 271 | } | |
| 268 | 272 | }; | |
| 269 | 273 | ||
| 270 | - | document.addEventListener("dumber:omnibox-ready", () => { | |
| 274 | + | const handleOmniboxReady = () => { | |
| 271 | 275 | omniboxReady = true; | |
| 272 | 276 | if (omniboxQueue && omniboxQueue.length) { | |
| 273 | 277 | const items = omniboxQueue.slice(); | |
| 274 | 278 | omniboxQueue.length = 0; | |
| 275 | 279 | items.forEach((s) => omniboxDispatch([s])); | |
| 276 | 280 | } | |
| 281 | + | }; | |
| 282 | + | ||
| 283 | + | document.addEventListener("dumber:omnibox-ready", handleOmniboxReady); | |
| 284 | + | cleanupHandlers.push(() => { | |
| 285 | + | document.removeEventListener("dumber:omnibox-ready", handleOmniboxReady); | |
| 277 | 286 | }); | |
| 278 | 287 | ||
| 279 | 288 | // Unified omnibox API | |
| @@ -283,6 +292,9 @@ function detectWindowType(features?: string | null): string { | |||
| 283 | 292 | }, | |
| 284 | 293 | }; | |
| 285 | 294 | ||
| 295 | + | let popupMessageInterval: ReturnType<typeof setInterval> | null = null; | |
| 296 | + | let oauthUrlCheckInterval: ReturnType<typeof setInterval> | null = null; | |
| 297 | + | ||
| 286 | 298 | // Popup window.opener bridge using shared localStorage | |
| 287 | 299 | const setupPopupOpenerBridge = () => { | |
| 288 | 300 | try { | |
| @@ -391,7 +403,13 @@ function detectWindowType(features?: string | null): string { | |||
| 391 | 403 | }; | |
| 392 | 404 | ||
| 393 | 405 | // Poll for messages every 100ms | |
| 394 | - | setInterval(pollForParentMessages, 100); | |
| 406 | + | popupMessageInterval = setInterval(pollForParentMessages, 100); | |
| 407 | + | cleanupHandlers.push(() => { | |
| 408 | + | if (popupMessageInterval) { | |
| 409 | + | clearInterval(popupMessageInterval); | |
| 410 | + | popupMessageInterval = null; | |
| 411 | + | } | |
| 412 | + | }); | |
| 395 | 413 | ||
| 396 | 414 | console.log(`[dumber-popup] window.opener bridge established successfully`); | |
| 397 | 415 | } | |
| @@ -405,6 +423,7 @@ function detectWindowType(features?: string | null): string { | |||
| 405 | 423 | ||
| 406 | 424 | // Parent window message polling - only for OAuth scenarios | |
| 407 | 425 | let parentPollingInterval: ReturnType<typeof setInterval> | null = null; | |
| 426 | + | let parentPollingHeartbeat: ReturnType<typeof setInterval> | null = null; | |
| 408 | 427 | ||
| 409 | 428 | const setupParentMessagePolling = () => { | |
| 410 | 429 | // Only start polling if we have popup mappings or OAuth callbacks | |
| @@ -511,15 +530,27 @@ function detectWindowType(features?: string | null): string { | |||
| 511 | 530 | if (!parentPollingInterval && hasRelevantData()) { | |
| 512 | 531 | console.log(`[dumber-parent] Starting OAuth/popup message polling`); | |
| 513 | 532 | parentPollingInterval = setInterval(pollForPopupMessages, 100); | |
| 533 | + | cleanupHandlers.push(() => { | |
| 534 | + | if (parentPollingInterval) { | |
| 535 | + | clearInterval(parentPollingInterval); | |
| 536 | + | parentPollingInterval = null; | |
| 537 | + | } | |
| 538 | + | }); | |
| 514 | 539 | } | |
| 515 | 540 | }; | |
| 516 | 541 | ||
| 517 | 542 | // Check periodically if polling should start | |
| 518 | - | setInterval(() => { | |
| 543 | + | parentPollingHeartbeat = setInterval(() => { | |
| 519 | 544 | if (hasRelevantData()) { | |
| 520 | 545 | startPollingForOAuthCallbacks(); | |
| 521 | 546 | } | |
| 522 | 547 | }, 1000); | |
| 548 | + | cleanupHandlers.push(() => { | |
| 549 | + | if (parentPollingHeartbeat) { | |
| 550 | + | clearInterval(parentPollingHeartbeat); | |
| 551 | + | parentPollingHeartbeat = null; | |
| 552 | + | } | |
| 553 | + | }); | |
| 523 | 554 | }; | |
| 524 | 555 | ||
| 525 | 556 | // Setup conditional parent message polling | |
| @@ -602,16 +633,26 @@ function detectWindowType(features?: string | null): string { | |||
| 602 | 633 | ||
| 603 | 634 | // Monitor for URL changes (for SPAs that don't reload) | |
| 604 | 635 | let lastUrl = window.location.href; | |
| 605 | - | setInterval(() => { | |
| 636 | + | oauthUrlCheckInterval = setInterval(() => { | |
| 606 | 637 | if (window.location.href !== lastUrl) { | |
| 607 | 638 | lastUrl = window.location.href; | |
| 608 | 639 | detectOAuthCallback(); | |
| 609 | 640 | } | |
| 610 | 641 | }, 500); | |
| 642 | + | cleanupHandlers.push(() => { | |
| 643 | + | if (oauthUrlCheckInterval) { | |
| 644 | + | clearInterval(oauthUrlCheckInterval); | |
| 645 | + | oauthUrlCheckInterval = null; | |
| 646 | + | } | |
| 647 | + | }); | |
| 611 | 648 | ||
| 612 | 649 | // Also check on navigation events | |
| 613 | 650 | window.addEventListener('popstate', detectOAuthCallback); | |
| 614 | 651 | window.addEventListener('hashchange', detectOAuthCallback); | |
| 652 | + | cleanupHandlers.push(() => { | |
| 653 | + | window.removeEventListener('popstate', detectOAuthCallback); | |
| 654 | + | window.removeEventListener('hashchange', detectOAuthCallback); | |
| 655 | + | }); | |
| 615 | 656 | }; | |
| 616 | 657 | ||
| 617 | 658 | // Setup OAuth callback detection | |
| @@ -759,9 +800,15 @@ function detectWindowType(features?: string | null): string { | |||
| 759 | 800 | ||
| 760 | 801 | ||
| 761 | 802 | // Track user interactions for popup validation | |
| 762 | - | ["click", "mousedown", "keydown", "touchstart"].forEach((eventType) => { | |
| 803 | + | const interactionEvents = ["click", "mousedown", "keydown", "touchstart"] as const; | |
| 804 | + | interactionEvents.forEach((eventType) => { | |
| 763 | 805 | document.addEventListener(eventType, trackUserInteraction, true); | |
| 764 | 806 | }); | |
| 807 | + | cleanupHandlers.push(() => { | |
| 808 | + | interactionEvents.forEach((eventType) => { | |
| 809 | + | document.removeEventListener(eventType, trackUserInteraction, true); | |
| 810 | + | }); | |
| 811 | + | }); | |
| 765 | 812 | ||
| 766 | 813 | console.log( | |
| 767 | 814 | `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`, | |
| @@ -866,6 +913,50 @@ function detectWindowType(features?: string | null): string { | |||
| 866 | 913 | } | |
| 867 | 914 | } | |
| 868 | 915 | ||
| 916 | + | const teardown = () => { | |
| 917 | + | if (teardownExecuted) { | |
| 918 | + | return; | |
| 919 | + | } | |
| 920 | + | teardownExecuted = true; | |
| 921 | + | ||
| 922 | + | while (cleanupHandlers.length) { | |
| 923 | + | const cleanup = cleanupHandlers.pop(); | |
| 924 | + | if (!cleanup) { | |
| 925 | + | continue; | |
| 926 | + | } | |
| 927 | + | try { | |
| 928 | + | cleanup(); | |
| 929 | + | } catch (cleanupErr) { | |
| 930 | + | console.warn('[dumber] Cleanup handler failed', cleanupErr); | |
| 931 | + | } | |
| 932 | + | } | |
| 933 | + | ||
| 934 | + | omniboxQueue.length = 0; | |
| 935 | + | omniboxReady = false; | |
| 936 | + | ||
| 937 | + | window.__dumber_window_open_intercepted = false; | |
| 938 | + | window.__dumber_page_bridge_installed = false; | |
| 939 | + | window.__dumber_teardown = undefined; | |
| 940 | + | window.__dumber_omnibox_suggestions = undefined; | |
| 941 | + | window.__dumber_showToast = undefined; | |
| 942 | + | window.__dumber_showZoomToast = undefined; | |
| 943 | + | window.__dumber_applyDomZoom = undefined; | |
| 944 | + | ||
| 945 | + | if (window.__dumber) { | |
| 946 | + | delete window.__dumber.omnibox; | |
| 947 | + | delete window.__dumber.toast; | |
| 948 | + | } | |
| 949 | + | }; | |
| 950 | + | ||
| 951 | + | window.__dumber_teardown = teardown; | |
| 952 | + | window.addEventListener('pagehide', teardown, { once: true }); | |
| 953 | + | window.addEventListener('beforeunload', teardown, { once: true }); | |
| 954 | + | cleanupHandlers.push(() => { | |
| 955 | + | window.removeEventListener('pagehide', teardown); | |
| 956 | + | window.removeEventListener('beforeunload', teardown); | |
| 957 | + | window.__dumber_teardown = undefined; | |
| 958 | + | }); | |
| 959 | + | ||
| 869 | 960 | // Console capture functionality | |
| 870 | 961 | const setupConsoleCapture = () => { | |
| 871 | 962 | const originalConsole = { | |
| @@ -921,6 +1012,14 @@ function detectWindowType(features?: string | null): string { | |||
| 921 | 1012 | console.error = createConsoleWrapper(originalConsole.error, "ERROR"); | |
| 922 | 1013 | console.info = createConsoleWrapper(originalConsole.info, "INFO"); | |
| 923 | 1014 | console.debug = createConsoleWrapper(originalConsole.debug, "DEBUG"); | |
| 1015 | + | ||
| 1016 | + | cleanupHandlers.push(() => { | |
| 1017 | + | console.log = originalConsole.log; | |
| 1018 | + | console.warn = originalConsole.warn; | |
| 1019 | + | console.error = originalConsole.error; | |
| 1020 | + | console.info = originalConsole.info; | |
| 1021 | + | console.debug = originalConsole.debug; | |
| 1022 | + | }); | |
| 924 | 1023 | }; | |
| 925 | 1024 | ||
| 926 | 1025 | // Initialize console capture | |
| @@ -29,6 +29,7 @@ declare global { | |||||
| 29 | __dumber_omnibox_suggestions?: (suggestions: Suggestion[]) => void; | 29 | __dumber_omnibox_suggestions?: (suggestions: Suggestion[]) => void; | ||
| 30 | __dumber_webview_id?: string; | 30 | __dumber_webview_id?: string; | ||
| 31 | __dumber_is_active?: boolean; | 31 | __dumber_is_active?: boolean; | ||
| 32 | + | __dumber_teardown?: () => void; | |||
| 32 | webkit?: { | 33 | webkit?: { | ||
| 33 | messageHandlers?: { | 34 | messageHandlers?: { | ||
| 34 | dumber?: { | 35 | dumber?: { | ||
| @@ -101,6 +102,9 @@ function detectWindowType(features?: string | null): string { | |||||
| 101 | } | 102 | } | ||
| 102 | window.__dumber_page_bridge_installed = true; | 103 | window.__dumber_page_bridge_installed = true; | ||
| 103 | 104 | ||||
| 105 | + | const cleanupHandlers: Array<() => void> = []; | |||
| 106 | + | let teardownExecuted = false; | |||
| 107 | + | ||||
| 104 | // Initialize zoom level placeholder (will be replaced by Go) | 108 | // Initialize zoom level placeholder (will be replaced by Go) | ||
| 105 | const initialZoom = 1.0; // __DOM_ZOOM_DEFAULT__ will be replaced by Go | 109 | const initialZoom = 1.0; // __DOM_ZOOM_DEFAULT__ will be replaced by Go | ||
| 106 | window.__dumber_dom_zoom_seed = initialZoom; | 110 | window.__dumber_dom_zoom_seed = initialZoom; | ||
| @@ -267,13 +271,18 @@ function detectWindowType(features?: string | null): string { | |||||
| 267 | } | 271 | } | ||
| 268 | }; | 272 | }; | ||
| 269 | 273 | ||||
| 270 | - | document.addEventListener("dumber:omnibox-ready", () => { | 274 | + | const handleOmniboxReady = () => { |
| 271 | omniboxReady = true; | 275 | omniboxReady = true; | ||
| 272 | if (omniboxQueue && omniboxQueue.length) { | 276 | if (omniboxQueue && omniboxQueue.length) { | ||
| 273 | const items = omniboxQueue.slice(); | 277 | const items = omniboxQueue.slice(); | ||
| 274 | omniboxQueue.length = 0; | 278 | omniboxQueue.length = 0; | ||
| 275 | items.forEach((s) => omniboxDispatch([s])); | 279 | items.forEach((s) => omniboxDispatch([s])); | ||
| 276 | } | 280 | } | ||
| 281 | + | }; | |||
| 282 | + | ||||
| 283 | + | document.addEventListener("dumber:omnibox-ready", handleOmniboxReady); | |||
| 284 | + | cleanupHandlers.push(() => { | |||
| 285 | + | document.removeEventListener("dumber:omnibox-ready", handleOmniboxReady); | |||
| 277 | }); | 286 | }); | ||
| 278 | 287 | ||||
| 279 | // Unified omnibox API | 288 | // Unified omnibox API | ||
| @@ -283,6 +292,9 @@ function detectWindowType(features?: string | null): string { | |||||
| 283 | }, | 292 | }, | ||
| 284 | }; | 293 | }; | ||
| 285 | 294 | ||||
| 295 | + | let popupMessageInterval: ReturnType<typeof setInterval> | null = null; | |||
| 296 | + | let oauthUrlCheckInterval: ReturnType<typeof setInterval> | null = null; | |||
| 297 | + | ||||
| 286 | // Popup window.opener bridge using shared localStorage | 298 | // Popup window.opener bridge using shared localStorage | ||
| 287 | const setupPopupOpenerBridge = () => { | 299 | const setupPopupOpenerBridge = () => { | ||
| 288 | try { | 300 | try { | ||
| @@ -391,7 +403,13 @@ function detectWindowType(features?: string | null): string { | |||||
| 391 | }; | 403 | }; | ||
| 392 | 404 | ||||
| 393 | // Poll for messages every 100ms | 405 | // Poll for messages every 100ms | ||
| 394 | - | setInterval(pollForParentMessages, 100); | 406 | + | popupMessageInterval = setInterval(pollForParentMessages, 100); |
| 407 | + | cleanupHandlers.push(() => { | |||
| 408 | + | if (popupMessageInterval) { | |||
| 409 | + | clearInterval(popupMessageInterval); | |||
| 410 | + | popupMessageInterval = null; | |||
| 411 | + | } | |||
| 412 | + | }); | |||
| 395 | 413 | ||||
| 396 | console.log(`[dumber-popup] window.opener bridge established successfully`); | 414 | console.log(`[dumber-popup] window.opener bridge established successfully`); | ||
| 397 | } | 415 | } | ||
| @@ -405,6 +423,7 @@ function detectWindowType(features?: string | null): string { | |||||
| 405 | 423 | ||||
| 406 | // Parent window message polling - only for OAuth scenarios | 424 | // Parent window message polling - only for OAuth scenarios | ||
| 407 | let parentPollingInterval: ReturnType<typeof setInterval> | null = null; | 425 | let parentPollingInterval: ReturnType<typeof setInterval> | null = null; | ||
| 426 | + | let parentPollingHeartbeat: ReturnType<typeof setInterval> | null = null; | |||
| 408 | 427 | ||||
| 409 | const setupParentMessagePolling = () => { | 428 | const setupParentMessagePolling = () => { | ||
| 410 | // Only start polling if we have popup mappings or OAuth callbacks | 429 | // Only start polling if we have popup mappings or OAuth callbacks | ||
| @@ -511,15 +530,27 @@ function detectWindowType(features?: string | null): string { | |||||
| 511 | if (!parentPollingInterval && hasRelevantData()) { | 530 | if (!parentPollingInterval && hasRelevantData()) { | ||
| 512 | console.log(`[dumber-parent] Starting OAuth/popup message polling`); | 531 | console.log(`[dumber-parent] Starting OAuth/popup message polling`); | ||
| 513 | parentPollingInterval = setInterval(pollForPopupMessages, 100); | 532 | parentPollingInterval = setInterval(pollForPopupMessages, 100); | ||
| 533 | + | cleanupHandlers.push(() => { | |||
| 534 | + | if (parentPollingInterval) { | |||
| 535 | + | clearInterval(parentPollingInterval); | |||
| 536 | + | parentPollingInterval = null; | |||
| 537 | + | } | |||
| 538 | + | }); | |||
| 514 | } | 539 | } | ||
| 515 | }; | 540 | }; | ||
| 516 | 541 | ||||
| 517 | // Check periodically if polling should start | 542 | // Check periodically if polling should start | ||
| 518 | - | setInterval(() => { | 543 | + | parentPollingHeartbeat = setInterval(() => { |
| 519 | if (hasRelevantData()) { | 544 | if (hasRelevantData()) { | ||
| 520 | startPollingForOAuthCallbacks(); | 545 | startPollingForOAuthCallbacks(); | ||
| 521 | } | 546 | } | ||
| 522 | }, 1000); | 547 | }, 1000); | ||
| 548 | + | cleanupHandlers.push(() => { | |||
| 549 | + | if (parentPollingHeartbeat) { | |||
| 550 | + | clearInterval(parentPollingHeartbeat); | |||
| 551 | + | parentPollingHeartbeat = null; | |||
| 552 | + | } | |||
| 553 | + | }); | |||
| 523 | }; | 554 | }; | ||
| 524 | 555 | ||||
| 525 | // Setup conditional parent message polling | 556 | // Setup conditional parent message polling | ||
| @@ -602,16 +633,26 @@ function detectWindowType(features?: string | null): string { | |||||
| 602 | 633 | ||||
| 603 | // Monitor for URL changes (for SPAs that don't reload) | 634 | // Monitor for URL changes (for SPAs that don't reload) | ||
| 604 | let lastUrl = window.location.href; | 635 | let lastUrl = window.location.href; | ||
| 605 | - | setInterval(() => { | 636 | + | oauthUrlCheckInterval = setInterval(() => { |
| 606 | if (window.location.href !== lastUrl) { | 637 | if (window.location.href !== lastUrl) { | ||
| 607 | lastUrl = window.location.href; | 638 | lastUrl = window.location.href; | ||
| 608 | detectOAuthCallback(); | 639 | detectOAuthCallback(); | ||
| 609 | } | 640 | } | ||
| 610 | }, 500); | 641 | }, 500); | ||
| 642 | + | cleanupHandlers.push(() => { | |||
| 643 | + | if (oauthUrlCheckInterval) { | |||
| 644 | + | clearInterval(oauthUrlCheckInterval); | |||
| 645 | + | oauthUrlCheckInterval = null; | |||
| 646 | + | } | |||
| 647 | + | }); | |||
| 611 | 648 | ||||
| 612 | // Also check on navigation events | 649 | // Also check on navigation events | ||
| 613 | window.addEventListener('popstate', detectOAuthCallback); | 650 | window.addEventListener('popstate', detectOAuthCallback); | ||
| 614 | window.addEventListener('hashchange', detectOAuthCallback); | 651 | window.addEventListener('hashchange', detectOAuthCallback); | ||
| 652 | + | cleanupHandlers.push(() => { | |||
| 653 | + | window.removeEventListener('popstate', detectOAuthCallback); | |||
| 654 | + | window.removeEventListener('hashchange', detectOAuthCallback); | |||
| 655 | + | }); | |||
| 615 | }; | 656 | }; | ||
| 616 | 657 | ||||
| 617 | // Setup OAuth callback detection | 658 | // Setup OAuth callback detection | ||
| @@ -759,9 +800,15 @@ function detectWindowType(features?: string | null): string { | |||||
| 759 | 800 | ||||
| 760 | 801 | ||||
| 761 | // Track user interactions for popup validation | 802 | // Track user interactions for popup validation | ||
| 762 | - | ["click", "mousedown", "keydown", "touchstart"].forEach((eventType) => { | 803 | + | const interactionEvents = ["click", "mousedown", "keydown", "touchstart"] as const; |
| 804 | + | interactionEvents.forEach((eventType) => { | |||
| 763 | document.addEventListener(eventType, trackUserInteraction, true); | 805 | document.addEventListener(eventType, trackUserInteraction, true); | ||
| 764 | }); | 806 | }); | ||
| 807 | + | cleanupHandlers.push(() => { | |||
| 808 | + | interactionEvents.forEach((eventType) => { | |||
| 809 | + | document.removeEventListener(eventType, trackUserInteraction, true); | |||
| 810 | + | }); | |||
| 811 | + | }); | |||
| 765 | 812 | ||||
| 766 | console.log( | 813 | console.log( | ||
| 767 | `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`, | 814 | `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`, | ||
| @@ -866,6 +913,50 @@ function detectWindowType(features?: string | null): string { | |||||
| 866 | } | 913 | } | ||
| 867 | } | 914 | } | ||
| 868 | 915 | ||||
| 916 | + | const teardown = () => { | |||
| 917 | + | if (teardownExecuted) { | |||
| 918 | + | return; | |||
| 919 | + | } | |||
| 920 | + | teardownExecuted = true; | |||
| 921 | + | ||||
| 922 | + | while (cleanupHandlers.length) { | |||
| 923 | + | const cleanup = cleanupHandlers.pop(); | |||
| 924 | + | if (!cleanup) { | |||
| 925 | + | continue; | |||
| 926 | + | } | |||
| 927 | + | try { | |||
| 928 | + | cleanup(); | |||
| 929 | + | } catch (cleanupErr) { | |||
| 930 | + | console.warn('[dumber] Cleanup handler failed', cleanupErr); | |||
| 931 | + | } | |||
| 932 | + | } | |||
| 933 | + | ||||
| 934 | + | omniboxQueue.length = 0; | |||
| 935 | + | omniboxReady = false; | |||
| 936 | + | ||||
| 937 | + | window.__dumber_window_open_intercepted = false; | |||
| 938 | + | window.__dumber_page_bridge_installed = false; | |||
| 939 | + | window.__dumber_teardown = undefined; | |||
| 940 | + | window.__dumber_omnibox_suggestions = undefined; | |||
| 941 | + | window.__dumber_showToast = undefined; | |||
| 942 | + | window.__dumber_showZoomToast = undefined; | |||
| 943 | + | window.__dumber_applyDomZoom = undefined; | |||
| 944 | + | ||||
| 945 | + | if (window.__dumber) { | |||
| 946 | + | delete window.__dumber.omnibox; | |||
| 947 | + | delete window.__dumber.toast; | |||
| 948 | + | } | |||
| 949 | + | }; | |||
| 950 | + | ||||
| 951 | + | window.__dumber_teardown = teardown; | |||
| 952 | + | window.addEventListener('pagehide', teardown, { once: true }); | |||
| 953 | + | window.addEventListener('beforeunload', teardown, { once: true }); | |||
| 954 | + | cleanupHandlers.push(() => { | |||
| 955 | + | window.removeEventListener('pagehide', teardown); | |||
| 956 | + | window.removeEventListener('beforeunload', teardown); | |||
| 957 | + | window.__dumber_teardown = undefined; | |||
| 958 | + | }); | |||
| 959 | + | ||||
| 869 | // Console capture functionality | 960 | // Console capture functionality | ||
| 870 | const setupConsoleCapture = () => { | 961 | const setupConsoleCapture = () => { | ||
| 871 | const originalConsole = { | 962 | const originalConsole = { | ||
| @@ -921,6 +1012,14 @@ function detectWindowType(features?: string | null): string { | |||||
| 921 | console.error = createConsoleWrapper(originalConsole.error, "ERROR"); | 1012 | console.error = createConsoleWrapper(originalConsole.error, "ERROR"); | ||
| 922 | console.info = createConsoleWrapper(originalConsole.info, "INFO"); | 1013 | console.info = createConsoleWrapper(originalConsole.info, "INFO"); | ||
| 923 | console.debug = createConsoleWrapper(originalConsole.debug, "DEBUG"); | 1014 | console.debug = createConsoleWrapper(originalConsole.debug, "DEBUG"); | ||
| 1015 | + | ||||
| 1016 | + | cleanupHandlers.push(() => { | |||
| 1017 | + | console.log = originalConsole.log; | |||
| 1018 | + | console.warn = originalConsole.warn; | |||
| 1019 | + | console.error = originalConsole.error; | |||
| 1020 | + | console.info = originalConsole.info; | |||
| 1021 | + | console.debug = originalConsole.debug; | |||
| 1022 | + | }); | |||
| 924 | }; | 1023 | }; | ||
| 925 | 1024 | ||||
| 926 | // Initialize console capture | 1025 | // Initialize console capture | ||
M internal/app/browser/pane.go +33 -8
| @@ -79,19 +79,44 @@ func (p *BrowserPane) SetGUIComponent(component string, loaded bool) { | |||
| 79 | 79 | ||
| 80 | 80 | // Cleanup releases pane resources | |
| 81 | 81 | func (p *BrowserPane) Cleanup() { | |
| 82 | - | // Cleanup shortcuts | |
| 82 | + | if p == nil { | |
| 83 | + | return | |
| 84 | + | } | |
| 85 | + | ||
| 83 | 86 | if p.shortcutHandler != nil { | |
| 84 | - | // TODO: Add cleanup method to ShortcutHandler | |
| 87 | + | p.shortcutHandler.Detach() | |
| 85 | 88 | log.Printf("[pane-%s] Cleaned up shortcuts", p.id) | |
| 86 | 89 | } | |
| 87 | 90 | ||
| 91 | + | if p.navigationController != nil { | |
| 92 | + | p.navigationController.Detach() | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | if p.zoomController != nil { | |
| 96 | + | p.zoomController.DetachWebView() | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | if p.clipboardController != nil { | |
| 100 | + | p.clipboardController.Detach() | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | if p.messageHandler != nil { | |
| 104 | + | p.messageHandler.SetWebView(nil) | |
| 105 | + | } | |
| 106 | + | ||
| 88 | 107 | // Notify JS to cleanup GUI | |
| 89 | - | if p.webView != nil && p.hasGUI { | |
| 90 | - | if err := p.webView.InjectScript(` | |
| 91 | - | if (window.__dumber_gui_manager) { | |
| 92 | - | window.__dumber_gui_manager.destroy(); | |
| 93 | - | } | |
| 94 | - | `); err != nil { | |
| 108 | + | if p.webView != nil && p.hasGUI && !p.webView.IsDestroyed() { | |
| 109 | + | cleanupScript := `try { | |
| 110 | + | if (typeof window.__dumber_teardown === 'function') { | |
| 111 | + | window.__dumber_teardown(); | |
| 112 | + | } | |
| 113 | + | if (window.__dumber_gui_manager) { | |
| 114 | + | window.__dumber_gui_manager.destroy(); | |
| 115 | + | } | |
| 116 | + | } catch (err) { | |
| 117 | + | console.warn('[dumber] GUI teardown failed', err); | |
| 118 | + | }` | |
| 119 | + | if err := p.webView.InjectScript(cleanupScript); err != nil { | |
| 95 | 120 | log.Printf("[pane-%s] failed to inject cleanup script: %v", p.id, err) | |
| 96 | 121 | } | |
| 97 | 122 | } | |
| @@ -79,19 +79,44 @@ func (p *BrowserPane) SetGUIComponent(component string, loaded bool) { | |||||
| 79 | 79 | ||||
| 80 | // Cleanup releases pane resources | 80 | // Cleanup releases pane resources | ||
| 81 | func (p *BrowserPane) Cleanup() { | 81 | func (p *BrowserPane) Cleanup() { | ||
| 82 | - | // Cleanup shortcuts | 82 | + | if p == nil { |
| 83 | + | return | |||
| 84 | + | } | |||
| 85 | + | ||||
| 83 | if p.shortcutHandler != nil { | 86 | if p.shortcutHandler != nil { | ||
| 84 | - | // TODO: Add cleanup method to ShortcutHandler | 87 | + | p.shortcutHandler.Detach() |
| 85 | log.Printf("[pane-%s] Cleaned up shortcuts", p.id) | 88 | log.Printf("[pane-%s] Cleaned up shortcuts", p.id) | ||
| 86 | } | 89 | } | ||
| 87 | 90 | ||||
| 91 | + | if p.navigationController != nil { | |||
| 92 | + | p.navigationController.Detach() | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | if p.zoomController != nil { | |||
| 96 | + | p.zoomController.DetachWebView() | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | if p.clipboardController != nil { | |||
| 100 | + | p.clipboardController.Detach() | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | if p.messageHandler != nil { | |||
| 104 | + | p.messageHandler.SetWebView(nil) | |||
| 105 | + | } | |||
| 106 | + | ||||
| 88 | // Notify JS to cleanup GUI | 107 | // Notify JS to cleanup GUI | ||
| 89 | - | if p.webView != nil && p.hasGUI { | 108 | + | if p.webView != nil && p.hasGUI && !p.webView.IsDestroyed() { |
| 90 | - | if err := p.webView.InjectScript(` | 109 | + | cleanupScript := `try { |
| 91 | - | if (window.__dumber_gui_manager) { | 110 | + | if (typeof window.__dumber_teardown === 'function') { |
| 92 | - | window.__dumber_gui_manager.destroy(); | 111 | + | window.__dumber_teardown(); |
| 93 | - | } | 112 | + | } |
| 94 | - | `); err != nil { | 113 | + | if (window.__dumber_gui_manager) { |
| 114 | + | window.__dumber_gui_manager.destroy(); | |||
| 115 | + | } | |||
| 116 | + | } catch (err) { | |||
| 117 | + | console.warn('[dumber] GUI teardown failed', err); | |||
| 118 | + | }` | |||
| 119 | + | if err := p.webView.InjectScript(cleanupScript); err != nil { | |||
| 95 | log.Printf("[pane-%s] failed to inject cleanup script: %v", p.id, err) | 120 | log.Printf("[pane-%s] failed to inject cleanup script: %v", p.id, err) | ||
| 96 | } | 121 | } | ||
| 97 | } | 122 | } | ||
M internal/app/browser/shortcuts.go +8 -0
| @@ -131,3 +131,11 @@ func (s *ShortcutHandler) exposeWorkspaceConfig() { | |||
| 131 | 131 | log.Printf("Shortcut: failed to expose workspace config: %v", err) | |
| 132 | 132 | } | |
| 133 | 133 | } | |
| 134 | + | ||
| 135 | + | // Detach releases the WebView reference to prevent post-destruction usage. | |
| 136 | + | func (s *ShortcutHandler) Detach() { | |
| 137 | + | if s == nil { | |
| 138 | + | return | |
| 139 | + | } | |
| 140 | + | s.webView = nil | |
| 141 | + | } | |
| @@ -131,3 +131,11 @@ func (s *ShortcutHandler) exposeWorkspaceConfig() { | |||||
| 131 | log.Printf("Shortcut: failed to expose workspace config: %v", err) | 131 | log.Printf("Shortcut: failed to expose workspace config: %v", err) | ||
| 132 | } | 132 | } | ||
| 133 | } | 133 | } | ||
| 134 | + | ||||
| 135 | + | // Detach releases the WebView reference to prevent post-destruction usage. | |||
| 136 | + | func (s *ShortcutHandler) Detach() { | |||
| 137 | + | if s == nil { | |||
| 138 | + | return | |||
| 139 | + | } | |||
| 140 | + | s.webView = nil | |||
| 141 | + | } | |||
M internal/app/browser/workspace_manager.go +113 -5
| @@ -1086,10 +1086,106 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||
| 1086 | 1086 | return errors.New("close target missing webview") | |
| 1087 | 1087 | } | |
| 1088 | 1088 | ||
| 1089 | + | if node.parent != nil && node.parent.container != 0 { | |
| 1090 | + | if node.parent.left == node { | |
| 1091 | + | webkit.PanedSetStartChild(node.parent.container, 0) | |
| 1092 | + | } else if node.parent.right == node { | |
| 1093 | + | webkit.PanedSetEndChild(node.parent.container, 0) | |
| 1094 | + | } | |
| 1095 | + | } | |
| 1096 | + | ||
| 1097 | + | if node.container != 0 { | |
| 1098 | + | webkit.WidgetUnparent(node.container) | |
| 1099 | + | } | |
| 1100 | + | ||
| 1089 | 1101 | remaining := len(wm.app.panes) | |
| 1090 | 1102 | willBeLastPane := remaining <= 1 | |
| 1103 | + | paneCleaned := false | |
| 1104 | + | ensureCleanup := func() { | |
| 1105 | + | if paneCleaned { | |
| 1106 | + | return | |
| 1107 | + | } | |
| 1108 | + | node.pane.Cleanup() | |
| 1109 | + | paneCleaned = true | |
| 1110 | + | } | |
| 1111 | + | ||
| 1112 | + | if remaining == 2 { | |
| 1113 | + | var sibling *paneNode | |
| 1114 | + | if node.parent != nil { | |
| 1115 | + | if node.parent.left == node { | |
| 1116 | + | sibling = node.parent.right | |
| 1117 | + | } else if node.parent.right == node { | |
| 1118 | + | sibling = node.parent.left | |
| 1119 | + | } | |
| 1120 | + | } | |
| 1121 | + | if sibling != nil && sibling.isLeaf && sibling.pane != nil && sibling.pane.webView != nil { | |
| 1122 | + | if sibling.isPopup && !node.isPopup { | |
| 1123 | + | parentNode := node.parent | |
| 1124 | + | parentContainer := uintptr(0) | |
| 1125 | + | if parentNode != nil { | |
| 1126 | + | parentContainer = parentNode.container | |
| 1127 | + | } | |
| 1128 | + | ||
| 1129 | + | // Detach remaining GTK widgets before destroying the WebViews so GTK | |
| 1130 | + | // does not attempt to dispose still-parented children. | |
| 1131 | + | if parentContainer != 0 { | |
| 1132 | + | if parentNode.left == sibling { | |
| 1133 | + | webkit.PanedSetStartChild(parentContainer, 0) | |
| 1134 | + | } else if parentNode.right == sibling { | |
| 1135 | + | webkit.PanedSetEndChild(parentContainer, 0) | |
| 1136 | + | } | |
| 1137 | + | } | |
| 1138 | + | if sibling.container != 0 { | |
| 1139 | + | webkit.WidgetUnparent(sibling.container) | |
| 1140 | + | } | |
| 1141 | + | if parentContainer != 0 && wm.window != nil { | |
| 1142 | + | wm.window.SetChild(0) | |
| 1143 | + | } | |
| 1144 | + | ||
| 1145 | + | log.Printf("[workspace] closing primary pane with related popup present; exiting") | |
| 1146 | + | ensureCleanup() | |
| 1147 | + | sibling.pane.Cleanup() | |
| 1148 | + | ||
| 1149 | + | wm.detachHover(node) | |
| 1150 | + | wm.detachHover(sibling) | |
| 1151 | + | ||
| 1152 | + | delete(wm.viewToNode, node.pane.webView) | |
| 1153 | + | delete(wm.viewToNode, sibling.pane.webView) | |
| 1154 | + | delete(wm.lastSplitMsg, node.pane.webView) | |
| 1155 | + | delete(wm.lastSplitMsg, sibling.pane.webView) | |
| 1156 | + | delete(wm.lastExitMsg, node.pane.webView) | |
| 1157 | + | delete(wm.lastExitMsg, sibling.pane.webView) | |
| 1158 | + | ||
| 1159 | + | for i := 0; i < len(wm.app.panes); i++ { | |
| 1160 | + | if wm.app.panes[i] == node.pane || wm.app.panes[i] == sibling.pane { | |
| 1161 | + | wm.app.panes = append(wm.app.panes[:i], wm.app.panes[i+1:]...) | |
| 1162 | + | i-- | |
| 1163 | + | } | |
| 1164 | + | } | |
| 1165 | + | ||
| 1166 | + | if node.pane.webView != nil { | |
| 1167 | + | if err := node.pane.webView.Destroy(); err != nil { | |
| 1168 | + | log.Printf("[workspace] failed to destroy primary webview: %v", err) | |
| 1169 | + | } | |
| 1170 | + | } | |
| 1171 | + | if sibling.pane.webView != nil { | |
| 1172 | + | if err := sibling.pane.webView.Destroy(); err != nil { | |
| 1173 | + | log.Printf("[workspace] failed to destroy popup webview: %v", err) | |
| 1174 | + | } | |
| 1175 | + | } | |
| 1176 | + | ||
| 1177 | + | wm.root = nil | |
| 1178 | + | wm.active = nil | |
| 1179 | + | wm.mainPane = nil | |
| 1180 | + | ||
| 1181 | + | webkit.QuitMainLoop() | |
| 1182 | + | return nil | |
| 1183 | + | } | |
| 1184 | + | } | |
| 1185 | + | } | |
| 1091 | 1186 | if willBeLastPane && wm.root == node { | |
| 1092 | 1187 | log.Printf("[workspace] closing final pane; exiting browser") | |
| 1188 | + | ensureCleanup() | |
| 1093 | 1189 | wm.detachHover(node) | |
| 1094 | 1190 | if err := node.pane.webView.Destroy(); err != nil { | |
| 1095 | 1191 | log.Printf("[workspace] failed to destroy webview: %v", err) | |
| @@ -1110,6 +1206,7 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||
| 1110 | 1206 | if replacement == nil { | |
| 1111 | 1207 | // No other panes exist, this is the final pane | |
| 1112 | 1208 | log.Printf("[workspace] closing final pane; exiting browser") | |
| 1209 | + | ensureCleanup() | |
| 1113 | 1210 | wm.detachHover(node) | |
| 1114 | 1211 | if err := node.pane.webView.Destroy(); err != nil { | |
| 1115 | 1212 | log.Printf("[workspace] failed to destroy webview: %v", err) | |
| @@ -1163,13 +1260,19 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||
| 1163 | 1260 | ||
| 1164 | 1261 | // Destroy the webview and detach hover AFTER rearranging hierarchy | |
| 1165 | 1262 | // Only destroy the webview if this is the final pane, otherwise just clean up | |
| 1263 | + | ensureCleanup() | |
| 1166 | 1264 | wm.detachHover(node) | |
| 1167 | - | if willBeLastPane { | |
| 1265 | + | switch { | |
| 1266 | + | case node.isPopup: | |
| 1267 | + | if err := node.pane.webView.Destroy(); err != nil { | |
| 1268 | + | log.Printf("[workspace] failed to destroy popup webview: %v", err) | |
| 1269 | + | } | |
| 1270 | + | case willBeLastPane: | |
| 1168 | 1271 | // This is the last pane, safe to destroy completely | |
| 1169 | 1272 | if err := node.pane.webView.Destroy(); err != nil { | |
| 1170 | 1273 | log.Printf("[workspace] failed to destroy webview: %v", err) | |
| 1171 | 1274 | } | |
| 1172 | - | } else { | |
| 1275 | + | default: | |
| 1173 | 1276 | // Multiple panes remain, don't destroy the window - just clean up the webview | |
| 1174 | 1277 | log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1) | |
| 1175 | 1278 | // TODO: Add a method to destroy just the webview without the window | |
| @@ -1286,14 +1389,19 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||
| 1286 | 1389 | } | |
| 1287 | 1390 | ||
| 1288 | 1391 | // Destroy the webview and detach hover AFTER all hierarchy changes are complete | |
| 1289 | - | // Only destroy the webview if this is the final pane, otherwise just clean up | |
| 1392 | + | ensureCleanup() | |
| 1290 | 1393 | wm.detachHover(node) | |
| 1291 | - | if willBeLastPane { | |
| 1394 | + | switch { | |
| 1395 | + | case node.isPopup: | |
| 1396 | + | if err := node.pane.webView.Destroy(); err != nil { | |
| 1397 | + | log.Printf("[workspace] failed to destroy popup webview: %v", err) | |
| 1398 | + | } | |
| 1399 | + | case willBeLastPane: | |
| 1292 | 1400 | // This is the last pane, safe to destroy completely | |
| 1293 | 1401 | if err := node.pane.webView.Destroy(); err != nil { | |
| 1294 | 1402 | log.Printf("[workspace] failed to destroy webview: %v", err) | |
| 1295 | 1403 | } | |
| 1296 | - | } else { | |
| 1404 | + | default: | |
| 1297 | 1405 | // Multiple panes remain, don't destroy the window - just clean up the webview | |
| 1298 | 1406 | log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1) | |
| 1299 | 1407 | // TODO: Add a method to destroy just the webview without the window | |
| @@ -1086,10 +1086,106 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||||
| 1086 | return errors.New("close target missing webview") | 1086 | return errors.New("close target missing webview") | ||
| 1087 | } | 1087 | } | ||
| 1088 | 1088 | ||||
| 1089 | + | if node.parent != nil && node.parent.container != 0 { | |||
| 1090 | + | if node.parent.left == node { | |||
| 1091 | + | webkit.PanedSetStartChild(node.parent.container, 0) | |||
| 1092 | + | } else if node.parent.right == node { | |||
| 1093 | + | webkit.PanedSetEndChild(node.parent.container, 0) | |||
| 1094 | + | } | |||
| 1095 | + | } | |||
| 1096 | + | ||||
| 1097 | + | if node.container != 0 { | |||
| 1098 | + | webkit.WidgetUnparent(node.container) | |||
| 1099 | + | } | |||
| 1100 | + | ||||
| 1089 | remaining := len(wm.app.panes) | 1101 | remaining := len(wm.app.panes) | ||
| 1090 | willBeLastPane := remaining <= 1 | 1102 | willBeLastPane := remaining <= 1 | ||
| 1103 | + | paneCleaned := false | |||
| 1104 | + | ensureCleanup := func() { | |||
| 1105 | + | if paneCleaned { | |||
| 1106 | + | return | |||
| 1107 | + | } | |||
| 1108 | + | node.pane.Cleanup() | |||
| 1109 | + | paneCleaned = true | |||
| 1110 | + | } | |||
| 1111 | + | ||||
| 1112 | + | if remaining == 2 { | |||
| 1113 | + | var sibling *paneNode | |||
| 1114 | + | if node.parent != nil { | |||
| 1115 | + | if node.parent.left == node { | |||
| 1116 | + | sibling = node.parent.right | |||
| 1117 | + | } else if node.parent.right == node { | |||
| 1118 | + | sibling = node.parent.left | |||
| 1119 | + | } | |||
| 1120 | + | } | |||
| 1121 | + | if sibling != nil && sibling.isLeaf && sibling.pane != nil && sibling.pane.webView != nil { | |||
| 1122 | + | if sibling.isPopup && !node.isPopup { | |||
| 1123 | + | parentNode := node.parent | |||
| 1124 | + | parentContainer := uintptr(0) | |||
| 1125 | + | if parentNode != nil { | |||
| 1126 | + | parentContainer = parentNode.container | |||
| 1127 | + | } | |||
| 1128 | + | ||||
| 1129 | + | // Detach remaining GTK widgets before destroying the WebViews so GTK | |||
| 1130 | + | // does not attempt to dispose still-parented children. | |||
| 1131 | + | if parentContainer != 0 { | |||
| 1132 | + | if parentNode.left == sibling { | |||
| 1133 | + | webkit.PanedSetStartChild(parentContainer, 0) | |||
| 1134 | + | } else if parentNode.right == sibling { | |||
| 1135 | + | webkit.PanedSetEndChild(parentContainer, 0) | |||
| 1136 | + | } | |||
| 1137 | + | } | |||
| 1138 | + | if sibling.container != 0 { | |||
| 1139 | + | webkit.WidgetUnparent(sibling.container) | |||
| 1140 | + | } | |||
| 1141 | + | if parentContainer != 0 && wm.window != nil { | |||
| 1142 | + | wm.window.SetChild(0) | |||
| 1143 | + | } | |||
| 1144 | + | ||||
| 1145 | + | log.Printf("[workspace] closing primary pane with related popup present; exiting") | |||
| 1146 | + | ensureCleanup() | |||
| 1147 | + | sibling.pane.Cleanup() | |||
| 1148 | + | ||||
| 1149 | + | wm.detachHover(node) | |||
| 1150 | + | wm.detachHover(sibling) | |||
| 1151 | + | ||||
| 1152 | + | delete(wm.viewToNode, node.pane.webView) | |||
| 1153 | + | delete(wm.viewToNode, sibling.pane.webView) | |||
| 1154 | + | delete(wm.lastSplitMsg, node.pane.webView) | |||
| 1155 | + | delete(wm.lastSplitMsg, sibling.pane.webView) | |||
| 1156 | + | delete(wm.lastExitMsg, node.pane.webView) | |||
| 1157 | + | delete(wm.lastExitMsg, sibling.pane.webView) | |||
| 1158 | + | ||||
| 1159 | + | for i := 0; i < len(wm.app.panes); i++ { | |||
| 1160 | + | if wm.app.panes[i] == node.pane || wm.app.panes[i] == sibling.pane { | |||
| 1161 | + | wm.app.panes = append(wm.app.panes[:i], wm.app.panes[i+1:]...) | |||
| 1162 | + | i-- | |||
| 1163 | + | } | |||
| 1164 | + | } | |||
| 1165 | + | ||||
| 1166 | + | if node.pane.webView != nil { | |||
| 1167 | + | if err := node.pane.webView.Destroy(); err != nil { | |||
| 1168 | + | log.Printf("[workspace] failed to destroy primary webview: %v", err) | |||
| 1169 | + | } | |||
| 1170 | + | } | |||
| 1171 | + | if sibling.pane.webView != nil { | |||
| 1172 | + | if err := sibling.pane.webView.Destroy(); err != nil { | |||
| 1173 | + | log.Printf("[workspace] failed to destroy popup webview: %v", err) | |||
| 1174 | + | } | |||
| 1175 | + | } | |||
| 1176 | + | ||||
| 1177 | + | wm.root = nil | |||
| 1178 | + | wm.active = nil | |||
| 1179 | + | wm.mainPane = nil | |||
| 1180 | + | ||||
| 1181 | + | webkit.QuitMainLoop() | |||
| 1182 | + | return nil | |||
| 1183 | + | } | |||
| 1184 | + | } | |||
| 1185 | + | } | |||
| 1091 | if willBeLastPane && wm.root == node { | 1186 | if willBeLastPane && wm.root == node { | ||
| 1092 | log.Printf("[workspace] closing final pane; exiting browser") | 1187 | log.Printf("[workspace] closing final pane; exiting browser") | ||
| 1188 | + | ensureCleanup() | |||
| 1093 | wm.detachHover(node) | 1189 | wm.detachHover(node) | ||
| 1094 | if err := node.pane.webView.Destroy(); err != nil { | 1190 | if err := node.pane.webView.Destroy(); err != nil { | ||
| 1095 | log.Printf("[workspace] failed to destroy webview: %v", err) | 1191 | log.Printf("[workspace] failed to destroy webview: %v", err) | ||
| @@ -1110,6 +1206,7 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||||
| 1110 | if replacement == nil { | 1206 | if replacement == nil { | ||
| 1111 | // No other panes exist, this is the final pane | 1207 | // No other panes exist, this is the final pane | ||
| 1112 | log.Printf("[workspace] closing final pane; exiting browser") | 1208 | log.Printf("[workspace] closing final pane; exiting browser") | ||
| 1209 | + | ensureCleanup() | |||
| 1113 | wm.detachHover(node) | 1210 | wm.detachHover(node) | ||
| 1114 | if err := node.pane.webView.Destroy(); err != nil { | 1211 | if err := node.pane.webView.Destroy(); err != nil { | ||
| 1115 | log.Printf("[workspace] failed to destroy webview: %v", err) | 1212 | log.Printf("[workspace] failed to destroy webview: %v", err) | ||
| @@ -1163,13 +1260,19 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||||
| 1163 | 1260 | ||||
| 1164 | // Destroy the webview and detach hover AFTER rearranging hierarchy | 1261 | // Destroy the webview and detach hover AFTER rearranging hierarchy | ||
| 1165 | // Only destroy the webview if this is the final pane, otherwise just clean up | 1262 | // Only destroy the webview if this is the final pane, otherwise just clean up | ||
| 1263 | + | ensureCleanup() | |||
| 1166 | wm.detachHover(node) | 1264 | wm.detachHover(node) | ||
| 1167 | - | if willBeLastPane { | 1265 | + | switch { |
| 1266 | + | case node.isPopup: | |||
| 1267 | + | if err := node.pane.webView.Destroy(); err != nil { | |||
| 1268 | + | log.Printf("[workspace] failed to destroy popup webview: %v", err) | |||
| 1269 | + | } | |||
| 1270 | + | case willBeLastPane: | |||
| 1168 | // This is the last pane, safe to destroy completely | 1271 | // This is the last pane, safe to destroy completely | ||
| 1169 | if err := node.pane.webView.Destroy(); err != nil { | 1272 | if err := node.pane.webView.Destroy(); err != nil { | ||
| 1170 | log.Printf("[workspace] failed to destroy webview: %v", err) | 1273 | log.Printf("[workspace] failed to destroy webview: %v", err) | ||
| 1171 | } | 1274 | } | ||
| 1172 | - | } else { | 1275 | + | default: |
| 1173 | // Multiple panes remain, don't destroy the window - just clean up the webview | 1276 | // Multiple panes remain, don't destroy the window - just clean up the webview | ||
| 1174 | log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1) | 1277 | log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1) | ||
| 1175 | // TODO: Add a method to destroy just the webview without the window | 1278 | // TODO: Add a method to destroy just the webview without the window | ||
| @@ -1286,14 +1389,19 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error { | |||||
| 1286 | } | 1389 | } | ||
| 1287 | 1390 | ||||
| 1288 | // Destroy the webview and detach hover AFTER all hierarchy changes are complete | 1391 | // Destroy the webview and detach hover AFTER all hierarchy changes are complete | ||
| 1289 | - | // Only destroy the webview if this is the final pane, otherwise just clean up | 1392 | + | ensureCleanup() |
| 1290 | wm.detachHover(node) | 1393 | wm.detachHover(node) | ||
| 1291 | - | if willBeLastPane { | 1394 | + | switch { |
| 1395 | + | case node.isPopup: | |||
| 1396 | + | if err := node.pane.webView.Destroy(); err != nil { | |||
| 1397 | + | log.Printf("[workspace] failed to destroy popup webview: %v", err) | |||
| 1398 | + | } | |||
| 1399 | + | case willBeLastPane: | |||
| 1292 | // This is the last pane, safe to destroy completely | 1400 | // This is the last pane, safe to destroy completely | ||
| 1293 | if err := node.pane.webView.Destroy(); err != nil { | 1401 | if err := node.pane.webView.Destroy(); err != nil { | ||
| 1294 | log.Printf("[workspace] failed to destroy webview: %v", err) | 1402 | log.Printf("[workspace] failed to destroy webview: %v", err) | ||
| 1295 | } | 1403 | } | ||
| 1296 | - | } else { | 1404 | + | default: |
| 1297 | // Multiple panes remain, don't destroy the window - just clean up the webview | 1405 | // Multiple panes remain, don't destroy the window - just clean up the webview | ||
| 1298 | log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1) | 1406 | log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1) | ||
| 1299 | // TODO: Add a method to destroy just the webview without the window | 1407 | // TODO: Add a method to destroy just the webview without the window | ||
M internal/app/control/clipboard.go +8 -0
| @@ -55,3 +55,11 @@ func (c *ClipboardController) showToast(message, toastType string) { | |||
| 55 | 55 | log.Printf("Failed to dispatch toast event: %v", err) | |
| 56 | 56 | } | |
| 57 | 57 | } | |
| 58 | + | ||
| 59 | + | // Detach releases the WebView reference when the pane is closed. | |
| 60 | + | func (c *ClipboardController) Detach() { | |
| 61 | + | if c == nil { | |
| 62 | + | return | |
| 63 | + | } | |
| 64 | + | c.webView = nil | |
| 65 | + | } | |
| @@ -55,3 +55,11 @@ func (c *ClipboardController) showToast(message, toastType string) { | |||||
| 55 | log.Printf("Failed to dispatch toast event: %v", err) | 55 | log.Printf("Failed to dispatch toast event: %v", err) | ||
| 56 | } | 56 | } | ||
| 57 | } | 57 | } | ||
| 58 | + | ||||
| 59 | + | // Detach releases the WebView reference when the pane is closed. | |||
| 60 | + | func (c *ClipboardController) Detach() { | |||
| 61 | + | if c == nil { | |||
| 62 | + | return | |||
| 63 | + | } | |||
| 64 | + | c.webView = nil | |||
| 65 | + | } | |||
M internal/app/control/navigation.go +8 -0
| @@ -134,3 +134,11 @@ func (n *NavigationController) HandleBrowseCommand() { | |||
| 134 | 134 | } | |
| 135 | 135 | } | |
| 136 | 136 | } | |
| 137 | + | ||
| 138 | + | // Detach releases the associated WebView to avoid calls after destruction. | |
| 139 | + | func (n *NavigationController) Detach() { | |
| 140 | + | if n == nil { | |
| 141 | + | return | |
| 142 | + | } | |
| 143 | + | n.webView = nil | |
| 144 | + | } | |
| @@ -134,3 +134,11 @@ func (n *NavigationController) HandleBrowseCommand() { | |||||
| 134 | } | 134 | } | ||
| 135 | } | 135 | } | ||
| 136 | } | 136 | } | ||
| 137 | + | ||||
| 138 | + | // Detach releases the associated WebView to avoid calls after destruction. | |||
| 139 | + | func (n *NavigationController) Detach() { | |||
| 140 | + | if n == nil { | |||
| 141 | + | return | |||
| 142 | + | } | |||
| 143 | + | n.webView = nil | |||
| 144 | + | } | |||
M internal/app/control/zoom.go +12 -0
| @@ -172,3 +172,15 @@ func (z *ZoomController) applyZoomLevel(url, domain string, zoomLevel float64, a | |||
| 172 | 172 | z.lastZoomDomain = domain | |
| 173 | 173 | }) | |
| 174 | 174 | } | |
| 175 | + | ||
| 176 | + | // DetachWebView releases the WebView reference and stops timers. | |
| 177 | + | func (z *ZoomController) DetachWebView() { | |
| 178 | + | if z == nil { | |
| 179 | + | return | |
| 180 | + | } | |
| 181 | + | if z.programmaticTimer != nil { | |
| 182 | + | z.programmaticTimer.Stop() | |
| 183 | + | z.programmaticTimer = nil | |
| 184 | + | } | |
| 185 | + | z.webView = nil | |
| 186 | + | } | |
| @@ -172,3 +172,15 @@ func (z *ZoomController) applyZoomLevel(url, domain string, zoomLevel float64, a | |||||
| 172 | z.lastZoomDomain = domain | 172 | z.lastZoomDomain = domain | ||
| 173 | }) | 173 | }) | ||
| 174 | } | 174 | } | ||
| 175 | + | ||||
| 176 | + | // DetachWebView releases the WebView reference and stops timers. | |||
| 177 | + | func (z *ZoomController) DetachWebView() { | |||
| 178 | + | if z == nil { | |||
| 179 | + | return | |||
| 180 | + | } | |||
| 181 | + | if z.programmaticTimer != nil { | |||
| 182 | + | z.programmaticTimer.Stop() | |||
| 183 | + | z.programmaticTimer = nil | |||
| 184 | + | } | |||
| 185 | + | z.webView = nil | |||
| 186 | + | } | |||
M internal/app/messaging/handler.go +6 -2
| @@ -106,6 +106,10 @@ func (h *Handler) Handle(payload string) { | |||
| 106 | 106 | h.handleNavigation(msg) | |
| 107 | 107 | case "query": | |
| 108 | 108 | log.Printf("[DEBUG] Handling query message: q=%s, limit=%d", msg.Q, msg.Limit) | |
| 109 | + | if h.webView == nil { | |
| 110 | + | log.Printf("[WARN] Dropping query response: webview unavailable for suggestions") | |
| 111 | + | return | |
| 112 | + | } | |
| 109 | 113 | h.handleQuery(msg) | |
| 110 | 114 | case "wails": | |
| 111 | 115 | h.handleWailsBridge(msg) | |
| @@ -217,11 +221,11 @@ func (h *Handler) handleClosePopup(msg Message) { | |||
| 217 | 221 | } | |
| 218 | 222 | ||
| 219 | 223 | func (h *Handler) handleQuery(msg Message) { | |
| 220 | - | log.Printf("[DEBUG] handleQuery called: q='%s', limit=%d", msg.Q, msg.Limit) | |
| 221 | 224 | if h.webView == nil { | |
| 222 | - | log.Printf("[ERROR] handleQuery: webView is nil") | |
| 225 | + | log.Printf("[WARN] Skipping query handling: webview is nil") | |
| 223 | 226 | return | |
| 224 | 227 | } | |
| 228 | + | log.Printf("[DEBUG] handleQuery called: q='%s', limit=%d", msg.Q, msg.Limit) | |
| 225 | 229 | if h.browserService == nil { | |
| 226 | 230 | log.Printf("[ERROR] handleQuery: browserService is nil") | |
| 227 | 231 | return | |
| @@ -106,6 +106,10 @@ func (h *Handler) Handle(payload string) { | |||||
| 106 | h.handleNavigation(msg) | 106 | h.handleNavigation(msg) | ||
| 107 | case "query": | 107 | case "query": | ||
| 108 | log.Printf("[DEBUG] Handling query message: q=%s, limit=%d", msg.Q, msg.Limit) | 108 | log.Printf("[DEBUG] Handling query message: q=%s, limit=%d", msg.Q, msg.Limit) | ||
| 109 | + | if h.webView == nil { | |||
| 110 | + | log.Printf("[WARN] Dropping query response: webview unavailable for suggestions") | |||
| 111 | + | return | |||
| 112 | + | } | |||
| 109 | h.handleQuery(msg) | 113 | h.handleQuery(msg) | ||
| 110 | case "wails": | 114 | case "wails": | ||
| 111 | h.handleWailsBridge(msg) | 115 | h.handleWailsBridge(msg) | ||
| @@ -217,11 +221,11 @@ func (h *Handler) handleClosePopup(msg Message) { | |||||
| 217 | } | 221 | } | ||
| 218 | 222 | ||||
| 219 | func (h *Handler) handleQuery(msg Message) { | 223 | func (h *Handler) handleQuery(msg Message) { | ||
| 220 | - | log.Printf("[DEBUG] handleQuery called: q='%s', limit=%d", msg.Q, msg.Limit) | |||
| 221 | if h.webView == nil { | 224 | if h.webView == nil { | ||
| 222 | - | log.Printf("[ERROR] handleQuery: webView is nil") | 225 | + | log.Printf("[WARN] Skipping query handling: webview is nil") |
| 223 | return | 226 | return | ||
| 224 | } | 227 | } | ||
| 228 | + | log.Printf("[DEBUG] handleQuery called: q='%s', limit=%d", msg.Q, msg.Limit) | |||
| 225 | if h.browserService == nil { | 229 | if h.browserService == nil { | ||
| 226 | log.Printf("[ERROR] handleQuery: browserService is nil") | 230 | log.Printf("[ERROR] handleQuery: browserService is nil") | ||
| 227 | return | 231 | return | ||
M pkg/webkit/script_cgo.go +21 -2
| @@ -7,13 +7,32 @@ package webkit | |||
| 7 | 7 | #cgo CFLAGS: -I/usr/include/webkitgtk-6.0 | |
| 8 | 8 | #include <stdlib.h> | |
| 9 | 9 | #include <webkit/webkit.h> | |
| 10 | + | ||
| 11 | + | static gboolean dumber_is_valid_web_view(WebKitWebView *wv) { | |
| 12 | + | return WEBKIT_IS_WEB_VIEW(wv); | |
| 13 | + | } | |
| 10 | 14 | */ | |
| 11 | 15 | import "C" | |
| 12 | 16 | ||
| 13 | 17 | import "unsafe" | |
| 14 | 18 | ||
| 15 | - | func (w *WebView) InjectScript(js string) error { | |
| 19 | + | func (w *WebView) canEvaluateJavaScript() bool { | |
| 16 | 20 | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | |
| 21 | + | return false | |
| 22 | + | } | |
| 23 | + | if C.dumber_is_valid_web_view(w.native.wv) == C.gboolean(0) { | |
| 24 | + | if w.native != nil { | |
| 25 | + | w.native.wv = nil | |
| 26 | + | w.native.view = nil | |
| 27 | + | } | |
| 28 | + | w.destroyed = true | |
| 29 | + | return false | |
| 30 | + | } | |
| 31 | + | return true | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func (w *WebView) InjectScript(js string) error { | |
| 35 | + | if !w.canEvaluateJavaScript() { | |
| 17 | 36 | return ErrNotImplemented | |
| 18 | 37 | } | |
| 19 | 38 | cjs := C.CString(js) | |
| @@ -35,7 +54,7 @@ func (w *WebView) InjectScript(js string) error { | |||
| 35 | 54 | ||
| 36 | 55 | // InjectScriptIntoWorld evaluates JavaScript in the specified script world. | |
| 37 | 56 | func (w *WebView) InjectScriptIntoWorld(js string, worldName string) error { | |
| 38 | - | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | |
| 57 | + | if !w.canEvaluateJavaScript() { | |
| 39 | 58 | return ErrNotImplemented | |
| 40 | 59 | } | |
| 41 | 60 | cjs := C.CString(js) | |
| @@ -7,13 +7,32 @@ package webkit | |||||
| 7 | #cgo CFLAGS: -I/usr/include/webkitgtk-6.0 | 7 | #cgo CFLAGS: -I/usr/include/webkitgtk-6.0 | ||
| 8 | #include <stdlib.h> | 8 | #include <stdlib.h> | ||
| 9 | #include <webkit/webkit.h> | 9 | #include <webkit/webkit.h> | ||
| 10 | + | ||||
| 11 | + | static gboolean dumber_is_valid_web_view(WebKitWebView *wv) { | |||
| 12 | + | return WEBKIT_IS_WEB_VIEW(wv); | |||
| 13 | + | } | |||
| 10 | */ | 14 | */ | ||
| 11 | import "C" | 15 | import "C" | ||
| 12 | 16 | ||||
| 13 | import "unsafe" | 17 | import "unsafe" | ||
| 14 | 18 | ||||
| 15 | - | func (w *WebView) InjectScript(js string) error { | 19 | + | func (w *WebView) canEvaluateJavaScript() bool { |
| 16 | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | 20 | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | ||
| 21 | + | return false | |||
| 22 | + | } | |||
| 23 | + | if C.dumber_is_valid_web_view(w.native.wv) == C.gboolean(0) { | |||
| 24 | + | if w.native != nil { | |||
| 25 | + | w.native.wv = nil | |||
| 26 | + | w.native.view = nil | |||
| 27 | + | } | |||
| 28 | + | w.destroyed = true | |||
| 29 | + | return false | |||
| 30 | + | } | |||
| 31 | + | return true | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func (w *WebView) InjectScript(js string) error { | |||
| 35 | + | if !w.canEvaluateJavaScript() { | |||
| 17 | return ErrNotImplemented | 36 | return ErrNotImplemented | ||
| 18 | } | 37 | } | ||
| 19 | cjs := C.CString(js) | 38 | cjs := C.CString(js) | ||
| @@ -35,7 +54,7 @@ func (w *WebView) InjectScript(js string) error { | |||||
| 35 | 54 | ||||
| 36 | // InjectScriptIntoWorld evaluates JavaScript in the specified script world. | 55 | // InjectScriptIntoWorld evaluates JavaScript in the specified script world. | ||
| 37 | func (w *WebView) InjectScriptIntoWorld(js string, worldName string) error { | 56 | func (w *WebView) InjectScriptIntoWorld(js string, worldName string) error { | ||
| 38 | - | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | 57 | + | if !w.canEvaluateJavaScript() { |
| 39 | return ErrNotImplemented | 58 | return ErrNotImplemented | ||
| 40 | } | 59 | } | ||
| 41 | cjs := C.CString(js) | 60 | cjs := C.CString(js) | ||
M pkg/webkit/webview_cgo.go +6 -1
| @@ -1627,7 +1627,12 @@ func (w *WebView) releaseNativeWidgets() { | |||
| 1627 | 1627 | return | |
| 1628 | 1628 | } | |
| 1629 | 1629 | if w.native.container != nil { | |
| 1630 | - | C.g_object_unref(C.gpointer(w.native.container)) | |
| 1630 | + | containerPtr := uintptr(unsafe.Pointer(w.native.container)) | |
| 1631 | + | if containerPtr != 0 && widgetIsValid(containerPtr) { | |
| 1632 | + | C.g_object_unref(C.gpointer(w.native.container)) | |
| 1633 | + | } else { | |
| 1634 | + | log.Printf("[webkit] skipping container unref; widget invalid=%#x", containerPtr) | |
| 1635 | + | } | |
| 1631 | 1636 | w.native.container = nil | |
| 1632 | 1637 | } | |
| 1633 | 1638 | } | |
| @@ -1627,7 +1627,12 @@ func (w *WebView) releaseNativeWidgets() { | |||||
| 1627 | return | 1627 | return | ||
| 1628 | } | 1628 | } | ||
| 1629 | if w.native.container != nil { | 1629 | if w.native.container != nil { | ||
| 1630 | - | C.g_object_unref(C.gpointer(w.native.container)) | 1630 | + | containerPtr := uintptr(unsafe.Pointer(w.native.container)) |
| 1631 | + | if containerPtr != 0 && widgetIsValid(containerPtr) { | |||
| 1632 | + | C.g_object_unref(C.gpointer(w.native.container)) | |||
| 1633 | + | } else { | |||
| 1634 | + | log.Printf("[webkit] skipping container unref; widget invalid=%#x", containerPtr) | |||
| 1635 | + | } | |||
| 1631 | w.native.container = nil | 1636 | w.native.container = nil | ||
| 1632 | } | 1637 | } | ||
| 1633 | } | 1638 | } | ||