SLOPSHOPPER

copy-blocks

Copy each quote or code block of Claude's last answer with one click, from buttons above the prompt.

newpanebandrowstoastprompt
★ 1v0.2.0MITupdated 2026-10-09y-hirakaw/claude-code-mods/copy-blocks
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · copy-blocks
│ ┃ copy-blocks ✕ › fix the failing auth test and add an audit log call │ ┃ Nothing to copy │ ⏺ 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 │ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · copy-blocks
Nothing to copy
README

claude-code-mods

Mods for Claude Code by y-hirakaw. Each installs and turns on or off on its own.

ModWhat it does
touch-mapShows which files Claude read, edited or never opened, as a tree and an activity map in a pane.
copy-blocksAdds copy buttons to the quotes and code blocks in Claude's answers, so you copy them without the terminal's line breaks.

touch-map

See how much of your codebase Claude actually looked at.

touch-map while Claude surveys a Unity project with two subagents: squares flash white for Claude and yellow for subagents, and the tree fills in

<sub>Claude and two subagents survey a Unity project, sped up. With the conversation beside it</sub>

You ask Claude something and get an answer. Did it read the file, or only see a few grep matches? Which parts did it never open? touch-map marks every file Claude touches in this session, down to the lines it read, in a pane beside the conversation.

Install

Needs Claude Code 2.1.287 or later. On Ghostty, use 1.3.1 or later: older versions may not pass clicks to the pane. From a shell:

claude plugin marketplace add y-hirakaw/claude-code-mods
claude plugin install touch-map@y-hirakaw-mods

Or inside Claude Code: run /plugin marketplace add y-hirakaw/claude-code-mods, then open /plugin, pick the marketplace, select touch-map and install it.

Then start a new session and run /touch-map once. From then on the pane opens by itself when the terminal is wide enough; otherwise run /touch-map.

git clone https://github.com/y-hirakaw/claude-code-mods
claude --plugin-dir ./claude-code-mods/touch-map

What the colors mean

Each file shows the deepest state it reached.

StateMeaningDetected from
<img src="docs/colors/created.svg" width="12" height="12"> createdClaude made the fileWrite, > redirects, mv, scripts
<img src="docs/colors/edited.svg" width="12" height="12"> editedClaude changed the fileEdit, >>, sed -i, scripts
<img src="docs/colors/read.svg" width="12" height="12"> readClaude saw the whole fileRead, cat, @file mentions
<img src="docs/colors/partial.svg" width="12" height="12"> partialClaude saw only part of itRead with a line range, head / tail / sed -n, grep matches
<img src="docs/colors/listed.svg" width="12" height="12"> listedClaude only saw the namels, find, grep -l, git status and similar output
<img src="docs/colors/deleted.svg" width="12" height="12"> deletedClaude removed the filerm, git rm, mv
<img src="docs/colors/auto.svg" width="12" height="12"> autoLoaded into context automaticallyCLAUDE.md and rules files

Reading the pane

The pane: the header, the activity map, and the tree with directory bars, files colored by state, dotted bars for grep-only reads and untouched entries folded into one line

  • A directory's ■■■■■■■■■■ is the share of its files in each state, then touched/total.
  • A file has a bar only when Claude did not see all of it: ━━━─── marks the lines it read, ┄┄┄ means only grep matches. A file read in full says so with the color of its name.
  • The activity map lays every file out in path order, so files of one directory sit together. A square flashes white when Claude touches it, yellow when a subagent does.
  • Click ▾ / ▸ to open or close a directory. Untouched files fold into one N untouched: … line.
  • [ save ] writes the record to ~/.claude/touch-map-logs/ and starts over. It is also saved on /clear, compaction and exit.
  • The pane's × shrinks it to one line above the prompt.
  • A file read inside a git worktree counts as the same path in the repository.

Commands

Command
/touch-mapOpen the pane
/touch-map clearSave the record and start over
/touch-map discardStart over without saving
/touch-map minShrink to one line above the prompt
`/touch-map map [on\off]`Show or hide the activity map
`/touch-map debug [on\off]`Write how each event was judged, and the pane as text, to ~/.claude/touch-map-logs/debug/

What it reads, runs and writes

  • Reads only what passes through Claude Code in this session: tool calls and their results, @file attachments and instruction-file loads. Nothing is sent anywhere.
  • Runs printenv HOME, git ls-files, git worktree list and git status, and nothing else, never through a shell. git status runs only after a Bash command that may have written files.
  • Writes ~/.claude/touch-map-logs/<time>.json when a record is saved, and debug files only while debug output is on.
  • Bash commands are read heuristically. Script writes are caught with git status and modification times, so files ignored by git are not seen, and a file another process writes while the command runs is counted as Claude's.
  • On a 50,000-file repository, git status adds about 0.1 s per writing command.
  • Other sessions (including the background sessions in the agents list) and other terminals are not tracked.
  • The activity map is drawn only in the terminal.

copy-blocks

Copy a quote or code block from Claude's answer without the terminal's line breaks.

copy-blocks: Claude answers with two quotes and a code block, each with a "⧉ copy" header line above it; clicking a header flashes "✓ copied" and the text is pasted back into the prompt

<sub>Copying from the answer and pasting into the prompt. With the row above the prompt</sub>

Selecting text in the terminal copies the wrapped lines and the │ gutter with it. copy-blocks puts a copy button on each > quote and code block, right above the block in the answer, and in a row above the prompt for Claude's last answer:

copy 1 ❝ Thanks for the update… · 2 sh git switch -c featu… · 3 tsx export const regist… 7L ×
  • In the answer, the header line above each block is the button: click anywhere on it. Earlier answers keep their buttons.
  • Code blocks show their language and the first line that is not an import or a bracket. Blocks of 5 lines or more show their line count.
  • The row stays on one line: labels shorten as the terminal narrows, the rest folds into +N, which opens a pane listing every block, and below 50 columns only the numbers remain.
  • A copied button turns into ✓ copied for a moment; in the row it keeps a ✓.
  • The row goes away when you send your next prompt, or with ×.
  • Choose where the buttons appear in /config → copy-blocks buttons: both (default), inline (in the answer only) or band (the row above the prompt only). With band, answers are drawn exactly as Claude Code draws them.

Install

claude plugin marketplace add y-hirakaw/claude-code-mods
claude plugin install copy-blocks@y-hirakaw-mods

Needs Claude Code 2.1.287 or later. Nothing to run afterwards: the buttons appear after Claude's next answer.

What it reads, runs and writes

  • Reads Claude's final answer at the end of each turn (subagents' answers and interrupted turns are skipped), and, unless set to band, the text of each answer as it is drawn, to place the buttons. Your stored conversation is not changed.
  • Runs nothing, and sends nothing over the network.
  • Writes the clipboard, only when you click a button, the same way /copy does. It writes no files.

Developing a mod: see CLAUDE.md.

日本語

modできること
touch-mapClaude がどのファイルを読んだか、変えたか、開かなかったかを、ペインにツリーとマップで表示する
copy-blocksClaude の返事にある引用やコードブロックにコピーボタンを付け、余計な改行なしにコピーできるようにする

touch-map は、Claude がこのセッションで触ったファイルを created・edited・read・partial・listed・deleted・auto に分けて、会話の横のペインにツリーとアクティビティマップで表示する Claude Code の mod です。触っていないところも畳んで見せるので、「どこまで見て答えたのか」がわかります。

claude plugin marketplace add y-hirakaw/claude-code-mods
claude plugin install touch-map@y-hirakaw-mods

Claude Code 2.1.287 以上が必要です。Ghostty は 1.3.1 以上を推奨します(古い版ではペインのボタンがクリックできないことがあります)。

インストールしたら、新しいセッションで一度 /touch-map を実行してください。外部に何かを送ることはなく、実行するのは printenv HOME・git ls-files・git worktree list・git status だけです。

copy-blocks は、Claude の返事にある引用(>)とコードブロックにコピーボタンを付ける mod です。ボタンは返事の中のブロックの直上と、入力欄の上の帯に出ます。ターミナルで選択したときのような余計な改行や │ は入りません。出す場所は /config の copy-blocks buttons で both(初期値)・inline(返事の中だけ)・band(帯だけ)から選べます。band なら返事の表示には手を加えません。

claude plugin install copy-blocks@y-hirakaw-mods

copy-blocks はコマンドを実行せず、外部に何も送らず、書き込むのはボタンを押したときのクリップボードだけです。

License

MIT

Source 2 files
hooks/register.tsx 337 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Block } from '../types'
5
6const blocks = atom({ plugin: 'copy-blocks', key: 'blocks' } as const, [] as Block[])
7const copied = atom({ plugin: 'copy-blocks', key: 'copied' } as const, [] as number[])
8const flash = atom({ plugin: 'copy-blocks', key: 'flash' } as const, null as number | null)
9const inline = atom({ plugin: 'copy-blocks', key: 'inline' } as const, null as string | null)
10
11const PANE = 'copy-blocks'
12// 抜き出すのは 9 個まで
13const MAX = 9
14// ラベルの長さ。入りきらないときは SHORT まで縮める
15const LABEL = 20
16const SHORT = 12
17// この幅より狭い帯では、番号と種類の印だけにする
18const NARROW = 50
19// 5 行以上のブロックには行数を添える
20const MANY_LINES = 5
21// 「✓ copied」を出しておく時間
22const FLASH_MS = 1500
23
24// lines[i] から始まる引用(> で始まる行の続き)かコードブロック(``` で囲んだ部分)の終わりと中身。どちらでもなければ null
25// 中身が空のブロックは block を持たない
26function blockAt(lines: string[], i: number): { end: number; block?: Block } | null {
27  const line = lines[i] ?? ''
28  const fence = line.match(/^\s*(`{3,}|~{3,})\s*([\w+#.-]*)/)
29  if (fence) {
30    const close = fence[1] ?? '```'
31    const lang = fence[2] ?? ''
32    let end = i + 1
33    while (end < lines.length && !(lines[end] ?? '').trim().startsWith(close)) end += 1
34    const body = lines.slice(i + 1, end)
35    end += 1
36    if (!body.some(l => l.trim() !== '')) return { end }
37    const text = body.join('\n')
38    return { end, block: lang ? { kind: 'code', text, lang } : { kind: 'code', text } }
39  }
40  if (/^\s*>/.test(line)) {
41    let end = i
42    while (end < lines.length && /^\s*>/.test(lines[end] ?? '')) end += 1
43    const body = lines.slice(i, end).map(l => l.replace(/^\s*> ?/, ''))
44    if (!body.some(l => l.trim() !== '')) return { end }
45    return { end, block: { kind: 'quote', text: body.join('\n').trim() } }
46  }
47  return null
48}
49
50// 回答を、地の文と、引用・コードブロックに切り分ける。raw は描くための元の markdown、block はコピーする中身
51export type Part = { raw: string; block?: Block }
52export function split(text: string): Part[] {
53  const out: Part[] = []
54  const lines = text.split('\n')
55  let prose: string[] = []
56  const flush = () => {
57    if (prose.join('').trim() !== '') out.push({ raw: prose.join('\n') })
58    prose = []
59  }
60  let i = 0
61  while (i < lines.length) {
62    const found = blockAt(lines, i)
63    if (!found) {
64      prose.push(lines[i] ?? '')
65      i += 1
66      continue
67    }
68    const raw = lines.slice(i, found.end).join('\n')
69    if (found.block) {
70      flush()
71      out.push({ raw, block: found.block })
72    } else prose.push(raw)
73    i = found.end
74  }
75  flush()
76  return out
77}
78
79// 引用とコードブロックを、出てきた順に抜き出す(帯とペインに並べる分)
80export function extract(answer: string): Block[] {
81  return split(answer)
82    .flatMap(p => (p.block ? [p.block] : []))
83    .slice(0, MAX)
84}
85
86// 端末で何セル使うか。全角は 2 セル
87export function cellsOf(s: string): number {
88  let n = 0
89  for (const ch of s) {
90    const c = ch.codePointAt(0) ?? 0
91    // ❝ は端末によって 2 セルで描かれるので、広いほうで数える
92    n += c === 0x275d || (c >= 0x1100 && c <= 0x115f) || (c >= 0x2e80 && c <= 0xa4cf) || (c >= 0xac00 && c <= 0xd7a3) || (c >= 0xf900 && c <= 0xfaff) || (c >= 0xfe30 && c <= 0xfe4f) || (c >= 0xff00 && c <= 0xff60) || (c >= 0xffe0 && c <= 0xffe6) || c >= 0x1f300 ? 2 : 1
93  }
94  return n
95}
96
97const cut = (s: string, width: number): string => {
98  if (cellsOf(s) <= width) return s
99  let out = ''
100  for (const ch of s) {
101    if (cellsOf(out + ch) > width - 1) break
102    out += ch
103  }
104  return out + '…'
105}
106
107// import や括弧だけの行、シバンを飛ばした、最初の意味のある行
108const NOISE = /^(import\b|from\b|#!|[\s()[\]{};,]*$)/
109export function labelOf(b: Block, width = LABEL): string {
110  const lines = b.text.split('\n').map(l => l.trim())
111  const first = lines.find(l => l !== '' && !(b.kind === 'code' && NOISE.test(l))) ?? lines.find(l => l !== '') ?? ''
112  return cut(first, width)
113}
114
115const tagOf = (b: Block): string => (b.kind === 'quote' ? '❝' : b.lang ?? '‹›')
116const linesOf = (b: Block): number => b.text.split('\n').length
117
118type Item = { index: number; tag: string; label: string; meta: string }
119
120// 帯に並べるものを、幅に合わせて 3 段階で決める。入りきらない分は rest に回して「+N」にする
121export function fit(list: Block[], columns: number): { items: Item[]; rest: number } {
122  const item = (b: Block, i: number, width: number | null): Item => ({
123    index: i,
124    tag: tagOf(b),
125    label: width === null ? '' : labelOf(b, width),
126    meta: width !== null && linesOf(b) >= MANY_LINES ? `${linesOf(b)}L` : '',
127  })
128  // 「copy 」と「 ×」、項目の間の「 · 」。端末ごとの幅の数え違いに備えて 2 セル余らせる
129  const widthOf = (items: Item[], rest: number) =>
130    5 + 2 + 2 + items.reduce((n, it) => n + cellsOf(`${it.index + 1} ${it.tag}${it.label ? ' ' + it.label : ''}${it.meta ? ' ' + it.meta : ''}`) + 3, 0) + (rest > 0 ? cellsOf(`+${rest}`) + 1 : 0)
131  if (columns >= NARROW) {
132    const full = list.map((b, i) => item(b, i, LABEL))
133    if (widthOf(full, 0) <= columns) return { items: full, rest: 0 }
134    const short = list.map((b, i) => item(b, i, SHORT))
135    for (let n = short.length; n >= 1; n--) if (widthOf(short.slice(0, n), short.length - n) <= columns) return { items: short.slice(0, n), rest: short.length - n }
136  }
137  const bare = list.map((b, i) => item(b, i, null))
138  for (let n = bare.length; n >= 1; n--) if (widthOf(bare.slice(0, n), bare.length - n) <= columns) return { items: bare.slice(0, n), rest: bare.length - n }
139  return { items: bare.slice(0, 1), rest: bare.length - 1 }
140}
141
142// クリップボードに書く。手応えは ✓ で返すので、トーストはうまくいかなかったときだけ
143async function toClipboard($: any, text: string, surface: any): Promise<boolean> {
144  const r = await $.ui.copy({ text, surface })
145  if (!r.isCopied) $.ui.toast(`Not copied: ${r.reason}`)
146  return r.isCopied
147}
148
149async function copy($: any, list: Block[], i: number, surface: any): Promise<void> {
150  const b = list[i]
151  if (!b) return
152  if (!(await toClipboard($, b.text, surface))) return
153  await update($, copied, (xs: number[]) => (xs.includes(i) ? xs : [...xs, i]))
154  await update($, flash, () => i)
155  await $.clock.sleep(FLASH_MS)
156  await update($, flash, (f: number | null) => (f === i ? null : f))
157}
158
159// 番号と本文を、同じホバーのグループで光る 2 つのボタンにする。番号は明るく、本文は薄く
160// site は帯とペインでキーが重ならないようにする接頭辞
161function entry($: any, e: any, list: Block[], it: Item, done: number[], lit: number | null, site = 'copy') {
162  const { Box, Button, Text } = $.ui.resolve(e)
163  const i = it.index
164  const scope = `copy-${i}`
165  const press = (p: { surface: unknown }) => void copy($, list, i, p.surface)
166  const hover = { scope, inverse: true }
167  const body = [it.tag, it.label, it.meta].filter(Boolean).join(' ')
168  return (
169    // 帯が詰まっても番号は縮めない。縮むのは本文のほうだけ
170    <Box key={`${site}-entry-${i}`} flexDirection="row" columnGap={1} flexShrink={0}>
171      <Box flexShrink={0}>
172        <Button key={`${site}-${i}-n`} label={String(i + 1)} plain hover={hover} onPress={press} />
173      </Box>
174      {lit === i ? (
175        <Text color="green">{`✓ copied${linesOf(list[i] as Block) > 1 ? ' ' + linesOf(list[i] as Block) + 'L' : ''}`}</Text>
176      ) : (
177        <Button key={`${site}-${i}`} label={done.includes(i) ? `✓ ${body}` : body} plain dimColor hover={hover} onPress={press} />
178      )}
179    </Box>
180  )
181}
182
183// 回答の中のボタンで押したブロックをコピーする。中身はボタンを描いたときの本文から取るので、次のターンのあとも押せる
184async function copyInline($: any, key: string, text: string, surface: any): Promise<void> {
185  if (!(await toClipboard($, text, surface))) return
186  await update($, inline, () => key)
187  await $.clock.sleep(FLASH_MS)
188  await update($, inline, (k: string | null) => (k === key ? null : k))
189}
190
191// ヘッダーの見出し。「bash · 2 lines」「quote」
192const headOf = (b: Block): string => [b.kind === 'quote' ? 'quote' : b.lang ?? 'code', linesOf(b) > 1 ? `${linesOf(b)} lines` : ''].filter(Boolean).join(' · ')
193
194// 見出しを挟む罫線。画面の端までは伸ばさず、⧉ copy と合わせて RULE セルほどで止める
195const RULE = 40
196export const ruleOf = (b: Block): string => {
197  const head = `── ${headOf(b)} `
198  return head + '─'.repeat(Math.max(2, RULE - cellsOf(head) - cellsOf(' ⧉ copy')))
199}
200
201// 本文の文字列から短い印を作る。同じ回答を描き直しても同じ key になるように
202export const hashOf = (s: string): string => {
203  let h = 0
204  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0
205  return (h >>> 0).toString(36)
206}
207
208export const register: Register = (on, options) => {
209  // /config の「copy-blocks buttons」。回答の中(inline)、プロンプトの上の帯(band)、両方(both)
210  const placement = options?.placement ?? 'both'
211  const inlineOn = placement !== 'band'
212  const bandOn = placement !== 'inline'
213
214  // 本体の返事だけを見る。サブエージェントの返事と、止めたターンは飛ばす
215  on('turn.complete', async ($, e, next) => {
216    if (e.agentId === undefined && !e.isAborted) {
217      await update($, blocks, () => extract(e.answer))
218      await update($, copied, () => [])
219      await update($, flash, () => null)
220    }
221    return next(e)
222  })
223
224  // 次のプロンプトを送ったら片付ける
225  on('prompt.submit', ($, e, next) => {
226    void update($, blocks, () => []).catch(() => {})
227    void $.ui.close({ id: PANE }).catch(() => {})
228    return next(e)
229  })
230
231  // 「+N」から一覧のペインを開く。人の操作として数えられるよう、ボタンの onPress ではなくここで開く
232  on('ui.press', { element: 'more' }, async ($, e, next) => {
233    await $.ui.open({ id: PANE, title: 'Copy blocks', closeOnEscape: true })
234    return next(e)
235  })
236
237  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
238    if (!bandOn) return next(e)
239    const list = await read($, blocks)
240    if (list.length === 0 || e.props.hasSurvey || e.props.isWorking) return next(e)
241    const done = await read($, copied)
242    const lit = await read($, flash)
243    const { Box, Button, Text } = $.ui.resolve(e)
244    const { items, rest } = fit(list, e.props.bodyColumns)
245    // 帯はほかの mod と分け合う。下の mod が描くもの(touch-map の最小化した行など)を消さないよう、ボタンの行の下に並べる
246    const below = await next(e)
247    const row: any[] = []
248    items.forEach((it, n) => {
249      if (n > 0) row.push(<Text key={`sep-${n}`} dimColor>·</Text>)
250      row.push(entry($, e, list, it, done, lit))
251    })
252    const mine = (
253      <Box flexDirection="row" columnGap={1}>
254        <Box flexShrink={0}>
255          <Text dimColor>copy</Text>
256        </Box>
257        {row}
258        {rest > 0 && <Button key="more" label={`+${rest}`} plain onPress={() => {}} />}
259        <Button key="dismiss" label="×" plain dimColor role="dismiss" onPress={() => update($, blocks, () => [])} />
260      </Box>
261    )
262    return below ? (
263      <Box flexDirection="column">
264        {mine}
265        {below}
266      </Box>
267    ) : (
268      mine
269    )
270  })
271
272  // 回答の本文の中で、引用とコードブロックの直上にコピーボタンを置く。ブロックがない回答は本体がそのまま描く
273  on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
274    if (!inlineOn) return next(e)
275    const parts = split(e.props.text)
276    if (!parts.some(p => p.block)) return next(e)
277    const lit = await read($, inline)
278    const { Box, Button, Markdown, Text } = $.ui.resolve(e) as any
279    const id = hashOf(e.props.text)
280    return (
281      // 本体の見た目に合わせる:左に 2 セルの列をとって返事の最初のブロックにだけ ● を置き、部分の間は 1 行あける
282      <Box flexDirection="row">
283        <Box width={2} flexShrink={0}>
284          <Text>{e.props.isFirstOfReply ? '●' : ' '}</Text>
285        </Box>
286        <Box flexDirection="column" rowGap={1} flexGrow={1} flexShrink={1}>
287          {parts.map((p, n) => {
288            const body = <Markdown key={`md-${id}-${n}`} text={p.raw} />
289            if (!p.block) return body
290            const key = `inline-${id}-${n}`
291            const text = p.block.text
292            const press = (x: { surface: unknown }) => void copyInline($, key, text, x.surface)
293            const hover = { scope: key, inverse: true }
294            return (
295              // ブロックの直上に、罫線の形のヘッダーを置く。罫線は薄く、⧉ copy だけ普通の明るさ(Button は色を持てない)
296              // 2 つは同じホバーのグループなので、行のどこを押してもコピーでき、乗せると行全体が反転する
297              <Box key={`blk-${id}-${n}`} flexDirection="column">
298                {lit === key ? (
299                  <Text color="green" bold>{`── ${headOf(p.block)} ── ✓ copied`}</Text>
300                ) : (
301                  <Box flexDirection="row" columnGap={1}>
302                    <Button key={`${key}-rule`} label={ruleOf(p.block)} plain dimColor hover={hover} onPress={press} />
303                    <Box flexShrink={0}>
304                      <Button key={key} label="⧉ copy" plain hover={hover} onPress={press} />
305                    </Box>
306                  </Box>
307                )}
308                {body}
309              </Box>
310            )
311          })}
312        </Box>
313      </Box>
314    )
315  })
316
317  // 一覧のペイン。帯に入りきらなかった分も含めて、全部を 1 行ずつ並べる
318  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
319    const list = await read($, blocks)
320    const done = await read($, copied)
321    const lit = await read($, flash)
322    const { Box, Text } = $.ui.resolve(e)
323    if (list.length === 0) return <Text dimColor>Nothing to copy</Text>
324    return (
325      <Box flexDirection="column">
326        <Text dimColor>Click a row to copy it</Text>
327        {list.map((b, i) => {
328          // 折り返すと崩れることがあるので、ペインの幅に収まるよう 1 行に切る。番号・印・行数・間の空白の分を引く
329          const meta = linesOf(b) >= MANY_LINES ? `${linesOf(b)}L` : ''
330          const room = e.props.bodyColumns - cellsOf(`${i + 1} ✓ ${tagOf(b)}  ${meta}`) - 1
331          return entry($, e, list, { index: i, tag: tagOf(b), label: labelOf(b, Math.max(4, room)), meta }, done, lit, 'list')
332        })}
333      </Box>
334    )
335  })
336}
337
types/index.d.ts 17 lines
1// 最後の返事から抜き出した、コピーできるブロック。lang はコードブロックの開始行に書かれた言語名
2export type Block = { kind: 'quote' | 'code'; text: string; lang?: string }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'copy-blocks': {
7      blocks: Block[]
8      // コピーしたブロックの番号。✓ を残して、どれを貼ったか分かるようにする
9      copied: number[]
10      // 「✓ copied」を出しているブロックの番号
11      flash: number | null
12      // 回答の中のボタンで直前にコピーしたもの(ボタンの key)
13      inline: string | null
14    }
15  }
16}
17