cc-context-stats 1.7.0 → 1.8.1

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 (102) hide show
  1. package/package.json +9 -1
  2. package/scripts/context-stats.sh +1 -1
  3. package/scripts/statusline.js +128 -18
  4. package/.editorconfig +0 -60
  5. package/.eslintrc.json +0 -35
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -49
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -31
  8. package/.github/PULL_REQUEST_TEMPLATE.md +0 -33
  9. package/.github/dependabot.yml +0 -44
  10. package/.github/workflows/ci.yml +0 -294
  11. package/.github/workflows/release.yml +0 -151
  12. package/.pre-commit-config.yaml +0 -74
  13. package/.prettierrc +0 -33
  14. package/.shellcheckrc +0 -10
  15. package/CHANGELOG.md +0 -163
  16. package/CLAUDE.md +0 -66
  17. package/CODE_OF_CONDUCT.md +0 -59
  18. package/CONTRIBUTING.md +0 -240
  19. package/RELEASE_NOTES.md +0 -19
  20. package/SECURITY.md +0 -44
  21. package/TODOS.md +0 -72
  22. package/assets/logo/favicon.svg +0 -19
  23. package/assets/logo/logo-black.svg +0 -24
  24. package/assets/logo/logo-full.svg +0 -40
  25. package/assets/logo/logo-icon.svg +0 -27
  26. package/assets/logo/logo-mark.svg +0 -28
  27. package/assets/logo/logo-white.svg +0 -24
  28. package/assets/logo/logo-wordmark.svg +0 -6
  29. package/config/settings-example.json +0 -7
  30. package/config/settings-node.json +0 -7
  31. package/config/settings-python.json +0 -7
  32. package/docs/ARCHITECTURE.md +0 -128
  33. package/docs/CSV_FORMAT.md +0 -42
  34. package/docs/DEPLOYMENT.md +0 -71
  35. package/docs/DEVELOPMENT.md +0 -161
  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 -304
  55. package/scripts/statusline-git.sh +0 -88
  56. package/scripts/statusline-minimal.sh +0 -67
  57. package/scripts/statusline.py +0 -485
  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 -512
  62. package/src/claude_statusline/cli/explain.py +0 -228
  63. package/src/claude_statusline/cli/statusline.py +0 -169
  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 -148
  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/renderer.py +0 -366
  75. package/src/claude_statusline/graphs/statistics.py +0 -92
  76. package/src/claude_statusline/ui/__init__.py +0 -1
  77. package/src/claude_statusline/ui/icons.py +0 -93
  78. package/src/claude_statusline/ui/waiting.py +0 -62
  79. package/tests/bash/test_delta_parity.bats +0 -199
  80. package/tests/bash/test_install.bats +0 -29
  81. package/tests/bash/test_parity.bats +0 -315
  82. package/tests/bash/test_statusline_full.bats +0 -139
  83. package/tests/bash/test_statusline_git.bats +0 -42
  84. package/tests/bash/test_statusline_minimal.bats +0 -37
  85. package/tests/fixtures/json/comma_in_path.json +0 -31
  86. package/tests/fixtures/json/high_usage.json +0 -17
  87. package/tests/fixtures/json/low_usage.json +0 -17
  88. package/tests/fixtures/json/medium_usage.json +0 -17
  89. package/tests/fixtures/json/valid_full.json +0 -30
  90. package/tests/fixtures/json/valid_minimal.json +0 -9
  91. package/tests/node/rotation.test.js +0 -89
  92. package/tests/node/statusline.test.js +0 -240
  93. package/tests/python/conftest.py +0 -84
  94. package/tests/python/test_colors.py +0 -105
  95. package/tests/python/test_config_colors.py +0 -78
  96. package/tests/python/test_data_pipeline.py +0 -446
  97. package/tests/python/test_explain.py +0 -177
  98. package/tests/python/test_icons.py +0 -152
  99. package/tests/python/test_layout.py +0 -127
  100. package/tests/python/test_state_rotation_validation.py +0 -232
  101. package/tests/python/test_statusline.py +0 -215
  102. package/tests/python/test_waiting.py +0 -127
@@ -1,78 +0,0 @@
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, ColorManager
9
-
10
-
11
- def get_git_info(
12
- project_dir: str | Path,
13
- colors_enabled: bool = True,
14
- color_manager: ColorManager | None = None,
15
- ) -> str:
16
- """Get git branch and change count for a directory.
17
-
18
- Args:
19
- project_dir: Path to the project directory
20
- colors_enabled: Whether to include ANSI color codes. Deprecated —
21
- prefer passing a ColorManager via color_manager instead.
22
- color_manager: Optional ColorManager for custom colors. If provided,
23
- colors_enabled is ignored (the manager handles that).
24
-
25
- Returns:
26
- Formatted string with branch and change count, or empty string if not a git repo
27
- """
28
- project_dir = Path(project_dir)
29
- git_dir = project_dir / ".git"
30
-
31
- if not git_dir.is_dir():
32
- return ""
33
-
34
- try:
35
- # Get branch name (skip optional locks for performance)
36
- result = subprocess.run(
37
- ["git", "--no-optional-locks", "rev-parse", "--abbrev-ref", "HEAD"],
38
- cwd=project_dir,
39
- capture_output=True,
40
- text=True,
41
- timeout=5,
42
- )
43
- if result.returncode != 0:
44
- return ""
45
- branch = result.stdout.strip()
46
-
47
- if not branch:
48
- return ""
49
-
50
- # Count changes
51
- result = subprocess.run(
52
- ["git", "--no-optional-locks", "status", "--porcelain"],
53
- cwd=project_dir,
54
- capture_output=True,
55
- text=True,
56
- timeout=5,
57
- )
58
- if result.returncode != 0:
59
- changes = 0
60
- else:
61
- changes = len([line for line in result.stdout.split("\n") if line.strip()])
62
-
63
- # Format output — use ColorManager if provided, else fallback to constants
64
- if color_manager is not None:
65
- magenta = color_manager.magenta
66
- cyan = color_manager.cyan
67
- reset = color_manager.reset
68
- elif colors_enabled:
69
- magenta, cyan, reset = MAGENTA, CYAN, RESET
70
- else:
71
- magenta = cyan = reset = ""
72
-
73
- if changes > 0:
74
- return f" | {magenta}{branch}{reset} {cyan}[{changes}]{reset}"
75
- return f" | {magenta}{branch}{reset}"
76
-
77
- except (subprocess.TimeoutExpired, OSError):
78
- return ""
@@ -1,323 +0,0 @@
1
- """State file management for token tracking."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- import shutil
7
- import sys
8
- import tempfile
9
- from dataclasses import dataclass
10
- from pathlib import Path
11
-
12
-
13
- @dataclass
14
- class StateEntry:
15
- """A single state file entry."""
16
-
17
- timestamp: int
18
- total_input_tokens: int
19
- total_output_tokens: int
20
- current_input_tokens: int
21
- current_output_tokens: int
22
- cache_creation: int
23
- cache_read: int
24
- cost_usd: float
25
- lines_added: int
26
- lines_removed: int
27
- session_id: str
28
- model_id: str
29
- workspace_project_dir: str
30
- context_window_size: int
31
-
32
- @classmethod
33
- def from_csv_line(cls, line: str) -> StateEntry | None:
34
- """Parse a CSV line into a StateEntry.
35
-
36
- Args:
37
- line: CSV line with comma-separated values
38
-
39
- Returns:
40
- StateEntry or None if parsing fails
41
- """
42
- parts = line.strip().split(",")
43
-
44
- # Handle old format (timestamp,tokens) and new format (14 fields)
45
- if len(parts) < 2:
46
- return None
47
-
48
- try:
49
- timestamp = int(parts[0])
50
-
51
- # Old format: timestamp,tokens
52
- if len(parts) == 2:
53
- tokens = int(parts[1])
54
- return cls(
55
- timestamp=timestamp,
56
- total_input_tokens=tokens,
57
- total_output_tokens=0,
58
- current_input_tokens=0,
59
- current_output_tokens=0,
60
- cache_creation=0,
61
- cache_read=0,
62
- cost_usd=0.0,
63
- lines_added=0,
64
- lines_removed=0,
65
- session_id="",
66
- model_id="",
67
- workspace_project_dir="",
68
- context_window_size=0,
69
- )
70
-
71
- # New format with all fields
72
- def safe_int(val: str, default: int = 0) -> int:
73
- try:
74
- return int(val) if val else default
75
- except ValueError:
76
- return default
77
-
78
- def safe_float(val: str, default: float = 0.0) -> float:
79
- try:
80
- return float(val) if val else default
81
- except ValueError:
82
- return default
83
-
84
- return cls(
85
- timestamp=timestamp,
86
- total_input_tokens=safe_int(parts[1] if len(parts) > 1 else ""),
87
- total_output_tokens=safe_int(parts[2] if len(parts) > 2 else ""),
88
- current_input_tokens=safe_int(parts[3] if len(parts) > 3 else ""),
89
- current_output_tokens=safe_int(parts[4] if len(parts) > 4 else ""),
90
- cache_creation=safe_int(parts[5] if len(parts) > 5 else ""),
91
- cache_read=safe_int(parts[6] if len(parts) > 6 else ""),
92
- cost_usd=safe_float(parts[7] if len(parts) > 7 else ""),
93
- lines_added=safe_int(parts[8] if len(parts) > 8 else ""),
94
- lines_removed=safe_int(parts[9] if len(parts) > 9 else ""),
95
- session_id=parts[10] if len(parts) > 10 else "",
96
- model_id=parts[11] if len(parts) > 11 else "",
97
- workspace_project_dir=parts[12] if len(parts) > 12 else "",
98
- context_window_size=safe_int(parts[13] if len(parts) > 13 else ""),
99
- )
100
-
101
- except (ValueError, IndexError):
102
- return None
103
-
104
- def to_csv_line(self) -> str:
105
- """Convert entry to CSV line."""
106
- return ",".join(
107
- str(x)
108
- for x in [
109
- self.timestamp,
110
- self.total_input_tokens,
111
- self.total_output_tokens,
112
- self.current_input_tokens,
113
- self.current_output_tokens,
114
- self.cache_creation,
115
- self.cache_read,
116
- self.cost_usd,
117
- self.lines_added,
118
- self.lines_removed,
119
- self.session_id,
120
- self.model_id,
121
- self.workspace_project_dir.replace(",", "_"),
122
- self.context_window_size,
123
- ]
124
- )
125
-
126
- @property
127
- def total_tokens(self) -> int:
128
- """Get combined input + output tokens."""
129
- return self.total_input_tokens + self.total_output_tokens
130
-
131
- @property
132
- def current_used_tokens(self) -> int:
133
- """Get current context usage (input + cache)."""
134
- return self.current_input_tokens + self.cache_creation + self.cache_read
135
-
136
-
137
- def _validate_session_id(session_id: str) -> None:
138
- """Validate that a session ID does not contain dangerous path characters.
139
-
140
- Args:
141
- session_id: Session ID to validate
142
-
143
- Raises:
144
- ValueError: If session_id contains '/', '\\', '..', or null bytes
145
- """
146
- for bad in ("/", "\\", "..", "\0"):
147
- if bad in session_id:
148
- raise ValueError(
149
- f"Invalid session_id: contains '{bad}'. "
150
- "Session IDs must not contain '/', '\\', '..', or null bytes."
151
- )
152
-
153
-
154
- class StateFile:
155
- """Manage state files for token tracking."""
156
-
157
- STATE_DIR = Path.home() / ".claude" / "statusline"
158
- OLD_STATE_DIR = Path.home() / ".claude"
159
- ROTATION_THRESHOLD = 10_000
160
- ROTATION_KEEP = 5_000
161
-
162
- def __init__(self, session_id: str | None = None) -> None:
163
- """Initialize state file manager.
164
-
165
- Args:
166
- session_id: Optional session ID. If not provided, uses latest session.
167
- """
168
- if session_id is not None:
169
- _validate_session_id(session_id)
170
- self.session_id = session_id
171
- self._ensure_state_dir()
172
- self._migrate_old_files()
173
-
174
- def _ensure_state_dir(self) -> None:
175
- """Create state directory if it doesn't exist."""
176
- self.STATE_DIR.mkdir(parents=True, exist_ok=True)
177
-
178
- def _migrate_old_files(self) -> None:
179
- """Migrate old state files from ~/.claude/ to ~/.claude/statusline/."""
180
- for old_file in self.OLD_STATE_DIR.glob("statusline*.state"):
181
- if old_file.is_file():
182
- new_file = self.STATE_DIR / old_file.name
183
- if not new_file.exists():
184
- try:
185
- shutil.move(str(old_file), str(new_file))
186
- except OSError:
187
- pass
188
- else:
189
- try:
190
- old_file.unlink()
191
- except OSError:
192
- pass
193
-
194
- @property
195
- def file_path(self) -> Path:
196
- """Get the state file path for the current session."""
197
- if self.session_id:
198
- return self.STATE_DIR / f"statusline.{self.session_id}.state"
199
- return self.STATE_DIR / "statusline.state"
200
-
201
- def find_latest_state_file(self) -> Path | None:
202
- """Find the most recently modified state file.
203
-
204
- Returns:
205
- Path to the latest state file, or None if no files exist
206
- """
207
- if self.session_id:
208
- file_path = self.STATE_DIR / f"statusline.{self.session_id}.state"
209
- return file_path if file_path.exists() else None
210
-
211
- # Find most recent state file by modification time
212
- state_files = list(self.STATE_DIR.glob("statusline.*.state"))
213
- if not state_files:
214
- # Try default state file
215
- default = self.STATE_DIR / "statusline.state"
216
- return default if default.exists() else None
217
-
218
- return max(state_files, key=lambda f: f.stat().st_mtime)
219
-
220
- def read_history(self) -> list[StateEntry]:
221
- """Read all entries from the state file.
222
-
223
- Returns:
224
- List of StateEntry objects
225
- """
226
- file_path = self.find_latest_state_file()
227
- if not file_path or not file_path.exists():
228
- return []
229
-
230
- entries = []
231
- try:
232
- content = file_path.read_text()
233
- for line in content.splitlines():
234
- if line.strip():
235
- entry = StateEntry.from_csv_line(line)
236
- if entry:
237
- entries.append(entry)
238
- except OSError as e:
239
- sys.stderr.write(f"[statusline] warning: failed to read state history {file_path}: {e}\n")
240
-
241
- return entries
242
-
243
- def read_last_entry(self) -> StateEntry | None:
244
- """Read only the last entry from the state file.
245
-
246
- Returns:
247
- The last StateEntry or None if file is empty/missing
248
- """
249
- # Use file_path for specific session, find_latest for unspecified session
250
- file_path = self.file_path if self.session_id else self.find_latest_state_file()
251
- if not file_path or not file_path.exists():
252
- return None
253
-
254
- try:
255
- content = file_path.read_text()
256
- lines = content.splitlines()
257
- for line in reversed(lines):
258
- if line.strip():
259
- return StateEntry.from_csv_line(line)
260
- except OSError as e:
261
- sys.stderr.write(f"[statusline] warning: failed to read last entry {file_path}: {e}\n")
262
-
263
- return None
264
-
265
- def append_entry(self, entry: StateEntry) -> None:
266
- """Append an entry to the state file.
267
-
268
- Args:
269
- entry: StateEntry to append
270
- """
271
- try:
272
- with open(self.file_path, "a") as f:
273
- f.write(f"{entry.to_csv_line()}\n")
274
- except OSError as e:
275
- sys.stderr.write(f"[statusline] warning: failed to write state {self.file_path}: {e}\n")
276
- return
277
- self._maybe_rotate()
278
-
279
- def _maybe_rotate(self) -> None:
280
- """Rotate state file if it exceeds the line threshold.
281
-
282
- If the file has more than ROTATION_THRESHOLD lines, truncate to
283
- the most recent ROTATION_KEEP lines via atomic temp-file + rename.
284
- """
285
- file_path = self.file_path
286
- try:
287
- if not file_path.exists():
288
- return
289
- lines = file_path.read_text().splitlines(keepends=True)
290
- if len(lines) <= self.ROTATION_THRESHOLD:
291
- return
292
- keep = lines[-self.ROTATION_KEEP :]
293
- fd = tempfile.NamedTemporaryFile(
294
- dir=str(self.STATE_DIR), delete=False, mode="w", suffix=".tmp"
295
- )
296
- try:
297
- fd.writelines(keep)
298
- fd.close()
299
- os.replace(fd.name, str(file_path))
300
- except BaseException:
301
- fd.close()
302
- try:
303
- os.unlink(fd.name)
304
- except OSError:
305
- pass
306
- raise
307
- except OSError as e:
308
- sys.stderr.write(f"[statusline] warning: failed to rotate state file {file_path}: {e}\n")
309
-
310
- def list_sessions(self) -> list[str]:
311
- """List all available session IDs.
312
-
313
- Returns:
314
- List of session ID strings
315
- """
316
- sessions = []
317
- for file_path in self.STATE_DIR.glob("statusline.*.state"):
318
- name = file_path.stem # statusline.{session_id}
319
- if name.startswith("statusline."):
320
- session_id = name[11:] # Remove "statusline." prefix
321
- if session_id:
322
- sessions.append(session_id)
323
- return sessions
@@ -1 +0,0 @@
1
- """Formatting utilities for claude-statusline."""
@@ -1,67 +0,0 @@
1
- """Layout utilities for fitting statusline output to terminal width."""
2
-
3
- from __future__ import annotations
4
-
5
- import re
6
- import shutil
7
-
8
- # Pattern to strip ANSI escape sequences
9
- _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
10
-
11
-
12
- def visible_width(s: str) -> int:
13
- """Return the visible width of a string after stripping ANSI escape sequences."""
14
- return len(_ANSI_RE.sub("", s))
15
-
16
-
17
- def get_terminal_width() -> int:
18
- """Return the terminal width in columns.
19
-
20
- When running inside Claude Code's statusline subprocess, neither $COLUMNS
21
- nor tput/shutil can detect the real terminal width (they always return 80).
22
- If COLUMNS is not explicitly set and shutil falls back to 80, we use a
23
- generous default of 200 so that no parts are unnecessarily dropped;
24
- Claude Code's own UI handles any overflow/truncation.
25
- """
26
- import os
27
-
28
- # If COLUMNS is explicitly set, trust it (real terminal or test override)
29
- if os.environ.get("COLUMNS"):
30
- return shutil.get_terminal_size().columns
31
- # No COLUMNS env var — likely a Claude Code subprocess with no real TTY.
32
- # shutil will fall back to 80, which is too narrow. Use 200 instead.
33
- cols = shutil.get_terminal_size(fallback=(200, 24)).columns
34
- return 200 if cols == 80 else cols
35
-
36
-
37
- def fit_to_width(parts: list[str], max_width: int) -> str:
38
- """Assemble parts into a single line that fits within max_width.
39
-
40
- Parts are added in priority order (first = highest priority).
41
- The first part (base) is always included. Subsequent parts are
42
- included only if adding them does not exceed max_width.
43
- Empty parts are skipped.
44
-
45
- Args:
46
- parts: List of strings in priority order (highest first).
47
- max_width: Maximum visible width allowed.
48
-
49
- Returns:
50
- Assembled string that fits within max_width.
51
- """
52
- if not parts:
53
- return ""
54
-
55
- # Base part is always included
56
- result = parts[0]
57
- current_width = visible_width(result)
58
-
59
- for part in parts[1:]:
60
- if not part:
61
- continue
62
- part_width = visible_width(part)
63
- if current_width + part_width <= max_width:
64
- result += part
65
- current_width += part_width
66
-
67
- return result
@@ -1,50 +0,0 @@
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())
@@ -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."""