feat(v2): add agent tree, status bar, notifications, settings dialog (Phase 5)

Agent tree visualization (SVG) with horizontal layout and bezier edges.
Global status bar with pane counts, active agents pulse, token/cost totals.
Toast notification system with auto-dismiss and agent dispatcher integration.
Settings dialog with SQLite persistence for shell, cwd, and max panes.
Keyboard shortcuts: Ctrl+W close pane, Ctrl+, open settings.
This commit is contained in:
Hibryda 2026-03-06 13:46:21 +01:00
parent cd1271adf0
commit be24d07c65
13 changed files with 809 additions and 2 deletions

View file

@ -132,6 +132,23 @@ fn layout_load(state: State<'_, AppState>) -> Result<LayoutState, String> {
state.session_db.load_layout()
}
// --- Settings commands ---
#[tauri::command]
fn settings_get(state: State<'_, AppState>, key: String) -> Result<Option<String>, String> {
state.session_db.get_setting(&key)
}
#[tauri::command]
fn settings_set(state: State<'_, AppState>, key: String, value: String) -> Result<(), String> {
state.session_db.set_setting(&key, &value)
}
#[tauri::command]
fn settings_list(state: State<'_, AppState>) -> Result<Vec<(String, String)>, String> {
state.session_db.get_all_settings()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let pty_manager = Arc::new(PtyManager::new());
@ -175,6 +192,9 @@ pub fn run() {
session_touch,
layout_save,
layout_load,
settings_get,
settings_set,
settings_list,
])
.setup(move |app| {
if cfg!(debug_assertions) {

View file

@ -68,11 +68,51 @@ impl SessionDb {
);
INSERT OR IGNORE INTO layout_state (id, preset, pane_ids) VALUES (1, '1-col', '[]');
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
"
).map_err(|e| format!("Migration failed: {e}"))?;
Ok(())
}
pub fn get_setting(&self, key: &str) -> Result<Option<String>, String> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn
.prepare("SELECT value FROM settings WHERE key = ?1")
.map_err(|e| format!("Settings query failed: {e}"))?;
let result = stmt.query_row(params![key], |row| row.get(0));
match result {
Ok(val) => Ok(Some(val)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Settings read failed: {e}")),
}
}
pub fn set_setting(&self, key: &str, value: &str) -> Result<(), String> {
let conn = self.conn.lock().unwrap();
conn.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
params![key, value],
).map_err(|e| format!("Settings write failed: {e}"))?;
Ok(())
}
pub fn get_all_settings(&self) -> Result<Vec<(String, String)>, String> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn
.prepare("SELECT key, value FROM settings ORDER BY key")
.map_err(|e| format!("Settings query failed: {e}"))?;
let settings = stmt
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.map_err(|e| format!("Settings query failed: {e}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Settings read failed: {e}"))?;
Ok(settings)
}
pub fn list_sessions(&self) -> Result<Vec<Session>, String> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn