diff --git a/ecc2/src/session/store.rs b/ecc2/src/session/store.rs index 64d1bad3..5786298a 100644 --- a/ecc2/src/session/store.rs +++ b/ecc2/src/session/store.rs @@ -1262,6 +1262,7 @@ impl StateStore { alternatives: &[String], reasoning: &str, ) -> Result<()> { + let session_entity = self.sync_context_graph_session(session_id)?; let mut metadata = BTreeMap::new(); metadata.insert( "alternatives_count".to_string(), @@ -1270,7 +1271,7 @@ impl StateStore { if !alternatives.is_empty() { metadata.insert("alternatives".to_string(), alternatives.join(" | ")); } - self.upsert_context_entity( + let decision_entity = self.upsert_context_entity( Some(session_id), "decision", decision, @@ -1278,6 +1279,14 @@ impl StateStore { reasoning, &metadata, )?; + let relation_summary = format!("{} recorded this decision", session_entity.name); + self.upsert_context_relation( + Some(session_id), + session_entity.id, + decision_entity.id, + "decided", + &relation_summary, + )?; Ok(()) } @@ -1287,6 +1296,7 @@ impl StateStore { tool_name: &str, event: &PersistedFileEvent, ) -> Result<()> { + let session_entity = self.sync_context_graph_session(session_id)?; let mut metadata = BTreeMap::new(); metadata.insert( "last_action".to_string(), @@ -1305,7 +1315,7 @@ impl StateStore { format!("Last activity: {action} via {tool_name}") }; let name = context_graph_file_name(&event.path); - self.upsert_context_entity( + let file_entity = self.upsert_context_entity( Some(session_id), "file", &name, @@ -1313,9 +1323,57 @@ impl StateStore { &summary, &metadata, )?; + self.upsert_context_relation( + Some(session_id), + session_entity.id, + file_entity.id, + action, + &summary, + )?; Ok(()) } + fn sync_context_graph_session(&self, session_id: &str) -> Result { + let session = self + .get_session(session_id)? + .ok_or_else(|| anyhow::anyhow!("Session not found for context graph sync: {session_id}"))?; + + let mut metadata = BTreeMap::new(); + metadata.insert("task".to_string(), session.task.clone()); + metadata.insert("project".to_string(), session.project.clone()); + metadata.insert("task_group".to_string(), session.task_group.clone()); + metadata.insert("agent_type".to_string(), session.agent_type.clone()); + metadata.insert("state".to_string(), session.state.to_string()); + metadata.insert( + "working_dir".to_string(), + session.working_dir.display().to_string(), + ); + if let Some(pid) = session.pid { + metadata.insert("pid".to_string(), pid.to_string()); + } + if let Some(worktree) = &session.worktree { + metadata.insert( + "worktree_path".to_string(), + worktree.path.display().to_string(), + ); + metadata.insert("worktree_branch".to_string(), worktree.branch.clone()); + metadata.insert("base_branch".to_string(), worktree.base_branch.clone()); + } + + let summary = format!( + "{} | {} | {} / {}", + session.state, session.agent_type, session.project, session.task_group + ); + self.upsert_context_entity( + Some(&session.id), + "session", + &session.id, + None, + &summary, + &metadata, + ) + } + pub fn increment_tool_calls(&self, session_id: &str) -> Result<()> { self.conn.execute( "UPDATE sessions @@ -3832,6 +3890,20 @@ mod tests { .summary .contains("SQLite keeps the graph queryable")); + let session_entities = db.list_context_entities(Some("session-1"), Some("session"), 10)?; + assert_eq!(session_entities.len(), 1); + assert_eq!(session_entities[0].name, "session-1"); + assert_eq!( + session_entities[0].metadata.get("task"), + Some(&"context graph".to_string()) + ); + + let relations = db.list_context_relations(Some(session_entities[0].id), 10)?; + assert_eq!(relations.len(), 1); + assert_eq!(relations[0].relation_type, "decided"); + assert_eq!(relations[0].to_entity_type, "decision"); + assert_eq!(relations[0].to_entity_name, "Use sqlite for shared context"); + Ok(()) } @@ -3883,6 +3955,14 @@ mod tests { .summary .contains("Last activity: modify via Edit")); + let session_entities = db.list_context_entities(Some("session-1"), Some("session"), 10)?; + assert_eq!(session_entities.len(), 1); + let relations = db.list_context_relations(Some(session_entities[0].id), 10)?; + assert_eq!(relations.len(), 1); + assert_eq!(relations[0].relation_type, "modify"); + assert_eq!(relations[0].to_entity_type, "file"); + assert_eq!(relations[0].to_entity_name, "config.ts"); + Ok(()) } @@ -3953,6 +4033,14 @@ mod tests { && entity.name == "Backfill historical decision")); assert!(entities.iter().any(|entity| entity.entity_type == "file" && entity.path.as_deref() == Some("src/backfill.rs"))); + let session_entity = entities + .iter() + .find(|entity| entity.entity_type == "session" && entity.name == "session-1") + .expect("session entity should exist"); + let relations = db.list_context_relations(Some(session_entity.id), 10)?; + assert_eq!(relations.len(), 2); + assert!(relations.iter().any(|relation| relation.relation_type == "decided")); + assert!(relations.iter().any(|relation| relation.relation_type == "modify")); Ok(()) } diff --git a/ecc2/src/tui/dashboard.rs b/ecc2/src/tui/dashboard.rs index 3a3a240b..b0fc1b88 100644 --- a/ecc2/src/tui/dashboard.rs +++ b/ecc2/src/tui/dashboard.rs @@ -10104,12 +10104,14 @@ diff --git a/src/lib.rs b/src/lib.rs\n\ dashboard.toggle_context_graph_mode(); dashboard.toggle_search_scope(); + dashboard.cycle_graph_entity_filter(); dashboard.begin_search(); for ch in "alpha.*".chars() { dashboard.push_input_char(ch); } dashboard.submit_search(); + assert_eq!(dashboard.graph_entity_filter, GraphEntityFilter::Decisions); assert_eq!(dashboard.search_matches.len(), 2); let first_session = dashboard.selected_session_id().map(str::to_string); dashboard.next_search_match(); @@ -10121,6 +10123,40 @@ diff --git a/src/lib.rs b/src/lib.rs\n\ Ok(()) } + #[test] + fn graph_sessions_filter_renders_auto_session_relations() -> Result<()> { + let session = sample_session( + "focus-12345678", + "planner", + SessionState::Running, + None, + 1, + 1, + ); + let mut dashboard = test_dashboard(vec![session.clone()], 0); + dashboard.db.insert_session(&session)?; + dashboard.db.insert_decision( + &session.id, + "Use graph relations", + &[], + "Edges make the context graph navigable", + )?; + + dashboard.toggle_context_graph_mode(); + dashboard.cycle_graph_entity_filter(); + dashboard.cycle_graph_entity_filter(); + dashboard.cycle_graph_entity_filter(); + dashboard.cycle_graph_entity_filter(); + + assert_eq!(dashboard.graph_entity_filter, GraphEntityFilter::Sessions); + assert_eq!(dashboard.output_title(), " Graph sessions "); + let rendered = dashboard.rendered_output_text(180, 30); + assert!(rendered.contains("focus-12345678")); + assert!(rendered.contains("summary running | planner |")); + assert!(rendered.contains("-> decided decision:Use graph relations")); + Ok(()) + } + #[test] fn worktree_diff_columns_split_removed_and_added_lines() { let patch = "\