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

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.
/plugin marketplace add PulseRH/claude-prompt-queue
/plugin install prompt-queue@pulserh-plugins
/reload-plugins
Then type /queue to show or hide it.
▥), or move it to a side window (◨).⇈ to top, ↑ ↓ reorder (the moved card lights up), ✎ edit, ➤ send now, ✕ delete. Buttons appear on hover (◉ turns that off).⚡): 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.◐), band height (⤢), paging (‹ ›), tooltips on every button.Built for the desktop Code tab; it also validates on the terminal surface.
claude plugin validate plugins/prompt-queue
claude plugin test plugins/prompt-queue
MIT
hooks/register.tsx 513 lines1import { 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}
513types/index.d.ts 29 lines1export 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