Stop hidden chats re-rendering on every tab change (#34020)

* perf(native-chat): stop hidden chats re-rendering on every tab change

Skill discovery now subscribes to tab and workspace state only while the
slash/skill picker is open, and the per-chat image owner subscription
reads only its own workspace's tabs. Previously every mounted chat
re-rendered and rescanned the tab maps on any tab change anywhere.

* Resolve native chat image context from its selected owner

* Cover selected chat image owners through preview consumers

* Use typed reads in chat subscription performance fixture

* Key chat subscription probes by fixture chat identity

* Type dense chat subscription measurement results

* Name the read-only floating chat resolver view

* Exercise document markdown in selected-owner preview test

* Check unavailable chat image through its preview dialog
This commit is contained in:
Brennan Benson
2026-10-26 15:41:53 -07:00
committed by GitHub
parent d4a5609d30
commit ffb7561f04
10 changed files with 1029 additions and 55 deletions
@@ -12,9 +12,17 @@ import {
} from '@/components/editor/useLocalImageSrc'
import { useAppStore } from '@/store'
import { NativeChatMarkdown } from './NativeChatMarkdown'
import { NativeChatMarkdownImageOwnerContext } from './native-chat-markdown-image-owner'
import {
NativeChatMarkdownImageOwnerContext,
NativeChatMarkdownImageOwnerProvider
} from './native-chat-markdown-image-owner'
import { useNativeChatLinkActions } from './use-native-chat-link-actions'
import { repoFixture, worktreeFixture } from './native-chat-workspace-test-fixtures'
import {
nativeChatFanoutFixture,
nativeChatFanoutWorkspace,
repoFixture,
worktreeFixture
} from './native-chat-workspace-test-fixtures'
import type * as TerminalFileOpenRouting from '@/components/terminal-pane/terminal-file-open-routing'
import type * as HttpLinkRouting from '@/lib/http-link-routing'
import { getDefaultSettings } from '../../../../shared/constants'
@@ -156,6 +164,108 @@ afterEach(() => {
})
describe('native chat Markdown image preview', () => {
it('routes image reads and actions through the selected owner provider across owner changes and removal', async () => {
const original = useAppStore.getState()
const fixture = nativeChatFanoutFixture(1, 1, 0)
const workspaceId = nativeChatFanoutWorkspace(0)
const folders = (owner: string, path: string) =>
fixture.folderWorkspaces.map((folder) => ({
...folder,
executionHostId: `runtime:${owner}` as const,
folderPath: path
}))
useAppStore.setState({
...useAppStore.getInitialState(),
...fixture,
folderWorkspaces: folders('markdown-first', '/remote/first'),
activeWorktreeId: null,
activeWorkspaceExecutionHostId: null,
runtimeEnvironmentCatalogHydrated: true
})
const view = render(
<TooltipProvider>
<NativeChatMarkdownImageOwnerProvider tabId="chat-0" isVisible sessionId="session-0">
<NativeChatMarkdown content="![Chart](chart.png)" variant="document" allowFileUriLinks />
</NativeChatMarkdownImageOwnerProvider>
</TooltipProvider>
)
try {
await screen.findByRole('img', { name: 'Chart' })
expect(mocks.readRuntimeFilePreview).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
target: { kind: 'environment', environmentId: 'markdown-first' },
worktreeId: workspaceId,
worktreePath: '/remote/first'
}),
'/remote/first/chart.png',
{ kind: 'chat-image' }
)
fireEvent.click(screen.getByRole('button', { name: 'View image: Chart' }), {
ctrlKey: true,
metaKey: true
})
expect(mocks.openDetectedFilePath).toHaveBeenLastCalledWith(
'/remote/first/chart.png',
null,
null,
expect.objectContaining({ worktreeId: workspaceId, runtimeEnvironmentId: 'markdown-first' })
)
act(() =>
useAppStore.setState({ folderWorkspaces: folders('markdown-next', '/remote/next') })
)
await waitFor(() =>
expect(screen.getByRole('img', { name: 'Chart' })).toHaveAttribute('src', 'blob:image-2')
)
expect(mocks.readRuntimeFilePreview).toHaveBeenLastCalledWith(
expect.objectContaining({
target: { kind: 'environment', environmentId: 'markdown-next' },
worktreePath: '/remote/next'
}),
'/remote/next/chart.png',
{ kind: 'chat-image' }
)
fireEvent.click(screen.getByRole('button', { name: 'View image: Chart' }), {
ctrlKey: true,
metaKey: true
})
expect(mocks.openDetectedFilePath).toHaveBeenLastCalledWith(
'/remote/next/chart.png',
null,
null,
expect.objectContaining({
worktreeId: workspaceId,
worktreePath: '/remote/next',
runtimeEnvironmentId: 'markdown-next'
})
)
mocks.openDetectedFilePath.mockClear()
await act(async () => useAppStore.setState({ unifiedTabsByWorktree: {} }))
expect(screen.queryByRole('img', { name: 'Chart' })).toBeNull()
const unavailableTrigger = screen.getByRole('button', { name: 'View image: Chart' })
expect(unavailableTrigger).toHaveTextContent('Chart')
expect(screen.queryByRole('dialog')).toBeNull()
expect(mocks.readRuntimeFilePreview).toHaveBeenCalledTimes(2)
fireEvent.click(unavailableTrigger, {
ctrlKey: true,
metaKey: true
})
expect(screen.queryByRole('dialog')).toBeNull()
expect(mocks.openDetectedFilePath).not.toHaveBeenCalled()
expect(mocks.readRuntimeFilePreview).toHaveBeenCalledTimes(2)
fireEvent.click(unavailableTrigger)
const unavailableDialog = screen.getByRole('dialog', { name: 'Chart' })
expect(within(unavailableDialog).getByRole('status')).toHaveTextContent('Preview unavailable')
expect(within(unavailableDialog).queryByRole('img')).toBeNull()
expect(within(unavailableDialog).queryByRole('button', { name: 'File actions' })).toBeNull()
expect(mocks.openDetectedFilePath).not.toHaveBeenCalled()
expect(mocks.readRuntimeFilePreview).toHaveBeenCalledTimes(2)
} finally {
view.unmount()
useAppStore.setState(original, true)
}
})
it.each([
['chart%23v2.png', '/repo/worktree/chart#v2.png'],
['chart%3Fv2.png', '/repo/worktree/chart?v2.png'],
@@ -11,15 +11,23 @@ import {
resetLocalImageSrcStateForTests
} from '@/components/editor/useLocalImageSrc'
import { chatImageAccess } from '@/lib/local-file-access'
import { useAppStore } from '@/store'
import { NativeChatImageAttachments } from './NativeChatTranscriptChrome'
import { useNativeChatImageRuntimeContext } from './native-chat-image-runtime-context'
import {
nativeChatFanoutFixture,
nativeChatFanoutWorkspace
} from './native-chat-workspace-test-fixtures'
// Chat images carry their runtime owner without resolving an editor connection.
const mocks = vi.hoisted(() => ({
callRuntimeRpc: vi.fn(),
getConnectionIdForFile: vi.fn(() => {
throw new Error('Chat image fixtures must not resolve editor file connections')
})
}))
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.callRuntimeRpc }))
vi.mock('@/lib/connection-context', () => ({
getConnectionIdForFile: mocks.getConnectionIdForFile
}))
@@ -41,6 +49,11 @@ async function flushPromises(): Promise<void> {
}
beforeEach(() => {
mocks.callRuntimeRpc.mockReset().mockResolvedValue({
content: 'AA==',
isBinary: true,
mimeType: 'image/png'
})
resetLocalImageSrcStateForTests()
vi.stubGlobal('IntersectionObserver', undefined)
let urlSequence = 0
@@ -69,6 +82,72 @@ afterEach(() => {
})
describe('NativeChatImageAttachments', () => {
it('reads on the selected folder owner and stops reading when that chat disappears', async () => {
const original = useAppStore.getState()
const fixture = nativeChatFanoutFixture(1, 1, 0)
const workspaceId = nativeChatFanoutWorkspace(0)
useAppStore.setState({
...useAppStore.getInitialState(),
...fixture,
activeWorktreeId: null,
activeWorkspaceExecutionHostId: null,
runtimeEnvironmentCatalogHydrated: true
})
function OwnedAttachments() {
const context = useNativeChatImageRuntimeContext('chat-0')
return createElement(NativeChatImageAttachments, {
blocks: [{ type: 'image-ref', path: '/repo/0/image.png' }],
runtimeContext: context
})
}
const container = document.createElement('div')
const root = createRoot(container)
try {
await act(async () => {
root.render(createElement(OwnedAttachments))
await flushPromises()
})
expect(container.querySelector('img')?.getAttribute('src')).toBe('blob:owner-1')
expect(window.api.fs.readFile).toHaveBeenCalledExactlyOnceWith({
filePath: '/repo/0/image.png',
connectionId: undefined,
access: { kind: 'chat-image' }
})
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
await act(async () => {
useAppStore.setState({
folderWorkspaces: fixture.folderWorkspaces.map((folder) => ({
...folder,
executionHostId: 'runtime:attachment-owner' as const
}))
})
await flushPromises()
})
expect(container.querySelector('img')?.getAttribute('src')).toBe('blob:owner-2')
expect(mocks.callRuntimeRpc).toHaveBeenCalledExactlyOnceWith(
{ kind: 'environment', environmentId: 'attachment-owner' },
'files.readPreview',
{ worktree: `id:${workspaceId}`, relativePath: 'image.png' },
expect.objectContaining({ timeoutMs: 15_000 })
)
expect(window.api.fs.readFile).toHaveBeenCalledTimes(1)
await act(async () => {
useAppStore.setState({ unifiedTabsByWorktree: {} })
await flushPromises()
})
expect(container.querySelector('img')).toBeNull()
expect(container.querySelector('button')).toBeNull()
expect(container.textContent).toContain('image.png')
expect(window.api.fs.readFile).toHaveBeenCalledTimes(1)
expect(mocks.callRuntimeRpc).toHaveBeenCalledTimes(1)
} finally {
await act(async () => root.unmount())
useAppStore.setState(original, true)
}
})
it('pools visibility observation across image refs', async () => {
class FakeIntersectionObserver {
static instances: FakeIntersectionObserver[] = []
@@ -88,8 +88,8 @@ export function createNativeChatFileLinkContextSelector(tabId: string) {
resolveNativeChatFileLinkContextForOwner(state, tabId, selectOwner(state))
}
function resolveNativeChatFileLinkContextForOwner(
state: NativeChatFileLinkState,
export function resolveNativeChatFileLinkContextForOwner(
state: Omit<NativeChatFileLinkState, 'tabsByWorktree'>,
terminalTabId: string,
worktreeId: string | null
): NativeChatFileLinkContext | null {
@@ -1,4 +1,8 @@
import { describe, expect, it } from 'vitest'
// @vitest-environment happy-dom
import { createElement } from 'react'
import { act, cleanup, render } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import { shallow } from 'zustand/shallow'
import type { AppState } from '@/store/types'
import { useAppStore } from '@/store'
@@ -6,10 +10,27 @@ import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import type { Tab } from '../../../../shared/tab-types'
import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultSettings } from '../../../../shared/constants'
import {
createNativeChatImageOwnerSelector,
resolveNativeChatImageRuntimeContext,
selectNativeChatImageOwnerState
resolveNativeChatImageRuntimeContextForOwner,
selectNativeChatImageOwnerState,
useNativeChatImageRuntimeContext,
type NativeChatImageRuntimeContext
} from './native-chat-image-runtime-context'
import { repoFixture, worktreeFixture } from './native-chat-workspace-test-fixtures'
import {
agentSessionTabFixture,
nativeChatFanoutFixture,
nativeChatFanoutWorkspace,
repoFixture,
terminalTabFixture,
worktreeFixture
} from './native-chat-workspace-test-fixtures'
const originalState = useAppStore.getState()
afterEach(() => {
cleanup()
useAppStore.setState(originalState, true)
})
function state(overrides: Partial<AppState> = {}): AppState {
const tab: TerminalTab = {
@@ -58,6 +79,21 @@ describe('resolveNativeChatImageRuntimeContext', () => {
expect(shallow(second, first)).toBe(true)
})
it("keeps tab changes in other workspaces out of a chat's owner inputs", () => {
const storeState = state()
const select = createNativeChatImageOwnerSelector('tab-1')
const first = select(storeState)
const otherWorkspaceTab = { ...storeState.tabsByWorktree['wt-1'][0], id: 'tab-2' }
const second = select({
...storeState,
tabsByWorktree: { ...storeState.tabsByWorktree, 'wt-2': [otherWorkspaceTab] },
unifiedTabsByWorktree: { 'wt-2': [] }
})
expect(first.owner).toBe('wt-1')
expect(shallow(second, first)).toBe(true)
})
it('reuses derived settings when owner inputs are unchanged', () => {
const storeState = state()
const first = resolveNativeChatImageRuntimeContext(storeState, 'tab-1')
@@ -151,4 +187,271 @@ describe('resolveNativeChatImageRuntimeContext', () => {
target: { kind: 'local' as const }
})
})
it('keeps selected-hook results equivalent to full-state resolution through ownership, pin and host transitions', () => {
const base = state({
worktreesByRepo: {
repo: [
worktreeFixture('wt-1', '/repo/worktree', { hostId: 'local' }),
worktreeFixture('wt-2', '/repo/second', { hostId: 'local' })
]
}
})
const duplicate = {
...base,
tabsByWorktree: {
'wt-1': [terminalTabFixture('tab-1', 'wt-1')],
'wt-2': [terminalTabFixture('tab-1', 'wt-2')]
},
unifiedTabsByWorktree: {
'wt-2': [agentSessionTabFixture('tab-1', 'wt-2')],
'wt-1': [agentSessionTabFixture('tab-1', 'wt-1')]
}
}
const fallback = { ...duplicate, tabsByWorktree: {} }
const moved = {
...fallback,
unifiedTabsByWorktree: { 'wt-1': [agentSessionTabFixture('tab-1', 'wt-1')] }
}
const floatingTab = agentSessionTabFixture('tab-1', FLOATING_TERMINAL_WORKTREE_ID)
const floating = {
...base,
tabsByWorktree: {},
unifiedTabsByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: [floatingTab] },
structuredSessionLaunchDirectoryByTabId: {},
floatingWorkspacePath: '/floating-setting'
}
const pinned = {
...floating,
structuredSessionLaunchDirectoryByTabId: {
'tab-1': { sessionId: floatingTab.entityId, launchDirectory: '/pinned' }
}
}
const replaced = {
...pinned,
unifiedTabsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [{ ...floatingTab, entityId: 'replacement' }]
}
}
const folder = nativeChatFanoutFixture(0, 1, 0)
const folderState = {
...base,
...folder,
unifiedTabsByWorktree: {
[nativeChatFanoutWorkspace(0)]: [
agentSessionTabFixture('tab-1', nativeChatFanoutWorkspace(0))
]
}
}
const environment = (id: string): AppState['runtimeEnvironments'][number] => ({
id,
name: id,
createdAt: 0,
updatedAt: 0,
lastUsedAt: null,
runtimeId: null,
preferredEndpointId: 'endpoint',
endpoints: []
})
const remote = (id: string): AppState => ({
...base,
activeWorkspaceExecutionHostId: `runtime:${id}`,
runtimeEnvironments: [environment(id)],
repos: [repoFixture({ executionHostId: `runtime:${id}` })],
worktreesByRepo: {
repo: [worktreeFixture('wt-1', '/remote', { hostId: `runtime:${id}` })]
}
})
const ssh = (generation?: number): AppState => ({
...remote('owner-a'),
activeWorkspaceExecutionHostId: 'ssh:ssh-1',
worktreesByRepo: {
repo: [
worktreeFixture('wt-1', '/ssh', {
hostId: 'ssh:ssh-1',
runtimeOwnerEnvironmentId: 'owner-a'
})
]
},
sshStateByEnvironment: new Map([
[
'owner-a',
{
targets: [],
targetGenerations: new Map(),
targetLabels: new Map(),
removedTargetLabels: new Map(),
targetsHydrated: true,
connectionStates: new Map([
[
'ssh-1',
{
targetId: 'ssh-1',
status: 'connected',
error: null,
reconnectAttempt: 0,
connectionGeneration: generation
}
]
])
}
]
])
})
const local = (worktreeId: string, worktreePath: string) => ({
worktreeId,
worktreePath,
target: { kind: 'local' as const }
})
const paired = ssh(3)
const cases: [
name: string,
snapshot: AppState,
expected: Partial<NonNullable<NativeChatImageRuntimeContext>> | null
][] = [
['terminal-first and first duplicate', duplicate, local('wt-1', '/repo/worktree')],
['unified fallback', fallback, local('wt-2', '/repo/second')],
['move', moved, local('wt-1', '/repo/worktree')],
['remove', { ...moved, unifiedTabsByWorktree: {} }, null],
['return', fallback, local('wt-2', '/repo/second')],
['missing pin', floating, null],
[
'wrong pin',
{
...pinned,
structuredSessionLaunchDirectoryByTabId: {
'tab-1': { sessionId: 'wrong', launchDirectory: '/wrong' }
}
},
null
],
['correct pin', pinned, local(FLOATING_TERMINAL_WORKTREE_ID, '/pinned')],
['replaced session', replaced, null],
[
'replaced pin',
{
...replaced,
structuredSessionLaunchDirectoryByTabId: {
'tab-1': { sessionId: 'replacement', launchDirectory: '/replacement' }
}
},
local(FLOATING_TERMINAL_WORKTREE_ID, '/replacement')
],
[
'floating directory predicate skips unregistered duplicate',
{
...pinned,
unifiedTabsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [
{ ...floatingTab, entityId: 'unregistered', agentSessionAgent: undefined },
floatingTab
]
}
},
local(FLOATING_TERMINAL_WORKTREE_ID, '/pinned')
],
[
'floating terminal fallback',
{
...floating,
tabsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [
terminalTabFixture('tab-1', FLOATING_TERMINAL_WORKTREE_ID)
]
},
unifiedTabsByWorktree: {}
},
local(FLOATING_TERMINAL_WORKTREE_ID, '/floating-setting')
],
[
'floating first registered duplicate wins',
{
...pinned,
unifiedTabsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [floatingTab, { ...floatingTab, entityId: 'second' }]
},
structuredSessionLaunchDirectoryByTabId: {
'tab-1': { sessionId: 'second', launchDirectory: '/wrong-second' }
}
},
null
],
['folder', folderState, local(nativeChatFanoutWorkspace(0), '/repo/0')],
[
'runtime',
remote('owner-a'),
{ worktreePath: '/remote', target: { kind: 'environment', environmentId: 'owner-a' } }
],
[
'runtime replacement',
remote('owner-b'),
{ target: { kind: 'environment', environmentId: 'owner-b' } }
],
[
'owner-only runtime hydration',
{
...remote('owner-a'),
activeWorktreeId: null,
activeWorkspaceExecutionHostId: null,
worktreesByRepo: {
repo: [
worktreeFixture('wt-1', '/remote', {
hostId: undefined,
runtimeOwnerEnvironmentId: 'owner-a'
})
]
}
},
{ worktreePath: '/remote', target: { kind: 'environment', environmentId: 'owner-a' } }
],
[
'paired SSH',
paired,
{
worktreePath: '/ssh',
expectedExecutionHostId: 'ssh:ssh-1',
expectedSshConnectionGeneration: 3,
target: { kind: 'environment', environmentId: 'owner-a' }
}
],
[
'SSH generation-only change',
{ ...paired, sshStateByEnvironment: ssh(4).sshStateByEnvironment },
{ expectedSshConnectionGeneration: 4 }
],
[
'SSH generation absent',
{ ...paired, sshStateByEnvironment: ssh().sshStateByEnvironment },
null
],
[
'unpaired SSH',
{
...paired,
repos: [repoFixture({ connectionId: 'ssh-1' })],
worktreesByRepo: {
repo: [worktreeFixture('wt-1', '/ssh', { hostId: 'ssh:ssh-1' })]
}
},
null
]
]
let observed: NativeChatImageRuntimeContext = null
function Probe() {
observed = useNativeChatImageRuntimeContext('tab-1')
return null
}
useAppStore.setState(base)
render(createElement(Probe))
for (const [name, snapshot, expected] of cases) {
act(() => useAppStore.setState(snapshot))
expect(observed, name).toEqual(resolveNativeChatImageRuntimeContext(snapshot, 'tab-1'))
if (expected === null) {
expect(observed, name).toBeNull()
} else {
expect(observed, name).toMatchObject(expected)
}
}
expect(resolveNativeChatImageRuntimeContextForOwner(base, 'tab-1', null)).toBeNull()
})
})
@@ -7,7 +7,11 @@ import {
resolveWorktreeOperationRouteResult,
type WorktreeOperationRouteResolution
} from '@/lib/worktree-operation-route'
import { resolveNativeChatFileLinkContext } from './native-chat-file-link'
import {
createNativeChatTabOwnerSelector,
findNativeChatTabOwnerWorktreeId,
resolveNativeChatFileLinkContextForOwner
} from './native-chat-file-link'
import { captureDirectSshMutationExpectation } from '@/lib/ssh-mutation-expectation'
import { parseExecutionHostId, toRuntimeExecutionHostId } from '../../../../shared/execution-host'
import { isFloatingWorkspaceId } from '../../../../shared/floating-workspace-worktree'
@@ -45,8 +49,10 @@ type OwnerState = Pick<
| 'unifiedTabsByWorktree'
>
type RouteState = Omit<OwnerState, 'tabsByWorktree' | 'unifiedTabsByWorktree'>
// Subscribe only to ownership inputs; derive context after Zustand filters unrelated updates.
export function selectNativeChatImageOwnerState(state: AppState): OwnerState {
function selectNativeChatImageRouteState(state: AppState): RouteState {
return {
settings: state.settings,
repos: state.repos,
@@ -63,7 +69,13 @@ export function selectNativeChatImageOwnerState(state: AppState): OwnerState {
sshStateByEnvironment: state.sshStateByEnvironment,
activeWorktreeId: state.activeWorktreeId,
activeWorkspaceExecutionHostId: state.activeWorkspaceExecutionHostId,
restoredRuntimeHostIdByWorkspaceSessionKey: state.restoredRuntimeHostIdByWorkspaceSessionKey,
restoredRuntimeHostIdByWorkspaceSessionKey: state.restoredRuntimeHostIdByWorkspaceSessionKey
}
}
export function selectNativeChatImageOwnerState(state: AppState): OwnerState {
return {
...selectNativeChatImageRouteState(state),
tabsByWorktree: state.tabsByWorktree,
unifiedTabsByWorktree: state.unifiedTabsByWorktree
}
@@ -95,7 +107,19 @@ export function resolveNativeChatImageRuntimeContext(
state: OwnerState,
tabId: string
): NativeChatImageRuntimeContext {
const linkContext = resolveNativeChatFileLinkContext(state, tabId)
return resolveNativeChatImageRuntimeContextForOwner(
state,
tabId,
findNativeChatTabOwnerWorktreeId(state, tabId)
)
}
export function resolveNativeChatImageRuntimeContextForOwner(
state: RouteState & Pick<OwnerState, 'unifiedTabsByWorktree'>,
tabId: string,
owner: string | null
): NativeChatImageRuntimeContext {
const linkContext = resolveNativeChatFileLinkContextForOwner(state, tabId, owner)
if (!linkContext) {
return null
}
@@ -148,7 +172,38 @@ export function resolveNativeChatImageRuntimeContext(
return context
}
/** Ordinary workspace directories need only the selected owner; floating also needs its tab list. */
export function createNativeChatImageOwnerSelector(tabId: string) {
const selectOwner = createNativeChatTabOwnerSelector(tabId)
return (state: AppState) => {
const owner = selectOwner(state)
return {
...selectNativeChatImageRouteState(state),
owner,
floatingTabs:
owner && isFloatingWorkspaceId(owner) && Object.hasOwn(state.unifiedTabsByWorktree, owner)
? state.unifiedTabsByWorktree[owner]
: undefined
}
}
}
export function useNativeChatImageRuntimeContext(tabId: string): NativeChatImageRuntimeContext {
const ownerState = useAppStore(useShallow(selectNativeChatImageOwnerState))
return useMemo(() => resolveNativeChatImageRuntimeContext(ownerState, tabId), [ownerState, tabId])
const selectOwnerState = useMemo(() => createNativeChatImageOwnerSelector(tabId), [tabId])
const ownerState = useAppStore(useShallow(selectOwnerState))
return useMemo(() => {
// This view serves only the existing floating-directory predicate, never an owner index.
const unifiedTabsByWorktree =
ownerState.owner && ownerState.floatingTabs
? { [ownerState.owner]: ownerState.floatingTabs }
: {}
return resolveNativeChatImageRuntimeContextForOwner(
{
...ownerState,
unifiedTabsByWorktree
},
tabId,
ownerState.owner
)
}, [ownerState, tabId])
}
@@ -6,8 +6,10 @@ import { useAppStore } from '@/store'
import { resolveNativeChatDraftOwner } from '@/lib/native-chat-draft-owner'
import type { Tab } from '../../../../shared/tab-types'
import { testOrcaSessionId } from '../../../../shared/orca-session-address-test-fixture'
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
import {
nativeChatFanoutFixture,
nativeChatFanoutWorkspace as workspace
} from './native-chat-workspace-test-fixtures'
import { useAgentMessageSenderLabel } from './native-chat-agent-message-sender-label'
import { useNativeChatFileLinkContext } from './use-native-chat-file-link-context'
import { useRestoredNativeChatComposerDraftImageCheck } from './native-chat-composer-draft-image-check'
@@ -19,7 +21,6 @@ const initial = useAppStore.getInitialState()
const originalApi = window.api
const reads = { terminal: 0, unified: 0 }
let renders = 0
const workspace = (index: number) => `folder:workspace-${index}`
const scope = (index: number) => `agent-session:session-${index}`
function counted<T extends object>(map: T, kind: keyof typeof reads): T {
return new Proxy(map, { ownKeys: (target) => (reads[kind]++, Reflect.ownKeys(target)) })
@@ -40,42 +41,11 @@ function tab(index: number, workspaces: number): Tab {
}
}
function seed(chats: number, workspaces: number, terminalsPerWorkspace: number) {
const tabsByWorktree: Record<string, TerminalTab[]> = {}
const unifiedTabsByWorktree: Record<string, Tab[]> = {}
const folderWorkspaces: FolderWorkspace[] = []
for (let index = 0; index < workspaces; index++) {
const id = workspace(index)
tabsByWorktree[id] = Array.from({ length: terminalsPerWorkspace }, (_, slot) => ({
id: `terminal-${index}-${slot}`,
worktreeId: id,
ptyId: null,
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: slot,
createdAt: 0
}))
unifiedTabsByWorktree[id] = []
folderWorkspaces.push({
id: `workspace-${index}`,
projectGroupId: 'group',
name: id,
folderPath: `/repo/${index}`,
executionHostId: 'local',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: index,
lastActivityAt: 0,
createdAt: 0,
updatedAt: 0
})
}
for (let index = 0; index < chats; index++) {
unifiedTabsByWorktree[workspace(index % workspaces)].push(tab(index, workspaces))
}
const { tabsByWorktree, unifiedTabsByWorktree, folderWorkspaces } = nativeChatFanoutFixture(
chats,
workspaces,
terminalsPerWorkspace
)
useAppStore.setState(
{
...initial,
@@ -0,0 +1,383 @@
// @vitest-environment happy-dom
import { act, cleanup, render, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import type { NativeChatSkillDiscovery } from './use-native-chat-skills'
import {
useNativeChatImageRuntimeContext,
type NativeChatImageRuntimeContext
} from './native-chat-image-runtime-context'
import {
agentSessionTabFixture,
nativeChatFanoutFixture,
nativeChatFanoutWorkspace as workspace,
terminalTabFixture
} from './native-chat-workspace-test-fixtures'
import { getDefaultSettings } from '../../../../shared/constants'
const rpc = vi.hoisted(() => vi.fn())
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: rpc }))
vi.mock('@/lib/native-chat-telemetry', () => ({ emitNativeChatSkillDiscovery: vi.fn() }))
import {
resetNativeChatSkillDiscoveryCacheForTests,
useNativeChatSkills
} from './use-native-chat-skills'
type Work = {
terminalRows: number
unifiedRows: number
terminalMapEnumerations: number
unifiedMapEnumerations: number
terminalWorkspaceReads: number
unifiedWorkspaceReads: number
}
function emptyWork(): Work {
return {
terminalRows: 0,
unifiedRows: 0,
terminalMapEnumerations: 0,
unifiedMapEnumerations: 0,
terminalWorkspaceReads: 0,
unifiedWorkspaceReads: 0
}
}
function countRow<T extends { id: string }>(row: T, count: () => void): T {
const id = row.id
Object.defineProperty(row, 'id', {
enumerable: true,
get() {
count()
return id
}
})
return row
}
function corpus(scale: number, dense = false) {
const mounted = 57 * scale
const workspaces = dense ? 1 : 100 * scale
const work = emptyWork()
const fixture = nativeChatFanoutFixture(mounted, workspaces, 1)
const { tabsByWorktree, unifiedTabsByWorktree } = fixture
const chatIds = Object.values(unifiedTabsByWorktree).flatMap((rows) => rows.map((row) => row.id))
for (const row of Object.values(tabsByWorktree).flat()) {
countRow(row, () => work.terminalRows++)
}
for (const row of Object.values(unifiedTabsByWorktree).flat()) {
countRow(row, () => work.unifiedRows++)
}
function terminalMap(map: AppState['tabsByWorktree']) {
return new Proxy(map, {
get(target, key) {
if (typeof key === 'string' && Object.hasOwn(target, key)) {
work.terminalWorkspaceReads += 1
}
return typeof key === 'string' ? target[key] : undefined
},
ownKeys(target) {
work.terminalMapEnumerations += 1
return Reflect.ownKeys(target)
}
})
}
function unifiedMap(map: AppState['unifiedTabsByWorktree']) {
return new Proxy(map, {
get(target, key) {
if (typeof key === 'string' && Object.hasOwn(target, key)) {
work.unifiedWorkspaceReads += 1
}
return typeof key === 'string' ? target[key] : undefined
},
ownKeys(target) {
work.unifiedMapEnumerations += 1
return Reflect.ownKeys(target)
}
})
}
useAppStore.setState({
...useAppStore.getInitialState(),
...fixture,
activeRepoId: null,
activeWorktreeId: workspace(0),
activeWorkspaceExecutionHostId: 'local',
repos: [],
settings: getDefaultSettings('/test-home'),
worktreesByRepo: {},
detectedWorktreesByRepo: {},
projectGroups: [],
projects: [],
runtimeEnvironments: [],
runtimeEnvironmentCatalogHydrated: true,
removedRuntimeEnvironmentIds: new Set(),
sshConnectionStates: new Map(),
sshStateByEnvironment: new Map(),
restoredRuntimeHostIdByWorkspaceSessionKey: {},
tabsByWorktree: terminalMap(tabsByWorktree),
unifiedTabsByWorktree: unifiedMap(unifiedTabsByWorktree)
})
return {
mounted,
chatIds,
workspaces,
work,
reset: () => Object.assign(work, emptyWork()),
renameUnrelated(kind: 'terminal' | 'unified', version: number) {
const owner = workspace(workspaces - 1)
const current = useAppStore.getState()
// Prepare the writer's immutable replacement before counting subscriber work.
const patch =
kind === 'terminal'
? {
tabsByWorktree: terminalMap({
...current.tabsByWorktree,
[owner]: [
countRow(
terminalTabFixture(`terminal-${workspaces - 1}-0`, owner, {
title: `${version}`
}),
() => work.terminalRows++
)
]
})
}
: {
unifiedTabsByWorktree: unifiedMap({
...current.unifiedTabsByWorktree,
[owner]: [
countRow(
agentSessionTabFixture('unmounted-chat', owner, {
label: `${version}`
}),
() => work.unifiedRows++
)
]
})
}
Object.assign(work, emptyWork())
act(() => useAppStore.setState(patch))
return { ...work }
},
replaceOwnList(rename: boolean) {
const current = useAppStore.getState()
const list = current.unifiedTabsByWorktree[workspace(0)]
const replacement = unifiedMap({
...current.unifiedTabsByWorktree,
[workspace(0)]: rename
? [countRow({ ...list[0], label: 'Renamed' }, () => work.unifiedRows++), ...list.slice(1)]
: [...list]
})
Object.assign(work, emptyWork())
act(() => useAppStore.setState({ unifiedTabsByWorktree: replacement }))
return { ...work }
},
moveLastChat() {
const source = workspace(mounted - 1)
const destination = workspace(workspaces - 2)
const current = useAppStore.getState()
act(() =>
useAppStore.setState({
unifiedTabsByWorktree: unifiedMap({
...current.unifiedTabsByWorktree,
[source]: [],
[destination]: [agentSessionTabFixture(`chat-${mounted - 1}`, destination)]
})
})
)
return destination
}
}
}
const initialState = useAppStore.getState()
afterEach(() => {
cleanup()
useAppStore.setState(initialState, true)
resetNativeChatSkillDiscoveryCacheForTests()
rpc.mockReset()
})
function measureImageSubscriptions(scale: number) {
const data = corpus(scale)
const contexts = new Map<string, NativeChatImageRuntimeContext>()
let renders = 0
function Probe({ chatId }: { chatId: string }) {
contexts.set(chatId, useNativeChatImageRuntimeContext(chatId))
renders += 1
return null
}
const view = render(data.chatIds.map((chatId) => <Probe key={chatId} chatId={chatId} />))
expect(contexts.size).toBe(data.mounted)
for (let index = 0; index < data.mounted; index += 1) {
expect(contexts.get(`chat-${index}`)?.worktreePath).toBe(`/repo/${index}`)
}
const baseline = renders
const rounds: Work[] = []
// Separate writes ensure a warm selector cache cannot hide a map invalidation.
for (let version = 0; version < 3; version += 1) {
rounds.push(data.renameUnrelated('terminal', version))
rounds.push(data.renameUnrelated('unified', version))
}
expect(renders - baseline).toBe(0)
data.reset()
act(() => useAppStore.setState({ agentStatusByPaneKey: {} }))
expect(data.work).toEqual(emptyWork())
expect(renders - baseline).toBe(0)
const destination = data.moveLastChat()
expect(contexts.get(`chat-${data.mounted - 1}`)).toMatchObject({
worktreeId: destination,
worktreePath: `/repo/${data.workspaces - 2}`,
target: { kind: 'local' }
})
expect(renders - baseline).toBe(1)
view.unmount()
return {
mounted: data.mounted,
workspaces: data.workspaces,
canonicalRows: { terminal: data.workspaces, unified: data.mounted + 1 },
unrelatedRenders: 0,
rounds
}
}
describe('native chat store subscription performance', () => {
it('does no tab work for closed pickers at N and 10N, then discovers the latest owner on activation', async () => {
rpc.mockResolvedValue({ skills: [], sources: [], scannedAt: 1 })
for (const scale of [1, 10]) {
const pending = Promise.withResolvers<never>()
rpc.mockReturnValueOnce(pending.promise)
const data = corpus(scale)
const lastChatId = `chat-${data.mounted - 1}`
const snapshots = new Map<string, NativeChatSkillDiscovery>()
let renders = 0
function Probe({ chatId, enabled }: { chatId: string; enabled: boolean }) {
snapshots.set(chatId, useNativeChatSkills('codex', chatId, enabled))
renders += 1
return null
}
const probes = (enabled: boolean) =>
data.chatIds.map((chatId) => (
<Probe key={chatId} chatId={chatId} enabled={enabled && chatId === lastChatId} />
))
const view = render(probes(false))
const baseline = renders
for (const kind of ['terminal', 'unified'] as const) {
expect(data.renameUnrelated(kind, 1)).toEqual(emptyWork())
}
const destination = data.moveLastChat()
expect(renders).toBe(baseline)
expect(rpc).not.toHaveBeenCalled()
view.rerender(probes(true))
expect(snapshots.get(lastChatId)?.status).toBe('loading')
expect(rpc).toHaveBeenCalledExactlyOnceWith(
{ kind: 'local' },
'skills.discover',
expect.objectContaining({ cwd: `/repo/${data.workspaces - 2}`, worktreeId: destination }),
{ timeoutMs: 10_000 }
)
view.rerender(probes(false))
const current = useAppStore.getState()
act(() =>
useAppStore.setState({
folderWorkspaces: current.folderWorkspaces.map((folder) =>
`folder:${folder.id}` === destination
? {
...folder,
folderPath: '/latest-owner-path',
executionHostId: 'runtime:latest' as const
}
: folder
)
})
)
expect(snapshots.get(lastChatId)?.status).toBe('idle')
view.rerender(probes(true))
await waitFor(() => expect(snapshots.get(lastChatId)?.status).toBe('ready'))
expect(rpc).toHaveBeenLastCalledWith(
{ kind: 'environment', environmentId: 'latest' },
'skills.discover',
{ cwd: '/latest-owner-path', worktreeId: destination },
{ timeoutMs: 10_000 }
)
await act(async () => pending.reject(new Error('Old owner request failed')))
expect(snapshots.get(lastChatId)?.status).toBe('ready')
view.unmount()
rpc.mockClear()
resetNativeChatSkillDiscoveryCacheForTests()
}
})
it('benchmarks image-owner scans separately from renders at N and 10N', () => {
const results = [1, 10].map(measureImageSubscriptions)
for (const result of results) {
for (const [index, work] of result.rounds.entries()) {
// One shared index build per changed map, never a population scan per mounted chat.
expect(work.terminalRows).toBeLessThanOrEqual(
index % 2 === 0 ? result.canonicalRows.terminal : 0
)
expect(work.unifiedRows).toBeLessThanOrEqual(
index % 2 === 1 ? result.canonicalRows.unified : 0
)
expect(work.terminalMapEnumerations).toBeLessThanOrEqual(index % 2 === 0 ? 1 : 0)
expect(work.unifiedMapEnumerations).toBeLessThanOrEqual(index % 2 === 1 ? 1 : 0)
expect(work.terminalWorkspaceReads).toBeLessThanOrEqual(
index % 2 === 0 ? result.workspaces : 0
)
expect(work.unifiedWorkspaceReads).toBeLessThanOrEqual(
index % 2 === 1 ? result.workspaces : 0
)
}
}
console.info('native-chat-subscription-work', JSON.stringify(results))
})
it('bounds dense ordinary-workspace image work on mount and own-list rename/replace at N and 10N', () => {
const results: {
mounted: number
mount: Work
updates: Work[]
updateRenders: number
}[] = []
for (const scale of [1, 10]) {
const data = corpus(scale, true)
let renders = 0
function Probe({ chatId }: { chatId: string }) {
const context = useNativeChatImageRuntimeContext(chatId)
expect(context?.worktreePath).toBe('/repo/0')
renders += 1
return null
}
data.reset()
const view = render(data.chatIds.map((chatId) => <Probe key={chatId} chatId={chatId} />))
const mount = { ...data.work }
// All canonical rows may be indexed once. Private per-hook maps would cost N + N².
expect(mount.terminalRows).toBeLessThanOrEqual(1)
expect(mount.unifiedRows).toBeLessThanOrEqual(data.mounted)
expect(mount.terminalMapEnumerations).toBeLessThanOrEqual(1)
expect(mount.unifiedMapEnumerations).toBeLessThanOrEqual(1)
expect(renders).toBe(data.mounted)
const baseline = renders
const updates = [data.replaceOwnList(true), data.replaceOwnList(false)]
for (const work of updates) {
expect(work.terminalRows).toBe(0)
expect(work.unifiedRows).toBeLessThanOrEqual(data.mounted)
expect(work.unifiedMapEnumerations).toBeLessThanOrEqual(1)
expect(work.unifiedWorkspaceReads).toBeLessThanOrEqual(1)
}
expect(renders - baseline).toBe(0)
results.push({ mounted: data.mounted, mount, updates, updateRenders: renders - baseline })
view.unmount()
}
console.info('native-chat-dense-subscription-work', JSON.stringify(results))
})
})
@@ -3,6 +3,55 @@ import type { Tab } from '../../../../shared/tab-types'
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import type { DetectedWorktreeListResult, Worktree } from '../../../../shared/worktree/types'
import type { AppState } from '@/store/types'
import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
export const nativeChatFanoutWorkspace = (index: number) => `folder:workspace-${index}`
export function nativeChatFanoutFixture(
chats: number,
workspaces: number,
terminalsPerWorkspace: number
) {
const tabsByWorktree: AppState['tabsByWorktree'] = {}
const unifiedTabsByWorktree: AppState['unifiedTabsByWorktree'] = {}
const folderWorkspaces: FolderWorkspace[] = []
for (let index = 0; index < workspaces; index++) {
const id = nativeChatFanoutWorkspace(index)
tabsByWorktree[id] = Array.from({ length: terminalsPerWorkspace }, (_, slot) =>
terminalTabFixture(`terminal-${index}-${slot}`, id, { sortOrder: slot })
)
unifiedTabsByWorktree[id] = []
folderWorkspaces.push({
id: `workspace-${index}`,
projectGroupId: 'group',
name: id,
folderPath: `/repo/${index}`,
executionHostId: 'local',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: index,
lastActivityAt: 0,
createdAt: 0,
updatedAt: 0
})
}
for (let index = 0; index < chats; index++) {
const id = nativeChatFanoutWorkspace(index % workspaces)
unifiedTabsByWorktree[id].push(
agentSessionTabFixture(`chat-${index}`, id, {
entityId: `session-${index}`,
executionHostId: 'local',
label: `Chat ${index}`,
customLabel: `Name ${index}`,
sortOrder: index
})
)
}
return { tabsByWorktree, unifiedTabsByWorktree, folderWorkspaces }
}
export function terminalTabFixture(
id: string,
@@ -119,6 +119,24 @@ describe('useNativeChatSkills', () => {
)
})
it('reads no tab maps while the picker is closed', () => {
let tabMapReads = 0
const base = stateForHost('local')
mocks.state = Object.defineProperties(
{ ...base },
{
tabsByWorktree: { get: () => (tabMapReads++, base.tabsByWorktree) },
unifiedTabsByWorktree: { get: () => (tabMapReads++, base.unifiedTabsByWorktree) }
}
)
const view = render(<Probe enabled={false} />)
view.rerender(<Probe enabled={false} />)
// Every mounted chat holds this hook; tab changes must not reach closed pickers.
expect(tabMapReads).toBe(0)
expect(mocks.snapshots.at(-1)?.status).toBe('idle')
})
it('resolves the catalog for a structured session tab', async () => {
mocks.state = {
...stateForHost('local'),
@@ -58,6 +58,7 @@ const MISSING_CONTEXT_STATE: StoredDiscoveryState = {
errorKind: 'unknown',
contextKey: null
}
const selectNoSkillStateInputs = (): null => null
const inFlightDiscovery = new Map<string, Promise<SkillDiscoveryResult>>()
export function isNativeChatSkillForAgent(
@@ -88,13 +89,19 @@ export function useNativeChatSkills(
terminalTabId: string,
enabled = false
): NativeChatSkillDiscovery {
const inputs = useAppStore(useShallow(selectNativeChatSkillStateInputs))
// Closed is the common state, so the tab-map subscription only runs while the picker is open.
const inputs = useAppStore(
useShallow(enabled ? selectNativeChatSkillStateInputs : selectNoSkillStateInputs)
)
const context = useMemo(
() => resolveNativeChatSkillDiscoveryContext(inputs, terminalTabId),
() => (inputs ? resolveNativeChatSkillDiscoveryContext(inputs, terminalTabId) : null),
[inputs, terminalTabId]
)
const awaitingContext = useMemo(
() => !context && isNativeChatSkillDiscoveryAwaitingDirectory(inputs, terminalTabId),
() =>
inputs !== null &&
!context &&
isNativeChatSkillDiscoveryAwaitingDirectory(inputs, terminalTabId),
[context, inputs, terminalTabId]
)
const [state, setState] = useState<StoredDiscoveryState>(IDLE_STATE)