perf: reuse chat tab ownership across unrelated app updates (#26150)

This commit is contained in:
Neil
2026-10-08 00:21:54 -07:00
committed by GitHub
parent 07c2379395
commit 67dbd2fd06
7 changed files with 413 additions and 10 deletions
@@ -49,7 +49,7 @@ import {
emptyNativeChatContextMenuActions,
useNativeChatContextMenu
} from './use-native-chat-context-menu'
import { selectNativeChatRuntimeEnvironmentId } from './native-chat-runtime-owner'
import { createNativeChatRuntimeSelector } from './native-chat-runtime-owner'
import { useNativeChatPasteBridge } from './use-native-chat-paste-bridge'
import { LinkActionPopover } from '@/components/link-actions/LinkActionPopover'
import { useNativeChatLinkActions } from './use-native-chat-link-actions'
@@ -77,9 +77,8 @@ export function NativeChatResolvedView({
}: NativeChatResolvedViewProps): React.JSX.Element {
// Primitive owner selection (no useShallow): routes the pane's read/subscribe to
// the remote runtime host for a runtime-owned pane; null keeps the local path.
const runtimeEnvironmentId = useAppStore((s) =>
selectNativeChatRuntimeEnvironmentId(s, terminalTabId)
)
const selectOwner = useMemo(() => createNativeChatRuntimeSelector(terminalTabId), [terminalTabId])
const runtimeEnvironmentId = useAppStore(selectOwner)
const keybindings = useAppStore((s) => s.keybindings)
const session = useNativeChatRetainedSession({
paneKey,
@@ -71,11 +71,43 @@ export function findNativeChatTabOwnerWorktreeId(
)
}
export function createNativeChatTabOwnerSelector(tabId: string) {
let terminalTabs: NativeChatFileLinkState['tabsByWorktree'] | null = null
let unifiedTabs: NativeChatFileLinkState['unifiedTabsByWorktree']
let owner: string | null = null
return (state: Pick<NativeChatFileLinkState, 'tabsByWorktree' | 'unifiedTabsByWorktree'>) => {
// Hidden chats still subscribe; ownership changes only with the immutable tab maps.
if (terminalTabs !== state.tabsByWorktree || unifiedTabs !== state.unifiedTabsByWorktree) {
owner = findNativeChatTabOwnerWorktreeId(state, tabId)
terminalTabs = state.tabsByWorktree
unifiedTabs = state.unifiedTabsByWorktree
}
return owner
}
}
export function resolveNativeChatFileLinkContext(
state: NativeChatFileLinkState,
terminalTabId: string
): NativeChatFileLinkContext | null {
const worktreeId = findNativeChatTabOwnerWorktreeId(state, terminalTabId)
return resolveNativeChatFileLinkContextForOwner(
state,
terminalTabId,
findNativeChatTabOwnerWorktreeId(state, terminalTabId)
)
}
export function createNativeChatFileLinkContextSelector(tabId: string) {
const selectOwner = createNativeChatTabOwnerSelector(tabId)
return (state: NativeChatFileLinkState) =>
resolveNativeChatFileLinkContextForOwner(state, tabId, selectOwner(state))
}
function resolveNativeChatFileLinkContextForOwner(
state: NativeChatFileLinkState,
terminalTabId: string,
worktreeId: string | null
): NativeChatFileLinkContext | null {
if (!worktreeId) {
return null
}
@@ -0,0 +1,232 @@
// @vitest-environment happy-dom
import { act, cleanup, renderHook } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultSettings } from '../../../../shared/constants'
import type { Tab } from '../../../../shared/tab-types'
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import type { Worktree } from '../../../../shared/worktree/types'
import {
createNativeChatFileLinkContextSelector,
createNativeChatTabOwnerSelector,
findNativeChatTabOwnerWorktreeId,
resolveNativeChatFileLinkContext
} from './native-chat-file-link'
import { useNativeChatTabOwnerWorktreeId } from './use-native-chat-tab-owner'
import { useNativeChatFileLinkContext } from './use-native-chat-file-link-context'
import { useAppStore } from '@/store'
type ContextState = Parameters<typeof resolveNativeChatFileLinkContext>[0]
vi.mock('@/store', async () => {
const { create } = await import('zustand')
const { getDefaultSettings } = await import('../../../../shared/constants')
return {
useAppStore: create<ContextState>(() => ({
settings: getDefaultSettings('/home/test'),
repos: [],
projectGroups: [],
folderWorkspaces: [],
worktreesByRepo: {},
tabsByWorktree: {},
unifiedTabsByWorktree: {},
getKnownWorktreeById: () => undefined
}))
}
})
afterEach(cleanup)
function terminal(id: string): TerminalTab {
return {
id,
ptyId: null,
worktreeId: 'terminal-owner',
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 0
}
}
function structured(id: string, contentType: Tab['contentType'] = 'agent-session'): Tab {
return {
id,
entityId: `session-${id}`,
worktreeId: 'chat-owner',
groupId: 'group',
contentType,
agentSessionAgent: 'codex',
label: 'Chat',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
}
}
function worktree(id: string, path = '/repo'): Worktree {
return {
id,
repoId: 'repo',
path,
displayName: id,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
head: '',
branch: 'main',
isBare: false,
isMainWorktree: false
}
}
function contextState(overrides: Partial<ContextState> = {}): ContextState {
return {
settings: getDefaultSettings('/home/test'),
repos: [],
projectGroups: [],
folderWorkspaces: [],
worktreesByRepo: {},
tabsByWorktree: {},
unifiedTabsByWorktree: {},
getKnownWorktreeById: () => undefined,
...overrides
}
}
describe('native chat ownership selectors', () => {
it.each(['chat', 'missing'])('does no ownership scans on unrelated hook updates for %s', (id) => {
let reads = 0
const counted = <T extends { id: string }>(tab: T): T => {
const tabId = tab.id
Object.defineProperty(tab, 'id', { enumerable: true, get: () => (reads++, tabId) })
return tab
}
const state = contextState({
tabsByWorktree: Object.fromEntries(
Array.from({ length: 100 }, (_, index) => [
`terminal-owner-${index}`,
[counted(terminal(`terminal-${index}`))]
])
),
unifiedTabsByWorktree: { 'chat-owner': [counted(structured('chat'))] },
getKnownWorktreeById: (owner) => worktree(owner)
})
useAppStore.setState(state)
const { result } = renderHook(() => ({
owner: useNativeChatTabOwnerWorktreeId(id),
context: useNativeChatFileLinkContext(id)
}))
expect(result.current.owner).toBe(id === 'chat' ? 'chat-owner' : null)
expect(reads).toBeGreaterThan(0)
reads = 0
act(() => {
for (let index = 0; index < 100; index++) {
useAppStore.setState({ lastTerminalInputAtByPaneKey: { pane: index } })
}
})
expect(reads).toBe(0)
expect(result.current.context?.worktreePath ?? null).toBe(id === 'chat' ? '/repo' : null)
})
it('refreshes both hooks when the queried tab changes, moves, closes, or returns', () => {
const state = contextState({
unifiedTabsByWorktree: { first: [structured('one')], second: [structured('two')] },
getKnownWorktreeById: (owner) => worktree(owner, `/${owner}`)
})
useAppStore.setState(state)
const { result, rerender, unmount } = renderHook(
({ id }) => ({
owner: useNativeChatTabOwnerWorktreeId(id),
context: useNativeChatFileLinkContext(id)
}),
{ initialProps: { id: 'one' } }
)
expect(result.current.context?.worktreePath).toBe('/first')
rerender({ id: 'two' })
expect(result.current.context?.worktreePath).toBe('/second')
act(() => useAppStore.setState({ unifiedTabsByWorktree: { first: [structured('two')] } }))
expect(result.current.owner).toBe('first')
act(() => useAppStore.setState({ unifiedTabsByWorktree: {} }))
expect(result.current).toEqual({ owner: null, context: null })
act(() => useAppStore.setState({ tabsByWorktree: { returned: [terminal('two')] } }))
expect(result.current.context?.worktreePath).toBe('/returned')
unmount()
const reopened = renderHook(() => useNativeChatTabOwnerWorktreeId('two'))
expect(reopened.result.current).toBe('returned')
})
it('keeps first-owner collisions, terminal precedence, and the structured content gate', () => {
const select = createNativeChatTabOwnerSelector('same')
const states = [
contextState({
tabsByWorktree: { first: [terminal('same')], second: [terminal('same')] },
unifiedTabsByWorktree: { structured: [structured('same')] }
}),
contextState({
unifiedTabsByWorktree: {
editor: [structured('same', 'editor')],
first: [structured('same')],
second: [structured('same')]
}
}),
contextState({ unifiedTabsByWorktree: { editor: [structured('same', 'editor')] } }),
contextState()
]
expect(states.map((state) => select(state))).toEqual(['first', 'first', null, null])
for (const state of states) {
expect(select(state)).toBe(findNativeChatTabOwnerWorktreeId(state, 'same'))
}
})
it('updates directory and runtime projections without replacing the ownership maps', () => {
let path = '/before'
const initial = contextState({
unifiedTabsByWorktree: { 'chat-owner': [structured('chat')] },
getKnownWorktreeById: (owner) => worktree(owner, path),
worktreesByRepo: { repo: [worktree('chat-owner')] }
})
const select = createNativeChatFileLinkContextSelector('chat')
expect(select(initial)?.worktreePath).toBe('/before')
path = '/after'
const current = {
...initial,
settings: { ...getDefaultSettings('/home/test'), activeRuntimeEnvironmentId: 'focused' },
worktreesByRepo: {
repo: [{ ...worktree('chat-owner'), runtimeOwnerEnvironmentId: 'owning-runtime' }]
}
}
expect(select(current)).toEqual(resolveNativeChatFileLinkContext(current, 'chat'))
expect(select(current)).toEqual({
worktreeId: 'chat-owner',
worktreePath: '/after',
runtimeEnvironmentId: 'owning-runtime'
})
})
it('waits for a floating chat pin and follows pin changes without an ownership change', () => {
const initial = contextState({
floatingWorkspacePath: '/setting',
unifiedTabsByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: [structured('chat')] }
})
const select = createNativeChatFileLinkContextSelector('chat')
expect(select(initial)).toBeNull()
for (const launchDirectory of ['/pinned', '/updated']) {
const current = {
...initial,
structuredSessionLaunchDirectoryByTabId: {
chat: { sessionId: 'session-chat', launchDirectory }
}
}
expect(select(current)?.worktreePath).toBe(launchDirectory)
expect(select(current)).toEqual(resolveNativeChatFileLinkContext(current, 'chat'))
}
})
})
@@ -0,0 +1,122 @@
import { describe, expect, it } from 'vitest'
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import {
createNativeChatRuntimeSelector,
selectNativeChatRuntimeEnvironmentId,
type NativeChatRuntimeOwnerState
} from './native-chat-runtime-owner'
function tab(id: string): TerminalTab {
return {
id,
ptyId: null,
worktreeId: 'owner',
title: id,
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 0
}
}
function state(): NativeChatRuntimeOwnerState {
return {
tabsByWorktree: { owner: [tab('target')] },
worktreesByRepo: { repo: [{ id: 'owner', repoId: 'repo', hostId: 'runtime:first' }] }
}
}
describe('createNativeChatRuntimeSelector', () => {
it('does not inspect terminal rows during unrelated publications and keeps runtime routing fresh', () => {
const initial = state()
const target = initial.tabsByWorktree.owner?.[0]
if (!target) {
throw new Error('Expected the target tab fixture')
}
let reads = 0
Object.defineProperty(target, 'id', {
get: () => {
reads++
return 'target'
}
})
const select = createNativeChatRuntimeSelector('target')
expect(select(initial)).toBe('first')
expect(reads).toBe(1)
for (let i = 0; i < 100; i++) {
const next: NativeChatRuntimeOwnerState = {
...initial,
worktreesByRepo: { repo: [{ id: 'owner', repoId: 'repo', hostId: `runtime:next-${i}` }] }
}
expect(select(next)).toBe(`next-${i}`)
}
expect(reads).toBe(1)
expect(select({ ...initial, tabsByWorktree: { ...initial.tabsByWorktree } })).toBe('first')
expect(reads).toBe(2)
})
it('preserves first terminal owner, move, close, and independent tab identities', () => {
const initial = state()
const withCollision: NativeChatRuntimeOwnerState = {
...initial,
tabsByWorktree: { ...initial.tabsByWorktree, later: [tab('target'), tab('second')] },
worktreesByRepo: {
repo: [
...(initial.worktreesByRepo?.repo ?? []),
{ id: 'later', repoId: 'repo', hostId: 'runtime:later' }
]
}
}
const select = createNativeChatRuntimeSelector('target')
const selectSecond = createNativeChatRuntimeSelector('second')
for (const next of [
initial,
withCollision,
{ ...withCollision, tabsByWorktree: { later: [tab('target'), tab('second')] } },
{ ...withCollision, tabsByWorktree: {} }
]) {
expect(select(next)).toBe(selectNativeChatRuntimeEnvironmentId(next, 'target'))
expect(selectSecond(next)).toBe(selectNativeChatRuntimeEnvironmentId(next, 'second'))
}
})
it('ignores structured-only tabs and refreshes active, detected, folder, and repo host projections', () => {
const select = createNativeChatRuntimeSelector('target')
const initial = state()
const missing = {
...initial,
tabsByWorktree: {},
unifiedTabsByWorktree: { owner: [{ id: 'target', contentType: 'agent-session' }] }
}
expect(select(missing)).toBeNull()
const projections: NativeChatRuntimeOwnerState[] = [
initial,
{
...initial,
activeWorktreeId: 'owner',
activeWorkspaceExecutionHostId: 'runtime:active' as const
},
{
...initial,
detectedWorktreesByRepo: {
repo: { worktrees: [{ id: 'owner', repoId: 'repo', hostId: 'runtime:detected' }] }
}
},
{
...initial,
worktreesByRepo: { repo: [{ id: 'owner', repoId: 'repo' }] },
repos: [{ id: 'repo', executionHostId: 'ssh:remote' as const }]
},
{
...initial,
tabsByWorktree: { 'folder:scope': [tab('target')] },
folderWorkspaces: [
{ id: 'scope', projectGroupId: 'group', executionHostId: 'runtime:folder' as const }
]
}
]
for (const next of projections) {
expect(select(next)).toBe(selectNativeChatRuntimeEnvironmentId(next, 'target'))
}
})
})
@@ -3,7 +3,10 @@ import {
type WorktreeRuntimeOwnerState
} from '@/lib/worktree-runtime-owner'
import type { AppState } from '@/store/types'
import { findTerminalTabWorktreeId } from './native-chat-file-link'
import {
createNativeChatTabOwnerSelector,
findTerminalTabWorktreeId
} from './native-chat-file-link'
export type NativeChatRuntimeOwnerState = Pick<AppState, 'tabsByWorktree'> &
WorktreeRuntimeOwnerState
@@ -28,3 +31,11 @@ export function selectNativeChatRuntimeEnvironmentId(
const worktreeId = findTerminalTabWorktreeId(state.tabsByWorktree, terminalTabId)
return worktreeId ? getRuntimeEnvironmentIdForWorktree(state, worktreeId) : null
}
export function createNativeChatRuntimeSelector(terminalTabId: string) {
const selectOwner = createNativeChatTabOwnerSelector(terminalTabId)
return (state: NativeChatRuntimeOwnerState): string | null => {
const worktreeId = selectOwner({ tabsByWorktree: state.tabsByWorktree })
return worktreeId ? getRuntimeEnvironmentIdForWorktree(state, worktreeId) : null
}
}
@@ -1,7 +1,12 @@
import { useMemo } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { useAppStore } from '../../store'
import { resolveNativeChatFileLinkContext } from './native-chat-file-link'
import { createNativeChatFileLinkContextSelector } from './native-chat-file-link'
export function useNativeChatFileLinkContext(terminalTabId: string) {
return useAppStore(useShallow((state) => resolveNativeChatFileLinkContext(state, terminalTabId)))
const selectContext = useMemo(
() => createNativeChatFileLinkContextSelector(terminalTabId),
[terminalTabId]
)
return useAppStore(useShallow(selectContext))
}
@@ -1,7 +1,9 @@
import { useMemo } from 'react'
import { useAppStore } from '@/store'
import { findNativeChatTabOwnerWorktreeId } from './native-chat-file-link'
import { createNativeChatTabOwnerSelector } from './native-chat-file-link'
/** Owning workspace of a chat tab; launch identity must not wait on its directory resolving. */
export function useNativeChatTabOwnerWorktreeId(tabId: string): string | null {
return useAppStore((state) => findNativeChatTabOwnerWorktreeId(state, tabId))
const selectOwner = useMemo(() => createNativeChatTabOwnerSelector(tabId), [tabId])
return useAppStore(selectOwner)
}