mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 08:02:52 +00:00
fix: keep terminal set title pane-local
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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) ──────────────────────────────────────────
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user