mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-30 08:20:33 +00:00
feat(agent-runtime): 接入 handoff 审阅状态
- 支持 handoff queued/shown 状态加载、mark shown 和 reject 持久化 - 为 Tauri/API/Pinia store 接入 handoff 审阅状态流转 - 新增 AgentHandoffDialog 审阅 UI,拒绝操作不执行 SQL
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from "vue";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { useAgentRuntimeStore } from "@/stores/agentRuntimeStore";
|
||||
|
||||
const agentRuntimeStore = useAgentRuntimeStore();
|
||||
let refreshTimer: number | null = null;
|
||||
|
||||
const active = computed(() => agentRuntimeStore.activeHandoff);
|
||||
const riskTone = computed(() => {
|
||||
if (!active.value) return "secondary";
|
||||
return active.value.riskLevel === "critical" || active.value.isProduction ? "destructive" : "secondary";
|
||||
});
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
await agentRuntimeStore.loadHandoffs();
|
||||
} catch (err) {
|
||||
console.debug("[DBX] Agent handoff refresh skipped:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectActive() {
|
||||
if (!active.value) return;
|
||||
await agentRuntimeStore.rejectHandoff(active.value.id);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void refresh();
|
||||
refreshTimer = window.setInterval(() => void refresh(), 5000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) window.clearInterval(refreshTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="agentRuntimeStore.handoffDialogOpen">
|
||||
<DialogContent class="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>DBX Agent Handoff</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="active" class="space-y-4">
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<Badge :variant="riskTone">{{ active.riskLevel }}</Badge>
|
||||
<span>{{ active.connectionName }}</span>
|
||||
<span v-if="active.database">/ {{ active.database }}</span>
|
||||
<span>/ {{ active.operationClass }}</span>
|
||||
<span v-if="active.status === 'shown'">/ shown</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-sm font-semibold">{{ active.title }}</h3>
|
||||
<p v-if="active.description" class="text-sm text-muted-foreground">{{ active.description }}</p>
|
||||
</div>
|
||||
|
||||
<pre class="max-h-96 overflow-auto rounded-md border bg-muted/60 p-3 text-xs leading-relaxed">{{
|
||||
active.sql
|
||||
}}</pre>
|
||||
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Review only: DBX does not execute agent handoff SQL from this dialog.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-sm text-muted-foreground">No pending agent handoffs.</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="agentRuntimeStore.handoffDialogOpen = false">Close</Button>
|
||||
<Button v-if="active" variant="destructive" @click="rejectActive">Reject</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import ConnectionDialog from "@/components/connection/ConnectionDialog.vue";
|
||||
import AgentHandoffDialog from "@/components/agent/AgentHandoffDialog.vue";
|
||||
import EditorSettingsDialog from "@/components/editor/EditorSettingsDialog.vue";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
const DataTransferDialog = defineAsyncComponent(() => import("@/components/transfer/DataTransferDialog.vue"));
|
||||
@@ -92,6 +93,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AgentHandoffDialog />
|
||||
<ConnectionDialog
|
||||
:open="showConnectionDialog"
|
||||
:edit-config="editConfig"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
export type AgentHandoffStatus = "queued" | "shown" | "approved" | "rejected" | "executed" | "failed";
|
||||
|
||||
export interface AgentHandoffItem {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
connectionId: string;
|
||||
connectionName: string;
|
||||
database?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
sql: string;
|
||||
operationClass: string;
|
||||
riskLevel: string;
|
||||
isProduction: boolean;
|
||||
status: AgentHandoffStatus;
|
||||
resultSummary?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function isPendingHandoff(item: AgentHandoffItem): boolean {
|
||||
return item.status === "queued" || item.status === "shown";
|
||||
}
|
||||
|
||||
export function mergeLoadedHandoffs(items: AgentHandoffItem[]): AgentHandoffItem[] {
|
||||
return items.filter(isPendingHandoff).sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
|
||||
}
|
||||
|
||||
export function updateHandoffStatus(
|
||||
items: AgentHandoffItem[],
|
||||
id: string,
|
||||
status: AgentHandoffStatus,
|
||||
): AgentHandoffItem[] {
|
||||
return mergeLoadedHandoffs(items.map((item) => (item.id === id ? { ...item, status } : item)));
|
||||
}
|
||||
|
||||
export function deriveHandoffDialogState(items: AgentHandoffItem[], activeId: string | null) {
|
||||
const active = (activeId ? items.find((item) => item.id === activeId) : undefined) ?? items[0] ?? null;
|
||||
return {
|
||||
open: items.length > 0,
|
||||
active,
|
||||
};
|
||||
}
|
||||
@@ -75,6 +75,8 @@ export const cancelQuery = forward("cancelQuery");
|
||||
// Agent Runtime
|
||||
export const agentRuntimeUpdateSnapshot = forward("agentRuntimeUpdateSnapshot");
|
||||
export const agentRuntimeLoadHandoffs = forward("agentRuntimeLoadHandoffs");
|
||||
export const agentRuntimeMarkHandoffShown = forward("agentRuntimeMarkHandoffShown");
|
||||
export const agentRuntimeRejectHandoff = forward("agentRuntimeRejectHandoff");
|
||||
|
||||
// AI
|
||||
export const aiComplete = forward("aiComplete");
|
||||
|
||||
+10
-1
@@ -40,6 +40,7 @@ import type {
|
||||
TableImportProgress,
|
||||
} from "./tauri";
|
||||
import type { AgentRuntimeSnapshot } from "@/lib/agentRuntimeSnapshot";
|
||||
import type { AgentHandoffItem } from "@/lib/agentHandoff";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -293,10 +294,18 @@ export async function cancelQuery(executionId: string): Promise<boolean> {
|
||||
|
||||
export async function agentRuntimeUpdateSnapshot(_snapshot: AgentRuntimeSnapshot): Promise<void> {}
|
||||
|
||||
export async function agentRuntimeLoadHandoffs(): Promise<unknown[]> {
|
||||
export async function agentRuntimeLoadHandoffs(): Promise<AgentHandoffItem[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function agentRuntimeMarkHandoffShown(_id: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function agentRuntimeRejectHandoff(_id: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+10
-1
@@ -20,6 +20,7 @@ import type {
|
||||
SavedSqlLibrary,
|
||||
} from "@/types/database";
|
||||
import type { AgentRuntimeSnapshot } from "@/lib/agentRuntimeSnapshot";
|
||||
import type { AgentHandoffItem } from "@/lib/agentHandoff";
|
||||
import type { AiConfig } from "@/stores/settingsStore";
|
||||
|
||||
export interface AiMessage {
|
||||
@@ -85,10 +86,18 @@ export async function agentRuntimeUpdateSnapshot(snapshot: AgentRuntimeSnapshot)
|
||||
return invoke("agent_runtime_update_snapshot", { snapshot });
|
||||
}
|
||||
|
||||
export async function agentRuntimeLoadHandoffs(): Promise<unknown[]> {
|
||||
export async function agentRuntimeLoadHandoffs(): Promise<AgentHandoffItem[]> {
|
||||
return invoke("agent_runtime_load_handoffs");
|
||||
}
|
||||
|
||||
export async function agentRuntimeMarkHandoffShown(id: string): Promise<boolean> {
|
||||
return invoke("agent_runtime_mark_handoff_shown", { id });
|
||||
}
|
||||
|
||||
export async function agentRuntimeRejectHandoff(id: string): Promise<boolean> {
|
||||
return invoke("agent_runtime_reject_handoff", { id });
|
||||
}
|
||||
|
||||
// --- AI Conversations ---
|
||||
|
||||
export interface AiChatMessage {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import * as api from "@/lib/api";
|
||||
import {
|
||||
deriveHandoffDialogState,
|
||||
mergeLoadedHandoffs,
|
||||
updateHandoffStatus,
|
||||
type AgentHandoffItem,
|
||||
} from "@/lib/agentHandoff";
|
||||
import { buildAgentRuntimeSnapshot, DEFAULT_RESULT_SAMPLE_LIMIT } from "@/lib/agentRuntimeSnapshot";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
@@ -8,8 +14,13 @@ import { useQueryStore } from "@/stores/queryStore";
|
||||
export const useAgentRuntimeStore = defineStore("agentRuntime", () => {
|
||||
const selection = ref<unknown>({ type: "none" });
|
||||
const selectedSql = ref("");
|
||||
const handoffs = ref<AgentHandoffItem[]>([]);
|
||||
const activeHandoffId = ref<string | null>(null);
|
||||
const handoffDialogOpen = ref(false);
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const activeHandoff = computed(() => handoffs.value.find((item) => item.id === activeHandoffId.value) ?? null);
|
||||
|
||||
function setSelection(value: unknown) {
|
||||
selection.value = value;
|
||||
scheduleSync();
|
||||
@@ -47,5 +58,49 @@ export const useAgentRuntimeStore = defineStore("agentRuntime", () => {
|
||||
}
|
||||
}
|
||||
|
||||
return { selection, selectedSql, setSelection, setSelectedSql, scheduleSync, syncNow };
|
||||
async function loadHandoffs() {
|
||||
const loaded = mergeLoadedHandoffs(await api.agentRuntimeLoadHandoffs());
|
||||
handoffs.value = loaded;
|
||||
const state = deriveHandoffDialogState(loaded, activeHandoffId.value);
|
||||
activeHandoffId.value = state.active?.id ?? null;
|
||||
handoffDialogOpen.value = state.open;
|
||||
if (state.active?.status === "queued") {
|
||||
await markHandoffShown(state.active.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function markHandoffShown(id: string) {
|
||||
handoffs.value = updateHandoffStatus(handoffs.value, id, "shown");
|
||||
await api.agentRuntimeMarkHandoffShown(id);
|
||||
}
|
||||
|
||||
async function rejectHandoff(id: string) {
|
||||
await api.agentRuntimeRejectHandoff(id);
|
||||
handoffs.value = updateHandoffStatus(handoffs.value, id, "rejected");
|
||||
const state = deriveHandoffDialogState(handoffs.value, activeHandoffId.value === id ? null : activeHandoffId.value);
|
||||
activeHandoffId.value = state.active?.id ?? null;
|
||||
handoffDialogOpen.value = state.open;
|
||||
}
|
||||
|
||||
function setActiveHandoff(id: string) {
|
||||
activeHandoffId.value = id;
|
||||
const item = handoffs.value.find((handoff) => handoff.id === id);
|
||||
if (item?.status === "queued") void markHandoffShown(id);
|
||||
}
|
||||
|
||||
return {
|
||||
selection,
|
||||
selectedSql,
|
||||
handoffs,
|
||||
activeHandoff,
|
||||
handoffDialogOpen,
|
||||
setSelection,
|
||||
setSelectedSql,
|
||||
scheduleSync,
|
||||
syncNow,
|
||||
loadHandoffs,
|
||||
markHandoffShown,
|
||||
rejectHandoff,
|
||||
setActiveHandoff,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user