codebyplan 1.10.3 → 1.11.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/dist/cli.js +534 -209
- package/package.json +1 -1
- package/templates/hooks/README.md +58 -16
- package/templates/hooks/cbp-statusline.mjs +385 -0
- package/templates/hooks/cbp-statusline.py +331 -0
- package/templates/hooks/cbp-statusline.sh +138 -82
- package/templates/hooks/cbp-subagent-statusline.mjs +200 -0
- package/templates/hooks/cbp-subagent-statusline.py +183 -0
- package/templates/hooks/cbp-subagent-statusline.sh +87 -39
|
@@ -1,21 +1,62 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# @hook: NOT-A-HOOK (statusLine renderer, invoked by settings.json statusLine.command)
|
|
3
|
-
# Claude Code Status Line -
|
|
3
|
+
# Claude Code Status Line — multi-runtime dispatcher + bash renderer
|
|
4
4
|
# Purpose: Renders up to 6 structured lines of Claude Code status from stdin JSON.
|
|
5
|
+
# This file is BOTH the dispatcher (selects bash/node/python at the top)
|
|
6
|
+
# AND the always-available bash renderer (inline below the dispatch block).
|
|
5
7
|
# Not a PreToolUse/PostToolUse/Notification hook — do NOT register in hooks[].
|
|
6
|
-
# Shows: Identity | Context | Cost | Rate Limits | Repo/PR | Worktree
|
|
8
|
+
# Shows: Identity (folder + branch + model …) | Context | Cost | Rate Limits | Repo/PR | Worktree
|
|
7
9
|
#
|
|
8
10
|
# --------------------------------------------------------------------------
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
11
|
+
# RENDERER SELECTION (per-device, gitignored)
|
|
12
|
+
# .codebyplan/statusline.local.json -> { "renderer": "bash" | "node" | "python" }
|
|
13
|
+
# Default bash. When the chosen runtime is unavailable, silently falls back to bash.
|
|
14
|
+
# Set/inspect via `codebyplan statusline [bash|node|python]`.
|
|
15
|
+
#
|
|
16
|
+
# DISPLAY OPTIONS (team-shared, committed)
|
|
17
|
+
# .codebyplan/statusline.json -> { "lines": {identity,context,cost,rate_limits,
|
|
18
|
+
# repo_pr,worktree}, "no_color": bool }
|
|
19
|
+
#
|
|
20
|
+
# ENV-VAR OVERRIDES (env > config > default)
|
|
21
|
+
# CBP_STATUSLINE_HIDE_IDENTITY=1 suppress line 1 (folder, branch, model, effort, …)
|
|
22
|
+
# CBP_STATUSLINE_HIDE_CONTEXT=1 suppress line 2 (context bar, tokens, cache)
|
|
23
|
+
# CBP_STATUSLINE_HIDE_COST=1 suppress line 3 (cost, duration, lines)
|
|
24
|
+
# CBP_STATUSLINE_HIDE_RATE_LIMITS=1 suppress line 4 (5h / 7d rate limits)
|
|
25
|
+
# CBP_STATUSLINE_HIDE_REPO_PR=1 suppress line 5 (repo host/owner/name, PR)
|
|
26
|
+
# CBP_STATUSLINE_HIDE_WORKTREE=1 suppress line 6 (worktree name/branch/path)
|
|
27
|
+
# CBP_STATUSLINE_NO_COLOR=1 strip all ANSI colour codes (also honoured by $NO_COLOR)
|
|
28
|
+
#
|
|
29
|
+
# TEST SEAMS (no effect in normal use)
|
|
30
|
+
# CBP_STATUSLINE_ROOT=<dir> override the .codebyplan/ lookup root (default: repo root)
|
|
31
|
+
# CBP_STATUSLINE_NOW=<epoch> override "now" for relative-time rendering (determinism)
|
|
17
32
|
# --------------------------------------------------------------------------
|
|
18
33
|
|
|
34
|
+
# ============================================================
|
|
35
|
+
# DISPATCHER — resolve root + renderer, delegate when possible
|
|
36
|
+
# ============================================================
|
|
37
|
+
CBP_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || CBP_HOOK_DIR="."
|
|
38
|
+
CBP_ROOT="${CBP_STATUSLINE_ROOT:-}"
|
|
39
|
+
if [ -z "$CBP_ROOT" ]; then
|
|
40
|
+
CBP_ROOT="$(cd "$CBP_HOOK_DIR/../.." && pwd 2>/dev/null)" || CBP_ROOT="$PWD"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
CBP_RENDERER="bash"
|
|
44
|
+
CBP_LOCAL_CFG="$CBP_ROOT/.codebyplan/statusline.local.json"
|
|
45
|
+
if [ -f "$CBP_LOCAL_CFG" ] && command -v jq >/dev/null 2>&1; then
|
|
46
|
+
_r="$(jq -r '.renderer // empty' "$CBP_LOCAL_CFG" 2>/dev/null)"
|
|
47
|
+
case "$_r" in bash|node|python) CBP_RENDERER="$_r" ;; esac
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if [ "$CBP_RENDERER" = "node" ] && command -v node >/dev/null 2>&1 \
|
|
51
|
+
&& [ -f "$CBP_HOOK_DIR/cbp-statusline.mjs" ]; then
|
|
52
|
+
CBP_STATUSLINE_ROOT="$CBP_ROOT" exec node "$CBP_HOOK_DIR/cbp-statusline.mjs"
|
|
53
|
+
fi
|
|
54
|
+
if [ "$CBP_RENDERER" = "python" ] && command -v python3 >/dev/null 2>&1 \
|
|
55
|
+
&& [ -f "$CBP_HOOK_DIR/cbp-statusline.py" ]; then
|
|
56
|
+
CBP_STATUSLINE_ROOT="$CBP_ROOT" exec python3 "$CBP_HOOK_DIR/cbp-statusline.py"
|
|
57
|
+
fi
|
|
58
|
+
# Fall through → inline bash renderer (default + universal fallback).
|
|
59
|
+
|
|
19
60
|
INPUT=$(cat)
|
|
20
61
|
|
|
21
62
|
# ---- One jq invocation captures all ~40 fields --------------------------------
|
|
@@ -25,8 +66,6 @@ eval "$(echo "$INPUT" | jq -r '
|
|
|
25
66
|
@sh "CWD=\(.cwd // "")",
|
|
26
67
|
@sh "WS_CURRENT_DIR=\(.workspace.current_dir // "")",
|
|
27
68
|
@sh "WS_PROJECT_DIR=\(.workspace.project_dir // "")",
|
|
28
|
-
@sh "WS_ADDED_DIRS_LEN=\((.workspace.added_dirs // []) | length)",
|
|
29
|
-
@sh "WS_GIT_WORKTREE=\(.workspace.git_worktree // "")",
|
|
30
69
|
@sh "WS_REPO_HOST=\(.workspace.repo.host // "")",
|
|
31
70
|
@sh "WS_REPO_OWNER=\(.workspace.repo.owner // "")",
|
|
32
71
|
@sh "WS_REPO_NAME=\(.workspace.repo.name // "")",
|
|
@@ -35,11 +74,8 @@ eval "$(echo "$INPUT" | jq -r '
|
|
|
35
74
|
@sh "API_DURATION=\(.cost.total_api_duration_ms // 0)",
|
|
36
75
|
@sh "LINES_ADD=\(.cost.total_lines_added // 0)",
|
|
37
76
|
@sh "LINES_DEL=\(.cost.total_lines_removed // 0)",
|
|
38
|
-
@sh "CTX_TOT_IN=\(.context_window.total_input_tokens // 0)",
|
|
39
|
-
@sh "CTX_TOT_OUT=\(.context_window.total_output_tokens // 0)",
|
|
40
77
|
@sh "CTX_SIZE=\(.context_window.context_window_size // 200000)",
|
|
41
78
|
@sh "CTX_PCT=\(.context_window.used_percentage // 0)",
|
|
42
|
-
@sh "CTX_REM=\(.context_window.remaining_percentage // 100)",
|
|
43
79
|
@sh "CUR_IN=\(.context_window.current_usage.input_tokens // 0)",
|
|
44
80
|
@sh "CUR_OUT=\(.context_window.current_usage.output_tokens // 0)",
|
|
45
81
|
@sh "CACHE_CREATE=\(.context_window.current_usage.cache_creation_input_tokens // 0)",
|
|
@@ -51,14 +87,7 @@ eval "$(echo "$INPUT" | jq -r '
|
|
|
51
87
|
@sh "RATE_5H_RESETS=\(.rate_limits.five_hour.resets_at // 0)",
|
|
52
88
|
@sh "RATE_7D_PCT=\(.rate_limits.seven_day.used_percentage // "")",
|
|
53
89
|
@sh "RATE_7D_RESETS=\(.rate_limits.seven_day.resets_at // 0)",
|
|
54
|
-
# Note: RATE_*_PCT use `// ""` (not `// 0`) so the `[ -n "$RATE_*_PCT" ]` absence gate
|
|
55
|
-
# below distinguishes "API said 0% used" from "rate_limits object absent". Do not
|
|
56
|
-
# change to `// 0` — it would silently suppress the rate-limit line on subscriber
|
|
57
|
-
# accounts at 0% usage at session start.
|
|
58
|
-
@sh "SESSION_ID=\(.session_id // "")",
|
|
59
90
|
@sh "SESSION_NAME=\(.session_name // "")",
|
|
60
|
-
@sh "TRANSCRIPT_PATH=\(.transcript_path // "")",
|
|
61
|
-
@sh "VERSION=\(.version // "")",
|
|
62
91
|
@sh "OUTPUT_STYLE=\(.output_style.name // "")",
|
|
63
92
|
@sh "VIM_MODE=\(.vim.mode // "")",
|
|
64
93
|
@sh "AGENT_NAME=\(.agent.name // "")",
|
|
@@ -68,13 +97,41 @@ eval "$(echo "$INPUT" | jq -r '
|
|
|
68
97
|
@sh "WT_NAME=\(.worktree.name // "")",
|
|
69
98
|
@sh "WT_PATH=\(.worktree.path // "")",
|
|
70
99
|
@sh "WT_BRANCH=\(.worktree.branch // "")",
|
|
71
|
-
@sh "WT_ORIG_CWD=\(.worktree.original_cwd // "")",
|
|
72
100
|
@sh "WT_ORIG_BRANCH=\(.worktree.original_branch // "")"
|
|
73
101
|
')"
|
|
102
|
+
# Note: RATE_*_PCT use `// ""` (not `// 0`) so the absence gate below distinguishes
|
|
103
|
+
# "API said 0% used" from "rate_limits object absent". Do not change to `// 0`.
|
|
104
|
+
|
|
105
|
+
# ---- Config: line toggles + no_color from .codebyplan/statusline.json --------
|
|
106
|
+
CFG_IDENTITY=true; CFG_CONTEXT=true; CFG_COST=true
|
|
107
|
+
CFG_RATE_LIMITS=true; CFG_REPO_PR=true; CFG_WORKTREE=true; CFG_NO_COLOR=false
|
|
108
|
+
CBP_CFG="$CBP_ROOT/.codebyplan/statusline.json"
|
|
109
|
+
if [ -f "$CBP_CFG" ] && command -v jq >/dev/null 2>&1; then
|
|
110
|
+
# Use `!= false` / `== true` (NOT jq `//`): the `//` operator treats an explicit
|
|
111
|
+
# `false` as absent and would coerce a hidden line back to visible. `!= false`
|
|
112
|
+
# yields true for missing/true and false only for an explicit false.
|
|
113
|
+
eval "$(jq -r '
|
|
114
|
+
"CFG_IDENTITY=\(.lines.identity != false)",
|
|
115
|
+
"CFG_CONTEXT=\(.lines.context != false)",
|
|
116
|
+
"CFG_COST=\(.lines.cost != false)",
|
|
117
|
+
"CFG_RATE_LIMITS=\(.lines.rate_limits != false)",
|
|
118
|
+
"CFG_REPO_PR=\(.lines.repo_pr != false)",
|
|
119
|
+
"CFG_WORKTREE=\(.lines.worktree != false)",
|
|
120
|
+
"CFG_NO_COLOR=\(.no_color == true)"
|
|
121
|
+
' "$CBP_CFG" 2>/dev/null)"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Effective line visibility: env HIDE=1 > config false > default show.
|
|
125
|
+
# Returns 0 (show) / 1 (hide). $1 = env suffix, $2 = config value (true/false).
|
|
126
|
+
should_show() {
|
|
127
|
+
local envvar="CBP_STATUSLINE_HIDE_$1"
|
|
128
|
+
if [ "${!envvar:-0}" = "1" ]; then return 1; fi
|
|
129
|
+
if [ "$2" = "false" ]; then return 1; fi
|
|
130
|
+
return 0
|
|
131
|
+
}
|
|
74
132
|
|
|
75
|
-
# ---- Colour setup
|
|
76
|
-
|
|
77
|
-
if [ -n "${NO_COLOR:-}" ] || [ "${CBP_STATUSLINE_NO_COLOR:-0}" = "1" ]; then
|
|
133
|
+
# ---- Colour setup (env > config) ---------------------------------------------
|
|
134
|
+
if [ -n "${NO_COLOR:-}" ] || [ "${CBP_STATUSLINE_NO_COLOR:-0}" = "1" ] || [ "$CFG_NO_COLOR" = "true" ]; then
|
|
78
135
|
RST=''; DIM=''; BOLD=''; GREEN=''; YELLOW=''; RED=''; CYAN=''; MAGENTA=''; BLUE=''
|
|
79
136
|
else
|
|
80
137
|
RST='\033[0m'
|
|
@@ -89,22 +146,28 @@ else
|
|
|
89
146
|
fi
|
|
90
147
|
|
|
91
148
|
# ---- Float-safe percentage comparison ----------------------------------------
|
|
92
|
-
# Usage: awk_gte VALUE THRESHOLD (returns 0 = true, 1 = false)
|
|
93
149
|
awk_gte() { awk -v v="$1" -v t="$2" 'BEGIN{exit !(v+0 >= t+0)}'; }
|
|
94
150
|
|
|
95
|
-
# ---- Token/size formatter (K / M)
|
|
151
|
+
# ---- Token/size formatter (K / M) — integer round-half-up (cross-runtime) -----
|
|
96
152
|
fmt_k() {
|
|
97
153
|
local val=$1
|
|
98
154
|
if [ "$val" -ge 1000000 ] 2>/dev/null; then
|
|
99
|
-
|
|
155
|
+
local t=$(( (val + 50000) / 100000 ))
|
|
156
|
+
printf "%d.%dM" $(( t / 10 )) $(( t % 10 ))
|
|
100
157
|
elif [ "$val" -ge 1000 ] 2>/dev/null; then
|
|
101
|
-
|
|
158
|
+
local t=$(( (val + 50) / 100 ))
|
|
159
|
+
printf "%d.%dK" $(( t / 10 )) $(( t % 10 ))
|
|
102
160
|
else
|
|
103
161
|
printf "%d" "$val"
|
|
104
162
|
fi
|
|
105
163
|
}
|
|
106
164
|
|
|
107
|
-
# ----
|
|
165
|
+
# ---- Cost formatter ($X.XXXX) — integer round-half-up (cross-runtime) ---------
|
|
166
|
+
fmt_cost() {
|
|
167
|
+
awk -v c="$1" 'BEGIN{ n=int(c*10000 + 0.5); printf "$%d.%04d", int(n/10000), n%10000 }'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# ---- Duration formatter (ms → Xh Xm Xs) --------------------------------------
|
|
108
171
|
fmt_dur() {
|
|
109
172
|
local ms=$1
|
|
110
173
|
local secs=$(( ms / 1000 ))
|
|
@@ -117,44 +180,59 @@ fmt_dur() {
|
|
|
117
180
|
fi
|
|
118
181
|
}
|
|
119
182
|
|
|
120
|
-
# ----
|
|
121
|
-
|
|
122
|
-
|
|
183
|
+
# ---- "now" with test seam ----------------------------------------------------
|
|
184
|
+
cbp_now() {
|
|
185
|
+
if [ -n "${CBP_STATUSLINE_NOW:-}" ]; then printf '%s' "$CBP_STATUSLINE_NOW"; else date +%s; fi
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# ---- Relative-time formatter (epoch → "now" / Xm / Xh / Xd) -------------------
|
|
123
189
|
fmt_rel_time() {
|
|
124
190
|
local epoch=$1
|
|
125
|
-
local now
|
|
126
|
-
now=$(date +%s)
|
|
191
|
+
local now; now=$(cbp_now)
|
|
127
192
|
local delta=$(( epoch - now ))
|
|
128
|
-
if [ "$delta" -le 0 ]; then
|
|
129
|
-
|
|
130
|
-
elif [ "$delta" -ge
|
|
131
|
-
|
|
132
|
-
elif [ "$delta" -ge 3600 ]; then
|
|
133
|
-
printf "%dh" $(( delta / 3600 ))
|
|
134
|
-
else
|
|
135
|
-
printf "%dm" $(( delta / 60 ))
|
|
136
|
-
fi
|
|
193
|
+
if [ "$delta" -le 0 ]; then printf "now"
|
|
194
|
+
elif [ "$delta" -ge 86400 ]; then printf "%dd" $(( delta / 86400 ))
|
|
195
|
+
elif [ "$delta" -ge 3600 ]; then printf "%dh" $(( delta / 3600 ))
|
|
196
|
+
else printf "%dm" $(( delta / 60 )); fi
|
|
137
197
|
}
|
|
138
198
|
|
|
199
|
+
# ---- Folder + branch (always-available "where am I") -------------------------
|
|
200
|
+
FOLDER=""
|
|
201
|
+
if [ -n "$CWD" ]; then FOLDER="$(basename "$CWD")"
|
|
202
|
+
elif [ -n "$WS_CURRENT_DIR" ]; then FOLDER="$(basename "$WS_CURRENT_DIR")"; fi
|
|
203
|
+
BRANCH="$WT_BRANCH"
|
|
204
|
+
if [ -z "$BRANCH" ] && [ -n "$CWD" ]; then
|
|
205
|
+
# Only trust a successful resolution: on an unborn branch git prints "HEAD"
|
|
206
|
+
# to stdout but exits non-zero — reset to empty so all renderers agree.
|
|
207
|
+
BRANCH="$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null)" || BRANCH=""
|
|
208
|
+
fi
|
|
209
|
+
|
|
139
210
|
# ============================================================
|
|
140
|
-
# LINE 1 — Identity
|
|
211
|
+
# LINE 1 — Identity (folder + branch + model + flags)
|
|
141
212
|
# ============================================================
|
|
142
|
-
if
|
|
213
|
+
if should_show IDENTITY "$CFG_IDENTITY"; then
|
|
143
214
|
L1=""
|
|
144
215
|
|
|
216
|
+
# Folder + branch — git-derived, visible even without a registered worktree
|
|
217
|
+
if [ -n "$FOLDER" ]; then
|
|
218
|
+
L1="${BOLD}${BLUE}${FOLDER}${RST}"
|
|
219
|
+
if [ -n "$BRANCH" ]; then
|
|
220
|
+
L1="${L1} ${DIM}⎇${RST}${CYAN}${BRANCH}${RST}"
|
|
221
|
+
fi
|
|
222
|
+
L1="${L1} "
|
|
223
|
+
fi
|
|
224
|
+
|
|
145
225
|
# Prefix: wt > session > agent (pick first present)
|
|
146
226
|
if [ -n "$WT_NAME" ]; then
|
|
147
|
-
L1="${DIM}wt:${RST}${MAGENTA}${WT_NAME}${RST} "
|
|
227
|
+
L1="${L1}${DIM}wt:${RST}${MAGENTA}${WT_NAME}${RST} "
|
|
148
228
|
elif [ -n "$SESSION_NAME" ]; then
|
|
149
|
-
L1="${DIM}session:${RST}${MAGENTA}${SESSION_NAME}${RST} "
|
|
229
|
+
L1="${L1}${DIM}session:${RST}${MAGENTA}${SESSION_NAME}${RST} "
|
|
150
230
|
elif [ -n "$AGENT_NAME" ]; then
|
|
151
|
-
L1="${DIM}agent:${RST}${MAGENTA}${AGENT_NAME}${RST} "
|
|
231
|
+
L1="${L1}${DIM}agent:${RST}${MAGENTA}${AGENT_NAME}${RST} "
|
|
152
232
|
fi
|
|
153
233
|
|
|
154
|
-
# Model
|
|
155
|
-
if [ -n "$MODEL_NAME" ]
|
|
156
|
-
L1="${L1}${BOLD}${CYAN}${MODEL_NAME}${RST} ${DIM}(${MODEL_ID})${RST}"
|
|
157
|
-
elif [ -n "$MODEL_NAME" ]; then
|
|
234
|
+
# Model — display name only (no redundant id-in-parens)
|
|
235
|
+
if [ -n "$MODEL_NAME" ]; then
|
|
158
236
|
L1="${L1}${BOLD}${CYAN}${MODEL_NAME}${RST}"
|
|
159
237
|
elif [ -n "$MODEL_ID" ]; then
|
|
160
238
|
L1="${L1}${BOLD}${CYAN}${MODEL_ID}${RST}"
|
|
@@ -175,27 +253,11 @@ if [ "${CBP_STATUSLINE_HIDE_IDENTITY:-0}" != "1" ]; then
|
|
|
175
253
|
L1="${L1} ${DIM}style:${RST}${OUTPUT_STYLE}"
|
|
176
254
|
fi
|
|
177
255
|
|
|
178
|
-
# Version
|
|
179
|
-
if [ -n "$VERSION" ]; then
|
|
180
|
-
L1="${L1} ${DIM}v:${VERSION}${RST}"
|
|
181
|
-
fi
|
|
182
|
-
|
|
183
256
|
# Vim mode
|
|
184
257
|
if [ -n "$VIM_MODE" ]; then
|
|
185
258
|
L1="${L1} ${DIM}[${VIM_MODE}]${RST}"
|
|
186
259
|
fi
|
|
187
260
|
|
|
188
|
-
# Session id (short 8-char prefix — distinguishes parallel sessions)
|
|
189
|
-
if [ -n "$SESSION_ID" ]; then
|
|
190
|
-
L1="${L1} ${DIM}sid:${SESSION_ID:0:8}${RST}"
|
|
191
|
-
fi
|
|
192
|
-
|
|
193
|
-
# Transcript file (basename only — full path would blow out the line)
|
|
194
|
-
if [ -n "$TRANSCRIPT_PATH" ]; then
|
|
195
|
-
L1="${L1} ${DIM}log:$(basename "$TRANSCRIPT_PATH")${RST}"
|
|
196
|
-
fi
|
|
197
|
-
|
|
198
|
-
# Only print when we have actual content
|
|
199
261
|
if [ -n "$L1" ]; then
|
|
200
262
|
printf "%b\n" "$L1"
|
|
201
263
|
fi
|
|
@@ -204,8 +266,7 @@ fi
|
|
|
204
266
|
# ============================================================
|
|
205
267
|
# LINE 2 — Context window
|
|
206
268
|
# ============================================================
|
|
207
|
-
if
|
|
208
|
-
# Context bar colour by usage
|
|
269
|
+
if should_show CONTEXT "$CFG_CONTEXT"; then
|
|
209
270
|
if awk_gte "$CTX_PCT" 75; then
|
|
210
271
|
BAR_COLOR="$RED"
|
|
211
272
|
elif awk_gte "$CTX_PCT" 50; then
|
|
@@ -214,7 +275,6 @@ if [ "${CBP_STATUSLINE_HIDE_CONTEXT:-0}" != "1" ]; then
|
|
|
214
275
|
BAR_COLOR="$GREEN"
|
|
215
276
|
fi
|
|
216
277
|
|
|
217
|
-
# 20-char progress bar (ceiling division so 99% shows 20/20, not 19/20)
|
|
218
278
|
FILLED=$(( (CTX_PCT + 4) / 5 ))
|
|
219
279
|
[ "$FILLED" -gt 20 ] && FILLED=20
|
|
220
280
|
EMPTY=$(( 20 - FILLED ))
|
|
@@ -241,8 +301,8 @@ fi
|
|
|
241
301
|
# ============================================================
|
|
242
302
|
# LINE 3 — Cost
|
|
243
303
|
# ============================================================
|
|
244
|
-
if
|
|
245
|
-
COST_F=$(
|
|
304
|
+
if should_show COST "$CFG_COST"; then
|
|
305
|
+
COST_F=$(fmt_cost "$COST")
|
|
246
306
|
DUR_F=$(fmt_dur "$DURATION")
|
|
247
307
|
API_DUR_F=$(fmt_dur "$API_DURATION")
|
|
248
308
|
|
|
@@ -253,8 +313,7 @@ fi
|
|
|
253
313
|
# ============================================================
|
|
254
314
|
# LINE 4 — Rate limits
|
|
255
315
|
# ============================================================
|
|
256
|
-
if
|
|
257
|
-
# Emit this line only when at least one window has resets_at != 0
|
|
316
|
+
if should_show RATE_LIMITS "$CFG_RATE_LIMITS"; then
|
|
258
317
|
HAVE_RATE=0
|
|
259
318
|
[ -n "$RATE_5H_PCT" ] && [ "$RATE_5H_RESETS" != "0" ] && HAVE_RATE=1
|
|
260
319
|
[ -n "$RATE_7D_PCT" ] && [ "$RATE_7D_RESETS" != "0" ] && HAVE_RATE=1
|
|
@@ -298,15 +357,13 @@ fi
|
|
|
298
357
|
# ============================================================
|
|
299
358
|
# LINE 5 — Repo / PR
|
|
300
359
|
# ============================================================
|
|
301
|
-
if
|
|
360
|
+
if should_show REPO_PR "$CFG_REPO_PR"; then
|
|
302
361
|
L5=""
|
|
303
362
|
|
|
304
|
-
# Repo segment (host/owner/name when host is present)
|
|
305
363
|
if [ -n "$WS_REPO_HOST" ]; then
|
|
306
364
|
L5="${DIM}${WS_REPO_HOST}/${WS_REPO_OWNER}/${WS_REPO_NAME}${RST}"
|
|
307
365
|
fi
|
|
308
366
|
|
|
309
|
-
# PR segment
|
|
310
367
|
if [ -n "$PR_NUMBER" ]; then
|
|
311
368
|
PR_SEG="${DIM}PR${RST} ${CYAN}#${PR_NUMBER}${RST}"
|
|
312
369
|
if [ -n "$PR_REVIEW_STATE" ]; then
|
|
@@ -330,13 +387,12 @@ fi
|
|
|
330
387
|
# ============================================================
|
|
331
388
|
# LINE 6 — Worktree
|
|
332
389
|
# ============================================================
|
|
333
|
-
if
|
|
390
|
+
if should_show WORKTREE "$CFG_WORKTREE"; then
|
|
334
391
|
if [ -n "$WT_NAME" ]; then
|
|
335
392
|
L6="${MAGENTA}${WT_NAME}${RST} ${DIM}@${RST} ${CYAN}${WT_BRANCH}${RST}"
|
|
336
393
|
if [ -n "$WT_ORIG_BRANCH" ] && [ "$WT_ORIG_BRANCH" != "$WT_BRANCH" ]; then
|
|
337
394
|
L6="${L6} ${DIM}(was: ${WT_ORIG_BRANCH})${RST}"
|
|
338
395
|
fi
|
|
339
|
-
# Truncate path if longer than 60 chars
|
|
340
396
|
WT_PATH_DISP="$WT_PATH"
|
|
341
397
|
if [ "${#WT_PATH}" -gt 60 ]; then
|
|
342
398
|
WT_PATH_DISP="...${WT_PATH: -57}"
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @hook: NOT-A-HOOK (subagentStatusLine renderer, invoked via the cbp-subagent-statusline.sh dispatcher)
|
|
3
|
+
// Claude Code Subagent Status Line — node renderer (ESM; .mjs forces ESM regardless
|
|
4
|
+
// of the host repo's package.json "type"). Byte-identical output to the bash renderer
|
|
5
|
+
// in cbp-subagent-statusline.sh and the python renderer in cbp-subagent-statusline.py.
|
|
6
|
+
// Output protocol: one {"id":...,"content":...} JSON line per task. See the .sh file
|
|
7
|
+
// for the full option/env/seam contract. Never throws to stdout.
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
function main() {
|
|
16
|
+
const input = fs.readFileSync(0, "utf8");
|
|
17
|
+
let data;
|
|
18
|
+
try {
|
|
19
|
+
data = JSON.parse(input);
|
|
20
|
+
} catch {
|
|
21
|
+
data = {};
|
|
22
|
+
}
|
|
23
|
+
if (data == null || typeof data !== "object") data = {};
|
|
24
|
+
|
|
25
|
+
let root = process.env.CBP_STATUSLINE_ROOT || process.argv[2] || "";
|
|
26
|
+
if (!root) root = path.resolve(__dirname, "..", "..");
|
|
27
|
+
|
|
28
|
+
const COLUMNS = Math.trunc(Number(data.columns ?? 0)) || 0;
|
|
29
|
+
const tasks = Array.isArray(data.tasks) ? data.tasks : [];
|
|
30
|
+
|
|
31
|
+
// ---- Config: no_color from statusline.json --------------------------------
|
|
32
|
+
let cfgNoColor = false;
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(
|
|
35
|
+
path.join(root, ".codebyplan", "statusline.json"),
|
|
36
|
+
"utf8"
|
|
37
|
+
);
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
if (
|
|
40
|
+
parsed &&
|
|
41
|
+
typeof parsed === "object" &&
|
|
42
|
+
typeof parsed.no_color === "boolean"
|
|
43
|
+
) {
|
|
44
|
+
cfgNoColor = parsed.no_color;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// absent / invalid → keep default
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const noColor =
|
|
51
|
+
(process.env.NO_COLOR != null && process.env.NO_COLOR !== "") ||
|
|
52
|
+
process.env.CBP_SUBAGENT_STATUSLINE_NO_COLOR === "1" ||
|
|
53
|
+
cfgNoColor === true;
|
|
54
|
+
const C = noColor
|
|
55
|
+
? { RST: "", DIM: "", BOLD: "", GREEN: "", RED: "", BLUE: "" }
|
|
56
|
+
: {
|
|
57
|
+
RST: "\x1b[0m",
|
|
58
|
+
DIM: "\x1b[2m",
|
|
59
|
+
BOLD: "\x1b[1m",
|
|
60
|
+
GREEN: "\x1b[32m",
|
|
61
|
+
RED: "\x1b[31m",
|
|
62
|
+
BLUE: "\x1b[34m",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const fmtK = (val) => {
|
|
66
|
+
const v = Number(val);
|
|
67
|
+
if (v >= 1000000) {
|
|
68
|
+
const t = Math.floor((v + 50000) / 100000);
|
|
69
|
+
return `${Math.floor(t / 10)}.${t % 10}M`;
|
|
70
|
+
} else if (v >= 1000) {
|
|
71
|
+
const t = Math.floor((v + 50) / 100);
|
|
72
|
+
return `${Math.floor(t / 10)}.${t % 10}K`;
|
|
73
|
+
}
|
|
74
|
+
return String(Math.trunc(v));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const cbpNow = () => {
|
|
78
|
+
if (
|
|
79
|
+
process.env.CBP_STATUSLINE_NOW != null &&
|
|
80
|
+
process.env.CBP_STATUSLINE_NOW !== ""
|
|
81
|
+
) {
|
|
82
|
+
return Math.trunc(Number(process.env.CBP_STATUSLINE_NOW));
|
|
83
|
+
}
|
|
84
|
+
return Math.floor(Date.now() / 1000);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const fmtAgeSecs = (start) => {
|
|
88
|
+
let delta = cbpNow() - Math.trunc(Number(start));
|
|
89
|
+
if (delta < 0) delta = 0;
|
|
90
|
+
if (delta >= 3600)
|
|
91
|
+
return `${Math.floor(delta / 3600)}h${Math.floor((delta % 3600) / 60)}m`;
|
|
92
|
+
if (delta >= 60) return `${Math.floor(delta / 60)}m${delta % 60}s`;
|
|
93
|
+
return `${delta}s`;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const isAsciiLetter = (b) =>
|
|
97
|
+
(b >= 0x41 && b <= 0x5a) || (b >= 0x61 && b <= 0x7a);
|
|
98
|
+
|
|
99
|
+
// ANSI-aware byte-based truncation — mirrors the bash byte walk exactly.
|
|
100
|
+
const truncate = (rendered) => {
|
|
101
|
+
if (!(COLUMNS > 0)) return rendered;
|
|
102
|
+
const buf = Buffer.from(rendered, "utf8");
|
|
103
|
+
const rawLen = buf.length;
|
|
104
|
+
let target = COLUMNS - 3;
|
|
105
|
+
if (target < 0) target = 0;
|
|
106
|
+
let vis = 0;
|
|
107
|
+
let i = 0;
|
|
108
|
+
let inEsc = false;
|
|
109
|
+
while (i < rawLen && vis < target) {
|
|
110
|
+
const b = buf[i];
|
|
111
|
+
if (inEsc) {
|
|
112
|
+
if (isAsciiLetter(b)) inEsc = false;
|
|
113
|
+
} else if (b === 0x1b) {
|
|
114
|
+
inEsc = true;
|
|
115
|
+
} else {
|
|
116
|
+
vis += 1;
|
|
117
|
+
}
|
|
118
|
+
i += 1;
|
|
119
|
+
}
|
|
120
|
+
while (i < rawLen) {
|
|
121
|
+
const b = buf[i];
|
|
122
|
+
if (inEsc) {
|
|
123
|
+
if (isAsciiLetter(b)) inEsc = false;
|
|
124
|
+
i += 1;
|
|
125
|
+
} else if (b === 0x1b) {
|
|
126
|
+
inEsc = true;
|
|
127
|
+
i += 1;
|
|
128
|
+
} else {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (i < rawLen) {
|
|
133
|
+
const head = buf.subarray(0, i);
|
|
134
|
+
const tail = Buffer.from(C.RST + "...", "utf8");
|
|
135
|
+
return Buffer.concat([head, tail]).toString("utf8");
|
|
136
|
+
}
|
|
137
|
+
return rendered;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const task of tasks) {
|
|
142
|
+
const T_ID = task && task.id != null ? String(task.id) : "";
|
|
143
|
+
if (T_ID === "") continue;
|
|
144
|
+
const T_NAME = task.name != null ? String(task.name) : "";
|
|
145
|
+
const T_TYPE = task.type != null ? String(task.type) : "";
|
|
146
|
+
const T_STATUS = task.status != null ? String(task.status) : "";
|
|
147
|
+
const T_DESC = task.description != null ? String(task.description) : "";
|
|
148
|
+
const T_START = Math.trunc(Number(task.startTime ?? 0)) || 0;
|
|
149
|
+
const T_TOKENS = Math.trunc(Number(task.tokenCount ?? 0)) || 0;
|
|
150
|
+
|
|
151
|
+
let icon;
|
|
152
|
+
switch (T_STATUS) {
|
|
153
|
+
case "running":
|
|
154
|
+
icon = `${C.GREEN}●${C.RST}`;
|
|
155
|
+
break;
|
|
156
|
+
case "pending":
|
|
157
|
+
icon = `${C.DIM}○${C.RST}`;
|
|
158
|
+
break;
|
|
159
|
+
case "completed":
|
|
160
|
+
icon = `${C.BLUE}✓${C.RST}`;
|
|
161
|
+
break;
|
|
162
|
+
case "failed":
|
|
163
|
+
icon = `${C.RED}✗${C.RST}`;
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
icon = `${C.DIM}•${C.RST}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let row = `${icon} ${C.BOLD}${T_NAME}${C.RST}`;
|
|
170
|
+
if (T_TYPE) row += ` ${C.DIM}(${T_TYPE})${C.RST}`;
|
|
171
|
+
if (
|
|
172
|
+
process.env.CBP_SUBAGENT_STATUSLINE_HIDE_DESCRIPTION !== "1" &&
|
|
173
|
+
T_DESC
|
|
174
|
+
) {
|
|
175
|
+
row += ` ${C.DIM}—${C.RST} ${T_DESC}`;
|
|
176
|
+
}
|
|
177
|
+
if (
|
|
178
|
+
process.env.CBP_SUBAGENT_STATUSLINE_HIDE_TOKENS !== "1" &&
|
|
179
|
+
T_TOKENS > 0
|
|
180
|
+
) {
|
|
181
|
+
row += ` ${C.DIM}·${C.RST} ${C.DIM}tokens:${C.RST}${fmtK(T_TOKENS)}`;
|
|
182
|
+
}
|
|
183
|
+
if (T_START > 0) {
|
|
184
|
+
row += ` ${C.DIM}·${C.RST} ${C.DIM}age:${C.RST}${fmtAgeSecs(T_START)}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rendered = truncate(row);
|
|
188
|
+
out.push(
|
|
189
|
+
`{"id":${JSON.stringify(T_ID)},"content":${JSON.stringify(rendered)}}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
process.stdout.write(out.length ? out.join("\n") + "\n" : "");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
main();
|
|
198
|
+
} catch {
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|