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 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.5.2...HEAD
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
- `pi-minimal-footer` remains the original, unscoped release path. The scoped
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 the theme's normal foreground; synced branches are dim. Changed counts stay bold until two subsequent user messages; further changes restart the highlight.
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
- Implemented adapters: **OpenAI Codex, Claude OAuth, GitHub Copilot, Gemini CLI, MiniMax, Kimi Coding, and OpenCode Go**. Uses Pi's existing credentials; ordinary Claude API keys do not expose subscription usage. Authenticated endpoints remain unverified live; see [verification limits](VISION.md).
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
- See [VISION.md](VISION.md) for design constraints, local development, provider details, and verification limits. Release history: [CHANGELOG.md](CHANGELOG.md).
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; never fetch automatically.
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 bolds the arrows/counts until two subsequent user submissions; further changes restart the window. Tool turns and extension prompts do not consume it.
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. Tests currently use Pi 1.0.3 and mock provider requests.
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.
@@ -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
- gitHighlightInputs: number;
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<void> {
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
- if (state.gitInitialized && state.gitSync !== gitSync) state.gitHighlightInputs = 2;
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.gitHighlightInputs = 0;
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, gitHighlightInputs: 0, gitRequest: 0,
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 updateGitSync(state).then(() => tui.requestRender());
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 syncText = state.gitHighlightInputs > 0 ? theme.bold(state.gitSync) : state.gitSync;
305
- const branchText = branch ? `${branch}${syncText}` : "";
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
- // Keep unsynced branches readable; bold counts separately mark recent changes.
312
- branchText ? theme.fg(state.gitSync ? "text" : "dim", ` ${branchText}`) : "",
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
- /** Cheap refresh: update plain state and request one render. */
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
- void updateGitSync(state).then(() => requestRender?.());
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
- refresh(ctx);
431
+ updateState(ctx, state);
341
432
  install(ctx);
433
+ refreshFooter();
342
434
  });
343
435
 
344
- // Count user submissions, not turn_end: one prompt can trigger many tool turns.
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
- refresh(ctx);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-minimal-footer",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "An opinionated minimal footer extension for Pi — clean status line with path, extension statuses, model, and context usage.",
5
5
  "keywords": [
6
6
  "pi-package",