feat(v2): add SSH management, ctx integration, themes, detached mode, auto-updater
SSH session management: - SshSession struct + ssh_sessions SQLite table in session.rs - CRUD Tauri commands (ssh_session_list/save/delete) in lib.rs - SshDialog.svelte (create/edit modal), SshSessionList.svelte (sidebar) - SSH pane routes to TerminalPane with shell=/usr/bin/ssh + args ctx context database integration: - ctx.rs: read-only CtxDb (SQLITE_OPEN_READ_ONLY for ~/.claude-context/context.db) - 5 Tauri commands (ctx_list_projects/get_context/get_shared/get_summaries/search) - ContextPane.svelte with project selector, tabs, search - ctx-bridge.ts adapter Catppuccin theme flavors (Latte/Frappe/Macchiato/Mocha): - themes.ts: all 4 palette definitions + buildXtermTheme/applyCssVariables - theme.svelte.ts: reactive store with SQLite persistence - SettingsDialog flavor dropdown, TerminalPane theme-aware Detached pane mode (pop-out windows): - detach.ts: isDetachedMode/getDetachedConfig from URL params - App.svelte: conditional rendering of single pane without chrome Other additions: - Shiki syntax highlighting (highlight.ts, lazy singleton, 13 languages) - Tauri auto-updater plugin (tauri-plugin-updater + updater.ts) - AgentPane markdown rendering with Shiki code highlighting - New deps: shiki, @tauri-apps/plugin-updater, tauri-plugin-updater
This commit is contained in:
parent
4f2614186d
commit
4db7ccff60
28 changed files with 2992 additions and 51 deletions
172
v2/src-tauri/src/ctx.rs
Normal file
172
v2/src-tauri/src/ctx.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// ctx — Read-only access to the Claude Code context manager database
|
||||
// Database: ~/.claude-context/context.db (managed by ctx CLI tool)
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::Serialize;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CtxProject {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub work_dir: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CtxEntry {
|
||||
pub project: String,
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CtxSummary {
|
||||
pub project: String,
|
||||
pub summary: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
pub struct CtxDb {
|
||||
conn: Mutex<Option<Connection>>,
|
||||
}
|
||||
|
||||
impl CtxDb {
|
||||
pub fn new() -> Self {
|
||||
let db_path = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".claude-context")
|
||||
.join("context.db");
|
||||
|
||||
let conn = if db_path.exists() {
|
||||
Connection::open_with_flags(
|
||||
&db_path,
|
||||
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self { conn: Mutex::new(conn) }
|
||||
}
|
||||
|
||||
pub fn list_projects(&self) -> Result<Vec<CtxProject>, String> {
|
||||
let lock = self.conn.lock().unwrap();
|
||||
let conn = lock.as_ref().ok_or("ctx database not found")?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT name, description, work_dir, created_at FROM sessions ORDER BY name")
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?;
|
||||
|
||||
let projects = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(CtxProject {
|
||||
name: row.get(0)?,
|
||||
description: row.get(1)?,
|
||||
work_dir: row.get(2)?,
|
||||
created_at: row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("ctx row read failed: {e}"))?;
|
||||
|
||||
Ok(projects)
|
||||
}
|
||||
|
||||
pub fn get_context(&self, project: &str) -> Result<Vec<CtxEntry>, String> {
|
||||
let lock = self.conn.lock().unwrap();
|
||||
let conn = lock.as_ref().ok_or("ctx database not found")?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT project, key, value, updated_at FROM contexts WHERE project = ?1 ORDER BY key")
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?;
|
||||
|
||||
let entries = stmt
|
||||
.query_map(params![project], |row| {
|
||||
Ok(CtxEntry {
|
||||
project: row.get(0)?,
|
||||
key: row.get(1)?,
|
||||
value: row.get(2)?,
|
||||
updated_at: row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("ctx row read failed: {e}"))?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn get_shared(&self) -> Result<Vec<CtxEntry>, String> {
|
||||
let lock = self.conn.lock().unwrap();
|
||||
let conn = lock.as_ref().ok_or("ctx database not found")?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT key, value, updated_at FROM shared ORDER BY key")
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?;
|
||||
|
||||
let entries = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(CtxEntry {
|
||||
project: "shared".to_string(),
|
||||
key: row.get(0)?,
|
||||
value: row.get(1)?,
|
||||
updated_at: row.get(2)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("ctx row read failed: {e}"))?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn get_summaries(&self, project: &str, limit: i64) -> Result<Vec<CtxSummary>, String> {
|
||||
let lock = self.conn.lock().unwrap();
|
||||
let conn = lock.as_ref().ok_or("ctx database not found")?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT project, summary, created_at FROM summaries WHERE project = ?1 ORDER BY created_at DESC LIMIT ?2")
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?;
|
||||
|
||||
let summaries = stmt
|
||||
.query_map(params![project, limit], |row| {
|
||||
Ok(CtxSummary {
|
||||
project: row.get(0)?,
|
||||
summary: row.get(1)?,
|
||||
created_at: row.get(2)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("ctx query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("ctx row read failed: {e}"))?;
|
||||
|
||||
Ok(summaries)
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str) -> Result<Vec<CtxEntry>, String> {
|
||||
let lock = self.conn.lock().unwrap();
|
||||
let conn = lock.as_ref().ok_or("ctx database not found")?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT project, key, value FROM contexts_fts WHERE contexts_fts MATCH ?1 LIMIT 50")
|
||||
.map_err(|e| format!("ctx search failed: {e}"))?;
|
||||
|
||||
let entries = stmt
|
||||
.query_map(params![query], |row| {
|
||||
Ok(CtxEntry {
|
||||
project: row.get(0)?,
|
||||
key: row.get(1)?,
|
||||
value: row.get(2)?,
|
||||
updated_at: String::new(),
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("ctx search failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("ctx row read failed: {e}"))?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
mod ctx;
|
||||
mod pty;
|
||||
mod sidecar;
|
||||
mod watcher;
|
||||
mod session;
|
||||
|
||||
use ctx::CtxDb;
|
||||
use pty::{PtyManager, PtyOptions};
|
||||
use session::{Session, SessionDb, LayoutState};
|
||||
use session::{Session, SessionDb, LayoutState, SshSession};
|
||||
use sidecar::{AgentQueryOptions, SidecarManager};
|
||||
use watcher::FileWatcherManager;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -15,6 +17,7 @@ struct AppState {
|
|||
sidecar_manager: Arc<SidecarManager>,
|
||||
session_db: Arc<SessionDb>,
|
||||
file_watcher: Arc<FileWatcherManager>,
|
||||
ctx_db: Arc<CtxDb>,
|
||||
}
|
||||
|
||||
// --- PTY commands ---
|
||||
|
|
@ -149,6 +152,50 @@ fn settings_list(state: State<'_, AppState>) -> Result<Vec<(String, String)>, St
|
|||
state.session_db.get_all_settings()
|
||||
}
|
||||
|
||||
// --- SSH session commands ---
|
||||
|
||||
#[tauri::command]
|
||||
fn ssh_session_list(state: State<'_, AppState>) -> Result<Vec<SshSession>, String> {
|
||||
state.session_db.list_ssh_sessions()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ssh_session_save(state: State<'_, AppState>, session: SshSession) -> Result<(), String> {
|
||||
state.session_db.save_ssh_session(&session)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ssh_session_delete(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
||||
state.session_db.delete_ssh_session(&id)
|
||||
}
|
||||
|
||||
// --- ctx commands ---
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_list_projects(state: State<'_, AppState>) -> Result<Vec<ctx::CtxProject>, String> {
|
||||
state.ctx_db.list_projects()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_get_context(state: State<'_, AppState>, project: String) -> Result<Vec<ctx::CtxEntry>, String> {
|
||||
state.ctx_db.get_context(&project)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_get_shared(state: State<'_, AppState>) -> Result<Vec<ctx::CtxEntry>, String> {
|
||||
state.ctx_db.get_shared()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_get_summaries(state: State<'_, AppState>, project: String, limit: i64) -> Result<Vec<ctx::CtxSummary>, String> {
|
||||
state.ctx_db.get_summaries(&project, limit)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_search(state: State<'_, AppState>, query: String) -> Result<Vec<ctx::CtxEntry>, String> {
|
||||
state.ctx_db.search(&query)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let pty_manager = Arc::new(PtyManager::new());
|
||||
|
|
@ -163,12 +210,14 @@ pub fn run() {
|
|||
);
|
||||
|
||||
let file_watcher = Arc::new(FileWatcherManager::new());
|
||||
let ctx_db = Arc::new(CtxDb::new());
|
||||
|
||||
let app_state = AppState {
|
||||
pty_manager,
|
||||
sidecar_manager: sidecar_manager.clone(),
|
||||
session_db,
|
||||
file_watcher,
|
||||
ctx_db,
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
|
|
@ -195,7 +244,16 @@ pub fn run() {
|
|||
settings_get,
|
||||
settings_set,
|
||||
settings_list,
|
||||
ssh_session_list,
|
||||
ssh_session_save,
|
||||
ssh_session_delete,
|
||||
ctx_list_projects,
|
||||
ctx_get_context,
|
||||
ctx_get_shared,
|
||||
ctx_get_summaries,
|
||||
ctx_search,
|
||||
])
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.setup(move |app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,20 @@ use serde::{Deserialize, Serialize};
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SshSession {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub host: String,
|
||||
pub port: i32,
|
||||
pub username: String,
|
||||
pub key_file: String,
|
||||
pub folder: String,
|
||||
pub color: String,
|
||||
pub created_at: i64,
|
||||
pub last_used_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
pub id: String,
|
||||
|
|
@ -73,6 +87,19 @@ impl SessionDb {
|
|||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER NOT NULL DEFAULT 22,
|
||||
username TEXT NOT NULL,
|
||||
key_file TEXT DEFAULT '',
|
||||
folder TEXT DEFAULT '',
|
||||
color TEXT DEFAULT '#89b4fa',
|
||||
created_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER NOT NULL
|
||||
);
|
||||
"
|
||||
).map_err(|e| format!("Migration failed: {e}"))?;
|
||||
Ok(())
|
||||
|
|
@ -212,4 +239,80 @@ impl SessionDb {
|
|||
Ok(LayoutState { preset, pane_ids })
|
||||
}).map_err(|e| format!("Layout read failed: {e}"))
|
||||
}
|
||||
|
||||
// --- SSH session methods ---
|
||||
|
||||
pub fn list_ssh_sessions(&self) -> Result<Vec<SshSession>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, name, host, port, username, key_file, folder, color, created_at, last_used_at FROM ssh_sessions ORDER BY last_used_at DESC")
|
||||
.map_err(|e| format!("SSH query prepare failed: {e}"))?;
|
||||
|
||||
let sessions = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(SshSession {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
host: row.get(2)?,
|
||||
port: row.get(3)?,
|
||||
username: row.get(4)?,
|
||||
key_file: row.get(5)?,
|
||||
folder: row.get(6)?,
|
||||
color: row.get(7)?,
|
||||
created_at: row.get(8)?,
|
||||
last_used_at: row.get(9)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("SSH query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("SSH row read failed: {e}"))?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
pub fn save_ssh_session(&self, session: &SshSession) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO ssh_sessions (id, name, host, port, username, key_file, folder, color, created_at, last_used_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||
params![
|
||||
session.id,
|
||||
session.name,
|
||||
session.host,
|
||||
session.port,
|
||||
session.username,
|
||||
session.key_file,
|
||||
session.folder,
|
||||
session.color,
|
||||
session.created_at,
|
||||
session.last_used_at,
|
||||
],
|
||||
).map_err(|e| format!("SSH insert failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_ssh_session(&self, id: &str) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM ssh_sessions WHERE id = ?1", params![id])
|
||||
.map_err(|e| format!("SSH delete failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_ssh_session(&self, session: &SshSession) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE ssh_sessions SET name = ?1, host = ?2, port = ?3, username = ?4, key_file = ?5, folder = ?6, color = ?7, last_used_at = ?8 WHERE id = ?9",
|
||||
params![
|
||||
session.name,
|
||||
session.host,
|
||||
session.port,
|
||||
session.username,
|
||||
session.key_file,
|
||||
session.folder,
|
||||
session.color,
|
||||
session.last_used_at,
|
||||
session.id,
|
||||
],
|
||||
).map_err(|e| format!("SSH update failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue