mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-01 00:25:38 +00:00
feat(runtime): 同步前端状态到 agent runtime
- 新增 snapshot 构造 helper 和 agent runtime store - 同步 active tab、连接、库、schema、SQL、选中 SQL、表格选择和结果采样 - 补充 snapshot payload 组装测试
This commit is contained in:
+9
-1
@@ -15,6 +15,7 @@ import UpdateDialog from "@/components/layout/UpdateDialog.vue";
|
||||
import LoginPage from "@/components/auth/LoginPage.vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useAgentRuntimeStore } from "@/stores/agentRuntimeStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
@@ -46,6 +47,7 @@ import type { HistoryEntry } from "@/lib/tauri";
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const agentRuntimeStore = useAgentRuntimeStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const savedSqlStore = useSavedSqlStore();
|
||||
const { message: toastMessage, visible: toastVisible, toast } = useToast();
|
||||
@@ -158,6 +160,7 @@ watch(
|
||||
() => queryStore.activeTabId,
|
||||
() => {
|
||||
selectedSql.value = "";
|
||||
agentRuntimeStore.setSelectedSql("");
|
||||
activeOutputView.value = "result";
|
||||
},
|
||||
);
|
||||
@@ -614,7 +617,12 @@ onUnmounted(() => {
|
||||
if (queryStore.activeTabId) queryStore.updateSql(queryStore.activeTabId, v);
|
||||
}
|
||||
"
|
||||
@editor-selection-change="(v: string) => (selectedSql = v)"
|
||||
@editor-selection-change="
|
||||
(v: string) => {
|
||||
selectedSql = v;
|
||||
agentRuntimeStore.setSelectedSql(v);
|
||||
}
|
||||
"
|
||||
@editor-cursor-change="(p: number) => (cursorPos = p)"
|
||||
@format-error="toast(t('toolbar.formatSqlFailed'))"
|
||||
@reload="
|
||||
|
||||
@@ -62,9 +62,17 @@ import type { QueryResult, ColumnInfo, DatabaseType } from "@/types/database";
|
||||
import * as api from "@/lib/api";
|
||||
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
|
||||
import { isHiddenGridColumn, usesSyntheticRowIdKey } from "@/lib/tableEditing";
|
||||
import { formatGridSqlLiteral } from "@/lib/dataGridSql";
|
||||
import { matchesRowStatusFilter, type RowStatus, type RowStatusFilter } from "@/lib/gridRowStatus";
|
||||
import { displayCellValue, type CellValue } from "@/lib/cellValue";
|
||||
import { buildDataGridSaveStatements, formatGridSqlLiteral } from "@/lib/dataGridSql";
|
||||
import { formatMarkdownTable } from "@/lib/markdownTable";
|
||||
import { buildXlsxWorkbook } from "@/lib/xlsxExport";
|
||||
import {
|
||||
matchesRowStatusFilter,
|
||||
rowStatusFilterAfterAddingRow,
|
||||
type RowStatus,
|
||||
type RowStatusFilter,
|
||||
} from "@/lib/gridRowStatus";
|
||||
import { useAgentRuntimeStore } from "@/stores/agentRuntimeStore";
|
||||
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useDataGridExport } from "@/composables/useDataGridExport";
|
||||
@@ -74,6 +82,7 @@ import { useDataGridEditor } from "@/composables/useDataGridEditor";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const agentRuntimeStore = useAgentRuntimeStore();
|
||||
|
||||
const props = defineProps<{
|
||||
result: QueryResult;
|
||||
@@ -1090,6 +1099,22 @@ const activeCellDetail = computed(() => {
|
||||
const detailEditValue = ref("");
|
||||
const isEditingDetail = ref(false);
|
||||
|
||||
watch(
|
||||
selectedCells,
|
||||
(data) => {
|
||||
agentRuntimeStore.setSelection(
|
||||
selectedCellCount.value > 0
|
||||
? {
|
||||
type: "grid-cells",
|
||||
range: selectedRange.value,
|
||||
data,
|
||||
}
|
||||
: { type: "none" },
|
||||
);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
function startDetailEdit() {
|
||||
const detail = activeCellDetail.value;
|
||||
if (!detail || !detail.isEditable) return;
|
||||
@@ -1611,6 +1636,7 @@ watch(
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupFrames();
|
||||
agentRuntimeStore.setSelection({ type: "none" });
|
||||
onDdlResizeEnd();
|
||||
finishCellSelection();
|
||||
clearTimeout(_searchTimer);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { ConnectionConfig, QueryResult, QueryTab } from "@/types/database";
|
||||
|
||||
export const DEFAULT_RESULT_SAMPLE_LIMIT = 50;
|
||||
|
||||
export interface RuntimeResultSample {
|
||||
columns: string[];
|
||||
rows: QueryResult["rows"];
|
||||
truncated: boolean;
|
||||
executionTimeMs: number;
|
||||
sampleLimit: number;
|
||||
}
|
||||
|
||||
export interface AgentRuntimeSnapshot {
|
||||
activeConnectionId?: string;
|
||||
activeConnectionName?: string;
|
||||
database?: string;
|
||||
schema?: string;
|
||||
activeTabId?: string;
|
||||
activeTabTitle?: string;
|
||||
sql?: string;
|
||||
selectedSql?: string;
|
||||
selection: unknown;
|
||||
result?: RuntimeResultSample;
|
||||
}
|
||||
|
||||
export interface BuildAgentRuntimeSnapshotOptions {
|
||||
tabs: QueryTab[];
|
||||
activeTabId: string | null;
|
||||
getConnection: (connectionId: string) => Pick<ConnectionConfig, "name"> | undefined;
|
||||
selectedSql?: string;
|
||||
selection?: unknown;
|
||||
resultSampleLimit?: number;
|
||||
}
|
||||
|
||||
export function buildAgentRuntimeSnapshot(options: BuildAgentRuntimeSnapshotOptions): AgentRuntimeSnapshot {
|
||||
const tab = options.tabs.find((item) => item.id === options.activeTabId);
|
||||
const conn = tab ? options.getConnection(tab.connectionId) : undefined;
|
||||
const selectedSql = options.selectedSql?.trim();
|
||||
const limit = options.resultSampleLimit ?? DEFAULT_RESULT_SAMPLE_LIMIT;
|
||||
const result = tab?.result
|
||||
? {
|
||||
columns: tab.result.columns,
|
||||
rows: tab.result.rows.slice(0, limit),
|
||||
truncated: tab.result.rows.length > limit || !!tab.result.truncated,
|
||||
executionTimeMs: tab.result.execution_time_ms,
|
||||
sampleLimit: limit,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
activeConnectionId: tab?.connectionId,
|
||||
activeConnectionName: conn?.name,
|
||||
database: tab?.database,
|
||||
schema: tab?.schema,
|
||||
activeTabId: tab?.id,
|
||||
activeTabTitle: tab?.title,
|
||||
sql: tab?.sql,
|
||||
selectedSql: selectedSql || undefined,
|
||||
selection: options.selection ?? { type: "none" },
|
||||
result,
|
||||
};
|
||||
}
|
||||
@@ -72,6 +72,10 @@ export const executeScript = forward("executeScript");
|
||||
export const executeInTransaction = forward("executeInTransaction");
|
||||
export const cancelQuery = forward("cancelQuery");
|
||||
|
||||
// Agent Runtime
|
||||
export const agentRuntimeUpdateSnapshot = forward("agentRuntimeUpdateSnapshot");
|
||||
export const agentRuntimeLoadHandoffs = forward("agentRuntimeLoadHandoffs");
|
||||
|
||||
// AI
|
||||
export const aiComplete = forward("aiComplete");
|
||||
export const aiStream = forward("aiStream");
|
||||
|
||||
@@ -39,6 +39,7 @@ import type {
|
||||
TableImportSummary,
|
||||
TableImportProgress,
|
||||
} from "./tauri";
|
||||
import type { AgentRuntimeSnapshot } from "@/lib/agentRuntimeSnapshot";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -290,6 +291,12 @@ export async function cancelQuery(executionId: string): Promise<boolean> {
|
||||
return post("/api/query/cancel", { executionId });
|
||||
}
|
||||
|
||||
export async function agentRuntimeUpdateSnapshot(_snapshot: AgentRuntimeSnapshot): Promise<void> {}
|
||||
|
||||
export async function agentRuntimeLoadHandoffs(): Promise<unknown[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
SavedSqlFolder,
|
||||
SavedSqlLibrary,
|
||||
} from "@/types/database";
|
||||
import type { AgentRuntimeSnapshot } from "@/lib/agentRuntimeSnapshot";
|
||||
import type { AiConfig } from "@/stores/settingsStore";
|
||||
|
||||
export interface AiMessage {
|
||||
@@ -80,6 +81,14 @@ export async function loadAiConfig(): Promise<AiConfig | null> {
|
||||
return invoke("load_ai_config");
|
||||
}
|
||||
|
||||
export async function agentRuntimeUpdateSnapshot(snapshot: AgentRuntimeSnapshot): Promise<void> {
|
||||
return invoke("agent_runtime_update_snapshot", { snapshot });
|
||||
}
|
||||
|
||||
export async function agentRuntimeLoadHandoffs(): Promise<unknown[]> {
|
||||
return invoke("agent_runtime_load_handoffs");
|
||||
}
|
||||
|
||||
// --- AI Conversations ---
|
||||
|
||||
export interface AiChatMessage {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import * as api from "@/lib/api";
|
||||
import { buildAgentRuntimeSnapshot, DEFAULT_RESULT_SAMPLE_LIMIT } from "@/lib/agentRuntimeSnapshot";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
|
||||
export const useAgentRuntimeStore = defineStore("agentRuntime", () => {
|
||||
const selection = ref<unknown>({ type: "none" });
|
||||
const selectedSql = ref("");
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function setSelection(value: unknown) {
|
||||
selection.value = value;
|
||||
scheduleSync();
|
||||
}
|
||||
|
||||
function setSelectedSql(value: string) {
|
||||
selectedSql.value = value;
|
||||
scheduleSync();
|
||||
}
|
||||
|
||||
function scheduleSync() {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
void syncNow();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function syncNow() {
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const snapshot = buildAgentRuntimeSnapshot({
|
||||
tabs: queryStore.tabs,
|
||||
activeTabId: queryStore.activeTabId,
|
||||
getConnection: (connectionId) => connectionStore.getConfig(connectionId),
|
||||
selectedSql: selectedSql.value,
|
||||
selection: selection.value,
|
||||
resultSampleLimit: DEFAULT_RESULT_SAMPLE_LIMIT,
|
||||
});
|
||||
|
||||
try {
|
||||
await api.agentRuntimeUpdateSnapshot(snapshot);
|
||||
} catch (err) {
|
||||
console.debug("[DBX] Agent runtime snapshot sync skipped:", err);
|
||||
}
|
||||
}
|
||||
|
||||
return { selection, selectedSql, setSelection, setSelectedSql, scheduleSync, syncNow };
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { analyzeEditableQuery, allPrimaryKeysPresent } from "@/lib/sqlAnalysis";
|
||||
import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersistence";
|
||||
import * as api from "@/lib/api";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useAgentRuntimeStore } from "@/stores/agentRuntimeStore";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import type { SavedSqlFile } from "@/types/database";
|
||||
|
||||
@@ -72,11 +73,18 @@ export const useQueryStore = defineStore("query", () => {
|
||||
return tabs.value.find((t) => t.connectionId === connectionId && t.database === database && t.title === title);
|
||||
}
|
||||
|
||||
function scheduleAgentRuntimeSync() {
|
||||
useAgentRuntimeStore().scheduleSync();
|
||||
}
|
||||
|
||||
watch(activeTabId, () => scheduleAgentRuntimeSync());
|
||||
|
||||
function createTab(connectionId: string, database: string, title?: string, mode: QueryTab["mode"] = "query") {
|
||||
if (title) {
|
||||
const existing = findTabByTitle(connectionId, database, title);
|
||||
if (existing) {
|
||||
activeTabId.value = existing.id;
|
||||
scheduleAgentRuntimeSync();
|
||||
return existing.id;
|
||||
}
|
||||
}
|
||||
@@ -95,6 +103,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
};
|
||||
tabs.value.push(tab);
|
||||
activeTabId.value = id;
|
||||
scheduleAgentRuntimeSync();
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -145,6 +154,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (activeTabId.value === id) {
|
||||
activeTabId.value = tabs.value[Math.min(idx, tabs.value.length - 1)]?.id ?? null;
|
||||
}
|
||||
scheduleAgentRuntimeSync();
|
||||
}
|
||||
|
||||
function closeOtherTabs(id: string) {
|
||||
@@ -153,6 +163,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const next = closeOtherTabsState(tabs.value, activeTabId.value, id);
|
||||
tabs.value = next.tabs;
|
||||
activeTabId.value = next.activeTabId;
|
||||
scheduleAgentRuntimeSync();
|
||||
}
|
||||
|
||||
function closeAllTabs() {
|
||||
@@ -161,6 +172,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const next = closeAllTabsState(tabs.value, activeTabId.value);
|
||||
tabs.value = next.tabs;
|
||||
activeTabId.value = next.activeTabId;
|
||||
scheduleAgentRuntimeSync();
|
||||
}
|
||||
|
||||
function updateSql(id: string, sql: string) {
|
||||
@@ -169,6 +181,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
tab.sql = sql;
|
||||
tab.resultSortedSql = undefined;
|
||||
tab.resultBaseSql = undefined;
|
||||
scheduleAgentRuntimeSync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +196,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const existing = tabs.value.find((tab) => tab.savedSqlId === file.id);
|
||||
if (existing) {
|
||||
activeTabId.value = existing.id;
|
||||
scheduleAgentRuntimeSync();
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
@@ -202,6 +216,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
};
|
||||
tabs.value.push(tab);
|
||||
activeTabId.value = id;
|
||||
scheduleAgentRuntimeSync();
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -224,6 +239,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
tab.resultSortedSql = undefined;
|
||||
clearExplain(tab);
|
||||
tab.tableMeta = undefined;
|
||||
scheduleAgentRuntimeSync();
|
||||
}
|
||||
|
||||
function updateSchema(id: string, schema: string | undefined) {
|
||||
@@ -231,6 +247,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (!tab || tab.schema === schema) return;
|
||||
tab.schema = schema;
|
||||
if (tab.mode === "objects") tab.objectBrowser = { ...tab.objectBrowser, schema };
|
||||
scheduleAgentRuntimeSync();
|
||||
}
|
||||
|
||||
function updateConnection(id: string, connectionId: string, database = "") {
|
||||
@@ -245,6 +262,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
tab.resultSortedSql = undefined;
|
||||
clearExplain(tab);
|
||||
tab.tableMeta = undefined;
|
||||
scheduleAgentRuntimeSync();
|
||||
}
|
||||
|
||||
function setTableMeta(id: string, meta: NonNullable<QueryTab["tableMeta"]>) {
|
||||
@@ -410,6 +428,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
console.info("[DBX][executeTabSql:metadata:start]", { traceId, elapsed: elapsed() });
|
||||
await analyzeQueryMetadata(current, current.resultBaseSql);
|
||||
console.info("[DBX][executeTabSql:metadata:done]", { traceId, elapsed: elapsed() });
|
||||
scheduleAgentRuntimeSync();
|
||||
} else {
|
||||
console.warn("[DBX][executeTabSql:stale-result]", {
|
||||
traceId,
|
||||
@@ -428,6 +447,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (current.mode !== "data") current.tableMeta = undefined;
|
||||
current.resultBaseSql = options?.resultBaseSql ?? sql;
|
||||
current.resultSortedSql = options?.resultSortedSql;
|
||||
scheduleAgentRuntimeSync();
|
||||
}
|
||||
} finally {
|
||||
const current = tabs.value.find((t) => t.id === id);
|
||||
@@ -538,6 +558,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (!tab?.results || index < 0 || index >= tab.results.length) return;
|
||||
tab.activeResultIndex = index;
|
||||
tab.result = tab.results[index];
|
||||
scheduleAgentRuntimeSync();
|
||||
}
|
||||
|
||||
function trimResultCache() {
|
||||
|
||||
Reference in New Issue
Block a user