diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 3c37c48153..edb1b10dbd 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -173,6 +173,7 @@ import { aiSqlActionFeedback, type AiSqlActionOutcome, type AiSqlActionReport } import type { SqlExecutionOutcome } from "@/lib/sql/sqlExecutionOutcome"; import { tabDisplayTitle } from "@/lib/tabs/tabPresentation"; import { buildAppendedEditorSql, buildDeduplicatedAppendedEditorSql } from "@/lib/ai/aiSqlAppend"; +import { resolveAiSqlTargetTabId, type AiSqlApplyTargetMode } from "@/lib/ai/aiSqlTargetTab"; import { assessProductionSql } from "@/lib/database/productionSafety"; import { normalizeSqlExecutionTarget, sqlExecutionTargetCapabilities } from "@/lib/database/sqlExecutionTargetCapabilities"; import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard"; @@ -3653,7 +3654,7 @@ async function locateTabInSidebar(tab: QueryTab) { /** * Query tab an AI-initiated action must run in: the one belonging to the - * *requested* connection, created on demand. + * *requested* connection, resolved according to `aiSqlApplyTargetMode` (#12438). * * Deliberately not `ensureQueryTab()` — that returns whichever query tab happens * to be active, which is how a conversation bound to one database appended its @@ -3661,15 +3662,13 @@ async function locateTabInSidebar(tab: QueryTab) { * alone (`activate: false`), so running a conversation never yanks the user's * editor away. */ -function ensureQueryTabForConnection(target: AiConversationBinding): string { +function ensureQueryTabForConnection(target: AiConversationBinding, options: { mode?: AiSqlApplyTargetMode } = {}): string { if (!target.connectionId) return ""; const database = target.database || ""; const schema = target.schema || undefined; - // Schema is matched exactly, empty included. Skipping the comparison when the - // target has no schema would reuse a tab on any schema, and execution inherits - // that tab's schema — silently running in the wrong namespace. - const existing = queryStore.tabs.find((tab) => tab.mode === "query" && tab.connectionId === target.connectionId && (tab.database || "") === database && (tab.schema || undefined) === schema); - if (existing) return existing.id; + const mode = options.mode ?? settingsStore.editorSettings.aiSqlApplyTargetMode; + const resolvedId = resolveAiSqlTargetTabId(queryStore.tabs, queryStore.activeTabId, target, mode); + if (resolvedId !== null) return resolvedId; return queryStore.createTab(target.connectionId, database, undefined, "query", schema, undefined, undefined, { activate: false }); } diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 6688452876..e8efa86437 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -102,6 +102,7 @@ import { type SidebarObjectInfoMode, type SqlSemanticDiagnosticsMode, type SavedSqlOpenTargetMode, + type AiSqlApplyTargetMode, type TabGroupMode, type TabPlacement, type TabSortMode, @@ -788,6 +789,7 @@ const editContinueOnErrorOnBatch = ref(settingsStore.editorSettings.continueOnEr const editConfirmUnsavedSqlClose = ref(settingsStore.editorSettings.confirmUnsavedSqlClose); const editAppCloseUnsavedTabsMode = ref(settingsStore.editorSettings.appCloseUnsavedTabsMode); const editSavedSqlOpenTargetMode = ref(settingsStore.editorSettings.savedSqlOpenTargetMode); +const editAiSqlApplyTargetMode = ref(settingsStore.editorSettings.aiSqlApplyTargetMode); const editWelcomePageMode = ref(settingsStore.editorSettings.welcomePageMode); const editAppLayout = ref(settingsStore.editorSettings.appLayout); const editWebLogoPosition = ref(settingsStore.editorSettings.webLogoPosition); @@ -1183,6 +1185,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { confirmUnsavedSqlClose: editConfirmUnsavedSqlClose.value, appCloseUnsavedTabsMode: editAppCloseUnsavedTabsMode.value, savedSqlOpenTargetMode: editSavedSqlOpenTargetMode.value, + aiSqlApplyTargetMode: editAiSqlApplyTargetMode.value, welcomePageMode: editWelcomePageMode.value, appLayout: editAppLayout.value, webLogoPosition: editWebLogoPosition.value, @@ -1877,6 +1880,7 @@ function syncEditorSettingsDraftFromStore() { editConfirmUnsavedSqlClose.value = settingsStore.editorSettings.confirmUnsavedSqlClose; editAppCloseUnsavedTabsMode.value = settingsStore.editorSettings.appCloseUnsavedTabsMode; editSavedSqlOpenTargetMode.value = settingsStore.editorSettings.savedSqlOpenTargetMode; + editAiSqlApplyTargetMode.value = settingsStore.editorSettings.aiSqlApplyTargetMode; editWelcomePageMode.value = settingsStore.editorSettings.welcomePageMode; editAppLayout.value = settingsStore.editorSettings.appLayout; editWebLogoPosition.value = settingsStore.editorSettings.webLogoPosition; @@ -2045,6 +2049,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = { confirmUnsavedSqlClose: editConfirmUnsavedSqlClose, appCloseUnsavedTabsMode: editAppCloseUnsavedTabsMode, savedSqlOpenTargetMode: editSavedSqlOpenTargetMode, + aiSqlApplyTargetMode: editAiSqlApplyTargetMode, welcomePageMode: editWelcomePageMode, appLayout: editAppLayout, webLogoPosition: editWebLogoPosition, @@ -2630,6 +2635,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { editConfirmUnsavedSqlClose.value = DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose; editAppCloseUnsavedTabsMode.value = DEFAULT_EDITOR_SETTINGS.appCloseUnsavedTabsMode; editSavedSqlOpenTargetMode.value = DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode; + editAiSqlApplyTargetMode.value = DEFAULT_EDITOR_SETTINGS.aiSqlApplyTargetMode; editShowTableDdlHoverPreview.value = DEFAULT_EDITOR_SETTINGS.showTableDdlHoverPreview; editTableHoverLookupMode.value = DEFAULT_EDITOR_SETTINGS.tableHoverLookupMode; editExternalSqlEditorMaxMb.value = DEFAULT_EDITOR_SETTINGS.externalSqlEditorMaxMb; @@ -2819,6 +2825,7 @@ function resetAllDefaults() { editConfirmUnsavedSqlClose.value = DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose; editAppCloseUnsavedTabsMode.value = DEFAULT_EDITOR_SETTINGS.appCloseUnsavedTabsMode; editSavedSqlOpenTargetMode.value = DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode; + editAiSqlApplyTargetMode.value = DEFAULT_EDITOR_SETTINGS.aiSqlApplyTargetMode; editSqlVariableSubstitutionEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlVariableSubstitutionEnabled; editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(DEFAULT_EDITOR_SETTINGS.sqlVariableSyntaxOverrides); editWelcomePageMode.value = DEFAULT_EDITOR_SETTINGS.welcomePageMode; @@ -8017,6 +8024,25 @@ onUnmounted(() => { +
+
+ +

+ {{ editAiSqlApplyTargetMode === "current" ? t("settings.aiSqlApplyTargetCurrentDescription") : editAiSqlApplyTargetMode === "first" ? t("settings.aiSqlApplyTargetFirstDescription") : t("settings.aiSqlApplyTargetNewDescription") }} +

+
+ +
+
@@ -11366,6 +11392,40 @@ LIMIT 100;
+ +
+ +
+
+ +

+ {{ editAiSqlApplyTargetMode === "current" ? t("settings.aiSqlApplyTargetCurrentDescription") : editAiSqlApplyTargetMode === "first" ? t("settings.aiSqlApplyTargetFirstDescription") : t("settings.aiSqlApplyTargetNewDescription") }} +

+
+ +
+
+
diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index eb25fe9ef1..e8afdb9b4e 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -8047,6 +8047,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "SQL faylı ilə saxlanılmış əlaqədən, verilənlər bazasından və sxemdən istifadə et.", savedSqlOpenTargetCurrent: "Cari vərəq", savedSqlOpenTargetCurrentDescription: "Mövcud olduqda aktiv vərəqin əlaqəsindən, verilənlər bazasından, sxemindən və kataloqundan istifadə et; əks halda yadda saxlanılmış hədəfdən istifadə et.", + aiSqlApplyTarget: "Sİ tərəfindən yaradılan SQL üçün hədəf redaktor", + aiSqlApplyTargetCurrent: "Cari redaktor (Tövsiyə olunur)", + aiSqlApplyTargetCurrentDescription: "Bağlantı, verilənlər bazası və sxem uyğun gəldikdə aktiv redaktora üstünlük verin; əks halda uyğun gələn ilk redaktordan istifadə edin və ya yeni tab yaradın.", + aiSqlApplyTargetFirst: "Uyğun gələn ilk redaktor", + aiSqlApplyTargetFirstDescription: "Həmişə bağlantı və verilənlər bazasına uyğun gələn ilk sorğu redaktoruna tətbiq edin; heç biri yoxdursa yeni tab yaradın.", + aiSqlApplyTargetNew: "Həmişə yeni tab", + aiSqlApplyTargetNewDescription: "Sİ tərəfindən yaradılan SQL-i həmişə yeni sorğu tabında açıb tətbiq edin.", prefillNewQueryWithSelect: "Yeni sorğunu əvvəlcədən SELECT * ilə doldur", prefillNewQueryWithSelectDescription: "Yeni sorğu yaradarkən aktiv cədvəl vərəqinə və ya yan paneldə seçilmiş cədvələ əsasən redaktoru əvvəlcədən SELECT * FROM ilə doldur.", generateSqlIncludeDatabaseName: "Yaradılan SQL-ə verilənlər bazasının adını daxil et", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 63efca0872..8148cd4ff8 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -9555,6 +9555,13 @@ export default { savedSqlOpenTargetSavedDescription: "Use the connection, database, and schema stored with the SQL file.", savedSqlOpenTargetCurrent: "Current tab", savedSqlOpenTargetCurrentDescription: "Use the active tab’s connection, database, schema, and catalog when available; otherwise use the saved target.", + aiSqlApplyTarget: "Target editor for AI-generated SQL", + aiSqlApplyTargetCurrent: "Current editor (Recommended)", + aiSqlApplyTargetCurrentDescription: "Prioritize the currently active editor when its connection, database, and schema match; otherwise reuse the first matching editor or create a new tab.", + aiSqlApplyTargetFirst: "First matching editor", + aiSqlApplyTargetFirstDescription: "Always apply to the first query editor matching the connection and database; create a new tab if none exists.", + aiSqlApplyTargetNew: "Always new tab", + aiSqlApplyTargetNewDescription: "Always open and apply AI-generated SQL in a newly created query tab.", prefillNewQueryWithSelect: "Prefill new query with SELECT *", openQueryOnConnectionOpen: "Open query page when opening a connection", openQueryOnConnectionOpenDescription: "When enabled, opening a connection or database opens its default.sql query. Changes are saved automatically.", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index db79fecf9d..b8933074c3 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -8615,6 +8615,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "Usar la conexión, la base de datos y el esquema guardados con el archivo SQL.", savedSqlOpenTargetCurrent: "Pestaña actual", savedSqlOpenTargetCurrentDescription: "Usar la conexión, la base de datos, el esquema y el catálogo de la pestaña activa cuando estén disponibles; de lo contrario, usar el destino guardado.", + aiSqlApplyTarget: "Editor de destino para SQL generado por IA", + aiSqlApplyTargetCurrent: "Editor actual (recomendado)", + aiSqlApplyTargetCurrentDescription: "Priorizar el editor activo cuando coincidan la conexión, la base de datos y el esquema; de lo contrario, reutilizar el primer editor que coincida o crear una nueva pestaña.", + aiSqlApplyTargetFirst: "Primer editor coincidente", + aiSqlApplyTargetFirstDescription: "Aplicar siempre al primer editor de consultas que coincida con la conexión y la base de datos; crear una nueva pestaña si no existe ninguno.", + aiSqlApplyTargetNew: "Siempre nueva pestaña", + aiSqlApplyTargetNewDescription: "Abrir y aplicar siempre el SQL generado por IA en una pestaña de consulta nueva.", prefillNewQueryWithSelect: "Rellenar nueva consulta con SELECT *", prefillNewQueryWithSelectDescription: "Al crear una nueva consulta, rellena el editor con SELECT * FROM según la pestaña de tabla activa o la tabla seleccionada en la barra lateral.", generateSqlIncludeDatabaseName: "Incluir el nombre de la base de datos en el SQL generado", diff --git a/apps/desktop/src/i18n/locales/id.ts b/apps/desktop/src/i18n/locales/id.ts index 57c81e251b..973aaa331e 100644 --- a/apps/desktop/src/i18n/locales/id.ts +++ b/apps/desktop/src/i18n/locales/id.ts @@ -8853,6 +8853,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "Gunakan koneksi, database, dan skema yang tersimpan bersama file SQL.", savedSqlOpenTargetCurrent: "Tab saat ini", savedSqlOpenTargetCurrentDescription: "Gunakan koneksi, database, skema, dan katalog tab aktif jika tersedia; jika tidak, gunakan target tersimpan.", + aiSqlApplyTarget: "Editor target untuk SQL buatan AI", + aiSqlApplyTargetCurrent: "Editor saat ini (Disarankan)", + aiSqlApplyTargetCurrentDescription: "Prioritaskan editor aktif jika koneksi, database, dan skema cocok; jika tidak, gunakan kembali editor pertama yang cocok atau buat tab baru.", + aiSqlApplyTargetFirst: "Editor pertama yang cocok", + aiSqlApplyTargetFirstDescription: "Selalu terapkan ke editor kueri pertama yang cocok dengan koneksi dan database; buat tab baru jika tidak ada.", + aiSqlApplyTargetNew: "Selalu tab baru", + aiSqlApplyTargetNewDescription: "Selalu buka dan terapkan SQL buatan AI di tab kueri baru.", prefillNewQueryWithSelect: "Isi awal kueri baru dengan SELECT *", prefillNewQueryWithSelectDescription: "Saat membuat kueri baru, isi awal editor dengan SELECT * FROM
berdasarkan tab tabel aktif atau tabel yang dipilih di bilah sisi.", generateSqlIncludeDatabaseName: "Sertakan nama database dalam SQL yang dihasilkan", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 340e1f0ed4..fbeab83e27 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -8378,6 +8378,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "Usa la connessione, il database e lo schema salvati con il file SQL.", savedSqlOpenTargetCurrent: "Scheda corrente", savedSqlOpenTargetCurrentDescription: "Usa la connessione, il database, lo schema e il catalogo della scheda attiva quando disponibili; altrimenti usa la destinazione salvata.", + aiSqlApplyTarget: "Editor di destinazione per SQL generato dall'IA", + aiSqlApplyTargetCurrent: "Editor corrente (consigliato)", + aiSqlApplyTargetCurrentDescription: "Dai priorità all'editor attivo quando connessione, database e schema corrispondono; altrimenti riutilizza il primo editor corrispondente o crea una nuova scheda.", + aiSqlApplyTargetFirst: "Primo editor corrispondente", + aiSqlApplyTargetFirstDescription: "Applica sempre al primo editor di query corrispondente a connessione e database; crea una nuova scheda se non ne esiste alcuno.", + aiSqlApplyTargetNew: "Sempre nuova scheda", + aiSqlApplyTargetNewDescription: "Apri e applica sempre il SQL generato dall'IA in una nuova scheda di query.", prefillNewQueryWithSelect: "Precompila nuova query con SELECT *", prefillNewQueryWithSelectDescription: "Alla creazione di una nuova query, precompila l'editor con SELECT * FROM in base alla scheda tabella attiva o alla tabella selezionata nella barra laterale.", generateSqlIncludeDatabaseName: "Includi il nome del database nell'SQL generato", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index bc1ecc33a5..96967660ed 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -8375,6 +8375,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "SQLファイルに保存された接続、データベース、スキーマを使用します。", savedSqlOpenTargetCurrent: "現在のタブ", savedSqlOpenTargetCurrentDescription: "利用可能な場合は現在のタブの接続、データベース、スキーマ、カタログを使用し、それ以外は保存時の実行先を使用します。", + aiSqlApplyTarget: "AI生成SQLの適用先エディタ", + aiSqlApplyTargetCurrent: "現在のアクティブエディタ(推奨)", + aiSqlApplyTargetCurrentDescription: "接続、データベース、スキーマが一致する場合、現在アクティブなエディタを優先して使用します。一致しない場合は最初に一致するエディタを使用するか新規タブを作成します。", + aiSqlApplyTargetFirst: "最初に一致するエディタ", + aiSqlApplyTargetFirstDescription: "常に接続とデータベースに一致する最初のクエリエディタに適用します。存在しない場合は新規タブを作成します。", + aiSqlApplyTargetNew: "常に新規タブ", + aiSqlApplyTargetNewDescription: "AIが生成したSQLを適用する際、常に新しいクエリタブを作成して開きます。", prefillNewQueryWithSelect: "新規クエリに SELECT を自動入力", prefillNewQueryWithSelectDescription: "新規クエリ作成時、アクティブなテーブルタブまたはサイドバーで選択したテーブルに基づき、エディターに SELECT * FROM <テーブル名> を自動入力します。", generateSqlIncludeDatabaseName: "生成 SQL にデータベース名を含める", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index c584edcc94..0c99c7cd32 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -8285,6 +8285,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "SQL 파일에 저장된 연결, 데이터베이스 및 스키마를 사용합니다.", savedSqlOpenTargetCurrent: "현재 탭", savedSqlOpenTargetCurrentDescription: "가능한 경우 현재 탭의 연결, 데이터베이스, 스키마 및 카탈로그를 사용하고, 그렇지 않으면 저장된 대상을 사용합니다.", + aiSqlApplyTarget: "AI 생성 SQL 적용 대상 에디터", + aiSqlApplyTargetCurrent: "현재 활성 에디터(권장)", + aiSqlApplyTargetCurrentDescription: "연결, 데이터베이스, 스키마가 일치하는 경우 현재 활성화된 에디터에 우선 적용합니다. 일치하지 않으면 첫 번째 일치하는 에디터를 사용하거나 새 탭을 생성합니다.", + aiSqlApplyTargetFirst: "첫 번째 일치하는 에디터", + aiSqlApplyTargetFirstDescription: "항상 연결 및 데이터베이스와 일치하는 첫 번째 쿼리 에디터에 적용합니다. 없으면 새 탭을 생성합니다.", + aiSqlApplyTargetNew: "항상 새 탭", + aiSqlApplyTargetNewDescription: "AI 생성 SQL을 적용할 때 항상 새 쿼리 탭을 열어 적용합니다.", prefillNewQueryWithSelect: "새 쿼리에 SELECT * 미리 채우기", prefillNewQueryWithSelectDescription: "새 쿼리를 만들 때 활성 테이블 탭이나 사이드바에서 선택한 테이블을 기반으로 편집기에 SELECT * FROM
을 미리 채웁니다.", clickTableNavigationTarget: "Ctrl+클릭 시 테이블 DDL 열기", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 150f75d677..2613a94dc7 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -8376,6 +8376,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "Usar a conexão, o banco de dados e o esquema salvos com o arquivo SQL.", savedSqlOpenTargetCurrent: "Aba atual", savedSqlOpenTargetCurrentDescription: "Usar a conexão, o banco de dados, o esquema e o catálogo da aba ativa quando disponíveis; caso contrário, usar o destino salvo.", + aiSqlApplyTarget: "Editor de destino para SQL gerado por IA", + aiSqlApplyTargetCurrent: "Editor atual (recomendado)", + aiSqlApplyTargetCurrentDescription: "Priorizar o editor ativo quando a conexão, banco de dados e esquema coincidirem; caso contrário, reutilizar o primeiro editor correspondente ou criar uma nova aba.", + aiSqlApplyTargetFirst: "Primeiro editor correspondente", + aiSqlApplyTargetFirstDescription: "Sempre aplicar ao primeiro editor de consulta correspondente à conexão e ao banco de dados; criar uma nova aba se nenhum existir.", + aiSqlApplyTargetNew: "Sempre nova aba", + aiSqlApplyTargetNewDescription: "Sempre abrir e aplicar o SQL gerado por IA em uma nova aba de consulta.", prefillNewQueryWithSelect: "Preencher nova consulta com SELECT *", prefillNewQueryWithSelectDescription: "Ao criar uma nova consulta, preenche o editor com SELECT * FROM com base na aba de tabela ativa ou na tabela selecionada na barra lateral.", generateSqlIncludeDatabaseName: "Incluir nome do banco de dados no SQL gerado", diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts index 5c8c0bc4dd..ffd8957c65 100644 --- a/apps/desktop/src/i18n/locales/ru.ts +++ b/apps/desktop/src/i18n/locales/ru.ts @@ -9749,6 +9749,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "Использовать подключение, базу данных и схему, сохранённые вместе с SQL-файлом.", savedSqlOpenTargetCurrent: "Текущая вкладка", savedSqlOpenTargetCurrentDescription: "Использовать подключение, базу данных, схему и каталог активной вкладки, когда они доступны; иначе использовать сохранённую цель.", + aiSqlApplyTarget: "Целевой редактор для SQL от ИИ", + aiSqlApplyTargetCurrent: "Текущий редактор (рекомендуется)", + aiSqlApplyTargetCurrentDescription: "Приоритетно применять к активному редактору при совпадении подключения, базы данных и схемы; иначе повторно использовать первый подходящий редактор или создать новую вкладку.", + aiSqlApplyTargetFirst: "Первый подходящий редактор", + aiSqlApplyTargetFirstDescription: "Всегда применять к первому редактору запросов, соответствующему подключению и базе данных; создать новую вкладку, если подходящего нет.", + aiSqlApplyTargetNew: "Всегда новая вкладка", + aiSqlApplyTargetNewDescription: "Всегда открывать и применять созданный ИИ SQL в новой вкладке запроса.", prefillNewQueryWithSelect: "Заполнять новый запрос через SELECT *", prefillNewQueryWithSelectDescription: "При создании нового запроса заполнять редактор конструкцией SELECT * FROM
на основе активной вкладки таблицы или таблицы, выбранной в боковой панели.", generateSqlIncludeDatabaseName: "Включать имя базы данных в генерируемый SQL", diff --git a/apps/desktop/src/i18n/locales/th.ts b/apps/desktop/src/i18n/locales/th.ts index 523122b093..00cb35880e 100644 --- a/apps/desktop/src/i18n/locales/th.ts +++ b/apps/desktop/src/i18n/locales/th.ts @@ -10188,6 +10188,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "ใช้การเชื่อมต่อ ฐานข้อมูล และ Schema ที่เก็บไว้กับไฟล์ SQL", savedSqlOpenTargetCurrent: "แท็บปัจจุบัน", savedSqlOpenTargetCurrentDescription: "ใช้การเชื่อมต่อ ฐานข้อมูล Schema และ Catalog ของแท็บที่ใช้งานเมื่อมี มิฉะนั้นใช้เป้าหมายที่บันทึก", + aiSqlApplyTarget: "ตัวแก้ไขเป้าหมายสำหรับ SQL ที่ AI สร้าง", + aiSqlApplyTargetCurrent: "ตัวแก้ไขปัจจุบัน (แนะนำ)", + aiSqlApplyTargetCurrentDescription: "ให้ความสำคัญกับตัวแก้ไขที่ใช้งานอยู่เมื่อการเชื่อมต่อ ฐานข้อมูล และสคีมาตรงกัน มิฉะนั้นจะใช้ตัวแก้ไขแรกที่ตรงกันหรือสร้างแท็บใหม่", + aiSqlApplyTargetFirst: "ตัวแก้ไขแรกที่ตรงกัน", + aiSqlApplyTargetFirstDescription: "นำไปใช้กับตัวแก้ไขคำสั่งแรกที่ตรงกับการเชื่อมต่อและฐานข้อมูลเสมอ หากไม่มีจะสร้างแท็บใหม่", + aiSqlApplyTargetNew: "แท็บใหม่เสมอ", + aiSqlApplyTargetNewDescription: "เปิดและนำ SQL ที่ AI สร้างไปใช้ในแท็บคำสั่งใหม่เสมอ", prefillNewQueryWithSelect: "เติม Query ใหม่ด้วย SELECT *", openQueryOnConnectionOpen: "เปิดหน้า Query เมื่อเปิดการเชื่อมต่อ", openQueryOnConnectionOpenDescription: "เมื่อเปิดการเชื่อมต่อหรือฐานข้อมูล จะเปิด default.sql และบันทึกการเปลี่ยนแปลงโดยอัตโนมัติ", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 0e99f4c557..06ccb2503d 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -7946,6 +7946,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "SQL dosyasıyla birlikte saklanan bağlantıyı, veritabanını ve şemayı kullan.", savedSqlOpenTargetCurrent: "Geçerli sekme", savedSqlOpenTargetCurrentDescription: "Varsa etkin sekmenin bağlantısını, veritabanını, şemasını ve kataloğunu kullan; yoksa kayıtlı hedefi kullan.", + aiSqlApplyTarget: "Yapay zeka SQL hedef editörü", + aiSqlApplyTargetCurrent: "Mevcut editör (Önerilen)", + aiSqlApplyTargetCurrentDescription: "Bağlantı, veritabanı ve şema eşleştiğinde etkin editöre öncelik verin; aksi takdirde eşleşen ilk editörü kullanın veya yeni bir sekme oluşturun.", + aiSqlApplyTargetFirst: "Eşleşen ilk editör", + aiSqlApplyTargetFirstDescription: "Her zaman bağlantı ve veritabanıyla eşleşen ilk sorgu editörüne uygulayın; yoksa yeni bir sekme oluşturun.", + aiSqlApplyTargetNew: "Her zaman yeni sekme", + aiSqlApplyTargetNewDescription: "Yapay zeka tarafından oluşturulan SQL'i her zaman yeni bir sorgu sekmesinde açın ve uygulayın.", prefillNewQueryWithSelect: "Yeni sorguyu SELECT * ile doldur", prefillNewQueryWithSelectDescription: "Yeni bir sorgu oluşturulurken düzenleyiciyi, etkin tablo sekmesine ya da kenar çubuğunda seçili tabloya göre SELECT * FROM ile doldur.", generateSqlIncludeDatabaseName: "Üretilen SQL'e veritabanı adını dâhil et", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 609141804f..eeb3f64e4f 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -9533,6 +9533,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "使用 SQL 文件保存时记录的连接、数据库和 Schema。", savedSqlOpenTargetCurrent: "当前标签页", savedSqlOpenTargetCurrentDescription: "优先使用当前标签页的连接、数据库、Schema 和 Catalog;不可用时回退到保存时的目标。", + aiSqlApplyTarget: "AI 生成 SQL 的目标编辑器", + aiSqlApplyTargetCurrent: "当前编辑器(推荐)", + aiSqlApplyTargetCurrentDescription: "优先应用到当前处于活动状态且连接、数据库和模式匹配的编辑器;若不匹配则使用首个匹配的编辑器或新建标签页。", + aiSqlApplyTargetFirst: "首个匹配编辑器", + aiSqlApplyTargetFirstDescription: "始终应用到当前连接与数据库的第一个查询编辑器;不存在时新建标签页。", + aiSqlApplyTargetNew: "总是新建标签页", + aiSqlApplyTargetNewDescription: "每次应用 AI 生成的 SQL 时始终在新的查询标签页中打开。", prefillNewQueryWithSelect: "新建查询时预填充 SELECT 语句", openQueryOnConnectionOpen: "打开连接时进入查询页", openQueryOnConnectionOpenDescription: "启用后,打开连接或数据库时进入对应的 default.sql 默认查询,修改自动保存。", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index a174cc1174..e1507dd3da 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -7717,6 +7717,13 @@ export default withEnglishFallback({ savedSqlOpenTargetSavedDescription: "使用 SQL 檔案儲存時記錄的連線、資料庫和 Schema。", savedSqlOpenTargetCurrent: "目前分頁", savedSqlOpenTargetCurrentDescription: "優先使用目前分頁的連線、資料庫、Schema 和 Catalog;無法使用時回復至儲存時的目標。", + aiSqlApplyTarget: "AI 生成 SQL 的目標編輯器", + aiSqlApplyTargetCurrent: "目前編輯器(推薦)", + aiSqlApplyTargetCurrentDescription: "優先套用到目前處於使用中且連線、資料庫和結構描述相符的編輯器;若不相符則使用第一個相符的編輯器或建立新分頁。", + aiSqlApplyTargetFirst: "第一個相符編輯器", + aiSqlApplyTargetFirstDescription: "始終套用到目前連線與資料庫的第一個查詢編輯器;不存在時建立新分頁。", + aiSqlApplyTargetNew: "一律建立新分頁", + aiSqlApplyTargetNewDescription: "每次套用 AI 生成的 SQL 時一律在新的查詢分頁中開啟。", prefillNewQueryWithSelect: "新建查詢時預填 SELECT 語句", prefillNewQueryWithSelectDescription: "新建查詢時,根據目前啟用的資料表分頁或側邊欄選取的資料表,自動在編輯器中填入 SELECT * FROM <資料表名稱>。", generateSqlIncludeDatabaseName: "產生 SQL 時包含資料庫名稱", diff --git a/apps/desktop/src/lib/ai/__tests__/aiSqlTargetTab.spec.ts b/apps/desktop/src/lib/ai/__tests__/aiSqlTargetTab.spec.ts new file mode 100644 index 0000000000..20d2811f28 --- /dev/null +++ b/apps/desktop/src/lib/ai/__tests__/aiSqlTargetTab.spec.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "vitest"; +import { normalizeAiSqlApplyTargetMode, resolveAiSqlTargetTabId, type AiSqlTargetTabCandidate } from "../aiSqlTargetTab"; + +describe("aiSqlTargetTab (#12438)", () => { + describe("normalizeAiSqlApplyTargetMode", () => { + it("accepts valid modes", () => { + expect(normalizeAiSqlApplyTargetMode("current")).toBe("current"); + expect(normalizeAiSqlApplyTargetMode("first")).toBe("first"); + expect(normalizeAiSqlApplyTargetMode("new")).toBe("new"); + }); + + it("falls back to current on invalid or missing inputs", () => { + expect(normalizeAiSqlApplyTargetMode(undefined)).toBe("current"); + expect(normalizeAiSqlApplyTargetMode(null)).toBe("current"); + expect(normalizeAiSqlApplyTargetMode("invalid")).toBe("current"); + expect(normalizeAiSqlApplyTargetMode("invalid", "first")).toBe("first"); + }); + }); + + describe("resolveAiSqlTargetTabId", () => { + const tabs: AiSqlTargetTabCandidate[] = [ + { id: "tab-1", mode: "query", connectionId: "conn-1", database: "app", schema: "public" }, + { id: "tab-2", mode: "query", connectionId: "conn-1", database: "app", schema: "public" }, + { id: "tab-other-db", mode: "query", connectionId: "conn-1", database: "analytics", schema: "public" }, + { id: "tab-other-conn", mode: "query", connectionId: "conn-2", database: "app", schema: "public" }, + { id: "tab-data", mode: "data", connectionId: "conn-1", database: "app", schema: "public" }, + { id: "tab-readonly", mode: "query", connectionId: "conn-1", database: "app", schema: "public", readOnly: true }, + { id: "tab-object-source", mode: "query", connectionId: "conn-1", database: "app", schema: "public", objectSource: {} }, + ]; + + it("returns empty string when target has no connectionId", () => { + expect(resolveAiSqlTargetTabId(tabs, "tab-2", { connectionId: "" })).toBe(""); + expect(resolveAiSqlTargetTabId(tabs, "tab-2", {})).toBe(""); + }); + + it("returns null in 'new' mode even if matching tabs exist", () => { + expect(resolveAiSqlTargetTabId(tabs, "tab-2", { connectionId: "conn-1", database: "app", schema: "public" }, "new")).toBeNull(); + }); + + it("in 'current' mode, prefers active tab if it matches target connection and database", () => { + const result = resolveAiSqlTargetTabId(tabs, "tab-2", { connectionId: "conn-1", database: "app", schema: "public" }, "current"); + expect(result).toBe("tab-2"); + }); + + it("in 'current' mode, falls back to first matching tab if active tab does not match", () => { + // active tab is on different db + expect(resolveAiSqlTargetTabId(tabs, "tab-other-db", { connectionId: "conn-1", database: "app", schema: "public" }, "current")).toBe("tab-1"); + + // active tab is data mode + expect(resolveAiSqlTargetTabId(tabs, "tab-data", { connectionId: "conn-1", database: "app", schema: "public" }, "current")).toBe("tab-1"); + + // active tab is readOnly + expect(resolveAiSqlTargetTabId(tabs, "tab-readonly", { connectionId: "conn-1", database: "app", schema: "public" }, "current")).toBe("tab-1"); + + // active tab is object source + expect(resolveAiSqlTargetTabId(tabs, "tab-object-source", { connectionId: "conn-1", database: "app", schema: "public" }, "current")).toBe("tab-1"); + + // activeTabId is null/undefined + expect(resolveAiSqlTargetTabId(tabs, null, { connectionId: "conn-1", database: "app", schema: "public" }, "current")).toBe("tab-1"); + }); + + it("in 'first' mode, returns first matching tab even if active tab also matches", () => { + const result = resolveAiSqlTargetTabId(tabs, "tab-2", { connectionId: "conn-1", database: "app", schema: "public" }, "first"); + expect(result).toBe("tab-1"); + }); + + it("returns null when no matching tab exists", () => { + expect(resolveAiSqlTargetTabId(tabs, "tab-1", { connectionId: "conn-1", database: "nonexistent" }, "current")).toBeNull(); + expect(resolveAiSqlTargetTabId(tabs, "tab-1", { connectionId: "conn-nonexistent", database: "app" }, "current")).toBeNull(); + }); + + it("treats undefined and empty string databases as matching", () => { + const emptyDbTabs: AiSqlTargetTabCandidate[] = [ + { id: "tab-empty-1", mode: "query", connectionId: "sqlite-1", database: "" }, + { id: "tab-empty-2", mode: "query", connectionId: "sqlite-1", database: "" }, + ]; + expect(resolveAiSqlTargetTabId(emptyDbTabs, "tab-empty-2", { connectionId: "sqlite-1" }, "current")).toBe("tab-empty-2"); + expect(resolveAiSqlTargetTabId(emptyDbTabs, "tab-empty-2", { connectionId: "sqlite-1" }, "first")).toBe("tab-empty-1"); + }); + }); +}); diff --git a/apps/desktop/src/lib/ai/aiSqlTargetTab.ts b/apps/desktop/src/lib/ai/aiSqlTargetTab.ts new file mode 100644 index 0000000000..e5ff982fb5 --- /dev/null +++ b/apps/desktop/src/lib/ai/aiSqlTargetTab.ts @@ -0,0 +1,51 @@ +export type AiSqlApplyTargetMode = "current" | "first" | "new"; + +export interface AiSqlTargetTabCandidate { + id: string; + mode?: string; + connectionId?: string; + database?: string; + schema?: string; + objectSource?: unknown; + readOnly?: boolean; +} + +export function normalizeAiSqlApplyTargetMode(value: unknown, fallback: AiSqlApplyTargetMode = "current"): AiSqlApplyTargetMode { + return value === "current" || value === "first" || value === "new" ? value : fallback; +} + +/** + * Resolves the query tab ID an AI-initiated SQL action should target based on + * the user's `aiSqlApplyTargetMode` preference (#12438). + * + * - "current": If the active tab is an editable query tab matching the connection, + * database, and schema, use it. Otherwise fall back to the first matching tab, + * or return null to create a new tab. + * - "first": Always use the first matching query tab, or return null to create one. + * - "new": Always return null to force creating a new query tab. + * + * Returns "" if target has no connectionId (no-connection error condition). + * Returns null if a new tab should be created on demand. + * Returns the matching tab's ID if found. + */ +export function resolveAiSqlTargetTabId(tabs: readonly T[], activeTabId: string | null | undefined, target: { connectionId?: string; database?: string; schema?: string }, mode: AiSqlApplyTargetMode = "current"): string | null { + if (!target.connectionId) return ""; + const database = target.database || ""; + const schema = target.schema || undefined; + + const isMatch = (tab: T) => tab.mode === "query" && !tab.objectSource && !tab.readOnly && tab.connectionId === target.connectionId && (tab.database || "") === database && (tab.schema || undefined) === schema; + + if (mode !== "new") { + if (mode === "current" && activeTabId) { + const activeTab = tabs.find((tab) => tab.id === activeTabId); + if (activeTab && isMatch(activeTab)) { + return activeTab.id; + } + } + + const existing = tabs.find(isMatch); + if (existing) return existing.id; + } + + return null; +} diff --git a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts index 8d29441e32..ba625b730d 100644 --- a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts @@ -83,6 +83,12 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("savedSqlOpenTargetMode"); }); + it("includes aiSqlApplyTargetMode (#12438)", () => { + expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("aiSqlApplyTargetMode"); + const draft = editorSettingsDraftFromSettings(makeSettings({ aiSqlApplyTargetMode: "new" } as Partial)); + expect(draft.aiSqlApplyTargetMode).toBe("new"); + }); + it("includes the privacy-safe welcome page mode", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("welcomePageMode"); expect(editorSettingsDraftFromSettings(makeSettings({ welcomePageMode: "intro" })).welcomePageMode).toBe("intro"); diff --git a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts index 8dc10a1560..d917cc498f 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts @@ -399,6 +399,8 @@ describe("settings search", () => { { titleKey: "ai.maxAgentTurns", category: "ai", targetId: "ai" }, { titleKey: "ai.maxRetriesGlobal", category: "ai", targetId: "ai" }, { titleKey: "ai.globalInstructions", category: "ai", targetId: "ai" }, + { titleKey: "settings.aiSqlApplyTarget", category: "editor", targetId: "editor" }, + { titleKey: "settings.aiSqlApplyTarget", category: "ai", targetId: "ai" }, ]; for (const expectedControl of expectedControls) { diff --git a/apps/desktop/src/lib/settings/__tests__/settingsTransfer.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsTransfer.spec.ts index 6a9fecdd11..32d1314a82 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsTransfer.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsTransfer.spec.ts @@ -606,4 +606,13 @@ describe("settingsTransfer", () => { expect(result.ok).toBe(true); if (result.ok) expect(result.value.editorSettings.caretBrowsingEnabled).toBe(true); }); + + it("round-trips aiSqlApplyTargetMode and rejects invalid values (#12438)", () => { + expect(transferCategoryForKey("aiSqlApplyTargetMode")).toBe("editor"); + const text = serializeSettingsTransfer({ ...DEFAULT_EDITOR_SETTINGS, aiSqlApplyTargetMode: "new" }); + const result = parseSettingsTransferFile(text); + expect(result.ok).toBe(true); + if (result.ok) expect(result.value.editorSettings.aiSqlApplyTargetMode).toBe("new"); + expect(parseSettingsTransferFile(fileWith({ aiSqlApplyTargetMode: "invalid" })).ok).toBe(false); + }); }); diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index 88f38306f7..2f50d12d9c 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -9,6 +9,7 @@ import { normalizeSqlTableCompletionSchemaQualification } from "@/lib/sql/sqlCom import { normalizeTableHoverLookupMode } from "@/lib/editor/hoverTableLookup"; import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates"; import { normalizeCsvNullMode } from "@/lib/export/csvNullMode"; +import { normalizeAiSqlApplyTargetMode } from "@/lib/ai/aiSqlTargetTab"; export const EDITOR_SETTINGS_DRAFT_KEYS = [ "fontFamily", @@ -49,6 +50,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "confirmUnsavedSqlClose", "appCloseUnsavedTabsMode", "savedSqlOpenTargetMode", + "aiSqlApplyTargetMode", "welcomePageMode", "appLayout", "webLogoPosition", @@ -203,6 +205,7 @@ function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unkn if (key === "backgroundImage") return normalizeBackgroundImageSettings(value); if (key === "snippetTriggerKey") return value === "space" || value === "both" ? value : "tab"; if (key === "webLogoPosition") return value === "right" || value === "hidden" ? value : "left"; + if (key === "aiSqlApplyTargetMode") return normalizeAiSqlApplyTargetMode(value); return value; } diff --git a/apps/desktop/src/lib/settings/settingsSearch.ts b/apps/desktop/src/lib/settings/settingsSearch.ts index 8f64acd0fc..be3cae9455 100644 --- a/apps/desktop/src/lib/settings/settingsSearch.ts +++ b/apps/desktop/src/lib/settings/settingsSearch.ts @@ -165,6 +165,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "editor-diagnostics", category: "editor", titleKey: "settings.sqlSemanticDiagnosticsEnabled", descriptionKey: "settings.sqlSemanticDiagnosticsEnabledDescription", targetId: "editor" }, { id: "editor-sql-variables", category: "editor", titleKey: "settings.sqlVariableSyntax", descriptionKey: "settings.sqlVariableSyntaxDescription", targetId: "editor" }, { id: "editor-saved-sql-target", category: "editor", titleKey: "settings.savedSqlOpenTarget", targetId: "editor" }, + { id: "editor-ai-sql-apply-target", category: "editor", titleKey: "settings.aiSqlApplyTarget", descriptionKey: "settings.aiSqlApplyTargetCurrentDescription", targetId: "editor" }, { id: "editor-confirm-dangerous-sql", category: "editor", titleKey: "settings.confirmDangerousSqlExecution", descriptionKey: "settings.confirmDangerousSqlExecutionDescription", targetId: "editor" }, { id: "editor-continue-batch-on-error", category: "editor", titleKey: "settings.continueOnErrorOnBatch", descriptionKey: "settings.continueOnErrorOnBatchDescription", targetId: "editor" }, { id: "editor-table-click-navigation", category: "navigation", titleKey: "settings.clickTableNavigationTarget", descriptionKey: "settings.clickTableNavigationTargetDescription", targetId: "navigation" }, @@ -310,6 +311,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "ai-prompts", category: "ai", titleKey: "ai.promptTemplates", descriptionKey: "ai.promptTemplatesDescription", targetId: "ai" }, { id: "ai-default-mode", category: "ai", titleKey: "ai.defaultAiMode", descriptionKey: "ai.defaultAiModeDescription", targetId: "ai" }, { id: "ai-default-auto-routing", category: "ai", titleKey: "ai.defaultAutoRouting", descriptionKey: "ai.defaultAutoRoutingDescription", targetId: "ai" }, + { id: "ai-sql-apply-target", category: "ai", titleKey: "settings.aiSqlApplyTarget", descriptionKey: "settings.aiSqlApplyTargetCurrentDescription", targetId: "ai" }, { id: "ai-agent-turn-limit", category: "ai", titleKey: "ai.maxAgentTurns", descriptionKey: "ai.maxAgentTurnsDescription", targetId: "ai" }, { id: "ai-global-retries", category: "ai", titleKey: "ai.maxRetriesGlobal", descriptionKey: "ai.maxRetriesGlobalDescription", targetId: "ai" }, { id: "ai-global-instructions", category: "ai", titleKey: "ai.globalInstructions", descriptionKey: "ai.globalInstructionsDescription", targetId: "ai" }, diff --git a/apps/desktop/src/lib/settings/settingsTransfer.ts b/apps/desktop/src/lib/settings/settingsTransfer.ts index d5ede875c7..62cd2d7f62 100644 --- a/apps/desktop/src/lib/settings/settingsTransfer.ts +++ b/apps/desktop/src/lib/settings/settingsTransfer.ts @@ -105,6 +105,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record typeof value === "number" && Number.isFinite(value) && value >= EDITOR_MIN_FONT_SIZE && value <= EDITOR_MAX_FONT_SIZE, welcomePageMode: (value) => value === "intro" || value === "workspace", + aiSqlApplyTargetMode: (value) => value === "current" || value === "first" || value === "new", appLayout: (value) => value === "separated" || value === "classic", webLogoPosition: (value) => value === "left" || value === "right" || value === "hidden", resultTabNamingMode: (value) => value === "source" || value === "ordinal" || value === "comment", diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts index ca97d931d8..8d3e831626 100644 --- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts +++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts @@ -75,6 +75,15 @@ describe("normalizeEditorSettings", () => { expect(normalizeEditorSettings({ queryResultExportFileNameSource: undefined }).queryResultExportFileNameSource).toBe("result-label"); }); + it("defaults aiSqlApplyTargetMode to current and validates options (#12438)", () => { + expect(DEFAULT_EDITOR_SETTINGS.aiSqlApplyTargetMode).toBe("current"); + expect(normalizeEditorSettings({}).aiSqlApplyTargetMode).toBe("current"); + expect(normalizeEditorSettings({ aiSqlApplyTargetMode: "current" }).aiSqlApplyTargetMode).toBe("current"); + expect(normalizeEditorSettings({ aiSqlApplyTargetMode: "first" }).aiSqlApplyTargetMode).toBe("first"); + expect(normalizeEditorSettings({ aiSqlApplyTargetMode: "new" }).aiSqlApplyTargetMode).toBe("new"); + expect(normalizeEditorSettings({ aiSqlApplyTargetMode: "invalid" as any }).aiSqlApplyTargetMode).toBe("current"); + }); + it("defaults caretBrowsingEnabled to false and normalizes boolean values (#11827)", () => { expect(DEFAULT_EDITOR_SETTINGS.caretBrowsingEnabled).toBe(false); expect(normalizeEditorSettings({}).caretBrowsingEnabled).toBe(false); diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 10c313b154..d3f0fd9ca4 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -22,6 +22,7 @@ import { normalizeExternalSqlEditorMaxMb } from "@/lib/sql/sqlFileOpen"; import { DEFAULT_QUERY_TIMEOUT_SECS, normalizeConnectTimeoutSecs, normalizeQueryTimeoutSecs } from "@/lib/connection/timeoutLimits"; import { needsTabNavigationHistoryShortcutMigration, normalizeShortcutSettings, isReservedShortcut, type ShortcutSettings } from "@/lib/editor/shortcutRegistry"; import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget"; +import { type AiSqlApplyTargetMode, normalizeAiSqlApplyTargetMode } from "@/lib/ai/aiSqlTargetTab"; import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort"; import { type ColumnNameCopySeparator } from "@/lib/dataGrid/dataGridColumnNameCopy"; import { normalizeRedisValuePreviewLimitKiB, REDIS_VALUE_PREVIEW_LIMIT_KIB_DEFAULT } from "@/lib/redis/redisValuePreviewLimit"; @@ -65,6 +66,7 @@ export type { DataTabReuseMode, QueryResultExportFileNameSource, SavedSqlOpenTargetMode, + AiSqlApplyTargetMode, SqlCompletionTriggerMode, SqlTableCompletionSchemaQualification, TableHoverLookupMode, @@ -1004,6 +1006,7 @@ export interface EditorSettings { confirmUnsavedSqlClose: boolean; appCloseUnsavedTabsMode: AppCloseUnsavedTabsMode; savedSqlOpenTargetMode: SavedSqlOpenTargetMode; + aiSqlApplyTargetMode: AiSqlApplyTargetMode; welcomePageMode: WelcomePageMode; welcomePageModeDefaultVersion: number; compactTabTitle: boolean; @@ -1380,6 +1383,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { confirmUnsavedSqlClose: true, appCloseUnsavedTabsMode: "keep-drafts", savedSqlOpenTargetMode: "saved", + aiSqlApplyTargetMode: "current", welcomePageMode: "workspace", welcomePageModeDefaultVersion: WELCOME_PAGE_DEFAULT_VERSION, compactTabTitle: true, @@ -2043,6 +2047,7 @@ export function normalizeEditorSettings(settings: Partial, exist confirmUnsavedSqlClose: settings.confirmUnsavedSqlClose ?? DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose, appCloseUnsavedTabsMode: normalizeAppCloseUnsavedTabsMode(settings.appCloseUnsavedTabsMode), savedSqlOpenTargetMode: settings.savedSqlOpenTargetMode === "current" ? "current" : DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode, + aiSqlApplyTargetMode: normalizeAiSqlApplyTargetMode(settings.aiSqlApplyTargetMode, DEFAULT_EDITOR_SETTINGS.aiSqlApplyTargetMode), welcomePageMode: hasCurrentWelcomePageDefault && (settings.welcomePageMode === "intro" || settings.welcomePageMode === "workspace") ? settings.welcomePageMode : DEFAULT_EDITOR_SETTINGS.welcomePageMode, welcomePageModeDefaultVersion, compactTabTitle: hasCurrentCompactTabTitleDefault ? settings.compactTabTitle === true : DEFAULT_EDITOR_SETTINGS.compactTabTitle, @@ -2933,6 +2938,7 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.confirmUnsavedSqlClose !== undefined) editorSettings.value.confirmUnsavedSqlClose = partial.confirmUnsavedSqlClose; if (partial.appCloseUnsavedTabsMode !== undefined) editorSettings.value.appCloseUnsavedTabsMode = normalizeAppCloseUnsavedTabsMode(partial.appCloseUnsavedTabsMode); if (partial.savedSqlOpenTargetMode !== undefined) editorSettings.value.savedSqlOpenTargetMode = partial.savedSqlOpenTargetMode === "current" ? "current" : "saved"; + if (partial.aiSqlApplyTargetMode !== undefined) editorSettings.value.aiSqlApplyTargetMode = normalizeAiSqlApplyTargetMode(partial.aiSqlApplyTargetMode, DEFAULT_EDITOR_SETTINGS.aiSqlApplyTargetMode); if (partial.welcomePageMode !== undefined) editorSettings.value.welcomePageMode = partial.welcomePageMode === "intro" || partial.welcomePageMode === "workspace" ? partial.welcomePageMode : DEFAULT_EDITOR_SETTINGS.welcomePageMode; if (partial.compactTabTitle !== undefined) { editorSettings.value.compactTabTitle = partial.compactTabTitle;