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.
108 lines
3.2 KiB
Rust
108 lines
3.2 KiB
Rust
use crate::error::{TuskError, TuskResult};
|
|
use crate::mcp;
|
|
use crate::models::settings::{AppSettings, McpStatus};
|
|
use crate::state::AppState;
|
|
use std::fs;
|
|
use std::sync::Arc;
|
|
use tauri::{AppHandle, Manager, State};
|
|
|
|
fn get_settings_path(app: &AppHandle) -> TuskResult<std::path::PathBuf> {
|
|
let dir = app
|
|
.path()
|
|
.app_data_dir()
|
|
.map_err(|e| TuskError::Config(e.to_string()))?;
|
|
fs::create_dir_all(&dir)?;
|
|
Ok(dir.join("app_settings.json"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn get_app_settings(app: AppHandle) -> TuskResult<AppSettings> {
|
|
let path = get_settings_path(&app)?;
|
|
if !path.exists() {
|
|
return Ok(AppSettings::default());
|
|
}
|
|
let data = fs::read_to_string(&path)?;
|
|
let settings: AppSettings = serde_json::from_str(&data)?;
|
|
Ok(settings)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn save_app_settings(
|
|
app: AppHandle,
|
|
state: State<'_, Arc<AppState>>,
|
|
settings: AppSettings,
|
|
) -> TuskResult<()> {
|
|
let path = get_settings_path(&app)?;
|
|
let data = serde_json::to_string_pretty(&settings)?;
|
|
fs::write(&path, data)?;
|
|
|
|
// Apply MCP setting: restart or stop
|
|
let is_running = *state.mcp_running.read().await;
|
|
|
|
if settings.mcp.enabled {
|
|
if is_running {
|
|
// Stop existing MCP server first
|
|
let _ = state.mcp_shutdown_tx.send(true);
|
|
// Give it a moment to shut down
|
|
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
|
*state.mcp_running.write().await = false;
|
|
}
|
|
|
|
// Start new MCP server
|
|
let connections_path = app
|
|
.path()
|
|
.app_data_dir()
|
|
.map_err(|e| TuskError::Config(e.to_string()))?
|
|
.join("connections.json");
|
|
|
|
let mcp_state = state.inner().clone();
|
|
let port = settings.mcp.port;
|
|
let shutdown_rx = state.mcp_shutdown_tx.subscribe();
|
|
|
|
tokio::spawn(async move {
|
|
*mcp_state.mcp_running.write().await = true;
|
|
if let Err(e) =
|
|
mcp::start_mcp_server(mcp_state.clone(), connections_path, port, shutdown_rx).await
|
|
{
|
|
log::error!("MCP server error: {}", e);
|
|
}
|
|
*mcp_state.mcp_running.write().await = false;
|
|
});
|
|
} else if is_running {
|
|
// Stop MCP server
|
|
let _ = state.mcp_shutdown_tx.send(true);
|
|
*state.mcp_running.write().await = false;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn get_mcp_status(app: AppHandle) -> TuskResult<McpStatus> {
|
|
// Read settings from file for enabled/port
|
|
let settings = {
|
|
let path = get_settings_path(&app)?;
|
|
if path.exists() {
|
|
let data = fs::read_to_string(&path)?;
|
|
serde_json::from_str::<AppSettings>(&data).unwrap_or_default()
|
|
} else {
|
|
AppSettings::default()
|
|
}
|
|
};
|
|
|
|
// Probe the actual port to determine if MCP is running
|
|
let running = tokio::time::timeout(
|
|
std::time::Duration::from_millis(500),
|
|
tokio::net::TcpStream::connect(format!("127.0.0.1:{}", settings.mcp.port)),
|
|
)
|
|
.await
|
|
.map(|r| r.is_ok())
|
|
.unwrap_or(false);
|
|
|
|
Ok(McpStatus {
|
|
enabled: settings.mcp.enabled,
|
|
port: settings.mcp.port,
|
|
running,
|
|
})
|
|
}
|