use chrono::{DateTime, Local};
use serde::Serialize;
use super::{
AgentKind, DeliveryAssessment, EventKind, TaskId, TaskStatus, VerifyStatus, WorkgroupId,
};
#[derive(Debug, Clone, Serialize)]
pub struct Task {
pub id: TaskId,
pub agent: AgentKind,
pub custom_agent_name: Option<String>,
pub prompt: String,
pub resolved_prompt: Option<String>,
pub category: Option<String>,
pub status: TaskStatus,
pub parent_task_id: Option<String>,
pub workgroup_id: Option<String>,
pub caller_kind: Option<String>,
pub caller_session_id: Option<String>,
pub agent_session_id: Option<String>,
pub repo_path: Option<String>,
pub worktree_path: Option<String>,
pub worktree_branch: Option<String>,
pub final_head_sha: Option<String>,
pub final_branch: Option<String>,
pub start_sha: Option<String>,
pub log_path: Option<String>,
pub output_path: Option<String>,
pub tokens: Option<i64>,
pub prompt_tokens: Option<i64>,
pub duration_ms: Option<i64>,
pub requested_model: Option<String>,
pub observed_model: Option<String>,
pub cost_usd: Option<f64>,
pub exit_code: Option<i32>,
pub created_at: DateTime<Local>,
pub completed_at: Option<DateTime<Local>>,
pub verify: Option<String>,
pub verify_status: VerifyStatus,
pub pending_reason: Option<String>,
pub read_only: bool,
pub budget: bool,
pub audit_verdict: Option<String>,
pub audit_report_path: Option<String>,
pub delivery_assessment: Option<DeliveryAssessment>,
}
impl Task {
pub fn attributed_model(&self) -> Option<&str> {
self.observed_model.as_deref()
}
pub fn costing_model(&self) -> Option<&str> {
self.observed_model
.as_deref()
.or(self.requested_model.as_deref())
}
pub fn display_model(&self) -> Option<String> {
format_model_display(
self.observed_model.as_deref(),
self.requested_model.as_deref(),
)
}
pub fn agent_display_name(&self) -> &str {
if self.agent == AgentKind::Custom {
self.custom_agent_name.as_deref().unwrap_or("custom")
} else {
self.agent.as_str()
}
}
pub fn delivery_assessment(&self) -> Option<DeliveryAssessment> {
self.delivery_assessment
}
pub fn has_verify_failure(&self) -> bool {
self.verify_status == VerifyStatus::Failed
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Workgroup {
pub id: WorkgroupId,
pub name: String,
pub shared_context: String,
pub created_by: Option<String>,
pub created_at: DateTime<Local>,
pub updated_at: DateTime<Local>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Finding {
pub id: i64,
pub workgroup_id: String,
pub content: String,
pub source_task_id: Option<String>,
pub severity: Option<String>,
pub title: Option<String>,
pub file: Option<String>,
pub lines: Option<String>,
pub category: Option<String>,
pub confidence: Option<String>,
pub verdict: Option<String>,
pub score: Option<String>,
pub note: Option<String>,
pub created_at: DateTime<Local>,
pub updated_at: Option<DateTime<Local>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TaskEvent {
pub task_id: TaskId,
pub timestamp: DateTime<Local>,
pub event_kind: EventKind,
pub detail: String,
pub metadata: Option<serde_json::Value>,
}
impl TaskEvent {
pub fn full_detail(&self) -> &str {
self.metadata
.as_ref()
.and_then(|meta| meta.get("full"))
.and_then(|value| value.as_str())
.unwrap_or(&self.detail)
}
}
#[derive(Debug, Clone, Copy)]
pub enum TaskFilter {
All,
Active,
Running,
Today,
}
#[derive(Debug, Clone)]
pub struct CompletionInfo {
pub tokens: Option<i64>,
pub status: TaskStatus,
pub model: Option<String>,
pub cost_usd: Option<f64>,
pub exit_code: Option<i32>,
}
pub fn format_model_display(observed: Option<&str>, requested: Option<&str>) -> Option<String> {
match (observed, requested) {
(Some(observed), Some(requested)) if observed != requested => {
Some(format!("{observed} (asked {requested})"))
}
(Some(observed), _) => Some(observed.to_string()),
(None, Some(requested)) => Some(format!("{requested}?")),
(None, None) => None,
}
}
#[cfg(test)]
mod model_display_tests {
use super::format_model_display;
#[test]
fn an_unconfirmed_request_is_marked_as_one() {
assert_eq!(format_model_display(None, Some("gpt-5.6")), Some("gpt-5.6?".to_string()));
}
#[test]
fn a_confirmed_model_is_shown_plainly() {
assert_eq!(
format_model_display(Some("gpt-5.6"), Some("gpt-5.6")),
Some("gpt-5.6".to_string())
);
}
#[test]
fn a_substitution_shows_both() {
assert_eq!(
format_model_display(Some("composer-2"), Some("auto")),
Some("composer-2 (asked auto)".to_string())
);
}
#[test]
fn nothing_known_renders_as_nothing() {
assert_eq!(format_model_display(None, None), None);
}
}