diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue index 85ac68678d..fb02efc757 100644 --- a/apps/desktop/src/components/editor/AiAssistant.vue +++ b/apps/desktop/src/components/editor/AiAssistant.vue @@ -25,6 +25,7 @@ import { HelpCircle, History, ArrowDownToLine, + Layers, Loader2, Maximize2, MessageSquarePlus, @@ -63,6 +64,7 @@ import { usePromptTemplateStore } from "@/stores/promptTemplateStore"; import { connectionIconType } from "@/lib/connection/connectionPresentation"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import ConnectionTreeSelect from "@/components/connection/ConnectionTreeSelect.vue"; +import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect.vue"; import { useQueryStore } from "@/stores/queryStore"; import { useToast } from "@/composables/useToast"; import { useNavigationTargets } from "@/composables/useNavigationTargets"; @@ -71,6 +73,7 @@ import { resolveAiDatabaseTarget, resolveAiNamespaceSelection, resolveDefaultAiSchema, + aiSchemaSelectionSupported, runAgentStream, isVectorDbType, isValidActionForMode, @@ -147,10 +150,11 @@ import type { AiMessage } from "@/lib/backend/api"; import type { AiConfigItem, AiEffortCapability, AiEffortOption, AiEffortSelection } from "@/types/ai"; import type { ConnectionConfig, QueryTab, SavedSqlFile, TableInfo } from "@/types/database"; import { fetchNamespaceOptionsForConnection, useDatabaseOptions } from "@/composables/useDatabaseOptions"; +import { useSchemaOptions } from "@/composables/useSchemaOptions"; import { decodeSelectableDatabaseValue, encodeSelectableDatabaseValue, formatDatabaseLabel, resolveDefaultDatabase } from "@/lib/database/defaultDatabase"; import { normalizeSqliteNamespace } from "@/lib/database/sqliteNamespace"; import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError"; -import { isSchemaAware } from "@/lib/database/databaseCapabilities"; +import { isSchemaAware, isSingleDatabase } from "@/lib/database/databaseCapabilities"; import ExplainPlanViewer from "@/components/explain/ExplainPlanViewer.vue"; import { parseExplainResult, parseOracleExplainText, type ParsedExplainPlan } from "@/lib/diagram/explainPlan"; import { copyToClipboard } from "@/lib/common/clipboard"; @@ -437,14 +441,14 @@ watch(showTemplateSelector, (open) => { if (open) void promptTemplateStore.ensureLoaded(); }); -// Reset template selection when the user switches to a different connection or database — -// a new database context warrants a fresh selection of scenario templates. +// Reset template selection when the user switches to a different connection, +// database, or schema — a new namespace context warrants a fresh selection. watch( // Return a stable primitive key: a fresh array literal is never Object.is-equal to the // previous one, so a getter returning `[id, database]` fires on every dependency // invalidation (e.g. the 30s backup scheduler replacing connection objects) even when the // id/database values are unchanged — spuriously clearing the selection mid agent-run. - () => `${props.connection?.id ?? ""}::${props.tab?.database ?? ""}`, + () => `${props.connection?.id ?? ""}::${props.tab?.database ?? ""}::${props.tab?.schema ?? ""}`, () => { activeTemplateIds.value = []; }, @@ -1277,6 +1281,7 @@ function selectModeActionItem(action: AiAction) { } const { databaseOptions, loadDatabaseOptions } = useDatabaseOptions(); +const { loadSchemaOptions, getSchemaOptionsForDb, isLoadingSchemas } = useSchemaOptions(); // Dameng presents schemas as its top-level namespace, unlike the other // connection types that rely on the shared database-options loader. @@ -1315,6 +1320,30 @@ const selectedDatabaseLabel = computed(() => { }); }); +const showAiSchemaSelector = computed(() => { + const connection = props.connection; + return !!connection && connection.db_type !== "dameng" && aiSchemaSelectionSupported(connection); +}); + +const aiSchemaDatabaseKey = computed(() => { + const connection = props.connection; + const tab = props.tab; + if (!connection || !tab) return ""; + return tab.database || (isSingleDatabase(connection.db_type) ? "_" : ""); +}); + +const aiSchemaOptions = computed(() => { + const connection = props.connection; + if (!connection) return []; + return getSchemaOptionsForDb(connection.id, aiSchemaDatabaseKey.value); +}); + +async function loadAiSchemas() { + const connection = props.connection; + if (!connection || !showAiSchemaSelector.value) return; + await loadSchemaOptions(connection.id, aiSchemaDatabaseKey.value); +} + async function loadDatabases(connection = props.connection): Promise { if (!connection) return []; if (connection.db_type !== "dameng") { @@ -1365,6 +1394,13 @@ function changeNamespace(value: string) { } } +function changeSchema(schema: string) { + const tab = props.tab; + if (!tab || tab.schema === schema) return; + clearContextReferences(); + queryStore.updateSchema(tab.id, schema || undefined); +} + function flushAssistantDeltas() { assistantDeltaFrame = null; lastAssistantFlushAt = performance.now(); @@ -4711,13 +4747,13 @@ async function openExternalUrl(url: string) {
-
+
{{ t("ai.attachmentDropHint") }}
-
+
- + - @@ -5260,6 +5323,30 @@ async function openExternalUrl(url: string) {