restore context-appropriate chat when switching between views in docked/panel mode

Previously the context watcher only handled notebook → non-notebook (clearing activeChatId). This caused two problems: navigating into a notebook (or between notebooks) left the panel showing whatever chat was last open in the non-notebook context, and the per-page notebook chat cache was ignored whenever a global activeChatId happened to be set.

The context watcher now handles all transitions for docked/panel mode:
- any → notebook (including notebook A → notebook B): restores the per-page cached exchange, falls back to the most recent chat in that page's history, then starts fresh if none exists
- notebook → non-notebook: clears proposals and starts a new chat so the panel doesn't keep showing a notebook-specific conversation

onMounted is updated to match the same logic: notebook context always looks up the per-page cache first, then falls back to page history rather than falling through to the global activeChatId (which may belong to a completely unrelated context).

Test updates:
- wrapper.destroy() added to afterEach to prevent stale watcher callbacks from leaking across tests and polluting the shared mock call counters
- Branch 4 updated: notebook context with no cached exchange now correctly falls back to page history, not activeChatId; chatHistoryItems option added to mountGalaxyAI to pre-populate history and stub loadHistory (chatStore imports @/api/client which is not mocked globally)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ahmed Awan
2026-06-10 11:09:23 -05:00
co-authored by Claude
parent 930a75b5a1
commit ed712c1b66
2 changed files with 90 additions and 26 deletions
+26 -6
View File
@@ -85,6 +85,7 @@ vi.mock("vue-router/composables", () => ({
// ── Helpers ───────────────────────────────────────────────────────────────────
const localVue = getLocalVue();
let lastWrapper: ReturnType<typeof shallowMount> | null = null;
const EXCHANGE_ID = "exchange-abc";
const MESSAGES_RESPONSE = [
@@ -109,6 +110,8 @@ interface MountOptions {
activeChatId?: string | null;
cachedExchangeId?: string | null;
pageId?: string;
/** Pre-populate chatStore.chatHistory and stub loadHistory as a no-op so no real HTTP call is made. */
chatHistoryItems?: { id: string }[];
}
function mountGalaxyAI({
@@ -116,6 +119,7 @@ function mountGalaxyAI({
activeChatId = null,
cachedExchangeId = null,
pageId = "page-1",
chatHistoryItems,
}: MountOptions = {}) {
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false });
setActivePinia(pinia);
@@ -123,6 +127,13 @@ function mountGalaxyAI({
const chatStore = useChatStore();
chatStore.activeChatId = activeChatId as any;
if (chatHistoryItems !== undefined) {
chatStore.chatHistory = chatHistoryItems as any;
// Stub loadHistory so it doesn't make real HTTP calls (chatStore imports @/api/client
// which is not mocked); keep chatHistory as pre-populated.
vi.spyOn(chatStore, "loadHistory").mockResolvedValue(undefined);
}
const pageEditorStore = usePageEditorStore();
if (cachedExchangeId) {
pageEditorStore.setCurrentChatExchangeId(pageId, cachedExchangeId);
@@ -144,6 +155,7 @@ function mountGalaxyAI({
},
});
lastWrapper = wrapper;
return { wrapper, chatStore, pageEditorStore };
}
@@ -157,6 +169,8 @@ describe("GalaxyAI fetch operations on mount", () => {
});
afterEach(() => {
lastWrapper?.destroy();
lastWrapper = null;
vi.restoreAllMocks();
});
@@ -217,20 +231,26 @@ describe("GalaxyAI fetch operations on mount", () => {
});
});
// Branch 4: docked/panel — notebook context, no cached exchange but activeChatId set
describe("docked/panel — notebook context, no cached ID but activeChatId present", () => {
it("fetches from activeChatId", async () => {
// Branch 4: docked/panel — notebook context, no cached exchange but page has history
describe("docked/panel — notebook context, no cached ID but page has history", () => {
it("fetches the most recent chat from page history (ignores global activeChatId)", async () => {
// activeChatId is set but belongs to a different context — must not be used for notebook.
makeSuccessfulFetch();
mockActiveContext.value = { contextType: "notebook", pageId: "page-1", historyId: "hist-1" };
mountGalaxyAI({ props: { panel: true }, activeChatId: EXCHANGE_ID });
mountGalaxyAI({
props: { panel: true },
activeChatId: "some-other-chat",
chatHistoryItems: [{ id: EXCHANGE_ID }],
});
await flushPromises();
expect(mockGetMessages).toHaveBeenCalledOnce();
});
});
// Branch 5: docked/panel — notebook context, no cached exchange, no activeChatId
describe("docked/panel — notebook context, no cached ID and no activeChatId", () => {
// Branch 5: docked/panel — notebook context, no cached exchange, empty history
describe("docked/panel — notebook context, no cached ID and empty history", () => {
it("starts a new chat without fetching", async () => {
// beforeEach already mocks history as empty
mockActiveContext.value = { contextType: "notebook", pageId: "page-1", historyId: "hist-1" };
mountGalaxyAI({ props: { docked: true }, activeChatId: null });
await flushPromises();
+64 -20
View File
@@ -62,18 +62,44 @@ const { activeContext, contextLabel } = useActiveContext();
const pageEditorStore = usePageEditorStore();
const contextDismissed = ref(false);
watch(activeContext, (newCtx, oldCtx) => {
watch(activeContext, async (newCtx, oldCtx) => {
contextDismissed.value = false;
// When navigating away from a notebook, clear activeChatId if it belongs to that
// page (i.e. was loaded from the page-scoped history) so the panel doesn't keep
// showing a notebook-specific conversation in a non-notebook context.
if (oldCtx?.contextType === "notebook" && newCtx?.contextType !== "notebook") {
const currentId = chatStore.activeChatId;
if (currentId && chatStore.chatHistory.some((item) => item.id === currentId)) {
chatStore.setActiveChatId(null);
if (!props.docked && !props.panel) {
return;
}
const switchingToNotebook =
newCtx?.contextType === "notebook" && (oldCtx?.contextType !== "notebook" || oldCtx.pageId !== newCtx.pageId);
if (switchingToNotebook) {
// Switched to a notebook page (or to a different notebook page): restore this page's
// cached exchange, or fall back to the most recent chat in this page's history.
const pageId = newCtx.pageId;
const cachedId = pageEditorStore.getCurrentChatExchangeId(pageId);
if (cachedId) {
await fetchConversation(cachedId);
if (messages.value.length === 0) {
pageEditorStore.clearCurrentChatExchangeId(pageId);
startNewChat();
}
} else {
try {
await chatStore.loadHistory(pageId);
} catch (e) {
Toast.error(errorMessageAsString(e), "Failed to load chat history");
}
const latestChat = chatStore.chatHistory[0];
if (latestChat) {
await fetchConversation(latestChat.id);
} else {
startNewChat();
}
}
} else if (oldCtx?.contextType === "notebook" && newCtx?.contextType !== "notebook") {
// Switched away from notebook: drop notebook-specific state and start fresh.
clearProposals();
startNewChat();
}
});
@@ -136,22 +162,40 @@ onMounted(async () => {
startNewChat();
} else if (props.docked || props.panel) {
const ctx = activeContext.value;
// For notebook pages, prefer the per-page cached exchange over the global activeChatId.
// For notebook pages, always prefer the per-page cached exchange over the global
// activeChatId — the global one may belong to a completely unrelated normal chat.
const notebookPageId = ctx?.contextType === "notebook" ? ctx.pageId : null;
const cachedId = notebookPageId && !chatStore.activeChatId
? pageEditorStore.getCurrentChatExchangeId(notebookPageId)
: null;
const chatId = cachedId || chatStore.activeChatId || null;
if (chatId) {
await fetchConversation(chatId);
// If the cached notebook exchange was deleted/empty, drop the stale ID.
if (cachedId && messages.value.length === 0) {
pageEditorStore.clearCurrentChatExchangeId(notebookPageId!);
startNewChat();
if (notebookPageId) {
// Notebook context: prefer per-page cached exchange, fall back to page history.
const cachedId = pageEditorStore.getCurrentChatExchangeId(notebookPageId);
if (cachedId) {
await fetchConversation(cachedId);
if (messages.value.length === 0) {
pageEditorStore.clearCurrentChatExchangeId(notebookPageId);
startNewChat();
}
} else {
try {
await chatStore.loadHistory(notebookPageId);
} catch (e) {
Toast.error(errorMessageAsString(e), "Failed to load chat history");
}
const latestChat = chatStore.chatHistory[0];
if (latestChat) {
await fetchConversation(latestChat.id);
} else {
startNewChat();
}
}
} else {
startNewChat();
// Non-notebook context: use global activeChatId if available.
const chatId = chatStore.activeChatId;
if (chatId) {
await fetchConversation(chatId);
} else {
startNewChat();
}
}
} else {
await loadLatestChat();