feat: rescope to AI-first DB harness with multi-DB chat agent
Removes enterprise/DBA features and replaces the marginal AI bar with a central chat agent that has progressive-discovery tools, cross-session memory, saved-query reuse, and inline result actions. Adds ClickHouse support alongside PostgreSQL/Greenplum. Cleanup - Drop ~10k LOC of advanced features: Docker, Snapshots, Validation, Index Advisor, Role/User Management, Data Generator, ERD, Lookup. - Trim deps: drop @xyflow/react, dagre, @types/dagre; cut tokio features to rt-multi-thread/sync/time/net/macros. - Remove unused TuskError variants and dead helpers (topological_sort, invalidate_schema_cache). Multi-DB (PostgreSQL + ClickHouse) - New src-tauri/src/db/ module: ChClient (HTTP-based, reuses reqwest), sql_guard (cross-flavor read-only whitelist with 8 tests). - ConnectionConfig gains db_flavor and secure fields with serde defaults for backwards-compatible connections.json. - All connection/query/schema/data commands dispatch by flavor; CH covers connect, execute_query, list_databases/schemas/tables/views/ columns/completion_schema, paginated table fetch. - Frontend: dbCapabilities matrix, ConnectionDialog engine selector with port auto-swap and HTTPS toggle, SqlEditor switches to StandardSQL dialect for CH, TableDataView surfaces CH connections as read-only. AI-first chat agent - New src/components/chat/ panel with composer, message rendering, collapsible tool-call/result blocks, top-level ErrorBoundary. - Backend agent loop in commands/chat.rs with strict-JSON tool protocol. Nine tools: list_databases, list_tables, get_columns, switch_database, run_query, remember, save_query, find_queries, final. Forgiving parser accepts both flat and nested-input shapes. - Compressed history: only the last 4 run_query results carry sample rows (≤10, cells truncated to 200 chars) into LLM context; older results marked omitted. - System prompt uses lite OVERVIEW (DB list + active-DB tables only) instead of full DDL — schema details are loaded on demand via get_columns. CH OVERVIEW shows cross-DB tables since CH allows db.table queries. Cross-session memory (F1) - Per-connection markdown file at app_data_dir/memory/<connection_id>.md, 16KB cap with oldest-block eviction. Agent appends via remember() tool; the file is injected into LEARNED NOTES section of every system prompt. - New Memory sidebar tab with editable textarea, badge for note count, empty-state with template. Edits picked up on the next agent turn. Saved-query reuse (F2) - Tools save_query and find_queries scoped to current connection. save_query attaches a UUID + timestamp; find_queries returns top 10 matches with SQL preview ≤500 chars. - Storage shared with the sidebar Saved panel. Inline result actions (F3) - run_query result block in chat gets Open-full (90vw × 80vh modal with full ResultsTable, no row cap) and Export (reuses ExportDialog for CSV/JSON via existing exportCsv/exportJson commands). Verification - cargo check clean, zero warnings. - cargo test --lib: 50 pass (20 chat parser + 4 memory + 8 sql_guard + 6 clean_sql + 12 escape_ident). - npx tsc --noEmit clean. - npx vitest run: 20 pass.
This commit is contained in:
140
src/components/memory/MemoryPanel.tsx
Normal file
140
src/components/memory/MemoryPanel.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Brain, RefreshCw, Save } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAppStore } from "@/stores/app-store";
|
||||
import { useConnections } from "@/hooks/use-connections";
|
||||
import { useMemory, useSaveMemory } from "@/hooks/use-memory";
|
||||
|
||||
export function MemoryPanel() {
|
||||
const activeConnectionId = useAppStore((s) => s.activeConnectionId);
|
||||
const { data: connections } = useConnections();
|
||||
const activeConn = connections?.find((c) => c.id === activeConnectionId);
|
||||
|
||||
const { data: serverContent, isFetching, refetch } = useMemory(activeConnectionId);
|
||||
const saveMutation = useSaveMemory();
|
||||
|
||||
const [draft, setDraft] = useState<string>("");
|
||||
const [dirty, setDirty] = useState(false);
|
||||
|
||||
// Sync local textarea when the active connection changes or server reloads.
|
||||
// Only overwrite if the user hasn't edited.
|
||||
useEffect(() => {
|
||||
if (!dirty) {
|
||||
setDraft(serverContent ?? "");
|
||||
}
|
||||
}, [serverContent, activeConnectionId, dirty]);
|
||||
|
||||
if (!activeConnectionId) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2 p-6 text-center">
|
||||
<Brain className="h-8 w-8 text-muted-foreground/20" />
|
||||
<p className="text-sm text-muted-foreground/60">
|
||||
Connect to a database to view its memory.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const noteCount = (draft.match(/^## /gm) ?? []).length;
|
||||
const isEmpty = draft.trim().length === 0;
|
||||
|
||||
const handleSave = () => {
|
||||
saveMutation.mutate(
|
||||
{ connectionId: activeConnectionId, content: draft },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setDirty(false);
|
||||
toast.success("Memory saved");
|
||||
},
|
||||
onError: (err) => toast.error("Save failed", { description: String(err) }),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleReload = () => {
|
||||
setDirty(false);
|
||||
refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between gap-1 border-b border-border/40 px-3 py-2">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Brain className="h-3.5 w-3.5 text-primary/70" />
|
||||
<span className="font-medium">Memory</span>
|
||||
{activeConn && (
|
||||
<span className="ml-1 truncate text-muted-foreground/60">· {activeConn.name}</span>
|
||||
)}
|
||||
{!isEmpty && (
|
||||
<span className="ml-1 text-[10px] text-muted-foreground/50">
|
||||
{noteCount} note{noteCount === 1 ? "" : "s"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
onClick={handleReload}
|
||||
disabled={isFetching}
|
||||
title="Reload from disk"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<RefreshCw className={`h-3.5 w-3.5 ${isFetching ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant={dirty ? "default" : "ghost"}
|
||||
onClick={handleSave}
|
||||
disabled={!dirty || saveMutation.isPending}
|
||||
title="Save"
|
||||
className="gap-1"
|
||||
>
|
||||
<Save className="h-3 w-3" />
|
||||
<span className="text-xs">Save</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEmpty ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 py-8 text-center">
|
||||
<Brain className="h-7 w-7 text-muted-foreground/30" />
|
||||
<p className="text-xs text-muted-foreground/70">
|
||||
No notes yet for this connection.
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground/50 max-w-[260px]">
|
||||
The agent will populate this as it learns about your database. You can also
|
||||
edit notes here directly — anything you type is loaded into the agent's
|
||||
context on its next turn.
|
||||
</p>
|
||||
<textarea
|
||||
className="mt-2 h-48 w-full max-w-[360px] resize-none rounded-md border border-border/40 bg-background/50 p-2 font-mono text-[11px] outline-none focus:border-primary/40"
|
||||
placeholder="# Memory ## (timestamp) your note..."
|
||||
value={draft}
|
||||
onChange={(e) => {
|
||||
setDraft(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<textarea
|
||||
className="min-h-0 flex-1 resize-none overflow-y-auto bg-background/40 p-3 font-mono text-[11px] leading-relaxed outline-none"
|
||||
spellCheck={false}
|
||||
value={draft}
|
||||
onChange={(e) => {
|
||||
setDraft(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dirty && (
|
||||
<div className="border-t border-border/40 px-3 py-1.5 text-[10px] text-amber-500/80">
|
||||
Unsaved changes
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user