Fix grok stale pane width (#13060)

* fix(terminal): restore canonical terminal rows from unified sessions

During session hydration, non-canonical terminal rows (closed/stale tabs)
were being restored alongside canonical ones. Filter to only canonical
terminal rows from unified sessions, treating legacy rows as terminal
backing data only.

* fix(terminal): preserve independent legacy PTY rows
This commit is contained in:
Jinjing 2026-08-07 11:42:40 -07:00 committed by GitHub
parent 2a2b517a85
commit 58531e4caa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 159 additions and 1 deletions

View File

@ -0,0 +1,130 @@
import { describe, expect, it, vi } from 'vitest'
import type { WorkspaceSessionState } from '../../../../shared/types'
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
import { buildWorkspaceSessionPayload } from '@/lib/workspace-session'
import { createTestStore, makeLayout, makeTab, makeWorktree, seedStore } from './store-test-helpers'
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
vi.mock('@/runtime/sync-runtime-graph', () => ({
scheduleRuntimeGraphSync: vi.fn()
}))
vi.mock('@/components/terminal-pane/pty-transport', () => ({
registerEagerPtyBuffer: vi.fn(),
ensurePtyDispatcher: vi.fn()
}))
const apiProxy = (): unknown =>
new Proxy(() => undefined, {
get: (_target, prop) => (prop === 'then' ? undefined : apiProxy()),
apply: () => Promise.resolve(null)
})
// @ts-expect-error -- mocked browser preload API
globalThis.window = { api: apiProxy() }
describe('hydrateWorkspaceSession canonical terminal rows', () => {
it('drops only legacy rows that duplicate canonical PTY ownership', () => {
const store = createTestStore()
const worktreeId = 'repo1::/wt-1'
const sharedPtyId = 'daemon-session-1'
const recoveryPtyId = 'daemon-session-2'
seedStore(store, {
worktreesByRepo: {
repo1: [makeWorktree({ id: worktreeId, repoId: 'repo1', path: '/wt-1' })]
}
})
const session: WorkspaceSessionState = {
...getDefaultWorkspaceSession(),
activeRepoId: 'repo1',
activeWorktreeId: worktreeId,
activeTabId: 'stale-tab',
activeWorktreeIdsOnShutdown: [worktreeId],
activeTabIdByWorktree: { [worktreeId]: 'stale-tab' },
tabsByWorktree: {
[worktreeId]: [
makeTab({ id: 'canonical-tab', worktreeId, ptyId: sharedPtyId }),
makeTab({ id: 'stale-tab', worktreeId, ptyId: sharedPtyId }),
makeTab({ id: 'recovery-tab', worktreeId, ptyId: recoveryPtyId })
]
},
terminalLayoutsByTabId: {
'canonical-tab': {
...makeLayout(),
ptyIdsByLeafId: { 'canonical-leaf': sharedPtyId }
},
'stale-tab': {
...makeLayout(),
ptyIdsByLeafId: { 'stale-leaf': sharedPtyId }
},
'recovery-tab': {
...makeLayout(),
ptyIdsByLeafId: { 'recovery-leaf': recoveryPtyId }
}
},
remoteSessionIdsByTabId: {
'canonical-tab': sharedPtyId,
'stale-tab': sharedPtyId,
'recovery-tab': recoveryPtyId
},
unifiedTabs: {
[worktreeId]: [
{
id: 'canonical-unified-tab',
entityId: 'canonical-tab',
groupId: 'group-1',
worktreeId,
contentType: 'terminal',
label: 'Grok',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
},
tabGroups: {
[worktreeId]: [
{
id: 'group-1',
worktreeId,
activeTabId: 'canonical-unified-tab',
tabOrder: ['canonical-unified-tab']
}
]
}
}
store.getState().hydrateWorkspaceSession(session)
store.getState().hydrateTabsSession(session)
const reconciliation = store.getState().reconcileWorktreeTabModel(worktreeId)
const state = store.getState()
const persisted = buildWorkspaceSessionPayload(state)
expect(reconciliation.renderableTabCount).toBe(2)
expect(state.unifiedTabsByWorktree[worktreeId]?.map((tab) => tab.entityId)).toEqual([
'canonical-tab',
'recovery-tab'
])
expect(state.tabsByWorktree[worktreeId]?.map((tab) => tab.id)).toEqual([
'canonical-tab',
'recovery-tab'
])
expect(state.terminalLayoutsByTabId['stale-tab']).toBeUndefined()
expect(persisted.tabsByWorktree[worktreeId]?.map((tab) => tab.id)).toEqual([
'canonical-tab',
'recovery-tab'
])
expect(persisted.terminalLayoutsByTabId['stale-tab']).toBeUndefined()
expect(state.pendingReconnectTabByWorktree[worktreeId]).toEqual([
'canonical-tab',
'recovery-tab'
])
expect(state.pendingReconnectPtyIdByTabId).toEqual({
'canonical-tab': sharedPtyId,
'recovery-tab': recoveryPtyId
})
expect(state.activeTabId).toBeNull()
expect(state.activeTabIdByWorktree).toEqual({})
})
})

View File

@ -492,6 +492,17 @@ function uniquePtyIds(ptyIds: readonly (string | null | undefined)[]): string[]
return [...new Set(ptyIds.filter((ptyId): ptyId is string => Boolean(ptyId)))]
}
function collectPersistedTerminalPtyIds(
session: WorkspaceSessionState,
tab: TerminalTab
): string[] {
return uniquePtyIds([
tab.ptyId,
session.remoteSessionIdsByTabId?.[tab.id],
...Object.values(session.terminalLayoutsByTabId[tab.id]?.ptyIdsByLeafId ?? {})
])
}
function resolvePrimaryLayoutPtyId(layout: TerminalLayoutSnapshot): string | null {
const ptyIdsByLeafId = layout.ptyIdsByLeafId ?? {}
const activePtyId = layout.activeLeafId ? ptyIdsByLeafId[layout.activeLeafId] : undefined
@ -3943,6 +3954,16 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
Object.entries(session.tabsByWorktree)
.filter(([worktreeId]) => validWorktreeIds.has(worktreeId))
.map(([worktreeId, tabs]) => {
const canonicalTerminalIds = new Set(
(session.unifiedTabs?.[worktreeId] ?? []).flatMap((tab) =>
tab.contentType === 'terminal' ? [tab.entityId] : []
)
)
const canonicalPtyIds = new Set(
tabs
.filter((tab) => canonicalTerminalIds.has(tab.id))
.flatMap((tab) => collectPersistedTerminalPtyIds(session, tab))
)
const quickCommandLabelByTerminalId = new Map(
(session.unifiedTabs?.[worktreeId] ?? [])
.filter((tab) => tab.contentType === 'terminal' && tab.quickCommandLabel?.trim())
@ -3957,8 +3978,15 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
worktreeId,
[...tabs]
.filter((tab) => {
// Why: canonical mounts win PTY ownership over stale legacy duplicates.
// Why: old web-client mirrors could persist host surface ids with "::"; makePaneKey reserves ":" as its separator.
return isValidTerminalTabId(tab.id)
return (
(canonicalTerminalIds.has(tab.id) ||
!collectPersistedTerminalPtyIds(session, tab).some((ptyId) =>
canonicalPtyIds.has(ptyId)
)) &&
isValidTerminalTabId(tab.id)
)
})
.sort((a, b) => a.sortOrder - b.sortOrder || a.createdAt - b.createdAt)
.map((tab, index) => {