mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
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:
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user