skill-statusline 2.1.1 → 2.2.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/lib/core.sh CHANGED
@@ -1,382 +1,408 @@
1
- #!/usr/bin/env bash
2
- # skill-statusline v2.0 — Core engine
3
- # Reads Claude Code JSON from stdin, computes all fields, renders via layout
4
-
5
- STATUSLINE_DIR="${HOME}/.claude/statusline"
6
- CONFIG_FILE="${HOME}/.claude/statusline-config.json"
7
-
8
- # ── 0. Read stdin JSON ──
9
- input=$(cat)
10
-
11
- # ── 1. Source modules ──
12
- source "${STATUSLINE_DIR}/json-parser.sh"
13
- source "${STATUSLINE_DIR}/helpers.sh"
14
-
15
- # ── 2. Load config ──
16
- active_theme="default"
17
- active_layout="standard"
18
- cfg_warn_threshold=85
19
- cfg_bar_width=40
20
- cfg_show_burn_rate="false"
21
- cfg_show_vim="true"
22
- cfg_show_agent="true"
23
-
24
- if [ -f "$CONFIG_FILE" ]; then
25
- _cfg=$(cat "$CONFIG_FILE" 2>/dev/null)
26
- _t=$(echo "$_cfg" | grep -o '"theme"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\(.*\)"/\1/')
27
- _l=$(echo "$_cfg" | grep -o '"layout"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\(.*\)"/\1/')
28
- [ -n "$_t" ] && active_theme="$_t"
29
- [ -n "$_l" ] && active_layout="$_l"
30
- _w=$(echo "$_cfg" | grep -o '"compaction_warning_threshold"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
31
- _bw=$(echo "$_cfg" | grep -o '"bar_width"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
32
- _br=$(echo "$_cfg" | grep -o '"show_burn_rate"[[:space:]]*:[[:space:]]*true' | head -1)
33
- _sv=$(echo "$_cfg" | grep -o '"show_vim_mode"[[:space:]]*:[[:space:]]*false' | head -1)
34
- _sa=$(echo "$_cfg" | grep -o '"show_agent_name"[[:space:]]*:[[:space:]]*false' | head -1)
35
- [ -n "$_w" ] && cfg_warn_threshold="$_w"
36
- [ -n "$_bw" ] && cfg_bar_width="$_bw"
37
- [ -n "$_br" ] && cfg_show_burn_rate="true"
38
- [ -n "$_sv" ] && cfg_show_vim="false"
39
- [ -n "$_sa" ] && cfg_show_agent="false"
40
- fi
41
-
42
- # Allow env override (for ccsl preview)
43
- [ -n "$STATUSLINE_THEME_OVERRIDE" ] && active_theme="$STATUSLINE_THEME_OVERRIDE"
44
- [ -n "$STATUSLINE_LAYOUT_OVERRIDE" ] && active_layout="$STATUSLINE_LAYOUT_OVERRIDE"
45
-
46
- # ── 3. Source theme ──
47
- theme_file="${STATUSLINE_DIR}/themes/${active_theme}.sh"
48
- if [ -f "$theme_file" ]; then
49
- source "$theme_file"
50
- else
51
- source "${STATUSLINE_DIR}/themes/default.sh"
52
- fi
53
-
54
- # ── 4. Terminal width detection ──
55
- SL_TERM_WIDTH=${COLUMNS:-0}
56
- if [ "$SL_TERM_WIDTH" -eq 0 ] 2>/dev/null; then
57
- _tw=$(tput cols 2>/dev/null)
58
- [ -n "$_tw" ] && [ "$_tw" -gt 0 ] && SL_TERM_WIDTH="$_tw"
59
- fi
60
- [ "$SL_TERM_WIDTH" -eq 0 ] && SL_TERM_WIDTH=80
61
-
62
- # Auto-downgrade layout for narrow terminals
63
- if [ "$SL_TERM_WIDTH" -lt 60 ]; then
64
- active_layout="compact"
65
- elif [ "$SL_TERM_WIDTH" -lt 80 ] && [ "$active_layout" = "full" ]; then
66
- active_layout="standard"
67
- fi
68
-
69
- # Dynamic bar width
70
- BAR_WIDTH="$cfg_bar_width"
71
- if [ "$SL_TERM_WIDTH" -gt 100 ]; then
72
- _dyn=$(( SL_TERM_WIDTH - 20 ))
73
- [ "$_dyn" -gt 60 ] && _dyn=60
74
- [ "$_dyn" -gt "$BAR_WIDTH" ] && BAR_WIDTH="$_dyn"
75
- elif [ "$SL_TERM_WIDTH" -lt 70 ]; then
76
- BAR_WIDTH=20
77
- fi
78
-
79
- # ── 5. Initialize cache ──
80
- _sl_cache_init
81
-
82
- # ── 6. Parse ALL JSON fields ──
83
-
84
- # --- 6a. Directory ---
85
- SL_CWD=$(json_nested_val "workspace" "current_dir")
86
- [ -z "$SL_CWD" ] && SL_CWD=$(json_val "cwd")
87
- if [ -z "$SL_CWD" ]; then
88
- SL_DIR="~"
89
- clean_cwd=""
90
- else
91
- clean_cwd=$(to_fwd "$SL_CWD")
92
- SL_DIR=$(echo "$clean_cwd" | awk -F'/' '{if(NF>3) print $(NF-2)"/"$(NF-1)"/"$NF; else if(NF>2) print $(NF-1)"/"$NF; else print $0}')
93
- [ -z "$SL_DIR" ] && SL_DIR="~"
94
- fi
95
-
96
- # --- 6b. Model ---
97
- SL_MODEL_DISPLAY=$(json_nested_val "model" "display_name")
98
- SL_MODEL_ID=$(json_nested_val "model" "id")
99
- # Flat fallback
100
- [ -z "$SL_MODEL_DISPLAY" ] && SL_MODEL_DISPLAY=$(json_val "display_name")
101
- [ -z "$SL_MODEL_ID" ] && SL_MODEL_ID=$(json_val "id")
102
- [ -z "$SL_MODEL_DISPLAY" ] && SL_MODEL_DISPLAY="unknown"
103
-
104
- model_ver=""
105
- if [ -n "$SL_MODEL_ID" ]; then
106
- model_ver=$(echo "$SL_MODEL_ID" | sed -n 's/.*-\([0-9]*\)-\([0-9]*\)$/\1.\2/p')
107
- fi
108
- if [ -n "$model_ver" ] && ! echo "$SL_MODEL_DISPLAY" | grep -q '[0-9]'; then
109
- SL_MODEL="${SL_MODEL_DISPLAY} ${model_ver}"
110
- else
111
- SL_MODEL="$SL_MODEL_DISPLAY"
112
- fi
113
-
114
- # --- 6c. Context — ACCURATE computation from current_usage ---
115
- ctx_size=$(json_nested "context_window" "context_window_size")
116
- cur_input=$(json_deep "context_window" "current_usage" "input_tokens")
117
- cur_output=$(json_deep "context_window" "current_usage" "output_tokens")
118
- cur_cache_create=$(json_deep "context_window" "current_usage" "cache_creation_input_tokens")
119
- cur_cache_read=$(json_deep "context_window" "current_usage" "cache_read_input_tokens")
120
-
121
- [ -z "$ctx_size" ] && ctx_size=200000
122
- [ -z "$cur_input" ] && cur_input=0
123
- [ -z "$cur_output" ] && cur_output=0
124
- [ -z "$cur_cache_create" ] && cur_cache_create=0
125
- [ -z "$cur_cache_read" ] && cur_cache_read=0
126
-
127
- # Claude's formula: input + cache_creation + cache_read (output excluded from context %)
128
- ctx_used=$(awk -v a="$cur_input" -v b="$cur_cache_create" -v c="$cur_cache_read" \
129
- 'BEGIN { printf "%d", a + b + c }')
130
-
131
- # Self-calculated percentage
132
- calc_pct=0
133
- if [ "$cur_input" -gt 0 ] 2>/dev/null; then
134
- calc_pct=$(awk -v used="$ctx_used" -v total="$ctx_size" \
135
- 'BEGIN { if (total > 0) printf "%d", (used * 100) / total; else print 0 }')
136
- fi
137
-
138
- # Reported percentage as fallback
139
- reported_pct=$(json_nested "context_window" "used_percentage")
140
-
141
- # Use self-calculated if we have current_usage data, else fallback
142
- if [ "$cur_input" -gt 0 ] 2>/dev/null; then
143
- SL_CTX_PCT="$calc_pct"
144
- elif [ -n "$reported_pct" ] && [ "$reported_pct" != "null" ]; then
145
- SL_CTX_PCT=$(echo "$reported_pct" | cut -d. -f1)
146
- else
147
- SL_CTX_PCT=0
148
- fi
149
-
150
- SL_CTX_REMAINING=$(( 100 - SL_CTX_PCT ))
151
- [ "$SL_CTX_REMAINING" -lt 0 ] && SL_CTX_REMAINING=0
152
-
153
- # Context color
154
- if [ "$SL_CTX_PCT" -gt 90 ] 2>/dev/null; then
155
- CTX_CLR="$CLR_CTX_CRIT"
156
- elif [ "$SL_CTX_PCT" -gt 75 ] 2>/dev/null; then
157
- CTX_CLR="$CLR_CTX_HIGH"
158
- elif [ "$SL_CTX_PCT" -gt 40 ] 2>/dev/null; then
159
- CTX_CLR="$CLR_CTX_MED"
160
- else
161
- CTX_CLR="$CLR_CTX_LOW"
162
- fi
163
-
164
- # Build context bar
165
- filled=$(( SL_CTX_PCT * BAR_WIDTH / 100 ))
166
- [ "$filled" -gt "$BAR_WIDTH" ] && filled=$BAR_WIDTH
167
- empty=$(( BAR_WIDTH - filled ))
168
- bar_filled=""; bar_empty=""
169
- i=0; while [ $i -lt $filled ]; do bar_filled="${bar_filled}${BAR_FILLED}"; i=$((i+1)); done
170
- i=0; while [ $i -lt $empty ]; do bar_empty="${bar_empty}${BAR_EMPTY}"; i=$((i+1)); done
171
- SL_CTX_BAR="${CTX_CLR}${bar_filled}${CLR_RST}${CLR_BAR_EMPTY}${bar_empty}${CLR_RST} ${CTX_CLR}${SL_CTX_PCT}%${CLR_RST}"
172
-
173
- # Compaction warning
174
- SL_COMPACT_WARNING=""
175
- if [ "$SL_CTX_PCT" -ge 95 ] 2>/dev/null; then
176
- SL_COMPACT_WARNING=" ${CLR_CTX_CRIT}${CLR_BOLD}COMPACTING${CLR_RST}"
177
- elif [ "$SL_CTX_PCT" -ge "$cfg_warn_threshold" ] 2>/dev/null; then
178
- SL_COMPACT_WARNING=" ${CLR_CTX_HIGH}${SL_CTX_REMAINING}% left${CLR_RST}"
179
- fi
180
-
181
- # --- 6d. GitHub (with caching) ---
182
- SL_BRANCH="no-git"
183
- SL_GIT_DIRTY=""
184
- SL_GITHUB=""
185
- gh_user=""
186
- gh_repo=""
187
-
188
- if [ -n "$clean_cwd" ]; then
189
- SL_BRANCH=$(cache_get "git-branch" "git --no-optional-locks -C '$clean_cwd' symbolic-ref --short HEAD 2>/dev/null || git --no-optional-locks -C '$clean_cwd' rev-parse --short HEAD 2>/dev/null" 5)
190
- [ -z "$SL_BRANCH" ] && SL_BRANCH="no-git"
191
-
192
- if [ "$SL_BRANCH" != "no-git" ]; then
193
- remote_url=$(cache_get "git-remote" "git --no-optional-locks -C '$clean_cwd' remote get-url origin" 10)
194
- if [ -n "$remote_url" ]; then
195
- gh_user=$(echo "$remote_url" | sed 's|.*github\.com[:/]\([^/]*\)/.*|\1|')
196
- [ "$gh_user" = "$remote_url" ] && gh_user=""
197
- gh_repo=$(echo "$remote_url" | sed 's|.*/\([^/]*\)\.git$|\1|; s|.*/\([^/]*\)$|\1|')
198
- [ "$gh_repo" = "$remote_url" ] && gh_repo=""
199
- fi
200
-
201
- # Dirty check (shorter cache — changes more often)
202
- _staged=$(cache_get "git-staged" "git --no-optional-locks -C '$clean_cwd' diff --cached --quiet 2>/dev/null && echo clean || echo dirty" 3)
203
- _unstaged=$(cache_get "git-unstaged" "git --no-optional-locks -C '$clean_cwd' diff --quiet 2>/dev/null && echo clean || echo dirty" 3)
204
- [ "$_staged" = "dirty" ] && SL_GIT_DIRTY="${CLR_GIT_STAGED}+${CLR_RST}"
205
- [ "$_unstaged" = "dirty" ] && SL_GIT_DIRTY="${SL_GIT_DIRTY}${CLR_GIT_UNSTAGED}~${CLR_RST}"
206
- fi
207
- fi
208
-
209
- if [ -n "$gh_repo" ]; then
210
- SL_GITHUB="${gh_user}/${gh_repo}/${SL_BRANCH}"
211
- else
212
- SL_GITHUB="$SL_BRANCH"
213
- fi
214
-
215
- # --- 6e. Cost ---
216
- cost_raw=$(json_nested "cost" "total_cost_usd")
217
- [ -z "$cost_raw" ] && cost_raw=$(json_num "total_cost_usd")
218
- if [ -z "$cost_raw" ] || [ "$cost_raw" = "0" ]; then
219
- SL_COST='$0.00'
220
- else
221
- SL_COST=$(awk -v c="$cost_raw" 'BEGIN { if (c < 0.01) printf "$%.4f", c; else printf "$%.2f", c }')
222
- fi
223
-
224
- # --- 6f. Tokens (window vs cumulative) ---
225
- # Current window tokens (what's actually loaded — accurate)
226
- [ -z "$cur_input" ] && cur_input=0
227
- [ -z "$cur_output" ] && cur_output=0
228
- SL_TOKENS_WIN_IN=$(fmt_tok "$cur_input")
229
- SL_TOKENS_WIN_OUT=$(fmt_tok "$cur_output")
230
-
231
- # Cumulative session tokens (grows forever, for reference)
232
- cum_input=$(json_nested "context_window" "total_input_tokens")
233
- cum_output=$(json_nested "context_window" "total_output_tokens")
234
- # Flat fallback
235
- [ -z "$cum_input" ] && cum_input=$(json_num "total_input_tokens")
236
- [ -z "$cum_output" ] && cum_output=$(json_num "total_output_tokens")
237
- [ -z "$cum_input" ] && cum_input=0
238
- [ -z "$cum_output" ] && cum_output=0
239
- SL_TOKENS_CUM_IN=$(fmt_tok "$cum_input")
240
- SL_TOKENS_CUM_OUT=$(fmt_tok "$cum_output")
241
-
242
- # --- 6g. Skill detection (with caching) ---
243
- SL_SKILL="Idle"
244
-
245
- _detect_skill() {
246
- local cwd="$1"
247
- local tpath="" search_path="$cwd" proj_hash proj_dir
248
-
249
- while [ -n "$search_path" ] && [ "$search_path" != "/" ]; do
250
- proj_hash=$(echo "$search_path" | sed 's|^/\([a-zA-Z]\)/|\U\1--|; s|^[A-Z]:/|&|; s|:/|--|; s|/|-|g')
251
- proj_dir="$HOME/.claude/projects/${proj_hash}"
252
- if [ -d "$proj_dir" ]; then
253
- tpath=$(ls -t "$proj_dir"/*.jsonl 2>/dev/null | head -1)
254
- [ -n "$tpath" ] && break
255
- fi
256
- search_path=$(echo "$search_path" | sed 's|/[^/]*$||')
257
- done
258
-
259
- if [ -n "$tpath" ] && [ -f "$tpath" ]; then
260
- local recent_block last_tool
261
- recent_block=$(tail -200 "$tpath" 2>/dev/null)
262
- last_tool=$(echo "$recent_block" | grep -o '"type":"tool_use","id":"[^"]*","name":"[^"]*"' | tail -1 | sed 's/.*"name":"\([^"]*\)".*/\1/')
263
-
264
- if [ -n "$last_tool" ]; then
265
- case "$last_tool" in
266
- Task)
267
- local agent_count
268
- agent_count=$(echo "$recent_block" | grep -c '"type":"tool_use","id":"[^"]*","name":"Task"')
269
- if [ "$agent_count" -gt 1 ]; then
270
- echo "${agent_count} Agents"
271
- else
272
- local agent_desc
273
- agent_desc=$(echo "$recent_block" | grep -o '"description":"[^"]*"' | tail -1 | sed 's/"description":"//;s/"$//')
274
- if [ -n "$agent_desc" ]; then
275
- echo "Agent($(echo "$agent_desc" | cut -c1-20))"
276
- else
277
- echo "Agent"
278
- fi
279
- fi ;;
280
- Read) echo "Read" ;;
281
- Write) echo "Write" ;;
282
- Edit) echo "Edit" ;;
283
- MultiEdit) echo "Multi Edit" ;;
284
- Glob) echo "Search(Files)" ;;
285
- Grep) echo "Search(Content)" ;;
286
- Bash) echo "Terminal" ;;
287
- WebSearch) echo "Web Search" ;;
288
- WebFetch) echo "Web Fetch" ;;
289
- Skill) echo "Skill" ;;
290
- AskUserQuestion) echo "Asking..." ;;
291
- EnterPlanMode) echo "Planning" ;;
292
- ExitPlanMode) echo "Plan Ready" ;;
293
- TaskCreate) echo "Task Create" ;;
294
- TaskUpdate) echo "Task Update" ;;
295
- TaskGet) echo "Task Get" ;;
296
- TaskList) echo "Task List" ;;
297
- TaskStop) echo "Task Stop" ;;
298
- TaskOutput) echo "Task Output" ;;
299
- NotebookEdit) echo "Notebook" ;;
300
- *) echo "$last_tool" ;;
301
- esac
302
- return
303
- fi
304
- fi
305
-
306
- # Fallback: check .ccs/task.md
307
- local task_file="${cwd}/.ccs/task.md"
308
- if [ -f "$task_file" ]; then
309
- local last_skill
310
- last_skill=$(grep -oE '/ccs-[a-z]+' "$task_file" 2>/dev/null | tail -1)
311
- [ -n "$last_skill" ] && { echo "$last_skill"; return; }
312
- fi
313
-
314
- echo "Idle"
315
- }
316
-
317
- if [ -n "$clean_cwd" ]; then
318
- SL_SKILL=$(cache_get "skill-label" "_detect_skill '$clean_cwd'" 2)
319
- fi
320
-
321
- # --- 6h. New fields ---
322
-
323
- # Session duration
324
- dur_ms=$(json_nested "cost" "total_duration_ms")
325
- [ -z "$dur_ms" ] && dur_ms=0
326
- SL_DURATION=$(fmt_duration "$dur_ms")
327
-
328
- # Lines changed
329
- SL_LINES_ADDED=$(json_nested "cost" "total_lines_added")
330
- SL_LINES_REMOVED=$(json_nested "cost" "total_lines_removed")
331
- [ -z "$SL_LINES_ADDED" ] && SL_LINES_ADDED=0
332
- [ -z "$SL_LINES_REMOVED" ] && SL_LINES_REMOVED=0
333
-
334
- # API duration
335
- api_ms=$(json_nested "cost" "total_api_duration_ms")
336
- [ -z "$api_ms" ] && api_ms=0
337
- SL_API_DURATION=$(fmt_duration "$api_ms")
338
-
339
- # Vim mode (absent when vim is off)
340
- SL_VIM_MODE=""
341
- if [ "$cfg_show_vim" = "true" ]; then
342
- SL_VIM_MODE=$(json_nested_val "vim" "mode")
343
- fi
344
-
345
- # Agent name (absent when not in agent mode)
346
- SL_AGENT_NAME=""
347
- if [ "$cfg_show_agent" = "true" ]; then
348
- SL_AGENT_NAME=$(json_nested_val "agent" "name")
349
- fi
350
-
351
- # Cache stats (formatted)
352
- SL_CACHE_CREATE=$(fmt_tok "$cur_cache_create")
353
- SL_CACHE_READ=$(fmt_tok "$cur_cache_read")
354
-
355
- # Burn rate (cost per minute)
356
- SL_BURN_RATE=""
357
- if [ "$cfg_show_burn_rate" = "true" ] && [ "$dur_ms" -gt 60000 ] 2>/dev/null; then
358
- SL_BURN_RATE=$(awk -v cost="$cost_raw" -v ms="$dur_ms" \
359
- 'BEGIN { if (ms > 0 && cost+0 > 0) { rate = cost / (ms / 60000); printf "$%.2f/m", rate } }')
360
- fi
361
-
362
- # Exceeds 200k flag
363
- SL_EXCEEDS_200K=""
364
- [ -n "$(json_bool "exceeds_200k_tokens")" ] && SL_EXCEEDS_200K="true"
365
-
366
- # Version
367
- SL_VERSION=$(json_val "version")
368
-
369
- # ── 7. Dynamic column widths ──
370
- SL_C1=$(( SL_TERM_WIDTH / 2 - 4 ))
371
- [ "$SL_C1" -lt 25 ] && SL_C1=25
372
- [ "$SL_C1" -gt 42 ] && SL_C1=42
373
-
374
- # ── 8. Source layout and render ──
375
- layout_file="${STATUSLINE_DIR}/layouts/${active_layout}.sh"
376
- if [ -f "$layout_file" ]; then
377
- source "$layout_file"
378
- else
379
- source "${STATUSLINE_DIR}/layouts/standard.sh"
380
- fi
381
-
382
- render_layout
1
+ #!/usr/bin/env bash
2
+ # skill-statusline v2.0 — Core engine
3
+ # Reads Claude Code JSON from stdin, computes all fields, renders via layout
4
+
5
+ STATUSLINE_DIR="${HOME}/.claude/statusline"
6
+ CONFIG_FILE="${HOME}/.claude/statusline-config.json"
7
+
8
+ # ── 0. Global safety: kill entire script if it takes too long ──
9
+ # Claude Code calls this on every refresh; a hang here freezes the terminal
10
+ _sl_cleanup() { exit 0; }
11
+ trap '_sl_cleanup' ALRM 2>/dev/null
12
+ ( sleep 3 && kill -ALRM $$ 2>/dev/null ) &
13
+ _SL_WATCHDOG_PID=$!
14
+
15
+ # ── 0b. Read stdin JSON (with timeout) ──
16
+ # If stdin isn't piped properly, `cat` hangs forever — use read with timeout
17
+ input=""
18
+ if [ -t 0 ]; then
19
+ # stdin is a terminal (not piped) — no data to read, output nothing
20
+ kill "$_SL_WATCHDOG_PID" 2>/dev/null; wait "$_SL_WATCHDOG_PID" 2>/dev/null
21
+ exit 0
22
+ fi
23
+ # Read all of stdin but bail after 2 seconds
24
+ input=$(timeout 2 cat 2>/dev/null || cat 2>/dev/null)
25
+ if [ -z "$input" ]; then
26
+ kill "$_SL_WATCHDOG_PID" 2>/dev/null; wait "$_SL_WATCHDOG_PID" 2>/dev/null
27
+ exit 0
28
+ fi
29
+
30
+ # ── 1. Source modules ──
31
+ source "${STATUSLINE_DIR}/json-parser.sh"
32
+ source "${STATUSLINE_DIR}/helpers.sh"
33
+
34
+ # ── 2. Load config ──
35
+ active_theme="default"
36
+ active_layout="standard"
37
+ cfg_warn_threshold=85
38
+ cfg_bar_width=40
39
+ cfg_show_burn_rate="false"
40
+ cfg_show_vim="true"
41
+ cfg_show_agent="true"
42
+
43
+ if [ -f "$CONFIG_FILE" ]; then
44
+ _cfg=$(cat "$CONFIG_FILE" 2>/dev/null)
45
+ _t=$(echo "$_cfg" | grep -o '"theme"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\(.*\)"/\1/')
46
+ _l=$(echo "$_cfg" | grep -o '"layout"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:.*"\(.*\)"/\1/')
47
+ [ -n "$_t" ] && active_theme="$_t"
48
+ [ -n "$_l" ] && active_layout="$_l"
49
+ _w=$(echo "$_cfg" | grep -o '"compaction_warning_threshold"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
50
+ _bw=$(echo "$_cfg" | grep -o '"bar_width"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
51
+ _br=$(echo "$_cfg" | grep -o '"show_burn_rate"[[:space:]]*:[[:space:]]*true' | head -1)
52
+ _sv=$(echo "$_cfg" | grep -o '"show_vim_mode"[[:space:]]*:[[:space:]]*false' | head -1)
53
+ _sa=$(echo "$_cfg" | grep -o '"show_agent_name"[[:space:]]*:[[:space:]]*false' | head -1)
54
+ [ -n "$_w" ] && cfg_warn_threshold="$_w"
55
+ [ -n "$_bw" ] && cfg_bar_width="$_bw"
56
+ [ -n "$_br" ] && cfg_show_burn_rate="true"
57
+ [ -n "$_sv" ] && cfg_show_vim="false"
58
+ [ -n "$_sa" ] && cfg_show_agent="false"
59
+ fi
60
+
61
+ # Allow env override (for ccsl preview)
62
+ [ -n "$STATUSLINE_THEME_OVERRIDE" ] && active_theme="$STATUSLINE_THEME_OVERRIDE"
63
+ [ -n "$STATUSLINE_LAYOUT_OVERRIDE" ] && active_layout="$STATUSLINE_LAYOUT_OVERRIDE"
64
+
65
+ # ── 3. Source theme ──
66
+ theme_file="${STATUSLINE_DIR}/themes/${active_theme}.sh"
67
+ if [ -f "$theme_file" ]; then
68
+ source "$theme_file"
69
+ else
70
+ source "${STATUSLINE_DIR}/themes/default.sh"
71
+ fi
72
+
73
+ # ── 4. Terminal width detection (with timeout) ──
74
+ SL_TERM_WIDTH=${COLUMNS:-0}
75
+ if [ "$SL_TERM_WIDTH" -eq 0 ] 2>/dev/null; then
76
+ _tw=$(timeout 1 tput cols 2>/dev/null || echo "")
77
+ [ -n "$_tw" ] && [ "$_tw" -gt 0 ] && SL_TERM_WIDTH="$_tw"
78
+ fi
79
+ [ "$SL_TERM_WIDTH" -eq 0 ] && SL_TERM_WIDTH=80
80
+
81
+ # Auto-downgrade layout for narrow terminals
82
+ if [ "$SL_TERM_WIDTH" -lt 60 ]; then
83
+ active_layout="compact"
84
+ elif [ "$SL_TERM_WIDTH" -lt 80 ] && [ "$active_layout" = "full" ]; then
85
+ active_layout="standard"
86
+ fi
87
+
88
+ # Dynamic bar width
89
+ BAR_WIDTH="$cfg_bar_width"
90
+ if [ "$SL_TERM_WIDTH" -gt 100 ]; then
91
+ _dyn=$(( SL_TERM_WIDTH - 20 ))
92
+ [ "$_dyn" -gt 60 ] && _dyn=60
93
+ [ "$_dyn" -gt "$BAR_WIDTH" ] && BAR_WIDTH="$_dyn"
94
+ elif [ "$SL_TERM_WIDTH" -lt 70 ]; then
95
+ BAR_WIDTH=20
96
+ fi
97
+
98
+ # ── 5. Initialize cache ──
99
+ _sl_cache_init
100
+
101
+ # ── 6. Parse ALL JSON fields ──
102
+
103
+ # --- 6a. Directory ---
104
+ SL_CWD=$(json_nested_val "workspace" "current_dir")
105
+ [ -z "$SL_CWD" ] && SL_CWD=$(json_val "cwd")
106
+ if [ -z "$SL_CWD" ]; then
107
+ SL_DIR="~"
108
+ clean_cwd=""
109
+ else
110
+ clean_cwd=$(to_fwd "$SL_CWD")
111
+ SL_DIR=$(echo "$clean_cwd" | awk -F'/' '{if(NF>3) print $(NF-2)"/"$(NF-1)"/"$NF; else if(NF>2) print $(NF-1)"/"$NF; else print $0}')
112
+ [ -z "$SL_DIR" ] && SL_DIR="~"
113
+ fi
114
+
115
+ # --- 6b. Model ---
116
+ SL_MODEL_DISPLAY=$(json_nested_val "model" "display_name")
117
+ SL_MODEL_ID=$(json_nested_val "model" "id")
118
+ # Flat fallback
119
+ [ -z "$SL_MODEL_DISPLAY" ] && SL_MODEL_DISPLAY=$(json_val "display_name")
120
+ [ -z "$SL_MODEL_ID" ] && SL_MODEL_ID=$(json_val "id")
121
+ [ -z "$SL_MODEL_DISPLAY" ] && SL_MODEL_DISPLAY="unknown"
122
+
123
+ model_ver=""
124
+ if [ -n "$SL_MODEL_ID" ]; then
125
+ model_ver=$(echo "$SL_MODEL_ID" | sed -n 's/.*-\([0-9]*\)-\([0-9]*\)$/\1.\2/p')
126
+ fi
127
+ if [ -n "$model_ver" ] && ! echo "$SL_MODEL_DISPLAY" | grep -q '[0-9]'; then
128
+ SL_MODEL="${SL_MODEL_DISPLAY} ${model_ver}"
129
+ else
130
+ SL_MODEL="$SL_MODEL_DISPLAY"
131
+ fi
132
+
133
+ # --- 6c. Context ACCURATE computation from current_usage ---
134
+ ctx_size=$(json_nested "context_window" "context_window_size")
135
+ cur_input=$(json_deep "context_window" "current_usage" "input_tokens")
136
+ cur_output=$(json_deep "context_window" "current_usage" "output_tokens")
137
+ cur_cache_create=$(json_deep "context_window" "current_usage" "cache_creation_input_tokens")
138
+ cur_cache_read=$(json_deep "context_window" "current_usage" "cache_read_input_tokens")
139
+
140
+ [ -z "$ctx_size" ] && ctx_size=200000
141
+ [ -z "$cur_input" ] && cur_input=0
142
+ [ -z "$cur_output" ] && cur_output=0
143
+ [ -z "$cur_cache_create" ] && cur_cache_create=0
144
+ [ -z "$cur_cache_read" ] && cur_cache_read=0
145
+
146
+ # Claude's formula: input + cache_creation + cache_read (output excluded from context %)
147
+ ctx_used=$(awk -v a="$cur_input" -v b="$cur_cache_create" -v c="$cur_cache_read" \
148
+ 'BEGIN { printf "%d", a + b + c }')
149
+
150
+ # Self-calculated percentage
151
+ calc_pct=0
152
+ if [ "$cur_input" -gt 0 ] 2>/dev/null; then
153
+ calc_pct=$(awk -v used="$ctx_used" -v total="$ctx_size" \
154
+ 'BEGIN { if (total > 0) printf "%d", (used * 100) / total; else print 0 }')
155
+ fi
156
+
157
+ # Reported percentage as fallback
158
+ reported_pct=$(json_nested "context_window" "used_percentage")
159
+
160
+ # Use self-calculated if we have current_usage data, else fallback
161
+ if [ "$cur_input" -gt 0 ] 2>/dev/null; then
162
+ SL_CTX_PCT="$calc_pct"
163
+ elif [ -n "$reported_pct" ] && [ "$reported_pct" != "null" ]; then
164
+ SL_CTX_PCT=$(echo "$reported_pct" | cut -d. -f1)
165
+ else
166
+ SL_CTX_PCT=0
167
+ fi
168
+
169
+ SL_CTX_REMAINING=$(( 100 - SL_CTX_PCT ))
170
+ [ "$SL_CTX_REMAINING" -lt 0 ] && SL_CTX_REMAINING=0
171
+
172
+ # Context color
173
+ if [ "$SL_CTX_PCT" -gt 90 ] 2>/dev/null; then
174
+ CTX_CLR="$CLR_CTX_CRIT"
175
+ elif [ "$SL_CTX_PCT" -gt 75 ] 2>/dev/null; then
176
+ CTX_CLR="$CLR_CTX_HIGH"
177
+ elif [ "$SL_CTX_PCT" -gt 40 ] 2>/dev/null; then
178
+ CTX_CLR="$CLR_CTX_MED"
179
+ else
180
+ CTX_CLR="$CLR_CTX_LOW"
181
+ fi
182
+
183
+ # Build context bar
184
+ filled=$(( SL_CTX_PCT * BAR_WIDTH / 100 ))
185
+ [ "$filled" -gt "$BAR_WIDTH" ] && filled=$BAR_WIDTH
186
+ empty=$(( BAR_WIDTH - filled ))
187
+ bar_filled=""; bar_empty=""
188
+ i=0; while [ $i -lt $filled ]; do bar_filled="${bar_filled}${BAR_FILLED}"; i=$((i+1)); done
189
+ i=0; while [ $i -lt $empty ]; do bar_empty="${bar_empty}${BAR_EMPTY}"; i=$((i+1)); done
190
+ SL_CTX_BAR="${CTX_CLR}${bar_filled}${CLR_RST}${CLR_BAR_EMPTY}${bar_empty}${CLR_RST} ${CTX_CLR}${SL_CTX_PCT}%${CLR_RST}"
191
+
192
+ # Compaction warning
193
+ SL_COMPACT_WARNING=""
194
+ if [ "$SL_CTX_PCT" -ge 95 ] 2>/dev/null; then
195
+ SL_COMPACT_WARNING=" ${CLR_CTX_CRIT}${CLR_BOLD}COMPACTING${CLR_RST}"
196
+ elif [ "$SL_CTX_PCT" -ge "$cfg_warn_threshold" ] 2>/dev/null; then
197
+ SL_COMPACT_WARNING=" ${CLR_CTX_HIGH}${SL_CTX_REMAINING}% left${CLR_RST}"
198
+ fi
199
+
200
+ # --- 6d. GitHub (with caching + timeouts) ---
201
+ SL_BRANCH="no-git"
202
+ SL_GIT_DIRTY=""
203
+ SL_GITHUB=""
204
+ gh_user=""
205
+ gh_repo=""
206
+
207
+ if [ -n "$clean_cwd" ]; then
208
+ SL_BRANCH=$(cache_get "git-branch" "timeout 2 git --no-optional-locks -C '$clean_cwd' symbolic-ref --short HEAD 2>/dev/null || timeout 2 git --no-optional-locks -C '$clean_cwd' rev-parse --short HEAD 2>/dev/null" 5)
209
+ [ -z "$SL_BRANCH" ] && SL_BRANCH="no-git"
210
+
211
+ if [ "$SL_BRANCH" != "no-git" ]; then
212
+ remote_url=$(cache_get "git-remote" "timeout 2 git --no-optional-locks -C '$clean_cwd' remote get-url origin" 10)
213
+ if [ -n "$remote_url" ]; then
214
+ gh_user=$(echo "$remote_url" | sed 's|.*github\.com[:/]\([^/]*\)/.*|\1|')
215
+ [ "$gh_user" = "$remote_url" ] && gh_user=""
216
+ gh_repo=$(echo "$remote_url" | sed 's|.*/\([^/]*\)\.git$|\1|; s|.*/\([^/]*\)$|\1|')
217
+ [ "$gh_repo" = "$remote_url" ] && gh_repo=""
218
+ fi
219
+
220
+ # Dirty check (shorter cache — changes more often)
221
+ _staged=$(cache_get "git-staged" "timeout 2 git --no-optional-locks -C '$clean_cwd' diff --cached --quiet 2>/dev/null && echo clean || echo dirty" 3)
222
+ _unstaged=$(cache_get "git-unstaged" "timeout 2 git --no-optional-locks -C '$clean_cwd' diff --quiet 2>/dev/null && echo clean || echo dirty" 3)
223
+ [ "$_staged" = "dirty" ] && SL_GIT_DIRTY="${CLR_GIT_STAGED}+${CLR_RST}"
224
+ [ "$_unstaged" = "dirty" ] && SL_GIT_DIRTY="${SL_GIT_DIRTY}${CLR_GIT_UNSTAGED}~${CLR_RST}"
225
+ fi
226
+ fi
227
+
228
+ if [ -n "$gh_repo" ]; then
229
+ SL_GITHUB="${gh_user}/${gh_repo}/${SL_BRANCH}"
230
+ else
231
+ SL_GITHUB="$SL_BRANCH"
232
+ fi
233
+
234
+ # --- 6e. Cost ---
235
+ cost_raw=$(json_nested "cost" "total_cost_usd")
236
+ [ -z "$cost_raw" ] && cost_raw=$(json_num "total_cost_usd")
237
+ if [ -z "$cost_raw" ] || [ "$cost_raw" = "0" ]; then
238
+ SL_COST='$0.00'
239
+ else
240
+ SL_COST=$(awk -v c="$cost_raw" 'BEGIN { if (c < 0.01) printf "$%.4f", c; else printf "$%.2f", c }')
241
+ fi
242
+
243
+ # --- 6f. Tokens (window vs cumulative) ---
244
+ # Current window tokens (what's actually loaded — accurate)
245
+ [ -z "$cur_input" ] && cur_input=0
246
+ [ -z "$cur_output" ] && cur_output=0
247
+ SL_TOKENS_WIN_IN=$(fmt_tok "$cur_input")
248
+ SL_TOKENS_WIN_OUT=$(fmt_tok "$cur_output")
249
+
250
+ # Cumulative session tokens (grows forever, for reference)
251
+ cum_input=$(json_nested "context_window" "total_input_tokens")
252
+ cum_output=$(json_nested "context_window" "total_output_tokens")
253
+ # Flat fallback
254
+ [ -z "$cum_input" ] && cum_input=$(json_num "total_input_tokens")
255
+ [ -z "$cum_output" ] && cum_output=$(json_num "total_output_tokens")
256
+ [ -z "$cum_input" ] && cum_input=0
257
+ [ -z "$cum_output" ] && cum_output=0
258
+ SL_TOKENS_CUM_IN=$(fmt_tok "$cum_input")
259
+ SL_TOKENS_CUM_OUT=$(fmt_tok "$cum_output")
260
+
261
+ # --- 6g. Skill detection (with caching longer TTL to avoid I/O thrashing) ---
262
+ SL_SKILL="Idle"
263
+
264
+ _detect_skill() {
265
+ local cwd="$1"
266
+ local tpath="" search_path="$cwd" proj_hash proj_dir
267
+
268
+ while [ -n "$search_path" ] && [ "$search_path" != "/" ]; do
269
+ proj_hash=$(echo "$search_path" | sed 's|^/\([a-zA-Z]\)/|\U\1--|; s|^[A-Z]:/|&|; s|:/|--|; s|/|-|g')
270
+ proj_dir="$HOME/.claude/projects/${proj_hash}"
271
+ if [ -d "$proj_dir" ]; then
272
+ # Use ls with head to avoid sorting huge file lists
273
+ tpath=$(ls -t "$proj_dir"/*.jsonl 2>/dev/null | head -1)
274
+ [ -n "$tpath" ] && break
275
+ fi
276
+ search_path=$(echo "$search_path" | sed 's|/[^/]*$||')
277
+ done
278
+
279
+ if [ -n "$tpath" ] && [ -f "$tpath" ]; then
280
+ local last_tool
281
+ # Only read the last 50 lines — enough to find the most recent tool_use
282
+ last_tool=$(tail -50 "$tpath" 2>/dev/null | grep -o '"type":"tool_use","id":"[^"]*","name":"[^"]*"' | tail -1 | sed 's/.*"name":"\([^"]*\)".*/\1/')
283
+
284
+ if [ -n "$last_tool" ]; then
285
+ case "$last_tool" in
286
+ Task)
287
+ # Lightweight agent count — check last 50 lines only
288
+ local agent_count
289
+ agent_count=$(tail -50 "$tpath" 2>/dev/null | grep -c '"type":"tool_use","id":"[^"]*","name":"Task"')
290
+ if [ "$agent_count" -gt 1 ]; then
291
+ echo "${agent_count} Agents"
292
+ else
293
+ local agent_desc
294
+ agent_desc=$(tail -50 "$tpath" 2>/dev/null | grep -o '"description":"[^"]*"' | tail -1 | sed 's/"description":"//;s/"$//')
295
+ if [ -n "$agent_desc" ]; then
296
+ echo "Agent($(echo "$agent_desc" | cut -c1-20))"
297
+ else
298
+ echo "Agent"
299
+ fi
300
+ fi ;;
301
+ Read) echo "Read" ;;
302
+ Write) echo "Write" ;;
303
+ Edit) echo "Edit" ;;
304
+ MultiEdit) echo "Multi Edit" ;;
305
+ Glob) echo "Search(Files)" ;;
306
+ Grep) echo "Search(Content)" ;;
307
+ Bash) echo "Terminal" ;;
308
+ WebSearch) echo "Web Search" ;;
309
+ WebFetch) echo "Web Fetch" ;;
310
+ Skill) echo "Skill" ;;
311
+ AskUserQuestion) echo "Asking..." ;;
312
+ EnterPlanMode) echo "Planning" ;;
313
+ ExitPlanMode) echo "Plan Ready" ;;
314
+ TaskCreate) echo "Task Create" ;;
315
+ TaskUpdate) echo "Task Update" ;;
316
+ TaskGet) echo "Task Get" ;;
317
+ TaskList) echo "Task List" ;;
318
+ TaskStop) echo "Task Stop" ;;
319
+ TaskOutput) echo "Task Output" ;;
320
+ NotebookEdit) echo "Notebook" ;;
321
+ *) echo "$last_tool" ;;
322
+ esac
323
+ return
324
+ fi
325
+ fi
326
+
327
+ # Fallback: check .ccs/task.md
328
+ local task_file="${cwd}/.ccs/task.md"
329
+ if [ -f "$task_file" ]; then
330
+ local last_skill
331
+ last_skill=$(grep -oE '/ccs-[a-z]+' "$task_file" 2>/dev/null | tail -1)
332
+ [ -n "$last_skill" ] && { echo "$last_skill"; return; }
333
+ fi
334
+
335
+ echo "Idle"
336
+ }
337
+
338
+ if [ -n "$clean_cwd" ]; then
339
+ # Cache skill detection for 5 seconds (was 2s too frequent for heavy I/O)
340
+ SL_SKILL=$(cache_get "skill-label" "_detect_skill '$clean_cwd'" 5)
341
+ fi
342
+
343
+ # --- 6h. New fields ---
344
+
345
+ # Session duration
346
+ dur_ms=$(json_nested "cost" "total_duration_ms")
347
+ [ -z "$dur_ms" ] && dur_ms=0
348
+ SL_DURATION=$(fmt_duration "$dur_ms")
349
+
350
+ # Lines changed
351
+ SL_LINES_ADDED=$(json_nested "cost" "total_lines_added")
352
+ SL_LINES_REMOVED=$(json_nested "cost" "total_lines_removed")
353
+ [ -z "$SL_LINES_ADDED" ] && SL_LINES_ADDED=0
354
+ [ -z "$SL_LINES_REMOVED" ] && SL_LINES_REMOVED=0
355
+
356
+ # API duration
357
+ api_ms=$(json_nested "cost" "total_api_duration_ms")
358
+ [ -z "$api_ms" ] && api_ms=0
359
+ SL_API_DURATION=$(fmt_duration "$api_ms")
360
+
361
+ # Vim mode (absent when vim is off)
362
+ SL_VIM_MODE=""
363
+ if [ "$cfg_show_vim" = "true" ]; then
364
+ SL_VIM_MODE=$(json_nested_val "vim" "mode")
365
+ fi
366
+
367
+ # Agent name (absent when not in agent mode)
368
+ SL_AGENT_NAME=""
369
+ if [ "$cfg_show_agent" = "true" ]; then
370
+ SL_AGENT_NAME=$(json_nested_val "agent" "name")
371
+ fi
372
+
373
+ # Cache stats (formatted)
374
+ SL_CACHE_CREATE=$(fmt_tok "$cur_cache_create")
375
+ SL_CACHE_READ=$(fmt_tok "$cur_cache_read")
376
+
377
+ # Burn rate (cost per minute)
378
+ SL_BURN_RATE=""
379
+ if [ "$cfg_show_burn_rate" = "true" ] && [ "$dur_ms" -gt 60000 ] 2>/dev/null; then
380
+ SL_BURN_RATE=$(awk -v cost="$cost_raw" -v ms="$dur_ms" \
381
+ 'BEGIN { if (ms > 0 && cost+0 > 0) { rate = cost / (ms / 60000); printf "$%.2f/m", rate } }')
382
+ fi
383
+
384
+ # Exceeds 200k flag
385
+ SL_EXCEEDS_200K=""
386
+ [ -n "$(json_bool "exceeds_200k_tokens")" ] && SL_EXCEEDS_200K="true"
387
+
388
+ # Version
389
+ SL_VERSION=$(json_val "version")
390
+
391
+ # ── 7. Dynamic column widths ──
392
+ SL_C1=$(( SL_TERM_WIDTH / 2 - 4 ))
393
+ [ "$SL_C1" -lt 25 ] && SL_C1=25
394
+ [ "$SL_C1" -gt 42 ] && SL_C1=42
395
+
396
+ # ── 8. Source layout and render ──
397
+ layout_file="${STATUSLINE_DIR}/layouts/${active_layout}.sh"
398
+ if [ -f "$layout_file" ]; then
399
+ source "$layout_file"
400
+ else
401
+ source "${STATUSLINE_DIR}/layouts/standard.sh"
402
+ fi
403
+
404
+ render_layout
405
+
406
+ # ── 9. Cleanup watchdog ──
407
+ kill "$_SL_WATCHDOG_PID" 2>/dev/null
408
+ wait "$_SL_WATCHDOG_PID" 2>/dev/null