detached 75a1db0

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 {
2929 __dumber_omnibox_suggestions?: (suggestions: Suggestion[]) => void;
3030 __dumber_webview_id?: string;
3131 __dumber_is_active?: boolean;
32+ __dumber_teardown?: () => void;
3233 webkit?: {
3334 messageHandlers?: {
3435 dumber?: {
@@ -101,6 +102,9 @@ function detectWindowType(features?: string | null): string {
101102 }
102103 window.__dumber_page_bridge_installed = true;
103104
105+ const cleanupHandlers: Array<() => void> = [];
106+ let teardownExecuted = false;
107+
104108 // Initialize zoom level placeholder (will be replaced by Go)
105109 const initialZoom = 1.0; // __DOM_ZOOM_DEFAULT__ will be replaced by Go
106110 window.__dumber_dom_zoom_seed = initialZoom;
@@ -267,13 +271,18 @@ function detectWindowType(features?: string | null): string {
267271 }
268272 };
269273
270- document.addEventListener("dumber:omnibox-ready", () => {
274+ const handleOmniboxReady = () => {
271275 omniboxReady = true;
272276 if (omniboxQueue && omniboxQueue.length) {
273277 const items = omniboxQueue.slice();
274278 omniboxQueue.length = 0;
275279 items.forEach((s) => omniboxDispatch([s]));
276280 }
281+ };
282+
283+ document.addEventListener("dumber:omnibox-ready", handleOmniboxReady);
284+ cleanupHandlers.push(() => {
285+ document.removeEventListener("dumber:omnibox-ready", handleOmniboxReady);
277286 });
278287
279288 // Unified omnibox API
@@ -283,6 +292,9 @@ function detectWindowType(features?: string | null): string {
283292 },
284293 };
285294
295+ let popupMessageInterval: ReturnType<typeof setInterval> | null = null;
296+ let oauthUrlCheckInterval: ReturnType<typeof setInterval> | null = null;
297+
286298 // Popup window.opener bridge using shared localStorage
287299 const setupPopupOpenerBridge = () => {
288300 try {
@@ -391,7 +403,13 @@ function detectWindowType(features?: string | null): string {
391403 };
392404
393405 // 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+ });
395413
396414 console.log(`[dumber-popup] window.opener bridge established successfully`);
397415 }
@@ -405,6 +423,7 @@ function detectWindowType(features?: string | null): string {
405423
406424 // Parent window message polling - only for OAuth scenarios
407425 let parentPollingInterval: ReturnType<typeof setInterval> | null = null;
426+ let parentPollingHeartbeat: ReturnType<typeof setInterval> | null = null;
408427
409428 const setupParentMessagePolling = () => {
410429 // Only start polling if we have popup mappings or OAuth callbacks
@@ -511,15 +530,27 @@ function detectWindowType(features?: string | null): string {
511530 if (!parentPollingInterval && hasRelevantData()) {
512531 console.log(`[dumber-parent] Starting OAuth/popup message polling`);
513532 parentPollingInterval = setInterval(pollForPopupMessages, 100);
533+ cleanupHandlers.push(() => {
534+ if (parentPollingInterval) {
535+ clearInterval(parentPollingInterval);
536+ parentPollingInterval = null;
537+ }
538+ });
514539 }
515540 };
516541
517542 // Check periodically if polling should start
518- setInterval(() => {
543+ parentPollingHeartbeat = setInterval(() => {
519544 if (hasRelevantData()) {
520545 startPollingForOAuthCallbacks();
521546 }
522547 }, 1000);
548+ cleanupHandlers.push(() => {
549+ if (parentPollingHeartbeat) {
550+ clearInterval(parentPollingHeartbeat);
551+ parentPollingHeartbeat = null;
552+ }
553+ });
523554 };
524555
525556 // Setup conditional parent message polling
@@ -602,16 +633,26 @@ function detectWindowType(features?: string | null): string {
602633
603634 // Monitor for URL changes (for SPAs that don't reload)
604635 let lastUrl = window.location.href;
605- setInterval(() => {
636+ oauthUrlCheckInterval = setInterval(() => {
606637 if (window.location.href !== lastUrl) {
607638 lastUrl = window.location.href;
608639 detectOAuthCallback();
609640 }
610641 }, 500);
642+ cleanupHandlers.push(() => {
643+ if (oauthUrlCheckInterval) {
644+ clearInterval(oauthUrlCheckInterval);
645+ oauthUrlCheckInterval = null;
646+ }
647+ });
611648
612649 // Also check on navigation events
613650 window.addEventListener('popstate', detectOAuthCallback);
614651 window.addEventListener('hashchange', detectOAuthCallback);
652+ cleanupHandlers.push(() => {
653+ window.removeEventListener('popstate', detectOAuthCallback);
654+ window.removeEventListener('hashchange', detectOAuthCallback);
655+ });
615656 };
616657
617658 // Setup OAuth callback detection
@@ -759,9 +800,15 @@ function detectWindowType(features?: string | null): string {
759800
760801
761802 // 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) => {
763805 document.addEventListener(eventType, trackUserInteraction, true);
764806 });
807+ cleanupHandlers.push(() => {
808+ interactionEvents.forEach((eventType) => {
809+ document.removeEventListener(eventType, trackUserInteraction, true);
810+ });
811+ });
765812
766813 console.log(
767814 `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`,
@@ -866,6 +913,50 @@ function detectWindowType(features?: string | null): string {
866913 }
867914 }
868915
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+
869960 // Console capture functionality
870961 const setupConsoleCapture = () => {
871962 const originalConsole = {
@@ -921,6 +1012,14 @@ function detectWindowType(features?: string | null): string {
9211012 console.error = createConsoleWrapper(originalConsole.error, "ERROR");
9221013 console.info = createConsoleWrapper(originalConsole.info, "INFO");
9231014 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+ });
9241023 };
9251024
9261025 // 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 Go109 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 API288 // 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 localStorage298 // 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 100ms405 // 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 scenarios424 // 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 callbacks429 // 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 start542 // 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 polling556 // 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 events649 // 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 detection658 // Setup OAuth callback detection
@@ -759,9 +800,15 @@ function detectWindowType(features?: string | null): string {
759 800
760 801
761 // Track user interactions for popup validation802 // 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 functionality960 // 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 capture1025 // Initialize console capture
M internal/app/browser/pane.go +33 -8
@@ -79,19 +79,44 @@ func (p *BrowserPane) SetGUIComponent(component string, loaded bool) {
7979
8080 // Cleanup releases pane resources
8181 func (p *BrowserPane) Cleanup() {
82- // Cleanup shortcuts
82+ if p == nil {
83+ return
84+ }
85+
8386 if p.shortcutHandler != nil {
84- // TODO: Add cleanup method to ShortcutHandler
87+ p.shortcutHandler.Detach()
8588 log.Printf("[pane-%s] Cleaned up shortcuts", p.id)
8689 }
8790
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+
88107 // 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 {
95120 log.Printf("[pane-%s] failed to inject cleanup script: %v", p.id, err)
96121 }
97122 }
@@ -79,19 +79,44 @@ func (p *BrowserPane) SetGUIComponent(component string, loaded bool) {
79 79
80 // Cleanup releases pane resources80 // Cleanup releases pane resources
81 func (p *BrowserPane) Cleanup() {81 func (p *BrowserPane) Cleanup() {
82- // Cleanup shortcuts82+ if p == nil {
83+ return
84+ }
85+
83 if p.shortcutHandler != nil {86 if p.shortcutHandler != nil {
84- // TODO: Add cleanup method to ShortcutHandler87+ 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 GUI107 // 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() {
131131 log.Printf("Shortcut: failed to expose workspace config: %v", err)
132132 }
133133 }
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 {
10861086 return errors.New("close target missing webview")
10871087 }
10881088
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+
10891101 remaining := len(wm.app.panes)
10901102 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+ }
10911186 if willBeLastPane && wm.root == node {
10921187 log.Printf("[workspace] closing final pane; exiting browser")
1188+ ensureCleanup()
10931189 wm.detachHover(node)
10941190 if err := node.pane.webView.Destroy(); err != nil {
10951191 log.Printf("[workspace] failed to destroy webview: %v", err)
@@ -1110,6 +1206,7 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
11101206 if replacement == nil {
11111207 // No other panes exist, this is the final pane
11121208 log.Printf("[workspace] closing final pane; exiting browser")
1209+ ensureCleanup()
11131210 wm.detachHover(node)
11141211 if err := node.pane.webView.Destroy(); err != nil {
11151212 log.Printf("[workspace] failed to destroy webview: %v", err)
@@ -1163,13 +1260,19 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
11631260
11641261 // Destroy the webview and detach hover AFTER rearranging hierarchy
11651262 // Only destroy the webview if this is the final pane, otherwise just clean up
1263+ ensureCleanup()
11661264 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:
11681271 // This is the last pane, safe to destroy completely
11691272 if err := node.pane.webView.Destroy(); err != nil {
11701273 log.Printf("[workspace] failed to destroy webview: %v", err)
11711274 }
1172- } else {
1275+ default:
11731276 // Multiple panes remain, don't destroy the window - just clean up the webview
11741277 log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1)
11751278 // TODO: Add a method to destroy just the webview without the window
@@ -1286,14 +1389,19 @@ func (wm *WorkspaceManager) closePane(node *paneNode) error {
12861389 }
12871390
12881391 // 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()
12901393 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:
12921400 // This is the last pane, safe to destroy completely
12931401 if err := node.pane.webView.Destroy(); err != nil {
12941402 log.Printf("[workspace] failed to destroy webview: %v", err)
12951403 }
1296- } else {
1404+ default:
12971405 // Multiple panes remain, don't destroy the window - just clean up the webview
12981406 log.Printf("[workspace] skipping webview destruction to preserve window (panes remaining: %d)", remaining-1)
12991407 // 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 <= 11102 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 pane1207 // 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 hierarchy1261 // Destroy the webview and detach hover AFTER rearranging hierarchy
1165 // Only destroy the webview if this is the final pane, otherwise just clean up1262 // 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 completely1271 // 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 webview1276 // 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 window1278 // 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 complete1391 // 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 up1392+ 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 completely1400 // 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 webview1405 // 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 window1407 // 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) {
5555 log.Printf("Failed to dispatch toast event: %v", err)
5656 }
5757 }
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() {
134134 }
135135 }
136136 }
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
172172 z.lastZoomDomain = domain
173173 })
174174 }
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 = domain172 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) {
106106 h.handleNavigation(msg)
107107 case "query":
108108 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+ }
109113 h.handleQuery(msg)
110114 case "wails":
111115 h.handleWailsBridge(msg)
@@ -217,11 +221,11 @@ func (h *Handler) handleClosePopup(msg Message) {
217221 }
218222
219223 func (h *Handler) handleQuery(msg Message) {
220- log.Printf("[DEBUG] handleQuery called: q='%s', limit=%d", msg.Q, msg.Limit)
221224 if h.webView == nil {
222- log.Printf("[ERROR] handleQuery: webView is nil")
225+ log.Printf("[WARN] Skipping query handling: webview is nil")
223226 return
224227 }
228+ log.Printf("[DEBUG] handleQuery called: q='%s', limit=%d", msg.Q, msg.Limit)
225229 if h.browserService == nil {
226230 log.Printf("[ERROR] handleQuery: browserService is nil")
227231 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 return226 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 return231 return
M pkg/webkit/script_cgo.go +21 -2
@@ -7,13 +7,32 @@ package webkit
77 #cgo CFLAGS: -I/usr/include/webkitgtk-6.0
88 #include <stdlib.h>
99 #include <webkit/webkit.h>
10+
11+static gboolean dumber_is_valid_web_view(WebKitWebView *wv) {
12+ return WEBKIT_IS_WEB_VIEW(wv);
13+}
1014 */
1115 import "C"
1216
1317 import "unsafe"
1418
15-func (w *WebView) InjectScript(js string) error {
19+func (w *WebView) canEvaluateJavaScript() bool {
1620 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() {
1736 return ErrNotImplemented
1837 }
1938 cjs := C.CString(js)
@@ -35,7 +54,7 @@ func (w *WebView) InjectScript(js string) error {
3554
3655 // InjectScriptIntoWorld evaluates JavaScript in the specified script world.
3756 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() {
3958 return ErrNotImplemented
4059 }
4160 cjs := C.CString(js)
@@ -7,13 +7,32 @@ package webkit
7 #cgo CFLAGS: -I/usr/include/webkitgtk-6.07 #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 ErrNotImplemented36 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 ErrNotImplemented58 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() {
16271627 return
16281628 }
16291629 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+ }
16311636 w.native.container = nil
16321637 }
16331638 }
@@ -1627,7 +1627,12 @@ func (w *WebView) releaseNativeWidgets() {
1627 return1627 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 = nil1636 w.native.container = nil
1632 }1637 }
1633 }1638 }

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