1// workspace_popup.go - Popup window handling and related pane management
2package browser
3
4import (
5 "fmt"
6 "log"
7 "strings"
8 "time"
9
10 "github.com/bnema/dumber/internal/app/messaging"
11 "github.com/bnema/dumber/pkg/webkit"
12)
13
14const (
15 // Default popup window dimensions - sized for OAuth flows
16 defaultPopupMinWidth = 500 // Minimum width for OAuth popups
17 defaultPopupMinHeight = 600 // Minimum height for OAuth popups
18)
19
20// HandlePopup handles popup window creation requests from WebViews
21func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *webkit.WebView {
22 log.Printf("[workspace] HandlePopup called for URL: %s", url)
23
24 // Check for frame type markers added by WebKit layer
25 isBlankTarget := strings.HasSuffix(url, "#__dumber_frame_blank")
26 isPopupTarget := strings.HasSuffix(url, "#__dumber_frame_popup")
27
28 // Clean the URL by removing our markers
29 if isBlankTarget || isPopupTarget {
30 if isBlankTarget {
31 url = strings.TrimSuffix(url, "#__dumber_frame_blank")
32 log.Printf("[workspace] Detected _blank target - will create regular pane for: %s", url)
33 } else {
34 url = strings.TrimSuffix(url, "#__dumber_frame_popup")
35 log.Printf("[workspace] Detected popup target - will create popup pane for: %s", url)
36 }
37 }
38
39 if wm == nil || source == nil {
40 log.Printf("[workspace] HandlePopup: nil workspace manager or source - allowing native popup")
41 return nil
42 }
43
44 node := wm.viewToNode[source]
45 if node == nil {
46 log.Printf("[workspace] popup from unknown webview - allowing native popup")
47 return nil
48 }
49
50 // Note: HandlePopup is now obsolete - window.open is handled directly via JavaScript bypass
51
52 cfg := wm.app.config
53 if cfg == nil {
54 log.Printf("[workspace] HandlePopup: nil config - allowing native popup")
55 return nil
56 }
57
58 popCfg := cfg.Workspace.Popups
59 log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement)
60
61 if !popCfg.OpenInNewPane {
62 log.Printf("[workspace] Popup creation disabled in config - allowing native popup")
63 return nil
64 }
65
66 // Smart detection path: create temporary view and decide placement once type is known
67 if popCfg.EnableSmartDetection {
68 webkitCfg, err := wm.app.buildWebkitConfig()
69 if err != nil {
70 log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
71 return nil
72 }
73 webkitCfg.CreateWindow = false
74 // Create as related to avoid WindowFeatures crash; we'll decide final placement later
75 newView, err := webkit.NewWebViewWithRelated(webkitCfg, source)
76 if err != nil {
77 log.Printf("[workspace] failed to create temp WebView: %v - allowing native popup", err)
78 return nil
79 }
80
81 // Register detection callback
82 newView.OnWindowTypeDetected(func(t webkit.WindowType, feat *webkit.WindowFeatures) {
83 wm.RunOnUI(func() {
84 wm.handleDetectedWindowType(node, newView, url, t, feat)
85 })
86 })
87
88 // Fallback: if detection never fires, treat as popup as before
89 go func() {
90 time.Sleep(1500 * time.Millisecond)
91 if newView != nil {
92 wm.RunOnUI(func() {
93 if wm.viewToNode[newView] == nil { // not yet placed
94 wm.handleDetectedWindowType(node, newView, url, webkit.WindowTypePopup, nil)
95 }
96 })
97 }
98 }()
99
100 return newView
101 }
102
103 // Legacy path preserved
104 webkitCfg, err := wm.app.buildWebkitConfig()
105 if err != nil {
106 log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
107 return nil
108 }
109 webkitCfg.CreateWindow = false
110 newView, err := webkit.NewWebViewWithRelated(webkitCfg, source)
111 if err != nil {
112 log.Printf("[workspace] failed to create placeholder WebView: %v - allowing native popup", err)
113 return nil
114 }
115
116 // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler
117
118 // Create a pane for the new WebView
119 newPane, err := wm.createPane(newView)
120 if err != nil {
121 log.Printf("[workspace] failed to create popup pane: %v - allowing native popup", err)
122 return nil
123 }
124
125 // Determine placement direction
126 direction := strings.ToLower(popCfg.Placement)
127 if direction == "" {
128 direction = "right"
129 }
130
131 // Determine target node for splitting
132 target := node
133 if !popCfg.FollowPaneContext && wm.GetActiveNode() != nil {
134 target = wm.GetActiveNode()
135 }
136
137 // Add the popup pane to the workspace using manual pane insertion
138 if err := wm.insertPopupPane(target, newPane, direction); err != nil {
139 log.Printf("[workspace] popup pane insertion failed: %v - allowing native popup", err)
140 return nil
141 }
142
143 // Get the new node and popup WebView ID for tracking
144 newNode := wm.viewToNode[newView]
145 popupWebViewID := newView.ID()
146
147 // Apply different behavior based on target type
148 if isBlankTarget {
149 log.Printf("[workspace] Treating _blank target as regular pane - no auto-close behavior")
150 // For _blank targets, set up parent relationship but no auto-close
151 if newNode != nil {
152 newNode.parentPane = node
153 }
154
155 // Register close handler to clean up activePopupChildren tracking
156 newView.RegisterCloseHandler(func() {
157 log.Printf("[workspace] _blank popup requesting close via window.close()")
158 // Remove this popup from parent's activePopupChildren
159 if node != nil && popupWebViewID != "" {
160 for i, childID := range node.activePopupChildren {
161 if childID == popupWebViewID {
162 node.activePopupChildren = append(node.activePopupChildren[:i], node.activePopupChildren[i+1:]...)
163 log.Printf("[workspace] Removed _blank popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(node.activePopupChildren))
164 break
165 }
166 }
167 }
168 })
169 } else {
170 log.Printf("[workspace] Treating as popup pane - applying popup-specific behavior")
171 // Mark as popup for auto-close handling (OAuth flows, etc.)
172 if newNode != nil {
173 newNode.isPopup = true
174 newNode.parentPane = node
175 log.Printf("[workspace] Marked pane as popup for auto-close handling")
176
177 // Register close handler for popup auto-close on window.close()
178 newView.RegisterCloseHandler(func() {
179 log.Printf("[workspace] Popup requesting close via window.close()")
180
181 // Remove this popup from parent's activePopupChildren
182 if node != nil && popupWebViewID != "" {
183 for i, childID := range node.activePopupChildren {
184 if childID == popupWebViewID {
185 node.activePopupChildren = append(node.activePopupChildren[:i], node.activePopupChildren[i+1:]...)
186 log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(node.activePopupChildren))
187 break
188 }
189 }
190 }
191
192 // Look up the node at close time
193 if node := wm.viewToNode[newView]; node != nil && node.isPopup {
194 log.Printf("[workspace] Closing popup pane")
195 // Brief delay to allow any final redirects to complete
196 time.AfterFunc(200*time.Millisecond, func() {
197 wm.scheduleIdleGuarded(func() bool {
198 if node == nil || !node.widgetValid {
199 return false
200 }
201 if err := wm.ClosePane(node); err != nil {
202 log.Printf("[workspace] Failed to close popup pane: %v", err)
203 }
204 return false
205 }, node)
206 })
207 } else {
208 log.Printf("[workspace] Could not find popup node for close handler")
209 }
210 })
211 } else {
212 log.Printf("[workspace] Warning: could not find node for popup WebView in viewToNode map")
213 }
214 }
215
216 // Track this popup in the parent's activePopupChildren (for both _blank and popup types)
217 // This is critical to prevent OAuth flows from hijacking the parent pane via window.opener
218 if node != nil && popupWebViewID != "" {
219 if node.activePopupChildren == nil {
220 node.activePopupChildren = make([]string, 0)
221 }
222 node.activePopupChildren = append(node.activePopupChildren, popupWebViewID)
223 log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(node.activePopupChildren))
224
225 // Register navigation policy handler for parent to block script-initiated navigations
226 // when it has active popup children (prevents OAuth from hijacking parent pane)
227 if node.pane != nil && node.pane.WebView() != nil {
228 node.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool {
229 // Always allow user-initiated navigations
230 if isUserGesture {
231 return true
232 }
233
234 // Block script-initiated navigations if this pane has active popup children
235 if len(node.activePopupChildren) > 0 {
236 log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(node.activePopupChildren), url)
237 return false
238 }
239
240 // Allow if no active popups
241 return true
242 })
243 log.Printf("[workspace] Registered navigation policy handler for parent pane")
244 }
245 }
246
247 // Ensure GUI components are available in the new pane
248 wm.ensureGUIInPane(newPane)
249
250 // Inject GUI components into the popup pane
251 wm.ensureGUIInPane(newPane)
252
253 // Load the URL if provided
254 if url != "" {
255 paneType := "popup"
256 if isBlankTarget {
257 paneType = "_blank target"
258 }
259 log.Printf("[webkit] LoadURL (%s): %s", paneType, url)
260 if err := newView.LoadURL(url); err != nil {
261 log.Printf("[workspace] failed to load %s URL: %v", paneType, err)
262 }
263 // Ensure the WebView is visible after loading URL
264 if err := newView.Show(); err != nil {
265 log.Printf("[workspace] failed to show popup WebView: %v", err)
266 }
267 }
268
269 if isBlankTarget {
270 log.Printf("[workspace] Created regular pane for _blank target URL: %s", url)
271 } else {
272 log.Printf("[workspace] Created popup pane for URL: %s", url)
273 }
274 return newView
275}
276
277// registerOAuthAutoClose sets up OAuth auto-close functionality for popups
278// Note: OAuth detection is now handled by the main-world.js injection script
279func (wm *WorkspaceManager) registerOAuthAutoClose(view *webkit.WebView, url string) {
280 log.Printf("[workspace] OAuth auto-close enabled for popup with URL: %s", url)
281 log.Printf("[workspace] OAuth detection will be handled by main-world.js injection script")
282}
283
284// applyWindowFeatures applies window features to a WebView based on intent
285func (wm *WorkspaceManager) applyWindowFeatures(view *webkit.WebView, intent *messaging.WindowIntent, isPopup bool) {
286 if intent == nil {
287 return
288 }
289
290 features := &webkit.WindowFeatures{}
291
292 // Apply dimensions if specified
293 if intent.Width != nil {
294 features.Width = *intent.Width
295 }
296 if intent.Height != nil {
297 features.Height = *intent.Height
298 }
299
300 // Apply visibility features based on window type
301 defaultToolbar := !isPopup
302 defaultLocation := !isPopup
303 defaultMenubar := !isPopup
304
305 if intent.Toolbar != nil {
306 features.ToolbarVisible = *intent.Toolbar
307 } else {
308 features.ToolbarVisible = defaultToolbar
309 }
310
311 if intent.Location != nil {
312 features.LocationbarVisible = *intent.Location
313 } else {
314 features.LocationbarVisible = defaultLocation
315 }
316
317 if intent.Menubar != nil {
318 features.MenubarVisible = *intent.Menubar
319 } else {
320 features.MenubarVisible = defaultMenubar
321 }
322
323 if intent.Resizable != nil {
324 features.Resizable = *intent.Resizable
325 } else {
326 features.Resizable = true // Usually resizable unless explicitly disabled
327 }
328
329 view.SetWindowFeatures(features)
330 windowTypeStr := "tab"
331 if isPopup {
332 windowTypeStr = "popup"
333 }
334 log.Printf("[workspace] Applied %s window features from intent: size=%dx%d, toolbar=%t, location=%t, menubar=%t, resizable=%t",
335 windowTypeStr, features.Width, features.Height, features.ToolbarVisible, features.LocationbarVisible, features.MenubarVisible, features.Resizable)
336}
337
338// handleIntentAsTab creates a tab pane based on window.open intent
339func (wm *WorkspaceManager) handleIntentAsTab(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView {
340 log.Printf("[workspace] Handling intent as tab: %s", url)
341
342 webkitCfg, err := wm.app.buildWebkitConfig()
343 if err != nil {
344 log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
345 return nil
346 }
347 webkitCfg.CreateWindow = false
348
349 newView, err := webkit.NewWebView(webkitCfg)
350 if err != nil {
351 log.Printf("[workspace] failed to create tab WebView: %v - allowing native popup", err)
352 return nil
353 }
354
355 newPane, err := wm.createPane(newView)
356 if err != nil {
357 log.Printf("[workspace] failed to create tab pane: %v - allowing native popup", err)
358 return nil
359 }
360
361 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
362 if direction == "" {
363 direction = "right"
364 }
365
366 if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil {
367 log.Printf("[workspace] tab pane insertion failed: %v - allowing native popup", err)
368 return nil
369 }
370
371 node := wm.viewToNode[newView]
372 if node != nil {
373 node.windowType = webkit.WindowTypeTab
374 node.isRelated = false
375
376 // Apply window features from JavaScript intent
377 wm.applyWindowFeatures(newView, intent, false)
378 }
379
380 wm.ensureGUIInPane(newPane)
381
382 if url != "" {
383 if err := newView.LoadURL(url); err != nil {
384 log.Printf("[workspace] failed to load tab URL: %v", err)
385 }
386 if err := newView.Show(); err != nil {
387 log.Printf("[workspace] failed to show tab WebView: %v", err)
388 }
389 }
390
391 log.Printf("[workspace] Created tab pane for URL: %s", url)
392 return newView
393}
394
395// handleIntentAsPopup creates a related popup pane based on window.open intent
396func (wm *WorkspaceManager) handleIntentAsPopup(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView {
397 log.Printf("[workspace] Handling intent as popup: %s", url)
398
399 webkitCfg, err := wm.app.buildWebkitConfig()
400 if err != nil {
401 log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err)
402 return nil
403 }
404 webkitCfg.CreateWindow = false
405
406 newView, err := webkit.NewWebViewWithRelated(webkitCfg, sourceNode.pane.webView)
407 if err != nil {
408 log.Printf("[workspace] failed to create popup WebView: %v - allowing native popup", err)
409 return nil
410 }
411
412 // Log the parent-popup WebView ID relationship for OAuth auto-close
413 parentWebViewID := sourceNode.pane.webView.ID()
414 popupWebViewID := newView.ID()
415 log.Printf("[workspace] Created popup WebView: parentID=%s popupID=%s url=%s", parentWebViewID, popupWebViewID, url)
416
417 // Store popup WebView ID in parent's localStorage for OAuth callback lookup
418 storeScript := fmt.Sprintf(`
419 try {
420 const parentWebViewId = '%s';
421 const popupWebViewId = '%s';
422 const popupMapping = {
423 parentId: parentWebViewId,
424 popupId: popupWebViewId,
425 timestamp: Date.now(),
426 url: '%s'
427 };
428 localStorage.setItem('popup_mapping_' + parentWebViewId, JSON.stringify(popupMapping));
429 console.log('[workspace] Stored popup mapping:', popupMapping);
430 } catch(e) {
431 console.warn('[workspace] Failed to store popup mapping:', e);
432 }
433 `, parentWebViewID, popupWebViewID, url)
434
435 // Inject into parent WebView so it can find its popup later
436 if err := sourceNode.pane.webView.InjectScript(storeScript); err != nil {
437 log.Printf("[workspace] Failed to inject popup mapping script into parent: %v", err)
438 }
439
440 newPane, err := wm.createPane(newView)
441 if err != nil {
442 log.Printf("[workspace] failed to create popup pane: %v - allowing native popup", err)
443 return nil
444 }
445
446 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
447 if direction == "" {
448 direction = "right"
449 }
450
451 if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil {
452 log.Printf("[workspace] popup pane insertion failed: %v - allowing native popup", err)
453 return nil
454 }
455
456 node := wm.viewToNode[newView]
457 var requestID string
458 if node != nil {
459 node.windowType = webkit.WindowTypePopup
460 node.isRelated = true
461 node.parentPane = sourceNode
462 node.isPopup = true
463 node.autoClose = wm.shouldAutoClose(url)
464
465 // Track this popup in the parent's activePopupChildren
466 if sourceNode != nil {
467 if sourceNode.activePopupChildren == nil {
468 sourceNode.activePopupChildren = make([]string, 0)
469 }
470 sourceNode.activePopupChildren = append(sourceNode.activePopupChildren, popupWebViewID)
471 log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(sourceNode.activePopupChildren))
472
473 // Register navigation policy handler for parent to block script-initiated navigations
474 // when it has active popup children (prevents OAuth from hijacking parent pane)
475 if sourceNode.pane != nil && sourceNode.pane.WebView() != nil {
476 sourceNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool {
477 // Always allow user-initiated navigations
478 if isUserGesture {
479 return true
480 }
481
482 // Block script-initiated navigations if this pane has active popup children
483 if len(sourceNode.activePopupChildren) > 0 {
484 log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(sourceNode.activePopupChildren), url)
485 return false
486 }
487
488 // Allow if no active popups
489 return true
490 })
491 log.Printf("[workspace] Registered navigation policy handler for parent pane")
492 }
493 }
494
495 // Store requestID for deduplication cleanup
496 if intent != nil {
497 requestID = intent.RequestID
498 node.requestID = requestID
499 }
500
501 // Apply window features from JavaScript intent
502 wm.applyWindowFeatures(newView, intent, true)
503
504 // Apply minimum size constraints to prevent compression
505 var width, height int
506 if intent != nil {
507 if intent.Width != nil {
508 width = *intent.Width
509 }
510 if intent.Height != nil {
511 height = *intent.Height
512 }
513 }
514 wm.applyPopupSizeConstraints(newView, width, height)
515 }
516
517 // Register close handler for popup auto-close
518 newView.RegisterCloseHandler(func() {
519 log.Printf("[workspace] Popup requesting close via window.close()")
520
521 // Clear the RequestID from deduplicator to allow new popups with same ID
522 if requestID != "" && wm.paneDeduplicator != nil {
523 wm.paneDeduplicator.ClearRequestID(requestID)
524 }
525
526 // Remove this popup from parent's activePopupChildren
527 if sourceNode != nil && popupWebViewID != "" {
528 for i, childID := range sourceNode.activePopupChildren {
529 if childID == popupWebViewID {
530 sourceNode.activePopupChildren = append(sourceNode.activePopupChildren[:i], sourceNode.activePopupChildren[i+1:]...)
531 log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(sourceNode.activePopupChildren))
532 break
533 }
534 }
535 }
536
537 if n := wm.viewToNode[newView]; n != nil && n.isPopup {
538 time.AfterFunc(200*time.Millisecond, func() {
539 wm.scheduleIdleGuarded(func() bool {
540 if n == nil || !n.widgetValid {
541 return false
542 }
543 if err := wm.ClosePane(n); err != nil {
544 log.Printf("[workspace] Failed to close popup pane: %v", err)
545 }
546 return false
547 }, n)
548 })
549 }
550 })
551
552 // URL-based auto-close for OAuth popups
553 if node != nil && node.isPopup && node.autoClose {
554 wm.registerOAuthAutoClose(newView, url)
555 }
556
557 wm.ensureGUIInPane(newPane)
558
559 if url != "" {
560 if err := newView.LoadURL(url); err != nil {
561 log.Printf("[workspace] failed to load popup URL: %v", err)
562 }
563 if err := newView.Show(); err != nil {
564 log.Printf("[workspace] failed to show popup WebView: %v", err)
565 }
566 }
567
568 log.Printf("[workspace] Created popup pane for URL: %s", url)
569 return newView
570}
571
572// insertIndependentPane inserts a new independent pane next to the source
573func (wm *WorkspaceManager) insertIndependentPane(sourceNode *paneNode, webView *webkit.WebView, url string) error {
574 newPane, err := wm.createPane(webView)
575 if err != nil {
576 return err
577 }
578 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
579 if direction == "" {
580 direction = "right"
581 }
582 if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { // reuse insertion primitive
583 return err
584 }
585 node := wm.viewToNode[webView]
586 if node != nil {
587 node.windowType = webkit.WindowTypeTab
588 node.isRelated = false
589 }
590 if url != "" {
591 _ = webView.LoadURL(url)
592 }
593 return nil
594}
595
596// configureRelatedPopup creates a related view and inserts it
597func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView *webkit.WebView, url string, feat *webkit.WindowFeatures) {
598 // Use the WebView that was already created and returned to WebKit
599 related := webView
600 newPane, err := wm.createPane(related)
601 if err != nil {
602 log.Printf("[workspace] failed to create related popup pane: %v", err)
603 return
604 }
605 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
606 if direction == "" {
607 direction = "right"
608 }
609 if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil {
610 log.Printf("[workspace] failed to insert related popup pane: %v", err)
611 return
612 }
613 node := wm.viewToNode[related]
614 popupWebViewID := related.ID()
615
616 if node != nil {
617 node.windowType = webkit.WindowTypePopup
618 node.windowFeatures = feat
619 node.isRelated = true
620 node.parentPane = sourceNode
621 node.isPopup = true
622 // Heuristic + config for auto-close intent
623 node.autoClose = wm.shouldAutoClose(url)
624
625 // Apply minimum size constraints to prevent compression
626 var width, height int
627 if feat != nil {
628 width = feat.Width
629 height = feat.Height
630 }
631 wm.applyPopupSizeConstraints(related, width, height)
632 }
633
634 // Track this popup in the parent's activePopupChildren
635 // This is critical to prevent OAuth flows from hijacking the parent pane via window.opener
636 if sourceNode != nil && popupWebViewID != "" {
637 if sourceNode.activePopupChildren == nil {
638 sourceNode.activePopupChildren = make([]string, 0)
639 }
640 sourceNode.activePopupChildren = append(sourceNode.activePopupChildren, popupWebViewID)
641 log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(sourceNode.activePopupChildren))
642
643 // Register navigation policy handler for parent to block script-initiated navigations
644 // when it has active popup children (prevents OAuth from hijacking parent pane)
645 if sourceNode.pane != nil && sourceNode.pane.WebView() != nil {
646 sourceNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool {
647 // Always allow user-initiated navigations
648 if isUserGesture {
649 return true
650 }
651
652 // Block script-initiated navigations if this pane has active popup children
653 if len(sourceNode.activePopupChildren) > 0 {
654 log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(sourceNode.activePopupChildren), url)
655 return false
656 }
657
658 // Allow if no active popups
659 return true
660 })
661 log.Printf("[workspace] Registered navigation policy handler for parent pane")
662 }
663 }
664
665 // Pipe into existing auto-close flow only for popups (confirmed by detection)
666 related.RegisterCloseHandler(func() {
667 log.Printf("[workspace] Popup requesting close via window.close()")
668
669 // Remove this popup from parent's activePopupChildren
670 if sourceNode != nil && popupWebViewID != "" {
671 for i, childID := range sourceNode.activePopupChildren {
672 if childID == popupWebViewID {
673 sourceNode.activePopupChildren = append(sourceNode.activePopupChildren[:i], sourceNode.activePopupChildren[i+1:]...)
674 log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(sourceNode.activePopupChildren))
675 break
676 }
677 }
678 }
679
680 if n := wm.viewToNode[related]; n != nil && n.isPopup {
681 time.AfterFunc(200*time.Millisecond, func() {
682 wm.scheduleIdleGuarded(func() bool {
683 if n == nil || !n.widgetValid {
684 return false
685 }
686 if err := wm.ClosePane(n); err != nil {
687 log.Printf("[workspace] Failed to close popup pane: %v", err)
688 }
689 return false
690 }, n)
691 })
692 }
693 })
694
695 // URL-based fallback: if providers don't call window.close(), auto-close on OAuth callback URLs
696 if node != nil && node.isPopup && node.autoClose {
697 wm.registerOAuthAutoClose(related, url)
698 }
699 if url != "" {
700 _ = related.LoadURL(url)
701 }
702}
703
704// applyPopupSizeConstraints applies minimum size constraints to prevent OAuth popup compression
705func (wm *WorkspaceManager) applyPopupSizeConstraints(view *webkit.WebView, width, height int) {
706 minWidth := defaultPopupMinWidth
707 minHeight := defaultPopupMinHeight
708
709 // Use provided dimensions if valid
710 if width > 0 {
711 minWidth = width
712 }
713 if height > 0 {
714 minHeight = height
715 }
716
717 // Apply to container
718 if container := view.RootWidget(); container != 0 && webkit.WidgetIsValid(container) {
719 webkit.WidgetSetSizeRequest(container, minWidth, minHeight)
720 webkit.WidgetQueueResize(container)
721 log.Printf("[workspace] Applied minimum size %dx%d to popup container=%#x", minWidth, minHeight, container)
722 }
723
724 // Apply to WebView widget
725 if webViewWidget := view.Widget(); webViewWidget != 0 && webkit.WidgetIsValid(webViewWidget) {
726 webkit.WidgetSetSizeRequest(webViewWidget, minWidth, minHeight)
727 webkit.WidgetQueueResize(webViewWidget)
728 log.Printf("[workspace] Applied minimum size %dx%d to popup webview=%#x", minWidth, minHeight, webViewWidget)
729 }
730}
731
732// shouldAutoClose checks simple OAuth-like URL patterns and config flag
733func (wm *WorkspaceManager) shouldAutoClose(url string) bool {
734 log.Printf("[workspace] shouldAutoClose called for URL: %s", url)
735
736 if wm == nil || wm.app == nil || wm.app.config == nil {
737 log.Printf("[workspace] shouldAutoClose: missing config, returning true")
738 return true
739 }
740 if !wm.app.config.Workspace.Popups.OAuthAutoClose {
741 log.Printf("[workspace] shouldAutoClose: OAuthAutoClose disabled in config, returning false")
742 return false
743 }
744
745 u := strings.ToLower(url)
746 log.Printf("[workspace] shouldAutoClose: checking lowercase URL: %s", u)
747
748 // RFC 6749 compliant OAuth 2.0 URL patterns
749 oauthPatterns := []string{
750 // Standard OAuth endpoints
751 "oauth", "authorize", "authorization",
752 // Standard callback/redirect patterns
753 "callback", "redirect", "auth/callback",
754 // OpenID Connect patterns
755 "oidc", "openid",
756 // Common OAuth parameter indicators
757 "response_type=", "client_id=", "redirect_uri=", "scope=", "state=",
758 // Standard OAuth response parameters
759 "code=", "access_token=", "id_token=", "token_type=",
760 // Error response parameters
761 "error=", "error_description=",
762 }
763
764 // Check for OAuth patterns in URL
765 for _, pattern := range oauthPatterns {
766 if strings.Contains(u, pattern) {
767 log.Printf("[workspace] shouldAutoClose: MATCHED pattern '%s' in URL, returning true", pattern)
768 return true
769 }
770 }
771
772 log.Printf("[workspace] shouldAutoClose: no OAuth patterns matched, returning false")
773 return false
774}
775
776// RunOnUI schedules a function; here simply executes inline as GTK main loop is single-threaded
777func (wm *WorkspaceManager) RunOnUI(fn func()) {
778 if fn != nil {
779 fn()
780 }
781}
782
783// handleDetectedWindowType handles window type detection from smart detection path
784func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webView *webkit.WebView, url string, windowType webkit.WindowType, features *webkit.WindowFeatures) {
785 if wm.viewToNode[webView] != nil {
786 return // Already placed
787 }
788
789 log.Printf("[workspace] Smart detection result: type=%d url=%s", windowType, url)
790
791 switch windowType {
792 case webkit.WindowTypeTab:
793 // For tabs, create a NEW independent WebView (can't use the related one)
794 webkitCfg, err := wm.app.buildWebkitConfig()
795 if err != nil {
796 log.Printf("[workspace] failed to build webkit config for tab: %v", err)
797 return
798 }
799 webkitCfg.CreateWindow = false
800
801 // Create independent WebView like handleIntentAsTab does
802 independentView, err := webkit.NewWebView(webkitCfg)
803 if err != nil {
804 log.Printf("[workspace] failed to create independent tab WebView: %v", err)
805 return
806 }
807
808 // The related webView was just for detection - we don't use it for tabs
809 // Insert the new independent view as a tab
810 if err := wm.insertIndependentPane(sourceNode, independentView, url); err != nil {
811 log.Printf("[workspace] Failed to insert independent pane: %v", err)
812 }
813
814 case webkit.WindowTypePopup:
815 // For popups, use the related WebView we already created
816 wm.configureRelatedPopup(sourceNode, webView, url, features)
817 default:
818 // Fallback to popup behavior
819 wm.configureRelatedPopup(sourceNode, webView, url, features)
820 }
821}