diff --git a/src/renderer/src/store/slices/editor-tab-layout-owner.test.ts b/src/renderer/src/store/slices/editor-tab-layout-owner.test.ts index da144a82ae5..ba161ab1c02 100644 --- a/src/renderer/src/store/slices/editor-tab-layout-owner.test.ts +++ b/src/renderer/src/store/slices/editor-tab-layout-owner.test.ts @@ -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', diff --git a/src/renderer/src/store/slices/editor/actions/markdown-preview-actions.ts b/src/renderer/src/store/slices/editor/actions/markdown-preview-actions.ts index 374ad453c31..8872ccdb7d5 100644 --- a/src/renderer/src/store/slices/editor/actions/markdown-preview-actions.ts +++ b/src/renderer/src/store/slices/editor/actions/markdown-preview-actions.ts @@ -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 diff --git a/src/renderer/src/store/slices/editor/actions/open-file-mutations.ts b/src/renderer/src/store/slices/editor/actions/open-file-mutations.ts index e114d048a5a..823cee33bcf 100644 --- a/src/renderer/src/store/slices/editor/actions/open-file-mutations.ts +++ b/src/renderer/src/store/slices/editor/actions/open-file-mutations.ts @@ -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 ) ]) diff --git a/src/renderer/src/store/slices/tabs/tab-preview-flag.ts b/src/renderer/src/store/slices/tabs/tab-preview-flag.ts new file mode 100644 index 00000000000..c6d0b427fcf --- /dev/null +++ b/src/renderer/src/store/slices/tabs/tab-preview-flag.ts @@ -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 { + 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 +} diff --git a/src/renderer/src/store/slices/tabs/tabs-create-actions.ts b/src/renderer/src/store/slices/tabs/tabs-create-actions.ts index 5cda1b606a3..31e22259316 100644 --- a/src/renderer/src/store/slices/tabs/tabs-create-actions.ts +++ b/src/renderer/src/store/slices/tabs/tabs-create-actions.ts @@ -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 = { diff --git a/src/renderer/src/store/slices/tabs/tabs-focus-actions.ts b/src/renderer/src/store/slices/tabs/tabs-focus-actions.ts index 61decb15688..87f73b3956d 100644 --- a/src/renderer/src/store/slices/tabs/tabs-focus-actions.ts +++ b/src/renderer/src/store/slices/tabs/tabs-focus-actions.ts @@ -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( diff --git a/src/renderer/src/store/slices/tabs/tabs-label-actions.ts b/src/renderer/src/store/slices/tabs/tabs-label-actions.ts index b70c64cd701..2003fc98d22 100644 --- a/src/renderer/src/store/slices/tabs/tabs-label-actions.ts +++ b/src/renderer/src/store/slices/tabs/tabs-label-actions.ts @@ -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)