fix: keep terminal set title pane-local

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-05-17 17:46:47 -07:00
co-authored by Orca
parent d9ad009820
commit a34b4cb969
9 changed files with 66 additions and 305 deletions
@@ -230,8 +230,7 @@ export default function TerminalPane({
const [paneTitles, setPaneTitles] = useState<Record<number, string>>({})
const paneTitlesRef = useRef<Record<number, string>>({})
paneTitlesRef.current = paneTitles
const mirroredPaneTitleCustomTitleRef = useRef<string | null>(null)
const previousPaneCountRef = useRef<number | null>(null)
const removedTitleLeafIdsRef = useRef<Set<string>>(new Set())
const [renamingPaneId, setRenamingPaneId] = useState<number | null>(null)
const [renameValue, setRenameValue] = useState('')
const renameInputRef = useRef<HTMLInputElement>(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<string, string> = {}
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
@@ -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')
})
})
@@ -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
}
+3 -27
View File
@@ -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<AppState, [], [], TerminalSlice>
})
},
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()
-2
View File
@@ -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<string, string>
/** Single-pane title mirrored into the tab custom title by Terminal Set Title. */
paneTitleMirroredCustomTitle?: string
}
/** Minimal subset of OpenFile persisted across restarts.
+1 -2
View File
@@ -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']
+1 -2
View File
@@ -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) ──────────────────────────────────────────
+23 -52
View File
@@ -83,19 +83,6 @@ async function expectTabCustomTitle(
.toBe(expected)
}
async function setTabCustomTitleDirect(
page: Page,
tabId: string,
title: string | null
): Promise<void> {
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 ({
+7 -95
View File
@@ -135,26 +135,6 @@ async function getTabCustomTitle(
)
}
async function setTabCustomTitleDirect(
page: Page,
tabId: string,
title: string | null
): Promise<void> {
await page.evaluate(
({ targetTabId, title }) => {
window.__store!.getState().setTabCustomTitle(targetTabId, title)
},
{ targetTabId: tabId, title }
)
}
async function getSavedLayoutMirrorTitle(page: Page, tabId: string): Promise<string | null> {
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)