Write no false preview flag on a new or promoted tab (dark, layout-owner build only) (#30041)

Switch-on builds only; switch-off behaviour is unchanged.

Main stores a tab's preview flag only while it is true. A non-preview
editor open created its tab-bar entry with `isPreview: false`, and making
a preview permanent by an edit, by "keep open" or by pinning wrote the
same, so the window's next save changed that tab in main's model (the
save-divergence check's `tab <id> changed: isPreview` after a CLI file
open). New tabs now carry the flag only when true, and those writers drop
it, by the rule E2-4 gave activation, now in one place (tab-preview-flag).

Claude-Session: 10569c9a-a0a1-4fc9-8469-0edfb7aed125
This commit is contained in:
Jinwoo Hong
2026-10-17 02:38:38 -07:00
committed by GitHub
parent 3d2e0bd26d
commit c3f52428dd
7 changed files with 93 additions and 11 deletions
@@ -10,6 +10,12 @@ import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import { applyLayoutCommand } from '../../../../shared/workspace-layout/workspace-layout-commands'
import type { LayoutCommand } from '../../../../shared/workspace-layout/workspace-layout-command-types'
import type { WorkspaceLayoutModel } from '../../../../shared/workspace-layout/workspace-layout-model'
import { saveWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-save'
import { asLoaded } from '../../../../shared/workspace-layout/workspace-layout-command.test-fixture'
import { buildPersistedUnifiedTabSessionData } from '@/lib/workspace-session-unified-tabs'
import { loadWorkspaceLayoutWithoutReport } from '../../../../shared/workspace-layout/workspace-layout-load'
import { nameBasedLoadContext } from '../../../../shared/workspace-layout/workspace-layout-minted-ids'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types'
const mocks = vi.hoisted(() => ({
layoutOwnerIsMain: true,
@@ -123,6 +129,23 @@ function mainView() {
}))
}
/** Main's tabs by id as loaded from a save, as main's save-divergence check reads them. */
function loadedTabs(session: WorkspaceSessionState) {
const { layout } = loadWorkspaceLayoutWithoutReport(
LOCAL_EXECUTION_HOST_ID,
session,
nameBasedLoadContext()
)
return Object.fromEntries((layout.workspaces[WT]?.tabs ?? []).map((tab) => [tab.id, tab]))
}
/** Main's tabs, then the same after the window's tab-bar save lands on main's session. */
function tabsBeforeAndAfterWindowSave() {
const mains = saveWorkspaceLayout(asLoaded(main))
const windows = { ...mains, ...buildPersistedUnifiedTabSessionData(store.getState()) }
return [loadedTabs(mains), loadedTabs(windows)]
}
function openFile(path: string, options?: { preview?: boolean; targetGroupId?: string }): string {
return store
.getState()
@@ -298,6 +321,42 @@ describe('editor tabs in a layout-owner build', () => {
).toBe(command!.type === 'openEditorTab' ? command.tabId : undefined)
})
it('saves the tabs as main holds them, with no preview flag main does not store', () => {
openFile('/a.ts')
openFile('/b.ts', { targetGroupId: 'g2' })
applySent()
let [was, now] = tabsBeforeAndAfterWindowSave()
expect(now).toStrictEqual(was)
const c = openFile('/c.ts', { preview: true })
store.getState().markFileDirty(c, true)
const d = openFile('/d.ts', { preview: true, targetGroupId: 'g2' })
store.getState().makePreviewFilePermanent(d)
applySent()
;[was, now] = tabsBeforeAndAfterWindowSave()
expect(now).toStrictEqual(was)
})
it('pins a preview tab without writing a preview flag', () => {
const a = openFile('/a.ts', { preview: true })
store.getState().pinTab(tabIn('g1', a))
const [tab] = store.getState().unifiedTabsByWorktree[WT]!
expect(tab?.isPinned).toBe(true)
expect(tab).not.toHaveProperty('isPreview')
})
it('keeps writing the preview flag as false with the switch off', () => {
mocks.layoutOwnerIsMain = false
const a = openFile('/a.ts')
const b = openFile('/b.ts', { preview: true })
store.getState().pinTab(tabIn('g1', b))
const tabs = store.getState().unifiedTabsByWorktree[WT]!
expect(tabs.find((tab) => tab.entityId === a)).toHaveProperty('isPreview', false)
expect(tabs.find((tab) => tab.entityId === b)).toHaveProperty('isPreview', false)
})
it("sends an edit-mode file's record exactly as the window's session save keeps it", () => {
const file = {
id: 'f1',
@@ -14,6 +14,7 @@ import { buildEditorActiveResult } from '../tabs/editor-open-target-group'
import { openWorkspaceEditorItem } from '../tabs/workspace-editor-item'
import type { Tab } from '../../../../../../shared/tab-types'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../../../shared/layout-owner-switch'
import { withPreviewEnded } from '../../tabs/tab-preview-flag'
import {
previewEditorTabsOf,
recordPromotedEditorTabsInLayoutOwner
@@ -188,7 +189,7 @@ export function createMarkdownPreviewActions(
return tab
}
changed = true
return { ...tab, isPreview: false }
return withPreviewEnded(tab)
})
}
return changed ? { openFiles, unifiedTabsByWorktree } : s
@@ -3,6 +3,7 @@ import type { EditorSlice } from '../types/editor-slice'
import { isEditorTabContentType } from '../tabs/editor-tab-content-type'
import type { Tab } from '../../../../../../shared/tab-types'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../../../shared/layout-owner-switch'
import { withPreviewEnded } from '../../tabs/tab-preview-flag'
import {
previewEditorTabsOf,
recordPromotedEditorTabsInLayoutOwner
@@ -97,7 +98,7 @@ export function createOpenFileMutations(
worktreeId,
tabs.map((tab) =>
tab.entityId === fileId && isEditorTabContentType(tab.contentType)
? { ...tab, isPreview: false }
? withPreviewEnded(tab)
: tab
)
])
@@ -0,0 +1,22 @@
// Switch point: main stores a tab's preview flag only while it is true, so a layout-owner build
// writes no `isPreview: false` either; a save carrying one would change the tab in main's model.
import type { Tab } from '../../../../../shared/tab-types'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../../shared/layout-owner-switch'
/** A new tab's preview flag. */
export function createdTabPreviewFlag(isPreview: boolean | undefined): Pick<Tab, 'isPreview'> {
if (!ORCA_LAYOUT_OWNER_IS_MAIN) {
return { isPreview }
}
return isPreview ? { isPreview: true } : {}
}
/** The tab with its preview ended. */
export function withPreviewEnded(tab: Tab): Tab {
if (!ORCA_LAYOUT_OWNER_IS_MAIN) {
return { ...tab, isPreview: false }
}
const { isPreview: _ended, ...rest } = tab
return rest
}
@@ -19,6 +19,7 @@ import {
import { resolveUnifiedTabCreatePlacement } from './tabs-create-placement'
import { emptySplitGroup, insertSplitGroup } from './tabs-group-actions'
import { folderWorkspaceToWorktree } from '../../../../../shared/folder-workspace-worktree'
import { createdTabPreviewFlag } from './tab-preview-flag'
export function createTabsCreateActions(
set: TabsSliceSet,
@@ -115,7 +116,7 @@ export function createTabsCreateActions(
createdAt,
// Why: creating an active tab is a focus event; Cmd+J recency reads lastFocusedAt.
...(recordsFocus ? { lastFocusedAt: createdAt } : {}),
isPreview: init?.isPreview,
...createdTabPreviewFlag(init?.isPreview),
isPinned: init?.isPinned
}
@@ -195,7 +196,7 @@ export function createTabsCreateActions(
createdAt,
// Why: creating an active tab is a focus event; Cmd+J recency reads lastFocusedAt.
...(shouldActivate ? { lastFocusedAt: createdAt } : {}),
isPreview: init?.isPreview,
...createdTabPreviewFlag(init?.isPreview),
isPinned: init?.isPinned
}
const newGroup: TabGroup = {
@@ -8,17 +8,14 @@ import {
import type { Tab } from '../../../../../shared/tab-types'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../../shared/layout-owner-switch'
import { recordPromotedEditorTabsInLayoutOwner } from '../editor/tabs/editor-tab-layout-owner'
import { withPreviewEnded } from './tab-preview-flag'
/** Activation ends a preview. Switch point: main stores no `isPreview: false`, so neither does this. */
/** Activation ends a preview unless it preserves it. */
function withPreviewOnActivation(tab: Tab, preservePreview: boolean): Tab {
if (!ORCA_LAYOUT_OWNER_IS_MAIN) {
return { ...tab, isPreview: preservePreview ? tab.isPreview : false }
}
if (preservePreview) {
return tab
}
const { isPreview: _ended, ...rest } = tab
return rest
return preservePreview ? tab : withPreviewEnded(tab)
}
export function createTabsFocusActions(
@@ -14,6 +14,7 @@ import {
} from './tabs-host-mirroring'
import { sendTabChangeToLayoutOwner } from '../../terminals/tab-change-through-layout-owner'
import { recordTabOrderInLayoutOwner } from './tab-order-layout-owner'
import { withPreviewEnded } from './tab-preview-flag'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../../shared/layout-owner-switch'
function oppositeViewMode(mode: 'terminal' | 'chat'): 'terminal' | 'chat' {
@@ -216,7 +217,7 @@ export function createTabsLabelActions(
}
const { tab, worktreeId } = found
const tabs = (state.unifiedTabsByWorktree[worktreeId] ?? []).map((candidate) =>
candidate.id === tabId ? { ...candidate, isPinned: true, isPreview: false } : candidate
candidate.id === tabId ? withPreviewEnded({ ...candidate, isPinned: true }) : candidate
)
const groups = state.groupsByWorktree[worktreeId] ?? []
const group = groups.find((candidate) => candidate.id === tab.groupId)