@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.
Files changed (140) hide show
  1. package/README.md +43 -64
  2. package/dist/bin/cli.js +270 -478
  3. package/dist/skills/statusline-setup/.claude-plugin/plugin.json +1 -1
  4. package/dist/skills/statusline-setup/SKILL.md +48 -448
  5. package/dist/skills/statusline-setup/mod/hooks/register.tsx +93 -103
  6. package/dist/skills/statusline-setup/mod/tests/work.test.ts +28 -10
  7. package/dist/skills/statusline-setup/mod/types/index.d.ts +2 -1
  8. package/dist/skills/universalize-agents/reference/mapping.md +3 -2
  9. package/package.json +3 -3
  10. package/dist/agents/backend-specialist.md +0 -277
  11. package/dist/agents/code-archaeologist.md +0 -114
  12. package/dist/agents/database-architect.md +0 -238
  13. package/dist/agents/debugger.md +0 -231
  14. package/dist/agents/devops-engineer.md +0 -242
  15. package/dist/agents/documentation-writer.md +0 -104
  16. package/dist/agents/explorer-agent.md +0 -79
  17. package/dist/agents/frontend-specialist.md +0 -594
  18. package/dist/agents/game-developer.md +0 -162
  19. package/dist/agents/mobile-developer.md +0 -383
  20. package/dist/agents/orchestrator.md +0 -435
  21. package/dist/agents/penetration-tester.md +0 -188
  22. package/dist/agents/performance-optimizer.md +0 -191
  23. package/dist/agents/product-manager.md +0 -122
  24. package/dist/agents/product-owner.md +0 -106
  25. package/dist/agents/project-planner.md +0 -408
  26. package/dist/agents/qa-automation-engineer.md +0 -109
  27. package/dist/agents/security-auditor.md +0 -171
  28. package/dist/agents/test-engineer.md +0 -158
  29. package/dist/skills/agent-identifier/SKILL.md +0 -431
  30. package/dist/skills/agent-identifier/examples/agent-creation-prompt.md +0 -247
  31. package/dist/skills/agent-identifier/examples/complete-agent-examples.md +0 -432
  32. package/dist/skills/agent-identifier/references/agent-creation-system-prompt.md +0 -215
  33. package/dist/skills/agent-identifier/references/system-prompt-design.md +0 -419
  34. package/dist/skills/agent-identifier/references/triggering-examples.md +0 -508
  35. package/dist/skills/agent-identifier/scripts/validate-agent.sh +0 -217
  36. package/dist/skills/code-review-excellence/SKILL.md +0 -525
  37. package/dist/skills/component-refactoring/SKILL.md +0 -492
  38. package/dist/skills/component-refactoring/references/complexity-patterns.md +0 -493
  39. package/dist/skills/component-refactoring/references/component-splitting.md +0 -477
  40. package/dist/skills/component-refactoring/references/hook-extraction.md +0 -317
  41. package/dist/skills/context7-guidelines/SKILL.md +0 -31
  42. package/dist/skills/feature-design-assistant/SKILL.md +0 -371
  43. package/dist/skills/find-bugs/SKILL.md +0 -75
  44. package/dist/skills/frontend-design/SKILL.md +0 -45
  45. package/dist/skills/go-concurrency-patterns/SKILL.md +0 -657
  46. package/dist/skills/grill-me/SKILL.md +0 -9
  47. package/dist/skills/handoff/SKILL.md +0 -16
  48. package/dist/skills/koreader-plugin-development/SKILL.md +0 -141
  49. package/dist/skills/koreader-plugin-development/examples/2-example-patch.lua +0 -17
  50. package/dist/skills/koreader-plugin-development/examples/_meta.lua +0 -6
  51. package/dist/skills/koreader-plugin-development/examples/helloworld.lua +0 -36
  52. package/dist/skills/koreader-plugin-development/references/debugging.md +0 -92
  53. package/dist/skills/koreader-plugin-development/references/events-and-widgets.md +0 -102
  54. package/dist/skills/koreader-plugin-development/references/patches.md +0 -118
  55. package/dist/skills/koreader-plugin-development/references/plugin-anatomy.md +0 -97
  56. package/dist/skills/make-instruction/SKILL.md +0 -68
  57. package/dist/skills/make-prompt/SKILL.md +0 -70
  58. package/dist/skills/make-skill/SKILL.md +0 -147
  59. package/dist/skills/prd-to-tasks/SKILL.md +0 -164
  60. package/dist/skills/react-native-async-patterns/SKILL.md +0 -256
  61. package/dist/skills/react-native-performance/SKILL.md +0 -425
  62. package/dist/skills/react-native-testing/SKILL.md +0 -458
  63. package/dist/skills/react-native-zustand-patterns/SKILL.md +0 -343
  64. package/dist/skills/skill-creator/SKILL.md +0 -385
  65. package/dist/skills/skill-creator/scripts/init_skill.cjs +0 -235
  66. package/dist/skills/skill-creator/scripts/package_skill.cjs +0 -84
  67. package/dist/skills/skill-creator/scripts/validate_skill.cjs +0 -127
  68. package/dist/skills/skill-development/SKILL.md +0 -676
  69. package/dist/skills/skill-development/references/skill-creator-original.md +0 -210
  70. package/dist/skills/sql-optimization-patterns/SKILL.md +0 -509
  71. package/dist/skills/test-driven-development/SKILL.md +0 -114
  72. package/dist/skills/test-driven-development/deep-modules.md +0 -33
  73. package/dist/skills/test-driven-development/interface-design.md +0 -35
  74. package/dist/skills/test-driven-development/mocking.md +0 -68
  75. package/dist/skills/test-driven-development/refactoring.md +0 -10
  76. package/dist/skills/test-driven-development/tests.md +0 -61
  77. package/dist/skills/vercel-react-best-practices/AGENTS.md +0 -2410
  78. package/dist/skills/vercel-react-best-practices/SKILL.md +0 -127
  79. package/dist/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  80. package/dist/skills/vercel-react-best-practices/rules/advanced-use-latest.md +0 -49
  81. package/dist/skills/vercel-react-best-practices/rules/async-api-routes.md +0 -38
  82. package/dist/skills/vercel-react-best-practices/rules/async-defer-await.md +0 -80
  83. package/dist/skills/vercel-react-best-practices/rules/async-dependencies.md +0 -36
  84. package/dist/skills/vercel-react-best-practices/rules/async-parallel.md +0 -28
  85. package/dist/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -99
  86. package/dist/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -59
  87. package/dist/skills/vercel-react-best-practices/rules/bundle-conditional.md +0 -31
  88. package/dist/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -49
  89. package/dist/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  90. package/dist/skills/vercel-react-best-practices/rules/bundle-preload.md +0 -50
  91. package/dist/skills/vercel-react-best-practices/rules/client-event-listeners.md +0 -74
  92. package/dist/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -71
  93. package/dist/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -48
  94. package/dist/skills/vercel-react-best-practices/rules/client-swr-dedup.md +0 -56
  95. package/dist/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -82
  96. package/dist/skills/vercel-react-best-practices/rules/js-cache-function-results.md +0 -80
  97. package/dist/skills/vercel-react-best-practices/rules/js-cache-property-access.md +0 -28
  98. package/dist/skills/vercel-react-best-practices/rules/js-cache-storage.md +0 -70
  99. package/dist/skills/vercel-react-best-practices/rules/js-combine-iterations.md +0 -32
  100. package/dist/skills/vercel-react-best-practices/rules/js-early-exit.md +0 -50
  101. package/dist/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -45
  102. package/dist/skills/vercel-react-best-practices/rules/js-index-maps.md +0 -37
  103. package/dist/skills/vercel-react-best-practices/rules/js-length-check-first.md +0 -50
  104. package/dist/skills/vercel-react-best-practices/rules/js-min-max-loop.md +0 -82
  105. package/dist/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -24
  106. package/dist/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -57
  107. package/dist/skills/vercel-react-best-practices/rules/rendering-activity.md +0 -26
  108. package/dist/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  109. package/dist/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -40
  110. package/dist/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -38
  111. package/dist/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  112. package/dist/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  113. package/dist/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -28
  114. package/dist/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -39
  115. package/dist/skills/vercel-react-best-practices/rules/rerender-dependencies.md +0 -45
  116. package/dist/skills/vercel-react-best-practices/rules/rerender-derived-state.md +0 -29
  117. package/dist/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -74
  118. package/dist/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  119. package/dist/skills/vercel-react-best-practices/rules/rerender-memo.md +0 -44
  120. package/dist/skills/vercel-react-best-practices/rules/rerender-transitions.md +0 -40
  121. package/dist/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -73
  122. package/dist/skills/vercel-react-best-practices/rules/server-cache-lru.md +0 -41
  123. package/dist/skills/vercel-react-best-practices/rules/server-cache-react.md +0 -76
  124. package/dist/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -83
  125. package/dist/skills/vercel-react-best-practices/rules/server-serialization.md +0 -38
  126. package/dist/skills/web-design-guidelines/SKILL.md +0 -40
  127. package/dist/skills/workflow-patterns/SKILL.md +0 -623
  128. package/dist/skills/write-prd/SKILL.md +0 -162
  129. package/dist/workflows/brainstorm.md +0 -113
  130. package/dist/workflows/create.md +0 -60
  131. package/dist/workflows/debug.md +0 -107
  132. package/dist/workflows/deploy.md +0 -178
  133. package/dist/workflows/enhance.md +0 -64
  134. package/dist/workflows/github-pull-request-create.md +0 -134
  135. package/dist/workflows/orchestrate.md +0 -246
  136. package/dist/workflows/plan.md +0 -90
  137. package/dist/workflows/preview.md +0 -83
  138. package/dist/workflows/status.md +0 -87
  139. package/dist/workflows/test.md +0 -144
  140. 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), 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).'
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 up to three side-by-side bordered boxes:
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 ─────────────╮ ╭─ 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
- ╰─────────────────────────────────────────────────────────────╯ ╰───────────────────────╯ ╰──────────────────────────────╯
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
- 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).
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
- - All rendered boxes are padded to the **same row count** with blank rows, so the frames start and end on the same terminal lines.
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. Arrays are passed
715
- # positionally because bash 3.2 (macOS) has no `local -n` nameref.
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 )) s
719
- for s in "$@"; do (( ${#s} > w )) && w=${#s}; done
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" "${repo_plain[@]}")
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[@]}")
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" plain="$3"
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 - ${#plain} )))" "$C_BOX" "$RESET"
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" "${repo_plain[$i]}")"
746
- out+=" $(box_row "${sess_color[$i]}" "$sw" "${sess_plain[$i]}")"
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
- $line = (BoxTop "repo" $rw) + " " + (BoxTop "session" $sw)
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
- $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
836
+ $lines += (BoxRow $repoColor[$i] $rw $repoPlain[$i]) + " " + (BoxRow $sessColor[$i] $sw $sessPlain[$i])
1209
837
  }
1210
- $line = (BoxBottom $rw) + " " + (BoxBottom $sw)
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 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.
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 or, for a branch without a PR, the same branch refs the work box uses.
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, once it has a PR or item to show. A new session never does.
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 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>`.
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 character count** — this is the alignment check, and it fails first when width is measured off a colored string or the locale is not UTF-8.
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; print length($_), "\n"' \
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`.