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.
- package/package.json +8 -1
- package/scripts/context-stats.sh +1 -1
- package/.editorconfig +0 -60
- package/.eslintrc.json +0 -35
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -49
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -31
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -33
- package/.github/dependabot.yml +0 -44
- package/.github/workflows/ci.yml +0 -294
- package/.github/workflows/release.yml +0 -151
- package/.pre-commit-config.yaml +0 -74
- package/.prettierrc +0 -33
- package/.shellcheckrc +0 -10
- package/CHANGELOG.md +0 -187
- package/CLAUDE.md +0 -66
- package/CODE_OF_CONDUCT.md +0 -59
- package/CONTRIBUTING.md +0 -240
- package/RELEASE_NOTES.md +0 -19
- package/SECURITY.md +0 -44
- package/TODOS.md +0 -72
- package/assets/logo/favicon.svg +0 -19
- package/assets/logo/logo-black.svg +0 -24
- package/assets/logo/logo-full.svg +0 -40
- package/assets/logo/logo-icon.svg +0 -27
- package/assets/logo/logo-mark.svg +0 -28
- package/assets/logo/logo-white.svg +0 -24
- package/assets/logo/logo-wordmark.svg +0 -6
- package/config/settings-example.json +0 -7
- package/config/settings-node.json +0 -7
- package/config/settings-python.json +0 -7
- package/docs/ARCHITECTURE.md +0 -128
- package/docs/CSV_FORMAT.md +0 -42
- package/docs/DEPLOYMENT.md +0 -71
- package/docs/DEVELOPMENT.md +0 -161
- package/docs/MODEL_INTELLIGENCE.md +0 -396
- package/docs/configuration.md +0 -118
- package/docs/context-stats.md +0 -143
- package/docs/installation.md +0 -255
- package/docs/scripts.md +0 -140
- package/docs/troubleshooting.md +0 -278
- package/images/claude-statusline-token-graph.gif +0 -0
- package/images/claude-statusline.png +0 -0
- package/images/context-status-dumbzone.png +0 -0
- package/images/context-status.png +0 -0
- package/images/statusline-detail.png +0 -0
- package/images/token-graph.jpeg +0 -0
- package/images/token-graph.png +0 -0
- package/images/v1.6.1.png +0 -0
- package/install +0 -351
- package/install.sh +0 -298
- package/jest.config.js +0 -11
- package/pyproject.toml +0 -115
- package/requirements-dev.txt +0 -12
- package/scripts/statusline-full.sh +0 -438
- package/scripts/statusline-git.sh +0 -88
- package/scripts/statusline-minimal.sh +0 -67
- package/scripts/statusline.py +0 -569
- package/src/claude_statusline/__init__.py +0 -11
- package/src/claude_statusline/__main__.py +0 -6
- package/src/claude_statusline/cli/__init__.py +0 -1
- package/src/claude_statusline/cli/context_stats.py +0 -542
- package/src/claude_statusline/cli/explain.py +0 -228
- package/src/claude_statusline/cli/statusline.py +0 -184
- package/src/claude_statusline/core/__init__.py +0 -1
- package/src/claude_statusline/core/colors.py +0 -124
- package/src/claude_statusline/core/config.py +0 -165
- package/src/claude_statusline/core/git.py +0 -78
- package/src/claude_statusline/core/state.py +0 -323
- package/src/claude_statusline/formatters/__init__.py +0 -1
- package/src/claude_statusline/formatters/layout.py +0 -67
- package/src/claude_statusline/formatters/time.py +0 -50
- package/src/claude_statusline/formatters/tokens.py +0 -70
- package/src/claude_statusline/graphs/__init__.py +0 -1
- package/src/claude_statusline/graphs/intelligence.py +0 -162
- package/src/claude_statusline/graphs/renderer.py +0 -401
- package/src/claude_statusline/graphs/statistics.py +0 -92
- package/src/claude_statusline/ui/__init__.py +0 -1
- package/src/claude_statusline/ui/icons.py +0 -93
- package/src/claude_statusline/ui/waiting.py +0 -62
- package/tests/bash/test_delta_parity.bats +0 -199
- package/tests/bash/test_install.bats +0 -29
- package/tests/bash/test_parity.bats +0 -315
- package/tests/bash/test_statusline_full.bats +0 -139
- package/tests/bash/test_statusline_git.bats +0 -42
- package/tests/bash/test_statusline_minimal.bats +0 -37
- package/tests/fixtures/json/comma_in_path.json +0 -31
- package/tests/fixtures/json/high_usage.json +0 -17
- package/tests/fixtures/json/low_usage.json +0 -17
- package/tests/fixtures/json/medium_usage.json +0 -17
- package/tests/fixtures/json/valid_full.json +0 -30
- package/tests/fixtures/json/valid_minimal.json +0 -9
- package/tests/fixtures/mi_test_vectors.json +0 -140
- package/tests/node/intelligence.test.js +0 -98
- package/tests/node/rotation.test.js +0 -89
- package/tests/node/statusline.test.js +0 -240
- package/tests/python/conftest.py +0 -84
- package/tests/python/test_colors.py +0 -105
- package/tests/python/test_config_colors.py +0 -78
- package/tests/python/test_data_pipeline.py +0 -446
- package/tests/python/test_explain.py +0 -177
- package/tests/python/test_icons.py +0 -152
- package/tests/python/test_intelligence.py +0 -314
- package/tests/python/test_layout.py +0 -127
- package/tests/python/test_state_rotation_validation.py +0 -232
- package/tests/python/test_statusline.py +0 -215
- 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()
|