cc-context-stats 1.3.0
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/.claude/commands/context-stats.md +17 -0
- package/.claude/settings.local.json +85 -0
- package/.editorconfig +60 -0
- package/.eslintrc.json +35 -0
- package/.github/dependabot.yml +44 -0
- package/.github/workflows/ci.yml +255 -0
- package/.github/workflows/release.yml +149 -0
- package/.pre-commit-config.yaml +74 -0
- package/.prettierrc +33 -0
- package/.shellcheckrc +10 -0
- package/CHANGELOG.md +100 -0
- package/CONTRIBUTING.md +240 -0
- package/PUBLISHING_GUIDE.md +69 -0
- package/README.md +179 -0
- package/config/settings-example.json +7 -0
- package/config/settings-node.json +7 -0
- package/config/settings-python.json +7 -0
- package/docs/configuration.md +83 -0
- package/docs/context-stats.md +132 -0
- package/docs/installation.md +195 -0
- package/docs/scripts.md +116 -0
- package/docs/troubleshooting.md +189 -0
- 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/install +344 -0
- package/install.sh +272 -0
- package/jest.config.js +11 -0
- package/npm-publish.sh +33 -0
- package/package.json +36 -0
- package/publish.sh +24 -0
- package/pyproject.toml +113 -0
- package/requirements-dev.txt +12 -0
- package/scripts/context-stats.sh +970 -0
- package/scripts/statusline-full.sh +241 -0
- package/scripts/statusline-git.sh +32 -0
- package/scripts/statusline-minimal.sh +11 -0
- package/scripts/statusline.js +350 -0
- package/scripts/statusline.py +312 -0
- package/show_raw_claude_code_api.js +11 -0
- package/src/claude_statusline/__init__.py +11 -0
- package/src/claude_statusline/__main__.py +6 -0
- package/src/claude_statusline/cli/__init__.py +1 -0
- package/src/claude_statusline/cli/context_stats.py +379 -0
- package/src/claude_statusline/cli/statusline.py +172 -0
- package/src/claude_statusline/core/__init__.py +1 -0
- package/src/claude_statusline/core/colors.py +55 -0
- package/src/claude_statusline/core/config.py +98 -0
- package/src/claude_statusline/core/git.py +67 -0
- package/src/claude_statusline/core/state.py +266 -0
- package/src/claude_statusline/formatters/__init__.py +1 -0
- package/src/claude_statusline/formatters/time.py +50 -0
- package/src/claude_statusline/formatters/tokens.py +70 -0
- package/src/claude_statusline/graphs/__init__.py +1 -0
- package/src/claude_statusline/graphs/renderer.py +346 -0
- package/src/claude_statusline/graphs/statistics.py +58 -0
- package/tests/bash/test_install.bats +29 -0
- package/tests/bash/test_statusline_full.bats +109 -0
- package/tests/bash/test_statusline_git.bats +42 -0
- package/tests/bash/test_statusline_minimal.bats +37 -0
- package/tests/fixtures/json/high_usage.json +17 -0
- package/tests/fixtures/json/low_usage.json +17 -0
- package/tests/fixtures/json/medium_usage.json +17 -0
- package/tests/fixtures/json/valid_full.json +30 -0
- package/tests/fixtures/json/valid_minimal.json +9 -0
- package/tests/node/statusline.test.js +199 -0
- package/tests/python/conftest.py +84 -0
- package/tests/python/test_statusline.py +154 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Git integration utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from claude_statusline.core.colors import CYAN, MAGENTA, RESET
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_git_info(project_dir: str | Path, colors_enabled: bool = True) -> str:
|
|
12
|
+
"""Get git branch and change count for a directory.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
project_dir: Path to the project directory
|
|
16
|
+
colors_enabled: Whether to include ANSI color codes
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Formatted string with branch and change count, or empty string if not a git repo
|
|
20
|
+
"""
|
|
21
|
+
project_dir = Path(project_dir)
|
|
22
|
+
git_dir = project_dir / ".git"
|
|
23
|
+
|
|
24
|
+
if not git_dir.is_dir():
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# Get branch name (skip optional locks for performance)
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
["git", "--no-optional-locks", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
31
|
+
cwd=project_dir,
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
timeout=5,
|
|
35
|
+
)
|
|
36
|
+
if result.returncode != 0:
|
|
37
|
+
return ""
|
|
38
|
+
branch = result.stdout.strip()
|
|
39
|
+
|
|
40
|
+
if not branch:
|
|
41
|
+
return ""
|
|
42
|
+
|
|
43
|
+
# Count changes
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
["git", "--no-optional-locks", "status", "--porcelain"],
|
|
46
|
+
cwd=project_dir,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
timeout=5,
|
|
50
|
+
)
|
|
51
|
+
if result.returncode != 0:
|
|
52
|
+
changes = 0
|
|
53
|
+
else:
|
|
54
|
+
changes = len([line for line in result.stdout.split("\n") if line.strip()])
|
|
55
|
+
|
|
56
|
+
# Format output
|
|
57
|
+
if colors_enabled:
|
|
58
|
+
magenta, cyan, reset = MAGENTA, CYAN, RESET
|
|
59
|
+
else:
|
|
60
|
+
magenta = cyan = reset = ""
|
|
61
|
+
|
|
62
|
+
if changes > 0:
|
|
63
|
+
return f" | {magenta}{branch}{reset} {cyan}[{changes}]{reset}"
|
|
64
|
+
return f" | {magenta}{branch}{reset}"
|
|
65
|
+
|
|
66
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
67
|
+
return ""
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""State file management for token tracking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class StateEntry:
|
|
12
|
+
"""A single state file entry."""
|
|
13
|
+
|
|
14
|
+
timestamp: int
|
|
15
|
+
total_input_tokens: int
|
|
16
|
+
total_output_tokens: int
|
|
17
|
+
current_input_tokens: int
|
|
18
|
+
current_output_tokens: int
|
|
19
|
+
cache_creation: int
|
|
20
|
+
cache_read: int
|
|
21
|
+
cost_usd: float
|
|
22
|
+
lines_added: int
|
|
23
|
+
lines_removed: int
|
|
24
|
+
session_id: str
|
|
25
|
+
model_id: str
|
|
26
|
+
workspace_project_dir: str
|
|
27
|
+
context_window_size: int
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_csv_line(cls, line: str) -> StateEntry | None:
|
|
31
|
+
"""Parse a CSV line into a StateEntry.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
line: CSV line with comma-separated values
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
StateEntry or None if parsing fails
|
|
38
|
+
"""
|
|
39
|
+
parts = line.strip().split(",")
|
|
40
|
+
|
|
41
|
+
# Handle old format (timestamp,tokens) and new format (14 fields)
|
|
42
|
+
if len(parts) < 2:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
timestamp = int(parts[0])
|
|
47
|
+
|
|
48
|
+
# Old format: timestamp,tokens
|
|
49
|
+
if len(parts) == 2:
|
|
50
|
+
tokens = int(parts[1])
|
|
51
|
+
return cls(
|
|
52
|
+
timestamp=timestamp,
|
|
53
|
+
total_input_tokens=tokens,
|
|
54
|
+
total_output_tokens=0,
|
|
55
|
+
current_input_tokens=0,
|
|
56
|
+
current_output_tokens=0,
|
|
57
|
+
cache_creation=0,
|
|
58
|
+
cache_read=0,
|
|
59
|
+
cost_usd=0.0,
|
|
60
|
+
lines_added=0,
|
|
61
|
+
lines_removed=0,
|
|
62
|
+
session_id="",
|
|
63
|
+
model_id="",
|
|
64
|
+
workspace_project_dir="",
|
|
65
|
+
context_window_size=0,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# New format with all fields
|
|
69
|
+
def safe_int(val: str, default: int = 0) -> int:
|
|
70
|
+
try:
|
|
71
|
+
return int(val) if val else default
|
|
72
|
+
except ValueError:
|
|
73
|
+
return default
|
|
74
|
+
|
|
75
|
+
def safe_float(val: str, default: float = 0.0) -> float:
|
|
76
|
+
try:
|
|
77
|
+
return float(val) if val else default
|
|
78
|
+
except ValueError:
|
|
79
|
+
return default
|
|
80
|
+
|
|
81
|
+
return cls(
|
|
82
|
+
timestamp=timestamp,
|
|
83
|
+
total_input_tokens=safe_int(parts[1] if len(parts) > 1 else ""),
|
|
84
|
+
total_output_tokens=safe_int(parts[2] if len(parts) > 2 else ""),
|
|
85
|
+
current_input_tokens=safe_int(parts[3] if len(parts) > 3 else ""),
|
|
86
|
+
current_output_tokens=safe_int(parts[4] if len(parts) > 4 else ""),
|
|
87
|
+
cache_creation=safe_int(parts[5] if len(parts) > 5 else ""),
|
|
88
|
+
cache_read=safe_int(parts[6] if len(parts) > 6 else ""),
|
|
89
|
+
cost_usd=safe_float(parts[7] if len(parts) > 7 else ""),
|
|
90
|
+
lines_added=safe_int(parts[8] if len(parts) > 8 else ""),
|
|
91
|
+
lines_removed=safe_int(parts[9] if len(parts) > 9 else ""),
|
|
92
|
+
session_id=parts[10] if len(parts) > 10 else "",
|
|
93
|
+
model_id=parts[11] if len(parts) > 11 else "",
|
|
94
|
+
workspace_project_dir=parts[12] if len(parts) > 12 else "",
|
|
95
|
+
context_window_size=safe_int(parts[13] if len(parts) > 13 else ""),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
except (ValueError, IndexError):
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def to_csv_line(self) -> str:
|
|
102
|
+
"""Convert entry to CSV line."""
|
|
103
|
+
return ",".join(
|
|
104
|
+
str(x)
|
|
105
|
+
for x in [
|
|
106
|
+
self.timestamp,
|
|
107
|
+
self.total_input_tokens,
|
|
108
|
+
self.total_output_tokens,
|
|
109
|
+
self.current_input_tokens,
|
|
110
|
+
self.current_output_tokens,
|
|
111
|
+
self.cache_creation,
|
|
112
|
+
self.cache_read,
|
|
113
|
+
self.cost_usd,
|
|
114
|
+
self.lines_added,
|
|
115
|
+
self.lines_removed,
|
|
116
|
+
self.session_id,
|
|
117
|
+
self.model_id,
|
|
118
|
+
self.workspace_project_dir,
|
|
119
|
+
self.context_window_size,
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def total_tokens(self) -> int:
|
|
125
|
+
"""Get combined input + output tokens."""
|
|
126
|
+
return self.total_input_tokens + self.total_output_tokens
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def current_used_tokens(self) -> int:
|
|
130
|
+
"""Get current context usage (input + cache)."""
|
|
131
|
+
return self.current_input_tokens + self.cache_creation + self.cache_read
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class StateFile:
|
|
135
|
+
"""Manage state files for token tracking."""
|
|
136
|
+
|
|
137
|
+
STATE_DIR = Path.home() / ".claude" / "statusline"
|
|
138
|
+
OLD_STATE_DIR = Path.home() / ".claude"
|
|
139
|
+
|
|
140
|
+
def __init__(self, session_id: str | None = None) -> None:
|
|
141
|
+
"""Initialize state file manager.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
session_id: Optional session ID. If not provided, uses latest session.
|
|
145
|
+
"""
|
|
146
|
+
self.session_id = session_id
|
|
147
|
+
self._ensure_state_dir()
|
|
148
|
+
self._migrate_old_files()
|
|
149
|
+
|
|
150
|
+
def _ensure_state_dir(self) -> None:
|
|
151
|
+
"""Create state directory if it doesn't exist."""
|
|
152
|
+
self.STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
|
|
154
|
+
def _migrate_old_files(self) -> None:
|
|
155
|
+
"""Migrate old state files from ~/.claude/ to ~/.claude/statusline/."""
|
|
156
|
+
for old_file in self.OLD_STATE_DIR.glob("statusline*.state"):
|
|
157
|
+
if old_file.is_file():
|
|
158
|
+
new_file = self.STATE_DIR / old_file.name
|
|
159
|
+
if not new_file.exists():
|
|
160
|
+
try:
|
|
161
|
+
shutil.move(str(old_file), str(new_file))
|
|
162
|
+
except OSError:
|
|
163
|
+
pass
|
|
164
|
+
else:
|
|
165
|
+
try:
|
|
166
|
+
old_file.unlink()
|
|
167
|
+
except OSError:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def file_path(self) -> Path:
|
|
172
|
+
"""Get the state file path for the current session."""
|
|
173
|
+
if self.session_id:
|
|
174
|
+
return self.STATE_DIR / f"statusline.{self.session_id}.state"
|
|
175
|
+
return self.STATE_DIR / "statusline.state"
|
|
176
|
+
|
|
177
|
+
def find_latest_state_file(self) -> Path | None:
|
|
178
|
+
"""Find the most recently modified state file.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Path to the latest state file, or None if no files exist
|
|
182
|
+
"""
|
|
183
|
+
if self.session_id:
|
|
184
|
+
file_path = self.STATE_DIR / f"statusline.{self.session_id}.state"
|
|
185
|
+
return file_path if file_path.exists() else None
|
|
186
|
+
|
|
187
|
+
# Find most recent state file by modification time
|
|
188
|
+
state_files = list(self.STATE_DIR.glob("statusline.*.state"))
|
|
189
|
+
if not state_files:
|
|
190
|
+
# Try default state file
|
|
191
|
+
default = self.STATE_DIR / "statusline.state"
|
|
192
|
+
return default if default.exists() else None
|
|
193
|
+
|
|
194
|
+
return max(state_files, key=lambda f: f.stat().st_mtime)
|
|
195
|
+
|
|
196
|
+
def read_history(self) -> list[StateEntry]:
|
|
197
|
+
"""Read all entries from the state file.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of StateEntry objects
|
|
201
|
+
"""
|
|
202
|
+
file_path = self.find_latest_state_file()
|
|
203
|
+
if not file_path or not file_path.exists():
|
|
204
|
+
return []
|
|
205
|
+
|
|
206
|
+
entries = []
|
|
207
|
+
try:
|
|
208
|
+
content = file_path.read_text()
|
|
209
|
+
for line in content.splitlines():
|
|
210
|
+
if line.strip():
|
|
211
|
+
entry = StateEntry.from_csv_line(line)
|
|
212
|
+
if entry:
|
|
213
|
+
entries.append(entry)
|
|
214
|
+
except OSError:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
return entries
|
|
218
|
+
|
|
219
|
+
def read_last_entry(self) -> StateEntry | None:
|
|
220
|
+
"""Read only the last entry from the state file.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
The last StateEntry or None if file is empty/missing
|
|
224
|
+
"""
|
|
225
|
+
# Use file_path for specific session, find_latest for unspecified session
|
|
226
|
+
file_path = self.file_path if self.session_id else self.find_latest_state_file()
|
|
227
|
+
if not file_path or not file_path.exists():
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
content = file_path.read_text()
|
|
232
|
+
lines = content.splitlines()
|
|
233
|
+
for line in reversed(lines):
|
|
234
|
+
if line.strip():
|
|
235
|
+
return StateEntry.from_csv_line(line)
|
|
236
|
+
except OSError:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
def append_entry(self, entry: StateEntry) -> None:
|
|
242
|
+
"""Append an entry to the state file.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
entry: StateEntry to append
|
|
246
|
+
"""
|
|
247
|
+
try:
|
|
248
|
+
with open(self.file_path, "a") as f:
|
|
249
|
+
f.write(f"{entry.to_csv_line()}\n")
|
|
250
|
+
except OSError:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
def list_sessions(self) -> list[str]:
|
|
254
|
+
"""List all available session IDs.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
List of session ID strings
|
|
258
|
+
"""
|
|
259
|
+
sessions = []
|
|
260
|
+
for file_path in self.STATE_DIR.glob("statusline.*.state"):
|
|
261
|
+
name = file_path.stem # statusline.{session_id}
|
|
262
|
+
if name.startswith("statusline."):
|
|
263
|
+
session_id = name[11:] # Remove "statusline." prefix
|
|
264
|
+
if session_id:
|
|
265
|
+
sessions.append(session_id)
|
|
266
|
+
return sessions
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Formatting utilities for claude-statusline."""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Time and duration formatting utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def format_timestamp(ts: int) -> str:
|
|
10
|
+
"""Format Unix timestamp as time string.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
ts: Unix timestamp (seconds since epoch)
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Formatted time string like "14:30"
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
return datetime.fromtimestamp(ts).strftime("%H:%M")
|
|
20
|
+
except (ValueError, OSError):
|
|
21
|
+
return str(ts)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def format_duration(seconds: int) -> str:
|
|
25
|
+
"""Format duration in seconds as human-readable string.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
seconds: Duration in seconds
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Formatted string like "2h 30m" or "45m" or "30s"
|
|
32
|
+
"""
|
|
33
|
+
hours = seconds // 3600
|
|
34
|
+
minutes = (seconds % 3600) // 60
|
|
35
|
+
|
|
36
|
+
if hours > 0:
|
|
37
|
+
return f"{hours}h {minutes}m"
|
|
38
|
+
elif minutes > 0:
|
|
39
|
+
return f"{minutes}m"
|
|
40
|
+
else:
|
|
41
|
+
return f"{seconds}s"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_current_timestamp() -> int:
|
|
45
|
+
"""Get current Unix timestamp.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Current time as Unix timestamp
|
|
49
|
+
"""
|
|
50
|
+
return int(time.time())
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Graph rendering utilities for token visualization."""
|