const TRACKER_CONTINUE_ITEM_CAP: usize = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum TrackerProbeOutcome {
Incomplete(Vec<String>),
Complete,
Unavailable,
}
pub(crate) fn tracker_probe_outcome(payload: &serde_json::Value) -> TrackerProbeOutcome {
let Some(status) = payload.get("status").and_then(serde_json::Value::as_str) else {
return TrackerProbeOutcome::Unavailable;
};
if status == "empty" {
return TrackerProbeOutcome::Complete;
}
let Some(checklist) = payload.get("checklist") else {
return TrackerProbeOutcome::Unavailable;
};
let Some(raw_items) = checklist.get("items").and_then(serde_json::Value::as_array) else {
return TrackerProbeOutcome::Unavailable;
};
let items: Vec<String> = raw_items
.iter()
.filter(|item| item.get("status").and_then(serde_json::Value::as_str) != Some("completed"))
.filter_map(|item| {
let description = item.get("description").and_then(serde_json::Value::as_str)?;
let status = item.get("status").and_then(serde_json::Value::as_str).unwrap_or("pending");
let index = item.get("index").and_then(serde_json::Value::as_u64);
Some(match index {
Some(index) if index > 0 => format!("#{} {} ({})", index, description, status),
_ => format!("{} ({})", description, status),
})
})
.take(TRACKER_CONTINUE_ITEM_CAP)
.collect();
if items.is_empty() {
TrackerProbeOutcome::Complete
} else {
TrackerProbeOutcome::Incomplete(items)
}
}
#[cfg(test)]
pub(crate) fn parse_incomplete_tracker_items(payload: &serde_json::Value) -> Option<Vec<String>> {
match tracker_probe_outcome(payload) {
TrackerProbeOutcome::Incomplete(items) => Some(items),
TrackerProbeOutcome::Complete | TrackerProbeOutcome::Unavailable => None,
}
}
pub(crate) fn parse_tracker_completed_count(payload: &serde_json::Value) -> u32 {
let Some(status) = payload.get("status").and_then(serde_json::Value::as_str) else {
return 0;
};
if status == "empty" {
return 0;
}
payload
.get("checklist")
.and_then(|c| c.get("items"))
.and_then(serde_json::Value::as_array)
.into_iter()
.flatten()
.filter(|item| item.get("status").and_then(serde_json::Value::as_str) == Some("completed"))
.count() as u32
}
pub(crate) fn apply_tracker_probe_to_cache(
cache: &mut Option<Vec<String>>,
probe: TrackerProbeOutcome,
) -> Option<&[String]> {
match probe {
TrackerProbeOutcome::Incomplete(items) => {
*cache = Some(items);
}
TrackerProbeOutcome::Complete => {
*cache = None;
}
TrackerProbeOutcome::Unavailable => {}
}
cache.as_deref()
}
pub(crate) async fn incomplete_tracker_items(
tool_registry: &vtcode_core::tools::registry::ToolRegistry,
) -> Option<Vec<String>> {
match probe_tracker_incomplete(tool_registry).await {
TrackerProbeOutcome::Incomplete(items) => Some(items),
TrackerProbeOutcome::Complete | TrackerProbeOutcome::Unavailable => None,
}
}
pub(crate) async fn probe_tracker_incomplete(
tool_registry: &vtcode_core::tools::registry::ToolRegistry,
) -> TrackerProbeOutcome {
let Some(tool) = tool_registry.get_tool(vtcode_core::config::constants::tools::TASK_TRACKER) else {
return TrackerProbeOutcome::Unavailable;
};
let Ok(payload) = tool.execute(serde_json::json!({ "action": "list" })).await else {
return TrackerProbeOutcome::Unavailable;
};
tracker_probe_outcome(&payload)
}
pub(crate) async fn tracker_completed_count(tool_registry: &vtcode_core::tools::registry::ToolRegistry) -> Option<u32> {
let tool = tool_registry.get_tool(vtcode_core::config::constants::tools::TASK_TRACKER)?;
let payload = tool.execute(serde_json::json!({ "action": "list" })).await.ok()?;
Some(parse_tracker_completed_count(&payload))
}