350 lines
15 KiB
Markdown
350 lines
15 KiB
Markdown
# gh-threads
|
|
|
|
A terminal UI for people receiving GitHub pull-request reviews. It
|
|
shows open PRs and a scrollable PR dashboard with the description, branches,
|
|
review state, merge conflicts and affected files, checks, people, labels,
|
|
milestone, activity, change statistics, thread totals, submitted reviews, and
|
|
the PR conversation. Review threads and comments are paginated rather than
|
|
silently stopping at the first page. The
|
|
thread viewer includes highlighted diff hunks, comment authors, and read-only
|
|
reaction counts on individual comments. Resolved threads start folded. GitHub suggestion blocks are shown as
|
|
syntax-highlighted remove/add previews. Comments and PR descriptions render
|
|
GitHub Flavored Markdown, including quoted replies, inline and fenced code,
|
|
lists and tasks, links, tables, emphasis, strikethrough, emoji, and GitHub
|
|
alerts. The current PR is refreshed in the background.
|
|
|
|
## Install and run
|
|
|
|
Requires Go 1.24+, Git 2.38+, and an authenticated GitHub CLI:
|
|
|
|
```sh
|
|
go install .
|
|
gh auth login
|
|
gh-threads
|
|
```
|
|
|
|
To run directly from a source checkout instead:
|
|
|
|
```sh
|
|
go run .
|
|
```
|
|
|
|
Use `go run .`, not `go run main.go`: the latter compiles only `main.go` and
|
|
omits the other files in the package.
|
|
|
|
For automation, `GH_TOKEN` or `GITHUB_TOKEN` can still be provided and takes
|
|
precedence over the GitHub CLI credential. Enterprise token environment
|
|
variables are also supported.
|
|
|
|
By default the PR picker searches all repositories for open PRs assigned to the
|
|
authenticated user and groups the results by repository. Use `--repo` to limit
|
|
the picker to one repository:
|
|
|
|
```sh
|
|
gh-threads --repo owner/repository
|
|
```
|
|
|
|
With a repository selected, pass `--all` to include every open PR in that
|
|
repository:
|
|
|
|
```sh
|
|
gh-threads --repo owner/repository --all --poll 15s
|
|
```
|
|
|
|
GitHub Enterprise Server can be used after authenticating that host:
|
|
|
|
```sh
|
|
gh auth login --hostname github.example.com
|
|
gh-threads --repo owner/repository \
|
|
--endpoint https://github.example.com/api/graphql
|
|
```
|
|
|
|
## Configuration
|
|
|
|
The optional TOML configuration is loaded from
|
|
`$GH_THREADS_CONFIG`, `$XDG_CONFIG_HOME/gh-threads/config.toml`, or the
|
|
operating system's user configuration directory at `gh-threads/config.toml`.
|
|
On Linux this is normally `~/.config/gh-threads/config.toml`. On macOS,
|
|
`~/Library/Application Support/gh-threads/config.toml` is preferred, with
|
|
`~/.config/gh-threads/config.toml` automatically used as a fallback when it
|
|
exists.
|
|
|
|
```toml
|
|
theme = "dark" # dark, light, high-contrast, or no-color
|
|
refresh_interval = "10s"
|
|
repository = "" # optional owner/repository default
|
|
show_all = false # requires repository
|
|
limit = 50
|
|
endpoint = "https://api.github.com/graphql"
|
|
|
|
[display]
|
|
fold_resolved = true
|
|
thread_list_width_percent = 33 # 20-60
|
|
dashboard_mode = "hotkey" # "hotkey" or "intermediate"
|
|
compact_reviews = true # aggregate submitted review history
|
|
|
|
[paths]
|
|
scroll = false
|
|
scroll_interval = "350ms" # minimum 50ms
|
|
|
|
[threads]
|
|
# Each status must occur exactly once. "outdated" means unresolved and outdated;
|
|
# resolved threads remain in "resolved" even when they are also outdated.
|
|
status_order = ["unresolved", "outdated", "resolved"]
|
|
within_status = "file" # "file" or "timestamp" (oldest first)
|
|
|
|
[cache]
|
|
enabled = true # instant stale view plus offline fallback
|
|
max_age = "168h" # 7 days; 0 means no age limit
|
|
directory = "" # defaults to the OS user cache directory
|
|
max_entries = 200 # bounded oldest-first pruning; 10-10000
|
|
|
|
[editing]
|
|
mode = "vim" # "vim" or "standard"; description field only for now
|
|
|
|
[keybindings.general]
|
|
quit = ["q", "ctrl+c"]
|
|
help = ["?", "f1"]
|
|
refresh = ["r"]
|
|
back = ["b", "esc"]
|
|
confirm = ["y"]
|
|
reject = ["n", "esc"]
|
|
|
|
[keybindings.navigation]
|
|
# Shared by the picker, dashboard, thread panes, help, and Vim Normal/Visual modes.
|
|
down = ["j", "down"]
|
|
up = ["k", "up"]
|
|
left = ["h", "left"]
|
|
right = ["l", "right"]
|
|
first = ["g"]
|
|
last = ["G"]
|
|
page_down = ["ctrl+d", "pgdown"]
|
|
page_up = ["ctrl+u", "pgup"]
|
|
|
|
[keybindings.views]
|
|
open = ["enter", "l"]
|
|
dashboard = ["d"]
|
|
health = ["H"]
|
|
edit = ["e"]
|
|
auto_merge = ["a"]
|
|
merge_now = ["M"]
|
|
toggle_list = ["tab"]
|
|
|
|
[keybindings.threads]
|
|
search = ["/"]
|
|
clear_filter = ["F"]
|
|
next_unread = ["n"]
|
|
previous_unread = ["N"]
|
|
reply = ["c"]
|
|
resolve = ["R"]
|
|
toggle = ["enter"]
|
|
fold_prefix = ["z"]
|
|
fold_toggle = ["a"]
|
|
|
|
[keybindings.input]
|
|
cancel = ["esc"]
|
|
submit = ["ctrl+s"]
|
|
newline = ["enter"]
|
|
delete_backward = ["backspace"]
|
|
delete_forward = ["delete"]
|
|
clear = ["ctrl+u"]
|
|
next_field = ["tab"]
|
|
previous_field = ["shift+tab"]
|
|
next_completion = ["ctrl+n"]
|
|
previous_completion = ["ctrl+p"]
|
|
line_start = ["home", "ctrl+a"]
|
|
line_end = ["end", "ctrl+e"]
|
|
|
|
[keybindings.vim]
|
|
insert = ["i"]
|
|
append = ["a"]
|
|
insert_line_start = ["I"]
|
|
append_line_end = ["A"]
|
|
open_below = ["o"]
|
|
open_above = ["O"]
|
|
replace_character = ["s"]
|
|
visual = ["v"]
|
|
visual_line = ["V"]
|
|
selection_other_end = ["o"]
|
|
yank = ["y"]
|
|
delete = ["d", "x", "delete"]
|
|
delete_before = ["X", "backspace"]
|
|
paste = ["p"]
|
|
line_start = ["0", "home"]
|
|
first_non_blank = ["^"]
|
|
line_end = ["$", "end"]
|
|
word_forward = ["w"]
|
|
big_word_forward = ["W"]
|
|
word_backward = ["b"]
|
|
big_word_backward = ["B"]
|
|
word_end = ["e"]
|
|
big_word_end = ["E"]
|
|
go_prefix = ["g"]
|
|
find_forward = ["f"]
|
|
find_backward = ["F"]
|
|
till_forward = ["t"]
|
|
till_backward = ["T"]
|
|
repeat_find = [";"]
|
|
repeat_find_reverse = [","]
|
|
```
|
|
|
|
Every command binding accepts one or more Bubble Tea key names. Omitted
|
|
settings retain their defaults, while an explicitly configured action replaces
|
|
its default keys. Printable keys remain text in Insert mode, reply drafts, and
|
|
search queries; command bindings apply in the appropriate non-text context.
|
|
The contextual `?` popup and compact screen footers use the configured keys.
|
|
Configuration loading also checks each active context independently. A key may
|
|
be reused on unrelated screens, but assigning it to two different actions that
|
|
can be active together reports the context and both conflicting actions.
|
|
|
|
When cached data exists, the picker and PR details are rendered immediately
|
|
from that snapshot while a live GitHub refresh runs in the background. Cached
|
|
screens are labelled with their save time and are replaced automatically when
|
|
fresh data arrives. Core PR and review data is rendered before check
|
|
annotations and conflict-file analysis finish. A failed subsection keeps its
|
|
last complete value, is marked partial, and does not discard the rest of a
|
|
successful refresh. Check annotations are fetched separately only for failed
|
|
checks and are reused by immutable check ID.
|
|
|
|
The cache uses separate JSON files for the picker and each visited PR. Cache
|
|
content is hashed before writing: unchanged responses do not rewrite their
|
|
files. Their modification time is touched at most once per day (or half the
|
|
configured maximum age, when shorter) so recently validated snapshots remain
|
|
usable without writing on every poll. Changed files are replaced atomically,
|
|
and oldest cache entries are pruned at the configured bound. Read state and
|
|
recoverable reply/metadata drafts use versioned, atomic files beside the
|
|
configuration.
|
|
|
|
Polling adapts to GitHub's reported rate-limit budget. It backs off as the
|
|
remaining budget gets low, honors server retry windows, and adds jitter to
|
|
avoid synchronized clients. Opening another PR or starting another refresh
|
|
cancels the superseded request.
|
|
|
|
GitHub's public APIs report whether a PR conflicts but do not expose its
|
|
conflicting file paths. For conflicting PRs only, `gh-threads` performs a
|
|
read-only `git merge-tree` analysis in a temporary bare repository. It never
|
|
touches or inspects the current checkout, so Git, Jujutsu (`jj`), and directories
|
|
without a local repository behave identically. The analysis fetches the exact
|
|
remote base branch and pull-request head ref using the existing GitHub
|
|
credential. Results are memoized by the base and head commit, and failed scans
|
|
are retried after one minute.
|
|
|
|
Command-line flags override the configuration. `GH_REPO` overrides the
|
|
configured repository when `--repo` is not provided. The corresponding flags
|
|
include `--config`, `--theme`, `--poll`, `--fold-resolved`,
|
|
`--thread-list-width`, `--dashboard-mode`, `--compact-reviews`,
|
|
`--path-scroll`, and `--path-scroll-interval`, plus `--cache`,
|
|
`--cache-max-age`, `--cache-dir`, and `--editor-mode`.
|
|
Boolean settings can be disabled explicitly, for
|
|
example `--compact-reviews=false`.
|
|
|
|
With the default `dashboard_mode = "hotkey"`, opening a PR goes directly to its
|
|
review threads and `d` opens the dashboard only when requested. Set
|
|
`dashboard_mode = "intermediate"` to follow picker → dashboard → review
|
|
threads instead.
|
|
|
|
Compact reviews aggregate submission counts by state and author. Reviews with
|
|
a written summary retain a compact one-line body, while timestamps and commit
|
|
SHAs are omitted. Set `compact_reviews = false` to restore the complete review
|
|
history and metadata.
|
|
|
|
## Default keys
|
|
|
|
| Key | Action |
|
|
| --- | --- |
|
|
| `h` / `l` | Focus the thread list / thread detail |
|
|
| `j` / `k` | Move between items or scroll the dashboard/focused detail |
|
|
| `?` | Show contextual keybinding help |
|
|
| `H` | Open application health and diagnostics |
|
|
| `d` | Open the current pull request dashboard |
|
|
| `e` | Edit the current PR title, target branch, and description from its dashboard |
|
|
| `a` | Enable or disable auto-merge from the dashboard |
|
|
| `M` | Merge now when GitHub reports that all represented requirements are satisfied |
|
|
| `/` | Fuzzy-search paths and filter with `status:`, `author:`, `updated:true` |
|
|
| `F` | Clear active thread filters |
|
|
| `n` / `N` | Next / previous thread with a new update |
|
|
| `c` | Compose a reply to the selected thread |
|
|
| `R` | Resolve or unresolve the selected thread |
|
|
| `ctrl-p` / `ctrl-n` | Choose the previous / next fuzzy-search or branch-completion match |
|
|
| `g` / `G` | First / last item |
|
|
| `enter` / `l` | Open the selected PR dashboard or its review threads |
|
|
| `enter` | Toggle the selected review thread |
|
|
| `za` | Toggle the selected thread |
|
|
| `ctrl-d` / `ctrl-u` | Scroll thread detail or page through lists |
|
|
| `tab` | Hide or reveal the thread list |
|
|
| `b` / `esc` | Return to the previous screen |
|
|
| `r` | Refresh now |
|
|
| `q` | Quit |
|
|
|
|
The Health modal reports the interactive loop, configuration, GitHub API,
|
|
rate-limit budget and reset/retry time, disk cache, unread-state persistence,
|
|
draft recovery, core PR data, and secondary enrichment. Session warnings and
|
|
errors are retained there with their component and timestamp. Long diagnostics
|
|
wrap to the modal width. Refresh activity occupies a stable informational row
|
|
so polling does not reorder the report. Press `H` from the picker, dashboard,
|
|
or thread view; `b` or `esc` closes it without changing the underlying scroll
|
|
position.
|
|
|
|
The reply composer appears inline beneath the selected thread so its code and
|
|
comments remain visible while writing. It supports multiple lines: `enter`
|
|
inserts a newline, `ctrl-s` opens the rendered confirmation preview, and `esc`
|
|
cancels. Replies and resolution changes require an explicit `y` confirmation.
|
|
Write keys remain disabled for cached snapshots, during refreshes, and whenever
|
|
GitHub does not grant the corresponding capability.
|
|
|
|
Auto-merge and immediate merge actions are available from the dashboard and
|
|
always require confirmation. The selected method is the repository's first
|
|
available method in `squash`, `merge`, then `rebase` preference order. Both
|
|
mutations include the currently displayed head commit OID, so a force-push or
|
|
new commit prevents a stale merge. “Merge now” is gated for drafts, conflicts,
|
|
required reviews, required checks, unresolved required conversations, closed
|
|
PRs, and branches that require a merge queue; GitHub performs the final
|
|
permission and mergeability validation.
|
|
|
|
The dashboard editor works with raw Markdown so template checklists can be
|
|
updated directly. The active line is highlighted without inserting a
|
|
layout-changing block character. It opens with the description focused;
|
|
`tab` and `shift-tab` move between the description, title, and target branch.
|
|
When the target branch is focused, repository branches are recommended using
|
|
the typed text, likely branch names, the current/default branch, and each
|
|
branch's latest commit time. The list updates as you type. Use `ctrl-p` and
|
|
`ctrl-n` to select the previous or next result, then `tab` or `enter` to complete it;
|
|
pressing `tab` again moves to the description.
|
|
|
|
With the default `editing.mode = "vim"`, the description starts in Normal mode.
|
|
It supports `hjkl`, `0`, `^`, `$`, `gg`, `G`, `w`/`W`, `b`/`B`, `e`/`E`,
|
|
`f`/`F`/`t`/`T` with `;` and `,`, `i`/`a`/`I`/`A`, `o`/`O`, `s`, and
|
|
`x`/`X`. `s` removes the character under the cursor and enters Insert mode.
|
|
Soft-wrapped rows behave as visual editor lines for vertical and line-local
|
|
motions, but do not add newlines to the Markdown submitted to GitHub.
|
|
`ctrl-d` and `ctrl-u` move the cursor and viewport down or up by half a page,
|
|
including while extending a Visual selection.
|
|
`v` starts character-wise Visual mode and `V` starts visual-line selection;
|
|
`d` or `x` deletes the selection, `y` copies it to the system clipboard, and
|
|
`p` pastes from the system clipboard. Normal mode uses a block cursor, while
|
|
Insert mode uses the terminal's hardware bar cursor at the boundary between
|
|
characters without hiding or shifting either character.
|
|
The description retains its raw Markdown while headings, emphasis, inline
|
|
code, links, quote markers, and HTML comments receive syntax highlighting.
|
|
Highlighting consists only of zero-width terminal styling and cannot alter
|
|
wrapping, selection, clipboard contents, cursor offsets, or submitted text.
|
|
`esc` returns from Insert to Normal mode; a second `esc` cancels the editor.
|
|
Set `editing.mode = "standard"` for direct insertion with arrow,
|
|
`home`, and `end` navigation. Title and target branch remain standard inputs
|
|
in either mode. `ctrl-s` opens an explicit confirmation. If the title,
|
|
description, or target branch changes remotely while the editor is open,
|
|
submission is blocked rather than overwriting the newer metadata.
|
|
|
|
## Current scope
|
|
|
|
The application can reply to review threads, resolve or unresolve them, update
|
|
the PR title, target branch, and description, enable or disable auto-merge, and
|
|
merge an eligible PR immediately. Comment reactions remain read-only. Other
|
|
write operations remain disabled. The dashboard shows the capability gate,
|
|
including why each action is unavailable. Read state
|
|
persists beside the configuration, and recent PR data is cached for offline
|
|
fallback. Check contexts and annotations are paginated. GitHub features which
|
|
depend on server-side context, such as unfurling issue references or displaying
|
|
uploaded images, are represented textually in the terminal. See
|
|
[`TODO.md`](TODO.md) for remaining read-only work and write-support preparation.
|