@@ -1606,13 +1606,30 @@ const WindowState = struct {
16061606 fn reconcileChildExit (self : * WindowState , allocator : std.mem.Allocator ) void {
16071607 const pid = self .launched_browser_pid orelse return ;
16081608
1609+ // IMPORTANT:
1610+ // In browser/web modes, the PID we track can be a short-lived launcher process
1611+ // (or helper) rather than the long-lived browser tab/window process.
1612+ // We must not close the backend just because that PID exits.
1613+ // Only native-webview-first mode treats tracked process death as a terminal close.
1614+ const should_close_on_pid_exit = self .launch_policy .first == .native_webview ;
1615+
16091616 if (self .launched_browser_lifecycle_linked and core_runtime .linkedChildExited (pid )) {
1610- self .markClosedFromTrackedBrowserExit (allocator , "child-exited" );
1617+ if (should_close_on_pid_exit ) {
1618+ self .markClosedFromTrackedBrowserExit (allocator , "child-exited" );
1619+ } else {
1620+ self .clearTrackedBrowserState (allocator );
1621+ self .emit (.window_state , "browser-detached" , "child-exited" );
1622+ }
16111623 return ;
16121624 }
16131625
16141626 if (! core_runtime .isProcessAlive (pid )) {
1615- self .markClosedFromTrackedBrowserExit (allocator , "browser-exited" );
1627+ if (should_close_on_pid_exit ) {
1628+ self .markClosedFromTrackedBrowserExit (allocator , "browser-exited" );
1629+ } else {
1630+ self .clearTrackedBrowserState (allocator );
1631+ self .emit (.window_state , "browser-detached" , "browser-exited" );
1632+ }
16161633 }
16171634 }
16181635
@@ -2548,6 +2565,15 @@ pub const Window = struct {
25482565 index : usize ,
25492566 id : usize ,
25502567
2568+ fn refreshRenderedContentLocked (self : * Window , win_state : * WindowState ) ! void {
2569+ try win_state .ensureBrowserRenderState (self .app .allocator , self .app .options );
2570+ if (win_state .isNativeWindowActive ()) {
2571+ if (win_state .last_url ) | url | {
2572+ _ = win_state .backend .showContent (.{ .url = url }) catch {};
2573+ }
2574+ }
2575+ }
2576+
25512577 pub fn showHtml (self : * Window , html : []const u8 ) ! void {
25522578 self .app .enforcePinnedMoveInvariant (.app );
25532579 if (html .len == 0 ) return error .EmptyHtml ;
@@ -2564,12 +2590,7 @@ pub const Window = struct {
25642590 }
25652591 win_state .shown = true ;
25662592
2567- try win_state .ensureBrowserRenderState (self .app .allocator , self .app .options );
2568- if (win_state .isNativeWindowActive ()) {
2569- if (win_state .last_url ) | url | {
2570- _ = win_state .backend .showContent (.{ .url = url }) catch {};
2571- }
2572- }
2593+ try self .refreshRenderedContentLocked (win_state );
25732594
25742595 self .emitRuntimeDiagnostics ();
25752596 self .emit (.navigation , "show-html" , html );
@@ -2601,12 +2622,7 @@ pub const Window = struct {
26012622 }
26022623 win_state .shown = true ;
26032624
2604- try win_state .ensureBrowserRenderState (self .app .allocator , self .app .options );
2605- if (win_state .isNativeWindowActive ()) {
2606- if (win_state .last_url ) | url | {
2607- _ = win_state .backend .showContent (.{ .url = url }) catch {};
2608- }
2609- }
2625+ try self .refreshRenderedContentLocked (win_state );
26102626
26112627 self .emitRuntimeDiagnostics ();
26122628 self .emit (.navigation , "show-file" , path );
@@ -3020,6 +3036,9 @@ pub const Service = struct {
30203036 const state = win .state ();
30213037 state .state_mutex .lock ();
30223038 defer state .state_mutex .unlock ();
3039+ // Reconcile tracked process state every loop.
3040+ // `reconcileChildExit()` is mode-aware and only turns PID exit into app close
3041+ // for native-webview-first mode. Browser/web modes only detach PID tracking.
30233042 state .reconcileChildExit (self .app .allocator );
30243043 return state .close_requested .load (.acquire );
30253044 }
@@ -3755,7 +3774,6 @@ fn handleLifecycleRoute(
37553774 if (! std .mem .eql (u8 , path_only , "/webui/lifecycle" )) return false ;
37563775
37573776 var parsed = std .json .parseFromSlice (std .json .Value , allocator , body , .{}) catch null ;
3758- var logged_event = false ;
37593777 const client_token = httpHeaderValue (headers , "x-webui-client-id" ) orelse default_client_token ;
37603778 state .state_mutex .lock ();
37613779 _ = state .findOrCreateClientSessionLocked (client_token ) catch null ;
@@ -3770,19 +3788,11 @@ fn handleLifecycleRoute(
37703788 state .requestLifecycleCloseFromFrontend ();
37713789 state .state_mutex .unlock ();
37723790 }
3773- if (state .rpc_state .log_enabled ) {
3774- std .debug .print ("[webui.lifecycle] event={s}\n " , .{event_value .string });
3775- logged_event = true ;
3776- }
37773791 }
37783792 }
37793793 }
37803794 }
37813795
3782- if (state .rpc_state .log_enabled and ! logged_event ) {
3783- std .debug .print ("[webui.lifecycle] body={s}\n " , .{body });
3784- }
3785-
37863796 try writeHttpResponse (stream , 200 , "application/json; charset=utf-8" , "{\" ok\" :true}" );
37873797 return true ;
37883798}
@@ -5120,7 +5130,7 @@ test "window_closing lifecycle event is ignored while tracked browser pid is ali
51205130 try std .testing .expect (! should_close );
51215131}
51225132
5123- test "non-linked tracked browser pid death requests close" {
5133+ test "non-linked tracked browser pid death detaches without close in web mode " {
51245134 if (builtin .os .tag != .linux ) return error .SkipZigTest ;
51255135
51265136 var gpa = std .heap .GeneralPurposeAllocator (.{}){};
@@ -5152,20 +5162,27 @@ test "non-linked tracked browser pid death requests close" {
51525162 win .state ().state_mutex .unlock ();
51535163
51545164 var closed = false ;
5165+ var detached = false ;
51555166 var attempts : usize = 0 ;
51565167 while (attempts < 120 ) : (attempts += 1 ) {
51575168 win .state ().state_mutex .lock ();
51585169 win .state ().reconcileChildExit (gpa .allocator ());
51595170 const requested = win .state ().close_requested .load (.acquire );
5171+ const tracked_pid = win .state ().launched_browser_pid ;
51605172 win .state ().state_mutex .unlock ();
51615173 if (requested ) {
51625174 closed = true ;
51635175 break ;
51645176 }
5177+ if (tracked_pid == null ) {
5178+ detached = true ;
5179+ break ;
5180+ }
51655181 std .Thread .sleep (5 * std .time .ns_per_ms );
51665182 }
51675183
5168- try std .testing .expect (closed );
5184+ try std .testing .expect (! closed );
5185+ try std .testing .expect (detached );
51695186}
51705187
51715188test "window style apply updates persisted state" {
0 commit comments