@diegopetrucci/pi-extensions 0.1.50 → 0.1.52
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/.pi-fleet-tested-version +1 -1
- package/README.md +5 -2
- package/extensions/agent-workflow-audit/.pi-fleet-tested-version +1 -1
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/.pi-fleet-tested-version +1 -1
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-last-message/.pi-fleet-tested-version +1 -1
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/brrr/.pi-fleet-tested-version +1 -1
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/.pi-fleet-tested-version +1 -1
- package/extensions/claude-fast/package.json +1 -1
- package/extensions/confirm-destructive/.pi-fleet-tested-version +1 -1
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/.pi-fleet-tested-version +1 -1
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/.pi-fleet-tested-version +1 -1
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/.pi-fleet-tested-version +1 -0
- package/extensions/contrarian/README.md +85 -0
- package/extensions/contrarian/index.ts +1463 -0
- package/extensions/contrarian/package.json +37 -0
- package/extensions/dirty-repo-guard/.pi-fleet-tested-version +1 -1
- package/extensions/dirty-repo-guard/package.json +1 -1
- package/extensions/git-footer/.pi-fleet-tested-version +1 -1
- package/extensions/git-footer/package.json +1 -1
- package/extensions/gnosis/.pi-fleet-tested-version +1 -1
- package/extensions/gnosis/package.json +1 -1
- package/extensions/illustrations-to-explain-things/.pi-fleet-tested-version +1 -1
- package/extensions/illustrations-to-explain-things/package.json +1 -1
- package/extensions/inline-bash/.pi-fleet-tested-version +1 -1
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/.pi-fleet-tested-version +1 -1
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/.pi-fleet-tested-version +1 -1
- package/extensions/minimal-footer/README.md +32 -3
- package/extensions/minimal-footer/index.ts +539 -2
- package/extensions/minimal-footer/minimal-footer.example.json +6 -0
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/notify/.pi-fleet-tested-version +1 -1
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/.pi-fleet-tested-version +1 -1
- package/extensions/openai-fast/package.json +1 -1
- package/extensions/oracle/.pi-fleet-tested-version +1 -1
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/.pi-fleet-tested-version +1 -1
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/package.json +1 -1
- package/extensions/review/.pi-fleet-tested-version +1 -1
- package/extensions/review/package.json +1 -1
- package/extensions/todo/.pi-fleet-tested-version +1 -1
- package/extensions/todo/package.json +1 -1
- package/extensions/triage-comments/.pi-fleet-tested-version +1 -1
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +3 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { basename, dirname, join } from "node:path";
|
|
3
4
|
import {
|
|
4
5
|
AuthStorage,
|
|
@@ -46,6 +47,12 @@ const DEFAULT_CONFIG: MinimalFooterConfig = {
|
|
|
46
47
|
label: "xp",
|
|
47
48
|
color: "warning",
|
|
48
49
|
},
|
|
50
|
+
gitStatus: {
|
|
51
|
+
enabled: true,
|
|
52
|
+
refreshIntervalMs: 8_000,
|
|
53
|
+
gitTimeoutMs: 1_500,
|
|
54
|
+
ghTimeoutMs: 3_000,
|
|
55
|
+
},
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
const DUMB_ZONE_COLORS = new Set<DumbZoneColor>([
|
|
@@ -92,6 +99,12 @@ interface MinimalFooterConfig {
|
|
|
92
99
|
label: string;
|
|
93
100
|
color: DumbZoneColor;
|
|
94
101
|
};
|
|
102
|
+
gitStatus: {
|
|
103
|
+
enabled: boolean;
|
|
104
|
+
refreshIntervalMs: number;
|
|
105
|
+
gitTimeoutMs: number;
|
|
106
|
+
ghTimeoutMs: number;
|
|
107
|
+
};
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
type UsageSessionState = {
|
|
@@ -103,6 +116,7 @@ type UsageSessionState = {
|
|
|
103
116
|
error?: string;
|
|
104
117
|
inflight?: Promise<void>;
|
|
105
118
|
requestRender?: () => void;
|
|
119
|
+
gitCache?: GitFooterCache;
|
|
106
120
|
};
|
|
107
121
|
|
|
108
122
|
type ProjectConfigContext = {
|
|
@@ -110,6 +124,67 @@ type ProjectConfigContext = {
|
|
|
110
124
|
isProjectTrusted?: () => boolean;
|
|
111
125
|
};
|
|
112
126
|
|
|
127
|
+
type GitStatusSnapshot = {
|
|
128
|
+
branch?: string;
|
|
129
|
+
staged: number;
|
|
130
|
+
unstaged: number;
|
|
131
|
+
untracked: number;
|
|
132
|
+
conflict: number;
|
|
133
|
+
ahead: number;
|
|
134
|
+
behind: number;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
type PullRequestSnapshot = {
|
|
138
|
+
number?: number | string;
|
|
139
|
+
state?: string;
|
|
140
|
+
isDraft?: boolean;
|
|
141
|
+
url?: string;
|
|
142
|
+
title?: string;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
type CommandResult = {
|
|
146
|
+
stdout: string;
|
|
147
|
+
stderr: string;
|
|
148
|
+
exitCode: number | null;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
type CommandRunner = (
|
|
152
|
+
command: string,
|
|
153
|
+
args: readonly string[],
|
|
154
|
+
options: { cwd: string; signal: AbortSignal },
|
|
155
|
+
) => Promise<CommandResult>;
|
|
156
|
+
|
|
157
|
+
type TimerHandle = unknown;
|
|
158
|
+
|
|
159
|
+
type Clock = {
|
|
160
|
+
setInterval(callback: () => void, ms: number): TimerHandle;
|
|
161
|
+
clearInterval(handle: TimerHandle): void;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
type GitFooterCacheOptions = {
|
|
165
|
+
cwd: () => string;
|
|
166
|
+
canRun?: () => boolean;
|
|
167
|
+
runner?: CommandRunner;
|
|
168
|
+
clock?: Clock;
|
|
169
|
+
refreshIntervalMs?: number;
|
|
170
|
+
gitTimeoutMs?: number;
|
|
171
|
+
ghTimeoutMs?: number;
|
|
172
|
+
onChange?: () => void;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const BRANCH_HEAD_PREFIX = "# branch.head ";
|
|
176
|
+
const BRANCH_AB_PREFIX = "# branch.ab ";
|
|
177
|
+
const STATUS_SEPARATOR = " • ";
|
|
178
|
+
const GIT_STATUS_ARGS = [
|
|
179
|
+
"--no-optional-locks",
|
|
180
|
+
"-c",
|
|
181
|
+
"core.fsmonitor=false",
|
|
182
|
+
"status",
|
|
183
|
+
"--porcelain=v2",
|
|
184
|
+
"--branch",
|
|
185
|
+
] as const;
|
|
186
|
+
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number,state,isDraft,url,title"] as const;
|
|
187
|
+
|
|
113
188
|
function readConfigFile(path: string): RecursivePartial<MinimalFooterConfig> {
|
|
114
189
|
if (!existsSync(path)) return {};
|
|
115
190
|
|
|
@@ -131,6 +206,7 @@ function mergeConfig(
|
|
|
131
206
|
const primaryWindow = codexUsage?.windows?.primary;
|
|
132
207
|
const secondaryWindow = codexUsage?.windows?.secondary;
|
|
133
208
|
const experimentalMarker = overrides.experimentalMarker;
|
|
209
|
+
const gitStatus = overrides.gitStatus;
|
|
134
210
|
|
|
135
211
|
return {
|
|
136
212
|
context: {
|
|
@@ -183,6 +259,21 @@ function mergeConfig(
|
|
|
183
259
|
base.experimentalMarker.color,
|
|
184
260
|
),
|
|
185
261
|
},
|
|
262
|
+
gitStatus: {
|
|
263
|
+
enabled: normalizeBoolean(gitStatus?.enabled, base.gitStatus.enabled),
|
|
264
|
+
refreshIntervalMs: normalizePositiveNumber(
|
|
265
|
+
gitStatus?.refreshIntervalMs,
|
|
266
|
+
base.gitStatus.refreshIntervalMs,
|
|
267
|
+
),
|
|
268
|
+
gitTimeoutMs: normalizePositiveNumber(
|
|
269
|
+
gitStatus?.gitTimeoutMs,
|
|
270
|
+
base.gitStatus.gitTimeoutMs,
|
|
271
|
+
),
|
|
272
|
+
ghTimeoutMs: normalizePositiveNumber(
|
|
273
|
+
gitStatus?.ghTimeoutMs,
|
|
274
|
+
base.gitStatus.ghTimeoutMs,
|
|
275
|
+
),
|
|
276
|
+
},
|
|
186
277
|
};
|
|
187
278
|
}
|
|
188
279
|
|
|
@@ -239,6 +330,410 @@ function shouldShowExperimentalMarker(config: MinimalFooterConfig): boolean {
|
|
|
239
330
|
return config.experimentalMarker.enabled && process.env.PI_EXPERIMENTAL === "1";
|
|
240
331
|
}
|
|
241
332
|
|
|
333
|
+
function shouldShowGitStatus(config: MinimalFooterConfig): boolean {
|
|
334
|
+
return config.gitStatus.enabled;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function canRunProjectGit(ctx: ProjectConfigContext): boolean {
|
|
338
|
+
return canReadProjectConfig(ctx);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function createEmptyGitStatus(): GitStatusSnapshot {
|
|
342
|
+
return {
|
|
343
|
+
branch: undefined,
|
|
344
|
+
staged: 0,
|
|
345
|
+
unstaged: 0,
|
|
346
|
+
untracked: 0,
|
|
347
|
+
conflict: 0,
|
|
348
|
+
ahead: 0,
|
|
349
|
+
behind: 0,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function positiveCount(value: number): number {
|
|
354
|
+
return Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function addTrackedStatusCounts(status: GitStatusSnapshot, xy: string): void {
|
|
358
|
+
if (xy.length !== 2) return;
|
|
359
|
+
if (xy[0] !== ".") status.staged += 1;
|
|
360
|
+
if (xy[1] !== ".") status.unstaged += 1;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function parseBranchAheadBehind(line: string, status: GitStatusSnapshot): void {
|
|
364
|
+
const match = /^# branch\.ab \+(\d+) -(\d+)$/.exec(line);
|
|
365
|
+
if (!match) return;
|
|
366
|
+
status.ahead = Number.parseInt(match[1]!, 10);
|
|
367
|
+
status.behind = Number.parseInt(match[2]!, 10);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function normalizeBranchHead(value: string): string {
|
|
371
|
+
const branch = value.trim();
|
|
372
|
+
return branch === "(detached)" ? "detached" : branch;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function parseGitStatusPorcelainV2(output: string): GitStatusSnapshot {
|
|
376
|
+
const status = createEmptyGitStatus();
|
|
377
|
+
|
|
378
|
+
for (const rawLine of output.split("\n")) {
|
|
379
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
380
|
+
if (!line) continue;
|
|
381
|
+
|
|
382
|
+
if (line.startsWith(BRANCH_HEAD_PREFIX)) {
|
|
383
|
+
status.branch = normalizeBranchHead(line.slice(BRANCH_HEAD_PREFIX.length)) || undefined;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (line.startsWith(BRANCH_AB_PREFIX)) {
|
|
388
|
+
parseBranchAheadBehind(line, status);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
393
|
+
addTrackedStatusCounts(status, line.slice(2, 4));
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (line.startsWith("u ")) {
|
|
398
|
+
status.conflict += 1;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (line.startsWith("? ")) status.untracked += 1;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return status;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function formatGitStatusFooterSegment(status: GitStatusSnapshot | undefined): string | undefined {
|
|
409
|
+
if (!status) return undefined;
|
|
410
|
+
|
|
411
|
+
const parts: string[] = [];
|
|
412
|
+
const indicators: Array<[string, number]> = [
|
|
413
|
+
["!", positiveCount(status.conflict)],
|
|
414
|
+
["+", positiveCount(status.staged)],
|
|
415
|
+
["~", positiveCount(status.unstaged)],
|
|
416
|
+
["?", positiveCount(status.untracked)],
|
|
417
|
+
["↑", positiveCount(status.ahead)],
|
|
418
|
+
["↓", positiveCount(status.behind)],
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
for (const [prefix, count] of indicators) {
|
|
422
|
+
if (count > 0) parts.push(`${prefix}${count}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function formatPullRequestFooterSegment(pullRequest: PullRequestSnapshot | undefined): string | undefined {
|
|
429
|
+
const value = pullRequest?.number;
|
|
430
|
+
if (Number.isSafeInteger(value) && Number(value) > 0) return `PR #${value}`;
|
|
431
|
+
if (typeof value === "string") {
|
|
432
|
+
const trimmed = value.trim();
|
|
433
|
+
if (/^[1-9]\d*$/.test(trimmed)) return `PR #${trimmed}`;
|
|
434
|
+
}
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function formatGitFooterStatus(
|
|
439
|
+
status: GitStatusSnapshot | undefined,
|
|
440
|
+
pullRequest: PullRequestSnapshot | undefined,
|
|
441
|
+
): string | undefined {
|
|
442
|
+
const parts = [
|
|
443
|
+
formatGitStatusFooterSegment(status),
|
|
444
|
+
formatPullRequestFooterSegment(pullRequest),
|
|
445
|
+
].filter((part): part is string => !!part);
|
|
446
|
+
return parts.length > 0 ? parts.join(STATUS_SEPARATOR) : undefined;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
|
|
450
|
+
const trimmed = stdout.trim();
|
|
451
|
+
if (!trimmed) return undefined;
|
|
452
|
+
let parsed: unknown;
|
|
453
|
+
try {
|
|
454
|
+
parsed = JSON.parse(trimmed);
|
|
455
|
+
} catch {
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
459
|
+
|
|
460
|
+
const record = parsed as Record<string, unknown>;
|
|
461
|
+
const snapshot: PullRequestSnapshot = {};
|
|
462
|
+
if (typeof record.number === "number" || typeof record.number === "string") {
|
|
463
|
+
snapshot.number = record.number;
|
|
464
|
+
}
|
|
465
|
+
if (typeof record.state === "string") snapshot.state = record.state;
|
|
466
|
+
if (typeof record.isDraft === "boolean") snapshot.isDraft = record.isDraft;
|
|
467
|
+
if (typeof record.url === "string") snapshot.url = record.url;
|
|
468
|
+
if (typeof record.title === "string") snapshot.title = record.title;
|
|
469
|
+
return snapshot;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function gitStatusSnapshotsEqual(
|
|
473
|
+
left: GitStatusSnapshot | undefined,
|
|
474
|
+
right: GitStatusSnapshot | undefined,
|
|
475
|
+
): boolean {
|
|
476
|
+
return (
|
|
477
|
+
left?.branch === right?.branch
|
|
478
|
+
&& left?.staged === right?.staged
|
|
479
|
+
&& left?.unstaged === right?.unstaged
|
|
480
|
+
&& left?.untracked === right?.untracked
|
|
481
|
+
&& left?.conflict === right?.conflict
|
|
482
|
+
&& left?.ahead === right?.ahead
|
|
483
|
+
&& left?.behind === right?.behind
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function pullRequestSnapshotsEqual(
|
|
488
|
+
left: PullRequestSnapshot | undefined,
|
|
489
|
+
right: PullRequestSnapshot | undefined,
|
|
490
|
+
): boolean {
|
|
491
|
+
return (
|
|
492
|
+
left?.number === right?.number
|
|
493
|
+
&& left?.state === right?.state
|
|
494
|
+
&& left?.isDraft === right?.isDraft
|
|
495
|
+
&& left?.url === right?.url
|
|
496
|
+
&& left?.title === right?.title
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function defaultRunner(
|
|
501
|
+
command: string,
|
|
502
|
+
args: readonly string[],
|
|
503
|
+
options: { cwd: string; signal: AbortSignal },
|
|
504
|
+
): Promise<CommandResult> {
|
|
505
|
+
return new Promise((resolve, reject) => {
|
|
506
|
+
let child;
|
|
507
|
+
try {
|
|
508
|
+
child = spawn(command, [...args], {
|
|
509
|
+
cwd: options.cwd,
|
|
510
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
511
|
+
windowsHide: true,
|
|
512
|
+
});
|
|
513
|
+
} catch (error) {
|
|
514
|
+
reject(error);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let stdout = "";
|
|
519
|
+
let stderr = "";
|
|
520
|
+
let settled = false;
|
|
521
|
+
|
|
522
|
+
const finish = (result: CommandResult | Error) => {
|
|
523
|
+
if (settled) return;
|
|
524
|
+
settled = true;
|
|
525
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
526
|
+
if (result instanceof Error) reject(result);
|
|
527
|
+
else resolve(result);
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const onAbort = () => {
|
|
531
|
+
try {
|
|
532
|
+
child.kill("SIGTERM");
|
|
533
|
+
} catch {
|
|
534
|
+
// Ignore: process may already be gone.
|
|
535
|
+
}
|
|
536
|
+
finish(new Error("aborted"));
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
child.stdout?.on("data", (chunk: Buffer | string) => {
|
|
540
|
+
stdout += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
541
|
+
});
|
|
542
|
+
child.stderr?.on("data", (chunk: Buffer | string) => {
|
|
543
|
+
stderr += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
544
|
+
});
|
|
545
|
+
child.on("error", finish);
|
|
546
|
+
child.on("close", (code) => finish({ stdout, stderr, exitCode: code }));
|
|
547
|
+
|
|
548
|
+
if (options.signal.aborted) {
|
|
549
|
+
onAbort();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function defaultClock(): Clock {
|
|
557
|
+
return {
|
|
558
|
+
setInterval(callback, ms) {
|
|
559
|
+
const handle = setInterval(callback, ms);
|
|
560
|
+
(handle as { unref?: () => void }).unref?.();
|
|
561
|
+
return handle;
|
|
562
|
+
},
|
|
563
|
+
clearInterval(handle) {
|
|
564
|
+
clearInterval(handle as ReturnType<typeof setInterval>);
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
class GitFooterCache {
|
|
570
|
+
private readonly cwd: () => string;
|
|
571
|
+
private readonly canRun: () => boolean;
|
|
572
|
+
private readonly runner: CommandRunner;
|
|
573
|
+
private readonly clock: Clock;
|
|
574
|
+
private readonly refreshIntervalMs: number;
|
|
575
|
+
private readonly gitTimeoutMs: number;
|
|
576
|
+
private readonly ghTimeoutMs: number;
|
|
577
|
+
private readonly onChange: (() => void) | undefined;
|
|
578
|
+
|
|
579
|
+
private intervalHandle: TimerHandle | undefined;
|
|
580
|
+
private readonly inflightControllers = new Set<AbortController>();
|
|
581
|
+
private disposed = false;
|
|
582
|
+
private refreshInFlight: Promise<void> | undefined;
|
|
583
|
+
private statusSnapshot: GitStatusSnapshot | undefined;
|
|
584
|
+
private pullRequestSnapshot: PullRequestSnapshot | undefined;
|
|
585
|
+
private lastSeenBranch: string | undefined;
|
|
586
|
+
|
|
587
|
+
constructor(options: GitFooterCacheOptions) {
|
|
588
|
+
this.cwd = options.cwd;
|
|
589
|
+
this.canRun = options.canRun ?? (() => true);
|
|
590
|
+
this.runner = options.runner ?? defaultRunner;
|
|
591
|
+
this.clock = options.clock ?? defaultClock();
|
|
592
|
+
this.refreshIntervalMs = options.refreshIntervalMs ?? DEFAULT_CONFIG.gitStatus.refreshIntervalMs;
|
|
593
|
+
this.gitTimeoutMs = options.gitTimeoutMs ?? DEFAULT_CONFIG.gitStatus.gitTimeoutMs;
|
|
594
|
+
this.ghTimeoutMs = options.ghTimeoutMs ?? DEFAULT_CONFIG.gitStatus.ghTimeoutMs;
|
|
595
|
+
this.onChange = options.onChange;
|
|
596
|
+
|
|
597
|
+
this.intervalHandle = this.clock.setInterval(() => {
|
|
598
|
+
void this.refresh();
|
|
599
|
+
}, this.refreshIntervalMs);
|
|
600
|
+
void this.refresh();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
getStatusSnapshot(): GitStatusSnapshot | undefined {
|
|
604
|
+
return this.statusSnapshot;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
getPullRequestSnapshot(): PullRequestSnapshot | undefined {
|
|
608
|
+
return this.pullRequestSnapshot;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
refresh(): Promise<void> {
|
|
612
|
+
if (this.disposed) return Promise.resolve();
|
|
613
|
+
if (!this.canRun()) {
|
|
614
|
+
const previousStatusSnapshot = this.statusSnapshot;
|
|
615
|
+
const previousPullRequestSnapshot = this.pullRequestSnapshot;
|
|
616
|
+
this.statusSnapshot = undefined;
|
|
617
|
+
this.pullRequestSnapshot = undefined;
|
|
618
|
+
this.lastSeenBranch = undefined;
|
|
619
|
+
this.emitChangeIfSnapshotsChanged(previousStatusSnapshot, previousPullRequestSnapshot);
|
|
620
|
+
return Promise.resolve();
|
|
621
|
+
}
|
|
622
|
+
if (this.refreshInFlight) return this.refreshInFlight;
|
|
623
|
+
const run = this.runRefresh()
|
|
624
|
+
.finally(() => {
|
|
625
|
+
this.refreshInFlight = undefined;
|
|
626
|
+
})
|
|
627
|
+
.catch(() => undefined);
|
|
628
|
+
this.refreshInFlight = run;
|
|
629
|
+
return run;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private async runRefresh(): Promise<void> {
|
|
633
|
+
const previousStatusSnapshot = this.statusSnapshot;
|
|
634
|
+
const previousPullRequestSnapshot = this.pullRequestSnapshot;
|
|
635
|
+
|
|
636
|
+
const result = await this.fetchGitStatus();
|
|
637
|
+
if (this.disposed) return;
|
|
638
|
+
if (result.kind === "transient") return;
|
|
639
|
+
if (result.kind === "not-a-repo") {
|
|
640
|
+
this.statusSnapshot = undefined;
|
|
641
|
+
this.pullRequestSnapshot = undefined;
|
|
642
|
+
this.lastSeenBranch = undefined;
|
|
643
|
+
this.emitChangeIfSnapshotsChanged(previousStatusSnapshot, previousPullRequestSnapshot);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const status = result.status;
|
|
648
|
+
this.statusSnapshot = status;
|
|
649
|
+
|
|
650
|
+
const branch = typeof status.branch === "string" ? status.branch : undefined;
|
|
651
|
+
const isValidBranch = !!branch && branch !== "detached";
|
|
652
|
+
const branchChanged = branch !== this.lastSeenBranch;
|
|
653
|
+
|
|
654
|
+
if (branchChanged) this.pullRequestSnapshot = undefined;
|
|
655
|
+
this.lastSeenBranch = branch;
|
|
656
|
+
|
|
657
|
+
if (!isValidBranch) {
|
|
658
|
+
this.pullRequestSnapshot = undefined;
|
|
659
|
+
this.emitChangeIfSnapshotsChanged(previousStatusSnapshot, previousPullRequestSnapshot);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const pr = await this.fetchPullRequest();
|
|
664
|
+
if (this.disposed) return;
|
|
665
|
+
if (pr !== undefined) this.pullRequestSnapshot = pr;
|
|
666
|
+
else if (branchChanged) this.pullRequestSnapshot = undefined;
|
|
667
|
+
|
|
668
|
+
this.emitChangeIfSnapshotsChanged(previousStatusSnapshot, previousPullRequestSnapshot);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private emitChangeIfSnapshotsChanged(
|
|
672
|
+
previousStatusSnapshot: GitStatusSnapshot | undefined,
|
|
673
|
+
previousPullRequestSnapshot: PullRequestSnapshot | undefined,
|
|
674
|
+
): void {
|
|
675
|
+
if (this.disposed) return;
|
|
676
|
+
if (
|
|
677
|
+
gitStatusSnapshotsEqual(previousStatusSnapshot, this.statusSnapshot)
|
|
678
|
+
&& pullRequestSnapshotsEqual(previousPullRequestSnapshot, this.pullRequestSnapshot)
|
|
679
|
+
) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
this.onChange?.();
|
|
684
|
+
} catch {
|
|
685
|
+
// Rendering hooks should not break refreshes.
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private async fetchGitStatus(): Promise<
|
|
690
|
+
| { kind: "ok"; status: GitStatusSnapshot }
|
|
691
|
+
| { kind: "not-a-repo" }
|
|
692
|
+
| { kind: "transient" }
|
|
693
|
+
> {
|
|
694
|
+
const result = await this.runCommandSafely("git", GIT_STATUS_ARGS, this.gitTimeoutMs);
|
|
695
|
+
if (!result) return { kind: "transient" };
|
|
696
|
+
if (result.exitCode !== 0) return { kind: "not-a-repo" };
|
|
697
|
+
return { kind: "ok", status: parseGitStatusPorcelainV2(result.stdout) };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private async fetchPullRequest(): Promise<PullRequestSnapshot | undefined> {
|
|
701
|
+
const result = await this.runCommandSafely("gh", GH_PR_VIEW_ARGS, this.ghTimeoutMs);
|
|
702
|
+
if (!result || result.exitCode !== 0) return undefined;
|
|
703
|
+
return parsePullRequestJson(result.stdout);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private async runCommandSafely(
|
|
707
|
+
command: string,
|
|
708
|
+
args: readonly string[],
|
|
709
|
+
timeoutMs: number,
|
|
710
|
+
): Promise<CommandResult | undefined> {
|
|
711
|
+
if (this.disposed) return undefined;
|
|
712
|
+
const controller = new AbortController();
|
|
713
|
+
this.inflightControllers.add(controller);
|
|
714
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
715
|
+
try {
|
|
716
|
+
return await this.runner(command, args, { cwd: this.cwd(), signal: controller.signal });
|
|
717
|
+
} catch {
|
|
718
|
+
return undefined;
|
|
719
|
+
} finally {
|
|
720
|
+
clearTimeout(timeoutId);
|
|
721
|
+
this.inflightControllers.delete(controller);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
dispose(): void {
|
|
726
|
+
if (this.disposed) return;
|
|
727
|
+
this.disposed = true;
|
|
728
|
+
if (this.intervalHandle !== undefined) {
|
|
729
|
+
this.clock.clearInterval(this.intervalHandle);
|
|
730
|
+
this.intervalHandle = undefined;
|
|
731
|
+
}
|
|
732
|
+
for (const controller of this.inflightControllers) controller.abort();
|
|
733
|
+
this.inflightControllers.clear();
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
242
737
|
function clearUsageState(state: UsageSessionState): void {
|
|
243
738
|
state.snapshot = undefined;
|
|
244
739
|
state.lastFetchedAt = undefined;
|
|
@@ -302,6 +797,28 @@ async function refreshUsageIfNeeded(
|
|
|
302
797
|
export default function (pi: ExtensionAPI) {
|
|
303
798
|
const states = new WeakMap<object, UsageSessionState>();
|
|
304
799
|
|
|
800
|
+
function disposeGitCache(state: UsageSessionState): void {
|
|
801
|
+
state.gitCache?.dispose();
|
|
802
|
+
state.gitCache = undefined;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function ensureGitCache(ctx: ExtensionContext, state: UsageSessionState): void {
|
|
806
|
+
if (!shouldShowGitStatus(state.config) || !canRunProjectGit(ctx)) {
|
|
807
|
+
disposeGitCache(state);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (state.gitCache) return;
|
|
812
|
+
state.gitCache = new GitFooterCache({
|
|
813
|
+
cwd: () => ctx.cwd,
|
|
814
|
+
canRun: () => canRunProjectGit(ctx),
|
|
815
|
+
refreshIntervalMs: state.config.gitStatus.refreshIntervalMs,
|
|
816
|
+
gitTimeoutMs: state.config.gitStatus.gitTimeoutMs,
|
|
817
|
+
ghTimeoutMs: state.config.gitStatus.ghTimeoutMs,
|
|
818
|
+
onChange: () => state.requestRender?.(),
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
305
822
|
pi.on("session_start", (_event, ctx) => {
|
|
306
823
|
const state: UsageSessionState = {
|
|
307
824
|
authStorage: AuthStorage.create(),
|
|
@@ -310,6 +827,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
310
827
|
};
|
|
311
828
|
states.set(ctx.sessionManager, state);
|
|
312
829
|
|
|
830
|
+
ensureGitCache(ctx, state);
|
|
831
|
+
|
|
313
832
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
314
833
|
const unsub = footerData.onBranchChange(() => tui.requestRender());
|
|
315
834
|
state.requestRender = () => tui.requestRender();
|
|
@@ -318,12 +837,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
318
837
|
return {
|
|
319
838
|
dispose() {
|
|
320
839
|
if (state.requestRender) state.requestRender = undefined;
|
|
840
|
+
disposeGitCache(state);
|
|
321
841
|
unsub();
|
|
322
842
|
},
|
|
323
843
|
invalidate() {},
|
|
324
844
|
render(width: number): string[] {
|
|
325
845
|
const repo = basename(ctx.cwd);
|
|
326
|
-
const
|
|
846
|
+
const gitStatus = shouldShowGitStatus(state.config)
|
|
847
|
+
? formatGitFooterStatus(
|
|
848
|
+
state.gitCache?.getStatusSnapshot(),
|
|
849
|
+
state.gitCache?.getPullRequestSnapshot(),
|
|
850
|
+
)
|
|
851
|
+
: undefined;
|
|
852
|
+
const branch = footerData.getGitBranch()
|
|
853
|
+
?? state.gitCache?.getStatusSnapshot()?.branch
|
|
854
|
+
?? "";
|
|
855
|
+
const branchText = gitStatus ? [branch, gitStatus].filter(Boolean).join(" · ") : branch;
|
|
327
856
|
|
|
328
857
|
const usage = ctx.getContextUsage();
|
|
329
858
|
const context = usage?.percent == null ? "?" : `${usage.percent.toFixed(1)}%`;
|
|
@@ -337,7 +866,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
337
866
|
const thinking = pi.getThinkingLevel();
|
|
338
867
|
const modelText = thinking === "off" ? model : `${model} ${thinking}`;
|
|
339
868
|
|
|
340
|
-
const branchStyled = theme.fg("dim",
|
|
869
|
+
const branchStyled = theme.fg("dim", branchText);
|
|
341
870
|
const repoStyled = theme.fg("dim", repo);
|
|
342
871
|
const contextParts: string[] = [];
|
|
343
872
|
if (state.config.context.showPercent) contextParts.push(theme.fg("dim", context));
|
|
@@ -386,5 +915,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
386
915
|
const state = states.get(ctx.sessionManager);
|
|
387
916
|
if (!state) return;
|
|
388
917
|
void refreshUsageIfNeeded(ctx, state);
|
|
918
|
+
ensureGitCache(ctx, state);
|
|
919
|
+
void state.gitCache?.refresh();
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
923
|
+
const state = states.get(ctx.sessionManager);
|
|
924
|
+
if (!state) return;
|
|
925
|
+
disposeGitCache(state);
|
|
389
926
|
});
|
|
390
927
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.79.10
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.79.10
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-openai-fast",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "A pi extension that enables OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5 by injecting the priority service tier.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.79.10
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.79.10
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.79.10
|