@ogulcancelik/pi-minimal-footer 0.1.3 → 0.1.4

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.
Files changed (3) hide show
  1. package/README.md +6 -1
  2. package/index.ts +56 -55
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,7 +11,7 @@ Minimal footer for [pi](https://github.com/badlogic/pi-mono) that replaces the d
11
11
  - **Context gauge** — working directory, git branch, model, thinking level, and context window usage with token counts
12
12
  - **Subscription usage bars** — rolling window quotas with reset timers for supported providers
13
13
  - **Auto-refresh** — fetches usage on startup and model switch, then every 5 minutes
14
- - **Git integration** — branch name, dirty state, ahead/behind counts (supports jj)
14
+ - **Git integration** — branch name, dirty state, ahead/behind counts
15
15
 
16
16
  ## Supported providers
17
17
 
@@ -39,6 +39,11 @@ Usage is fetched:
39
39
  - Immediately on model switch (Ctrl+P)
40
40
  - Every 5 minutes after that
41
41
 
42
+ Git state is refreshed:
43
+ - Once on startup
44
+ - When pi reports a branch change
45
+ - At the end of each turn
46
+
42
47
  The footer adapts to narrow terminals by stacking lines vertically instead of the single-line wide layout.
43
48
 
44
49
  ## Known issues
package/index.ts CHANGED
@@ -35,7 +35,6 @@ interface GitCache {
35
35
  dirty: boolean;
36
36
  ahead: number;
37
37
  behind: number;
38
- isJj: boolean;
39
38
  }
40
39
 
41
40
  // ============ Usage Cache ============
@@ -51,63 +50,58 @@ let codexEmailPrefix: string | null | undefined; // undefined = not yet resolved
51
50
 
52
51
  let gitCache: GitCache | null = null;
53
52
 
54
- function refreshGitCache(): void {
55
- gitCache = null;
53
+ function parseGitStatus(output: string): GitCache {
54
+ let branch: string | null = null;
55
+ let dirty = false;
56
+ let ahead = 0;
57
+ let behind = 0;
56
58
 
57
- try {
58
- const gitRoot = execSync("git rev-parse --show-toplevel 2>/dev/null", {
59
- encoding: "utf8",
60
- timeout: 500,
61
- }).trim();
62
-
63
- if (!gitRoot) return;
64
-
65
- const isJj = existsSync(`${gitRoot}/.jj`);
66
- let branch: string | null = null;
67
-
68
- if (isJj) {
69
- try {
70
- const jjBranch = execSync(
71
- `jj log -r 'heads(ancestors(@) & bookmarks())' --no-graph -T 'bookmarks.map(|b| b.name()).join(" ")' --limit 1 2>/dev/null`,
72
- { encoding: "utf8", timeout: 1000 }
73
- ).trim();
74
- branch = jjBranch ? jjBranch.split(" ")[0] : null;
75
- } catch {}
76
- } else {
77
- try {
78
- const gitBranch = execSync("git rev-parse --abbrev-ref HEAD 2>/dev/null", {
79
- encoding: "utf8",
80
- timeout: 500,
81
- }).trim();
82
- branch = gitBranch && gitBranch !== "HEAD" ? gitBranch : null;
83
- } catch {}
84
- }
85
-
86
- let dirty = false;
87
- try {
88
- const status = execSync("git status --porcelain 2>/dev/null", {
89
- encoding: "utf8",
90
- timeout: 500,
91
- });
92
- dirty = status.trim().length > 0;
93
- } catch {}
59
+ for (const line of output.split("\n")) {
60
+ if (!line) continue;
94
61
 
95
- let ahead = 0;
96
- let behind = 0;
97
- try {
98
- const counts = execSync("git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null", {
99
- encoding: "utf8",
100
- timeout: 500,
101
- }).trim();
102
- const [a, b] = counts.split(/\s+/);
103
- ahead = parseInt(a, 10) || 0;
104
- behind = parseInt(b, 10) || 0;
105
- } catch {}
62
+ if (line.startsWith("# branch.head ")) {
63
+ const head = line.slice("# branch.head ".length).trim();
64
+ branch = head && head !== "(detached)" ? head : null;
65
+ continue;
66
+ }
106
67
 
107
- gitCache = { branch, dirty, ahead, behind, isJj };
68
+ if (line.startsWith("# branch.ab ")) {
69
+ const match = line.match(/^# branch\.ab \+(\d+) -(\d+)$/);
70
+ if (match) {
71
+ ahead = parseInt(match[1], 10) || 0;
72
+ behind = parseInt(match[2], 10) || 0;
73
+ }
74
+ continue;
75
+ }
76
+
77
+ if (!line.startsWith("# ")) dirty = true;
78
+ }
79
+
80
+ return { branch, dirty, ahead, behind };
81
+ }
82
+
83
+ function sameGitCache(a: GitCache | null, b: GitCache | null): boolean {
84
+ if (a === b) return true;
85
+ if (!a || !b) return false;
86
+ return a.branch === b.branch && a.dirty === b.dirty && a.ahead === b.ahead && a.behind === b.behind;
87
+ }
88
+
89
+ function refreshGitCache(): boolean {
90
+ let next: GitCache | null = null;
91
+
92
+ try {
93
+ const status = execSync("git status --porcelain=v2 --branch 2>/dev/null", {
94
+ encoding: "utf8",
95
+ timeout: 1000,
96
+ });
97
+ next = parseGitStatus(status.trimEnd());
108
98
  } catch {
109
- gitCache = null;
99
+ next = null;
110
100
  }
101
+
102
+ const changed = !sameGitCache(gitCache, next);
103
+ gitCache = next;
104
+ return changed;
111
105
  }
112
106
 
113
107
  // ============ JWT Helpers ============
@@ -778,6 +772,10 @@ export default function (pi: ExtensionAPI) {
778
772
  // Store tui reference for triggering re-renders from event handlers
779
773
  let tuiRef: { requestRender: () => void } | null = null;
780
774
 
775
+ function refreshGitFooter(): void {
776
+ if (refreshGitCache()) tuiRef?.requestRender();
777
+ }
778
+
781
779
  /** Fetch usage for the active provider. Shows cached data immediately,
782
780
  * then fetches fresh in the background. Discards results if provider
783
781
  * changed while the fetch was in flight. */
@@ -848,8 +846,7 @@ export default function (pi: ExtensionAPI) {
848
846
  tuiRef = tui;
849
847
 
850
848
  const unsub = footerData.onBranchChange(() => {
851
- refreshGitCache();
852
- tui.requestRender();
849
+ refreshGitFooter();
853
850
  });
854
851
 
855
852
  // Initial fetch inside factory — tui is guaranteed available here,
@@ -936,6 +933,10 @@ export default function (pi: ExtensionAPI) {
936
933
 
937
934
  });
938
935
 
936
+ pi.on("turn_end", async () => {
937
+ refreshGitFooter();
938
+ });
939
+
939
940
  // Refresh when model changes — fetch immediately, restart timer
940
941
  pi.on("model_select", (event, _ctx) => {
941
942
  if (!event.model?.provider) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogulcancelik/pi-minimal-footer",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Minimal footer for pi with context gauge and subscription usage bars",
5
5
  "type": "module",
6
6
  "keywords": [