@jaggerxtrm/pi-extensions 0.9.4 → 0.9.5
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/extensions/custom-footer/index.ts +99 -65
- package/extensions/xtprompt/index.test.ts +496 -0
- package/extensions/xtprompt/index.ts +421 -238
- package/extensions/xtprompt/package.json +1 -1
- package/extensions/xtrm-loader/index.ts +2 -2
- package/extensions/xtrm-ui/format.ts +5 -7
- package/extensions/xtrm-ui/index.ts +376 -833
- package/extensions/xtrm-ui/package.json +1 -1
- package/package.json +1 -1
- package/src/registry.ts +0 -2
- package/themes/xtrm-ui/{pidex-dark-flattools.json → xtrm-dark-flattools.json} +1 -1
- package/themes/xtrm-ui/{pidex-dark.json → xtrm-dark.json} +1 -1
- package/themes/xtrm-ui/{pidex-light-flattools.json → xtrm-light-flattools.json} +1 -1
- package/themes/xtrm-ui/{pidex-light.json → xtrm-light.json} +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { EventAdapter, SubprocessRunner } from "../../src/core";
|
|
14
14
|
|
|
15
15
|
export default function (pi: ExtensionAPI) {
|
|
16
16
|
interface BeadClaim {
|
|
@@ -32,6 +32,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
32
32
|
lastFetch: number;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
type RefreshKind = "runtime" | "beads";
|
|
36
|
+
|
|
35
37
|
const STATUS_ICONS: Record<string, string> = {
|
|
36
38
|
open: "○",
|
|
37
39
|
in_progress: "◐",
|
|
@@ -39,7 +41,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
39
41
|
closed: "✓",
|
|
40
42
|
};
|
|
41
43
|
|
|
42
|
-
// Chip background colours (raw ANSI — theme has no bg() API)
|
|
43
44
|
const CHIP_BG_NEUTRAL = "\x1b[48;5;238m";
|
|
44
45
|
const CHIP_BG_ACTIVE = "\x1b[48;5;39m";
|
|
45
46
|
const CHIP_BG_BLOCKED = "\x1b[48;5;88m";
|
|
@@ -52,13 +53,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
52
53
|
blocked: CHIP_BG_BLOCKED,
|
|
53
54
|
};
|
|
54
55
|
|
|
56
|
+
const CACHE_TTL = 5000;
|
|
57
|
+
const REFRESH_TIMEOUT_MS = 2000;
|
|
58
|
+
const TOOL_RESULT_REFRESH_DELAY_MS = 200;
|
|
59
|
+
const FOOTER_REAPPLY_DELAY_MS = 40;
|
|
60
|
+
|
|
55
61
|
const chip = (text: string, bg = CHIP_BG_NEUTRAL): string => `${bg}${CHIP_FG} ${text} ${CHIP_RESET}`;
|
|
56
62
|
|
|
57
63
|
let capturedPi: ExtensionAPI = pi;
|
|
58
64
|
let capturedCtx: any = null;
|
|
59
65
|
let requestRender: (() => void) | null = null;
|
|
66
|
+
let footerReapplyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
67
|
+
let scheduledRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
68
|
+
let scheduledRefreshKinds = new Set<RefreshKind>();
|
|
69
|
+
let runningRefreshPromise: Promise<void> | null = null;
|
|
70
|
+
let branchChangeUnsub: (() => void) | null = null;
|
|
60
71
|
|
|
61
|
-
const CACHE_TTL = 5000;
|
|
62
72
|
let refreshingBeads = false;
|
|
63
73
|
let refreshingRuntime = false;
|
|
64
74
|
|
|
@@ -76,10 +86,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
76
86
|
|
|
77
87
|
const getCwd = () => capturedCtx?.cwd || process.cwd();
|
|
78
88
|
const getShortId = (id: string) => id.split("-").pop() ?? id;
|
|
89
|
+
const runCommand = (command: string, args: string[], cwd: string) =>
|
|
90
|
+
SubprocessRunner.run(command, args, { cwd, timeoutMs: REFRESH_TIMEOUT_MS });
|
|
79
91
|
|
|
80
|
-
/**
|
|
81
|
-
* Parse git status --porcelain output into status flags
|
|
82
|
-
*/
|
|
83
92
|
const parseGitFlags = (porcelain: string): string => {
|
|
84
93
|
let modified = false;
|
|
85
94
|
let staged = false;
|
|
@@ -92,9 +101,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
92
101
|
return `${modified ? "*" : ""}${staged ? "+" : ""}${deleted ? "-" : ""}`;
|
|
93
102
|
};
|
|
94
103
|
|
|
95
|
-
/**
|
|
96
|
-
* Fetch git branch and status
|
|
97
|
-
*/
|
|
98
104
|
const refreshRuntimeState = async () => {
|
|
99
105
|
if (refreshingRuntime || Date.now() - runtimeState.lastFetch < CACHE_TTL) return;
|
|
100
106
|
refreshingRuntime = true;
|
|
@@ -103,25 +109,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
103
109
|
let branch: string | null = null;
|
|
104
110
|
let gitStatus = "";
|
|
105
111
|
|
|
106
|
-
const rootResult = await
|
|
112
|
+
const rootResult = await runCommand("git", ["rev-parse", "--show-toplevel"], cwd);
|
|
107
113
|
const repoRoot = rootResult.code === 0 ? rootResult.stdout.trim() : null;
|
|
108
114
|
|
|
109
115
|
if (repoRoot) {
|
|
110
|
-
const branchResult = await
|
|
116
|
+
const branchResult = await runCommand("git", ["branch", "--show-current"], cwd);
|
|
111
117
|
branch = branchResult.code === 0 ? branchResult.stdout.trim() || null : null;
|
|
112
118
|
|
|
113
|
-
const porcelainResult = await
|
|
114
|
-
"git",
|
|
115
|
-
["--no-optional-locks", "status", "--porcelain"],
|
|
116
|
-
{ cwd },
|
|
117
|
-
);
|
|
119
|
+
const porcelainResult = await runCommand("git", ["--no-optional-locks", "status", "--porcelain"], cwd);
|
|
118
120
|
const baseFlags = porcelainResult.code === 0 ? parseGitFlags(porcelainResult.stdout) : "";
|
|
119
121
|
|
|
120
122
|
let upstreamFlags = "";
|
|
121
|
-
const abResult = await
|
|
123
|
+
const abResult = await runCommand(
|
|
122
124
|
"git",
|
|
123
125
|
["--no-optional-locks", "rev-list", "--left-right", "--count", "@{upstream}...HEAD"],
|
|
124
|
-
|
|
126
|
+
cwd,
|
|
125
127
|
);
|
|
126
128
|
if (abResult.code === 0) {
|
|
127
129
|
const [behindRaw, aheadRaw] = abResult.stdout.trim().split(/\s+/);
|
|
@@ -148,9 +150,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
148
150
|
}
|
|
149
151
|
};
|
|
150
152
|
|
|
151
|
-
/**
|
|
152
|
-
* Format token counts (from original footer)
|
|
153
|
-
*/
|
|
154
153
|
const formatTokens = (count: number): string => {
|
|
155
154
|
if (count < 1000) return count.toString();
|
|
156
155
|
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
@@ -165,14 +164,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
165
164
|
if (!EventAdapter.isBeadsProject(cwd)) return;
|
|
166
165
|
refreshingBeads = true;
|
|
167
166
|
try {
|
|
168
|
-
const inProgressResult = await
|
|
167
|
+
const inProgressResult = await runCommand("bd", ["list", "--status=in_progress"], cwd);
|
|
169
168
|
const inProgressRaw = inProgressResult.code === 0 ? inProgressResult.stdout : "";
|
|
170
169
|
const ids = [...new Set([...inProgressRaw.matchAll(/^◐\s+([a-z][\w-]+)/gm)].map((m) => m[1]).filter((id) => id.includes("-")))];
|
|
171
170
|
|
|
172
171
|
let claims: BeadClaim[] = [];
|
|
173
172
|
if (ids.length === 1) {
|
|
174
173
|
const [id] = ids;
|
|
175
|
-
const showResult = await
|
|
174
|
+
const showResult = await runCommand("bd", ["show", id, "--json"], cwd);
|
|
176
175
|
if (showResult.code === 0) {
|
|
177
176
|
try {
|
|
178
177
|
const issue = JSON.parse(showResult.stdout)?.[0];
|
|
@@ -189,10 +188,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
189
188
|
|
|
190
189
|
let openCount = 0;
|
|
191
190
|
if (claims.length === 0) {
|
|
192
|
-
const listResult = await
|
|
191
|
+
const listResult = await runCommand("bd", ["list"], cwd);
|
|
193
192
|
if (listResult.code === 0) {
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
193
|
+
const match = listResult.stdout.match(/\((\d+)\s+open/);
|
|
194
|
+
if (match) openCount = parseInt(match[1], 10);
|
|
196
195
|
}
|
|
197
196
|
}
|
|
198
197
|
|
|
@@ -209,9 +208,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
209
208
|
}
|
|
210
209
|
};
|
|
211
210
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
const runRefreshKinds = async (refreshKinds: ReadonlySet<RefreshKind>) => {
|
|
212
|
+
await Promise.all([
|
|
213
|
+
refreshKinds.has("runtime") ? refreshRuntimeState() : Promise.resolve(),
|
|
214
|
+
refreshKinds.has("beads") ? refreshBeadState() : Promise.resolve(),
|
|
215
|
+
]);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const flushScheduledRefreshes = async () => {
|
|
219
|
+
if (runningRefreshPromise) return runningRefreshPromise;
|
|
220
|
+
const refreshKinds = scheduledRefreshKinds;
|
|
221
|
+
scheduledRefreshKinds = new Set<RefreshKind>();
|
|
222
|
+
runningRefreshPromise = runRefreshKinds(refreshKinds).finally(async () => {
|
|
223
|
+
runningRefreshPromise = null;
|
|
224
|
+
if (scheduledRefreshKinds.size > 0) {
|
|
225
|
+
await flushScheduledRefreshes();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return runningRefreshPromise;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const scheduleRefresh = (kinds: readonly RefreshKind[], delayMs = 0) => {
|
|
232
|
+
for (const kind of kinds) scheduledRefreshKinds.add(kind);
|
|
233
|
+
if (scheduledRefreshTimer) return;
|
|
234
|
+
scheduledRefreshTimer = setTimeout(() => {
|
|
235
|
+
scheduledRefreshTimer = null;
|
|
236
|
+
void flushScheduledRefreshes();
|
|
237
|
+
}, delayMs);
|
|
238
|
+
};
|
|
239
|
+
|
|
215
240
|
const buildBeadsLine = (width: number, theme: any): string => {
|
|
216
241
|
const { claims, openCount } = beadState;
|
|
217
242
|
|
|
@@ -239,55 +264,50 @@ export default function (pi: ExtensionAPI) {
|
|
|
239
264
|
return truncateToWidth(`○ no open issues`, width);
|
|
240
265
|
};
|
|
241
266
|
|
|
242
|
-
|
|
267
|
+
const attachBranchChangeListener = (footerData: any) => {
|
|
268
|
+
branchChangeUnsub?.();
|
|
269
|
+
const unsubscribe = footerData.onBranchChange(() => {
|
|
270
|
+
runtimeState.lastFetch = 0;
|
|
271
|
+
scheduleRefresh(["runtime"]);
|
|
272
|
+
});
|
|
273
|
+
branchChangeUnsub = unsubscribe;
|
|
274
|
+
return unsubscribe;
|
|
275
|
+
};
|
|
243
276
|
|
|
244
277
|
const applyCustomFooter = (ctx: any) => {
|
|
245
278
|
capturedCtx = ctx;
|
|
246
279
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
tui.requestRender();
|
|
251
|
-
});
|
|
280
|
+
const instanceRequestRender = () => tui.requestRender();
|
|
281
|
+
requestRender = instanceRequestRender;
|
|
282
|
+
const instanceBranchChangeUnsub = attachBranchChangeListener(footerData);
|
|
252
283
|
|
|
253
284
|
return {
|
|
254
285
|
dispose() {
|
|
255
|
-
|
|
256
|
-
|
|
286
|
+
instanceBranchChangeUnsub?.();
|
|
287
|
+
if (branchChangeUnsub === instanceBranchChangeUnsub) branchChangeUnsub = null;
|
|
288
|
+
if (requestRender === instanceRequestRender) requestRender = null;
|
|
257
289
|
},
|
|
258
290
|
invalidate() {},
|
|
259
291
|
render(width: number): string[] {
|
|
260
|
-
refreshRuntimeState().catch(() => {});
|
|
261
|
-
refreshBeadState().catch(() => {});
|
|
262
|
-
|
|
263
|
-
// === LINE 1: ~/path (branch *+↑) ===
|
|
264
|
-
// Like original, no session name, but with git status flags
|
|
265
292
|
let pwd = process.cwd();
|
|
266
293
|
const home = process.env.HOME || process.env.USERPROFILE;
|
|
267
294
|
if (home && pwd.startsWith(home)) {
|
|
268
295
|
pwd = `~${pwd.slice(home.length)}`;
|
|
269
296
|
}
|
|
270
297
|
|
|
271
|
-
// Use runtimeState branch (with git status) or fallback to footerData
|
|
272
298
|
const branch = runtimeState.branch || footerData.getGitBranch();
|
|
273
299
|
if (branch) {
|
|
274
|
-
const branchWithStatus = runtimeState.gitStatus
|
|
275
|
-
? `${branch} ${runtimeState.gitStatus}`
|
|
276
|
-
: branch;
|
|
300
|
+
const branchWithStatus = runtimeState.gitStatus ? `${branch} ${runtimeState.gitStatus}` : branch;
|
|
277
301
|
pwd = `${pwd} (${branchWithStatus})`;
|
|
278
302
|
}
|
|
279
303
|
|
|
280
304
|
const pwdLine = truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "..."));
|
|
281
305
|
|
|
282
|
-
// === LINE 2: XX%/window (provider) model • thinking ===
|
|
283
306
|
const usage = ctx.getContextUsage();
|
|
284
307
|
const model = ctx.model;
|
|
285
|
-
|
|
286
308
|
const contextWindow = usage?.contextWindow ?? model?.contextWindow ?? 0;
|
|
287
309
|
const contextPercentValue = usage?.percent ?? 0;
|
|
288
310
|
const contextPercent = usage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
|
|
289
|
-
|
|
290
|
-
// Build left side: context %/window (color-coded like original)
|
|
291
311
|
const contextDisplay =
|
|
292
312
|
contextPercent === "?"
|
|
293
313
|
? `?/${formatTokens(contextWindow)}`
|
|
@@ -299,11 +319,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
299
319
|
return text;
|
|
300
320
|
};
|
|
301
321
|
|
|
302
|
-
// Build right side: (provider) model • thinking
|
|
303
322
|
const modelName = model?.id || "no-model";
|
|
304
323
|
const providerCount = footerData.getAvailableProviderCount();
|
|
305
|
-
|
|
306
|
-
// Thinking level if model supports reasoning
|
|
307
324
|
let rightSideWithoutProvider = modelName;
|
|
308
325
|
if (model?.reasoning) {
|
|
309
326
|
const thinkingLevel = capturedPi.getThinkingLevel() || "off";
|
|
@@ -311,7 +328,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
311
328
|
thinkingLevel === "off" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;
|
|
312
329
|
}
|
|
313
330
|
|
|
314
|
-
// Prepend provider if >1
|
|
315
331
|
let rightSide = rightSideWithoutProvider;
|
|
316
332
|
if (providerCount > 1 && model) {
|
|
317
333
|
rightSide = `(${model.provider}) ${rightSideWithoutProvider}`;
|
|
@@ -320,7 +336,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
320
336
|
}
|
|
321
337
|
}
|
|
322
338
|
|
|
323
|
-
// Keep provider/model adjacent to usage (no right-bound alignment)
|
|
324
339
|
const separator = " ";
|
|
325
340
|
const leftWidth = visibleWidth(contextDisplay);
|
|
326
341
|
const separatorWidth = visibleWidth(separator);
|
|
@@ -334,16 +349,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
334
349
|
line2 = `${colorizeUsage(contextDisplay)}${theme.fg("dim", separator)}${theme.fg("dim", truncatedRight)}`;
|
|
335
350
|
}
|
|
336
351
|
|
|
337
|
-
// === LINE 3: ◐ 4843.5 Rework project bootstrap... ===
|
|
338
352
|
const line3 = buildBeadsLine(width, theme);
|
|
339
|
-
|
|
340
353
|
return [pwdLine, line2, line3];
|
|
341
354
|
},
|
|
342
355
|
};
|
|
343
356
|
});
|
|
344
357
|
};
|
|
345
358
|
|
|
346
|
-
const scheduleFooterReapply = (ctx: any, delayMs =
|
|
359
|
+
const scheduleFooterReapply = (ctx: any, delayMs = FOOTER_REAPPLY_DELAY_MS) => {
|
|
347
360
|
if (footerReapplyTimer) clearTimeout(footerReapplyTimer);
|
|
348
361
|
footerReapplyTimer = setTimeout(() => {
|
|
349
362
|
applyCustomFooter(ctx);
|
|
@@ -351,25 +364,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
351
364
|
}, delayMs);
|
|
352
365
|
};
|
|
353
366
|
|
|
354
|
-
|
|
355
|
-
capturedCtx = ctx;
|
|
367
|
+
const resetRefreshCaches = () => {
|
|
356
368
|
runtimeState.lastFetch = 0;
|
|
357
369
|
beadState.lastFetch = 0;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
373
|
+
capturedCtx = ctx;
|
|
374
|
+
resetRefreshCaches();
|
|
358
375
|
applyCustomFooter(ctx);
|
|
359
376
|
scheduleFooterReapply(ctx);
|
|
377
|
+
scheduleRefresh(["runtime", "beads"]);
|
|
360
378
|
});
|
|
361
379
|
|
|
362
380
|
pi.on("session_switch", async (_event, ctx) => {
|
|
363
|
-
|
|
381
|
+
capturedCtx = ctx;
|
|
382
|
+
resetRefreshCaches();
|
|
364
383
|
scheduleFooterReapply(ctx);
|
|
384
|
+
scheduleRefresh(["runtime", "beads"]);
|
|
365
385
|
});
|
|
366
386
|
|
|
367
387
|
pi.on("session_fork", async (_event, ctx) => {
|
|
368
|
-
|
|
388
|
+
capturedCtx = ctx;
|
|
389
|
+
resetRefreshCaches();
|
|
369
390
|
scheduleFooterReapply(ctx);
|
|
391
|
+
scheduleRefresh(["runtime", "beads"]);
|
|
370
392
|
});
|
|
371
393
|
|
|
372
394
|
pi.on("model_select", async (_event, ctx) => {
|
|
395
|
+
capturedCtx = ctx;
|
|
373
396
|
scheduleFooterReapply(ctx);
|
|
374
397
|
});
|
|
375
398
|
|
|
@@ -378,20 +401,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
401
|
clearTimeout(footerReapplyTimer);
|
|
379
402
|
footerReapplyTimer = null;
|
|
380
403
|
}
|
|
404
|
+
if (scheduledRefreshTimer) {
|
|
405
|
+
clearTimeout(scheduledRefreshTimer);
|
|
406
|
+
scheduledRefreshTimer = null;
|
|
407
|
+
}
|
|
408
|
+
scheduledRefreshKinds.clear();
|
|
409
|
+
branchChangeUnsub?.();
|
|
410
|
+
branchChangeUnsub = null;
|
|
411
|
+
requestRender = null;
|
|
381
412
|
});
|
|
382
413
|
|
|
383
|
-
// Bust caches immediately after relevant writes
|
|
384
414
|
pi.on("tool_result", async (event: any) => {
|
|
385
415
|
const cmd = event?.input?.command;
|
|
386
416
|
if (!cmd) return undefined;
|
|
387
417
|
|
|
418
|
+
const refreshKinds: RefreshKind[] = [];
|
|
388
419
|
if (/\bbd\s+(close|update|create|claim)\b/.test(cmd)) {
|
|
389
420
|
beadState.lastFetch = 0;
|
|
390
|
-
|
|
421
|
+
refreshKinds.push("beads");
|
|
391
422
|
}
|
|
392
423
|
if (/\bgit\s+/.test(cmd)) {
|
|
393
424
|
runtimeState.lastFetch = 0;
|
|
394
|
-
|
|
425
|
+
refreshKinds.push("runtime");
|
|
426
|
+
}
|
|
427
|
+
if (refreshKinds.length > 0) {
|
|
428
|
+
scheduleRefresh(refreshKinds, TOOL_RESULT_REFRESH_DELAY_MS);
|
|
395
429
|
}
|
|
396
430
|
return undefined;
|
|
397
431
|
});
|