SLOPSHOPPER

pr-autocomplete

Typeahead for GitHub PRs when you type # in a git repo

newpromptprocess
A shopper browsing a rack in a slop shop
README

pr-autocomplete

A Claude Code mod that autocompletes GitHub pull requests in the prompt box. Type # inside a git repo and pick a PR from the typeahead; keep typing to filter by number (#63) or title (#beta).

Install

At the prompt of a Claude Code terminal session:

/plugin install pr-autocomplete --marketplace gugahoi/claude-mods

Answer y to add the marketplace, then pick a scope.

Needs the GitHub CLI installed and signed in (gh auth login).

How it works

  • Outside a git repo it does nothing.
  • Inside one it runs gh pr list --state all --limit 100 and caches the result for 60 seconds per directory, so the first # takes about a second.
  • It suggests up to 10 PRs, below Claude Code's own suggestions.

Develop

claude plugin validate .
claude plugin test .
claude --plugin-dir .
Source 1 files
hooks/register.ts 47 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3export type PR = { number: number; title: string; state: string }
4
5const TTL_MS = 60_000
6const cache = new Map<string, { at: number; prs: Promise<PR[]> }>()
7
8export async function load($: EngineInterface, cwd: string): Promise<PR[]> {
9  const git = await $.process.run(['git', 'rev-parse', '--is-inside-work-tree'], { cwd })
10  if (git.exitCode !== 0) return []
11  const gh = await $.process.run(
12    ['gh', 'pr', 'list', '--state', 'all', '--limit', '100', '--json', 'number,title,state'],
13    { cwd },
14  )
15  return gh.exitCode === 0 ? JSON.parse(gh.stdout) : []
16}
17
18async function prs($: EngineInterface): Promise<PR[]> {
19  const cwd = await $.session.cwd()
20  const hit = cache.get(cwd)
21  if (hit && Date.now() - hit.at < TTL_MS) return hit.prs
22  const fresh = load($, cwd).catch(() => {
23    cache.delete(cwd) // gh missing or the call was cut short: retry on the next keystroke
24    return []
25  })
26  cache.set(cwd, { at: Date.now(), prs: fresh })
27  return fresh
28}
29
30export const match = (list: readonly PR[], query: string) => {
31  const q = query.toLowerCase()
32  return list
33    .filter(p => String(p.number).startsWith(q) || p.title.toLowerCase().includes(q))
34    .slice(0, 10)
35}
36
37export const register: Register = on => {
38  on('prompt.autocomplete', { token: /^#/ }, async ($, e, next) => {
39    const [list, below] = await Promise.all([prs($), next(e)])
40    const mine = match(list, e.token.slice(1)).map(p => ({
41      text: `#${p.number}`,
42      description: `${p.state.toLowerCase()} · ${p.title}`,
43    }))
44    return { suggestions: [...below.suggestions, ...mine] }
45  })
46}
47