mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
feat(ecc2): add tool call logging and history
This commit is contained in:
@@ -13,7 +13,10 @@ pub enum MessageType {
|
|||||||
/// Response to a query
|
/// Response to a query
|
||||||
Response { answer: String },
|
Response { answer: String },
|
||||||
/// Notification of completion
|
/// Notification of completion
|
||||||
Completed { summary: String, files_changed: Vec<String> },
|
Completed {
|
||||||
|
summary: String,
|
||||||
|
files_changed: Vec<String>,
|
||||||
|
},
|
||||||
/// Conflict detected (e.g., two agents editing the same file)
|
/// Conflict detected (e.g., two agents editing the same file)
|
||||||
Conflict { file: String, description: String },
|
Conflict { file: String, description: String },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
mod comms;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod observability;
|
||||||
mod session;
|
mod session;
|
||||||
mod tui;
|
mod tui;
|
||||||
mod worktree;
|
mod worktree;
|
||||||
mod observability;
|
|
||||||
mod comms;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@@ -63,10 +63,13 @@ async fn main() -> Result<()> {
|
|||||||
Some(Commands::Dashboard) | None => {
|
Some(Commands::Dashboard) | None => {
|
||||||
tui::app::run(db, cfg).await?;
|
tui::app::run(db, cfg).await?;
|
||||||
}
|
}
|
||||||
Some(Commands::Start { task, agent, worktree: use_worktree }) => {
|
Some(Commands::Start {
|
||||||
let session_id = session::manager::create_session(
|
task,
|
||||||
&db, &cfg, &task, &agent, use_worktree,
|
agent,
|
||||||
).await?;
|
worktree: use_worktree,
|
||||||
|
}) => {
|
||||||
|
let session_id =
|
||||||
|
session::manager::create_session(&db, &cfg, &task, &agent, use_worktree).await?;
|
||||||
println!("Session started: {session_id}");
|
println!("Session started: {session_id}");
|
||||||
}
|
}
|
||||||
Some(Commands::Sessions) => {
|
Some(Commands::Sessions) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{bail, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::session::store::StateStore;
|
use crate::session::store::StateStore;
|
||||||
@@ -14,6 +14,26 @@ pub struct ToolCallEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolCallEvent {
|
impl ToolCallEvent {
|
||||||
|
pub fn new(
|
||||||
|
session_id: impl Into<String>,
|
||||||
|
tool_name: impl Into<String>,
|
||||||
|
input_summary: impl Into<String>,
|
||||||
|
output_summary: impl Into<String>,
|
||||||
|
duration_ms: u64,
|
||||||
|
) -> Self {
|
||||||
|
let tool_name = tool_name.into();
|
||||||
|
let input_summary = input_summary.into();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
session_id: session_id.into(),
|
||||||
|
risk_score: Self::compute_risk(&tool_name, &input_summary),
|
||||||
|
tool_name,
|
||||||
|
input_summary,
|
||||||
|
output_summary: output_summary.into(),
|
||||||
|
duration_ms,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Compute risk score based on tool type and input patterns.
|
/// Compute risk score based on tool type and input patterns.
|
||||||
pub fn compute_risk(tool_name: &str, input: &str) -> f64 {
|
pub fn compute_risk(tool_name: &str, input: &str) -> f64 {
|
||||||
let mut score: f64 = 0.0;
|
let mut score: f64 = 0.0;
|
||||||
@@ -43,12 +63,119 @@ impl ToolCallEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log_tool_call(db: &StateStore, event: &ToolCallEvent) -> Result<()> {
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
db.send_message(
|
pub struct ToolLogEntry {
|
||||||
&event.session_id,
|
pub id: i64,
|
||||||
"observability",
|
pub session_id: String,
|
||||||
&serde_json::to_string(event)?,
|
pub tool_name: String,
|
||||||
"tool_call",
|
pub input_summary: String,
|
||||||
)?;
|
pub output_summary: String,
|
||||||
Ok(())
|
pub duration_ms: u64,
|
||||||
|
pub risk_score: f64,
|
||||||
|
pub timestamp: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct ToolLogPage {
|
||||||
|
pub entries: Vec<ToolLogEntry>,
|
||||||
|
pub page: u64,
|
||||||
|
pub page_size: u64,
|
||||||
|
pub total: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ToolLogger<'a> {
|
||||||
|
db: &'a StateStore,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ToolLogger<'a> {
|
||||||
|
pub fn new(db: &'a StateStore) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log(&self, event: &ToolCallEvent) -> Result<ToolLogEntry> {
|
||||||
|
let timestamp = chrono::Utc::now().to_rfc3339();
|
||||||
|
|
||||||
|
self.db.insert_tool_log(
|
||||||
|
&event.session_id,
|
||||||
|
&event.tool_name,
|
||||||
|
&event.input_summary,
|
||||||
|
&event.output_summary,
|
||||||
|
event.duration_ms,
|
||||||
|
event.risk_score,
|
||||||
|
×tamp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query(&self, session_id: &str, page: u64, page_size: u64) -> Result<ToolLogPage> {
|
||||||
|
if page_size == 0 {
|
||||||
|
bail!("page_size must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.db.query_tool_logs(session_id, page.max(1), page_size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_tool_call(db: &StateStore, event: &ToolCallEvent) -> Result<ToolLogEntry> {
|
||||||
|
ToolLogger::new(db).log(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{ToolCallEvent, ToolLogger};
|
||||||
|
use crate::session::store::StateStore;
|
||||||
|
use crate::session::{Session, SessionMetrics, SessionState};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn test_db_path() -> PathBuf {
|
||||||
|
std::env::temp_dir().join(format!("ecc2-observability-{}.db", uuid::Uuid::new_v4()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_session(id: &str) -> Session {
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
|
||||||
|
Session {
|
||||||
|
id: id.to_string(),
|
||||||
|
task: "test task".to_string(),
|
||||||
|
agent_type: "claude".to_string(),
|
||||||
|
state: SessionState::Pending,
|
||||||
|
worktree: None,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
metrics: SessionMetrics::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compute_risk_caps_high_risk_bash_commands() {
|
||||||
|
let score = ToolCallEvent::compute_risk("Bash", "sudo rm -rf /tmp --force");
|
||||||
|
assert_eq!(score, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logger_persists_entries_and_paginates() -> anyhow::Result<()> {
|
||||||
|
let db_path = test_db_path();
|
||||||
|
let db = StateStore::open(&db_path)?;
|
||||||
|
db.insert_session(&test_session("sess-1"))?;
|
||||||
|
|
||||||
|
let logger = ToolLogger::new(&db);
|
||||||
|
|
||||||
|
logger.log(&ToolCallEvent::new("sess-1", "Read", "first", "ok", 5))?;
|
||||||
|
logger.log(&ToolCallEvent::new("sess-1", "Write", "second", "ok", 15))?;
|
||||||
|
logger.log(&ToolCallEvent::new("sess-1", "Bash", "third", "ok", 25))?;
|
||||||
|
|
||||||
|
let first_page = logger.query("sess-1", 1, 2)?;
|
||||||
|
assert_eq!(first_page.total, 3);
|
||||||
|
assert_eq!(first_page.entries.len(), 2);
|
||||||
|
assert_eq!(first_page.entries[0].tool_name, "Bash");
|
||||||
|
assert_eq!(first_page.entries[1].tool_name, "Write");
|
||||||
|
|
||||||
|
let second_page = logger.query("sess-1", 2, 2)?;
|
||||||
|
assert_eq!(second_page.total, 3);
|
||||||
|
assert_eq!(second_page.entries.len(), 1);
|
||||||
|
assert_eq!(second_page.entries[0].tool_name, "Read");
|
||||||
|
|
||||||
|
std::fs::remove_file(&db_path).ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use super::{Session, SessionMetrics, SessionState};
|
|
||||||
use super::store::StateStore;
|
use super::store::StateStore;
|
||||||
|
use super::{Session, SessionMetrics, SessionState};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::observability::{log_tool_call, ToolCallEvent, ToolLogEntry, ToolLogPage, ToolLogger};
|
||||||
use crate::worktree;
|
use crate::worktree;
|
||||||
|
|
||||||
pub async fn create_session(
|
pub async fn create_session(
|
||||||
@@ -53,6 +54,44 @@ pub async fn stop_session(db: &StateStore, id: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn record_tool_call(
|
||||||
|
db: &StateStore,
|
||||||
|
session_id: &str,
|
||||||
|
tool_name: &str,
|
||||||
|
input_summary: &str,
|
||||||
|
output_summary: &str,
|
||||||
|
duration_ms: u64,
|
||||||
|
) -> Result<ToolLogEntry> {
|
||||||
|
let session = db
|
||||||
|
.get_session(session_id)?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Session not found: {session_id}"))?;
|
||||||
|
|
||||||
|
let event = ToolCallEvent::new(
|
||||||
|
session.id.clone(),
|
||||||
|
tool_name,
|
||||||
|
input_summary,
|
||||||
|
output_summary,
|
||||||
|
duration_ms,
|
||||||
|
);
|
||||||
|
let entry = log_tool_call(db, &event)?;
|
||||||
|
db.increment_tool_calls(&session.id)?;
|
||||||
|
|
||||||
|
Ok(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_tool_calls(
|
||||||
|
db: &StateStore,
|
||||||
|
session_id: &str,
|
||||||
|
page: u64,
|
||||||
|
page_size: u64,
|
||||||
|
) -> Result<ToolLogPage> {
|
||||||
|
let session = db
|
||||||
|
.get_session(session_id)?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Session not found: {session_id}"))?;
|
||||||
|
|
||||||
|
ToolLogger::new(db).query(&session.id, page, page_size)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SessionStatus(Session);
|
pub struct SessionStatus(Session);
|
||||||
|
|
||||||
impl fmt::Display for SessionStatus {
|
impl fmt::Display for SessionStatus {
|
||||||
@@ -74,3 +113,41 @@ impl fmt::Display for SessionStatus {
|
|||||||
write!(f, "Updated: {}", s.updated_at)
|
write!(f, "Updated: {}", s.updated_at)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{create_session, query_tool_calls, record_tool_call};
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::session::store::StateStore;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn record_tool_call_updates_session_metrics() -> anyhow::Result<()> {
|
||||||
|
let db_path =
|
||||||
|
std::env::temp_dir().join(format!("ecc2-session-manager-{}.db", uuid::Uuid::new_v4()));
|
||||||
|
let db = StateStore::open(&db_path)?;
|
||||||
|
|
||||||
|
let cfg = Config {
|
||||||
|
db_path: db_path.clone(),
|
||||||
|
..Config::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let session_id =
|
||||||
|
create_session(&db, &cfg, "implement tool logging", "claude", false).await?;
|
||||||
|
|
||||||
|
let entry = record_tool_call(&db, &session_id, "Bash", "git status", "clean worktree", 18)?;
|
||||||
|
|
||||||
|
assert_eq!(entry.session_id, session_id);
|
||||||
|
assert_eq!(entry.tool_name, "Bash");
|
||||||
|
|
||||||
|
let session = db.get_session(&session_id)?.expect("session should exist");
|
||||||
|
assert_eq!(session.metrics.tool_calls, 1);
|
||||||
|
|
||||||
|
let page = query_tool_calls(&db, &session_id[..4], 1, 10)?;
|
||||||
|
assert_eq!(page.total, 1);
|
||||||
|
assert_eq!(page.entries[0].output_summary, "clean worktree");
|
||||||
|
|
||||||
|
std::fs::remove_file(&db_path).ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use rusqlite::Connection;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::{Session, SessionMetrics, SessionState};
|
use super::{Session, SessionMetrics, SessionState};
|
||||||
|
use crate::observability::{ToolLogEntry, ToolLogPage};
|
||||||
|
|
||||||
pub struct StateStore {
|
pub struct StateStore {
|
||||||
conn: Connection,
|
conn: Connection,
|
||||||
@@ -112,6 +113,14 @@ impl StateStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn increment_tool_calls(&self, session_id: &str) -> Result<()> {
|
||||||
|
self.conn.execute(
|
||||||
|
"UPDATE sessions SET tool_calls = tool_calls + 1, updated_at = ?1 WHERE id = ?2",
|
||||||
|
rusqlite::params![chrono::Utc::now().to_rfc3339(), session_id],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn list_sessions(&self) -> Result<Vec<Session>> {
|
pub fn list_sessions(&self) -> Result<Vec<Session>> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
"SELECT id, task, agent_type, state, worktree_path, worktree_branch, worktree_base,
|
"SELECT id, task, agent_type, state, worktree_path, worktree_branch, worktree_base,
|
||||||
@@ -170,16 +179,12 @@ impl StateStore {
|
|||||||
|
|
||||||
pub fn get_session(&self, id: &str) -> Result<Option<Session>> {
|
pub fn get_session(&self, id: &str) -> Result<Option<Session>> {
|
||||||
let sessions = self.list_sessions()?;
|
let sessions = self.list_sessions()?;
|
||||||
Ok(sessions.into_iter().find(|s| s.id == id || s.id.starts_with(id)))
|
Ok(sessions
|
||||||
|
.into_iter()
|
||||||
|
.find(|s| s.id == id || s.id.starts_with(id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_message(
|
pub fn send_message(&self, from: &str, to: &str, content: &str, msg_type: &str) -> Result<()> {
|
||||||
&self,
|
|
||||||
from: &str,
|
|
||||||
to: &str,
|
|
||||||
content: &str,
|
|
||||||
msg_type: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"INSERT INTO messages (from_session, to_session, content, msg_type, timestamp)
|
"INSERT INTO messages (from_session, to_session, content, msg_type, timestamp)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
@@ -187,4 +192,86 @@ impl StateStore {
|
|||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_tool_log(
|
||||||
|
&self,
|
||||||
|
session_id: &str,
|
||||||
|
tool_name: &str,
|
||||||
|
input_summary: &str,
|
||||||
|
output_summary: &str,
|
||||||
|
duration_ms: u64,
|
||||||
|
risk_score: f64,
|
||||||
|
timestamp: &str,
|
||||||
|
) -> Result<ToolLogEntry> {
|
||||||
|
self.conn.execute(
|
||||||
|
"INSERT INTO tool_log (session_id, tool_name, input_summary, output_summary, duration_ms, risk_score, timestamp)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
|
rusqlite::params![
|
||||||
|
session_id,
|
||||||
|
tool_name,
|
||||||
|
input_summary,
|
||||||
|
output_summary,
|
||||||
|
duration_ms,
|
||||||
|
risk_score,
|
||||||
|
timestamp,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(ToolLogEntry {
|
||||||
|
id: self.conn.last_insert_rowid(),
|
||||||
|
session_id: session_id.to_string(),
|
||||||
|
tool_name: tool_name.to_string(),
|
||||||
|
input_summary: input_summary.to_string(),
|
||||||
|
output_summary: output_summary.to_string(),
|
||||||
|
duration_ms,
|
||||||
|
risk_score,
|
||||||
|
timestamp: timestamp.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_tool_logs(
|
||||||
|
&self,
|
||||||
|
session_id: &str,
|
||||||
|
page: u64,
|
||||||
|
page_size: u64,
|
||||||
|
) -> Result<ToolLogPage> {
|
||||||
|
let page = page.max(1);
|
||||||
|
let offset = (page - 1) * page_size;
|
||||||
|
|
||||||
|
let total: u64 = self.conn.query_row(
|
||||||
|
"SELECT COUNT(*) FROM tool_log WHERE session_id = ?1",
|
||||||
|
rusqlite::params![session_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut stmt = self.conn.prepare(
|
||||||
|
"SELECT id, session_id, tool_name, input_summary, output_summary, duration_ms, risk_score, timestamp
|
||||||
|
FROM tool_log
|
||||||
|
WHERE session_id = ?1
|
||||||
|
ORDER BY timestamp DESC, id DESC
|
||||||
|
LIMIT ?2 OFFSET ?3",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let entries = stmt
|
||||||
|
.query_map(rusqlite::params![session_id, page_size, offset], |row| {
|
||||||
|
Ok(ToolLogEntry {
|
||||||
|
id: row.get(0)?,
|
||||||
|
session_id: row.get(1)?,
|
||||||
|
tool_name: row.get(2)?,
|
||||||
|
input_summary: row.get::<_, Option<String>>(3)?.unwrap_or_default(),
|
||||||
|
output_summary: row.get::<_, Option<String>>(4)?.unwrap_or_default(),
|
||||||
|
duration_ms: row.get::<_, Option<u64>>(5)?.unwrap_or_default(),
|
||||||
|
risk_score: row.get::<_, Option<f64>>(6)?.unwrap_or_default(),
|
||||||
|
timestamp: row.get(7)?,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
Ok(ToolLogPage {
|
||||||
|
entries,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
total,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::session::{Session, SessionState};
|
|
||||||
use crate::session::store::StateStore;
|
use crate::session::store::StateStore;
|
||||||
|
use crate::session::{Session, SessionState};
|
||||||
|
|
||||||
pub struct Dashboard {
|
pub struct Dashboard {
|
||||||
db: StateStore,
|
db: StateStore,
|
||||||
@@ -42,7 +42,7 @@ impl Dashboard {
|
|||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Length(3), // Header
|
Constraint::Length(3), // Header
|
||||||
Constraint::Min(10), // Main content
|
Constraint::Min(10), // Main content
|
||||||
Constraint::Length(3), // Status bar
|
Constraint::Length(3), // Status bar
|
||||||
])
|
])
|
||||||
@@ -79,7 +79,11 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_header(&self, frame: &mut Frame, area: Rect) {
|
fn render_header(&self, frame: &mut Frame, area: Rect) {
|
||||||
let running = self.sessions.iter().filter(|s| s.state == SessionState::Running).count();
|
let running = self
|
||||||
|
.sessions
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.state == SessionState::Running)
|
||||||
|
.count();
|
||||||
let total = self.sessions.len();
|
let total = self.sessions.len();
|
||||||
|
|
||||||
let title = format!(" ECC 2.0 | {running} running / {total} total ");
|
let title = format!(" ECC 2.0 | {running} running / {total} total ");
|
||||||
@@ -90,7 +94,11 @@ impl Dashboard {
|
|||||||
Pane::Output => 1,
|
Pane::Output => 1,
|
||||||
Pane::Metrics => 2,
|
Pane::Metrics => 2,
|
||||||
})
|
})
|
||||||
.highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD));
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Cyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
);
|
||||||
|
|
||||||
frame.render_widget(tabs, area);
|
frame.render_widget(tabs, area);
|
||||||
}
|
}
|
||||||
@@ -110,11 +118,18 @@ impl Dashboard {
|
|||||||
SessionState::Pending => "◌",
|
SessionState::Pending => "◌",
|
||||||
};
|
};
|
||||||
let style = if i == self.selected_session {
|
let style = if i == self.selected_session {
|
||||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
Style::default()
|
||||||
|
.fg(Color::Cyan)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default()
|
Style::default()
|
||||||
};
|
};
|
||||||
let text = format!("{state_icon} {} [{}] {}", &s.id[..8.min(s.id.len())], s.agent_type, s.task);
|
let text = format!(
|
||||||
|
"{state_icon} {} [{}] {}",
|
||||||
|
&s.id[..8.min(s.id.len())],
|
||||||
|
s.agent_type,
|
||||||
|
s.task
|
||||||
|
);
|
||||||
ListItem::new(text).style(style)
|
ListItem::new(text).style(style)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -136,7 +151,10 @@ impl Dashboard {
|
|||||||
|
|
||||||
fn render_output(&self, frame: &mut Frame, area: Rect) {
|
fn render_output(&self, frame: &mut Frame, area: Rect) {
|
||||||
let content = if let Some(session) = self.sessions.get(self.selected_session) {
|
let content = if let Some(session) = self.sessions.get(self.selected_session) {
|
||||||
format!("Agent output for session {}...\n\n(Live streaming coming soon)", session.id)
|
format!(
|
||||||
|
"Agent output for session {}...\n\n(Live streaming coming soon)",
|
||||||
|
session.id
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
"No sessions. Press 'n' to start one.".to_string()
|
"No sessions. Press 'n' to start one.".to_string()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ pub fn create_for_session(session_id: &str, cfg: &Config) -> Result<WorktreeInfo
|
|||||||
anyhow::bail!("git worktree add failed: {stderr}");
|
anyhow::bail!("git worktree add failed: {stderr}");
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!("Created worktree at {} on branch {}", path.display(), branch);
|
tracing::info!(
|
||||||
|
"Created worktree at {} on branch {}",
|
||||||
|
path.display(),
|
||||||
|
branch
|
||||||
|
);
|
||||||
|
|
||||||
Ok(WorktreeInfo {
|
Ok(WorktreeInfo {
|
||||||
path,
|
path,
|
||||||
|
|||||||
Reference in New Issue
Block a user