/show redraws Claude's last reply as big cards in the transcript: done, open, next.

A Claude Code mod. /show redraws Claude's last reply as big cards in the transcript.
| Card | Colour | Means |
|---|---|---|
| ✓ DONE | green | finished |
| ○ OPEN | yellow | not done |
| → NEXT | magenta | what you must do or decide |
| ! WARN | red | a risk or an error |
| • INFO | cyan | a fact |
A small model (Haiku by default) sorts the reply into 2 to 6 cards, with at most 3 short lines each. The cards stay in the transcript, and Claude reads them as plain lines.
/plugin install show --marketplace whykothari/whystack
| Option | Default | What it does |
|---|---|---|
model | haiku | The model that sorts a reply into cards |
hooks/register.tsx 113 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Card, Tone } from '../types'
5
6// /show redraws Claude's last reply as big cards in the transcript. A small
7// model sorts the reply into cards. The command's row stores them as plain
8// lines, so the cards draw again after a reload, and the model reads them too.
9
10const COMMAND = 'show'
11
12const TONES: Record<Tone, string> = { done: 'green', open: 'yellow', next: 'magenta', warn: 'red', info: 'cyan' }
13const MARKS: Record<Tone, string> = { done: '✓', open: '○', next: '→', warn: '!', info: '•' }
14
15const lastAnswer = atom({ plugin: 'show', key: 'lastAnswer' } as const, '')
16
17const SYSTEM = `You turn an AI assistant's reply into big cards for a reader with ADHD.
18Return JSON only, no prose: {"cards":[{"tone":"done","title":"2 to 4 words","lines":["at most 8 words"]}]}
19- 2 to 6 cards. Each card has 0 to 3 lines.
20- tone is one of: done (finished), open (not done), next (what the reader must do or decide), warn (a risk or error), info (a fact).
21- Keep names, numbers, paths, and commands exact. Drop filler and reasons that change nothing.
22- Put a question or a step for the reader in one "next" card, last.`
23
24const FAILED = 'Could not draw the cards. Run /show again.'
25
26const CARD_LINE = /^\[(done|open|next|warn|info)\] (.+)$/
27
28export function toText(cards: Card[]): string {
29 return cards.map(card => [`[${card.tone}] ${card.title}`, ...card.lines.map(line => ` ${line}`)].join('\n')).join('\n')
30}
31
32export function fromText(text: string): Card[] | undefined {
33 const cards: Card[] = []
34 for (const row of text.split('\n')) {
35 const head = CARD_LINE.exec(row)
36 if (head) cards.push({ tone: head[1] as Tone, title: head[2], lines: [] })
37 else if (row.startsWith(' ') && cards.length) cards[cards.length - 1].lines.push(row.trim())
38 else if (row.trim()) return undefined
39 }
40 return cards.length ? cards : undefined
41}
42
43export function parseCards(reply: string): Card[] | undefined {
44 const start = reply.indexOf('{')
45 const end = reply.lastIndexOf('}')
46 if (start < 0 || end < start) return undefined
47 try {
48 const raw = JSON.parse(reply.slice(start, end + 1)).cards
49 if (!Array.isArray(raw)) return undefined
50 const cards = raw
51 .filter((c: any) => c && c.tone in TONES && typeof c.title === 'string' && c.title.trim())
52 .slice(0, 6)
53 .map((c: any): Card => ({
54 tone: c.tone,
55 title: c.title.trim().replace(/\s+/g, ' '),
56 lines: (Array.isArray(c.lines) ? c.lines : [])
57 .filter((l: unknown) => typeof l === 'string' && l.trim())
58 .slice(0, 3)
59 .map((l: string) => l.trim().replace(/\s+/g, ' ')),
60 }))
61 return cards.length ? cards : undefined
62 } catch {
63 return undefined
64 }
65}
66
67export const register: Register = (on, options) => {
68 on('session.start', async ($, e, next) => {
69 const started = await next(e)
70 await $.command.register({ name: COMMAND, description: "Redraw Claude's last reply as big cards" })
71 return started
72 })
73
74 on('turn.complete', async ($, e, next) => {
75 const result = await next(e)
76 if (e.agentId === undefined && e.reason === 'answer' && e.answer.trim()) {
77 const answer = e.answer
78 await update($, lastAnswer, () => answer)
79 }
80 return result
81 })
82
83 on('command.run', { command: COMMAND }, async $ => {
84 const answer = await read($, lastAnswer)
85 if (!answer) return { text: 'Nothing to show yet. Run /show after a reply.' }
86 const reply = await $.model.complete({
87 model: String(options?.model || 'haiku'),
88 system: SYSTEM,
89 prompt: answer.slice(-8000),
90 maxTokens: 800,
91 timeoutMs: 30000,
92 })
93 const cards = reply.isAnswered ? parseCards(reply.text) : undefined
94 return { text: cards ? toText(cards) : FAILED }
95 }).catch(() => ({ text: FAILED }))
96
97 on('ui.render', { component: 'CommandOutput', props: { command: COMMAND } }, async ($, e, next) => {
98 const cards = fromText(e.props.text)
99 if (!cards) return next(e)
100 const { Box, Text } = $.ui.resolve(e)
101 return (
102 <Box flexDirection="column" gap={1}>
103 {cards.map((card, index) => (
104 <Box key={`card-${index}`} flexDirection="column" borderStyle="round" borderColor={TONES[card.tone]} paddingX={2} paddingY={1}>
105 <Text bold color={TONES[card.tone]}>{MARKS[card.tone]} {card.title.toUpperCase()}</Text>
106 {card.lines.map(line => <Text>{line}</Text>)}
107 </Box>
108 ))}
109 </Box>
110 )
111 })
112}
113types/index.d.ts 10 lines1export type Tone = 'done' | 'open' | 'next' | 'warn' | 'info'
2
3export type Card = { tone: Tone; title: string; lines: string[] }
4
5declare module 'claude-code' {
6 interface PluginState {
7 show: { lastAnswer: string }
8 }
9}
10