gh-glance 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGELOG.md +91 -1
  2. package/README.md +215 -116
  3. package/index.mjs +4033 -546
  4. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -7,6 +7,93 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.0] - 2026-08-19
11
+
12
+ ### Fixed
13
+
14
+ - **Concurrent panes preserve a hard reserve instead of slowing all the way to
15
+ exhaustion.** Local panes on one GitHub host and account now share atomic
16
+ grants, worst-case reservations, and one budget probe. gh-glance starts no
17
+ request when its latest fresh, conservatively debited REST or GraphQL sample
18
+ cannot pay for it outside the final 20% of that resource. Missing, corrupt,
19
+ locked, or unwritable coordination fails closed. REST exhaustion does not
20
+ stop healthy GraphQL tabs, and manual refresh cannot bypass the hold.
21
+
22
+ ### Changed
23
+
24
+ - **Startup, reset, polling, and status now reflect the shared safe schedule.**
25
+ Stable pane phases spread startup and post-reset work, active checks precede
26
+ one rotating background check, and the old 60-second pacing ceiling is gone.
27
+ The active footer says Watching, Checking, Waiting, Paused, Failed, or Limited.
28
+ Startup and manual Checking animate while admitted; adapted automatic checks
29
+ and non-working states stay static. `next HH:MM` names one current grant, not
30
+ a recurring interval.
31
+
32
+ ## [0.9.1] - 2026-08-11
33
+
34
+ ### Fixed
35
+
36
+ - **Package-boundary verification supports npm 12.** The release test accepts
37
+ both npm 11's array-shaped `npm pack --json` result and npm 12's
38
+ package-name-keyed result, so the existing OIDC publication workflow can
39
+ verify the CLI-only package boundary before publishing.
40
+
41
+ ## [0.9.0] - 2026-08-11
42
+
43
+ ### Added
44
+
45
+ - **Security keeps a bounded priority view when newest pages fill.** Dependabot
46
+ and code-scanning sources add critical/high lanes only when their newest 100
47
+ open alerts fill the base page, then merge and de-duplicate the result. A `+`
48
+ marker stays explicit when any lane fills; the app never claims an unbounded
49
+ repository is complete.
50
+ - **Adaptive polling now protects both GitHub budgets.** REST and GraphQL spend
51
+ are measured separately, including failed calls, and the safer interval wins
52
+ without lowering the configured refresh floor. `--doctor` reports the same
53
+ conservative model.
54
+ - **Terminal state has explicit non-colour and linear-rendering channels.** A
55
+ stable ASCII `x` marks fetch failure, selected rows include `selected` in
56
+ their accessibility label, narrow errors begin with the recovery action, and
57
+ short help always retains Quit, Refresh, and the full-help command.
58
+ - **PTY child-process coverage is available as an informational signal.**
59
+ `npm run test:coverage:runtime` summarizes observed `index.mjs` functions on
60
+ scheduled/manual coverage runs without adding a threshold or release gate.
61
+
62
+ ### Changed
63
+
64
+ - **Dashboard recovery state is account-scoped and safe across panes.** Cache
65
+ version 2 includes `GH_CONFIG_DIR`/`hosts.yml` identity and one-way digests of
66
+ supported token variables, rejects the old unscoped cache, and uses bounded
67
+ locked three-way merges so simultaneous panes preserve unrelated targets,
68
+ tabs, and width choices. Successful quiet polls checkpoint freshness on a
69
+ bounded cadence.
70
+ - **Settled automatic polling is visually quiet.** `Fetching` is reserved for
71
+ startup/manual work, unchanged Security observations slow to a 60-second
72
+ cadence, and cache-hydrated tabs clear their initial loading state.
73
+ - **Mouse reporting is opt-in through Width mode.** Press `w` before dragging a
74
+ header divider; outside that mode the terminal keeps ordinary mouse behavior.
75
+ - **The npm package is explicitly CLI-only.** The executable remains supported,
76
+ while package-root and deep JavaScript imports now fail through `exports: {}`.
77
+
78
+ ### Fixed
79
+
80
+ - **Manual Security refresh now clears every source backoff after the in-flight
81
+ guard admits the request.** Re-authenticating and pressing `r` cannot leave a
82
+ previously failed source silently asleep.
83
+ - **Concurrent persistence no longer loses a later update.** Same-target tabs
84
+ merge independently, and both cache and width writers adopt the merged disk
85
+ snapshot before their next save.
86
+ - **Selections stay attached to visible items as lists change.** Insertions
87
+ above a row reconcile the viewport without opening a different item; a missing
88
+ item clears selection instead of selecting a replacement.
89
+ - **Quitting aborts in-flight `gh ... view --web` children.** The app no longer
90
+ waits for a stalled browser-open command after terminal teardown begins.
91
+ - **Row failures reset only when row content changes.** Unchanged parent renders
92
+ no longer clear an error boundary, while corrected data can recover in place.
93
+ - **Status hints now describe only controls that can act.** Empty lists do not
94
+ advertise movement or opening, compact layouts do not advertise width mode,
95
+ and Security does not advertise an unsupported open action.
96
+
10
97
  ## [0.8.0] - 2026-08-11
11
98
 
12
99
  ### Added
@@ -647,7 +734,10 @@ engineering, security, QA and UX. What follows is what changed as a result.
647
734
  - The `main` field from `package.json`. It advertised the file as importable,
648
735
  but importing it took over the terminal or exited the host process.
649
736
 
650
- [Unreleased]: https://github.com/juan294/gh-glance/compare/v0.8.0...HEAD
737
+ [Unreleased]: https://github.com/juan294/gh-glance/compare/v0.10.0...HEAD
738
+ [0.10.0]: https://github.com/juan294/gh-glance/compare/v0.9.1...v0.10.0
739
+ [0.9.1]: https://github.com/juan294/gh-glance/compare/v0.9.0...v0.9.1
740
+ [0.9.0]: https://github.com/juan294/gh-glance/compare/v0.8.0...v0.9.0
651
741
  [0.8.0]: https://github.com/juan294/gh-glance/compare/v0.7.0...v0.8.0
652
742
  [0.7.0]: https://github.com/juan294/gh-glance/compare/v0.6.1...v0.7.0
653
743
  [0.6.1]: https://github.com/juan294/gh-glance/releases/tag/v0.6.1
package/README.md CHANGED
@@ -11,38 +11,43 @@
11
11
 
12
12
  A live-refreshing GitHub dashboard that fits in a narrow terminal pane --
13
13
  **Actions, Issues, Pull Requests, and Security alerts**, one tab at a time,
14
- refreshing every 5 seconds with no flicker.
14
+ with a healthy single-pane refresh floor of 5 seconds and no flicker. Shared
15
+ budget coordination can schedule a later safe check when several panes or other
16
+ tools use the same GitHub account.
15
17
 
16
18
  Built for pairing with a terminal multiplexer/workspace tool (e.g. a sidebar
17
19
  pane next to your editor) where you want an always-on glance at repo activity
18
20
  without switching to the browser.
19
21
 
20
22
  ```
21
- [1:Actions (4)] 2:Issues (3) 3:PRs (2) 4:Security (0)
22
- ────────────────────────────────────────────────────────────────────────────
23
- ╭─ Actions ────────────────────────────────────────────────────────────────╮
24
- │ TITLE │WORKFLOW │BRANCH │TIME │UPDATED │
25
- │ ──────────────────────────────────────────────────────────────────────── │
26
- │ >+ ci: pin actions to comm… #443 CI develop 1m20s 9d ago │
27
- │ x fix: restore the primar… #442 Code… develop 1m28s 9d ago │
28
- │ ! chore: bump dependencies #441 CI dependa…int-10 222h8m 9d ago │
29
- │ - docs: update the readme #440 CI develop 15s 10d ago │
30
- │ │
31
- │ │
32
- ╰───────────────────────────────────────────────────────────────── 4 of 4 ─╯
33
- ⣾ Fetching Move: ↑↓ | Open: Ent | Refresh: r | Width: w | Quit: q 0.8.0
23
+ [1:Actions (4)] 2:Issues 3:PRs 4:Security
24
+ ───────────────────────────────────────────────────────────────────────────
25
+ ╭─ Actions · owner/repo ──────────────────────────────────────────────────╮
26
+ │ TITLE │WORKFLOW │BRANCH │TIME │UPDATED │
27
+ │ ─────────────────────────────────────────────────────────────────────── │
28
+ │ >+ ci: pin actions to com… #443 CI develop 1m20s 17d ago │
29
+ │ x fix: restore the prima… #442 Code… develop 1m28s 17d ago │
30
+ │ + chore: bump dependenci… #441 CI dependa…int-10 30s 18d ago │
31
+ │ - docs: update the readme #440 CI develop 15s 18d ago │
32
+ │ │
33
+ │ │
34
+ ╰──────────────────────────────────────────────────────────────── 4 of 4 ─╯
35
+ · Watching Refresh: r Quit: q Move: ↑↓ Open: Ent Width: w 0.10.0
34
36
  ```
35
37
 
36
- > Captured from a real run at 76 columns with `GH_GLANCE_ICONS=unicode`, so the
37
- > status icons render in a browser. With a Nerd Font (the default) they are
38
- > Octicon glyphs instead.
38
+ > Generated from this candidate's real binary under the repository PTY harness
39
+ > at 76 columns, using its deterministic fixture data and
40
+ > `GH_GLANCE_ICONS=unicode`; the repeatable command is in
41
+ > [CONTRIBUTING.md](./CONTRIBUTING.md). With a Nerd Font (the default), row
42
+ > icons are Octicon glyphs instead.
39
43
  >
40
- > The `>` on the first row is the cursor. Three more markers appear when they
44
+ > The `>` on the first row is the cursor. Four more markers appear when they
41
45
  > apply: a `+` after a tab's count means the list was truncated by the fetch
42
46
  > limit, a `!` after the Actions count means the newest run failed — so "is CI
43
- > red" is answerable without switching to that tab — and a `?` in place of the
44
- > Security count means its endpoints could not be read, which is not the same
45
- > as there being nothing to report.
47
+ > red" is answerable without switching to that tab — an `x` means the tab's
48
+ > latest fetch failed, and a `?` in place of the Security count means its
49
+ > endpoints could not be read, which is not the same as there being nothing to
50
+ > report.
46
51
 
47
52
  <!-- contract:allow-emoji -- the check/cross above stand in for Nerd Font
48
53
  Octicons the app actually draws; they are literal examples, not decoration. -->
@@ -64,23 +69,27 @@ current.
64
69
  - **Issues** -- open issues: title, author, first label, age
65
70
  - **Pull Requests** -- open PRs: title, author, branch, review status
66
71
  (approved / changes requested / pending), age
67
- - **Security** -- open Dependabot alerts: package, summary, age, sorted with the
68
- most severe first and with severity spelled out in its own column, not carried
69
- by colour alone. One page of 100 newest alerts is fetched and ranked, so on a
70
- repository with more open alerts than that the ranking covers the newest 100
71
- rather than everything -- the `+` marker on the count is what tells you.
72
- Code scanning and secret scanning alerts are included too, on repos/plans
73
- that have GitHub Advanced Security enabled -- see [Limitations](#limitations).
72
+ - **Security** -- open Dependabot, code-scanning, and secret-scanning alerts,
73
+ sorted with the most severe first and with severity spelled out rather than
74
+ carried by colour alone. Each source starts with its newest 100 open alerts.
75
+ When that page is full, Dependabot and code scanning also fetch bounded
76
+ critical/high priority lanes before merging and de-duplicating the result.
77
+ The `+` marker means at least one bounded lane filled, so the visible priority
78
+ set may still be incomplete. Code and secret scanning require GitHub Advanced
79
+ Security -- see [Limitations](#limitations).
74
80
  - Tab bar with live counts, pinned to the top of the pane above a divider,
75
81
  switchable via `1`-`4`, arrow keys, or `Tab`/`Shift+Tab`
76
82
  - Adjustable full-table columns, with visible header grips, a keyboard width
77
83
  mode, and preferences kept separately for each tab
78
84
  - `lazygit`-style panel frame: the tab name sits in the top border, the
79
85
  visible-of-total row count in the bottom
80
- - A `Fetching` indicator that brightens on every refresh and animates during the
81
- first load or while a workflow run is executing, so the pane says when it's
82
- working without spending a line on it
83
- - Status icons are real GitHub Octicons (via the Nerd Font glyph set), not emoji
86
+ - A semantic footer for the active tab: **Watching** is settled; **Checking**
87
+ means admitted work is live; **Waiting** means a probe or future grant;
88
+ **Paused** protects a held or unavailable budget; **Failed** names a normal
89
+ fetch error; and **Limited** means Security visibility is incomplete. Startup
90
+ and manual Checking can animate. Adapted automatic checks, Waiting, Paused,
91
+ Failed, Limited, and Watching are static
92
+ - Row state icons are real GitHub Octicons (via the Nerd Font glyph set), not emoji
84
93
  -- with a plain-ASCII fallback for terminals without one
85
94
  - Readable without colour: severity has its own column, a failing newest run puts
86
95
  a `!` on the Actions tab, and the active tab is bracketed, so `NO_COLOR=1` and
@@ -106,7 +115,7 @@ current.
106
115
  a GitHub Enterprise or EMU tenant -- see [GitHub Enterprise and
107
116
  EMU](#github-enterprise-and-emu))
108
117
  - A terminal font with [Nerd Font](https://www.nerdfonts.com/) glyphs (for the
109
- status icons to render correctly -- without one, they'll show as blank
118
+ row icons to render correctly -- without one, they'll show as blank
110
119
  boxes; see [Icons without a Nerd Font](#icons-without-a-nerd-font))
111
120
 
112
121
  ## Install
@@ -119,6 +128,10 @@ That is the whole install. `gh-glance` ships as a single `index.mjs` with no
119
128
  build step, so there is nothing to compile and nothing to configure -- the
120
129
  package is the source you can read in this repository.
121
130
 
131
+ The installed package is a CLI, not a JavaScript library. The `gh-glance`
132
+ executable is supported; package-root and deep imports are intentionally
133
+ blocked so an internal test seam cannot become an accidental public API.
134
+
122
135
  Every published version is built and signed by GitHub Actions from a tagged
123
136
  commit on `main`, never from a maintainer's laptop, and carries [npm
124
137
  provenance](https://docs.npmjs.com/generating-provenance-statements) linking
@@ -234,7 +247,7 @@ stdout isn't a terminal rather than streaming redraw frames into a pipe.
234
247
  | `↑` / `↓` or `j` / `k` | Move the cursor between rows |
235
248
  | `PgUp` / `PgDn` | Move a page at a time |
236
249
  | `Enter` | Open the selected item, or accept an onboarding prompt |
237
- | `r` | Refresh the current tab now |
250
+ | `r` | Request a current-tab refresh; a safe grant is still required. |
238
251
  | `w` | Adjust table column widths |
239
252
  | `?` | Show the keys without leaving the dashboard (any key closes it) |
240
253
  | `q` / `Esc` / `Ctrl+C` | Quit (`Esc` leaves width mode) |
@@ -263,14 +276,15 @@ line such as:
263
276
  Width: BRANCH 14 Tab select <- -> resize r reset Esc done
264
277
  ```
265
278
 
266
- In an interactive terminal, you can also left-drag a visible `│` header grip.
267
- Pressing a grip selects its column and enters width mode; releasing the button,
268
- including outside the header, ends the drag. Row clicking, hover behavior, and
269
- pointer-shape changes are not supported. Mouse resizing has the same interactive
270
- terminal requirement as keyboard input.
279
+ After pressing `w`, you can also left-drag a visible `│` header grip. Mouse
280
+ reporting is enabled only while width mode owns the input, so a click outside
281
+ that mode does not enter it. Releasing the button, including outside the header,
282
+ ends the drag. Row clicking, hover behavior, and pointer-shape changes are not
283
+ supported.
271
284
 
272
- While gh-glance is running, terminal-native text selection may require your
273
- terminal's mouse-reporting bypass modifier, commonly `Shift`.
285
+ While width mode is active, terminal-native text selection may require your
286
+ terminal's mouse-reporting bypass modifier, commonly `Shift`. Outside width
287
+ mode, gh-glance leaves mouse reporting disabled.
274
288
 
275
289
  The cursor tracks the *item*, not the row position, so it stays on what you
276
290
  selected as new rows arrive above it. `Enter` works on Actions, Issues and Pull
@@ -303,7 +317,7 @@ pane definition. Flags are there when you want them:
303
317
  | Flag | Effect |
304
318
  |---|---|
305
319
  | `-R`, `--repo [host/]owner/name` | Watch a specific repository instead of the current directory's. Works from anywhere -- you do not need a local clone. The optional host targets a GitHub Enterprise or EMU data-residency tenant, e.g. `tenant.ghe.com/acme/widget`. |
306
- | `--refresh <seconds>` | Active-tab poll interval, 2-3600, default 5. Background tabs stay at 12x this. |
320
+ | `--refresh <seconds>` | Minimum active-tab poll interval, 2-3600, default 5. Safe shared grants can make a check later; each background tab is considered about every 12 floors. |
307
321
  | `--tab <name>` | Start on `actions`, `issues`, `prs` or `security`. |
308
322
  | `--verbose` | Log one line per `gh` call to stderr, with timing and outcome. stderr must be redirected: `gh-glance --verbose 2>gh-glance.log`. |
309
323
  | `--doctor` | Print a diagnostic report and exit. See [Diagnostics](#diagnostics). |
@@ -314,14 +328,16 @@ Environment variables work too, and the flags take precedence:
314
328
 
315
329
  | Variable | Effect |
316
330
  |---|---|
317
- | `GH_REPO=owner/name` | Watch a specific repository instead of the current directory's. A host-qualified `GH_REPO` does **not** route the security-alert endpoints -- use `GH_HOST` or `--repo host/owner/name` for that |
318
- | `GH_HOST=<host>` | Send every call to a GitHub Enterprise or EMU host instead of `github.com`. Routes both the list commands and the alert endpoints |
319
- | `GH_TOKEN`, `GH_ENTERPRISE_TOKEN`, `GH_CONFIG_DIR` | Not read by `gh-glance` -- passed through to `gh` untouched, along with the proxy variables. `gh-glance` handles no credentials of its own. Separate `GH_CONFIG_DIR` values can isolate simultaneous panes on different accounts; see [No account pinning](#limitations) |
320
- | `GH_GLANCE_REFRESH=<seconds>` | Active-tab poll interval, 2-3600. Sets it once for every pane in a shell; `--refresh` takes precedence. This is a floor -- see [Rate limit](#rate-limit) |
321
- | `GH_GLANCE_ICONS=unicode` | Plain ASCII status icons, for terminals without a Nerd Font |
322
- | `GH_GLANCE_NO_ANIMATION=1` | Freeze the spinner — no motion at all |
331
+ | `GH_REPO=[host/]owner/name` | Watch a specific repository instead of the current directory's. A qualified value supplies the host; an unqualified value means `github.com`. An explicit `--repo` wins. |
332
+ | `GH_HOST=<host>` | When no explicit `--repo` overrides it, send every call and the account governor to a GitHub Enterprise or EMU host instead of `github.com`. |
333
+ | `GH_TOKEN`, `GITHUB_TOKEN`, `GH_ENTERPRISE_TOKEN`, `GITHUB_ENTERPRISE_TOKEN` | Used by `gh`. gh-glance never logs or stores these values, but hashes each set value locally as part of the account-scoped cache and governor namespaces so panes with different credentials cannot share rows or admission. The raw values are not written to disk. |
334
+ | `GH_CONFIG_DIR` | Selects `gh`'s account configuration and contributes its normalized identity to the cache and governor namespaces. Separate values can isolate simultaneous panes on different accounts; see [No account pinning](#limitations). |
335
+ | `GH_GLANCE_REFRESH=<seconds>` | Minimum active-tab poll interval, 2-3600. Sets the floor for every pane in a shell; `--refresh` takes precedence. See [Rate limit](#rate-limit). |
336
+ | `GH_GLANCE_ICONS=unicode` | Unicode status glyphs and single-cell text substitutes for Nerd Font row icons |
337
+ | `GH_GLANCE_ICONS=ascii` | ASCII-only status and row icons |
338
+ | `GH_GLANCE_NO_ANIMATION=1` | Stop status and run-state motion. Semantic words such as Checking, Waiting, and Paused still report what is happening. |
323
339
  | `NO_COLOR=1` | Disable colour. Status stays readable: severity has its own column, a failing newest run puts a `!` on the Actions tab, and the active tab is bracketed. Note that run-state glyphs are not all distinct -- see the feature list above |
324
- | `INK_SCREEN_READER=true` | Switch the renderer to a linear, unthrottled mode. The status icons carry text labels for it, but this path has not been tested against a real screen reader — treat it as unverified rather than supported. |
340
+ | `INK_SCREEN_READER=true` | Switch the renderer to a linear, unthrottled mode. Automated PTY coverage checks that status and selection labels reach this Ink path, but it has not been validated with real assistive technology — treat it as automated rendering coverage, not claimed screen-reader support. |
325
341
 
326
342
  ### Saved column widths
327
343
 
@@ -358,11 +374,13 @@ $XDG_CONFIG_HOME/gh-glance/dashboard-cache.json
358
374
  ~/.config/gh-glance/dashboard-cache.json
359
375
  ```
360
376
 
361
- The cache is scoped to the repository and host being watched. In inferred mode,
362
- the host and current working directory form the identity, so rows from one
363
- checkout cannot appear in another. It retains at most five recent targets and
364
- 60 rows per tab; a shortened cached tab keeps its `+` marker rather than
365
- presenting the saved count as complete.
377
+ The cache is scoped to the repository, host, and effective `gh` account
378
+ namespace. In inferred mode, the host and current working directory also form
379
+ the identity, so rows from one checkout or credential context cannot appear in
380
+ another. The namespace uses `GH_CONFIG_DIR`/`hosts.yml` identity plus one-way
381
+ digests of supported token environment variables; raw credentials are never
382
+ stored. It retains at most five recent targets and 60 rows per tab; a shortened
383
+ cached tab keeps its `+` marker rather than presenting the saved count as exact.
366
384
 
367
385
  Only successfully parsed, non-blind observations replace saved rows. A failed
368
386
  request can add a live error or `?` marker, but it cannot turn known Security
@@ -371,7 +389,35 @@ unknown-version, or unwritable cache state is ignored rather than blocking the
371
389
  dashboard. On POSIX systems, gh-glance restricts its config directory to `0700`
372
390
  and the cache file to `0600`. The file contains repository data, including
373
391
  titles, authors, branches, and Security findings, but never GitHub credentials.
374
- It is recovery state owned by the UI, not a configuration interface.
392
+ Both persistence files use a bounded advisory lock before atomic replacement.
393
+ The cache merges independent targets and tabs, while preferences merge columns,
394
+ so simultaneous panes do not overwrite one another's unrelated state. It is
395
+ recovery state owned by the UI, not a configuration interface.
396
+
397
+ ### Private API governor state
398
+
399
+ API admission uses a separate file beside the cache:
400
+
401
+ ```text
402
+ $XDG_CONFIG_HOME/gh-glance/rate-governor-v1-<scope hash>.json
403
+ ~/Library/Application Support/gh-glance/rate-governor-v1-<scope hash>.json
404
+ ~/.config/gh-glance/rate-governor-v1-<scope hash>.json
405
+ ```
406
+
407
+ The hash binds the effective GitHub host to the local `gh` account namespace
408
+ without putting either raw value in the file name. The file records only the
409
+ protocol needed to coordinate panes: REST and GraphQL budget observations and
410
+ epochs, leases, intents, reservations, fair-lane cursors, probe ownership and
411
+ outcomes, and temporary rate-limit blocks. It stores no token, raw host, login,
412
+ repository, working directory, title, author, branch, alert, or other dashboard
413
+ row. Ephemeral lock records contain only a PID and random nonce.
414
+
415
+ This state has a stricter role than `dashboard-cache.json`. A missing cache can
416
+ be ignored because it only supplies last-good rows. Missing governor state can
417
+ be initialized under its private lock, but corrupt, stale, locked, or unwritable
418
+ coordination fails closed: quota-consuming calls wait or pause instead of
419
+ falling back to independent polling. On POSIX systems the directory is `0700`,
420
+ and governor, temporary, lock, recovery, and quarantine files are `0600`.
375
421
 
376
422
  ## GitHub Enterprise and EMU
377
423
 
@@ -395,11 +441,19 @@ The `[host/]owner/name` form is the same one `gh --repo` accepts. The host must
395
441
  contain a dot, so `owner/name/extra` is still rejected as the typo it is rather
396
442
  than read as a request to a host named `owner`.
397
443
 
398
- **`GH_HOST` works too**, and is the better lever when every repository you watch
399
- lives on one enterprise host -- it routes the list commands and the alert
400
- endpoints alike. A host-qualified **`GH_REPO`**, by contrast, does *not* route
401
- `gh api`: it supplies the owner and repository and ignores the host, which is
402
- why `--repo` is the right tool for a one-off cross-host target.
444
+ **Every call and its governor use one effective host.** Resolution is explicit
445
+ and fail-closed: an explicit `--repo host/owner/name` uses that host, while an
446
+ explicit unqualified `--repo owner/name` means `github.com` even when `GH_HOST`
447
+ or `GH_REPO` is also set. Without `--repo`, `GH_HOST` is next, followed by a
448
+ qualified or unqualified `GH_REPO`, then one unambiguous host found across all
449
+ local remotes. An invalid present `GH_HOST`, or ambiguous remotes with no
450
+ explicit choice, cannot silently fall through to another server.
451
+
452
+ The resolved host routes list commands, alert endpoints, the free rate-limit
453
+ probe, and the shared account governor together. This corrects `gh api`'s native
454
+ behavior for host-qualified `GH_REPO`: gh-glance adds the required `--hostname`
455
+ itself. A running pane also rechecks the active account namespace and moves to a
456
+ fresh closed scope after `gh auth switch`, before it can receive another grant.
403
457
 
404
458
  **SAML sessions expire.** When an enterprise session lapses, the endpoints
405
459
  answer 403 and the tab shows the real `gh` message. Re-authorize in the browser
@@ -414,11 +468,21 @@ gh-glance --doctor > report.txt
414
468
 
415
469
  Collects, in one plain-text block: the `gh-glance`, Node and `gh` versions;
416
470
  which hosts `gh` is authenticated for; how the repository target resolved and
417
- from where; your remaining REST and GraphQL budget plus what this configuration
418
- will spend per hour; the relevant environment variables; a read-only
419
- `Repository access` probe; and one probe per dashboard endpoint with the exact
420
- argv it sent, its outcome, and how any error was classified (`unavailable`,
421
- `rate-limited`, `auth-problem` or `other`).
471
+ from where; your remaining REST and GraphQL budget plus the configuration's
472
+ unpaced cost projection; shared governor health; the relevant environment
473
+ variables; a read-only `Repository access` probe; and each bounded dashboard
474
+ probe, including the Security priority lanes, with the exact argv it sent, its
475
+ outcome, and how any error was classified (`unavailable`, `rate-limited`,
476
+ `auth-problem` or `other`).
477
+
478
+ Doctor first makes one free `rate_limit` request to the effective host. It then
479
+ uses an ephemeral governor lease and admits every quota-consuming endpoint at
480
+ its exact declared cost. A probe whose safe slot is later, whose resource is
481
+ held, or whose budget is unavailable is reported as `SKIPPED`; diagnostics do
482
+ not bypass the reserve. The `API governor` section reports `healthy`, `waiting
483
+ for probe`, `stale`, `blocked`, or `unavailable`, plus the number of live leases
484
+ and each resource's remaining calls, hard reserve, and reset. It does not print
485
+ the scope hash, account identity, state path, lock owner, or reservation IDs.
422
486
 
423
487
  The `Repository access` probe shows whether the target resolves for the active
424
488
  `gh` credentials. A failed GitHub resolution response cannot distinguish a
@@ -443,34 +507,61 @@ printed if `gh-glance` crashes.
443
507
 
444
508
  ## Rate limit
445
509
 
446
- The visible tab refreshes every 5 seconds; the other three refresh every 60
447
- seconds, purely to keep their counts honest.
510
+ The default five seconds is a healthy single-pane floor, not an unconditional
511
+ request frequency. Each check first needs an atomic grant from the private
512
+ governor shared by local panes using the same effective host and account
513
+ namespace. The active tab is considered at its floor. One rotating inactive tab
514
+ is considered every four floors, so each of the three background tabs is
515
+ considered about every 12 floors without starting them together.
448
516
 
449
- The tabs do not all draw on the same budget. Actions and Security spend REST
450
- calls; Issues and Pull Requests are sorted with `--search`, which routes them
451
- through GraphQL instead — a separate 5,000/hour allowance. With the default
452
- 5-second refresh:
517
+ The resources are independent. Actions and Security spend REST `core` calls;
518
+ Issues and Pull Requests are sorted with `--search`, which routes them through
519
+ GraphQL. The following table is the conservative demand before shared pacing,
520
+ not a promise that the governor will start every listed request. At the default
521
+ floor:
453
522
 
454
523
  | Visible tab | REST / hour | GraphQL / hour |
455
524
  |---|---|---|
456
- | Actions | ~1,620 | ~240 |
457
- | Issues or Pull Requests | ~300 | ~1,560 |
458
- | Security | ~2,280 | ~240 |
459
-
460
- Security is the most expensive, because it is three endpoints rather than one,
461
- and watching it costs just under half the REST budget. But Actions is not cheap
462
- either -- it is a third of the budget, and it is the tab every pane opens on,
463
- because one `gh run list` issues two REST requests rather than one. You do not
464
- have to take these figures on trust: `gh-glance --doctor` reports your actual
465
- remaining budget alongside what your configuration projects, which matters on a
466
- GitHub Enterprise tenant where the ceiling may not be 5,000 at all.
467
-
468
- Two things pull the real number down. Any endpoint that fails in a way
469
- `gh-glance` recognises backs off instead of being re-asked every tick — a
470
- feature that is not enabled for the repository retries on a ladder up to an
471
- hour, an authorization failure every 30 seconds, a rate limit every minute.
472
- Pressing `r` clears that backoff and retries immediately. And `--refresh`
473
- scales the whole figure: doubling the interval halves it.
525
+ | Actions | up to ~1,800 | ~240 |
526
+ | Issues or Pull Requests | up to ~480 | ~1,560 |
527
+ | Security | ~2,280 normally; up to ~4,440 | ~240 |
528
+
529
+ Security is the most expensive. A repository whose newest alert pages are not
530
+ full uses the three base endpoint calls and lands near 2,280 REST requests per
531
+ hour. A full page activates bounded critical/high lanes for Dependabot and code
532
+ scanning, raising the safe projection to about 4,440. Actions is not cheap
533
+ either: one `gh run list` issues two REST requests. `gh-glance --doctor` reports
534
+ this projection beside the server's actual REST and GraphQL budgets. Enterprise
535
+ ceilings can differ from 5,000.
536
+
537
+ For each resource, gh-glance reserves 20% of the reported limit for other work:
538
+
539
+ ```text
540
+ reserve = ceil(limit * 0.2)
541
+ spendable = max(0, remaining - reserve - charged reservations)
542
+ ```
543
+
544
+ That is 1,000 calls for a normal 5,000-call resource. Before any quota-consuming
545
+ `gh` process starts, its declared worst-case REST/GraphQL cost must fit inside
546
+ the latest fresh observation without entering the reserve. A grant is charged
547
+ immediately, then reconciled only when completion evidence proves a lower cost.
548
+ Started, interrupted, or process-lost work stays conservative until a later
549
+ clean probe can account for it. Missing, stale, corrupt, locked, or unwritable
550
+ coordination denies the call instead of returning to five-second polling.
551
+
552
+ One pane owns the free `rate_limit` probe for a control window and publishes it
553
+ for the others. Manual and diagnostic work is considered before tab-switch,
554
+ active, and background work; equal-priority panes rotate fairly. Manual `r`
555
+ coalesces repeated presses, gives immediate Waiting or Paused feedback, and
556
+ clears an endpoint backoff only after admission. It cannot bypass a held budget
557
+ or force a request into the reserve.
558
+
559
+ The shared lane uses spendable capacity, time to reset, outstanding
560
+ reservations, and observed external spend. Startup and each new reset epoch add
561
+ a stable per-pane phase, so panes do not resume as a herd. There is no
562
+ 60-second maximum: if the next safe slot is farther away, the pane waits that
563
+ long. `next HH:MM` is the time of one current grant, not a promise of a recurring
564
+ poll at that time or interval.
474
565
 
475
566
  When a refresh fails, the last successful rows stay visible below the live
476
567
  error instead of disappearing. If gh-glance is restarted while GitHub is still
@@ -479,22 +570,19 @@ age remains explicit through the `stale` label, and the rate-limit banner still
479
570
  describes the current failed request; cached data never turns a failure into a
480
571
  false success.
481
572
 
482
- One pane on Actions costs about a third of an hourly REST budget, so roughly
483
- three panes fill it. Past that, `gh-glance` widens its own poll interval: it
484
- reads your remaining budget once a minute, works out how much of it belongs to
485
- this pane by comparing its own spend against the token's total, and slows down
486
- to fit -- up to a 60-second ceiling. The status bar says `throttled 18s` while
487
- that is in effect, and `r` still refreshes immediately. Nothing is shared
488
- between panes: the token's own counter is what they all read, so it also notices
489
- a `gh pr checks --watch` or anything else spending on the same token.
573
+ The enforceable boundary is local admission from fresh, conservatively debited
574
+ evidence. Another program can spend after the probe, and panes on another
575
+ machine or in a different local account scope cannot share this file. GitHub
576
+ does not offer an atomic global quota reservation. The token-wide counter still
577
+ lets gh-glance measure external use and reduce future capacity, but the hard
578
+ reserve is not a claim that unrelated consumers can never cross it.
490
579
 
491
580
  `GH_GLANCE_REFRESH=30` sets a wider floor for every pane in a shell; `--refresh`
492
- still wins per pane. The adaptive interval only ever widens from that floor, so
493
- a single pane on a healthy budget stays at 5 seconds and shows no badge.
581
+ still wins per pane. A single pane on a healthy budget normally stays at its
582
+ floor and shows Watching between checks.
494
583
 
495
584
  ## Limitations
496
585
 
497
-
498
586
  - **Security tab**: code scanning and secret scanning alerts require [GitHub
499
587
  Advanced Security](https://docs.github.com/en/code-security/getting-started/github-security-features).
500
588
  On repos/plans without it, `gh-glance` doesn't crash or spam errors -- it
@@ -526,12 +614,13 @@ a single pane on a healthy budget stays at 5 seconds and shows no badge.
526
614
  has its own breakpoint and drops to bare keys (`↑↓ Ent r q`) rather than
527
615
  truncating.
528
616
  - **Mouse reporting lifecycle.** Interactive sessions enable terminal mouse
529
- reporting for header drags and disable it before handing the terminal back.
530
- See [Adjusting column widths](#adjusting-column-widths) for text-selection
531
- behavior while it is active.
532
- - Issues and pull requests are fetched 150 at a time and alerts 100 at a time.
533
- A count is shown as `n+` when it was truncated, so the number is never
534
- presented as exact when it is not.
617
+ reporting only after `w` enters width mode, and disable it when that mode or
618
+ the app exits. See [Adjusting column widths](#adjusting-column-widths) for
619
+ text-selection behavior while it is active.
620
+ - Issues and pull requests are fetched 150 at a time. Each Security source has
621
+ a newest-100 lane; full Dependabot and code-scanning pages activate bounded
622
+ priority lanes. A count is shown as `n+` when any lane fills, so the number is
623
+ never presented as exact when it is not.
535
624
  - The Actions **TIME** column measures from the run's start to its last update.
536
625
  `gh` exposes no completion timestamp, so a workflow that was re-run later
537
626
  reports the span up to the re-run rather than its original duration.
@@ -545,20 +634,26 @@ a single pane on a healthy budget stays at 5 seconds and shows no badge.
545
634
 
546
635
  ### Icons without a Nerd Font
547
636
 
548
- The status icons are Octicon glyphs from the [Nerd
637
+ The row icons are Octicon glyphs from the [Nerd
549
638
  Fonts](https://www.nerdfonts.com/) private-use-area range. If your terminal
550
- font isn't a Nerd Font they render as blank boxes -- which would leave status
639
+ font isn't a Nerd Font they render as blank boxes -- which would leave row state
551
640
  carried by the colour of a blank box, and by nothing at all under `NO_COLOR`.
552
641
 
553
- Set `GH_GLANCE_ICONS=unicode` for plain ASCII equivalents:
642
+ Set `GH_GLANCE_ICONS=unicode` for Unicode status glyphs and single-cell text
643
+ substitutes for the Nerd Font row icons:
554
644
 
555
645
  ```bash
556
646
  GH_GLANCE_ICONS=unicode gh-glance
557
647
  ```
558
648
 
559
- They are deliberately single-cell ASCII rather than prettier symbols like `✓`
560
- and `✗`, because those are East-Asian-Ambiguous width and render two cells wide
561
- in some terminals, which would shift every column to their right.
649
+ The row substitutes are deliberately single-cell ASCII rather than symbols like
650
+ `✓` and `✗`, because those are East-Asian-Ambiguous width and render two cells
651
+ wide in some terminals. Use `GH_GLANCE_ICONS=ascii` when the status glyphs must
652
+ also stay within plain ASCII:
653
+
654
+ ```bash
655
+ GH_GLANCE_ICONS=ascii gh-glance
656
+ ```
562
657
 
563
658
  ## Troubleshooting
564
659
 
@@ -571,16 +666,20 @@ in some terminals, which would shift every column to their right.
571
666
  | `Repository not found or inaccessible to the active gh account` | The active identity cannot resolve the target. Check `gh auth status`, `git remote -v` or the explicit `--repo`, and use `gh auth switch` only if the wrong account is active. |
572
667
  | `GraphQL: Could not resolve to a Repository...` in older gh-glance versions | The response has the same ambiguity: a missing or renamed target, or a private repository not visible to the active account. Run `gh-glance --doctor`. |
573
668
  | Actions says `not available for this repository` while Repository access is `ok` | The repository resolved, but that endpoint is unavailable; inspect the corresponding doctor block. |
574
- | Status icons are blank boxes | Your terminal font is not a Nerd Font. Use `GH_GLANCE_ICONS=unicode`. |
669
+ | Row icons are blank boxes | Your terminal font is not a Nerd Font. Use `GH_GLANCE_ICONS=unicode`. |
575
670
  | Security tab shows a "not enabled" note | Code scanning and secret scanning need GitHub Advanced Security. Dependabot alerts work independently. The note now appears only when the feature genuinely is unavailable: auth, SSO and network failures show the real error instead. |
576
671
  | `none of the git remotes ... point to a known GitHub host` | `gh` is not authenticated for that host. Run `gh auth login --hostname <host>`. |
577
- | Security tab empty on an enterprise host, other tabs fine | The alert endpoints were sent to the wrong host. Use `--repo host/owner/name` or set `GH_HOST` -- a host-qualified `GH_REPO` alone does not route them. |
672
+ | Security tab empty on an enterprise host in an older gh-glance version | Older versions could route alert endpoints separately. Current gh-glance routes every API call and the governor to one effective host; use `--repo host/owner/name`, `GH_HOST`, or a qualified `GH_REPO`, then confirm the resolved host with `--doctor`. |
578
673
  | Tabs start failing after working for a while | The enterprise SAML session lapsed. Re-authorize in the browser; the dashboard recovers within about 30 seconds. Run `gh-glance --doctor` to confirm. |
579
674
  | A tab's count is red | That tab's last fetch failed. The error itself is shown when you switch to it, translated into what to do about it where `gh-glance` recognises the failure. |
580
675
  | Security tab shows `?` instead of a number | The alert endpoints could not be read at all -- an expired SAML session, a token without `security_events`, or an org OAuth restriction. `?` means "unknown", not "zero"; run `gh-glance --doctor` to see which probe failed and how it was classified. |
581
- | A failing tab seems to have stopped retrying | It backs off deliberately, rather than re-spawning `gh` every five seconds against an endpoint that is refusing. Press `r` to clear the backoff and retry now. |
676
+ | `Waiting` or `next HH:MM` | The active tab is waiting for a shared budget probe or for its current safe grant. The time names this grant only; it is not a recurring polling interval. Pressing `r` raises safe priority but cannot bypass the lane or reserve. |
677
+ | `Paused` with a reset time | The active tab's REST or GraphQL resource is at its reserve, exhausted, or under a shared rate-limit block. Wait for the stated reset/probe. Other tabs can continue when they use the healthy resource. |
678
+ | `Paused` without a reset time | Budget or coordinator evidence is unknown, corrupt, locked, or unwritable. No data call is started. Run `gh-glance --doctor`; also check the config directory permissions and whether another live process owns its private lock. |
679
+ | A failing tab seems to have stopped retrying | Recognized endpoint failures back off rather than re-spawning `gh` at the floor. Press `r` to request a higher-priority retry; the backoff clears only after a safe grant, so a held budget remains Waiting or Paused. |
582
680
  | `unknown argument: -v` | `-v` used to mean `--version` and no longer does, because this CLI also has `--verbose`. Use `--version` or `--verbose` explicitly. |
583
- | `stale 2m` in the status bar | The visible tab has not refreshed successfully for a while — usually a network drop or rate limit. Last-known-good rows can remain visible, including after restart; the live error above the table explains why they are stale. |
681
+ | Cached rows plus `Paused` and `stale 2m` | The rows came from the separate last-known-good dashboard cache, while the live request is blocked or unsafe. Stale age is not extended by a pause. The error and footer describe current coordination; cached data never means the live check succeeded. |
682
+ | Repeated GitHub rate-limit messages | A classified rate-limit response is published as one shared resource block. Local panes make no data retry before that block's probe/reset deadline. Use `--doctor` to inspect the resource and reset; repeated manual refresh cannot override it. |
584
683
  | It exits immediately when piped | Intentional. It is a full-screen dashboard, not a reporting command. |
585
684
  | It stopped updating and you cannot tell why | Run `gh-glance --verbose 2>gh-glance.log`, reproduce, then read the log: one line per `gh` call with its duration and outcome. It is redacted the same way `--doctor` is, so it is safe to attach to a bug report. `--doctor --verbose` logs the probes too. |
586
685
  | `--verbose` refuses to start | stderr is still your terminal, where the log would draw over the dashboard. Redirect it to a file. |