/qa pane: every question you asked this session with the start of Claude's answer; click to jump the transcript to the answer or the question.

Personal Claude Code mods, built or vendored at a reviewed commit. Each mod lives in its own folder and is listed in .claude-plugin/marketplace.json.
hooks/register.tsx 133 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Entry } from '../types'
5
6// A /qa pane listing every question you typed this session, newest first, with
7// the start of Claude's answer under it. A click scrolls the transcript to the
8// answer (or back to the question). It learns the rows as the main conversation
9// stores them (each row's id is the one its transcript row is drawn under) and
10// changes nothing. No network, no processes, no model calls.
11
12const PANE = 'qa'
13const HUMAN = new Set(['composer', 'bridge'])
14const KEEP = 300
15const SHOW = 40
16
17const entries = atom({ plugin: 'qa-jump', key: 'entries' } as const, [])
18
19const oneLine = (text: string, max: number) => {
20 const t = text
21 .replace(/```[\s\S]*?```/g, ' ')
22 .replace(/[#*_`>|]/g, '')
23 .replace(/\s+/g, ' ')
24 .trim()
25 return t.length > max ? t.slice(0, Math.max(1, max - 1)) + '…' : t
26}
27
28/** Typed (composer), sent from a phone (bridge), or sent for the person by another mod's button (`asUser`). */
29const byPerson = (origin: { kind: string; asUser?: boolean }) => HUMAN.has(origin.kind) || (origin.kind === 'plugin' && origin.asUser === true)
30
31/** A prompt folded into a running turn, out of the engine's framing around it. */
32const folded = (text: string) => {
33 const body = text.replace(/<\/?system-reminder>/g, '').trim()
34 return /^The user sent [^\n]*:\n([\s\S]*?)(?:\n\nThis is how Claude Code surfaces[\s\S]*)?$/.exec(body)?.[1]?.trim() ?? body
35}
36
37/** The transcript row a press scrolls to: the answer where it lands (else where it starts), or the question. */
38export const targetOf = (x: Entry, to: 'answer' | 'ask'): string | undefined => (to === 'ask' ? x.id : (x.answerId ?? x.firstAnswerId))
39
40async function jump($: EngineInterface, id: string | undefined) {
41 if (!id) return
42 const why = await $.ui
43 .scroll({ to: { requestId: id }, block: 'start' })
44 .then(r => r.deny)
45 .catch((err: unknown) => (err instanceof Error ? err.message : String(err)))
46 if (why) $.ui.toast(`qa-jump: couldn't scroll there (${why})`)
47}
48
49export const register: Register = on => {
50 on('session.start', async ($, e, next) => {
51 await $.command.register({ name: 'qa', description: 'Show or hide the questions pane: click one to jump to its answer' })
52 return next(e)
53 })
54
55 on('command.run', { command: 'qa' }, async $ => {
56 if ((await $.ui.panes()).some(p => p.id === PANE)) {
57 await $.ui.close({ id: PANE })
58 return { text: 'Questions pane closed.' }
59 }
60 await $.ui.open({ id: PANE, title: 'Questions' })
61 return { text: 'Questions pane opened.' }
62 })
63
64 // A prompt you typed starts an entry; the response rows after it are its
65 // answer: the first one opens it, the last one with text is where it lands.
66 // One typed while Claude works is folded into the running turn (a
67 // `queued_command` row by the delivery door) and starts an entry the same way.
68 // A prompt nobody typed (a task notification, a plugin, another agent) closes
69 // it: what answers that prompt isn't the answer to yours.
70 on('session.append', { door: ['prompt', 'response', 'delivery'] }, async ($, e, next) => {
71 const kept = await next(e)
72 if (e.agentId) return kept
73 const text = e.message.content
74 .map(b => (b.type === 'text' ? b.text : ''))
75 .join('\n')
76 .trim()
77 if (e.door !== 'response') {
78 const isFolded = e.door === 'delivery'
79 if (isFolded && e.message.name !== 'queued_command') return kept
80 if (!byPerson(e.origin)) {
81 if (!isFolded)
82 await update($, entries, list => {
83 const last = list.at(-1)
84 return !last || last.isClosed ? list : [...list.slice(0, -1), { ...last, isClosed: true }]
85 })
86 return kept
87 }
88 const asked = isFolded ? folded(text) : text
89 if (e.message.isMeta || !asked || asked.startsWith('/')) return kept
90 await update($, entries, list => [...list, { n: (list.at(-1)?.n ?? 0) + 1, id: e.uuid, question: oneLine(asked, 200) }].slice(-KEEP))
91 } else {
92 await update($, entries, list => {
93 const last = list.at(-1)
94 if (!last || last.isClosed) return list
95 const now: Entry = { ...last, firstAnswerId: last.firstAnswerId ?? e.uuid }
96 if (text) {
97 now.answerId = e.uuid
98 now.preview = oneLine(text, 160)
99 }
100 return [...list.slice(0, -1), now]
101 })
102 }
103 return kept
104 }).catch(($, e, next) => next(e))
105
106 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
107 const { Box, Button, Text } = $.ui.resolve(e)
108 const list = (await read($, entries)).slice(-SHOW).reverse()
109 const width = e.props.bodyColumns
110 return (
111 <Box flexDirection="column" width={width}>
112 {list.length === 0 && <Text dimColor>Questions you type show here; click one to jump to its answer.</Text>}
113 {list.map(x => (
114 <Box flexDirection="column">
115 <Box flexDirection="row" columnGap={1}>
116 <Text color="#38bdf8">{String(x.n).padStart(3)}</Text>
117 <Box flexGrow={1} flexShrink={1}>
118 <Text wrap="truncate-end">{x.question}</Text>
119 </Box>
120 {targetOf(x, 'answer') ? <Button key={`a-${x.n}`} label="answer" variant="primary" onPress={() => void jump($, targetOf(x, 'answer'))} /> : null}
121 <Button key={`q-${x.n}`} label="ask" onPress={() => void jump($, targetOf(x, 'ask'))} />
122 </Box>
123 <Text dimColor wrap="truncate-end">
124 {' ↳ '}
125 {x.preview ?? (x.firstAnswerId ? 'working…' : 'no answer yet')}
126 </Text>
127 </Box>
128 ))}
129 </Box>
130 )
131 })
132}
133types/index.d.ts 13 lines1/**
2 * One question you asked, and the transcript rows of Claude's reply to it.
3 * `isClosed`: a prompt you didn't type (a task notification, a plugin, another
4 * agent) came after it, so the response rows that follow aren't its answer.
5 */
6export type Entry = { n: number; id: string; question: string; firstAnswerId?: string; answerId?: string; preview?: string; isClosed?: boolean }
7
8declare module 'claude-code' {
9 interface PluginState {
10 'qa-jump': { entries: Entry[] }
11 }
12}
13