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.
161 lines
5.0 KiB
TypeScript
161 lines
5.0 KiB
TypeScript
import { useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ConnectionSelector } from "@/components/connections/ConnectionSelector";
|
|
import { ConnectionList } from "@/components/connections/ConnectionList";
|
|
import { ConnectionDialog } from "@/components/connections/ConnectionDialog";
|
|
import { ReadOnlyToggle } from "@/components/layout/ReadOnlyToggle";
|
|
import { useAppStore } from "@/stores/app-store";
|
|
import { useConnections, useReconnect } from "@/hooks/use-connections";
|
|
import { toast } from "sonner";
|
|
import { Database, Plus, RefreshCw, Settings, Sparkles } from "lucide-react";
|
|
import type { ConnectionConfig, Tab } from "@/types";
|
|
import { getEnvironment } from "@/lib/environment";
|
|
import { AppSettingsSheet } from "@/components/settings/AppSettingsSheet";
|
|
|
|
export function Toolbar() {
|
|
const [listOpen, setListOpen] = useState(false);
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
const [editingConn, setEditingConn] = useState<ConnectionConfig | null>(null);
|
|
const { activeConnectionId, currentDatabase, addTab } = useAppStore();
|
|
const { data: connections } = useConnections();
|
|
const reconnectMutation = useReconnect();
|
|
const activeConn = connections?.find((c) => c.id === activeConnectionId);
|
|
const activeEnv = getEnvironment(activeConn?.environment);
|
|
const activeColor = activeEnv?.color ?? activeConn?.color;
|
|
|
|
const handleReconnect = () => {
|
|
if (!activeConn) return;
|
|
reconnectMutation.mutate(activeConn, {
|
|
onSuccess: () => toast.success("Reconnected"),
|
|
onError: (err) => toast.error("Reconnect failed", { description: String(err) }),
|
|
});
|
|
};
|
|
|
|
const handleNewQuery = () => {
|
|
if (!activeConnectionId) return;
|
|
const tab: Tab = {
|
|
id: crypto.randomUUID(),
|
|
type: "query",
|
|
title: "New Query",
|
|
connectionId: activeConnectionId,
|
|
database: currentDatabase ?? undefined,
|
|
sql: "",
|
|
};
|
|
addTab(tab);
|
|
};
|
|
|
|
const handleNewChat = () => {
|
|
if (!activeConnectionId) return;
|
|
const tab: Tab = {
|
|
id: crypto.randomUUID(),
|
|
type: "chat",
|
|
title: "Chat",
|
|
connectionId: activeConnectionId,
|
|
database: currentDatabase ?? undefined,
|
|
};
|
|
addTab(tab);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className="tusk-toolbar tusk-conn-strip flex h-10 items-center gap-1.5 px-3"
|
|
style={{
|
|
"--strip-width": activeColor ? "3px" : "0px",
|
|
"--strip-color": activeColor ?? "transparent",
|
|
} as React.CSSProperties}
|
|
>
|
|
<Button
|
|
variant="ghost"
|
|
size="xs"
|
|
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
|
onClick={() => setListOpen(true)}
|
|
>
|
|
<Database className="h-3.5 w-3.5" />
|
|
<span className="text-xs font-medium">Connections</span>
|
|
</Button>
|
|
|
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
|
|
<ConnectionSelector />
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-xs"
|
|
onClick={handleReconnect}
|
|
disabled={!activeConnectionId || reconnectMutation.isPending}
|
|
title="Reconnect"
|
|
className="text-muted-foreground hover:text-foreground"
|
|
>
|
|
<RefreshCw className={`h-3.5 w-3.5 ${reconnectMutation.isPending ? "animate-spin" : ""}`} />
|
|
</Button>
|
|
|
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
|
|
<ReadOnlyToggle />
|
|
|
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="xs"
|
|
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
|
onClick={handleNewChat}
|
|
disabled={!activeConnectionId}
|
|
>
|
|
<Sparkles className="h-3.5 w-3.5" />
|
|
<span className="text-xs font-medium">Ask AI</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="xs"
|
|
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
|
onClick={handleNewQuery}
|
|
disabled={!activeConnectionId}
|
|
>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
<span className="text-xs font-medium">New Query</span>
|
|
</Button>
|
|
|
|
<div className="flex-1" />
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-xs"
|
|
onClick={() => setSettingsOpen(true)}
|
|
title="Settings"
|
|
className="text-muted-foreground hover:text-foreground"
|
|
>
|
|
<Settings className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
|
|
<ConnectionList
|
|
open={listOpen}
|
|
onOpenChange={setListOpen}
|
|
onEdit={(conn) => {
|
|
setEditingConn(conn);
|
|
setDialogOpen(true);
|
|
}}
|
|
onNew={() => {
|
|
setEditingConn(null);
|
|
setDialogOpen(true);
|
|
}}
|
|
/>
|
|
|
|
<ConnectionDialog
|
|
open={dialogOpen}
|
|
onOpenChange={setDialogOpen}
|
|
connection={editingConn}
|
|
/>
|
|
|
|
<AppSettingsSheet
|
|
open={settingsOpen}
|
|
onOpenChange={setSettingsOpen}
|
|
/>
|
|
</>
|
|
);
|
|
}
|