Skip to main content

linesmith_core/segments/
cost.rs

1//! Cost segment: renders session cost in USD. Hidden when the payload
2//! doesn't carry cost metrics (currently always present in Claude Code).
3
4use super::{RenderContext, RenderResult, RenderedSegment, Segment, SegmentDefaults};
5use crate::data_context::DataContext;
6use crate::theme::Role;
7
8pub struct CostSegment;
9
10/// Highest droppable priority in the built-in set: cost is useful but
11/// least time-sensitive, so it yields first under width pressure.
12const PRIORITY: u8 = 192;
13
14impl Segment for CostSegment {
15    fn render(&self, ctx: &DataContext, _rc: &RenderContext) -> RenderResult {
16        let Some(cost) = ctx.status.cost.as_ref() else {
17            crate::lsm_debug!("cost: status.cost absent; hiding");
18            return Ok(None);
19        };
20        // Per ADR-0014, `total_cost_usd` is a per-leaf Option; hide
21        // when null. The other CostMetrics leaves are independent —
22        // a malformed `total_lines_added` doesn't blank the cost.
23        let Some(usd) = cost.total_cost_usd else {
24            crate::lsm_debug!("cost: total_cost_usd null; hiding");
25            return Ok(None);
26        };
27        Ok(Some(
28            RenderedSegment::new(format!("${usd:.2}")).with_role(Role::Muted),
29        ))
30    }
31
32    fn defaults(&self) -> SegmentDefaults {
33        SegmentDefaults::with_priority(PRIORITY)
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40    use crate::input::{CostMetrics, ModelInfo, StatusContext, Tool, WorkspaceInfo};
41    use std::path::PathBuf;
42    use std::sync::Arc;
43
44    fn rc() -> RenderContext {
45        RenderContext::new(80)
46    }
47
48    fn ctx(cost: Option<CostMetrics>) -> DataContext {
49        DataContext::new(StatusContext {
50            tool: Tool::ClaudeCode,
51            model: Some(ModelInfo {
52                display_name: "X".into(),
53            }),
54            workspace: Some(WorkspaceInfo {
55                project_dir: PathBuf::from("/repo"),
56                git_worktree: None,
57            }),
58            context_window: None,
59            cost,
60            effort: None,
61            vim: None,
62            output_style: None,
63            agent_name: None,
64            version: None,
65            raw: Arc::new(serde_json::Value::Null),
66        })
67    }
68
69    fn cost_of(usd: f64) -> CostMetrics {
70        CostMetrics {
71            total_cost_usd: Some(usd),
72            total_duration_ms: Some(0),
73            total_api_duration_ms: Some(0),
74            total_lines_added: Some(0),
75            total_lines_removed: Some(0),
76        }
77    }
78
79    #[test]
80    fn renders_two_decimal_places_with_muted_role() {
81        assert_eq!(
82            CostSegment
83                .render(&ctx(Some(cost_of(1.234))), &rc())
84                .unwrap(),
85            Some(RenderedSegment::new("$1.23").with_role(Role::Muted))
86        );
87    }
88
89    #[test]
90    fn renders_zero_cost() {
91        assert_eq!(
92            CostSegment.render(&ctx(Some(cost_of(0.0))), &rc()).unwrap(),
93            Some(RenderedSegment::new("$0.00").with_role(Role::Muted))
94        );
95    }
96
97    #[test]
98    fn hidden_when_cost_absent() {
99        assert_eq!(CostSegment.render(&ctx(None), &rc()).unwrap(), None);
100    }
101
102    #[test]
103    fn defaults_use_expected_priority() {
104        assert_eq!(CostSegment.defaults().priority, PRIORITY);
105    }
106}