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

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).
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).
gh pr list --state all --limit 100 and caches the result for 60 seconds per directory, so the first # takes about a second.claude plugin validate .
claude plugin test .
claude --plugin-dir .hooks/register.ts 47 lines1import 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