detached c948582

fix: stabilize webview signals, pane history cleanup, and ghost selection

commit
c948582e81d2563d7d796b4b7923c68b0a11125a
author
brice · · 2026-02-07 07:35 UTC
parent
964a47e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
5252 ZoomLevel float64
5353 }
5454
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+
5565 // PopupRequest contains metadata about a popup window request.
5666 type PopupRequest struct {
5767 TargetURI string
@@ -92,6 +102,8 @@ type WebViewCallbacks struct {
92102 // OnLinkHover is called when hovering over a link, image, or media element.
93103 // The uri parameter contains the target URL, or empty string when leaving.
94104 OnLinkHover func(uri string)
105+ // OnWebProcessTerminated is called when the web process exits unexpectedly.
106+ OnWebProcessTerminated func(reason WebProcessTerminationReason, reasonLabel string, uri string)
95107 }
96108
97109 // FindOptions configures search behavior.
@@ -111,10 +123,10 @@ type FindController interface {
111123 GetSearchText() string
112124
113125 // 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)
118130 }
119131
120132 // WebView defines the port interface for browser view operations.
@@ -52,6 +52,16 @@ type WebViewState struct {
52 ZoomLevel float6452 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 string67 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() string123 GetSearchText() string
112 124
113 // Signal connections125 // Signal connections
114- OnFoundText(callback func(matchCount uint)) uint32126+ OnFoundText(callback func(matchCount uint)) uint
115- OnFailedToFindText(callback func()) uint32127+ OnFailedToFindText(callback func()) uint
116- OnCountedMatches(callback func(matchCount uint)) uint32128+ 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 {
4343 ctx context.Context
4444
4545 controller port.FindController
46- signalIDs []uint32
46+ signalIDs []uint
4747
4848 query string
4949 matchCount uint
@@ -43,7 +43,7 @@ type FindInPageUseCase struct {
43 ctx context.Context43 ctx context.Context
44 44
45 controller port.FindController45 controller port.FindController
46- signalIDs []uint3246+ signalIDs []uint
47 47
48 query string48 query string
49 matchCount uint49 matchCount uint
M internal/application/usecase/navigate.go +29 -10
@@ -24,6 +24,9 @@ const (
2424
2525 // historyWorkerFlushInterval coalesces bursts into fewer persistence writes.
2626 historyWorkerFlushInterval = 100 * time.Millisecond
27+
28+ // historyFallbackIncrementCap bounds DB writes when delta-increment API is unavailable.
29+ historyFallbackIncrementCap = 256
2730 )
2831
2932 // historyRecord holds data for async history recording.
@@ -158,9 +161,7 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str
158161 return
159162 }
160163
161- if paneID == "" {
162- paneID = "__default__"
163- }
164+ paneID = normalizePaneID(paneID)
164165
165166 now := time.Now()
166167
@@ -198,6 +199,15 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, paneID, rawURL str
198199 }
199200 }
200201
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+
201211 // historyWorker is a background goroutine that processes history records.
202212 // It drains the queue and persists records to the database without blocking the UI.
203213 func (uc *NavigateUseCase) historyWorker() {
@@ -262,7 +272,7 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec
262272 }
263273
264274 if existing != nil {
265- delta := maxInt(1, record.visits)
275+ delta := max(1, record.visits)
266276 if deltaWriter, ok := uc.historyRepo.(historyVisitDeltaIncrementer); ok {
267277 if err := deltaWriter.IncrementVisitCountBy(ctx, record.url, delta); err != nil {
268278 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
270280 }
271281 return
272282 }
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++ {
274293 if err := uc.historyRepo.IncrementVisitCount(ctx, record.url); err != nil {
275294 log.Warn().Err(err).Str("url", record.url).Msg("failed to increment visit count")
276295 return
@@ -279,7 +298,7 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec
279298 } else {
280299 // Create new entry
281300 entry := entity.NewHistoryEntry(record.url, "")
282- entry.VisitCount = int64(maxInt(1, record.visits))
301+ entry.VisitCount = int64(max(1, record.visits))
283302 if err := uc.historyRepo.Save(ctx, entry); err != nil {
284303 log.Warn().Err(err).Str("url", record.url).Msg("failed to save history")
285304 }
@@ -420,9 +439,9 @@ func isHashOnlyTransition(previous, current string) bool {
420439 return prevParsed.Fragment != currParsed.Fragment
421440 }
422441
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__"
426445 }
427- return b
446+ return paneID
428447 }
@@ -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.Millisecond26 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 return161 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 return281 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 return295 return
@@ -279,7 +298,7 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec
279 } else {298 } else {
280 // Create new entry299 // 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.Fragment439 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 a444+ return "__default__"
426 }445 }
427- return b446+ return paneID
428 }447 }
M internal/infrastructure/webkit/messaging.go +6 -6
@@ -47,7 +47,7 @@ type MessageRouter struct {
4747
4848 mu sync.RWMutex
4949 callbacks []interface{}
50- signals []uint32
50+ signals []uint
5151
5252 idMu sync.Mutex
5353 syncedIDs map[WebViewID]bool
@@ -121,7 +121,7 @@ func (r *MessageRouter) RegisterHandlerWithCallbacks(msgType, callback, errorCal
121121 // It registers the script message handler in the MAIN world (not isolated).
122122 // WebKit's messageHandlers is only available in main world.
123123 // 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) {
125125 log := logging.FromContext(r.baseCtx).With().Str("component", "message-router").Logger()
126126
127127 log.Debug().Msg("SetupMessageHandler called")
@@ -146,10 +146,10 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st
146146
147147 // Connect to "script-message-received::dumber" with handler name as signal detail
148148 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")
150150
151151 r.mu.Lock()
152- r.signals = append(r.signals, uint32(signalID))
152+ r.signals = append(r.signals, signalID)
153153 r.mu.Unlock()
154154
155155 log.Debug().Msg("SetupMessageHandler: registering handler in main world")
@@ -164,10 +164,10 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st
164164 log.Info().
165165 Str("handler", MessageHandlerName).
166166 Str("world", "main").
167- Uint32("signal_id", uint32(signalID)).
167+ Uint64("signal_id", uint64(signalID)).
168168 Msg("script message handler connected")
169169
170- return uint32(signalID), nil
170+ return signalID, nil
171171 }
172172
173173 // handleScriptMessage decodes the JSC value and routes it to the correct handler.
@@ -47,7 +47,7 @@ type MessageRouter struct {
47 47
48 mu sync.RWMutex48 mu sync.RWMutex
49 callbacks []interface{}49 callbacks []interface{}
50- signals []uint3250+ signals []uint
51 51
52 idMu sync.Mutex52 idMu sync.Mutex
53 syncedIDs map[WebViewID]bool53 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 detail147 // 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), nil170+ 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 {
136136 lastProgressUpdate atomic.Int64 // Unix nanoseconds
137137
138138 // Signal handler IDs for disconnection
139- signalIDs []uint32
139+ signalIDs []uintptr
140140
141141 // Callbacks (set by UI layer)
142142 OnLoadChanged func(LoadEvent)
@@ -171,7 +171,7 @@ type WebView struct {
171171 findControllerOnce sync.Once
172172
173173 backForwardList *webkit.BackForwardList
174- backForwardListSignalID uint32
174+ backForwardListSignalID uint
175175 }
176176
177177 type runJSErrorStat struct {
@@ -255,42 +255,42 @@ func (a *findControllerAdapter) GetSearchText() string {
255255 return a.fc.GetSearchText()
256256 }
257257
258-func (a *findControllerAdapter) OnFoundText(callback func(matchCount uint)) uint32 {
258+func (a *findControllerAdapter) OnFoundText(callback func(matchCount uint)) uint {
259259 if a == nil || a.fc == nil || callback == nil {
260260 return 0
261261 }
262262 cb := func(_ webkit.FindController, matchCount uint) {
263263 callback(matchCount)
264264 }
265- return uint32(a.fc.ConnectFoundText(&cb))
265+ return a.fc.ConnectFoundText(&cb)
266266 }
267267
268-func (a *findControllerAdapter) OnFailedToFindText(callback func()) uint32 {
268+func (a *findControllerAdapter) OnFailedToFindText(callback func()) uint {
269269 if a == nil || a.fc == nil || callback == nil {
270270 return 0
271271 }
272272 cb := func(_ webkit.FindController) {
273273 callback()
274274 }
275- return uint32(a.fc.ConnectFailedToFindText(&cb))
275+ return a.fc.ConnectFailedToFindText(&cb)
276276 }
277277
278-func (a *findControllerAdapter) OnCountedMatches(callback func(matchCount uint)) uint32 {
278+func (a *findControllerAdapter) OnCountedMatches(callback func(matchCount uint)) uint {
279279 if a == nil || a.fc == nil || callback == nil {
280280 return 0
281281 }
282282 cb := func(_ webkit.FindController, matchCount uint) {
283283 callback(matchCount)
284284 }
285- return uint32(a.fc.ConnectCountedMatches(&cb))
285+ return a.fc.ConnectCountedMatches(&cb)
286286 }
287287
288-func (a *findControllerAdapter) DisconnectSignal(id uint32) {
288+func (a *findControllerAdapter) DisconnectSignal(id uint) {
289289 if a == nil || a.fc == nil || id == 0 {
290290 return
291291 }
292292 obj := gobject.ObjectNewFromInternalPtr(a.fc.GoPointer())
293- gobject.SignalHandlerDisconnect(obj, uint(id))
293+ gobject.SignalHandlerDisconnect(obj, id)
294294 }
295295
296296 // NewWebView creates a new WebView with the given context and settings.
@@ -324,7 +324,7 @@ func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsMan
324324 inner: inner,
325325 ucm: inner.GetUserContentManager(),
326326 logger: log.With().Str("component", "webview").Logger(),
327- signalIDs: make([]uint32, 0, 4),
327+ signalIDs: make([]uintptr, 0, 4),
328328 runJSErrorStats: make(map[string]runJSErrorStat),
329329 }
330330
@@ -375,7 +375,7 @@ func NewWebViewWithRelated(ctx context.Context, parent *WebView, settings *Setti
375375 isRelated: true, // Shares web process with parent - must not terminate process on destroy
376376 ucm: inner.GetUserContentManager(),
377377 logger: log.With().Str("component", "webview-popup").Logger(),
378- signalIDs: make([]uint32, 0, 6),
378+ signalIDs: make([]uintptr, 0, 6),
379379 runJSErrorStats: make(map[string]runJSErrorStat),
380380 }
381381
@@ -446,7 +446,7 @@ func (wv *WebView) connectLoadChangedSignal() {
446446 }
447447 }
448448 sigID := wv.inner.ConnectLoadChanged(&loadChangedCb)
449- wv.signalIDs = append(wv.signalIDs, uint32(sigID))
449+ wv.signalIDs = append(wv.signalIDs, uintptr(sigID))
450450 }
451451
452452 func (wv *WebView) connectCloseSignal() {
@@ -456,7 +456,7 @@ func (wv *WebView) connectCloseSignal() {
456456 }
457457 }
458458 sigID := wv.inner.ConnectClose(&closeCb)
459- wv.signalIDs = append(wv.signalIDs, uint32(sigID))
459+ wv.signalIDs = append(wv.signalIDs, uintptr(sigID))
460460 }
461461
462462 func (wv *WebView) connectCreateSignal() {
@@ -498,7 +498,7 @@ func (wv *WebView) connectCreateSignal() {
498498 return newWV.inner.Widget
499499 }
500500 sigID := wv.inner.ConnectCreate(&createCb)
501- wv.signalIDs = append(wv.signalIDs, uint32(sigID))
501+ wv.signalIDs = append(wv.signalIDs, uintptr(sigID))
502502 }
503503
504504 func (wv *WebView) connectReadyToShowSignal() {
@@ -508,7 +508,7 @@ func (wv *WebView) connectReadyToShowSignal() {
508508 }
509509 }
510510 sigID := wv.inner.ConnectReadyToShow(&readyToShowCb)
511- wv.signalIDs = append(wv.signalIDs, uint32(sigID))
511+ wv.signalIDs = append(wv.signalIDs, uintptr(sigID))
512512 }
513513
514514 func (wv *WebView) connectTitleSignal() {
@@ -523,7 +523,7 @@ func (wv *WebView) connectTitleSignal() {
523523 }
524524 }
525525 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))
527527 }
528528
529529 func (wv *WebView) connectURISignal() {
@@ -539,7 +539,7 @@ func (wv *WebView) connectURISignal() {
539539 }
540540 }
541541 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))
543543 }
544544
545545 func (wv *WebView) connectFaviconSignal() {
@@ -550,7 +550,7 @@ func (wv *WebView) connectFaviconSignal() {
550550 }
551551 }
552552 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))
554554 }
555555
556556 // progressThrottleInterval limits progress callbacks to ~60fps to reduce UI overhead.
@@ -580,7 +580,7 @@ func (wv *WebView) connectProgressSignal() {
580580 }
581581 }
582582 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))
584584 }
585585
586586 func (wv *WebView) connectDecidePolicySignal() {
@@ -596,7 +596,7 @@ func (wv *WebView) connectDecidePolicySignal() {
596596 }
597597 }
598598 sigID := wv.inner.ConnectDecidePolicy(&decidePolicyCb)
599- wv.signalIDs = append(wv.signalIDs, uint32(sigID))
599+ wv.signalIDs = append(wv.signalIDs, uintptr(sigID))
600600 }
601601
602602 // handleResponsePolicyDecision handles response policy decisions (e.g., forcing downloads).
@@ -795,7 +795,7 @@ func (wv *WebView) connectEnterFullscreenSignal() {
795795 return false // Allow fullscreen
796796 }
797797 sigID := wv.inner.ConnectEnterFullscreen(&enterFullscreenCb)
798- wv.signalIDs = append(wv.signalIDs, uint32(sigID))
798+ wv.signalIDs = append(wv.signalIDs, uintptr(sigID))
799799 }
800800
801801 func (wv *WebView) connectLeaveFullscreenSignal() {
@@ -808,7 +808,7 @@ func (wv *WebView) connectLeaveFullscreenSignal() {
808808 return false // Allow leaving fullscreen
809809 }
810810 sigID := wv.inner.ConnectLeaveFullscreen(&leaveFullscreenCb)
811- wv.signalIDs = append(wv.signalIDs, uint32(sigID))
811+ wv.signalIDs = append(wv.signalIDs, uintptr(sigID))
812812 }
813813
814814 func (wv *WebView) connectAudioStateSignal() {
@@ -826,7 +826,7 @@ func (wv *WebView) connectAudioStateSignal() {
826826 }
827827 }
828828 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))
830830 }
831831
832832 func (wv *WebView) connectMouseTargetChangedSignal() {
@@ -854,7 +854,7 @@ func (wv *WebView) connectMouseTargetChangedSignal() {
854854 wv.OnLinkHover(uri)
855855 }
856856 sigID := wv.inner.ConnectMouseTargetChanged(&mouseTargetCb)
857- wv.signalIDs = append(wv.signalIDs, uint32(sigID))
857+ wv.signalIDs = append(wv.signalIDs, uintptr(sigID))
858858 }
859859
860860 func (wv *WebView) connectBackForwardListChangedSignal() {
@@ -882,7 +882,7 @@ func (wv *WebView) connectBackForwardListChangedSignal() {
882882 }
883883 sigID := backForwardList.ConnectChanged(&changedCb)
884884 wv.backForwardList = backForwardList
885- wv.backForwardListSignalID = uint32(sigID)
885+ wv.backForwardListSignalID = sigID
886886 }
887887
888888 func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason) string {
@@ -898,6 +898,19 @@ func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason
898898 }
899899 }
900900
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+
901914 func (wv *WebView) connectWebProcessTerminatedSignal() {
902915 terminatedCb := func(_ webkit.WebView, reason webkit.WebProcessTerminationReason) {
903916 uri := wv.URI()
@@ -914,7 +927,7 @@ func (wv *WebView) connectWebProcessTerminatedSignal() {
914927 }
915928 }
916929 sigID := wv.inner.ConnectWebProcessTerminated(&terminatedCb)
917- wv.signalIDs = append(wv.signalIDs, uint32(sigID))
930+ wv.signalIDs = append(wv.signalIDs, uintptr(sigID))
918931 }
919932
920933 // ID returns the unique identifier for this WebView.
@@ -1199,6 +1212,8 @@ func (wv *WebView) SetCallbacks(callbacks *port.WebViewCallbacks) {
11991212 wv.OnFaviconChanged = nil
12001213 wv.OnClose = nil
12011214 wv.OnCreate = nil
1215+ wv.OnLinkHover = nil
1216+ wv.OnWebProcessTerminated = nil
12021217 return
12031218 }
12041219
@@ -1238,6 +1253,13 @@ func (wv *WebView) SetCallbacks(callbacks *port.WebViewCallbacks) {
12381253 }
12391254 }
12401255 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+ }
12411263 }
12421264
12431265 // ShowDevTools opens the WebKit inspector/developer tools.
@@ -1311,7 +1333,7 @@ func (wv *WebView) DisconnectSignals() {
13111333
13121334 if wv.backForwardList != nil && wv.backForwardListSignalID != 0 {
13131335 bfObj := gobject.ObjectNewFromInternalPtr(wv.backForwardList.GoPointer())
1314- gobject.SignalHandlerDisconnect(bfObj, uint(wv.backForwardListSignalID))
1336+ gobject.SignalHandlerDisconnect(bfObj, wv.backForwardListSignalID)
13151337 wv.backForwardListSignalID = 0
13161338 wv.backForwardList = nil
13171339 }
@@ -136,7 +136,7 @@ type WebView struct {
136 lastProgressUpdate atomic.Int64 // Unix nanoseconds136 lastProgressUpdate atomic.Int64 // Unix nanoseconds
137 137
138 // Signal handler IDs for disconnection138 // Signal handler IDs for disconnection
139- signalIDs []uint32139+ 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.Once171 findControllerOnce sync.Once
172 172
173 backForwardList *webkit.BackForwardList173 backForwardList *webkit.BackForwardList
174- backForwardListSignalID uint32174+ 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 0260 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 0270 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 0280 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 return290 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 destroy375 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.Widget498 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 fullscreen795 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 fullscreen808 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 = backForwardList884 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 = nil1212 wv.OnFaviconChanged = nil
1200 wv.OnClose = nil1213 wv.OnClose = nil
1201 wv.OnCreate = nil1214 wv.OnCreate = nil
1215+ wv.OnLinkHover = nil
1216+ wv.OnWebProcessTerminated = nil
1202 return1217 return
1203 }1218 }
1204 1219
@@ -1238,6 +1253,13 @@ func (wv *WebView) SetCallbacks(callbacks *port.WebViewCallbacks) {
1238 }1253 }
1239 }1254 }
1240 wv.OnLinkHover = callbacks.OnLinkHover1255 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 = 01337 wv.backForwardListSignalID = 0
1316 wv.backForwardList = nil1338 wv.backForwardList = nil
1317 }1339 }
M internal/ui/app.go +3 -0
@@ -1393,6 +1393,9 @@ func (a *App) initCoordinators(ctx context.Context) {
13931393 a.deps.NavigateUC,
13941394 a.contentCoord,
13951395 )
1396+ a.wsCoord.SetOnPaneClosed(func(paneID entity.PaneID) {
1397+ a.navCoord.ClearPaneHistory(paneID)
1398+ })
13961399
13971400 // Wire title updates to history persistence
13981401 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 persistence1400 // 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
850850 }
851851 prefixRunes := utf8.RuneCountInString(realInput)
852852 fullRunes := utf8.RuneCountInString(ghostFullText)
853- if prefixRunes < 0 || fullRunes <= prefixRunes {
853+ if fullRunes <= prefixRunes {
854854 return false
855855 }
856856 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 false854 return false
855 }855 }
856 return selectionStart == prefixRunes && selectionEnd == fullRunes856 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
236236 c.navigateUC.RecordHistory(ctx, string(paneID), url)
237237 }
238238
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+
239247 // ActiveWebView returns the WebView for the active pane (for zoom operations).
240248 func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebView {
241249 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 {
3535 onCloseLastPane func(ctx context.Context) error
3636 onCreatePopupTab func(ctx context.Context, input InsertPopupInput) error // For tabbed popup behavior
3737 onStateChanged func() // For session snapshots
38+ onPaneClosed func(paneID entity.PaneID) // For pane-specific cleanup hooks
3839 }
3940
4041 // WorkspaceCoordinatorConfig holds configuration for WorkspaceCoordinator.
@@ -100,6 +101,11 @@ func (c *WorkspaceCoordinator) SetOnStateChanged(fn func()) {
100101 c.onStateChanged = fn
101102 }
102103
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+
103109 // notifyStateChanged triggers the state changed callback if set.
104110 func (c *WorkspaceCoordinator) notifyStateChanged() {
105111 if c.onStateChanged != nil {
@@ -744,6 +750,9 @@ func (c *WorkspaceCoordinator) ClosePane(ctx context.Context) error {
744750 closingPaneID,
745751 closeCtx,
746752 )
753+ if c.onPaneClosed != nil {
754+ c.onPaneClosed(closingPaneID)
755+ }
747756
748757 // Notify state change for session snapshots
749758 c.notifyStateChanged()
@@ -802,6 +811,9 @@ func (c *WorkspaceCoordinator) ClosePaneByID(ctx context.Context, paneID entity.
802811 paneID,
803812 closeCtx,
804813 )
814+ if c.onPaneClosed != nil {
815+ c.onPaneClosed(paneID)
816+ }
805817
806818 // Notify state change for session snapshots
807819 c.notifyStateChanged()
@@ -35,6 +35,7 @@ type WorkspaceCoordinator struct {
35 onCloseLastPane func(ctx context.Context) error35 onCloseLastPane func(ctx context.Context) error
36 onCreatePopupTab func(ctx context.Context, input InsertPopupInput) error // For tabbed popup behavior36 onCreatePopupTab func(ctx context.Context, input InsertPopupInput) error // For tabbed popup behavior
37 onStateChanged func() // For session snapshots37 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 = fn101 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 snapshots757 // 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 snapshots818 // Notify state change for session snapshots
807 c.notifyStateChanged()819 c.notifyStateChanged()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close