pi-minimal-footer 0.5.3 → 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 +20 -1
- package/README.md +29 -8
- package/VISION.md +20 -5
- package/extensions/index.ts +91 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ 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
|
+
|
|
10
28
|
## [0.5.3] - 2026-10-07
|
|
11
29
|
|
|
12
30
|
### Changed
|
|
@@ -216,7 +234,8 @@ First npm-ready release.
|
|
|
216
234
|
|
|
217
235
|
- Project scaffold — `extensions/index.ts` with basic footer structure, `package.json` with pi extension manifest, `README.md`, `LICENSE` (MIT)
|
|
218
236
|
|
|
219
|
-
[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
|
|
220
239
|
[0.5.3]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.2...v0.5.3
|
|
221
240
|
[0.5.2]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.1...v0.5.2
|
|
222
241
|
[0.5.1]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.0...v0.5.1
|
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
|
|
|
@@ -63,6 +60,8 @@ Edit `minFooter` in Pi's agent `settings.json` (normally `~/.pi/agent/settings.j
|
|
|
63
60
|
"showContext": true,
|
|
64
61
|
"powerlineSeparator": true,
|
|
65
62
|
"gitRefreshSeconds": 91,
|
|
63
|
+
"gitFetch": false,
|
|
64
|
+
"gitFetchTimeoutSeconds": 17,
|
|
66
65
|
"maxUsageBarCells": 10
|
|
67
66
|
}
|
|
68
67
|
}
|
|
@@ -70,14 +69,16 @@ Edit `minFooter` in Pi's agent `settings.json` (normally `~/.pi/agent/settings.j
|
|
|
70
69
|
|
|
71
70
|
- `showSkills` shows **extension status text**, not installed skills. The footer does not add the example statuses itself.
|
|
72
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.
|
|
73
|
-
- `gitRefreshSeconds`: local
|
|
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.
|
|
74
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`.
|
|
75
76
|
|
|
76
77
|
## Reading the footer
|
|
77
78
|
|
|
78
79
|
### Git
|
|
79
80
|
|
|
80
|
-
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.
|
|
81
82
|
|
|
82
83
|
### Context
|
|
83
84
|
|
|
@@ -92,8 +93,28 @@ Counts require a configured upstream. Unsynced branches use the theme's normal f
|
|
|
92
93
|
- **Cached data:** dim after a refresh failure.
|
|
93
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.
|
|
94
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.
|
|
95
97
|
|
|
96
|
-
|
|
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).
|
|
97
118
|
|
|
98
119
|
## Development & design
|
|
99
120
|
|
|
@@ -103,7 +124,7 @@ Install from source:
|
|
|
103
124
|
pi install git:github.com/Ryu-CZ/pi-minimal-footer
|
|
104
125
|
```
|
|
105
126
|
|
|
106
|
-
|
|
127
|
+
Developer guide: [VISION.md](VISION.md). Release history: [CHANGELOG.md](CHANGELOG.md).
|
|
107
128
|
|
|
108
129
|
## Credits
|
|
109
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, plus every 91 seconds while the footer is active so external commits are noticed even when Pi is idle. `minFooter.gitRefreshSeconds` accepts
|
|
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
|
@@ -33,6 +33,8 @@ interface Settings {
|
|
|
33
33
|
showContext?: boolean;
|
|
34
34
|
powerlineSeparator?: boolean;
|
|
35
35
|
gitRefreshSeconds?: number;
|
|
36
|
+
gitFetch?: boolean;
|
|
37
|
+
gitFetchTimeoutSeconds?: number;
|
|
36
38
|
maxUsageBarCells?: number;
|
|
37
39
|
};
|
|
38
40
|
}
|
|
@@ -40,6 +42,8 @@ interface Settings {
|
|
|
40
42
|
type FooterSettings = NonNullable<Settings["minFooter"]>;
|
|
41
43
|
|
|
42
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);
|
|
43
47
|
|
|
44
48
|
const DEFAULT_SETTINGS: FooterSettings = {
|
|
45
49
|
enabled: true,
|
|
@@ -50,6 +54,8 @@ const DEFAULT_SETTINGS: FooterSettings = {
|
|
|
50
54
|
showContext: true,
|
|
51
55
|
powerlineSeparator: true,
|
|
52
56
|
gitRefreshSeconds: 91,
|
|
57
|
+
gitFetch: false,
|
|
58
|
+
gitFetchTimeoutSeconds: 17,
|
|
53
59
|
maxUsageBarCells: MAX_BAR_CELLS,
|
|
54
60
|
};
|
|
55
61
|
|
|
@@ -83,9 +89,12 @@ function writeSettings(patch: Partial<Settings>): void {
|
|
|
83
89
|
function readConfig(): FooterSettings {
|
|
84
90
|
const s = readSettings();
|
|
85
91
|
const config = { ...DEFAULT_SETTINGS, ...s.minFooter };
|
|
86
|
-
if (typeof config.gitRefreshSeconds !== "number" || !Number.isSafeInteger(config.gitRefreshSeconds) || config.gitRefreshSeconds < 1) {
|
|
92
|
+
if (typeof config.gitRefreshSeconds !== "number" || !Number.isSafeInteger(config.gitRefreshSeconds) || config.gitRefreshSeconds < 1 || config.gitRefreshSeconds > MAX_TIMER_SECONDS) {
|
|
87
93
|
config.gitRefreshSeconds = 91;
|
|
88
94
|
}
|
|
95
|
+
if (typeof config.gitFetchTimeoutSeconds !== "number" || !Number.isSafeInteger(config.gitFetchTimeoutSeconds) || config.gitFetchTimeoutSeconds < 1 || config.gitFetchTimeoutSeconds > MAX_TIMER_SECONDS) {
|
|
96
|
+
config.gitFetchTimeoutSeconds = 17;
|
|
97
|
+
}
|
|
89
98
|
if (typeof config.maxUsageBarCells !== "number" || !Number.isSafeInteger(config.maxUsageBarCells) || config.maxUsageBarCells < 1) {
|
|
90
99
|
config.maxUsageBarCells = MAX_BAR_CELLS;
|
|
91
100
|
}
|
|
@@ -122,7 +131,7 @@ interface FooterState {
|
|
|
122
131
|
context: string;
|
|
123
132
|
gitSync: string;
|
|
124
133
|
gitInitialized: boolean;
|
|
125
|
-
|
|
134
|
+
gitHighlighted: boolean;
|
|
126
135
|
gitRequest: number;
|
|
127
136
|
}
|
|
128
137
|
|
|
@@ -145,15 +154,16 @@ async function getGitSync(cwd: string): Promise<string> {
|
|
|
145
154
|
}
|
|
146
155
|
}
|
|
147
156
|
|
|
148
|
-
async function updateGitSync(state: FooterState): Promise<
|
|
157
|
+
async function updateGitSync(state: FooterState): Promise<boolean> {
|
|
149
158
|
const cwd = state.cwd;
|
|
150
159
|
const request = ++state.gitRequest;
|
|
151
160
|
const gitSync = await getGitSync(cwd);
|
|
152
161
|
// Refresh events can overlap; an older result must not restart the highlight.
|
|
153
|
-
if (state.cwd !== cwd || request !== state.gitRequest) return;
|
|
154
|
-
|
|
162
|
+
if (state.cwd !== cwd || request !== state.gitRequest) return false;
|
|
163
|
+
const changed = state.gitInitialized && state.gitSync !== gitSync;
|
|
155
164
|
state.gitSync = gitSync;
|
|
156
165
|
state.gitInitialized = true;
|
|
166
|
+
return changed;
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
function updateState(ctx: ExtensionContext, state: FooterState): void {
|
|
@@ -161,7 +171,7 @@ function updateState(ctx: ExtensionContext, state: FooterState): void {
|
|
|
161
171
|
if (state.cwd !== cwd) {
|
|
162
172
|
state.gitSync = "";
|
|
163
173
|
state.gitInitialized = false;
|
|
164
|
-
state.
|
|
174
|
+
state.gitHighlighted = false;
|
|
165
175
|
}
|
|
166
176
|
state.cwd = cwd;
|
|
167
177
|
state.model = ctx.model?.id ?? "no-model";
|
|
@@ -254,11 +264,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
254
264
|
let enabled = config.enabled !== false;
|
|
255
265
|
const state: FooterState = {
|
|
256
266
|
cwd: process.cwd(), model: "no-model", context: "?", gitSync: "",
|
|
257
|
-
gitInitialized: false,
|
|
267
|
+
gitInitialized: false, gitHighlighted: false, gitRequest: 0,
|
|
258
268
|
};
|
|
259
269
|
let requestRender: (() => void) | null = null;
|
|
260
270
|
let disposeFooter: (() => void) | null = null;
|
|
261
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
|
+
}
|
|
262
288
|
|
|
263
289
|
function selectUsage(ctx: ExtensionContext): void {
|
|
264
290
|
// The preference may remain enabled after another extension replaces our footer.
|
|
@@ -281,7 +307,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
281
307
|
// Branch changes are Pi's invalidation signal; refresh the async Git
|
|
282
308
|
// divergence separately because footer rendering must remain synchronous.
|
|
283
309
|
const unsub = footerData.onBranchChange(() => {
|
|
284
|
-
void
|
|
310
|
+
void refreshGit().then(() => tui.requestRender());
|
|
285
311
|
tui.requestRender();
|
|
286
312
|
});
|
|
287
313
|
const request = () => tui.requestRender();
|
|
@@ -295,22 +321,61 @@ export default function (pi: ExtensionAPI) {
|
|
|
295
321
|
if (disposed) return;
|
|
296
322
|
gitTimer = setTimeout(() => {
|
|
297
323
|
gitTimer = null;
|
|
298
|
-
void
|
|
324
|
+
void refreshGit().then(() => tui.requestRender()).finally(scheduleGitRefresh);
|
|
299
325
|
}, gitRefreshMs);
|
|
300
326
|
// Do not keep a non-interactive process alive solely for footer refresh.
|
|
301
327
|
gitTimer.unref?.();
|
|
302
328
|
};
|
|
303
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();
|
|
304
363
|
const dispose = () => {
|
|
305
364
|
if (disposed) return;
|
|
306
365
|
disposed = true;
|
|
307
366
|
unsub();
|
|
308
367
|
if (gitTimer) clearTimeout(gitTimer);
|
|
309
368
|
gitTimer = null;
|
|
369
|
+
if (fetchTimer) clearTimeout(fetchTimer);
|
|
370
|
+
fetchTimer = null;
|
|
371
|
+
fetchController?.abort();
|
|
310
372
|
// A superseded renderer must not stop its replacement's usage polling.
|
|
311
373
|
if (requestRender === request) {
|
|
312
374
|
requestRender = null;
|
|
313
375
|
usageLimits.stop();
|
|
376
|
+
if (gitPulseTimer) clearTimeout(gitPulseTimer);
|
|
377
|
+
gitPulseTimer = null;
|
|
378
|
+
state.gitHighlighted = false;
|
|
314
379
|
}
|
|
315
380
|
if (disposeFooter === dispose) disposeFooter = null;
|
|
316
381
|
};
|
|
@@ -322,15 +387,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
322
387
|
? [...footerData.getExtensionStatuses().values()].filter((s) => s.trim())
|
|
323
388
|
: [];
|
|
324
389
|
const branch = config.showGitBranch ? footerData.getGitBranch() : null;
|
|
325
|
-
const
|
|
326
|
-
const
|
|
390
|
+
const branchText = branch ? ` ${branch}${state.gitSync}` : "";
|
|
391
|
+
const gitText = state.gitHighlighted ? theme.bold(branchText) : branchText;
|
|
327
392
|
const statusSeparator = config.powerlineSeparator ? theme.fg("dim", " ") : " ";
|
|
328
393
|
const line = buildLine(
|
|
329
394
|
width,
|
|
330
395
|
config.showPath ? theme.fg("dim", abbreviateHome(state.cwd, homedir())) : "",
|
|
331
396
|
statuses.length ? statusSeparator + statuses.map((status) => theme.fg("dim", status)).join(statusSeparator) : "",
|
|
332
|
-
//
|
|
333
|
-
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) : "",
|
|
334
399
|
config.showModel ? theme.bold(state.model) : "",
|
|
335
400
|
config.showContext ? theme.fg("dim", theme.bold(state.context)) : "",
|
|
336
401
|
(available, maxCells = BASE_BAR_CELLS) => usageLimits.line(available, theme, maxCells),
|
|
@@ -347,25 +412,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
347
412
|
selectUsage(ctx);
|
|
348
413
|
}
|
|
349
414
|
|
|
350
|
-
/**
|
|
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
|
+
|
|
351
422
|
function refresh(ctx: ExtensionContext): void {
|
|
352
423
|
updateState(ctx, state);
|
|
353
|
-
|
|
354
|
-
requestRender?.();
|
|
424
|
+
refreshFooter();
|
|
355
425
|
}
|
|
356
426
|
|
|
357
427
|
// ── Lifecycle events (passive; no footer re-install) ────────────────
|
|
358
428
|
|
|
359
429
|
pi.on("session_start", async (_event, ctx) => {
|
|
360
430
|
usageLimits.stop();
|
|
361
|
-
|
|
431
|
+
updateState(ctx, state);
|
|
362
432
|
install(ctx);
|
|
433
|
+
refreshFooter();
|
|
363
434
|
});
|
|
364
435
|
|
|
365
|
-
|
|
366
|
-
// Extension-generated prompts do not consume the user's highlight window.
|
|
367
|
-
pi.on("input", (event, ctx) => {
|
|
368
|
-
if (event.source !== "extension" && state.gitHighlightInputs > 0) state.gitHighlightInputs--;
|
|
436
|
+
pi.on("input", (_event, ctx) => {
|
|
369
437
|
refresh(ctx);
|
|
370
438
|
});
|
|
371
439
|
|
|
@@ -412,8 +480,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
412
480
|
else if (args === "off") enabled = false;
|
|
413
481
|
else enabled = !enabled;
|
|
414
482
|
writeEnabled(enabled);
|
|
415
|
-
|
|
483
|
+
updateState(ctx, state);
|
|
416
484
|
install(ctx);
|
|
485
|
+
refreshFooter();
|
|
417
486
|
ctx.ui.notify(`Minimal footer ${enabled ? "enabled" : "disabled"}`, "info");
|
|
418
487
|
},
|
|
419
488
|
});
|
package/package.json
CHANGED