feat(runtime): 同步前端状态到 agent runtime

- 新增 snapshot 构造 helper 和 agent runtime store

- 同步 active tab、连接、库、schema、SQL、选中 SQL、表格选择和结果采样

- 补充 snapshot payload 组装测试
This commit is contained in:
Illuminated2020
2026-05-12 16:51:34 +08:00
parent feff43c459
commit c144330d79
9 changed files with 271 additions and 3 deletions
+9 -1
View File
@@ -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="
+28 -2
View File
@@ -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);
+62
View File
@@ -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,
};
}
+4
View File
@@ -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");
+7
View File
@@ -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
// ---------------------------------------------------------------------------
+9
View File
@@ -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 {
+51
View File
@@ -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 };
});
+21
View File
@@ -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() {