SLOPSHOPPER

prompt-pin

Pins your latest prompt above the input; /prompts lists this session's prompts

newpanebandcommandprompt
v0.2.0MITupdated 2026-10-16AJMarquez99/prompt-pin
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · prompt-pin
│ ┃ Prompts ✕ › fix the failing auth test and add an audit log call │ ┃ ▶ 08:53 fix the failing auth test and add an │ ┃ audit log call ⏺ 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 │ │ › /prompts │ ⎿ prompt-pin: Prompts pane opened. │ │ 📌 fix the failing auth test and add an audit log call [ History ] ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
📌 fix the failing auth test and add an audit log call [ History ] ⟨Claude Code's own drawing⟩
Pane · Prompts
▶ 08:53 fix the failing auth test and add an audit log call
README

prompt-pin

A Claude Code mod that keeps your latest prompt in view.

  • The band: a 📌 line above the input shows the prompt you last sent, so you can see what Claude is working on. It dims while Claude is idle, and its History button opens the pane.
  • The pane: /prompts opens a list of this session's prompts, newest first, with the time each was sent. Run it again, or press Esc, to close it. It works mid-turn too.

Only your own prompts are recorded: ones typed at the terminal, sent over Remote Control, or from an SDK host. Task notifications, messages from other sessions and prompts other plugins submit are left out. The pane keeps the last 100.

Install

claude plugin marketplace add AJMarquez99/prompt-pin
claude plugin install prompt-pin@prompt-pin

Start a new session to load it. Tested with Claude Code 2.1.300.

Without an SSH key for GitHub, add the marketplace by its HTTPS URL instead:

claude plugin marketplace add https://github.com/AJMarquez99/prompt-pin.git

To update to the latest version:

claude plugin marketplace update prompt-pin
claude plugin update prompt-pin@prompt-pin

To uninstall:

claude plugin uninstall prompt-pin@prompt-pin
claude plugin marketplace remove prompt-pin

Behavior notes

  • Shares the band. Other mods that draw above the input still show, under the pin.
  • After /resume or /branch, the history is rebuilt from the transcript. The transcript keeps no times, so those rows show none. /clear starts an empty history.
  • Hidden while a survey is up, so the survey has the room.

Develop

claude --plugin-dir .      # load the working copy in a session
claude plugin test         # run the tests
claude plugin validate .   # check the manifest and module

tsconfig.json extends .claude-plugin/types/tsconfig.json, which Claude Code generates (and .gitignore excludes) the first time it loads the mod. Run claude --plugin-dir . once before type-checking with tsc.

The mod lives in hooks/register.tsx; text helpers are in hooks/format.ts.

License

MIT

Source 3 files
hooks/register.tsx 111 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { PromptEntry } from '../types'
5import { clip, clock, label, oneLine } from './format'
6
7const PANE = 'prompt-pin'
8const CAP = 100
9// A person's own prompts: typed at the terminal, or sent over Remote Control or the SDK host.
10const USER_ORIGINS: readonly string[] = ['composer', 'bridge', 'sdk']
11// The pane takes the keyboard so Esc closes it, like the engine's own views.
12const OPEN = { id: PANE, title: 'Prompts', focus: true, closeOnEscape: true } as const
13const history = atom({ plugin: 'prompt-pin', key: 'history' } as const, [] as PromptEntry[])
14
15// The toggle is not a prompt worth pinning.
16const isOwnCommand = (text: string): boolean => /^\/prompts(\s|$)/.test(text.trim())
17
18export const register: Register = on => {
19  on('session.start', async ($, e, next) => {
20    await $.command.register({ name: 'prompts', description: "Show this session's prompts in a pane", immediate: true })
21
22    return next(e)
23  })
24
25  // /resume and /branch reset $.state, so rebuild the history from the transcript.
26  // The transcript keeps no times, and other user-role rows (notifications, peer
27  // messages) arrive wrapped in a tag, so those are skipped.
28  on('classic.SessionStart', { source: ['resume', 'fork'] }, async ($, e, next) => {
29    const entries = (await $.session.messages())
30      .filter(m => m.role === 'user' && (m.toolResults ?? []).length === 0)
31      .map(m => m.text.trim())
32      .filter(text => text !== '' && !text.startsWith('<') && !isOwnCommand(text))
33      .map((text): PromptEntry => ({ text }))
34    await update($, history, () => entries.slice(-CAP))
35
36    return next(e)
37  })
38
39  on('command.run', { command: 'prompts' }, async $ => {
40    if ((await $.ui.panes()).some(pane => pane.id === PANE)) {
41      await $.ui.close({ id: PANE })
42
43      return { text: 'Prompts pane closed.' }
44    }
45
46    await $.ui.open(OPEN)
47
48    return { text: 'Prompts pane opened.' }
49  })
50
51  on('prompt.submit', async ($, e, next) => {
52    const out = await next(e)
53
54    // Record what entered, after any later mod's rewrite.
55    if (USER_ORIGINS.includes(e.origin.kind) && out.drop === undefined && !isOwnCommand(out.text)) {
56      const entry: PromptEntry = { text: out.text, at: await $.clock.now() }
57      await update($, history, list => [...list, entry].slice(-CAP))
58    }
59
60    return out
61  })
62
63  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
64    const { Box, Text } = $.ui.resolve(e)
65    const list = [...(await read($, history))].reverse()
66    // Approximates 3 wrapped rows by character count.
67    const max = Math.max(20, e.props.bodyColumns * 3 - 8)
68
69    return (
70      <Box flexDirection="column">
71        {list.length === 0 && <Text dimColor>No prompts yet.</Text>}
72        {list.map((entry, i) => (
73          <Text bold={i === 0} dimColor={i !== 0}>
74            {i === 0 ? '▶ ' : '  '}
75            {clock(entry.at)} {clip(oneLine(label(entry.text)), max)}
76          </Text>
77        ))}
78      </Box>
79    )
80  })
81
82  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
83    const list = await read($, history)
84    const latest = list[list.length - 1]
85
86    if (e.props.hasSurvey || latest === undefined) {
87      return next(e)
88    }
89
90    const { Box, Button, Text } = $.ui.resolve(e)
91    // Leave room for the button; the surface also truncates at the edge.
92    const max = Math.max(10, e.props.bodyColumns - 14)
93    // Keep what the mods after this one draw in the band, under the pin.
94    const theirs = await next(e)
95
96    return (
97      <Box flexDirection="column">
98        <Box flexDirection="row">
99          <Box flexGrow={1}>
100            <Text wrap="truncate-end" dimColor={!e.props.isWorking}>
101              📌 {clip(oneLine(label(latest.text)), max)}
102            </Text>
103          </Box>
104          <Button key="history" label="History" onPress={() => $.ui.open(OPEN)} />
105        </Box>
106        {theirs}
107      </Box>
108    )
109  })
110}
111
hooks/format.ts 16 lines
1export const oneLine = (text: string): string => text.replace(/\s+/g, ' ').trim()
2
3export const clip = (text: string, max: number): string =>
4  text.length <= max ? text : `${text.slice(0, Math.max(0, max - 1))}…`
5
6export const label = (text: string): string => (text.trim() === '' ? '(attachment)' : text)
7
8// Rebuilt prompts have no time; the blank keeps the column aligned.
9export const clock = (at: number | undefined): string => {
10  if (at === undefined) return '     '
11
12  const d = new Date(at)
13
14  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
15}
16
types/index.d.ts 9 lines
1// `at` is absent for a prompt rebuilt from the transcript after /resume or /branch.
2export type PromptEntry = { text: string; at?: number }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'prompt-pin': { history: PromptEntry[] }
7  }
8}
9