The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan.

<a href="https://lendwise.fi"> <img src="public/logo.png" alt="Lendwise logo" width="380" /> </a>
Lendwise compares and monitors 700+ lending markets across Aave, Morpho, Compound, Blend and more. It standardizes rates to net APY (base ± fees ± rewards) and optimizes your capital allocation.
Live App · Documentation · X / Twitter · Farcaster · Discussions
<img src=".github/assets/app-demo.gif" alt="Lendwise app — DeFi supply rates compared across protocols and chains" width="900" />
⭐ If Lendwise helps you find better yields, star the repo — it helps other DeFi users discover it.
Comparing lending yields across protocols is harder than it looks: every protocol quotes rates differently (APR vs APY, per-second vs daily compounding), rewards live in separate systems (protocol emissions, Merkl campaigns), and fees quietly eat into headline numbers.
Lendwise reduces it to two promises:
Here are standardized APYs — now you can actually compare them.
base − fees + rewards, borrow base + fees − rewards. Headline numbers that mean the same thing on every protocol.Here is how to allocate your capital — matched to your risk profile and investment horizon.
| Protocol | Ethereum | Optimism | Polygon | Base | Arbitrum | Avalanche | Linea | BSC | Stellar |
|---|---|---|---|---|---|---|---|---|---|
| Aave V3 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| Morpho (Blue + MetaMorpho) | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | — | — |
| Compound V3 | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | — | — |
| Blend (V1 + V2) | — | — | — | — | — | — | — | — | ✅ |
Want another protocol? Request it — or build the adapter yourself: it's ~5 files with a test harness to validate it.
flowchart LR
A["Protocol adapters<br/>Aave · Morpho · Compound · Blend"] -->|"every 10 min"| B["APY collector"]
B --> C[("Postgres<br/>apy_hourly · apy_daily")]
C -->|"daily 00:30 UTC"| D["Reconcile<br/>detect → repair → aggregate → prune"]
D --> C
C --> E["GraphQL API<br/>/api/graphql"]
E --> F["Next.js app<br/>lendwise.fi"]
Each protocol is an isolated adapter that transforms its source (GraphQL API, subgraph, RPC) into a shared data model. Aggregation uses Promise.allSettled everywhere — one protocol having a bad day never blocks the others. The full design is documented in src/lib/protocols/README.md.
Prerequisites: Node.js 24, pnpm 11.
git clone https://github.com/lendwise-fi/lendwise
cd lendwise
pnpm install
cp .env.example .env.local # minimal: THEGRAPH_API_KEY (free) — see CONTRIBUTING.md
pnpm dev # → http://localhost:3000
pnpm codegen # regenerate GraphQL types (run before typecheck/test)
pnpm lint # ESLint
pnpm typecheck # tsc --noEmit (strict)
pnpm test # vitest
pnpm adapter:test <id> # live-test a protocol adapter (e.g. aave_v3)
pnpm build # codegen + next build
Next.js 16 (App Router) · TypeScript strict · Tailwind 4 + Radix UI · viem/wagmi · PostgreSQL (Neon) + Drizzle ORM · GraphQL (graphql-yoga + URQL + codegen) · The Graph · QStash cron.
| Doc | What's inside |
|---|---|
| Protocol adapters | Architecture, adapter contract, how to add a protocol |
| Contributing guide | Setup, quality bar, PR process |
Contributions are welcome — the most valuable one is a new protocol adapter, and the codebase is explicitly designed to make that a contained, testable unit of work (~5 files + a live test harness).
good first issue and protocol-request labelsMIT © Lendwise
hooks/register.tsx 517 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { RnessRow, RnessSnapshot } from '../types'
5
6// The rness plugin's mod (spec 0029 §3): the workspace on screen, drawn from
7// what the pinned CLI prints (`rness status --json`). Nothing is computed
8// here: the CLI words every line, the module only draws and refreshes.
9//
10// `sync` writes this file whole into `.claude/skills/rness/hooks/`, at the
11// workspace root and at the root of each clone, with `RNESS` set to the path
12// of `.rness` from that place, as the settings hooks have it. One path, never
13// a guess: a clone runs the workspace's pinned copy, never one its own tree
14// could hold.
15
16const snapshot = atom({ plugin: 'rness', key: 'snapshot' } as const, null)
17const tab = atom({ plugin: 'rness', key: 'tab' } as const, 0)
18const selected = atom({ plugin: 'rness', key: 'selected' } as const, 0)
19const interactive = atom(
20 { plugin: 'rness', key: 'interactive' } as const,
21 false
22)
23const open = atom({ plugin: 'rness', key: 'open' } as const, null)
24
25const PANE = 'rness'
26const BIN = 'node_modules/@rness/cli/dist/bin/rness.js'
27/** `.rness` from the project directory; `sync` writes `../../.rness` in a clone. */
28const RNESS = '../../.rness'
29const STATE = /[\\/]\.rness[\\/]/
30
31/**
32 * Agent Pulse's option colours, which the CLI names for each row (spec 0017
33 * §3), as the surface names them: the board's purple and pink are the
34 * terminal's magenta, its orange the terminal's yellow.
35 */
36const COLORS: Readonly<Record<string, string>> = {
37 gray: 'gray',
38 blue: 'blue',
39 green: 'green',
40 yellow: 'yellow',
41 orange: 'yellow',
42 red: 'red',
43 pink: 'magenta',
44 purple: 'magenta',
45}
46
47/** What one `Markdown` draws at most (10000 on Claude Code 2.1.289), with room. */
48const PIECE = 9000
49
50/** The project directory: the plugin lives in its `.claude/skills/rness`. */
51const projectOf = (root: string): string =>
52 root.replace(/[\\/]\.claude[\\/]skills[\\/]rness[\\/]?$/, '')
53
54const clamp = (n: number, low: number, high: number): number =>
55 Math.min(Math.max(n, low), high)
56
57/**
58 * Blank lines under the rows, so that the body is always taller than the
59 * pane: in a pane ↑/↓ are scroll keys (`pane:scrollUp`/`Down` on 2.1.289),
60 * which raise `ui.scroll` — the selection's keys here — only while there is
61 * something to scroll; otherwise they walk the buttons.
62 */
63const padOf = (bodyRows: number, rows: number): number =>
64 Math.max(0, bodyRows + 1 - (1 + rows))
65
66// The values as the state holds them now, read outside a drawing: each
67// reference a literal, so that the engine can list what the module reads.
68const SNAPSHOT = { plugin: 'rness', key: 'snapshot' } as const
69const TAB = { plugin: 'rness', key: 'tab' } as const
70const SELECTED = { plugin: 'rness', key: 'selected' } as const
71const OPEN = { plugin: 'rness', key: 'open' } as const
72const INTERACTIVE = { plugin: 'rness', key: 'interactive' } as const
73const snapshotNow = async ($: EngineInterface) =>
74 (await $.state.get(SNAPSHOT)).value ?? null
75const tabNow = async ($: EngineInterface) => (await $.state.get(TAB)).value ?? 0
76const selectedNow = async ($: EngineInterface) =>
77 (await $.state.get(SELECTED)).value ?? 0
78const openNow = async ($: EngineInterface) =>
79 (await $.state.get(OPEN)).value ?? null
80
81/**
82 * Reads the snapshot from the pinned CLI and keeps it; the footer's label
83 * follows, and so does the document open in the pane, read again. No
84 * pinned copy, or a run that fails: no snapshot, nothing drawn.
85 */
86async function refresh($: EngineInterface): Promise<RnessSnapshot | null> {
87 let next: RnessSnapshot | null = null
88 try {
89 const project = projectOf($.plugin.root)
90 const bin = `${project}/${RNESS}/${BIN}`
91 if (await $.fs.exists(bin)) {
92 // `RNESS_NO_DELEGATE`: a pin that drifts is reported (a note, in the
93 // band), never installed from inside the session, as the settings
94 // hooks have it (spec 0015 §3). `status` is not among the commands
95 // catch-up spares, and its install would replace a local copy.
96 const ran = await $.process.run(
97 ['node', bin, 'status', '--json', '--cwd', project],
98 { cwd: project, timeoutMs: 20_000, env: { RNESS_NO_DELEGATE: '1' } }
99 )
100 if (ran.exitCode === 0) next = JSON.parse(ran.stdout) as RnessSnapshot
101 }
102 } catch {
103 next = null
104 }
105 await update($, snapshot, () => next)
106 const opened = await openNow($)
107 if (opened !== null) await show($, opened.path)
108 return next
109}
110
111/** A refresh off the dispatch that asked for it: a turn never waits for node. */
112const later = ($: EngineInterface): void => {
113 $.clock.after(0, () => void refresh($))
114}
115
116/** The document at `path` in `.rness/`, read and shown in place of the list. */
117async function show($: EngineInterface, path: string): Promise<void> {
118 let text: string | null
119 try {
120 text = await $.fs.read(`${projectOf($.plugin.root)}/${RNESS}/${path}`)
121 } catch {
122 text = null
123 }
124 await update($, open, () => ({ path, text }))
125}
126
127/** The ring onto the row at `path`; a site without the keys refuses, and that is fine. */
128const ringOn = ($: EngineInterface, path: string): void => {
129 void $.ui.focus({ requestId: PANE, key: `open-${path}` }).catch(() => {})
130}
131
132/**
133 * Shows tab `i`: its newest row selected, the pane at its top, the ring on
134 * that row — from a digit, a click, or Tab and Shift+Tab (`cycleTab`).
135 */
136async function showTab($: EngineInterface, i: number): Promise<void> {
137 const shown = await snapshotNow($)
138 await update($, tab, () => i)
139 await update($, selected, () => 0)
140 void $.ui.scroll({ in: PANE, to: 'start' }).catch(() => {})
141 const first = shown?.tabs[i]?.rows[0]
142 if (first !== undefined) ringOn($, first.path)
143}
144
145/** The next tab (`+1`) or the previous (`-1`), wrapping. */
146async function cycleTab($: EngineInterface, by: 1 | -1): Promise<void> {
147 const shown = await snapshotNow($)
148 const n = shown?.tabs.length ?? 0
149 if (n === 0) return
150 const at = await tabNow($)
151 await showTab($, (at + by + n) % n)
152}
153
154/** The row of the document at `path`, whichever tab lists it. */
155const rowOf = (shown: RnessSnapshot, path: string): RnessRow | undefined =>
156 shown.tabs.flatMap((t) => t.rows).find((row) => row.path === path)
157
158/** `text` cut to `max` cells with an ellipsis, as the full-screen view cuts a title. */
159const cut = (text: string, max: number): string =>
160 text.length <= max ? text : `${text.slice(0, Math.max(1, max - 1))}…`
161
162/**
163 * The document without its front matter, as its issue's body starts (spec
164 * 0018 §3): the CLI's `stripFrontMatter`, the blank lines after it included.
165 */
166const bodyOf = (text: string): string =>
167 text
168 .replace(/^\uFEFF/, '')
169 .replace(/^---\r?\n(?:[\s\S]*?\r?\n)?---(?:\r?\n|$)/, '')
170 .replace(/^(?:[ \t]*\r?\n)+/, '')
171
172/** A line opening or closing a code fence, in a paragraph. */
173const FENCES = /^(`{3,}|~{3,})/gm
174
175/**
176 * `text` in pieces a `Markdown` can draw: cut at a blank line, a fenced block
177 * kept whole; a paragraph or a block longer than a piece is cut as it is. A
178 * control character other than tab and newline is dropped: a `Markdown`
179 * refuses it.
180 */
181function pieces(text: string): string[] {
182 const out: string[] = []
183 let piece = ''
184 const put = (unit: string): void => {
185 if (piece !== '' && piece.length + 2 + unit.length > PIECE) {
186 out.push(piece)
187 piece = ''
188 }
189 piece = piece === '' ? unit : `${piece}\n\n${unit}`
190 while (piece.length > PIECE) {
191 out.push(piece.slice(0, PIECE))
192 piece = piece.slice(PIECE)
193 }
194 }
195 let block: string | null = null
196 for (const para of text.replace(/[^\P{Cc}\t\n]/gu, '').split('\n\n')) {
197 const opensOrCloses = (para.match(FENCES) ?? []).length % 2 === 1
198 if (block !== null) {
199 block = `${block}\n\n${para}`
200 if (opensOrCloses) {
201 put(block)
202 block = null
203 }
204 } else if (opensOrCloses) block = para
205 else put(para)
206 }
207 if (block !== null) put(block)
208 if (piece !== '') out.push(piece)
209 return out
210}
211
212export const register: Register = (on) => {
213 on('session.start', async ($, e, next) => {
214 await update($, interactive, () => e.isInteractive)
215 later($)
216 return next(e)
217 })
218
219 // An edit of `.rness/` changes what the band and the pane show.
220 on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
221 const ran = await next(e)
222 if (STATE.test(e.file_path)) later($)
223 return ran
224 })
225 on('tool.call', { tool: 'Write' }, async ($, e, next) => {
226 const ran = await next(e)
227 if (STATE.test(e.file_path)) later($)
228 return ran
229 })
230
231 // What no tool shows: a `git pull`, a file written through Bash.
232 on('turn.complete', async ($, e, next) => {
233 if (e.agentId === undefined) later($)
234 return next(e)
235 })
236
237 // The notes to act on, whenever there are some (plan 0042). The banner is
238 // the session-start line's, said once in the transcript: drawn here too,
239 // it said the same thing twice.
240 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
241 const shown = await read($, snapshot)
242 if (e.props.hasSurvey || shown === null || shown.notes.length === 0)
243 return next(e)
244 const { Box, Text } = $.ui.resolve(e)
245 return (
246 <Box flexDirection="column">
247 {shown.notes.map((note, i) => (
248 <Box key={`note-${i}`}>
249 <Text color="yellow" wrap="truncate-end">
250 {note}
251 </Text>
252 </Box>
253 ))}
254 </Box>
255 )
256 })
257
258 // The workspace's label among the session modes at the right of the
259 // prompt footer (plan 0044): `rness · <scope> · <n> in progress`, worded
260 // by the CLI, after the engine's own (`focus`). A mode is dim and carries
261 // no sign; `$.ui.status` would draw it as a warning. No snapshot: the
262 // footer as the engine has it.
263 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
264 const shown = await read($, snapshot)
265 if (shown === null) return next(e)
266 return next({
267 ...e,
268 props: { modes: [...e.props.modes, shown.statusLine] },
269 })
270 })
271
272 // `/rness:status` is the skill's name: answered here with the pane, it
273 // runs no model turn; with no snapshot, or no one to see a pane, the
274 // skill's tables answer it.
275 on('command.run', { command: 'rness:status' }, async ($, e, next) => {
276 // Nobody to see a pane (`claude -p`): the tables are the answer.
277 if (!(await $.state.get(INTERACTIVE)).value) return next(e)
278 const shown = (await snapshotNow($)) ?? (await refresh($))
279 if (shown === null) return next(e)
280 const wanted = e.args.trim().toLowerCase()
281 const index = shown.tabs.findIndex(
282 (t) => t.name.toLowerCase() === wanted || t.label.toLowerCase() === wanted
283 )
284 if (index !== -1) await update($, tab, () => index)
285 // The command asks for the list, from its first row, whatever the pane
286 // showed before.
287 await update($, selected, () => 0)
288 await update($, open, () => null)
289 // The pane takes the keys (plans 0042, 0043): ↑/↓ select a row, Enter
290 // opens it, Tab and Shift+Tab change the tab, a digit too, Esc and `q`
291 // close. A pane binds no ←/→ (Claude Code 2.1.289).
292 const opened = await $.ui.open({
293 id: PANE,
294 title: `${shown.workspace} · status`,
295 focus: true,
296 closeOnEscape: true,
297 })
298 // A surface that places no pane (an older desktop): the tables instead.
299 if (!opened.isPlaced) return next(e)
300 return {
301 text: 'The status of the workspace is open beside the conversation.',
302 }
303 })
304
305 // ↑/↓, PgUp/PgDn, Home/End are the pane's scroll keys (2.1.289): here they
306 // move the selection, and the window follows it, the selected row kept
307 // near the middle. A document open scrolls as the engine scrolls.
308 on(
309 'ui.scroll',
310 { component: 'Pane', requestId: PANE },
311 async ($, e, next) => {
312 if (e.origin.kind !== 'person' || e.by === 0) return next(e)
313 const shown = await snapshotNow($)
314 if (shown === null || (await openNow($)) !== null) return next(e)
315 const rows = shown.tabs[await tabNow($)]?.rows ?? []
316 if (rows.length === 0) return next(e)
317 const last = rows.length - 1
318 const was = clamp(await selectedNow($), 0, last)
319 // An arrow is a row, Home and End are `contentRows`, a page key `bodyRows`.
320 const step =
321 Math.abs(e.by) <= 1
322 ? e.by
323 : Math.abs(e.by) >= e.contentRows
324 ? Math.sign(e.by) * rows.length
325 : Math.sign(e.by) * Math.max(1, e.bodyRows - 1)
326 const to = clamp(was + step, 0, last)
327 await update($, selected, () => to)
328 ringOn($, rows[to]!.path)
329 const header =
330 e.contentRows - rows.length - padOf(e.bodyRows, rows.length)
331 const half = Math.floor((e.bodyRows - 1) / 2)
332 const offset = clamp(
333 header + to - half,
334 0,
335 Math.max(0, e.contentRows - e.bodyRows)
336 )
337 return next({ ...e, offset })
338 }
339 )
340
341 // Tab and Shift+Tab walk the ring: from the selected row they land on the
342 // `›` and `‹` beside it, drawn for that. The ring stays on the row and the
343 // tab changes, as Tab does in the full-screen `rness status`. A click on
344 // another row makes it the selection; the press that follows opens it.
345 on('ui.focus', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
346 if (e.origin.kind !== 'person' || e.element === undefined) return next(e)
347 if (e.element === 'tab-next' || e.element === 'tab-prev') {
348 await cycleTab($, e.element === 'tab-next' ? 1 : -1)
349 return {}
350 }
351 if (e.element.startsWith('open-')) {
352 const path = e.element.slice('open-'.length)
353 const shown = await snapshotNow($)
354 const rows = shown?.tabs[await tabNow($)]?.rows ?? []
355 const i = rows.findIndex((row) => row.path === path)
356 if (i !== -1) await update($, selected, () => i)
357 }
358 return next(e)
359 })
360
361 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
362 const { Box, Button, Link, Markdown, Text } = $.ui.resolve(e)
363 const shown = await read($, snapshot)
364 if (shown === null)
365 return <Text dimColor>rness: the workspace status cannot be read.</Text>
366 const opened = await read($, open)
367
368 // One document, in place of the list: `q` brings the list back, Esc
369 // still closes the pane. Its item on Agent Pulse is a link.
370 if (opened !== null) {
371 const row = rowOf(shown, opened.path)
372 return (
373 <Box flexDirection="column">
374 <Box flexDirection="row" gap={1}>
375 {[
376 <Button
377 key="back"
378 label="Back"
379 plain
380 hotkey="q"
381 autoFocus
382 onPress={() => update($, open, () => null)}
383 />,
384 ...(row?.link != null
385 ? [<Link key="pulse" href={row.link} label="Agent Pulse" />]
386 : []),
387 ]}
388 </Box>
389 <Box key="head">
390 {row === undefined ? (
391 <Text bold wrap="truncate-end">
392 {opened.path}
393 </Text>
394 ) : (
395 <Text wrap="truncate-end">
396 <Text bold>
397 {row.id} {row.title}
398 </Text>{' '}
399 <Text color={COLORS[row.color]}>{row.status ?? '?'}</Text>
400 </Text>
401 )}
402 </Box>
403 {opened.text === null ? (
404 <Box key="unread">
405 <Text dimColor>rness: {opened.path} cannot be read.</Text>
406 </Box>
407 ) : (
408 pieces(bodyOf(opened.text)).map((piece, i) => (
409 <Markdown key={`doc-${i}`} text={piece} />
410 ))
411 )}
412 </Box>
413 )
414 }
415
416 const at = clamp(await read($, tab), 0, Math.max(0, shown.tabs.length - 1))
417 const current = shown.tabs[at]
418 const rows = current?.rows ?? []
419 const sel = clamp(await read($, selected), 0, Math.max(0, rows.length - 1))
420 const width = e.props.bodyColumns ?? 80
421 const pad = padOf(e.props.scroll?.bodyRows ?? 0, rows.length)
422 return (
423 <Box flexDirection="column">
424 <Box flexDirection="row" gap={1} flexWrap="wrap">
425 {[
426 ...shown.tabs.map((t, i) => (
427 <Button
428 key={`tab-${t.name}`}
429 label={`${i < 9 ? `${i + 1} ` : ''}${t.label} (${t.rows.length})`}
430 {...(i < 9 ? { hotkey: String(i + 1) } : {})}
431 variant={i === at ? 'primary' : 'secondary'}
432 // The ring starts here only when the tab has no row.
433 {...(i === at && rows.length === 0
434 ? { autoFocus: true as const }
435 : {})}
436 onPress={() => void showTab($, i)}
437 />
438 )),
439 <Button
440 key="close"
441 label="Close"
442 plain
443 hotkey="q"
444 role="dismiss"
445 onPress={() => $.ui.close({ id: PANE })}
446 />,
447 ...(shown.pulse != null
448 ? [<Link key="pulse" href={shown.pulse} label="Agent Pulse" />]
449 : []),
450 <Text key="keys" dimColor>
451 ⇥ tab · ↑↓ row · ⏎ open
452 </Text>,
453 ]}
454 </Box>
455 {rows.length === 0 ? (
456 <Box key="empty">
457 <Text dimColor>Nothing here yet.</Text>
458 </Box>
459 ) : (
460 // Every row, keyed by path (two documents of one date share an
461 // id): Enter or a click opens it. The selected row carries the
462 // ring and, beside it, `‹` and `›`: where Shift+Tab and Tab land.
463 rows.map((row, i) => {
464 const status = row.status ?? '?'
465 const isSel = i === sel
466 const label = cut(
467 `${row.id} ${row.title}`,
468 width - status.length - (isSel ? 5 : 3)
469 )
470 return (
471 <Box key={`row-${row.path}`} flexDirection="row" gap={1}>
472 {[
473 isSel ? (
474 <Button
475 key="tab-prev"
476 plain
477 dimColor
478 label="‹"
479 onPress={() => void cycleTab($, -1)}
480 />
481 ) : (
482 <Text> </Text>
483 ),
484 <Button
485 key={`open-${row.path}`}
486 plain
487 label={label}
488 {...(isSel ? { autoFocus: true as const } : {})}
489 onPress={() => void show($, row.path)}
490 />,
491 <Text color={COLORS[row.color]}>{status}</Text>,
492 ...(isSel
493 ? [
494 <Button
495 key="tab-next"
496 plain
497 dimColor
498 label="›"
499 onPress={() => void cycleTab($, 1)}
500 />,
501 ]
502 : []),
503 ]}
504 </Box>
505 )
506 })
507 )}
508 {Array.from({ length: pad }, (_, i) => (
509 <Box key={`pad-${i}`}>
510 <Text> </Text>
511 </Box>
512 ))}
513 </Box>
514 )
515 })
516}
517types/index.d.ts 50 lines1// The values the rness mod keeps in `$.state` (spec 0029 §3.2). The
2// snapshot is what `rness status --json` prints: `StatusSnapshot` in
3// `src/commands/snapshot.ts`, read here as data.
4
5export type RnessRow = {
6 id: string
7 title: string
8 status: string | null
9 path: string
10 /** The colour of its status on Agent Pulse, by the board's name (`yellow`). */
11 color: string
12 /** Its item on Agent Pulse; null while the workspace declares no pulse. */
13 link: string | null
14}
15
16export type RnessTab = { name: string; label: string; rows: RnessRow[] }
17
18export type RnessSnapshot = {
19 version: string
20 workspace: string
21 scope: string | null
22 banner: string
23 statusLine: string
24 inProgress: string[]
25 notes: string[]
26 /** Agent Pulse, the board; null while none is declared. */
27 pulse: string | null
28 tabs: RnessTab[]
29}
30
31/** The document the pane shows in place of the list: its path, and its text, or null when it could not be read. */
32export type RnessOpen = { path: string; text: string | null }
33
34declare module 'claude-code' {
35 interface PluginState {
36 rness: {
37 /** The last snapshot; null while none could be read. */
38 snapshot: RnessSnapshot | null
39 /** The pane's tab, by its index in the snapshot. */
40 tab: number
41 /** The selected row of that tab, by its index: ↑/↓ move it, Enter opens it. */
42 selected: number
43 /** Whether someone watches the session: no pane for `claude -p`. */
44 interactive: boolean
45 /** The document open in the pane; null while the list shows. */
46 open: RnessOpen | null
47 }
48 }
49}
50