SLOPSHOPPER

handoff-autocomplete

Typeahead for recent handoff plan files when you type handoff

newprompt
A shopper browsing a rack in a slop shop
README

handoff-autocomplete

A Claude Code mod that autocompletes handoff plan files in the prompt box. Type handoff after /implement (or anywhere in a prompt) and pick a recent handoff file from the typeahead; it is replaced with the file's absolute path. Keep typing to filter by file name (handoff-learn).

Install

At the prompt of a Claude Code terminal session:

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

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

How it works

  • It lists *handoff*.md files in the OS temp directory ($TMPDIR, else /tmp), where the /handoff skill saves them.
  • It suggests up to 10, newest first, below Claude Code's own suggestions, each with its age and first heading.
  • Whatever follows handoff in the typed word must appear in the file name (case-insensitive): handoff-learn matches handoff-learn-pull-to-refresh.md.

Develop

claude plugin validate .
claude plugin test .
claude --plugin-dir .
Source 1 files
hooks/register.ts 46 lines
1import type { EngineInterface, PromptAutocompleteSuggestion, Register } from 'claude-code'
2
3let tmpdir: string | undefined // TMPDIR never changes for the life of the process
4
5export const ago = (ms: number) => {
6  const m = Math.floor(ms / 60_000)
7  if (m < 1) return 'just now'
8  if (m < 60) return `${m}m ago`
9  return m < 1440 ? `${Math.floor(m / 60)}h ago` : `${Math.floor(m / 1440)}d ago`
10}
11
12// First markdown heading, else the first non-empty line.
13export const heading = (text: string) => {
14  const lines = text.split('\n').map(l => l.trim()).filter(Boolean)
15  return (lines.find(l => l.startsWith('#')) ?? lines[0] ?? '').replace(/^#+\s*/, '')
16}
17
18// `query` is what follows `handoff` in the token, matched anywhere in the file name.
19export async function handoffs($: EngineInterface, query: string): Promise<PromptAutocompleteSuggestion[]> {
20  tmpdir ??= ((await $.env.get('TMPDIR')) ?? '/tmp').replace(/\/+$/, '')
21  const dir = tmpdir
22  const q = query.toLowerCase()
23  const files = (await $.fs.list(dir))
24    .filter(f => {
25      const name = f.name.toLowerCase()
26      return f.kind === 'file' && name.endsWith('.md') && name.includes('handoff') && name.includes(q)
27    })
28    .sort((a, b) => b.mtimeMs - a.mtimeMs)
29    .slice(0, 10)
30  return Promise.all(
31    files.map(async f => {
32      const text = `${dir}/${f.name}`
33      const title = heading(await $.fs.read(text).catch(() => ''))
34      const age = ago(Date.now() - f.mtimeMs)
35      return { text, label: f.name, description: title ? `${age} · ${title}` : age }
36    }),
37  )
38}
39
40export const register: Register = on => {
41  on('prompt.autocomplete', { token: /^handoff/i }, async ($, e, next) => {
42    const [mine, below] = await Promise.all([handoffs($, e.token.slice('handoff'.length)).catch(() => []), next(e)])
43    return mine.length ? { suggestions: [...below.suggestions, ...mine] } : below
44  })
45}
46