From 5a666b1f483fb077fb5b38499badfd1d92d62280 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 10 Jul 2026 14:31:08 +0800 Subject: [PATCH] fix(ai): improve assistant execution and MCP settings --- .../src/components/editor/AiAssistant.vue | 60 ++++++++++++--- .../editor/EditorSettingsDialog.vue | 12 ++- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/ja.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + .../lib/__tests__/ai/aiMessageRender.spec.ts | 24 ++++++ apps/desktop/src/lib/ai/ai.ts | 2 + apps/desktop/src/lib/ai/aiMessageRender.ts | 18 +++-- apps/desktop/src/lib/backend/http.ts | 4 +- apps/desktop/src/lib/backend/tauri.ts | 4 +- crates/dbx-core/src/agent_loop.rs | 14 +++- crates/dbx-core/src/agent_tools.rs | 77 +++++++++++++++---- crates/dbx-core/src/ai_cli_agent.rs | 19 ++++- crates/dbx-core/src/ai_codex_cli.rs | 7 +- crates/dbx-web/src/routes/ai.rs | 6 ++ src-tauri/src/commands/ai.rs | 6 ++ 20 files changed, 213 insertions(+), 47 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/ai/aiMessageRender.spec.ts diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue index c334e5cfe4..7e36afa933 100644 --- a/apps/desktop/src/components/editor/AiAssistant.vue +++ b/apps/desktop/src/components/editor/AiAssistant.vue @@ -159,6 +159,10 @@ const MESSAGE_SCROLL_BUTTON_HIDE_THRESHOLD_PX = 48; let messageScrollViewport: HTMLElement | null = null; let messageTouchStartY: number | null = null; let lastMessageScrollTop = 0; +let assistantDeltaFrame: number | null = null; +let pendingAssistantDelta = ""; +let pendingAssistantReasoning = ""; +let pendingAssistantIndex = -1; function startEditMessage(visibleIndex: number) { if (isGenerating.value) return; @@ -488,6 +492,12 @@ const proposalConfirmMessage = computed(() => { return null; }); +let allowWriteSqlForNextRun = false; + +function proposalContainsWriteSql(content: string) { + return /\b(insert|update|delete|replace|merge|create|alter|drop|truncate|rename|grant|revoke)\b/i.test(content); +} + function sendProposalReply(positive: boolean) { // Disable while a stream is in flight or no proposal is currently active. if (isGenerating.value) return; @@ -497,6 +507,7 @@ function sendProposalReply(positive: boolean) { 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; + allowWriteSqlForNextRun = positive && assistantMode.value === "agent" && proposalContainsWriteSql(target.content); // Use the existing send pipeline so the message is added to history, persisted, etc. send(); } @@ -596,19 +607,41 @@ function changeDatabase(value: string) { queryStore.updateDatabase(tab.id, decodeSelectableDatabaseValue(connection.db_type, value)); } -function appendAssistantDelta(assistantIdx: number, delta: string) { - const msg = messages.value[assistantIdx]; - if (msg.isThinking) msg.isThinking = false; - msg.content += delta; +function flushAssistantDeltas() { + assistantDeltaFrame = null; + const msg = messages.value[pendingAssistantIndex]; + if (!msg) return; + if (pendingAssistantReasoning) { + msg.reasoning = (msg.reasoning || "") + pendingAssistantReasoning; + msg.isThinking = true; + } + if (pendingAssistantDelta) { + msg.isThinking = false; + msg.content += pendingAssistantDelta; + } + pendingAssistantDelta = ""; + pendingAssistantReasoning = ""; scrollToBottom(); } -function appendAssistantReasoning(assistantIdx: number, delta: string) { +function scheduleAssistantDeltaFlush(assistantIdx: number) { + pendingAssistantIndex = assistantIdx; + if (assistantDeltaFrame !== null) return; + // Providers can emit many tiny chunks. Render once per animation frame so + // Markdown parsing, highlighting, and layout do not run for every token. + assistantDeltaFrame = requestAnimationFrame(flushAssistantDeltas); +} + +function appendAssistantDelta(assistantIdx: number, delta: string) { const msg = messages.value[assistantIdx]; - if (!msg.reasoning) msg.reasoning = ""; - msg.reasoning += delta; - msg.isThinking = true; - scrollToBottom(); + if (msg.isThinking) msg.isThinking = false; + pendingAssistantDelta += delta; + scheduleAssistantDeltaFlush(assistantIdx); +} + +function appendAssistantReasoning(assistantIdx: number, delta: string) { + pendingAssistantReasoning += delta; + scheduleAssistantDeltaFlush(assistantIdx); } const reasoningExpanded = ref(false); @@ -1358,6 +1391,8 @@ async function send() { const requestedAction = activeAction.value; const requestedMode = assistantMode.value; + const allowWriteSql = requestedMode === "agent" && allowWriteSqlForNextRun; + allowWriteSqlForNextRun = false; isGenerating.value = true; messages.value.push({ role: "assistant", content: "" }); const assistantIdx = messages.value.length - 1; @@ -1379,6 +1414,7 @@ async function send() { mode: requestedMode, instruction: modelInstruction, context, + allowWriteSql, }, history, (event: AgentEvent) => { @@ -1420,6 +1456,8 @@ async function send() { const message = e instanceof Error ? e.message : String(e); messages.value[assistantIdx].content = `Error: ${message}`; } finally { + if (assistantDeltaFrame !== null) cancelAnimationFrame(assistantDeltaFrame); + flushAssistantDeltas(); const msg = messages.value[assistantIdx]; if (msg) msg.isThinking = false; isGenerating.value = false; @@ -1628,6 +1666,7 @@ function stopResize() { } onUnmounted(() => { + if (assistantDeltaFrame !== null) cancelAnimationFrame(assistantDeltaFrame); clearTimeout(mentionTimer); cancelStream(); detachMessageScrollListener(); @@ -1838,7 +1877,8 @@ async function openExternalUrl(url: string) { -