Skip to main content

kaizen/shell/
feedback.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2use crate::feedback::types::{FeedbackLabel, FeedbackRecord, FeedbackScore};
3use crate::store::Store;
4use anyhow::Result;
5use std::path::Path;
6
7fn now_ms() -> u64 {
8    std::time::SystemTime::now()
9        .duration_since(std::time::UNIX_EPOCH)
10        .unwrap_or_default()
11        .as_millis() as u64
12}
13
14fn parse_since(since: &str) -> Option<u64> {
15    let s = since.trim();
16    let (n, unit) = s.split_at(s.len().saturating_sub(1));
17    let days: u64 = match unit {
18        "d" => n.parse().ok()?,
19        "w" => n.parse::<u64>().ok()?.checked_mul(7)?,
20        _ => return None,
21    };
22    Some(days * 86_400_000)
23}
24
25fn open_store(workspace: Option<&Path>) -> Result<Store> {
26    let ws = crate::core::workspace::resolve(workspace)?;
27    Store::open(&crate::core::workspace::db_path(&ws)?)
28}
29
30/// `kaizen sessions annotate <id>` — attach score/label/note to a session.
31pub fn cmd_sessions_annotate(
32    id: &str,
33    score: Option<u8>,
34    label: Option<FeedbackLabel>,
35    note: Option<String>,
36    workspace: Option<&Path>,
37) -> Result<()> {
38    let store = open_store(workspace)?;
39    let record = FeedbackRecord {
40        id: uuid::Uuid::now_v7().to_string(),
41        session_id: id.to_string(),
42        score: score.and_then(FeedbackScore::new),
43        label,
44        note,
45        created_at_ms: now_ms(),
46    };
47    store.upsert_feedback(&record)?;
48    println!("annotated session {id}");
49    Ok(())
50}
51
52/// `kaizen feedback list` — show feedback records in a time window.
53pub fn cmd_feedback_list(
54    workspace: Option<&Path>,
55    label_filter: Option<FeedbackLabel>,
56    since: Option<String>,
57    json: bool,
58) -> Result<()> {
59    let store = open_store(workspace)?;
60    let now = now_ms();
61    let start_ms = since
62        .as_deref()
63        .and_then(parse_since)
64        .map(|d| now.saturating_sub(d))
65        .unwrap_or(0);
66    let mut records = store.list_feedback_in_window(start_ms, now)?;
67    if let Some(lf) = &label_filter {
68        records.retain(|r| r.label.as_ref() == Some(lf));
69    }
70    if json {
71        println!("{}", serde_json::to_string_pretty(&records)?);
72        return Ok(());
73    }
74    if records.is_empty() {
75        println!("no feedback records");
76        return Ok(());
77    }
78    for r in &records {
79        let score = r
80            .score
81            .as_ref()
82            .map(|s| s.0.to_string())
83            .unwrap_or_else(|| "-".into());
84        let label = r
85            .label
86            .as_ref()
87            .map(|l| l.to_string())
88            .unwrap_or_else(|| "-".into());
89        let note = r.note.as_deref().unwrap_or("-");
90        println!(
91            "{} session={} score={} label={} note={}",
92            r.id, r.session_id, score, label, note
93        );
94    }
95    Ok(())
96}