@rness/cli 0.19.0 → 0.20.0
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/README.md +37 -11
- package/dist/bin/rness.js +1 -1
- package/dist/{chunk-U5SBNWVD.js → chunk-WIZHU4WT.js} +27 -5
- package/dist/chunk-WIZHU4WT.js.map +1 -0
- package/dist/index.js +1 -1
- package/mod/hooks/register.tsx +353 -42
- package/mod/types/index.d.ts +13 -0
- package/package.json +1 -1
- package/dist/chunk-U5SBNWVD.js.map +0 -1
package/dist/index.js
CHANGED
package/mod/hooks/register.tsx
CHANGED
|
@@ -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
|
+
|
|
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
|
+
*/
|
|
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
|
+
|
|
33
81
|
/**
|
|
34
82
|
* Reads the snapshot from the pinned CLI and keeps it; the status line
|
|
35
|
-
* follows
|
|
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
|
|
@@ -51,6 +100,8 @@ async function refresh($: EngineInterface): Promise<RnessSnapshot | null> {
|
|
|
51
100
|
}
|
|
52
101
|
await update($, snapshot, () => next)
|
|
53
102
|
$.ui.status(next?.statusLine)
|
|
103
|
+
const opened = await openNow($)
|
|
104
|
+
if (opened !== null) await show($, opened.path)
|
|
54
105
|
return next
|
|
55
106
|
}
|
|
56
107
|
|
|
@@ -59,6 +110,102 @@ const later = ($: EngineInterface): void => {
|
|
|
59
110
|
$.clock.after(0, () => void refresh($))
|
|
60
111
|
}
|
|
61
112
|
|
|
113
|
+
/** The document at `path` in `.rness/`, read and shown in place of the list. */
|
|
114
|
+
async function show($: EngineInterface, path: string): Promise<void> {
|
|
115
|
+
let text: string | null
|
|
116
|
+
try {
|
|
117
|
+
text = await $.fs.read(`${projectOf($.plugin.root)}/${RNESS}/${path}`)
|
|
118
|
+
} catch {
|
|
119
|
+
text = null
|
|
120
|
+
}
|
|
121
|
+
await update($, open, () => ({ path, text }))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The ring onto the row at `path`; a site without the keys refuses, and that is fine. */
|
|
125
|
+
const ringOn = ($: EngineInterface, path: string): void => {
|
|
126
|
+
void $.ui.focus({ requestId: PANE, key: `open-${path}` }).catch(() => {})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Shows tab `i`: its newest row selected, the pane at its top, the ring on
|
|
131
|
+
* that row — from a digit, a click, or Tab and Shift+Tab (`cycleTab`).
|
|
132
|
+
*/
|
|
133
|
+
async function showTab($: EngineInterface, i: number): Promise<void> {
|
|
134
|
+
const shown = await snapshotNow($)
|
|
135
|
+
await update($, tab, () => i)
|
|
136
|
+
await update($, selected, () => 0)
|
|
137
|
+
void $.ui.scroll({ in: PANE, to: 'start' }).catch(() => {})
|
|
138
|
+
const first = shown?.tabs[i]?.rows[0]
|
|
139
|
+
if (first !== undefined) ringOn($, first.path)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The next tab (`+1`) or the previous (`-1`), wrapping. */
|
|
143
|
+
async function cycleTab($: EngineInterface, by: 1 | -1): Promise<void> {
|
|
144
|
+
const shown = await snapshotNow($)
|
|
145
|
+
const n = shown?.tabs.length ?? 0
|
|
146
|
+
if (n === 0) return
|
|
147
|
+
const at = await tabNow($)
|
|
148
|
+
await showTab($, (at + by + n) % n)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The row of the document at `path`, whichever tab lists it. */
|
|
152
|
+
const rowOf = (shown: RnessSnapshot, path: string): RnessRow | undefined =>
|
|
153
|
+
shown.tabs.flatMap((t) => t.rows).find((row) => row.path === path)
|
|
154
|
+
|
|
155
|
+
/** `text` cut to `max` cells with an ellipsis, as the full-screen view cuts a title. */
|
|
156
|
+
const cut = (text: string, max: number): string =>
|
|
157
|
+
text.length <= max ? text : `${text.slice(0, Math.max(1, max - 1))}…`
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The document without its front matter, as its issue's body starts (spec
|
|
161
|
+
* 0018 §3): the CLI's `stripFrontMatter`, the blank lines after it included.
|
|
162
|
+
*/
|
|
163
|
+
const bodyOf = (text: string): string =>
|
|
164
|
+
text
|
|
165
|
+
.replace(/^\uFEFF/, '')
|
|
166
|
+
.replace(/^---\r?\n(?:[\s\S]*?\r?\n)?---(?:\r?\n|$)/, '')
|
|
167
|
+
.replace(/^(?:[ \t]*\r?\n)+/, '')
|
|
168
|
+
|
|
169
|
+
/** A line opening or closing a code fence, in a paragraph. */
|
|
170
|
+
const FENCES = /^(`{3,}|~{3,})/gm
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* `text` in pieces a `Markdown` can draw: cut at a blank line, a fenced block
|
|
174
|
+
* kept whole; a paragraph or a block longer than a piece is cut as it is. A
|
|
175
|
+
* control character other than tab and newline is dropped: a `Markdown`
|
|
176
|
+
* refuses it.
|
|
177
|
+
*/
|
|
178
|
+
function pieces(text: string): string[] {
|
|
179
|
+
const out: string[] = []
|
|
180
|
+
let piece = ''
|
|
181
|
+
const put = (unit: string): void => {
|
|
182
|
+
if (piece !== '' && piece.length + 2 + unit.length > PIECE) {
|
|
183
|
+
out.push(piece)
|
|
184
|
+
piece = ''
|
|
185
|
+
}
|
|
186
|
+
piece = piece === '' ? unit : `${piece}\n\n${unit}`
|
|
187
|
+
while (piece.length > PIECE) {
|
|
188
|
+
out.push(piece.slice(0, PIECE))
|
|
189
|
+
piece = piece.slice(PIECE)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let block: string | null = null
|
|
193
|
+
for (const para of text.replace(/[^\P{Cc}\t\n]/gu, '').split('\n\n')) {
|
|
194
|
+
const opensOrCloses = (para.match(FENCES) ?? []).length % 2 === 1
|
|
195
|
+
if (block !== null) {
|
|
196
|
+
block = `${block}\n\n${para}`
|
|
197
|
+
if (opensOrCloses) {
|
|
198
|
+
put(block)
|
|
199
|
+
block = null
|
|
200
|
+
}
|
|
201
|
+
} else if (opensOrCloses) block = para
|
|
202
|
+
else put(para)
|
|
203
|
+
}
|
|
204
|
+
if (block !== null) put(block)
|
|
205
|
+
if (piece !== '') out.push(piece)
|
|
206
|
+
return out
|
|
207
|
+
}
|
|
208
|
+
|
|
62
209
|
export const register: Register = (on) => {
|
|
63
210
|
on('session.start', async ($, e, next) => {
|
|
64
211
|
await update($, interactive, () => e.isInteractive)
|
|
@@ -110,20 +257,21 @@ export const register: Register = (on) => {
|
|
|
110
257
|
// skill's tables answer it.
|
|
111
258
|
on('command.run', { command: 'rness:status' }, async ($, e, next) => {
|
|
112
259
|
// Nobody to see a pane (`claude -p`): the tables are the answer.
|
|
113
|
-
if (!(await $.state.get(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
(await $.state.get({ plugin: 'rness', key: 'snapshot' })).value ??
|
|
117
|
-
(await refresh($))
|
|
118
|
-
if (shown === null || shown === undefined) return next(e)
|
|
260
|
+
if (!(await $.state.get(INTERACTIVE)).value) return next(e)
|
|
261
|
+
const shown = (await snapshotNow($)) ?? (await refresh($))
|
|
262
|
+
if (shown === null) return next(e)
|
|
119
263
|
const wanted = e.args.trim().toLowerCase()
|
|
120
264
|
const index = shown.tabs.findIndex(
|
|
121
265
|
(t) => t.name.toLowerCase() === wanted || t.label.toLowerCase() === wanted
|
|
122
266
|
)
|
|
123
|
-
await update($, tab, (
|
|
124
|
-
// The
|
|
125
|
-
//
|
|
126
|
-
|
|
267
|
+
if (index !== -1) await update($, tab, () => index)
|
|
268
|
+
// The command asks for the list, from its first row, whatever the pane
|
|
269
|
+
// showed before.
|
|
270
|
+
await update($, selected, () => 0)
|
|
271
|
+
await update($, open, () => null)
|
|
272
|
+
// The pane takes the keys (plans 0042, 0043): ↑/↓ select a row, Enter
|
|
273
|
+
// opens it, Tab and Shift+Tab change the tab, a digit too, Esc and `q`
|
|
274
|
+
// close. A pane binds no ←/→ (Claude Code 2.1.289).
|
|
127
275
|
const opened = await $.ui.open({
|
|
128
276
|
id: PANE,
|
|
129
277
|
title: `${shown.workspace} · status`,
|
|
@@ -137,51 +285,214 @@ export const register: Register = (on) => {
|
|
|
137
285
|
}
|
|
138
286
|
})
|
|
139
287
|
|
|
288
|
+
// ↑/↓, PgUp/PgDn, Home/End are the pane's scroll keys (2.1.289): here they
|
|
289
|
+
// move the selection, and the window follows it, the selected row kept
|
|
290
|
+
// near the middle. A document open scrolls as the engine scrolls.
|
|
291
|
+
on(
|
|
292
|
+
'ui.scroll',
|
|
293
|
+
{ component: 'Pane', requestId: PANE },
|
|
294
|
+
async ($, e, next) => {
|
|
295
|
+
if (e.origin.kind !== 'person' || e.by === 0) return next(e)
|
|
296
|
+
const shown = await snapshotNow($)
|
|
297
|
+
if (shown === null || (await openNow($)) !== null) return next(e)
|
|
298
|
+
const rows = shown.tabs[await tabNow($)]?.rows ?? []
|
|
299
|
+
if (rows.length === 0) return next(e)
|
|
300
|
+
const last = rows.length - 1
|
|
301
|
+
const was = clamp(await selectedNow($), 0, last)
|
|
302
|
+
// An arrow is a row, Home and End are `contentRows`, a page key `bodyRows`.
|
|
303
|
+
const step =
|
|
304
|
+
Math.abs(e.by) <= 1
|
|
305
|
+
? e.by
|
|
306
|
+
: Math.abs(e.by) >= e.contentRows
|
|
307
|
+
? Math.sign(e.by) * rows.length
|
|
308
|
+
: Math.sign(e.by) * Math.max(1, e.bodyRows - 1)
|
|
309
|
+
const to = clamp(was + step, 0, last)
|
|
310
|
+
await update($, selected, () => to)
|
|
311
|
+
ringOn($, rows[to]!.path)
|
|
312
|
+
const header =
|
|
313
|
+
e.contentRows - rows.length - padOf(e.bodyRows, rows.length)
|
|
314
|
+
const half = Math.floor((e.bodyRows - 1) / 2)
|
|
315
|
+
const offset = clamp(
|
|
316
|
+
header + to - half,
|
|
317
|
+
0,
|
|
318
|
+
Math.max(0, e.contentRows - e.bodyRows)
|
|
319
|
+
)
|
|
320
|
+
return next({ ...e, offset })
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
// Tab and Shift+Tab walk the ring: from the selected row they land on the
|
|
325
|
+
// `›` and `‹` beside it, drawn for that. The ring stays on the row and the
|
|
326
|
+
// tab changes, as Tab does in the full-screen `rness status`. A click on
|
|
327
|
+
// another row makes it the selection; the press that follows opens it.
|
|
328
|
+
on('ui.focus', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
|
|
329
|
+
if (e.origin.kind !== 'person' || e.element === undefined) return next(e)
|
|
330
|
+
if (e.element === 'tab-next' || e.element === 'tab-prev') {
|
|
331
|
+
await cycleTab($, e.element === 'tab-next' ? 1 : -1)
|
|
332
|
+
return {}
|
|
333
|
+
}
|
|
334
|
+
if (e.element.startsWith('open-')) {
|
|
335
|
+
const path = e.element.slice('open-'.length)
|
|
336
|
+
const shown = await snapshotNow($)
|
|
337
|
+
const rows = shown?.tabs[await tabNow($)]?.rows ?? []
|
|
338
|
+
const i = rows.findIndex((row) => row.path === path)
|
|
339
|
+
if (i !== -1) await update($, selected, () => i)
|
|
340
|
+
}
|
|
341
|
+
return next(e)
|
|
342
|
+
})
|
|
343
|
+
|
|
140
344
|
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
141
|
-
const { Box, Button, Text } = $.ui.resolve(e)
|
|
345
|
+
const { Box, Button, Link, Markdown, Text } = $.ui.resolve(e)
|
|
142
346
|
const shown = await read($, snapshot)
|
|
143
347
|
if (shown === null)
|
|
144
348
|
return <Text dimColor>rness: the workspace status cannot be read.</Text>
|
|
145
|
-
const
|
|
349
|
+
const opened = await read($, open)
|
|
350
|
+
|
|
351
|
+
// One document, in place of the list: `q` brings the list back, Esc
|
|
352
|
+
// still closes the pane. Its item on Agent Pulse is a link.
|
|
353
|
+
if (opened !== null) {
|
|
354
|
+
const row = rowOf(shown, opened.path)
|
|
355
|
+
return (
|
|
356
|
+
<Box flexDirection="column">
|
|
357
|
+
<Box flexDirection="row" gap={1}>
|
|
358
|
+
{[
|
|
359
|
+
<Button
|
|
360
|
+
key="back"
|
|
361
|
+
label="Back"
|
|
362
|
+
plain
|
|
363
|
+
hotkey="q"
|
|
364
|
+
autoFocus
|
|
365
|
+
onPress={() => update($, open, () => null)}
|
|
366
|
+
/>,
|
|
367
|
+
...(row?.link != null
|
|
368
|
+
? [<Link key="pulse" href={row.link} label="Agent Pulse" />]
|
|
369
|
+
: []),
|
|
370
|
+
]}
|
|
371
|
+
</Box>
|
|
372
|
+
<Box key="head">
|
|
373
|
+
{row === undefined ? (
|
|
374
|
+
<Text bold wrap="truncate-end">
|
|
375
|
+
{opened.path}
|
|
376
|
+
</Text>
|
|
377
|
+
) : (
|
|
378
|
+
<Text wrap="truncate-end">
|
|
379
|
+
<Text bold>
|
|
380
|
+
{row.id} {row.title}
|
|
381
|
+
</Text>{' '}
|
|
382
|
+
<Text color={COLORS[row.color]}>{row.status ?? '?'}</Text>
|
|
383
|
+
</Text>
|
|
384
|
+
)}
|
|
385
|
+
</Box>
|
|
386
|
+
{opened.text === null ? (
|
|
387
|
+
<Box key="unread">
|
|
388
|
+
<Text dimColor>rness: {opened.path} cannot be read.</Text>
|
|
389
|
+
</Box>
|
|
390
|
+
) : (
|
|
391
|
+
pieces(bodyOf(opened.text)).map((piece, i) => (
|
|
392
|
+
<Markdown key={`doc-${i}`} text={piece} />
|
|
393
|
+
))
|
|
394
|
+
)}
|
|
395
|
+
</Box>
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const at = clamp(await read($, tab), 0, Math.max(0, shown.tabs.length - 1))
|
|
146
400
|
const current = shown.tabs[at]
|
|
401
|
+
const rows = current?.rows ?? []
|
|
402
|
+
const sel = clamp(await read($, selected), 0, Math.max(0, rows.length - 1))
|
|
403
|
+
const width = e.props.bodyColumns ?? 80
|
|
404
|
+
const pad = padOf(e.props.scroll?.bodyRows ?? 0, rows.length)
|
|
147
405
|
return (
|
|
148
406
|
<Box flexDirection="column">
|
|
149
407
|
<Box flexDirection="row" gap={1} flexWrap="wrap">
|
|
150
|
-
{
|
|
408
|
+
{[
|
|
409
|
+
...shown.tabs.map((t, i) => (
|
|
410
|
+
<Button
|
|
411
|
+
key={`tab-${t.name}`}
|
|
412
|
+
label={`${i < 9 ? `${i + 1} ` : ''}${t.label} (${t.rows.length})`}
|
|
413
|
+
{...(i < 9 ? { hotkey: String(i + 1) } : {})}
|
|
414
|
+
variant={i === at ? 'primary' : 'secondary'}
|
|
415
|
+
// The ring starts here only when the tab has no row.
|
|
416
|
+
{...(i === at && rows.length === 0
|
|
417
|
+
? { autoFocus: true as const }
|
|
418
|
+
: {})}
|
|
419
|
+
onPress={() => void showTab($, i)}
|
|
420
|
+
/>
|
|
421
|
+
)),
|
|
151
422
|
<Button
|
|
152
|
-
key=
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
hotkey="q"
|
|
167
|
-
role="dismiss"
|
|
168
|
-
onPress={() => $.ui.close({ id: PANE })}
|
|
169
|
-
/>
|
|
423
|
+
key="close"
|
|
424
|
+
label="Close"
|
|
425
|
+
plain
|
|
426
|
+
hotkey="q"
|
|
427
|
+
role="dismiss"
|
|
428
|
+
onPress={() => $.ui.close({ id: PANE })}
|
|
429
|
+
/>,
|
|
430
|
+
...(shown.pulse != null
|
|
431
|
+
? [<Link key="pulse" href={shown.pulse} label="Agent Pulse" />]
|
|
432
|
+
: []),
|
|
433
|
+
<Text key="keys" dimColor>
|
|
434
|
+
⇥ tab · ↑↓ row · ⏎ open
|
|
435
|
+
</Text>,
|
|
436
|
+
]}
|
|
170
437
|
</Box>
|
|
171
|
-
{
|
|
438
|
+
{rows.length === 0 ? (
|
|
172
439
|
<Box key="empty">
|
|
173
440
|
<Text dimColor>Nothing here yet.</Text>
|
|
174
441
|
</Box>
|
|
175
442
|
) : (
|
|
176
|
-
// Every row
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
443
|
+
// Every row, keyed by path (two documents of one date share an
|
|
444
|
+
// id): Enter or a click opens it. The selected row carries the
|
|
445
|
+
// ring and, beside it, `‹` and `›`: where Shift+Tab and Tab land.
|
|
446
|
+
rows.map((row, i) => {
|
|
447
|
+
const status = row.status ?? '?'
|
|
448
|
+
const isSel = i === sel
|
|
449
|
+
const label = cut(
|
|
450
|
+
`${row.id} ${row.title}`,
|
|
451
|
+
width - status.length - (isSel ? 5 : 3)
|
|
452
|
+
)
|
|
453
|
+
return (
|
|
454
|
+
<Box key={`row-${row.path}`} flexDirection="row" gap={1}>
|
|
455
|
+
{[
|
|
456
|
+
isSel ? (
|
|
457
|
+
<Button
|
|
458
|
+
key="tab-prev"
|
|
459
|
+
plain
|
|
460
|
+
dimColor
|
|
461
|
+
label="‹"
|
|
462
|
+
onPress={() => void cycleTab($, -1)}
|
|
463
|
+
/>
|
|
464
|
+
) : (
|
|
465
|
+
<Text> </Text>
|
|
466
|
+
),
|
|
467
|
+
<Button
|
|
468
|
+
key={`open-${row.path}`}
|
|
469
|
+
plain
|
|
470
|
+
label={label}
|
|
471
|
+
{...(isSel ? { autoFocus: true as const } : {})}
|
|
472
|
+
onPress={() => void show($, row.path)}
|
|
473
|
+
/>,
|
|
474
|
+
<Text color={COLORS[row.color]}>{status}</Text>,
|
|
475
|
+
...(isSel
|
|
476
|
+
? [
|
|
477
|
+
<Button
|
|
478
|
+
key="tab-next"
|
|
479
|
+
plain
|
|
480
|
+
dimColor
|
|
481
|
+
label="›"
|
|
482
|
+
onPress={() => void cycleTab($, 1)}
|
|
483
|
+
/>,
|
|
484
|
+
]
|
|
485
|
+
: []),
|
|
486
|
+
]}
|
|
487
|
+
</Box>
|
|
488
|
+
)
|
|
489
|
+
})
|
|
184
490
|
)}
|
|
491
|
+
{Array.from({ length: pad }, (_, i) => (
|
|
492
|
+
<Box key={`pad-${i}`}>
|
|
493
|
+
<Text> </Text>
|
|
494
|
+
</Box>
|
|
495
|
+
))}
|
|
185
496
|
</Box>
|
|
186
497
|
)
|
|
187
498
|
})
|
package/mod/types/index.d.ts
CHANGED
|
@@ -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