From 766462be18dcf6e564eeafca6cd1b51b09e4db37 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:51:41 -0700 Subject: [PATCH] perf(native-chat): memoize file-link context per chat on its resolver inputs (#34755) * perf(native-chat): memoize file-link context per chat on its resolver inputs Every mounted chat (hidden ones included) re-resolved its directory and runtime owner on every store write. Reuse the context until an input the resolvers read changes. * docs(native-chat): scope the file-link input completeness comment --- .../native-chat/native-chat-file-link.test.ts | 16 ++++++ .../native-chat/native-chat-file-link.ts | 51 +++++++++++++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/native-chat/native-chat-file-link.test.ts b/src/renderer/src/components/native-chat/native-chat-file-link.test.ts index 740a7fb93eb..83eb143ee44 100644 --- a/src/renderer/src/components/native-chat/native-chat-file-link.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-file-link.test.ts @@ -5,6 +5,7 @@ import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultSettings } from '../../../../shared/constants' import { createNativeChatFileHref } from '../../../../shared/native-chat-href-routing' import { + createNativeChatFileLinkContextSelector, findNativeChatTabOwnerWorktreeId, resolveNativeChatFileLink, resolveNativeChatFileLinkContext, @@ -144,6 +145,21 @@ describe('resolveNativeChatFileLinkContext', () => { }) }) +describe('createNativeChatFileLinkContextSelector', () => { + it('reuses the context across unrelated writes and re-resolves when a route input changes', () => { + const select = createNativeChatFileLinkContextSelector('tab-1') + const base = state() + const first = select(base) + expect(select({ ...base })).toBe(first) + + const moved = select({ + ...base, + worktreesByRepo: { repo: [worktreeFixture('wt-1', '/repo/moved')] } + }) + expect(moved?.worktreePath).toBe('/repo/moved') + }) +}) + describe('floating workspace native chat', () => { const floatingTab = { id: 'floating-chat-1', diff --git a/src/renderer/src/components/native-chat/native-chat-file-link.ts b/src/renderer/src/components/native-chat/native-chat-file-link.ts index 73eb4b9adf3..18fd1f46d15 100644 --- a/src/renderer/src/components/native-chat/native-chat-file-link.ts +++ b/src/renderer/src/components/native-chat/native-chat-file-link.ts @@ -12,7 +12,11 @@ import { } from '../../../../shared/explicit-file-link-target' import { runtimeEnvironmentIdOrLocalUnlessTwin } from '@/lib/worktree-runtime-owner' import type { AppState } from '@/store/types' -import { resolveNativeChatTabDirectory } from './native-chat-tab-directory' +import type { WorktreeRuntimeOwnerState } from '@/lib/worktree-runtime-owner-state' +import { + resolveNativeChatTabDirectory, + type NativeChatTabDirectoryState +} from './native-chat-tab-directory' export type NativeChatFileLinkContext = { worktreeId: string @@ -87,10 +91,51 @@ export function resolveNativeChatFileLinkContext( ) } +type FileLinkContextInputState = WorktreeRuntimeOwnerState & NativeChatTabDirectoryState + +// Every field the directory and owner resolvers read. +const FILE_LINK_CONTEXT_INPUT_KEYS = [ + 'repos', + 'settings', + 'worktreesByRepo', + 'detectedWorktreesByRepo', + 'folderWorkspaces', + 'projectGroups', + 'restoredRuntimeHostIdByWorkspaceSessionKey', + 'activeWorktreeId', + 'activeWorkspaceExecutionHostId', + 'runtimeEnvironments', + 'runtimeEnvironmentCatalogHydrated', + 'removedRuntimeEnvironmentIds', + 'floatingWorkspacePath', + 'structuredSessionLaunchDirectoryByTabId' +] as const satisfies readonly (keyof FileLinkContextInputState)[] +type UnlistedInput = Exclude< + keyof FileLinkContextInputState, + (typeof FILE_LINK_CONTEXT_INPUT_KEYS)[number] | 'unifiedTabsByWorktree' +> +// Fails to compile when either state type gains a key not listed above; deeper resolver types are not checked. +const allInputsListed: [UnlistedInput] extends [never] ? true : UnlistedInput = true +void allInputsListed + export function createNativeChatFileLinkContextSelector(tabId: string) { const selectOwner = createNativeChatTabOwnerSelector(tabId) - return (state: NativeChatFileLinkState) => - resolveNativeChatFileLinkContextForOwner(state, tabId, selectOwner(state)) + let inputs: unknown[] | null = null + let context: NativeChatFileLinkContext | null = null + // Hidden chats still subscribe, so unrelated store writes must not re-resolve directory/owner. + return (state: NativeChatFileLinkState & Partial) => { + const owner = selectOwner(state) + const next = [ + owner, + state.unifiedTabsByWorktree, + ...FILE_LINK_CONTEXT_INPUT_KEYS.map((key) => state[key]) + ] + if (!inputs || next.some((input, index) => input !== inputs?.[index])) { + inputs = next + context = resolveNativeChatFileLinkContextForOwner(state, tabId, owner) + } + return context + } } export function resolveNativeChatFileLinkContextForOwner(