@diegopetrucci/pi-extensions 0.1.39 → 0.1.40

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/README.md CHANGED
@@ -9,7 +9,7 @@ A collection of [pi](https://github.com/earendil-works/pi-mono) agent extensions
9
9
  - [`context-cap`](./extensions/context-cap): Caps effective model context windows at 200k tokens by default so pi avoids the `dumb zone`; toggle temporarily with `/context-cap`.
10
10
  - [`context-inspector`](./extensions/context-inspector): Adds `/context`, a local self-contained HTML dashboard that breaks down where the current session context is going, with category overview, top offenders, and drilldown search.
11
11
  - [`dirty-repo-guard`](./extensions/dirty-repo-guard): Prompts before new sessions, session switches, or forks when the current git repo has uncommitted changes.
12
- - [`git-footer`](./extensions/git-footer): Standalone footer extension that replaces pi's built-in footer with a TLH-style git summary: branch, dirty counts, ahead/behind, optional PR number, session name, plus context/model status. Not auto-loaded by the collection package because it conflicts with `minimal-footer`.
12
+ - [`git-footer`](./extensions/git-footer): Standalone extension that adds TLH-style git dirty counts, ahead/behind, and optional PR number to pi's built-in footer status area.
13
13
  - [`gnosis`](./extensions/gnosis): Exposes the `gn` repo-local knowledge base CLI as an agent tool for searching and recording durable project decisions, constraints, and intent.
14
14
  - [`inline-bash`](./extensions/inline-bash): Expands `!{command}` snippets in user prompts by running them through bash before the prompt reaches the agent.
15
15
  - [`librarian`](./extensions/librarian): Adds a GitHub research scout with a local repo checkout cache enabled by default under the OS user cache directory, toggleable with `/librarian-cache`, with cached repos expiring after 7 days of non-use.
@@ -1,14 +1,15 @@
1
1
  # git-footer
2
2
 
3
- A TLH-style git status footer for pi.
3
+ A TLH-style git status add-on for pi's built-in footer.
4
4
 
5
- This package is standalone-only and is not auto-loaded by the `@diegopetrucci/pi-extensions` collection package because only one custom footer should win.
5
+ This package is standalone-only and is not auto-loaded by the `@diegopetrucci/pi-extensions` collection package.
6
6
 
7
- It replaces pi's built-in footer with a compact two-line layout whose first line mirrors how [`the-last-harness`](https://github.com/diegopetrucci/the-last-harness) summarizes repository state:
7
+ It keeps pi's default footer intact and adds a compact git status segment through pi's extension status API:
8
8
 
9
9
  ```text
10
- <repo> <branch> <git-status> • PR #<number> • <session-name>
11
- ctx <percent>% • <model> <thinking> • <extension-statuses>
10
+ ~/repo (main) • session-name
11
+ ↑12k ↓3k 44.1%/200k model
12
+ +2 ~1 ?3 ↑1 • PR #123
12
13
  ```
13
14
 
14
15
  Git status indicators:
@@ -20,7 +21,7 @@ Git status indicators:
20
21
  - `↑N`: commits ahead of upstream
21
22
  - `↓N`: commits behind upstream
22
23
 
23
- The extension polls git status in the background, caches the latest snapshot, and keeps footer rendering synchronous. It also performs a best-effort `gh pr view` lookup for the current branch; if `gh` is unavailable or the branch has no PR, the PR segment is omitted.
24
+ The extension polls git status in the background and caches the latest snapshot. It also performs a best-effort `gh pr view` lookup for the current branch; if `gh` is unavailable or the branch has no PR, the PR segment is omitted.
24
25
 
25
26
  ## Install
26
27
 
@@ -36,6 +37,7 @@ Then reload pi:
36
37
 
37
38
  ## Notes
38
39
 
39
- - Replaces pi's built-in footer entirely.
40
- - Intended as a separate footer extension; do not enable it at the same time as another custom-footer extension such as `minimal-footer` unless you want the last-loaded footer to win.
41
- - `render()` never spawns subprocesses. Git and GitHub CLI lookups run on a short background interval with timeouts, and the footer reads only cached snapshots.
40
+ - Does not replace pi's built-in footer.
41
+ - Uses `ctx.ui.setStatus()`, so pi renders the git summary with other extension statuses.
42
+ - The current pi extension API does not support literally appending text inside the built-in footer's first `cwd (branch)` line without replacing the footer.
43
+ - Git and GitHub CLI lookups run on a short background interval with timeouts.
@@ -1,7 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { basename } from "node:path";
3
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
- import { truncateToWidth } from "@earendil-works/pi-tui";
5
3
 
6
4
  type GitStatusSnapshot = {
7
5
  branch?: string;
@@ -48,12 +46,12 @@ type GitFooterCacheOptions = {
48
46
  gitTimeoutMs?: number;
49
47
  ghTimeoutMs?: number;
50
48
  onChange?: () => void;
51
- onBranchChangeSource?: (callback: () => void) => () => void;
52
49
  };
53
50
 
54
51
  const BRANCH_HEAD_PREFIX = "# branch.head ";
55
52
  const BRANCH_AB_PREFIX = "# branch.ab ";
56
- const FOOTER_SEPARATOR = "";
53
+ const STATUS_KEY = "git-footer";
54
+ const STATUS_SEPARATOR = " • ";
57
55
  const DEFAULT_REFRESH_INTERVAL_MS = 8_000;
58
56
  const DEFAULT_GIT_TIMEOUT_MS = 1_500;
59
57
  const DEFAULT_GH_TIMEOUT_MS = 3_000;
@@ -157,21 +155,15 @@ function formatPullRequestFooterSegment(pullRequest: PullRequestSnapshot | undef
157
155
  return undefined;
158
156
  }
159
157
 
160
- function formatGitFooterSegments(
158
+ function formatGitFooterStatus(
161
159
  status: GitStatusSnapshot | undefined,
162
160
  pullRequest: PullRequestSnapshot | undefined,
163
- ): string[] {
164
- const segments: string[] = [];
165
- const branch = typeof status?.branch === "string" ? status.branch.trim() : "";
166
- if (branch) segments.push(branch);
167
-
168
- const statusSegment = formatGitStatusFooterSegment(status);
169
- if (statusSegment) segments.push(statusSegment);
170
-
171
- const pullRequestSegment = formatPullRequestFooterSegment(pullRequest);
172
- if (pullRequestSegment) segments.push(pullRequestSegment);
173
-
174
- return segments;
161
+ ): string | undefined {
162
+ const parts = [
163
+ formatGitStatusFooterSegment(status),
164
+ formatPullRequestFooterSegment(pullRequest),
165
+ ].filter((part): part is string => !!part);
166
+ return parts.length > 0 ? parts.join(STATUS_SEPARATOR) : undefined;
175
167
  }
176
168
 
177
169
  function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
@@ -307,7 +299,6 @@ class GitFooterCache {
307
299
  private readonly inflightControllers = new Set<AbortController>();
308
300
  private disposed = false;
309
301
  private refreshInFlight: Promise<void> | undefined;
310
- private branchChangeUnsubscribe: (() => void) | undefined;
311
302
  private statusSnapshot: GitStatusSnapshot | undefined;
312
303
  private pullRequestSnapshot: PullRequestSnapshot | undefined;
313
304
  private lastSeenBranch: string | undefined;
@@ -325,12 +316,6 @@ class GitFooterCache {
325
316
  void this.refresh();
326
317
  }, this.refreshIntervalMs);
327
318
  void this.refresh();
328
-
329
- if (options.onBranchChangeSource) {
330
- this.branchChangeUnsubscribe = options.onBranchChangeSource(() => {
331
- void this.refresh();
332
- });
333
- }
334
319
  }
335
320
 
336
321
  getStatusSnapshot(): GitStatusSnapshot | undefined {
@@ -453,81 +438,43 @@ class GitFooterCache {
453
438
  this.clock.clearInterval(this.intervalHandle);
454
439
  this.intervalHandle = undefined;
455
440
  }
456
- if (this.branchChangeUnsubscribe) {
457
- try {
458
- this.branchChangeUnsubscribe();
459
- } catch {
460
- // Ignore misbehaving notifier.
461
- }
462
- this.branchChangeUnsubscribe = undefined;
463
- }
464
441
  for (const controller of this.inflightControllers) controller.abort();
465
442
  this.inflightControllers.clear();
466
443
  }
467
444
  }
468
445
 
469
- function composeFooterFirstLine(input: {
470
- cwd: string;
471
- sessionName?: string | null;
472
- status?: GitStatusSnapshot;
473
- pullRequest?: PullRequestSnapshot;
474
- }): string {
475
- const segments = [input.cwd];
476
- if (input.status !== undefined) {
477
- segments.push(...formatGitFooterSegments(input.status, input.pullRequest));
478
- }
479
- if (input.sessionName) segments.push(input.sessionName);
480
- return segments.join(FOOTER_SEPARATOR);
481
- }
446
+ export default function (pi: ExtensionAPI) {
447
+ let cache: GitFooterCache | undefined;
482
448
 
483
- function sanitizeFooterSegment(value: string): string {
484
- return value.replace(/[\r\n\t]+/g, " ").trim();
485
- }
449
+ function disposeCache(): void {
450
+ cache?.dispose();
451
+ cache = undefined;
452
+ }
486
453
 
487
- export default function (pi: ExtensionAPI) {
488
454
  pi.on("session_start", (_event, ctx) => {
489
- let cache: GitFooterCache | undefined;
490
-
491
- ctx.ui.setFooter((tui, theme, footerData) => {
492
- cache = new GitFooterCache({
493
- cwd: () => ctx.cwd,
494
- onChange: () => tui.requestRender(),
495
- onBranchChangeSource: (callback) => footerData.onBranchChange(callback),
496
- });
455
+ disposeCache();
456
+
457
+ const updateStatus = () => {
458
+ const text = formatGitFooterStatus(
459
+ cache?.getStatusSnapshot(),
460
+ cache?.getPullRequestSnapshot(),
461
+ );
462
+ ctx.ui.setStatus(STATUS_KEY, text ? ctx.ui.theme.fg("dim", text) : undefined);
463
+ };
497
464
 
498
- return {
499
- dispose() {
500
- cache?.dispose();
501
- cache = undefined;
502
- },
503
- invalidate() {},
504
- render(width: number): string[] {
505
- const status = cache?.getStatusSnapshot();
506
- const pullRequest = cache?.getPullRequestSnapshot();
507
- const firstLine = composeFooterFirstLine({
508
- cwd: basename(ctx.cwd),
509
- status,
510
- pullRequest,
511
- sessionName: pi.getSessionName(),
512
- });
513
-
514
- const usage = ctx.getContextUsage();
515
- const context = usage?.percent == null ? "ctx ?" : `ctx ${usage.percent.toFixed(1)}%`;
516
- const thinking = pi.getThinkingLevel();
517
- const model = ctx.model?.id ?? "no-model";
518
- const modelText = thinking === "off" ? model : `${model} ${thinking}`;
519
- const statuses = [...footerData.getExtensionStatuses().values()]
520
- .map(sanitizeFooterSegment)
521
- .filter(Boolean);
522
- const secondLine = [theme.fg("dim", context), theme.fg("dim", modelText), ...statuses]
523
- .join(theme.fg("dim", FOOTER_SEPARATOR));
524
-
525
- return [
526
- truncateToWidth(theme.fg("dim", firstLine), width),
527
- truncateToWidth(secondLine, width),
528
- ];
529
- },
530
- };
465
+ cache = new GitFooterCache({
466
+ cwd: () => ctx.cwd,
467
+ onChange: updateStatus,
531
468
  });
469
+ updateStatus();
470
+ });
471
+
472
+ pi.on("turn_end", () => {
473
+ void cache?.refresh();
474
+ });
475
+
476
+ pi.on("session_shutdown", (_event, ctx) => {
477
+ disposeCache();
478
+ ctx.ui.setStatus(STATUS_KEY, undefined);
532
479
  });
533
480
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-git-footer",
3
- "version": "0.1.0",
4
- "description": "A TLH-style git status footer for pi.",
3
+ "version": "0.1.1",
4
+ "description": "A TLH-style git status add-on for pi's built-in footer.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-extensions",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "A collection of pi extensions for context management, workflow audits, review-comment triage, notifications, brrr push alerts, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, and model/provider helpers.",
5
5
  "keywords": [
6
6
  "pi-package",