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
package/pyproject.toml DELETED
@@ -1,115 +0,0 @@
1
- [build-system]
2
- requires = ["hatchling"]
3
- build-backend = "hatchling.build"
4
-
5
- [project]
6
- name = "cc-context-stats"
7
- version = "1.8.0"
8
- description = "Monitor your Claude Code session context in real-time - track token usage and never run out of context"
9
- readme = "README.md"
10
- license = { text = "MIT" }
11
- requires-python = ">=3.9"
12
- authors = [
13
- { name = "Nguyen Luong", email = "luongnv89@gmail.com" }
14
- ]
15
- keywords = [
16
- "claude",
17
- "claude-code",
18
- "context",
19
- "tokens",
20
- "monitoring",
21
- "statusline",
22
- "terminal",
23
- "cli",
24
- "dashboard",
25
- ]
26
- classifiers = [
27
- "Development Status :: 4 - Beta",
28
- "Environment :: Console",
29
- "Intended Audience :: Developers",
30
- "License :: OSI Approved :: MIT License",
31
- "Operating System :: OS Independent",
32
- "Programming Language :: Python :: 3",
33
- "Programming Language :: Python :: 3.9",
34
- "Programming Language :: Python :: 3.10",
35
- "Programming Language :: Python :: 3.11",
36
- "Programming Language :: Python :: 3.12",
37
- "Programming Language :: Python :: 3.13",
38
- "Topic :: Software Development :: Libraries :: Python Modules",
39
- "Topic :: Utilities",
40
- ]
41
- dependencies = []
42
-
43
- [project.scripts]
44
- claude-statusline = "claude_statusline.cli.statusline:main"
45
- context-stats = "claude_statusline.cli.context_stats:main"
46
-
47
- [project.urls]
48
- Homepage = "https://github.com/luongnv89/cc-context-stats"
49
- Documentation = "https://github.com/luongnv89/cc-context-stats#readme"
50
- Repository = "https://github.com/luongnv89/cc-context-stats"
51
- Issues = "https://github.com/luongnv89/cc-context-stats/issues"
52
-
53
- [project.optional-dependencies]
54
- dev = [
55
- "pytest>=7.4.0",
56
- "pytest-cov>=4.1.0",
57
- "ruff>=0.1.0",
58
- "mypy>=1.7.0",
59
- "build>=1.0.0",
60
- "twine>=4.0.0",
61
- ]
62
-
63
- [tool.hatch.build.targets.wheel]
64
- packages = ["src/claude_statusline"]
65
-
66
- [tool.hatch.build.targets.sdist]
67
- include = [
68
- "/src",
69
- "/README.md",
70
- "/LICENSE",
71
- ]
72
-
73
- [tool.ruff]
74
- target-version = "py39"
75
- line-length = 100
76
- src = ["src"]
77
-
78
- [tool.ruff.lint]
79
- select = [
80
- "E", # pycodestyle errors
81
- "W", # pycodestyle warnings
82
- "F", # Pyflakes
83
- "I", # isort
84
- "B", # flake8-bugbear
85
- "C4", # flake8-comprehensions
86
- "UP", # pyupgrade
87
- ]
88
- ignore = ["E501"] # line too long (handled by formatter)
89
-
90
- [tool.ruff.format]
91
- quote-style = "double"
92
- indent-style = "space"
93
-
94
- [tool.mypy]
95
- python_version = "3.9"
96
- warn_return_any = true
97
- warn_unused_ignores = true
98
- strict_optional = true
99
- ignore_missing_imports = true
100
-
101
- [tool.pytest.ini_options]
102
- testpaths = ["tests/python"]
103
- addopts = "-v --tb=short"
104
- python_files = ["test_*.py"]
105
- python_functions = ["test_*"]
106
-
107
- [tool.coverage.run]
108
- source = ["src/claude_statusline"]
109
- omit = ["tests/*"]
110
-
111
- [tool.coverage.report]
112
- exclude_lines = [
113
- "pragma: no cover",
114
- "if __name__ == .__main__.:",
115
- ]
@@ -1,12 +0,0 @@
1
- # Development dependencies for claude-statusline
2
-
3
- # Testing
4
- pytest>=7.4.0
5
- pytest-cov>=4.1.0
6
-
7
- # Linting and formatting
8
- ruff>=0.1.0
9
- mypy>=1.7.0
10
-
11
- # Pre-commit hooks
12
- pre-commit>=3.6.0
@@ -1,438 +0,0 @@
1
- #!/bin/bash
2
- # Full-featured status line with context window usage
3
- # Usage: Copy to ~/.claude/statusline.sh and make executable
4
- #
5
- # Configuration:
6
- # Create/edit ~/.claude/statusline.conf and set:
7
- #
8
- # autocompact=true (when autocompact is enabled in Claude Code - default)
9
- # autocompact=false (when you disable autocompact via /config in Claude Code)
10
- #
11
- # token_detail=true (show exact token count like 64,000 - default)
12
- # token_detail=false (show abbreviated tokens like 64.0k)
13
- #
14
- # show_delta=true (show token delta since last refresh like [+2,500] - default)
15
- # show_delta=false (disable delta display - saves file I/O on every refresh)
16
- #
17
- # show_session=true (show session_id in status line - default)
18
- # show_session=false (hide session_id from status line)
19
- #
20
- # When AC is enabled, 22.5% of context window is reserved for autocompact buffer.
21
- #
22
- # State file format (CSV):
23
- # timestamp,total_input_tokens,total_output_tokens,current_usage_input_tokens,current_usage_output_tokens,current_usage_cache_creation,current_usage_cache_read,total_cost_usd,total_lines_added,total_lines_removed,session_id,model_id,workspace_project_dir
24
-
25
- # Colors (defaults, overridable via config)
26
- BLUE='\033[0;34m'
27
- MAGENTA='\033[0;35m'
28
- CYAN='\033[0;36m'
29
- GREEN='\033[0;32m'
30
- YELLOW='\033[0;33m'
31
- RED='\033[0;31m'
32
- DIM='\033[2m'
33
- RESET='\033[0m'
34
-
35
- # Named colors for config parsing
36
- declare -A COLOR_NAMES=(
37
- [black]='\033[0;30m' [red]='\033[0;31m' [green]='\033[0;32m'
38
- [yellow]='\033[0;33m' [blue]='\033[0;34m' [magenta]='\033[0;35m'
39
- [cyan]='\033[0;36m' [white]='\033[0;37m'
40
- [bright_black]='\033[0;90m' [bright_red]='\033[0;91m' [bright_green]='\033[0;92m'
41
- [bright_yellow]='\033[0;93m' [bright_blue]='\033[0;94m' [bright_magenta]='\033[0;95m'
42
- [bright_cyan]='\033[0;96m' [bright_white]='\033[0;97m'
43
- )
44
-
45
- # Color config key to slot mapping
46
- declare -A COLOR_KEYS=(
47
- [color_green]=GREEN [color_yellow]=YELLOW [color_red]=RED
48
- [color_blue]=BLUE [color_magenta]=MAGENTA [color_cyan]=CYAN
49
- )
50
-
51
- # Parse a color name or #rrggbb hex into an ANSI escape code
52
- parse_color() {
53
- local value
54
- value=$(echo "$1" | tr '[:upper:]' '[:lower:]' | xargs)
55
- if [[ -n "${COLOR_NAMES[$value]+x}" ]]; then
56
- echo "${COLOR_NAMES[$value]}"
57
- return
58
- fi
59
- if [[ "$value" =~ ^#[0-9a-f]{6}$ ]]; then
60
- local r=$((16#${value:1:2}))
61
- local g=$((16#${value:3:2}))
62
- local b=$((16#${value:5:2}))
63
- echo "\033[38;2;${r};${g};${b}m"
64
- return
65
- fi
66
- }
67
-
68
- # State file rotation constants
69
- ROTATION_THRESHOLD=10000
70
- ROTATION_KEEP=5000
71
-
72
- # Rotate state file if it exceeds threshold
73
- maybe_rotate_state_file() {
74
- local state_file="$1"
75
- [[ -f "$state_file" ]] || return
76
- local line_count
77
- line_count=$(wc -l < "$state_file" | tr -d ' ')
78
- if [[ "$line_count" -gt "$ROTATION_THRESHOLD" ]]; then
79
- local tmp_file="${state_file}.tmp.$$"
80
- tail -n "$ROTATION_KEEP" "$state_file" > "$tmp_file" && mv "$tmp_file" "$state_file" || rm -f "$tmp_file"
81
- fi
82
- }
83
-
84
- # Model Intelligence computation (uses awk for float math)
85
- compute_mi() {
86
- local used_tokens=$1 context_window=$2 cache_read_val=$3 total_context=$4
87
- local delta_lines=$5 delta_output=$6 beta=$7
88
- awk -v used="$used_tokens" -v cw="$context_window" -v cr="$cache_read_val" \
89
- -v tc="$total_context" -v dl="$delta_lines" -v do_val="$delta_output" -v b="$beta" '
90
- BEGIN {
91
- if (cw == 0) { printf "1.00 1.000 1.000 0.500"; exit }
92
- # CPS
93
- u = used / cw
94
- if (u <= 0) cps = 1.0
95
- else { cps = 1.0 - (u ^ b); if (cps < 0) cps = 0.0 }
96
- # ES
97
- if (tc == 0) es = 1.0
98
- else es = 0.3 + 0.7 * (cr / tc)
99
- # PS
100
- if (do_val == "" || do_val + 0 <= 0) ps = 0.5
101
- else {
102
- ratio = dl / do_val
103
- normalized = ratio / 0.2
104
- if (normalized > 1.0) normalized = 1.0
105
- ps = 0.2 + 0.8 * normalized
106
- }
107
- mi = 0.60 * cps + 0.25 * es + 0.15 * ps
108
- printf "%.2f %.3f %.3f %.3f", mi, cps, es, ps
109
- }'
110
- }
111
-
112
- get_mi_color() {
113
- local mi_val="$1"
114
- awk -v mi="$mi_val" 'BEGIN {
115
- if (mi + 0 > 0.65) print "green"
116
- else if (mi + 0 > 0.35) print "yellow"
117
- else print "red"
118
- }'
119
- }
120
-
121
- # Read JSON input from stdin
122
- input=$(cat)
123
-
124
- # Extract information from JSON
125
- cwd=$(echo "$input" | jq -r '.workspace.current_dir')
126
- project_dir=$(echo "$input" | jq -r '.workspace.project_dir')
127
- model=$(echo "$input" | jq -r '.model.display_name // "Claude"')
128
- session_id=$(echo "$input" | jq -r '.session_id // empty')
129
- dir_name=$(basename "$cwd")
130
-
131
- # Git information (skip optional locks for performance)
132
- git_info=""
133
- if [[ -d "$project_dir/.git" ]]; then
134
- git_branch=$(cd "$project_dir" 2>/dev/null && git --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null)
135
- git_status_count=$(cd "$project_dir" 2>/dev/null && git --no-optional-locks status --porcelain 2>/dev/null | wc -l | tr -d ' ')
136
-
137
- if [[ -n "$git_branch" ]]; then
138
- if [[ "$git_status_count" != "0" ]]; then
139
- git_info=" | ${MAGENTA}${git_branch}${RESET} ${CYAN}[${git_status_count}]${RESET}"
140
- else
141
- git_info=" | ${MAGENTA}${git_branch}${RESET}"
142
- fi
143
- fi
144
- fi
145
-
146
- # Read settings from ~/.claude/statusline.conf
147
- # Sync this manually when you change settings in Claude Code via /config
148
- autocompact_enabled=true
149
- token_detail_enabled=true
150
- show_delta_enabled=true
151
- show_session_enabled=true
152
- show_mi_enabled=true
153
- mi_curve_beta=1.5
154
- ac_info=""
155
- delta_info=""
156
- mi_info=""
157
- session_info=""
158
-
159
- # Create config file with defaults if it doesn't exist
160
- if [[ ! -f ~/.claude/statusline.conf ]]; then
161
- mkdir -p ~/.claude
162
- cat >~/.claude/statusline.conf <<'EOF'
163
- # Autocompact setting - sync with Claude Code's /config
164
- autocompact=true
165
-
166
- # Token display format
167
- token_detail=true
168
-
169
- # Show token delta since last refresh (adds file I/O on every refresh)
170
- # Disable if you don't need it to reduce overhead
171
- show_delta=true
172
-
173
- # Show session_id in status line
174
- show_session=true
175
-
176
- # Model Intelligence (MI) score display
177
- show_mi=true
178
-
179
- # MI degradation curve shape (higher = steeper initial drop)
180
- # mi_curve_beta=1.5
181
- EOF
182
- fi
183
-
184
- if [[ -f ~/.claude/statusline.conf ]]; then
185
- while IFS= read -r line; do
186
- line=$(echo "$line" | xargs)
187
- [[ -z "$line" || "$line" == \#* ]] && continue
188
- [[ "$line" != *=* ]] && continue
189
- key="${line%%=*}"
190
- key=$(echo "$key" | xargs)
191
- raw_value="${line#*=}"
192
- raw_value=$(echo "$raw_value" | xargs)
193
- value_lower=$(echo "$raw_value" | tr '[:upper:]' '[:lower:]')
194
- case "$key" in
195
- autocompact) [[ "$value_lower" == "false" ]] && autocompact_enabled=false ;;
196
- token_detail) [[ "$value_lower" == "false" ]] && token_detail_enabled=false ;;
197
- show_delta) [[ "$value_lower" == "false" ]] && show_delta_enabled=false ;;
198
- show_session) [[ "$value_lower" == "false" ]] && show_session_enabled=false ;;
199
- show_mi) [[ "$value_lower" == "false" ]] && show_mi_enabled=false ;;
200
- mi_curve_beta) mi_curve_beta="$raw_value" ;;
201
- color_*)
202
- if [[ -n "${COLOR_KEYS[$key]+x}" ]]; then
203
- local slot="${COLOR_KEYS[$key]}"
204
- local ansi
205
- ansi=$(parse_color "$raw_value")
206
- if [[ -n "$ansi" ]]; then
207
- eval "$slot='$ansi'"
208
- fi
209
- fi
210
- ;;
211
- esac
212
- done < ~/.claude/statusline.conf
213
- fi
214
-
215
- # Width-fitting helpers
216
- visible_width() {
217
- # Strip ANSI escape sequences (both literal \033 and actual ESC byte) and return string length
218
- local stripped
219
- stripped=$(printf '%s' "$1" | sed -e $'s/\033\[[0-9;]*m//g' -e 's/\\033\[[0-9;]*m//g')
220
- printf '%s' "$stripped" | wc -m | tr -d ' '
221
- }
222
-
223
- get_terminal_width() {
224
- # Return terminal width for fit_to_width truncation.
225
- # When running inside Claude Code's statusline subprocess, neither $COLUMNS
226
- # nor tput can detect the real terminal width (they always return 80).
227
- # If COLUMNS is explicitly set, trust it. Otherwise use 200 as default
228
- # so no parts are unnecessarily dropped; Claude Code handles overflow.
229
- if [[ -n "$COLUMNS" ]]; then
230
- echo "$COLUMNS"
231
- else
232
- local cols
233
- cols=$(tput cols 2>/dev/null || echo 80)
234
- if [[ "$cols" -eq 80 ]]; then
235
- echo 200
236
- else
237
- echo "$cols"
238
- fi
239
- fi
240
- }
241
-
242
- fit_to_width() {
243
- # Assemble parts into a single line that fits within max_width.
244
- # Usage: fit_to_width max_width part1 part2 part3 ...
245
- # First part (base) is always included. Subsequent parts are
246
- # included only if adding them does not exceed max_width.
247
- local max_width=$1
248
- shift
249
- local parts=("$@")
250
-
251
- if [[ ${#parts[@]} -eq 0 ]]; then
252
- echo ""
253
- return
254
- fi
255
-
256
- local result="${parts[0]}"
257
- local current_width
258
- current_width=$(visible_width "$result")
259
-
260
- for ((i = 1; i < ${#parts[@]}; i++)); do
261
- local part="${parts[$i]}"
262
- if [[ -z "$part" ]]; then
263
- continue
264
- fi
265
- local part_width
266
- part_width=$(visible_width "$part")
267
- if (( current_width + part_width <= max_width )); then
268
- result+="$part"
269
- (( current_width += part_width ))
270
- fi
271
- done
272
-
273
- echo -e "$result"
274
- }
275
-
276
- # Calculate context window - show remaining free space
277
- context_info=""
278
- total_size=$(echo "$input" | jq -r '.context_window.context_window_size // 0')
279
- current_usage=$(echo "$input" | jq '.context_window.current_usage')
280
- total_input_tokens=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0')
281
- total_output_tokens=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0')
282
- cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
283
- lines_added=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
284
- lines_removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
285
- model_id=$(echo "$input" | jq -r '.model.id // ""')
286
- workspace_project_dir=$(echo "$input" | jq -r '.workspace.project_dir // ""' | tr ',' '_')
287
-
288
- if [[ "$total_size" -gt 0 && "$current_usage" != "null" ]]; then
289
- # Get tokens from current_usage (includes cache)
290
- input_tokens=$(echo "$current_usage" | jq -r '.input_tokens // 0')
291
- cache_creation=$(echo "$current_usage" | jq -r '.cache_creation_input_tokens // 0')
292
- cache_read=$(echo "$current_usage" | jq -r '.cache_read_input_tokens // 0')
293
-
294
- # Total used from current request
295
- used_tokens=$((input_tokens + cache_creation + cache_read))
296
-
297
- # Calculate autocompact buffer (22.5% of context window = 45k for 200k)
298
- autocompact_buffer=$((total_size * 225 / 1000))
299
-
300
- # Free tokens calculation depends on autocompact setting
301
- if [[ "$autocompact_enabled" == "true" ]]; then
302
- # When AC enabled: subtract buffer to show actual usable space
303
- free_tokens=$((total_size - used_tokens - autocompact_buffer))
304
- buffer_k=$(awk "BEGIN {printf \"%.0f\", $autocompact_buffer / 1000}")
305
- ac_info=" ${DIM}[AC:${buffer_k}k]${RESET}"
306
- else
307
- # When AC disabled: show full free space
308
- free_tokens=$((total_size - used_tokens))
309
- ac_info=" ${DIM}[AC:off]${RESET}"
310
- fi
311
-
312
- if [[ "$free_tokens" -lt 0 ]]; then
313
- free_tokens=0
314
- fi
315
-
316
- # Calculate percentage with one decimal (relative to total size)
317
- free_pct=$(awk "BEGIN {printf \"%.1f\", ($free_tokens * 100.0 / $total_size)}")
318
- free_pct_int=${free_pct%.*}
319
-
320
- # Format tokens based on token_detail setting
321
- if [[ "$token_detail_enabled" == "true" ]]; then
322
- # Use awk for portable comma formatting (works regardless of locale)
323
- free_display=$(awk -v n="$free_tokens" 'BEGIN { printf "%\047d", n }')
324
- else
325
- free_display=$(awk "BEGIN {printf \"%.1fk\", $free_tokens / 1000}")
326
- fi
327
-
328
- # Color based on free percentage
329
- if [[ "$free_pct_int" -gt 50 ]]; then
330
- ctx_color="$GREEN"
331
- elif [[ "$free_pct_int" -gt 25 ]]; then
332
- ctx_color="$YELLOW"
333
- else
334
- ctx_color="$RED"
335
- fi
336
-
337
- context_info=" | ${ctx_color}${free_display} (${free_pct}%)${RESET}"
338
-
339
- # Read previous entry if needed for delta OR MI
340
- if [[ "$show_delta_enabled" == "true" || "$show_mi_enabled" == "true" ]]; then
341
- # Use session_id for per-session state (avoids conflicts with parallel sessions)
342
- state_dir=~/.claude/statusline
343
- mkdir -p "$state_dir"
344
-
345
- # Migrate old state files from ~/.claude/ to ~/.claude/statusline/ (one-time migration)
346
- old_state_dir=~/.claude
347
- for old_file in "$old_state_dir"/statusline*.state; do
348
- if [[ -f "$old_file" ]]; then
349
- new_file="${state_dir}/$(basename "$old_file")"
350
- if [[ ! -f "$new_file" ]]; then
351
- mv "$old_file" "$new_file" 2>/dev/null || true
352
- else
353
- rm -f "$old_file" 2>/dev/null || true
354
- fi
355
- fi
356
- done
357
-
358
- if [[ -n "$session_id" ]]; then
359
- state_file=${state_dir}/statusline.${session_id}.state
360
- else
361
- state_file=${state_dir}/statusline.state
362
- fi
363
- has_prev=false
364
- prev_tokens=0
365
- prev_output_tokens=0
366
- prev_lines_added=0
367
- prev_lines_removed=0
368
- if [[ -f "$state_file" ]]; then
369
- has_prev=true
370
- # Read last line and calculate previous state
371
- # CSV: ts[0],in[1],out[2],cur_in[3],cur_out[4],cache_create[5],cache_read[6],
372
- # cost[7],+lines[8],-lines[9],session[10],model[11],dir[12],size[13]
373
- last_line=$(tail -1 "$state_file" 2>/dev/null)
374
- if [[ -n "$last_line" ]]; then
375
- prev_cur_in=$(echo "$last_line" | cut -d',' -f4)
376
- prev_cache_create=$(echo "$last_line" | cut -d',' -f6)
377
- prev_cache_read=$(echo "$last_line" | cut -d',' -f7)
378
- prev_tokens=$(( ${prev_cur_in:-0} + ${prev_cache_create:-0} + ${prev_cache_read:-0} ))
379
- prev_output_tokens=$(echo "$last_line" | cut -d',' -f3)
380
- prev_lines_added=$(echo "$last_line" | cut -d',' -f9)
381
- prev_lines_removed=$(echo "$last_line" | cut -d',' -f10)
382
- fi
383
- fi
384
-
385
- # Calculate and display token delta if enabled
386
- if [[ "$show_delta_enabled" == "true" ]]; then
387
- delta=$((used_tokens - prev_tokens))
388
- if [[ "$has_prev" == "true" && "$delta" -gt 0 ]]; then
389
- if [[ "$token_detail_enabled" == "true" ]]; then
390
- delta_display=$(awk -v n="$delta" 'BEGIN { printf "%\047d", n }')
391
- else
392
- delta_display=$(awk "BEGIN {printf \"%.1fk\", $delta / 1000}")
393
- fi
394
- delta_info=" ${DIM}[+${delta_display}]${RESET}"
395
- fi
396
- fi
397
-
398
- # Calculate and display MI score if enabled
399
- if [[ "$show_mi_enabled" == "true" ]]; then
400
- if [[ "$has_prev" == "true" ]]; then
401
- delta_la=$(( ${lines_added:-0} - ${prev_lines_added:-0} ))
402
- delta_lr=$(( ${lines_removed:-0} - ${prev_lines_removed:-0} ))
403
- mi_delta_lines=$(( delta_la + delta_lr ))
404
- mi_delta_output=$(( ${total_output_tokens:-0} - ${prev_output_tokens:-0} ))
405
- else
406
- mi_delta_lines=0
407
- mi_delta_output=""
408
- fi
409
- mi_result=$(compute_mi "$used_tokens" "$total_size" "$cache_read" "$used_tokens" "$mi_delta_lines" "$mi_delta_output" "$mi_curve_beta")
410
- mi_val=$(echo "$mi_result" | cut -d' ' -f1)
411
- mi_color_name=$(get_mi_color "$mi_val")
412
- case "$mi_color_name" in
413
- green) mi_color="$GREEN" ;;
414
- yellow) mi_color="$YELLOW" ;;
415
- red) mi_color="$RED" ;;
416
- esac
417
- mi_info=" ${mi_color}MI:${mi_val}${RESET}"
418
- fi
419
-
420
- # Only append if context usage changed (avoid duplicates from multiple refreshes)
421
- cur_input_tokens=$(echo "$current_usage" | jq -r '.input_tokens // 0')
422
- cur_output_tokens=$(echo "$current_usage" | jq -r '.output_tokens // 0')
423
- if [[ "$has_prev" != "true" || "$used_tokens" != "$prev_tokens" ]]; then
424
- echo "$(date +%s),$total_input_tokens,$total_output_tokens,$cur_input_tokens,$cur_output_tokens,$cache_creation,$cache_read,$cost_usd,$lines_added,$lines_removed,$session_id,$model_id,$workspace_project_dir,$total_size" >>"$state_file"
425
- maybe_rotate_state_file "$state_file"
426
- fi
427
- fi
428
- fi
429
-
430
- # Display session_id if enabled
431
- if [[ "$show_session_enabled" == "true" && -n "$session_id" ]]; then
432
- session_info=" ${DIM}${session_id}${RESET}"
433
- fi
434
-
435
- # Output: [Model] directory | branch [changes] | XXk free (XX%) [+delta] [AC] [S:session_id]
436
- base="${DIM}[${model}]${RESET} ${BLUE}${dir_name}${RESET}"
437
- max_width=$(get_terminal_width)
438
- fit_to_width "$max_width" "$base" "$git_info" "$context_info" "$delta_info" "$mi_info" "$ac_info" "$session_info"
@@ -1,88 +0,0 @@
1
- #!/bin/bash
2
- # Git-aware status line - shows model, directory, and git branch
3
- # Usage: Copy to ~/.claude/statusline.sh and make executable
4
-
5
- # Colors
6
- BLUE='\033[0;34m'
7
- MAGENTA='\033[0;35m'
8
- CYAN='\033[0;36m'
9
- RESET='\033[0m'
10
-
11
- input=$(cat)
12
-
13
- MODEL_DISPLAY=$(echo "$input" | jq -r '.model.display_name // "Claude"')
14
- CURRENT_DIR=$(echo "$input" | jq -r '.workspace.current_dir // "~"')
15
- DIR_NAME="${CURRENT_DIR##*/}"
16
-
17
- # Width-fitting helpers
18
- visible_width() {
19
- local stripped
20
- stripped=$(printf '%s' "$1" | sed -e $'s/\033\[[0-9;]*m//g' -e 's/\\033\[[0-9;]*m//g')
21
- printf '%s' "$stripped" | wc -m | tr -d ' '
22
- }
23
-
24
- get_terminal_width() {
25
- # When running inside Claude Code's statusline subprocess, $COLUMNS is not set
26
- # and tput falls back to 80. If COLUMNS is set, trust it. Otherwise use 200
27
- # so no parts are dropped; Claude Code handles overflow.
28
- if [[ -n "$COLUMNS" ]]; then
29
- echo "$COLUMNS"
30
- else
31
- local cols
32
- cols=$(tput cols 2>/dev/null || echo 80)
33
- if [[ "$cols" -eq 80 ]]; then
34
- echo 200
35
- else
36
- echo "$cols"
37
- fi
38
- fi
39
- }
40
-
41
- fit_to_width() {
42
- local max_width=$1
43
- shift
44
- local parts=("$@")
45
-
46
- if [[ ${#parts[@]} -eq 0 ]]; then
47
- echo ""
48
- return
49
- fi
50
-
51
- local result="${parts[0]}"
52
- local current_width
53
- current_width=$(visible_width "$result")
54
-
55
- for ((i = 1; i < ${#parts[@]}; i++)); do
56
- local part="${parts[$i]}"
57
- if [[ -z "$part" ]]; then
58
- continue
59
- fi
60
- local part_width
61
- part_width=$(visible_width "$part")
62
- if (( current_width + part_width <= max_width )); then
63
- result+="$part"
64
- (( current_width += part_width ))
65
- fi
66
- done
67
-
68
- echo -e "$result"
69
- }
70
-
71
- # Git branch detection
72
- GIT_INFO=""
73
- if git -C "$CURRENT_DIR" rev-parse --git-dir > /dev/null 2>&1; then
74
- BRANCH=$(git -C "$CURRENT_DIR" branch --show-current 2>/dev/null)
75
- if [ -n "$BRANCH" ]; then
76
- # Count uncommitted changes
77
- CHANGES=$(git -C "$CURRENT_DIR" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
78
- if [ "$CHANGES" -gt 0 ]; then
79
- GIT_INFO=" | ${MAGENTA}${BRANCH}${RESET} ${CYAN}[${CHANGES}]${RESET}"
80
- else
81
- GIT_INFO=" | ${MAGENTA}${BRANCH}${RESET}"
82
- fi
83
- fi
84
- fi
85
-
86
- base="[${MODEL_DISPLAY}] ${BLUE}${DIR_NAME}${RESET}"
87
- max_width=$(get_terminal_width)
88
- fit_to_width "$max_width" "$base" "$GIT_INFO"