SLOPSHOPPER

git-agent-launcher

A Claude Code mod that opens a pane of every git-agent skill and command; pressing one stages /git-agent:<name> in the prompt for you to run, so a misclick…

newpanecommandtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · git-agent-launcher
│ ┃ git-agent ✕ › fix the failing auth test and add an audit log call │ ┃ git-agent is not installed. Run /plugin │ ┃ install git-agent@agentics-kit ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /git-agent-launcher │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · git-agent
git-agent is not installed. Run /plugin install git-agent@agentics-kit
README

git-agent-launcher

A clickable list of git-agent's skills and commands, so you never have to remember thirteen /git-agent:<name> spellings.

/git-agent-launcher opens a pane listing every git-agent entry with its description. Pressing one stages /git-agent:<name> in the prompt box. It does not run it. You add arguments if you want, and Enter is what runs it. This matters because commit-agent and pr-agent push without asking, so a misclick has to stay harmless.

This is a mod: a Claude Code plugin whose hooks/hooks.json names a hooks module. It has no skills, agents, or settings hooks.

Requirements

  • Claude Code 2.1.287+ in the terminal, 2.1.286+ in the Desktop Code tab. Mods are early access, and an older CLI does not load the module.
  • git-agent installed. Without it the pane shows the install command and nothing else.

Install

/plugin marketplace add shawn-sandy/agentics-kit
/plugin install git-agent-launcher@agentics-kit

Usage

  1. Type /git-agent-launcher. The pane opens and takes the keyboard.
  2. Press a digit 1–9 for the first nine entries, or click any entry. Entries 10–13 are reached by click, or with Tab and Enter.
  3. The prompt box now holds /git-agent:<name> , followed by whatever you had typed. The keyboard is back in the prompt box, so add arguments or press Enter.
Prompt before the pressEntry pressedPrompt after
(empty)commit-agent/git-agent:commit-agent
fix typo in readmecommit-bg/git-agent:commit-bg fix typo in readme
/git-agent:commit-agent fix typocommit-bg/git-agent:commit-bg fix typo

A command staged by an earlier press is replaced, not stacked, and what followed it is kept. The pane stays open for another press. Escape, or the pane's close mark, closes it. Ctrl+X Tab returns the keyboard to an open pane.

Where it draws

SurfaceWhat you get
TerminalThe pane: docked beside the transcript in the fullscreen layout, inline above the prompt otherwise
Desktop Code tabThe pane
VS Code chat panel, claude -p, cloud sessionsNo pane; the command answers with the list as text, one /git-agent:<name> — <description> per line

If a surface's prompt box does not accept the staged text, a toast names the command to type instead: Type /git-agent:<name> in the prompt.

Trust contract

Mods are unsandboxed and run with your permissions. This one reads the command list and the prompt box. It writes only the prompt box, its own pane, and a toast. claude plugin validate kit/plugins/git-agent-launcher should report exactly the following, and anything else is a defect:

./register.tsx hooks: session.start, session.attach, session.detach, command.run{command=git-agent-launcher}, ui.render{component=Pane, requestId=git-agent-launcher}
./register.tsx answers its own command: command.run{command=git-agent-launcher}
./register.tsx calls: $.command.list (via gitAgentEntries), $.command.register, $.prompt.fill (via stage), $.prompt.read (via stage), $.ui.open, $.ui.resolve, $.ui.toast (via stage)
  • Not in that report: $.fs, $.process, $.http, $.model, $.session, or $.env.
  • session.attach and session.detach: these hooks only observe. They track which drawing surfaces are attached, so a claude -p run, or a session whose last client has left, gets the text list instead of a pane nobody can see.
  • The one warning: the manifest's version: No version specified warning is expected. This repo keeps versions in marketplace.json only.

Plugin Structure

git-agent-launcher/
├── .claude-plugin/
│   └── plugin.json         # manifest, no version key
├── hooks/
│   ├── hooks.json          # { "modules": ["./register.tsx"] }
│   ├── register.tsx        # the hooks module
│   └── register.test.ts    # claude plugin test, terminal and desktop surfaces
├── tsconfig.json           # extends the API types the engine lays in .claude-plugin/types/ (gitignored)
├── CHANGELOG.md
└── README.md

Components

ComponentWhat it does
/git-agent-launcherRegistered on session.start; opens the pane (focus, closeOnEscape), or answers with the text list where no pane draws
Pane renderLists $.command.list() entries whose plugin is git-agent, in typeahead order: a plain Button per entry and its description, dimmed
Press$.prompt.read() and then $.prompt.fill(..., mode: 'replace'); a toast when the fill is refused

Development

claude --plugin-dir ./kit/plugins/git-agent-launcher   # load it for one session
claude plugin validate kit/plugins/git-agent-launcher   # the trust report above
claude plugin test kit/plugins/git-agent-launcher       # the regression tests
bash tests/plugins/test-mod-plugins.sh                  # both, for every mod; the merge gate runs this
Source 1 files
hooks/register.tsx 82 lines
1import type { CommandInfo, EngineInterface, Register } from 'claude-code'
2
3const PANE = 'git-agent-launcher'
4const PREFIX = 'git-agent:'
5const INSTALL = 'git-agent is not installed. Run /plugin install git-agent@agentics-kit'
6
7// A `claude -p` run has no surface, yet `ui.open` still answers isPlaced there,
8// so the text listing keys on whether a drawing surface is present: the one the
9// session started on, or a remote client still attached.
10let startedOnSurface = false
11const attached = new Set<string>()
12
13/** git-agent's skills and commands, in the order the typeahead lists them. */
14async function gitAgentEntries($: EngineInterface): Promise<CommandInfo[]> {
15  return (await $.command.list()).filter(c => c.plugin === 'git-agent')
16}
17
18/** The text answer for surfaces that draw no pane: one `/name — description` per line. */
19function listing(entries: CommandInfo[]): string {
20  return entries.length === 0 ? INSTALL : entries.map(c => `/${c.name} — ${c.description}`).join('\n')
21}
22
23/**
24 * Stages `/<name> ` in the prompt, never runs it: commit-agent and pr-agent
25 * push, so a misclick must stay harmless and Enter is what runs it. A command
26 * a previous press staged is replaced rather than stacked; what follows it is
27 * kept as arguments.
28 */
29async function stage($: EngineInterface, name: string): Promise<void> {
30  const { text: draft } = await $.prompt.read()
31  const command = `/${name}`
32  const args = draft.replace(/^\/git-agent:\S*\s*/, '')
33  const { isFilled } = await $.prompt.fill({ text: `${command} ${args}`, mode: 'replace' })
34  if (!isFilled) $.ui.toast(`Type ${command} in the prompt`)
35}
36
37export const register: Register = on => {
38  on('session.start', async ($, e, next) => {
39    startedOnSurface ||= e.surface !== null
40    await $.command.register({ name: 'git-agent-launcher', description: 'Open a pane of git-agent skills and commands' })
41    return next(e)
42  })
43
44  on('session.attach', ($, e, next) => {
45    attached.add(e.clientId)
46    return next(e)
47  })
48
49  on('session.detach', ($, e, next) => {
50    attached.delete(e.clientId)
51    return next(e)
52  })
53
54  on('command.run', { command: 'git-agent-launcher' }, async $ => {
55    const opened = await $.ui.open({ id: PANE, title: 'git-agent', focus: true, closeOnEscape: true })
56    if (opened.isPlaced && (startedOnSurface || attached.size > 0)) return {}
57    return { text: listing(await gitAgentEntries($)) }
58  })
59
60  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
61    const { Box, Text, Button } = $.ui.resolve(e)
62    const entries = await gitAgentEntries($)
63    if (entries.length === 0) return <Text>{INSTALL}</Text>
64
65    return (
66      <Box flexDirection="column" width={e.props.bodyColumns}>
67        <Text dimColor>Press an entry to stage it in the prompt · Esc closes</Text>
68        {entries.map((c, i) => {
69          const label = c.name.slice(PREFIX.length)
70          const hotkey = i < 9 ? { hotkey: String(i + 1) } : {}
71          return (
72            <Box flexDirection="column">
73              <Button key={label} label={label} plain {...hotkey} onPress={() => stage($, c.name)} />
74              <Text dimColor>{c.description}</Text>
75            </Box>
76          )
77        })}
78      </Box>
79    )
80  })
81}
82