feat(health): add project health store, Mission Control bar, and session metrics
This commit is contained in:
parent
072316d63f
commit
42094eac2a
11 changed files with 773 additions and 16 deletions
|
|
@ -448,6 +448,25 @@ fn project_agent_state_load(
|
|||
state.session_db.load_project_agent_state(&project_id)
|
||||
}
|
||||
|
||||
// --- Session metrics commands ---
|
||||
|
||||
#[tauri::command]
|
||||
fn session_metric_save(
|
||||
state: State<'_, AppState>,
|
||||
metric: session::SessionMetric,
|
||||
) -> Result<(), String> {
|
||||
state.session_db.save_session_metric(&metric)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_metrics_load(
|
||||
state: State<'_, AppState>,
|
||||
project_id: String,
|
||||
limit: i64,
|
||||
) -> Result<Vec<session::SessionMetric>, String> {
|
||||
state.session_db.load_session_metrics(&project_id, limit)
|
||||
}
|
||||
|
||||
// --- File browser commands (Files tab) ---
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
|
|
@ -761,6 +780,8 @@ pub fn run() {
|
|||
agent_messages_load,
|
||||
project_agent_state_save,
|
||||
project_agent_state_load,
|
||||
session_metric_save,
|
||||
session_metrics_load,
|
||||
cli_get_group,
|
||||
pick_directory,
|
||||
open_url,
|
||||
|
|
|
|||
|
|
@ -154,7 +154,24 @@ impl SessionDb {
|
|||
output_tokens INTEGER DEFAULT 0,
|
||||
last_prompt TEXT,
|
||||
updated_at INTEGER NOT NULL
|
||||
);"
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_metrics (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
start_time INTEGER NOT NULL,
|
||||
end_time INTEGER NOT NULL,
|
||||
peak_tokens INTEGER DEFAULT 0,
|
||||
turn_count INTEGER DEFAULT 0,
|
||||
tool_call_count INTEGER DEFAULT 0,
|
||||
cost_usd REAL DEFAULT 0,
|
||||
model TEXT,
|
||||
status TEXT NOT NULL,
|
||||
error_message TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_metrics_project
|
||||
ON session_metrics(project_id);"
|
||||
).map_err(|e| format!("Migration (v3 tables) failed: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
|
|
@ -507,6 +524,81 @@ pub struct ProjectAgentState {
|
|||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionMetric {
|
||||
#[serde(default)]
|
||||
pub id: i64,
|
||||
pub project_id: String,
|
||||
pub session_id: String,
|
||||
pub start_time: i64,
|
||||
pub end_time: i64,
|
||||
pub peak_tokens: i64,
|
||||
pub turn_count: i64,
|
||||
pub tool_call_count: i64,
|
||||
pub cost_usd: f64,
|
||||
pub model: Option<String>,
|
||||
pub status: String,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl SessionDb {
|
||||
pub fn save_session_metric(&self, metric: &SessionMetric) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO session_metrics (project_id, session_id, start_time, end_time, peak_tokens, turn_count, tool_call_count, cost_usd, model, status, error_message) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
|
||||
params![
|
||||
metric.project_id,
|
||||
metric.session_id,
|
||||
metric.start_time,
|
||||
metric.end_time,
|
||||
metric.peak_tokens,
|
||||
metric.turn_count,
|
||||
metric.tool_call_count,
|
||||
metric.cost_usd,
|
||||
metric.model,
|
||||
metric.status,
|
||||
metric.error_message,
|
||||
],
|
||||
).map_err(|e| format!("Save session metric failed: {e}"))?;
|
||||
|
||||
// Enforce retention: keep last 100 per project
|
||||
conn.execute(
|
||||
"DELETE FROM session_metrics WHERE project_id = ?1 AND id NOT IN (SELECT id FROM session_metrics WHERE project_id = ?1 ORDER BY end_time DESC LIMIT 100)",
|
||||
params![metric.project_id],
|
||||
).map_err(|e| format!("Prune session metrics failed: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_session_metrics(&self, project_id: &str, limit: i64) -> Result<Vec<SessionMetric>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, project_id, session_id, start_time, end_time, peak_tokens, turn_count, tool_call_count, cost_usd, model, status, error_message FROM session_metrics WHERE project_id = ?1 ORDER BY end_time DESC LIMIT ?2"
|
||||
).map_err(|e| format!("Query prepare failed: {e}"))?;
|
||||
|
||||
let metrics = stmt.query_map(params![project_id, limit], |row| {
|
||||
Ok(SessionMetric {
|
||||
id: row.get(0)?,
|
||||
project_id: row.get(1)?,
|
||||
session_id: row.get(2)?,
|
||||
start_time: row.get(3)?,
|
||||
end_time: row.get(4)?,
|
||||
peak_tokens: row.get(5)?,
|
||||
turn_count: row.get(6)?,
|
||||
tool_call_count: row.get(7)?,
|
||||
cost_usd: row.get(8)?,
|
||||
model: row.get(9)?,
|
||||
status: row.get(10)?,
|
||||
error_message: row.get(11)?,
|
||||
})
|
||||
}).map_err(|e| format!("Query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Row read failed: {e}"))?;
|
||||
|
||||
Ok(metrics)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue