From de3a8799d6880caa55a99aebb3a07e859da3f110 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:15:27 -0400 Subject: [PATCH] fix(studio): invalidate notebook caches after assistant create/update (#49415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - The assistant's `create_notebook`/`update_notebook` tools run entirely server-side, so an open notebook tab's React Query cache and Valtio store never learn a write happened — the tab keeps showing stale content until a manual reload. - Adds `collectNotebookCacheEffects`/`applyNotebookCacheEffects` (`apps/studio/lib/ai/notebook-cache-invalidation.ts`), which scan finished assistant messages for completed `create_notebook`/`update_notebook` tool calls and evict the affected notebook via `evictNotebookFromCaches` (`apps/studio/data/content/notebooks/notebook-cache.ts`), plus invalidate the nav list. - Wired into `createChatInstance`'s `onFinish` in `state/ai-assistant-state.tsx`, with per-chat dedupe so replayed history isn't reprocessed. - Removes the cache entry outright rather than invalidating it, since a remounting `useNotebookQuery` would otherwise read the stale cached value synchronously before its refetch lands. - Explicitly skips eviction when the open tab has unsaved local edits, so an assistant write can't silently discard them. Related: [FE-4235](https://linear.app/supabase/issue/FE-4235) **Out of scope:** this only protects the client-side cache/store from being clobbered after the fact. Preventing the assistant's `update_notebook` tool call itself from overwriting a user's unsaved edits (a data-layer conflict, not a cache-freshness one) is tracked separately in [FE-4255](https://linear.app/supabase/issue/FE-4255). ## Test plan - [x] `pnpm test:studio -- notebook-cache notebook-cache-invalidation ai-assistant-state.notebook-cache-invalidation ExplorerNotebookTab.assistant-cache-invalidation ExplorerNotebookTabCoordinator` — all passing - [x] Reproduction-first component test (`ExplorerNotebookTab.assistant-cache-invalidation.test.tsx`) — verified it fails without the fix (stale content persists) and passes with it - [x] Regression test for the dirty-notebook guard (an edited, unsaved notebook is left untouched by an assistant write) - [x] `pnpm typecheck --filter=studio` / `pnpm lint --filter=studio` clean ## Summary by CodeRabbit * **Bug Fixes** * Notebook changes made through the AI assistant now appear correctly in open notebook tabs and after reopening them. * Saved notebook caches are refreshed after completed create or update actions, preventing stale content from being displayed. * Unsaved notebook changes are preserved during cache cleanup. * Closing a notebook tab now consistently removes its cached content. * **Tests** * Added coverage for assistant-driven updates, remounts, duplicate actions, project context changes, and cache behavior. --- .../ExplorerNotebookTabCoordinator.tsx | 2 +- ...kTab.assistant-cache-invalidation.test.tsx | 176 +++++++++++++++++ .../content/notebooks/notebook-cache.test.ts | 24 +-- .../data/content/notebooks/notebook-cache.ts | 18 +- .../ai/notebook-cache-invalidation.test.ts | 182 ++++++++++++++++++ .../lib/ai/notebook-cache-invalidation.ts | 67 +++++++ apps/studio/lib/ai/test-fixtures.ts | 22 +++ ...-state.notebook-cache-invalidation.test.ts | 181 +++++++++++++++++ apps/studio/state/ai-assistant-state.tsx | 29 +++ 9 files changed, 666 insertions(+), 35 deletions(-) create mode 100644 apps/studio/components/interfaces/Explorer/__tests__/ExplorerNotebookTab.assistant-cache-invalidation.test.tsx create mode 100644 apps/studio/lib/ai/notebook-cache-invalidation.test.ts create mode 100644 apps/studio/lib/ai/notebook-cache-invalidation.ts create mode 100644 apps/studio/state/ai-assistant-state.notebook-cache-invalidation.test.ts diff --git a/apps/studio/components/interfaces/Explorer/ExplorerNotebookTabCoordinator.tsx b/apps/studio/components/interfaces/Explorer/ExplorerNotebookTabCoordinator.tsx index ceccee89c71..21f9af24ffa 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerNotebookTabCoordinator.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerNotebookTabCoordinator.tsx @@ -54,7 +54,7 @@ export const ExplorerNotebookTabCoordinator = () => { const notebookId = tab.metadata?.notebookId if (!ref || !notebookId) return - evictNotebookFromCaches({ queryClient, projectRef: ref, id: notebookId, mode: 'remove' }) + evictNotebookFromCaches({ queryClient, projectRef: ref, id: notebookId }) }, confirmClose: (notebookTabs) => { const dirtyCount = notebookTabs.filter((tab) => { diff --git a/apps/studio/components/interfaces/Explorer/__tests__/ExplorerNotebookTab.assistant-cache-invalidation.test.tsx b/apps/studio/components/interfaces/Explorer/__tests__/ExplorerNotebookTab.assistant-cache-invalidation.test.tsx new file mode 100644 index 00000000000..6762874a9b9 --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/__tests__/ExplorerNotebookTab.assistant-cache-invalidation.test.tsx @@ -0,0 +1,176 @@ +import { QueryClient } from '@tanstack/react-query' +import { screen } from '@testing-library/react' +import { HttpResponse } from 'msw' +import { describe, expect, it, vi } from 'vitest' + +import { ExplorerNotebookTab } from '../ExplorerNotebookTab' +import type { components } from '@/data/api' +import { contentKeys } from '@/data/content/keys' +import { + applyNotebookCacheEffects, + collectNotebookCacheEffects, +} from '@/lib/ai/notebook-cache-invalidation' +import { createAssistantMessageWithUpdateNotebookTool } from '@/lib/ai/test-fixtures' +import { notebooksState } from '@/state/notebooks/notebooks-state' +import type { Notebook } from '@/state/notebooks/types' +import { createTabsState, TabsStateContext } from '@/state/tabs' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' +import { setupSqlEditorMocks } from '@/tests/lib/sql-editor-test-utils' + +const PROJECT_REF = 'default' +const NOTEBOOK_ID = 'notebook-assistant-cache-test' + +vi.mock('common', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + IS_PLATFORM: true, + useParams: () => ({ ref: 'default', id: 'notebook-assistant-cache-test' }), + useFlag: () => false, + } +}) + +vi.mock('@/components/ui/CodeEditor/CodeEditor', () => ({ + CodeEditor: ({ value }: { value: string }) => ( +