fix(ai): add inline proposal confirmation buttons

* fix(ai): add quick proposal confirmation buttons (all locales)

When the assistant proposes an action, render Yes/No buttons below the
message so the user does not need to type a short reply — fixing the
issue where short replies caused the LLM to re-ask instead of executing.

- aiProposalDetect.ts: pure detectors for proposal sentences/short replies
- AiAssistant.vue: inline Yes/No buttons on last assistant proposal message
- ai.ts: system-prompt rule to skip re-confirmation on short affirmatives
- i18n: 3 keys added to all 7 locales (en/zh-CN/zh-TW/ja/es/it/pt-BR)

Tests: 55/55. TypeCheck: passed.

* fix(ai): add i18n keys and UI changes for proposal confirmation
This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-06-23 18:25:57 +08:00
1 parent 21ec91a3d5
commit bbd3ccf864
11 files changed
+375

No files matched your search

@@ -35,6 +35,7 @@ import { parseExplainResult, type ParsedExplainPlan } from "@/lib/explainPlan";
import { copyToClipboard } from "@/lib/clipboard";
import { formatAiTableMention, parseAiTableMentions, type AiTableMention } from "@/lib/aiTableMentions";
import { isAiPromptImeCompositionEvent, shouldSubmitAiPromptOnKeydown } from "@/lib/aiPromptKeyboard";
import { looksLikeActionProposal, containsChinese } from "@/lib/aiProposalDetect";
const { t } = useI18n();
const settings = useSettingsStore();
@@ -221,6 +222,37 @@ const isWaitingForFirstDelta = computed(() => {
return isGenerating.value && last?.role === "assistant" && !last.content && !last.reasoning;
});
/**
* The last assistant message whose final line looks like an action
* proposal question. Used to render an inline "Yes / No" confirmation bar
* so the user can answer without typing. `null` while the assistant is
* still generating or when no such message exists.
*/
const proposalConfirmMessage = computed<ChatMessage | null>(() => {
if (isGenerating.value) return null;
for (let i = messages.value.length - 1; i >= 0; i--) {
const msg = messages.value[i];
if (msg.kind === "contextSummary") continue;
if (msg.role !== "assistant") return null;
if (!msg.content) return null;
return looksLikeActionProposal(msg.content) ? msg : null;
}
return null;
});
function sendProposalReply(positive: boolean) {
// Disable while a stream is in flight or no proposal is currently active.
if (isGenerating.value) return;
const target = proposalConfirmMessage.value;
if (!target) return;
const isZh = containsChinese(target.content || "");
const replyZh = positive ? "请执行上面你刚提议的操作,不要再反问确认。" : "不用执行上面提到的操作,继续当前对话。";
const replyEn = positive ? "Execute the action you just proposed above; do not ask for confirmation again." : "Do not execute the action mentioned above; continue the current conversation.";
prompt.value = isZh ? replyZh : replyEn;
// Use the existing send pipeline so the message is added to history, persisted, etc.
send();
}
const activePlaceholder = computed(() => `${t(`ai.placeholders.${activeAction.value}`)} ${t("ai.tableMentionPlaceholderHint")}`);
const activeModeHint = computed(() => t(`ai.modeHints.${assistantMode.value}`));
const assistantModeItems = computed(() => [
@@ -1106,6 +1138,16 @@ const messageRenderer = computed(() => {
<pre class="ai-code-block whitespace-pre-wrap break-words p-3 text-xs leading-relaxed text-zinc-900 dark:text-zinc-100"><code v-html="seg.html"></code></pre>
</div>
</template>
<div v-if="msg === proposalConfirmMessage" class="mt-2 flex gap-2" :title="t('ai.proposalConfirmTitle')">
<Button size="sm" variant="default" class="h-7 gap-1 text-[11px]" @click="sendProposalReply(true)">
<Check class="h-3 w-3" />
{{ t("ai.proposalConfirmYes") }}
</Button>
<Button size="sm" variant="outline" class="h-7 gap-1 text-[11px]" @click="sendProposalReply(false)">
<X class="h-3 w-3" />
{{ t("ai.proposalConfirmNo") }}
</Button>
</div>
</div>
</div>
</template>