pi-minimal 0.4.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/CHANGELOG.md +81 -1
- package/CONTEXT.md +107 -0
- package/README.md +9 -6
- package/dist/background-overlay.js +95 -0
- package/dist/background.js +206 -0
- package/dist/config.js +10 -1
- package/dist/cwd.js +2 -2
- package/dist/edit-diff.js +74 -0
- package/dist/footer-contribution-events.js +5 -3
- package/dist/footer-contributions.js +15 -13
- package/dist/footer.js +15 -3
- package/dist/git-state.js +31 -11
- package/dist/herdr.js +78 -0
- package/dist/index.js +79 -202
- package/dist/listeners.js +23 -0
- package/dist/prs-command.js +76 -0
- package/dist/prs.js +270 -0
- package/dist/reclaim.js +0 -3
- package/dist/rex.js +289 -0
- package/dist/session.js +168 -0
- package/dist/siblings.js +14 -0
- package/dist/tools.js +213 -374
- package/dist/tracker.js +2 -2
- package/dist/working-status.js +2 -1
- package/doc/archive/README.md +3 -0
- package/doc/footer-contributions.md +7 -7
- package/package.json +5 -3
- package/release-notes.md +33 -12
- package/src/background-overlay.ts +103 -0
- package/src/background.ts +250 -0
- package/src/config.ts +10 -1
- package/src/cwd.ts +5 -4
- package/src/edit-diff.ts +73 -0
- package/src/footer-contribution-events.ts +7 -4
- package/src/footer-contributions.ts +15 -13
- package/src/footer.ts +23 -3
- package/src/git-state.ts +29 -10
- package/src/herdr.ts +78 -5
- package/src/index.ts +81 -195
- package/src/listeners.ts +22 -0
- package/src/prs-command.ts +81 -0
- package/src/prs.ts +348 -0
- package/src/reclaim.ts +0 -4
- package/src/rex.ts +304 -0
- package/src/session.ts +185 -0
- package/src/siblings.ts +38 -0
- package/src/tools.ts +261 -389
- package/src/tracker.ts +11 -5
- package/src/working-status.ts +2 -1
- package/test/background.test.ts +198 -0
- package/test/config.test.ts +24 -11
- package/test/footer-composition.test.ts +61 -0
- package/test/footer-contribution-events.test.ts +15 -0
- package/test/git-state.test.ts +35 -0
- package/test/index.test.ts +202 -4
- package/test/prs.test.ts +272 -0
- package/test/rex.test.ts +249 -0
- package/test/tools.test.ts +36 -3
- package/test/working-status.test.ts +53 -0
- /package/doc/{plans/2026-09-22-pi-minimal-package.md → archive/2026-09-22-plan-pi-minimal-package.md} +0 -0
- /package/doc/{specs/2026-09-22-pi-minimal-package.md → archive/2026-09-22-spec-pi-minimal-package.md} +0 -0
package/dist/reclaim.js
CHANGED
|
@@ -2,9 +2,6 @@ export function reclaimPolicy(reason) {
|
|
|
2
2
|
if (reason === "session_start") {
|
|
3
3
|
return { resetEditor: true, reinstallChrome: true };
|
|
4
4
|
}
|
|
5
|
-
if (reason === "interval") {
|
|
6
|
-
return { resetEditor: false, reinstallChrome: false };
|
|
7
|
-
}
|
|
8
5
|
// session_settle / turn_end / tool_execution_end: restore chrome, keep editor
|
|
9
6
|
return { resetEditor: false, reinstallChrome: true };
|
|
10
7
|
}
|
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
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { setChromeMargin } from "./chrome.js";
|
|
3
|
+
import { loadPiMinimalConfig } from "./config.js";
|
|
4
|
+
import { installCwd } from "./cwd.js";
|
|
5
|
+
import { installFooter } from "./footer.js";
|
|
6
|
+
import { bindFooterContributionEvents } from "./footer-contribution-events.js";
|
|
7
|
+
import { createFooterContributions } from "./footer-contributions.js";
|
|
8
|
+
import { createGitState } from "./git-state.js";
|
|
9
|
+
import { installHeader } from "./header.js";
|
|
10
|
+
import { createBackgroundTracker } from "./background.js";
|
|
11
|
+
import { backgroundSegment } from "./background-overlay.js";
|
|
12
|
+
import { createPrSource, findPrUrls, prSegment } from "./prs.js";
|
|
13
|
+
import { applyReclaim, reclaimPolicy } from "./reclaim.js";
|
|
14
|
+
import { createSiblingSource } from "./siblings.js";
|
|
15
|
+
import { setSessionCwd } from "./tool-rows.js";
|
|
16
|
+
import { setEditDiffMode } from "./tools.js";
|
|
17
|
+
import { hideGauntletWidgets } from "./tracker.js";
|
|
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
|
+
}
|
|
36
|
+
/**
|
|
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
|
|
39
|
+
* late async results inert, so two sessions in one process never share or overwrite each other's
|
|
40
|
+
* state. Non-TUI sessions get a session too, but it only binds the contribution protocol (which
|
|
41
|
+
* replies `not-tui`) and installs no chrome.
|
|
42
|
+
*/
|
|
43
|
+
export function createSession(pi, ctx, deps) {
|
|
44
|
+
const id = ctx.sessionManager.getSessionId();
|
|
45
|
+
let alive = true;
|
|
46
|
+
const contributions = createFooterContributions();
|
|
47
|
+
const stopContributionEvents = bindFooterContributionEvents(pi.events, contributions, id, ctx.mode);
|
|
48
|
+
if (ctx.mode !== "tui") {
|
|
49
|
+
return { id, pin() { }, refreshGit() { }, notePrUrls() { }, dispose() { if (!alive)
|
|
50
|
+
return; alive = false; stopContributionEvents(); } };
|
|
51
|
+
}
|
|
52
|
+
const config = loadPiMinimalConfig({ agentDir: getAgentDir(), cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() });
|
|
53
|
+
setSessionCwd(ctx.cwd);
|
|
54
|
+
setEditDiffMode(config.editDiff);
|
|
55
|
+
try {
|
|
56
|
+
const settings = SettingsManager.create(ctx.cwd, undefined, { projectTrusted: ctx.isProjectTrusted() });
|
|
57
|
+
setChromeMargin(settings.getOutputPad());
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Keep the current margin when settings are unavailable.
|
|
61
|
+
}
|
|
62
|
+
const gitState = createGitState(pi, () => ctx.cwd);
|
|
63
|
+
let stopFooter = () => { };
|
|
64
|
+
let siblings;
|
|
65
|
+
const background = createBackgroundTracker({ pi });
|
|
66
|
+
const prs = createPrSource({ pi, cwd: () => ctx.cwd, sessionPrs: readSessionPrs(ctx) });
|
|
67
|
+
const settleTimers = [];
|
|
68
|
+
const contextPct = () => {
|
|
69
|
+
const usage = ctx.getContextUsage?.();
|
|
70
|
+
const window = usage?.contextWindow || ctx.model?.contextWindow;
|
|
71
|
+
if (typeof usage?.tokens !== "number" || !window)
|
|
72
|
+
return undefined;
|
|
73
|
+
return Math.max(0, Math.min(100, Math.round((usage.tokens / window) * 100)));
|
|
74
|
+
};
|
|
75
|
+
const installChromeFooter = () => {
|
|
76
|
+
stopFooter();
|
|
77
|
+
stopFooter = installFooter(ctx, {
|
|
78
|
+
busy: deps.busy,
|
|
79
|
+
editorText: () => (typeof ctx.ui.getEditorText === "function" ? ctx.ui.getEditorText() : ""),
|
|
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,
|
|
88
|
+
}, contributions);
|
|
89
|
+
};
|
|
90
|
+
const installStatusLine = () => {
|
|
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
|
+
});
|
|
100
|
+
};
|
|
101
|
+
const pin = (reason) => {
|
|
102
|
+
if (!alive)
|
|
103
|
+
return;
|
|
104
|
+
applyReclaim(reason, ctx.ui, {
|
|
105
|
+
widgets: () => { if (config.hideGauntletWidgets)
|
|
106
|
+
hideGauntletWidgets(ctx); },
|
|
107
|
+
chrome: () => { installHeader(ctx); installChromeFooter(); },
|
|
108
|
+
});
|
|
109
|
+
if (!reclaimPolicy(reason).reinstallChrome)
|
|
110
|
+
return;
|
|
111
|
+
ctx.ui.setWidget("tw-cwd", undefined);
|
|
112
|
+
installStatusLine();
|
|
113
|
+
};
|
|
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 });
|
|
117
|
+
// Install once, then re-assert shortly after: other extensions' session_start handlers may draw after ours.
|
|
118
|
+
installHeader(ctx);
|
|
119
|
+
installChromeFooter();
|
|
120
|
+
installStatusLine();
|
|
121
|
+
void gitState.refresh(true);
|
|
122
|
+
pin("session_start");
|
|
123
|
+
ctx.ui.setHiddenThinkingLabel("◆ Thought");
|
|
124
|
+
ctx.ui.setWorkingIndicator();
|
|
125
|
+
for (const delay of [0, 50])
|
|
126
|
+
settleTimers.push(setTimeout(() => pin("session_settle"), delay));
|
|
127
|
+
return {
|
|
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
|
+
},
|
|
148
|
+
pin,
|
|
149
|
+
refreshGit(force) { if (alive) {
|
|
150
|
+
void gitState.refresh(force);
|
|
151
|
+
void prs.refresh();
|
|
152
|
+
} },
|
|
153
|
+
dispose() {
|
|
154
|
+
if (!alive)
|
|
155
|
+
return;
|
|
156
|
+
alive = false;
|
|
157
|
+
for (const timer of settleTimers.splice(0))
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
background.dispose();
|
|
160
|
+
prs.dispose();
|
|
161
|
+
siblings?.dispose();
|
|
162
|
+
siblings = undefined;
|
|
163
|
+
gitState.dispose();
|
|
164
|
+
stopFooter();
|
|
165
|
+
stopContributionEvents();
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
package/dist/siblings.js
ADDED
|
@@ -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
|
+
}
|