Skip to content

git-stats: a sidebar card for the working tree and the session's pull requests - #21

Merged
navbytes merged 2 commits into
mainfrom
add-git-stats-plugin
Sep 10, 2026
Merged

navbytes merged 2 commits into
mainfrom
add-git-stats-plugin

Conversation

@navbytes

Copy link
Copy Markdown
Owner

A second plugin in the monorepo: opencode-git-stats, a TUI-only sidebar card
holding the whole folder's diff figures and a GitHub-coloured chip per pull
request the session touched.

Git Stats
⎇ add-git-stats-plugin
+123 -45 · 7 files
 #21 Open  ×  #20 Merged  ×

Shape

sidebar_content slot at order 450 — the host's own sections are 100 Context …
400 Todo, 500 Modified Files, drawn lowest first, so the worktree totals land
directly above that file list. Everything else is opencode's own API:
client.vcs.status(), state.vcs, state.session.messages() / state.part(),
event.on(...), kv, keymap, lifecycle.

The figures are client.vcs.status(), not state.session.diff(). Those are
different things: despite the name, session.diff accumulates per-turn snapshot
diffs for the files this session changed — which is the list the host already
draws two lines below. "Diff stats in the folder" means the working tree.

Chips are sighted incrementally, from completed tool parts via
message.part.updated, with a count-guarded catch-up scan for whatever happened
before the card mounted. Walking the transcript inside a createEffect instead
would rescan every message on every streamed token. A chip exists only because a
command actually printed the URL — the assistant's prose is not scanned. Pull
requests the session created lead the ones it merely referenced.

State comes from gh pr view --json state,isDraft, the one question
opencode's API cannot answer, and the chip is painted with GitHub's own colours
(#59636e draft, #1f883d open, #cf222e closed, #8250df merged) on white,
the way the badge on the pull request page is. Merged is terminal and never
re-fetched; failures back off exponentially to ~16 minutes and reset at a turn
boundary in case gh was just installed or logged into. Dismissed chips are
never fetched at all.

Security

Only github.com is trusted without configuration. The URLs are found in tool
output
, which can carry whatever a fetched page or a pasted file put there, and
the host goes straight to gh --repo <host>/… — which treats an unknown host as
Enterprise and would send it a request with an Enterprise token attached. So
github.evil.example and github.com.evil.example are ignored, and a real
Enterprise host has to be named in the plugin's hosts option. gh is spawned
with an argv array, never a shell string. At most 24 chips are tracked per
session, so one gh pr list --json url cannot mint a process per pull request.

Closing a chip

Click the × (the host's own sidebar uses onMouseDown, so mouse works), or
/prs to hide one, hide all, or bring the hidden ones back. Dismissals live in
kv per session, unioned with an in-memory set for the window before kv is
ready. Each chip label is an OSC 8 hyperlink to the pull request.

Verified

79 unit tests, plus a real OpenCode TUI driven under a pty, asserting on composed
screens and raw terminal bytes:

  • card renders Git Stats / ⎇ main / +6 -1 · 3 files, and those figures match
    what git itself reported (+4/-1 across 2 tracked files, plus a 2-line untracked
    one)
  • chip renders #20 Merged as
    \x1b[38;2;255;255;255m\x1b[48;2;130;80;223m — #8250df on white — and carries
    an OSC 8 link to the PR; on a non-truecolor terminal it degrades to 48;5;98
  • /prs opens "Pull request chips" with the PR title as the row description;
    selecting hides the chip, and a second /prs offers "Show 1 hidden chip again"
    and restores it
  • with gh logged out the chip reads #20 … and the card says
    gh: run `gh auth login` rather than guessing a colour
  • no errors, exceptions or tracebacks in any capture

gh itself is covered against live GitHub and against a stubbed binary for every
failure path (missing, logged out, deleted PR, unreadable JSON, killed/aborted).

Also

release.yml and publish.yml gain git-stats in the package choice list, and
the root README gains its row. Per the repo's own note, the first version of a new
package still has to be npm published by hand once before OIDC trusted
publishing will attach.

… requests

A second plugin in the monorepo, TUI-only, in the shape opencode actually
offers: a `sidebar_content` slot at order 450 — directly above the host's own
Modified Files list — holding the whole folder's diff figures and a chip per
GitHub pull request the session touched.

The figures come from `client.vcs.status()`, not `state.session.diff()`. Those
are different things: the latter accumulates the files *this session* changed,
which is the list the host already draws two lines below. "Diff stats in the
folder" means the working tree, so it is the VCS endpoint, refreshed on
`session.diff` and `session.idle` and on a 10s tick for edits made elsewhere.
A failed or timed-out call keeps the last figures rather than reporting a dirty
tree as clean.

Chips are sighted from completed tool parts, incrementally via
`message.part.updated` — walking the transcript inside a `createEffect` rescans
every message on every streamed token — with a count-guarded catch-up scan for
whatever happened before the card mounted. A chip only exists because a command
actually printed the URL; the assistant's prose is not scanned. Ones the session
created lead the ones it only referenced.

The state behind a chip is the one question opencode's API cannot answer, so it
comes from `gh pr view --json state,isDraft`, and the chip is painted with
GitHub's own colours (#59636e draft, #1f883d open, #cf222e closed, #8250df
merged) on white, the way the badge on the pull request page is. Merged is
terminal and never re-fetched; failures back off exponentially to ~16 minutes
and reset at a turn boundary, in case `gh` was just installed or logged into.

Only github.com is trusted without configuration. The URLs are found in tool
output, which can carry whatever a fetched page put there, and the host goes
straight to `gh --repo` — which treats an unknown host as Enterprise and would
send it a request with an Enterprise token attached. A real Enterprise host has
to be named in the plugin's `hosts` option, and at most 24 chips are tracked per
session so one `gh pr list --json url` cannot mint a process per pull request.

Chips close by clicking the `×` (the host's own sidebar uses onMouseDown, so
mouse works) or through `/prs`, which also hides all or restores them.
Dismissals are per session in `kv`, unioned with an in-memory set for the window
before `kv` is ready. Each label is an OSC 8 hyperlink to the pull request.
@navbytes
navbytes merged commit 3b9883d into main Sep 10, 2026
1 check passed
@navbytes
navbytes deleted the add-git-stats-plugin branch September 10, 2026 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant