From 37c314bcc96063c42c468e564df2c34bac8ef8a1 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:15:43 -0700 Subject: [PATCH] Send a browser tab's window times to main with its open (dark, layout-owner build only) (#29988) * fix(browser): keep slept workspace browser tabs off the reopen-closed list Sleeping a workspace closed its browser tabs as if the user had closed them, so Reopen Closed Tab could bring back a tab that was only slept. Sleep now closes them with a 'sleep' reason that skips the reopen stack while still releasing host pages, doc grants and webviews as before. * feat(layout): send a browser tab's window timestamps with its open (dark) A layout-owner build's openBrowserTab carried no createdAt, so main stamped the tab-bar entry and the browser record with its own apply time and the window's next save diverged on createdAt for both. The window now sends both times, as createTerminalTab does; only switch-on builds send them. * Revert "fix(browser): keep slept workspace browser tabs off the reopen-closed list" This reverts commit 6798c3c701a409a7059df9ef978189bedfc201a7. * test: drop the slept-tab close test that went with the held sleep fix --- src/renderer/src/runtime/layout-apply-send.ts | 2 ++ .../store/slices/browser-layout-owner-commands.test.ts | 4 +++- .../slices/browser/browser-layout-owner-commands.ts | 2 ++ src/shared/rpc-contract/workspace-layout-params.ts | 2 ++ .../workspace-layout-browser-commands.test.ts | 9 +++++++++ .../workspace-layout/workspace-layout-command-types.ts | 3 +++ .../workspace-layout-content-commands.ts | 5 +++-- 7 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/runtime/layout-apply-send.ts b/src/renderer/src/runtime/layout-apply-send.ts index c451d39e8aa..61f7fd3a7f4 100644 --- a/src/renderer/src/runtime/layout-apply-send.ts +++ b/src/renderer/src/runtime/layout-apply-send.ts @@ -65,6 +65,8 @@ export type WindowLayoutCommand = groupId?: string afterTabId?: string index?: number + createdAt: number + browserCreatedAt: number split?: NewGroupSplit } | { type: 'openBrowserPage' | 'closeBrowserPage'; browserTabId: string; pageId: string } diff --git a/src/renderer/src/store/slices/browser-layout-owner-commands.test.ts b/src/renderer/src/store/slices/browser-layout-owner-commands.test.ts index 7a215eba314..5b3d7abc809 100644 --- a/src/renderer/src/store/slices/browser-layout-owner-commands.test.ts +++ b/src/renderer/src/store/slices/browser-layout-owner-commands.test.ts @@ -59,7 +59,9 @@ describe('browser tabs in a layout-owner build', () => { profileId: 'p1', sessionPartition: null, groupId: firstEntry.groupId, - index: 0 + index: 0, + createdAt: firstEntry.createdAt, + browserCreatedAt: first.createdAt } }, { diff --git a/src/renderer/src/store/slices/browser/browser-layout-owner-commands.ts b/src/renderer/src/store/slices/browser/browser-layout-owner-commands.ts index 17b50f74997..565bc5ffee8 100644 --- a/src/renderer/src/store/slices/browser/browser-layout-owner-commands.ts +++ b/src/renderer/src/store/slices/browser/browser-layout-owner-commands.ts @@ -32,6 +32,8 @@ export function recordBrowserTabOpen(state: BrowserCommandState, tab: BrowserWor sessionPartition: tab.sessionPartition ?? null, groupId: entry.groupId, ...(afterTabId ? { afterTabId } : position === 0 ? { index: 0 } : {}), + createdAt: entry.createdAt, + browserCreatedAt: tab.createdAt, // Open to the side: main makes the window's new split with the tab. ...newGroupSplitOf(state, tab.worktreeId, entry.groupId, entry.id) }) diff --git a/src/shared/rpc-contract/workspace-layout-params.ts b/src/shared/rpc-contract/workspace-layout-params.ts index 5d3ef4a17ac..4458e5f32e5 100644 --- a/src/shared/rpc-contract/workspace-layout-params.ts +++ b/src/shared/rpc-contract/workspace-layout-params.ts @@ -222,6 +222,8 @@ const OpenBrowserTabCommand = z.object({ groupId: z.string().min(1).optional(), afterTabId: z.string().min(1).optional(), index: z.number().int().nonnegative().optional(), + createdAt: z.number().int().nonnegative().optional(), + browserCreatedAt: z.number().int().nonnegative().optional(), split: NewGroupSplit }) diff --git a/src/shared/workspace-layout/workspace-layout-browser-commands.test.ts b/src/shared/workspace-layout/workspace-layout-browser-commands.test.ts index 0e9b01b991c..3e3a256dbfc 100644 --- a/src/shared/workspace-layout/workspace-layout-browser-commands.test.ts +++ b/src/shared/workspace-layout/workspace-layout-browser-commands.test.ts @@ -50,6 +50,15 @@ describe('openBrowserTab under the window’s ids', () => { expect(saved.browserTabsByWorktree?.[WS]?.at(-1)).toMatchObject({ pageIds: ['page-1'] }) }) + // Why: the window's next save carries its own timestamps; main's copy must hold the same ones. + it('keeps the times the window drew the entry and the record at', () => { + const { model } = build(testContext(), [ + { ...WINDOW_OPEN, createdAt: 1_700_000_000_001, browserCreatedAt: 1_700_000_000_000 } + ]) + expect(model.workspaces[WS]!.tabs.at(-1)?.createdAt).toBe(1_700_000_000_001) + expect(records(model)?.at(-1)?.createdAt).toBe(1_700_000_000_000) + }) + it('places the tab where the window drew it', () => { const { ctx, model, hostTab } = setup() const placed = build( diff --git a/src/shared/workspace-layout/workspace-layout-command-types.ts b/src/shared/workspace-layout/workspace-layout-command-types.ts index adce68f91ca..ef89676d50a 100644 --- a/src/shared/workspace-layout/workspace-layout-command-types.ts +++ b/src/shared/workspace-layout/workspace-layout-command-types.ts @@ -150,6 +150,9 @@ export type LayoutCommand = /** The window's own ids for a tab it drew: its tab-bar entry and its browser record. */ tabId?: string browserTabId?: string + /** When the window drew the tab-bar entry and the browser record, so its later save matches. */ + createdAt?: number + browserCreatedAt?: number split?: NewGroupSplit }) | (On & { type: 'openBrowserPage'; browserTabId: string; pageId: string }) diff --git a/src/shared/workspace-layout/workspace-layout-content-commands.ts b/src/shared/workspace-layout/workspace-layout-content-commands.ts index 4305fb22aeb..00eefff82c0 100644 --- a/src/shared/workspace-layout/workspace-layout-content-commands.ts +++ b/src/shared/workspace-layout/workspace-layout-content-commands.ts @@ -134,7 +134,8 @@ export function openBrowserTab( const tab = contentTab(context, { kind: 'browser', id: tabId, - entityId: browserTabId + entityId: browserTabId, + ...(command.createdAt !== undefined ? { createdAt: command.createdAt } : {}) }) const next = placeContentTab(model, key, tab, command, context).model const workspace = next.workspaces[key]! @@ -143,7 +144,7 @@ export function openBrowserTab( sessionProfileId: command.profileId ?? null, ...(command.sessionPartition ? { sessionPartition: command.sessionPartition } : {}), pageIds: [pageId], - createdAt: tab.createdAt + createdAt: command.browserCreatedAt ?? tab.createdAt } return applied( withWorkspace(next, key, {