# diple 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 diple ``` 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 diple --repo owner/repository ``` With a repository selected, pass `--all` to include every open PR in that repository: ```sh diple --repo owner/repository --all --poll 15s ``` GitHub Enterprise Server can be used after authenticating that host: ```sh gh auth login --hostname github.example.com diple --repo owner/repository \ --endpoint https://github.example.com/api/graphql ``` ## Shell completion `diple` generates completion scripts without contacting GitHub or loading the configuration. Choose the command for your shell: ```sh # Bash: current session source <(diple completion bash) # Zsh: current session source <(diple completion zsh) # Fish: install for the current user diple completion fish > ~/.config/fish/completions/diple.fish ``` For persistent Bash completion, write the generated output to a directory loaded by your distribution's `bash-completion` package. For persistent Zsh completion, write it to a file named `_diple` in a directory on `$fpath`, then run `compinit`. `diple completion --help` lists the supported shells, while `diple --help` shows grouped command-line options, defaults, configuration precedence, and authentication behavior. ## Configuration The optional TOML configuration is loaded from `$DIPLE_CONFIG`, `$XDG_CONFIG_HOME/diple/config.toml`, or the operating system's user configuration directory at `diple/config.toml`. On Linux this is normally `~/.config/diple/config.toml`. On macOS, `~/Library/Application Support/diple/config.toml` is preferred, with `~/.config/diple/config.toml` automatically used as a fallback when it exists. For migration, `GH_THREADS_CONFIG` and existing `gh-threads` configuration or cache directories remain fallback locations when their new `diple` counterparts do not yet exist. ```toml theme = "dark" # built-in name, "custom", or an accessibility mode 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 [ai] # Experimental and local-only. Disabled unless explicitly enabled. Each run # still requires confirmation after its exact scope and redactions are shown. enabled = false provider = "codex-cli" # provider abstraction; only Codex CLI is implemented model = "" # empty uses the provider default for the whole run command = "codex" timeout = "3m" max_calls = 8 max_request_bytes = 180000 max_run_bytes = 900000 max_file_bytes = 150000 store_directory = "" # defaults beside config.toml, mode 0700/0600 exclude = ["*.lock", "go.sum", "package-lock.json", "vendor/", "node_modules/", "dist/", "build/", "generated/", "coverage/", "*.generated.*", "*_generated.*", "*.min.js", "*.map", ".env", ".env.*", "*.pem", "*.key", "*.p12", "*.pfx", "*credentials*"] [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"] ai = ["A"] [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 = [","] ``` Themes are compiled into `diple`; they do not require a separate download. Available names are `dark`, `light`, `catppuccin` (`catppuccin-mocha`), `catppuccin-latte`, `gruvbox` (`gruvbox-dark`), `gruvbox-light`, `one-dark-pro`, `github` (`github-dark`), `github-light`, `high-contrast`, and `no-color`. Set `theme = "custom"` to inherit a built-in palette and replace only the roles you care about: ```toml theme = "custom" [custom_theme] base = "catppuccin-mocha" # defaults to "dark" mode = "dark" # "dark" or "light"; controls Markdown rendering title = "#F5C2E7" active_foreground = "#1E1E2E" active_background = "#89B4FA" selection_background = "#313244" author_palette = ["#89B4FA", "#CBA6F7", "#94E2D5", "#F9E2AF"] syntax_theme = "catppuccin-mocha" ``` Every color override uses `#RRGGBB`. The complete set of roles is `title`, `dim`, `text`, `active_foreground`, `active_background`, `success`, `warning`, `error`, `editor_foreground`, `editor_background`, `pane_inactive`, `pane_active`, `quote`, `selection_background`, `suggestion_remove_background`, `suggestion_add_background`, `changed_remove_background`, and `changed_add_background`. `author_palette` accepts one or more colors. `syntax_theme` accepts an installed Chroma style name; invalid colors, bases, and syntax styles are reported as configuration errors at startup. The `[custom_theme]` table is ignored unless `theme = "custom"`. 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. ## Experimental local AI review Set `ai.enabled = true` to expose the `A` menu on the dashboard and thread screens. The initial provider uses the authenticated Codex CLI, so run `codex login` first. A full review creates clearly labelled `LOCAL AI · LOCAL ONLY` threads; it may also attach local-only context to unresolved GitHub threads. Pressing the normal reply key on a local AI thread starts a discussion with the same configured model. Resolving or unresolving those threads changes only the permission-restricted local per-PR state file. For small, self-contained replacements the model can include a standard GitHub-style suggestion in its local comment. These use the existing syntax-aware remove/add preview and remain local; diple does not apply or publish them. Every model run is manually confirmed. The preview shows the head commit, model, files, byte budget, call count, exclusions, and redaction count. Input comes exclusively from GitHub's authenticated PR diff and PR metadata: diple does not read the local checkout for AI review. Secret-like values are redacted, binary/generated/vendor/lock/oversized files are excluded, and the provider subprocess receives a small environment allowlist. Codex is launched ephemerally in an empty temporary directory with project instructions ignored, read-only sandboxing, approvals disabled, and all supported tool surfaces disabled. Any attempted tool event or malformed/out-of-range structured result fails the run closed. The AI menu distinguishes the inference-free provider status refresh from a provider test that makes one deliberately small structured model call. The test requires its own confirmation, consumes provider quota, and sends no PR content or local files. Preparing and running a review displays animated, stable progress; multi-chunk reviews report completed model calls. When the provider exposes a reasoning summary, diple shows a bounded, sanitized summary beside the progress bar. It never requests or displays hidden chain-of-thought. PR content is untrusted and is explicitly delimited as data in the model prompt. Results are validated against changed paths and lines, deduplicated, and retained as outdated when the PR head moves. No AI result is published to GitHub. Publishing proposed replies and additional providers remain future work; a future direct API provider must require no-training and zero-data- retention guarantees. 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, `diple` 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.