@kurokeita/add-skill 1.21.0 → 2.1.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.
@@ -1,50 +1,125 @@
1
1
  ---
2
2
  name: statusline-setup
3
- description: 'Set up the Claude Code statusline with cwd, git branch, model, context %, session token totals, and 5h/7d quota usage with ETA. Use when asked to "set up my statusline", "install statusline", "configure Claude Code statusline", or to add a colored status line showing rate limits and token usage. Detects OS and installs the appropriate variant (bash + jq on Linux/macOS, PowerShell on Windows).'
3
+ description: 'Set up the Claude Code or Gemini/Antigravity CLI statusline as two side-by-side bordered boxes — a repo box (cwd, git branch, working-tree state, last commit) and a session box (model, context % + token totals, 5h/7d quota with ETA). Use when asked to "set up my statusline", "install statusline", "configure statusline", or to add a colored multi-line status line showing rate limits and token usage. Detects OS and installs the appropriate variant (bash + jq on Linux/macOS, PowerShell on Windows).'
4
4
  ---
5
5
 
6
6
  # Statusline Setup
7
7
 
8
- Install a Claude Code statusline that renders, left to right, pipe-separated:
8
+ Install a Claude Code or Gemini/Antigravity statusline that renders two side-by-side bordered boxes:
9
9
 
10
- 1. **cwd** — 24-bit ANSI `#5EFFFF` (`38;2;94;255;255`), with `$HOME` / `%USERPROFILE%` shortened to `~`.
11
- 2. **git branch** in parentheses — 24-bit ANSI `#C24870` (`38;2;194;72;112`), only when inside a repo.
12
- 3. **model display name + effort** in parens — 24-bit ANSI `#E89440` (`38;2;232;148;64`), effort only when present.
13
- 4. **`ctx N%`** — 24-bit ANSI `#009AFB` (`38;2;0;154;251`), the `context_window.used_percentage`.
14
- 5. **tokens** as `↑<in> ↓<out>` — 24-bit ANSI `#937bda` (`38;2;147;123;218`), its own pipe-separated segment immediately after `ctx`.
15
- 6. **`5h:N%(eta)`** and **`7d:N%(eta)`** — colored independently by remaining quota (see below).
10
+ ```text
11
+ ╭─ repo ──────────────────────────────────────────────────────╮ ╭─ session ─────────────╮
12
+ │ ~/dev/ai_agents │ │ Opus 5 (high) │
13
+ │ feat/statusline-boxed-layout │ │ ctx 42% ↑1.2M ↓18.4k │
14
+ │ +2 ~5 ?1 ↑3 │ │ 5h 38% resets 2h 14m │
15
+ │ ffd30ce feat(skills): add iso-response plain language skill │ │ 7d 61% resets 3d 4h │
16
+ ╰─────────────────────────────────────────────────────────────╯ ╰───────────────────────╯
17
+ ```
18
+
19
+ ## Box Contents
20
+
21
+ ### repo box
22
+
23
+ 1. **cwd** — 24-bit ANSI `#5EFFFF` (`38;2;94;255;255`), shown at **full length**; only `$HOME` / `%USERPROFILE%` is replaced with `~`. No component abbreviation.
24
+ 2. **git branch** — 24-bit ANSI `#C24870` (`38;2;194;72;112`), bare name with no parentheses. Detached HEAD renders `detached@<short-sha>`.
25
+ 3. **working-tree state** — `+<staged> ~<modified> -<deleted> ?<untracked>` in bright green `#4eba65`, followed by `↑<ahead> ↓<behind>` in dim `#787e8a`. A clean tree renders a dim `clean`. Each counter is omitted when zero.
26
+ 4. **last commit** — `%h %s` in dim `#787e8a`, truncated to 80 characters with a trailing `…`.
27
+
28
+ Rows 2-4 appear only inside a git repo.
29
+
30
+ ### session box
31
+
32
+ 1. **model display name + effort** in parens — 24-bit ANSI `#E89440` (`38;2;232;148;64`), effort only when present.
33
+ 2. **`ctx N%`** — 24-bit ANSI `#009AFB` (`38;2;0;154;251`) — followed on the same row, after two spaces, by **tokens** `↑<in> ↓<out>` in `#937bda` (`38;2;147;123;218`). When no token data exists the row is just `ctx N%`.
34
+ 3. **`5h N% resets <eta>`** — colored by remaining quota (see below).
35
+ 4. **`7d N% resets <eta>`** — colored independently of the 5h row.
36
+
37
+ ## Box Rendering Rules
38
+
39
+ - Border glyphs `╭ ╮ ╰ ╯ ─ │` in `#5a5f69` (`38;2;90;95;105`); the box title in `#8c929e` (`38;2;140;146;158`).
40
+ - Each box's inner width is the longest of its content rows, or its title + 2, whichever is larger.
41
+ - Both boxes are padded to the **same row count** with blank rows, so the two frames start and end on the same terminal lines.
42
+ - The two boxes are joined by a single space on every line.
43
+ - Rows are emitted newline-separated; the statusline host renders them as multiple lines.
44
+ - When both boxes have zero content rows the script prints nothing and exits 0.
45
+
46
+ ### Measuring width with ANSI escapes present
16
47
 
17
- Segments are joined with ANSI `0;37` pipes (` | `).
48
+ Build **two parallel arrays** per box: one holding the plain text, one holding the same text wrapped in color escapes. Measure padding from the plain array only — measuring the colored string counts the escape bytes and destroys alignment.
49
+
50
+ ### UTF-8 locale guard (bash only)
51
+
52
+ `${#str}` counts **bytes**, not characters, unless the shell is in a UTF-8 locale. The rows contain `↑`, `↓` and `…`, so under `LC_ALL=C` every such row is padded 2 characters short. Guard at the top of the script with a subprocess-free probe:
53
+
54
+ ```bash
55
+ probe='↑'
56
+ if (( ${#probe} != 1 )); then
57
+ for loc in C.UTF-8 en_US.UTF-8 en_US.utf8; do
58
+ export LC_ALL="$loc"
59
+ (( ${#probe} == 1 )) && break
60
+ done
61
+ fi
62
+ ```
63
+
64
+ Assigning `LC_ALL` makes bash call `setlocale` immediately, so `${#probe}` re-evaluates inside the loop; a locale the system lacks leaves the previous value and the loop moves on.
18
65
 
19
66
  ## When to Use This Skill
20
67
 
21
- - "Set up my Claude Code statusline"
68
+ - "Set up my Claude Code statusline" or "Set up my Antigravity statusline"
22
69
  - "Install the statusline"
23
70
  - "Configure my statusline with token counts and quota"
24
71
  - Any request to add a colored status line showing context %, rate limits, or session tokens.
25
72
 
26
- ## OS Detection
73
+ ## OS & Platform Detection
27
74
 
28
- Detect the platform first and install only the matching variant:
75
+ Detect the operating system and targeted platform first, then write and wire the corresponding configuration:
76
+
77
+ ### Claude Code
29
78
 
30
79
  - **Linux / macOS** → write `~/.claude/statusline-command.sh` (pure bash + `jq`), wire into `~/.claude/settings.json`.
31
80
  - **Windows** → write `%USERPROFILE%\.claude\statusline-command.ps1`, wire into `%USERPROFILE%\.claude\settings.json`.
32
81
 
82
+ ### Antigravity / Gemini CLI
83
+
84
+ - **Linux / macOS** → write `~/.gemini/antigravity-cli/statusline-command.sh` (pure bash + `jq`), wire into `~/.gemini/antigravity-cli/settings.json`.
85
+ - **Windows** → write `%USERPROFILE%\.gemini\antigravity-cli\statusline-command.ps1`, wire into `%USERPROFILE%\.gemini\antigravity-cli\settings.json`.
86
+
87
+ Multi-line rendering is verified on Claude Code. Antigravity's handling of multi-line statusline output is untested.
88
+
89
+ ## Shell Compatibility
90
+
91
+ macOS ships bash 3.2, so the bash variant must avoid: `local -n` namerefs, `mapfile`/`readarray`, associative arrays, and negative array indices. Pass arrays into helpers as positional parameters instead.
92
+
93
+ The PowerShell variant targets Windows PowerShell 5.1, so avoid PowerShell 7 syntax: null-coalescing `??`, ternary `? :`, and chain operators `&&`/`||`.
94
+
33
95
  ## Token Formatting
34
96
 
35
97
  - `< 1000` → raw integer (e.g. `↑850 ↓120`)
36
98
  - `>= 1_000` → one decimal + `k` (e.g. `↑1.2k ↓34.5k`)
37
99
  - `>= 1_000_000` → one decimal + `M` (e.g. `↑1.2M ↓3.4M`)
38
- - Missing/unparseable transcript → omit the segment entirely (no stray separator, never render `↑0 ↓0`).
100
+ - Missing/unparseable transcript → omit the tokens from the `ctx` row entirely, never render `↑0 ↓0`.
39
101
 
40
102
  ## Token Source
41
103
 
42
- Claude Code does **not** include token totals in the statusline JSON. Read `transcript_path` (JSONL, one message per line) and sum across **assistant** messages:
104
+ Tokens are calculated dynamically:
43
105
 
44
- - `input` = `message.usage.input_tokens` + `message.usage.cache_creation_input_tokens` + `message.usage.cache_read_input_tokens`
45
- - `output` = `message.usage.output_tokens`
106
+ 1. **Transcript Parsing (Claude Code / general)**: Sum across assistant messages in the JSONL `transcript_path` file:
107
+ - `input` = `message.usage.input_tokens` + `message.usage.cache_creation_input_tokens` + `message.usage.cache_read_input_tokens`
108
+ - `output` = `message.usage.output_tokens`
109
+ 2. **Payload Fallback (Antigravity / Gemini CLI)**: If transcript parsing yields `0` tokens (or the transcript doesn't store step token metrics), fallback to:
110
+ - `input` = `context_window.total_input_tokens`
111
+ - `output` = `context_window.total_output_tokens`
46
112
 
47
- Skip lines that fail JSON parsing or lack `message.usage`. If `transcript_path` is missing or the file does not exist, omit the tokens segment.
113
+ ## Git State
114
+
115
+ Read **all** git state from a single `git -C "$cwd" status -b --porcelain` call, never from `PWD`. That one call yields branch name, upstream ahead/behind, and the staged/modified/untracked counts:
116
+
117
+ - The `##` header line carries `branch...upstream [ahead N, behind M]`, or the literal `HEAD (no branch)` when detached — fall back to `git -C "$cwd" rev-parse --short HEAD` for that case.
118
+ - `??` entries are untracked.
119
+ - For every other entry, column 1 is the index and column 2 the working tree. A `D` in either column counts as deleted; any other non-space counts as staged (column 1) or modified (column 2). One file can count in several buckets.
120
+ - Deletions get their own `-N` counter so a staged removal does not read as `+N`, which looks like an addition.
121
+
122
+ The last-commit row needs one further call: `git -C "$cwd" log -1 --format='%h %s'`.
48
123
 
49
124
  ## Quota Colors (by REMAINING quota = 100 - used%)
50
125
 
@@ -57,7 +132,7 @@ Skip lines that fail JSON parsing or lack `message.usage`. If `transcript_path`
57
132
 
58
133
  The 5h and 7d windows must be colored **independently**.
59
134
 
60
- ## ETA Format (from epoch seconds in `resets_at`)
135
+ ## ETA Format (from epoch seconds or reset duration in seconds)
61
136
 
62
137
  - `> 1 day` → `Xd Yh`
63
138
  - `> 1 hour` → `Xh Ym`
@@ -66,34 +141,57 @@ The 5h and 7d windows must be colored **independently**.
66
141
 
67
142
  ## Input Schema (stdin JSON)
68
143
 
69
- Any field may be missing:
144
+ The input schema depends on the platform:
145
+
146
+ ### Claude Code Schema
70
147
 
148
+ ```json
149
+ {
150
+ "cwd": "/path/to/cwd",
151
+ "transcript_path": "/path/to/transcript.jsonl",
152
+ "model": { "display_name": "Opus 4.7" },
153
+ "effort": { "level": "medium" },
154
+ "context_window": { "used_percentage": 42 },
155
+ "rate_limits": {
156
+ "five_hour": { "used_percentage": 80, "resets_at": 1719213265 },
157
+ "seven_day": { "used_percentage": 50, "resets_at": 1719213265 }
158
+ }
159
+ }
71
160
  ```
72
- cwd
73
- transcript_path
74
- model.display_name
75
- effort.level
76
- context_window.used_percentage
77
- rate_limits.five_hour.used_percentage
78
- rate_limits.five_hour.resets_at
79
- rate_limits.seven_day.used_percentage
80
- rate_limits.seven_day.resets_at
161
+
162
+ ### Antigravity Schema
163
+
164
+ ```json
165
+ {
166
+ "cwd": "/path/to/cwd",
167
+ "transcript_path": "/path/to/transcript.jsonl",
168
+ "model": { "display_name": "Gemini 3.5 Flash" },
169
+ "context_window": {
170
+ "used_percentage": 6.7,
171
+ "total_input_tokens": 70419,
172
+ "total_output_tokens": 16667
173
+ },
174
+ "quota": {
175
+ "gemini-5h": { "remaining_fraction": 0.87, "reset_in_seconds": 2515 },
176
+ "gemini-weekly": { "remaining_fraction": 0.97, "reset_in_seconds": 589315 }
177
+ }
178
+ }
81
179
  ```
82
180
 
83
181
  ## Linux / macOS Implementation
84
182
 
85
- Write `~/.claude/statusline-command.sh` (chmod +x). Pure bash + `jq` — no Python. `jq` must be installed (`apt install jq`, `brew install jq`, etc.); if missing, the script prints a one-line hint and exits 0 so the statusline area stays clean.
183
+ Write `statusline-command.sh` (chmod +x) to the targeted platform directory. Pure bash + `jq` — no Python. `jq` must be installed; if missing, the script prints a one-line hint and exits 0.
86
184
 
87
185
  JSON parsing rules:
88
186
 
89
187
  - Read the full stdin payload **once** with `cat`, parse all fields with a single `jq` call.
90
- - For the JSONL transcript, invoke `jq` **once over the whole file** (do not call `jq` per line) — use a streaming filter that reduces over `inputs`.
188
+ - Use `fromjson?` to process JSONL files line-by-line resiliently to ignore malformed/plain-text entries.
91
189
 
92
- `~/.claude/statusline-command.sh`:
190
+ `statusline-command.sh`:
93
191
 
94
192
  ```bash
95
193
  #!/usr/bin/env bash
96
- # Claude Code statusline — pure bash + jq.
194
+ # Statusline — pure bash + jq. Supports Claude Code & Antigravity payload formats.
97
195
 
98
196
  set -u
99
197
 
@@ -102,12 +200,25 @@ if ! command -v jq >/dev/null 2>&1; then
102
200
  exit 0
103
201
  fi
104
202
 
203
+ # ${#str} counts bytes outside a UTF-8 locale, which mis-pads every row holding
204
+ # an arrow or ellipsis. Assigning LC_ALL re-runs setlocale, so the probe below
205
+ # re-evaluates; an unavailable locale leaves the previous value untouched.
206
+ probe='↑'
207
+ if (( ${#probe} != 1 )); then
208
+ for loc in C.UTF-8 en_US.UTF-8 en_US.utf8; do
209
+ export LC_ALL="$loc"
210
+ (( ${#probe} == 1 )) && break
211
+ done
212
+ fi
213
+
105
214
  C_CWD=$'\033[38;2;94;255;255m'
106
215
  C_BRANCH=$'\033[38;2;194;72;112m'
107
- C_PIPE=$'\033[0;37m'
216
+ C_BOX=$'\033[38;2;90;95;105m'
217
+ C_TITLE=$'\033[38;2;140;146;158m'
108
218
  C_MODEL=$'\033[38;2;232;148;64m'
109
219
  C_CTX=$'\033[38;2;0;154;251m'
110
220
  C_TOK=$'\033[38;2;147;123;218m'
221
+ C_DIM=$'\033[38;2;120;126;138m'
111
222
  RESET=$'\033[0m'
112
223
 
113
224
  GREEN=$'\033[38;2;78;186;101m'
@@ -129,8 +240,13 @@ fmt_eta() {
129
240
  local resets="$1"
130
241
  [[ -z "$resets" || "$resets" == "null" ]] && { printf ''; return; }
131
242
  local now delta days hours mins
132
- now=$(date +%s)
133
- delta=$(( resets - now ))
243
+ # Claude Code sends an absolute epoch; Antigravity sends a duration.
244
+ if (( resets < 10000000 )); then
245
+ delta=$resets
246
+ else
247
+ now=$(date +%s)
248
+ delta=$(( resets - now ))
249
+ fi
134
250
  if (( delta <= 0 )); then printf 'now'
135
251
  elif (( delta >= 86400 )); then
136
252
  days=$(( delta / 86400 )); hours=$(( (delta % 86400) / 3600 ))
@@ -152,13 +268,26 @@ fmt_tokens() {
152
268
  fi
153
269
  }
154
270
 
271
+ truncate_to() {
272
+ local s="$1" max="$2"
273
+ if (( ${#s} > max )); then printf '%s…' "${s:0:$(( max - 1 ))}"
274
+ else printf '%s' "$s"
275
+ fi
276
+ }
277
+
155
278
  shorten_home() {
156
279
  local p="$1"
157
280
  [[ -z "$p" ]] && return
158
- if [[ "$p" == "$HOME" ]]; then printf '~'
159
- elif [[ "$p" == "$HOME"/* ]]; then printf '~%s' "${p#$HOME}"
160
- else printf '%s' "$p"
281
+ if [[ "$p" == "$HOME" ]]; then p='~'
282
+ elif [[ "$p" == "$HOME"/* ]]; then p="~${p#$HOME}"
161
283
  fi
284
+ printf '%s' "$p"
285
+ }
286
+
287
+ repeat_char() {
288
+ local ch="$1" n="$2" out=""
289
+ while (( n-- > 0 )); do out+="$ch"; done
290
+ printf '%s' "$out"
162
291
  }
163
292
 
164
293
  # --- read & parse stdin payload in a single jq call ---
@@ -173,48 +302,133 @@ read_fields() {
173
302
  .model.display_name // "",
174
303
  .effort.level // "",
175
304
  (.context_window.used_percentage // "" | tostring),
176
- (.rate_limits.five_hour.used_percentage // "" | tostring),
177
- (.rate_limits.five_hour.resets_at // "" | tostring),
178
- (.rate_limits.seven_day.used_percentage // "" | tostring),
179
- (.rate_limits.seven_day.resets_at // "" | tostring)
180
- ] | join("\u001f")
305
+ (
306
+ .rate_limits.five_hour.used_percentage //
307
+ (if .quota."gemini-5h" != null then ((1.0 - .quota."gemini-5h".remaining_fraction) * 100)
308
+ elif .quota."3p-5h" != null then ((1.0 - .quota."3p-5h".remaining_fraction) * 100)
309
+ else "" end)
310
+ | tostring
311
+ ),
312
+ (
313
+ .rate_limits.five_hour.resets_at //
314
+ .quota."gemini-5h".reset_in_seconds //
315
+ .quota."3p-5h".reset_in_seconds //
316
+ ""
317
+ | tostring
318
+ ),
319
+ (
320
+ .rate_limits.seven_day.used_percentage //
321
+ (if .quota."gemini-weekly" != null then ((1.0 - .quota."gemini-weekly".remaining_fraction) * 100)
322
+ elif .quota."3p-weekly" != null then ((1.0 - .quota."3p-weekly".remaining_fraction) * 100)
323
+ else "" end)
324
+ | tostring
325
+ ),
326
+ (
327
+ .rate_limits.seven_day.resets_at //
328
+ .quota."gemini-weekly".reset_in_seconds //
329
+ .quota."3p-weekly".reset_in_seconds //
330
+ ""
331
+ | tostring
332
+ ),
333
+ (.context_window.total_input_tokens // "" | tostring),
334
+ (.context_window.total_output_tokens // "" | tostring)
335
+ ] | join("")
181
336
  ' 2>/dev/null <<<"$payload"
182
337
  }
183
338
 
184
- IFS=$'\x1f' read -r cwd transcript model effort ctx five_used five_reset seven_used seven_reset < <(read_fields)
339
+ IFS=$'\x1f' read -r cwd transcript model effort ctx five_used five_reset seven_used seven_reset payload_in payload_out < <(read_fields)
185
340
 
186
- segments=()
341
+ repo_plain=(); repo_color=()
187
342
 
188
343
  if [[ -n "$cwd" ]]; then
189
- segments+=("${C_CWD}$(shorten_home "$cwd")${RESET}")
344
+ p=$(shorten_home "$cwd")
345
+ repo_plain+=("$p"); repo_color+=("${C_CWD}${p}${RESET}")
190
346
  fi
191
347
 
192
- # Branch: must use git -C "$cwd", never PWD.
193
- branch=""
348
+ # One `status -b --porcelain` call yields branch, dirty counts and ahead/behind.
349
+ # Must use git -C "$cwd", never PWD.
350
+ branch=""; staged=0; modified=0; deleted=0; untracked=0; ahead=0; behind=0
194
351
  if [[ -n "$cwd" ]]; then
195
- branch=$(git -C "$cwd" rev-parse --abbrev-ref HEAD 2>/dev/null) || branch=""
352
+ while IFS= read -r line; do
353
+ if [[ "$line" == '## '* ]]; then
354
+ head=${line#'## '}
355
+ if [[ "$head" == 'HEAD (no branch)' ]]; then
356
+ branch=$(git -C "$cwd" rev-parse --short HEAD 2>/dev/null)
357
+ [[ -n "$branch" ]] && branch="detached@${branch}"
358
+ else
359
+ branch=${head%%...*}
360
+ if [[ "$head" == *'[ahead '* ]]; then t=${head#*'[ahead '}; ahead=${t%%[,\]]*}; fi
361
+ if [[ "$head" == *'behind '* ]]; then t=${head#*'behind '}; behind=${t%%[,\]]*}; fi
362
+ fi
363
+ elif [[ "$line" == '??'* ]]; then
364
+ (( ++untracked ))
365
+ else
366
+ case "${line:0:1}" in
367
+ ' ') ;;
368
+ 'D') (( ++deleted )) ;;
369
+ *) (( ++staged )) ;;
370
+ esac
371
+ case "${line:1:1}" in
372
+ ' ') ;;
373
+ 'D') (( ++deleted )) ;;
374
+ *) (( ++modified )) ;;
375
+ esac
376
+ fi
377
+ done < <(git -C "$cwd" status -b --porcelain 2>/dev/null)
196
378
  fi
379
+
197
380
  if [[ -n "$branch" ]]; then
198
- segments+=("${C_BRANCH}(${branch})${RESET}")
381
+ repo_plain+=("$branch"); repo_color+=("${C_BRANCH}${branch}${RESET}")
382
+
383
+ if (( staged || modified || deleted || untracked )); then
384
+ dirty=""
385
+ (( staged )) && dirty+="+${staged} "
386
+ (( modified )) && dirty+="~${modified} "
387
+ (( deleted )) && dirty+="-${deleted} "
388
+ (( untracked )) && dirty+="?${untracked} "
389
+ dirty=${dirty% }
390
+ dirty_c="${GREEN}${dirty}${RESET}"
391
+ else
392
+ dirty="clean"; dirty_c="${C_DIM}clean${RESET}"
393
+ fi
394
+ sync=""
395
+ (( ahead )) && sync+=" ↑${ahead}"
396
+ (( behind )) && sync+=" ↓${behind}"
397
+ repo_plain+=("${dirty}${sync}")
398
+ repo_color+=("${dirty_c}${C_DIM}${sync}${RESET}")
399
+
400
+ last=$(git -C "$cwd" log -1 --format='%h %s' 2>/dev/null)
401
+ if [[ -n "$last" ]]; then
402
+ last=$(truncate_to "$last" 80)
403
+ repo_plain+=("$last"); repo_color+=("${C_DIM}${last}${RESET}")
404
+ fi
199
405
  fi
200
406
 
407
+ sess_plain=(); sess_color=()
408
+
201
409
  if [[ -n "$model" ]]; then
202
- if [[ -n "$effort" ]]; then
203
- segments+=("${C_MODEL}${model} (${effort})${RESET}")
204
- else
205
- segments+=("${C_MODEL}${model}${RESET}")
206
- fi
410
+ m="$model"
411
+ [[ -n "$effort" ]] && m="${model} (${effort})"
412
+ sess_plain+=("$m"); sess_color+=("${C_MODEL}${m}${RESET}")
207
413
  fi
208
414
 
209
- if [[ -n "$ctx" ]]; then
210
- ctx_int=$(awk -v n="$ctx" 'BEGIN{printf "%d", (n+0.5)}')
211
- segments+=("${C_CTX}ctx ${ctx_int}%${RESET}")
415
+ ti=0
416
+ to=0
417
+
418
+ # --- tokens: sum transcripts, fallback to context_window if needed ---
419
+ if [[ -n "$transcript" ]]; then
420
+ # Resolve path mismatch (if it has .gemini/antigravity/ but it actually is at .gemini/antigravity-cli/)
421
+ if [[ ! -f "$transcript" && "$transcript" == *"/antigravity/"* ]]; then
422
+ corrected_transcript="${transcript//\/antigravity\//\/antigravity-cli\/}"
423
+ if [[ -f "$corrected_transcript" ]]; then
424
+ transcript="$corrected_transcript"
425
+ fi
426
+ fi
212
427
  fi
213
428
 
214
- # --- tokens: one jq pass over the whole JSONL transcript ---
215
429
  if [[ -n "$transcript" && -f "$transcript" ]]; then
216
- tok=$(jq -rs '
217
- reduce .[] as $m ({i:0,o:0};
430
+ tok=$(jq -Rrn '
431
+ reduce (inputs | fromjson?) as $m ({i:0,o:0};
218
432
  ($m.message.usage // null) as $u
219
433
  | if $u == null then .
220
434
  else
@@ -226,49 +440,95 @@ if [[ -n "$transcript" && -f "$transcript" ]]; then
226
440
  ' "$transcript" 2>/dev/null) || tok=""
227
441
  if [[ -n "$tok" ]]; then
228
442
  IFS=$'\t' read -r ti to <<<"$tok"
229
- if (( ti > 0 || to > 0 )); then
230
- segments+=("${C_TOK}↑$(fmt_tokens "$ti") ↓$(fmt_tokens "$to")${RESET}")
231
- fi
232
443
  fi
233
444
  fi
234
445
 
446
+ # Fallback to payload context_window tokens if transcript sum is 0
447
+ if (( ti == 0 && to == 0 )); then
448
+ if [[ -n "$payload_in" && -n "$payload_out" ]]; then
449
+ ti=$payload_in
450
+ to=$payload_out
451
+ fi
452
+ fi
453
+
454
+ tokens=""
455
+ if (( ti > 0 || to > 0 )); then
456
+ tokens="↑$(fmt_tokens "$ti") ↓$(fmt_tokens "$to")"
457
+ fi
458
+
459
+ if [[ -n "$ctx" ]]; then
460
+ ctx_int=$(awk -v n="$ctx" 'BEGIN{printf "%d", (n+0.5)}')
461
+ c="ctx ${ctx_int}%"
462
+ c_col="${C_CTX}${c}${RESET}"
463
+ if [[ -n "$tokens" ]]; then
464
+ c+=" ${tokens}"; c_col+=" ${C_TOK}${tokens}${RESET}"
465
+ fi
466
+ sess_plain+=("$c"); sess_color+=("$c_col")
467
+ elif [[ -n "$tokens" ]]; then
468
+ sess_plain+=("$tokens"); sess_color+=("${C_TOK}${tokens}${RESET}")
469
+ fi
470
+
235
471
  if [[ -n "$five_used" ]]; then
236
472
  fu=$(awk -v n="$five_used" 'BEGIN{printf "%d", (n+0.5)}')
237
473
  eta=$(fmt_eta "$five_reset")
238
- segments+=("$(quota_color "$fu")5h:${fu}%(${eta})${RESET}")
474
+ s="5h ${fu}%"; [[ -n "$eta" ]] && s+=" resets ${eta}"
475
+ sess_plain+=("$s"); sess_color+=("$(quota_color "$fu")${s}${RESET}")
239
476
  fi
240
477
  if [[ -n "$seven_used" ]]; then
241
478
  su=$(awk -v n="$seven_used" 'BEGIN{printf "%d", (n+0.5)}')
242
479
  eta=$(fmt_eta "$seven_reset")
243
- segments+=("$(quota_color "$su")7d:${su}%(${eta})${RESET}")
480
+ s="7d ${su}%"; [[ -n "$eta" ]] && s+=" resets ${eta}"
481
+ sess_plain+=("$s"); sess_color+=("$(quota_color "$su")${s}${RESET}")
244
482
  fi
245
483
 
246
- sep=" ${C_PIPE}|${RESET} "
247
- out=""
248
- for i in "${!segments[@]}"; do
249
- if (( i == 0 )); then out="${segments[$i]}"
250
- else out="${out}${sep}${segments[$i]}"
251
- fi
252
- done
253
- printf '%s' "$out"
254
- ```
255
-
256
- Wire into `~/.claude/settings.json` (merge into existing JSON, don't clobber other keys):
484
+ rows=${#repo_plain[@]}
485
+ (( ${#sess_plain[@]} > rows )) && rows=${#sess_plain[@]}
486
+ if (( rows == 0 )); then printf ''; exit 0; fi
487
+ for (( i = ${#repo_plain[@]}; i < rows; i++ )); do repo_plain+=(""); repo_color+=(""); done
488
+ for (( i = ${#sess_plain[@]}; i < rows; i++ )); do sess_plain+=(""); sess_color+=(""); done
489
+
490
+ # Width comes from the plain rows; the parallel colored rows carry ANSI escapes
491
+ # that would otherwise be counted as visible characters. Arrays are passed
492
+ # positionally because bash 3.2 (macOS) has no `local -n` nameref.
493
+ box_width() {
494
+ local title="$1"; shift
495
+ local w=$(( ${#title} + 2 )) s
496
+ for s in "$@"; do (( ${#s} > w )) && w=${#s}; done
497
+ printf '%d' "$w"
498
+ }
499
+ rw=$(box_width "repo" "${repo_plain[@]}")
500
+ sw=$(box_width "session" "${sess_plain[@]}")
257
501
 
258
- ```json
259
- {
260
- "statusLine": {
261
- "type": "command",
262
- "command": "bash ~/.claude/statusline-command.sh"
263
- }
502
+ box_top() {
503
+ local title="$1" w="$2"
504
+ printf '%s╭─ %s%s%s %s╮%s' "$C_BOX" "$C_TITLE" "$title" "$C_BOX" \
505
+ "$(repeat_char '─' $(( w - ${#title} - 1 )))" "$RESET"
506
+ }
507
+ box_bottom() {
508
+ printf '%s╰%s╯%s' "$C_BOX" "$(repeat_char '─' $(( $1 + 2 )))" "$RESET"
264
509
  }
510
+ box_row() {
511
+ local colored="$1" w="$2" plain="$3"
512
+ printf '%s│%s %s%s %s│%s' "$C_BOX" "$RESET" "$colored" \
513
+ "$(repeat_char ' ' $(( w - ${#plain} )))" "$C_BOX" "$RESET"
514
+ }
515
+
516
+ out="$(box_top repo "$rw") $(box_top session "$sw")"
517
+ for (( i = 0; i < rows; i++ )); do
518
+ out+=$'\n'
519
+ out+="$(box_row "${repo_color[$i]}" "$rw" "${repo_plain[$i]}")"
520
+ out+=" $(box_row "${sess_color[$i]}" "$sw" "${sess_plain[$i]}")"
521
+ done
522
+ out+=$'\n'"$(box_bottom "$rw") $(box_bottom "$sw")"
523
+
524
+ printf '%s' "$out"
265
525
  ```
266
526
 
267
527
  ## Windows Implementation
268
528
 
269
- Write `%USERPROFILE%\.claude\statusline-command.ps1`. Use `ConvertFrom-Json` (no Python dep). Read the JSONL transcript with `Get-Content -ReadCount 0` and `ConvertFrom-Json` per line inside try/catch. Use `[char]27` for ANSI escapes (works on PS 5.1 and 7+). Read stdin with `[Console]::In.ReadToEnd()`.
529
+ Write `statusline-command.ps1` to the targeted platform directory. Use `ConvertFrom-Json` (no Python dep). Read the JSONL transcript with `Get-Content -ReadCount 0` and `ConvertFrom-Json` per line inside try/catch. Use `[char]27` for ANSI escapes, and set UTF-8 output encoding so the box-drawing glyphs survive.
270
530
 
271
- `%USERPROFILE%\.claude\statusline-command.ps1`:
531
+ `statusline-command.ps1`:
272
532
 
273
533
  ```powershell
274
534
  $ErrorActionPreference = 'SilentlyContinue'
@@ -278,10 +538,12 @@ $ESC = [char]27
278
538
 
279
539
  $C_CWD = "$ESC[38;2;94;255;255m"
280
540
  $C_BRANCH = "$ESC[38;2;194;72;112m"
281
- $C_PIPE = "$ESC[0;37m"
541
+ $C_BOX = "$ESC[38;2;90;95;105m"
542
+ $C_TITLE = "$ESC[38;2;140;146;158m"
282
543
  $C_MODEL = "$ESC[38;2;232;148;64m"
283
544
  $C_CTX = "$ESC[38;2;0;154;251m"
284
545
  $C_TOK = "$ESC[38;2;147;123;218m"
546
+ $C_DIM = "$ESC[38;2;120;126;138m"
285
547
  $RESET = "$ESC[0m"
286
548
 
287
549
  $GREEN = "$ESC[38;2;78;186;101m"
@@ -289,6 +551,10 @@ $AMBER = "$ESC[38;2;222;142;62m"
289
551
  $ORANGE = "$ESC[38;2;234;88;12m"
290
552
  $REDORG = "$ESC[38;2;183;68;38m"
291
553
 
554
+ $TL = [char]0x256D; $TR = [char]0x256E; $BL = [char]0x2570; $BR = [char]0x256F
555
+ $HZ = [char]0x2500; $VT = [char]0x2502
556
+ $UP = [char]0x2191; $DN = [char]0x2193; $ELLIPSIS = [char]0x2026
557
+
292
558
  function QuotaColor([double]$used) {
293
559
  $remaining = 100 - $used
294
560
  if ($remaining -gt 60) { return $GREEN }
@@ -298,8 +564,14 @@ function QuotaColor([double]$used) {
298
564
  }
299
565
 
300
566
  function FmtEta($resetsAt) {
301
- if ($null -eq $resetsAt) { return "" }
302
- $delta = [int64]$resetsAt - [int64](Get-Date -UFormat %s)
567
+ if ($null -eq $resetsAt -or $resetsAt -eq "") { return "" }
568
+ $val = [int64]$resetsAt
569
+ # Claude Code sends an absolute epoch; Antigravity sends a duration.
570
+ if ($val -lt 10000000) {
571
+ $delta = $val
572
+ } else {
573
+ $delta = $val - [int64](Get-Date -UFormat %s)
574
+ }
303
575
  if ($delta -le 0) { return "now" }
304
576
  $days = [math]::Floor($delta / 86400); $delta = $delta % 86400
305
577
  $hours = [math]::Floor($delta / 3600); $delta = $delta % 3600
@@ -315,25 +587,28 @@ function FmtTokens([int64]$n) {
315
587
  return ("{0:N1}M" -f ($n / 1000000.0))
316
588
  }
317
589
 
590
+ function TruncateTo($s, [int]$max) {
591
+ if ($s.Length -gt $max) { return $s.Substring(0, $max - 1) + $ELLIPSIS }
592
+ return $s
593
+ }
594
+
318
595
  function ShortenHome($p) {
319
596
  if (-not $p) { return "" }
320
- $home = $env:USERPROFILE
321
- if ($p -eq $home) { return "~" }
322
- if ($p.StartsWith("$home\")) { return "~" + $p.Substring($home.Length) }
597
+ $userHome = $env:USERPROFILE
598
+ if ($p -eq $userHome) { return "~" }
599
+ if ($p.StartsWith("$userHome\")) { return "~" + $p.Substring($userHome.Length) }
323
600
  return $p
324
601
  }
325
602
 
326
- function GetBranch($cwd) {
327
- if (-not $cwd) { return $null }
328
- try {
329
- $b = & git -C "$cwd" rev-parse --abbrev-ref HEAD 2>$null
330
- if ($LASTEXITCODE -eq 0 -and $b) { return $b.Trim() }
331
- } catch {}
332
- return $null
333
- }
334
-
335
603
  function SumTokens($path) {
336
604
  if (-not $path) { return $null }
605
+ # Resolve path mismatch (if it has .gemini/antigravity/ but it actually is at .gemini/antigravity-cli/)
606
+ if (-not (Test-Path -LiteralPath $path) -and $path.Contains("/.gemini/antigravity/")) {
607
+ $corrected = $path.Replace(".gemini/antigravity/", ".gemini/antigravity-cli/").Replace(".gemini\antigravity\", ".gemini\antigravity-cli\")
608
+ if (Test-Path -LiteralPath $corrected) {
609
+ $path = $corrected
610
+ }
611
+ }
337
612
  if (-not (Test-Path -LiteralPath $path)) { return $null }
338
613
  $sumIn = 0; $sumOut = 0; $found = $false
339
614
  $lines = Get-Content -LiteralPath $path -ReadCount 0 -ErrorAction SilentlyContinue
@@ -358,63 +633,234 @@ function SumTokens($path) {
358
633
  $raw = [Console]::In.ReadToEnd()
359
634
  try { $data = if ($raw.Trim()) { $raw | ConvertFrom-Json } else { @{} } } catch { $data = @{} }
360
635
 
361
- $segments = @()
636
+ $repoPlain = @(); $repoColor = @()
637
+ $sessPlain = @(); $sessColor = @()
362
638
 
363
639
  $cwd = $data.cwd
364
- if ($cwd) { $segments += "$C_CWD$(ShortenHome $cwd)$RESET" }
640
+ if ($cwd) {
641
+ $p = ShortenHome $cwd
642
+ $repoPlain += $p; $repoColor += "$C_CWD$p$RESET"
643
+ }
365
644
 
366
- $branch = GetBranch $cwd
367
- if ($branch) { $segments += "$C_BRANCH($branch)$RESET" }
645
+ # One `status -b --porcelain` call yields branch, dirty counts and ahead/behind.
646
+ # Must use git -C "$cwd", never PWD.
647
+ $branch = ""; $staged = 0; $modified = 0; $deleted = 0; $untracked = 0; $ahead = 0; $behind = 0
648
+ if ($cwd) {
649
+ $statusLines = & git -C "$cwd" status -b --porcelain 2>$null
650
+ if ($LASTEXITCODE -eq 0 -and $statusLines) {
651
+ foreach ($line in @($statusLines)) {
652
+ if ($line.StartsWith("## ")) {
653
+ $head = $line.Substring(3)
654
+ if ($head -eq "HEAD (no branch)") {
655
+ $sha = & git -C "$cwd" rev-parse --short HEAD 2>$null
656
+ if ($sha) { $branch = "detached@" + $sha.Trim() }
657
+ } else {
658
+ $branch = ($head -split '\.\.\.')[0]
659
+ if ($head -match '\[ahead (\d+)') { $ahead = [int]$Matches[1] }
660
+ if ($head -match 'behind (\d+)') { $behind = [int]$Matches[1] }
661
+ }
662
+ } elseif ($line.StartsWith("??")) {
663
+ $untracked++
664
+ } else {
665
+ $idx = $line.Substring(0, 1)
666
+ if ($idx -eq "D") { $deleted++ } elseif ($idx -ne " ") { $staged++ }
667
+ $wt = $line.Substring(1, 1)
668
+ if ($wt -eq "D") { $deleted++ } elseif ($wt -ne " ") { $modified++ }
669
+ }
670
+ }
671
+ }
672
+ }
673
+
674
+ if ($branch) {
675
+ $repoPlain += $branch; $repoColor += "$C_BRANCH$branch$RESET"
676
+
677
+ if ($staged -gt 0 -or $modified -gt 0 -or $deleted -gt 0 -or $untracked -gt 0) {
678
+ $parts = @()
679
+ if ($staged -gt 0) { $parts += "+$staged" }
680
+ if ($modified -gt 0) { $parts += "~$modified" }
681
+ if ($deleted -gt 0) { $parts += "-$deleted" }
682
+ if ($untracked -gt 0) { $parts += "?$untracked" }
683
+ $dirty = [string]::Join(" ", $parts)
684
+ $dirtyColored = "$GREEN$dirty$RESET"
685
+ } else {
686
+ $dirty = "clean"; $dirtyColored = "$C_DIM" + "clean$RESET"
687
+ }
688
+ $sync = ""
689
+ if ($ahead -gt 0) { $sync += " $UP$ahead" }
690
+ if ($behind -gt 0) { $sync += " $DN$behind" }
691
+ $repoPlain += "$dirty$sync"
692
+ $repoColor += "$dirtyColored$C_DIM$sync$RESET"
693
+
694
+ $last = & git -C "$cwd" log -1 --format='%h %s' 2>$null
695
+ if ($last) {
696
+ $last = TruncateTo $last.Trim() 80
697
+ $repoPlain += $last; $repoColor += "$C_DIM$last$RESET"
698
+ }
699
+ }
368
700
 
369
701
  $model = $data.model.display_name
370
702
  $effort = $data.effort.level
371
703
  if ($model) {
372
- if ($effort) { $segments += "$C_MODEL$model ($effort)$RESET" }
373
- else { $segments += "$C_MODEL$model$RESET" }
704
+ if ($effort) { $m = "$model ($effort)" } else { $m = "$model" }
705
+ $sessPlain += $m; $sessColor += "$C_MODEL$m$RESET"
706
+ }
707
+
708
+ $tokenSums = SumTokens $data.transcript_path
709
+ $ti = 0
710
+ $to = 0
711
+ if ($null -ne $tokenSums) {
712
+ $ti = $tokenSums[0]
713
+ $to = $tokenSums[1]
714
+ }
715
+
716
+ # Fallback to payload context_window tokens if transcript sum is 0
717
+ if ($ti -eq 0 -and $to -eq 0) {
718
+ if ($null -ne $data.context_window.total_input_tokens -and $null -ne $data.context_window.total_output_tokens) {
719
+ $ti = [int64]$data.context_window.total_input_tokens
720
+ $to = [int64]$data.context_window.total_output_tokens
721
+ }
722
+ }
723
+
724
+ $tokens = ""
725
+ if ($ti -gt 0 -or $to -gt 0) {
726
+ $tokens = "$UP" + (FmtTokens $ti) + " $DN" + (FmtTokens $to)
374
727
  }
375
728
 
376
729
  $ctx = $data.context_window.used_percentage
377
- if ($null -ne $ctx) { $segments += "$C_CTX" + "ctx " + [int][math]::Round([double]$ctx) + "%$RESET" }
730
+ if ($null -ne $ctx) {
731
+ $c = "ctx " + [int][math]::Round([double]$ctx) + "%"
732
+ $cColored = "$C_CTX$c$RESET"
733
+ if ($tokens) {
734
+ $c += " $tokens"
735
+ $cColored += " $C_TOK$tokens$RESET"
736
+ }
737
+ $sessPlain += $c; $sessColor += $cColored
738
+ } elseif ($tokens) {
739
+ $sessPlain += $tokens; $sessColor += "$C_TOK$tokens$RESET"
740
+ }
741
+
742
+ $five_used = $null
743
+ $five_reset = $null
744
+ $seven_used = $null
745
+ $seven_reset = $null
746
+
747
+ if ($null -ne $data.rate_limits.five_hour.used_percentage) {
748
+ $five_used = $data.rate_limits.five_hour.used_percentage
749
+ $five_reset = $data.rate_limits.five_hour.resets_at
750
+ } elseif ($null -ne $data.quota) {
751
+ $q5 = if ($null -ne $data.quota."gemini-5h") { $data.quota."gemini-5h" } else { $data.quota."3p-5h" }
752
+ if ($null -ne $q5) {
753
+ $five_used = (1.0 - $q5.remaining_fraction) * 100
754
+ $five_reset = $q5.reset_in_seconds
755
+ }
756
+ }
757
+
758
+ if ($null -ne $data.rate_limits.seven_day.used_percentage) {
759
+ $seven_used = $data.rate_limits.seven_day.used_percentage
760
+ $seven_reset = $data.rate_limits.seven_day.resets_at
761
+ } elseif ($null -ne $data.quota) {
762
+ $qw = if ($null -ne $data.quota."gemini-weekly") { $data.quota."gemini-weekly" } else { $data.quota."3p-weekly" }
763
+ if ($null -ne $qw) {
764
+ $seven_used = (1.0 - $qw.remaining_fraction) * 100
765
+ $seven_reset = $qw.reset_in_seconds
766
+ }
767
+ }
768
+
769
+ if ($null -ne $five_used) {
770
+ $s = "5h " + [int][math]::Round($five_used) + "%"
771
+ $eta = FmtEta $five_reset
772
+ if ($eta) { $s += " resets $eta" }
773
+ $sessPlain += $s; $sessColor += "$(QuotaColor $five_used)$s$RESET"
774
+ }
775
+ if ($null -ne $seven_used) {
776
+ $s = "7d " + [int][math]::Round($seven_used) + "%"
777
+ $eta = FmtEta $seven_reset
778
+ if ($eta) { $s += " resets $eta" }
779
+ $sessPlain += $s; $sessColor += "$(QuotaColor $seven_used)$s$RESET"
780
+ }
378
781
 
379
- $tokens = SumTokens $data.transcript_path
380
- if ($null -ne $tokens) {
381
- $segments += "$C_TOK↑$(FmtTokens $tokens[0]) ↓$(FmtTokens $tokens[1])$RESET"
782
+ $rows = [math]::Max($repoPlain.Count, $sessPlain.Count)
783
+ if ($rows -eq 0) { exit 0 }
784
+ while ($repoPlain.Count -lt $rows) { $repoPlain += ""; $repoColor += "" }
785
+ while ($sessPlain.Count -lt $rows) { $sessPlain += ""; $sessColor += "" }
786
+
787
+ # Width comes from the plain rows; the parallel colored rows carry ANSI escapes
788
+ # that would otherwise be counted as visible characters.
789
+ function BoxWidth($title, $plainRows) {
790
+ $w = $title.Length + 2
791
+ foreach ($s in $plainRows) { if ($s.Length -gt $w) { $w = $s.Length } }
792
+ return $w
382
793
  }
794
+ $rw = BoxWidth "repo" $repoPlain
795
+ $sw = BoxWidth "session" $sessPlain
383
796
 
384
- $five = $data.rate_limits.five_hour
385
- $seven = $data.rate_limits.seven_day
386
- if ($null -ne $five.used_percentage) {
387
- $u = [double]$five.used_percentage
388
- $segments += "$(QuotaColor $u)5h:$([int][math]::Round($u))%($(FmtEta $five.resets_at))$RESET"
797
+ function BoxTop($title, [int]$w) {
798
+ $dashes = [string]$HZ * ($w - $title.Length - 1)
799
+ return "$C_BOX$TL$HZ $C_TITLE$title$C_BOX $dashes$TR$RESET"
800
+ }
801
+ function BoxBottom([int]$w) {
802
+ return "$C_BOX$BL" + ([string]$HZ * ($w + 2)) + "$BR$RESET"
389
803
  }
390
- if ($null -ne $seven.used_percentage) {
391
- $u = [double]$seven.used_percentage
392
- $segments += "$(QuotaColor $u)7d:$([int][math]::Round($u))%($(FmtEta $seven.resets_at))$RESET"
804
+ function BoxRow($colored, [int]$w, $plain) {
805
+ $pad = " " * ($w - $plain.Length)
806
+ return "$C_BOX$VT$RESET $colored$pad $C_BOX$VT$RESET"
393
807
  }
394
808
 
395
- $sep = " $C_PIPE|$RESET "
396
- [Console]::Out.Write([string]::Join($sep, $segments))
809
+ $lines = @()
810
+ $lines += (BoxTop "repo" $rw) + " " + (BoxTop "session" $sw)
811
+ for ($i = 0; $i -lt $rows; $i++) {
812
+ $lines += (BoxRow $repoColor[$i] $rw $repoPlain[$i]) + " " + (BoxRow $sessColor[$i] $sw $sessPlain[$i])
813
+ }
814
+ $lines += (BoxBottom $rw) + " " + (BoxBottom $sw)
815
+
816
+ [Console]::Out.Write([string]::Join("`n", $lines))
817
+ ```
818
+
819
+ ## Settings Wiring
820
+
821
+ ### Claude Code
822
+
823
+ Wire into `~/.claude/settings.json` (or `%USERPROFILE%\.claude\settings.json` on Windows):
824
+
825
+ ```json
826
+ {
827
+ "statusLine": {
828
+ "type": "command",
829
+ "command": "bash ~/.claude/statusline-command.sh"
830
+ }
831
+ }
397
832
  ```
398
833
 
399
- Wire into `%USERPROFILE%\.claude\settings.json`:
834
+ (Or the `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.claude\statusline-command.ps1"` variant on Windows).
835
+
836
+ ### Antigravity CLI
837
+
838
+ Wire into `~/.gemini/antigravity-cli/settings.json` (or `%USERPROFILE%\.gemini\antigravity-cli\settings.json` on Windows):
400
839
 
401
840
  ```json
402
841
  {
403
842
  "statusLine": {
404
843
  "type": "command",
405
- "command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"%USERPROFILE%\\.claude\\statusline-command.ps1\""
844
+ "command": "bash ~/.gemini/antigravity-cli/statusline-command.sh",
845
+ "enabled": true
406
846
  }
407
847
  }
408
848
  ```
409
849
 
850
+ (Or the `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.gemini\antigravity-cli\statusline-command.ps1"` variant on Windows).
851
+
410
852
  ## Smoke Test
411
853
 
412
- After installing, generate a fake JSON payload with 5h at 80% used, 7d at 50% used, both with future `resets_at`, plus a `transcript_path` pointing at a small fixture JSONL containing two assistant messages with `usage` blocks. Pipe it into the script and confirm:
854
+ After installing, pipe a fake payload into the script and confirm:
413
855
 
414
- - 5h renders **orange**, 7d renders **amber**.
415
- - cwd, model, and context % are present.
416
- - Tokens segment renders `↑<sum> ↓<sum>` matching the fixture, in `#937bda`, as its own pipe-separated segment immediately after `ctx`.
417
- - Omitting `transcript_path` hides the tokens segment cleanly (no stray separator).
856
+ - **Every rendered line has the same character count** — this is the alignment check, and it fails first when width is measured off a colored string or the locale is not UTF-8.
857
+ - Both boxes show the same number of rows, framed top and bottom on the same lines.
858
+ - Inside a repo the repo box shows 4 rows: full cwd, branch, working-tree state, last commit.
859
+ - Staging a deletion (`git rm`) renders `-N`, not `+N`.
860
+ - 5h renders **orange** at 80% used, 7d renders **amber** at 50% used.
861
+ - The tokens sum matches the fixture and sits on the `ctx` row.
862
+ - Omitting `transcript_path` leaves the `ctx` row as a bare `ctx N%` — no `↑0 ↓0`, no stray spacing.
863
+ - Outside a repo the repo box collapses to the cwd row alone and the session box pads with blank rows.
418
864
 
419
865
  ### Linux/macOS smoke test
420
866
 
@@ -422,13 +868,31 @@ After installing, generate a fake JSON payload with 5h at 80% used, 7d at 50% us
422
868
  mkdir -p /tmp/statusline-smoke
423
869
  cat >/tmp/statusline-smoke/transcript.jsonl <<'EOF'
424
870
  {"type":"assistant","message":{"role":"assistant","usage":{"input_tokens":100,"cache_creation_input_tokens":200,"cache_read_input_tokens":300,"output_tokens":50}}}
871
+ not json at all - must be skipped, not fatal
425
872
  {"type":"assistant","message":{"role":"assistant","usage":{"input_tokens":10,"cache_creation_input_tokens":20,"cache_read_input_tokens":30,"output_tokens":5}}}
426
873
  EOF
427
874
  FUTURE=$(($(date +%s) + 7200))
428
875
  FUTURE7=$(($(date +%s) + 3*86400))
429
- printf '{"cwd":"%s","transcript_path":"/tmp/statusline-smoke/transcript.jsonl","model":{"display_name":"Opus 4.7"},"effort":{"level":"medium"},"context_window":{"used_percentage":42},"rate_limits":{"five_hour":{"used_percentage":80,"resets_at":%d},"seven_day":{"used_percentage":50,"resets_at":%d}}}' "$HOME" "$FUTURE" "$FUTURE7" | bash ~/.claude/statusline-command.sh; echo
876
+
877
+ # Claude Code payload
878
+ printf '{"cwd":"%s","transcript_path":"/tmp/statusline-smoke/transcript.jsonl","model":{"display_name":"Opus 4.7"},"effort":{"level":"medium"},"context_window":{"used_percentage":42},"rate_limits":{"five_hour":{"used_percentage":80,"resets_at":%d},"seven_day":{"used_percentage":50,"resets_at":%d}}}' "$PWD" "$FUTURE" "$FUTURE7" | bash ~/.claude/statusline-command.sh; echo
879
+
880
+ # Antigravity payload - exercises the quota/reset_in_seconds and token fallbacks
881
+ printf '{"cwd":"%s","model":{"display_name":"Gemini 3.5 Flash"},"context_window":{"used_percentage":6.7,"total_input_tokens":70419,"total_output_tokens":16667},"quota":{"gemini-5h":{"remaining_fraction":0.87,"reset_in_seconds":2515},"gemini-weekly":{"remaining_fraction":0.97,"reset_in_seconds":589315}}}' "$PWD" | bash ~/.claude/statusline-command.sh; echo
430
882
  ```
431
883
 
884
+ Assert equal line widths mechanically:
885
+
886
+ ```bash
887
+ printf '{"cwd":"%s","transcript_path":"/tmp/statusline-smoke/transcript.jsonl","model":{"display_name":"Opus 4.7"},"context_window":{"used_percentage":42}}' "$PWD" \
888
+ | bash ~/.claude/statusline-command.sh \
889
+ | perl -CSD -pe 's/\e\[[0-9;]*m//g' \
890
+ | perl -CSD -ne 'chomp; print length($_), "\n"' \
891
+ | sort -u
892
+ ```
893
+
894
+ That must print exactly one number. Re-run the same pipeline wrapped in `env -i PATH="$PATH" HOME="$HOME" bash -c '...'` to confirm the locale guard holds with no `LANG` set.
895
+
432
896
  ### Windows smoke test (PowerShell)
433
897
 
434
898
  ```powershell
@@ -441,7 +905,7 @@ New-Item -ItemType Directory -Force -Path $tmp | Out-Null
441
905
  $future = [int64](Get-Date -UFormat %s) + 7200
442
906
  $future7 = [int64](Get-Date -UFormat %s) + 3*86400
443
907
  $payload = @{
444
- cwd = $env:USERPROFILE
908
+ cwd = $PWD.Path
445
909
  transcript_path = "$tmp\transcript.jsonl"
446
910
  model = @{ display_name = "Opus 4.7" }
447
911
  effort = @{ level = "medium" }
@@ -451,23 +915,27 @@ $payload = @{
451
915
  seven_day = @{ used_percentage = 50; resets_at = $future7 }
452
916
  }
453
917
  } | ConvertTo-Json -Depth 5 -Compress
454
- $payload | powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$env:USERPROFILE\.claude\statusline-command.ps1"
918
+ $out = $payload | powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$env:USERPROFILE\.claude\statusline-command.ps1"
919
+ $out
920
+ ($out -split "`n" | ForEach-Object { ($_ -replace "$([char]27)\[[0-9;]*m", "").Length } | Sort-Object -Unique)
455
921
  ```
456
922
 
923
+ The last line must print exactly one number.
924
+
457
925
  ## Color Preview One-Liner
458
926
 
459
- After install, output a one-liner the user can paste to preview all four quota tiers, with both 5h and 7d on each line, separated by ` | `, one tier per line.
927
+ After install, output a one-liner the user can paste to preview all four quota tiers, one tier per line, in the row format the session box uses.
460
928
 
461
929
  ### Bash form
462
930
 
463
931
  ```bash
464
- printf '\033[38;2;78;186;101m5h:20%%(2h30m)\033[0m \033[0;37m|\033[0m \033[38;2;78;186;101m7d:25%%(3d12h)\033[0m\n\033[38;2;222;142;62m5h:55%%(2h30m)\033[0m \033[0;37m|\033[0m \033[38;2;222;142;62m7d:45%%(3d12h)\033[0m\n\033[38;2;234;88;12m5h:80%%(1h15m)\033[0m \033[0;37m|\033[0m \033[38;2;234;88;12m7d:75%%(2d3h)\033[0m\n\033[38;2;183;68;38m5h:95%%(0h30m)\033[0m \033[0;37m|\033[0m \033[38;2;183;68;38m7d:92%%(1d2h)\033[0m\n'
932
+ printf '\033[38;2;78;186;101m5h 20%% resets 2h 30m\033[0m\n\033[38;2;222;142;62m5h 55%% resets 2h 30m\033[0m\n\033[38;2;234;88;12m5h 80%% resets 1h 15m\033[0m\n\033[38;2;183;68;38m5h 95%% resets 30m\033[0m\n'
465
933
  ```
466
934
 
467
935
  ### PowerShell form
468
936
 
469
937
  ```powershell
470
- $e=[char]27; Write-Host "$e[38;2;78;186;101m5h:20%(2h30m)$e[0m $e[0;37m|$e[0m $e[38;2;78;186;101m7d:25%(3d12h)$e[0m`n$e[38;2;222;142;62m5h:55%(2h30m)$e[0m $e[0;37m|$e[0m $e[38;2;222;142;62m7d:45%(3d12h)$e[0m`n$e[38;2;234;88;12m5h:80%(1h15m)$e[0m $e[0;37m|$e[0m $e[38;2;234;88;12m7d:75%(2d3h)$e[0m`n$e[38;2;183;68;38m5h:95%(0h30m)$e[0m $e[0;37m|$e[0m $e[38;2;183;68;38m7d:92%(1d2h)$e[0m"
938
+ $e=[char]27; Write-Host "$e[38;2;78;186;101m5h 20% resets 2h 30m$e[0m`n$e[38;2;222;142;62m5h 55% resets 2h 30m$e[0m`n$e[38;2;234;88;12m5h 80% resets 1h 15m$e[0m`n$e[38;2;183;68;38m5h 95% resets 30m$e[0m"
471
939
  ```
472
940
 
473
941
  ## Other Rules
@@ -475,5 +943,6 @@ $e=[char]27; Write-Host "$e[38;2;78;186;101m5h:20%(2h30m)$e[0m $e[0;37m|$e[0m $e
475
943
  - Do not add features beyond this spec.
476
944
  - Do not auto-commit any changes.
477
945
  - Do not add Co-Authored-By trailers.
478
- - Read git branch with `git -C "$cwd" rev-parse --abbrev-ref HEAD`, never from `PWD`.
946
+ - Read git state with `git -C "$cwd" status -b --porcelain`, never from `PWD`.
947
+ - Never measure padding from a string containing ANSI escapes.
479
948
  - When merging into existing `settings.json`, preserve all other keys; only set/replace `statusLine`.