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,970 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Context Stats Visualizer for Claude Code
|
|
3
|
+
# Displays ASCII graphs of token consumption over time
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# context-stats.sh [session_id] [options]
|
|
7
|
+
#
|
|
8
|
+
# Options:
|
|
9
|
+
# --type <cumulative|delta|both> Graph type to display (default: both)
|
|
10
|
+
# --watch, -w [interval] Real-time monitoring mode (default: 2s)
|
|
11
|
+
# --no-color Disable color output
|
|
12
|
+
# --help Show this help
|
|
13
|
+
#
|
|
14
|
+
# Examples:
|
|
15
|
+
# context-stats.sh # Latest session, both graphs
|
|
16
|
+
# context-stats.sh abc123 # Specific session
|
|
17
|
+
# context-stats.sh --type delta # Only delta graph
|
|
18
|
+
# context-stats.sh --watch # Real-time mode (2s refresh)
|
|
19
|
+
# context-stats.sh -w 5 # Real-time mode (5s refresh)
|
|
20
|
+
|
|
21
|
+
# Note: This script is compatible with bash 3.2+ (macOS default)
|
|
22
|
+
|
|
23
|
+
# === CONFIGURATION ===
|
|
24
|
+
# shellcheck disable=SC2034
|
|
25
|
+
VERSION="1.2.3"
|
|
26
|
+
COMMIT_HASH="dev" # Will be replaced during installation
|
|
27
|
+
STATE_DIR=~/.claude/statusline
|
|
28
|
+
CONFIG_FILE=~/.claude/statusline.conf
|
|
29
|
+
|
|
30
|
+
# === COLOR DEFINITIONS ===
|
|
31
|
+
BLUE='\033[0;34m'
|
|
32
|
+
MAGENTA='\033[0;35m'
|
|
33
|
+
CYAN='\033[0;36m'
|
|
34
|
+
GREEN='\033[0;32m'
|
|
35
|
+
YELLOW='\033[0;33m'
|
|
36
|
+
RED='\033[0;31m'
|
|
37
|
+
BOLD='\033[1m'
|
|
38
|
+
DIM='\033[2m'
|
|
39
|
+
RESET='\033[0m'
|
|
40
|
+
|
|
41
|
+
# === GLOBAL VARIABLES ===
|
|
42
|
+
# Use simple arrays for bash 3.2 compatibility
|
|
43
|
+
TIMESTAMPS=""
|
|
44
|
+
TOKENS=""
|
|
45
|
+
INPUT_TOKENS=""
|
|
46
|
+
OUTPUT_TOKENS=""
|
|
47
|
+
DELTAS=""
|
|
48
|
+
DELTA_TIMES=""
|
|
49
|
+
DATA_COUNT=0
|
|
50
|
+
TERM_WIDTH=80
|
|
51
|
+
TERM_HEIGHT=24
|
|
52
|
+
GRAPH_WIDTH=60
|
|
53
|
+
GRAPH_HEIGHT=15
|
|
54
|
+
SESSION_ID=""
|
|
55
|
+
GRAPH_TYPE="delta"
|
|
56
|
+
COLOR_ENABLED=true
|
|
57
|
+
TOKEN_DETAIL_ENABLED=true
|
|
58
|
+
WATCH_MODE=true
|
|
59
|
+
WATCH_INTERVAL=2
|
|
60
|
+
|
|
61
|
+
# === UTILITY FUNCTIONS ===
|
|
62
|
+
|
|
63
|
+
show_help() {
|
|
64
|
+
cat <<'EOF'
|
|
65
|
+
Context Stats Visualizer for Claude Code
|
|
66
|
+
|
|
67
|
+
USAGE:
|
|
68
|
+
context-stats.sh [session_id] [options]
|
|
69
|
+
|
|
70
|
+
ARGUMENTS:
|
|
71
|
+
session_id Optional session ID. If not provided, uses the latest session.
|
|
72
|
+
|
|
73
|
+
OPTIONS:
|
|
74
|
+
--type <type> Graph type to display:
|
|
75
|
+
- delta: Context growth per interaction (default)
|
|
76
|
+
- cumulative: Total context usage over time
|
|
77
|
+
- io: Input/output tokens over time
|
|
78
|
+
- both: Show cumulative and delta graphs
|
|
79
|
+
- all: Show all graphs including I/O
|
|
80
|
+
-w [interval] Set refresh interval in seconds (default: 2)
|
|
81
|
+
--no-watch Show graphs once and exit (disable live monitoring)
|
|
82
|
+
--no-color Disable color output
|
|
83
|
+
--help Show this help message
|
|
84
|
+
|
|
85
|
+
NOTE:
|
|
86
|
+
By default, context-stats runs in live monitoring mode, refreshing every 2 seconds.
|
|
87
|
+
Press Ctrl+C to exit. Use --no-watch to display graphs once and exit.
|
|
88
|
+
|
|
89
|
+
EXAMPLES:
|
|
90
|
+
# Live monitoring (default, refreshes every 2s)
|
|
91
|
+
context-stats.sh
|
|
92
|
+
|
|
93
|
+
# Live monitoring with custom interval
|
|
94
|
+
context-stats.sh -w 5
|
|
95
|
+
|
|
96
|
+
# Show graphs once and exit
|
|
97
|
+
context-stats.sh --no-watch
|
|
98
|
+
|
|
99
|
+
# Show graphs for specific session
|
|
100
|
+
context-stats.sh abc123def
|
|
101
|
+
|
|
102
|
+
# Show cumulative graph instead of delta
|
|
103
|
+
context-stats.sh --type cumulative
|
|
104
|
+
|
|
105
|
+
# Combine options
|
|
106
|
+
context-stats.sh abc123 --type cumulative -w 3
|
|
107
|
+
|
|
108
|
+
# Output to file (no colors, single run)
|
|
109
|
+
context-stats.sh --no-watch --no-color > output.txt
|
|
110
|
+
|
|
111
|
+
DATA SOURCE:
|
|
112
|
+
Reads token history from ~/.claude/statusline/statusline.<session_id>.state
|
|
113
|
+
|
|
114
|
+
EOF
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
error_exit() {
|
|
118
|
+
echo -e "${RED}Error:${RESET} $1" >&2
|
|
119
|
+
exit "${2:-1}"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
warn() {
|
|
123
|
+
echo -e "${YELLOW}Warning:${RESET} $1" >&2
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
info() {
|
|
127
|
+
echo -e "${DIM}$1${RESET}"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
show_waiting_message() {
|
|
131
|
+
local session_id=$1
|
|
132
|
+
local message=${2:-"Waiting for session data..."}
|
|
133
|
+
|
|
134
|
+
echo ""
|
|
135
|
+
if [ -n "$session_id" ]; then
|
|
136
|
+
echo -e "${BOLD}${MAGENTA}Context Stats${RESET} ${DIM}(Session: $session_id)${RESET}"
|
|
137
|
+
else
|
|
138
|
+
echo -e "${BOLD}${MAGENTA}Context Stats${RESET}"
|
|
139
|
+
fi
|
|
140
|
+
echo ""
|
|
141
|
+
echo -e " ${CYAN}⏳ ${message}${RESET}"
|
|
142
|
+
echo ""
|
|
143
|
+
echo -e " ${DIM}The session has just started and no data has been recorded yet.${RESET}"
|
|
144
|
+
echo -e " ${DIM}Data will appear after the first Claude interaction.${RESET}"
|
|
145
|
+
echo ""
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
init_colors() {
|
|
149
|
+
if [ "$COLOR_ENABLED" != "true" ] || [ "${NO_COLOR:-}" = "1" ] || [ ! -t 1 ]; then
|
|
150
|
+
# shellcheck disable=SC2034
|
|
151
|
+
BLUE='' # Kept for consistency with other color definitions
|
|
152
|
+
MAGENTA=''
|
|
153
|
+
CYAN=''
|
|
154
|
+
GREEN=''
|
|
155
|
+
YELLOW=''
|
|
156
|
+
RED=''
|
|
157
|
+
BOLD=''
|
|
158
|
+
DIM=''
|
|
159
|
+
RESET=''
|
|
160
|
+
fi
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get_terminal_dimensions() {
|
|
164
|
+
# Try tput first
|
|
165
|
+
if command -v tput >/dev/null 2>&1; then
|
|
166
|
+
TERM_WIDTH=$(tput cols 2>/dev/null || echo 80)
|
|
167
|
+
TERM_HEIGHT=$(tput lines 2>/dev/null || echo 24)
|
|
168
|
+
else
|
|
169
|
+
# Fallback to stty
|
|
170
|
+
local dims
|
|
171
|
+
dims=$(stty size 2>/dev/null || echo "24 80")
|
|
172
|
+
TERM_HEIGHT=$(echo "$dims" | cut -d' ' -f1)
|
|
173
|
+
TERM_WIDTH=$(echo "$dims" | cut -d' ' -f2)
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Calculate graph dimensions
|
|
177
|
+
GRAPH_WIDTH=$((TERM_WIDTH - 15)) # Reserve space for Y-axis labels
|
|
178
|
+
GRAPH_HEIGHT=$((TERM_HEIGHT / 3)) # Each graph takes 1/3 of terminal
|
|
179
|
+
|
|
180
|
+
# Enforce minimums and maximums
|
|
181
|
+
[ $GRAPH_WIDTH -lt 30 ] && GRAPH_WIDTH=30
|
|
182
|
+
[ $GRAPH_HEIGHT -lt 8 ] && GRAPH_HEIGHT=8
|
|
183
|
+
[ $GRAPH_HEIGHT -gt 20 ] && GRAPH_HEIGHT=20
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
format_number() {
|
|
187
|
+
local num=$1
|
|
188
|
+
if [ "$TOKEN_DETAIL_ENABLED" = "true" ]; then
|
|
189
|
+
# Comma-separated format
|
|
190
|
+
echo "$num" | awk '{ printf "%\047d", $1 }' 2>/dev/null || echo "$num"
|
|
191
|
+
else
|
|
192
|
+
# Abbreviated format
|
|
193
|
+
echo "$num" | awk '{
|
|
194
|
+
if ($1 >= 1000000) printf "%.1fM", $1/1000000
|
|
195
|
+
else if ($1 >= 1000) printf "%.1fk", $1/1000
|
|
196
|
+
else printf "%d", $1
|
|
197
|
+
}'
|
|
198
|
+
fi
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
format_timestamp() {
|
|
202
|
+
local ts=$1
|
|
203
|
+
# Try BSD date first (macOS), then GNU date
|
|
204
|
+
date -r "$ts" +%H:%M 2>/dev/null || date -d "@$ts" +%H:%M 2>/dev/null || echo "$ts"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
format_duration() {
|
|
208
|
+
local seconds=$1
|
|
209
|
+
local hours=$((seconds / 3600))
|
|
210
|
+
local minutes=$(((seconds % 3600) / 60))
|
|
211
|
+
|
|
212
|
+
if [ $hours -gt 0 ]; then
|
|
213
|
+
echo "${hours}h ${minutes}m"
|
|
214
|
+
elif [ $minutes -gt 0 ]; then
|
|
215
|
+
echo "${minutes}m"
|
|
216
|
+
else
|
|
217
|
+
echo "${seconds}s"
|
|
218
|
+
fi
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# === DATA FUNCTIONS ===
|
|
222
|
+
|
|
223
|
+
migrate_old_state_files() {
|
|
224
|
+
local old_dir=~/.claude
|
|
225
|
+
local new_file
|
|
226
|
+
mkdir -p "$STATE_DIR"
|
|
227
|
+
for old_file in "$old_dir"/statusline*.state; do
|
|
228
|
+
if [ -f "$old_file" ]; then
|
|
229
|
+
new_file="${STATE_DIR}/$(basename "$old_file")"
|
|
230
|
+
if [ ! -f "$new_file" ]; then
|
|
231
|
+
mv "$old_file" "$new_file" 2>/dev/null || true
|
|
232
|
+
else
|
|
233
|
+
rm -f "$old_file" 2>/dev/null || true
|
|
234
|
+
fi
|
|
235
|
+
fi
|
|
236
|
+
done
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
find_latest_state_file() {
|
|
240
|
+
migrate_old_state_files
|
|
241
|
+
|
|
242
|
+
if [ -n "$SESSION_ID" ]; then
|
|
243
|
+
# Specific session requested - return path even if file doesn't exist yet
|
|
244
|
+
local file="$STATE_DIR/statusline.${SESSION_ID}.state"
|
|
245
|
+
echo "$file"
|
|
246
|
+
return 0
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
# Find most recent state file
|
|
250
|
+
local latest
|
|
251
|
+
latest=$(find "$STATE_DIR" -maxdepth 1 -name 'statusline.*.state' -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
|
|
252
|
+
|
|
253
|
+
if [ -z "$latest" ]; then
|
|
254
|
+
# Try the default state file
|
|
255
|
+
if [ -f "$STATE_DIR/statusline.state" ]; then
|
|
256
|
+
echo "$STATE_DIR/statusline.state"
|
|
257
|
+
return 0
|
|
258
|
+
fi
|
|
259
|
+
# Return empty - no state files found
|
|
260
|
+
return 1
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
echo "$latest"
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
validate_state_file() {
|
|
267
|
+
local file=$1
|
|
268
|
+
|
|
269
|
+
if [ ! -f "$file" ]; then
|
|
270
|
+
error_exit "State file not found: $file"
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
if [ ! -r "$file" ]; then
|
|
274
|
+
error_exit "Cannot read state file: $file"
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
local line_count
|
|
278
|
+
line_count=$(wc -l <"$file" | tr -d ' ')
|
|
279
|
+
|
|
280
|
+
if [ "$line_count" -lt 2 ]; then
|
|
281
|
+
error_exit "Need at least 2 data points to generate graphs.\nFound: $line_count entry. Use Claude Code to accumulate more data."
|
|
282
|
+
fi
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
load_token_history() {
|
|
286
|
+
local file=$1
|
|
287
|
+
local line_num=0
|
|
288
|
+
local valid_lines=0
|
|
289
|
+
local skipped_lines=0
|
|
290
|
+
|
|
291
|
+
TIMESTAMPS=""
|
|
292
|
+
TOKENS=""
|
|
293
|
+
INPUT_TOKENS=""
|
|
294
|
+
OUTPUT_TOKENS=""
|
|
295
|
+
CONTEXT_SIZES=""
|
|
296
|
+
CURRENT_USED_TOKENS=""
|
|
297
|
+
CURRENT_INPUT_TOKENS=""
|
|
298
|
+
CURRENT_OUTPUT_TOKENS=""
|
|
299
|
+
LAST_MODEL_ID=""
|
|
300
|
+
LAST_PROJECT_DIR=""
|
|
301
|
+
LAST_COST_USD=""
|
|
302
|
+
LAST_LINES_ADDED=""
|
|
303
|
+
LAST_LINES_REMOVED=""
|
|
304
|
+
DATA_COUNT=0
|
|
305
|
+
|
|
306
|
+
while IFS=',' read -r ts total_in total_out cur_in cur_out cache_creation cache_read cost_usd lines_added lines_removed session_id model_id workspace_project_dir context_size rest || [ -n "$ts" ]; do
|
|
307
|
+
line_num=$((line_num + 1))
|
|
308
|
+
|
|
309
|
+
# Skip empty lines
|
|
310
|
+
[ -z "$ts" ] && continue
|
|
311
|
+
|
|
312
|
+
# Validate timestamp (simple numeric check)
|
|
313
|
+
case "$ts" in
|
|
314
|
+
'' | *[!0-9]*)
|
|
315
|
+
skipped_lines=$((skipped_lines + 1))
|
|
316
|
+
[ $skipped_lines -le 3 ] && warn "Skipping invalid line $line_num"
|
|
317
|
+
continue
|
|
318
|
+
;;
|
|
319
|
+
esac
|
|
320
|
+
|
|
321
|
+
# Handle both old format (timestamp,tokens) and new format (timestamp,total_in,total_out,...)
|
|
322
|
+
if [ -z "$total_out" ]; then
|
|
323
|
+
# Old format: timestamp,tokens - use tokens as both input and output combined
|
|
324
|
+
local tok="$total_in"
|
|
325
|
+
case "$tok" in
|
|
326
|
+
'' | *[!0-9]*)
|
|
327
|
+
skipped_lines=$((skipped_lines + 1))
|
|
328
|
+
continue
|
|
329
|
+
;;
|
|
330
|
+
esac
|
|
331
|
+
total_in=$tok
|
|
332
|
+
total_out=0
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
# Validate numeric fields
|
|
336
|
+
case "$total_in" in
|
|
337
|
+
'' | *[!0-9]*) total_in=0 ;;
|
|
338
|
+
esac
|
|
339
|
+
case "$total_out" in
|
|
340
|
+
'' | *[!0-9]*) total_out=0 ;;
|
|
341
|
+
esac
|
|
342
|
+
case "$cur_in" in
|
|
343
|
+
'' | *[!0-9]*) cur_in=0 ;;
|
|
344
|
+
esac
|
|
345
|
+
case "$cache_creation" in
|
|
346
|
+
'' | *[!0-9]*) cache_creation=0 ;;
|
|
347
|
+
esac
|
|
348
|
+
case "$cache_read" in
|
|
349
|
+
'' | *[!0-9]*) cache_read=0 ;;
|
|
350
|
+
esac
|
|
351
|
+
|
|
352
|
+
# Calculate combined tokens for backward compatibility
|
|
353
|
+
local combined=$((total_in + total_out))
|
|
354
|
+
|
|
355
|
+
# Calculate current context usage (what's actually in the context window)
|
|
356
|
+
local current_used=$((cur_in + cache_creation + cache_read))
|
|
357
|
+
|
|
358
|
+
# Validate context size (new format)
|
|
359
|
+
case "$context_size" in
|
|
360
|
+
'' | *[!0-9]*) context_size=0 ;;
|
|
361
|
+
esac
|
|
362
|
+
|
|
363
|
+
# Append to space-separated strings (bash 3.2 compatible)
|
|
364
|
+
if [ -z "$TIMESTAMPS" ]; then
|
|
365
|
+
TIMESTAMPS="$ts"
|
|
366
|
+
TOKENS="$combined"
|
|
367
|
+
INPUT_TOKENS="$total_in"
|
|
368
|
+
OUTPUT_TOKENS="$total_out"
|
|
369
|
+
CONTEXT_SIZES="$context_size"
|
|
370
|
+
CURRENT_USED_TOKENS="$current_used"
|
|
371
|
+
CURRENT_INPUT_TOKENS="$cur_in"
|
|
372
|
+
CURRENT_OUTPUT_TOKENS="$cur_out"
|
|
373
|
+
else
|
|
374
|
+
TIMESTAMPS="$TIMESTAMPS $ts"
|
|
375
|
+
TOKENS="$TOKENS $combined"
|
|
376
|
+
INPUT_TOKENS="$INPUT_TOKENS $total_in"
|
|
377
|
+
OUTPUT_TOKENS="$OUTPUT_TOKENS $total_out"
|
|
378
|
+
CONTEXT_SIZES="$CONTEXT_SIZES $context_size"
|
|
379
|
+
CURRENT_USED_TOKENS="$CURRENT_USED_TOKENS $current_used"
|
|
380
|
+
CURRENT_INPUT_TOKENS="$CURRENT_INPUT_TOKENS $cur_in"
|
|
381
|
+
CURRENT_OUTPUT_TOKENS="$CURRENT_OUTPUT_TOKENS $cur_out"
|
|
382
|
+
fi
|
|
383
|
+
# Store last values (will be kept)
|
|
384
|
+
LAST_MODEL_ID="$model_id"
|
|
385
|
+
LAST_PROJECT_DIR="$workspace_project_dir"
|
|
386
|
+
LAST_COST_USD="$cost_usd"
|
|
387
|
+
LAST_LINES_ADDED="$lines_added"
|
|
388
|
+
LAST_LINES_REMOVED="$lines_removed"
|
|
389
|
+
valid_lines=$((valid_lines + 1))
|
|
390
|
+
done <"$file"
|
|
391
|
+
|
|
392
|
+
DATA_COUNT=$valid_lines
|
|
393
|
+
|
|
394
|
+
if [ $skipped_lines -gt 3 ]; then
|
|
395
|
+
warn "... and $((skipped_lines - 3)) more invalid lines"
|
|
396
|
+
fi
|
|
397
|
+
|
|
398
|
+
if [ $valid_lines -lt 2 ]; then
|
|
399
|
+
error_exit "Loaded only $valid_lines valid data points. Need at least 2."
|
|
400
|
+
fi
|
|
401
|
+
|
|
402
|
+
# Only show info message in non-watch mode
|
|
403
|
+
if [ "$WATCH_MODE" != "true" ]; then
|
|
404
|
+
info "Loaded $valid_lines data points from $(basename "$file")"
|
|
405
|
+
fi
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
calculate_deltas() {
|
|
409
|
+
local prev_tok=""
|
|
410
|
+
local idx=0
|
|
411
|
+
DELTAS=""
|
|
412
|
+
DELTA_TIMES=""
|
|
413
|
+
|
|
414
|
+
# Use CURRENT_USED_TOKENS (actual context usage) for delta calculation
|
|
415
|
+
for tok in $CURRENT_USED_TOKENS; do
|
|
416
|
+
idx=$((idx + 1))
|
|
417
|
+
if [ -z "$prev_tok" ]; then
|
|
418
|
+
# Skip first data point - no previous value to compare against
|
|
419
|
+
prev_tok=$tok
|
|
420
|
+
continue
|
|
421
|
+
fi
|
|
422
|
+
|
|
423
|
+
local delta=$((tok - prev_tok))
|
|
424
|
+
# Handle negative deltas (session reset) by showing 0
|
|
425
|
+
[ $delta -lt 0 ] && delta=0
|
|
426
|
+
|
|
427
|
+
# Get corresponding timestamp for this delta
|
|
428
|
+
local ts
|
|
429
|
+
ts=$(get_element "$TIMESTAMPS" "$idx")
|
|
430
|
+
|
|
431
|
+
if [ -z "$DELTAS" ]; then
|
|
432
|
+
DELTAS="$delta"
|
|
433
|
+
DELTA_TIMES="$ts"
|
|
434
|
+
else
|
|
435
|
+
DELTAS="$DELTAS $delta"
|
|
436
|
+
DELTA_TIMES="$DELTA_TIMES $ts"
|
|
437
|
+
fi
|
|
438
|
+
prev_tok=$tok
|
|
439
|
+
done
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
# Get Nth element from space-separated string (1-indexed)
|
|
443
|
+
get_element() {
|
|
444
|
+
local str=$1
|
|
445
|
+
local idx=$2
|
|
446
|
+
echo "$str" | awk -v n="$idx" '{ print $n }'
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
# Get min/max/avg from space-separated numbers
|
|
450
|
+
get_stats() {
|
|
451
|
+
local data=$1
|
|
452
|
+
echo "$data" | tr ' ' '\n' | awk '
|
|
453
|
+
BEGIN { min=999999999999; max=0; sum=0; n=0 }
|
|
454
|
+
{
|
|
455
|
+
if ($1 < min) min = $1
|
|
456
|
+
if ($1 > max) max = $1
|
|
457
|
+
sum += $1
|
|
458
|
+
n++
|
|
459
|
+
}
|
|
460
|
+
END {
|
|
461
|
+
avg = (n > 0) ? int(sum/n) : 0
|
|
462
|
+
print min, max, avg, sum, n
|
|
463
|
+
}
|
|
464
|
+
'
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
# === GRAPH RENDERING ===
|
|
468
|
+
|
|
469
|
+
render_timeseries_graph() {
|
|
470
|
+
local title=$1
|
|
471
|
+
local data=$2
|
|
472
|
+
local times=$3
|
|
473
|
+
local color=$4
|
|
474
|
+
|
|
475
|
+
local n
|
|
476
|
+
n=$(echo "$data" | wc -w | tr -d ' ')
|
|
477
|
+
[ "$n" -eq 0 ] && return
|
|
478
|
+
|
|
479
|
+
# Get min/max
|
|
480
|
+
local stats
|
|
481
|
+
stats=$(get_stats "$data")
|
|
482
|
+
local min max
|
|
483
|
+
min=$(echo "$stats" | cut -d' ' -f1)
|
|
484
|
+
max=$(echo "$stats" | cut -d' ' -f2)
|
|
485
|
+
# avg is available but not used in graph rendering
|
|
486
|
+
|
|
487
|
+
# Avoid division by zero
|
|
488
|
+
[ "$min" -eq "$max" ] && max=$((min + 1))
|
|
489
|
+
local range=$((max - min))
|
|
490
|
+
|
|
491
|
+
# Print title
|
|
492
|
+
echo ""
|
|
493
|
+
echo -e "${BOLD}$title${RESET}"
|
|
494
|
+
echo -e "${DIM}Max: $(format_number "$max") Min: $(format_number "$min") Points: $n${RESET}"
|
|
495
|
+
echo ""
|
|
496
|
+
|
|
497
|
+
# Build grid using awk - smooth line with filled area below
|
|
498
|
+
local grid_output
|
|
499
|
+
grid_output=$(echo "$data" | awk -v width="$GRAPH_WIDTH" -v height="$GRAPH_HEIGHT" \
|
|
500
|
+
-v min="$min" -v max="$max" -v range="$range" '
|
|
501
|
+
BEGIN {
|
|
502
|
+
# Characters for different parts of the graph
|
|
503
|
+
# Line: dots for the trend line
|
|
504
|
+
# Fill: lighter shading below the line
|
|
505
|
+
dot = "●"
|
|
506
|
+
fill_dark = "░"
|
|
507
|
+
fill_light = "▒"
|
|
508
|
+
empty = " "
|
|
509
|
+
|
|
510
|
+
# Initialize grid with empty spaces
|
|
511
|
+
for (r = 0; r < height; r++) {
|
|
512
|
+
for (c = 0; c < width; c++) {
|
|
513
|
+
grid[r,c] = empty
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
# Store y values for each x position (for interpolation)
|
|
518
|
+
for (c = 0; c < width; c++) {
|
|
519
|
+
line_y[c] = -1
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
{
|
|
523
|
+
n = NF
|
|
524
|
+
|
|
525
|
+
# First pass: calculate y position for each data point
|
|
526
|
+
for (i = 1; i <= n; i++) {
|
|
527
|
+
val = $i
|
|
528
|
+
|
|
529
|
+
# Map index to x coordinate
|
|
530
|
+
if (n == 1) {
|
|
531
|
+
x = int(width / 2)
|
|
532
|
+
} else {
|
|
533
|
+
x = int((i - 1) * (width - 1) / (n - 1))
|
|
534
|
+
}
|
|
535
|
+
if (x >= width) x = width - 1
|
|
536
|
+
if (x < 0) x = 0
|
|
537
|
+
|
|
538
|
+
# Map value to y coordinate (inverted: 0=top)
|
|
539
|
+
y = (max - val) * (height - 1) / range
|
|
540
|
+
if (y >= height) y = height - 1
|
|
541
|
+
if (y < 0) y = 0
|
|
542
|
+
|
|
543
|
+
data_x[i] = x
|
|
544
|
+
data_y[i] = y
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
# Second pass: interpolate between points to fill every x position
|
|
548
|
+
for (i = 1; i < n; i++) {
|
|
549
|
+
x1 = data_x[i]
|
|
550
|
+
y1 = data_y[i]
|
|
551
|
+
x2 = data_x[i+1]
|
|
552
|
+
y2 = data_y[i+1]
|
|
553
|
+
|
|
554
|
+
# Linear interpolation for each x between x1 and x2
|
|
555
|
+
for (x = x1; x <= x2; x++) {
|
|
556
|
+
if (x2 == x1) {
|
|
557
|
+
y = y1
|
|
558
|
+
} else {
|
|
559
|
+
# Linear interpolation
|
|
560
|
+
t = (x - x1) / (x2 - x1)
|
|
561
|
+
y = y1 + t * (y2 - y1)
|
|
562
|
+
}
|
|
563
|
+
line_y[x] = y
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# Third pass: draw the filled area and line
|
|
568
|
+
for (c = 0; c < width; c++) {
|
|
569
|
+
if (line_y[c] >= 0) {
|
|
570
|
+
line_row = int(line_y[c] + 0.5) # Round to nearest integer
|
|
571
|
+
if (line_row >= height) line_row = height - 1
|
|
572
|
+
if (line_row < 0) line_row = 0
|
|
573
|
+
|
|
574
|
+
# Fill area below the line with gradient
|
|
575
|
+
for (r = line_row; r < height; r++) {
|
|
576
|
+
if (r == line_row) {
|
|
577
|
+
grid[r, c] = dot # The line itself
|
|
578
|
+
} else if (r < line_row + 2) {
|
|
579
|
+
grid[r, c] = fill_light # Darker fill near line
|
|
580
|
+
} else {
|
|
581
|
+
grid[r, c] = fill_dark # Lighter fill further down
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
# Fourth pass: mark actual data points with larger dots
|
|
588
|
+
for (i = 1; i <= n; i++) {
|
|
589
|
+
x = data_x[i]
|
|
590
|
+
y = int(data_y[i] + 0.5)
|
|
591
|
+
if (y >= height) y = height - 1
|
|
592
|
+
if (y < 0) y = 0
|
|
593
|
+
grid[y, x] = dot
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
END {
|
|
597
|
+
# Print grid
|
|
598
|
+
for (r = 0; r < height; r++) {
|
|
599
|
+
row = ""
|
|
600
|
+
for (c = 0; c < width; c++) {
|
|
601
|
+
row = row grid[r,c]
|
|
602
|
+
}
|
|
603
|
+
print row
|
|
604
|
+
}
|
|
605
|
+
}')
|
|
606
|
+
|
|
607
|
+
# Print grid with Y-axis labels
|
|
608
|
+
local r=0
|
|
609
|
+
while [ $r -lt $GRAPH_HEIGHT ]; do
|
|
610
|
+
local val=$((max - r * range / (GRAPH_HEIGHT - 1)))
|
|
611
|
+
local label=""
|
|
612
|
+
|
|
613
|
+
# Show labels at top, middle, and bottom
|
|
614
|
+
if [ $r -eq 0 ] || [ $r -eq $((GRAPH_HEIGHT / 2)) ] || [ $r -eq $((GRAPH_HEIGHT - 1)) ]; then
|
|
615
|
+
label=$(format_number $val)
|
|
616
|
+
fi
|
|
617
|
+
|
|
618
|
+
local row
|
|
619
|
+
row=$(echo "$grid_output" | sed -n "$((r + 1))p")
|
|
620
|
+
printf '%10s %b│%b%b%s%b\n' "$label" "${DIM}" "${RESET}" "${color}" "$row" "${RESET}"
|
|
621
|
+
r=$((r + 1))
|
|
622
|
+
done
|
|
623
|
+
|
|
624
|
+
# X-axis
|
|
625
|
+
printf '%10s %b└' "" "${DIM}"
|
|
626
|
+
local c=0
|
|
627
|
+
while [ $c -lt $GRAPH_WIDTH ]; do
|
|
628
|
+
printf "─"
|
|
629
|
+
c=$((c + 1))
|
|
630
|
+
done
|
|
631
|
+
printf '%b\n' "${RESET}"
|
|
632
|
+
|
|
633
|
+
# Time labels
|
|
634
|
+
local first_time last_time mid_time
|
|
635
|
+
first_time=$(format_timestamp "$(get_element "$times" 1)")
|
|
636
|
+
last_time=$(format_timestamp "$(get_element "$times" "$n")")
|
|
637
|
+
local mid_idx=$(((n + 1) / 2))
|
|
638
|
+
mid_time=$(format_timestamp "$(get_element "$times" "$mid_idx")")
|
|
639
|
+
|
|
640
|
+
printf '%11s%b%-*s%s%*s%b\n' "" "${DIM}" "$((GRAPH_WIDTH / 3))" "$first_time" "$mid_time" "$((GRAPH_WIDTH / 3))" "$last_time" "${RESET}"
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
render_summary() {
|
|
644
|
+
local first_ts last_ts duration current_tokens total_growth
|
|
645
|
+
first_ts=$(get_element "$TIMESTAMPS" 1)
|
|
646
|
+
last_ts=$(get_element "$TIMESTAMPS" "$DATA_COUNT")
|
|
647
|
+
duration=$((last_ts - first_ts))
|
|
648
|
+
current_tokens=$(get_element "$TOKENS" "$DATA_COUNT")
|
|
649
|
+
local first_tokens
|
|
650
|
+
first_tokens=$(get_element "$TOKENS" 1)
|
|
651
|
+
total_growth=$((current_tokens - first_tokens))
|
|
652
|
+
|
|
653
|
+
# Get I/O token stats (current request tokens for display)
|
|
654
|
+
local current_input current_output
|
|
655
|
+
current_input=$(get_element "$CURRENT_INPUT_TOKENS" "$DATA_COUNT")
|
|
656
|
+
current_output=$(get_element "$CURRENT_OUTPUT_TOKENS" "$DATA_COUNT")
|
|
657
|
+
current_context=$(get_element "$CONTEXT_SIZES" "$DATA_COUNT")
|
|
658
|
+
|
|
659
|
+
# Get actual context window usage (current_input + cache_creation + cache_read)
|
|
660
|
+
local current_used
|
|
661
|
+
current_used=$(get_element "$CURRENT_USED_TOKENS" "$DATA_COUNT")
|
|
662
|
+
|
|
663
|
+
# Calculate remaining context window
|
|
664
|
+
local remaining_context=$((current_context - current_used))
|
|
665
|
+
local context_percentage=0
|
|
666
|
+
if [ "$current_context" -gt 0 ]; then
|
|
667
|
+
context_percentage=$((remaining_context * 100 / current_context))
|
|
668
|
+
fi
|
|
669
|
+
|
|
670
|
+
# Get statistics
|
|
671
|
+
local del_stats
|
|
672
|
+
del_stats=$(get_stats "$DELTAS")
|
|
673
|
+
local del_max del_avg
|
|
674
|
+
del_max=$(echo "$del_stats" | cut -d' ' -f2)
|
|
675
|
+
del_avg=$(echo "$del_stats" | cut -d' ' -f3)
|
|
676
|
+
|
|
677
|
+
echo ""
|
|
678
|
+
echo -e "${BOLD}Session Summary${RESET}"
|
|
679
|
+
local line_width=$((GRAPH_WIDTH + 11))
|
|
680
|
+
printf '%b' "${DIM}"
|
|
681
|
+
local i=0
|
|
682
|
+
while [ $i -lt $line_width ]; do
|
|
683
|
+
printf "-"
|
|
684
|
+
i=$((i + 1))
|
|
685
|
+
done
|
|
686
|
+
printf '%b\n' "${RESET}"
|
|
687
|
+
|
|
688
|
+
# Determine status zone based on context usage
|
|
689
|
+
if [ "$current_context" -gt 0 ]; then
|
|
690
|
+
local usage_percentage=$((100 - context_percentage))
|
|
691
|
+
local status_color status_text status_hint
|
|
692
|
+
if [ "$usage_percentage" -lt 40 ]; then
|
|
693
|
+
status_color="${GREEN}"
|
|
694
|
+
status_text="SMART ZONE"
|
|
695
|
+
status_hint="You are in the smart zone"
|
|
696
|
+
elif [ "$usage_percentage" -lt 80 ]; then
|
|
697
|
+
status_color="${YELLOW}"
|
|
698
|
+
status_text="DUMB ZONE"
|
|
699
|
+
status_hint="You are in the dumb zone - Dex Horthy says so"
|
|
700
|
+
else
|
|
701
|
+
status_color="${RED}"
|
|
702
|
+
status_text="WRAP UP ZONE"
|
|
703
|
+
status_hint="Better to wrap up and start a new session"
|
|
704
|
+
fi
|
|
705
|
+
# Context remaining (before status)
|
|
706
|
+
printf ' %b%-20s%b %s/%s (%s%%)\n' "${status_color}" "Context Remaining:" "${RESET}" "$(format_number "$remaining_context")" "$(format_number "$current_context")" "$context_percentage"
|
|
707
|
+
# Status indicator
|
|
708
|
+
printf ' %b%b>>> %s <<<%b %b(%s)%b\n' "${status_color}" "${BOLD}" "$status_text" "${RESET}" "${DIM}" "$status_hint" "${RESET}"
|
|
709
|
+
echo ""
|
|
710
|
+
fi
|
|
711
|
+
# Session details (ordered: Last Growth, I/O, Lines, Cost, Model, Duration)
|
|
712
|
+
if [ -n "$DELTAS" ]; then
|
|
713
|
+
local delta_count last_growth
|
|
714
|
+
delta_count=$(echo "$DELTAS" | wc -w | tr -d ' ')
|
|
715
|
+
last_growth=$(get_element "$DELTAS" "$delta_count")
|
|
716
|
+
if [ -n "$last_growth" ] && [ "$last_growth" -gt 0 ] 2>/dev/null; then
|
|
717
|
+
printf ' %b%-20s%b +%s\n' "${CYAN}" "Last Growth:" "${RESET}" "$(format_number "$last_growth")"
|
|
718
|
+
fi
|
|
719
|
+
fi
|
|
720
|
+
printf ' %b%-20s%b %s\n' "${BLUE}" "Input Tokens:" "${RESET}" "$(format_number "$current_input")"
|
|
721
|
+
printf ' %b%-20s%b %s\n' "${MAGENTA}" "Output Tokens:" "${RESET}" "$(format_number "$current_output")"
|
|
722
|
+
if [ -n "$LAST_LINES_ADDED" ] && [ "$LAST_LINES_ADDED" != "0" ] || [ -n "$LAST_LINES_REMOVED" ] && [ "$LAST_LINES_REMOVED" != "0" ]; then
|
|
723
|
+
printf ' %b%-20s%b %b+%s%b / %b-%s%b\n' "${DIM}" "Lines Changed:" "${RESET}" "${GREEN}" "$(format_number "$LAST_LINES_ADDED")" "${RESET}" "${RED}" "$(format_number "$LAST_LINES_REMOVED")" "${RESET}"
|
|
724
|
+
fi
|
|
725
|
+
if [ -n "$LAST_COST_USD" ] && [ "$LAST_COST_USD" != "0" ]; then
|
|
726
|
+
printf ' %b%-20s%b $%s\n' "${YELLOW}" "Total Cost:" "${RESET}" "$LAST_COST_USD"
|
|
727
|
+
fi
|
|
728
|
+
if [ -n "$LAST_MODEL_ID" ]; then
|
|
729
|
+
printf ' %b%-20s%b %s\n' "${DIM}" "Model:" "${RESET}" "$LAST_MODEL_ID"
|
|
730
|
+
fi
|
|
731
|
+
printf ' %b%-20s%b %s\n' "${CYAN}" "Session Duration:" "${RESET}" "$(format_duration "$duration")"
|
|
732
|
+
echo ""
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
render_footer() {
|
|
736
|
+
echo -e "${DIM}Powered by ${CYAN}claude-statusline${DIM} v${VERSION}-${COMMIT_HASH} - https://github.com/luongnv89/cc-context-stats${RESET}"
|
|
737
|
+
echo ""
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
# === ARGUMENT PARSING ===
|
|
741
|
+
|
|
742
|
+
parse_args() {
|
|
743
|
+
while [ $# -gt 0 ]; do
|
|
744
|
+
case "$1" in
|
|
745
|
+
--help | -h)
|
|
746
|
+
show_help
|
|
747
|
+
exit 0
|
|
748
|
+
;;
|
|
749
|
+
--no-color)
|
|
750
|
+
COLOR_ENABLED=false
|
|
751
|
+
shift
|
|
752
|
+
;;
|
|
753
|
+
--no-watch)
|
|
754
|
+
WATCH_MODE=false
|
|
755
|
+
shift
|
|
756
|
+
;;
|
|
757
|
+
-w)
|
|
758
|
+
# Set refresh interval
|
|
759
|
+
if [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+$ ]]; then
|
|
760
|
+
WATCH_INTERVAL="$2"
|
|
761
|
+
shift 2
|
|
762
|
+
else
|
|
763
|
+
shift
|
|
764
|
+
fi
|
|
765
|
+
;;
|
|
766
|
+
--type)
|
|
767
|
+
if [ $# -lt 2 ]; then
|
|
768
|
+
error_exit "--type requires an argument: cumulative, delta, or both"
|
|
769
|
+
fi
|
|
770
|
+
case "$2" in
|
|
771
|
+
cumulative | delta | io | both | all)
|
|
772
|
+
GRAPH_TYPE="$2"
|
|
773
|
+
;;
|
|
774
|
+
*)
|
|
775
|
+
error_exit "Invalid graph type: $2. Use: cumulative, delta, io, both, or all"
|
|
776
|
+
;;
|
|
777
|
+
esac
|
|
778
|
+
shift 2
|
|
779
|
+
;;
|
|
780
|
+
--*)
|
|
781
|
+
error_exit "Unknown option: $1\nUse --help for usage information."
|
|
782
|
+
;;
|
|
783
|
+
*)
|
|
784
|
+
# Assume it's a session ID
|
|
785
|
+
if [ -z "$SESSION_ID" ]; then
|
|
786
|
+
SESSION_ID="$1"
|
|
787
|
+
else
|
|
788
|
+
error_exit "Unexpected argument: $1"
|
|
789
|
+
fi
|
|
790
|
+
shift
|
|
791
|
+
;;
|
|
792
|
+
esac
|
|
793
|
+
done
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
# === MAIN ===
|
|
797
|
+
|
|
798
|
+
# Load configuration from file
|
|
799
|
+
load_config() {
|
|
800
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
801
|
+
while IFS='=' read -r key value || [ -n "$key" ]; do
|
|
802
|
+
# Skip comments and empty lines
|
|
803
|
+
case "$key" in
|
|
804
|
+
'#'* | '') continue ;;
|
|
805
|
+
esac
|
|
806
|
+
|
|
807
|
+
# Sanitize key and value
|
|
808
|
+
key=$(echo "$key" | tr -d '[:space:]')
|
|
809
|
+
value=$(echo "$value" | tr -d '"' | tr -d "'" | tr -d '[:space:]')
|
|
810
|
+
|
|
811
|
+
case "$key" in
|
|
812
|
+
token_detail)
|
|
813
|
+
if [ "$value" = "false" ]; then
|
|
814
|
+
TOKEN_DETAIL_ENABLED=false
|
|
815
|
+
fi
|
|
816
|
+
;;
|
|
817
|
+
esac
|
|
818
|
+
done <"$CONFIG_FILE"
|
|
819
|
+
fi
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
# Render graphs once
|
|
823
|
+
render_once() {
|
|
824
|
+
local state_file=$1
|
|
825
|
+
|
|
826
|
+
# Load data
|
|
827
|
+
load_token_history "$state_file"
|
|
828
|
+
calculate_deltas
|
|
829
|
+
|
|
830
|
+
# Display header
|
|
831
|
+
local session_name project_name
|
|
832
|
+
session_name=$(basename "$state_file" .state | sed 's/statusline\.//')
|
|
833
|
+
# Extract project name from path (last component)
|
|
834
|
+
if [ -n "$LAST_PROJECT_DIR" ]; then
|
|
835
|
+
project_name=$(basename "$LAST_PROJECT_DIR")
|
|
836
|
+
fi
|
|
837
|
+
echo ""
|
|
838
|
+
if [ -n "$project_name" ]; then
|
|
839
|
+
echo -e "${BOLD}${MAGENTA}Context Stats${RESET} ${DIM}(${CYAN}$project_name${DIM} • $session_name)${RESET}"
|
|
840
|
+
else
|
|
841
|
+
echo -e "${BOLD}${MAGENTA}Context Stats${RESET} ${DIM}(Session: $session_name)${RESET}"
|
|
842
|
+
fi
|
|
843
|
+
|
|
844
|
+
# Render graphs (use CURRENT_USED_TOKENS for actual context window usage)
|
|
845
|
+
case "$GRAPH_TYPE" in
|
|
846
|
+
cumulative)
|
|
847
|
+
render_timeseries_graph "Context Usage Over Time" "$CURRENT_USED_TOKENS" "$TIMESTAMPS" "$GREEN"
|
|
848
|
+
;;
|
|
849
|
+
delta)
|
|
850
|
+
render_timeseries_graph "Context Growth Per Interaction" "$DELTAS" "$DELTA_TIMES" "$CYAN"
|
|
851
|
+
;;
|
|
852
|
+
io)
|
|
853
|
+
render_timeseries_graph "Input Tokens (per request)" "$CURRENT_INPUT_TOKENS" "$TIMESTAMPS" "$BLUE"
|
|
854
|
+
render_timeseries_graph "Output Tokens (per request)" "$CURRENT_OUTPUT_TOKENS" "$TIMESTAMPS" "$MAGENTA"
|
|
855
|
+
;;
|
|
856
|
+
both)
|
|
857
|
+
render_timeseries_graph "Context Usage Over Time" "$CURRENT_USED_TOKENS" "$TIMESTAMPS" "$GREEN"
|
|
858
|
+
render_timeseries_graph "Context Growth Per Interaction" "$DELTAS" "$DELTA_TIMES" "$CYAN"
|
|
859
|
+
;;
|
|
860
|
+
all)
|
|
861
|
+
render_timeseries_graph "Input Tokens (per request)" "$CURRENT_INPUT_TOKENS" "$TIMESTAMPS" "$BLUE"
|
|
862
|
+
render_timeseries_graph "Output Tokens (per request)" "$CURRENT_OUTPUT_TOKENS" "$TIMESTAMPS" "$MAGENTA"
|
|
863
|
+
render_timeseries_graph "Context Usage Over Time" "$CURRENT_USED_TOKENS" "$TIMESTAMPS" "$GREEN"
|
|
864
|
+
render_timeseries_graph "Context Growth Per Interaction" "$DELTAS" "$DELTA_TIMES" "$CYAN"
|
|
865
|
+
;;
|
|
866
|
+
esac
|
|
867
|
+
|
|
868
|
+
# Render summary
|
|
869
|
+
render_summary
|
|
870
|
+
|
|
871
|
+
# Render footer
|
|
872
|
+
render_footer
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
# Watch mode - continuously refresh the display
|
|
876
|
+
run_watch_mode() {
|
|
877
|
+
local state_file=$1
|
|
878
|
+
|
|
879
|
+
# ANSI escape codes for cursor control (using $'...' for proper interpretation)
|
|
880
|
+
local CURSOR_HOME=$'\033[H'
|
|
881
|
+
local CLEAR_SCREEN=$'\033[2J'
|
|
882
|
+
local HIDE_CURSOR=$'\033[?25l'
|
|
883
|
+
local SHOW_CURSOR=$'\033[?25h'
|
|
884
|
+
local CLEAR_TO_END=$'\033[J'
|
|
885
|
+
|
|
886
|
+
# Set up signal handler for clean exit
|
|
887
|
+
trap 'printf "%s\n" "${SHOW_CURSOR}"; echo -e "${DIM}Watch mode stopped.${RESET}"; exit 0' INT TERM
|
|
888
|
+
|
|
889
|
+
# Hide cursor for cleaner display
|
|
890
|
+
printf "%s" "${HIDE_CURSOR}"
|
|
891
|
+
|
|
892
|
+
# Initial clear
|
|
893
|
+
printf "%s%s" "${CLEAR_SCREEN}" "${CURSOR_HOME}"
|
|
894
|
+
|
|
895
|
+
while true; do
|
|
896
|
+
# Move cursor to home position (top-left) instead of clearing
|
|
897
|
+
# This prevents flickering by overwriting in place
|
|
898
|
+
printf "%s" "${CURSOR_HOME}"
|
|
899
|
+
|
|
900
|
+
# Re-read terminal dimensions in case of resize
|
|
901
|
+
get_terminal_dimensions
|
|
902
|
+
|
|
903
|
+
# Show watch mode indicator with live timestamp
|
|
904
|
+
local current_time
|
|
905
|
+
current_time=$(date +%H:%M:%S)
|
|
906
|
+
echo -e "${DIM}[LIVE ${current_time}] Refresh: ${WATCH_INTERVAL}s | Ctrl+C to exit${RESET}"
|
|
907
|
+
|
|
908
|
+
# Handle case where state_file is empty (no sessions found at all)
|
|
909
|
+
if [ -z "$state_file" ]; then
|
|
910
|
+
show_waiting_message "" "Waiting for session data..."
|
|
911
|
+
# Re-validate and render (file might have new data)
|
|
912
|
+
elif [ -f "$state_file" ]; then
|
|
913
|
+
local line_count
|
|
914
|
+
line_count=$(wc -l <"$state_file" | tr -d ' ')
|
|
915
|
+
if [ "$line_count" -ge 2 ]; then
|
|
916
|
+
render_once "$state_file"
|
|
917
|
+
else
|
|
918
|
+
show_waiting_message "$SESSION_ID" "Waiting for more data points..."
|
|
919
|
+
echo -e " ${DIM}Current: ${line_count} point(s), need at least 2${RESET}"
|
|
920
|
+
fi
|
|
921
|
+
else
|
|
922
|
+
# File doesn't exist yet (new session)
|
|
923
|
+
show_waiting_message "$SESSION_ID" "Waiting for session data..."
|
|
924
|
+
fi
|
|
925
|
+
|
|
926
|
+
# Clear any remaining lines from previous render (in case terminal resized smaller)
|
|
927
|
+
printf "%s" "${CLEAR_TO_END}"
|
|
928
|
+
|
|
929
|
+
sleep "$WATCH_INTERVAL"
|
|
930
|
+
done
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
main() {
|
|
934
|
+
parse_args "$@"
|
|
935
|
+
init_colors
|
|
936
|
+
get_terminal_dimensions
|
|
937
|
+
load_config
|
|
938
|
+
|
|
939
|
+
# Find state file
|
|
940
|
+
local state_file
|
|
941
|
+
if ! state_file=$(find_latest_state_file); then
|
|
942
|
+
# No state files found at all
|
|
943
|
+
if [ "$WATCH_MODE" = "true" ]; then
|
|
944
|
+
# Watch mode - wait for data
|
|
945
|
+
run_watch_mode ""
|
|
946
|
+
else
|
|
947
|
+
# Single run mode - show friendly message
|
|
948
|
+
echo -e "${YELLOW}No session data found.${RESET}"
|
|
949
|
+
echo -e "${DIM}Run Claude Code to generate token usage data.${RESET}"
|
|
950
|
+
exit 0
|
|
951
|
+
fi
|
|
952
|
+
return
|
|
953
|
+
fi
|
|
954
|
+
|
|
955
|
+
if [ "$WATCH_MODE" = "true" ]; then
|
|
956
|
+
# Watch mode - don't exit on validation errors, keep trying
|
|
957
|
+
run_watch_mode "$state_file"
|
|
958
|
+
else
|
|
959
|
+
# Single run mode - check if file exists
|
|
960
|
+
if [ ! -f "$state_file" ]; then
|
|
961
|
+
# Specific session requested but file doesn't exist yet
|
|
962
|
+
show_waiting_message "$SESSION_ID"
|
|
963
|
+
exit 0
|
|
964
|
+
fi
|
|
965
|
+
validate_state_file "$state_file"
|
|
966
|
+
render_once "$state_file"
|
|
967
|
+
fi
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
main "$@"
|