codebyplan 1.10.3 → 1.11.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/dist/cli.js +534 -209
- package/package.json +1 -1
- package/templates/agents/cbp-round-executor.md +1 -1
- package/templates/agents/cbp-task-check.md +0 -1
- package/templates/agents/cbp-test-e2e-agent.md +2 -2
- package/templates/agents/cbp-testing-qa-agent.md +7 -29
- package/templates/hooks/README.md +58 -16
- package/templates/hooks/cbp-statusline.mjs +385 -0
- package/templates/hooks/cbp-statusline.py +331 -0
- package/templates/hooks/cbp-statusline.sh +138 -82
- package/templates/hooks/cbp-subagent-statusline.mjs +200 -0
- package/templates/hooks/cbp-subagent-statusline.py +183 -0
- package/templates/hooks/cbp-subagent-statusline.sh +87 -39
- package/templates/skills/cbp-checkpoint-complete/SKILL.md +1 -1
- package/templates/skills/cbp-frontend-ui/SKILL.md +2 -2
- package/templates/skills/cbp-round-end/SKILL.md +16 -26
- package/templates/skills/cbp-round-execute/SKILL.md +2 -2
package/package.json
CHANGED
|
@@ -436,7 +436,7 @@ If none match, skip — proceed directly to Step 4.
|
|
|
436
436
|
- Aggregate `summary` totals into `round.context.frontend_self_review.summary` (combined critical / warning / suggestion / auto_fixed / out_of_scope_fixes).
|
|
437
437
|
|
|
438
438
|
4. **Surface non-mechanical findings** to the round summary:
|
|
439
|
-
- `baseline_regression` and `rendered_visual` findings from `cbp-frontend-ui` are NOT auto-fixed (root cause is typically in app state/data, not styling) — surface
|
|
439
|
+
- `baseline_regression` and `rendered_visual` findings from `cbp-frontend-ui` are NOT auto-fixed (root cause is typically in app state/data, not styling) — surface in `round.context.frontend_ui_review` findings; `/cbp-round-end` Step 7 surfaces baseline-regression findings as a blocking accept-or-fix gate (baselines never auto-accepted).
|
|
440
440
|
- `out_of_scope_fixes` from either skill (findings whose target file is outside `files_changed`) — surface in `improvements_noted[]` for follow-up rounds; the scope gate prevented silent absorption.
|
|
441
441
|
|
|
442
442
|
**Why inline (not a separate spawn)**: the post-implementation review consumes the same files the executor just touched. Spawning a separate agent doubles token cost (re-reading the files) and serialises wall time; invoking via Skill keeps both review passes inside the executor's working memory and lets fixes apply with the same Edit/Write tools that wrote the original code. The Pre-Edit Scope Gate inside each skill provides the same boundary the standalone agent enforced.
|
|
@@ -80,7 +80,6 @@ Compare task work against `checkpoint.goal`:
|
|
|
80
80
|
|
|
81
81
|
Review all QA items across all rounds:
|
|
82
82
|
- **Auto items**: Verify all passed (build, lint, types, tests)
|
|
83
|
-
- **User items**: Verify all marked pass/skip
|
|
84
83
|
- **Default items**: Verify all resolved (pass or skipped with reason)
|
|
85
84
|
|
|
86
85
|
**E2E pass vs skipped distinction**: When reading `auto_qa.items[]` for `check: 'e2e'`, do NOT conflate `status: 'pass'` with `status: 'skipped'`. A spec that ran with `passed === 0 && skipped > 0` for any path touching `files_changed` is a hard fail, not a pass — verdict text MUST explicitly call this out: "E2E spec authored but assertions did not execute (skip-gated)." Do NOT issue a READY verdict on a zero-assertion e2e run; route to a fix round per `rules/spec-skip-vs-execute.md`.
|
|
@@ -289,7 +289,7 @@ For each failed test, classify into exactly one category:
|
|
|
289
289
|
| `auth` | Login-page redirect after credential submit, 401 on authenticated request, `invalid_grant`, `email_not_confirmed` | AskUserQuestion as in Step 6.5.3 |
|
|
290
290
|
| `access` | 403, 404 on a route the user should have access to, RLS policy denial text, missing seed data | AskUserQuestion: "Test failed with access error: `{error}`. Seed data or RLS policy may be missing. Options: (1) reply with steps you took to fix, (2) abort." |
|
|
291
291
|
| `flake` | Timeout on first run, passes on immediate retry, network jitter | Retry up to 3 times before reclassifying to `real` |
|
|
292
|
-
| `visual_regression` | `toHaveScreenshot` pixel-diff exceeded threshold | Do NOT retry. Include baseline + actual paths in `screenshots[]` with `baseline_diff_pct`. Do NOT auto-accept baseline — leave for `frontend-ui` (`/cbp-round-execute` Step 5b under `phase: 'screenshot_review'`)
|
|
292
|
+
| `visual_regression` | `toHaveScreenshot` pixel-diff exceeded threshold | Do NOT retry. Include baseline + actual paths in `screenshots[]` with `baseline_diff_pct`. Do NOT auto-accept baseline — leave for `frontend-ui` (`/cbp-round-execute` Step 5b under `phase: 'screenshot_review'`); baseline regressions surface at `/cbp-round-end` Step 7 as a blocking gate. |
|
|
293
293
|
| `real` | Assertion failure on app behavior (text missing, wrong state, navigation broken) | Attempt fix (see Step 8), then report to executor |
|
|
294
294
|
|
|
295
295
|
Failures with `category` of `env`, `auth`, or `access` MUST NOT be counted as test failures in `test_results.failed` until pre-flight passes — they block the run instead.
|
|
@@ -355,7 +355,7 @@ Populate all output contract fields. Include test file paths in `tests_written`,
|
|
|
355
355
|
## Integration
|
|
356
356
|
|
|
357
357
|
- **Spawned by**: `/cbp-round-execute` Step 5 (parallel sibling of `testing-qa-agent`); also invoked by `/cbp-checkpoint-check` (TASK-2 deliverable) with `whole_checkpoint_mode: true`
|
|
358
|
-
- **Parallel sibling**: `cbp-testing-qa-agent` (owns build/lint/types/unit/audit
|
|
358
|
+
- **Parallel sibling**: `cbp-testing-qa-agent` (owns build/lint/types/unit/audit). **Fully independent — no cross-read.** This agent's screenshots are consumed by `/cbp-round-execute` Step 5b (`frontend-ui` skill, `phase: 'screenshot_review'`) which writes `round.context.frontend_ui_review.findings`; baseline-regression findings surface as a BLOCKING gate at `/cbp-round-end` Step 7 (baselines never auto-accepted).
|
|
359
359
|
- **Returns to**: `/cbp-round-execute` which persists output to `round.context.e2e_output`. Step 5b then invokes the `frontend-ui` skill with `phase: 'screenshot_review'` and the screenshots; Step 6 considers `e2e_output.test_results.failed > 0` and `status === 'failed'` as hard-fail signals.
|
|
360
360
|
- **Reads**: `.claude/context/testing/e2e.md`, page/screen source files, existing specs, `.env.local`, `.codebyplan/server.json` `port_allocations`, MCP `get_repos` (for `tech_stack` reconciliation at Step 1.5)
|
|
361
361
|
- **May modify source**: Only to add testID/data-testid attributes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
scope: org-shared
|
|
3
3
|
name: cbp-testing-qa-agent
|
|
4
|
-
description: Combined testing, QA generation, and default checklists. Runs build/lint/types/unit-tests/audit, generates auto
|
|
4
|
+
description: Combined testing, QA generation, and default checklists. Runs build/lint/types/unit-tests/audit, generates auto QA items, applies default production checklists. Does NOT consume e2e screenshots or frontend-ui findings.
|
|
5
5
|
tools: Read, Glob, Grep, Bash, AskUserQuestion
|
|
6
6
|
model: sonnet
|
|
7
7
|
effort: xhigh
|
|
@@ -16,11 +16,11 @@ Combined testing, QA generation, and default production checklists in a single a
|
|
|
16
16
|
Single agent that handles non-e2e quality validation in the per-wave validation phase of `/cbp-round-execute` Step 5:
|
|
17
17
|
- Run all 18 automated checks (work + quality verification)
|
|
18
18
|
- **EXECUTE** automated testing commands (build, lint, types, unit tests, visual checks, audit)
|
|
19
|
-
- Generate auto
|
|
19
|
+
- Generate auto QA items
|
|
20
20
|
- Apply default production checklist items
|
|
21
21
|
- Detect unrelated issues and missing tests
|
|
22
22
|
|
|
23
|
-
E2E execution (Playwright / Maestro / WebDriverIO / XCUITest / vscode-test) is owned by `cbp-test-e2e-agent`, spawned in parallel with this agent by `/cbp-round-execute` Step 5. **The two agents are fully independent — this agent does NOT read `round.context.e2e_output` or `round.context.frontend_ui_review`.**
|
|
23
|
+
E2E execution (Playwright / Maestro / WebDriverIO / XCUITest / vscode-test) is owned by `cbp-test-e2e-agent`, spawned in parallel with this agent by `/cbp-round-execute` Step 5. **The two agents are fully independent — this agent does NOT read `round.context.e2e_output` or `round.context.frontend_ui_review`.** This agent emits auto QA items and default checklist items. Baseline-regression findings surface as a BLOCKING gate at `/cbp-round-end` Step 7 (an explicit accept-or-fix user decision; baselines are NEVER auto-accepted).
|
|
24
24
|
|
|
25
25
|
## Input Contract
|
|
26
26
|
|
|
@@ -63,13 +63,6 @@ output:
|
|
|
63
63
|
stdout: string # captured command output
|
|
64
64
|
stderr: string # captured error output
|
|
65
65
|
screenshots: [{page, viewport, status, file}] # visual check only
|
|
66
|
-
user_qa:
|
|
67
|
-
items:
|
|
68
|
-
- type: 'user'
|
|
69
|
-
check: string
|
|
70
|
-
status: 'pending'
|
|
71
|
-
instructions: string
|
|
72
|
-
round_number: number
|
|
73
66
|
default_checklist:
|
|
74
67
|
items:
|
|
75
68
|
- type: 'default'
|
|
@@ -256,7 +249,7 @@ Report findings in `build_analysis` even if the build succeeded.
|
|
|
256
249
|
|
|
257
250
|
When `files_changed` includes a new route file under any `apps/*/src/app/api/` or `apps/*/src/app/mcp/` directory:
|
|
258
251
|
- If dev server is running: curl the endpoint without credentials, assert response is 401/403 (not 200). Log as auto QA item `auth_enforcement`.
|
|
259
|
-
- If dev server is not running:
|
|
252
|
+
- If dev server is not running: log a skipped auto QA item with the exact curl command noted in `notes` for reference.
|
|
260
253
|
|
|
261
254
|
### Phase 3.58: Missing Unit Tests for New API Routes
|
|
262
255
|
|
|
@@ -331,22 +324,7 @@ This aligns with `immediate-issue-capture.md` (resolve-in-current-scope by defau
|
|
|
331
324
|
|
|
332
325
|
**4a. Auto QA items**: Generate from Phase 3 results. One item per test category. Include stdout/stderr.
|
|
333
326
|
|
|
334
|
-
**4b.
|
|
335
|
-
|
|
336
|
-
**4b.0. Connection smoke test suppression**: Before emitting any connection smoke test user QA item (MCP connection, server health, service wiring), check whether the governing config file is unchanged. Governing config map: MCP (Claude Code) → `.mcp.json`; Dev server → `.env.local`, `.codebyplan/server.json` port_allocations; API integrations → `.env.local`. **Suppression rule**: if the governing config is NOT in `files_changed` AND `git diff HEAD -- <config>` is empty, log `{type:"user", check:"<name>", status:"skipped", notes:"Governing config <file> unchanged in this round; connection behavior is unaffected."}` — do NOT emit a pending user QA item.
|
|
337
|
-
|
|
338
|
-
**4b.1. Design source comparison** (mandatory when `has_ui_work` is true): Search the project's design-sources directory (e.g., `docs/design/`, `docs/development/product/sources/design/`) for PNG files matching the page or component being changed. If design PNGs exist, add a mandatory user QA item with check: "Design source fidelity" and instructions: "Compare rendered output against design source PNG. Verify: column layout matches, control shapes match (flat vs pill vs toggle), background colors match, row structure and dividers match, action controls are in the correct column."
|
|
339
|
-
|
|
340
|
-
**4b.2. Volume-gated mechanical-sweep spot-check** (volume-triggered, runs regardless of `has_ui_work`): when `files_changed.length > 100` AND the round is mechanical (`work_type == 'mechanical'` OR round requirements match `/sweep|auto.?fix|batch|backlog/i`), emit a mandatory user QA item:
|
|
341
|
-
|
|
342
|
-
- `check`: `"High-volume mechanical round spot-check"`
|
|
343
|
-
- `status`: `"pending"`
|
|
344
|
-
- `instructions`: "This round modified {N} files mechanically. Open 3–5 changed files in the running app and verify behavior is unchanged. Prioritize files with business logic (services, hooks, reducers) over pure presentation. Spot-check at least one file from each touched module."
|
|
345
|
-
- `round_number`: current
|
|
346
|
-
|
|
347
|
-
Volume gating exists because automated checks (build/lint/types/unit) verify shape but not behaviour preservation; large mechanical sweeps (auto-fix, codemod, refactor) can pass all gates while silently changing semantics in code paths the test suite doesn't cover.
|
|
348
|
-
|
|
349
|
-
**4c. Default checklist items**: See Phase 5.
|
|
327
|
+
**4b. Default checklist items**: See Phase 5.
|
|
350
328
|
|
|
351
329
|
### Phase 5: Default Production Checklist
|
|
352
330
|
|
|
@@ -379,7 +357,7 @@ Return complete output contract.
|
|
|
379
357
|
- Build output analyzed for warnings/deprecations/console.logs (with client/server classification)
|
|
380
358
|
- npm audit executed, vulnerabilities reported by severity, critical/high contribute to hard_fail
|
|
381
359
|
- Unrelated issues discovered and logged
|
|
382
|
-
- Auto
|
|
360
|
+
- Auto and default QA items generated
|
|
383
361
|
- `hard_fail` flag correctly set
|
|
384
362
|
- **Vitest/Jest/Cargo unit-test hard_fail enforced** when source files changed
|
|
385
363
|
- E2E execution + preflight delegated entirely to `test-e2e-agent` (this agent never runs Playwright/Maestro/wdio/etc.)
|
|
@@ -397,4 +375,4 @@ Return complete output contract.
|
|
|
397
375
|
|
|
398
376
|
- **Spawned by**: `/cbp-round-execute` Step 5 (per-wave; runs in parallel with `test-e2e-agent` and may also run in parallel with next wave's executor)
|
|
399
377
|
- **Parallel sibling**: `cbp-test-e2e-agent` (fully independent — no cross-read; both agents complete on their own timeline using only their own inputs)
|
|
400
|
-
- **Output consumed by**: `/cbp-round-execute` Step 6 (hard-fail routing — this agent's `totals.hard_fail` is OR'd with `e2e_output.test_results.failed > 0` and `e2e_output.status === 'failed'`), `/cbp-round-end` Step
|
|
378
|
+
- **Output consumed by**: `/cbp-round-execute` Step 6 (hard-fail routing — this agent's `totals.hard_fail` is OR'd with `e2e_output.test_results.failed > 0` and `e2e_output.status === 'failed'`), `/cbp-round-end` Step 3 (reads this agent's `auto_qa[]` and `default_checklist[]`). This agent does not emit `user_qa` items; baseline-regression findings surface as a BLOCKING gate at `/cbp-round-end` Step 7 (an explicit accept-or-fix user decision; baselines are NEVER auto-accepted).
|
|
@@ -6,41 +6,33 @@ Hook registration lives in [`hooks/hooks.json`](./hooks.json) — PreToolUse, Po
|
|
|
6
6
|
|
|
7
7
|
**`cbp-statusline.sh` is auto-wired via `settings.project.base.json`.** The `statusLine` block is shipped inside `templates/settings.project.base.json` and merged into the consumer's `.claude/settings.json` automatically by `codebyplan claude install` (and on every `codebyplan claude update`). No manual copy-paste is required.
|
|
8
8
|
|
|
9
|
+
**Multi-runtime renderers.** Both `cbp-statusline.sh` and `cbp-subagent-statusline.sh` are *dispatchers*: each reads a per-device renderer choice and execs a `bash`, `node` (`.mjs`), or `python` (`.py`) variant that produces **byte-identical** output. The selected `.sh` always remains the wired entry point, so the bash renderer is a universal, never-error fallback when the chosen runtime is unavailable. Selection + display options come from two `.codebyplan/` files (see [Statusline configuration](#statusline-configuration) below).
|
|
10
|
+
|
|
9
11
|
---
|
|
10
12
|
|
|
11
13
|
## Hooks included
|
|
12
14
|
|
|
13
15
|
### `cbp-statusline.sh` — auto-registered via `settings.project.base.json`
|
|
14
16
|
|
|
15
|
-
Renders up to 6 structured lines below Claude Code's prompt area. Reads JSON from stdin (Claude Code's `statusLine` stream) and emits ANSI-coloured output.
|
|
17
|
+
Renders up to 6 structured lines below Claude Code's prompt area. Reads JSON from stdin (Claude Code's `statusLine` stream) and emits ANSI-coloured output. The file is a *dispatcher + bash renderer*: it selects the configured runtime (bash / node / python), execs the matching renderer when available, and otherwise renders inline in bash. The `node` (`cbp-statusline.mjs`) and `python` (`cbp-statusline.py`) variants produce **byte-identical** output.
|
|
16
18
|
|
|
17
19
|
**Render lines:**
|
|
18
20
|
|
|
19
|
-
- **Line 1 — Identity**: single prefix (`wt:NAME` / `session:NAME` / `agent:NAME`, priority order); model display name
|
|
21
|
+
- **Line 1 — Identity**: **folder name** (basename of `cwd`) + **git branch** (always shown — taken from `worktree.branch`, else derived via `git -C <cwd> rev-parse --abbrev-ref HEAD`, so it appears even without a registered worktree); single prefix (`wt:NAME` / `session:NAME` / `agent:NAME`, priority order); model **display name only**; `effort:LEVEL` (when set); `thinking:on` (only when thinking is enabled); `style:NAME` (when output style is not "default"); `[VIM_MODE]`
|
|
20
22
|
- **Line 2 — Context**: 20-character progress bar (green / yellow / red at 50% / 75%); `used% / ctx_size`; per-call token breakdown — `in:N out:N cache_cr:N cache_rd:N` (from `context_window.current_usage`); `⚠ 200k+` banner when `exceeds_200k_tokens` is true
|
|
21
23
|
- **Line 3 — Cost**: session cost in USD; total wall duration (`dur:`) and API-only duration (`api:`) via human-readable format; lines added/removed
|
|
22
24
|
- **Line 4 — Rate limits**: `5h: PCT% (resets in Xh)` and `7d: PCT% (resets in Xd)` with relative-time helper; colour-coded green / yellow / red at 60% / 80%; emitted only when at least one window has a non-zero `resets_at` epoch
|
|
23
25
|
- **Line 5 — Repo / PR**: `host/owner/name` (from `workspace.repo`); `PR #N <review_state> <url>` (when `pr.number` is present); emitted when at least one segment is present
|
|
24
26
|
- **Line 6 — Worktree**: `name @ branch (was: original_branch)` and truncated path; emitted only when `worktree.name` is present
|
|
25
27
|
|
|
26
|
-
**
|
|
27
|
-
|
|
28
|
-
| Variable | Suppresses |
|
|
29
|
-
|---|---|
|
|
30
|
-
| `CBP_STATUSLINE_HIDE_IDENTITY` | Line 1 — model / session / agent identity |
|
|
31
|
-
| `CBP_STATUSLINE_HIDE_CONTEXT` | Line 2 — context bar, token breakdown |
|
|
32
|
-
| `CBP_STATUSLINE_HIDE_COST` | Line 3 — cost, duration, lines changed |
|
|
33
|
-
| `CBP_STATUSLINE_HIDE_RATE_LIMITS` | Line 4 — 5h / 7d rate-limit windows |
|
|
34
|
-
| `CBP_STATUSLINE_HIDE_REPO_PR` | Line 5 — repo identity and PR |
|
|
35
|
-
| `CBP_STATUSLINE_HIDE_WORKTREE` | Line 6 — worktree name / branch / path |
|
|
36
|
-
| `CBP_STATUSLINE_NO_COLOR` | Strip all ANSI colour codes (also honoured by `$NO_COLOR`) |
|
|
28
|
+
> **Removed in the multi-runtime rewrite:** the `v:VERSION` field, the `sid:…` session-id field, the `log:…` transcript field, and the redundant model-id-in-parens beside the display name.
|
|
37
29
|
|
|
38
30
|
**Sample invocation** (pipe mock JSON, run from the hooks directory; `NO_COLOR=1` shown so the comment-output below matches verbatim without ANSI codes):
|
|
39
31
|
|
|
40
32
|
```bash
|
|
41
|
-
echo '{"model":{"display_name":"Opus","id":"claude-opus-4"},"context_window":{"used_percentage":25,"context_window_size":200000,"current_usage":{"input_tokens":50000,"output_tokens":1000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}' \
|
|
33
|
+
echo '{"model":{"display_name":"Opus 4.8","id":"claude-opus-4-8[1m]"},"cwd":"/work/myrepo","worktree":{"branch":"feat/x"},"context_window":{"used_percentage":25,"context_window_size":200000,"current_usage":{"input_tokens":50000,"output_tokens":1000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}' \
|
|
42
34
|
| NO_COLOR=1 bash cbp-statusline.sh
|
|
43
|
-
# Opus
|
|
35
|
+
# myrepo ⎇feat/x Opus 4.8
|
|
44
36
|
# ▓▓▓▓▓░░░░░░░░░░░░░░░ 25%/200.0K in:50.0K out:1.0K cache_cr:0 cache_rd:0
|
|
45
37
|
# $0.0000 dur:0s api:0s +0 -0 lines
|
|
46
38
|
```
|
|
@@ -49,7 +41,57 @@ Pure renderer — no project state read, no commands run. Works on any host.
|
|
|
49
41
|
|
|
50
42
|
**Blocks vs warns**: neither — it only renders. Cannot fail your workflow.
|
|
51
43
|
|
|
52
|
-
**Opt out**:
|
|
44
|
+
**Opt out**: hide individual lines via config or `CBP_STATUSLINE_HIDE_*` env vars (see below), or remove the `statusLine` block from your `.claude/settings.json`.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### `cbp-subagent-statusline.sh` — auto-registered via `settings.project.base.json`
|
|
49
|
+
|
|
50
|
+
Renders one row per active subagent task below the main statusline. Reads JSON from stdin (Claude Code's `subagentStatusLine` stream, with a `tasks[]` array) and emits the upstream **JSON-per-line** protocol — one `{"id":…,"content":…}` object per task. Like the main statusline it is a dispatcher with byte-identical `bash` / `node` (`cbp-subagent-statusline.mjs`) / `python` (`cbp-subagent-statusline.py`) renderers. Each row carries a status icon + colour (running `●` / pending `○` / completed `✓` / failed `✗`), name, type, description, `tokens:N`, and `age:X`, ANSI-aware-truncated to the terminal's `columns`.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
echo '{"columns":0,"tasks":[{"id":"a1","name":"Explorer","type":"general","status":"running","description":"scanning files","startTime":0,"tokenCount":12000}]}' \
|
|
54
|
+
| NO_COLOR=1 bash cbp-subagent-statusline.sh
|
|
55
|
+
# {"id":"a1","content":"● Explorer (general) — scanning files · tokens:12.0K"}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### Statusline configuration
|
|
61
|
+
|
|
62
|
+
Both statuslines are driven by two `.codebyplan/` files plus environment-variable overrides. Precedence is **env > config > default**.
|
|
63
|
+
|
|
64
|
+
| File | Scope | Shape | Purpose |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `.codebyplan/statusline.json` | **committed** (team-shared) | `{ "lines": { "identity", "context", "cost", "rate_limits", "repo_pr", "worktree": bool }, "no_color": bool }` | Which of the 6 main lines render, and whether colour is stripped. All lines default `true`; `no_color` defaults `false`. |
|
|
67
|
+
| `.codebyplan/statusline.local.json` | **gitignored** (per-device) | `{ "renderer": "bash" \| "node" \| "python" }` | Which runtime renders. Per-device because runtime availability is machine-specific. Default `bash`. |
|
|
68
|
+
|
|
69
|
+
Set or inspect the renderer with the CLI (writes the gitignored local file):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
codebyplan statusline # print the current renderer + which runtimes are available
|
|
73
|
+
codebyplan statusline node # switch this device to the node renderer (or: bash | python)
|
|
74
|
+
codebyplan statusline --python # flag form is equivalent
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The same `--bash` / `--node` / `--python` flags are accepted on `codebyplan claude install` and `codebyplan claude update`, and `codebyplan setup` asks once when the renderer is unset. When the chosen runtime is missing at render time, the statusline silently falls back to bash (it never errors).
|
|
78
|
+
|
|
79
|
+
**Env-var overrides** — set to `1`; these win over the committed config:
|
|
80
|
+
|
|
81
|
+
| Variable | Effect |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `CBP_STATUSLINE_HIDE_IDENTITY` | Hide line 1 — folder / branch / model / identity |
|
|
84
|
+
| `CBP_STATUSLINE_HIDE_CONTEXT` | Hide line 2 — context bar, token breakdown |
|
|
85
|
+
| `CBP_STATUSLINE_HIDE_COST` | Hide line 3 — cost, duration, lines changed |
|
|
86
|
+
| `CBP_STATUSLINE_HIDE_RATE_LIMITS` | Hide line 4 — 5h / 7d rate-limit windows |
|
|
87
|
+
| `CBP_STATUSLINE_HIDE_REPO_PR` | Hide line 5 — repo identity and PR |
|
|
88
|
+
| `CBP_STATUSLINE_HIDE_WORKTREE` | Hide line 6 — worktree name / branch / path |
|
|
89
|
+
| `CBP_STATUSLINE_NO_COLOR` | Strip all ANSI colour codes (also honoured by `$NO_COLOR`) |
|
|
90
|
+
| `CBP_SUBAGENT_STATUSLINE_HIDE_DESCRIPTION` | Subagent rows: omit the description segment |
|
|
91
|
+
| `CBP_SUBAGENT_STATUSLINE_HIDE_TOKENS` | Subagent rows: omit the `tokens:N` segment |
|
|
92
|
+
| `CBP_SUBAGENT_STATUSLINE_NO_COLOR` | Subagent rows: strip ANSI colour codes |
|
|
93
|
+
|
|
94
|
+
The committed `statusline.json` line toggles apply only to the six main-statusline lines; the subagent rows honour only the colour / description / tokens controls above.
|
|
53
95
|
|
|
54
96
|
---
|
|
55
97
|
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @hook: NOT-A-HOOK (statusLine renderer, invoked via the cbp-statusline.sh dispatcher)
|
|
3
|
+
// Claude Code Status Line — node renderer (ESM; .mjs forces ESM regardless of the
|
|
4
|
+
// host repo's package.json "type", so the script is portable into any consumer).
|
|
5
|
+
// Byte-identical output to the bash renderer in cbp-statusline.sh and the python
|
|
6
|
+
// renderer in cbp-statusline.py. See that .sh file for the full option/env/seam
|
|
7
|
+
// contract. Selected via .codebyplan/statusline.local.json -> {"renderer":"node"}.
|
|
8
|
+
//
|
|
9
|
+
// Reads stdin JSON; resolves .codebyplan/ root from CBP_STATUSLINE_ROOT (set by the
|
|
10
|
+
// dispatcher) or argv[2], else the script's ../.. directory. Never throws to stdout.
|
|
11
|
+
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
function main() {
|
|
20
|
+
const input = fs.readFileSync(0, "utf8");
|
|
21
|
+
let data;
|
|
22
|
+
try {
|
|
23
|
+
data = JSON.parse(input);
|
|
24
|
+
} catch {
|
|
25
|
+
data = {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---- Root resolution (matches the dispatcher contract) --------------------
|
|
29
|
+
let root = process.env.CBP_STATUSLINE_ROOT || process.argv[2] || "";
|
|
30
|
+
if (!root) {
|
|
31
|
+
root = path.resolve(__dirname, "..", "..");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---- jq `//` semantics: default on null/undefined (and false → false) -----
|
|
35
|
+
const get = (obj, keys, dflt) => {
|
|
36
|
+
let cur = obj;
|
|
37
|
+
for (const k of keys) {
|
|
38
|
+
if (cur == null || typeof cur !== "object") return dflt;
|
|
39
|
+
cur = cur[k];
|
|
40
|
+
}
|
|
41
|
+
return cur == null ? dflt : cur;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const MODEL_ID = get(data, ["model", "id"], "");
|
|
45
|
+
const MODEL_NAME = get(data, ["model", "display_name"], "");
|
|
46
|
+
const CWD = get(data, ["cwd"], "");
|
|
47
|
+
const WS_CURRENT_DIR = get(data, ["workspace", "current_dir"], "");
|
|
48
|
+
const WS_REPO_HOST = get(data, ["workspace", "repo", "host"], "");
|
|
49
|
+
const WS_REPO_OWNER = get(data, ["workspace", "repo", "owner"], "");
|
|
50
|
+
const WS_REPO_NAME = get(data, ["workspace", "repo", "name"], "");
|
|
51
|
+
const COST = get(data, ["cost", "total_cost_usd"], 0);
|
|
52
|
+
const DURATION = get(data, ["cost", "total_duration_ms"], 0);
|
|
53
|
+
const API_DURATION = get(data, ["cost", "total_api_duration_ms"], 0);
|
|
54
|
+
const LINES_ADD = get(data, ["cost", "total_lines_added"], 0);
|
|
55
|
+
const LINES_DEL = get(data, ["cost", "total_lines_removed"], 0);
|
|
56
|
+
const CTX_SIZE = get(data, ["context_window", "context_window_size"], 200000);
|
|
57
|
+
const CTX_PCT = get(data, ["context_window", "used_percentage"], 0);
|
|
58
|
+
const CUR_IN = get(
|
|
59
|
+
data,
|
|
60
|
+
["context_window", "current_usage", "input_tokens"],
|
|
61
|
+
0
|
|
62
|
+
);
|
|
63
|
+
const CUR_OUT = get(
|
|
64
|
+
data,
|
|
65
|
+
["context_window", "current_usage", "output_tokens"],
|
|
66
|
+
0
|
|
67
|
+
);
|
|
68
|
+
const CACHE_CREATE = get(
|
|
69
|
+
data,
|
|
70
|
+
["context_window", "current_usage", "cache_creation_input_tokens"],
|
|
71
|
+
0
|
|
72
|
+
);
|
|
73
|
+
const CACHE_READ = get(
|
|
74
|
+
data,
|
|
75
|
+
["context_window", "current_usage", "cache_read_input_tokens"],
|
|
76
|
+
0
|
|
77
|
+
);
|
|
78
|
+
const EXCEEDS_200K = get(data, ["exceeds_200k_tokens"], false);
|
|
79
|
+
const EFFORT = get(data, ["effort", "level"], "");
|
|
80
|
+
const THINKING = get(data, ["thinking", "enabled"], false);
|
|
81
|
+
const RATE_5H_PCT = get(
|
|
82
|
+
data,
|
|
83
|
+
["rate_limits", "five_hour", "used_percentage"],
|
|
84
|
+
""
|
|
85
|
+
);
|
|
86
|
+
const RATE_5H_RESETS = get(
|
|
87
|
+
data,
|
|
88
|
+
["rate_limits", "five_hour", "resets_at"],
|
|
89
|
+
0
|
|
90
|
+
);
|
|
91
|
+
const RATE_7D_PCT = get(
|
|
92
|
+
data,
|
|
93
|
+
["rate_limits", "seven_day", "used_percentage"],
|
|
94
|
+
""
|
|
95
|
+
);
|
|
96
|
+
const RATE_7D_RESETS = get(
|
|
97
|
+
data,
|
|
98
|
+
["rate_limits", "seven_day", "resets_at"],
|
|
99
|
+
0
|
|
100
|
+
);
|
|
101
|
+
const SESSION_NAME = get(data, ["session_name"], "");
|
|
102
|
+
const OUTPUT_STYLE = get(data, ["output_style", "name"], "");
|
|
103
|
+
const VIM_MODE = get(data, ["vim", "mode"], "");
|
|
104
|
+
const AGENT_NAME = get(data, ["agent", "name"], "");
|
|
105
|
+
const PR_NUMBER = get(data, ["pr", "number"], "");
|
|
106
|
+
const PR_URL = get(data, ["pr", "url"], "");
|
|
107
|
+
const PR_REVIEW_STATE = get(data, ["pr", "review_state"], "");
|
|
108
|
+
const WT_NAME = get(data, ["worktree", "name"], "");
|
|
109
|
+
const WT_PATH = get(data, ["worktree", "path"], "");
|
|
110
|
+
const WT_BRANCH = get(data, ["worktree", "branch"], "");
|
|
111
|
+
const WT_ORIG_BRANCH = get(data, ["worktree", "original_branch"], "");
|
|
112
|
+
|
|
113
|
+
// ---- Config: line toggles + no_color --------------------------------------
|
|
114
|
+
const cfg = {
|
|
115
|
+
identity: true,
|
|
116
|
+
context: true,
|
|
117
|
+
cost: true,
|
|
118
|
+
rate_limits: true,
|
|
119
|
+
repo_pr: true,
|
|
120
|
+
worktree: true,
|
|
121
|
+
no_color: false,
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
const raw = fs.readFileSync(
|
|
125
|
+
path.join(root, ".codebyplan", "statusline.json"),
|
|
126
|
+
"utf8"
|
|
127
|
+
);
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
if (parsed && typeof parsed === "object") {
|
|
130
|
+
if (typeof parsed.no_color === "boolean") cfg.no_color = parsed.no_color;
|
|
131
|
+
if (parsed.lines && typeof parsed.lines === "object") {
|
|
132
|
+
for (const k of [
|
|
133
|
+
"identity",
|
|
134
|
+
"context",
|
|
135
|
+
"cost",
|
|
136
|
+
"rate_limits",
|
|
137
|
+
"repo_pr",
|
|
138
|
+
"worktree",
|
|
139
|
+
]) {
|
|
140
|
+
if (typeof parsed.lines[k] === "boolean") cfg[k] = parsed.lines[k];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// absent / invalid → keep defaults
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// env HIDE=1 > config false > default show
|
|
149
|
+
const shouldShow = (envSuffix, cfgValue) => {
|
|
150
|
+
if (process.env[`CBP_STATUSLINE_HIDE_${envSuffix}`] === "1") return false;
|
|
151
|
+
if (cfgValue === false) return false;
|
|
152
|
+
return true;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ---- Colour setup (env > config) ------------------------------------------
|
|
156
|
+
const noColor =
|
|
157
|
+
(process.env.NO_COLOR != null && process.env.NO_COLOR !== "") ||
|
|
158
|
+
process.env.CBP_STATUSLINE_NO_COLOR === "1" ||
|
|
159
|
+
cfg.no_color === true;
|
|
160
|
+
const C = noColor
|
|
161
|
+
? {
|
|
162
|
+
RST: "",
|
|
163
|
+
DIM: "",
|
|
164
|
+
BOLD: "",
|
|
165
|
+
GREEN: "",
|
|
166
|
+
YELLOW: "",
|
|
167
|
+
RED: "",
|
|
168
|
+
CYAN: "",
|
|
169
|
+
MAGENTA: "",
|
|
170
|
+
BLUE: "",
|
|
171
|
+
}
|
|
172
|
+
: {
|
|
173
|
+
RST: "\x1b[0m",
|
|
174
|
+
DIM: "\x1b[2m",
|
|
175
|
+
BOLD: "\x1b[1m",
|
|
176
|
+
GREEN: "\x1b[32m",
|
|
177
|
+
YELLOW: "\x1b[33m",
|
|
178
|
+
RED: "\x1b[31m",
|
|
179
|
+
CYAN: "\x1b[36m",
|
|
180
|
+
MAGENTA: "\x1b[35m",
|
|
181
|
+
BLUE: "\x1b[34m",
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// ---- Numeric helpers (integer round-half-up; cross-runtime identical) ------
|
|
185
|
+
const numStr = (n) => {
|
|
186
|
+
const x = Number(n);
|
|
187
|
+
if (Number.isFinite(x) && Number.isInteger(x)) return String(x);
|
|
188
|
+
return String(n);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const fmtK = (val) => {
|
|
192
|
+
const v = Number(val);
|
|
193
|
+
if (v >= 1000000) {
|
|
194
|
+
const t = Math.floor((v + 50000) / 100000);
|
|
195
|
+
return `${Math.floor(t / 10)}.${t % 10}M`;
|
|
196
|
+
} else if (v >= 1000) {
|
|
197
|
+
const t = Math.floor((v + 50) / 100);
|
|
198
|
+
return `${Math.floor(t / 10)}.${t % 10}K`;
|
|
199
|
+
}
|
|
200
|
+
return String(Math.trunc(v));
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const fmtCost = (c) => {
|
|
204
|
+
const n = Math.floor(Number(c) * 10000 + 0.5);
|
|
205
|
+
const frac = String(n % 10000).padStart(4, "0");
|
|
206
|
+
return `$${Math.floor(n / 10000)}.${frac}`;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const fmtDur = (ms) => {
|
|
210
|
+
const secs = Math.trunc(Number(ms) / 1000);
|
|
211
|
+
if (secs >= 3600)
|
|
212
|
+
return `${Math.floor(secs / 3600)}h${Math.floor((secs % 3600) / 60)}m`;
|
|
213
|
+
if (secs >= 60) return `${Math.floor(secs / 60)}m${secs % 60}s`;
|
|
214
|
+
return `${secs}s`;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const cbpNow = () => {
|
|
218
|
+
if (
|
|
219
|
+
process.env.CBP_STATUSLINE_NOW != null &&
|
|
220
|
+
process.env.CBP_STATUSLINE_NOW !== ""
|
|
221
|
+
) {
|
|
222
|
+
return Math.trunc(Number(process.env.CBP_STATUSLINE_NOW));
|
|
223
|
+
}
|
|
224
|
+
return Math.floor(Date.now() / 1000);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const fmtRelTime = (epoch) => {
|
|
228
|
+
const delta = Math.trunc(Number(epoch)) - cbpNow();
|
|
229
|
+
if (delta <= 0) return "now";
|
|
230
|
+
if (delta >= 86400) return `${Math.floor(delta / 86400)}d`;
|
|
231
|
+
if (delta >= 3600) return `${Math.floor(delta / 3600)}h`;
|
|
232
|
+
return `${Math.floor(delta / 60)}m`;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const gte = (v, t) => Number(v) >= t;
|
|
236
|
+
|
|
237
|
+
// ---- Folder + branch ------------------------------------------------------
|
|
238
|
+
let FOLDER = "";
|
|
239
|
+
if (CWD) FOLDER = path.basename(CWD);
|
|
240
|
+
else if (WS_CURRENT_DIR) FOLDER = path.basename(WS_CURRENT_DIR);
|
|
241
|
+
let BRANCH = WT_BRANCH;
|
|
242
|
+
if (!BRANCH && CWD) {
|
|
243
|
+
try {
|
|
244
|
+
BRANCH = execFileSync(
|
|
245
|
+
"git",
|
|
246
|
+
["-C", CWD, "rev-parse", "--abbrev-ref", "HEAD"],
|
|
247
|
+
{
|
|
248
|
+
encoding: "utf8",
|
|
249
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
250
|
+
}
|
|
251
|
+
).replace(/\s+$/, "");
|
|
252
|
+
} catch {
|
|
253
|
+
BRANCH = "";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const out = [];
|
|
258
|
+
|
|
259
|
+
// ============================================================
|
|
260
|
+
// LINE 1 — Identity
|
|
261
|
+
// ============================================================
|
|
262
|
+
if (shouldShow("IDENTITY", cfg.identity)) {
|
|
263
|
+
let L1 = "";
|
|
264
|
+
if (FOLDER) {
|
|
265
|
+
L1 = `${C.BOLD}${C.BLUE}${FOLDER}${C.RST}`;
|
|
266
|
+
if (BRANCH) L1 += ` ${C.DIM}⎇${C.RST}${C.CYAN}${BRANCH}${C.RST}`;
|
|
267
|
+
L1 += " ";
|
|
268
|
+
}
|
|
269
|
+
if (WT_NAME) {
|
|
270
|
+
L1 += `${C.DIM}wt:${C.RST}${C.MAGENTA}${WT_NAME}${C.RST} `;
|
|
271
|
+
} else if (SESSION_NAME) {
|
|
272
|
+
L1 += `${C.DIM}session:${C.RST}${C.MAGENTA}${SESSION_NAME}${C.RST} `;
|
|
273
|
+
} else if (AGENT_NAME) {
|
|
274
|
+
L1 += `${C.DIM}agent:${C.RST}${C.MAGENTA}${AGENT_NAME}${C.RST} `;
|
|
275
|
+
}
|
|
276
|
+
if (MODEL_NAME) {
|
|
277
|
+
L1 += `${C.BOLD}${C.CYAN}${MODEL_NAME}${C.RST}`;
|
|
278
|
+
} else if (MODEL_ID) {
|
|
279
|
+
L1 += `${C.BOLD}${C.CYAN}${MODEL_ID}${C.RST}`;
|
|
280
|
+
}
|
|
281
|
+
if (EFFORT) L1 += ` ${C.DIM}effort:${C.RST}${EFFORT}`;
|
|
282
|
+
if (THINKING === true) L1 += ` ${C.YELLOW}thinking:on${C.RST}`;
|
|
283
|
+
if (OUTPUT_STYLE && OUTPUT_STYLE !== "default")
|
|
284
|
+
L1 += ` ${C.DIM}style:${C.RST}${OUTPUT_STYLE}`;
|
|
285
|
+
if (VIM_MODE) L1 += ` ${C.DIM}[${VIM_MODE}]${C.RST}`;
|
|
286
|
+
if (L1) out.push(L1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ============================================================
|
|
290
|
+
// LINE 2 — Context window
|
|
291
|
+
// ============================================================
|
|
292
|
+
if (shouldShow("CONTEXT", cfg.context)) {
|
|
293
|
+
let barColor;
|
|
294
|
+
if (gte(CTX_PCT, 75)) barColor = C.RED;
|
|
295
|
+
else if (gte(CTX_PCT, 50)) barColor = C.YELLOW;
|
|
296
|
+
else barColor = C.GREEN;
|
|
297
|
+
|
|
298
|
+
let filled = Math.floor((Math.trunc(Number(CTX_PCT)) + 4) / 5);
|
|
299
|
+
if (filled > 20) filled = 20;
|
|
300
|
+
const empty = 20 - filled;
|
|
301
|
+
const bar = "▓".repeat(filled) + "░".repeat(empty);
|
|
302
|
+
|
|
303
|
+
let L2 = `${barColor}${bar}${C.RST} ${barColor}${numStr(CTX_PCT)}%${C.RST}${C.DIM}/${fmtK(CTX_SIZE)}${C.RST}`;
|
|
304
|
+
L2 += ` ${C.DIM}in:${C.RST}${C.BLUE}${fmtK(CUR_IN)}${C.RST} ${C.DIM}out:${C.RST}${C.MAGENTA}${fmtK(CUR_OUT)}${C.RST} ${C.DIM}cache_cr:${C.RST}${fmtK(CACHE_CREATE)} ${C.DIM}cache_rd:${C.RST}${fmtK(CACHE_READ)}`;
|
|
305
|
+
if (EXCEEDS_200K === true) L2 += ` ${C.YELLOW}⚠ 200k+${C.RST}`;
|
|
306
|
+
out.push(L2);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================================
|
|
310
|
+
// LINE 3 — Cost
|
|
311
|
+
// ============================================================
|
|
312
|
+
if (shouldShow("COST", cfg.cost)) {
|
|
313
|
+
const L3 = `${C.GREEN}${fmtCost(COST)}${C.RST} ${C.DIM}dur:${C.RST}${fmtDur(DURATION)} ${C.DIM}api:${C.RST}${fmtDur(API_DURATION)} ${C.GREEN}+${numStr(LINES_ADD)}${C.RST} ${C.RED}-${numStr(LINES_DEL)}${C.RST} ${C.DIM}lines${C.RST}`;
|
|
314
|
+
out.push(L3);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ============================================================
|
|
318
|
+
// LINE 4 — Rate limits
|
|
319
|
+
// ============================================================
|
|
320
|
+
if (shouldShow("RATE_LIMITS", cfg.rate_limits)) {
|
|
321
|
+
const has5h = RATE_5H_PCT !== "" && String(RATE_5H_RESETS) !== "0";
|
|
322
|
+
const has7d = RATE_7D_PCT !== "" && String(RATE_7D_RESETS) !== "0";
|
|
323
|
+
if (has5h || has7d) {
|
|
324
|
+
let L4 = "";
|
|
325
|
+
if (has5h) {
|
|
326
|
+
let c5;
|
|
327
|
+
if (gte(RATE_5H_PCT, 80)) c5 = C.RED;
|
|
328
|
+
else if (gte(RATE_5H_PCT, 60)) c5 = C.YELLOW;
|
|
329
|
+
else c5 = C.GREEN;
|
|
330
|
+
L4 = `${C.DIM}5h:${C.RST}${c5}${numStr(RATE_5H_PCT)}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_5H_RESETS)})${C.RST}`;
|
|
331
|
+
}
|
|
332
|
+
if (has7d) {
|
|
333
|
+
let c7;
|
|
334
|
+
if (gte(RATE_7D_PCT, 80)) c7 = C.RED;
|
|
335
|
+
else if (gte(RATE_7D_PCT, 60)) c7 = C.YELLOW;
|
|
336
|
+
else c7 = C.GREEN;
|
|
337
|
+
const seg7 = `${C.DIM}7d:${C.RST}${c7}${numStr(RATE_7D_PCT)}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_7D_RESETS)})${C.RST}`;
|
|
338
|
+
L4 = L4 ? `${L4} ${C.DIM}|${C.RST} ${seg7}` : seg7;
|
|
339
|
+
}
|
|
340
|
+
out.push(L4);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================================
|
|
345
|
+
// LINE 5 — Repo / PR
|
|
346
|
+
// ============================================================
|
|
347
|
+
if (shouldShow("REPO_PR", cfg.repo_pr)) {
|
|
348
|
+
let L5 = "";
|
|
349
|
+
if (WS_REPO_HOST) {
|
|
350
|
+
L5 = `${C.DIM}${WS_REPO_HOST}/${WS_REPO_OWNER}/${WS_REPO_NAME}${C.RST}`;
|
|
351
|
+
}
|
|
352
|
+
if (PR_NUMBER !== "") {
|
|
353
|
+
let prSeg = `${C.DIM}PR${C.RST} ${C.CYAN}#${numStr(PR_NUMBER)}${C.RST}`;
|
|
354
|
+
if (PR_REVIEW_STATE) prSeg += ` ${C.DIM}${PR_REVIEW_STATE}${C.RST}`;
|
|
355
|
+
if (PR_URL) prSeg += ` ${C.BLUE}${PR_URL}${C.RST}`;
|
|
356
|
+
L5 = L5 ? `${L5} ${C.DIM}|${C.RST} ${prSeg}` : prSeg;
|
|
357
|
+
}
|
|
358
|
+
if (L5) out.push(L5);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============================================================
|
|
362
|
+
// LINE 6 — Worktree
|
|
363
|
+
// ============================================================
|
|
364
|
+
if (shouldShow("WORKTREE", cfg.worktree)) {
|
|
365
|
+
if (WT_NAME) {
|
|
366
|
+
let L6 = `${C.MAGENTA}${WT_NAME}${C.RST} ${C.DIM}@${C.RST} ${C.CYAN}${WT_BRANCH}${C.RST}`;
|
|
367
|
+
if (WT_ORIG_BRANCH && WT_ORIG_BRANCH !== WT_BRANCH) {
|
|
368
|
+
L6 += ` ${C.DIM}(was: ${WT_ORIG_BRANCH})${C.RST}`;
|
|
369
|
+
}
|
|
370
|
+
let wtPathDisp = WT_PATH;
|
|
371
|
+
if (WT_PATH.length > 60) wtPathDisp = "..." + WT_PATH.slice(-57);
|
|
372
|
+
L6 += ` ${C.DIM}${wtPathDisp}${C.RST}`;
|
|
373
|
+
out.push(L6);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
process.stdout.write(out.length ? out.join("\n") + "\n" : "");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
main();
|
|
382
|
+
} catch {
|
|
383
|
+
// Statusline must never error to stdout.
|
|
384
|
+
process.exit(0);
|
|
385
|
+
}
|