fix(ai): initialize conversation bindings before default action selection

This commit is contained in:
t8y2 committed 2026-09-25 01:56:25 +08:00
1 parent c4a82a1e7a
commit 1bc6f70cd8
2 files changed
+86 -13

No files matched your search

@@ -327,19 +327,6 @@ let defaultModeInitialized = false;
let initialConversationStateLoaded = false;
let initialConversationRestored = false;
let assistantViewMounted = false;
watch(
() => settings.isAiConfigLoaded,
(loaded) => {
if (loaded && !defaultModeInitialized) {
assistantMode.value = settings.defaultAiMode;
// Same apply-once rule as the mode: later setting changes must not
// disturb an active conversation.
activeAction.value = resolveDefaultActionSelection(settings.defaultAiMode);
defaultModeInitialized = true;
}
},
{ immediate: true },
);
const currentSessionId = ref("");
const conversationId = ref("");
const conversations = ref<AiConversation[]>([]);
@@ -368,6 +355,25 @@ const boundConnection = computed(() => (boundConnectionId.value ? connectionStor
const boundDatabase = computed(() => conversationBinding.value.database);
const boundSchema = computed(() => conversationBinding.value.schema);
// `immediate` runs this during setup whenever the AI config finished loading
// before the panel mounted (the usual case: the app loads it at startup), and
// `resolveDefaultActionSelection` reads `boundConnection`. It must therefore
// stay below the binding computeds, or setup throws a TDZ ReferenceError and
// the panel never mounts.
watch(
() => settings.isAiConfigLoaded,
(loaded) => {
if (loaded && !defaultModeInitialized) {
assistantMode.value = settings.defaultAiMode;
// Same apply-once rule as the mode: later setting changes must not
// disturb an active conversation.
activeAction.value = resolveDefaultActionSelection(settings.defaultAiMode);
defaultModeInitialized = true;
}
},
{ immediate: true },
);
/**
* Binding the visible conversation's *active run* is executing against.
*
@@ -0,0 +1,67 @@
// @vitest-environment happy-dom
//
// Mounted smoke test for the AI panel. The app loads the AI config at startup,
// so the panel normally mounts with `isAiConfigLoaded` already true and its
// `immediate` default-selection watcher runs during setup. That path once read
// `boundConnection` before it was declared, and the TDZ ReferenceError kept the
// panel from opening at all.
import { createApp, h } from "vue";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createPinia } from "pinia";
import i18n from "@/i18n";
import { TooltipProvider } from "@/components/ui/tooltip";
import AiAssistant from "@/components/editor/AiAssistant.vue";
import { useSettingsStore } from "@/stores/settingsStore";
vi.mock("@/lib/backend/api", async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>();
const empty = () => Promise.resolve([]);
return {
...actual,
loadAiConversations: empty,
loadAiRuns: empty,
readUserSkills: empty,
loadAiConfigs: empty,
loadPromptTemplates: empty,
getAiGlobalCustomInstructions: () => Promise.resolve(""),
saveAiChatSelection: () => Promise.resolve(),
loadAiChatSelection: () => Promise.resolve(null),
};
});
const cleanups: Array<() => void> = [];
afterEach(() => {
while (cleanups.length) cleanups.pop()?.();
});
async function mountPanel(aiConfigLoaded: boolean): Promise<unknown[]> {
const pinia = createPinia();
const errors: unknown[] = [];
const app = createApp({ render: () => h(TooltipProvider, () => h(AiAssistant)) });
app.use(pinia);
app.use(i18n);
app.config.errorHandler = (error) => errors.push(error);
app.config.warnHandler = () => {};
useSettingsStore(pinia).isAiConfigLoaded = aiConfigLoaded;
const container = document.createElement("div");
document.body.append(container);
app.mount(container);
cleanups.push(() => {
app.unmount();
container.remove();
});
// Let mount-time loads settle so their failures surface here too.
await new Promise((resolve) => setTimeout(resolve, 20));
return errors;
}
describe("AiAssistant mount", () => {
it("opens when the AI config was loaded before the panel mounted", async () => {
expect((await mountPanel(true)).map(String)).toEqual([]);
});
it("opens while the AI config is still loading", async () => {
expect((await mountPanel(false)).map(String)).toEqual([]);
});
});