GitHub team statistics in your terminal.
Statline (gh statline) is a terminal dashboard for the numbers behind your
team's pull-request workflow — who's opening, merging, reviewing, approving,
and commenting, across the repos your team actually works in.
(All names in the demo are generated by the built-in seed command — no real data.)
- Team stats — one sortable stat line per member: PRs opened and merged, reviews given (approved / commented / changes requested), comments given vs received, median cycle time (open → merge), median time to first review, and median PR size.
- Charts — a 3×3 dashboard that always fits one screen: PR throughput, review outcomes, cycle-time trend, who-reviews-whom matrix, first-review latency, PR sizes, open-PR aging, and an activity punch card, topped by stat tiles comparing the window to the previous one (▲/▼ deltas appear once the cache covers both). Any card expands to a scrollable fullscreen view; the review matrix pans with its name labels pinned.
- Trends — where the numbers are heading: a card per headline metric showing its 12-week trajectory with week-over-week deltas, plus a movers card for the members whose recent volume rose or fell the most ("reviews 2 → 8, up 4 weeks running"). Cards expand to fullscreen — count metrics break down into per-member weekly sparklines, movers drop the top-3 cap. Weekly buckets are fixed regardless of the time window, and the series only reaches as far back as the cache honestly covers.
- Person drill-down — headline stats, daily activity sparkline, and a per-repo breakdown for any teammate.
- Time windows — cycle 7/14/30/90-day presets with
w, or pick a custom date range withr. - Teams your way — a setup wizard imports a GitHub org team (members +
assigned repos) into a local config you can edit freely: add contractors,
hide alumni, track repos the team isn't formally assigned. Multiple team
profiles, switchable in-app with
t. - Local cache — incremental sync into SQLite (pure Go, no CGO): instant
startup, offline browsing, no re-fetching what you already have. A
headless
gh statline synckeeps the cache warm from cron. - Markdown export —
ycopies the current view as a Markdown table for standups, retros, and 1:1 notes. - Keyboard-first (vim keys + arrows) with clickable tabs and rows, wheel scrolling, and an adaptive light/dark Charm-style theme.
As a GitHub CLI extension (recommended):
gh extension install byte2pixel/gh-statline
gh statlineStandalone binaries are attached to each release
(download, rename to gh-statline, put it on your PATH), or build from
source with go install github.com/byte2pixel/gh-statline@latest.
Statline reuses your GitHub CLI credentials (gh auth login), falling back
to GITHUB_TOKEN. It needs the repo and read:org scopes.
gh statline # open the TUI (first run launches the setup wizard)
gh statline init # add another team profile
gh statline sync # refresh the cache without the TUI (cron-friendly)
gh statline sync --team platform --backfill 180| Key | Action |
|---|---|
1 / 2 / 3 / tab |
Team stats / charts / trends |
enter / esc |
Drill into member / back |
j/k, arrows |
Move selection |
h/l, ←/→ |
Change sort column |
- |
Flip sort direction |
f / enter |
Expand the focused card (charts, trends) |
j/k · h/l |
Scroll / pan a fullscreen card |
pgup/pgdn, d/u, g/G |
Page / half-page / jump in a fullscreen card |
w |
Cycle time window |
r |
Custom date range |
t |
Switch team |
s |
Sync now |
y |
Copy view as Markdown |
? |
Full help |
q |
Quit |
Lives at ~/.config/gh-statline/config.yml (Windows:
%AppData%\gh-statline\config.yml) — wizard-written, human-editable:
default_team: platform
exclude_bots: ["*[bot]", "dependabot*", "renovate*", "copilot*"]
teams:
- name: platform
org: acme
gh_team_slug: platform-eng # provenance of the import; optional
# no_sync: true # local-only profile; sync never touches GitHub
members:
- {login: alice}
- {login: bob, hidden: true} # kept in cache, hidden from views
repos:
- {owner: acme, name: api}
- {owner: acme, name: web}
sync: {backfill_days: 120, page_size: 25, concurrency: 3}
ui: {window: 30d, sort: prs_merged} # updated as you use the app; see belowexclude_bots globs (*, ?, case-insensitive, brackets literal) match
reviewer and commenter logins in addition to GitHub's own bot flag.
copilot* is in the defaults because Copilot code review reviews as
copilot-pull-request-reviewer, which has no [bot] suffix, so no other
glob catches it. A Copilot review lands within minutes of the PR opening,
and without the glob it would count as the first review whenever the
synced bot flag missed it. Defaults only fill in when the key is absent
from the file, so a config written by an older version keeps its old list.
Add new globs by hand.
sync tunes the GitHub walk: backfill_days is how far back a fresh cache
reaches, page_size is PRs per request (1–100), and concurrency is how
many repos sync in parallel, capped at 10. GitHub's secondary rate limits
trigger on concurrent requests from one token, so more workers than that
earn a block, not a faster sync.
Statline remembers how you left it: switching teams (t) updates
default_team, and changing the time window (w) or the sort column
(←/→) updates ui, so the next launch reopens the same view. Custom
date ranges (r) and --team <name> are one-shot and never persist. These
in-app changes rewrite the file, so YAML comments don't survive a session —
keep notes elsewhere if you hand-edit.
The SQLite cache lives in the user cache dir and is safe to delete — it just re-syncs.
- Counts are attributed to the person acting: reviews to the reviewer, comments to their author. Commenting on your own PR never counts.
- Review-thread replies arrive as GitHub "commented" reviews; v0.1 counts them as such (a known inflation of the commented bucket).
- Reviews GitHub has since marked dismissed still count as reviews given, in their own column: the review happened, and a later push invalidating an approval shouldn't erase the reviewer's work.
- Time to first review ignores bots and the PR author.
- Hidden members and bots are excluded as actors from every number, including the charts: what the team table shows and what the charts total are the same set of people. Their activity towards a visible member still counts, so hiding a teammate never rewrites someone else's stat line.
- Medians use the lower-middle value; a
–means no data in the window. - Percent changes (tiles, trends, movers) are rounded to the nearest whole
percent, with one shared definition. A change from a zero base has no
percentage and is labelled
new; in the movers ranking, new activity sorts ahead of every percentage change, by volume. - The review matrix only counts reviews on PRs authored by humans: reviews
on bot-authored PRs are excluded, and the
(others)column (reviews on non-member PRs) doesn't influence the heat-map scale. - The
updatedAt-ordered incremental walk cannot see PRs untouched since before the backfill horizon (default 120 days) — deepen withsync --backfill N. Fetched data is never deleted, so local coverage grows the longer you use statline; the tile deltas turn on per window once the cache provably covers the previous period.
go build ./... # pure Go, no C toolchain needed
go test ./...
go run .
go run . seed # dev helper: seeds a deterministic local-only "demo" team
# (38 members, 120d of history) to inspect charts at scaleTo render views headlessly (sizes, scroll states, seeded data), use the
dump harness: STATLINE_DUMP=1 go test ./internal/tui/app -run TestDumpView -v
— see dump_test.go for the STATLINE_DUMP_VIEW/_PRE/_POST options.
The README demo GIF is scripted with VHS
against seeded data — vhs/demo.tape has the Docker one-liner to regenerate
it after UI changes.
Built with Bubble Tea v2, Lip Gloss, Bubbles, ntcharts, BubbleZone, Harmonica, go-gh, and modernc.org/sqlite.
MIT