@pi-archimedes/footer 2.10.3 → 2.11.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.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A tiny change bus for the VCS facts the footer displays (git status counts,
3
+ * linked-worktree answer, jj bookmark and changed-file count).
4
+ *
5
+ * Those facts are refreshed in the background (`execFile`) so a render never
6
+ * blocks, but a background reply has no way to say "the value you painted is
7
+ * now wrong": pi draws a frame only when the host requests one, so the footer
8
+ * would keep showing the stale number until unrelated activity repaints it.
9
+ * The utils that own a cache publish here when a refresh actually changed the
10
+ * visible value, and the footer subscribes and requests a render.
11
+ *
12
+ * Listeners are notified only on a *change* — a refresh that returns the same
13
+ * value stays silent, otherwise the 2s cache TTL would drive a render every
14
+ * two seconds forever.
15
+ */
16
+
17
+ export type VcsChangeListener = () => void;
18
+
19
+ const listeners = new Set<VcsChangeListener>();
20
+
21
+ /** Subscribe to visible VCS changes. Returns an unsubscribe function. */
22
+ export function onVcsChange(listener: VcsChangeListener): () => void {
23
+ listeners.add(listener);
24
+ return () => {
25
+ listeners.delete(listener);
26
+ };
27
+ }
28
+
29
+ /** Tell subscribers the displayed VCS facts changed. Never throws. */
30
+ export function notifyVcsChange(): void {
31
+ for (const listener of [...listeners]) {
32
+ try {
33
+ listener();
34
+ } catch {
35
+ /* a listener must not break the refresh callback that published */
36
+ }
37
+ }
38
+ }
@@ -1,85 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { CostAccumulator } from "./cost-accumulator.js";
3
- import { getBus, Events } from "@pi-archimedes/core/bus";
4
-
5
- // ── globalThis cleanup ──────────────────────────────────────────────────────
6
-
7
- const BUS_KEY = Symbol.for("archimedes:bus");
8
- const QUEUE_KEY = Symbol.for("archimedes:busQueue");
9
-
10
- afterEach(() => {
11
- delete (globalThis as Record<symbol, unknown>)[BUS_KEY];
12
- delete (globalThis as Record<symbol, unknown>)[QUEUE_KEY];
13
- });
14
-
15
- // ── CostAccumulator ─────────────────────────────────────────────────────────
16
-
17
- describe("CostAccumulator", () => {
18
- let accumulator: CostAccumulator;
19
-
20
- beforeEach(() => {
21
- accumulator = new CostAccumulator();
22
- accumulator.subscribe();
23
- });
24
-
25
- it("accumulates input tokens from cost events", () => {
26
- getBus().emit(Events.COST_UPDATE, { inputTokens: 100, outputTokens: 50 });
27
- expect(accumulator.inputTokens).toBe(100);
28
- expect(accumulator.outputTokens).toBe(50);
29
- });
30
-
31
- it("accumulates cache read/write tokens", () => {
32
- getBus().emit(Events.COST_UPDATE, {
33
- cacheReadTokens: 200,
34
- cacheWriteTokens: 100,
35
- });
36
- expect(accumulator.cacheReadTokens).toBe(200);
37
- expect(accumulator.cacheWriteTokens).toBe(100);
38
- });
39
-
40
- it("accumulates cost", () => {
41
- getBus().emit(Events.COST_UPDATE, { cost: 0.015 });
42
- expect(accumulator.cost).toBe(0.015);
43
- });
44
-
45
- it("multiple cost updates accumulate correctly", () => {
46
- getBus().emit(Events.COST_UPDATE, { inputTokens: 100, cost: 0.01 });
47
- getBus().emit(Events.COST_UPDATE, { inputTokens: 200, cost: 0.02 });
48
- getBus().emit(Events.COST_UPDATE, { inputTokens: 300, cost: 0.03 });
49
- expect(accumulator.inputTokens).toBe(600);
50
- expect(accumulator.cost).toBe(0.06);
51
- });
52
-
53
- it("missing fields in payload default to 0", () => {
54
- getBus().emit(Events.COST_UPDATE, {});
55
- expect(accumulator.inputTokens).toBe(0);
56
- expect(accumulator.outputTokens).toBe(0);
57
- expect(accumulator.cacheReadTokens).toBe(0);
58
- expect(accumulator.cacheWriteTokens).toBe(0);
59
- expect(accumulator.cost).toBe(0);
60
- });
61
-
62
- it("reset zeroes all counters", () => {
63
- getBus().emit(Events.COST_UPDATE, { inputTokens: 100, cost: 0.01 });
64
- accumulator.reset();
65
- expect(accumulator.inputTokens).toBe(0);
66
- expect(accumulator.outputTokens).toBe(0);
67
- expect(accumulator.cacheReadTokens).toBe(0);
68
- expect(accumulator.cacheWriteTokens).toBe(0);
69
- expect(accumulator.cost).toBe(0);
70
- });
71
-
72
- it("dispose unsubscribes — subsequent events not accumulated", () => {
73
- getBus().emit(Events.COST_UPDATE, { inputTokens: 100 });
74
- accumulator.dispose();
75
- getBus().emit(Events.COST_UPDATE, { inputTokens: 500 });
76
- expect(accumulator.inputTokens).toBe(100);
77
- });
78
-
79
- it("subsequent events not accumulated after dispose", () => {
80
- getBus().emit(Events.COST_UPDATE, { cost: 0.01 });
81
- accumulator.dispose();
82
- getBus().emit(Events.COST_UPDATE, { cost: 0.05 });
83
- expect(accumulator.cost).toBe(0.01);
84
- });
85
- });
@@ -1,35 +0,0 @@
1
- import { getBus, Events, type CostUpdatePayload } from "@pi-archimedes/core/bus";
2
-
3
- export class CostAccumulator {
4
- inputTokens = 0;
5
- outputTokens = 0;
6
- cacheReadTokens = 0;
7
- cacheWriteTokens = 0;
8
- cost = 0;
9
- private unsubscribes: Array<() => void> = [];
10
-
11
- subscribe(): void {
12
- const unsub = getBus().on(Events.COST_UPDATE, (payload: unknown) => {
13
- const data = payload as CostUpdatePayload;
14
- this.inputTokens += data.inputTokens ?? 0;
15
- this.outputTokens += data.outputTokens ?? 0;
16
- this.cacheReadTokens += data.cacheReadTokens ?? 0;
17
- this.cacheWriteTokens += data.cacheWriteTokens ?? 0;
18
- this.cost += data.cost ?? 0;
19
- });
20
- this.unsubscribes.push(unsub);
21
- }
22
-
23
- reset(): void {
24
- this.inputTokens = 0;
25
- this.outputTokens = 0;
26
- this.cacheReadTokens = 0;
27
- this.cacheWriteTokens = 0;
28
- this.cost = 0;
29
- }
30
-
31
- dispose(): void {
32
- this.unsubscribes.forEach(unsub => unsub());
33
- this.unsubscribes = [];
34
- }
35
- }