1use super::{RenderContext, RenderResult, RenderedSegment, Segment, SegmentDefaults};
6use crate::data_context::DataContext;
7use crate::theme::Role;
8
9pub struct ContextWindowSegment;
10
11const PRIORITY: u8 = 32;
15
16impl Segment for ContextWindowSegment {
17 fn render(&self, ctx: &DataContext, _rc: &RenderContext) -> RenderResult {
18 let Some(cw) = ctx.status.context_window.as_ref() else {
19 crate::lsm_debug!("context_window: status.context_window absent; hiding");
20 return Ok(None);
21 };
22 let text = match (cw.used, cw.size) {
28 (Some(used), Some(size)) => format!("{:.0}% · {}", used.value(), format_size(size)),
29 (None, Some(size)) => format_size(size),
30 (Some(used), None) => format!("{:.0}%", used.value()),
31 (None, None) => {
32 crate::lsm_debug!("context_window: used and size both null; hiding");
33 return Ok(None);
34 }
35 };
36 Ok(Some(RenderedSegment::new(text).with_role(Role::Info)))
37 }
38
39 fn defaults(&self) -> SegmentDefaults {
40 SegmentDefaults::with_priority(PRIORITY)
41 }
42}
43
44fn format_size(size: u32) -> String {
45 if size >= 1_000_000 && size.is_multiple_of(1_000_000) {
46 format!("{}M", size / 1_000_000)
47 } else if size >= 1_000 && size.is_multiple_of(1_000) {
48 format!("{}k", size / 1_000)
49 } else {
50 size.to_string()
51 }
52}
53
54#[cfg(test)]
55fn format_size_for(size: u32) -> String {
56 format_size(size)
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::input::{ContextWindow, ModelInfo, Percent, StatusContext, Tool, WorkspaceInfo};
63 use std::path::PathBuf;
64 use std::sync::Arc;
65
66 fn rc() -> RenderContext {
67 RenderContext::new(80)
68 }
69
70 fn ctx(window: Option<ContextWindow>) -> DataContext {
71 DataContext::new(StatusContext {
72 tool: Tool::ClaudeCode,
73 model: Some(ModelInfo {
74 display_name: "X".into(),
75 }),
76 workspace: Some(WorkspaceInfo {
77 project_dir: PathBuf::from("/repo"),
78 git_worktree: None,
79 }),
80 context_window: window,
81 cost: None,
82 effort: None,
83 vim: None,
84 output_style: None,
85 agent_name: None,
86 version: None,
87 raw: Arc::new(serde_json::Value::Null),
88 })
89 }
90
91 fn window(used: f32, size: u32) -> ContextWindow {
92 ContextWindow {
93 used: Some(Percent::new(used).expect("in range")),
94 size: Some(size),
95 total_input_tokens: Some(0),
96 total_output_tokens: Some(0),
97 current_usage: None,
98 }
99 }
100
101 #[test]
102 fn renders_percent_and_sonnet_200k() {
103 assert_eq!(
104 ContextWindowSegment
105 .render(&ctx(Some(window(42.3, 200_000))), &rc())
106 .unwrap(),
107 Some(RenderedSegment::new("42% · 200k").with_role(Role::Info))
108 );
109 }
110
111 #[test]
112 fn renders_one_million_size_as_m_suffix() {
113 assert_eq!(
114 ContextWindowSegment
115 .render(&ctx(Some(window(5.0, 1_000_000))), &rc())
116 .unwrap(),
117 Some(RenderedSegment::new("5% · 1M").with_role(Role::Info))
118 );
119 }
120
121 #[test]
122 fn renders_non_round_size_literally() {
123 assert_eq!(
124 ContextWindowSegment
125 .render(&ctx(Some(window(10.0, 131_072))), &rc())
126 .unwrap(),
127 Some(RenderedSegment::new("10% · 131072").with_role(Role::Info))
128 );
129 }
130
131 #[test]
132 fn rounds_percent_to_nearest_integer() {
133 assert_eq!(
134 ContextWindowSegment
135 .render(&ctx(Some(window(99.9, 200_000))), &rc())
136 .unwrap(),
137 Some(RenderedSegment::new("100% · 200k").with_role(Role::Info))
138 );
139 }
140
141 #[test]
142 fn hidden_when_context_window_absent() {
143 assert_eq!(
144 ContextWindowSegment.render(&ctx(None), &rc()).unwrap(),
145 None
146 );
147 }
148
149 #[test]
150 fn renders_size_only_when_used_is_null() {
151 let cw = ContextWindow {
155 used: None,
156 size: Some(200_000),
157 total_input_tokens: Some(0),
158 total_output_tokens: Some(0),
159 current_usage: None,
160 };
161 assert_eq!(
162 ContextWindowSegment.render(&ctx(Some(cw)), &rc()).unwrap(),
163 Some(RenderedSegment::new("200k").with_role(Role::Info))
164 );
165 }
166
167 #[test]
168 fn renders_percent_only_when_size_is_null() {
169 let cw = ContextWindow {
172 used: Some(Percent::new(42.0).expect("in range")),
173 size: None,
174 total_input_tokens: Some(0),
175 total_output_tokens: Some(0),
176 current_usage: None,
177 };
178 assert_eq!(
179 ContextWindowSegment.render(&ctx(Some(cw)), &rc()).unwrap(),
180 Some(RenderedSegment::new("42%").with_role(Role::Info))
181 );
182 }
183
184 #[test]
185 fn hidden_when_both_used_and_size_null() {
186 let cw = ContextWindow {
187 used: None,
188 size: None,
189 total_input_tokens: None,
190 total_output_tokens: None,
191 current_usage: None,
192 };
193 assert_eq!(
194 ContextWindowSegment.render(&ctx(Some(cw)), &rc()).unwrap(),
195 None
196 );
197 }
198
199 #[test]
200 fn format_size_round_values_use_k_suffix() {
201 assert_eq!(format_size_for(1_000), "1k");
202 assert_eq!(format_size_for(200_000), "200k");
203 }
204
205 #[test]
206 fn format_size_round_millions_use_m_suffix() {
207 assert_eq!(format_size_for(1_000_000), "1M");
208 assert_eq!(format_size_for(2_000_000), "2M");
209 }
210
211 #[test]
212 fn format_size_non_round_values_rendered_literally() {
213 assert_eq!(format_size_for(999), "999");
214 assert_eq!(format_size_for(131_072), "131072");
215 assert_eq!(format_size_for(1_500_000), "1500k");
217 }
218
219 #[test]
220 fn format_size_zero_renders_literally() {
221 assert_eq!(format_size_for(0), "0");
225 }
226
227 #[test]
228 fn defaults_use_expected_priority() {
229 assert_eq!(ContextWindowSegment.defaults().priority, PRIORITY);
230 }
231}