mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-11 02:20:29 +08:00
feat: add ecc2 harness metadata detection
This commit is contained in:
parent
9c525009d7
commit
29ff44e23e
@ -1150,8 +1150,13 @@ async fn main() -> Result<()> {
|
||||
Some(Commands::Sessions) => {
|
||||
sync_runtime_session_metrics(&db, &cfg)?;
|
||||
let sessions = session::manager::list_sessions(&db)?;
|
||||
let harnesses = db.list_session_harnesses().unwrap_or_default();
|
||||
for s in sessions {
|
||||
println!("{} [{}] {}", s.id, s.state, s.task);
|
||||
let harness = harnesses
|
||||
.get(&s.id)
|
||||
.map(|info| info.primary.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
println!("{} [{}] [{}] {}", s.id, s.state, harness, s.task);
|
||||
}
|
||||
}
|
||||
Some(Commands::Status { session_id }) => {
|
||||
|
||||
@ -11,7 +11,7 @@ use super::runtime::capture_command_output;
|
||||
use super::store::StateStore;
|
||||
use super::{
|
||||
default_project_label, default_task_group_label, normalize_group_label, Session,
|
||||
SessionAgentProfile, SessionGrouping, SessionMetrics, SessionState,
|
||||
SessionAgentProfile, SessionGrouping, SessionHarnessInfo, SessionMetrics, SessionState,
|
||||
};
|
||||
use crate::comms::{self, MessageType};
|
||||
use crate::config::Config;
|
||||
@ -116,6 +116,11 @@ pub fn get_status(db: &StateStore, id: &str) -> Result<SessionStatus> {
|
||||
let session = resolve_session(db, id)?;
|
||||
let session_id = session.id.clone();
|
||||
Ok(SessionStatus {
|
||||
harness: db
|
||||
.get_session_harness_info(&session_id)?
|
||||
.unwrap_or_else(|| {
|
||||
SessionHarnessInfo::detect(&session.agent_type, &session.working_dir)
|
||||
}),
|
||||
profile: db.get_session_profile(&session_id)?,
|
||||
session,
|
||||
parent_session: db.latest_task_handoff_source(&session_id)?,
|
||||
@ -2670,6 +2675,7 @@ async fn kill_process(pid: u32) -> Result<()> {
|
||||
}
|
||||
|
||||
pub struct SessionStatus {
|
||||
harness: SessionHarnessInfo,
|
||||
profile: Option<SessionAgentProfile>,
|
||||
session: Session,
|
||||
parent_session: Option<String>,
|
||||
@ -2962,6 +2968,8 @@ impl fmt::Display for SessionStatus {
|
||||
writeln!(f, "Session: {}", s.id)?;
|
||||
writeln!(f, "Task: {}", s.task)?;
|
||||
writeln!(f, "Agent: {}", s.agent_type)?;
|
||||
writeln!(f, "Harness: {}", self.harness.primary)?;
|
||||
writeln!(f, "Detected: {}", self.harness.detected_summary())?;
|
||||
writeln!(f, "State: {}", s.state)?;
|
||||
if let Some(profile) = self.profile.as_ref() {
|
||||
writeln!(f, "Profile: {}", profile.profile_name)?;
|
||||
|
||||
@ -13,6 +13,139 @@ use std::path::PathBuf;
|
||||
|
||||
pub type SessionAgentProfile = crate::config::ResolvedAgentProfile;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HarnessKind {
|
||||
#[default]
|
||||
Unknown,
|
||||
Claude,
|
||||
Codex,
|
||||
OpenCode,
|
||||
Gemini,
|
||||
Cursor,
|
||||
Kiro,
|
||||
Trae,
|
||||
Zed,
|
||||
FactoryDroid,
|
||||
Windsurf,
|
||||
}
|
||||
|
||||
impl HarnessKind {
|
||||
pub fn from_agent_type(agent_type: &str) -> Self {
|
||||
match agent_type.trim().to_ascii_lowercase().as_str() {
|
||||
"claude" | "claude-code" => Self::Claude,
|
||||
"codex" => Self::Codex,
|
||||
"opencode" => Self::OpenCode,
|
||||
"gemini" | "gemini-cli" => Self::Gemini,
|
||||
"cursor" => Self::Cursor,
|
||||
"kiro" => Self::Kiro,
|
||||
"trae" => Self::Trae,
|
||||
"zed" => Self::Zed,
|
||||
"factory-droid" | "factory_droid" | "factorydroid" => Self::FactoryDroid,
|
||||
"windsurf" => Self::Windsurf,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_db_value(value: &str) -> Self {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"claude" => Self::Claude,
|
||||
"codex" => Self::Codex,
|
||||
"opencode" => Self::OpenCode,
|
||||
"gemini" => Self::Gemini,
|
||||
"cursor" => Self::Cursor,
|
||||
"kiro" => Self::Kiro,
|
||||
"trae" => Self::Trae,
|
||||
"zed" => Self::Zed,
|
||||
"factory_droid" => Self::FactoryDroid,
|
||||
"windsurf" => Self::Windsurf,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Unknown => "unknown",
|
||||
Self::Claude => "claude",
|
||||
Self::Codex => "codex",
|
||||
Self::OpenCode => "opencode",
|
||||
Self::Gemini => "gemini",
|
||||
Self::Cursor => "cursor",
|
||||
Self::Kiro => "kiro",
|
||||
Self::Trae => "trae",
|
||||
Self::Zed => "zed",
|
||||
Self::FactoryDroid => "factory_droid",
|
||||
Self::Windsurf => "windsurf",
|
||||
}
|
||||
}
|
||||
|
||||
fn project_markers(self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::Claude => &[".claude"],
|
||||
Self::Codex => &[".codex", ".codex-plugin"],
|
||||
Self::OpenCode => &[".opencode"],
|
||||
Self::Gemini => &[".gemini"],
|
||||
Self::Cursor => &[".cursor"],
|
||||
Self::Kiro => &[".kiro"],
|
||||
Self::Trae => &[".trae"],
|
||||
Self::Unknown | Self::Zed | Self::FactoryDroid | Self::Windsurf => &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HarnessKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SessionHarnessInfo {
|
||||
pub primary: HarnessKind,
|
||||
pub detected: Vec<HarnessKind>,
|
||||
}
|
||||
|
||||
impl SessionHarnessInfo {
|
||||
pub fn detect(agent_type: &str, working_dir: &Path) -> Self {
|
||||
let detected = [
|
||||
HarnessKind::Claude,
|
||||
HarnessKind::Codex,
|
||||
HarnessKind::OpenCode,
|
||||
HarnessKind::Gemini,
|
||||
HarnessKind::Cursor,
|
||||
HarnessKind::Kiro,
|
||||
HarnessKind::Trae,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|harness| {
|
||||
harness
|
||||
.project_markers()
|
||||
.iter()
|
||||
.any(|marker| working_dir.join(marker).exists())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let primary = match HarnessKind::from_agent_type(agent_type) {
|
||||
HarnessKind::Unknown => detected.first().copied().unwrap_or(HarnessKind::Unknown),
|
||||
harness => harness,
|
||||
};
|
||||
|
||||
Self { primary, detected }
|
||||
}
|
||||
|
||||
pub fn detected_summary(&self) -> String {
|
||||
if self.detected.is_empty() {
|
||||
"none detected".to_string()
|
||||
} else {
|
||||
self.detected
|
||||
.iter()
|
||||
.map(|harness| harness.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
pub id: String,
|
||||
@ -315,3 +448,61 @@ pub struct SessionGrouping {
|
||||
pub project: Option<String>,
|
||||
pub task_group: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
struct TestDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TestDir {
|
||||
fn new(label: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("ecc2-{}-{}", label, uuid::Uuid::new_v4()));
|
||||
fs::create_dir_all(&path)?;
|
||||
Ok(Self { path })
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_session_harness_prefers_agent_type_and_collects_project_markers(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let repo = TestDir::new("session-harness-detect")?;
|
||||
fs::create_dir_all(repo.path().join(".codex"))?;
|
||||
fs::create_dir_all(repo.path().join(".claude"))?;
|
||||
|
||||
let harness = SessionHarnessInfo::detect("claude", repo.path());
|
||||
assert_eq!(harness.primary, HarnessKind::Claude);
|
||||
assert_eq!(
|
||||
harness.detected,
|
||||
vec![HarnessKind::Claude, HarnessKind::Codex]
|
||||
);
|
||||
assert_eq!(harness.detected_summary(), "claude, codex");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_session_harness_falls_back_to_project_markers_for_unknown_agent(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let repo = TestDir::new("session-harness-markers")?;
|
||||
fs::create_dir_all(repo.path().join(".gemini"))?;
|
||||
|
||||
let harness = SessionHarnessInfo::detect("custom-runner", repo.path());
|
||||
assert_eq!(harness.primary, HarnessKind::Gemini);
|
||||
assert_eq!(harness.detected, vec![HarnessKind::Gemini]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,8 +16,9 @@ use super::{
|
||||
default_project_label, default_task_group_label, normalize_group_label,
|
||||
ContextGraphCompactionStats, ContextGraphEntity, ContextGraphEntityDetail,
|
||||
ContextGraphObservation, ContextGraphRecallEntry, ContextGraphRelation, ContextGraphSyncStats,
|
||||
ContextObservationPriority, DecisionLogEntry, FileActivityAction, FileActivityEntry, Session,
|
||||
SessionAgentProfile, SessionMessage, SessionMetrics, SessionState, WorktreeInfo,
|
||||
ContextObservationPriority, DecisionLogEntry, FileActivityAction, FileActivityEntry,
|
||||
HarnessKind, Session, SessionAgentProfile, SessionHarnessInfo, SessionMessage, SessionMetrics,
|
||||
SessionState, WorktreeInfo,
|
||||
};
|
||||
|
||||
pub struct StateStore {
|
||||
@ -171,6 +172,8 @@ impl StateStore {
|
||||
project TEXT NOT NULL DEFAULT '',
|
||||
task_group TEXT NOT NULL DEFAULT '',
|
||||
agent_type TEXT NOT NULL,
|
||||
harness TEXT NOT NULL DEFAULT 'unknown',
|
||||
detected_harnesses_json TEXT NOT NULL DEFAULT '[]',
|
||||
working_dir TEXT NOT NULL DEFAULT '.',
|
||||
state TEXT NOT NULL DEFAULT 'pending',
|
||||
pid INTEGER,
|
||||
@ -399,6 +402,24 @@ impl StateStore {
|
||||
.context("Failed to add task_group column to sessions table")?;
|
||||
}
|
||||
|
||||
if !self.has_column("sessions", "harness")? {
|
||||
self.conn
|
||||
.execute(
|
||||
"ALTER TABLE sessions ADD COLUMN harness TEXT NOT NULL DEFAULT 'unknown'",
|
||||
[],
|
||||
)
|
||||
.context("Failed to add harness column to sessions table")?;
|
||||
}
|
||||
|
||||
if !self.has_column("sessions", "detected_harnesses_json")? {
|
||||
self.conn
|
||||
.execute(
|
||||
"ALTER TABLE sessions ADD COLUMN detected_harnesses_json TEXT NOT NULL DEFAULT '[]'",
|
||||
[],
|
||||
)
|
||||
.context("Failed to add detected_harnesses_json column to sessions table")?;
|
||||
}
|
||||
|
||||
if !self.has_column("sessions", "input_tokens")? {
|
||||
self.conn
|
||||
.execute(
|
||||
@ -624,6 +645,8 @@ impl StateStore {
|
||||
WHERE hook_event_id IS NOT NULL;",
|
||||
)?;
|
||||
|
||||
self.backfill_session_harnesses()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -637,16 +660,51 @@ impl StateStore {
|
||||
Ok(columns.iter().any(|existing| existing == column))
|
||||
}
|
||||
|
||||
fn backfill_session_harnesses(&self) -> Result<()> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT id, agent_type, working_dir FROM sessions")?;
|
||||
let updates = stmt
|
||||
.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
))
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
for (session_id, agent_type, working_dir) in updates {
|
||||
let harness = SessionHarnessInfo::detect(&agent_type, Path::new(&working_dir));
|
||||
let detected_json =
|
||||
serde_json::to_string(&harness.detected).context("serialize detected harnesses")?;
|
||||
self.conn.execute(
|
||||
"UPDATE sessions
|
||||
SET harness = ?2,
|
||||
detected_harnesses_json = ?3
|
||||
WHERE id = ?1",
|
||||
rusqlite::params![session_id, harness.primary.to_string(), detected_json],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_session(&self, session: &Session) -> Result<()> {
|
||||
let harness = SessionHarnessInfo::detect(&session.agent_type, &session.working_dir);
|
||||
let detected_json =
|
||||
serde_json::to_string(&harness.detected).context("serialize detected harnesses")?;
|
||||
self.conn.execute(
|
||||
"INSERT INTO sessions (id, task, project, task_group, agent_type, working_dir, state, pid, worktree_path, worktree_branch, worktree_base, created_at, updated_at, last_heartbeat_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
|
||||
"INSERT INTO sessions (id, task, project, task_group, agent_type, harness, detected_harnesses_json, working_dir, state, pid, worktree_path, worktree_branch, worktree_base, created_at, updated_at, last_heartbeat_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)",
|
||||
rusqlite::params![
|
||||
session.id,
|
||||
session.task,
|
||||
session.project,
|
||||
session.task_group,
|
||||
session.agent_type,
|
||||
harness.primary.to_string(),
|
||||
detected_json,
|
||||
session.working_dir.to_string_lossy().to_string(),
|
||||
session.state.to_string(),
|
||||
session.pid.map(i64::from),
|
||||
@ -1553,6 +1611,55 @@ impl StateStore {
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
pub fn list_session_harnesses(&self) -> Result<HashMap<String, SessionHarnessInfo>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, harness, detected_harnesses_json, agent_type, working_dir FROM sessions",
|
||||
)?;
|
||||
|
||||
let harnesses = stmt
|
||||
.query_map([], |row| {
|
||||
let session_id: String = row.get(0)?;
|
||||
let primary = HarnessKind::from_db_value(&row.get::<_, String>(1)?);
|
||||
let detected = serde_json::from_str::<Vec<HarnessKind>>(&row.get::<_, String>(2)?)
|
||||
.unwrap_or_default();
|
||||
let agent_type: String = row.get(3)?;
|
||||
let working_dir = PathBuf::from(row.get::<_, String>(4)?);
|
||||
let info = if primary == HarnessKind::Unknown && detected.is_empty() {
|
||||
SessionHarnessInfo::detect(&agent_type, &working_dir)
|
||||
} else {
|
||||
SessionHarnessInfo { primary, detected }
|
||||
};
|
||||
Ok((session_id, info))
|
||||
})?
|
||||
.collect::<std::result::Result<HashMap<_, _>, _>>()?;
|
||||
|
||||
Ok(harnesses)
|
||||
}
|
||||
|
||||
pub fn get_session_harness_info(&self, session_id: &str) -> Result<Option<SessionHarnessInfo>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT harness, detected_harnesses_json, agent_type, working_dir
|
||||
FROM sessions
|
||||
WHERE id = ?1",
|
||||
)?;
|
||||
|
||||
stmt.query_row([session_id], |row| {
|
||||
let primary = HarnessKind::from_db_value(&row.get::<_, String>(0)?);
|
||||
let detected = serde_json::from_str::<Vec<HarnessKind>>(&row.get::<_, String>(1)?)
|
||||
.unwrap_or_default();
|
||||
let agent_type: String = row.get(2)?;
|
||||
let working_dir = PathBuf::from(row.get::<_, String>(3)?);
|
||||
let info = if primary == HarnessKind::Unknown && detected.is_empty() {
|
||||
SessionHarnessInfo::detect(&agent_type, &working_dir)
|
||||
} else {
|
||||
SessionHarnessInfo { primary, detected }
|
||||
};
|
||||
Ok(info)
|
||||
})
|
||||
.optional()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_latest_session(&self) -> Result<Option<Session>> {
|
||||
Ok(self.list_sessions()?.into_iter().next())
|
||||
}
|
||||
@ -3800,12 +3907,83 @@ mod tests {
|
||||
assert!(column_names.iter().any(|column| column == "pid"));
|
||||
assert!(column_names.iter().any(|column| column == "input_tokens"));
|
||||
assert!(column_names.iter().any(|column| column == "output_tokens"));
|
||||
assert!(column_names.iter().any(|column| column == "harness"));
|
||||
assert!(column_names
|
||||
.iter()
|
||||
.any(|column| column == "detected_harnesses_json"));
|
||||
assert!(column_names
|
||||
.iter()
|
||||
.any(|column| column == "last_heartbeat_at"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_backfills_session_harness_metadata_for_legacy_rows() -> Result<()> {
|
||||
let tempdir = TestDir::new("store-harness-backfill")?;
|
||||
let repo_root = tempdir.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".codex"))?;
|
||||
let db_path = tempdir.path().join("state.db");
|
||||
|
||||
let conn = Connection::open(&db_path)?;
|
||||
conn.execute_batch(
|
||||
"
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
task TEXT NOT NULL,
|
||||
project TEXT NOT NULL DEFAULT '',
|
||||
task_group TEXT NOT NULL DEFAULT '',
|
||||
agent_type TEXT NOT NULL,
|
||||
working_dir TEXT NOT NULL DEFAULT '.',
|
||||
state TEXT NOT NULL DEFAULT 'pending',
|
||||
pid INTEGER,
|
||||
worktree_path TEXT,
|
||||
worktree_branch TEXT,
|
||||
worktree_base TEXT,
|
||||
input_tokens INTEGER DEFAULT 0,
|
||||
output_tokens INTEGER DEFAULT 0,
|
||||
tokens_used INTEGER DEFAULT 0,
|
||||
tool_calls INTEGER DEFAULT 0,
|
||||
files_changed INTEGER DEFAULT 0,
|
||||
duration_secs INTEGER DEFAULT 0,
|
||||
cost_usd REAL DEFAULT 0.0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
last_heartbeat_at TEXT NOT NULL
|
||||
);
|
||||
",
|
||||
)?;
|
||||
let now = Utc::now().to_rfc3339();
|
||||
conn.execute(
|
||||
"INSERT INTO sessions (
|
||||
id, task, project, task_group, agent_type, working_dir, state, pid,
|
||||
worktree_path, worktree_branch, worktree_base, input_tokens, output_tokens,
|
||||
tokens_used, tool_calls, files_changed, duration_secs, cost_usd, created_at,
|
||||
updated_at, last_heartbeat_at
|
||||
) VALUES (
|
||||
?1, ?2, ?3, ?4, ?5, ?6, 'pending', NULL,
|
||||
NULL, NULL, NULL, 0, 0, 0, 0, 0, 0, 0.0, ?7, ?7, ?7
|
||||
)",
|
||||
rusqlite::params![
|
||||
"sess-legacy",
|
||||
"Backfill harness metadata",
|
||||
"ecc",
|
||||
"legacy",
|
||||
"claude",
|
||||
repo_root.display().to_string(),
|
||||
now,
|
||||
],
|
||||
)?;
|
||||
drop(conn);
|
||||
|
||||
let db = StateStore::open(&db_path)?;
|
||||
let harness = db
|
||||
.get_session_harness_info("sess-legacy")?
|
||||
.expect("legacy row should be backfilled");
|
||||
assert_eq!(harness.primary, HarnessKind::Claude);
|
||||
assert_eq!(harness.detected, vec![HarnessKind::Codex]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_profile_round_trips_with_launch_settings() -> Result<()> {
|
||||
let tempdir = TestDir::new("store-session-profile")?;
|
||||
|
||||
@ -24,7 +24,7 @@ use crate::session::output::{
|
||||
use crate::session::store::{DaemonActivity, FileActivityOverlap, StateStore};
|
||||
use crate::session::{
|
||||
ContextObservationPriority, DecisionLogEntry, FileActivityEntry, Session, SessionGrouping,
|
||||
SessionMessage, SessionState,
|
||||
SessionHarnessInfo, SessionMessage, SessionState,
|
||||
};
|
||||
use crate::worktree;
|
||||
|
||||
@ -87,6 +87,7 @@ pub struct Dashboard {
|
||||
notifier: DesktopNotifier,
|
||||
webhook_notifier: WebhookNotifier,
|
||||
sessions: Vec<Session>,
|
||||
session_harnesses: HashMap<String, SessionHarnessInfo>,
|
||||
session_output_cache: HashMap<String, Vec<OutputLine>>,
|
||||
unread_message_counts: HashMap<String, usize>,
|
||||
approval_queue_counts: HashMap<String, usize>,
|
||||
@ -497,6 +498,7 @@ impl Dashboard {
|
||||
let _ = db.sync_tool_activity_metrics(&cfg.tool_activity_metrics_path());
|
||||
}
|
||||
let sessions = db.list_sessions().unwrap_or_default();
|
||||
let session_harnesses = db.list_session_harnesses().unwrap_or_default();
|
||||
let initial_session_states = sessions
|
||||
.iter()
|
||||
.map(|session| (session.id.clone(), session.state.clone()))
|
||||
@ -522,6 +524,7 @@ impl Dashboard {
|
||||
notifier,
|
||||
webhook_notifier,
|
||||
sessions,
|
||||
session_harnesses,
|
||||
session_output_cache: HashMap::new(),
|
||||
unread_message_counts: HashMap::new(),
|
||||
approval_queue_counts: HashMap::new(),
|
||||
@ -4035,6 +4038,13 @@ impl Dashboard {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
self.session_harnesses = match self.db.list_session_harnesses() {
|
||||
Ok(harnesses) => harnesses,
|
||||
Err(error) => {
|
||||
tracing::warn!("Failed to refresh session harnesses: {error}");
|
||||
HashMap::new()
|
||||
}
|
||||
};
|
||||
self.unread_message_counts = match self.db.unread_message_counts() {
|
||||
Ok(counts) => counts,
|
||||
Err(error) => {
|
||||
@ -6332,6 +6342,14 @@ impl Dashboard {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(harness) = self.session_harnesses.get(&session.id) {
|
||||
lines.push(format!(
|
||||
"Harness {} | Detected {}",
|
||||
harness.primary,
|
||||
harness.detected_summary()
|
||||
));
|
||||
}
|
||||
|
||||
lines.push(format!(
|
||||
"Tokens {} total | In {} | Out {}",
|
||||
format_token_count(metrics.tokens_used),
|
||||
@ -12281,6 +12299,40 @@ diff --git a/src/lib.rs b/src/lib.rs
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_session_metrics_text_includes_harness_summary() -> Result<()> {
|
||||
let tempdir = std::env::temp_dir().join(format!(
|
||||
"ecc2-dashboard-harness-metrics-{}",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
fs::create_dir_all(tempdir.join(".claude"))?;
|
||||
fs::create_dir_all(tempdir.join(".codex"))?;
|
||||
|
||||
let now = Utc::now();
|
||||
let session = Session {
|
||||
id: "sess-harness".to_string(),
|
||||
task: "Map harness metadata".to_string(),
|
||||
project: "ecc".to_string(),
|
||||
task_group: "compat".to_string(),
|
||||
agent_type: "claude".to_string(),
|
||||
working_dir: tempdir.clone(),
|
||||
state: SessionState::Running,
|
||||
pid: Some(4242),
|
||||
worktree: None,
|
||||
created_at: now - Duration::minutes(3),
|
||||
updated_at: now - Duration::minutes(1),
|
||||
last_heartbeat_at: now - Duration::minutes(1),
|
||||
metrics: SessionMetrics::default(),
|
||||
};
|
||||
|
||||
let dashboard = test_dashboard(vec![session], 0);
|
||||
let metrics_text = dashboard.selected_session_metrics_text();
|
||||
assert!(metrics_text.contains("Harness claude | Detected claude, codex"));
|
||||
|
||||
let _ = fs::remove_dir_all(tempdir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_session_task_uses_selected_session_context() {
|
||||
let dashboard = test_dashboard(
|
||||
@ -14429,6 +14481,15 @@ diff --git a/src/lib.rs b/src/lib.rs
|
||||
.iter()
|
||||
.map(|session| (session.id.clone(), session.state.clone()))
|
||||
.collect();
|
||||
let session_harnesses = sessions
|
||||
.iter()
|
||||
.map(|session| {
|
||||
(
|
||||
session.id.clone(),
|
||||
SessionHarnessInfo::detect(&session.agent_type, &session.working_dir),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let output_store = SessionOutputStore::default();
|
||||
let output_rx = output_store.subscribe();
|
||||
let mut session_table_state = TableState::default();
|
||||
@ -14445,6 +14506,7 @@ diff --git a/src/lib.rs b/src/lib.rs
|
||||
notifier,
|
||||
webhook_notifier,
|
||||
sessions,
|
||||
session_harnesses,
|
||||
session_output_cache: HashMap::new(),
|
||||
unread_message_counts: HashMap::new(),
|
||||
approval_queue_counts: HashMap::new(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user