fix(ai): reset action when reselecting active mode

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-07-27 12:33:06 +08:00
1 parent f9c2fb27d3
commit 122853d089
10 files changed
+245 -114

No files matched your search

+149 -107
View File
@@ -40,7 +40,6 @@ import {
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import LightDropdown from "@/components/ui/LightDropdown.vue";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useTheme } from "@/composables/useTheme";
import { useSettingsStore, AI_PROVIDER_PRESETS, normalizeAiConfig } from "@/stores/settingsStore";
@@ -147,12 +146,13 @@ const messages = ref<ChatMessage[]>([]);
const isGenerating = ref(false);
const scrollRef = ref<InstanceType<typeof ScrollArea> | null>(null);
const activeAction = ref<AiAction>("general");
const assistantMode = ref<"ask" | "agent">("ask");
const assistantMode = ref<AiAssistantMode>("ask");
const currentSessionId = ref("");
const conversationId = ref("");
const conversations = ref<AiConversation[]>([]);
const showConversationList = ref(false);
const showTemplateSelector = ref(false);
const modeActionOpen = ref(false);
// Prompt template selection (panel-session scope)
const activeTemplateIds = ref<string[]>([]);
@@ -213,6 +213,12 @@ const templateSelectorLabel = computed(() => {
if (count === 1) return name;
return `${name} +${count - 1}`;
});
const templateSelectorTriggerLabel = computed(() => {
if (activeTemplates.value.length === 0) {
return t("ai.templateSelectorLabel", { label: templateSelectorLabel.value });
}
return templateSelectorLabel.value;
});
const promptTextareaRef = ref<HTMLTextAreaElement | null>(null);
const shouldAutoScroll = ref(true);
const userPausedAutoScroll = ref(false);
@@ -588,28 +594,6 @@ function sendProposalReply(positive: boolean) {
}
const activePlaceholder = computed(() => `${t(`ai.placeholders.${activeAction.value}`)} ${t("ai.tableMentionPlaceholderHint")}`);
const activeModeHint = computed(() => t(`ai.modeHints.${assistantMode.value}`));
const assistantModeItems = computed(() => [
{
value: "ask",
label: t("ai.modes.ask"),
title: t("ai.modeHints.ask"),
icon: MessageSquarePlus,
},
{
value: "agent",
label: t("ai.modes.agent"),
title: t("ai.modeHints.agent"),
icon: Bot,
},
]);
const actionMenuItems = computed(() =>
actionButtons.value.map((button) => ({
value: button.action,
label: t(button.key),
icon: button.icon,
})),
);
const aiCodeAppearance = computed(() => (isDark.value ? "dark" : "light"));
const showActionButtons = computed(() => {
@@ -617,6 +601,30 @@ const showActionButtons = computed(() => {
return !isVectorDbType(props.connection.db_type);
});
const modeIcon = computed<Component>(() => (assistantMode.value === "agent" ? Bot : MessageSquarePlus));
const modeLabel = computed(() => t(`ai.modes.${assistantMode.value}`));
const selectedActionButton = computed<AiActionButton | undefined>(() => actionButtons.value.find((b) => b.action === activeAction.value));
const modeActionTriggerLabel = computed(() => {
const modePart = `${modeLabel.value}`;
if (!showActionButtons.value || !selectedActionButton.value) return modePart;
return `${modePart} · ${t(selectedActionButton.value.key)}`;
});
function switchModeActionTab(mode: "ask" | "agent") {
activeAction.value = resolveDefaultAction(mode);
if (assistantMode.value !== mode) {
// Set the mode after the action so the tab label and picker stay aligned.
assistantMode.value = mode;
}
}
function selectModeActionItem(action: AiAction) {
// Vector databases only support generation; keep this constraint at the selection boundary.
if (!showActionButtons.value) return;
selectAction(action);
modeActionOpen.value = false;
}
const { databaseOptions: allDbOptions, loadDatabaseOptions } = useDatabaseOptions();
const dbOptions = computed(() => {
@@ -1713,6 +1721,9 @@ async function deleteConversation(id: string) {
function startNewChat() {
clearMessages();
showConversationList.value = false;
// A fresh conversation always starts from the safe, non-executing default.
assistantMode.value = "ask";
activeAction.value = resolveDefaultAction("ask");
}
onMounted(async () => {
@@ -2088,54 +2099,95 @@ async function openExternalUrl(url: string) {
<div ref="promptPanelRef" class="relative rounded-[6px] border bg-background">
<div class="resize-handle" @mousedown="startResize"></div>
<div class="px-2 pb-2 pt-1">
<div v-if="connectionStore.connections.length" class="flex items-center gap-1 mb-1 text-xs text-foreground/80">
<DatabaseIcon v-if="connection" :db-type="connectionIconType(connection)" class="h-3 w-3 shrink-0" />
<Server v-else class="h-3 w-3 shrink-0" />
<Select
:model-value="connection?.id || ''"
@update:model-value="
(v) => {
if (typeof v === 'string') changeConnection(v);
}
"
>
<SelectTrigger class="h-5 w-auto border-0 rounded-md bg-transparent dark:bg-transparent p-0 px-1 text-xs text-foreground/80 shadow-none focus:ring-0 focus-visible:ring-0 [&_svg]:size-3">
<SelectValue :placeholder="t('editor.selectConnection')">{{ connection?.name || t("editor.selectConnection") }}</SelectValue>
</SelectTrigger>
<SelectContent class="min-w-48">
<SelectItem v-for="conn in connectionStore.connections" :key="conn.id" :value="conn.id">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(conn)" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="conn.id" />
<span class="truncate">{{ conn.name }}</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<template v-if="connection">
<Database class="h-3 w-3 shrink-0 text-foreground/40" />
<div class="flex items-center gap-1 mb-1 text-xs text-foreground/80">
<template v-if="connectionStore.connections.length">
<DatabaseIcon v-if="connection" :db-type="connectionIconType(connection)" class="h-3 w-3 shrink-0" />
<Server v-else class="h-3 w-3 shrink-0" />
<Select
:model-value="selectedDatabaseSelectValue"
:model-value="connection?.id || ''"
@update:model-value="
(v) => {
if (typeof v === 'string') changeDatabase(v);
}
"
@update:open="
(open: boolean) => {
if (open) loadDatabases();
if (typeof v === 'string') changeConnection(v);
}
"
>
<SelectTrigger class="h-5 w-auto border-0 rounded-md bg-transparent dark:bg-transparent p-0 px-1 text-xs text-foreground/80 shadow-none focus:ring-0 focus-visible:ring-0 [&_svg]:size-3">
<SelectValue :placeholder="t('editor.selectDatabase')">{{ selectedDatabaseLabel }}</SelectValue>
<SelectValue :placeholder="t('editor.selectConnection')">{{ connection?.name || t("editor.selectConnection") }}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="option in dbSelectOptions" :key="option.value" :value="option.value">{{ option.label }}</SelectItem>
<SelectItem v-if="!dbSelectOptions.length && connection && tab" :value="selectedDatabaseSelectValue">{{ selectedDatabaseLabel }}</SelectItem>
<SelectContent class="min-w-48">
<SelectItem v-for="conn in connectionStore.connections" :key="conn.id" :value="conn.id">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(conn)" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="conn.id" />
<span class="truncate">{{ conn.name }}</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<template v-if="connection">
<Database class="h-3 w-3 shrink-0 text-foreground/40" />
<Select
:model-value="selectedDatabaseSelectValue"
@update:model-value="
(v) => {
if (typeof v === 'string') changeDatabase(v);
}
"
@update:open="
(open: boolean) => {
if (open) loadDatabases();
}
"
>
<SelectTrigger class="h-5 w-auto border-0 rounded-md bg-transparent dark:bg-transparent p-0 px-1 text-xs text-foreground/80 shadow-none focus:ring-0 focus-visible:ring-0 [&_svg]:size-3">
<SelectValue :placeholder="t('editor.selectDatabase')">{{ selectedDatabaseLabel }}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="option in dbSelectOptions" :key="option.value" :value="option.value">{{ option.label }}</SelectItem>
<SelectItem v-if="!dbSelectOptions.length && connection && tab" :value="selectedDatabaseSelectValue">{{ selectedDatabaseLabel }}</SelectItem>
</SelectContent>
</Select>
</template>
</template>
<span class="min-w-0 flex-1" />
<!-- Template selector -->
<Popover v-model:open="showTemplateSelector">
<PopoverTrigger as-child>
<button type="button" class="flex min-w-0 max-w-[40%] items-center gap-1 rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground" :aria-label="templateSelectorTriggerLabel" :title="templateSelectorTriggerLabel">
<FileCode class="h-3 w-3" />
<span class="truncate">{{ templateSelectorTriggerLabel }}</span>
<svg class="h-3 w-3 shrink-0 opacity-60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
</button>
</PopoverTrigger>
<PopoverContent align="end" class="w-64 gap-0 p-1.5">
<div class="max-h-64 overflow-auto">
<div v-if="!promptTemplateStore.isLoaded" class="px-3 py-4 text-center text-xs text-muted-foreground">
{{ t("ai.templateSelectorLoading") }}
</div>
<div v-else-if="promptTemplateStore.templates.length === 0" class="px-3 py-4 text-center text-xs text-muted-foreground">
{{ t("ai.templateSelectorEmpty") }}
</div>
<template v-else>
<template v-for="tpl in promptTemplateStore.templates" :key="tpl.id">
<button type="button" class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs hover:bg-muted" @click="toggleTemplateId(tpl.id)">
<div class="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border" :class="activeTemplateIds.includes(tpl.id) ? 'border-primary bg-primary text-primary-foreground' : ''">
<Check v-if="activeTemplateIds.includes(tpl.id)" class="h-3 w-3" />
</div>
<div class="flex-1 truncate text-left">
<div class="font-medium">{{ tpl.name }}</div>
<div class="text-[10px] text-muted-foreground truncate">{{ tpl.content.slice(0, 60) }}</div>
</div>
</button>
</template>
</template>
</div>
<div v-if="promptTemplateStore.isLoaded && promptTemplateStore.templates.length > 0" class="border-t mt-1 pt-1 px-1">
<button type="button" class="flex w-full items-center gap-2 rounded-sm px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground" @click="deselectAllTemplates">
{{ t("ai.templateSelectorDeselectAll") }}
</button>
</div>
</PopoverContent>
</Popover>
</div>
<div v-if="mentionOpen" class="absolute bottom-full left-2 right-2 z-20 mb-1 max-h-56 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md">
<div v-if="mentionLoading" class="flex items-center gap-2 px-2 py-2 text-xs text-muted-foreground">
@@ -2214,60 +2266,50 @@ async function openExternalUrl(url: string) {
@keydown="onPromptKeydown"
/>
<div class="flex min-w-0 flex-nowrap items-center gap-1.5 overflow-hidden">
<!-- Template selector -->
<Popover v-model:open="showTemplateSelector">
<!-- Combined mode + action selector -->
<Popover v-model:open="modeActionOpen">
<PopoverTrigger as-child>
<button type="button" class="flex shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground">
<FileCode class="h-3 w-3" />
{{ t("ai.templateSelectorLabel", { label: templateSelectorLabel }) }}
<button type="button" class="flex shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground" :aria-label="modeActionTriggerLabel">
<component :is="modeIcon" class="h-3 w-3" />
<span>{{ modeActionTriggerLabel }}</span>
<svg class="h-3 w-3 shrink-0 opacity-60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
</button>
</PopoverTrigger>
<PopoverContent align="start" class="w-64 gap-0 p-1.5">
<div class="max-h-64 overflow-auto">
<div v-if="!promptTemplateStore.isLoaded" class="px-3 py-4 text-center text-xs text-muted-foreground">
{{ t("ai.templateSelectorLoading") }}
</div>
<div v-else-if="promptTemplateStore.templates.length === 0" class="px-3 py-4 text-center text-xs text-muted-foreground">
{{ t("ai.templateSelectorEmpty") }}
</div>
<template v-else>
<template v-for="tpl in promptTemplateStore.templates" :key="tpl.id">
<button type="button" class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs hover:bg-muted" @click="toggleTemplateId(tpl.id)">
<div class="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border" :class="activeTemplateIds.includes(tpl.id) ? 'border-primary bg-primary text-primary-foreground' : ''">
<Check v-if="activeTemplateIds.includes(tpl.id)" class="h-3 w-3" />
</div>
<div class="flex-1 truncate text-left">
<div class="font-medium">{{ tpl.name }}</div>
<div class="text-[10px] text-muted-foreground truncate">{{ tpl.content.slice(0, 60) }}</div>
</div>
</button>
</template>
</template>
</div>
<div v-if="promptTemplateStore.isLoaded && promptTemplateStore.templates.length > 0" class="border-t mt-1 pt-1 px-1">
<button type="button" class="flex w-full items-center gap-2 rounded-sm px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground" @click="deselectAllTemplates">
{{ t("ai.templateSelectorDeselectAll") }}
<PopoverContent align="start" class="w-56 gap-0 p-1.5" @click.stop>
<!-- Mode tabs -->
<div class="flex items-center gap-1 mb-1.5 px-0.5">
<button
type="button"
class="flex-1 flex items-center justify-center gap-1.5 rounded-sm px-2 py-1 text-xs"
:class="assistantMode === 'ask' ? 'bg-accent text-accent-foreground font-medium' : 'text-muted-foreground hover:text-foreground hover:bg-muted'"
@click="switchModeActionTab('ask')"
>
<MessageSquarePlus class="h-3 w-3" />
{{ t("ai.modes.ask") }}
</button>
<button
type="button"
class="flex-1 flex items-center justify-center gap-1.5 rounded-sm px-2 py-1 text-xs"
:class="assistantMode === 'agent' ? 'bg-accent text-accent-foreground font-medium' : 'text-muted-foreground hover:text-foreground hover:bg-muted'"
@click="switchModeActionTab('agent')"
>
<Bot class="h-3 w-3" />
{{ t("ai.modes.agent") }}
</button>
</div>
<template v-if="showActionButtons">
<div class="border-t my-1" />
<!-- Action list -->
<div class="max-h-56 overflow-auto">
<button v-for="button in actionButtons" :key="button.action" type="button" class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs" :class="activeAction === button.action ? 'bg-accent' : 'hover:bg-muted'" @click="selectModeActionItem(button.action)">
<component :is="button.icon" class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span class="flex-1 text-left">{{ t(button.key) }}</span>
<Check v-if="activeAction === button.action" class="h-3.5 w-3.5 shrink-0" />
</button>
</div>
</template>
</PopoverContent>
</Popover>
<LightDropdown
v-model="assistantMode"
:items="assistantModeItems"
:aria-label="activeModeHint"
trigger-class="flex shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground"
item-class="text-xs px-2"
/>
<LightDropdown
v-if="showActionButtons"
:model-value="activeAction"
:items="actionMenuItems"
content-class="w-max min-w-0"
trigger-class="flex shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground"
item-class="text-xs px-2"
@update:model-value="(value) => selectAction(value as AiAction)"
/>
<span class="min-w-0 flex-1" />
<template v-if="settings.aiConfigs.length > 0">
<!-- Combined provider + model selector -->
+1 -1
View File
@@ -1793,7 +1793,7 @@ export default {
promptTemplateNoTemplates: "No templates yet. Create one to get started.",
promptTemplateManage: "Manage Templates",
promptTemplateSavedHint: "Changes are saved automatically.",
templateSelectorNone: "None",
templateSelectorNone: "Not selected",
templateSelectorButton: "Templates",
templateSelectorLabel: "Templates: {label}",
templateSelectorDeselectAll: "Deselect All",
+1 -1
View File
@@ -1668,7 +1668,7 @@ export default withEnglishFallback({
promptTemplateNoTemplates: "Aún no hay plantillas. Crea una para empezar.",
promptTemplateManage: "Gestionar plantillas",
promptTemplateSavedHint: "Los cambios se guardan automáticamente.",
templateSelectorNone: "Ninguna",
templateSelectorNone: "Sin seleccionar",
templateSelectorButton: "Plantillas",
templateSelectorLabel: "Plantillas: {label}",
templateSelectorDeselectAll: "Deseleccionar todas",
+1 -1
View File
@@ -1618,7 +1618,7 @@ export default withEnglishFallback({
promptTemplateNoTemplates: "Nessun modello ancora. Creane uno per iniziare.",
promptTemplateManage: "Gestisci modelli",
promptTemplateSavedHint: "Le modifiche vengono salvate automaticamente.",
templateSelectorNone: "Nessuno",
templateSelectorNone: "Non selezionato",
templateSelectorButton: "Modelli",
templateSelectorLabel: "Modelli: {label}",
templateSelectorDeselectAll: "Deseleziona tutto",
+1 -1
View File
@@ -1672,7 +1672,7 @@ export default withEnglishFallback({
promptTemplateNoTemplates: "テンプレートがありません。新規作成してください。",
promptTemplateManage: "テンプレート管理",
promptTemplateSavedHint: "変更は自動的に保存されます。",
templateSelectorNone: "なし",
templateSelectorNone: "未選択",
templateSelectorButton: "テンプレート",
templateSelectorLabel: "テンプレート: {label}",
templateSelectorDeselectAll: "すべて解除",
+1 -1
View File
@@ -1670,7 +1670,7 @@ export default withEnglishFallback({
promptTemplateNoTemplates: "Nenhum modelo ainda. Crie um para começar.",
promptTemplateManage: "Gerenciar modelos",
promptTemplateSavedHint: "As alterações são salvas automaticamente.",
templateSelectorNone: "Nenhum",
templateSelectorNone: "Não selecionado",
templateSelectorButton: "Modelos",
templateSelectorLabel: "Modelos: {label}",
templateSelectorDeselectAll: "Desmarcar todos",
+1 -1
View File
@@ -1794,7 +1794,7 @@ export default withEnglishFallback({
promptTemplateNoTemplates: "暂无模板,请新建一个。",
promptTemplateManage: "管理模板",
promptTemplateSavedHint: "修改后自动保存。",
templateSelectorNone: "无",
templateSelectorNone: "未选择",
templateSelectorButton: "模板",
templateSelectorLabel: "模板: {label}",
templateSelectorDeselectAll: "全不选",
+1 -1
View File
@@ -1619,7 +1619,7 @@ export default withEnglishFallback({
promptTemplateNoTemplates: "尚無範本,請新增一個。",
promptTemplateManage: "管理範本",
promptTemplateSavedHint: "修改後自動儲存。",
templateSelectorNone: "無",
templateSelectorNone: "未選取",
templateSelectorButton: "範本",
templateSelectorLabel: "範本: {label}",
templateSelectorDeselectAll: "取消全選",