@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.
- package/README.md +17 -8
- package/dist/bin/cli.js +576 -792
- package/dist/skills/git-commit/SKILL.md +101 -37
- package/dist/skills/handoff/SKILL.md +16 -0
- package/dist/skills/init-agents-md/SKILL.md +51 -0
- package/dist/skills/init-agents-md/references/agents_template.md +42 -0
- package/dist/skills/init-agents-md/references/patterns_template.md +19 -0
- package/dist/skills/statusline-setup/SKILL.md +603 -134
- package/dist/skills/universalize-agents/SKILL.md +245 -0
- package/dist/skills/universalize-agents/reference/hook-templates/README.md +30 -0
- package/dist/skills/universalize-agents/reference/hook-templates/claude-code.json +15 -0
- package/dist/skills/universalize-agents/reference/hook-templates/codex.toml +10 -0
- package/dist/skills/universalize-agents/reference/hook-templates/copilot.json +9 -0
- package/dist/skills/universalize-agents/reference/hook-templates/gemini.json +11 -0
- package/dist/skills/universalize-agents/reference/mapping.md +100 -0
- package/dist/skills/universalize-agents/scripts/agent-setup.ps1 +83 -0
- package/dist/skills/universalize-agents/scripts/agent-setup.sh +98 -0
- package/dist/skills/update-agents-md/SKILL.md +43 -0
- package/package.json +2 -5
- package/dist/skills/serena-file-processing/SKILL.md +0 -47
|
@@ -1,50 +1,125 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: statusline-setup
|
|
3
|
-
description: 'Set up the Claude Code statusline
|
|
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
|
|
8
|
+
Install a Claude Code or Gemini/Antigravity statusline that renders two side-by-side bordered boxes:
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
104
|
+
Tokens are calculated dynamically:
|
|
43
105
|
|
|
44
|
-
|
|
45
|
-
- `
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
-
-
|
|
188
|
+
- Use `fromjson?` to process JSONL files line-by-line resiliently to ignore malformed/plain-text entries.
|
|
91
189
|
|
|
92
|
-
|
|
190
|
+
`statusline-command.sh`:
|
|
93
191
|
|
|
94
192
|
```bash
|
|
95
193
|
#!/usr/bin/env bash
|
|
96
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
159
|
-
elif [[ "$p" == "$HOME"/* ]]; then
|
|
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
|
-
(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
341
|
+
repo_plain=(); repo_color=()
|
|
187
342
|
|
|
188
343
|
if [[ -n "$cwd" ]]; then
|
|
189
|
-
|
|
344
|
+
p=$(shorten_home "$cwd")
|
|
345
|
+
repo_plain+=("$p"); repo_color+=("${C_CWD}${p}${RESET}")
|
|
190
346
|
fi
|
|
191
347
|
|
|
192
|
-
#
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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 -
|
|
217
|
-
reduce
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
$
|
|
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
|
-
$
|
|
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
|
-
$
|
|
321
|
-
if ($p -eq $
|
|
322
|
-
if ($p.StartsWith("$
|
|
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
|
-
$
|
|
636
|
+
$repoPlain = @(); $repoColor = @()
|
|
637
|
+
$sessPlain = @(); $sessColor = @()
|
|
362
638
|
|
|
363
639
|
$cwd = $data.cwd
|
|
364
|
-
if ($cwd) {
|
|
640
|
+
if ($cwd) {
|
|
641
|
+
$p = ShortenHome $cwd
|
|
642
|
+
$repoPlain += $p; $repoColor += "$C_CWD$p$RESET"
|
|
643
|
+
}
|
|
365
644
|
|
|
366
|
-
|
|
367
|
-
|
|
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) { $
|
|
373
|
-
|
|
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) {
|
|
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
|
-
$
|
|
380
|
-
if ($
|
|
381
|
-
|
|
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
|
-
$
|
|
385
|
-
$
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
391
|
-
$
|
|
392
|
-
|
|
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
|
-
$
|
|
396
|
-
|
|
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
|
-
|
|
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": "
|
|
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,
|
|
854
|
+
After installing, pipe a fake payload into the script and confirm:
|
|
413
855
|
|
|
414
|
-
-
|
|
415
|
-
-
|
|
416
|
-
-
|
|
417
|
-
-
|
|
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
|
-
|
|
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 = $
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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`.
|