SLOPSHOPPER

prompt-queue

Queue prompts in a card grid above the input and send each later with a button, or auto-send them after each reply with a visible countdown

newpanebandspinnerrowscommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · prompt-queue
› 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 › /queue ⎿ prompt-queue: Prompt queue hidden (v0.3.1). ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-prompt-queue

A Claude Code mod that lets you queue prompts in a card grid above the input box and send each one later with a button, so you don't have to write prompts in another app and paste them in.

Install

/plugin marketplace add PulseRH/claude-prompt-queue
/plugin install prompt-queue@pulserh-plugins
/reload-plugins

Then type /queue to show or hide it.

What it does

  • Queue prompts in a 1, 2 or 3 column grid of cards (▥), or move it to a side window (◨).
  • Per card: ⇈ to top, ↑ ↓ reorder (the moved card lights up), ✎ edit, ➤ send now, ✕ delete. Buttons appear on hover (◉ turns that off).
  • Auto-send (⚡): after each finished reply it counts down (3-30 s, visibly) and sends the first queued prompt. Pick how many to send before it switches itself off (1-10 or no limit). Cancel during the countdown stops it.
  • The input expands to the full row while you are typing in it, and the buttons come back when focus leaves.
  • Card color (◐), band height (⤢), paging (‹ ›), tooltips on every button.
  • Each conversation has its own queue, saved between sessions (keyed by session id). Display settings and auto-send reset each session.

Built for the desktop Code tab; it also validates on the terminal surface.

Develop

claude plugin validate plugins/prompt-queue
claude plugin test plugins/prompt-queue

License

MIT

Source 2 files
hooks/register.tsx 513 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Flash, QueuedPrompt } from '../types'
5
6const PANE = 'prompt-queue'
7const VERSION = '0.3.1'
8const items = atom({ plugin: 'prompt-queue', key: 'items' } as const, [])
9const isOpen = atom({ plugin: 'prompt-queue', key: 'isOpen' } as const, true)
10const mode = atom({ plugin: 'prompt-queue', key: 'mode' } as const, 'band')
11const size = atom({ plugin: 'prompt-queue', key: 'size' } as const, 'M')
12const start = atom({ plugin: 'prompt-queue', key: 'start' } as const, 0)
13const history = atom({ plugin: 'prompt-queue', key: 'history' } as const, [])
14const numCols = atom({ plugin: 'prompt-queue', key: 'numCols' } as const, 2)
15const tone = atom({ plugin: 'prompt-queue', key: 'tone' } as const, 0)
16const isHover = atom({ plugin: 'prompt-queue', key: 'isHover' } as const, true)
17const editId = atom({ plugin: 'prompt-queue', key: 'editId' } as const, '')
18const draft = atom({ plugin: 'prompt-queue', key: 'draft' } as const, '')
19const rev = atom({ plugin: 'prompt-queue', key: 'rev' } as const, 0)
20// true while the input has the keyboard: it takes the whole header row, the controls step aside
21const isTyping = atom({ plugin: 'prompt-queue', key: 'isTyping' } as const, false)
22
23// auto-send: on/off is its own switch; the count and the delay are settings beside it
24const isAuto = atom({ plugin: 'prompt-queue', key: 'isAuto' } as const, false)
25const autoLimit = atom({ plugin: 'prompt-queue', key: 'autoLimit' } as const, 3)
26const autoLeft = atom({ plugin: 'prompt-queue', key: 'autoLeft' } as const, 0)
27const delay = atom({ plugin: 'prompt-queue', key: 'delay' } as const, 5)
28const countdown = atom({ plugin: 'prompt-queue', key: 'countdown' } as const, 0)
29const LIMITS = [1, 2, 3, 5, 10, -1] // -1 is no limit
30const DELAYS = [3, 5, 10, 15, 30]
31
32// the card that just moved, and how far its highlight has faded
33const flash = atom({ plugin: 'prompt-queue', key: 'flash' } as const, null)
34const ACCENT = '#d97757'
35const FADE = [0.9, 0.6, 0.35, 0.15]
36
37const BUDGET: Record<string, number> = { S: 5, M: 9, L: 15 }
38// card fills: warm Claude-ish brown, slate, and the live theme colours
39const TONES: { bg: string; fg?: string }[] = [
40  { bg: '#3a2e28' },
41  { bg: '#2b303b' },
42  { bg: 'claude', fg: 'inverseText' },
43  { bg: 'permission', fg: 'inverseText' },
44]
45const MAX_TEXT_LINES = 3
46
47const isHex = (c: string) => /^#[0-9a-f]{6}$/i.test(c)
48const mix = (from: string, to: string, t: number) => {
49  const ch = (s: string, i: number) => parseInt(s.slice(1 + i * 2, 3 + i * 2), 16)
50  const out = [0, 1, 2].map(i => Math.round(ch(from, i) + (ch(to, i) - ch(from, i)) * t))
51  return '#' + out.map(v => v.toString(16).padStart(2, '0')).join('')
52}
53
54export const register: Register = on => {
55  // the running countdown to the next auto-send (a hot reload drops it; session.start clears the number)
56  let pending: { cancel: () => void } | undefined
57  // each conversation keeps its own queue, under its own key in the plugin's store
58  let storeKey = 'items'
59
60  on('session.start', async ($, e, next) => {
61    const sid = await $.session.id()
62    storeKey = 'items:' + sid
63    let saved = (await $.store.get(storeKey)) as QueuedPrompt[] | undefined
64    if (saved === undefined) {
65      // the queue from before queues were per conversation moves to the first conversation that opens
66      const legacy = (await $.store.get('items')) as QueuedPrompt[] | undefined
67      if (legacy !== undefined) {
68        saved = legacy
69        await $.store.set(storeKey, legacy)
70        await $.store.delete('items')
71      }
72    }
73    await update($, items, () => saved ?? [])
74    await update($, countdown, () => 0)
75    await $.command.register({ name: 'queue', description: 'Show or hide the prompt queue' })
76    return next(e)
77  })
78
79  on('command.run', { command: 'queue' }, async $ => {
80    const open = await update($, isOpen, (v: boolean) => !v)
81    if (open && (await read($, mode)) === 'pane') {
82      await $.ui.open({ id: PANE, title: 'Prompt queue', focus: true })
83    }
84    if (!open) await $.ui.close({ id: PANE })
85    return { text: (open ? 'Prompt queue shown' : 'Prompt queue hidden') + ' (v' + VERSION + ').' }
86  })
87
88  on('turn.complete', async ($, e, next) => {
89    if (e.isAborted || e.reason !== 'answer' || pending) return next(e)
90    if (!(await read($, isAuto))) return next(e)
91    if (((await read($, items)) as QueuedPrompt[]).length === 0) return next(e)
92
93    // count down visibly so the person can cancel, then send the first prompt
94    const secs = (await read($, delay)) as number
95    await update($, countdown, () => secs)
96    pending = $.clock.every(1000, async () => {
97      const left = await update($, countdown, (n: number) => n - 1)
98      if (left > 0) return
99      pending?.cancel()
100      pending = undefined
101      await update($, countdown, () => 0)
102      if (!(await read($, isAuto))) return
103
104      const list = (await read($, items)) as QueuedPrompt[]
105      if (list.length === 0) return
106      const first = list[0]
107      const saved = await update($, items, (l: QueuedPrompt[]) => l.filter(i => i.id !== first.id))
108      await $.store.set(storeKey, saved)
109      const remaining = (await read($, autoLeft)) as number
110      if (remaining > 0) {
111        const now = await update($, autoLeft, (n: number) => n - 1)
112        if (now === 0) await update($, isAuto, () => false)
113      }
114      void $.prompt.submit({ text: first.text, asUser: true })
115    })
116    return next(e)
117  })
118
119  // the engine tells us whenever the focus ring moves: onto our input means expand, anywhere else collapse
120  on('ui.focus', async ($, e, next) => {
121    const mine = e.plugin === undefined || e.plugin === 'prompt-queue'
122    // a real focus move names the element; a plugin's own $.ui.focus call names it as `key`
123    const el = e.element ?? (e as { key?: string }).key ?? ''
124    const inInput = mine && el.startsWith('new-')
125    await update($, isTyping, () => inInput)
126    return next(e)
127  })
128
129  // sending a normal prompt means the person has moved on from the queue's input
130  on('prompt.submit', async ($, e, next) => {
131    await update($, isTyping, () => false)
132    return next(e)
133  })
134
135  on('ui.render', async ($, e, next) => {
136    const isBand = e.component === 'AbovePrompt'
137    const isPane = e.component === 'Pane' && e.requestId === PANE
138    if (!isBand && !isPane) return next(e)
139
140    const where = (await read($, mode)) as string
141    if (!(await read($, isOpen))) return next(e)
142    if (isBand && (where !== 'band' || e.props.hasSurvey)) return next(e)
143    if (isPane && where !== 'pane') return next(e)
144
145    const { Box, Text, Button, Input } = $.ui.resolve(e)
146    const list = (await read($, items)) as QueuedPrompt[]
147    const sz = (await read($, size)) as string
148    const hist = (await read($, history)) as number[]
149    let from = (await read($, start)) as number
150    if (from >= list.length) from = 0
151
152    const nc = (await read($, numCols)) as number
153    const toneIdx = (await read($, tone)) as number
154    const look = TONES[toneIdx % TONES.length]
155    const moved = (await read($, flash)) as Flash | null
156
157    const cols = Math.max(20, e.props.bodyColumns ?? 80)
158    const cellW = Math.floor((cols - 1) / nc)
159    // the desktop draws proportional text, so a cell holds more than one character
160    const fudge = e.surface === 'desktop' ? 1.5 : 1
161    const perLine = Math.max(8, Math.floor((cellW - 6) * fudge))
162    const budget = isBand ? BUDGET[sz] : Math.max(4, (e.viewport?.rows ?? 24) - 7)
163
164    // short prompt: one row, icons on the same line. Longer: text left, icons stacked right.
165    const ROW_CTRL = Math.round(14 * fudge)
166    const SIDE_CTRL = Math.round(9 * fudge)
167    // hover mode: the icons float over the card's corner on hover, so the text gets the whole card
168    const hoverMode = (await read($, isHover)) as boolean
169    const isInline = (t: string) => !hoverMode && t.length + 4 + ROW_CTRL <= perLine
170    const sideW = hoverMode ? perLine : Math.max(6, perLine - SIDE_CTRL)
171    const textLines = (t: string) =>
172      isInline(t) ? 1 : Math.min(MAX_TEXT_LINES, Math.max(1, Math.ceil((t.length + 4) / sideW)))
173    const cellHeight = (t: string) => (isInline(t) || hoverMode ? textLines(t) : Math.max(2, textLines(t))) + 1
174
175    // pack rows of nc cells until the line budget is used
176    const rows: QueuedPrompt[][] = []
177    let used = 0
178    let at = from
179    while (at < list.length) {
180      const group = list.slice(at, at + nc)
181      const h = Math.max(...group.map(p => cellHeight(p.text)))
182      if (rows.length > 0 && used + h > budget) break
183      rows.push(group)
184      used += h
185      at += group.length
186    }
187    const shown = at - from
188
189    const auto = (await read($, isAuto)) as boolean
190    const limit = (await read($, autoLimit)) as number
191    const left = (await read($, autoLeft)) as number
192    const secs = (await read($, delay)) as number
193    const ticking = (await read($, countdown)) as number
194    const typing = (await read($, isTyping)) as boolean
195    const editing = (await read($, editId)) as string
196    const draftText = (await read($, draft)) as string
197    const revision = (await read($, rev)) as number
198
199    const add = async (text: string) => {
200      const t = text.trim()
201      if (!t) return
202      const id = editing
203      const saved = id
204        ? await update($, items, (l: QueuedPrompt[]) => l.map(i => (i.id === id ? { ...i, text: t } : i)))
205        : await update($, items, (l: QueuedPrompt[]) => [...l, { id: String(Date.now()) + Math.random(), text: t }])
206      await $.store.set(storeKey, saved)
207      await update($, editId, () => '')
208      await update($, draft, () => '')
209      await update($, rev, (n: number) => n + 1)
210    }
211    // every keystroke: keep the draft in step (a redraw must not put the old text back) and expand while there is text
212    const typed = async (v: string) => {
213      await update($, draft, () => v)
214      await update($, isTyping, () => v.length > 0)
215    }
216    const startEdit = async (item: QueuedPrompt) => {
217      await update($, editId, () => item.id)
218      await update($, draft, () => item.text)
219      await update($, rev, (n: number) => n + 1)
220    }
221    const cancelEdit = async () => {
222      await update($, editId, () => '')
223      await update($, draft, () => '')
224      await update($, rev, (n: number) => n + 1)
225    }
226
227    // auto-send: one press turns it on or off; the count is set separately
228    const toggleAuto = async () => {
229      const on_ = await update($, isAuto, (v: boolean) => !v)
230      if (on_) {
231        await update($, autoLeft, () => limit)
232      } else {
233        pending?.cancel()
234        pending = undefined
235        await update($, countdown, () => 0)
236      }
237    }
238    const cycleLimit = async () => {
239      const next_ = LIMITS[(LIMITS.indexOf(limit) + 1) % LIMITS.length]
240      await update($, autoLimit, () => next_)
241      if (auto) await update($, autoLeft, () => next_)
242    }
243    const setLimit = async (n: number) => {
244      await update($, autoLimit, () => n)
245      if (auto) await update($, autoLeft, () => n)
246    }
247    const setDelay = async (s: number) => {
248      await update($, delay, () => s)
249    }
250    const cycleDelay = async () => {
251      await update($, delay, (s: number) => DELAYS[(DELAYS.indexOf(s) + 1) % DELAYS.length])
252    }
253    // cancel a send that is counting down, and stop auto-send so nothing follows it
254    const cancelSend = async () => {
255      pending?.cancel()
256      pending = undefined
257      await update($, countdown, () => 0)
258      await update($, isAuto, () => false)
259    }
260
261    const toggleHover = async () => {
262      await update($, isHover, (v: boolean) => !v)
263    }
264    const remove = async (id: string) => {
265      const saved = await update($, items, (l: QueuedPrompt[]) => l.filter(i => i.id !== id))
266      await $.store.set(storeKey, saved)
267    }
268    // the plugin UI cannot slide a card, so the one that moved lights up and fades
269    const lightUp = async (id: string, dir: number) => {
270      const seq = Math.random()
271      await update($, flash, () => ({ id, dir, step: 0, seq }))
272      void (async () => {
273        try {
274          for (const step of [1, 2, 3]) {
275            await $.clock.sleep(150)
276            await update($, flash, (f: Flash | null) => (f && f.seq === seq ? { ...f, step } : f))
277          }
278          await $.clock.sleep(150)
279          await update($, flash, (f: Flash | null) => (f && f.seq === seq ? null : f))
280        } catch {
281          // the plugin was reloaded mid-fade: the highlight just stops
282        }
283      })()
284    }
285    const move = async (id: string, by: number) => {
286      const saved = await update($, items, (l: QueuedPrompt[]) => {
287        const i = l.findIndex(x => x.id === id)
288        const j = i + by
289        if (i < 0 || j < 0 || j >= l.length) return l
290        const c = [...l]
291        c[i] = l[j]
292        c[j] = l[i]
293        return c
294      })
295      await $.store.set(storeKey, saved)
296      await lightUp(id, by)
297    }
298    // jump a prompt to the front of the queue
299    const toTop = async (id: string) => {
300      const saved = await update($, items, (l: QueuedPrompt[]) => {
301        const found = l.find(x => x.id === id)
302        return found ? [found, ...l.filter(x => x.id !== id)] : l
303      })
304      await $.store.set(storeKey, saved)
305      await update($, start, () => 0)
306      await update($, history, () => [])
307      await lightUp(id, -1)
308    }
309    const send = async (item: QueuedPrompt) => {
310      await remove(item.id)
311      void $.prompt.submit({ text: item.text, asUser: true })
312    }
313    const next_ = async () => {
314      await update($, history, (h: number[]) => [...h, from])
315      await update($, start, () => from + shown)
316    }
317    const prev = async () => {
318      const back = hist[hist.length - 1] ?? 0
319      await update($, history, (h: number[]) => h.slice(0, -1))
320      await update($, start, () => back)
321    }
322    const cycleSize = async () => {
323      await update($, size, (s: string) => (s === 'S' ? 'M' : s === 'M' ? 'L' : 'S'))
324      await update($, history, () => [])
325    }
326    const cycleCols = async () => {
327      await update($, numCols, (c: number) => (c >= 3 ? 1 : c + 1))
328      await update($, history, () => [])
329    }
330    const cycleTone = async () => {
331      await update($, tone, (t: number) => (t + 1) % TONES.length)
332    }
333    const toggleWhere = async () => {
334      const w = await update($, mode, (m: string) => (m === 'band' ? 'pane' : 'band'))
335      await update($, history, () => [])
336      if (w === 'pane') await $.ui.open({ id: PANE, title: 'Prompt queue', focus: true })
337      else await $.ui.close({ id: PANE })
338    }
339
340    // no native tooltip exists, so a button's name appears beside it while the pointer is over it
341    const tip = (name: string, button: unknown, text: string) => (
342      <Box key={'tip-' + name} flexDirection="row" alignItems="center">
343        {button}
344        <Box display="none" hover={{ display: 'flex' }} marginLeft={1}>
345          <Text dimColor>{text}</Text>
346        </Box>
347      </Box>
348    )
349
350    const cell = (item: QueuedPrompt, n: number) => {
351      const lines = textLines(item.text)
352      const inline = isInline(item.text)
353      const width = inline ? perLine - ROW_CTRL : sideW
354      const cap = lines * width - 4
355      const body = item.text.length > cap ? item.text.slice(0, Math.max(1, cap - 1)) + '…' : item.text
356      // the first prompt is already at the top, so it gets no jump-to-top button
357      const top = n > 1 ? tip('top-' + item.id, <Button key={'top-' + item.id} plain onPress={() => toTop(item.id)}>⇈</Button>, 'to top') : null
358      const up = tip('up-' + item.id, <Button key={'up-' + item.id} plain onPress={() => move(item.id, -1)}>↑</Button>, 'up one')
359      const dn = tip('dn-' + item.id, <Button key={'dn-' + item.id} plain onPress={() => move(item.id, 1)}>↓</Button>, 'down one')
360      const ed = tip('edit-' + item.id, <Button key={'edit-' + item.id} plain onPress={() => startEdit(item)}>✎</Button>, 'edit')
361      const go = tip('send-' + item.id, <Button key={'send-' + item.id} variant="primary" onPress={() => send(item)}>➤</Button>, 'send now')
362      const del = tip('del-' + item.id, <Button key={'del-' + item.id} dimColor onPress={() => remove(item.id)}>✕</Button>, 'delete')
363
364      const lit = moved && moved.id === item.id ? moved : null
365      const fill = lit ? (isHex(look.bg) ? mix(look.bg, ACCENT, FADE[lit.step]) : lit.step < 2 ? ACCENT : look.bg) : look.bg
366      const arrow = lit ? (lit.dir < 0 ? '↑ ' : '↓ ') : ''
367      return (
368        <Box
369          key={item.id}
370          flexDirection="row"
371          width={cellW - 1}
372          marginRight={1}
373          marginBottom={1}
374          paddingX={1}
375          borderStyle="round"
376          borderColor={fill}
377          backgroundColor={fill}
378        >
379          <Box flexGrow={1} flexShrink={1}>
380            <Text wrap="wrap" color={look.fg}>{arrow}{n}. {body}</Text>
381          </Box>
382          {hoverMode ? (
383            <Box
384              position="absolute"
385              top={0}
386              right={0}
387              display="none"
388              hover={{ display: 'flex' }}
389              flexDirection="row"
390              gap={1}
391              paddingX={1}
392              backgroundColor={fill}
393            >
394              {top}{up}{dn}{ed}{go}{del}
395            </Box>
396          ) : inline ? (
397            <Box flexDirection="row" flexShrink={0} gap={1}>{top}{up}{dn}{ed}{go}{del}</Box>
398          ) : (
399            <Box flexDirection="column" flexShrink={0} alignItems="flex-end">
400              <Box flexDirection="row" gap={1}>{top}{up}{dn}{del}</Box>
401              <Box flexDirection="row" gap={1}>{ed}{go}</Box>
402            </Box>
403          )}
404        </Box>
405      )
406    }
407
408    // lit up while auto-send is on, so the three controls read as one thing
409    const autoBg = auto ? mix('#2a2a2a', ACCENT, 0.35) : '#2e2e2e'
410
411    const barW = Math.min(secs, 15)
412    const filled = Math.ceil((ticking / secs) * barW)
413    const nextText = list[0]?.text ?? ''
414    const nextShort = nextText.length > 40 ? nextText.slice(0, 39) + '…' : nextText
415
416    return (
417      <Box flexDirection="column">
418        <Box flexDirection="row" gap={1}>
419          <Box flexGrow={1} flexShrink={1}>
420            <Input
421              key={'new-' + revision}
422              label={editing ? 'Edit: ' : 'Queue: '}
423              placeholder="type a prompt, Enter to add"
424              value={draftText}
425              submitLabel={editing ? 'save' : 'add'}
426              onSubmit={add}
427              onInput={(v: string) => typed(v)}
428            />
429          </Box>
430          {/* while typing, everything but the input steps out of the way (display none keeps it mounted) */}
431          <Box display={typing ? 'none' : 'flex'} flexDirection="row" gap={1} flexShrink={0}>
432          {/* the auto-send controls share one filled group; hovering it opens the pickers */}
433          <Box flexShrink={0} flexDirection="row" alignItems="center" gap={1} paddingX={1} backgroundColor={autoBg}>
434            {tip('auto', <Button key="auto" variant={auto ? 'primary' : 'secondary'} onPress={toggleAuto}>
435              {auto ? '⚡ On' : '⚡ Off'}
436            </Button>, 'auto-send after each reply')}
437            {/* each of these reveals its own choices beside it on hover: wider, never taller */}
438            <Box key="limgrp" flexDirection="row" alignItems="center">
439              <Button key="limit" plain onPress={cycleLimit}>
440                {auto ? (left < 0 ? '∞ left' : left + ' left') : '×' + (limit < 0 ? '∞' : limit)}
441              </Button>
442              <Box display="none" hover={{ display: 'flex' }} flexDirection="row" gap={1} marginLeft={1}>
443                <Text dimColor>send</Text>
444                {LIMITS.map(n => (
445                  <Button key={'lim-' + n} plain variant={n === limit ? 'primary' : 'secondary'} onPress={() => setLimit(n)}>
446                    {n < 0 ? '∞' : String(n)}
447                  </Button>
448                ))}
449              </Box>
450            </Box>
451            <Box key="dlygrp" flexDirection="row" alignItems="center">
452              <Button key="delay" plain onPress={cycleDelay}>{'⏱ ' + secs + 's'}</Button>
453              <Box display="none" hover={{ display: 'flex' }} flexDirection="row" gap={1} marginLeft={1}>
454                <Text dimColor>wait</Text>
455                {DELAYS.map(s => (
456                  <Button key={'dly-' + s} plain variant={s === secs ? 'primary' : 'secondary'} onPress={() => setDelay(s)}>
457                    {s + 's'}
458                  </Button>
459                ))}
460              </Box>
461            </Box>
462          </Box>
463          {editing && (
464            <Box flexShrink={0}>
465              <Button key="cancel" plain onPress={cancelEdit}>✕ Cancel</Button>
466            </Box>
467          )}
468          <Box flexShrink={0} flexDirection="row" gap={1}>
469            {tip('prev', <Button key="prev" plain dimColor={hist.length === 0} onPress={prev}>‹</Button>, 'previous page')}
470            <Text dimColor wrap="truncate-end">
471              {list.length === 0 ? '0' : `${from + 1}-${from + shown}/${list.length}`}
472            </Text>
473            {tip('next', <Button key="next" plain dimColor={from + shown >= list.length} onPress={next_}>›</Button>, 'next page')}
474          </Box>
475          {isBand && (
476            <Box flexShrink={0}>
477              {tip('size', <Button key="size" plain onPress={cycleSize}>{'⤢ ' + sz}</Button>, 'band height')}
478            </Box>
479          )}
480          <Box flexShrink={0}>
481            {tip('cols', <Button key="cols" plain onPress={cycleCols}>{'▥ ' + nc}</Button>, 'columns')}
482          </Box>
483          <Box flexShrink={0}>
484            {tip('hov', <Button key="hov" plain onPress={toggleHover}>{hoverMode ? '◉' : '○'}</Button>, 'card buttons on hover')}
485          </Box>
486          <Box flexShrink={0}>
487            {tip('tone', <Button key="tone" plain onPress={cycleTone}>◐</Button>, 'card color')}
488          </Box>
489          <Box flexShrink={0}>
490            {tip('where', <Button key="where" plain onPress={toggleWhere}>{isBand ? '◨' : '⬒'}</Button>, isBand ? 'side window' : 'above input')}
491          </Box>
492          </Box>
493        </Box>
494        {ticking > 0 && (
495          <Box flexDirection="row" gap={1} marginY={1}>
496            <Text bold color="claude">{'⏱ ' + ticking}</Text>
497            <Text color="claude">{'●'.repeat(filled) + '○'.repeat(barW - filled)}</Text>
498            <Box flexGrow={1} flexShrink={1}>
499              <Text wrap="truncate-end">{'sending "' + nextShort + '"'}</Text>
500            </Box>
501            <Button key="cdcancel" variant="primary" onPress={cancelSend}>✕ Cancel</Button>
502          </Box>
503        )}
504        {rows.map((pair, r) => (
505          <Box key={'row' + r} flexDirection="row">
506            {pair.map((item, c) => cell(item, from + r * nc + c + 1))}
507          </Box>
508        ))}
509      </Box>
510    )
511  })
512}
513
types/index.d.ts 29 lines
1export type QueuedPrompt = { id: string; text: string }
2export type Flash = { id: string; dir: number; step: number; seq: number }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'prompt-queue': {
7      items: QueuedPrompt[]
8      isOpen: boolean
9      mode: 'band' | 'pane'
10      size: 'S' | 'M' | 'L'
11      start: number
12      history: number[]
13      numCols: number
14      tone: number
15      isAuto: boolean
16      autoLimit: number
17      autoLeft: number
18      delay: number
19      countdown: number
20      flash: Flash | null
21      isHover: boolean
22      editId: string
23      draft: string
24      rev: number
25      isTyping: boolean
26    }
27  }
28}
29