From 088a4a081ba82d302e6a45a30a012af772de9a2a Mon Sep 17 00:00:00 2001 From: Abeautifulsnow Date: Sat, 11 Jul 2026 08:55:51 +0800 Subject: [PATCH] fix(ai): preserve editor content for SQL actions --- apps/desktop/src/App.vue | 33 +++++++++++++---- .../src/components/editor/AiAssistant.vue | 10 +++++- 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 + apps/desktop/src/lib/ai/aiSqlAppend.ts | 19 ++++++++++ packages/app-tests/aiSqlAppend.test.ts | 35 +++++++++++++++++++ 11 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 apps/desktop/src/lib/ai/aiSqlAppend.ts create mode 100644 packages/app-tests/aiSqlAppend.test.ts diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index a4acc4e58..bd013e582 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -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(() => {
- +
diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue index 398698bfc..45c60b146 100644 --- a/apps/desktop/src/components/editor/AiAssistant.vue +++ b/apps/desktop/src/components/editor/AiAssistant.vue @@ -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) { {{ seg.lang }}
+ diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 55b3b5a70..0c7dcbe3f 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index ae3bb6b88..a672c9f53 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index e26f83b2a..9763bf219 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 1a9de2f61..0a65e1060 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1199,6 +1199,7 @@ export default withEnglishFallback({ copySql: "SQLをコピー", copyCode: "コードをコピー", executeSql: "SQLを実行", + tempRunSql: "編集せずに実行", copyAll: "すべてコピー", copied: "コピーしました", copyTestResult: "テスト結果をコピー", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 313743b0c..5b912fc1f 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index bf212257a..e9289e7a2 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1235,6 +1235,7 @@ export default withEnglishFallback({ copySql: "复制 SQL", copyCode: "复制代码", executeSql: "立即执行", + tempRunSql: "临时运行", copyAll: "复制全部", copied: "已复制", copyTestResult: "复制测试结果", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index ef084e956..b4e2b4192 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1177,6 +1177,7 @@ export default withEnglishFallback({ copySql: "複製 SQL", copyCode: "複製程式碼", executeSql: "立即執行", + tempRunSql: "臨時執行", copyAll: "複製全部", copied: "已複製", copyTestResult: "複製測試結果", diff --git a/apps/desktop/src/lib/ai/aiSqlAppend.ts b/apps/desktop/src/lib/ai/aiSqlAppend.ts new file mode 100644 index 000000000..11e01ce95 --- /dev/null +++ b/apps/desktop/src/lib/ai/aiSqlAppend.ts @@ -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}`; +} diff --git a/packages/app-tests/aiSqlAppend.test.ts b/packages/app-tests/aiSqlAppend.test.ts new file mode 100644 index 000000000..fd6130d24 --- /dev/null +++ b/packages/app-tests/aiSqlAppend.test.ts @@ -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"); +});