fix(ai): preserve editor content for SQL actions

This commit is contained in:
Abeautifulsnow 2026-07-11 08:55:51 +08:00 committed by GitHub
parent 343a5ba6c9
commit 088a4a081b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 97 additions and 7 deletions

View File

@ -72,6 +72,7 @@ import {
import { isPreviewTab } from "@/lib/tabs/tabPresentation";
import { supportsSqlFileExecution } from "@/lib/database/databaseCapabilities";
import { classifyAiSqlExecution } from "@/lib/ai/aiSqlExecutionPolicy";
import { buildAppendedEditorSql } from "@/lib/ai/aiSqlAppend";
import { buildHistoryAiAnalysisPrompt } from "@/lib/history/historyAiAnalysis";
import { countAvailableAgentDriverUpdates, type AgentDriverUpdateBadgeState } from "@/lib/connection/agentDriverUpdateBadge";
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
@ -1250,16 +1251,25 @@ function onAiReplaceSql(sql: string) {
queryStore.updateSql(tabId, sql);
}
function onAiExecuteSql(sql: string) {
const tabId = ensureQueryTab();
queryStore.updateSql(tabId, sql);
function runAiGeneratedSql(sql: string) {
selectedSql.value = "";
nextTick(() => tryExecute(sql));
}
function onAiExecuteSql(sql: string) {
const tabId = ensureQueryTab();
queryStore.updateSql(tabId, buildAppendedEditorSql(activeTab.value?.sql || "", sql));
runAiGeneratedSql(sql);
}
function onAiTempRunSql(sql: string) {
ensureQueryTab();
runAiGeneratedSql(sql);
}
function onAiRequestAutoExecuteSql(sql: string) {
const tabId = ensureQueryTab();
queryStore.updateSql(tabId, sql);
queryStore.updateSql(tabId, buildAppendedEditorSql(activeTab.value?.sql || "", sql));
selectedSql.value = "";
const decision = classifyAiSqlExecution(sql, activeConnection.value);
@ -1281,7 +1291,7 @@ function onAiRequestAutoExecuteSql(sql: string) {
function onAiOpenExplainPlan(sql: string) {
const tabId = ensureQueryTab();
queryStore.updateSql(tabId, sql);
queryStore.updateSql(tabId, buildAppendedEditorSql(activeTab.value?.sql || "", sql));
selectedSql.value = "";
nextTick(() => {
void tryExplain(sql);
@ -1952,7 +1962,18 @@ onUnmounted(() => {
<div v-if="showAiPanel" :class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'" :style="{ width: aiPanelWidth + 'px' }">
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startAiPanelResize" />
<div class="h-full min-h-0 overflow-hidden">
<AiAssistant v-if="aiPanelReady" ref="aiAssistantRef" :tab="activeTab" :connection="activeConnection" @replace-sql="onAiReplaceSql" @execute-sql="onAiExecuteSql" @request-auto-execute-sql="onAiRequestAutoExecuteSql" @open-explain-plan="onAiOpenExplainPlan" @close="toggleAiPanel" />
<AiAssistant
v-if="aiPanelReady"
ref="aiAssistantRef"
:tab="activeTab"
:connection="activeConnection"
@replace-sql="onAiReplaceSql"
@execute-sql="onAiExecuteSql"
@temp-run-sql="onAiTempRunSql"
@request-auto-execute-sql="onAiRequestAutoExecuteSql"
@open-explain-plan="onAiOpenExplainPlan"
@close="toggleAiPanel"
/>
</div>
</div>

View File

@ -15,6 +15,7 @@ import {
Copy,
Database,
FileCode,
FlaskConical,
GitBranch,
HelpCircle,
History,
@ -123,6 +124,7 @@ const props = defineProps<{
const emit = defineEmits<{
replaceSql: [sql: string];
executeSql: [sql: string];
tempRunSql: [sql: string];
requestAutoExecuteSql: [sql: string];
openExplainPlan: [sql: string];
close: [];
@ -1516,10 +1518,13 @@ function applySql(code: string) {
}
function executeSql(code: string) {
emit("replaceSql", code);
emit("executeSql", code);
}
function tempRunSql(code: string) {
emit("tempRunSql", code);
}
const copiedIndex = ref("");
async function copyCode(code: string, key: string) {
@ -1891,6 +1896,9 @@ async function openExternalUrl(url: string) {
<span>{{ seg.lang }}</span>
<span class="flex-1" />
<div class="flex items-center gap-1.5">
<button v-if="seg.isSql" class="rounded p-0.5 text-zinc-500 hover:bg-zinc-200 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:hover:text-zinc-200" :title="t('ai.tempRunSql')" @click="tempRunSql(seg.content)">
<FlaskConical class="h-3.5 w-3.5" />
</button>
<button v-if="seg.isSql" class="rounded p-0.5 text-zinc-500 hover:bg-zinc-200 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:hover:text-zinc-200" :title="t('ai.executeSql')" @click="executeSql(seg.content)">
<Play class="h-3.5 w-3.5" />
</button>

View File

@ -1234,6 +1234,7 @@ export default {
copySql: "Copy SQL",
copyCode: "Copy Code",
executeSql: "Execute SQL",
tempRunSql: "Run without modifying editor",
copyAll: "Copy All",
copied: "Copied",
copyTestResult: "Copy test result",

View File

@ -1196,6 +1196,7 @@ export default withEnglishFallback({
copySql: "Copiar SQL",
copyCode: "Copiar código",
executeSql: "Ejecutar SQL",
tempRunSql: "Ejecutar sin modificar",
copyAll: "Copiar todo",
copied: "Copiado",
copyTestResult: "Copiar resultado de prueba",

View File

@ -1177,6 +1177,7 @@ export default withEnglishFallback({
copySql: "Copia SQL",
copyCode: "Copia Codice",
executeSql: "Esegui SQL",
tempRunSql: "Esegui senza modificare",
copyAll: "Copia Tutto",
copied: "Copiato",
copyTestResult: "Copia risultato test",

View File

@ -1199,6 +1199,7 @@ export default withEnglishFallback({
copySql: "SQLをコピー",
copyCode: "コードをコピー",
executeSql: "SQLを実行",
tempRunSql: "編集せずに実行",
copyAll: "すべてコピー",
copied: "コピーしました",
copyTestResult: "テスト結果をコピー",

View File

@ -1197,6 +1197,7 @@ export default withEnglishFallback({
copySql: "Copiar SQL",
copyCode: "Copiar Código",
executeSql: "Executar SQL",
tempRunSql: "Executar sem modificar",
copyAll: "Copiar Tudo",
copied: "Copiado",
copyTestResult: "Copiar resultado do teste",

View File

@ -1235,6 +1235,7 @@ export default withEnglishFallback({
copySql: "复制 SQL",
copyCode: "复制代码",
executeSql: "立即执行",
tempRunSql: "临时运行",
copyAll: "复制全部",
copied: "已复制",
copyTestResult: "复制测试结果",

View File

@ -1177,6 +1177,7 @@ export default withEnglishFallback({
copySql: "複製 SQL",
copyCode: "複製程式碼",
executeSql: "立即執行",
tempRunSql: "臨時執行",
copyAll: "複製全部",
copied: "已複製",
copyTestResult: "複製測試結果",

View File

@ -0,0 +1,19 @@
/**
* Build the editor content after appending AI-generated SQL to existing editor SQL.
* Preserves the existing editor content exactly and adds only the newline separator
* needed to leave a blank line before the appended SQL.
*/
export function buildAppendedEditorSql(currentEditorSql: string, newSql: string): string {
if (!currentEditorSql) return newSql;
let separator = "\n\n";
if (currentEditorSql.endsWith("\r\n\r\n") || currentEditorSql.endsWith("\n\n")) {
separator = "";
} else if (currentEditorSql.endsWith("\r\n")) {
separator = "\r\n";
} else if (currentEditorSql.endsWith("\n")) {
separator = "\n";
}
return `${currentEditorSql}${separator}${newSql}`;
}

View File

@ -0,0 +1,35 @@
import { strict as assert } from "node:assert";
import { test } from "vitest";
import { buildAppendedEditorSql } from "../../apps/desktop/src/lib/ai/aiSqlAppend.ts";
test("buildAppendedEditorSql returns newSql unchanged when editor is empty", () => {
assert.equal(buildAppendedEditorSql("", "SELECT 1"), "SELECT 1");
});
test("buildAppendedEditorSql prepends blank-line separator when editor has content", () => {
assert.equal(buildAppendedEditorSql("SELECT 1", "SELECT 2"), "SELECT 1\n\nSELECT 2");
});
test("buildAppendedEditorSql preserves multiline existing content", () => {
assert.equal(buildAppendedEditorSql("SELECT *\nFROM users", "SELECT *\nFROM orders"), "SELECT *\nFROM users\n\nSELECT *\nFROM orders");
});
test("buildAppendedEditorSql preserves trailing newlines already present in the editor", () => {
assert.equal(buildAppendedEditorSql("SELECT 1\n\n\n", "SELECT 2"), "SELECT 1\n\n\nSELECT 2");
});
test("buildAppendedEditorSql preserves trailing spaces", () => {
assert.equal(buildAppendedEditorSql("SELECT 1 ", "SELECT 2"), "SELECT 1 \n\nSELECT 2");
});
test("buildAppendedEditorSql preserves trailing tabs", () => {
assert.equal(buildAppendedEditorSql("SELECT 1\t\t", "SELECT 2"), "SELECT 1\t\t\n\nSELECT 2");
});
test("buildAppendedEditorSql preserves whitespace-only editor content", () => {
assert.equal(buildAppendedEditorSql(" \t ", "SELECT 2"), " \t \n\nSELECT 2");
});
test("buildAppendedEditorSql preserves unfinished SQL", () => {
assert.equal(buildAppendedEditorSql("SELECT * FROM", "SELECT * FROM users"), "SELECT * FROM\n\nSELECT * FROM users");
});