A pane beside the transcript that holds the next things to tell the agent: write blocks of text while a turn runs, then press [+] to put one block into the…

All of my Claude Code mods in one plugin marketplace.
claude plugin marketplace add jidhra/claude-mods
claude plugin install <mod>@claude-mods --scope user
| Mod | Folder | Origin |
|---|---|---|
| Mod Tools | plugins/mod-tools | mine |
| Clean View | plugins/clean-view | mine |
| Usage | plugins/usage-pane | mine |
| Pinboard | plugins/pinboard | personal copy of sirkitree/pinboard (MIT) |
| Buffer Pane | plugins/buffer-pane (plugin in plugin/) | personal copy of meganemura/buffer-pane (MIT) |
| Flightdeck | plugins/flightdeck | copy of scasella/claude-flightdeck (MIT) |
| Secret Redactor | plugins/secret-redactor | copy of plugins/secret-redactor from ray-amjad/awesome-claude-code-function-hooks (MIT) |
Each mod was brought in with git subtree, so its full history is here. The mods by other authors keep tracking their originals; see UPSTREAM.md for pulling in their changes.
The .claude-plugin/marketplace.json files inside each mod's folder are left over from when each was its own marketplace. Only the one at the repo root is used.
claude plugin validate plugins/<mod>
claude plugin test plugins/<mod>hooks/mod.ts 530 lines1// The plugin's one function-hooks module (the validator admits one per plugin). `/buffer-pane`
2// opens a pane beside the transcript that holds text the person writes for later: the next
3// things to tell the agent. The buffer is one text. Blank lines split it into blocks. Each
4// block has a `[+]` that writes the block into the prompt box, a `[>]` that submits the block as
5// a prompt and deletes it, and a `[x]` that deletes it. The
6// buffer lives in the plugin store, one key for each working directory, so it survives sessions
7// and hot reloads.
8//
9// Must NOT know about: what the person does with the prompt box after a fill; what the prompt box holds
10// (the API cannot read it, and `prompt.fill` always replaces it); any other repository's
11// buffer (one key, the current working directory's).
12//
13// It loads only where Claude Code has function hooks enabled. The engine's validator reads
14// this file statically, so every call on `$` is spelled `$.noun.event(...)` and `$` is handed
15// only to the function declarations at the top of the file; the rest of the module holds a
16// `Host`, a bundle of closures built once at `session.start`.
17
18import type { Elements, On, RenderElement } from 'claude-code'
19
20const PANE_ID = 'buffer-pane'
21const PANE_TITLE = 'Buffer pane'
22const COMMAND = 'buffer-pane'
23
24const STORE_KEY_PREFIX = 'buffer:'
25
26const PANE_PADDING_RIGHT = 1
27// Docked width; matches Flightdeck's so the dock doesn't jump between panes
28const PANE_COLUMNS = 66
29
30// `✓` is a long-established narrow symbol: it draws in one cell. A symbol that a font draws
31// two cells wide would push the text field out of line with the rows that have no mark.
32const SENT_MARK = '✓'
33const NOT_SENT_MARK = ' '
34
35const FILL_REFUSED_TEXT = 'buffer-pane: the prompt box did not take the block (a dialog is open, or there is no prompt box)'
36const SUBMIT_REFUSED_TEXT = 'buffer-pane: the prompt was refused: '
37// The surface draws `⏎ <submitLabel>` beside a field while it has the focus, and the field
38// shrinks by that width, so the row moved on each focus change (real-terminal feedback). An
39// empty label is the one value the props offer to give it nothing to draw. What Enter does is
40// in the note at the bottom of the pane instead.
41const SUBMIT_LABEL = ''
42
43const REPLACE_NOTE = '[+] replaces the prompt box with the block. [>] sends the block as a prompt. [x] deletes the block. [^] [v] move the block. Enter in the last field adds a block.'
44
45type Host = {
46 cwd: () => Promise<string>
47 status: (text: string | undefined) => void
48 open: () => Promise<void>
49 close: () => Promise<void>
50 invalidate: () => void
51 log: (text: string) => void
52 register: () => Promise<unknown>
53 fill: (text: string) => Promise<{ isFilled: boolean }>
54 submit: (text: string) => Promise<{ drop?: string | undefined }>
55 storeGet: (key: string) => Promise<unknown>
56 storeSet: (key: string, value: unknown) => Promise<void>
57 // Whether the pane is up, for Mod Tools' Show column (it reads buffer-pane.paneOpen)
58 isPaneUp: () => Promise<boolean>
59 publishOpen: (isOpen: boolean) => Promise<unknown>
60}
61
62// A block holds an id because its position changes when `[x]` deletes a block above it. The
63// id is the element key, so the text field of block 3 does not become the text field of
64// block 2 under the person's cursor.
65export type Block = { id: number; text: string }
66
67// What the pane edits. `sent` holds the text of each block that a `[+]` wrote into the prompt
68// box. A block shows the mark only while its text is in `sent`, so an edit removes the mark
69// with no separate bookkeeping. `draft` is the text in the new-block field before Enter.
70export type Buffer = { blocks: Block[]; sent: string[]; draft: string; nextId: number }
71
72// What the store holds. The buffer is one text with blank lines between blocks, not a list:
73// a later multi-line editor edits the same text, and a person can read the stored value.
74export type Stored = { text: string; sent: string[]; draft: string }
75
76type State = {
77 host: Host | null
78 isOpen: boolean
79 storeKey: string | null
80 buffer: Buffer
81 // Counts Enter presses in the new-block field. The count is part of that field's key, so
82 // each Enter draws a new element. The surface keeps the typed text of an element it has
83 // seen before, and an empty `value` drawn again for the same key might not clear it.
84 draftGeneration: number
85}
86
87// The host is a bundle of closures over `$`, built once at `session.start`, so the rest of
88// this file never holds `$` itself. That is the validator's rule and also the seam a test
89// fakes: every world a test builds stubs these same calls with `on(...)`.
90function hostOf($: any): Host {
91 return {
92 cwd: () => $.session.cwd(),
93 status: (text) => $.ui.status(text),
94 open: () => $.ui.open({ id: PANE_ID, title: PANE_TITLE, columns: PANE_COLUMNS, focus: true }),
95 close: () => $.ui.close({ id: PANE_ID }),
96 invalidate: () => $.ui.invalidate('ui.render'),
97 log: (text) => $.ui.log(text),
98 register: () => $.command.register({ name: COMMAND, description: 'Show or hide the buffer-pane' }),
99 fill: (text) => $.prompt.fill({ text }),
100 submit: (text) => $.prompt.submit({ text }),
101 storeGet: (key) => $.store.get(key),
102 storeSet: (key, value) => $.store.set(key, value),
103 isPaneUp: async () => ((await $.ui.panes()) as { id: string }[]).some((pane) => pane.id === PANE_ID),
104 publishOpen: (isOpen) => $.state.set({ plugin: 'buffer-pane', key: 'paneOpen' }, isOpen),
105 }
106}
107
108function messageOf(error: unknown): string {
109 return error instanceof Error ? error.message : String(error)
110}
111
112export function storeKeyOf(cwd: string): string {
113 return `${STORE_KEY_PREFIX}${cwd}`
114}
115
116export function emptyBuffer(): Buffer {
117 return { blocks: [], sent: [], draft: '', nextId: 1 }
118}
119
120// A line that holds only spaces also separates blocks: a person cannot see the difference
121// between that line and an empty line.
122export function blockTextsOf(text: string): string[] {
123 return text
124 .split(/\n[ \t]*(?:\n[ \t]*)+/)
125 .map((block) => block.trim())
126 .filter((block) => block !== '')
127}
128
129export function textOfBlocks(blocks: readonly Block[]): string {
130 return blocks
131 .map((block) => block.text.trim())
132 .filter((text) => text !== '')
133 .join('\n\n')
134}
135
136// A mark for a text that is no longer in the buffer is dropped. Without this, a block that
137// the person edits and then edits back shows a mark for a fill of a different text.
138function withSentPruned(buffer: Buffer): Buffer {
139 const texts = new Set(buffer.blocks.map((block) => block.text.trim()))
140 return { ...buffer, sent: buffer.sent.filter((text) => texts.has(text)) }
141}
142
143export function afterSubmitOf(buffer: Buffer, value: string): Buffer {
144 const text = value.trim()
145 if (text === '') return { ...buffer, draft: '' }
146 return { ...buffer, blocks: [...buffer.blocks, { id: buffer.nextId, text }], draft: '', nextId: buffer.nextId + 1 }
147}
148
149export function afterDraftOf(buffer: Buffer, value: string): Buffer {
150 return { ...buffer, draft: value }
151}
152
153export function afterEditOf(buffer: Buffer, id: number, value: string): Buffer {
154 return withSentPruned({ ...buffer, blocks: buffer.blocks.map((block) => (block.id === id ? { ...block, text: value } : block)) })
155}
156
157// Enter in the field of a block that the person emptied deletes the block. An empty block
158// has nothing to send, and the stored text could not hold it.
159export function afterEditSubmitOf(buffer: Buffer, id: number, value: string): Buffer {
160 if (value.trim() === '') return afterRemoveOf(buffer, id)
161 return afterEditOf(buffer, id, value.trim())
162}
163
164// Moves a block one place up (`-1`) or down (`+1`). A block at the end stays where it is.
165// Buttons in place of a drag: a drag needs pointer events, which only a `Client` receives,
166// and keys and pointer events did not reach a `Client` in a repeatable way (docs/decisions/0008).
167export function afterMoveOf(buffer: Buffer, id: number, direction: -1 | 1): Buffer {
168 const index = buffer.blocks.findIndex((block) => block.id === id)
169 const target = index + direction
170 if (index < 0 || target < 0 || target >= buffer.blocks.length) return buffer
171 const blocks = [...buffer.blocks]
172 const [moved] = blocks.splice(index, 1)
173 blocks.splice(target, 0, moved!)
174 return { ...buffer, blocks }
175}
176
177export function afterRemoveOf(buffer: Buffer, id: number): Buffer {
178 return withSentPruned({ ...buffer, blocks: buffer.blocks.filter((block) => block.id !== id) })
179}
180
181// `sent` holds trimmed text, the form the store keeps (`textOfBlocks`). A field holds the text
182// as typed, with a possible space at the end, so each comparison trims first. Without this, a
183// block sent with a space at its end loses its mark on the next load.
184export function afterSentOf(buffer: Buffer, text: string): Buffer {
185 const sent = text.trim()
186 return buffer.sent.includes(sent) ? buffer : { ...buffer, sent: [...buffer.sent, sent] }
187}
188
189export function isSentOf(buffer: Buffer, block: Block): boolean {
190 return buffer.sent.includes(block.text.trim())
191}
192
193export function storedOf(buffer: Buffer): Stored {
194 return { text: textOfBlocks(buffer.blocks), sent: withSentPruned(buffer).sent, draft: buffer.draft }
195}
196
197// What comes out of the store is this file's own past write, not the engine's word. A
198// version with a different shape could have written it, so each field is checked. A value
199// that does not fit reads as an empty buffer, never as an error: the pane must still open.
200export function bufferFromStore(value: unknown): Buffer {
201 if (typeof value !== 'object' || value === null) return emptyBuffer()
202 const text = Reflect.get(value, 'text')
203 const sent = Reflect.get(value, 'sent')
204 const draft = Reflect.get(value, 'draft')
205 if (typeof text !== 'string') return emptyBuffer()
206 const blocks = blockTextsOf(text).map((blockText, index) => ({ id: index + 1, text: blockText }))
207 return withSentPruned({
208 blocks,
209 sent: Array.isArray(sent) ? sent.filter((item): item is string => typeof item === 'string') : [],
210 draft: typeof draft === 'string' ? draft : '',
211 nextId: blocks.length + 1,
212 })
213}
214
215// Reads the buffer of the current working directory. It runs at `session.start` and again
216// each time the pane opens, because the working directory can change during a session.
217// It does not read again for a key that is already loaded: the state is newer than the store
218// while a write is in flight.
219async function load(state: State, host: Host): Promise<void> {
220 const key = storeKeyOf(await host.cwd())
221 if (key === state.storeKey) return
222 state.buffer = bufferFromStore(await host.storeGet(key).catch(() => undefined))
223 state.storeKey = key
224}
225
226// Every change goes through here: state first, then the store, then a redraw. The render
227// hook never writes the store. It fires many times a second.
228function commit(state: State, host: Host, buffer: Buffer): void {
229 state.buffer = buffer
230 if (state.storeKey !== null) {
231 void host.storeSet(state.storeKey, storedOf(buffer)).catch((error: unknown) => {
232 host.log(`buffer-pane: the buffer was not saved: ${messageOf(error)}`)
233 })
234 }
235 host.invalidate()
236}
237
238function blockTextOf(state: State, id: number): string {
239 const block = state.buffer.blocks.find((candidate) => candidate.id === id)
240 return block?.text.trim() ?? ''
241}
242
243// `[+]`: the block goes into the prompt box, and the person presses Enter.
244async function fill(state: State, host: Host, id: number): Promise<void> {
245 const text = blockTextOf(state, id)
246 if (text === '') return
247 const { isFilled } = await host.fill(text)
248 if (!isFilled) {
249 host.status(FILL_REFUSED_TEXT)
250 return
251 }
252 host.status(undefined)
253 // The block stays in the buffer. The pane is the source of truth: the person can lose the
254 // text in the prompt box with one key, and then sends the same block again.
255 commit(state, host, afterSentOf(state.buffer, text))
256}
257
258// `[>]`: the block goes to the model as a prompt (asked for, beside `[+]`: a block that needs no
259// second look is sent in one press). `$.prompt.submit` runs the prompt when the session is idle,
260// so a press during a turn queues it. The block is deleted once the prompt entered (asked for:
261// a sent request is done, and a block that stays reads as one still to send). A refused prompt
262// leaves the block in place.
263async function submit(state: State, host: Host, id: number): Promise<void> {
264 const text = blockTextOf(state, id)
265 if (text === '') return
266 const result = await host.submit(text)
267 if (result.drop !== undefined) {
268 host.status(`${SUBMIT_REFUSED_TEXT}${result.drop}`)
269 return
270 }
271 host.status(undefined)
272 commit(state, host, afterRemoveOf(state.buffer, id))
273}
274
275// The real element types, so the typecheck refuses a prop the engine would refuse. One
276// unknown prop drops the whole tree with no message. `Text` takes no `key`.
277type Ui = Pick<Elements['terminal'], 'Box' | 'Button' | 'Text' | 'Input'>
278
279// Flightdeck's look (its `theme` palette): theme keys, never fixed hex, so light, dark and
280// colour-blind themes all work. Only the keys this pane uses.
281const C = {
282 main: 'claude',
283 agent: 'suggestion',
284 gate: 'success',
285 dim: 'inactive',
286 faint: 'subtle',
287} as const
288
289// The narrowest pane the layout is sized for; a narrower body clips at its right edge.
290const MIN_WIDTH = 40
291// The number column at the left of each block row: a next-up glyph and a two-digit number.
292const GUTTER_WIDTH = 3
293const NEXT_UP_MARK = '▶'
294
295// Legend items that fit on one row of `width` cells, in order; the rest are dropped (Flightdeck's
296// `fitLegend`).
297function fitLegend<T extends { label: string }>(items: readonly T[], width: number): T[] {
298 const out: T[] = []
299 let used = 0
300 for (const item of items) {
301 const cells = item.label.length + 4
302 if (used + cells > width) break
303 out.push(item)
304 used += cells
305 }
306 return out
307}
308
309// No `hotkey` on a Button: a hotkey does not fire in a pane (measured in pull-request-pane,
310// two terminal setups). The arrow keys with Enter, or a click, press a Button. `plain` draws
311// the label alone, and the focus and the pointer still invert it, so the brackets of `[+]` are
312// the only chrome and the gutter keeps a fixed width.
313function blockRowOf(ui: Ui, block: Block, index: number, state: State, host: Host): RenderElement {
314 const { Box, Button, Text, Input } = ui
315 const key = `block:${block.id}`
316 const isNext = index === 0
317 const isSent = isSentOf(state.buffer, block)
318 return Box({
319 key,
320 flexDirection: 'row',
321 width: '100%',
322 columnGap: 1,
323 children: [
324 // The number sits in a Box so the row's own Text children stay the sent mark alone.
325 Box({
326 width: GUTTER_WIDTH,
327 flexShrink: 0,
328 children: [Text(isNext
329 ? { color: C.main, bold: true, children: `${NEXT_UP_MARK}${String(index + 1).padStart(GUTTER_WIDTH - 1)}` }
330 : { color: C.faint, children: String(index + 1).padStart(GUTTER_WIDTH) })],
331 }),
332 Button({ key: `${key}:fill`, label: '[+]', plain: true, onPress: () => void fill(state, host, block.id).catch(() => undefined) }),
333 Button({ key: `${key}:submit`, label: '[>]', plain: true, onPress: () => void submit(state, host, block.id).catch(() => undefined) }),
334 Button({ key: `${key}:remove`, label: '[x]', plain: true, onPress: () => commit(state, host, afterRemoveOf(state.buffer, block.id)) }),
335 // `^` and `v` are ASCII: no font draws them wider than one cell (an arrow glyph can).
336 Button({ key: `${key}:up`, label: '[^]', plain: true, onPress: () => commit(state, host, afterMoveOf(state.buffer, block.id, -1)) }),
337 Button({ key: `${key}:down`, label: '[v]', plain: true, onPress: () => commit(state, host, afterMoveOf(state.buffer, block.id, 1)) }),
338 Text({ color: C.gate, bold: true, children: isSent ? SENT_MARK : NOT_SENT_MARK }),
339 fieldBoxOf(ui, `${key}:field`, Input({
340 key: `${key}:text`,
341 value: block.text,
342 submitLabel: SUBMIT_LABEL,
343 onInput: (value) => commit(state, host, afterEditOf(state.buffer, block.id, value)),
344 onSubmit: (value) => commit(state, host, afterEditSubmitOf(state.buffer, block.id, value)),
345 })),
346 ],
347 })
348}
349
350// A field on its own draws as wide as its text (real-terminal feedback: the new-block field was
351// too narrow to write in). `InputProps` has no width, so the Box around it takes the rest of
352// the row and the field fills the Box.
353function fieldBoxOf(ui: Ui, key: string, field: RenderElement): RenderElement {
354 return ui.Box({ key, flexGrow: 1, width: '100%', children: [field] })
355}
356
357function draftRowOf(ui: Ui, state: State, host: Host): RenderElement {
358 const { Box, Text, Input } = ui
359 return Box({
360 key: 'draft',
361 flexDirection: 'row',
362 width: '100%',
363 columnGap: 1,
364 children: [
365 Text({ color: C.agent, bold: true, children: '›' }),
366 fieldBoxOf(ui, 'draft:field', Input({
367 key: `draft:${state.draftGeneration}`,
368 value: state.buffer.draft,
369 placeholder: 'the next thing to tell the agent',
370 submitLabel: SUBMIT_LABEL,
371 autoFocus: true,
372 onInput: (value) => commit(state, host, afterDraftOf(state.buffer, value)),
373 onSubmit: (value) => {
374 state.draftGeneration += 1
375 commit(state, host, afterSubmitOf(state.buffer, value))
376 },
377 })),
378 ],
379 })
380}
381
382// A card's first row: an upper-case label in the accent on the left, a dim state on the right.
383function cardHeadOf(ui: Ui, label: RenderElement, state: string): RenderElement {
384 const { Box, Text } = ui
385 return Box({ justifyContent: 'space-between', children: [label, Text({ dimColor: true, children: state })] })
386}
387
388function queueCardOf(ui: Ui, state: State, host: Host, width: number): RenderElement {
389 const { Box, Text } = ui
390 const blocks = state.buffer.blocks
391 const inPrompt = blocks.filter((block) => isSentOf(state.buffer, block)).length
392 const count = `${blocks.length} queued${inPrompt > 0 ? ` · ${inPrompt} in prompt` : ''}`
393 if (blocks.length === 0) {
394 return Box({
395 flexDirection: 'column',
396 borderStyle: 'round',
397 borderColor: C.faint,
398 paddingX: 1,
399 width,
400 children: [
401 cardHeadOf(ui, Text({ dimColor: true, children: 'queue' }), '0 queued'),
402 Text({ color: C.faint, children: 'nothing yet' }),
403 ],
404 })
405 }
406 return Box({
407 flexDirection: 'column',
408 borderStyle: 'round',
409 borderColor: C.main,
410 paddingX: 1,
411 width,
412 children: [
413 cardHeadOf(ui, Text({ color: C.main, bold: true, children: 'QUEUE · next up' }), count),
414 Box({ flexDirection: 'column', children: blocks.map((block, index) => blockRowOf(ui, block, index, state, host)) }),
415 ],
416 })
417}
418
419function newBlockCardOf(ui: Ui, state: State, host: Host, width: number): RenderElement {
420 const { Box, Text } = ui
421 return Box({
422 flexDirection: 'column',
423 borderStyle: 'round',
424 borderColor: C.agent,
425 paddingX: 1,
426 width,
427 children: [
428 cardHeadOf(ui, Text({ color: C.agent, bold: true, children: 'NEW BLOCK' }), '⏎ adds'),
429 draftRowOf(ui, state, host),
430 Box({ flexDirection: 'column', marginTop: 1, children: [Text({ dimColor: true, children: REPLACE_NOTE })] }),
431 ],
432 })
433}
434
435function paneOf(ui: Ui, state: State, host: Host, bodyColumns: number): RenderElement {
436 const { Box, Text } = ui
437 const width = Math.max(MIN_WIDTH, bodyColumns) - PANE_PADDING_RIGHT
438 const queued = state.buffer.blocks.length
439 const legend = fitLegend(
440 [
441 { label: 'next up', color: C.main },
442 { label: 'in prompt', color: C.gate },
443 { label: 'new block', color: C.agent },
444 ],
445 width,
446 )
447 return Box({
448 key: 'buffer-pane',
449 flexDirection: 'column',
450 paddingRight: PANE_PADDING_RIGHT,
451 width: width + PANE_PADDING_RIGHT,
452 children: [
453 Box({
454 justifyContent: 'center',
455 children: [Text({
456 bold: true,
457 wrap: 'truncate',
458 children: [
459 Text({ children: 'BUFFER PANE' }),
460 Text({ color: C.dim, children: ' · ' }),
461 Text({ color: queued > 0 ? C.main : C.dim, children: String(queued) }),
462 Text({ children: ' QUEUED' }),
463 ],
464 })],
465 }),
466 Box({
467 justifyContent: 'center',
468 columnGap: 2,
469 children: legend.map((item) => Text({ children: [Text({ color: item.color, children: '■' }), Text({ dimColor: true, children: ` ${item.label}` })] })),
470 }),
471 queueCardOf(ui, state, host, width),
472 Text({ color: C.faint, children: '─'.repeat(width) }),
473 newBlockCardOf(ui, state, host, width),
474 ],
475 })
476}
477
478export function register(on: On) {
479 const state: State = { host: null, isOpen: false, storeKey: null, buffer: emptyBuffer(), draftGeneration: 0 }
480
481 on('session.start', async ($, e, next) => {
482 state.host = hostOf($)
483 await state.host.register().catch((error: unknown) => {
484 state.host?.log(`buffer-pane: /${COMMAND} is not available: ${messageOf(error)}`)
485 })
486 // A hot reload of this module starts a new session under an open pane. The buffer is
487 // read here so the first redraw after the reload shows it.
488 await load(state, state.host).catch(() => undefined)
489 // A reload keeps the pane up but starts this module over: ask the engine whether it is open
490 state.isOpen = await state.host.isPaneUp().catch(() => false)
491 await state.host.publishOpen(state.isOpen).catch(() => undefined)
492 // Local patch: no open on launch. Only Flightdeck opens unasked; /buffer-pane opens this one.
493 return next(e)
494 })
495
496 on('command.run', { command: COMMAND }, async ($, e, next) => {
497 const host = state.host
498 if (host === null) return next(e)
499
500 if (state.isOpen) {
501 await host.close()
502 state.isOpen = false
503 await host.publishOpen(false).catch(() => undefined)
504 return { text: 'buffer-pane hidden' }
505 }
506
507 await load(state, host)
508 await host.open()
509 state.isOpen = true
510 await host.publishOpen(true).catch(() => undefined)
511 return { text: 'buffer-pane shown' }
512 })
513
514 on('ui.render', { component: 'Pane' }, async ($, e, next) => {
515 if (e.requestId !== PANE_ID || state.host === null) return next(e)
516 if (e.surface !== 'terminal') return next(e)
517 const { Box, Button, Text, Input } = await $.ui.resolve(e)
518 return paneOf({ Box, Button, Text, Input }, state, state.host, e.props.bodyColumns)
519 })
520
521 on('ui.close', { id: PANE_ID }, async ($, e, next) => {
522 const result = await next(e)
523 if (result.deny === undefined) {
524 state.isOpen = false
525 await state.host?.publishOpen(false).catch(() => undefined)
526 }
527 return result
528 })
529}
530types/index.d.ts 11 lines1/** Whether the Buffer pane is open; Mod Tools reads it for its Show column. */
2export type PaneOpen = boolean
3
4declare module 'claude-code' {
5 interface PluginState {
6 'buffer-pane': {
7 paneOpen: PaneOpen
8 }
9 }
10}
11