@kurokeita/add-skill 2.6.1 → 3.0.1
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 +43 -64
- package/dist/bin/cli.js +270 -478
- package/dist/skills/statusline-setup/.claude-plugin/plugin.json +1 -1
- package/dist/skills/statusline-setup/SKILL.md +48 -448
- package/dist/skills/statusline-setup/mod/hooks/register.tsx +93 -103
- package/dist/skills/statusline-setup/mod/tests/work.test.ts +28 -10
- package/dist/skills/statusline-setup/mod/types/index.d.ts +2 -1
- package/dist/skills/universalize-agents/reference/mapping.md +3 -2
- package/package.json +3 -3
- package/dist/agents/backend-specialist.md +0 -277
- package/dist/agents/code-archaeologist.md +0 -114
- package/dist/agents/database-architect.md +0 -238
- package/dist/agents/debugger.md +0 -231
- package/dist/agents/devops-engineer.md +0 -242
- package/dist/agents/documentation-writer.md +0 -104
- package/dist/agents/explorer-agent.md +0 -79
- package/dist/agents/frontend-specialist.md +0 -594
- package/dist/agents/game-developer.md +0 -162
- package/dist/agents/mobile-developer.md +0 -383
- package/dist/agents/orchestrator.md +0 -435
- package/dist/agents/penetration-tester.md +0 -188
- package/dist/agents/performance-optimizer.md +0 -191
- package/dist/agents/product-manager.md +0 -122
- package/dist/agents/product-owner.md +0 -106
- package/dist/agents/project-planner.md +0 -408
- package/dist/agents/qa-automation-engineer.md +0 -109
- package/dist/agents/security-auditor.md +0 -171
- package/dist/agents/test-engineer.md +0 -158
- package/dist/skills/agent-identifier/SKILL.md +0 -431
- package/dist/skills/agent-identifier/examples/agent-creation-prompt.md +0 -247
- package/dist/skills/agent-identifier/examples/complete-agent-examples.md +0 -432
- package/dist/skills/agent-identifier/references/agent-creation-system-prompt.md +0 -215
- package/dist/skills/agent-identifier/references/system-prompt-design.md +0 -419
- package/dist/skills/agent-identifier/references/triggering-examples.md +0 -508
- package/dist/skills/agent-identifier/scripts/validate-agent.sh +0 -217
- package/dist/skills/code-review-excellence/SKILL.md +0 -525
- package/dist/skills/component-refactoring/SKILL.md +0 -492
- package/dist/skills/component-refactoring/references/complexity-patterns.md +0 -493
- package/dist/skills/component-refactoring/references/component-splitting.md +0 -477
- package/dist/skills/component-refactoring/references/hook-extraction.md +0 -317
- package/dist/skills/context7-guidelines/SKILL.md +0 -31
- package/dist/skills/feature-design-assistant/SKILL.md +0 -371
- package/dist/skills/find-bugs/SKILL.md +0 -75
- package/dist/skills/frontend-design/SKILL.md +0 -45
- package/dist/skills/go-concurrency-patterns/SKILL.md +0 -657
- package/dist/skills/grill-me/SKILL.md +0 -9
- package/dist/skills/handoff/SKILL.md +0 -16
- package/dist/skills/koreader-plugin-development/SKILL.md +0 -141
- package/dist/skills/koreader-plugin-development/examples/2-example-patch.lua +0 -17
- package/dist/skills/koreader-plugin-development/examples/_meta.lua +0 -6
- package/dist/skills/koreader-plugin-development/examples/helloworld.lua +0 -36
- package/dist/skills/koreader-plugin-development/references/debugging.md +0 -92
- package/dist/skills/koreader-plugin-development/references/events-and-widgets.md +0 -102
- package/dist/skills/koreader-plugin-development/references/patches.md +0 -118
- package/dist/skills/koreader-plugin-development/references/plugin-anatomy.md +0 -97
- package/dist/skills/make-instruction/SKILL.md +0 -68
- package/dist/skills/make-prompt/SKILL.md +0 -70
- package/dist/skills/make-skill/SKILL.md +0 -147
- package/dist/skills/prd-to-tasks/SKILL.md +0 -164
- package/dist/skills/react-native-async-patterns/SKILL.md +0 -256
- package/dist/skills/react-native-performance/SKILL.md +0 -425
- package/dist/skills/react-native-testing/SKILL.md +0 -458
- package/dist/skills/react-native-zustand-patterns/SKILL.md +0 -343
- package/dist/skills/skill-creator/SKILL.md +0 -385
- package/dist/skills/skill-creator/scripts/init_skill.cjs +0 -235
- package/dist/skills/skill-creator/scripts/package_skill.cjs +0 -84
- package/dist/skills/skill-creator/scripts/validate_skill.cjs +0 -127
- package/dist/skills/skill-development/SKILL.md +0 -676
- package/dist/skills/skill-development/references/skill-creator-original.md +0 -210
- package/dist/skills/sql-optimization-patterns/SKILL.md +0 -509
- package/dist/skills/test-driven-development/SKILL.md +0 -114
- package/dist/skills/test-driven-development/deep-modules.md +0 -33
- package/dist/skills/test-driven-development/interface-design.md +0 -35
- package/dist/skills/test-driven-development/mocking.md +0 -68
- package/dist/skills/test-driven-development/refactoring.md +0 -10
- package/dist/skills/test-driven-development/tests.md +0 -61
- package/dist/skills/vercel-react-best-practices/AGENTS.md +0 -2410
- package/dist/skills/vercel-react-best-practices/SKILL.md +0 -127
- package/dist/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -55
- package/dist/skills/vercel-react-best-practices/rules/advanced-use-latest.md +0 -49
- package/dist/skills/vercel-react-best-practices/rules/async-api-routes.md +0 -38
- package/dist/skills/vercel-react-best-practices/rules/async-defer-await.md +0 -80
- package/dist/skills/vercel-react-best-practices/rules/async-dependencies.md +0 -36
- package/dist/skills/vercel-react-best-practices/rules/async-parallel.md +0 -28
- package/dist/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -99
- package/dist/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -59
- package/dist/skills/vercel-react-best-practices/rules/bundle-conditional.md +0 -31
- package/dist/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -49
- package/dist/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -35
- package/dist/skills/vercel-react-best-practices/rules/bundle-preload.md +0 -50
- package/dist/skills/vercel-react-best-practices/rules/client-event-listeners.md +0 -74
- package/dist/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -71
- package/dist/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -48
- package/dist/skills/vercel-react-best-practices/rules/client-swr-dedup.md +0 -56
- package/dist/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -82
- package/dist/skills/vercel-react-best-practices/rules/js-cache-function-results.md +0 -80
- package/dist/skills/vercel-react-best-practices/rules/js-cache-property-access.md +0 -28
- package/dist/skills/vercel-react-best-practices/rules/js-cache-storage.md +0 -70
- package/dist/skills/vercel-react-best-practices/rules/js-combine-iterations.md +0 -32
- package/dist/skills/vercel-react-best-practices/rules/js-early-exit.md +0 -50
- package/dist/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -45
- package/dist/skills/vercel-react-best-practices/rules/js-index-maps.md +0 -37
- package/dist/skills/vercel-react-best-practices/rules/js-length-check-first.md +0 -50
- package/dist/skills/vercel-react-best-practices/rules/js-min-max-loop.md +0 -82
- package/dist/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -24
- package/dist/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -57
- package/dist/skills/vercel-react-best-practices/rules/rendering-activity.md +0 -26
- package/dist/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
- package/dist/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -40
- package/dist/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -38
- package/dist/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -46
- package/dist/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
- package/dist/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -28
- package/dist/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -39
- package/dist/skills/vercel-react-best-practices/rules/rerender-dependencies.md +0 -45
- package/dist/skills/vercel-react-best-practices/rules/rerender-derived-state.md +0 -29
- package/dist/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -74
- package/dist/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -58
- package/dist/skills/vercel-react-best-practices/rules/rerender-memo.md +0 -44
- package/dist/skills/vercel-react-best-practices/rules/rerender-transitions.md +0 -40
- package/dist/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -73
- package/dist/skills/vercel-react-best-practices/rules/server-cache-lru.md +0 -41
- package/dist/skills/vercel-react-best-practices/rules/server-cache-react.md +0 -76
- package/dist/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -83
- package/dist/skills/vercel-react-best-practices/rules/server-serialization.md +0 -38
- package/dist/skills/web-design-guidelines/SKILL.md +0 -40
- package/dist/skills/workflow-patterns/SKILL.md +0 -623
- package/dist/skills/write-prd/SKILL.md +0 -162
- package/dist/workflows/brainstorm.md +0 -113
- package/dist/workflows/create.md +0 -60
- package/dist/workflows/debug.md +0 -107
- package/dist/workflows/deploy.md +0 -178
- package/dist/workflows/enhance.md +0 -64
- package/dist/workflows/github-pull-request-create.md +0 -134
- package/dist/workflows/orchestrate.md +0 -246
- package/dist/workflows/plan.md +0 -90
- package/dist/workflows/preview.md +0 -83
- package/dist/workflows/status.md +0 -87
- package/dist/workflows/test.md +0 -144
- package/dist/workflows/ui-ux-pro-max.md +0 -315
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: statusline-setup
|
|
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)
|
|
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). Also ships the Claude Code work pane (`/work`), which shows the session''s pull requests and work items. Use when asked to "set up my statusline", "install statusline", "configure statusline", or to add a colored multi-line status line showing rate limits or 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 or Gemini/Antigravity statusline that renders
|
|
8
|
+
Install a Claude Code or Gemini/Antigravity statusline that renders two 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 ─────────────╮
|
|
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
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
In Claude Code, `/work` shows the session's pull requests and work items in a side pane (see Work Pane).
|
|
20
20
|
|
|
21
21
|
## Box Contents
|
|
22
22
|
|
|
@@ -36,20 +36,11 @@ Rows 2-4 appear only inside a git repo.
|
|
|
36
36
|
3. **`5h N% resets <eta>`** — colored by remaining quota (see below).
|
|
37
37
|
4. **`7d N% resets <eta>`** — colored independently of the 5h row.
|
|
38
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
|
-
|
|
48
39
|
## Box Rendering Rules
|
|
49
40
|
|
|
50
41
|
- Border glyphs `╭ ╮ ╰ ╯ ─ │` in `#5a5f69` (`38;2;90;95;105`); the box title in `#8c929e` (`38;2;140;146;158`).
|
|
51
42
|
- Each box's inner width is the longest of its content rows, or its title + 2, whichever is larger.
|
|
52
|
-
-
|
|
43
|
+
- Both boxes are padded to the **same row count** with blank rows, so the frames start and end on the same terminal lines.
|
|
53
44
|
- The boxes are joined by a single space on every line.
|
|
54
45
|
- Rows are emitted newline-separated; the statusline host renders them as multiple lines.
|
|
55
46
|
- When the repo and session boxes both have zero content rows the script prints nothing and exits 0.
|
|
@@ -58,6 +49,8 @@ The box is left out entirely when it has no rows: detached HEAD, a remote that i
|
|
|
58
49
|
|
|
59
50
|
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.
|
|
60
51
|
|
|
52
|
+
Widths are **terminal columns**, not characters. Emoji and CJK take two columns, so a commit subject with `📝` pushes its row one column past the border when measured with `${#str}`. The bash variant measures every plain row in one `perl -CS` call: per grapheme cluster (`\X`), 2 columns when it holds an East Asian Wide or Fullwidth character or `U+FE0F`, else 1. Without perl it falls back to character counts.
|
|
53
|
+
|
|
61
54
|
### UTF-8 locale guard (bash only)
|
|
62
55
|
|
|
63
56
|
`${#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:
|
|
@@ -134,68 +127,6 @@ The last-commit row needs one further call: `git -C "$cwd" log -1 --format='%h %
|
|
|
134
127
|
|
|
135
128
|
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`.
|
|
136
129
|
|
|
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
|
-
|
|
199
130
|
## Quota Colors (by REMAINING quota = 100 - used%)
|
|
200
131
|
|
|
201
132
|
| Remaining | Color | 24-bit ANSI |
|
|
@@ -255,7 +186,7 @@ The input schema depends on the platform:
|
|
|
255
186
|
|
|
256
187
|
## Linux / macOS Implementation
|
|
257
188
|
|
|
258
|
-
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.
|
|
189
|
+
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. `perl`, which macOS and most Linux systems ship, measures row widths; without it the boxes still render but misalign on emoji.
|
|
259
190
|
|
|
260
191
|
JSON parsing rules:
|
|
261
192
|
|
|
@@ -560,169 +491,34 @@ if [[ -n "$seven_used" ]]; then
|
|
|
560
491
|
sess_plain+=("$s"); sess_color+=("$(quota_color "$su")${s}${RESET}")
|
|
561
492
|
fi
|
|
562
493
|
|
|
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
|
-
|
|
703
494
|
rows=${#repo_plain[@]}
|
|
704
495
|
(( ${#sess_plain[@]} > rows )) && rows=${#sess_plain[@]}
|
|
705
496
|
if (( rows == 0 )); then printf ''; exit 0; fi
|
|
706
|
-
(( work_rows > rows )) && rows=$work_rows
|
|
707
497
|
for (( i = ${#repo_plain[@]}; i < rows; i++ )); do repo_plain+=(""); repo_color+=(""); done
|
|
708
498
|
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
|
|
712
499
|
|
|
713
500
|
# Width comes from the plain rows; the parallel colored rows carry ANSI escapes
|
|
714
|
-
# that would otherwise be counted as visible characters.
|
|
715
|
-
#
|
|
501
|
+
# that would otherwise be counted as visible characters. ${#s} counts characters,
|
|
502
|
+
# but emoji and CJK take two terminal columns, so one perl call measures every
|
|
503
|
+
# row by grapheme. Each box holds `rows` rows, so box N's widths start at N*rows.
|
|
504
|
+
all=("${repo_plain[@]}" "${sess_plain[@]}")
|
|
505
|
+
cols=()
|
|
506
|
+
while IFS= read -r n; do cols+=("$n"); done < <(printf '%s\n' "${all[@]}" \
|
|
507
|
+
| perl -CS -ne 'chomp; my $w = 0; $w += /[\p{Ea=W}\p{Ea=F}\x{FE0F}]/ ? 2 : 1 for /\X/g; print "$w\n"' 2>/dev/null)
|
|
508
|
+
# Without perl (or its Unicode tables) fall back to character counts.
|
|
509
|
+
if (( ${#cols[@]} != ${#all[@]} )); then
|
|
510
|
+
cols=(); for s in "${all[@]}"; do cols+=("${#s}"); done
|
|
511
|
+
fi
|
|
512
|
+
|
|
513
|
+
# Widths are passed positionally because bash 3.2 (macOS) has no `local -n` nameref.
|
|
716
514
|
box_width() {
|
|
717
515
|
local title="$1"; shift
|
|
718
|
-
local w=$(( ${#title} + 2 ))
|
|
719
|
-
for
|
|
516
|
+
local w=$(( ${#title} + 2 )) n
|
|
517
|
+
for n in "$@"; do (( n > w )) && w=$n; done
|
|
720
518
|
printf '%d' "$w"
|
|
721
519
|
}
|
|
722
|
-
rw=$(box_width "repo" "${
|
|
723
|
-
sw=$(box_width "session" "${
|
|
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[@]}")
|
|
520
|
+
rw=$(box_width "repo" "${cols[@]:0:rows}")
|
|
521
|
+
sw=$(box_width "session" "${cols[@]:rows:rows}")
|
|
726
522
|
|
|
727
523
|
box_top() {
|
|
728
524
|
local title="$1" w="$2"
|
|
@@ -733,21 +529,18 @@ box_bottom() {
|
|
|
733
529
|
printf '%s╰%s╯%s' "$C_BOX" "$(repeat_char '─' $(( $1 + 2 )))" "$RESET"
|
|
734
530
|
}
|
|
735
531
|
box_row() {
|
|
736
|
-
local colored="$1" w="$2"
|
|
532
|
+
local colored="$1" w="$2" used="$3"
|
|
737
533
|
printf '%s│%s %s%s %s│%s' "$C_BOX" "$RESET" "$colored" \
|
|
738
|
-
"$(repeat_char ' ' $(( w -
|
|
534
|
+
"$(repeat_char ' ' $(( w - used )))" "$C_BOX" "$RESET"
|
|
739
535
|
}
|
|
740
536
|
|
|
741
537
|
out="$(box_top repo "$rw") $(box_top session "$sw")"
|
|
742
|
-
(( work_rows )) && out+=" $(box_top work "$ww")"
|
|
743
538
|
for (( i = 0; i < rows; i++ )); do
|
|
744
539
|
out+=$'\n'
|
|
745
|
-
out+="$(box_row "${repo_color[$i]}" "$rw" "${
|
|
746
|
-
out+=" $(box_row "${sess_color[$i]}" "$sw" "${
|
|
747
|
-
(( work_rows )) && out+=" $(box_row "${work_color[$i]}" "$ww" "${work_plain[$i]}")"
|
|
540
|
+
out+="$(box_row "${repo_color[$i]}" "$rw" "${cols[$i]}")"
|
|
541
|
+
out+=" $(box_row "${sess_color[$i]}" "$sw" "${cols[rows + i]}")"
|
|
748
542
|
done
|
|
749
543
|
out+=$'\n'"$(box_bottom "$rw") $(box_bottom "$sw")"
|
|
750
|
-
(( work_rows )) && out+=" $(box_bottom "$ww")"
|
|
751
544
|
|
|
752
545
|
printf '%s' "$out"
|
|
753
546
|
```
|
|
@@ -759,9 +552,6 @@ Write `statusline-command.ps1` to the targeted platform directory. Use `ConvertF
|
|
|
759
552
|
`statusline-command.ps1`:
|
|
760
553
|
|
|
761
554
|
```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
|
-
|
|
765
555
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
766
556
|
# The statusline re-runs on every Claude turn. A plain `git status` grabs
|
|
767
557
|
# .git/index.lock to refresh stat info, racing the user's own git writes.
|
|
@@ -769,7 +559,6 @@ $env:GIT_OPTIONAL_LOCKS = '0'
|
|
|
769
559
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
770
560
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
771
561
|
$ESC = [char]27
|
|
772
|
-
$BEL = [char]7
|
|
773
562
|
|
|
774
563
|
$C_CWD = "$ESC[38;2;94;255;255m"
|
|
775
564
|
$C_BRANCH = "$ESC[38;2;194;72;112m"
|
|
@@ -865,96 +654,6 @@ function SumTokens($path) {
|
|
|
865
654
|
return ,@($sumIn, $sumOut)
|
|
866
655
|
}
|
|
867
656
|
|
|
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
|
-
|
|
958
657
|
$raw = [Console]::In.ReadToEnd()
|
|
959
658
|
try { $data = if ($raw.Trim()) { $raw | ConvertFrom-Json } else { @{} } } catch { $data = @{} }
|
|
960
659
|
|
|
@@ -1104,76 +803,10 @@ if ($null -ne $seven_used) {
|
|
|
1104
803
|
$sessPlain += $s; $sessColor += "$(QuotaColor $seven_used)$s$RESET"
|
|
1105
804
|
}
|
|
1106
805
|
|
|
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
|
-
|
|
1169
806
|
$rows = [math]::Max($repoPlain.Count, $sessPlain.Count)
|
|
1170
807
|
if ($rows -eq 0) { exit 0 }
|
|
1171
|
-
$rows = [math]::Max($rows, $workRows)
|
|
1172
808
|
while ($repoPlain.Count -lt $rows) { $repoPlain += ""; $repoColor += "" }
|
|
1173
809
|
while ($sessPlain.Count -lt $rows) { $sessPlain += ""; $sessColor += "" }
|
|
1174
|
-
if ($workRows -gt 0) {
|
|
1175
|
-
while ($workPlain.Count -lt $rows) { $workPlain += ""; $workColor += "" }
|
|
1176
|
-
}
|
|
1177
810
|
|
|
1178
811
|
# Width comes from the plain rows; the parallel colored rows carry ANSI escapes
|
|
1179
812
|
# that would otherwise be counted as visible characters.
|
|
@@ -1184,7 +817,6 @@ function BoxWidth($title, $plainRows) {
|
|
|
1184
817
|
}
|
|
1185
818
|
$rw = BoxWidth "repo" $repoPlain
|
|
1186
819
|
$sw = BoxWidth "session" $sessPlain
|
|
1187
|
-
$ww = BoxWidth "work" $workPlain
|
|
1188
820
|
|
|
1189
821
|
function BoxTop($title, [int]$w) {
|
|
1190
822
|
$dashes = [string]$HZ * ($w - $title.Length - 1)
|
|
@@ -1199,17 +831,11 @@ function BoxRow($colored, [int]$w, $plain) {
|
|
|
1199
831
|
}
|
|
1200
832
|
|
|
1201
833
|
$lines = @()
|
|
1202
|
-
$
|
|
1203
|
-
if ($workRows -gt 0) { $line += " " + (BoxTop "work" $ww) }
|
|
1204
|
-
$lines += $line
|
|
834
|
+
$lines += (BoxTop "repo" $rw) + " " + (BoxTop "session" $sw)
|
|
1205
835
|
for ($i = 0; $i -lt $rows; $i++) {
|
|
1206
|
-
$
|
|
1207
|
-
if ($workRows -gt 0) { $line += " " + (BoxRow $workColor[$i] $ww $workPlain[$i]) }
|
|
1208
|
-
$lines += $line
|
|
836
|
+
$lines += (BoxRow $repoColor[$i] $rw $repoPlain[$i]) + " " + (BoxRow $sessColor[$i] $sw $sessPlain[$i])
|
|
1209
837
|
}
|
|
1210
|
-
$
|
|
1211
|
-
if ($workRows -gt 0) { $line += " " + (BoxBottom $ww) }
|
|
1212
|
-
$lines += $line
|
|
838
|
+
$lines += (BoxBottom $rw) + " " + (BoxBottom $sw)
|
|
1213
839
|
|
|
1214
840
|
[Console]::Out.Write([string]::Join("`n", $lines))
|
|
1215
841
|
```
|
|
@@ -1224,16 +850,13 @@ Wire into `~/.claude/settings.json` (or `%USERPROFILE%\.claude\settings.json` on
|
|
|
1224
850
|
{
|
|
1225
851
|
"statusLine": {
|
|
1226
852
|
"type": "command",
|
|
1227
|
-
"command": "bash ~/.claude/statusline-command.sh"
|
|
1228
|
-
"refreshInterval": 10
|
|
853
|
+
"command": "bash ~/.claude/statusline-command.sh"
|
|
1229
854
|
}
|
|
1230
855
|
}
|
|
1231
856
|
```
|
|
1232
857
|
|
|
1233
858
|
(Or the `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.claude\statusline-command.ps1"` variant on Windows).
|
|
1234
859
|
|
|
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
|
-
|
|
1237
860
|
### Antigravity CLI
|
|
1238
861
|
|
|
1239
862
|
Wire into `~/.gemini/antigravity-cli/settings.json` (or `%USERPROFILE%\.gemini\antigravity-cli\settings.json` on Windows):
|
|
@@ -1252,26 +875,29 @@ Wire into `~/.gemini/antigravity-cli/settings.json` (or `%USERPROFILE%\.gemini\a
|
|
|
1252
875
|
|
|
1253
876
|
## Work Pane (Claude Code)
|
|
1254
877
|
|
|
1255
|
-
The work
|
|
878
|
+
The work pane is 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
879
|
|
|
1257
880
|
What it shows, in a side pane:
|
|
1258
881
|
|
|
1259
882
|
- **Summary and Steps** of the session, written by a Haiku call. It runs when the pane first opens, at the end of each turn while the pane is open, and on **Refresh**.
|
|
1260
883
|
- **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
|
|
884
|
+
- **Work items**: the PRs' linked issues or work items. For a branch without a PR, the item numbers in the branch name (a 3 to 9 digit segment, or a `gh issue develop` `<n>-` prefix) and in commits not yet on the default branch (`#<n>` on GitHub, `AB#<n>` on Azure DevOps).
|
|
885
|
+
|
|
886
|
+
When `origin` is neither GitHub nor Azure DevOps, or there is no repo, the pane hides both sections and shows only the summary and steps.
|
|
1262
887
|
|
|
1263
888
|
How it opens:
|
|
1264
889
|
|
|
1265
890
|
- `/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,
|
|
891
|
+
- A resumed session (`--resume`, `--continue`, the picker or `/resume`) opens it by itself, unfocused, inside a repo or not, and with or without a PR or item. A new session and `/clear` never do.
|
|
1267
892
|
|
|
1268
|
-
It
|
|
893
|
+
It fetches with `gh` (GitHub) or `az` with the `azure-devops` extension (Azure DevOps), so it needs that CLI logged in. Its tests run with `claude plugin test <skill folder>`.
|
|
1269
894
|
|
|
1270
895
|
## Smoke Test
|
|
1271
896
|
|
|
1272
897
|
After installing, pipe a fake payload into the script and confirm:
|
|
1273
898
|
|
|
1274
|
-
- **Every rendered line has the same
|
|
899
|
+
- **Every rendered line has the same width in terminal columns** — this is the alignment check, and it fails first when width is measured off a colored string, by characters instead of columns, or the locale is not UTF-8.
|
|
900
|
+
- A last commit whose subject holds an emoji such as `📝` or `♻️` still passes the width check.
|
|
1275
901
|
- Both boxes show the same number of rows, framed top and bottom on the same lines.
|
|
1276
902
|
- Inside a repo the repo box shows 4 rows: full cwd, branch, working-tree state, last commit.
|
|
1277
903
|
- Staging a deletion (`git rm`) renders `-N`, not `+N`.
|
|
@@ -1279,10 +905,6 @@ After installing, pipe a fake payload into the script and confirm:
|
|
|
1279
905
|
- The tokens sum matches the fixture and sits on the `ctx` row.
|
|
1280
906
|
- Omitting `transcript_path` leaves the `ctx` row as a bare `ctx N%` — no `↑0 ↓0`, no stray spacing.
|
|
1281
907
|
- 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
908
|
- 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.
|
|
1287
909
|
|
|
1288
910
|
### Linux/macOS smoke test
|
|
@@ -1310,20 +932,11 @@ Assert equal line widths mechanically:
|
|
|
1310
932
|
printf '{"cwd":"%s","transcript_path":"/tmp/statusline-smoke/transcript.jsonl","model":{"display_name":"Opus 4.7"},"context_window":{"used_percentage":42}}' "$PWD" \
|
|
1311
933
|
| bash ~/.claude/statusline-command.sh \
|
|
1312
934
|
| perl -CSD -pe 's/\e\[[0-9;]*m//g' \
|
|
1313
|
-
| perl -CSD -ne 'chomp;
|
|
935
|
+
| perl -CSD -ne 'chomp; my $w = 0; $w += /[\p{Ea=W}\p{Ea=F}\x{FE0F}]/ ? 2 : 1 for /\X/g; print "$w\n"' \
|
|
1314
936
|
| sort -u
|
|
1315
937
|
```
|
|
1316
938
|
|
|
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.
|
|
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
|
-
```
|
|
939
|
+
That must print exactly one number. Counting with `length` instead would pass on a row an emoji pushes out of line. 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.
|
|
1327
940
|
|
|
1328
941
|
### Windows smoke test (PowerShell)
|
|
1329
942
|
|
|
@@ -1354,18 +967,6 @@ $out
|
|
|
1354
967
|
|
|
1355
968
|
The last line must print exactly one number.
|
|
1356
969
|
|
|
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
|
-
|
|
1369
970
|
## Color Preview One-Liner
|
|
1370
971
|
|
|
1371
972
|
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.
|
|
@@ -1388,6 +989,5 @@ $e=[char]27; Write-Host "$e[38;2;78;186;101m5h 20% resets 2h 30m$e[0m`n$e[38;2;
|
|
|
1388
989
|
- Do not auto-commit any changes.
|
|
1389
990
|
- Do not add Co-Authored-By trailers.
|
|
1390
991
|
- 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.
|
|
1392
992
|
- Never measure padding from a string containing ANSI escapes.
|
|
1393
993
|
- When merging into existing `settings.json`, preserve all other keys; only set/replace `statusLine`.
|