@ogulcancelik/pi-minimal-footer 0.1.2 → 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.
- package/README.md +6 -1
- package/index.ts +63 -56
- 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
|
|
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
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
}
|
|
67
|
+
|
|
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
|
+
}
|
|
106
82
|
|
|
107
|
-
|
|
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
|
-
|
|
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,12 +772,22 @@ 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. */
|
|
784
782
|
function fetchUsage(modelProvider: string): void {
|
|
785
783
|
const provider = detectProvider(modelProvider);
|
|
786
|
-
if (!provider)
|
|
784
|
+
if (!provider) {
|
|
785
|
+
activeProvider = null;
|
|
786
|
+
latestUsage = null;
|
|
787
|
+
stopRefreshTimer();
|
|
788
|
+
tuiRef?.requestRender();
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
787
791
|
|
|
788
792
|
activeProvider = provider;
|
|
789
793
|
|
|
@@ -842,8 +846,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
842
846
|
tuiRef = tui;
|
|
843
847
|
|
|
844
848
|
const unsub = footerData.onBranchChange(() => {
|
|
845
|
-
|
|
846
|
-
tui.requestRender();
|
|
849
|
+
refreshGitFooter();
|
|
847
850
|
});
|
|
848
851
|
|
|
849
852
|
// Initial fetch inside factory — tui is guaranteed available here,
|
|
@@ -930,6 +933,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
930
933
|
|
|
931
934
|
});
|
|
932
935
|
|
|
936
|
+
pi.on("turn_end", async () => {
|
|
937
|
+
refreshGitFooter();
|
|
938
|
+
});
|
|
939
|
+
|
|
933
940
|
// Refresh when model changes — fetch immediately, restart timer
|
|
934
941
|
pi.on("model_select", (event, _ctx) => {
|
|
935
942
|
if (!event.model?.provider) return;
|