Probe only, not shipped: a plain band and pane (mark, jump, prompts, read) that do nothing but open panes and log every focus move, to see how Claude Code's…

Vim-style marks, a reading position, and a numbered prompt history, inside a Claude Code conversation
A Claude Code plugin that lets you mark a line of a conversation with a letter and jump back to it from anywhere, keep your place while you scroll, browse every prompt you have typed, and let Claude cite earlier places as links you can click. It is a mod, meaning it draws inside Claude Code's terminal app, with nothing else to install. In Claude Code's official directory the plugin is called bookmarks (published by DazzleML), whereas the Github repository here is claude-bookmarks. (It is a terminal plugin, not a browser extension -- for navigating the claude.ai website, AI Chat Nav does a similar job).
A long Claude Code conversation is hard to move around in. The answer you need is three hundred messages up, the prompt that started this line of work is somewhere above that, and after you scroll up to check something, finding your way back down to where you were reading is an exercise in scrolling-squinting-and-praying. Claude Code can already jump to the top and the bottom of the conversation, and its transcript view can search, but it has no way to say "remember this spot" and come back to it.
claude-bookmarks gives you named spots in the conversation. This includes tagged letters you set on any line (essentially Vim marks), a reading position you can swap to and back from, and a numbered list of every prompt you have typed, so "go back to where I asked about the cache" is two keystrokes instead of a text safari.
[!NOTE] Pre-alpha (v0.3.x).
claude-bookmarksis part of my daily active workflow. I've been dogfooding since Claude Code 2.1.288 (now at 2.1.295) on Win11 using Windows Terminal, fullscreen, and at different sizes. The mod for the most part correctly handles: inserting in-place highlights, jumps, using the reading position, navigating the prompts pane, and now works with bookmarks including clickable links in Claude's replies. Windows is the only tested platform. MacOS and Linux are expected to work but is untried (I'll try on a VPS after a few more minor versions). The engine underhooks/core/is tested and reasonably solid. Be aware the key layout will still change (#26). Also note: docs/status.md tracks known issues along with what is coming in the Roadmap and issue #1 (the longer view). Please file issues for anything rough.
<img src="plugin/images/prompts-pane-marked-line-and-bookmark-link.png" alt="The prompts pane open on the right, numbered from the first prompt; a marked line in the conversation with its c tag; a bookmark link in a reply; the band above the prompt in prompt mode"> <sub>In use: the prompts pane (<code>Ctrl+] p</code>) on the right, a marked line with its <code>«c»</code> tag, a <code>⚓</code> bookmark link Claude wrote in a reply, and the band above the input box waiting for a number. <a href="docs/images/jump-pane-marks-and-reading-position.png">The jump pane</a>, with three marks and the reading position.</sub>
One command installs it on Claude Code 2.1.275 or later, since this repository is also a marketplace:
/plugin install bookmarks --marketplace DazzleML/claude-bookmarks
To work on it, or to run a version before it was listed, load a local clone:
# 1. Clone the plugin
git clone https://github.com/DazzleML/claude-bookmarks.git
# 2. Load it (the plugin is the repository's plugin/ folder). For one session:
claude --plugin-dir /path/to/claude-bookmarks/plugin
# ...or for every session, add it to ~/.claude/settings.json:
# "env": { "CLAUDE_CODE_PLUGIN_DIRS": "/path/to/claude-bookmarks/plugin" }
# 3. Bind the leader key in ~/.claude/keybindings.json (see "Set up the leader" below)
# 4. Restart Claude Code, and switch to the fullscreen renderer if you are not on it
/tui fullscreen
# 5. Check it loaded
/bm-env
When the plugin is loaded, a one-line band sits above the prompt: a small bm: field, then four buttons, mark jump prompts read.
The plugin's keys all start with one leader key, Ctrl+]: a single key that every terminal passes on, over SSH as well. It is Claude Code's own "focus the band" action, abovePrompt:focus, bound to that key. A plugin cannot add a keybinding for you, so add this once to your ~/.claude/keybindings.json (back the file up first; if the file already has a Chat block, add the one line to it):
{
"bindings": [
{
"context": "Chat",
"bindings": {
"ctrl+]": "abovePrompt:focus"
}
}
]
}
That's the whole setup. Every key in these docs is written as Ctrl+]. Until the binding is in place, Claude Code's built-in key for the same action, Ctrl+X Tab (two keys), does the same job, and the band's buttons can be clicked.
[!TIP] New to vim-style keys? Start with the tutorial: it explains chords, leader keys and vim marks in plain terms, then walks you through each feature in about five minutes.
Ctrl+] m and a letter (a-z). With nothing freshly selected, the letter marks the message at the top of your screenCtrl+] ' and a letter scrolls back to that mark from anywhere in the conversation; the jump pane lists only the marks you haveCtrl+] Space Space to keep your place; press it again from anywhere to go there, and again to return to where you wereCtrl+] p lists every prompt of the conversation, numbered from your first, including prompts from before the plugin was loaded; type a number, or browse with j/k or the arrows, then Enter to jumpCtrl+] b); promote any mark into a bookmark with Ctrl+] P and its letterCtrl+] o returns to where you were before the last jump, Ctrl+] i goes forward again, like vim's jumplist--resumesh and grep elsewhere)| Keys | Band button | What it does |
|---|---|---|
Ctrl+] m then a-z | mark | Mark the selected line (or the top of the screen) with that letter; Enter cancels |
Ctrl+] ' (or j) then a-z | jump | Jump to that mark |
Ctrl+] ' then Enter | jump | Jump to the reading position (it heads the jump pane) |
Ctrl+] Space Space | read | Set, go to, or return from the reading position |
Ctrl+] p then a number, Enter | prompts | Jump to prompt #N |
Ctrl+] p then j/k or Down/Up, Enter | prompts | Browse the prompts one by one, then jump |
Ctrl+] p, pick a prompt, then s | prompts | Pin or unpin it (the band stays open to pin more) |
Ctrl+] b then a number, Enter | Open the bookmarks pane (yours, then Claude's) and jump to bookmark #N | |
Ctrl+] P then a-z | Promote that mark into a bookmark on your list (its link is copied to the clipboard) | |
Ctrl+] o / Ctrl+] i | Back to before the last jump / forward again | |
Click a ⚓ link in a reply | Jump to the bookmarked message, words highlighted; ctrl-click opens its file | |
Esc | Leave the band and go back to typing |
After a command the keyboard stays on the band, ready for the next one; Esc returns to the input box. In the mark pane, letters already in use show as ●. Prompts drawn dimmed in the prompts pane are ones Claude Code hasn't drawn on screen, usually from before the last compaction, and a jump to one may be refused (see Tips).
Power users can add one-step keys that press a band button directly, such as Ctrl+X Space for the reading position; they borrow actions of Claude Code's diff panel, so they aren't part of the default setup. See Optional fast keys.
# Keep an answer you will need again
select a line of it -> Ctrl+] m a (mark a)
...later, anywhere -> Ctrl+] ' a (back to it)
# Check something above, then come back
select the line you are reading -> Ctrl+] Space Space (reading position set)
scroll up, read what you needed
Ctrl+] Space Space (back to the reading position)
Ctrl+] Space Space again (and back to where you were)
# Go back to where a line of work started
Ctrl+] p 12 Enter (prompt #12)
# Ask Claude where to read, and click your way there
"Where should I read to catch up? Bookmark each place." (Claude answers with ⚓ links)
click a link (jump; the words light up)
Ctrl+] o (back to the answer)
# Keep a mark for good
Ctrl+] P a (mark a becomes a bookmark; link on the clipboard)
| Command | What it does | |||
|---|---|---|---|---|
/bm-mark, /bm-goto, /bm-prompts, /bm-read | The same as the leader keys, typed; from an empty input box their pane takes the keyboard | |||
/bm-bookmarks, /bm-promote a | Open the bookmarks pane; promote mark a into a bookmark | |||
/bm-delmarks a b, /bm-delmarks all | Delete marks | |||
/bm-pin [N] | Pin or unpin prompt #N in the prompts pane (*N in its # field does the same) | |||
/bm-env | Plugin version, session id, and what it has captured | |||
| `/bm-debug on\ | off` | Echo the plugin's log into this conversation as dim rows (this conversation only; `default on\ | off and force on\ | off` reach every conversation) |
/bm-marks | The marks set in this conversation | |||
/bm-timeline | The last few plugin events (draws, panes, jumps) |
For each feature in detail, see docs/usage.md.
The known limits, briefly; docs/troubleshooting.md has the details.
Esc to go back to typing. After a command the keyboard stays on the band; a plugin can't hand it back to the input box itself.Ctrl+O, then (writes what Claude Code holds to your terminal's scrollback), then your terminal's Find (Ctrl+Shift+F or Cmd+F), and paste. Older messages may only be in the session file; opening it in full is planned ([#16). For long conversations there is a way to keep the whole history in view after a restart, described in the engine notes; the plugin never depends on it./fork, the session may be hosted by Claude Code's background daemon, which doesn't load CLAUDE_CODE_PLUGIN_DIRS. Stop it with claude stop <short id> and resume it from a shell with claude --resume <session id>.Ctrl+X fast keys and have disabled Claude Code's built-in diff mod, or have the diff panel open, the diff panel may take those keys.Nothing to configure yet beyond the leader key. Settings are planned (#7), among them:
Enter (now) or jumps as soon as it's complete;Every key press, pane and jump is written to a per-conversation log, kept to its last 400 lines:
${CLAUDE_USER_DIR:-~/claude}/bookmarks/debug/<session id>.log
Read its last lines first when something behaves oddly; they make a good attachment to an issue.
| Platform | Status |
|---|---|
| Windows 11, Windows Terminal, fullscreen | Tested |
| macOS, fullscreen | Expected to work |
| Linux, fullscreen | Expected to work |
| Classic (non-fullscreen) renderer | Not supported |
Details, including tmux, IDE terminals and the Desktop app: docs/platform-support.md.
claude-bookmarks/
├── plugin/ # The plugin, as Claude Code installs it (point --plugin-dir here)
│ ├── .claude-plugin/
│ │ └── plugin.json # Plugin manifest (name: bookmarks)
│ ├── hooks/
│ │ ├── hooks.json # Points Claude Code at the mod
│ │ ├── register.tsx # The mod: the band, panes, highlight, bookmarks, back-fill
│ │ ├── core/ # Pure modules with their node --test suites (anchor URL, transcript, register)
│ │ └── scripts/ # PowerShell fallbacks for reading the transcript on Windows
│ ├── types/index.d.ts # Declared $.state values
│ ├── README.md, LICENSE # What the plugin directory requires inside the plugin folder
│ └── tsconfig.json # Type-check the mod: npx tsc -p plugin --noEmit
├── .claude-plugin/
│ └── marketplace.json # This repository as a marketplace (dazzle-claude-plugins)
├── docs/ # Status, keys, tutorial, usage, troubleshooting, platform support, engine quirks
├── scripts/repokit-common/ # Shared repo tooling (git subtree from DazzleTools/git-repokit-common)
├── tests/
│ ├── checklists/ # Human test checklists
│ └── one-offs/ # Probes and measurements
├── .repokit-common.toml # Repo tooling settings (version sync into plugin/.claude-plugin/plugin.json)
└── version.py # Version source for the repo tooling
abovePrompt:focus) moves the keyboard to the band, where a small field takes the next key, any key, ' and Space included. The band then shows what it waits for and takes the rest of the keys itself, while a pane beside the conversation shows the list. Claude Code credits a plugin's scroll or pane to your keystroke only while the key's handler is still running, so every jump happens inside that handler. The band exists because Claude Code refuses a pane the keyboard while the input box holds text; #26 is the plan to land in the pane whenever it is allowed.Select-String on Windows, grep elsewhere), then keeps the list current as you type.Contributions welcome! Please open an issue or submit a pull request.
See CONTRIBUTING.md for:
claude --plugin-dir . reloads the mod live when you save)claude plugin validate . and python -m pytest tests/ -vsync-versions.pytests/checklists/Like the project?
claude-bookmarks, copyright (C) 2026 Dustin Darcy
This project is licensed under the GNU General Public License v3.0 or later - see the LICENSE file for details.
hooks/register.tsx 297 lines1// focus-probe: a probe, not the product (djdarcy, 2026-10-07: "let's disable how all
2// input works and then I'd like to see what native ctrl-] looks like with a simple
3// band, and a simple panel, with nothing fancy").
4//
5// The band draws four Buttons, mark / jump / prompts / read, with hotkeys m j p r. Each
6// opens one pane, `probe`, with a few Buttons that do nothing but toast and close it.
7// Nothing redirects focus: the ui.focus, ui.press and ui.close hooks only watch and pass
8// every event on unchanged. What convo-bookmarks layers on top can be switched back on
9// one piece at a time with /probe-set (all off but openFocus and readout by default):
10//
11// bandField an Input in the band before the Buttons; typing m j p r opens the pane
12// bandButtons the four Buttons (on); off with bandField on leaves the band one stop
13// autoFocus autoFocus on the band's first element (the field, if drawn)
14// paneField an autoFocus Input at the top of the pane
15// openFocus band presses open the pane with `focus: true` (a request Claude Code
16// refuses while the band holds the keys; kept to watch it be refused)
17// readout the band's second row shows the last focus moves as they happen
18// vanish after an action the band draws nothing focusable for a few seconds:
19// does the keyboard fall into the pane, or back to the input box?
20//
21// Every event goes to <CLAUDE_USER_DIR or ~/claude>/bookmarks/debug/focus-probe-<session>.log
22// (outside the plugin folder: a write inside it reloads the mod).
23//
24// Run it alone, without convo-bookmarks: claude --plugin-dir <this folder>
25
26import { atom, read, update } from 'claude-code'
27import type { EngineInterface, Register, Timer } from 'claude-code'
28
29import type { Flags, Mode } from '../types'
30
31const PANE = 'probe'
32const BAND: [Mode, string, string][] = [
33 ['mark', 'mark', 'm'],
34 ['jump', 'jump', 'j'],
35 ['prompts', 'prompts', 'p'],
36 ['read', 'read', 'r'],
37]
38const DEFAULT_FLAGS: Flags = {
39 bandField: false,
40 bandButtons: true,
41 autoFocus: false,
42 paneField: false,
43 openFocus: true,
44 readout: true,
45 vanish: false,
46}
47// How long the band stays empty after an action under `vanish`, unless the pane closes first.
48const VANISH_MS = 4000
49
50const mode = atom({ plugin: 'focus-probe', key: 'mode' } as const, 'prompts')
51const flags = atom({ plugin: 'focus-probe', key: 'flags' } as const, DEFAULT_FLAGS)
52const readout = atom({ plugin: 'focus-probe', key: 'readout' } as const, [])
53const fieldRev = atom({ plugin: 'focus-probe', key: 'fieldRev' } as const, 0)
54const vanished = atom({ plugin: 'focus-probe', key: 'vanished' } as const, false)
55
56let busy = false
57let vanishTimer: Timer | undefined
58
59// The log file, rewritten whole ($.fs has no append), last LOG_LINES lines.
60const LOG_LINES = 600
61const logLines: string[] = []
62let logPath: string | null | undefined
63let flushing: Promise<void> = Promise.resolve()
64
65function log($: EngineInterface, line: string) {
66 logLines.push(`${new Date().toISOString()} ${line}`)
67 if (logLines.length > LOG_LINES) logLines.splice(0, logLines.length - LOG_LINES)
68 flushing = flushing.then(() => flushLog($)).catch(() => {})
69}
70
71async function flushLog($: EngineInterface) {
72 if (logPath === undefined) {
73 const home = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME'))
74 const root = (await $.env.get('CLAUDE_USER_DIR')) ?? (home ? `${home}/claude` : undefined)
75 logPath = root ? `${root}/bookmarks/debug/focus-probe-${await $.session.id()}.log` : null
76 }
77 if (logPath) await $.fs.write(logPath, logLines.join('\n') + '\n')
78}
79
80// One event: to the log with the draft's length and whether a turn runs, and to the
81// band's readout row.
82async function event($: EngineInterface, what: string) {
83 const draft = (await $.prompt.read()).text.length
84 log($, `${what} [draft=${draft}${busy ? ' busy' : ''}]`)
85 const stamp = new Date().toISOString().slice(14, 19)
86 await update($, readout, list => [...list, `${stamp} ${what}`].slice(-3))
87}
88
89// Flag `vanish`: after an action the band draws nothing focusable for VANISH_MS, so the
90// element holding the keyboard disappears. Where does Claude Code put the keyboard then:
91// into the pane just opened (the hand-off we want), or back to the input box (the
92// hand-back after a command)? `read` opens no pane under this flag, to see the second.
93async function vanishBand($: EngineInterface, why: string) {
94 await update($, vanished, () => true)
95 await event($, `band emptied (${why})`)
96 vanishTimer?.cancel()
97 vanishTimer = $.clock.after(VANISH_MS, () => void restoreBand($, 'timer'))
98}
99
100async function restoreBand($: EngineInterface, why: string) {
101 vanishTimer?.cancel()
102 vanishTimer = undefined
103 if (!(await read($, vanished))) return
104 await update($, vanished, () => false)
105 await event($, `band redrawn (${why})`)
106}
107
108async function act($: EngineInterface, m: Mode, via: string) {
109 const f = await read($, flags)
110 if (f.vanish && m === 'read') {
111 $.ui.toast('probe: read (no pane)')
112 await vanishBand($, 'read, no pane')
113 return
114 }
115 await openPane($, m, via)
116 if (f.vanish) await vanishBand($, `${m} opened`)
117}
118
119async function openPane($: EngineInterface, m: Mode, via: string) {
120 await update($, mode, () => m)
121 const f = await read($, flags)
122 const r = await $.ui.open({ id: PANE, title: `probe: ${m}`, closeOnEscape: true, ...(f.openFocus ? { focus: true as const } : {}) })
123 const p = (await $.ui.panes()).find(x => x.id === PANE)
124 await event($, `open ${m} from ${via} (focus ${f.openFocus ? 'asked' : 'not asked'}) -> placed=${r.isPlaced} paneFocused=${p?.isFocused}`)
125 $.clock.after(500, () => {
126 void (async () => {
127 const q = (await $.ui.panes()).find(x => x.id === PANE)
128 log($, ` +500ms: pane ${q ? `open, focused=${q.isFocused}` : 'closed'}`)
129 })()
130 })
131}
132
133async function picked($: EngineInterface, what: string) {
134 await event($, `picked ${what}`)
135 $.ui.toast(`probe: ${what}`)
136 await $.ui.close({ id: PANE })
137}
138
139export const register: Register = on => {
140 on('session.start', async ($, e, next) => {
141 const commands: [string, string][] = [
142 ['probe-open', 'Open the probe pane from the input box: mark | jump | prompts | read'],
143 ['probe-set', 'Switch a layer on or off: bandField | bandButtons | autoFocus | paneField | openFocus | readout | vanish [on|off]'],
144 ['probe-log', 'Show where the focus log is written'],
145 ]
146 for (const [name, description] of commands) {
147 await $.command.register({ name, description, immediate: true })
148 }
149 const v = await $.session.version()
150 log($, `--- focus-probe loaded on Claude Code ${v.version}; flags ${JSON.stringify(await read($, flags))}`)
151 return next(e)
152 })
153
154 on('turn.start', async ($, e, next) => {
155 busy = true
156 return next(e)
157 })
158 on('turn.complete', async ($, e, next) => {
159 busy = false
160 return next(e)
161 })
162
163 // Watch only: every focus move in the band or the pane, passed on unchanged.
164 on('ui.focus', async ($, e, next) => {
165 const r = await next(e)
166 const deny = r && typeof r === 'object' && 'deny' in r && r.deny ? ` DENY ${String(r.deny)}` : ''
167 const site = e.component === 'Pane' ? 'pane' : 'band'
168 await event($, `focus ${site}:${e.element ?? '(engine stop)'} by ${e.origin.kind}${deny}`)
169 return r
170 })
171
172 on('ui.press', { plugin: 'focus-probe' }, async ($, e, next) => {
173 await event($, `press ${e.element}`)
174 return next(e)
175 })
176
177 on('ui.close', { id: PANE }, async ($, e, next) => {
178 await event($, `close pane by ${e.origin.kind}`)
179 await restoreBand($, 'pane closed')
180 return next(e)
181 })
182
183 on('command.run', { command: 'probe-open' }, async ($, e) => {
184 const m = e.args.trim() as Mode
185 await openPane($, BAND.some(([x]) => x === m) ? m : 'prompts', '/probe-open')
186 return {}
187 })
188
189 on('command.run', { command: 'probe-set' }, async ($, e) => {
190 const [name, value] = e.args.trim().split(/\s+/)
191 const f = await read($, flags)
192 if (!name || !(name in f)) {
193 $.ui.toast(`probe flags: ${Object.entries(f).map(([k, v]) => `${k}=${v ? 'on' : 'off'}`).join(' ')}`)
194 return {}
195 }
196 const key = name as keyof Flags
197 const on_ = value === 'on' ? true : value === 'off' ? false : !f[key]
198 await update($, flags, old => ({ ...old, [key]: on_ }))
199 log($, `--- flag ${key} ${on_ ? 'on' : 'off'}`)
200 $.ui.toast(`probe: ${key} ${on_ ? 'on' : 'off'}`)
201 return {}
202 })
203
204 on('command.run', { command: 'probe-log' }, async $ => {
205 await flushLog($)
206 $.ui.toast(`probe log: ${logPath ?? '(no place to write it)'}`)
207 return {}
208 })
209
210 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
211 const elements = $.ui.resolve(e)
212 const { Box, Button, Text } = elements
213 const Input = 'Input' in elements ? elements.Input : undefined
214 const theirs = await next(e)
215 const f = await read($, flags)
216 const lines = f.readout ? await read($, readout) : []
217 const empty = await read($, vanished)
218 let drawn = 0
219 const first = () => (f.autoFocus && drawn++ === 0 ? { autoFocus: true as const } : {})
220 const items: any[] = []
221 if (f.bandField && Input && !empty) {
222 items.push(
223 <Input key={`band-field-${await read($, fieldRev)}`} label="probe:" placeholder="m j p r" {...first()}
224 onInput={(typed: string) => {
225 const m = BAND.find(([, , k]) => k === typed.slice(-1))?.[0]
226 if (!m) return
227 void (async () => {
228 await update($, fieldRev, n => n + 1) // drawn under a new key, so it starts empty
229 await act($, m, 'band field')
230 })()
231 }}
232 onSubmit={(typed: string) => void event($, `band field Enter "${typed}"`)} />,
233 )
234 }
235 // Off (with bandField on): the band has one stop, so each focus-key press can only
236 // leave the site; does the leader then rotate input -> band -> pane -> input?
237 for (const [m, label, hotkey] of f.bandButtons && !empty ? BAND : []) {
238 items.push(<Button key={`band-${m}`} label={label} hotkey={hotkey} plain {...first()} onPress={() => act($, m, 'band')} />)
239 }
240 return (
241 <Box flexDirection="column">
242 {theirs}
243 <Box flexDirection="row" columnGap={1}>
244 <Text dimColor>probe</Text>
245 {empty ? <Text dimColor>(nothing focusable for a moment)</Text> : items}
246 </Box>
247 {lines.length > 0 && <Text dimColor wrap="truncate-end">{lines.join(' · ')}</Text>}
248 </Box>
249 )
250 })
251
252 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
253 const elements = $.ui.resolve(e)
254 const { Box, Button, Text } = elements
255 const Input = 'Input' in elements ? elements.Input : undefined
256 const m = await read($, mode)
257 const f = await read($, flags)
258 const status = <Text dimColor>{e.props.isFocused ? 'this pane HAS the keyboard' : 'this pane does NOT have the keyboard'}</Text>
259 const field = f.paneField && Input
260 ? [<Input key="pane-field" label="#" placeholder="type, Enter" autoFocus onSubmit={(t: string) => void picked($, `field "${t}"`)} />]
261 : []
262
263 let body: any
264 if (m === 'mark') {
265 body = (
266 <Box flexDirection="row" columnGap={1} flexWrap="wrap">
267 <Text dimColor>mark as:</Text>
268 {'abcdef'.split('').map(l => <Button key={`mark-${l}`} hotkey={l} label="·" plain onPress={() => picked($, `mark ${l}`)} />)}
269 </Box>
270 )
271 } else if (m === 'jump') {
272 body = [
273 ['a', 'a heading near the top'],
274 ['b', 'some code further down'],
275 ['c', 'a long answer'],
276 ].map(([l, text]) => <Button key={`jump-${l}`} hotkey={l} label={text ?? ''} plain onPress={() => picked($, `jump ${l}`)} />)
277 } else if (m === 'read') {
278 body = [
279 <Button key="read-there" hotkey="t" label="there (the reading position)" plain onPress={() => picked($, 'read there')} />,
280 <Button key="read-back" hotkey="b" label="back (where you were)" plain onPress={() => picked($, 'read back')} />,
281 ]
282 } else {
283 // Thirty rows and no hotkeys: enough to see whether the arrows scroll or move the ring.
284 body = Array.from({ length: 30 }, (_, k) => 30 - k).map(n => (
285 <Button key={`prompt-${n}`} label={`${String(n).padStart(2)}) a pretend prompt, number ${n}`} plain onPress={() => picked($, `prompt ${n}`)} />
286 ))
287 }
288 return (
289 <Box flexDirection="column">
290 {status}
291 {field}
292 {body}
293 </Box>
294 )
295 })
296}
297types/index.d.ts 27 lines1// Which pane the probe shows, and the convo-bookmarks layers /probe-set switches on.
2export type Mode = 'mark' | 'jump' | 'prompts' | 'read'
3export type Flags = {
4 bandField: boolean
5 bandButtons: boolean
6 autoFocus: boolean
7 paneField: boolean
8 openFocus: boolean
9 readout: boolean
10 vanish: boolean
11}
12
13declare module 'claude-code' {
14 interface PluginState {
15 'focus-probe': {
16 mode: Mode
17 flags: Flags
18 // The last few events, shown in the band's second row.
19 readout: string[]
20 // Bumped to redraw the band's field under a new key, so it starts empty.
21 fieldRev: number
22 // True while the band draws nothing focusable after an action (flag vanish).
23 vanished: boolean
24 }
25 }
26}
27