pi-minimal 0.5.0 → 0.6.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/dist/rex.js ADDED
@@ -0,0 +1,289 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ import { createListeners } from "./listeners.js";
4
+ export function createRexState() {
5
+ return { layout: { tabs: new Map(), blocks: new Map() }, status: new Map() };
6
+ }
7
+ const TERMINAL_CREATOR = "com.superlogical.terminal";
8
+ /** Parses `rex window ls -s SESSION --json`. */
9
+ export function parseWindowList(stdout) {
10
+ const data = JSON.parse(stdout);
11
+ if (!Array.isArray(data.windows))
12
+ throw new Error("invalid rex window list");
13
+ const layout = { tabs: new Map(), blocks: new Map() };
14
+ for (const window of data.windows) {
15
+ const windowId = typeof window.window_id === "string" ? window.window_id : undefined;
16
+ if (!windowId)
17
+ continue;
18
+ layout.tabs.set(windowId, typeof window.label === "string" ? window.label : "");
19
+ for (const layer of window.layers ?? []) {
20
+ for (const block of layer.blocks ?? []) {
21
+ if (typeof block.block_id !== "string")
22
+ continue;
23
+ if (block.creator_name !== undefined && block.creator_name !== TERMINAL_CREATOR)
24
+ continue;
25
+ layout.blocks.set(block.block_id, windowId);
26
+ }
27
+ }
28
+ }
29
+ return layout;
30
+ }
31
+ /** Parses `rex block call -b BLOCK program_status`; returns the root record, if any. */
32
+ export function parseProgramStatus(stdout) {
33
+ const data = JSON.parse(stdout);
34
+ if (!Array.isArray(data.records))
35
+ throw new Error("invalid rex program status");
36
+ const root = data.records.find((record) => record.id === undefined || record.id === "");
37
+ return root ? toRecord(root) : undefined;
38
+ }
39
+ function toRecord(raw) {
40
+ if (typeof raw.state !== "string")
41
+ return undefined;
42
+ const record = { state: raw.state };
43
+ const app = typeof raw.effective_app === "string" ? raw.effective_app : typeof raw.app === "string" ? raw.app : undefined;
44
+ if (app)
45
+ record.app = app;
46
+ if (typeof raw.msg === "string")
47
+ record.msg = raw.msg;
48
+ return record;
49
+ }
50
+ /**
51
+ * Applies one line of `rex events --json` to the state. Returns what changed: `status` (a pane's
52
+ * record) or `layout` (the caller should re-read the window list). Unknown events are ignored.
53
+ */
54
+ export function applyRexEvent(state, line) {
55
+ // Shape: {"type":"session_event","payload":{"session_id":…,"event":{"type":…,"payload":{…}}}}
56
+ let outer;
57
+ try {
58
+ outer = JSON.parse(line);
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ if (outer.type !== "session_event")
64
+ return undefined;
65
+ const event = outer.payload?.event;
66
+ const payload = event?.payload ?? {};
67
+ switch (event?.type) {
68
+ case "block_event": {
69
+ const blockId = payload.block_id;
70
+ if (typeof blockId !== "string")
71
+ return undefined;
72
+ const inner = (payload.payload ?? {});
73
+ if (payload.name === "program_status_changed") {
74
+ if (inner.id !== "" && inner.id !== undefined)
75
+ return undefined;
76
+ const record = toRecord((inner.record ?? {}));
77
+ if (!record)
78
+ return undefined;
79
+ state.status.set(blockId, record);
80
+ return "status";
81
+ }
82
+ if (payload.name === "program_status_removed") {
83
+ const ids = Array.isArray(inner.ids) ? inner.ids : [];
84
+ if (!ids.includes(""))
85
+ return undefined;
86
+ return state.status.delete(blockId) ? "status" : undefined;
87
+ }
88
+ return undefined;
89
+ }
90
+ case "block_closed": {
91
+ const blockId = payload.block_id;
92
+ if (typeof blockId !== "string")
93
+ return undefined;
94
+ state.status.delete(blockId);
95
+ state.layout.blocks.delete(blockId);
96
+ return "layout";
97
+ }
98
+ case "block_created":
99
+ case "window_label_changed":
100
+ case "window_created":
101
+ case "window_closed":
102
+ case "session_label_changed":
103
+ return "layout";
104
+ default:
105
+ return undefined;
106
+ }
107
+ }
108
+ /** A Rex auto-label such as `Window 395a96cb` says nothing about the work; prefer the program. */
109
+ const AUTO_LABEL = /^Window [0-9a-f]+$/i;
110
+ /**
111
+ * Siblings are named after their tab, like Herdr siblings. A pane in the same tab as this pi
112
+ * would share our tab's name, so it is named after its program instead.
113
+ */
114
+ export function listRexSiblings(state, selfBlock) {
115
+ if (!selfBlock)
116
+ return undefined;
117
+ const selfWindow = state.layout.blocks.get(selfBlock);
118
+ const parts = [];
119
+ for (const [blockId, record] of state.status) {
120
+ if (blockId === selfBlock)
121
+ continue;
122
+ const windowId = state.layout.blocks.get(blockId);
123
+ const label = windowId && windowId !== selfWindow ? state.layout.tabs.get(windowId) : undefined;
124
+ const name = label && !AUTO_LABEL.test(label) ? label : record.app;
125
+ if (!name)
126
+ continue;
127
+ parts.push({ name, status: record.state });
128
+ }
129
+ if (parts.length === 0)
130
+ return undefined;
131
+ return parts;
132
+ }
133
+ const REX_EXEC_TIMEOUT_MS = 3000;
134
+ const REX_RESTART_MS = 10000;
135
+ const REX_LAYOUT_DEBOUNCE_MS = 150;
136
+ export function createRexSiblings(deps) {
137
+ const env = deps.env ?? process.env;
138
+ const spawn = deps.spawn ?? ((command, args) => nodeSpawn(command, args, { stdio: ["ignore", "pipe", "pipe"] }));
139
+ const session = env.REX_SESSION ?? "";
140
+ const selfBlock = env.REX_BLOCK;
141
+ let alive = true;
142
+ let child;
143
+ let restartTimer;
144
+ let layoutTimer;
145
+ let layoutInFlight = false;
146
+ let layoutDirty = false;
147
+ let siblings;
148
+ const state = createRexState();
149
+ const listeners = createListeners();
150
+ const recompute = () => {
151
+ if (!alive)
152
+ return;
153
+ const next = listRexSiblings(state, selfBlock);
154
+ const changed = JSON.stringify(next) !== JSON.stringify(siblings);
155
+ siblings = next;
156
+ if (changed)
157
+ listeners.notify();
158
+ };
159
+ const refreshLayout = async (seedStatus) => {
160
+ if (!alive)
161
+ return;
162
+ if (layoutInFlight) {
163
+ layoutDirty = true;
164
+ return;
165
+ }
166
+ layoutInFlight = true;
167
+ try {
168
+ const run = await deps.pi.exec("rex", ["window", "ls", "-s", session, "--json"], { timeout: REX_EXEC_TIMEOUT_MS });
169
+ if (!alive)
170
+ return;
171
+ if (run.code !== 0 || run.killed)
172
+ return;
173
+ const layout = parseWindowList(run.stdout);
174
+ state.layout = layout;
175
+ for (const blockId of [...state.status.keys()]) {
176
+ if (!layout.blocks.has(blockId))
177
+ state.status.delete(blockId);
178
+ }
179
+ if (seedStatus) {
180
+ await Promise.all([...layout.blocks.keys()]
181
+ .filter((blockId) => blockId !== selfBlock)
182
+ .map(async (blockId) => {
183
+ try {
184
+ const status = await deps.pi.exec("rex", ["block", "call", "-s", session, "-b", blockId, "program_status"], { timeout: REX_EXEC_TIMEOUT_MS });
185
+ if (!alive || status.code !== 0 || status.killed)
186
+ return;
187
+ const record = parseProgramStatus(status.stdout);
188
+ if (record)
189
+ state.status.set(blockId, record);
190
+ else
191
+ state.status.delete(blockId);
192
+ }
193
+ catch {
194
+ // A pane that vanished mid-seed shows up via events later.
195
+ }
196
+ }));
197
+ }
198
+ recompute();
199
+ }
200
+ catch {
201
+ // Keep the last known layout; the next layout event retries.
202
+ }
203
+ finally {
204
+ layoutInFlight = false;
205
+ if (layoutDirty && alive) {
206
+ layoutDirty = false;
207
+ void refreshLayout(false);
208
+ }
209
+ }
210
+ };
211
+ const scheduleLayoutRefresh = () => {
212
+ if (!alive || layoutTimer)
213
+ return;
214
+ layoutTimer = setTimeout(() => {
215
+ layoutTimer = undefined;
216
+ void refreshLayout(false);
217
+ }, REX_LAYOUT_DEBOUNCE_MS);
218
+ };
219
+ const start = () => {
220
+ if (!alive)
221
+ return;
222
+ restartTimer = undefined;
223
+ let proc;
224
+ try {
225
+ proc = spawn("rex", ["events", "-s", session, "--json"]);
226
+ }
227
+ catch {
228
+ scheduleRestart();
229
+ return;
230
+ }
231
+ child = proc;
232
+ if (proc.stdout) {
233
+ const lines = createInterface({ input: proc.stdout });
234
+ lines.on("line", (line) => {
235
+ if (!alive || child !== proc)
236
+ return;
237
+ const effect = applyRexEvent(state, line);
238
+ if (effect === "status")
239
+ recompute();
240
+ else if (effect === "layout") {
241
+ recompute();
242
+ scheduleLayoutRefresh();
243
+ }
244
+ });
245
+ }
246
+ proc.on("error", () => { if (child === proc)
247
+ onExit(); });
248
+ proc.on("exit", () => { if (child === proc)
249
+ onExit(); });
250
+ void refreshLayout(true);
251
+ };
252
+ const onExit = () => {
253
+ child = undefined;
254
+ if (!alive)
255
+ return;
256
+ state.status.clear();
257
+ recompute();
258
+ scheduleRestart();
259
+ };
260
+ const scheduleRestart = () => {
261
+ if (!alive || restartTimer)
262
+ return;
263
+ restartTimer = setTimeout(start, REX_RESTART_MS);
264
+ };
265
+ start();
266
+ return {
267
+ get: () => siblings,
268
+ onChange: listeners.add,
269
+ dispose() {
270
+ if (!alive)
271
+ return;
272
+ alive = false;
273
+ if (restartTimer)
274
+ clearTimeout(restartTimer);
275
+ if (layoutTimer)
276
+ clearTimeout(layoutTimer);
277
+ restartTimer = undefined;
278
+ layoutTimer = undefined;
279
+ const proc = child;
280
+ child = undefined;
281
+ try {
282
+ proc?.kill();
283
+ }
284
+ catch { /* already gone */ }
285
+ siblings = undefined;
286
+ listeners.clear();
287
+ },
288
+ };
289
+ }
package/dist/session.js CHANGED
@@ -7,17 +7,35 @@ import { bindFooterContributionEvents } from "./footer-contribution-events.js";
7
7
  import { createFooterContributions } from "./footer-contributions.js";
8
8
  import { createGitState } from "./git-state.js";
9
9
  import { installHeader } from "./header.js";
10
- import { listSiblings, parseAgentList, parseTabList } from "./herdr.js";
10
+ import { createBackgroundTracker } from "./background.js";
11
+ import { backgroundSegment } from "./background-overlay.js";
12
+ import { createPrSource, findPrUrls, prSegment } from "./prs.js";
11
13
  import { applyReclaim, reclaimPolicy } from "./reclaim.js";
14
+ import { createSiblingSource } from "./siblings.js";
12
15
  import { setSessionCwd } from "./tool-rows.js";
13
16
  import { setEditDiffMode } from "./tools.js";
14
17
  import { hideGauntletWidgets } from "./tracker.js";
15
- const HERDR_FAST_MS = 2000;
16
- const HERDR_SLOW_MS = 10000;
17
- const HERDR_EXEC_TIMEOUT_MS = 3000;
18
+ /** Custom session entry remembering a PR this session created, so a resumed session shows it. */
19
+ export const PR_ENTRY_TYPE = "pi-minimal.pr";
20
+ function readSessionPrs(ctx) {
21
+ const urls = [];
22
+ try {
23
+ for (const entry of ctx.sessionManager.getBranch()) {
24
+ if (entry.type !== "custom" || entry.customType !== PR_ENTRY_TYPE)
25
+ continue;
26
+ const url = entry.data?.url;
27
+ if (typeof url === "string")
28
+ urls.push(url);
29
+ }
30
+ }
31
+ catch {
32
+ // No branch available (tests, odd hosts): start empty.
33
+ }
34
+ return urls;
35
+ }
18
36
  /**
19
- * Everything pi-minimal owns for one session: config, cwd, the contribution registry, the git and
20
- * Herdr pollers, and the header/footer/status-line chrome. Disposal stops every timer and makes
37
+ * Everything pi-minimal owns for one session: config, cwd, the contribution registry, the git
38
+ * poller, the sibling source (Herdr or Rex), and the header/footer/status-line chrome. Disposal stops every timer and makes
21
39
  * late async results inert, so two sessions in one process never share or overwrite each other's
22
40
  * state. Non-TUI sessions get a session too, but it only binds the contribution protocol (which
23
41
  * replies `not-tui`) and installs no chrome.
@@ -28,7 +46,7 @@ export function createSession(pi, ctx, deps) {
28
46
  const contributions = createFooterContributions();
29
47
  const stopContributionEvents = bindFooterContributionEvents(pi.events, contributions, id, ctx.mode);
30
48
  if (ctx.mode !== "tui") {
31
- return { id, pin() { }, refreshGit() { }, dispose() { if (!alive)
49
+ return { id, pin() { }, refreshGit() { }, notePrUrls() { }, dispose() { if (!alive)
32
50
  return; alive = false; stopContributionEvents(); } };
33
51
  }
34
52
  const config = loadPiMinimalConfig({ agentDir: getAgentDir(), cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() });
@@ -44,9 +62,8 @@ export function createSession(pi, ctx, deps) {
44
62
  const gitState = createGitState(pi, () => ctx.cwd);
45
63
  let stopFooter = () => { };
46
64
  let siblings;
47
- let herdrTimer;
48
- let herdrPeriod = 0;
49
- let herdrInFlight;
65
+ const background = createBackgroundTracker({ pi });
66
+ const prs = createPrSource({ pi, cwd: () => ctx.cwd, sessionPrs: readSessionPrs(ctx) });
50
67
  const settleTimers = [];
51
68
  const contextPct = () => {
52
69
  const usage = ctx.getContextUsage?.();
@@ -61,10 +78,25 @@ export function createSession(pi, ctx, deps) {
61
78
  busy: deps.busy,
62
79
  editorText: () => (typeof ctx.ui.getEditorText === "function" ? ctx.ui.getEditorText() : ""),
63
80
  contextPct,
81
+ prs: (theme) => {
82
+ const snap = prs.get();
83
+ return prSegment(snap.prs, theme, 4, snap.repo ? `${snap.repo.host}/${snap.repo.nameWithOwner}` : undefined);
84
+ },
85
+ onPrsChange: prs.onChange,
86
+ background: () => backgroundSegment(background.get()),
87
+ onBackgroundChange: background.onChange,
64
88
  }, contributions);
65
89
  };
66
90
  const installStatusLine = () => {
67
- installCwd(ctx, { gitState: gitState.get, siblings: () => siblings, onGitChange: gitState.onChange });
91
+ installCwd(ctx, {
92
+ gitState: gitState.get,
93
+ siblings: () => siblings?.get(),
94
+ onChange(listener) {
95
+ const offGit = gitState.onChange(listener);
96
+ const offSiblings = siblings?.onChange(listener) ?? (() => { });
97
+ return () => { offGit(); offSiblings(); };
98
+ },
99
+ });
68
100
  };
69
101
  const pin = (reason) => {
70
102
  if (!alive)
@@ -79,45 +111,9 @@ export function createSession(pi, ctx, deps) {
79
111
  ctx.ui.setWidget("tw-cwd", undefined);
80
112
  installStatusLine();
81
113
  };
82
- const scheduleHerdr = (periodMs) => {
83
- if (!alive || herdrPeriod === periodMs)
84
- return;
85
- if (herdrTimer)
86
- clearInterval(herdrTimer);
87
- herdrPeriod = periodMs;
88
- herdrTimer = setInterval(() => { void probeHerdr(); }, periodMs);
89
- };
90
- const probeHerdr = async () => {
91
- if (!alive || herdrInFlight)
92
- return;
93
- const ac = new AbortController();
94
- herdrInFlight = ac;
95
- try {
96
- const agentRun = await pi.exec("herdr", ["agent", "list"], { timeout: HERDR_EXEC_TIMEOUT_MS, signal: ac.signal });
97
- if (!alive)
98
- return;
99
- const tabRun = await pi.exec("herdr", ["tab", "list"], { timeout: HERDR_EXEC_TIMEOUT_MS, signal: ac.signal });
100
- if (!alive)
101
- return;
102
- if (agentRun.code !== 0 || tabRun.code !== 0 || agentRun.killed || tabRun.killed) {
103
- siblings = undefined;
104
- scheduleHerdr(HERDR_SLOW_MS);
105
- return;
106
- }
107
- siblings = listSiblings(parseAgentList(agentRun.stdout), parseTabList(tabRun.stdout), process.env.HERDR_PANE_ID, process.env.HERDR_TAB_ID);
108
- scheduleHerdr(siblings ? HERDR_FAST_MS : HERDR_SLOW_MS);
109
- }
110
- catch {
111
- if (!alive)
112
- return;
113
- siblings = undefined;
114
- scheduleHerdr(HERDR_SLOW_MS);
115
- }
116
- finally {
117
- if (herdrInFlight === ac)
118
- herdrInFlight = undefined;
119
- }
120
- };
114
+ // The source is chosen from the environment (HERDR_ENV, else REX_SESSION/REX_BLOCK); start it
115
+ // before the status line so its onChange subscription is live.
116
+ siblings = createSiblingSource({ pi, spawn: deps.spawn });
121
117
  // Install once, then re-assert shortly after: other extensions' session_start handlers may draw after ours.
122
118
  installHeader(ctx);
123
119
  installChromeFooter();
@@ -128,24 +124,41 @@ export function createSession(pi, ctx, deps) {
128
124
  ctx.ui.setWorkingIndicator();
129
125
  for (const delay of [0, 50])
130
126
  settleTimers.push(setTimeout(() => pin("session_settle"), delay));
131
- if (process.env.HERDR_ENV === "1")
132
- void probeHerdr();
133
127
  return {
134
128
  id,
129
+ background,
130
+ backgroundShortcut: config.backgroundShortcut,
131
+ prs,
132
+ notePrUrls(text) {
133
+ if (!alive)
134
+ return;
135
+ const known = new Set(prs.sessionPrs());
136
+ for (const url of findPrUrls(text)) {
137
+ if (known.has(url))
138
+ continue;
139
+ prs.addSessionPr(url);
140
+ try {
141
+ pi.appendEntry?.(PR_ENTRY_TYPE, { url });
142
+ }
143
+ catch {
144
+ // A session that cannot accept entries just forgets the PR on restart.
145
+ }
146
+ }
147
+ },
135
148
  pin,
136
- refreshGit(force) { if (alive)
137
- void gitState.refresh(force); },
149
+ refreshGit(force) { if (alive) {
150
+ void gitState.refresh(force);
151
+ void prs.refresh();
152
+ } },
138
153
  dispose() {
139
154
  if (!alive)
140
155
  return;
141
156
  alive = false;
142
157
  for (const timer of settleTimers.splice(0))
143
158
  clearTimeout(timer);
144
- if (herdrTimer)
145
- clearInterval(herdrTimer);
146
- herdrTimer = undefined;
147
- herdrInFlight?.abort();
148
- herdrInFlight = undefined;
159
+ background.dispose();
160
+ prs.dispose();
161
+ siblings?.dispose();
149
162
  siblings = undefined;
150
163
  gitState.dispose();
151
164
  stopFooter();
@@ -0,0 +1,14 @@
1
+ import { createHerdrSiblings } from "./herdr.js";
2
+ import { createRexSiblings } from "./rex.js";
3
+ /**
4
+ * Picks the host the session runs in. Herdr wins when both are present (Rex inside a Herdr pane):
5
+ * Herdr knows every agent it launched, Rex only the programs that report status.
6
+ */
7
+ export function createSiblingSource(deps) {
8
+ const env = deps.env ?? process.env;
9
+ if (env.HERDR_ENV === "1")
10
+ return createHerdrSiblings(deps.pi, env);
11
+ if (env.REX_SESSION && env.REX_BLOCK)
12
+ return createRexSiblings({ pi: deps.pi, env, spawn: deps.spawn });
13
+ return undefined;
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-minimal",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -28,7 +28,8 @@
28
28
  },
29
29
  "peerDependencies": {
30
30
  "@earendil-works/pi-coding-agent": "*",
31
- "@earendil-works/pi-tui": "*"
31
+ "@earendil-works/pi-tui": "*",
32
+ "@earendil-works/pi-ai": "*"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@earendil-works/pi-coding-agent": "^1.4.0",
@@ -37,7 +38,8 @@
37
38
  "@types/node": "*",
38
39
  "eslint": "^10.11.0",
39
40
  "typescript": "*",
40
- "typescript-eslint": "^8.70.1"
41
+ "typescript-eslint": "^8.70.1",
42
+ "@earendil-works/pi-ai": "^1.4.0"
41
43
  },
42
44
  "scripts": {
43
45
  "build": "tsc -p tsconfig.json",
package/release-notes.md CHANGED
@@ -1,57 +1,44 @@
1
- Session lifecycle, one tool-row lifecycle, and a domain glossary. `hideGauntletWidgets`
2
- is now opt-in and the 200 ms chrome steal interval is gone.
1
+ Rex support and a footer hints line that carries live state: sibling agents from Rex as well as
2
+ Herdr, background processes left by bash calls, and the repo's open PRs as clickable chips.
3
3
 
4
4
  ### Added
5
5
 
6
- - `write` rows show the written content (first 400 lines) when expanded with `ctrl+o`.
7
- - With `editDiff: "expanded"` (the default), diffs of up to 8 lines are shown inline;
8
- hiding them saved nothing and cost a keypress. Longer diffs still wait for `ctrl+o`.
9
- - `CONTEXT.md`: a glossary of the domain (chrome, reclaim, slots, owner token, the snapshot
10
- kinds, plan vs the legacy pi-gauntlet projection, activity rows, hints, turn rule, working
11
- status). The 0.1 spec and plan moved to `doc/archive/`.
12
- - The plan slot can be addressed as `slot: "plan"`; `summary` stays accepted as its legacy
13
- name and the ready event now lists `["activity", "plan", "summary"]`.
6
+ - Open PRs: the footer shows `#1675 #1670 +3` chips, right-aligned on the hints line below the editor, for open PRs in the repo of cwd that you
7
+ authored or were asked to review, each an OSC 8 hyperlink to the PR, coloured accent (mine), dim
8
+ (my draft), amber (needs my review) or red (failing CI / changes requested). `/prs [mine|review|all]`
9
+ lists them with open / paste URL / checkout actions; `list_open_prs` tool for the model. Uses the
10
+ `gh` CLI; works on GitHub.com and GitHub Enterprise. PRs created by the session (`gh pr create`,
11
+ `gt submit`) are remembered as session entries and shown from any directory, as `repo#n` when
12
+ outside the cwd repo, until merged. New peer dependency `@earendil-works/pi-ai` (supplied by pi)
13
+ for the tool schema.
14
+ - Background processes: `⚙ N bg` after the key hints in the footer while processes left behind by `bash` tool
15
+ calls are running; `ctrl+b` / `/bg` opens an overlay listing each one with running/ended state and
16
+ duration, `k` kills the selection, `K` its process group. Attribution diffs `ps` around each bash
17
+ call using pi's detached process groups, so no command is rewritten. New `backgroundShortcut`
18
+ setting (`"ctrl+b"`; `null` disables the key).
19
+
20
+ - Rex siblings: inside a Rex session (`REX_SESSION` + `REX_BLOCK`) the status line shows other
21
+ panes' agent state, read from Rex's Program Status Protocol (OSC 7501) over one
22
+ `rex events --json` stream instead of polling. Needs a reporter in each pi (an extension that
23
+ writes OSC 7501 on `agent_start` / `agent_settled`); programs such as claude-code or terraform
24
+ that already speak the protocol appear as well. `error` renders like `blocked`.
14
25
 
15
26
  ### Changed
16
27
 
17
- - `hideGauntletWidgets` now defaults to `false`. Hiding another extension's widgets is
18
- opt-in; set it to `true` to keep the previous behaviour.
19
- - Chrome is re-asserted only on events (session start and its two settle ticks, turn
20
- end, tool end). The 200 ms interval that cleared pi-gauntlet's widgets five times a
21
- second is gone; it never reinstalled the header or footer anyway.
22
- - All session-scoped state (config, cwd, the footer contribution registry, the git and
23
- Herdr pollers, the chrome) now lives on one `Session` object created at session start
24
- and disposed at shutdown or when the next session starts. Late async results from a
25
- disposed session are dropped instead of drawing into the new one.
26
- - The compact, generic and codemode tool rows are now three small strategies over one
27
- row lifecycle (`createRowRenderers`): running state, live spinner, status colours,
28
- the error line and expansion are implemented once. Edit-diff colouring moved to
29
- `src/edit-diff.ts`. No visible change beyond the two items above.
30
- - Internal renames for clarity: `TrackerSnapshot` → `GauntletProjection`,
31
- `readSnapshot` → `readGauntletProjection`, the tracker's `TaskStatus` →
32
- `GauntletTaskStatus`, and the slot's authority symbol is `ownerToken`. No wire change.
33
- - `doc/footer-contributions.md` now states precisely when the legacy projection is replaced
34
- (only while a plan line renders), how control characters are sanitised (VT stripped,
35
- C0/C1/bidi replaced with spaces), and that `inReview`/`blocked` are bounded individually.
36
-
37
- ### Fixed
38
-
39
- - The working message is cleared on session shutdown; it used to drop the context
40
- before clearing and leave the last status on screen.
41
- - Git state reads the working directory once per refresh, so branch and dirty count
42
- always describe the same tree, and one throwing change listener no longer blocks
43
- the others.
28
+ - Sibling status now comes from a `SiblingSource` (`src/siblings.ts`) picked from the environment;
29
+ the Herdr poller moved from `session.ts` into `herdr.ts` unchanged. Herdr wins when both hosts
30
+ are present. The status line re-renders when the sibling list changes, not only on git changes.
44
31
 
45
32
  ## What's Changed
46
- * refactor: per-session lifecycle, no chrome steal interval, hideGauntletWidgets opt-in by @raviy06 in https://github.com/raviy06/pi-minimal/pull/19
47
- * refactor(tools): one row lifecycle + strategies; write content on expand; small diffs inline by @raviy06 in https://github.com/raviy06/pi-minimal/pull/20
48
- * docs: CONTEXT.md glossary, clearer internal names, 'plan' slot alias (0.5.0) by @raviy06 in https://github.com/raviy06/pi-minimal/pull/21
33
+ * feat: Rex siblings on the status line via OSC 7501 event stream by @raviy06 in https://github.com/raviy06/pi-minimal/pull/22
34
+ * feat: background processes on the status line with a ctrl+b overlay by @raviy06 in https://github.com/raviy06/pi-minimal/pull/23
35
+ * feat: open PR chips and background count on the footer hints line by @raviy06 in https://github.com/raviy06/pi-minimal/pull/24
49
36
 
50
37
 
51
- **Full Changelog**: https://github.com/raviy06/pi-minimal/compare/v0.4.0...v0.5.0
38
+ **Full Changelog**: https://github.com/raviy06/pi-minimal/compare/v0.5.0...v0.6.0
52
39
 
53
40
  ---
54
41
  **Install / upgrade**
55
42
  ```bash
56
- pi install npm:pi-minimal@0.5.0 # or: pi update --extensions
43
+ pi install npm:pi-minimal@0.6.0 # or: pi update --extensions
57
44
  ```