@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 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, there are no runtime dependencies and no sibling plugin is required. 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.
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.6.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. Machine coverage and the last successful
97
- remote import are shown; unsynced machines show `local-only` / `never synced`. Invalid or missing
98
- snapshots retain last-good data and show `incomplete coverage`.
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.
@@ -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 UI labels cached coverage, successful import time, local-only/never-synced state and incomplete
105
- coverage. Future calls are ignored in display totals, not removed from durable history. Statistics
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.6.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.6/earendil-works-pi-coding-agent-1.3.0.tgz",
44
- "@earendil-works/pi-tui": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.6/earendil-works-pi-tui-1.3.0.tgz",
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 { features: Object.fromEntries(FEATURES.map((f) => [f, true])) as Record<Feature, boolean> };
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 and monitor batching/timeouts. Defaults to real time. */
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. */