cc-context-stats 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/package.json +8 -1
  2. package/scripts/context-stats.sh +1 -1
  3. package/.editorconfig +0 -60
  4. package/.eslintrc.json +0 -35
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -49
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -31
  7. package/.github/PULL_REQUEST_TEMPLATE.md +0 -33
  8. package/.github/dependabot.yml +0 -44
  9. package/.github/workflows/ci.yml +0 -294
  10. package/.github/workflows/release.yml +0 -151
  11. package/.pre-commit-config.yaml +0 -74
  12. package/.prettierrc +0 -33
  13. package/.shellcheckrc +0 -10
  14. package/CHANGELOG.md +0 -187
  15. package/CLAUDE.md +0 -66
  16. package/CODE_OF_CONDUCT.md +0 -59
  17. package/CONTRIBUTING.md +0 -240
  18. package/RELEASE_NOTES.md +0 -19
  19. package/SECURITY.md +0 -44
  20. package/TODOS.md +0 -72
  21. package/assets/logo/favicon.svg +0 -19
  22. package/assets/logo/logo-black.svg +0 -24
  23. package/assets/logo/logo-full.svg +0 -40
  24. package/assets/logo/logo-icon.svg +0 -27
  25. package/assets/logo/logo-mark.svg +0 -28
  26. package/assets/logo/logo-white.svg +0 -24
  27. package/assets/logo/logo-wordmark.svg +0 -6
  28. package/config/settings-example.json +0 -7
  29. package/config/settings-node.json +0 -7
  30. package/config/settings-python.json +0 -7
  31. package/docs/ARCHITECTURE.md +0 -128
  32. package/docs/CSV_FORMAT.md +0 -42
  33. package/docs/DEPLOYMENT.md +0 -71
  34. package/docs/DEVELOPMENT.md +0 -161
  35. package/docs/MODEL_INTELLIGENCE.md +0 -396
  36. package/docs/configuration.md +0 -118
  37. package/docs/context-stats.md +0 -143
  38. package/docs/installation.md +0 -255
  39. package/docs/scripts.md +0 -140
  40. package/docs/troubleshooting.md +0 -278
  41. package/images/claude-statusline-token-graph.gif +0 -0
  42. package/images/claude-statusline.png +0 -0
  43. package/images/context-status-dumbzone.png +0 -0
  44. package/images/context-status.png +0 -0
  45. package/images/statusline-detail.png +0 -0
  46. package/images/token-graph.jpeg +0 -0
  47. package/images/token-graph.png +0 -0
  48. package/images/v1.6.1.png +0 -0
  49. package/install +0 -351
  50. package/install.sh +0 -298
  51. package/jest.config.js +0 -11
  52. package/pyproject.toml +0 -115
  53. package/requirements-dev.txt +0 -12
  54. package/scripts/statusline-full.sh +0 -438
  55. package/scripts/statusline-git.sh +0 -88
  56. package/scripts/statusline-minimal.sh +0 -67
  57. package/scripts/statusline.py +0 -569
  58. package/src/claude_statusline/__init__.py +0 -11
  59. package/src/claude_statusline/__main__.py +0 -6
  60. package/src/claude_statusline/cli/__init__.py +0 -1
  61. package/src/claude_statusline/cli/context_stats.py +0 -542
  62. package/src/claude_statusline/cli/explain.py +0 -228
  63. package/src/claude_statusline/cli/statusline.py +0 -184
  64. package/src/claude_statusline/core/__init__.py +0 -1
  65. package/src/claude_statusline/core/colors.py +0 -124
  66. package/src/claude_statusline/core/config.py +0 -165
  67. package/src/claude_statusline/core/git.py +0 -78
  68. package/src/claude_statusline/core/state.py +0 -323
  69. package/src/claude_statusline/formatters/__init__.py +0 -1
  70. package/src/claude_statusline/formatters/layout.py +0 -67
  71. package/src/claude_statusline/formatters/time.py +0 -50
  72. package/src/claude_statusline/formatters/tokens.py +0 -70
  73. package/src/claude_statusline/graphs/__init__.py +0 -1
  74. package/src/claude_statusline/graphs/intelligence.py +0 -162
  75. package/src/claude_statusline/graphs/renderer.py +0 -401
  76. package/src/claude_statusline/graphs/statistics.py +0 -92
  77. package/src/claude_statusline/ui/__init__.py +0 -1
  78. package/src/claude_statusline/ui/icons.py +0 -93
  79. package/src/claude_statusline/ui/waiting.py +0 -62
  80. package/tests/bash/test_delta_parity.bats +0 -199
  81. package/tests/bash/test_install.bats +0 -29
  82. package/tests/bash/test_parity.bats +0 -315
  83. package/tests/bash/test_statusline_full.bats +0 -139
  84. package/tests/bash/test_statusline_git.bats +0 -42
  85. package/tests/bash/test_statusline_minimal.bats +0 -37
  86. package/tests/fixtures/json/comma_in_path.json +0 -31
  87. package/tests/fixtures/json/high_usage.json +0 -17
  88. package/tests/fixtures/json/low_usage.json +0 -17
  89. package/tests/fixtures/json/medium_usage.json +0 -17
  90. package/tests/fixtures/json/valid_full.json +0 -30
  91. package/tests/fixtures/json/valid_minimal.json +0 -9
  92. package/tests/fixtures/mi_test_vectors.json +0 -140
  93. package/tests/node/intelligence.test.js +0 -98
  94. package/tests/node/rotation.test.js +0 -89
  95. package/tests/node/statusline.test.js +0 -240
  96. package/tests/python/conftest.py +0 -84
  97. package/tests/python/test_colors.py +0 -105
  98. package/tests/python/test_config_colors.py +0 -78
  99. package/tests/python/test_data_pipeline.py +0 -446
  100. package/tests/python/test_explain.py +0 -177
  101. package/tests/python/test_icons.py +0 -152
  102. package/tests/python/test_intelligence.py +0 -314
  103. package/tests/python/test_layout.py +0 -127
  104. package/tests/python/test_state_rotation_validation.py +0 -232
  105. package/tests/python/test_statusline.py +0 -215
  106. package/tests/python/test_waiting.py +0 -127
@@ -1,70 +0,0 @@
1
- """Token formatting utilities."""
2
-
3
- from __future__ import annotations
4
-
5
-
6
- def format_tokens(count: int, detail: bool = True) -> str:
7
- """Format token count for display.
8
-
9
- Args:
10
- count: Number of tokens
11
- detail: If True, show exact count with commas. If False, use abbreviated format.
12
-
13
- Returns:
14
- Formatted string like "64,000" or "64.0k"
15
- """
16
- if detail:
17
- return f"{count:,}"
18
- else:
19
- if count >= 1_000_000:
20
- return f"{count / 1_000_000:.1f}M"
21
- elif count >= 1_000:
22
- return f"{count / 1_000:.1f}k"
23
- else:
24
- return str(count)
25
-
26
-
27
- def format_percentage(value: float, decimals: int = 1) -> str:
28
- """Format a percentage value.
29
-
30
- Args:
31
- value: Percentage value (0-100)
32
- decimals: Number of decimal places
33
-
34
- Returns:
35
- Formatted percentage string like "75.5%"
36
- """
37
- return f"{value:.{decimals}f}%"
38
-
39
-
40
- def calculate_context_usage(
41
- used_tokens: int,
42
- total_size: int,
43
- autocompact_enabled: bool = True,
44
- autocompact_ratio: float = 0.225,
45
- ) -> tuple[int, float, int]:
46
- """Calculate context window usage statistics.
47
-
48
- Args:
49
- used_tokens: Number of tokens currently used
50
- total_size: Total context window size
51
- autocompact_enabled: Whether autocompact is enabled
52
- autocompact_ratio: Ratio of context window reserved for autocompact (default 22.5%)
53
-
54
- Returns:
55
- Tuple of (free_tokens, free_percentage, autocompact_buffer)
56
- """
57
- if total_size <= 0:
58
- return 0, 0.0, 0
59
-
60
- autocompact_buffer = int(total_size * autocompact_ratio)
61
-
62
- if autocompact_enabled:
63
- free_tokens = total_size - used_tokens - autocompact_buffer
64
- else:
65
- free_tokens = total_size - used_tokens
66
-
67
- free_tokens = max(0, free_tokens)
68
- free_pct = (free_tokens * 100.0) / total_size
69
-
70
- return free_tokens, free_pct, autocompact_buffer
@@ -1 +0,0 @@
1
- """Graph rendering utilities for token visualization."""
@@ -1,162 +0,0 @@
1
- """Model Intelligence (MI) score computation.
2
-
3
- Estimates answer quality based on context utilization, cache efficiency,
4
- and output productivity. Inspired by the Michelangelo paper
5
- (arXiv:2409.12640, Google DeepMind, Sep 2024).
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from dataclasses import dataclass
11
-
12
- from claude_statusline.core.state import StateEntry
13
-
14
- # Hardcoded constants — not configurable, to minimize cross-implementation sync burden
15
- MI_WEIGHT_CPS = 0.60
16
- MI_WEIGHT_ES = 0.25
17
- MI_WEIGHT_PS = 0.15
18
- MI_GREEN_THRESHOLD = 0.65
19
- MI_YELLOW_THRESHOLD = 0.35
20
- MI_PRODUCTIVITY_TARGET = 0.2
21
-
22
-
23
- @dataclass
24
- class IntelligenceConfig:
25
- """Configuration for MI computation."""
26
-
27
- beta: float = 1.5
28
-
29
-
30
- @dataclass
31
- class IntelligenceScore:
32
- """MI score with sub-components."""
33
-
34
- cps: float
35
- es: float
36
- ps: float
37
- mi: float
38
- utilization: float
39
-
40
-
41
- def calculate_context_pressure(utilization: float, beta: float = 1.5) -> float:
42
- """Calculate Context Pressure Score (CPS).
43
-
44
- CPS = max(0, 1 - u^beta) where u is utilization ratio [0, 1+].
45
-
46
- Args:
47
- utilization: Context utilization ratio (current_used / context_window_size)
48
- beta: Curve shape parameter (default 1.5)
49
-
50
- Returns:
51
- CPS value in [0, 1]
52
- """
53
- if utilization <= 0:
54
- return 1.0
55
- return max(0.0, 1.0 - utilization**beta)
56
-
57
-
58
- def calculate_efficiency(entry: StateEntry) -> float:
59
- """Calculate Efficiency Score (ES).
60
-
61
- ES = 0.3 + 0.7 * cache_hit_ratio, where cache_hit_ratio = cache_read / total_context.
62
-
63
- Args:
64
- entry: Current state entry
65
-
66
- Returns:
67
- ES value in [0.3, 1.0]
68
- """
69
- total_context = entry.current_used_tokens
70
- if total_context == 0:
71
- return 1.0
72
- cache_hit_ratio = entry.cache_read / total_context
73
- return 0.3 + 0.7 * cache_hit_ratio
74
-
75
-
76
- def calculate_productivity(
77
- current: StateEntry, previous: StateEntry | None
78
- ) -> float:
79
- """Calculate Productivity Score (PS).
80
-
81
- Uses consecutive entry diffs for delta_lines and delta_output_tokens.
82
-
83
- Args:
84
- current: Current state entry
85
- previous: Previous state entry, or None
86
-
87
- Returns:
88
- PS value in [0.2, 1.0], or 0.5 if no previous entry
89
- """
90
- if previous is None:
91
- return 0.5
92
-
93
- delta_lines_added = current.lines_added - previous.lines_added
94
- delta_lines_removed = current.lines_removed - previous.lines_removed
95
- delta_output_tokens = current.total_output_tokens - previous.total_output_tokens
96
-
97
- if delta_output_tokens <= 0:
98
- return 0.5
99
-
100
- delta_lines = delta_lines_added + delta_lines_removed
101
- ratio = delta_lines / delta_output_tokens
102
- normalized = min(1.0, ratio / MI_PRODUCTIVITY_TARGET)
103
- return 0.2 + 0.8 * normalized
104
-
105
-
106
- def calculate_intelligence(
107
- current: StateEntry,
108
- previous: StateEntry | None,
109
- context_window_size: int,
110
- beta: float = 1.5,
111
- ) -> IntelligenceScore:
112
- """Calculate composite Model Intelligence score.
113
-
114
- Args:
115
- current: Current state entry
116
- previous: Previous state entry (for productivity delta)
117
- context_window_size: Total context window size in tokens
118
- beta: CPS curve shape parameter
119
-
120
- Returns:
121
- IntelligenceScore with all sub-scores and composite MI
122
- """
123
- # Guard clause: unknown context window
124
- if context_window_size == 0:
125
- return IntelligenceScore(cps=1.0, es=1.0, ps=0.5, mi=1.0, utilization=0.0)
126
-
127
- utilization = current.current_used_tokens / context_window_size
128
- cps = calculate_context_pressure(utilization, beta)
129
- es = calculate_efficiency(current)
130
- ps = calculate_productivity(current, previous)
131
-
132
- mi = MI_WEIGHT_CPS * cps + MI_WEIGHT_ES * es + MI_WEIGHT_PS * ps
133
-
134
- return IntelligenceScore(cps=cps, es=es, ps=ps, mi=mi, utilization=utilization)
135
-
136
-
137
- def get_mi_color(mi: float) -> str:
138
- """Get color name for MI score.
139
-
140
- Args:
141
- mi: MI score value
142
-
143
- Returns:
144
- Color name: "green", "yellow", or "red"
145
- """
146
- if mi > MI_GREEN_THRESHOLD:
147
- return "green"
148
- if mi > MI_YELLOW_THRESHOLD:
149
- return "yellow"
150
- return "red"
151
-
152
-
153
- def format_mi_score(mi: float) -> str:
154
- """Format MI score for display.
155
-
156
- Args:
157
- mi: MI score value
158
-
159
- Returns:
160
- Formatted string like "0.82"
161
- """
162
- return f"{mi:.2f}"
@@ -1,401 +0,0 @@
1
- """ASCII graph rendering engine."""
2
-
3
- from __future__ import annotations
4
-
5
- import shutil
6
- from collections.abc import Callable
7
- from dataclasses import dataclass
8
-
9
- from claude_statusline.core.colors import ColorManager
10
- from claude_statusline.formatters.time import format_duration, format_timestamp
11
- from claude_statusline.formatters.tokens import format_tokens
12
- from claude_statusline.graphs.statistics import calculate_deltas, calculate_stats
13
-
14
-
15
- @dataclass
16
- class GraphDimensions:
17
- """Terminal and graph dimensions."""
18
-
19
- term_width: int
20
- term_height: int
21
- graph_width: int
22
- graph_height: int
23
-
24
- @classmethod
25
- def detect(cls) -> GraphDimensions:
26
- """Detect terminal dimensions and calculate graph size."""
27
- term_size = shutil.get_terminal_size((80, 24))
28
- term_width = term_size.columns
29
- term_height = term_size.lines
30
-
31
- # Calculate graph dimensions
32
- graph_width = term_width - 15 # Reserve space for Y-axis labels
33
- graph_height = term_height // 3 # Each graph takes 1/3 of terminal
34
-
35
- # Enforce minimums and maximums
36
- graph_width = max(30, graph_width)
37
- graph_height = max(8, min(20, graph_height))
38
-
39
- return cls(
40
- term_width=term_width,
41
- term_height=term_height,
42
- graph_width=graph_width,
43
- graph_height=graph_height,
44
- )
45
-
46
-
47
- class GraphRenderer:
48
- """ASCII graph rendering engine."""
49
-
50
- # Characters for graph rendering
51
- DOT = "●"
52
- FILL_LIGHT = "▒"
53
- FILL_DARK = "░"
54
-
55
- def __init__(
56
- self,
57
- colors: ColorManager | None = None,
58
- dimensions: GraphDimensions | None = None,
59
- token_detail: bool = True,
60
- ) -> None:
61
- """Initialize graph renderer.
62
-
63
- Args:
64
- colors: ColorManager instance. Creates default if None.
65
- dimensions: GraphDimensions instance. Detects if None.
66
- token_detail: Whether to show detailed token counts.
67
- """
68
- self.colors = colors or ColorManager(enabled=True)
69
- self.dimensions = dimensions or GraphDimensions.detect()
70
- self.token_detail = token_detail
71
- self._output_lines: list[str] | None = None
72
-
73
- def begin_buffering(self) -> None:
74
- """Start buffering output instead of printing directly."""
75
- self._output_lines = []
76
-
77
- def get_buffer(self) -> str:
78
- """Return buffered output as a single string and stop buffering."""
79
- if self._output_lines is None:
80
- return ""
81
- result = "\n".join(self._output_lines)
82
- self._output_lines = None
83
- return result
84
-
85
- def _emit(self, line: str = "") -> None:
86
- """Emit a line of output. Buffers if buffering is active, otherwise prints."""
87
- if self._output_lines is not None:
88
- self._output_lines.append(line)
89
- else:
90
- print(line)
91
-
92
- def render_timeseries(
93
- self,
94
- data: list[int],
95
- timestamps: list[int],
96
- title: str,
97
- color: str,
98
- label_fn: Callable[[int], str] | None = None,
99
- ) -> None:
100
- """Render a timeseries ASCII graph.
101
-
102
- Args:
103
- data: List of values to plot
104
- timestamps: Corresponding timestamps for x-axis labels
105
- title: Graph title
106
- color: ANSI color code for the graph
107
- label_fn: Optional function to format Y-axis labels. If None, uses format_tokens().
108
- """
109
- n = len(data)
110
- if n == 0:
111
- return
112
-
113
- stats = calculate_stats(data)
114
- min_val = stats.min_val
115
- max_val = stats.max_val
116
-
117
- # Avoid division by zero
118
- if min_val == max_val:
119
- max_val = min_val + 1
120
- value_range = max_val - min_val
121
-
122
- width = self.dimensions.graph_width
123
- height = self.dimensions.graph_height
124
-
125
- fmt = label_fn if label_fn else lambda v: format_tokens(v, self.token_detail)
126
-
127
- # Print title and stats
128
- self._emit()
129
- self._emit(f"{self.colors.bold}{title}{self.colors.reset}")
130
- self._emit(
131
- f"{self.colors.dim}Max: {fmt(max_val)} "
132
- f"Min: {fmt(min_val)} "
133
- f"Points: {n}{self.colors.reset}"
134
- )
135
- self._emit()
136
-
137
- # Build the graph grid
138
- grid = self._build_grid(data, min_val, max_val, value_range, width, height)
139
-
140
- # Print grid with Y-axis labels
141
- for r in range(height):
142
- val = max_val - r * value_range // (height - 1)
143
-
144
- # Show labels at top, middle, and bottom
145
- if r == 0 or r == height // 2 or r == height - 1:
146
- label = fmt(val)
147
- else:
148
- label = ""
149
-
150
- row_data = grid[r] if r < len(grid) else " " * width
151
- self._emit(
152
- f"{label:>10} {self.colors.dim}│{self.colors.reset}"
153
- f"{color}{row_data}{self.colors.reset}"
154
- )
155
-
156
- # X-axis
157
- self._emit(f"{'':>10} {self.colors.dim}└{'─' * width}{self.colors.reset}")
158
-
159
- # Time labels
160
- if timestamps:
161
- first_time = format_timestamp(timestamps[0])
162
- last_time = format_timestamp(timestamps[-1])
163
- mid_idx = (n - 1) // 2
164
- mid_time = format_timestamp(timestamps[mid_idx]) if n > 2 else ""
165
-
166
- spacing = width // 3
167
- self._emit(
168
- f"{' ':>11}{self.colors.dim}"
169
- f"{first_time:<{spacing}}{mid_time}{last_time:>{spacing}}"
170
- f"{self.colors.reset}"
171
- )
172
-
173
- def _build_grid(
174
- self,
175
- data: list[int],
176
- min_val: int,
177
- max_val: int,
178
- value_range: int,
179
- width: int,
180
- height: int,
181
- ) -> list[str]:
182
- """Build the ASCII grid for the graph.
183
-
184
- Args:
185
- data: List of values to plot
186
- min_val: Minimum value in data
187
- max_val: Maximum value in data
188
- value_range: max_val - min_val
189
- width: Graph width in characters
190
- height: Graph height in rows
191
-
192
- Returns:
193
- List of strings, one per row
194
- """
195
- n = len(data)
196
- if n == 0:
197
- return [" " * width for _ in range(height)]
198
-
199
- # Initialize grid with empty spaces
200
- grid = [[" " for _ in range(width)] for _ in range(height)]
201
-
202
- # Calculate y positions for each data point
203
- data_x = []
204
- data_y = []
205
- for i, val in enumerate(data):
206
- # Map index to x coordinate
207
- if n == 1:
208
- x = width // 2
209
- else:
210
- x = int((i) * (width - 1) / (n - 1))
211
- x = max(0, min(width - 1, x))
212
-
213
- # Map value to y coordinate (inverted: 0=top)
214
- if value_range == 0:
215
- y = height // 2
216
- else:
217
- y = int((max_val - val) * (height - 1) / value_range)
218
- y = max(0, min(height - 1, y))
219
-
220
- data_x.append(x)
221
- data_y.append(y)
222
-
223
- # Interpolate between points to fill every x position
224
- line_y = [-1.0] * width
225
- for i in range(len(data) - 1):
226
- x1, y1 = data_x[i], data_y[i]
227
- x2, y2 = data_x[i + 1], data_y[i + 1]
228
-
229
- # Ensure we don't go out of bounds
230
- for x in range(x1, min(x2 + 1, width)):
231
- if x2 == x1:
232
- y_interp = float(y1)
233
- else:
234
- # Linear interpolation
235
- t = (x - x1) / (x2 - x1)
236
- y_interp = y1 + t * (y2 - y1)
237
- line_y[x] = y_interp
238
-
239
- # Draw filled area and line
240
- for c in range(width):
241
- if line_y[c] >= 0:
242
- line_row = int(line_y[c] + 0.5) # Round to nearest integer
243
- line_row = max(0, min(height - 1, line_row))
244
-
245
- # Fill area below the line with gradient
246
- for r in range(line_row, height):
247
- if r == line_row:
248
- grid[r][c] = self.DOT
249
- elif r < line_row + 2:
250
- grid[r][c] = self.FILL_LIGHT
251
- else:
252
- grid[r][c] = self.FILL_DARK
253
-
254
- # Mark actual data points
255
- for i in range(len(data)):
256
- x = data_x[i]
257
- y = max(0, min(height - 1, int(data_y[i] + 0.5)))
258
- grid[y][x] = self.DOT
259
-
260
- # Convert grid to strings
261
- return ["".join(row) for row in grid]
262
-
263
- def render_summary(
264
- self,
265
- entries: list, # list[StateEntry]
266
- deltas: list[int],
267
- mi_score: object | None = None, # IntelligenceScore
268
- ) -> None:
269
- """Render summary statistics.
270
-
271
- Args:
272
- entries: List of StateEntry objects
273
- deltas: List of token deltas
274
- mi_score: Optional IntelligenceScore for MI display
275
- """
276
- if not entries:
277
- return
278
-
279
- first = entries[0]
280
- last = entries[-1]
281
- duration = last.timestamp - first.timestamp
282
-
283
- # Context window info - use current_used_tokens which represents actual context usage
284
- remaining_context = 0
285
- remaining_percentage = 0
286
- usage_percentage = 0
287
- if last.context_window_size > 0:
288
- # current_used_tokens = current_input_tokens + cache_creation + cache_read
289
- current_used = last.current_used_tokens
290
- remaining_context = max(0, last.context_window_size - current_used)
291
- remaining_percentage = remaining_context * 100 // last.context_window_size
292
- usage_percentage = 100 - remaining_percentage
293
-
294
- # Determine status based on context usage
295
- if usage_percentage < 40:
296
- status_color = self.colors.green
297
- status_text = "Smart Zone"
298
- status_hint = "You are in the smart zone"
299
- elif usage_percentage < 80:
300
- status_color = self.colors.yellow
301
- status_text = "Dumb Zone"
302
- status_hint = "You are in the dumb zone - Dex Horthy says so"
303
- else:
304
- status_color = self.colors.red
305
- status_text = "Wrap Up Zone"
306
- status_hint = "Better to wrap up and start a new session"
307
-
308
- self._emit()
309
- self._emit(f"{self.colors.bold}Session Summary{self.colors.reset}")
310
- line_width = self.dimensions.graph_width + 11
311
- self._emit(f"{self.colors.dim}{'-' * line_width}{self.colors.reset}")
312
-
313
- # Context remaining (before status)
314
- if last.context_window_size > 0:
315
- self._emit(
316
- f" {status_color}{'Context Remaining:':<20}{self.colors.reset} "
317
- f"{format_tokens(remaining_context, self.token_detail)}/{format_tokens(last.context_window_size, self.token_detail)} ({remaining_percentage}%)"
318
- )
319
-
320
- # Status indicator - highlighted
321
- if last.context_window_size > 0:
322
- self._emit(
323
- f" {status_color}{self.colors.bold}>>> {status_text.upper()} <<<{self.colors.reset} "
324
- f"{self.colors.dim}({status_hint}){self.colors.reset}"
325
- )
326
-
327
- # Model Intelligence score
328
- if mi_score is not None:
329
- from claude_statusline.graphs.intelligence import (
330
- format_mi_score,
331
- get_mi_color,
332
- )
333
-
334
- mi_color_name = get_mi_color(mi_score.mi)
335
- mi_color = getattr(self.colors, mi_color_name)
336
- if mi_score.mi > 0.65:
337
- mi_hint = "Model is operating well"
338
- elif mi_score.mi > 0.35:
339
- mi_hint = "Context pressure is degrading answer quality"
340
- else:
341
- mi_hint = "Severely degraded, consider new session"
342
- self._emit(
343
- f" {mi_color}{'Model Intelligence:':<20}{self.colors.reset} "
344
- f"{format_mi_score(mi_score.mi)} "
345
- f"{self.colors.dim}({mi_hint}){self.colors.reset}"
346
- )
347
- self._emit(
348
- f" {self.colors.dim} CPS: {mi_score.cps:.2f} "
349
- f"ES: {mi_score.es:.2f} "
350
- f"PS: {mi_score.ps:.2f}{self.colors.reset}"
351
- )
352
-
353
- if last.context_window_size > 0:
354
- self._emit()
355
-
356
- # Session details (ordered: Last Growth, I/O, Lines, Cost, Model, Duration)
357
- if deltas:
358
- current_growth = deltas[-1]
359
- self._emit(
360
- f" {self.colors.cyan}{'Last Growth:':<20}{self.colors.reset} "
361
- f"+{format_tokens(current_growth, self.token_detail)}"
362
- )
363
- self._emit(
364
- f" {self.colors.blue}{'Input Tokens:':<20}{self.colors.reset} "
365
- f"{format_tokens(last.current_input_tokens, self.token_detail)}"
366
- )
367
- self._emit(
368
- f" {self.colors.magenta}{'Output Tokens:':<20}{self.colors.reset} "
369
- f"{format_tokens(last.current_output_tokens, self.token_detail)}"
370
- )
371
- if last.lines_added > 0 or last.lines_removed > 0:
372
- self._emit(
373
- f" {self.colors.dim}{'Lines Changed:':<20}{self.colors.reset} "
374
- f"{self.colors.green}+{last.lines_added:,}{self.colors.reset} / "
375
- f"{self.colors.red}-{last.lines_removed:,}{self.colors.reset}"
376
- )
377
- if last.cost_usd > 0:
378
- self._emit(
379
- f" {self.colors.yellow}{'Total Cost:':<20}{self.colors.reset} ${last.cost_usd:.4f}"
380
- )
381
- if last.model_id:
382
- self._emit(f" {self.colors.dim}{'Model:':<20}{self.colors.reset} {last.model_id}")
383
- self._emit(
384
- f" {self.colors.cyan}{'Session Duration:':<20}{self.colors.reset} "
385
- f"{format_duration(duration)}"
386
- )
387
- self._emit()
388
-
389
- def render_footer(self, version: str = "1.6.1", commit_hash: str = "dev") -> None:
390
- """Render the footer with version info.
391
-
392
- Args:
393
- version: Package version
394
- commit_hash: Git commit hash
395
- """
396
- self._emit(
397
- f"{self.colors.dim}Powered by {self.colors.cyan}cc-context-stats"
398
- f"{self.colors.dim} v{version}-{commit_hash} - "
399
- f"https://github.com/luongnv89/cc-context-stats{self.colors.reset}"
400
- )
401
- self._emit()