feat(ai): add panel shortcut and conversation restore

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-08 22:34:26 +08:00
1 parent b70d4153a4
commit 798733fd29
23 files changed
+165 -2

No files matched your search

@@ -292,6 +292,9 @@ const assistantMode = ref<AiAssistantMode>("ask");
// The selection is loaded asynchronously. Apply it once when this panel mounts,
// but do not let later setting changes alter an active conversation.
let defaultModeInitialized = false;
let initialConversationStateLoaded = false;
let initialConversationRestored = false;
let assistantViewMounted = false;
watch(
() => settings.isAiConfigLoaded,
(loaded) => {
@@ -305,6 +308,17 @@ watch(
const currentSessionId = ref("");
const conversationId = ref("");
const conversations = ref<AiConversation[]>([]);
function restoreInitialConversation() {
if (!assistantViewMounted || initialConversationRestored || !initialConversationStateLoaded || !settings.isAiConfigLoaded) return;
initialConversationRestored = true;
if (settings.restoreLastConversation && conversations.value[0]) {
selectConversation(conversations.value[0]);
}
}
watch(
() => settings.isAiConfigLoaded,
() => restoreInitialConversation(),
);
const conversationSearchQuery = ref("");
const conversationSearchInput = ref<HTMLInputElement | null>(null);
const conversationSearchIndex = computed(() => buildAiConversationSearchIndex(conversations.value));
@@ -312,7 +326,6 @@ const filteredConversations = computed(() => filterAiConversationSearchIndex(con
const showConversationList = ref(false);
const showTemplateSelector = ref(false);
const modeActionOpen = ref(false);
let assistantViewMounted = false;
// A normal-send FIFO run recovered at startup as an editable pending draft.
// When the user opens that conversation, the draft is loaded into the input
// box and this banner explains it is an unsent, resendable request.
@@ -3934,7 +3947,11 @@ async function persistPendingInputRecovery(conversation: AiConversation, message
createdAt: conversation.createdAt,
updatedAt,
};
await saveAiRunState(snapshot, run).catch(() => {});
await saveAiRunState(snapshot, run)
// Recovery updates the conversation timestamp. Keep the in-memory list in
// the same order before the initial "restore latest" decision runs.
.then(() => syncPersistedConversation(snapshot))
.catch(() => {});
}
async function persistConversation() {
@@ -4314,6 +4331,10 @@ onMounted(async () => {
}
}
}
initialConversationStateLoaded = true;
// Restore only after persisted background runs have been registered, so the
// selected conversation reflects the same runtime state as manual selection.
restoreInitialConversation();
shikiCodeHighlighter.value = await createAiShikiCodeHighlighter({
appearance: () => aiCodeAppearance.value,
}).catch(() => undefined);
@@ -7941,6 +7941,22 @@ LIMIT 100;</pre
</div>
</div>
<!-- Restore last AI conversation (list mode, global) -->
<div v-if="aiConfigListMode === 'list'" class="space-y-3">
<Separator />
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="ai-restore-last-conversation">
{{ t("ai.restoreLastConversation") }}
</Label>
<p class="text-xs text-muted-foreground">
{{ t("ai.restoreLastConversationDescription") }}
</p>
</div>
<Switch id="ai-restore-last-conversation" :model-value="settingsStore.restoreLastConversation" @update:model-value="(value) => settingsStore.setRestoreLastConversation(Boolean(value))" />
</div>
</div>
<!-- Max Retries (list mode, global) -->
<div v-if="aiConfigListMode === 'list'" class="space-y-3">
<Separator />
@@ -396,6 +396,13 @@ describe("AI assistant uses platform-specific conversation lifecycle", () => {
expect(syncBody).toContain("conversations.value.sort(");
});
it("reorders recovered conversations before restoring the most recent one", () => {
const recoveryBody = bodyOf("async function persistPendingInputRecovery(");
expect(recoveryBody).toContain(".then(() => syncPersistedConversation(snapshot))");
const mountedBody = bodyOf("onMounted(async () => {");
expect(mountedBody.indexOf("initialConversationStateLoaded = true;")).toBeLessThan(mountedBody.indexOf("restoreInitialConversation();"));
});
it("does not replace an in-memory desktop run when the AI panel remounts", () => {
const mountedBody = bodyOf("onMounted(async () => {");
expect(mountedBody).toContain("const liveRun = desktopAiRun<ChatMessage>(persistedRun.conversationId);");