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:
2026-05-06 19:30:44 +03:00
parent 652937f7f5
commit 4f7afc17f4
89 changed files with 4151 additions and 10756 deletions

View 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&#10;&#10;## (timestamp)&#10;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>
);
}