@kurokeita/add-skill 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +1 -1
- package/dist/skills/git-commit/SKILL.md +5 -1
- package/dist/skills/git-commit/hooks/commit-gate.py +2 -0
- package/dist/skills/statusline-setup/.claude-plugin/plugin.json +7 -0
- package/dist/skills/statusline-setup/SKILL.md +451 -15
- package/dist/skills/statusline-setup/mod/hooks/discover.ts +142 -0
- package/dist/skills/statusline-setup/mod/hooks/fetch.ts +327 -0
- package/dist/skills/statusline-setup/mod/hooks/hooks.json +1 -0
- package/dist/skills/statusline-setup/mod/hooks/register.tsx +411 -0
- package/dist/skills/statusline-setup/mod/tests/work.test.ts +306 -0
- package/dist/skills/statusline-setup/mod/types/index.d.ts +35 -0
- package/package.json +1 -1
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: statusline-setup
|
|
3
|
-
description: 'Set up the Claude Code or Gemini/Antigravity CLI statusline as
|
|
3
|
+
description: 'Set up the Claude Code or Gemini/Antigravity CLI statusline as side-by-side bordered boxes: a repo box (cwd, git branch, working-tree state, last commit), a session box (model, context % + token totals, 5h/7d quota with ETA), and a work box (the current branch's pull request and its linked issues or work items, fetched from GitHub with gh or Azure DevOps with az). Use when asked to "set up my statusline", "install statusline", "configure statusline", or to add a colored multi-line status line showing rate limits, token usage, or the PR and work items for the current branch. 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 or Gemini/Antigravity statusline that renders
|
|
8
|
+
Install a Claude Code or Gemini/Antigravity statusline that renders up to three side-by-side bordered boxes:
|
|
9
9
|
|
|
10
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
|
-
╰─────────────────────────────────────────────────────────────╯ ╰───────────────────────╯
|
|
11
|
+
╭─ repo ──────────────────────────────────────────────────────╮ ╭─ session ─────────────╮ ╭─ work ───────────────────────╮
|
|
12
|
+
│ ~/dev/ai_agents │ │ Opus 5 (high) │ │ PR #46 open approved │
|
|
13
|
+
│ feat/statusline-boxed-layout │ │ ctx 42% ↑1.2M ↓18.4k │ │ #45 Show PRs in statusline │
|
|
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
17
|
```
|
|
18
18
|
|
|
19
|
+
The work box appears when the current branch has a pull request or related work items. Its rows are clickable links. In Claude Code, `/work` opens the same data in a scrollable pane (see Work Pane).
|
|
20
|
+
|
|
19
21
|
## Box Contents
|
|
20
22
|
|
|
21
23
|
### repo box
|
|
@@ -34,14 +36,23 @@ Rows 2-4 appear only inside a git repo.
|
|
|
34
36
|
3. **`5h N% resets <eta>`** — colored by remaining quota (see below).
|
|
35
37
|
4. **`7d N% resets <eta>`** — colored independently of the 5h row.
|
|
36
38
|
|
|
39
|
+
### work box
|
|
40
|
+
|
|
41
|
+
1. **pull request** in `#009AFB` (`38;2;0;154;251`): `PR #<n> <state>` on GitHub, `PR !<n> <state>` on Azure DevOps, followed by a review word when there is one. See [Work Box Data](#work-box-data) for the state and review words.
|
|
42
|
+
2. **related items**, one per row, in dim `#787e8a`: `#<number> <title>`, truncated to 50 characters with a trailing `…`. With a PR, GitHub lists the issues the PR closes and Azure DevOps lists the work items linked to the PR. Without a PR, the items come from the branch name and the branch's commits (see [Branch refs](#branch-refs)). At most 3 rows; any more collapse into one `+N more` row.
|
|
43
|
+
|
|
44
|
+
Every row with a URL is wrapped in an OSC 8 hyperlink (`ESC ]8;;<url> BEL` … `ESC ]8;; BEL`), so Cmd+click (macOS) or Ctrl+click opens the PR or item. The escape goes in the colored copy only, never the plain copy used for width. Terminal.app shows the text but cannot click it.
|
|
45
|
+
|
|
46
|
+
The box is left out entirely when it has no rows: detached HEAD, a remote that is neither GitHub nor Azure DevOps, `gh`/`az` not installed, no PR and no related items, or no data fetched yet.
|
|
47
|
+
|
|
37
48
|
## Box Rendering Rules
|
|
38
49
|
|
|
39
50
|
- Border glyphs `╭ ╮ ╰ ╯ ─ │` in `#5a5f69` (`38;2;90;95;105`); the box title in `#8c929e` (`38;2;140;146;158`).
|
|
40
51
|
- Each box's inner width is the longest of its content rows, or its title + 2, whichever is larger.
|
|
41
|
-
-
|
|
42
|
-
- The
|
|
52
|
+
- All rendered boxes are padded to the **same row count** with blank rows, so the frames start and end on the same terminal lines.
|
|
53
|
+
- The boxes are joined by a single space on every line.
|
|
43
54
|
- Rows are emitted newline-separated; the statusline host renders them as multiple lines.
|
|
44
|
-
- When
|
|
55
|
+
- When the repo and session boxes both have zero content rows the script prints nothing and exits 0.
|
|
45
56
|
|
|
46
57
|
### Measuring width with ANSI escapes present
|
|
47
58
|
|
|
@@ -123,6 +134,68 @@ The last-commit row needs one further call: `git -C "$cwd" log -1 --format='%h %
|
|
|
123
134
|
|
|
124
135
|
Every git call must skip optional locks: export `GIT_OPTIONAL_LOCKS=0` once at the top of the script. A plain `git status` takes `.git/index.lock` to refresh stat info, and the statusline re-runs on every turn, so it races the user's own `git add`/`commit` and makes them fail with `Unable to create '.git/index.lock': File exists`.
|
|
125
136
|
|
|
137
|
+
## Work Box Data
|
|
138
|
+
|
|
139
|
+
The statusline payload says nothing about pull requests, so the current branch stands in for the session.
|
|
140
|
+
|
|
141
|
+
### Platform detection
|
|
142
|
+
|
|
143
|
+
Read the remote with `git -C "$cwd" remote get-url origin`. This is the one git call outside the single status call.
|
|
144
|
+
|
|
145
|
+
| Remote matches | Platform | CLI |
|
|
146
|
+
|----------------|----------|-----|
|
|
147
|
+
| `github.com:` or `github.com/` | GitHub | `gh` |
|
|
148
|
+
| `dev.azure.com:`, `dev.azure.com/` or `.visualstudio.com/` | Azure DevOps | `az` with the `azure-devops` extension |
|
|
149
|
+
|
|
150
|
+
Anything else, or a missing CLI, renders no work box. Both CLIs read the repository from the current directory, so every fetch runs inside `cd "$cwd"` in the background job.
|
|
151
|
+
|
|
152
|
+
### Branch refs
|
|
153
|
+
|
|
154
|
+
Without a PR, the related items are the item numbers found locally, in this order, with duplicates and leading zeros removed and at most 10 kept:
|
|
155
|
+
|
|
156
|
+
1. A 3 to 9 digit segment of the branch name, splitting on anything that is not a letter or digit (`user/minhle/43553/feat/x` → `43553`). On GitHub, also the leading number of a `gh issue develop` name (`41-fix-version` → `41`).
|
|
157
|
+
2. Refs in the messages of commits on the branch but not on the default branch (`git log --format=%B <origin/HEAD>..HEAD`): `AB#<n>` on Azure DevOps, `#<n>` on GitHub. No `refs/remotes/origin/HEAD` means no commit scan.
|
|
158
|
+
|
|
159
|
+
Ids are capped at 9 digits because both APIs take 32-bit ids, and one out-of-range literal fails the whole query. A number that is not an item is dropped silently.
|
|
160
|
+
|
|
161
|
+
### Fetch
|
|
162
|
+
|
|
163
|
+
- **GitHub**: one `gh api graphql` call. `-F owner='{owner}' -F name='{repo}'` lets `gh` fill in the repository; pass the branch with `-f` so a numeric branch name stays a string. Query the newest PR whose head is the branch (`states: [OPEN, MERGED, CLOSED]`) with `number state isDraft reviewDecision url` and `closingIssuesReferences(first: 10) { nodes { number title url } }`. The same query carries one `i<n>: issueOrPullRequest(number: <n>)` alias per branch ref, used only when there is no PR. `gh` exits 1 when a ref does not exist but still prints the rest, so judge success by `.data.repository`, not the exit code.
|
|
164
|
+
- **Azure DevOps**: `az repos pr list --source-branch <branch> --status all --top 1`, then `az repos pr work-item list --id <n>` for the linked work items. Without a PR, one `az boards query --wiql "SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.Id] IN (<refs>)"` instead. Pass `--only-show-errors -o json` to every call. `--detect` (on by default) scopes them to the repository of the current directory.
|
|
165
|
+
|
|
166
|
+
### Links
|
|
167
|
+
|
|
168
|
+
| Row | GitHub | Azure DevOps |
|
|
169
|
+
|-----|--------|--------------|
|
|
170
|
+
| PR | `url` | API `url` with `/_apis/git/repositories/` → `/_git/` and `/pullRequests/` → `/pullrequest/` |
|
|
171
|
+
| item | `url` | API `url` with `/_apis/wit/workItems/` → `/_workitems/edit/` |
|
|
172
|
+
|
|
173
|
+
Only `https://` URLs without spaces become links; anything else renders as plain text.
|
|
174
|
+
|
|
175
|
+
### Row vocabulary
|
|
176
|
+
|
|
177
|
+
| Field | GitHub | Azure DevOps |
|
|
178
|
+
|-------|--------|--------------|
|
|
179
|
+
| state | `draft` when an open PR is a draft, else `open`, `merged`, `closed` | `draft` when an active PR is a draft, else `active` → `open`, `completed` → `merged`, `abandoned` → `closed` |
|
|
180
|
+
| review | `reviewDecision`: `APPROVED` → `approved`, `CHANGES_REQUESTED` → `changes requested`, `REVIEW_REQUIRED` → `review required`, null → nothing | reviewer votes: any `-10` → `rejected`, else any `-5` → `waiting for author`, else any `5` or `10` → `approved`, else nothing |
|
|
181
|
+
| item | `#<issue number> <issue title>` | `#<work item id> <System.Title>` |
|
|
182
|
+
|
|
183
|
+
Tabs and newlines in titles become spaces, so each row stays one line and the tab before the URL stays unique.
|
|
184
|
+
|
|
185
|
+
### Cache and background refresh
|
|
186
|
+
|
|
187
|
+
`gh` takes about 0.7s and the two `az` calls about 5s, and the statusline re-runs on every turn. The render path therefore **never calls `gh` or `az`**:
|
|
188
|
+
|
|
189
|
+
1. The cache lives in a per-user directory: `${TMPDIR:-/tmp}/statusline-work-$UID` (mode 700) or `%TEMP%\statusline-work`. The file name is a hash of `<remote URL>|<branch>`, so a token embedded in the remote URL never lands in a file name.
|
|
190
|
+
2. `<cache>.at` holds the epoch of the last fetch attempt. When it is missing or older than 60 seconds, the script writes the current epoch to it, starts a detached refresh, and carries on rendering.
|
|
191
|
+
3. The refresh writes the rows to a temporary file, then renames it over `<cache>`, so a half-written cache never renders. A failed fetch (offline, logged out) leaves the previous rows in place; the `.at` stamp stops a retry until the next 60-second window.
|
|
192
|
+
4. The render reads `<cache>`: one `<text><TAB><url>` row per line, the `PR` row first, then the item rows. It strips control characters from the text and the URL first, because titles come from other people and could otherwise inject terminal escape sequences.
|
|
193
|
+
|
|
194
|
+
Detaching rules, or the host waits for the fetch and the statusline stalls:
|
|
195
|
+
|
|
196
|
+
- **bash**: run the refresh as `( ... ) </dev/null >/dev/null 2>&1 &` followed by `disown`. No file descriptor may stay attached to the statusline's stdout.
|
|
197
|
+
- **PowerShell**: `Start-Job` dies with its parent, so re-invoke the same script with `Start-Process -WindowStyle Hidden` and a `-Refresh` argument.
|
|
198
|
+
|
|
126
199
|
## Quota Colors (by REMAINING quota = 100 - used%)
|
|
127
200
|
|
|
128
201
|
| Remaining | Color | 24-bit ANSI |
|
|
@@ -487,11 +560,155 @@ if [[ -n "$seven_used" ]]; then
|
|
|
487
560
|
sess_plain+=("$s"); sess_color+=("$(quota_color "$su")${s}${RESET}")
|
|
488
561
|
fi
|
|
489
562
|
|
|
563
|
+
# --- work box: PR + linked items for the current branch ---
|
|
564
|
+
# gh takes ~0.7s and az ~5s, so rendering only reads a cache; a stale cache
|
|
565
|
+
# starts a detached refresh and this turn shows the previous rows.
|
|
566
|
+
WORK_TTL=60
|
|
567
|
+
|
|
568
|
+
# Item numbers related to the branch without a PR: a 3-9 digit segment of the
|
|
569
|
+
# branch name (plus gh issue develop's "<n>-slug"), then AB#N (Azure DevOps) or
|
|
570
|
+
# #N (GitHub) in commits not yet on the default branch. Capped at 9 digits
|
|
571
|
+
# because both APIs take 32-bit ids, and one bad literal fails the whole query.
|
|
572
|
+
item_refs() {
|
|
573
|
+
local platform="$1" branch="$2" base pat
|
|
574
|
+
{
|
|
575
|
+
[[ "$platform" == github && "$branch" =~ ^([0-9]{1,9})- ]] && echo "${BASH_REMATCH[1]}"
|
|
576
|
+
printf '%s\n' "$branch" | tr -c 'A-Za-z0-9' '\n' | grep -Ex '[0-9]{3,9}'
|
|
577
|
+
base=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)
|
|
578
|
+
if [[ -n "$base" ]]; then
|
|
579
|
+
if [[ "$platform" == github ]]; then pat='#[0-9]{1,9}'; else pat='AB#[0-9]{1,9}'; fi
|
|
580
|
+
git log --format=%B "$base..HEAD" 2>/dev/null | grep -Eo "$pat" | tr -cd '0-9\n'
|
|
581
|
+
fi
|
|
582
|
+
} | awk 'NF { n = $0 + 0; if (!seen[n]++) print n }' | head -n 10
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
# Prints the PR row, then one row per linked item, each as "<text>\t<url>".
|
|
586
|
+
# Without a PR, the items come from item_refs instead. Fails on a CLI error so
|
|
587
|
+
# the caller keeps the previous cache. Runs inside the repo (gh/az detect from PWD).
|
|
588
|
+
fetch_work() {
|
|
589
|
+
local platform="$1" branch="$2" refs out pr id n q=""
|
|
590
|
+
local title='gsub("[\t\r\n]"; " ")'
|
|
591
|
+
refs=$(item_refs "$platform" "$branch")
|
|
592
|
+
if [[ "$platform" == github ]]; then
|
|
593
|
+
for n in $refs; do
|
|
594
|
+
q+=" i$n: issueOrPullRequest(number: $n) {
|
|
595
|
+
... on Issue { number title url } ... on PullRequest { number title url } }"
|
|
596
|
+
done
|
|
597
|
+
# gh exits 1 when a ref is not an issue but still prints the rest, so
|
|
598
|
+
# success is judged by .data.repository instead of the exit code.
|
|
599
|
+
out=$(gh api graphql -F owner='{owner}' -F name='{repo}' -f branch="$branch" -f query='
|
|
600
|
+
query($owner: String!, $name: String!, $branch: String!) {
|
|
601
|
+
repository(owner: $owner, name: $name) {
|
|
602
|
+
pullRequests(headRefName: $branch, first: 1, states: [OPEN, MERGED, CLOSED],
|
|
603
|
+
orderBy: {field: CREATED_AT, direction: DESC}) {
|
|
604
|
+
nodes {
|
|
605
|
+
number state isDraft reviewDecision url
|
|
606
|
+
closingIssuesReferences(first: 10) { nodes { number title url } }
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
'"$q"'
|
|
610
|
+
}
|
|
611
|
+
}')
|
|
612
|
+
jq -r '.data.repository // error("no repository")
|
|
613
|
+
| if .pullRequests.nodes[0] then
|
|
614
|
+
.pullRequests.nodes[0]
|
|
615
|
+
| "PR #\(.number) \(if .isDraft and .state == "OPEN" then "draft" else (.state | ascii_downcase) end)"
|
|
616
|
+
+ ({"APPROVED": " approved", "CHANGES_REQUESTED": " changes requested",
|
|
617
|
+
"REVIEW_REQUIRED": " review required"}[.reviewDecision // ""] // "")
|
|
618
|
+
+ "\t\(.url)",
|
|
619
|
+
(.closingIssuesReferences.nodes[] | "#\(.number) \(.title | '"$title"')\t\(.url)")
|
|
620
|
+
else
|
|
621
|
+
to_entries[] | select(.key | startswith("i")) | .value // empty
|
|
622
|
+
| "#\(.number) \(.title | '"$title"')\t\(.url)"
|
|
623
|
+
end' <<<"$out"
|
|
624
|
+
else
|
|
625
|
+
local rows='.[] | "#\(.id) \(.fields."System.Title" | '"$title"')\t\(.url | sub("/_apis/wit/workItems/"; "/_workitems/edit/"))"'
|
|
626
|
+
pr=$(az repos pr list --source-branch "$branch" --status all --top 1 --only-show-errors -o json) || return 1
|
|
627
|
+
id=$(jq -r '.[0].pullRequestId // empty' <<<"$pr")
|
|
628
|
+
if [[ -n "$id" ]]; then
|
|
629
|
+
jq -r '.[0]
|
|
630
|
+
| "PR !\(.pullRequestId) \(if .isDraft and .status == "active" then "draft"
|
|
631
|
+
else ({"active": "open", "completed": "merged", "abandoned": "closed"}[.status] // .status) end)"
|
|
632
|
+
+ ([.reviewers[]?.vote] | if any(. == -10) then " rejected"
|
|
633
|
+
elif any(. == -5) then " waiting for author"
|
|
634
|
+
elif any(. >= 5) then " approved" else "" end)
|
|
635
|
+
+ "\t\(.url | sub("/_apis/git/repositories/"; "/_git/") | sub("/pullRequests/"; "/pullrequest/"))"' <<<"$pr"
|
|
636
|
+
az repos pr work-item list --id "$id" --only-show-errors -o json | jq -r "$rows"
|
|
637
|
+
elif [[ -n "$refs" ]]; then
|
|
638
|
+
az boards query --only-show-errors -o json \
|
|
639
|
+
--wiql "SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.Id] IN (${refs//$'\n'/, })" \
|
|
640
|
+
| jq -r "$rows"
|
|
641
|
+
fi
|
|
642
|
+
fi
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
work_plain=(); work_color=()
|
|
646
|
+
platform=""
|
|
647
|
+
if [[ -n "$branch" && "$branch" != detached@* ]]; then
|
|
648
|
+
remote=$(git -C "$cwd" remote get-url origin 2>/dev/null)
|
|
649
|
+
case "$remote" in
|
|
650
|
+
*github.com[:/]*)
|
|
651
|
+
command -v gh >/dev/null 2>&1 && platform=github ;;
|
|
652
|
+
*dev.azure.com[:/]*|*.visualstudio.com/*)
|
|
653
|
+
command -v az >/dev/null 2>&1 && platform=azure ;;
|
|
654
|
+
esac
|
|
655
|
+
fi
|
|
656
|
+
|
|
657
|
+
if [[ -n "$platform" ]]; then
|
|
658
|
+
work_dir="${TMPDIR:-/tmp}/statusline-work-${UID}"
|
|
659
|
+
mkdir -p -m 700 "$work_dir"
|
|
660
|
+
# Hashed so a token embedded in the remote URL never lands in a file name.
|
|
661
|
+
read -r key _ < <(printf '%s|%s' "$remote" "$branch" | cksum)
|
|
662
|
+
cache="$work_dir/$key"
|
|
663
|
+
now=$(date +%s); at=0
|
|
664
|
+
[[ -f "$cache.at" ]] && read -r at < "$cache.at"
|
|
665
|
+
if (( now - ${at:-0} > WORK_TTL )); then
|
|
666
|
+
printf '%s\n' "$now" > "$cache.at"
|
|
667
|
+
# Every fd is detached, or the host waits for the fetch to finish.
|
|
668
|
+
(
|
|
669
|
+
set -o pipefail
|
|
670
|
+
cd "$cwd" && rows=$(fetch_work "$platform" "$branch") \
|
|
671
|
+
&& printf '%s\n' "$rows" > "$cache.$$" && mv -f "$cache.$$" "$cache"
|
|
672
|
+
) </dev/null >/dev/null 2>&1 &
|
|
673
|
+
disown
|
|
674
|
+
fi
|
|
675
|
+
|
|
676
|
+
items=0
|
|
677
|
+
if [[ -f "$cache" ]]; then
|
|
678
|
+
while IFS=$'\t' read -r line url; do
|
|
679
|
+
# Titles come from other people; drop escapes before they reach the terminal.
|
|
680
|
+
line=${line//[[:cntrl:]]/}; url=${url//[[:cntrl:]]/}
|
|
681
|
+
[[ -z "$line" ]] && continue
|
|
682
|
+
[[ "$url" == https://* && "$url" != *' '* ]] || url=""
|
|
683
|
+
if [[ "$line" == 'PR '* ]]; then
|
|
684
|
+
color="$C_CTX"
|
|
685
|
+
elif (( ++items <= 3 )); then
|
|
686
|
+
color="$C_DIM"; line=$(truncate_to "$line" 50)
|
|
687
|
+
else
|
|
688
|
+
continue
|
|
689
|
+
fi
|
|
690
|
+
# OSC 8 makes the row clickable; it lives only in the colored copy.
|
|
691
|
+
colored="${color}${line}${RESET}"
|
|
692
|
+
[[ -n "$url" ]] && colored=$'\033]8;;'"${url}"$'\a'"${colored}"$'\033]8;;\a'
|
|
693
|
+
work_plain+=("$line"); work_color+=("$colored")
|
|
694
|
+
done < "$cache"
|
|
695
|
+
fi
|
|
696
|
+
if (( items > 3 )); then
|
|
697
|
+
more="+$(( items - 3 )) more"
|
|
698
|
+
work_plain+=("$more"); work_color+=("${C_DIM}${more}${RESET}")
|
|
699
|
+
fi
|
|
700
|
+
fi
|
|
701
|
+
work_rows=${#work_plain[@]}
|
|
702
|
+
|
|
490
703
|
rows=${#repo_plain[@]}
|
|
491
704
|
(( ${#sess_plain[@]} > rows )) && rows=${#sess_plain[@]}
|
|
492
705
|
if (( rows == 0 )); then printf ''; exit 0; fi
|
|
706
|
+
(( work_rows > rows )) && rows=$work_rows
|
|
493
707
|
for (( i = ${#repo_plain[@]}; i < rows; i++ )); do repo_plain+=(""); repo_color+=(""); done
|
|
494
708
|
for (( i = ${#sess_plain[@]}; i < rows; i++ )); do sess_plain+=(""); sess_color+=(""); done
|
|
709
|
+
if (( work_rows )); then
|
|
710
|
+
for (( i = work_rows; i < rows; i++ )); do work_plain+=(""); work_color+=(""); done
|
|
711
|
+
fi
|
|
495
712
|
|
|
496
713
|
# Width comes from the plain rows; the parallel colored rows carry ANSI escapes
|
|
497
714
|
# that would otherwise be counted as visible characters. Arrays are passed
|
|
@@ -504,6 +721,8 @@ box_width() {
|
|
|
504
721
|
}
|
|
505
722
|
rw=$(box_width "repo" "${repo_plain[@]}")
|
|
506
723
|
sw=$(box_width "session" "${sess_plain[@]}")
|
|
724
|
+
# An empty array trips `set -u` on bash 3.2, so only expand it when it has rows.
|
|
725
|
+
(( work_rows )) && ww=$(box_width "work" "${work_plain[@]}")
|
|
507
726
|
|
|
508
727
|
box_top() {
|
|
509
728
|
local title="$1" w="$2"
|
|
@@ -520,12 +739,15 @@ box_row() {
|
|
|
520
739
|
}
|
|
521
740
|
|
|
522
741
|
out="$(box_top repo "$rw") $(box_top session "$sw")"
|
|
742
|
+
(( work_rows )) && out+=" $(box_top work "$ww")"
|
|
523
743
|
for (( i = 0; i < rows; i++ )); do
|
|
524
744
|
out+=$'\n'
|
|
525
745
|
out+="$(box_row "${repo_color[$i]}" "$rw" "${repo_plain[$i]}")"
|
|
526
746
|
out+=" $(box_row "${sess_color[$i]}" "$sw" "${sess_plain[$i]}")"
|
|
747
|
+
(( work_rows )) && out+=" $(box_row "${work_color[$i]}" "$ww" "${work_plain[$i]}")"
|
|
527
748
|
done
|
|
528
749
|
out+=$'\n'"$(box_bottom "$rw") $(box_bottom "$sw")"
|
|
750
|
+
(( work_rows )) && out+=" $(box_bottom "$ww")"
|
|
529
751
|
|
|
530
752
|
printf '%s' "$out"
|
|
531
753
|
```
|
|
@@ -537,6 +759,9 @@ Write `statusline-command.ps1` to the targeted platform directory. Use `ConvertF
|
|
|
537
759
|
`statusline-command.ps1`:
|
|
538
760
|
|
|
539
761
|
```powershell
|
|
762
|
+
# -Refresh is only passed by the script itself, to fetch the work box in the background.
|
|
763
|
+
param([string]$Refresh, [string]$Branch, [string]$Cache)
|
|
764
|
+
|
|
540
765
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
541
766
|
# The statusline re-runs on every Claude turn. A plain `git status` grabs
|
|
542
767
|
# .git/index.lock to refresh stat info, racing the user's own git writes.
|
|
@@ -544,6 +769,7 @@ $env:GIT_OPTIONAL_LOCKS = '0'
|
|
|
544
769
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
545
770
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
546
771
|
$ESC = [char]27
|
|
772
|
+
$BEL = [char]7
|
|
547
773
|
|
|
548
774
|
$C_CWD = "$ESC[38;2;94;255;255m"
|
|
549
775
|
$C_BRANCH = "$ESC[38;2;194;72;112m"
|
|
@@ -639,6 +865,96 @@ function SumTokens($path) {
|
|
|
639
865
|
return ,@($sumIn, $sumOut)
|
|
640
866
|
}
|
|
641
867
|
|
|
868
|
+
$WORK_TTL = 60
|
|
869
|
+
|
|
870
|
+
# Item numbers related to the branch without a PR: a 3-9 digit segment of the
|
|
871
|
+
# branch name (plus gh issue develop's "<n>-slug"), then AB#N (Azure DevOps) or
|
|
872
|
+
# #N (GitHub) in commits not yet on the default branch. Capped at 9 digits
|
|
873
|
+
# because both APIs take 32-bit ids, and one bad literal fails the whole query.
|
|
874
|
+
function ItemRefs($platform, $branch) {
|
|
875
|
+
$refs = @()
|
|
876
|
+
if ($platform -eq "github" -and $branch -match '^(\d{1,9})-') { $refs += $Matches[1] }
|
|
877
|
+
$refs += @($branch -split '[^A-Za-z0-9]' | Where-Object { $_ -match '^\d{3,9}$' })
|
|
878
|
+
$base = & git symbolic-ref --short refs/remotes/origin/HEAD 2>$null
|
|
879
|
+
if ($base) {
|
|
880
|
+
if ($platform -eq "github") { $pat = '#(\d{1,9})' } else { $pat = 'AB#(\d{1,9})' }
|
|
881
|
+
$log = (& git log --format=%B "$base..HEAD" 2>$null) -join "`n"
|
|
882
|
+
foreach ($m in [regex]::Matches($log, $pat)) { $refs += $m.Groups[1].Value }
|
|
883
|
+
}
|
|
884
|
+
return @($refs | ForEach-Object { [int]$_ } | Select-Object -Unique | Select-Object -First 10)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function OneLine($s) { return ([string]$s) -replace '[\t\r\n]', ' ' }
|
|
888
|
+
|
|
889
|
+
# Returns the PR row, then one row per linked item, each as "<text>`t<url>",
|
|
890
|
+
# or $null on a CLI error so the caller keeps the previous cache. Without a PR,
|
|
891
|
+
# the items come from ItemRefs instead. Runs inside the repo (gh/az detect from PWD).
|
|
892
|
+
function FetchWork($platform, $branch) {
|
|
893
|
+
$rows = @()
|
|
894
|
+
$refs = ItemRefs $platform $branch
|
|
895
|
+
if ($platform -eq "github") {
|
|
896
|
+
$q = ''
|
|
897
|
+
foreach ($n in $refs) { $q += " i$($n): issueOrPullRequest(number: $n) { ... on Issue { number title url } ... on PullRequest { number title url } }" }
|
|
898
|
+
# No double quotes: Windows PowerShell 5.1 strips them from native arguments.
|
|
899
|
+
$query = 'query($owner: String!, $name: String!, $branch: String!) { repository(owner: $owner, name: $name) { pullRequests(headRefName: $branch, first: 1, states: [OPEN, MERGED, CLOSED], orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number state isDraft reviewDecision url closingIssuesReferences(first: 10) { nodes { number title url } } } }' + $q + ' } }'
|
|
900
|
+
# gh exits 1 when a ref is not an issue but still prints the rest, so
|
|
901
|
+
# success is judged by .data.repository instead of the exit code.
|
|
902
|
+
$raw = & gh api graphql -F 'owner={owner}' -F 'name={repo}' -f "branch=$branch" -f "query=$query"
|
|
903
|
+
$repo = (($raw -join "`n") | ConvertFrom-Json).data.repository
|
|
904
|
+
if (-not $repo) { return $null }
|
|
905
|
+
$pr = $repo.pullRequests.nodes | Select-Object -First 1
|
|
906
|
+
if ($pr) {
|
|
907
|
+
if ($pr.isDraft -and $pr.state -eq "OPEN") { $state = "draft" } else { $state = $pr.state.ToLower() }
|
|
908
|
+
$review = @{ APPROVED = " approved"; CHANGES_REQUESTED = " changes requested"; REVIEW_REQUIRED = " review required" }[[string]$pr.reviewDecision]
|
|
909
|
+
$rows += "PR #$($pr.number) $state$review`t$($pr.url)"
|
|
910
|
+
foreach ($i in $pr.closingIssuesReferences.nodes) { $rows += "#$($i.number) $(OneLine $i.title)`t$($i.url)" }
|
|
911
|
+
} else {
|
|
912
|
+
foreach ($n in $refs) {
|
|
913
|
+
$i = $repo."i$n"
|
|
914
|
+
if ($i) { $rows += "#$($i.number) $(OneLine $i.title)`t$($i.url)" }
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
} else {
|
|
918
|
+
$raw = & az repos pr list --source-branch $branch --status all --top 1 --only-show-errors -o json
|
|
919
|
+
if ($LASTEXITCODE -ne 0) { return $null }
|
|
920
|
+
$pr = @(($raw -join "`n") | ConvertFrom-Json) | Select-Object -First 1
|
|
921
|
+
if ($pr) {
|
|
922
|
+
if ($pr.isDraft -and $pr.status -eq "active") {
|
|
923
|
+
$state = "draft"
|
|
924
|
+
} else {
|
|
925
|
+
$state = @{ active = "open"; completed = "merged"; abandoned = "closed" }[[string]$pr.status]
|
|
926
|
+
if (-not $state) { $state = $pr.status }
|
|
927
|
+
}
|
|
928
|
+
$votes = @($pr.reviewers | ForEach-Object { $_.vote })
|
|
929
|
+
$review = ""
|
|
930
|
+
if ($votes -contains -10) { $review = " rejected" }
|
|
931
|
+
elseif ($votes -contains -5) { $review = " waiting for author" }
|
|
932
|
+
elseif (@($votes | Where-Object { $_ -ge 5 }).Count -gt 0) { $review = " approved" }
|
|
933
|
+
$url = $pr.url -replace '/_apis/git/repositories/', '/_git/' -replace '/pullRequests/', '/pullrequest/'
|
|
934
|
+
$rows += "PR !$($pr.pullRequestId) $state$review`t$url"
|
|
935
|
+
$raw = & az repos pr work-item list --id $pr.pullRequestId --only-show-errors -o json
|
|
936
|
+
} elseif ($refs.Count -gt 0) {
|
|
937
|
+
$raw = & az boards query --only-show-errors -o json --wiql "SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.Id] IN ($($refs -join ', '))"
|
|
938
|
+
} else {
|
|
939
|
+
return ,$rows
|
|
940
|
+
}
|
|
941
|
+
if ($LASTEXITCODE -ne 0) { return $null }
|
|
942
|
+
foreach ($w in @(($raw -join "`n") | ConvertFrom-Json)) {
|
|
943
|
+
$rows += "#$($w.id) $(OneLine $w.fields.'System.Title')`t$($w.url -replace '/_apis/wit/workItems/', '/_workitems/edit/')"
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return ,$rows
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if ($Refresh) {
|
|
950
|
+
$rows = FetchWork $Refresh $Branch
|
|
951
|
+
if ($null -ne $rows) {
|
|
952
|
+
[System.IO.File]::WriteAllLines("$Cache.$PID", [string[]]$rows)
|
|
953
|
+
Move-Item -LiteralPath "$Cache.$PID" -Destination $Cache -Force
|
|
954
|
+
}
|
|
955
|
+
exit 0
|
|
956
|
+
}
|
|
957
|
+
|
|
642
958
|
$raw = [Console]::In.ReadToEnd()
|
|
643
959
|
try { $data = if ($raw.Trim()) { $raw | ConvertFrom-Json } else { @{} } } catch { $data = @{} }
|
|
644
960
|
|
|
@@ -788,10 +1104,76 @@ if ($null -ne $seven_used) {
|
|
|
788
1104
|
$sessPlain += $s; $sessColor += "$(QuotaColor $seven_used)$s$RESET"
|
|
789
1105
|
}
|
|
790
1106
|
|
|
1107
|
+
# --- work box: PR + linked items for the current branch ---
|
|
1108
|
+
# gh takes ~0.7s and az ~5s, so rendering only reads a cache; a stale cache
|
|
1109
|
+
# starts a detached refresh and this turn shows the previous rows.
|
|
1110
|
+
$workPlain = @(); $workColor = @()
|
|
1111
|
+
$platform = ""
|
|
1112
|
+
if ($branch -and -not $branch.StartsWith("detached@")) {
|
|
1113
|
+
$remote = & git -C "$cwd" remote get-url origin 2>$null
|
|
1114
|
+
if ($remote -match 'github\.com[:/]') {
|
|
1115
|
+
if (Get-Command gh) { $platform = "github" }
|
|
1116
|
+
} elseif ($remote -match 'dev\.azure\.com[:/]|\.visualstudio\.com/') {
|
|
1117
|
+
if (Get-Command az) { $platform = "azure" }
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if ($platform) {
|
|
1122
|
+
$workDir = Join-Path $env:TEMP "statusline-work"
|
|
1123
|
+
New-Item -ItemType Directory -Force -Path $workDir | Out-Null
|
|
1124
|
+
# Hashed so a token embedded in the remote URL never lands in a file name.
|
|
1125
|
+
$hash = [System.Security.Cryptography.MD5]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes("$remote|$branch"))
|
|
1126
|
+
$cache = Join-Path $workDir ([BitConverter]::ToString($hash) -replace '-', '')
|
|
1127
|
+
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
1128
|
+
$at = 0
|
|
1129
|
+
if (Test-Path -LiteralPath "$cache.at") { $at = [int64](Get-Content -LiteralPath "$cache.at" -TotalCount 1) }
|
|
1130
|
+
if ($now - $at -gt $WORK_TTL) {
|
|
1131
|
+
Set-Content -LiteralPath "$cache.at" -Value $now
|
|
1132
|
+
# Start-Job dies with this process, so re-run the script detached instead.
|
|
1133
|
+
$self = (Get-Process -Id $PID).Path
|
|
1134
|
+
Start-Process -FilePath $self -WindowStyle Hidden -WorkingDirectory $cwd -ArgumentList @(
|
|
1135
|
+
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"",
|
|
1136
|
+
'-Refresh', $platform, '-Branch', $branch, '-Cache', "`"$cache`"")
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
$items = 0
|
|
1140
|
+
if (Test-Path -LiteralPath $cache) {
|
|
1141
|
+
foreach ($row in [System.IO.File]::ReadAllLines($cache)) {
|
|
1142
|
+
$parts = $row.Split("`t")
|
|
1143
|
+
# Titles come from other people; drop escapes before they reach the terminal.
|
|
1144
|
+
$line = $parts[0] -replace '[\x00-\x1f\x7f-\x9f]', ''
|
|
1145
|
+
$url = ""
|
|
1146
|
+
if ($parts.Count -gt 1) { $url = $parts[1] -replace '[\x00-\x1f\x7f-\x9f]', '' }
|
|
1147
|
+
if (-not $line) { continue }
|
|
1148
|
+
if (-not ($url.StartsWith("https://") -and -not $url.Contains(" "))) { $url = "" }
|
|
1149
|
+
if ($line.StartsWith("PR ")) {
|
|
1150
|
+
$color = $C_CTX
|
|
1151
|
+
} else {
|
|
1152
|
+
$items++
|
|
1153
|
+
if ($items -gt 3) { continue }
|
|
1154
|
+
$color = $C_DIM; $line = TruncateTo $line 50
|
|
1155
|
+
}
|
|
1156
|
+
# OSC 8 makes the row clickable; it lives only in the colored copy.
|
|
1157
|
+
$colored = "$color$line$RESET"
|
|
1158
|
+
if ($url) { $colored = "$ESC]8;;$url$BEL$colored$ESC]8;;$BEL" }
|
|
1159
|
+
$workPlain += $line; $workColor += $colored
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if ($items -gt 3) {
|
|
1163
|
+
$more = "+$($items - 3) more"
|
|
1164
|
+
$workPlain += $more; $workColor += "$C_DIM$more$RESET"
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
$workRows = $workPlain.Count
|
|
1168
|
+
|
|
791
1169
|
$rows = [math]::Max($repoPlain.Count, $sessPlain.Count)
|
|
792
1170
|
if ($rows -eq 0) { exit 0 }
|
|
1171
|
+
$rows = [math]::Max($rows, $workRows)
|
|
793
1172
|
while ($repoPlain.Count -lt $rows) { $repoPlain += ""; $repoColor += "" }
|
|
794
1173
|
while ($sessPlain.Count -lt $rows) { $sessPlain += ""; $sessColor += "" }
|
|
1174
|
+
if ($workRows -gt 0) {
|
|
1175
|
+
while ($workPlain.Count -lt $rows) { $workPlain += ""; $workColor += "" }
|
|
1176
|
+
}
|
|
795
1177
|
|
|
796
1178
|
# Width comes from the plain rows; the parallel colored rows carry ANSI escapes
|
|
797
1179
|
# that would otherwise be counted as visible characters.
|
|
@@ -802,6 +1184,7 @@ function BoxWidth($title, $plainRows) {
|
|
|
802
1184
|
}
|
|
803
1185
|
$rw = BoxWidth "repo" $repoPlain
|
|
804
1186
|
$sw = BoxWidth "session" $sessPlain
|
|
1187
|
+
$ww = BoxWidth "work" $workPlain
|
|
805
1188
|
|
|
806
1189
|
function BoxTop($title, [int]$w) {
|
|
807
1190
|
$dashes = [string]$HZ * ($w - $title.Length - 1)
|
|
@@ -816,11 +1199,17 @@ function BoxRow($colored, [int]$w, $plain) {
|
|
|
816
1199
|
}
|
|
817
1200
|
|
|
818
1201
|
$lines = @()
|
|
819
|
-
$
|
|
1202
|
+
$line = (BoxTop "repo" $rw) + " " + (BoxTop "session" $sw)
|
|
1203
|
+
if ($workRows -gt 0) { $line += " " + (BoxTop "work" $ww) }
|
|
1204
|
+
$lines += $line
|
|
820
1205
|
for ($i = 0; $i -lt $rows; $i++) {
|
|
821
|
-
$
|
|
1206
|
+
$line = (BoxRow $repoColor[$i] $rw $repoPlain[$i]) + " " + (BoxRow $sessColor[$i] $sw $sessPlain[$i])
|
|
1207
|
+
if ($workRows -gt 0) { $line += " " + (BoxRow $workColor[$i] $ww $workPlain[$i]) }
|
|
1208
|
+
$lines += $line
|
|
822
1209
|
}
|
|
823
|
-
$
|
|
1210
|
+
$line = (BoxBottom $rw) + " " + (BoxBottom $sw)
|
|
1211
|
+
if ($workRows -gt 0) { $line += " " + (BoxBottom $ww) }
|
|
1212
|
+
$lines += $line
|
|
824
1213
|
|
|
825
1214
|
[Console]::Out.Write([string]::Join("`n", $lines))
|
|
826
1215
|
```
|
|
@@ -835,13 +1224,16 @@ Wire into `~/.claude/settings.json` (or `%USERPROFILE%\.claude\settings.json` on
|
|
|
835
1224
|
{
|
|
836
1225
|
"statusLine": {
|
|
837
1226
|
"type": "command",
|
|
838
|
-
"command": "bash ~/.claude/statusline-command.sh"
|
|
1227
|
+
"command": "bash ~/.claude/statusline-command.sh",
|
|
1228
|
+
"refreshInterval": 10
|
|
839
1229
|
}
|
|
840
1230
|
}
|
|
841
1231
|
```
|
|
842
1232
|
|
|
843
1233
|
(Or the `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.claude\statusline-command.ps1"` variant on Windows).
|
|
844
1234
|
|
|
1235
|
+
`refreshInterval` re-runs the script every 10 seconds on top of the event triggers. Claude Code renders the statusline once at startup, which only starts the background work-box fetch; without the timer the box would stay empty until the first assistant message. The cache stamp still limits `gh`/`az` to one call per 60 seconds.
|
|
1236
|
+
|
|
845
1237
|
### Antigravity CLI
|
|
846
1238
|
|
|
847
1239
|
Wire into `~/.gemini/antigravity-cli/settings.json` (or `%USERPROFILE%\.gemini\antigravity-cli\settings.json` on Windows):
|
|
@@ -858,6 +1250,23 @@ Wire into `~/.gemini/antigravity-cli/settings.json` (or `%USERPROFILE%\.gemini\a
|
|
|
858
1250
|
|
|
859
1251
|
(Or the `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.gemini\antigravity-cli\statusline-command.ps1"` variant on Windows).
|
|
860
1252
|
|
|
1253
|
+
## Work Pane (Claude Code)
|
|
1254
|
+
|
|
1255
|
+
The work box shows a few rows. The work pane is its scrollable companion: a Claude Code mod that ships inside this skill folder, in `.claude-plugin/plugin.json` and `mod/`. Claude Code loads a plugin from any folder under `~/.claude/skills/` (or a project's `.claude/skills/`), so installing this skill installs the pane. **Do not write any settings for it**, in particular not `CLAUDE_CODE_PLUGIN_DIRS`: that would load it a second time.
|
|
1256
|
+
|
|
1257
|
+
What it shows, in a side pane:
|
|
1258
|
+
|
|
1259
|
+
- **Summary and Steps** of the session, written by a Haiku call. It runs when the pane first opens and on **Refresh**, never per turn.
|
|
1260
|
+
- **Pull requests**: the current branch's, those of branches this session committed to, and any PR of this repository linked in the conversation. Each shows its state, review, checks and comment count.
|
|
1261
|
+
- **Work items**: the PRs' linked issues or work items or, for a branch without a PR, the same branch refs the work box uses.
|
|
1262
|
+
|
|
1263
|
+
How it opens:
|
|
1264
|
+
|
|
1265
|
+
- `/work` opens and focuses it. Up/Down or PgUp/PgDn scroll, `r` refreshes, Esc closes.
|
|
1266
|
+
- A resumed session (`--resume`, `--continue`, the picker or `/resume`) opens it by itself, unfocused, once it has a PR or item to show. A new session never does.
|
|
1267
|
+
|
|
1268
|
+
It needs the same `gh` or `az` login as the work box. It fetches by itself and does not read the statusline's cache. Its tests run with `claude plugin test <skill folder>`.
|
|
1269
|
+
|
|
861
1270
|
## Smoke Test
|
|
862
1271
|
|
|
863
1272
|
After installing, pipe a fake payload into the script and confirm:
|
|
@@ -870,6 +1279,11 @@ After installing, pipe a fake payload into the script and confirm:
|
|
|
870
1279
|
- The tokens sum matches the fixture and sits on the `ctx` row.
|
|
871
1280
|
- Omitting `transcript_path` leaves the `ctx` row as a bare `ctx N%` — no `↑0 ↓0`, no stray spacing.
|
|
872
1281
|
- Outside a repo the repo box collapses to the cwd row alone and the session box pads with blank rows.
|
|
1282
|
+
- With a fixture work cache, a third `work` box shows the PR row and item rows, and every line still has the same width.
|
|
1283
|
+
- Work rows with an `https://` URL carry an OSC 8 link; strip `\e]8;;[^\a]*\a` along with the color codes before measuring widths.
|
|
1284
|
+
- With an empty work cache, only the repo and session boxes render.
|
|
1285
|
+
- With no work cache at all, the script still returns in well under a second, and the refresh writes the cache a few seconds later.
|
|
1286
|
+
- In a new Claude Code session inside a repo, `/work` opens the work pane; `claude plugin validate` and `claude plugin test` on the installed skill folder both pass.
|
|
873
1287
|
|
|
874
1288
|
### Linux/macOS smoke test
|
|
875
1289
|
|
|
@@ -902,6 +1316,15 @@ printf '{"cwd":"%s","transcript_path":"/tmp/statusline-smoke/transcript.jsonl","
|
|
|
902
1316
|
|
|
903
1317
|
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.
|
|
904
1318
|
|
|
1319
|
+
Work box from a fixture cache, run inside a GitHub or Azure DevOps repo. The fresh `.at` stamp stops a network refresh, so this stays offline:
|
|
1320
|
+
|
|
1321
|
+
```bash
|
|
1322
|
+
d="${TMPDIR:-/tmp}/statusline-work-$UID"; mkdir -p -m 700 "$d"
|
|
1323
|
+
read -r key _ < <(printf '%s|%s' "$(git remote get-url origin)" "$(git branch --show-current)" | cksum)
|
|
1324
|
+
printf 'PR #46 open approved\thttps://github.com/o/r/pull/46\n#45 Show PRs in statusline\thttps://github.com/o/r/issues/45\n' > "$d/$key"; date +%s > "$d/$key.at"
|
|
1325
|
+
printf '{"cwd":"%s","model":{"display_name":"Opus 4.7"}}' "$PWD" | bash ~/.claude/statusline-command.sh; echo
|
|
1326
|
+
```
|
|
1327
|
+
|
|
905
1328
|
### Windows smoke test (PowerShell)
|
|
906
1329
|
|
|
907
1330
|
```powershell
|
|
@@ -931,6 +1354,18 @@ $out
|
|
|
931
1354
|
|
|
932
1355
|
The last line must print exactly one number.
|
|
933
1356
|
|
|
1357
|
+
Work box from a fixture cache, run inside a GitHub or Azure DevOps repo. The fresh `.at` stamp stops a network refresh, so this stays offline:
|
|
1358
|
+
|
|
1359
|
+
```powershell
|
|
1360
|
+
$d = "$env:TEMP\statusline-work"; New-Item -ItemType Directory -Force -Path $d | Out-Null
|
|
1361
|
+
$md5 = [System.Security.Cryptography.MD5]::Create()
|
|
1362
|
+
$key = [BitConverter]::ToString($md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes("$(git remote get-url origin)|$(git branch --show-current)"))) -replace '-', ''
|
|
1363
|
+
"PR #46 open approved`thttps://github.com/o/r/pull/46`n#45 Show PRs in statusline`thttps://github.com/o/r/issues/45" | Set-Content -LiteralPath "$d\$key" -Encoding UTF8
|
|
1364
|
+
[DateTimeOffset]::UtcNow.ToUnixTimeSeconds() | Set-Content -LiteralPath "$d\$key.at"
|
|
1365
|
+
$payload = @{ cwd = $PWD.Path; model = @{ display_name = "Opus 4.7" } } | ConvertTo-Json -Compress
|
|
1366
|
+
$payload | powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$env:USERPROFILE\.claude\statusline-command.ps1"
|
|
1367
|
+
```
|
|
1368
|
+
|
|
934
1369
|
## Color Preview One-Liner
|
|
935
1370
|
|
|
936
1371
|
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.
|
|
@@ -953,5 +1388,6 @@ $e=[char]27; Write-Host "$e[38;2;78;186;101m5h 20% resets 2h 30m$e[0m`n$e[38;2;
|
|
|
953
1388
|
- Do not auto-commit any changes.
|
|
954
1389
|
- Do not add Co-Authored-By trailers.
|
|
955
1390
|
- Read git state with `git -C "$cwd" status -b --porcelain`, never from `PWD`.
|
|
1391
|
+
- Never call `gh` or `az` in the render path; only the detached refresh calls them.
|
|
956
1392
|
- Never measure padding from a string containing ANSI escapes.
|
|
957
1393
|
- When merging into existing `settings.json`, preserve all other keys; only set/replace `statusLine`.
|