From ef8eb625ca8d4ee78cd57a841b42fd7a34feacd6 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sun, 18 Oct 2026 03:06:23 -0400 Subject: [PATCH] fix(layout): tab reconcile and terminal reconnect stop writing tab structure (dark, layout-owner build only) Main's frames draw the tab bar and rows: reconcile still returns its counts but writes nothing, and reconnect still advertises restored terminals but leaves the rows to main. --- .../layout-owner-shared-path-gates.test.ts | 69 +++++++++++++++++++ .../store/slices/tabs/tabs-session-actions.ts | 8 ++- .../terminals/workspace-terminal-reconnect.ts | 6 +- 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/store/slices/tabs/layout-owner-shared-path-gates.test.ts diff --git a/src/renderer/src/store/slices/tabs/layout-owner-shared-path-gates.test.ts b/src/renderer/src/store/slices/tabs/layout-owner-shared-path-gates.test.ts new file mode 100644 index 00000000000..187a683384e --- /dev/null +++ b/src/renderer/src/store/slices/tabs/layout-owner-shared-path-gates.test.ts @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createTabsSliceMockApi } from '../tabs-slice-test-harness' +import { createTestStore, makeTab } from '../store-test-helpers' + +vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) +vi.mock('../../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) + +createTabsSliceMockApi() + +const WT = 'repo1::/tmp/feature' + +// Main draws the tab bar from its frames; these shared paths must not write structure of their own. +describe('shared tab paths in a layout-owner build', () => { + let store: ReturnType + + beforeEach(() => { + store = createTestStore() + }) + + it('reconcile counts what would render and leaves the tab bar to main', () => { + const entry = { + id: 'stale-terminal', + entityId: 'stale-terminal', + groupId: 'g-1', + worktreeId: WT, + contentType: 'terminal' as const, + label: 'Terminal 1', + customLabel: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + store.setState({ + unifiedTabsByWorktree: { [WT]: [entry] }, + groupsByWorktree: { + [WT]: [{ id: 'g-1', worktreeId: WT, activeTabId: entry.id, tabOrder: [entry.id] }] + }, + activeGroupIdByWorktree: { [WT]: 'g-1' }, + tabsByWorktree: { [WT]: [] } + }) + const before = store.getState() + + expect(store.getState().reconcileWorktreeTabModel(WT)).toEqual({ + renderableTabCount: 0, + activeRenderableTabId: null + }) + store.getState().reconcileWorktreeTabModels([WT]) + + expect(store.getState().unifiedTabsByWorktree).toBe(before.unifiedTabsByWorktree) + expect(store.getState().groupsByWorktree).toBe(before.groupsByWorktree) + }) + + it('reconnect advertises restored terminals without rewriting their rows', async () => { + const rows = { [WT]: [makeTab({ id: 'tab-1', worktreeId: WT, ptyId: null })] } + store.setState({ + tabsByWorktree: rows, + terminalLayoutsByTabId: {}, + pendingReconnectWorktreeIds: [WT], + pendingReconnectTabByWorktree: { [WT]: ['tab-1'] }, + pendingReconnectPtyIdByTabId: { 'tab-1': 'pty-1' } + }) + + await store.getState().reconnectPersistedTerminals() + + expect(store.getState().ptyIdsByTabId['tab-1']).toEqual(['pty-1']) + expect(store.getState().tabsByWorktree).toBe(rows) + expect(store.getState().workspaceSessionReady).toBe(true) + }) +}) diff --git a/src/renderer/src/store/slices/tabs/tabs-session-actions.ts b/src/renderer/src/store/slices/tabs/tabs-session-actions.ts index 43955147687..6fb187f53aa 100644 --- a/src/renderer/src/store/slices/tabs/tabs-session-actions.ts +++ b/src/renderer/src/store/slices/tabs/tabs-session-actions.ts @@ -10,6 +10,7 @@ import { buildHydratedTabState } from '../tabs-hydration' import { projectWorktreeTabModelReconciliation } from './tabs-reconciliation' import { createWorktreeTabModelReconciliationBatch } from './tabs-reconciliation-batch' import type { AppState } from '../../types' +import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../../shared/layout-owner-switch' function replaceWorkspaceRecordKeys( current: Record, @@ -59,6 +60,10 @@ export function createTabsSessionActions( if (worktreeIds.length === 0) { return } + // Switch point: main's frames draw the tab bar, so reconcile writes no structure of its own. + if (ORCA_LAYOUT_OWNER_IS_MAIN) { + return + } const patch = projectWorktreeTabModelReconciliations(get(), worktreeIds) if (Object.keys(patch).length > 0) { set(patch) @@ -67,7 +72,8 @@ export function createTabsSessionActions( reconcileWorktreeTabModel: (worktreeId) => { const reconciliation = projectWorktreeTabModelReconciliation(get(), worktreeId) - if (Object.keys(reconciliation.patch).length > 0) { + // Switch point: the counts only; main's frames draw the tab bar. + if (!ORCA_LAYOUT_OWNER_IS_MAIN && Object.keys(reconciliation.patch).length > 0) { set(reconciliation.patch) } return { diff --git a/src/renderer/src/store/terminals/workspace-terminal-reconnect.ts b/src/renderer/src/store/terminals/workspace-terminal-reconnect.ts index 7c4a916ab41..df2c5dc2494 100644 --- a/src/renderer/src/store/terminals/workspace-terminal-reconnect.ts +++ b/src/renderer/src/store/terminals/workspace-terminal-reconnect.ts @@ -2,6 +2,7 @@ import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { buildByIdIndex, buildWorktreeByIdIndex } from '../slices/worktree-by-id-index' import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' import type { TerminalSlice, TerminalStoreGet, TerminalStoreSet } from './terminal-state' +import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch' export function createWorkspaceTerminalReconnectActions( set: TerminalStoreSet, @@ -117,7 +118,10 @@ export function createWorkspaceTerminalReconnectActions( return } set({ - ...(reconnectedTabsByWorktree ? { tabsByWorktree: reconnectedTabsByWorktree } : {}), + // Switch point: main's frames draw the rows, with main's own bindings. + ...(reconnectedTabsByWorktree && !ORCA_LAYOUT_OWNER_IS_MAIN + ? { tabsByWorktree: reconnectedTabsByWorktree } + : {}), ...(reconnectedPtyIdsByTabId ? { ptyIdsByTabId: reconnectedPtyIdsByTabId } : {}), workspaceSessionReady: true, pendingReconnectWorktreeIds: [],