From a34b4cb969af2b63601ca68e1fc0b70aa08f97c6 Mon Sep 17 00:00:00 2001 From: brennanb2025 Date: Sun, 17 May 2026 17:46:47 -0700 Subject: [PATCH] fix: keep terminal set title pane-local Co-authored-by: Orca --- .../components/terminal-pane/TerminalPane.tsx | 149 ++++-------------- .../terminal-shutdown-layout-capture.test.ts | 4 +- .../terminal-shutdown-layout-capture.ts | 3 - src/renderer/src/store/slices/terminals.ts | 30 +--- src/shared/types.ts | 2 - src/shared/workspace-session-schema.test.ts | 3 +- src/shared/workspace-session-schema.ts | 3 +- tests/e2e/terminal-panes.spec.ts | 75 +++------ .../e2e/terminal-restart-persistence.spec.ts | 102 +----------- 9 files changed, 66 insertions(+), 305 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index 0c3222761c8..d79559244ab 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -230,8 +230,7 @@ export default function TerminalPane({ const [paneTitles, setPaneTitles] = useState>({}) const paneTitlesRef = useRef>({}) paneTitlesRef.current = paneTitles - const mirroredPaneTitleCustomTitleRef = useRef(null) - const previousPaneCountRef = useRef(null) + const removedTitleLeafIdsRef = useRef>(new Set()) const [renamingPaneId, setRenamingPaneId] = useState(null) const [renameValue, setRenameValue] = useState('') const renameInputRef = useRef(null) @@ -256,12 +255,6 @@ export default function TerminalPane({ const setTabLayout = useAppStore((store) => store.setTabLayout) const initialLayoutRef = useRef(savedLayout) const updateTabTitle = useAppStore((store) => store.updateTabTitle) - const setTabCustomTitle = useAppStore((store) => store.setTabCustomTitle) - const tabCustomTitle = useAppStore( - (store) => - (store.tabsByWorktree[worktreeId] ?? []).find((entry) => entry.id === tabId)?.customTitle ?? - null - ) const setRuntimePaneTitle = useAppStore((store) => store.setRuntimePaneTitle) const clearRuntimePaneTitle = useAppStore((store) => store.clearRuntimePaneTitle) const updateTabPtyId = useAppStore((store) => store.updateTabPtyId) @@ -322,25 +315,6 @@ export default function TerminalPane({ const dispatchNotification = useNotificationDispatch(worktreeId) const setCacheTimerStartedAt = useAppStore((store) => store.setCacheTimerStartedAt) - const getCurrentTabCustomTitle = useCallback((): string | null => { - const state = useAppStore.getState() - return ( - (state.tabsByWorktree[worktreeId] ?? []).find((entry) => entry.id === tabId)?.customTitle ?? - null - ) - }, [tabId, worktreeId]) - - const clearMirroredPaneTitleCustomTitle = useCallback((): void => { - const mirroredTitle = mirroredPaneTitleCustomTitleRef.current - if (!mirroredTitle) { - return - } - if (getCurrentTabCustomTitle() === mirroredTitle) { - setTabCustomTitle(tabId, null) - } - mirroredPaneTitleCustomTitleRef.current = null - }, [getCurrentTabCustomTitle, setTabCustomTitle, tabId]) - // Memoized with useCallback so downstream hooks (useTerminalKeyboardShortcuts, // useTerminalPaneLifecycle, createExpandCollapseActions) don't tear down and // re-register event listeners on every render. All data it reads comes from @@ -397,31 +371,30 @@ export default function TerminalPane({ // Preserve pane titles — uses the live React state (via ref) rather than // the stale Zustand value because React state reflects in-flight title // edits that haven't been persisted yet. - const titles = paneTitlesRef.current - const titleEntries = currentPanes - .filter((p) => titles[p.id]) - .map((p) => [p.leafId, titles[p.id]] as const) - if (titleEntries.length > 0) { - layout.titlesByLeafId = Object.fromEntries(titleEntries) + const titlesByLeafId: Record = {} + const removedTitleLeafIds = removedTitleLeafIdsRef.current + for (const pane of currentPanes) { + const existingTitle = existing?.titlesByLeafId?.[pane.leafId] + if (existingTitle && !removedTitleLeafIds.has(pane.leafId)) { + titlesByLeafId[pane.leafId] = existingTitle + } } - const existingMirrorCustomTitle = existing?.paneTitleMirroredCustomTitle?.trim() - const currentTabCustomTitle = getCurrentTabCustomTitle() - const restoredMirrorCustomTitle = - existingMirrorCustomTitle && - currentPanes.length === 1 && - currentTabCustomTitle === existingMirrorCustomTitle - ? existingMirrorCustomTitle - : null - const mirroredCustomTitle = mirroredPaneTitleCustomTitleRef.current ?? restoredMirrorCustomTitle - if ( - mirroredCustomTitle && - currentPanes.length === 1 && - currentTabCustomTitle === mirroredCustomTitle - ) { - layout.paneTitleMirroredCustomTitle = mirroredCustomTitle + // Why: active agents can trigger layout persists while pane-title React + // state is catching up. Preserve existing leaf titles unless this pane + // explicitly removed them, then overlay the live local pane-title state. + const titles = paneTitlesRef.current + for (const pane of currentPanes) { + const title = titles[pane.id] + if (title) { + titlesByLeafId[pane.leafId] = title + removedTitleLeafIds.delete(pane.leafId) + } + } + if (Object.keys(titlesByLeafId).length > 0) { + layout.titlesByLeafId = titlesByLeafId } setTabLayout(tabId, layout) - }, [getCurrentTabCustomTitle, tabId, setTabLayout]) + }, [tabId, setTabLayout]) const syncPanePtyLayoutBinding = useCallback( (paneId: number, ptyId: string | null): void => { @@ -988,58 +961,6 @@ export default function TerminalPane({ } }, [paneTitles, renamingPaneId]) - useEffect(() => { - const manager = managerRef.current - if (!manager || mirroredPaneTitleCustomTitleRef.current !== null) { - return - } - const panes = manager.getPanes() - if (panes.length !== 1) { - return - } - const paneTitle = paneTitles[panes[0].id]?.trim() - const mirroredCustomTitle = savedLayout?.paneTitleMirroredCustomTitle?.trim() - if (paneTitle && mirroredCustomTitle && tabCustomTitle === mirroredCustomTitle) { - // Why: after session restore, a single-pane title mirrored into - // customTitle before shutdown should still be invalidated if the user - // later splits the terminal. - mirroredPaneTitleCustomTitleRef.current = mirroredCustomTitle - previousPaneCountRef.current = panes.length - } - }, [paneCount, paneTitles, savedLayout?.paneTitleMirroredCustomTitle, tabCustomTitle]) - - useEffect(() => { - const mirroredTitle = mirroredPaneTitleCustomTitleRef.current - if (mirroredTitle && tabCustomTitle !== null && tabCustomTitle !== mirroredTitle) { - // Why: a tab-bar rename is a separate, explicit label owner. Once the - // value diverges from the pane-title mirror, later split/remove actions - // must not clear that unrelated customTitle. - mirroredPaneTitleCustomTitleRef.current = null - } - }, [tabCustomTitle]) - - useEffect(() => { - const currentPaneCount = managerRef.current?.getPanes().length ?? paneCount - const previousPaneCount = previousPaneCountRef.current - previousPaneCountRef.current = currentPaneCount - if (previousPaneCount !== null && previousPaneCount <= 1 && currentPaneCount > 1) { - clearMirroredPaneTitleCustomTitle() - } - }, [clearMirroredPaneTitleCustomTitle, paneCount]) - - useEffect(() => { - const mirroredTitle = mirroredPaneTitleCustomTitleRef.current - if (!mirroredTitle) { - return - } - if (savedLayout?.paneTitleMirroredCustomTitle !== mirroredTitle) { - // Why: an explicit tab rename is allowed to match the pane title text. - // The persisted owner marker, not text equality, decides whether later - // split/remove cleanup may clear customTitle. - mirroredPaneTitleCustomTitleRef.current = null - } - }, [savedLayout?.paneTitleMirroredCustomTitle]) - // Register a capture callback for shutdown. The beforeunload handler in // App.tsx calls all registered callbacks to serialize terminal buffers. useEffect(() => { @@ -1109,12 +1030,13 @@ export default function TerminalPane({ delete next[paneId] paneTitlesRef.current = next } - if ((managerRef.current?.getPanes().length ?? 1) <= 1) { - clearMirroredPaneTitleCustomTitle() + const leafId = managerRef.current?.getPanes().find((pane) => pane.id === paneId)?.leafId + if (leafId) { + removedTitleLeafIdsRef.current.add(leafId) } persistLayoutSnapshot() }, - [clearMirroredPaneTitleCustomTitle, persistLayoutSnapshot] + [persistLayoutSnapshot] ) const handleRenameSubmit = useCallback(() => { @@ -1136,25 +1058,14 @@ export default function TerminalPane({ // waiting for React to re-render and assign it during the next // render pass. paneTitlesRef.current = { ...paneTitlesRef.current, [renamingPaneId]: trimmed } - if ((managerRef.current?.getPanes().length ?? 1) <= 1) { - // Why: with one pane, the terminal context menu's title action is the - // tab title action users expect; customTitle keeps Codex OSC title churn - // from immediately replacing their label. - mirroredPaneTitleCustomTitleRef.current = trimmed - previousPaneCountRef.current = managerRef.current?.getPanes().length ?? 1 - setTabCustomTitle(tabId, trimmed, { preservePaneTitleMirror: true }) + const leafId = managerRef.current?.getPanes().find((pane) => pane.id === renamingPaneId)?.leafId + if (leafId) { + removedTitleLeafIdsRef.current.delete(leafId) } setRenamingPaneId(null) // Persist immediately so the title survives restarts. persistLayoutSnapshot() - }, [ - renamingPaneId, - renameValue, - removePaneTitle, - setTabCustomTitle, - tabId, - persistLayoutSnapshot - ]) + }, [renamingPaneId, renameValue, removePaneTitle, persistLayoutSnapshot]) const handleRenameCancel = useCallback(() => { renameSubmittedRef.current = true diff --git a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts index 15dbaedbba1..e9608dcd120 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts @@ -126,8 +126,7 @@ describe('captureTerminalShutdownLayout', () => { root: null, activeLeafId: null, expandedLeafId: null, - buffersByLeafId: { [LEAF_ID]: 'previous-local-scrollback' }, - paneTitleMirroredCustomTitle: 'local shell' + buffersByLeafId: { [LEAF_ID]: 'previous-local-scrollback' } }, captureBuffers: false }) @@ -137,6 +136,5 @@ describe('captureTerminalShutdownLayout', () => { expect(layout.buffersByLeafId).toBeUndefined() expect(layout.ptyIdsByLeafId).toEqual({ [LEAF_ID]: 'pty-1' }) expect(layout.titlesByLeafId).toEqual({ [LEAF_ID]: 'local shell' }) - expect(layout.paneTitleMirroredCustomTitle).toBe('local shell') }) }) diff --git a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts index 5cd75a31eb3..709af1fee8b 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts @@ -108,9 +108,6 @@ export function captureTerminalShutdownLayout({ if (titleEntries.length > 0) { layout.titlesByLeafId = Object.fromEntries(titleEntries) } - if (panes.length === 1 && existingLayout?.paneTitleMirroredCustomTitle) { - layout.paneTitleMirroredCustomTitle = existingLayout.paneTitleMirroredCustomTitle - } return layout } diff --git a/src/renderer/src/store/slices/terminals.ts b/src/renderer/src/store/slices/terminals.ts index e79982910fd..a368ee00367 100644 --- a/src/renderer/src/store/slices/terminals.ts +++ b/src/renderer/src/store/slices/terminals.ts @@ -221,11 +221,7 @@ export type TerminalSlice = { * model where the bell stays visible until the user engages with the * surface that raised it. */ clearTerminalTabUnread: (tabId: string) => void - setTabCustomTitle: ( - tabId: string, - title: string | null, - options?: { preservePaneTitleMirror?: boolean } - ) => void + setTabCustomTitle: (tabId: string, title: string | null) => void setTabColor: (tabId: string, color: string | null) => void updateTabPtyId: (tabId: string, ptyId: string) => void clearTabPtyId: (tabId: string, ptyId?: string) => void @@ -962,34 +958,14 @@ export const createTerminalSlice: StateCreator }) }, - setTabCustomTitle: (tabId, title, options) => { + setTabCustomTitle: (tabId, title) => { set((s) => { const next = { ...s.tabsByWorktree } for (const wId of Object.keys(next)) { next[wId] = next[wId].map((t) => (t.id === tabId ? { ...t, customTitle: title } : t)) } - let terminalLayoutsByTabId = s.terminalLayoutsByTabId - const layout = s.terminalLayoutsByTabId[tabId] - if (options?.preservePaneTitleMirror && title) { - terminalLayoutsByTabId = { - ...s.terminalLayoutsByTabId, - [tabId]: { - ...(layout ?? emptyLayoutSnapshot()), - paneTitleMirroredCustomTitle: title - } - } - } else if (layout?.paneTitleMirroredCustomTitle !== undefined) { - const { paneTitleMirroredCustomTitle: _mirrorOwner, ...layoutWithoutMirrorOwner } = layout - // Why: generic tab renames, including same-text renames, are explicit - // owners. TerminalPane immediately restores this marker when Set Title - // is the caller that is intentionally mirroring a single-pane title. - terminalLayoutsByTabId = { - ...s.terminalLayoutsByTabId, - [tabId]: layoutWithoutMirrorOwner - } - } scheduleRuntimeGraphSync() - return { tabsByWorktree: next, terminalLayoutsByTabId } + return { tabsByWorktree: next } }) const item = Object.values(get().unifiedTabsByWorktree) .flat() diff --git a/src/shared/types.ts b/src/shared/types.ts index d01811238d2..c537f64806f 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -485,8 +485,6 @@ export type TerminalLayoutSnapshot = { /** User-assigned pane titles, keyed by stable layout leaf UUID. * Persisted alongside buffers via the existing session:set flow. */ titlesByLeafId?: Record - /** Single-pane title mirrored into the tab custom title by Terminal Set Title. */ - paneTitleMirroredCustomTitle?: string } /** Minimal subset of OpenFile persisted across restarts. diff --git a/src/shared/workspace-session-schema.test.ts b/src/shared/workspace-session-schema.test.ts index 8db1cf6d87f..0bcc95b4890 100644 --- a/src/shared/workspace-session-schema.test.ts +++ b/src/shared/workspace-session-schema.test.ts @@ -43,8 +43,7 @@ describe('parseWorkspaceSession', () => { }, activeLeafId: 'pane:1', expandedLeafId: null, - ptyIdsByLeafId: { 'pane:1': 'daemon-session-A' }, - paneTitleMirroredCustomTitle: 'build' + ptyIdsByLeafId: { 'pane:1': 'daemon-session-A' } } }, activeWorktreeIdsOnShutdown: ['repo1::/path/wt1'] diff --git a/src/shared/workspace-session-schema.ts b/src/shared/workspace-session-schema.ts index 063c3eed540..97d40e72ab5 100644 --- a/src/shared/workspace-session-schema.ts +++ b/src/shared/workspace-session-schema.ts @@ -49,8 +49,7 @@ const terminalLayoutSnapshotSchema = z.object({ expandedLeafId: z.string().nullable(), ptyIdsByLeafId: z.record(z.string(), z.string()).optional(), buffersByLeafId: z.record(z.string(), z.string()).optional(), - titlesByLeafId: z.record(z.string(), z.string()).optional(), - paneTitleMirroredCustomTitle: z.string().optional() + titlesByLeafId: z.record(z.string(), z.string()).optional() }) // ─── Terminal tab (legacy) ────────────────────────────────────────── diff --git a/tests/e2e/terminal-panes.spec.ts b/tests/e2e/terminal-panes.spec.ts index 20a26078f94..a1f677ef368 100644 --- a/tests/e2e/terminal-panes.spec.ts +++ b/tests/e2e/terminal-panes.spec.ts @@ -83,19 +83,6 @@ async function expectTabCustomTitle( .toBe(expected) } -async function setTabCustomTitleDirect( - page: Page, - tabId: string, - title: string | null -): Promise { - await page.evaluate( - ({ targetTabId, title }) => { - window.__store!.getState().setTabCustomTitle(targetTabId, title) - }, - { targetTabId: tabId, title } - ) -} - async function expectSavedLayoutNotToContainTitle( page: Page, tabId: string, @@ -216,54 +203,54 @@ test.describe('Terminal Panes', () => { expect(activeLeafId).toMatch(UUID_RE) }) - test('Set Title ownership survives agent churn and preserves split semantics', async ({ - orcaPage - }) => { + test('Set Title stays pane-local during agent title churn', async ({ orcaPage }) => { const worktreeId = (await getActiveWorktreeId(orcaPage))! - let tabId = (await getActiveTabId(orcaPage))! - const customTitle = `Codex label ${Date.now()}` + const tabId = (await getActiveTabId(orcaPage))! + const paneTitle = `Codex pane ${Date.now()}` const removeButtonTitle = `Remove button label ${Date.now()}` const splitTitle = `Split label ${Date.now()}` - const runtimeTitle = '⠋ Codex split working' - const explicitTitle = `Explicit tab ${Date.now()}` - const paneOnlyTitle = `Pane only ${Date.now()}` + const runtimeTitle = '⠋ Codex working' - await setPaneTitleFromTerminalMenu(orcaPage, customTitle) - await expectTabCustomTitle(orcaPage, worktreeId, tabId, customTitle) + await setPaneTitleFromTerminalMenu(orcaPage, paneTitle) + await expect(orcaPage.locator('.pane-title-text', { hasText: paneTitle })).toBeVisible() + await expectTabCustomTitle(orcaPage, worktreeId, tabId, null) - await orcaPage.getByRole('button', { name: `Edit pane title: ${customTitle}` }).focus() + await orcaPage.getByRole('button', { name: `Edit pane title: ${paneTitle}` }).focus() await orcaPage.keyboard.press('Enter') await expect(orcaPage.getByRole('textbox', { name: 'Pane title' })).toBeVisible() await orcaPage.keyboard.press('Escape') - await orcaPage.evaluate((targetTabId) => { - window.__store!.getState().updateTabTitle(targetTabId, '⠋ Codex working') - }, tabId) + await orcaPage.evaluate( + ({ targetTabId, title }) => { + window.__store!.getState().updateTabTitle(targetTabId, title) + }, + { targetTabId: tabId, title: runtimeTitle } + ) - // Why: active agent CLIs continuously write OSC titles. A user-set label - // must stay visible as customTitle while the live title keeps changing. + // Why: active agents continuously write OSC titles. Set Title is Orca's + // pane-local overlay and must remain visible while the tab runtime title + // continues to follow the active PTY. + await expect(orcaPage.locator('.pane-title-text', { hasText: paneTitle })).toBeVisible() await expect( orcaPage.locator(`[data-testid="sortable-tab"][data-tab-id="${tabId}"]`) - ).toHaveAttribute('data-tab-title', customTitle) + ).toHaveAttribute('data-tab-title', runtimeTitle) + await expectTabCustomTitle(orcaPage, worktreeId, tabId, null) await setPaneTitleFromTerminalMenu(orcaPage, '') - await expectTabCustomTitle(orcaPage, worktreeId, tabId, null) - await expect(orcaPage.locator('.pane-title-text', { hasText: customTitle })).toBeHidden() - await expectSavedLayoutNotToContainTitle(orcaPage, tabId, customTitle) + await expect(orcaPage.locator('.pane-title-text', { hasText: paneTitle })).toBeHidden() + await expectSavedLayoutNotToContainTitle(orcaPage, tabId, paneTitle) await setPaneTitleFromTerminalMenu(orcaPage, removeButtonTitle) await orcaPage.locator('.pane-title-bar', { hasText: removeButtonTitle }).hover() await orcaPage.getByRole('button', { name: `Remove pane title: ${removeButtonTitle}` }).click() - await expectTabCustomTitle(orcaPage, worktreeId, tabId, null) await expect(orcaPage.locator('.pane-title-text', { hasText: removeButtonTitle })).toBeHidden() await expectSavedLayoutNotToContainTitle(orcaPage, tabId, removeButtonTitle) await setPaneTitleFromTerminalMenu(orcaPage, splitTitle) - await expectTabCustomTitle(orcaPage, worktreeId, tabId, splitTitle) + await expectTabCustomTitle(orcaPage, worktreeId, tabId, null) await splitActiveTerminalPane(orcaPage, 'vertical') await waitForPaneCount(orcaPage, 2) - await expectTabCustomTitle(orcaPage, worktreeId, tabId, null) await expect(orcaPage.locator('.pane-title-text', { hasText: splitTitle })).toBeVisible() await orcaPage.evaluate( @@ -275,22 +262,6 @@ test.describe('Terminal Panes', () => { await expect( orcaPage.locator(`[data-testid="sortable-tab"][data-tab-id="${tabId}"]`) ).toHaveAttribute('data-tab-title', runtimeTitle) - - await orcaPage.evaluate((targetWorktreeId) => { - window.__store!.getState().createTab(targetWorktreeId) - }, worktreeId) - await waitForPaneCount(orcaPage, 1) - tabId = (await getActiveTabId(orcaPage))! - - await setPaneTitleFromTerminalMenu(orcaPage, `Mirror before explicit ${Date.now()}`) - await setTabCustomTitleDirect(orcaPage, tabId, explicitTitle) - await splitActiveTerminalPane(orcaPage, 'vertical') - await waitForPaneCount(orcaPage, 2) - await expectTabCustomTitle(orcaPage, worktreeId, tabId, explicitTitle) - - await setPaneTitleFromTerminalMenu(orcaPage, paneOnlyTitle) - await expect(orcaPage.locator('.pane-title-text', { hasText: paneOnlyTitle })).toBeVisible() - await expectTabCustomTitle(orcaPage, worktreeId, tabId, explicitTitle) }) test('closing a split pane prunes its leaf-keyed PTY binding without remapping siblings', async ({ diff --git a/tests/e2e/terminal-restart-persistence.spec.ts b/tests/e2e/terminal-restart-persistence.spec.ts index d2c7f0b1366..de508e86b18 100644 --- a/tests/e2e/terminal-restart-persistence.spec.ts +++ b/tests/e2e/terminal-restart-persistence.spec.ts @@ -135,26 +135,6 @@ async function getTabCustomTitle( ) } -async function setTabCustomTitleDirect( - page: Page, - tabId: string, - title: string | null -): Promise { - await page.evaluate( - ({ targetTabId, title }) => { - window.__store!.getState().setTabCustomTitle(targetTabId, title) - }, - { targetTabId: tabId, title } - ) -} - -async function getSavedLayoutMirrorTitle(page: Page, tabId: string): Promise { - return page.evaluate((targetTabId) => { - const layout = window.__store!.getState().terminalLayoutsByTabId[targetTabId] - return layout?.paneTitleMirroredCustomTitle ?? null - }, tabId) -} - async function expectSavedLayoutToContainTitle( page: Page, tabId: string, @@ -301,7 +281,7 @@ test.describe('Terminal restart persistence', () => { } }) - test('restored single-pane Set Title ownership clears after split', async (// oxlint-disable-next-line no-empty-pattern -- Playwright's second fixture arg is testInfo; the first must be an object destructure to opt out of the default fixture set. + test('restored Set Title pane label survives agent title churn', async (// oxlint-disable-next-line no-empty-pattern -- Playwright's second fixture arg is testInfo; the first must be an object destructure to opt out of the default fixture set. {}, testInfo) => { const repoPath = readFileSync(TEST_REPO_PATH_FILE, 'utf-8').trim() if (!repoPath || !existsSync(repoPath)) { @@ -325,7 +305,7 @@ test.describe('Terminal restart persistence', () => { .poll(() => getTabCustomTitle(firstLaunch.page, worktreeId, firstTabId), { timeout: 3_000 }) - .toBe(title) + .toBe(null) await session.close(firstApp) firstApp = null @@ -336,27 +316,14 @@ test.describe('Terminal restart persistence', () => { const restoredTabId = (await getActiveTabId(secondLaunch.page))! await expect(secondLaunch.page.locator('.pane-title-text', { hasText: title })).toBeVisible() - await expect - .poll(() => getTabCustomTitle(secondLaunch.page, worktreeId, restoredTabId), { - timeout: 3_000 - }) - .toBe(title) - await expectSavedLayoutToContainTitle(secondLaunch.page, restoredTabId, title) - await expect - .poll(() => getSavedLayoutMirrorTitle(secondLaunch.page, restoredTabId), { - timeout: 3_000 - }) - .toBe(title) - - await splitActiveTerminalPane(secondLaunch.page, 'vertical') - await waitForPaneCount(secondLaunch.page, 2) await expect .poll(() => getTabCustomTitle(secondLaunch.page, worktreeId, restoredTabId), { timeout: 3_000 }) .toBe(null) + await expectSavedLayoutToContainTitle(secondLaunch.page, restoredTabId, title) - const runtimeTitle = '⠋ Codex restored split working' + const runtimeTitle = '⠋ Codex restored working' await secondLaunch.page.evaluate( ({ targetTabId, title }) => { window.__store!.getState().updateTabTitle(targetTabId, title) @@ -366,71 +333,16 @@ test.describe('Terminal restart persistence', () => { await expect( secondLaunch.page.locator(`[data-testid="sortable-tab"][data-tab-id="${restoredTabId}"]`) ).toHaveAttribute('data-tab-title', runtimeTitle) - } finally { - if (secondApp) { - await session.close(secondApp) - } - if (firstApp) { - await session.close(firstApp) - } - await session.dispose() - } - }) - - test('restored same-text explicit tab title survives split', async (// oxlint-disable-next-line no-empty-pattern -- Playwright's second fixture arg is testInfo; the first must be an object destructure to opt out of the default fixture set. - {}, testInfo) => { - const repoPath = readFileSync(TEST_REPO_PATH_FILE, 'utf-8').trim() - if (!repoPath || !existsSync(repoPath)) { - test.skip(true, 'Global setup did not produce a seeded test repo') - return - } - - const session = createRestartSession(testInfo) - let firstApp: ElectronApplication | null = null - let secondApp: ElectronApplication | null = null - - try { - const firstLaunch = await session.launch() - firstApp = firstLaunch.app - const { worktreeId } = await bootstrapFirstLaunch(firstLaunch.page, repoPath) - const title = `Explicit same label ${Date.now()}` - const firstTabId = (await getActiveTabId(firstLaunch.page))! - - await setPaneTitleFromTerminalMenu(firstLaunch.page, title) + await expect(secondLaunch.page.locator('.pane-title-text', { hasText: title })).toBeVisible() await expect - .poll(() => getTabCustomTitle(firstLaunch.page, worktreeId, firstTabId), { - timeout: 3_000 - }) - .toBe(title) - - await session.close(firstApp) - firstApp = null - - const secondLaunch = await session.launch() - secondApp = secondLaunch.app - await bootstrapRestoredLaunch(secondLaunch.page, worktreeId) - const restoredTabId = (await getActiveTabId(secondLaunch.page))! - - await expect - .poll(() => getSavedLayoutMirrorTitle(secondLaunch.page, restoredTabId), { - timeout: 3_000 - }) - .toBe(title) - - await setTabCustomTitleDirect(secondLaunch.page, restoredTabId, title) - await expect - .poll(() => getSavedLayoutMirrorTitle(secondLaunch.page, restoredTabId), { + .poll(() => getTabCustomTitle(secondLaunch.page, worktreeId, restoredTabId), { timeout: 3_000 }) .toBe(null) await splitActiveTerminalPane(secondLaunch.page, 'vertical') await waitForPaneCount(secondLaunch.page, 2) - await expect - .poll(() => getTabCustomTitle(secondLaunch.page, worktreeId, restoredTabId), { - timeout: 3_000 - }) - .toBe(title) + await expect(secondLaunch.page.locator('.pane-title-text', { hasText: title })).toBeVisible() } finally { if (secondApp) { await session.close(secondApp)