SLOPSHOPPER

jr-draft

/jrdraft: a scratch pane to write your next prompt while Claude works, kept per project, sent to the prompt box when you are ready.

newpanebandcommandtoasttimer
★ 2v0.1.0MITupdated 2026-10-15jrlprost/claude-code-mods/plugins/jr-draft
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · jr-draft
│ ┃ jr draft ✕ › fix the failing auth test and add an audit log call │ ┃ jr draft app · 0 words · kept until you se │ ┃ Draft the next prompt here while Claude work ⏺ Read(src/auth.ts) │ ┃ [ Send to prompt box ] [ Clear ] ⎿ 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 │ │ › /jrdraft │ ⎿ jr-draft: jr draft opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · jr draft
jr draft app · 0 words · kept until you send it Draft the next prompt here while Claude works… ⏎ to prompt b [ Send to prompt box ] [ Clear ]
README

jr-draft

While Claude works, you already know what comes next. /jrdraft opens a scratch pane to write it, so the thought is not lost and the prompt box stays free.

jr draft   acme-site · 6 words · kept until you send it
┌──────────────────────────────────────────────────────┐
│ then add tests for the parser                         │
│                                                       │
└──────────────────────────────────────────────────────┘
[ Send to prompt box (s) ]  [ Clear (c) ]
  • One draft per project, kept across sessions until you send or clear it.
  • Enter, or Send to prompt box, puts the draft in your prompt box and empties the scratch area. You still press Enter in the prompt box to send it to Claude.
  • If the prompt box is busy, the draft stays where it is.

Commands

Command
/jrdraftOpen the scratch pane

In the pane: type freely (several lines), Enter or s sends to the prompt box, c clears, Esc closes.

Install

/plugin install jr-draft --marketplace jrlprost/claude-code-mods

Access

Keeps drafts in the plugin's own store and fills the prompt box when you ask. Reads no files, runs nothing, sends nothing over the network.

Source 4 files
hooks/register.tsx 101 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { gradientText } from './jr/draw'
5import { GRADIENTS } from './jr/gradient'
6
7const PANE = 'jr-draft'
8const draft = atom({ plugin: 'jr-draft', key: 'text' } as const, '')
9const project = atom({ plugin: 'jr-draft', key: 'project' } as const, '')
10const boot = { isDone: false }
11
12const keyOf = (name: string) => `draft:${name}`
13
14/** Command, project and saved draft, once: at session start or first thing after a mid-session install. */
15async function ensureReady($: EngineInterface) {
16  if (boot.isDone) return
17  boot.isDone = true
18  await $.command.register({ name: 'jrdraft', description: 'A scratch pane to draft your next prompt while Claude works' })
19  const root = await $.session.root()
20  const name = root.split('/').pop() || root
21  await update($, project, () => name)
22  const saved = (await $.store.get(keyOf(name))) as string | undefined
23  if (saved) await update($, draft, () => saved)
24}
25
26async function save($: EngineInterface, text: string) {
27  await update($, draft, () => text)
28  await $.store.set(keyOf(await read($, project)), text)
29}
30
31/** Puts the draft in the prompt box and empties the scratch area. */
32async function sendToPrompt($: EngineInterface) {
33  const text = await read($, draft)
34  if (!text.trim()) return
35  const filled = await $.prompt.fill({ text, mode: 'replace' })
36  if (filled.isFilled) {
37    await save($, '')
38    $.ui.toast('jr draft · in the prompt box: edit it or press Enter')
39  } else {
40    $.ui.toast('jr draft · the prompt box is busy; your draft is kept')
41  }
42}
43
44export const register: Register = on => {
45  on('session.start', async ($, e, next) => {
46    const started = await next(e)
47    await ensureReady($)
48    return started
49  })
50
51  on('ui.render', { component: 'AbovePrompt' }, ($, e, next) => {
52    if (!boot.isDone) {
53      $.clock.after(0, () => {
54        void ensureReady($)
55      })
56    }
57    return next(e)
58  })
59
60  on('command.run', { command: 'jrdraft' }, async $ => {
61    await ensureReady($)
62    await $.ui.open({ id: PANE, title: 'jr draft', focus: true, closeOnEscape: true, rows: 12 })
63    return { text: 'jr draft opened.' }
64  })
65
66  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
67    const elements = $.ui.resolve(e)
68    const { Box, Text, Button } = elements
69    const Input = 'Input' in elements ? elements.Input : undefined
70    const text = await read($, draft)
71    const name = await read($, project)
72    const words = text.trim() ? text.trim().split(/\s+/).length : 0
73    return (
74      <Box flexDirection="column">
75        <Box>
76          {gradientText(Text, 'jr draft', GRADIENTS.brand)}
77          <Text dimColor>{`   ${name} · ${words} word${words === 1 ? '' : 's'} · kept until you send it`}</Text>
78        </Box>
79        {Input ? (
80          <Input
81            key="draft"
82            value={text}
83            rows={6}
84            autoFocus
85            placeholder="Draft the next prompt here while Claude works…"
86            submitLabel="to prompt box"
87            onInput={(value: string) => void save($, value)}
88            onSubmit={(value: string) => void save($, value).then(() => sendToPrompt($))}
89          />
90        ) : (
91          <Text>{text || 'Open /jrdraft in the terminal or the desktop app to type here.'}</Text>
92        )}
93        <Box columnGap={2}>
94          <Button key="send" label="Send to prompt box" hotkey="s" onPress={() => void sendToPrompt($)} />
95          <Button key="clear" label="Clear" hotkey="c" onPress={() => void save($, '')} />
96        </Box>
97      </Box>
98    )
99  })
100}
101
hooks/jr/draw.tsx 35 lines
1// Drawing helpers shared by every jr mod. Synced into each plugin's hooks/jr/.
2import type { Elements } from 'claude-code'
3
4import { bar, paint } from './gradient'
5
6type TextElement = Elements['terminal']['Text']
7
8/** `text` in a gradient, one color per character, bold by default. */
9export const gradientText = (Text: TextElement, text: string, stops: readonly string[], bold = true) => (
10  <Text bold={bold}>
11    {paint(text, stops).map(({ char, color }) => (
12      <Text color={color}>{char}</Text>
13    ))}
14  </Text>
15)
16
17/** A gauge: filled cells in the gradient, empty cells dim. */
18export const gauge = (Text: TextElement, ratio: number, width: number, stops: readonly string[]) => (
19  <Text>
20    {bar(ratio, width, stops).map(cell =>
21      cell.empty ? (
22        <Text color={cell.color} dimColor>
23          {cell.char}
24        </Text>
25      ) : (
26        <Text color={cell.color}>{cell.char}</Text>
27      ),
28    )}
29  </Text>
30)
31
32/** The jr tag that opens every band line: a small gradient label. */
33export const tag = (Text: TextElement, label: string, stops: readonly string[]) =>
34  gradientText(Text, `${label} `, stops)
35
hooks/jr/gradient.ts 75 lines
1// Shared look of every jr mod: one palette, gradients drawn one cell at a time.
2// Synced into each plugin's hooks/jr/ by scripts/sync-shared.sh: edit it here only.
3
4export type Rgb = readonly [number, number, number]
5
6const hexToRgb = (hex: string): Rgb => {
7  const n = Number.parseInt(hex.replace('#', ''), 16)
8  return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
9}
10
11const rgbToHex = ([r, g, b]: Rgb) =>
12  `#${[r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join('')}`
13
14export const GRADIENTS = {
15  // The jr signature: violet to pink to amber.
16  brand: ['#7f5af0', '#ff5ca8', '#ffb86b'],
17  // Calm to alarm, for gauges where full is bad.
18  heat: ['#2cb67d', '#f4d35e', '#ff8c42', '#ef4565'],
19  // Cool, for neutral figures.
20  ice: ['#3da9fc', '#7f5af0'],
21  // Money and growth.
22  money: ['#2cb67d', '#a8e6cf'],
23} as const satisfies Record<string, readonly string[]>
24
25export type GradientName = keyof typeof GRADIENTS
26
27/** The color at `t` (0..1) along the stops. */
28export const colorAt = (stops: readonly string[], t: number): string => {
29  if (stops.length === 1) return stops[0]!
30  const clamped = Math.min(1, Math.max(0, t))
31  const span = clamped * (stops.length - 1)
32  const i = Math.min(stops.length - 2, Math.floor(span))
33  const a = hexToRgb(stops[i]!)
34  const b = hexToRgb(stops[i + 1]!)
35  const local = span - i
36  return rgbToHex([0, 1, 2].map(k => a[k]! + (b[k]! - a[k]!) * local) as unknown as Rgb)
37}
38
39/** One color per character of `text`, spread across the stops. */
40export const paint = (text: string, stops: readonly string[]) => {
41  const chars = [...text]
42  return chars.map((char, i) => ({ char, color: colorAt(stops, chars.length > 1 ? i / (chars.length - 1) : 0) }))
43}
44
45const EIGHTHS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']
46
47/**
48 * A bar `width` cells wide filled to `ratio` (0..1), with eighth-cell precision.
49 * Each filled cell takes the gradient color of its position along the whole bar,
50 * so a nearly full heat bar ends red and a low one stays green.
51 */
52export const bar = (ratio: number, width: number, stops: readonly string[]) => {
53  const filled = Math.min(1, Math.max(0, ratio)) * width
54  const full = Math.floor(filled)
55  const partial = EIGHTHS[Math.round((filled - full) * 7)] ?? ''
56  const cells: { char: string; color: string; empty?: true }[] = []
57  for (let i = 0; i < width; i++) {
58    const color = colorAt(stops, width > 1 ? i / (width - 1) : 0)
59    if (i < full) cells.push({ char: '█', color })
60    else if (i === full && partial) cells.push({ char: partial, color })
61    else cells.push({ char: '·', color, empty: true })
62  }
63  return cells
64}
65
66export const formatTokens = (n: number) =>
67  n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 10_000 ? `${Math.round(n / 1000)}k` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`
68
69export const formatDuration = (ms: number) => {
70  const minutes = Math.max(0, Math.round(ms / 60_000))
71  const h = Math.floor(minutes / 60)
72  const m = minutes % 60
73  return h > 0 ? `${h}h${String(m).padStart(2, '0')}` : `${m}min`
74}
75
types/index.d.ts 8 lines
1export type JrDraftText = string
2
3declare module 'claude-code' {
4  interface PluginState {
5    'jr-draft': { text: JrDraftText; project: string }
6  }
7}
8