diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 5ea507eaaf..ab928d613c 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -48f5e2ec3b3bb903b7c172a6e7925967a464b331 \ No newline at end of file +03ef0ebfad8a36a15d0af576b29e9379f87d484b \ No newline at end of file diff --git a/frontend/src/lib/aiStore.ts b/frontend/src/lib/aiStore.ts index 7eb6f45324..4d374735d9 100644 --- a/frontend/src/lib/aiStore.ts +++ b/frontend/src/lib/aiStore.ts @@ -97,8 +97,14 @@ let loadCopilotToken = 0 // matching its committed workspace so getCurrentModel() can't read the previous // workspace's provider/model while the scoped load is still in flight. export const copilotWorkspace = writable(undefined) +// The workspace of the most recent loadCopilot *request*, set synchronously before the +// await — as opposed to `copilotWorkspace`, which only updates once a load resolves. A +// background refresh must compare against this so it can't supersede an in-flight load for +// a newer workspace (which would otherwise win the monotonic token and restore stale state). +export const copilotWorkspaceRequested = writable(undefined) export async function loadCopilot(workspace: string) { const token = ++loadCopilotToken + copilotWorkspaceRequested.set(workspace) workspaceAIClients.init(workspace) try { const info = await WorkspaceService.getCopilotInfo({ workspace }) diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index e60f98c678..b6db1f216a 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -64,7 +64,7 @@ import { untrack } from 'svelte' import { get } from 'svelte/store' import { BROWSER } from 'esm-env' import { workspaceStore, type DBSchemas } from '$lib/stores' -import { copilotInfo, copilotWorkspace, loadCopilot } from '$lib/aiStore' +import { copilotInfo, copilotWorkspaceRequested, loadCopilot } from '$lib/aiStore' import { askTools, prepareAskSystemMessage, prepareAskUserMessage } from './ask/core' import { readDocsPageTool, searchDocsTool } from './docs/core' import { TypewriterReveal } from './typewriterReveal' @@ -285,9 +285,11 @@ async function refreshFreeTierUsage(workspace: string | undefined) { // The global copilotInfo/client are a singleton shared by every mounted session. A warm // session can finish a turn after the user has switched to another workspace; refreshing // then would call loadCopilot for the completing manager's (now background) workspace and - // clobber the active one's models/client/copilotWorkspace. Only refresh when the global - // state still reflects this manager's workspace. - if (get(copilotWorkspace) !== workspace) return + // clobber the active one's models/client. Compare against the most-recently-*requested* + // workspace (set synchronously), not the last-*resolved* one — otherwise a refresh could + // fire while a newer workspace's load is still in flight, win the monotonic token, and + // restore this (stale) workspace over it. + if (get(copilotWorkspaceRequested) !== workspace) return const info = get(copilotInfo) if (!info.freeTier || info.freeTier.exhausted) return try { diff --git a/frontend/src/lib/components/home/ItemsList.svelte b/frontend/src/lib/components/home/ItemsList.svelte index a8cd23eeab..9edc3c66bc 100644 --- a/frontend/src/lib/components/home/ItemsList.svelte +++ b/frontend/src/lib/components/home/ItemsList.svelte @@ -344,14 +344,13 @@ }) // Content-filter view: reuse the Ctrl-K "Content" search (full-text, EE-gated). - // It loads its own dataset via `.open()`, then filters client-side by `search`. + // It loads its own dataset via `.open()`, then filters client-side by `search`. The + // component is keyed by workspace in the markup, so a workspace switch remounts it (this + // `bind:this` then points at the fresh instance and re-runs `open()`); the old instance is + // discarded, so its late in-flight responses can't overwrite the new workspace's results. let contentSearchEl: ContentSearchInner | undefined = $state() $effect(() => { const el = contentSearchEl - // Re-run on workspace switch so content results follow the active workspace; `open()` - // reads $workspaceStore internally but under untrack (it must not re-run per keystroke), - // so depend on it explicitly here. - $workspaceStore if (el) untrack(() => el.open()) }) @@ -859,7 +858,11 @@ contents via the indexer and shows the matching snippets; when the instance isn't EE it renders its own warning + a limited fallback search. -->
- + + {#key $workspaceStore} + + {/key}
{:else if filteredItems == undefined}