@rness/cli 0.19.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ import {
62
62
  workspaceDirs,
63
63
  writeFileAtomic,
64
64
  writeManifest
65
- } from "./chunk-U5SBNWVD.js";
65
+ } from "./chunk-2AFAQO5B.js";
66
66
 
67
67
  // src/index.ts
68
68
  async function run2(argv) {
@@ -1,7 +1,7 @@
1
1
  import { atom, read, update } from 'claude-code'
2
2
  import type { EngineInterface, Register } from 'claude-code'
3
3
 
4
- import type { RnessSnapshot } from '../types'
4
+ import type { RnessRow, RnessSnapshot } from '../types'
5
5
 
6
6
  // The rness plugin's mod (spec 0029 §3): the workspace on screen, drawn from
7
7
  // what the pinned CLI prints (`rness status --json`). Nothing is computed
@@ -15,10 +15,12 @@ import type { RnessSnapshot } from '../types'
15
15
 
16
16
  const snapshot = atom({ plugin: 'rness', key: 'snapshot' } as const, null)
17
17
  const tab = atom({ plugin: 'rness', key: 'tab' } as const, 0)
18
+ const selected = atom({ plugin: 'rness', key: 'selected' } as const, 0)
18
19
  const interactive = atom(
19
20
  { plugin: 'rness', key: 'interactive' } as const,
20
21
  false
21
22
  )
23
+ const open = atom({ plugin: 'rness', key: 'open' } as const, null)
22
24
 
23
25
  const PANE = 'rness'
24
26
  const BIN = 'node_modules/@rness/cli/dist/bin/rness.js'
@@ -26,13 +28,60 @@ const BIN = 'node_modules/@rness/cli/dist/bin/rness.js'
26
28
  const RNESS = '.rness'
27
29
  const STATE = /[\\/]\.rness[\\/]/
28
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
+ */
36
+ const 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. */
48
+ const PIECE = 9000
49
+
29
50
  /** The project directory: the plugin lives in its `.claude/skills/rness`. */
30
51
  const projectOf = (root: string): string =>
31
52
  root.replace(/[\\/]\.claude[\\/]skills[\\/]rness[\\/]?$/, '')
32
53
 
54
+ const clamp = (n: number, low: number, high: number): number =>
55
+ Math.min(Math.max(n, low), high)
56
+
33
57
  /**
34
- * Reads the snapshot from the pinned CLI and keeps it; the status line
35
- * follows. No pinned copy, or a run that fails: no snapshot, nothing drawn.
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
+ */
63
+ const 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.
68
+ const SNAPSHOT = { plugin: 'rness', key: 'snapshot' } as const
69
+ const TAB = { plugin: 'rness', key: 'tab' } as const
70
+ const SELECTED = { plugin: 'rness', key: 'selected' } as const
71
+ const OPEN = { plugin: 'rness', key: 'open' } as const
72
+ const INTERACTIVE = { plugin: 'rness', key: 'interactive' } as const
73
+ const snapshotNow = async ($: EngineInterface) =>
74
+ (await $.state.get(SNAPSHOT)).value ?? null
75
+ const tabNow = async ($: EngineInterface) => (await $.state.get(TAB)).value ?? 0
76
+ const selectedNow = async ($: EngineInterface) =>
77
+ (await $.state.get(SELECTED)).value ?? 0
78
+ const 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.
36
85
  */
37
86
  async function refresh($: EngineInterface): Promise<RnessSnapshot | null> {
38
87
  let next: RnessSnapshot | null = null
@@ -40,9 +89,13 @@ async function refresh($: EngineInterface): Promise<RnessSnapshot | null> {
40
89
  const project = projectOf($.plugin.root)
41
90
  const bin = `${project}/${RNESS}/${BIN}`
42
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.
43
96
  const ran = await $.process.run(
44
97
  ['node', bin, 'status', '--json', '--cwd', project],
45
- { cwd: project, timeoutMs: 20_000 }
98
+ { cwd: project, timeoutMs: 20_000, env: { RNESS_NO_DELEGATE: '1' } }
46
99
  )
47
100
  if (ran.exitCode === 0) next = JSON.parse(ran.stdout) as RnessSnapshot
48
101
  }
@@ -50,7 +103,8 @@ async function refresh($: EngineInterface): Promise<RnessSnapshot | null> {
50
103
  next = null
51
104
  }
52
105
  await update($, snapshot, () => next)
53
- $.ui.status(next?.statusLine)
106
+ const opened = await openNow($)
107
+ if (opened !== null) await show($, opened.path)
54
108
  return next
55
109
  }
56
110
 
@@ -59,6 +113,102 @@ const later = ($: EngineInterface): void => {
59
113
  $.clock.after(0, () => void refresh($))
60
114
  }
61
115
 
116
+ /** The document at `path` in `.rness/`, read and shown in place of the list. */
117
+ async 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. */
128
+ const 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
+ */
136
+ async 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. */
146
+ async 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. */
155
+ const 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. */
159
+ const 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
+ */
166
+ const 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. */
173
+ const 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
+ */
181
+ function 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
+
62
212
  export const register: Register = (on) => {
63
213
  on('session.start', async ($, e, next) => {
64
214
  await update($, interactive, () => e.isInteractive)
@@ -105,25 +255,40 @@ export const register: Register = (on) => {
105
255
  )
106
256
  })
107
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
+
108
272
  // `/rness:status` is the skill's name: answered here with the pane, it
109
273
  // runs no model turn; with no snapshot, or no one to see a pane, the
110
274
  // skill's tables answer it.
111
275
  on('command.run', { command: 'rness:status' }, async ($, e, next) => {
112
276
  // Nobody to see a pane (`claude -p`): the tables are the answer.
113
- if (!(await $.state.get({ plugin: 'rness', key: 'interactive' })).value)
114
- return next(e)
115
- const shown =
116
- (await $.state.get({ plugin: 'rness', key: 'snapshot' })).value ??
117
- (await refresh($))
118
- if (shown === null || shown === undefined) return next(e)
277
+ if (!(await $.state.get(INTERACTIVE)).value) return next(e)
278
+ const shown = (await snapshotNow($)) ?? (await refresh($))
279
+ if (shown === null) return next(e)
119
280
  const wanted = e.args.trim().toLowerCase()
120
281
  const index = shown.tabs.findIndex(
121
282
  (t) => t.name.toLowerCase() === wanted || t.label.toLowerCase() === wanted
122
283
  )
123
- await update($, tab, (i) => (index === -1 ? i : index))
124
- // The pane takes the keys (plan 0042): a digit or Tab then Enter shows a
125
- // tab, ↑/↓ scroll as in the full-screen `rness status`, Esc and `q`
126
- // close it. A pane binds no ←/→ (Claude Code 2.1.289).
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).
127
292
  const opened = await $.ui.open({
128
293
  id: PANE,
129
294
  title: `${shown.workspace} · status`,
@@ -137,51 +302,214 @@ export const register: Register = (on) => {
137
302
  }
138
303
  })
139
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
+
140
361
  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
141
- const { Box, Button, Text } = $.ui.resolve(e)
362
+ const { Box, Button, Link, Markdown, Text } = $.ui.resolve(e)
142
363
  const shown = await read($, snapshot)
143
364
  if (shown === null)
144
365
  return <Text dimColor>rness: the workspace status cannot be read.</Text>
145
- const at = Math.min(await read($, tab), Math.max(0, shown.tabs.length - 1))
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))
146
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)
147
422
  return (
148
423
  <Box flexDirection="column">
149
424
  <Box flexDirection="row" gap={1} flexWrap="wrap">
150
- {shown.tabs.map((t, i) => (
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
+ )),
151
439
  <Button
152
- key={`tab-${t.name}`}
153
- // Where the body has nothing to scroll, ↑/↓ walk these buttons:
154
- // only a press (Enter, a digit, a click) changes the tab.
155
- label={`${i < 9 ? `${i + 1} ` : ''}${t.label} (${t.rows.length})`}
156
- {...(i < 9 ? { hotkey: String(i + 1) } : {})}
157
- variant={i === at ? 'primary' : 'secondary'}
158
- {...(i === at ? { autoFocus: true as const } : {})}
159
- onPress={() => update($, tab, () => i)}
160
- />
161
- ))}
162
- <Button
163
- key="close"
164
- label="Close"
165
- plain
166
- hotkey="q"
167
- role="dismiss"
168
- onPress={() => $.ui.close({ id: PANE })}
169
- />
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
+ ]}
170
454
  </Box>
171
- {current === undefined || current.rows.length === 0 ? (
455
+ {rows.length === 0 ? (
172
456
  <Box key="empty">
173
457
  <Text dimColor>Nothing here yet.</Text>
174
458
  </Box>
175
459
  ) : (
176
- // Every row: the engine scrolls the body (↑/↓, PgUp/PgDn, Home/End).
177
- current.rows.map((row) => (
178
- <Box key={`row-${row.id}`}>
179
- <Text wrap="truncate-end">
180
- {row.id} {row.title} <Text dimColor>{row.status ?? '?'}</Text>
181
- </Text>
182
- </Box>
183
- ))
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
+ })
184
507
  )}
508
+ {Array.from({ length: pad }, (_, i) => (
509
+ <Box key={`pad-${i}`}>
510
+ <Text> </Text>
511
+ </Box>
512
+ ))}
185
513
  </Box>
186
514
  )
187
515
  })
@@ -7,6 +7,10 @@ export type RnessRow = {
7
7
  title: string
8
8
  status: string | null
9
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
10
14
  }
11
15
 
12
16
  export type RnessTab = { name: string; label: string; rows: RnessRow[] }
@@ -19,9 +23,14 @@ export type RnessSnapshot = {
19
23
  statusLine: string
20
24
  inProgress: string[]
21
25
  notes: string[]
26
+ /** Agent Pulse, the board; null while none is declared. */
27
+ pulse: string | null
22
28
  tabs: RnessTab[]
23
29
  }
24
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. */
32
+ export type RnessOpen = { path: string; text: string | null }
33
+
25
34
  declare module 'claude-code' {
26
35
  interface PluginState {
27
36
  rness: {
@@ -29,8 +38,12 @@ declare module 'claude-code' {
29
38
  snapshot: RnessSnapshot | null
30
39
  /** The pane's tab, by its index in the snapshot. */
31
40
  tab: number
41
+ /** The selected row of that tab, by its index: ↑/↓ move it, Enter opens it. */
42
+ selected: number
32
43
  /** Whether someone watches the session: no pane for `claude -p`. */
33
44
  interactive: boolean
45
+ /** The document open in the pane; null while the list shows. */
46
+ open: RnessOpen | null
34
47
  }
35
48
  }
36
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rness/cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Configuration-plane CLI for rness workspaces: resolve, validate and sync org-level context for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",