pi-minimal-footer 0.5.2 → 0.6.0
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/CHANGELOG.md +27 -1
- package/README.md +30 -7
- package/VISION.md +20 -5
- package/extensions/index.ts +110 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Run Git queries only while the extension owns a live TUI footer; explicit enable resumes querying
|
|
15
|
+
- Reject Git refresh and fetch timeout settings above Node's maximum timer delay, preserving the 91- and 17-second fallbacks
|
|
16
|
+
- Replace fixed sleeps in Git fetch jitter tests with bounded waits for child completion and retry scheduling
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Keep the README user-focused; move compatibility and plugin API details to the developer guide
|
|
21
|
+
- Document the rationale for brief Git pulses and live-footer refresh ownership
|
|
22
|
+
- Pulse the whole Git section bold for one second when divergence changes, replacing the two-user-submission count highlight; preserve unsynced/synced foreground colors and cancel the pulse on disposal
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Opt-in `minFooter.gitFetch` background fetch on footer startup and every five minutes with ±⅛ jitter; silent failures, no interactive credential prompts, configurable `gitFetchTimeoutSeconds` (default 17), and cancellation on footer disposal
|
|
27
|
+
|
|
28
|
+
## [0.5.3] - 2026-10-07
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Refresh local Git divergence while Pi is idle every 91 seconds by default, configurable through `minFooter.gitRefreshSeconds`; stop polling when the footer is disposed and never fetch remote refs
|
|
33
|
+
|
|
10
34
|
## [0.5.2] - 2026-10-07
|
|
11
35
|
|
|
12
36
|
### Added
|
|
@@ -210,7 +234,9 @@ First npm-ready release.
|
|
|
210
234
|
|
|
211
235
|
- Project scaffold — `extensions/index.ts` with basic footer structure, `package.json` with pi extension manifest, `README.md`, `LICENSE` (MIT)
|
|
212
236
|
|
|
213
|
-
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.
|
|
237
|
+
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.6.0...HEAD
|
|
238
|
+
[0.6.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.3...v0.6.0
|
|
239
|
+
[0.5.3]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.2...v0.5.3
|
|
214
240
|
[0.5.2]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.1...v0.5.2
|
|
215
241
|
[0.5.1]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.0...v0.5.1
|
|
216
242
|
[0.5.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.4.2...v0.5.0
|
package/README.md
CHANGED
|
@@ -35,10 +35,7 @@ pi install npm:pi-minimal-footer
|
|
|
35
35
|
pi install npm:@ryu-cz/pi-minimal-footer
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
`@ryu-cz/pi-minimal-footer` package is an official mirror intended to improve
|
|
40
|
-
package indexing; it is not a separate extension. The scoped name is an
|
|
41
|
-
indexing workaround, not a guaranteed fix for pi.dev catalog search.
|
|
38
|
+
The scoped package is an official mirror, not a separate extension.
|
|
42
39
|
|
|
43
40
|
## Commands
|
|
44
41
|
|
|
@@ -62,6 +59,9 @@ Edit `minFooter` in Pi's agent `settings.json` (normally `~/.pi/agent/settings.j
|
|
|
62
59
|
"showModel": true,
|
|
63
60
|
"showContext": true,
|
|
64
61
|
"powerlineSeparator": true,
|
|
62
|
+
"gitRefreshSeconds": 91,
|
|
63
|
+
"gitFetch": false,
|
|
64
|
+
"gitFetchTimeoutSeconds": 17,
|
|
65
65
|
"maxUsageBarCells": 10
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -69,13 +69,16 @@ Edit `minFooter` in Pi's agent `settings.json` (normally `~/.pi/agent/settings.j
|
|
|
69
69
|
|
|
70
70
|
- `showSkills` shows **extension status text**, not installed skills. The footer does not add the example statuses itself.
|
|
71
71
|
- Set `powerlineSeparator` to `false` for plain spaces instead of `` / `` if your font lacks those glyphs. The git icon `` and sync arrows also need compatible glyphs; hide Git data with `showGitBranch: false` if needed.
|
|
72
|
+
- `gitRefreshSeconds`: local refresh interval; `1`–`2147483` seconds; default `91`.
|
|
73
|
+
- `gitFetch`: background fetch on startup, then roughly 5 minutes after each attempt completes; failures are silent; default `false`.
|
|
74
|
+
- `gitFetchTimeoutSeconds`: fetch timeout; `1`–`2147483` seconds; default `17`. Invalid timer settings use their defaults.
|
|
72
75
|
- `maxUsageBarCells` caps the Braille bar length (excluding brackets and reset time). Use a positive integer, e.g. `4` for a compact bar; invalid values fall back to `10`.
|
|
73
76
|
|
|
74
77
|
## Reading the footer
|
|
75
78
|
|
|
76
79
|
### Git
|
|
77
80
|
|
|
78
|
-
Counts require a configured upstream. Unsynced branches use
|
|
81
|
+
Counts require a configured upstream. Unsynced branches use normal text color; synced branches are dim. Count changes pulse the whole Git section bold for one second; further changes restart the pulse.
|
|
79
82
|
|
|
80
83
|
### Context
|
|
81
84
|
|
|
@@ -90,8 +93,28 @@ Counts require a configured upstream. Unsynced branches use the theme's normal f
|
|
|
90
93
|
- **Cached data:** dim after a refresh failure.
|
|
91
94
|
- Reset: local `↻HH:mm` through 24 hours; whole days/hours above 24 hours (`↻1d8h`); whole days above 10 days (`↻12d`). Remaining durations round down. No reset reported means bar only.
|
|
92
95
|
- Shows the shortest available usage window for the selected provider. Missing credentials, unsupported/local models, or absent limits hide the usage bar. Custom proxy endpoints are not polled.
|
|
96
|
+
- Virtual/routed models show model and context usage, but no subscription bar.
|
|
93
97
|
|
|
94
|
-
|
|
98
|
+
#### Adapters
|
|
99
|
+
|
|
100
|
+
Adapters use Pi's existing credentials; ordinary Claude API keys do not expose subscription usage.
|
|
101
|
+
|
|
102
|
+
*Authenticated endpoints remain unverified live; see [verification limits](VISION.md).*
|
|
103
|
+
|
|
104
|
+
Implemented adapters:
|
|
105
|
+
- **OpenAI Codex**
|
|
106
|
+
- **Claude OAuth**
|
|
107
|
+
- **GitHub Copilot**
|
|
108
|
+
- **Gemini CLI**
|
|
109
|
+
- **MiniMax**
|
|
110
|
+
- **Kimi Coding**
|
|
111
|
+
- **OpenCode Go**
|
|
112
|
+
|
|
113
|
+
### Extension statuses
|
|
114
|
+
|
|
115
|
+
Provided by other extensions; hidden with `showSkills: false`. Narrow terminals may shorten or omit them.
|
|
116
|
+
|
|
117
|
+
Only one footer extension can be active. For plugin integration, see [VISION.md](VISION.md#plugin-integration).
|
|
95
118
|
|
|
96
119
|
## Development & design
|
|
97
120
|
|
|
@@ -101,7 +124,7 @@ Install from source:
|
|
|
101
124
|
pi install git:github.com/Ryu-CZ/pi-minimal-footer
|
|
102
125
|
```
|
|
103
126
|
|
|
104
|
-
|
|
127
|
+
Developer guide: [VISION.md](VISION.md). Release history: [CHANGELOG.md](CHANGELOG.md).
|
|
105
128
|
|
|
106
129
|
## Credits
|
|
107
130
|
|
package/VISION.md
CHANGED
|
@@ -38,11 +38,12 @@ The bar and reset label have no intervening space. Reset formatting uses local c
|
|
|
38
38
|
|
|
39
39
|
## Git contract
|
|
40
40
|
|
|
41
|
-
- Pi supplies the branch name. Query `git rev-list --left-right --count HEAD...@{upstream}` for `↑N` local-only and `↓N` upstream-only commits. Use local refs;
|
|
41
|
+
- Pi supplies the branch name. Query `git rev-list --left-right --count HEAD...@{upstream}` for `↑N` local-only and `↓N` upstream-only commits. Use local refs; no remote fetch by default.
|
|
42
42
|
- Query outside rendering with a one-second timeout so Git cannot block the TUI. Missing upstreams and command failures omit counts.
|
|
43
|
-
- Refresh on branch-change signals and footer state updates. Ignore older query results to prevent stale counts from restarting highlights.
|
|
44
|
-
- Use the theme's normal text foreground while unsynced, otherwise dim. A count change
|
|
43
|
+
- Refresh on branch-change signals and footer state updates, plus every 91 seconds while the footer is active so external commits are noticed even when Pi is idle. `minFooter.gitRefreshSeconds` accepts integer seconds from 1 through 2,147,483 (Node's maximum timer delay); invalid values fall back to 91. Local-only queries permit a shorter interval than subscription usage polling without network traffic. Stop the timer on footer disposal. Ignore older query results to prevent stale counts from restarting highlights.
|
|
44
|
+
- Use the theme's normal text foreground while unsynced, otherwise dim. A count change pulses the whole Git section (icon, branch, and counts) bold for one second to call attention without persistent emphasis or turn-count bookkeeping; this is independent of synced/unsynced foreground color. Further changes restart the timer. Expiry requests a redraw even while idle. User submissions and tool turns do not affect the pulse. Dispose cancels the timer.
|
|
45
45
|
- Initial loading and working-directory changes establish an unhighlighted baseline. Zero counts hide the arrows entirely.
|
|
46
|
+
- `minFooter.gitFetch` (default `false`) opts into asynchronous `git fetch --quiet` on footer startup and every five minutes with ±⅛ jitter (262.5–337.5 seconds), sampled after each completed attempt. Jitter spreads network load across sessions; local divergence polling remains independent. Disable interactive credential prompts; failures are silent and retry at the next interval. `gitFetchTimeoutSeconds` accepts integer seconds from 1 through 2,147,483 (Node's maximum timer delay), default/fallback 17. Successful fetches refresh divergence. Footer disposal clears the timer and aborts an in-flight fetch; only interactive sessions with an active footer fetch.
|
|
46
47
|
|
|
47
48
|
## Subscription usage contract
|
|
48
49
|
|
|
@@ -79,7 +80,7 @@ The comparison with [mtrojnar/pi-usage](https://github.com/mtrojnar/pi-usage) in
|
|
|
79
80
|
|
|
80
81
|
## Local development
|
|
81
82
|
|
|
82
|
-
Requires Node.js >=22.19.0.
|
|
83
|
+
Requires Node.js >=22.19.0. Local dependencies pin Pi 1.0.3; compatibility CI tests Pi 1.0.3 and 1.1.0 on Node.js 22.19.0. Provider requests are mocked. These are tested versions, not a guarantee for every intervening or future release.
|
|
83
84
|
|
|
84
85
|
```bash
|
|
85
86
|
npm ci
|
|
@@ -98,9 +99,23 @@ ln -s "$PWD/extensions" ~/.pi/agent/extensions/minimal-footer
|
|
|
98
99
|
|
|
99
100
|
For a manual install, copy `extensions/*` into `~/.pi/agent/extensions/`.
|
|
100
101
|
|
|
102
|
+
### Plugin integration
|
|
103
|
+
|
|
104
|
+
Use Pi's standard status API to contribute text without replacing the footer:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
ctx.ui.setStatus("my-extension", "Working");
|
|
108
|
+
ctx.ui.setStatus("my-extension", "Ready"); // update the same entry
|
|
109
|
+
ctx.ui.setStatus("my-extension", undefined); // remove it
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Use a unique key. Pi requests a redraw when statuses change; no footer-specific event is needed. `showSkills` controls status visibility. Narrow terminals may truncate or omit entries to preserve model/context and usage information.
|
|
113
|
+
|
|
114
|
+
Footer ownership is exclusive: another extension calling `ctx.ui.setFooter()` replaces this footer. Usage requests, new Git queries, and footer-owned timers stop on disposal; an in-flight Git query may complete, but lifecycle state refreshes alone never start one.
|
|
115
|
+
|
|
101
116
|
### Code map
|
|
102
117
|
|
|
103
|
-
- `extensions/index.ts`: settings, state refresh, layout, footer lifecycle, and `/minfooter`.
|
|
118
|
+
- `extensions/index.ts`: settings, plain state updates, active-footer refresh gate, layout, footer lifecycle, and `/minfooter`.
|
|
104
119
|
- `extensions/lib/usage-limits.ts`: usage refresh, cache, cancellation, and bar/reset rendering.
|
|
105
120
|
- `extensions/lib/usage-providers.ts`: authentication, endpoint selection, and response normalization.
|
|
106
121
|
- `tests/`: lifecycle/layout regressions, Git highlighting with a temporary repository, and usage parsing tests.
|
package/extensions/index.ts
CHANGED
|
@@ -32,6 +32,9 @@ interface Settings {
|
|
|
32
32
|
showModel?: boolean;
|
|
33
33
|
showContext?: boolean;
|
|
34
34
|
powerlineSeparator?: boolean;
|
|
35
|
+
gitRefreshSeconds?: number;
|
|
36
|
+
gitFetch?: boolean;
|
|
37
|
+
gitFetchTimeoutSeconds?: number;
|
|
35
38
|
maxUsageBarCells?: number;
|
|
36
39
|
};
|
|
37
40
|
}
|
|
@@ -39,6 +42,8 @@ interface Settings {
|
|
|
39
42
|
type FooterSettings = NonNullable<Settings["minFooter"]>;
|
|
40
43
|
|
|
41
44
|
const execFileAsync = promisify(execFile);
|
|
45
|
+
// Node clamps timer delays above this signed 32-bit millisecond limit to 1ms.
|
|
46
|
+
const MAX_TIMER_SECONDS = Math.floor(2_147_483_647 / 1000);
|
|
42
47
|
|
|
43
48
|
const DEFAULT_SETTINGS: FooterSettings = {
|
|
44
49
|
enabled: true,
|
|
@@ -48,6 +53,9 @@ const DEFAULT_SETTINGS: FooterSettings = {
|
|
|
48
53
|
showModel: true,
|
|
49
54
|
showContext: true,
|
|
50
55
|
powerlineSeparator: true,
|
|
56
|
+
gitRefreshSeconds: 91,
|
|
57
|
+
gitFetch: false,
|
|
58
|
+
gitFetchTimeoutSeconds: 17,
|
|
51
59
|
maxUsageBarCells: MAX_BAR_CELLS,
|
|
52
60
|
};
|
|
53
61
|
|
|
@@ -81,6 +89,12 @@ function writeSettings(patch: Partial<Settings>): void {
|
|
|
81
89
|
function readConfig(): FooterSettings {
|
|
82
90
|
const s = readSettings();
|
|
83
91
|
const config = { ...DEFAULT_SETTINGS, ...s.minFooter };
|
|
92
|
+
if (typeof config.gitRefreshSeconds !== "number" || !Number.isSafeInteger(config.gitRefreshSeconds) || config.gitRefreshSeconds < 1 || config.gitRefreshSeconds > MAX_TIMER_SECONDS) {
|
|
93
|
+
config.gitRefreshSeconds = 91;
|
|
94
|
+
}
|
|
95
|
+
if (typeof config.gitFetchTimeoutSeconds !== "number" || !Number.isSafeInteger(config.gitFetchTimeoutSeconds) || config.gitFetchTimeoutSeconds < 1 || config.gitFetchTimeoutSeconds > MAX_TIMER_SECONDS) {
|
|
96
|
+
config.gitFetchTimeoutSeconds = 17;
|
|
97
|
+
}
|
|
84
98
|
if (typeof config.maxUsageBarCells !== "number" || !Number.isSafeInteger(config.maxUsageBarCells) || config.maxUsageBarCells < 1) {
|
|
85
99
|
config.maxUsageBarCells = MAX_BAR_CELLS;
|
|
86
100
|
}
|
|
@@ -117,7 +131,7 @@ interface FooterState {
|
|
|
117
131
|
context: string;
|
|
118
132
|
gitSync: string;
|
|
119
133
|
gitInitialized: boolean;
|
|
120
|
-
|
|
134
|
+
gitHighlighted: boolean;
|
|
121
135
|
gitRequest: number;
|
|
122
136
|
}
|
|
123
137
|
|
|
@@ -140,15 +154,16 @@ async function getGitSync(cwd: string): Promise<string> {
|
|
|
140
154
|
}
|
|
141
155
|
}
|
|
142
156
|
|
|
143
|
-
async function updateGitSync(state: FooterState): Promise<
|
|
157
|
+
async function updateGitSync(state: FooterState): Promise<boolean> {
|
|
144
158
|
const cwd = state.cwd;
|
|
145
159
|
const request = ++state.gitRequest;
|
|
146
160
|
const gitSync = await getGitSync(cwd);
|
|
147
161
|
// Refresh events can overlap; an older result must not restart the highlight.
|
|
148
|
-
if (state.cwd !== cwd || request !== state.gitRequest) return;
|
|
149
|
-
|
|
162
|
+
if (state.cwd !== cwd || request !== state.gitRequest) return false;
|
|
163
|
+
const changed = state.gitInitialized && state.gitSync !== gitSync;
|
|
150
164
|
state.gitSync = gitSync;
|
|
151
165
|
state.gitInitialized = true;
|
|
166
|
+
return changed;
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
function updateState(ctx: ExtensionContext, state: FooterState): void {
|
|
@@ -156,7 +171,7 @@ function updateState(ctx: ExtensionContext, state: FooterState): void {
|
|
|
156
171
|
if (state.cwd !== cwd) {
|
|
157
172
|
state.gitSync = "";
|
|
158
173
|
state.gitInitialized = false;
|
|
159
|
-
state.
|
|
174
|
+
state.gitHighlighted = false;
|
|
160
175
|
}
|
|
161
176
|
state.cwd = cwd;
|
|
162
177
|
state.model = ctx.model?.id ?? "no-model";
|
|
@@ -249,11 +264,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
249
264
|
let enabled = config.enabled !== false;
|
|
250
265
|
const state: FooterState = {
|
|
251
266
|
cwd: process.cwd(), model: "no-model", context: "?", gitSync: "",
|
|
252
|
-
gitInitialized: false,
|
|
267
|
+
gitInitialized: false, gitHighlighted: false, gitRequest: 0,
|
|
253
268
|
};
|
|
254
269
|
let requestRender: (() => void) | null = null;
|
|
255
270
|
let disposeFooter: (() => void) | null = null;
|
|
256
271
|
const usageLimits = new UsageLimits(() => requestRender?.());
|
|
272
|
+
let gitPulseTimer: ReturnType<typeof setTimeout> | null = null;
|
|
273
|
+
|
|
274
|
+
// Pulse the whole section for one second to call attention without persistent emphasis
|
|
275
|
+
// or turn-count bookkeeping; foreground color independently shows sync state.
|
|
276
|
+
// Design contract: VISION.md#git-contract.
|
|
277
|
+
async function refreshGit(): Promise<void> {
|
|
278
|
+
if (!await updateGitSync(state) || disposeFooter === null) return;
|
|
279
|
+
if (gitPulseTimer) clearTimeout(gitPulseTimer);
|
|
280
|
+
state.gitHighlighted = true;
|
|
281
|
+
gitPulseTimer = setTimeout(() => {
|
|
282
|
+
gitPulseTimer = null;
|
|
283
|
+
state.gitHighlighted = false;
|
|
284
|
+
requestRender?.();
|
|
285
|
+
}, 1000);
|
|
286
|
+
gitPulseTimer.unref?.();
|
|
287
|
+
}
|
|
257
288
|
|
|
258
289
|
function selectUsage(ctx: ExtensionContext): void {
|
|
259
290
|
// The preference may remain enabled after another extension replaces our footer.
|
|
@@ -276,20 +307,75 @@ export default function (pi: ExtensionAPI) {
|
|
|
276
307
|
// Branch changes are Pi's invalidation signal; refresh the async Git
|
|
277
308
|
// divergence separately because footer rendering must remain synchronous.
|
|
278
309
|
const unsub = footerData.onBranchChange(() => {
|
|
279
|
-
void
|
|
310
|
+
void refreshGit().then(() => tui.requestRender());
|
|
280
311
|
tui.requestRender();
|
|
281
312
|
});
|
|
282
313
|
const request = () => tui.requestRender();
|
|
283
314
|
requestRender = request;
|
|
284
315
|
let disposed = false;
|
|
316
|
+
// External commits need to become visible even when Pi is idle. Local-only
|
|
317
|
+
// queries allow a shorter interval than usage polling without network traffic.
|
|
318
|
+
const gitRefreshMs = (config.gitRefreshSeconds ?? 91) * 1000;
|
|
319
|
+
let gitTimer: ReturnType<typeof setTimeout> | null = null;
|
|
320
|
+
const scheduleGitRefresh = () => {
|
|
321
|
+
if (disposed) return;
|
|
322
|
+
gitTimer = setTimeout(() => {
|
|
323
|
+
gitTimer = null;
|
|
324
|
+
void refreshGit().then(() => tui.requestRender()).finally(scheduleGitRefresh);
|
|
325
|
+
}, gitRefreshMs);
|
|
326
|
+
// Do not keep a non-interactive process alive solely for footer refresh.
|
|
327
|
+
gitTimer.unref?.();
|
|
328
|
+
};
|
|
329
|
+
scheduleGitRefresh();
|
|
330
|
+
let fetchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
331
|
+
let fetchController: AbortController | null = null;
|
|
332
|
+
const fetchTimeoutMs = (config.gitFetchTimeoutSeconds ?? 17) * 1000;
|
|
333
|
+
const fetchGit = async () => {
|
|
334
|
+
if (disposed) return;
|
|
335
|
+
const cwd = state.cwd;
|
|
336
|
+
fetchController = new AbortController();
|
|
337
|
+
try {
|
|
338
|
+
// Never prompt for credentials in a background job. Jitter spreads
|
|
339
|
+
// network requests from concurrent Pi sessions; local polling stays independent.
|
|
340
|
+
await execFileAsync("git", ["-C", cwd, "fetch", "--quiet"], {
|
|
341
|
+
timeout: fetchTimeoutMs,
|
|
342
|
+
signal: fetchController.signal,
|
|
343
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" },
|
|
344
|
+
});
|
|
345
|
+
if (!disposed && state.cwd === cwd) {
|
|
346
|
+
await refreshGit();
|
|
347
|
+
if (!disposed) tui.requestRender();
|
|
348
|
+
}
|
|
349
|
+
} catch {
|
|
350
|
+
// Offline, authentication, timeout and cancellation failures are silent.
|
|
351
|
+
} finally {
|
|
352
|
+
fetchController = null;
|
|
353
|
+
if (!disposed) {
|
|
354
|
+
fetchTimer = setTimeout(() => {
|
|
355
|
+
fetchTimer = null;
|
|
356
|
+
void fetchGit();
|
|
357
|
+
}, 5 * 60 * 1000 * (1 + (Math.random() * 2 - 1) / 8));
|
|
358
|
+
fetchTimer.unref?.();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
if (config.gitFetch === true) void fetchGit();
|
|
285
363
|
const dispose = () => {
|
|
286
364
|
if (disposed) return;
|
|
287
365
|
disposed = true;
|
|
288
366
|
unsub();
|
|
367
|
+
if (gitTimer) clearTimeout(gitTimer);
|
|
368
|
+
gitTimer = null;
|
|
369
|
+
if (fetchTimer) clearTimeout(fetchTimer);
|
|
370
|
+
fetchTimer = null;
|
|
371
|
+
fetchController?.abort();
|
|
289
372
|
// A superseded renderer must not stop its replacement's usage polling.
|
|
290
373
|
if (requestRender === request) {
|
|
291
374
|
requestRender = null;
|
|
292
375
|
usageLimits.stop();
|
|
376
|
+
if (gitPulseTimer) clearTimeout(gitPulseTimer);
|
|
377
|
+
gitPulseTimer = null;
|
|
378
|
+
state.gitHighlighted = false;
|
|
293
379
|
}
|
|
294
380
|
if (disposeFooter === dispose) disposeFooter = null;
|
|
295
381
|
};
|
|
@@ -301,15 +387,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
301
387
|
? [...footerData.getExtensionStatuses().values()].filter((s) => s.trim())
|
|
302
388
|
: [];
|
|
303
389
|
const branch = config.showGitBranch ? footerData.getGitBranch() : null;
|
|
304
|
-
const
|
|
305
|
-
const
|
|
390
|
+
const branchText = branch ? ` ${branch}${state.gitSync}` : "";
|
|
391
|
+
const gitText = state.gitHighlighted ? theme.bold(branchText) : branchText;
|
|
306
392
|
const statusSeparator = config.powerlineSeparator ? theme.fg("dim", " ") : " ";
|
|
307
393
|
const line = buildLine(
|
|
308
394
|
width,
|
|
309
395
|
config.showPath ? theme.fg("dim", abbreviateHome(state.cwd, homedir())) : "",
|
|
310
396
|
statuses.length ? statusSeparator + statuses.map((status) => theme.fg("dim", status)).join(statusSeparator) : "",
|
|
311
|
-
//
|
|
312
|
-
branchText ? theme.fg(state.gitSync ? "text" : "dim",
|
|
397
|
+
// Pulse weight independently of the existing divergence color.
|
|
398
|
+
branchText ? theme.fg(state.gitSync ? "text" : "dim", gitText) : "",
|
|
313
399
|
config.showModel ? theme.bold(state.model) : "",
|
|
314
400
|
config.showContext ? theme.fg("dim", theme.bold(state.context)) : "",
|
|
315
401
|
(available, maxCells = BASE_BAR_CELLS) => usageLimits.line(available, theme, maxCells),
|
|
@@ -326,25 +412,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
326
412
|
selectUsage(ctx);
|
|
327
413
|
}
|
|
328
414
|
|
|
329
|
-
/**
|
|
415
|
+
/** Gate active work on a live footer so passive state updates cannot resurrect hidden Git queries. */
|
|
416
|
+
function refreshFooter(): void {
|
|
417
|
+
if (disposeFooter === null) return;
|
|
418
|
+
void refreshGit().then(() => requestRender?.());
|
|
419
|
+
requestRender?.();
|
|
420
|
+
}
|
|
421
|
+
|
|
330
422
|
function refresh(ctx: ExtensionContext): void {
|
|
331
423
|
updateState(ctx, state);
|
|
332
|
-
|
|
333
|
-
requestRender?.();
|
|
424
|
+
refreshFooter();
|
|
334
425
|
}
|
|
335
426
|
|
|
336
427
|
// ── Lifecycle events (passive; no footer re-install) ────────────────
|
|
337
428
|
|
|
338
429
|
pi.on("session_start", async (_event, ctx) => {
|
|
339
430
|
usageLimits.stop();
|
|
340
|
-
|
|
431
|
+
updateState(ctx, state);
|
|
341
432
|
install(ctx);
|
|
433
|
+
refreshFooter();
|
|
342
434
|
});
|
|
343
435
|
|
|
344
|
-
|
|
345
|
-
// Extension-generated prompts do not consume the user's highlight window.
|
|
346
|
-
pi.on("input", (event, ctx) => {
|
|
347
|
-
if (event.source !== "extension" && state.gitHighlightInputs > 0) state.gitHighlightInputs--;
|
|
436
|
+
pi.on("input", (_event, ctx) => {
|
|
348
437
|
refresh(ctx);
|
|
349
438
|
});
|
|
350
439
|
|
|
@@ -391,8 +480,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
391
480
|
else if (args === "off") enabled = false;
|
|
392
481
|
else enabled = !enabled;
|
|
393
482
|
writeEnabled(enabled);
|
|
394
|
-
|
|
483
|
+
updateState(ctx, state);
|
|
395
484
|
install(ctx);
|
|
485
|
+
refreshFooter();
|
|
396
486
|
ctx.ui.notify(`Minimal footer ${enabled ? "enabled" : "disabled"}`, "info");
|
|
397
487
|
},
|
|
398
488
|
});
|
package/package.json
CHANGED