@nredd/pi-session-kit 0.6.0 → 0.8.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/CHANGELOG.md +28 -0
- package/README.md +35 -8
- package/docs/mon-dashboard.md +123 -0
- package/docs/stats-snapshots.md +19 -2
- package/package.json +8 -3
- package/src/config.ts +73 -2
- package/src/index.ts +4 -1
- package/src/mon/component.ts +486 -0
- package/src/mon/index.ts +105 -0
- package/src/mon/model.ts +153 -0
- package/src/mon/poll.ts +80 -0
- package/src/mon/source.ts +121 -0
- package/src/monitor/index.ts +16 -3
- package/src/monitor/manager.ts +5 -0
- package/src/monitor/types.ts +2 -0
- package/src/monitor/widget.ts +209 -26
- package/src/stats/component.ts +23 -5
- package/src/stats/freshness.ts +46 -0
- package/src/stats/index.ts +5 -2
- package/src/stats/snapshot.ts +24 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## 0.8.0
|
|
6
|
+
|
|
7
|
+
- Add `/mon`: a read-only TUI dashboard of host CPU history, non-free memory, load and sampled processes, on by default (`mon.enabled`)
|
|
8
|
+
- Depend on the exact published `@nredd/pi-resource-sampler@0.1.1` (executable names, never argv or process titles; Bun host CPU measured), locked as a nested registry install so tests never borrow the workspace copy; one sampler per open view, disposed (children reaped, no timers) on close, session shutdown or replacement
|
|
9
|
+
- Map warming/unavailable metrics to `--`, never zero; split warnings (budget-limited lists, unavailable host CPU) from muted platform hints (Linux kernel-thread skips, unknown names, macOS process CPU)
|
|
10
|
+
- Render unreadable names as `(unknown)` and mark macOS names that may be cut at 16 bytes with `…`
|
|
11
|
+
- Header shows collector state with the last successful collection's local time; every resolved sample counts as live, separate from host CPU `warming`/`unavailable`
|
|
12
|
+
- Side-by-side cards from 72 columns with a two-row CPU history at the observed monotonic interval; process table `PID CPU% RSS MEM% NAME` with a selection marker and overflow scrollbar
|
|
13
|
+
- Pack `docs/mon-dashboard.md`, which the README links
|
|
14
|
+
|
|
15
|
+
## 0.7.0
|
|
16
|
+
|
|
17
|
+
- Add a `/stats` **Machines** tab (Tab/Shift-Tab): each known machine's label and opaque UUID, local scan status, and remote producer `exportedAt` UTC instant plus age at opening
|
|
18
|
+
- Label the header clock `viewer last import` (or `viewer never imported`); it is not producer freshness, so an old export stays old after a fresh import
|
|
19
|
+
- Flag future export clocks as `future; clock skew`, absent/invalid timestamps as `unknown`, and missing/rejected sources with `Incomplete import; retained last-good snapshot`; print/JSON output carries the same provenance
|
|
20
|
+
- Keep snapshot schema v1 and the exporter CLI contract unchanged; existing snapshots need no migration or re-export
|
|
21
|
+
- Redesign the monitors widget like the Todos and subagents panels: disclosure heading with running/failed/done counts, one bulleted row per monitor (running, matched, failed, exited) with flush-right `elapsed · N lines`, and a constant-height window with `↑/↓ N more`
|
|
22
|
+
- Collapse with a heading click or `monitor.collapseKey` (default `ctrl+shift+m`, `"off"` disables, validated with the rpiv-todo key grammar); cap rows with `monitor.maxWidgetLines` (default 12, minimum 4)
|
|
23
|
+
- Keep finished monitors visible beside running ones while any monitor runs; the widget still never writes to the transcript or model context
|
|
24
|
+
- Add `MonitorInfo.matches` and `KitConfig.monitor`; hosts passing a config without `monitor` get the defaults
|
|
25
|
+
|
|
26
|
+
## 0.6.0
|
|
27
|
+
|
|
28
|
+
- Keep passive monitor output only in the monitor ring buffer; `monitor_read` is the explicit output surface and match/exit/timeout wakes are compact summaries with a cursor
|
package/README.md
CHANGED
|
@@ -3,20 +3,25 @@
|
|
|
3
3
|
Published to npm as `@nredd/pi-session-kit` (public). From `0.4.0`, each version is cut by the monorepo `Publish` workflow from a signed `@nredd/pi-session-kit@<version>` tag, with npm provenance and a GitHub release carrying the tarball, checksums and manifest (`docs/releases.md`); earlier versions were published manually without provenance. `0.4.0` adds `createSessionKit(options)` (explicit `config`, `agentDir`, `monitorRoot`, injectable `clock` and `spawnMonitor`; the default export is unchanged) and fixes leaderless orphan-group sweeps at `ps` start-time resolution. `0.5.0` redesigns `/stats` around the full pane (pinned header, aligned stat cells, a Models table on wide panes) and gives every chart a clock- or calendar-aligned time axis, including the 5h and 24h hourly views.
|
|
4
4
|
Use the [monorepo development commands](https://github.com/nredd/pi-harness/blob/main/README.md). The Git install and standalone harness
|
|
5
5
|
references below describe the legacy deployment, not installation or candidate verification of
|
|
6
|
-
this package. `0.6.0` keeps passive monitor output only in the monitor ring buffer rather than adding it to the transcript or model context; `monitor_read` is the explicit output surface, and match/exit/timeout wakes are compact summaries with a cursor.
|
|
6
|
+
this package. `0.6.0` keeps passive monitor output only in the monitor ring buffer rather than adding it to the transcript or model context; `monitor_read` is the explicit output surface, and match/exit/timeout wakes are compact summaries with a cursor. `0.7.0` adds the `/stats` Machines tab with per-machine producer export age, separate from the viewer's last import, and the collapsible monitors widget. `0.8.0` adds `/mon`.
|
|
7
7
|
|
|
8
8
|
## Standalone use
|
|
9
9
|
|
|
10
|
-
The packed tarball is self-contained: Pi host modules (`@earendil-works/pi-*`, `typebox`) are `*` peers supplied by Pi,
|
|
10
|
+
The packed tarball is self-contained: Pi host modules (`@earendil-works/pi-*`, `typebox`) are `*` peers supplied by Pi, and no sibling plugin is required. Its only runtime dependency is the exact published `@nredd/pi-resource-sampler@0.1.1` library (no Pi imports), which npm installs from the registry for `/mon`. Verified by installing the tarball alone and running it in exact stock Pi 1.3.0 (see the repository's `docs/package-contracts.md`). Optional fork integrations (subagents, Plan mode) are absent in that setup; the exercised behavior does not need them.
|
|
11
11
|
|
|
12
12
|
Session-scoped pi extensions for tmux users. No daemon, supervisor, core patch or cross-pane
|
|
13
13
|
registry. Each runtime owns its goal, loops and monitors; persisted goal state follows the active
|
|
14
14
|
session branch. Closing pi stops its work.
|
|
15
15
|
|
|
16
|
+
`/mon` opens a read-only, full-pane view of local CPU, memory, load and processes, sampled
|
|
17
|
+
every ~2s by the shared `@nredd/pi-resource-sampler` library only while it is open. Details and
|
|
18
|
+
limits (macOS process CPU is unavailable, macOS names keep 16 bytes, memory is non-free, partial coverage) are in
|
|
19
|
+
[docs/mon-dashboard.md](docs/mon-dashboard.md). It is not a second `/monitor` process runner.
|
|
20
|
+
|
|
16
21
|
## Install and config
|
|
17
22
|
|
|
18
23
|
```sh
|
|
19
|
-
pi install npm:@nredd/pi-session-kit@0.
|
|
24
|
+
pi install npm:@nredd/pi-session-kit@0.8.0
|
|
20
25
|
```
|
|
21
26
|
|
|
22
27
|
Optional `~/.pi/agent/pi-session-kit.json` (respects `PI_CODING_AGENT_DIR`):
|
|
@@ -27,9 +32,10 @@ Optional `~/.pi/agent/pi-session-kit.json` (respects `PI_CODING_AGENT_DIR`):
|
|
|
27
32
|
"rules": { "enabled": true },
|
|
28
33
|
"goal": { "enabled": true },
|
|
29
34
|
"loop": { "enabled": true },
|
|
30
|
-
"monitor": { "enabled": true },
|
|
35
|
+
"monitor": { "enabled": true, "collapseKey": "ctrl+shift+m", "maxWidgetLines": 12 },
|
|
31
36
|
"diff": { "enabled": true },
|
|
32
|
-
"stats": { "enabled": true }
|
|
37
|
+
"stats": { "enabled": true },
|
|
38
|
+
"mon": { "enabled": true }
|
|
33
39
|
}
|
|
34
40
|
```
|
|
35
41
|
|
|
@@ -79,6 +85,23 @@ persistent cron is installed. Deferred-tool configurations need `goal_update` in
|
|
|
79
85
|
possibly reused group, so its record is kept, not signalled. Within that window a later-born member
|
|
80
86
|
in a reused group is indistinguishable; the dead leader, matching group and session, and no prior
|
|
81
87
|
members bound that case, they do not exclude it.
|
|
88
|
+
- Widget: while any monitor runs, a Todos/subagents-style panel sits above the editor. Heading
|
|
89
|
+
`▾ ● Monitors (2 running · 1 failed · 1 done)`; one row per retained monitor (running monitors
|
|
90
|
+
plus up to ten finished ones) with a bullet by state (green running, yellow matched, red
|
|
91
|
+
failed = non-zero exit, signal or timeout, dim exited/stopped), the name and last line truncated,
|
|
92
|
+
and `elapsed · N lines` flush right. It is display only: the quiet rules above are unchanged, and
|
|
93
|
+
a repaint never adds anything to the transcript or model context. Clicking the heading or
|
|
94
|
+
pressing `monitor.collapseKey` collapses it to the heading plus a `ctrl+shift+m to expand` hint
|
|
95
|
+
(`▸`). `monitor.maxWidgetLines` caps content rows (heading included, default 12, integer >= 4,
|
|
96
|
+
else the default), excluding the one trailing spacer row. It is a constant-height window:
|
|
97
|
+
`↑ N more` / `↓ N more` take a row each, so it never resizes while you scroll with the mouse
|
|
98
|
+
wheel. Pi's tool-output expansion shows every row.
|
|
99
|
+
`collapseKey` takes the same grammar as rpiv-todo's `collapseKey` (trimmed, lowercased; distinct
|
|
100
|
+
`ctrl`/`shift`/`alt`/`super` modifiers plus one printable key or a named key; `"off"` removes the
|
|
101
|
+
binding; anything else falls back to the default). It is bound once per runtime, so a change needs
|
|
102
|
+
`/reload`. The default `ctrl+shift+m` is unused by pi core, Todos (`ctrl+shift+t`), subagents and
|
|
103
|
+
Plan mode (no default key). Like Todos' key it needs a terminal with the kitty keyboard protocol.
|
|
104
|
+
There is no `collapseOnResume`: monitors never survive a resume, so there is nothing to collapse.
|
|
82
105
|
- Plan mode admits only `monitor_read`; start and stop are mutating tools. Ordinary tool hooks apply
|
|
83
106
|
to the tool call, but a custom argv tool does not automatically inherit Bash-specific policies.
|
|
84
107
|
|
|
@@ -93,9 +116,13 @@ non-triggering display message. The overlay is TUI-only and works in Plan mode.
|
|
|
93
116
|
## Stats
|
|
94
117
|
|
|
95
118
|
`/stats` is Claude Code's `/stats` pane over fresh local sessions plus imported machine snapshots,
|
|
96
|
-
split by provider and model. It NEVER performs network I/O.
|
|
97
|
-
|
|
98
|
-
|
|
119
|
+
split by provider and model. It NEVER performs network I/O. The header shows `viewer last import`,
|
|
120
|
+
not producer freshness; without imports it says `viewer never imported`. Tab/Shift-Tab reaches
|
|
121
|
+
**Machines**, with each known label/opaque UUID, local scan status, and remote exported-at UTC
|
|
122
|
+
instant plus age at opening. Old exports stay old after a fresh import; future clocks are flagged.
|
|
123
|
+
Unknown/rejected sources and retained last-good snapshots are explicit. This lists known sources,
|
|
124
|
+
not undiscovered machines. Invalid or missing snapshots show `incomplete coverage`.
|
|
125
|
+
Print/JSON output includes the same provenance. Details: [here](docs/stats-snapshots.md).
|
|
99
126
|
|
|
100
127
|
Pinned header: tabs and the coverage note, then the date ranges and the model filter, then a rule.
|
|
101
128
|
Overview: an activity heatmap (one column per week) with the headline numbers (favorite model,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# /mon resource dashboard
|
|
2
|
+
|
|
3
|
+
`/mon` is a read-only, interactive-TUI view of local host resources. It is on by default and
|
|
4
|
+
disabled with `{ "mon": { "enabled": false } }` in `pi-session-kit.json`. In RPC/print/JSON modes
|
|
5
|
+
it only warns; it never samples there.
|
|
6
|
+
|
|
7
|
+
## Source
|
|
8
|
+
|
|
9
|
+
`src/mon/source.ts` is a thin mapping over the published `@nredd/pi-resource-sampler`, declared
|
|
10
|
+
as the exact runtime dependency `0.1.1` (`0.1.0` leaked argv-style process titles as macOS names
|
|
11
|
+
and could fake Bun host CPU). No collector lives in session-kit. Each open view creates
|
|
12
|
+
one sampler (its own CPU baseline) and disposes it on close; disposal aborts and awaits any active
|
|
13
|
+
sample, including its `ps` child on macOS. Mapping rules:
|
|
14
|
+
|
|
15
|
+
- Collector success and host CPU state are separate. Every resolved sample is `LIVE` and refreshes
|
|
16
|
+
the last-success time, even while host CPU is `warming` or `unavailable`, because its memory,
|
|
17
|
+
load and process data are fresh. The CPU readout then says `-- warming` or `-- unavailable`.
|
|
18
|
+
The header pairs the state (`● LIVE`, `○ WAITING`, `▲ STALE`) with the local wall time of the
|
|
19
|
+
last successful collection (`updated HH:MM:SS`, `last ok` when stale), so freshness is never
|
|
20
|
+
inferred from the CPU state.
|
|
21
|
+
- `warming` and `unavailable` metrics become `--`, never zero. The first observation is a warming
|
|
22
|
+
baseline; persistent warming shows the sampler's reason (e.g. `Host CPU warming: CPU counters
|
|
23
|
+
have not advanced`).
|
|
24
|
+
- Two diagnostic tones. A warning (`▲`, warning color) means a limit hides data the user would
|
|
25
|
+
expect: `Partial list: S of E seen (reasons)` for budget caps or collector failures (at most three
|
|
26
|
+
reasons, in words: `time budget`, `process cap`, `read budget`, `ps failed`, ...), unavailable host
|
|
27
|
+
CPU, persistent warming. A muted hint states an expected platform limit: Linux always skips kernel
|
|
28
|
+
threads (no RSS) and processes that exit mid-scan, so a scan whose only reason is
|
|
29
|
+
`process-unreadable-or-changed` is `N skipped: kernel threads, exited or unreadable`, not a
|
|
30
|
+
warning; `N names unknown (no access, e.g. other users)`; macOS process CPU. The process header
|
|
31
|
+
counts *sampled* rows, not host processes.
|
|
32
|
+
- Names are the sampler's executable basenames, never argv or process titles. `""` renders as a dim
|
|
33
|
+
`(unknown)` (Linux hides other users' `/proc/PID/exe` without ptrace access). macOS `p_comm` keeps
|
|
34
|
+
16 bytes, so a macOS name of 16+ UTF-8 bytes ends in a dim `…` (`Google Chrome He…`): possibly cut,
|
|
35
|
+
never claimed complete. Names stay untrusted text: sanitized, never interpreted.
|
|
36
|
+
- macOS process interval CPU is always unavailable (the sampler's `ps` start time has one-second
|
|
37
|
+
precision, so PID reuse cannot be excluded). The view says so, explains it in help, and orders by
|
|
38
|
+
RSS until the user picks a sort. Linux first samples are warming, so CPU order stays stable.
|
|
39
|
+
- History spacing uses the sampler's monotonic `monotonicTimeMs`, so wall-clock steps (NTP, manual
|
|
40
|
+
changes) cannot invent or hide gaps; the wall `wallTimeMs` is only displayed (header clock and
|
|
41
|
+
last-success UTC).
|
|
42
|
+
Node's monotonic clock pauses during system sleep on macOS and Linux, so a suspend shows no gap.
|
|
43
|
+
`scan Nms` (80+ columns) is the sampler's collection duration. Missing/zero memory totals and non-finite values become unknown.
|
|
44
|
+
|
|
45
|
+
## Presentation
|
|
46
|
+
|
|
47
|
+
- From 72 columns and 17 rows (one more per diagnostic line), CPU and memory/load cards sit side
|
|
48
|
+
by side; CPU history is two rows (16 levels) there. Narrower or shorter terminals get a
|
|
49
|
+
three-line summary (`CPU`, `RAM`, 1-minute load, one-row history); at six rows summary/chrome
|
|
50
|
+
yields to the selected process, but a compact partial-list or stopped/retry warning remains visible.
|
|
51
|
+
At eight rows, core CPU/RAM/load metrics take priority over optional history and platform hints.
|
|
52
|
+
Compact metrics tighten spacing when needed to keep CPU, RAM and the 1-minute load visible.
|
|
53
|
+
The last row reserves close/help. Below 60 columns the header
|
|
54
|
+
keeps the update clock over the tagline.
|
|
55
|
+
- Columns: `PID`, `CPU%` (100 = one core), `RSS`, `MEM%` (RSS over total RAM, 60+ columns),
|
|
56
|
+
`NAME` last so long names never push numbers off screen. `--` is dimmed. A `▸` marks the
|
|
57
|
+
selection; a theme scrollbar appears only when the list overflows. The memory card adds 1-minute
|
|
58
|
+
load per logical CPU.
|
|
59
|
+
- Host CPU severity colors the value (success under 50%, warning under 85%, error above) next to
|
|
60
|
+
the number itself. Stale CPU retains the last measured value, muted rather than live severity-colored.
|
|
61
|
+
Cards add a `last value` label; compact summaries rely on the `STALE` header instead, reserving
|
|
62
|
+
the metric row for CPU, RAM and load.
|
|
63
|
+
- Host CPU history keeps each capture timestamp, uses a fixed 0..100% scale and at most 120
|
|
64
|
+
samples, newest on the right. A column is the lower-median interval between captures, widened
|
|
65
|
+
only when the history is longer than the width, so completion-paced drift never invents gaps.
|
|
66
|
+
Columns with no CPU value after the first one are gaps (`·`), never zero or interpolation;
|
|
67
|
+
columns before it are blank, including the warming baseline. Stale snapshots add no point. The caption states the actual span. The latest sample in a bucket wins.
|
|
68
|
+
Stale views show the last successful capture's UTC timestamp (or `none`). Waiting/live/stale and
|
|
69
|
+
CPU warming/unavailable use semantic theme colors and text, so meaning does not depend on color.
|
|
70
|
+
- Memory is total minus free, explicitly **non-free**, not available memory or a pressure score
|
|
71
|
+
(on macOS free pages are small by design). Load averages are not percentages. Process CPU is
|
|
72
|
+
interval CPU, where 100% is one logical CPU and multithreaded processes can exceed 100%.
|
|
73
|
+
- The process viewport preserves selection by sampler identity, not PID or sorted row.
|
|
74
|
+
Exited identities choose a bounded neighboring row; PID reuse cannot inherit selection.
|
|
75
|
+
Unknown/non-finite measurements sort last with deterministic PID/identity tie breaks.
|
|
76
|
+
- `j/k` and arrows select; PageUp/PageDown page; `g/G` jump first/last; `c/m/p` sort
|
|
77
|
+
CPU/RSS/PID; `s` cycles sort; `?` toggles help; `q`/Escape close. CPU/RSS descend,
|
|
78
|
+
PID ascends. Help has a bounded, wrapped viewport with a visible range and scroll hint;
|
|
79
|
+
`j/k`, arrows, PageUp/PageDown and `g/G` scroll help without moving the process selection.
|
|
80
|
+
No kill, locks, workload control, process arguments or environment display. Process names
|
|
81
|
+
are the sampler's sanitized display names and are never interpreted.
|
|
82
|
+
|
|
83
|
+
## Ownership
|
|
84
|
+
|
|
85
|
+
`MonView` accepts presentation snapshots and never collects data. It caches rendered lines
|
|
86
|
+
by dimensions until update/input/theme invalidation. `MonPoll` owns one completion-paced probe
|
|
87
|
+
(~2s after the previous one completed), with no catch-up queue. Closing aborts collection,
|
|
88
|
+
cancels the timeout and awaits the sampler's disposal. Errors stop collection and retain
|
|
89
|
+
explicitly stale values; reopening is the retry mechanism. Late results are discarded.
|
|
90
|
+
|
|
91
|
+
`registerMon` opens a source only inside a TUI interaction and serializes replacement through
|
|
92
|
+
completed asynchronous disposal, including concurrent opens. Session shutdown/start invalidates
|
|
93
|
+
pending openers before they can create a source, even when Pi defers the UI factory. It registers
|
|
94
|
+
no model tool, persists no history, sends no model messages and does no network work.
|
|
95
|
+
|
|
96
|
+
## Verification
|
|
97
|
+
|
|
98
|
+
- `npm run check` in `packages/session-kit`. The root lock pins a nested registry install
|
|
99
|
+
(`packages/session-kit/node_modules/@nredd/pi-resource-sampler`, exact `0.1.1` tarball and
|
|
100
|
+
integrity), so tests run the published bytes, not the same-version workspace that npm would
|
|
101
|
+
otherwise link. `source.test.ts` fails if resolution ever reaches the workspace.
|
|
102
|
+
- `test/mon/source.test.ts`: two actual host observations (warming, then measured), a real poll's
|
|
103
|
+
two updates, and mapping edge cases. In-flight close wraps the real sampler to hold its pending
|
|
104
|
+
sample, then requires that sample to reject with `AbortError`, no update, no child and no timer.
|
|
105
|
+
On macOS it first requires the sampler's live `ps` child; Linux reads `/proc` in process, so
|
|
106
|
+
there the `AbortError` is the only provider evidence and no child is claimed.
|
|
107
|
+
- `test/mon/theme.test.ts`: actual built-in dark and light themes over an actual observation at
|
|
108
|
+
40x12, 60x20, 80x24 and 120x40.
|
|
109
|
+
- `test/scenarios/mon.test.ts` (Vitest and Bun): the real interactive Pi host, `createSessionKit`
|
|
110
|
+
and the real sampler; two observations, resize matrix, close/Escape/reopen, close before the
|
|
111
|
+
first sample and session shutdown all leave no kit timer or child process.
|
|
112
|
+
- `test/mon/{component,poll,index}.test.ts`: layout, identity, history, lifecycle interleavings
|
|
113
|
+
with fakes; `resource-fixture.ts` is type-checked against the package's `ResourceSnapshot`.
|
|
114
|
+
- `make check-package-contracts` packs session-kit, installs it alone (sampler from npm, exact
|
|
115
|
+
lockfile closure) and drives it through stock Pi.
|
|
116
|
+
- Real macOS terminal recordings exercise the packed, registry-backed view, resize, navigation,
|
|
117
|
+
close and reopen. Empty direct-child lists before and after close show only that no child was
|
|
118
|
+
observed at those instants; they do NOT prove an in-flight helper was cancelled or reaped.
|
|
119
|
+
In-flight cancellation/reaping evidence comes from the deterministic real-sampler tests above,
|
|
120
|
+
not those terminal snapshots. No Linux terminal recording is claimed.
|
|
121
|
+
- Manual fixture preview (no sampling): `bun test/mon/terminal.ts dark|light`. It is labelled
|
|
122
|
+
`FIXTURE DATA` and uses `fixture-*` names, so a capture can never pass for host data; it is not
|
|
123
|
+
evidence of real measurements.
|
package/docs/stats-snapshots.md
CHANGED
|
@@ -101,8 +101,25 @@ Producer updates are invisible until the producer exports AND the viewer imports
|
|
|
101
101
|
known machines, not undiscovered installations; it is not proof that every machine has synced.
|
|
102
102
|
|
|
103
103
|
`/stats` freshly scans local sessions and combines them with this cache without network access.
|
|
104
|
-
Its
|
|
105
|
-
coverage.
|
|
104
|
+
Its header labels cached coverage, **viewer last import**, local-only/never-imported state and
|
|
105
|
+
incomplete coverage. Tab/Shift-Tab cycles Overview, Models and **Machines**. Machines lists each
|
|
106
|
+
known installation's label plus full opaque UUID (default labels may repeat), fresh local scan
|
|
107
|
+
status, and each remote producer's **exported-at** UTC instant and age at opening. Use j/k or
|
|
108
|
+
arrows to scroll; narrow panes wrap details. Print/JSON output includes the same provenance;
|
|
109
|
+
empty histories include it in the notification. Range/model filters do not filter provenance.
|
|
110
|
+
|
|
111
|
+
The viewer import clock is NOT producer freshness: importing a ten-day-old export today still
|
|
112
|
+
shows a ten-day producer age. Future export clocks say `future; clock skew`, never fresh.
|
|
113
|
+
Absent/invalid display timestamps say `unknown`. Invalid v1 manifests still fail validation and
|
|
114
|
+
retain last-good data; rejected known UUIDs without last-good data show unknown export time.
|
|
115
|
+
Missing/rejected sources with retained data say `Incomplete import; retained last-good snapshot`.
|
|
116
|
+
Existing v1 snapshots require no migration or re-export. Unreadable caches report incomplete
|
|
117
|
+
coverage, not invented machine identities. Only known sources are listed; this cannot discover
|
|
118
|
+
machines that have never supplied a snapshot. No freshness threshold implies fleet completeness.
|
|
119
|
+
|
|
120
|
+
Labels come only from the existing local `machine.json`, never automatic hostname/environment
|
|
121
|
+
collection. Keep custom labels non-sensitive: they are exported. No schema or sync cadence changes.
|
|
122
|
+
Future calls are ignored in display totals, not removed from durable history. Statistics
|
|
106
123
|
are not subscription quota measurements or router inputs.
|
|
107
124
|
|
|
108
125
|
Harness API:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nredd/pi-session-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"src",
|
|
13
13
|
"dist/pi-stats-export.mjs",
|
|
14
14
|
"docs/stats-snapshots.md",
|
|
15
|
+
"docs/mon-dashboard.md",
|
|
16
|
+
"CHANGELOG.md",
|
|
15
17
|
"LICENSE",
|
|
16
18
|
"README.md"
|
|
17
19
|
],
|
|
@@ -24,6 +26,9 @@
|
|
|
24
26
|
"pi-package",
|
|
25
27
|
"pi-extension"
|
|
26
28
|
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@nredd/pi-resource-sampler": "0.1.1"
|
|
31
|
+
},
|
|
27
32
|
"peerDependencies": {
|
|
28
33
|
"@earendil-works/pi-coding-agent": "*",
|
|
29
34
|
"@earendil-works/pi-tui": "*",
|
|
@@ -40,8 +45,8 @@
|
|
|
40
45
|
},
|
|
41
46
|
"devDependencies": {
|
|
42
47
|
"@biomejs/biome": "2.5.15",
|
|
43
|
-
"@earendil-works/pi-coding-agent": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.
|
|
44
|
-
"@earendil-works/pi-tui": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.
|
|
48
|
+
"@earendil-works/pi-coding-agent": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.7/earendil-works-pi-coding-agent-1.3.0.tgz",
|
|
49
|
+
"@earendil-works/pi-tui": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.7/earendil-works-pi-tui-1.3.0.tgz",
|
|
45
50
|
"@types/node": "26.6.5",
|
|
46
51
|
"@xterm/headless": "5.5.0",
|
|
47
52
|
"esbuild": "0.28.2",
|
package/src/config.ts
CHANGED
|
@@ -9,13 +9,73 @@ import { readFileSync } from "node:fs";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
|
-
export const FEATURES = ["titles", "rules", "goal", "loop", "monitor", "diff", "stats"] as const;
|
|
12
|
+
export const FEATURES = ["titles", "rules", "goal", "loop", "monitor", "diff", "stats", "mon"] as const;
|
|
13
13
|
export type Feature = (typeof FEATURES)[number];
|
|
14
14
|
|
|
15
15
|
export interface KitConfig {
|
|
16
16
|
features: Record<Feature, boolean>;
|
|
17
17
|
/** Overrides the auto-title model, as `provider/id`. */
|
|
18
18
|
titleModel?: string;
|
|
19
|
+
monitor: MonitorWidgetConfig;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `monitor.*` widget settings, mirroring rpiv-todo's `collapseKey` / `maxWidgetLines`. */
|
|
23
|
+
export interface MonitorWidgetConfig {
|
|
24
|
+
/** Key spec that collapses/expands the widget, or `"off"` for no binding. */
|
|
25
|
+
collapseKey: string;
|
|
26
|
+
/** Content-row budget including the heading, excluding the trailing spacer; floor `MIN_WIDGET_LINES`. */
|
|
27
|
+
maxWidgetLines: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_COLLAPSE_KEY = "ctrl+shift+m";
|
|
31
|
+
export const COLLAPSE_KEY_OFF = "off";
|
|
32
|
+
export const DEFAULT_MAX_WIDGET_LINES = 12;
|
|
33
|
+
/** Heading + two edge hints + one row: below this a scrolled window shows nothing. */
|
|
34
|
+
export const MIN_WIDGET_LINES = 4;
|
|
35
|
+
|
|
36
|
+
// Named keys accepted by pi-tui's `matchesKey`; lowercase spellings are canonical.
|
|
37
|
+
const SPECIAL_KEYS = new Set([
|
|
38
|
+
"escape",
|
|
39
|
+
"esc",
|
|
40
|
+
"enter",
|
|
41
|
+
"return",
|
|
42
|
+
"tab",
|
|
43
|
+
"space",
|
|
44
|
+
"backspace",
|
|
45
|
+
"delete",
|
|
46
|
+
"insert",
|
|
47
|
+
"clear",
|
|
48
|
+
"home",
|
|
49
|
+
"end",
|
|
50
|
+
"pageup",
|
|
51
|
+
"pagedown",
|
|
52
|
+
"up",
|
|
53
|
+
"down",
|
|
54
|
+
"left",
|
|
55
|
+
"right",
|
|
56
|
+
...Array.from({ length: 12 }, (_, i) => `f${i + 1}`),
|
|
57
|
+
]);
|
|
58
|
+
const MODIFIERS = new Set(["ctrl", "shift", "alt", "super"]);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Strict pi-tui KeyId grammar: distinct modifiers, then one printable char or a named key.
|
|
62
|
+
* Loose checks are not enough: pi-tui takes the LAST `+` part as the key and ignores unknown
|
|
63
|
+
* parts, so a typo like `ctr+]` would silently bind every bare `]`.
|
|
64
|
+
*/
|
|
65
|
+
export function isValidKeySpec(spec: string): boolean {
|
|
66
|
+
if (!spec || spec.startsWith("+") || spec.endsWith("+") || spec.includes("++")) return false;
|
|
67
|
+
const parts = spec.split("+");
|
|
68
|
+
const base = parts[parts.length - 1] ?? "";
|
|
69
|
+
const modifiers = parts.slice(0, -1);
|
|
70
|
+
if (modifiers.length !== new Set(modifiers).size || !modifiers.every((m) => MODIFIERS.has(m))) return false;
|
|
71
|
+
return base.length === 1 ? /[a-z0-9_\-!@#$%^&*()|~`'":;,./<>?[\]{}=\\]/.test(base) : SPECIAL_KEYS.has(base);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Trim and lowercase; `"off"` disables, anything missing or invalid falls back to the default. */
|
|
75
|
+
export function resolveCollapseKey(raw: unknown): string {
|
|
76
|
+
const key = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
|
77
|
+
if (key === COLLAPSE_KEY_OFF) return COLLAPSE_KEY_OFF;
|
|
78
|
+
return isValidKeySpec(key) ? key : DEFAULT_COLLAPSE_KEY;
|
|
19
79
|
}
|
|
20
80
|
|
|
21
81
|
export function configPath(agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent")): string {
|
|
@@ -23,7 +83,10 @@ export function configPath(agentDir = process.env.PI_CODING_AGENT_DIR ?? join(ho
|
|
|
23
83
|
}
|
|
24
84
|
|
|
25
85
|
export function defaultConfig(): KitConfig {
|
|
26
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
features: Object.fromEntries(FEATURES.map((f) => [f, true])) as Record<Feature, boolean>,
|
|
88
|
+
monitor: { collapseKey: DEFAULT_COLLAPSE_KEY, maxWidgetLines: DEFAULT_MAX_WIDGET_LINES },
|
|
89
|
+
};
|
|
27
90
|
}
|
|
28
91
|
|
|
29
92
|
/** Parse a config document, ignoring unknown keys and wrong-typed values. */
|
|
@@ -38,6 +101,14 @@ export function parseConfig(raw: unknown): KitConfig {
|
|
|
38
101
|
if (typeof enabled === "boolean") config.features[feature] = enabled;
|
|
39
102
|
}
|
|
40
103
|
}
|
|
104
|
+
const monitor = doc.monitor;
|
|
105
|
+
if (typeof monitor === "object" && monitor !== null) {
|
|
106
|
+
const section = monitor as Record<string, unknown>;
|
|
107
|
+
config.monitor.collapseKey = resolveCollapseKey(section.collapseKey);
|
|
108
|
+
const lines = section.maxWidgetLines;
|
|
109
|
+
if (typeof lines === "number" && Number.isInteger(lines) && lines >= MIN_WIDGET_LINES)
|
|
110
|
+
config.monitor.maxWidgetLines = lines;
|
|
111
|
+
}
|
|
41
112
|
const titles = doc.titles;
|
|
42
113
|
if (typeof titles === "object" && titles !== null) {
|
|
43
114
|
const model = (titles as Record<string, unknown>).model;
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { configPath, type Feature, type KitConfig, loadConfig } from "./config.t
|
|
|
9
9
|
import { register as registerDiff } from "./diff/index.ts";
|
|
10
10
|
import { register as registerGoal } from "./goal/index.ts";
|
|
11
11
|
import { register as registerLoop } from "./loop/index.ts";
|
|
12
|
+
import { registerMon } from "./mon/index.ts";
|
|
13
|
+
import { createSamplerSource } from "./mon/source.ts";
|
|
12
14
|
import { register as registerMonitor } from "./monitor/index.ts";
|
|
13
15
|
import type { MonitorProcess, SpawnOptions } from "./monitor/process.ts";
|
|
14
16
|
import { register as registerRules } from "./rules/index.ts";
|
|
@@ -20,7 +22,7 @@ export interface SessionKitOptions {
|
|
|
20
22
|
config?: KitConfig;
|
|
21
23
|
agentDir?: string;
|
|
22
24
|
monitorRoot?: string;
|
|
23
|
-
/** Time for stats ranges, loops, goal wakes
|
|
25
|
+
/** Time for stats ranges, loops, goal wakes, monitor batching/timeouts and /mon cadence. Defaults to real time. */
|
|
24
26
|
clock?: Clock;
|
|
25
27
|
/** Child boundary only; validation, matching, ring buffering and message delivery stay in the manager. */
|
|
26
28
|
spawnMonitor?: (argv: readonly string[], options: SpawnOptions) => Promise<MonitorProcess>;
|
|
@@ -37,6 +39,7 @@ const MODULES: Record<Feature, Register> = {
|
|
|
37
39
|
registerMonitor(pi, config, options.monitorRoot, { clock: options.clock, spawnMonitor: options.spawnMonitor }),
|
|
38
40
|
diff: registerDiff,
|
|
39
41
|
stats: (pi, config, options) => registerStats(pi, config, options),
|
|
42
|
+
mon: (pi, _config, options) => registerMon(pi, { createSource: () => createSamplerSource(), clock: options.clock }),
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
/** Create the real kit without requiring a machine-local agent configuration. */
|