fix: stabilize webview signals, pane history cleanup, and ghost selection
- commit
- c948582e81d2563d7d796b4b7923c68b0a11125a
- author
- brice · · 2026-02-07 07:35 UTC
- parent
- 964a47e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +126 -50
M internal/application/port/webview.go +16 -4
| @@ -52,6 +52,16 @@ type WebViewState struct { | |||
| 52 | 52 | ZoomLevel float64 | |
| 53 | 53 | } | |
| 54 | 54 | ||
| 55 | + | // WebProcessTerminationReason describes why the web process terminated. | |
| 56 | + | type WebProcessTerminationReason int | |
| 57 | + | ||
| 58 | + | const ( | |
| 59 | + | WebProcessTerminationUnknown WebProcessTerminationReason = iota | |
| 60 | + | WebProcessTerminationCrashed | |
| 61 | + | WebProcessTerminationExceededMemory | |
| 62 | + | WebProcessTerminationByAPI | |
| 63 | + | ) | |
| 64 | + | ||
| 55 | 65 | // PopupRequest contains metadata about a popup window request. | |
| 56 | 66 | type PopupRequest struct { | |
| 57 | 67 | TargetURI string | |
| @@ -92,6 +102,8 @@ type WebViewCallbacks struct { | |||
| 92 | 102 | // OnLinkHover is called when hovering over a link, image, or media element. | |
| 93 | 103 | // The uri parameter contains the target URL, or empty string when leaving. | |
| 94 | 104 | OnLinkHover func(uri string) | |
| 105 | + | // OnWebProcessTerminated is called when the web process exits unexpectedly. | |
| 106 | + | OnWebProcessTerminated func(reason WebProcessTerminationReason, reasonLabel string, uri string) | |
| 95 | 107 | } | |
| 96 | 108 | ||
| 97 | 109 | // FindOptions configures search behavior. | |
| @@ -111,10 +123,10 @@ type FindController interface { | |||
| 111 | 123 | GetSearchText() string | |
| 112 | 124 | ||
| 113 | 125 | // Signal connections | |
| 114 | - | OnFoundText(callback func(matchCount uint)) uint32 | |
| 115 | - | OnFailedToFindText(callback func()) uint32 | |
| 116 | - | OnCountedMatches(callback func(matchCount uint)) uint32 | |
| 117 | - | DisconnectSignal(id uint32) | |
| 126 | + | OnFoundText(callback func(matchCount uint)) uint | |
| 127 | + | OnFailedToFindText(callback func()) uint | |
| 128 | + | OnCountedMatches(callback func(matchCount uint)) uint | |
| 129 | + | DisconnectSignal(id uint) | |
| 118 | 130 | } | |
| 119 | 131 | ||
| 120 | 132 | // WebView defines the port interface for browser view operations. | |
| @@ -52,6 +52,16 @@ type WebViewState struct { | |||||
| 52 | ZoomLevel float64 | 52 | ZoomLevel float64 | ||
| 53 | } | 53 | } | ||
| 54 | 54 | ||||
| 55 | + | // WebProcessTerminationReason describes why the web process terminated. | |||
| 56 | + | type WebProcessTerminationReason int | |||
| 57 | + | ||||
| 58 | + | const ( | |||
| 59 | + | WebProcessTerminationUnknown WebProcessTerminationReason = iota | |||
| 60 | + | WebProcessTerminationCrashed | |||
| 61 | + | WebProcessTerminationExceededMemory | |||
| 62 | + | WebProcessTerminationByAPI | |||
| 63 | + | ) | |||
| 64 | + | ||||
| 55 | // PopupRequest contains metadata about a popup window request. | 65 | // PopupRequest contains metadata about a popup window request. | ||
| 56 | type PopupRequest struct { | 66 | type PopupRequest struct { | ||
| 57 | TargetURI string | 67 | TargetURI string | ||
| @@ -92,6 +102,8 @@ type WebViewCallbacks struct { | |||||
| 92 | // OnLinkHover is called when hovering over a link, image, or media element. | 102 | // OnLinkHover is called when hovering over a link, image, or media element. | ||
| 93 | // The uri parameter contains the target URL, or empty string when leaving. | 103 | // The uri parameter contains the target URL, or empty string when leaving. | ||
| 94 | OnLinkHover func(uri string) | 104 | OnLinkHover func(uri string) | ||
| 105 | + | // OnWebProcessTerminated is called when the web process exits unexpectedly. | |||
| 106 | + | OnWebProcessTerminated func(reason WebProcessTerminationReason, reasonLabel string, uri string) | |||
| 95 | } | 107 | } | ||
| 96 | 108 | ||||
| 97 | // FindOptions configures search behavior. | 109 | // FindOptions configures search behavior. | ||
| @@ -111,10 +123,10 @@ type FindController interface { | |||||
| 111 | GetSearchText() string | 123 | GetSearchText() string | ||
| 112 | 124 | ||||
| 113 | // Signal connections | 125 | // Signal connections | ||
| 114 | - | OnFoundText(callback func(matchCount uint)) uint32 | 126 | + | OnFoundText(callback func(matchCount uint)) uint |
| 115 | - | OnFailedToFindText(callback func()) uint32 | 127 | + | OnFailedToFindText(callback func()) uint |
| 116 | - | OnCountedMatches(callback func(matchCount uint)) uint32 | 128 | + | OnCountedMatches(callback func(matchCount uint)) uint |
| 117 | - | DisconnectSignal(id uint32) | 129 | + | DisconnectSignal(id uint) |
| 118 | } | 130 | } | ||
| 119 | 131 | ||||
| 120 | // WebView defines the port interface for browser view operations. | 132 | // WebView defines the port interface for browser view operations. | ||
M internal/application/usecase/find_in_page.go +1 -1
| @@ -43,7 +43,7 @@ type FindInPageUseCase struct { | |||
| 43 | 43 | ctx context.Context | |
| 44 | 44 | ||
| 45 | 45 | controller port.FindController | |
| 46 | - | signalIDs []uint32 | |
| 46 | + | signalIDs []uint | |
| 47 | 47 | ||
| 48 | 48 | query string | |
| 49 | 49 | matchCount uint | |
| @@ -43,7 +43,7 @@ type FindInPageUseCase struct { | |||||
| 43 | ctx context.Context | 43 | ctx context.Context | ||
| 44 | 44 | ||||
| 45 | controller port.FindController | 45 | controller port.FindController | ||
| 46 | - | signalIDs []uint32 | 46 | + | signalIDs []uint |
| 47 | 47 | ||||
| 48 | query string | 48 | query string | ||
| 49 | matchCount uint | 49 | matchCount uint | ||
M internal/application/usecase/navigate.go +29 -10
| @@ -24,6 +24,9 @@ const ( | |||
| 24 | 24 | ||
| 25 | 25 | // historyWorkerFlushInterval coalesces bursts into fewer persistence writes. | |
| 26 | 26 | historyWorkerFlushInterval = 100 * time.Millisecond | |
| 27 | + | ||
| 28 | + | // historyFallbackIncrementCap bounds DB writes when delta-increment API is unavailable. | |
| 29 | + | historyFallbackIncrementCap = 256 | |
| 27 | 30 | ) | |
| 28 | 31 | ||
| 29 | 32 | // historyRecord holds data for async history recording. | |
| @@ -158,9 +161,7 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str | |||
| 158 | 161 | return | |
| 159 | 162 | } | |
| 160 | 163 | ||
| 161 | - | if paneID == "" { | |
| 162 | - | paneID = "__default__" | |
| 163 | - | } | |
| 164 | + | paneID = normalizePaneID(paneID) | |
| 164 | 165 | ||
| 165 | 166 | now := time.Now() | |
| 166 | 167 | ||
| @@ -198,6 +199,15 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str | |||
| 198 | 199 | } | |
| 199 | 200 | } | |
| 200 | 201 | ||
| 202 | + | // ClearPaneHistory removes per-pane deduplication state to prevent unbounded growth. | |
| 203 | + | // Callers should invoke this when a pane is closed. | |
| 204 | + | func (uc *NavigateUseCase) ClearPaneHistory(paneID string) { | |
| 205 | + | paneID = normalizePaneID(paneID) | |
| 206 | + | uc.recentMu.Lock() | |
| 207 | + | delete(uc.recentVisits, paneID) | |
| 208 | + | uc.recentMu.Unlock() | |
| 209 | + | } | |
| 210 | + | ||
| 201 | 211 | // historyWorker is a background goroutine that processes history records. | |
| 202 | 212 | // It drains the queue and persists records to the database without blocking the UI. | |
| 203 | 213 | func (uc *NavigateUseCase) historyWorker() { | |
| @@ -262,7 +272,7 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||
| 262 | 272 | } | |
| 263 | 273 | ||
| 264 | 274 | if existing != nil { | |
| 265 | - | delta := maxInt(1, record.visits) | |
| 275 | + | delta := max(1, record.visits) | |
| 266 | 276 | if deltaWriter, ok := uc.historyRepo.(historyVisitDeltaIncrementer); ok { | |
| 267 | 277 | if err := deltaWriter.IncrementVisitCountBy(ctx, record.url, delta); err != nil { | |
| 268 | 278 | log.Warn().Err(err).Str("url", record.url).Int("delta", delta).Msg("failed to increment visit count by delta") | |
| @@ -270,7 +280,16 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||
| 270 | 280 | } | |
| 271 | 281 | return | |
| 272 | 282 | } | |
| 273 | - | for i := 0; i < delta; i++ { | |
| 283 | + | iterations := delta | |
| 284 | + | if iterations > historyFallbackIncrementCap { | |
| 285 | + | log.Warn(). | |
| 286 | + | Str("url", record.url). | |
| 287 | + | Int("delta", delta). | |
| 288 | + | Int("cap", historyFallbackIncrementCap). | |
| 289 | + | Msg("visit count fallback increment capped") | |
| 290 | + | iterations = historyFallbackIncrementCap | |
| 291 | + | } | |
| 292 | + | for i := 0; i < iterations; i++ { | |
| 274 | 293 | if err := uc.historyRepo.IncrementVisitCount(ctx, record.url); err != nil { | |
| 275 | 294 | log.Warn().Err(err).Str("url", record.url).Msg("failed to increment visit count") | |
| 276 | 295 | return | |
| @@ -279,7 +298,7 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||
| 279 | 298 | } else { | |
| 280 | 299 | // Create new entry | |
| 281 | 300 | entry := entity.NewHistoryEntry(record.url, "") | |
| 282 | - | entry.VisitCount = int64(maxInt(1, record.visits)) | |
| 301 | + | entry.VisitCount = int64(max(1, record.visits)) | |
| 283 | 302 | if err := uc.historyRepo.Save(ctx, entry); err != nil { | |
| 284 | 303 | log.Warn().Err(err).Str("url", record.url).Msg("failed to save history") | |
| 285 | 304 | } | |
| @@ -420,9 +439,9 @@ func isHashOnlyTransition(previous, current string) bool { | |||
| 420 | 439 | return prevParsed.Fragment != currParsed.Fragment | |
| 421 | 440 | } | |
| 422 | 441 | ||
| 423 | - | func maxInt(a, b int) int { | |
| 424 | - | if a > b { | |
| 425 | - | return a | |
| 442 | + | func normalizePaneID(paneID string) string { | |
| 443 | + | if strings.TrimSpace(paneID) == "" { | |
| 444 | + | return "__default__" | |
| 426 | 445 | } | |
| 427 | - | return b | |
| 446 | + | return paneID | |
| 428 | 447 | } | |
| @@ -24,6 +24,9 @@ const ( | |||||
| 24 | 24 | ||||
| 25 | // historyWorkerFlushInterval coalesces bursts into fewer persistence writes. | 25 | // historyWorkerFlushInterval coalesces bursts into fewer persistence writes. | ||
| 26 | historyWorkerFlushInterval = 100 * time.Millisecond | 26 | historyWorkerFlushInterval = 100 * time.Millisecond | ||
| 27 | + | ||||
| 28 | + | // historyFallbackIncrementCap bounds DB writes when delta-increment API is unavailable. | |||
| 29 | + | historyFallbackIncrementCap = 256 | |||
| 27 | ) | 30 | ) | ||
| 28 | 31 | ||||
| 29 | // historyRecord holds data for async history recording. | 32 | // historyRecord holds data for async history recording. | ||
| @@ -158,9 +161,7 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str | |||||
| 158 | return | 161 | return | ||
| 159 | } | 162 | } | ||
| 160 | 163 | ||||
| 161 | - | if paneID == "" { | 164 | + | paneID = normalizePaneID(paneID) |
| 162 | - | paneID = "__default__" | |||
| 163 | - | } | |||
| 164 | 165 | ||||
| 165 | now := time.Now() | 166 | now := time.Now() | ||
| 166 | 167 | ||||
| @@ -198,6 +199,15 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str | |||||
| 198 | } | 199 | } | ||
| 199 | } | 200 | } | ||
| 200 | 201 | ||||
| 202 | + | // ClearPaneHistory removes per-pane deduplication state to prevent unbounded growth. | |||
| 203 | + | // Callers should invoke this when a pane is closed. | |||
| 204 | + | func (uc *NavigateUseCase) ClearPaneHistory(paneID string) { | |||
| 205 | + | paneID = normalizePaneID(paneID) | |||
| 206 | + | uc.recentMu.Lock() | |||
| 207 | + | delete(uc.recentVisits, paneID) | |||
| 208 | + | uc.recentMu.Unlock() | |||
| 209 | + | } | |||
| 210 | + | ||||
| 201 | // historyWorker is a background goroutine that processes history records. | 211 | // historyWorker is a background goroutine that processes history records. | ||
| 202 | // It drains the queue and persists records to the database without blocking the UI. | 212 | // It drains the queue and persists records to the database without blocking the UI. | ||
| 203 | func (uc *NavigateUseCase) historyWorker() { | 213 | func (uc *NavigateUseCase) historyWorker() { | ||
| @@ -262,7 +272,7 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||||
| 262 | } | 272 | } | ||
| 263 | 273 | ||||
| 264 | if existing != nil { | 274 | if existing != nil { | ||
| 265 | - | delta := maxInt(1, record.visits) | 275 | + | delta := max(1, record.visits) |
| 266 | if deltaWriter, ok := uc.historyRepo.(historyVisitDeltaIncrementer); ok { | 276 | if deltaWriter, ok := uc.historyRepo.(historyVisitDeltaIncrementer); ok { | ||
| 267 | if err := deltaWriter.IncrementVisitCountBy(ctx, record.url, delta); err != nil { | 277 | if err := deltaWriter.IncrementVisitCountBy(ctx, record.url, delta); err != nil { | ||
| 268 | log.Warn().Err(err).Str("url", record.url).Int("delta", delta).Msg("failed to increment visit count by delta") | 278 | log.Warn().Err(err).Str("url", record.url).Int("delta", delta).Msg("failed to increment visit count by delta") | ||
| @@ -270,7 +280,16 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||||
| 270 | } | 280 | } | ||
| 271 | return | 281 | return | ||
| 272 | } | 282 | } | ||
| 273 | - | for i := 0; i < delta; i++ { | 283 | + | iterations := delta |
| 284 | + | if iterations > historyFallbackIncrementCap { | |||
| 285 | + | log.Warn(). | |||
| 286 | + | Str("url", record.url). | |||
| 287 | + | Int("delta", delta). | |||
| 288 | + | Int("cap", historyFallbackIncrementCap). | |||
| 289 | + | Msg("visit count fallback increment capped") | |||
| 290 | + | iterations = historyFallbackIncrementCap | |||
| 291 | + | } | |||
| 292 | + | for i := 0; i < iterations; i++ { | |||
| 274 | if err := uc.historyRepo.IncrementVisitCount(ctx, record.url); err != nil { | 293 | if err := uc.historyRepo.IncrementVisitCount(ctx, record.url); err != nil { | ||
| 275 | log.Warn().Err(err).Str("url", record.url).Msg("failed to increment visit count") | 294 | log.Warn().Err(err).Str("url", record.url).Msg("failed to increment visit count") | ||
| 276 | return | 295 | return | ||
| @@ -279,7 +298,7 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec | |||||
| 279 | } else { | 298 | } else { | ||
| 280 | // Create new entry | 299 | // Create new entry | ||
| 281 | entry := entity.NewHistoryEntry(record.url, "") | 300 | entry := entity.NewHistoryEntry(record.url, "") | ||
| 282 | - | entry.VisitCount = int64(maxInt(1, record.visits)) | 301 | + | entry.VisitCount = int64(max(1, record.visits)) |
| 283 | if err := uc.historyRepo.Save(ctx, entry); err != nil { | 302 | if err := uc.historyRepo.Save(ctx, entry); err != nil { | ||
| 284 | log.Warn().Err(err).Str("url", record.url).Msg("failed to save history") | 303 | log.Warn().Err(err).Str("url", record.url).Msg("failed to save history") | ||
| 285 | } | 304 | } | ||
| @@ -420,9 +439,9 @@ func isHashOnlyTransition(previous, current string) bool { | |||||
| 420 | return prevParsed.Fragment != currParsed.Fragment | 439 | return prevParsed.Fragment != currParsed.Fragment | ||
| 421 | } | 440 | } | ||
| 422 | 441 | ||||
| 423 | - | func maxInt(a, b int) int { | 442 | + | func normalizePaneID(paneID string) string { |
| 424 | - | if a > b { | 443 | + | if strings.TrimSpace(paneID) == "" { |
| 425 | - | return a | 444 | + | return "__default__" |
| 426 | } | 445 | } | ||
| 427 | - | return b | 446 | + | return paneID |
| 428 | } | 447 | } | ||
M internal/infrastructure/webkit/messaging.go +6 -6
| @@ -47,7 +47,7 @@ type MessageRouter struct { | |||
| 47 | 47 | ||
| 48 | 48 | mu sync.RWMutex | |
| 49 | 49 | callbacks []interface{} | |
| 50 | - | signals []uint32 | |
| 50 | + | signals []uint | |
| 51 | 51 | ||
| 52 | 52 | idMu sync.Mutex | |
| 53 | 53 | syncedIDs map[WebViewID]bool | |
| @@ -121,7 +121,7 @@ func (r *MessageRouter) RegisterHandlerWithCallbacks(msgType, callback, errorCal | |||
| 121 | 121 | // It registers the script message handler in the MAIN world (not isolated). | |
| 122 | 122 | // WebKit's messageHandlers is only available in main world. | |
| 123 | 123 | // The isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler. | |
| 124 | - | func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ string) (uint32, error) { | |
| 124 | + | func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ string) (uint, error) { | |
| 125 | 125 | log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger() | |
| 126 | 126 | ||
| 127 | 127 | log.Debug().Msg("SetupMessageHandler called") | |
| @@ -146,10 +146,10 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st | |||
| 146 | 146 | ||
| 147 | 147 | // Connect to "script-message-received::dumber" with handler name as signal detail | |
| 148 | 148 | signalID := ucm.ConnectScriptMessageReceivedWithDetail(MessageHandlerName, &cb) | |
| 149 | - | log.Debug().Uint32("signal_id", uint32(signalID)).Str("handler", MessageHandlerName).Msg("signal connected with detail") | |
| 149 | + | log.Debug().Uint64("signal_id", uint64(signalID)).Str("handler", MessageHandlerName).Msg("signal connected with detail") | |
| 150 | 150 | ||
| 151 | 151 | r.mu.Lock() | |
| 152 | - | r.signals = append(r.signals, uint32(signalID)) | |
| 152 | + | r.signals = append(r.signals, signalID) | |
| 153 | 153 | r.mu.Unlock() | |
| 154 | 154 | ||
| 155 | 155 | log.Debug().Msg("SetupMessageHandler: registering handler in main world") | |
| @@ -164,10 +164,10 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st | |||
| 164 | 164 | log.Info(). | |
| 165 | 165 | Str("handler", MessageHandlerName). | |
| 166 | 166 | Str("world", "main"). | |
| 167 | - | Uint32("signal_id", uint32(signalID)). | |
| 167 | + | Uint64("signal_id", uint64(signalID)). | |
| 168 | 168 | Msg("script message handler connected") | |
| 169 | 169 | ||
| 170 | - | return uint32(signalID), nil | |
| 170 | + | return signalID, nil | |
| 171 | 171 | } | |
| 172 | 172 | ||
| 173 | 173 | // handleScriptMessage decodes the JSC value and routes it to the correct handler. | |
| @@ -47,7 +47,7 @@ type MessageRouter struct { | |||||
| 47 | 47 | ||||
| 48 | mu sync.RWMutex | 48 | mu sync.RWMutex | ||
| 49 | callbacks []interface{} | 49 | callbacks []interface{} | ||
| 50 | - | signals []uint32 | 50 | + | signals []uint |
| 51 | 51 | ||||
| 52 | idMu sync.Mutex | 52 | idMu sync.Mutex | ||
| 53 | syncedIDs map[WebViewID]bool | 53 | syncedIDs map[WebViewID]bool | ||
| @@ -121,7 +121,7 @@ func (r *MessageRouter) RegisterHandlerWithCallbacks(msgType, callback, errorCal | |||||
| 121 | // It registers the script message handler in the MAIN world (not isolated). | 121 | // It registers the script message handler in the MAIN world (not isolated). | ||
| 122 | // WebKit's messageHandlers is only available in main world. | 122 | // WebKit's messageHandlers is only available in main world. | ||
| 123 | // The isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler. | 123 | // The isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler. | ||
| 124 | - | func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ string) (uint32, error) { | 124 | + | func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ string) (uint, error) { |
| 125 | log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger() | 125 | log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger() | ||
| 126 | 126 | ||||
| 127 | log.Debug().Msg("SetupMessageHandler called") | 127 | log.Debug().Msg("SetupMessageHandler called") | ||
| @@ -146,10 +146,10 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st | |||||
| 146 | 146 | ||||
| 147 | // Connect to "script-message-received::dumber" with handler name as signal detail | 147 | // Connect to "script-message-received::dumber" with handler name as signal detail | ||
| 148 | signalID := ucm.ConnectScriptMessageReceivedWithDetail(MessageHandlerName, &cb) | 148 | signalID := ucm.ConnectScriptMessageReceivedWithDetail(MessageHandlerName, &cb) | ||
| 149 | - | log.Debug().Uint32("signal_id", uint32(signalID)).Str("handler", MessageHandlerName).Msg("signal connected with detail") | 149 | + | log.Debug().Uint64("signal_id", uint64(signalID)).Str("handler", MessageHandlerName).Msg("signal connected with detail") |
| 150 | 150 | ||||
| 151 | r.mu.Lock() | 151 | r.mu.Lock() | ||
| 152 | - | r.signals = append(r.signals, uint32(signalID)) | 152 | + | r.signals = append(r.signals, signalID) |
| 153 | r.mu.Unlock() | 153 | r.mu.Unlock() | ||
| 154 | 154 | ||||
| 155 | log.Debug().Msg("SetupMessageHandler: registering handler in main world") | 155 | log.Debug().Msg("SetupMessageHandler: registering handler in main world") | ||
| @@ -164,10 +164,10 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st | |||||
| 164 | log.Info(). | 164 | log.Info(). | ||
| 165 | Str("handler", MessageHandlerName). | 165 | Str("handler", MessageHandlerName). | ||
| 166 | Str("world", "main"). | 166 | Str("world", "main"). | ||
| 167 | - | Uint32("signal_id", uint32(signalID)). | 167 | + | Uint64("signal_id", uint64(signalID)). |
| 168 | Msg("script message handler connected") | 168 | Msg("script message handler connected") | ||
| 169 | 169 | ||||
| 170 | - | return uint32(signalID), nil | 170 | + | return signalID, nil |
| 171 | } | 171 | } | ||
| 172 | 172 | ||||
| 173 | // handleScriptMessage decodes the JSC value and routes it to the correct handler. | 173 | // handleScriptMessage decodes the JSC value and routes it to the correct handler. | ||
M internal/infrastructure/webkit/webview.go +50 -28
| @@ -136,7 +136,7 @@ type WebView struct { | |||
| 136 | 136 | lastProgressUpdate atomic.Int64 // Unix nanoseconds | |
| 137 | 137 | ||
| 138 | 138 | // Signal handler IDs for disconnection | |
| 139 | - | signalIDs []uint32 | |
| 139 | + | signalIDs []uintptr | |
| 140 | 140 | ||
| 141 | 141 | // Callbacks (set by UI layer) | |
| 142 | 142 | OnLoadChanged func(LoadEvent) | |
| @@ -171,7 +171,7 @@ type WebView struct { | |||
| 171 | 171 | findControllerOnce sync.Once | |
| 172 | 172 | ||
| 173 | 173 | backForwardList *webkit.BackForwardList | |
| 174 | - | backForwardListSignalID uint32 | |
| 174 | + | backForwardListSignalID uint | |
| 175 | 175 | } | |
| 176 | 176 | ||
| 177 | 177 | type runJSErrorStat struct { | |
| @@ -255,42 +255,42 @@ func (a *findControllerAdapter) GetSearchText() string { | |||
| 255 | 255 | return a.fc.GetSearchText() | |
| 256 | 256 | } | |
| 257 | 257 | ||
| 258 | - | func (a *findControllerAdapter) OnFoundText(callback func(matchCount uint)) uint32 { | |
| 258 | + | func (a *findControllerAdapter) OnFoundText(callback func(matchCount uint)) uint { | |
| 259 | 259 | if a == nil || a.fc == nil || callback == nil { | |
| 260 | 260 | return 0 | |
| 261 | 261 | } | |
| 262 | 262 | cb := func(_ webkit.FindController, matchCount uint) { | |
| 263 | 263 | callback(matchCount) | |
| 264 | 264 | } | |
| 265 | - | return uint32(a.fc.ConnectFoundText(&cb)) | |
| 265 | + | return a.fc.ConnectFoundText(&cb) | |
| 266 | 266 | } | |
| 267 | 267 | ||
| 268 | - | func (a *findControllerAdapter) OnFailedToFindText(callback func()) uint32 { | |
| 268 | + | func (a *findControllerAdapter) OnFailedToFindText(callback func()) uint { | |
| 269 | 269 | if a == nil || a.fc == nil || callback == nil { | |
| 270 | 270 | return 0 | |
| 271 | 271 | } | |
| 272 | 272 | cb := func(_ webkit.FindController) { | |
| 273 | 273 | callback() | |
| 274 | 274 | } | |
| 275 | - | return uint32(a.fc.ConnectFailedToFindText(&cb)) | |
| 275 | + | return a.fc.ConnectFailedToFindText(&cb) | |
| 276 | 276 | } | |
| 277 | 277 | ||
| 278 | - | func (a *findControllerAdapter) OnCountedMatches(callback func(matchCount uint)) uint32 { | |
| 278 | + | func (a *findControllerAdapter) OnCountedMatches(callback func(matchCount uint)) uint { | |
| 279 | 279 | if a == nil || a.fc == nil || callback == nil { | |
| 280 | 280 | return 0 | |
| 281 | 281 | } | |
| 282 | 282 | cb := func(_ webkit.FindController, matchCount uint) { | |
| 283 | 283 | callback(matchCount) | |
| 284 | 284 | } | |
| 285 | - | return uint32(a.fc.ConnectCountedMatches(&cb)) | |
| 285 | + | return a.fc.ConnectCountedMatches(&cb) | |
| 286 | 286 | } | |
| 287 | 287 | ||
| 288 | - | func (a *findControllerAdapter) DisconnectSignal(id uint32) { | |
| 288 | + | func (a *findControllerAdapter) DisconnectSignal(id uint) { | |
| 289 | 289 | if a == nil || a.fc == nil || id == 0 { | |
| 290 | 290 | return | |
| 291 | 291 | } | |
| 292 | 292 | obj := gobject.ObjectNewFromInternalPtr(a.fc.GoPointer()) | |
| 293 | - | gobject.SignalHandlerDisconnect(obj, uint(id)) | |
| 293 | + | gobject.SignalHandlerDisconnect(obj, id) | |
| 294 | 294 | } | |
| 295 | 295 | ||
| 296 | 296 | // NewWebView creates a new WebView with the given context and settings. | |
| @@ -324,7 +324,7 @@ func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsMan | |||
| 324 | 324 | inner: inner, | |
| 325 | 325 | ucm: inner.GetUserContentManager(), | |
| 326 | 326 | logger: log.With().Str("component", "webview").Logger(), | |
| 327 | - | signalIDs: make([]uint32, 0, 4), | |
| 327 | + | signalIDs: make([]uintptr, 0, 4), | |
| 328 | 328 | runJSErrorStats: make(map[string]runJSErrorStat), | |
| 329 | 329 | } | |
| 330 | 330 | ||
| @@ -375,7 +375,7 @@ func NewWebViewWithRelated(ctx context.Context, parent *WebView, settings *Setti | |||
| 375 | 375 | isRelated: true, // Shares web process with parent - must not terminate process on destroy | |
| 376 | 376 | ucm: inner.GetUserContentManager(), | |
| 377 | 377 | logger: log.With().Str("component", "webview-popup").Logger(), | |
| 378 | - | signalIDs: make([]uint32, 0, 6), | |
| 378 | + | signalIDs: make([]uintptr, 0, 6), | |
| 379 | 379 | runJSErrorStats: make(map[string]runJSErrorStat), | |
| 380 | 380 | } | |
| 381 | 381 | ||
| @@ -446,7 +446,7 @@ func (wv *WebView) connectLoadChangedSignal() { | |||
| 446 | 446 | } | |
| 447 | 447 | } | |
| 448 | 448 | sigID := wv.inner.ConnectLoadChanged(&loadChangedCb) | |
| 449 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 449 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 450 | 450 | } | |
| 451 | 451 | ||
| 452 | 452 | func (wv *WebView) connectCloseSignal() { | |
| @@ -456,7 +456,7 @@ func (wv *WebView) connectCloseSignal() { | |||
| 456 | 456 | } | |
| 457 | 457 | } | |
| 458 | 458 | sigID := wv.inner.ConnectClose(&closeCb) | |
| 459 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 459 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 460 | 460 | } | |
| 461 | 461 | ||
| 462 | 462 | func (wv *WebView) connectCreateSignal() { | |
| @@ -498,7 +498,7 @@ func (wv *WebView) connectCreateSignal() { | |||
| 498 | 498 | return newWV.inner.Widget | |
| 499 | 499 | } | |
| 500 | 500 | sigID := wv.inner.ConnectCreate(&createCb) | |
| 501 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 501 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 502 | 502 | } | |
| 503 | 503 | ||
| 504 | 504 | func (wv *WebView) connectReadyToShowSignal() { | |
| @@ -508,7 +508,7 @@ func (wv *WebView) connectReadyToShowSignal() { | |||
| 508 | 508 | } | |
| 509 | 509 | } | |
| 510 | 510 | sigID := wv.inner.ConnectReadyToShow(&readyToShowCb) | |
| 511 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 511 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 512 | 512 | } | |
| 513 | 513 | ||
| 514 | 514 | func (wv *WebView) connectTitleSignal() { | |
| @@ -523,7 +523,7 @@ func (wv *WebView) connectTitleSignal() { | |||
| 523 | 523 | } | |
| 524 | 524 | } | |
| 525 | 525 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::title", glib.NewCallback(&titleCb)) | |
| 526 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 526 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 527 | 527 | } | |
| 528 | 528 | ||
| 529 | 529 | func (wv *WebView) connectURISignal() { | |
| @@ -539,7 +539,7 @@ func (wv *WebView) connectURISignal() { | |||
| 539 | 539 | } | |
| 540 | 540 | } | |
| 541 | 541 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::uri", glib.NewCallback(&uriCb)) | |
| 542 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 542 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 543 | 543 | } | |
| 544 | 544 | ||
| 545 | 545 | func (wv *WebView) connectFaviconSignal() { | |
| @@ -550,7 +550,7 @@ func (wv *WebView) connectFaviconSignal() { | |||
| 550 | 550 | } | |
| 551 | 551 | } | |
| 552 | 552 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::favicon", glib.NewCallback(&faviconCb)) | |
| 553 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 553 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 554 | 554 | } | |
| 555 | 555 | ||
| 556 | 556 | // progressThrottleInterval limits progress callbacks to ~60fps to reduce UI overhead. | |
| @@ -580,7 +580,7 @@ func (wv *WebView) connectProgressSignal() { | |||
| 580 | 580 | } | |
| 581 | 581 | } | |
| 582 | 582 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::estimated-load-progress", glib.NewCallback(&progressCb)) | |
| 583 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 583 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 584 | 584 | } | |
| 585 | 585 | ||
| 586 | 586 | func (wv *WebView) connectDecidePolicySignal() { | |
| @@ -596,7 +596,7 @@ func (wv *WebView) connectDecidePolicySignal() { | |||
| 596 | 596 | } | |
| 597 | 597 | } | |
| 598 | 598 | sigID := wv.inner.ConnectDecidePolicy(&decidePolicyCb) | |
| 599 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 599 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 600 | 600 | } | |
| 601 | 601 | ||
| 602 | 602 | // handleResponsePolicyDecision handles response policy decisions (e.g., forcing downloads). | |
| @@ -795,7 +795,7 @@ func (wv *WebView) connectEnterFullscreenSignal() { | |||
| 795 | 795 | return false // Allow fullscreen | |
| 796 | 796 | } | |
| 797 | 797 | sigID := wv.inner.ConnectEnterFullscreen(&enterFullscreenCb) | |
| 798 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 798 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 799 | 799 | } | |
| 800 | 800 | ||
| 801 | 801 | func (wv *WebView) connectLeaveFullscreenSignal() { | |
| @@ -808,7 +808,7 @@ func (wv *WebView) connectLeaveFullscreenSignal() { | |||
| 808 | 808 | return false // Allow leaving fullscreen | |
| 809 | 809 | } | |
| 810 | 810 | sigID := wv.inner.ConnectLeaveFullscreen(&leaveFullscreenCb) | |
| 811 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 811 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 812 | 812 | } | |
| 813 | 813 | ||
| 814 | 814 | func (wv *WebView) connectAudioStateSignal() { | |
| @@ -826,7 +826,7 @@ func (wv *WebView) connectAudioStateSignal() { | |||
| 826 | 826 | } | |
| 827 | 827 | } | |
| 828 | 828 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::is-playing-audio", glib.NewCallback(&audioCb)) | |
| 829 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 829 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 830 | 830 | } | |
| 831 | 831 | ||
| 832 | 832 | func (wv *WebView) connectMouseTargetChangedSignal() { | |
| @@ -854,7 +854,7 @@ func (wv *WebView) connectMouseTargetChangedSignal() { | |||
| 854 | 854 | wv.OnLinkHover(uri) | |
| 855 | 855 | } | |
| 856 | 856 | sigID := wv.inner.ConnectMouseTargetChanged(&mouseTargetCb) | |
| 857 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 857 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 858 | 858 | } | |
| 859 | 859 | ||
| 860 | 860 | func (wv *WebView) connectBackForwardListChangedSignal() { | |
| @@ -882,7 +882,7 @@ func (wv *WebView) connectBackForwardListChangedSignal() { | |||
| 882 | 882 | } | |
| 883 | 883 | sigID := backForwardList.ConnectChanged(&changedCb) | |
| 884 | 884 | wv.backForwardList = backForwardList | |
| 885 | - | wv.backForwardListSignalID = uint32(sigID) | |
| 885 | + | wv.backForwardListSignalID = sigID | |
| 886 | 886 | } | |
| 887 | 887 | ||
| 888 | 888 | func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason) string { | |
| @@ -898,6 +898,19 @@ func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason | |||
| 898 | 898 | } | |
| 899 | 899 | } | |
| 900 | 900 | ||
| 901 | + | func mapWebProcessTerminationReason(reason webkit.WebProcessTerminationReason) port.WebProcessTerminationReason { | |
| 902 | + | switch reason { | |
| 903 | + | case webkit.WebProcessCrashedValue: | |
| 904 | + | return port.WebProcessTerminationCrashed | |
| 905 | + | case webkit.WebProcessExceededMemoryLimitValue: | |
| 906 | + | return port.WebProcessTerminationExceededMemory | |
| 907 | + | case webkit.WebProcessTerminatedByApiValue: | |
| 908 | + | return port.WebProcessTerminationByAPI | |
| 909 | + | default: | |
| 910 | + | return port.WebProcessTerminationUnknown | |
| 911 | + | } | |
| 912 | + | } | |
| 913 | + | ||
| 901 | 914 | func (wv *WebView) connectWebProcessTerminatedSignal() { | |
| 902 | 915 | terminatedCb := func(_ webkit.WebView, reason webkit.WebProcessTerminationReason) { | |
| 903 | 916 | uri := wv.URI() | |
| @@ -914,7 +927,7 @@ func (wv *WebView) connectWebProcessTerminatedSignal() { | |||
| 914 | 927 | } | |
| 915 | 928 | } | |
| 916 | 929 | sigID := wv.inner.ConnectWebProcessTerminated(&terminatedCb) | |
| 917 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 930 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 918 | 931 | } | |
| 919 | 932 | ||
| 920 | 933 | // ID returns the unique identifier for this WebView. | |
| @@ -1199,6 +1212,8 @@ func (wv *WebView) SetCallbacks(callbacks *port.WebViewCallbacks) { | |||
| 1199 | 1212 | wv.OnFaviconChanged = nil | |
| 1200 | 1213 | wv.OnClose = nil | |
| 1201 | 1214 | wv.OnCreate = nil | |
| 1215 | + | wv.OnLinkHover = nil | |
| 1216 | + | wv.OnWebProcessTerminated = nil | |
| 1202 | 1217 | return | |
| 1203 | 1218 | } | |
| 1204 | 1219 | ||
| @@ -1238,6 +1253,13 @@ func (wv *WebView) SetCallbacks(callbacks *port.WebViewCallbacks) { | |||
| 1238 | 1253 | } | |
| 1239 | 1254 | } | |
| 1240 | 1255 | wv.OnLinkHover = callbacks.OnLinkHover | |
| 1256 | + | if callbacks.OnWebProcessTerminated != nil { | |
| 1257 | + | wv.OnWebProcessTerminated = func(reason webkit.WebProcessTerminationReason, reasonLabel string, uri string) { | |
| 1258 | + | callbacks.OnWebProcessTerminated(mapWebProcessTerminationReason(reason), reasonLabel, uri) | |
| 1259 | + | } | |
| 1260 | + | } else { | |
| 1261 | + | wv.OnWebProcessTerminated = nil | |
| 1262 | + | } | |
| 1241 | 1263 | } | |
| 1242 | 1264 | ||
| 1243 | 1265 | // ShowDevTools opens the WebKit inspector/developer tools. | |
| @@ -1311,7 +1333,7 @@ func (wv *WebView) DisconnectSignals() { | |||
| 1311 | 1333 | ||
| 1312 | 1334 | if wv.backForwardList != nil && wv.backForwardListSignalID != 0 { | |
| 1313 | 1335 | bfObj := gobject.ObjectNewFromInternalPtr(wv.backForwardList.GoPointer()) | |
| 1314 | - | gobject.SignalHandlerDisconnect(bfObj, uint(wv.backForwardListSignalID)) | |
| 1336 | + | gobject.SignalHandlerDisconnect(bfObj, wv.backForwardListSignalID) | |
| 1315 | 1337 | wv.backForwardListSignalID = 0 | |
| 1316 | 1338 | wv.backForwardList = nil | |
| 1317 | 1339 | } | |
| @@ -136,7 +136,7 @@ type WebView struct { | |||||
| 136 | lastProgressUpdate atomic.Int64 // Unix nanoseconds | 136 | lastProgressUpdate atomic.Int64 // Unix nanoseconds | ||
| 137 | 137 | ||||
| 138 | // Signal handler IDs for disconnection | 138 | // Signal handler IDs for disconnection | ||
| 139 | - | signalIDs []uint32 | 139 | + | signalIDs []uintptr |
| 140 | 140 | ||||
| 141 | // Callbacks (set by UI layer) | 141 | // Callbacks (set by UI layer) | ||
| 142 | OnLoadChanged func(LoadEvent) | 142 | OnLoadChanged func(LoadEvent) | ||
| @@ -171,7 +171,7 @@ type WebView struct { | |||||
| 171 | findControllerOnce sync.Once | 171 | findControllerOnce sync.Once | ||
| 172 | 172 | ||||
| 173 | backForwardList *webkit.BackForwardList | 173 | backForwardList *webkit.BackForwardList | ||
| 174 | - | backForwardListSignalID uint32 | 174 | + | backForwardListSignalID uint |
| 175 | } | 175 | } | ||
| 176 | 176 | ||||
| 177 | type runJSErrorStat struct { | 177 | type runJSErrorStat struct { | ||
| @@ -255,42 +255,42 @@ func (a *findControllerAdapter) GetSearchText() string { | |||||
| 255 | return a.fc.GetSearchText() | 255 | return a.fc.GetSearchText() | ||
| 256 | } | 256 | } | ||
| 257 | 257 | ||||
| 258 | - | func (a *findControllerAdapter) OnFoundText(callback func(matchCount uint)) uint32 { | 258 | + | func (a *findControllerAdapter) OnFoundText(callback func(matchCount uint)) uint { |
| 259 | if a == nil || a.fc == nil || callback == nil { | 259 | if a == nil || a.fc == nil || callback == nil { | ||
| 260 | return 0 | 260 | return 0 | ||
| 261 | } | 261 | } | ||
| 262 | cb := func(_ webkit.FindController, matchCount uint) { | 262 | cb := func(_ webkit.FindController, matchCount uint) { | ||
| 263 | callback(matchCount) | 263 | callback(matchCount) | ||
| 264 | } | 264 | } | ||
| 265 | - | return uint32(a.fc.ConnectFoundText(&cb)) | 265 | + | return a.fc.ConnectFoundText(&cb) |
| 266 | } | 266 | } | ||
| 267 | 267 | ||||
| 268 | - | func (a *findControllerAdapter) OnFailedToFindText(callback func()) uint32 { | 268 | + | func (a *findControllerAdapter) OnFailedToFindText(callback func()) uint { |
| 269 | if a == nil || a.fc == nil || callback == nil { | 269 | if a == nil || a.fc == nil || callback == nil { | ||
| 270 | return 0 | 270 | return 0 | ||
| 271 | } | 271 | } | ||
| 272 | cb := func(_ webkit.FindController) { | 272 | cb := func(_ webkit.FindController) { | ||
| 273 | callback() | 273 | callback() | ||
| 274 | } | 274 | } | ||
| 275 | - | return uint32(a.fc.ConnectFailedToFindText(&cb)) | 275 | + | return a.fc.ConnectFailedToFindText(&cb) |
| 276 | } | 276 | } | ||
| 277 | 277 | ||||
| 278 | - | func (a *findControllerAdapter) OnCountedMatches(callback func(matchCount uint)) uint32 { | 278 | + | func (a *findControllerAdapter) OnCountedMatches(callback func(matchCount uint)) uint { |
| 279 | if a == nil || a.fc == nil || callback == nil { | 279 | if a == nil || a.fc == nil || callback == nil { | ||
| 280 | return 0 | 280 | return 0 | ||
| 281 | } | 281 | } | ||
| 282 | cb := func(_ webkit.FindController, matchCount uint) { | 282 | cb := func(_ webkit.FindController, matchCount uint) { | ||
| 283 | callback(matchCount) | 283 | callback(matchCount) | ||
| 284 | } | 284 | } | ||
| 285 | - | return uint32(a.fc.ConnectCountedMatches(&cb)) | 285 | + | return a.fc.ConnectCountedMatches(&cb) |
| 286 | } | 286 | } | ||
| 287 | 287 | ||||
| 288 | - | func (a *findControllerAdapter) DisconnectSignal(id uint32) { | 288 | + | func (a *findControllerAdapter) DisconnectSignal(id uint) { |
| 289 | if a == nil || a.fc == nil || id == 0 { | 289 | if a == nil || a.fc == nil || id == 0 { | ||
| 290 | return | 290 | return | ||
| 291 | } | 291 | } | ||
| 292 | obj := gobject.ObjectNewFromInternalPtr(a.fc.GoPointer()) | 292 | obj := gobject.ObjectNewFromInternalPtr(a.fc.GoPointer()) | ||
| 293 | - | gobject.SignalHandlerDisconnect(obj, uint(id)) | 293 | + | gobject.SignalHandlerDisconnect(obj, id) |
| 294 | } | 294 | } | ||
| 295 | 295 | ||||
| 296 | // NewWebView creates a new WebView with the given context and settings. | 296 | // NewWebView creates a new WebView with the given context and settings. | ||
| @@ -324,7 +324,7 @@ func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsMan | |||||
| 324 | inner: inner, | 324 | inner: inner, | ||
| 325 | ucm: inner.GetUserContentManager(), | 325 | ucm: inner.GetUserContentManager(), | ||
| 326 | logger: log.With().Str("component", "webview").Logger(), | 326 | logger: log.With().Str("component", "webview").Logger(), | ||
| 327 | - | signalIDs: make([]uint32, 0, 4), | 327 | + | signalIDs: make([]uintptr, 0, 4), |
| 328 | runJSErrorStats: make(map[string]runJSErrorStat), | 328 | runJSErrorStats: make(map[string]runJSErrorStat), | ||
| 329 | } | 329 | } | ||
| 330 | 330 | ||||
| @@ -375,7 +375,7 @@ func NewWebViewWithRelated(ctx context.Context, parent *WebView, settings *Setti | |||||
| 375 | isRelated: true, // Shares web process with parent - must not terminate process on destroy | 375 | isRelated: true, // Shares web process with parent - must not terminate process on destroy | ||
| 376 | ucm: inner.GetUserContentManager(), | 376 | ucm: inner.GetUserContentManager(), | ||
| 377 | logger: log.With().Str("component", "webview-popup").Logger(), | 377 | logger: log.With().Str("component", "webview-popup").Logger(), | ||
| 378 | - | signalIDs: make([]uint32, 0, 6), | 378 | + | signalIDs: make([]uintptr, 0, 6), |
| 379 | runJSErrorStats: make(map[string]runJSErrorStat), | 379 | runJSErrorStats: make(map[string]runJSErrorStat), | ||
| 380 | } | 380 | } | ||
| 381 | 381 | ||||
| @@ -446,7 +446,7 @@ func (wv *WebView) connectLoadChangedSignal() { | |||||
| 446 | } | 446 | } | ||
| 447 | } | 447 | } | ||
| 448 | sigID := wv.inner.ConnectLoadChanged(&loadChangedCb) | 448 | sigID := wv.inner.ConnectLoadChanged(&loadChangedCb) | ||
| 449 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 449 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 450 | } | 450 | } | ||
| 451 | 451 | ||||
| 452 | func (wv *WebView) connectCloseSignal() { | 452 | func (wv *WebView) connectCloseSignal() { | ||
| @@ -456,7 +456,7 @@ func (wv *WebView) connectCloseSignal() { | |||||
| 456 | } | 456 | } | ||
| 457 | } | 457 | } | ||
| 458 | sigID := wv.inner.ConnectClose(&closeCb) | 458 | sigID := wv.inner.ConnectClose(&closeCb) | ||
| 459 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 459 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 460 | } | 460 | } | ||
| 461 | 461 | ||||
| 462 | func (wv *WebView) connectCreateSignal() { | 462 | func (wv *WebView) connectCreateSignal() { | ||
| @@ -498,7 +498,7 @@ func (wv *WebView) connectCreateSignal() { | |||||
| 498 | return newWV.inner.Widget | 498 | return newWV.inner.Widget | ||
| 499 | } | 499 | } | ||
| 500 | sigID := wv.inner.ConnectCreate(&createCb) | 500 | sigID := wv.inner.ConnectCreate(&createCb) | ||
| 501 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 501 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 502 | } | 502 | } | ||
| 503 | 503 | ||||
| 504 | func (wv *WebView) connectReadyToShowSignal() { | 504 | func (wv *WebView) connectReadyToShowSignal() { | ||
| @@ -508,7 +508,7 @@ func (wv *WebView) connectReadyToShowSignal() { | |||||
| 508 | } | 508 | } | ||
| 509 | } | 509 | } | ||
| 510 | sigID := wv.inner.ConnectReadyToShow(&readyToShowCb) | 510 | sigID := wv.inner.ConnectReadyToShow(&readyToShowCb) | ||
| 511 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 511 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 512 | } | 512 | } | ||
| 513 | 513 | ||||
| 514 | func (wv *WebView) connectTitleSignal() { | 514 | func (wv *WebView) connectTitleSignal() { | ||
| @@ -523,7 +523,7 @@ func (wv *WebView) connectTitleSignal() { | |||||
| 523 | } | 523 | } | ||
| 524 | } | 524 | } | ||
| 525 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::title", glib.NewCallback(&titleCb)) | 525 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::title", glib.NewCallback(&titleCb)) | ||
| 526 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 526 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 527 | } | 527 | } | ||
| 528 | 528 | ||||
| 529 | func (wv *WebView) connectURISignal() { | 529 | func (wv *WebView) connectURISignal() { | ||
| @@ -539,7 +539,7 @@ func (wv *WebView) connectURISignal() { | |||||
| 539 | } | 539 | } | ||
| 540 | } | 540 | } | ||
| 541 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::uri", glib.NewCallback(&uriCb)) | 541 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::uri", glib.NewCallback(&uriCb)) | ||
| 542 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 542 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 543 | } | 543 | } | ||
| 544 | 544 | ||||
| 545 | func (wv *WebView) connectFaviconSignal() { | 545 | func (wv *WebView) connectFaviconSignal() { | ||
| @@ -550,7 +550,7 @@ func (wv *WebView) connectFaviconSignal() { | |||||
| 550 | } | 550 | } | ||
| 551 | } | 551 | } | ||
| 552 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::favicon", glib.NewCallback(&faviconCb)) | 552 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::favicon", glib.NewCallback(&faviconCb)) | ||
| 553 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 553 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 554 | } | 554 | } | ||
| 555 | 555 | ||||
| 556 | // progressThrottleInterval limits progress callbacks to ~60fps to reduce UI overhead. | 556 | // progressThrottleInterval limits progress callbacks to ~60fps to reduce UI overhead. | ||
| @@ -580,7 +580,7 @@ func (wv *WebView) connectProgressSignal() { | |||||
| 580 | } | 580 | } | ||
| 581 | } | 581 | } | ||
| 582 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::estimated-load-progress", glib.NewCallback(&progressCb)) | 582 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::estimated-load-progress", glib.NewCallback(&progressCb)) | ||
| 583 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 583 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 584 | } | 584 | } | ||
| 585 | 585 | ||||
| 586 | func (wv *WebView) connectDecidePolicySignal() { | 586 | func (wv *WebView) connectDecidePolicySignal() { | ||
| @@ -596,7 +596,7 @@ func (wv *WebView) connectDecidePolicySignal() { | |||||
| 596 | } | 596 | } | ||
| 597 | } | 597 | } | ||
| 598 | sigID := wv.inner.ConnectDecidePolicy(&decidePolicyCb) | 598 | sigID := wv.inner.ConnectDecidePolicy(&decidePolicyCb) | ||
| 599 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 599 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 600 | } | 600 | } | ||
| 601 | 601 | ||||
| 602 | // handleResponsePolicyDecision handles response policy decisions (e.g., forcing downloads). | 602 | // handleResponsePolicyDecision handles response policy decisions (e.g., forcing downloads). | ||
| @@ -795,7 +795,7 @@ func (wv *WebView) connectEnterFullscreenSignal() { | |||||
| 795 | return false // Allow fullscreen | 795 | return false // Allow fullscreen | ||
| 796 | } | 796 | } | ||
| 797 | sigID := wv.inner.ConnectEnterFullscreen(&enterFullscreenCb) | 797 | sigID := wv.inner.ConnectEnterFullscreen(&enterFullscreenCb) | ||
| 798 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 798 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 799 | } | 799 | } | ||
| 800 | 800 | ||||
| 801 | func (wv *WebView) connectLeaveFullscreenSignal() { | 801 | func (wv *WebView) connectLeaveFullscreenSignal() { | ||
| @@ -808,7 +808,7 @@ func (wv *WebView) connectLeaveFullscreenSignal() { | |||||
| 808 | return false // Allow leaving fullscreen | 808 | return false // Allow leaving fullscreen | ||
| 809 | } | 809 | } | ||
| 810 | sigID := wv.inner.ConnectLeaveFullscreen(&leaveFullscreenCb) | 810 | sigID := wv.inner.ConnectLeaveFullscreen(&leaveFullscreenCb) | ||
| 811 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 811 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 812 | } | 812 | } | ||
| 813 | 813 | ||||
| 814 | func (wv *WebView) connectAudioStateSignal() { | 814 | func (wv *WebView) connectAudioStateSignal() { | ||
| @@ -826,7 +826,7 @@ func (wv *WebView) connectAudioStateSignal() { | |||||
| 826 | } | 826 | } | ||
| 827 | } | 827 | } | ||
| 828 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::is-playing-audio", glib.NewCallback(&audioCb)) | 828 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::is-playing-audio", glib.NewCallback(&audioCb)) | ||
| 829 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 829 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 830 | } | 830 | } | ||
| 831 | 831 | ||||
| 832 | func (wv *WebView) connectMouseTargetChangedSignal() { | 832 | func (wv *WebView) connectMouseTargetChangedSignal() { | ||
| @@ -854,7 +854,7 @@ func (wv *WebView) connectMouseTargetChangedSignal() { | |||||
| 854 | wv.OnLinkHover(uri) | 854 | wv.OnLinkHover(uri) | ||
| 855 | } | 855 | } | ||
| 856 | sigID := wv.inner.ConnectMouseTargetChanged(&mouseTargetCb) | 856 | sigID := wv.inner.ConnectMouseTargetChanged(&mouseTargetCb) | ||
| 857 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 857 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 858 | } | 858 | } | ||
| 859 | 859 | ||||
| 860 | func (wv *WebView) connectBackForwardListChangedSignal() { | 860 | func (wv *WebView) connectBackForwardListChangedSignal() { | ||
| @@ -882,7 +882,7 @@ func (wv *WebView) connectBackForwardListChangedSignal() { | |||||
| 882 | } | 882 | } | ||
| 883 | sigID := backForwardList.ConnectChanged(&changedCb) | 883 | sigID := backForwardList.ConnectChanged(&changedCb) | ||
| 884 | wv.backForwardList = backForwardList | 884 | wv.backForwardList = backForwardList | ||
| 885 | - | wv.backForwardListSignalID = uint32(sigID) | 885 | + | wv.backForwardListSignalID = sigID |
| 886 | } | 886 | } | ||
| 887 | 887 | ||||
| 888 | func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason) string { | 888 | func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason) string { | ||
| @@ -898,6 +898,19 @@ func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason | |||||
| 898 | } | 898 | } | ||
| 899 | } | 899 | } | ||
| 900 | 900 | ||||
| 901 | + | func mapWebProcessTerminationReason(reason webkit.WebProcessTerminationReason) port.WebProcessTerminationReason { | |||
| 902 | + | switch reason { | |||
| 903 | + | case webkit.WebProcessCrashedValue: | |||
| 904 | + | return port.WebProcessTerminationCrashed | |||
| 905 | + | case webkit.WebProcessExceededMemoryLimitValue: | |||
| 906 | + | return port.WebProcessTerminationExceededMemory | |||
| 907 | + | case webkit.WebProcessTerminatedByApiValue: | |||
| 908 | + | return port.WebProcessTerminationByAPI | |||
| 909 | + | default: | |||
| 910 | + | return port.WebProcessTerminationUnknown | |||
| 911 | + | } | |||
| 912 | + | } | |||
| 913 | + | ||||
| 901 | func (wv *WebView) connectWebProcessTerminatedSignal() { | 914 | func (wv *WebView) connectWebProcessTerminatedSignal() { | ||
| 902 | terminatedCb := func(_ webkit.WebView, reason webkit.WebProcessTerminationReason) { | 915 | terminatedCb := func(_ webkit.WebView, reason webkit.WebProcessTerminationReason) { | ||
| 903 | uri := wv.URI() | 916 | uri := wv.URI() | ||
| @@ -914,7 +927,7 @@ func (wv *WebView) connectWebProcessTerminatedSignal() { | |||||
| 914 | } | 927 | } | ||
| 915 | } | 928 | } | ||
| 916 | sigID := wv.inner.ConnectWebProcessTerminated(&terminatedCb) | 929 | sigID := wv.inner.ConnectWebProcessTerminated(&terminatedCb) | ||
| 917 | - | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | 930 | + | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) |
| 918 | } | 931 | } | ||
| 919 | 932 | ||||
| 920 | // ID returns the unique identifier for this WebView. | 933 | // ID returns the unique identifier for this WebView. | ||
| @@ -1199,6 +1212,8 @@ func (wv *WebView) SetCallbacks(callbacks *port.WebViewCallbacks) { | |||||
| 1199 | wv.OnFaviconChanged = nil | 1212 | wv.OnFaviconChanged = nil | ||
| 1200 | wv.OnClose = nil | 1213 | wv.OnClose = nil | ||
| 1201 | wv.OnCreate = nil | 1214 | wv.OnCreate = nil | ||
| 1215 | + | wv.OnLinkHover = nil | |||
| 1216 | + | wv.OnWebProcessTerminated = nil | |||
| 1202 | return | 1217 | return | ||
| 1203 | } | 1218 | } | ||
| 1204 | 1219 | ||||
| @@ -1238,6 +1253,13 @@ func (wv *WebView) SetCallbacks(callbacks *port.WebViewCallbacks) { | |||||
| 1238 | } | 1253 | } | ||
| 1239 | } | 1254 | } | ||
| 1240 | wv.OnLinkHover = callbacks.OnLinkHover | 1255 | wv.OnLinkHover = callbacks.OnLinkHover | ||
| 1256 | + | if callbacks.OnWebProcessTerminated != nil { | |||
| 1257 | + | wv.OnWebProcessTerminated = func(reason webkit.WebProcessTerminationReason, reasonLabel string, uri string) { | |||
| 1258 | + | callbacks.OnWebProcessTerminated(mapWebProcessTerminationReason(reason), reasonLabel, uri) | |||
| 1259 | + | } | |||
| 1260 | + | } else { | |||
| 1261 | + | wv.OnWebProcessTerminated = nil | |||
| 1262 | + | } | |||
| 1241 | } | 1263 | } | ||
| 1242 | 1264 | ||||
| 1243 | // ShowDevTools opens the WebKit inspector/developer tools. | 1265 | // ShowDevTools opens the WebKit inspector/developer tools. | ||
| @@ -1311,7 +1333,7 @@ func (wv *WebView) DisconnectSignals() { | |||||
| 1311 | 1333 | ||||
| 1312 | if wv.backForwardList != nil && wv.backForwardListSignalID != 0 { | 1334 | if wv.backForwardList != nil && wv.backForwardListSignalID != 0 { | ||
| 1313 | bfObj := gobject.ObjectNewFromInternalPtr(wv.backForwardList.GoPointer()) | 1335 | bfObj := gobject.ObjectNewFromInternalPtr(wv.backForwardList.GoPointer()) | ||
| 1314 | - | gobject.SignalHandlerDisconnect(bfObj, uint(wv.backForwardListSignalID)) | 1336 | + | gobject.SignalHandlerDisconnect(bfObj, wv.backForwardListSignalID) |
| 1315 | wv.backForwardListSignalID = 0 | 1337 | wv.backForwardListSignalID = 0 | ||
| 1316 | wv.backForwardList = nil | 1338 | wv.backForwardList = nil | ||
| 1317 | } | 1339 | } | ||
M internal/ui/app.go +3 -0
| @@ -1393,6 +1393,9 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 1393 | 1393 | a.deps.NavigateUC, | |
| 1394 | 1394 | a.contentCoord, | |
| 1395 | 1395 | ) | |
| 1396 | + | a.wsCoord.SetOnPaneClosed(func(paneID entity.PaneID) { | |
| 1397 | + | a.navCoord.ClearPaneHistory(paneID) | |
| 1398 | + | }) | |
| 1396 | 1399 | ||
| 1397 | 1400 | // Wire title updates to history persistence | |
| 1398 | 1401 | a.contentCoord.SetOnTitleUpdated(func(ctx context.Context, paneID entity.PaneID, url, title string) { | |
| @@ -1393,6 +1393,9 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 1393 | a.deps.NavigateUC, | 1393 | a.deps.NavigateUC, | ||
| 1394 | a.contentCoord, | 1394 | a.contentCoord, | ||
| 1395 | ) | 1395 | ) | ||
| 1396 | + | a.wsCoord.SetOnPaneClosed(func(paneID entity.PaneID) { | |||
| 1397 | + | a.navCoord.ClearPaneHistory(paneID) | |||
| 1398 | + | }) | |||
| 1396 | 1399 | ||||
| 1397 | // Wire title updates to history persistence | 1400 | // Wire title updates to history persistence | ||
| 1398 | a.contentCoord.SetOnTitleUpdated(func(ctx context.Context, paneID entity.PaneID, url, title string) { | 1401 | a.contentCoord.SetOnTitleUpdated(func(ctx context.Context, paneID entity.PaneID, url, title string) { | ||
M internal/ui/component/omnibox.go +1 -1
| @@ -850,7 +850,7 @@ func isInlineGhostSelection(realInput, ghostFullText string, selectionStart, sel | |||
| 850 | 850 | } | |
| 851 | 851 | prefixRunes := utf8.RuneCountInString(realInput) | |
| 852 | 852 | fullRunes := utf8.RuneCountInString(ghostFullText) | |
| 853 | - | if prefixRunes < 0 || fullRunes <= prefixRunes { | |
| 853 | + | if fullRunes <= prefixRunes { | |
| 854 | 854 | return false | |
| 855 | 855 | } | |
| 856 | 856 | return selectionStart == prefixRunes && selectionEnd == fullRunes | |
| @@ -850,7 +850,7 @@ func isInlineGhostSelection(realInput, ghostFullText string, selectionStart, sel | |||||
| 850 | } | 850 | } | ||
| 851 | prefixRunes := utf8.RuneCountInString(realInput) | 851 | prefixRunes := utf8.RuneCountInString(realInput) | ||
| 852 | fullRunes := utf8.RuneCountInString(ghostFullText) | 852 | fullRunes := utf8.RuneCountInString(ghostFullText) | ||
| 853 | - | if prefixRunes < 0 || fullRunes <= prefixRunes { | 853 | + | if fullRunes <= prefixRunes { |
| 854 | return false | 854 | return false | ||
| 855 | } | 855 | } | ||
| 856 | return selectionStart == prefixRunes && selectionEnd == fullRunes | 856 | return selectionStart == prefixRunes && selectionEnd == fullRunes | ||
M internal/ui/coordinator/navigation.go +8 -0
| @@ -236,6 +236,14 @@ func (c *NavigationCoordinator) RecordHistory(ctx context.Context, paneID entity | |||
| 236 | 236 | c.navigateUC.RecordHistory(ctx, string(paneID), url) | |
| 237 | 237 | } | |
| 238 | 238 | ||
| 239 | + | // ClearPaneHistory clears per-pane navigation history deduplication state. | |
| 240 | + | func (c *NavigationCoordinator) ClearPaneHistory(paneID entity.PaneID) { | |
| 241 | + | if c.navigateUC == nil { | |
| 242 | + | return | |
| 243 | + | } | |
| 244 | + | c.navigateUC.ClearPaneHistory(string(paneID)) | |
| 245 | + | } | |
| 246 | + | ||
| 239 | 247 | // ActiveWebView returns the WebView for the active pane (for zoom operations). | |
| 240 | 248 | func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebView { | |
| 241 | 249 | return c.contentCoord.ActiveWebView(ctx) | |
| @@ -236,6 +236,14 @@ func (c *NavigationCoordinator) RecordHistory(ctx context.Context, paneID entity | |||||
| 236 | c.navigateUC.RecordHistory(ctx, string(paneID), url) | 236 | c.navigateUC.RecordHistory(ctx, string(paneID), url) | ||
| 237 | } | 237 | } | ||
| 238 | 238 | ||||
| 239 | + | // ClearPaneHistory clears per-pane navigation history deduplication state. | |||
| 240 | + | func (c *NavigationCoordinator) ClearPaneHistory(paneID entity.PaneID) { | |||
| 241 | + | if c.navigateUC == nil { | |||
| 242 | + | return | |||
| 243 | + | } | |||
| 244 | + | c.navigateUC.ClearPaneHistory(string(paneID)) | |||
| 245 | + | } | |||
| 246 | + | ||||
| 239 | // ActiveWebView returns the WebView for the active pane (for zoom operations). | 247 | // ActiveWebView returns the WebView for the active pane (for zoom operations). | ||
| 240 | func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebView { | 248 | func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebView { | ||
| 241 | return c.contentCoord.ActiveWebView(ctx) | 249 | return c.contentCoord.ActiveWebView(ctx) | ||
M internal/ui/coordinator/workspace.go +12 -0
| @@ -35,6 +35,7 @@ type WorkspaceCoordinator struct { | |||
| 35 | 35 | onCloseLastPane func(ctx context.Context) error | |
| 36 | 36 | onCreatePopupTab func(ctx context.Context, input InsertPopupInput) error // For tabbed popup behavior | |
| 37 | 37 | onStateChanged func() // For session snapshots | |
| 38 | + | onPaneClosed func(paneID entity.PaneID) // For pane-specific cleanup hooks | |
| 38 | 39 | } | |
| 39 | 40 | ||
| 40 | 41 | // WorkspaceCoordinatorConfig holds configuration for WorkspaceCoordinator. | |
| @@ -100,6 +101,11 @@ func (c *WorkspaceCoordinator) SetOnStateChanged(fn func()) { | |||
| 100 | 101 | c.onStateChanged = fn | |
| 101 | 102 | } | |
| 102 | 103 | ||
| 104 | + | // SetOnPaneClosed sets a callback invoked after a pane is successfully closed. | |
| 105 | + | func (c *WorkspaceCoordinator) SetOnPaneClosed(fn func(paneID entity.PaneID)) { | |
| 106 | + | c.onPaneClosed = fn | |
| 107 | + | } | |
| 108 | + | ||
| 103 | 109 | // notifyStateChanged triggers the state changed callback if set. | |
| 104 | 110 | func (c *WorkspaceCoordinator) notifyStateChanged() { | |
| 105 | 111 | if c.onStateChanged != nil { | |
| @@ -744,6 +750,9 @@ func (c *WorkspaceCoordinator) ClosePane(ctx context.Context) error { | |||
| 744 | 750 | closingPaneID, | |
| 745 | 751 | closeCtx, | |
| 746 | 752 | ) | |
| 753 | + | if c.onPaneClosed != nil { | |
| 754 | + | c.onPaneClosed(closingPaneID) | |
| 755 | + | } | |
| 747 | 756 | ||
| 748 | 757 | // Notify state change for session snapshots | |
| 749 | 758 | c.notifyStateChanged() | |
| @@ -802,6 +811,9 @@ func (c *WorkspaceCoordinator) ClosePaneByID(ctx context.Context, paneID entity. | |||
| 802 | 811 | paneID, | |
| 803 | 812 | closeCtx, | |
| 804 | 813 | ) | |
| 814 | + | if c.onPaneClosed != nil { | |
| 815 | + | c.onPaneClosed(paneID) | |
| 816 | + | } | |
| 805 | 817 | ||
| 806 | 818 | // Notify state change for session snapshots | |
| 807 | 819 | c.notifyStateChanged() | |
| @@ -35,6 +35,7 @@ type WorkspaceCoordinator struct { | |||||
| 35 | onCloseLastPane func(ctx context.Context) error | 35 | onCloseLastPane func(ctx context.Context) error | ||
| 36 | onCreatePopupTab func(ctx context.Context, input InsertPopupInput) error // For tabbed popup behavior | 36 | onCreatePopupTab func(ctx context.Context, input InsertPopupInput) error // For tabbed popup behavior | ||
| 37 | onStateChanged func() // For session snapshots | 37 | onStateChanged func() // For session snapshots | ||
| 38 | + | onPaneClosed func(paneID entity.PaneID) // For pane-specific cleanup hooks | |||
| 38 | } | 39 | } | ||
| 39 | 40 | ||||
| 40 | // WorkspaceCoordinatorConfig holds configuration for WorkspaceCoordinator. | 41 | // WorkspaceCoordinatorConfig holds configuration for WorkspaceCoordinator. | ||
| @@ -100,6 +101,11 @@ func (c *WorkspaceCoordinator) SetOnStateChanged(fn func()) { | |||||
| 100 | c.onStateChanged = fn | 101 | c.onStateChanged = fn | ||
| 101 | } | 102 | } | ||
| 102 | 103 | ||||
| 104 | + | // SetOnPaneClosed sets a callback invoked after a pane is successfully closed. | |||
| 105 | + | func (c *WorkspaceCoordinator) SetOnPaneClosed(fn func(paneID entity.PaneID)) { | |||
| 106 | + | c.onPaneClosed = fn | |||
| 107 | + | } | |||
| 108 | + | ||||
| 103 | // notifyStateChanged triggers the state changed callback if set. | 109 | // notifyStateChanged triggers the state changed callback if set. | ||
| 104 | func (c *WorkspaceCoordinator) notifyStateChanged() { | 110 | func (c *WorkspaceCoordinator) notifyStateChanged() { | ||
| 105 | if c.onStateChanged != nil { | 111 | if c.onStateChanged != nil { | ||
| @@ -744,6 +750,9 @@ func (c *WorkspaceCoordinator) ClosePane(ctx context.Context) error { | |||||
| 744 | closingPaneID, | 750 | closingPaneID, | ||
| 745 | closeCtx, | 751 | closeCtx, | ||
| 746 | ) | 752 | ) | ||
| 753 | + | if c.onPaneClosed != nil { | |||
| 754 | + | c.onPaneClosed(closingPaneID) | |||
| 755 | + | } | |||
| 747 | 756 | ||||
| 748 | // Notify state change for session snapshots | 757 | // Notify state change for session snapshots | ||
| 749 | c.notifyStateChanged() | 758 | c.notifyStateChanged() | ||
| @@ -802,6 +811,9 @@ func (c *WorkspaceCoordinator) ClosePaneByID(ctx context.Context, paneID entity. | |||||
| 802 | paneID, | 811 | paneID, | ||
| 803 | closeCtx, | 812 | closeCtx, | ||
| 804 | ) | 813 | ) | ||
| 814 | + | if c.onPaneClosed != nil { | |||
| 815 | + | c.onPaneClosed(paneID) | |||
| 816 | + | } | |||
| 805 | 817 | ||||
| 806 | // Notify state change for session snapshots | 818 | // Notify state change for session snapshots | ||
| 807 | c.notifyStateChanged() | 819 | c.notifyStateChanged() | ||