pi-mega-compact 0.11.2 → 0.11.3
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/dist/extensions/dashboard-server/api-contracts/endpoints.js +9 -1
- package/dist/extensions/dashboard-server/api-contracts/provider-cache.js +7 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
- package/dist/extensions/dashboard-server/index-reader.js +22 -0
- package/dist/extensions/dashboard-server/routes-cache.js +80 -0
- package/dist/extensions/dashboard-server/routes-cache.test.js +248 -0
- package/dist/extensions/dashboard-server/routes-maintenance.js +7 -7
- package/dist/extensions/dashboard-server/routes-repo.js +24 -0
- package/dist/extensions/dashboard-server/routes.js +1 -0
- package/dist/extensions/dashboard-server/server.js +3 -1
- package/dist/extensions/mega-cache-replay.test.js +233 -24
- package/dist/extensions/mega-compact-s38.test.js +201 -6
- package/dist/extensions/mega-config.js +3 -0
- package/dist/extensions/mega-events/agent-handlers.js +23 -7
- package/dist/extensions/mega-events/context-handler.js +37 -19
- package/dist/extensions/mega-events/error-classifier.js +27 -16
- package/dist/extensions/mega-runtime/runtime-snapshot.js +14 -4
- package/dist/extensions/mega-runtime/snapshot.js +1 -1
- package/dist/extensions/mega-runtime/snapshot.test.js +127 -0
- package/dist/extensions/mega-runtime/widget.js +4 -4
- package/dist/extensions/mega-runtime/widget.test.js +103 -12
- package/dist/src/pricing.js +78 -0
- package/dist/src/store/sqlite/perf-samples.js +104 -0
- package/dist/src/store/sqlite/perf-samples.test.js +93 -1
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-Cml6YC8w.js → AchievementTiles-vEo2MtE2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-Cml6YC8w.js.map → AchievementTiles-vEo2MtE2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/AchievementsTab-DjxfYrDc.js +2 -0
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-B8NqDt7I.js.map → AchievementsTab-DjxfYrDc.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js +2 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CBYvp2mV.js → ConfigTab-Cm6ymbmx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CBYvp2mV.js.map → ConfigTab-Cm6ymbmx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-DlTJqiDm.js → EventsTab-DXWl_YzI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-DlTJqiDm.js.map → EventsTab-DXWl_YzI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-DkGTqKJW.js → GameTab-BLDudcO5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-DkGTqKJW.js.map → GameTab-BLDudcO5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js +2 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-BFo7pKM-.js → MetricsTab-Cq4u-rZF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-BFo7pKM-.js.map → MetricsTab-Cq4u-rZF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CClCUd3-.js → OverviewTab-D4kGvsiQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CClCUd3-.js.map → OverviewTab-D4kGvsiQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js +2 -0
- package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CCYd-kSz.js → SessionsTab-DZC2-GCn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CCYd-kSz.js.map → SessionsTab-DZC2-GCn.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js +2 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DRLJmVv0.js → TopicsTab-01G71hv1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DRLJmVv0.js.map → TopicsTab-01G71hv1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CLNM0T5R.js → TurnsTab-DLRg0LZT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CLNM0T5R.js.map → TurnsTab-DLRg0LZT.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/index-BEdKlpAB.css +1 -0
- package/extensions/dashboard-client/dist/assets/{index-BcoSz-L4.js → index-BX0bcgLu.js} +4 -4
- package/extensions/dashboard-client/dist/assets/{index-BcoSz-L4.js.map → index-BX0bcgLu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-Cezlih5V.js → useSSE-BBYt5UPB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-Cezlih5V.js.map → useSSE-BBYt5UPB.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/api/client.ts +6 -0
- package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +13 -1
- package/extensions/dashboard-client/src/components/ProviderCacheCard.tsx +141 -0
- package/extensions/dashboard-client/src/components/SavingsByModelTable.tsx +31 -3
- package/extensions/dashboard-client/src/main.tsx +1 -0
- package/extensions/dashboard-client/src/styles/maintenance.css +267 -0
- package/extensions/dashboard-client/src/styles/overview-extra.css +9 -0
- package/extensions/dashboard-client/src/tabs/CacheTab.tsx +83 -29
- package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +301 -136
- package/extensions/dashboard-client/src/utils/format.ts +42 -7
- package/extensions/dashboard-server/api-contracts/endpoints.ts +31 -8
- package/extensions/dashboard-server/api-contracts/index.ts +4 -0
- package/extensions/dashboard-server/api-contracts/multi-repo.ts +6 -0
- package/extensions/dashboard-server/api-contracts/provider-cache.ts +30 -0
- package/extensions/dashboard-server/api-contracts.test.ts +3 -2
- package/extensions/dashboard-server/index-reader.ts +23 -0
- package/extensions/dashboard-server/routes-cache.test.ts +302 -0
- package/extensions/dashboard-server/routes-cache.ts +123 -0
- package/extensions/dashboard-server/routes-maintenance.ts +18 -11
- package/extensions/dashboard-server/routes-repo.ts +31 -4
- package/extensions/dashboard-server/routes.ts +1 -0
- package/extensions/dashboard-server/server.ts +2 -0
- package/extensions/mega-cache-replay.test.ts +481 -166
- package/extensions/mega-compact-s38.test.ts +210 -6
- package/extensions/mega-config.ts +6 -0
- package/extensions/mega-events/agent-handlers.ts +24 -6
- package/extensions/mega-events/context-handler.ts +59 -24
- package/extensions/mega-events/error-classifier.ts +30 -13
- package/extensions/mega-runtime/runtime-snapshot.ts +31 -9
- package/extensions/mega-runtime/snapshot.test.ts +139 -0
- package/extensions/mega-runtime/snapshot.ts +6 -4
- package/extensions/mega-runtime/widget.test.ts +290 -105
- package/extensions/mega-runtime/widget.ts +29 -6
- package/package.json +1 -1
- package/src/pricing.ts +95 -0
- package/src/store/sqlite/perf-samples.test.ts +107 -3
- package/src/store/sqlite/perf-samples.ts +142 -6
- package/extensions/dashboard-client/dist/assets/AchievementsTab-B8NqDt7I.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-BCLzVL4S.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-BCLzVL4S.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js +0 -2
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-_3R4RSLZ.js +0 -2
- package/extensions/dashboard-client/dist/assets/ReposTab-_3R4RSLZ.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-0TqsrY6y.js +0 -2
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-0TqsrY6y.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-gn66lYD2.css +0 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* snapshot.test.ts — C.4 computeMegaSnapshot tests for providerCachePct
|
|
3
|
+
* and megaCacheFlare fields.
|
|
4
|
+
*
|
|
5
|
+
* Uses MEGACOMPACT_STATE_DIR + mkdtemp (G7). No pi runtime.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, before, after } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
+
import { computeMegaSnapshot, type SnapshotInput } from "./snapshot.js";
|
|
13
|
+
|
|
14
|
+
function baseInput(overrides: Partial<SnapshotInput> = {}): SnapshotInput {
|
|
15
|
+
return {
|
|
16
|
+
rtTokensSaved: 1000,
|
|
17
|
+
lastCtxPercent: null,
|
|
18
|
+
lastCtxTokens: 0,
|
|
19
|
+
lastCtxWindow: 0,
|
|
20
|
+
activeAgents: 0,
|
|
21
|
+
currentTurn: 0,
|
|
22
|
+
statusKey: undefined,
|
|
23
|
+
ready: false,
|
|
24
|
+
armed: false,
|
|
25
|
+
st: {
|
|
26
|
+
totalTokenEstimate: 800,
|
|
27
|
+
originalTokens: 1200,
|
|
28
|
+
storageDedupRate: 0.12,
|
|
29
|
+
checkpointCount: 3,
|
|
30
|
+
lastCheckpointId: undefined,
|
|
31
|
+
lastSummary: undefined,
|
|
32
|
+
injectedCount: 0,
|
|
33
|
+
dedupHitRate: 0.05,
|
|
34
|
+
tokensSaved: 200,
|
|
35
|
+
dedupAttempts: 40,
|
|
36
|
+
dedupCollapsed: 5,
|
|
37
|
+
},
|
|
38
|
+
repo: {
|
|
39
|
+
tokensSaved: 5000,
|
|
40
|
+
totalTokenEstimate: 4000,
|
|
41
|
+
checkpointCount: 9,
|
|
42
|
+
sessionCount: 2,
|
|
43
|
+
originalTokens: 6000,
|
|
44
|
+
dedupAttempts: 100,
|
|
45
|
+
dedupCollapsed: 30,
|
|
46
|
+
storageDedupRate: 0.3,
|
|
47
|
+
},
|
|
48
|
+
pressureBand: "low",
|
|
49
|
+
configTier: "default",
|
|
50
|
+
modelSnap: undefined,
|
|
51
|
+
lastCompactAt: null,
|
|
52
|
+
embedderName: () => "Trigram",
|
|
53
|
+
driftStatus: () => "ok" as const,
|
|
54
|
+
getCachedGameState: () => ({
|
|
55
|
+
game_mode_on: false,
|
|
56
|
+
theme: "transparent",
|
|
57
|
+
tui_display_mode: "full",
|
|
58
|
+
}),
|
|
59
|
+
getTurnLevel: () => 1,
|
|
60
|
+
providerCachePct: 56.2,
|
|
61
|
+
megaCacheFlare: false,
|
|
62
|
+
megaCacheFlarePct: 0,
|
|
63
|
+
levelUpFlare: false,
|
|
64
|
+
achievementFlare: false,
|
|
65
|
+
achievementFlareTitles: [],
|
|
66
|
+
activeEffect: null,
|
|
67
|
+
lastActivityAt: Date.now(),
|
|
68
|
+
ticker: [],
|
|
69
|
+
lastWhy: undefined,
|
|
70
|
+
tierTrace: undefined,
|
|
71
|
+
pulsing: false,
|
|
72
|
+
...overrides,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("computeMegaSnapshot (C.4)", () => {
|
|
77
|
+
let dir: string;
|
|
78
|
+
before(() => {
|
|
79
|
+
dir = mkdtempSync(join(tmpdir(), "mc-snap-"));
|
|
80
|
+
process.env.MEGACOMPACT_STATE_DIR = dir;
|
|
81
|
+
});
|
|
82
|
+
after(() => {
|
|
83
|
+
delete process.env.MEGACOMPACT_STATE_DIR;
|
|
84
|
+
rmSync(dir, { recursive: true, force: true });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ── C.1: providerCachePct flows into cachePct ─────────────────────────
|
|
88
|
+
it("cachePct equals providerCachePct (not dedup hit rate)", () => {
|
|
89
|
+
const res = computeMegaSnapshot(baseInput({ providerCachePct: 56.2 }));
|
|
90
|
+
assert.equal(res.widgetData.cachePct, 56.2, "cachePct is providerCachePct");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("cachePct is 0 when providerCachePct is 0", () => {
|
|
94
|
+
const res = computeMegaSnapshot(baseInput({ providerCachePct: 0 }));
|
|
95
|
+
assert.equal(res.widgetData.cachePct, 0, "cachePct is 0");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("cachePct is independent of dedup hit rate", () => {
|
|
99
|
+
const res = computeMegaSnapshot(
|
|
100
|
+
baseInput({
|
|
101
|
+
providerCachePct: 12.5,
|
|
102
|
+
st: {
|
|
103
|
+
totalTokenEstimate: 99999,
|
|
104
|
+
originalTokens: 99999,
|
|
105
|
+
storageDedupRate: 0.99,
|
|
106
|
+
checkpointCount: 1,
|
|
107
|
+
lastCheckpointId: undefined,
|
|
108
|
+
lastSummary: undefined,
|
|
109
|
+
injectedCount: 0,
|
|
110
|
+
dedupHitRate: 0.05,
|
|
111
|
+
tokensSaved: 200,
|
|
112
|
+
dedupAttempts: 40,
|
|
113
|
+
dedupCollapsed: 5,
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
assert.equal(res.widgetData.cachePct, 12.5, "cachePct ignores dedup");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ── megaCacheFlare fields pass through ────────────────────────────────
|
|
121
|
+
it("megaCacheFlare + megaCacheFlarePct pass through to widgetData", () => {
|
|
122
|
+
const res = computeMegaSnapshot(
|
|
123
|
+
baseInput({ megaCacheFlare: true, megaCacheFlarePct: 287 }),
|
|
124
|
+
);
|
|
125
|
+
assert.equal(res.widgetData.megaCacheFlare, true);
|
|
126
|
+
assert.equal(res.widgetData.megaCacheFlarePct, 287);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("megaCacheFlare is false by default", () => {
|
|
130
|
+
const res = computeMegaSnapshot(baseInput());
|
|
131
|
+
assert.equal(res.widgetData.megaCacheFlare, false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── level pass-through ────────────────────────────────────────────────
|
|
135
|
+
it("level flows from getTurnLevel", () => {
|
|
136
|
+
const res = computeMegaSnapshot(baseInput({ getTurnLevel: () => 42 }));
|
|
137
|
+
assert.equal(res.curLevel, 42);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -81,6 +81,9 @@ export interface SnapshotInput {
|
|
|
81
81
|
// Model snapshot (pre-fetched)
|
|
82
82
|
modelSnap: ModelSnapshot | undefined;
|
|
83
83
|
|
|
84
|
+
// Provider prompt cache hit rate (lifetime avg, required — callers compute once).
|
|
85
|
+
providerCachePct: number;
|
|
86
|
+
|
|
84
87
|
// Game-mode state (callers that need real-time values pass lambdas)
|
|
85
88
|
getCachedGameState: () => GameState;
|
|
86
89
|
getTurnLevel: () => number;
|
|
@@ -150,8 +153,7 @@ export function computeMegaSnapshot(p: SnapshotInput): SnapshotResult {
|
|
|
150
153
|
const ctxPct = p.lastCtxPercent != null ? p.lastCtxPercent / 100 : 0;
|
|
151
154
|
|
|
152
155
|
// ── model + provider (S26 capture) for the header ─────────────────────
|
|
153
|
-
const modelName =
|
|
154
|
-
p.modelSnap?.modelName ?? p.modelSnap?.modelId ?? "?";
|
|
156
|
+
const modelName = p.modelSnap?.modelName ?? p.modelSnap?.modelId ?? "?";
|
|
155
157
|
const modelStr = p.modelSnap?.provider
|
|
156
158
|
? `${modelName}·${p.modelSnap.provider}`
|
|
157
159
|
: modelName;
|
|
@@ -177,7 +179,7 @@ export function computeMegaSnapshot(p: SnapshotInput): SnapshotResult {
|
|
|
177
179
|
// ── S31: game-mode state ──────────────────────────────────────────────
|
|
178
180
|
const gs = p.getCachedGameState();
|
|
179
181
|
const curLevel = p.getTurnLevel();
|
|
180
|
-
const cachePct =
|
|
182
|
+
const cachePct = p.providerCachePct;
|
|
181
183
|
|
|
182
184
|
const widgetData: WidgetData = {
|
|
183
185
|
version: ownVersion(),
|
|
@@ -227,4 +229,4 @@ export function computeMegaSnapshot(p: SnapshotInput): SnapshotResult {
|
|
|
227
229
|
};
|
|
228
230
|
|
|
229
231
|
return { widgetData, curLevel };
|
|
230
|
-
}
|
|
232
|
+
}
|
|
@@ -99,6 +99,8 @@ describe("buildWidgetLines (S31)", () => {
|
|
|
99
99
|
level: 1,
|
|
100
100
|
cachePct,
|
|
101
101
|
megaCacheFlare: flare,
|
|
102
|
+
// C.3: flare gate reads megaCacheFlarePct, not cachePct
|
|
103
|
+
...(flare ? { megaCacheFlarePct: cachePct } : {}),
|
|
102
104
|
});
|
|
103
105
|
const lines = buildWidgetLines(wd, WIDTH, 0);
|
|
104
106
|
const body = contentLines(lines);
|
|
@@ -161,123 +163,306 @@ describe("buildWidgetLines (S31)", () => {
|
|
|
161
163
|
}
|
|
162
164
|
});
|
|
163
165
|
|
|
164
|
-
|
|
165
166
|
describe("buildWidgetLines ambient border effect (v0.8.3)", () => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
const effBase = (overrides: Partial<WidgetData> = {}): WidgetData =>
|
|
168
|
+
baseWd({
|
|
169
|
+
theme: DEFAULT_THEME,
|
|
170
|
+
tuiMode: "full",
|
|
171
|
+
gameMode: true,
|
|
172
|
+
level: 1,
|
|
173
|
+
cachePct: 42,
|
|
174
|
+
...overrides,
|
|
175
|
+
});
|
|
176
|
+
const isBorder = (l: string): boolean => l.includes("─");
|
|
177
|
+
|
|
178
|
+
it("activeEffect (pulse, mid-window) -> border lines carry a 256-color fg SGR", () => {
|
|
179
|
+
const ae = {
|
|
180
|
+
type: "pulse" as const,
|
|
181
|
+
role: "accent" as const,
|
|
182
|
+
startedAt: Date.now() - 250,
|
|
183
|
+
durationMs: 2000,
|
|
184
|
+
};
|
|
185
|
+
const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
|
|
186
|
+
const borders = lines.filter(isBorder);
|
|
187
|
+
assert.ok(borders.length >= 2, "has top + bottom borders");
|
|
188
|
+
for (const b of borders) {
|
|
189
|
+
assert.ok(
|
|
190
|
+
b.includes("\x1b[38;5;"),
|
|
191
|
+
`border carries 256-color fg: ${JSON.stringify(b)}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("activeEffect null -> plain borders, no 38;5 fg SGR on border lines", () => {
|
|
197
|
+
const lines = buildWidgetLines(effBase({ activeEffect: null }), WIDTH, 0);
|
|
198
|
+
const borders = lines.filter(isBorder);
|
|
199
|
+
for (const b of borders) {
|
|
200
|
+
assert.ok(
|
|
201
|
+
!b.includes("\x1b[38;5;"),
|
|
202
|
+
`no effect SGR on plain border: ${JSON.stringify(b)}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("expired activeEffect -> plain borders (per-frame expiry enforced)", () => {
|
|
208
|
+
const ae = {
|
|
209
|
+
type: "pulse" as const,
|
|
210
|
+
role: "accent" as const,
|
|
211
|
+
startedAt: Date.now() - 5000,
|
|
212
|
+
durationMs: 1000,
|
|
213
|
+
};
|
|
214
|
+
const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
|
|
215
|
+
const borders = lines.filter(isBorder);
|
|
216
|
+
for (const b of borders) {
|
|
217
|
+
assert.ok(
|
|
218
|
+
!b.includes("\x1b[38;5;"),
|
|
219
|
+
`expired effect -> plain border: ${JSON.stringify(b)}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("activeEffect border lines are width-safe (pulse, minimal + full)", () => {
|
|
225
|
+
for (const tuiMode of ["minimal", "full"] as const) {
|
|
226
|
+
const ae = {
|
|
227
|
+
type: "pulse" as const,
|
|
228
|
+
role: "mega" as const,
|
|
229
|
+
startedAt: Date.now() - 100,
|
|
230
|
+
durationMs: 2000,
|
|
231
|
+
};
|
|
232
|
+
const lines = buildWidgetLines(
|
|
233
|
+
effBase({ activeEffect: ae, tuiMode }),
|
|
234
|
+
60,
|
|
235
|
+
0,
|
|
236
|
+
);
|
|
237
|
+
for (const l of lines)
|
|
238
|
+
assert.ok(
|
|
239
|
+
visibleWidth(l) <= 60,
|
|
240
|
+
`width safe (${tuiMode}): ${visibleWidth(l)}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
170
244
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
245
|
+
it("flash effect mid-window border carries the full base index SGR", () => {
|
|
246
|
+
// Force an 'on' phase of the 120ms alternate by starting just now.
|
|
247
|
+
const ae = {
|
|
248
|
+
type: "flash" as const,
|
|
249
|
+
role: "red" as const,
|
|
250
|
+
startedAt: Date.now(),
|
|
251
|
+
durationMs: 1200,
|
|
252
|
+
};
|
|
253
|
+
const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
|
|
254
|
+
const borders = lines.filter(isBorder);
|
|
255
|
+
assert.ok(
|
|
256
|
+
borders.some((b) => b.includes("\x1b[38;5;203m")),
|
|
257
|
+
`flash-on phase uses red base 203`,
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
180
261
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
262
|
+
describe("buildWidgetLines C.3 flare gate regression", () => {
|
|
263
|
+
const flareBase = (overrides: Partial<WidgetData> = {}): WidgetData =>
|
|
264
|
+
baseWd({
|
|
265
|
+
theme: DEFAULT_THEME,
|
|
266
|
+
tuiMode: "full",
|
|
267
|
+
gameMode: true,
|
|
268
|
+
level: 1,
|
|
269
|
+
cachePct: 42,
|
|
270
|
+
...overrides,
|
|
271
|
+
});
|
|
188
272
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
273
|
+
it("flare fires when megaCacheFlarePct >= 100 even when cachePct < 100", () => {
|
|
274
|
+
// Regression: the old code read (wd.cachePct ?? 0) >= 100, so when
|
|
275
|
+
// cachePct was 56 (provider hit rate) the flare silently broke.
|
|
276
|
+
const lines = buildWidgetLines(
|
|
277
|
+
flareBase({
|
|
278
|
+
cachePct: 56,
|
|
279
|
+
megaCacheFlare: true,
|
|
280
|
+
megaCacheFlarePct: 287,
|
|
281
|
+
}),
|
|
282
|
+
WIDTH,
|
|
283
|
+
0,
|
|
284
|
+
);
|
|
285
|
+
assert.ok(
|
|
286
|
+
lines.join("\n").includes("MEGA CACHE"),
|
|
287
|
+
"flare fires from megaCacheFlarePct",
|
|
288
|
+
);
|
|
289
|
+
});
|
|
197
290
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
291
|
+
it("flare suppressed when megaCacheFlarePct < 100 even when cachePct >= 100", () => {
|
|
292
|
+
// Symmetric regression: high cachePct should NOT alone trigger flare.
|
|
293
|
+
const lines = buildWidgetLines(
|
|
294
|
+
flareBase({
|
|
295
|
+
cachePct: 150,
|
|
296
|
+
megaCacheFlare: true,
|
|
297
|
+
megaCacheFlarePct: 42,
|
|
298
|
+
}),
|
|
299
|
+
WIDTH,
|
|
300
|
+
0,
|
|
301
|
+
);
|
|
302
|
+
assert.ok(
|
|
303
|
+
!lines.join("\n").includes("MEGA CACHE"),
|
|
304
|
+
"no flare when megaCacheFlarePct below threshold",
|
|
305
|
+
);
|
|
306
|
+
});
|
|
205
307
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
308
|
+
it("flare suppressed when megaCacheFlarePct is missing (undefined)", () => {
|
|
309
|
+
const lines = buildWidgetLines(
|
|
310
|
+
flareBase({
|
|
311
|
+
cachePct: 150,
|
|
312
|
+
megaCacheFlare: true,
|
|
313
|
+
}),
|
|
314
|
+
WIDTH,
|
|
315
|
+
0,
|
|
316
|
+
);
|
|
317
|
+
assert.ok(
|
|
318
|
+
!lines.join("\n").includes("MEGA CACHE"),
|
|
319
|
+
"no flare without megaCacheFlarePct",
|
|
320
|
+
);
|
|
321
|
+
});
|
|
213
322
|
});
|
|
214
323
|
|
|
215
324
|
describe("buildWidgetLines footer stability (P1 — no 250ms rotation)", () => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
325
|
+
// P1: the L5 ticker branch used to re-pick the head text every 250ms via
|
|
326
|
+
// `step = floor(Date.now()/250)`, flipping the footer line on a 250·N ms
|
|
327
|
+
// cycle. After the fix it pins to the most-recent entry, so two renders of
|
|
328
|
+
// the SAME WidgetData at t and t+500ms (well past one rotation slot) must
|
|
329
|
+
// produce byte-identical footer lines. Proves the metronome is gone.
|
|
330
|
+
const stabBase = (overrides: Partial<WidgetData> = {}): WidgetData =>
|
|
331
|
+
baseWd({
|
|
332
|
+
theme: DEFAULT_THEME,
|
|
333
|
+
tuiMode: "full",
|
|
334
|
+
gameMode: false,
|
|
335
|
+
level: 1,
|
|
336
|
+
cachePct: 42,
|
|
337
|
+
...overrides,
|
|
338
|
+
});
|
|
224
339
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
340
|
+
it("ticker footer is byte-identical across simulated frames 500ms apart", () => {
|
|
341
|
+
const wd = stabBase({
|
|
342
|
+
ticker: [
|
|
343
|
+
{ text: "first", at: 1000 },
|
|
344
|
+
{ text: "second", at: 2000 },
|
|
345
|
+
{ text: "third", at: 3000 },
|
|
346
|
+
],
|
|
347
|
+
lastWhy: "because",
|
|
348
|
+
});
|
|
349
|
+
const realNow = Date.now;
|
|
350
|
+
try {
|
|
351
|
+
Date.now = () => 1_000_000; // t
|
|
352
|
+
const a = buildWidgetLines(wd, WIDTH, 0);
|
|
353
|
+
Date.now = () => 1_000_500; // t+500ms (would have rotated twice under the old code)
|
|
354
|
+
const b = buildWidgetLines(wd, WIDTH, 0);
|
|
355
|
+
assert.deepEqual(
|
|
356
|
+
a,
|
|
357
|
+
b,
|
|
358
|
+
"footer byte-identical across 500ms with no state change",
|
|
359
|
+
);
|
|
360
|
+
// And the pinned head is the most-recent entry (third), not a rotation:
|
|
361
|
+
assert.ok(
|
|
362
|
+
a.some((l) => l.includes("third")),
|
|
363
|
+
"shows most-recent ticker entry",
|
|
364
|
+
);
|
|
365
|
+
assert.ok(
|
|
366
|
+
a.some((l) => l.includes("(+2 more)")),
|
|
367
|
+
"(+N more) suffix preserved",
|
|
368
|
+
);
|
|
369
|
+
assert.ok(
|
|
370
|
+
a.some((l) => l.includes("because")),
|
|
371
|
+
"lastWhy preserved",
|
|
372
|
+
);
|
|
373
|
+
} finally {
|
|
374
|
+
Date.now = realNow;
|
|
375
|
+
}
|
|
376
|
+
});
|
|
249
377
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
378
|
+
it("footer still flips when the ticker actually changes (new pushTicker)", () => {
|
|
379
|
+
const realNow = Date.now;
|
|
380
|
+
try {
|
|
381
|
+
Date.now = () => 1_000_000;
|
|
382
|
+
const before = buildWidgetLines(
|
|
383
|
+
stabBase({ ticker: [{ text: "a", at: 1 }] }),
|
|
384
|
+
WIDTH,
|
|
385
|
+
0,
|
|
386
|
+
);
|
|
387
|
+
const after = buildWidgetLines(
|
|
388
|
+
stabBase({
|
|
389
|
+
ticker: [
|
|
390
|
+
{ text: "a", at: 1 },
|
|
391
|
+
{ text: "b", at: 2 },
|
|
392
|
+
],
|
|
393
|
+
}),
|
|
394
|
+
WIDTH,
|
|
395
|
+
0,
|
|
396
|
+
);
|
|
397
|
+
assert.notDeepEqual(before, after, "footer changes when ticker grows");
|
|
398
|
+
} finally {
|
|
399
|
+
Date.now = realNow;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
261
402
|
});
|
|
262
403
|
|
|
263
404
|
describe("buildWidgetLines achievement flare (S35)", () => {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
405
|
+
const achBase = (overrides: Partial<WidgetData> = {}): WidgetData =>
|
|
406
|
+
baseWd({
|
|
407
|
+
theme: DEFAULT_THEME,
|
|
408
|
+
tuiMode: "full",
|
|
409
|
+
gameMode: true,
|
|
410
|
+
level: 1,
|
|
411
|
+
cachePct: 42,
|
|
412
|
+
...overrides,
|
|
413
|
+
});
|
|
414
|
+
it("achievementFlare + titles -> renders the unlock toast line", () => {
|
|
415
|
+
const lines = buildWidgetLines(
|
|
416
|
+
achBase({
|
|
417
|
+
achievementFlare: true,
|
|
418
|
+
achievementFlareTitles: ["First Compact"],
|
|
419
|
+
}),
|
|
420
|
+
WIDTH,
|
|
421
|
+
0,
|
|
422
|
+
);
|
|
423
|
+
assert.ok(
|
|
424
|
+
lines.some((l) => l.includes("Achievement unlocked: First Compact")),
|
|
425
|
+
"toast line present",
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
it("achievementFlare off -> no toast line", () => {
|
|
429
|
+
const lines = buildWidgetLines(
|
|
430
|
+
achBase({
|
|
431
|
+
achievementFlare: false,
|
|
432
|
+
achievementFlareTitles: ["First Compact"],
|
|
433
|
+
}),
|
|
434
|
+
WIDTH,
|
|
435
|
+
0,
|
|
436
|
+
);
|
|
437
|
+
assert.ok(
|
|
438
|
+
!lines.some((l) => l.includes("Achievement unlocked")),
|
|
439
|
+
"no toast when flare off",
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
it("gameMode off -> no toast even if flare set", () => {
|
|
443
|
+
const lines = buildWidgetLines(
|
|
444
|
+
achBase({
|
|
445
|
+
gameMode: false,
|
|
446
|
+
achievementFlare: true,
|
|
447
|
+
achievementFlareTitles: ["X"],
|
|
448
|
+
}),
|
|
449
|
+
WIDTH,
|
|
450
|
+
0,
|
|
451
|
+
);
|
|
452
|
+
assert.ok(
|
|
453
|
+
!lines.some((l) => l.includes("Achievement unlocked")),
|
|
454
|
+
"no toast when game off",
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
it("achievement toast is width-safe", () => {
|
|
458
|
+
const lines = buildWidgetLines(
|
|
459
|
+
achBase({
|
|
460
|
+
achievementFlare: true,
|
|
461
|
+
achievementFlareTitles: ["First Compact", "Turn Veteran"],
|
|
462
|
+
}),
|
|
463
|
+
60,
|
|
464
|
+
0,
|
|
465
|
+
);
|
|
466
|
+
for (const l of lines) assert.ok(visibleWidth(l) <= 60, "width safe");
|
|
467
|
+
});
|
|
283
468
|
});
|
|
@@ -33,7 +33,22 @@ export {
|
|
|
33
33
|
} from "./widget-ansi.js";
|
|
34
34
|
export type { TickerEntry, WidgetData } from "./widget-types.js";
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
C,
|
|
38
|
+
PULSE,
|
|
39
|
+
DEFAULT_PANEL_BG,
|
|
40
|
+
panelBgFor,
|
|
41
|
+
themeAnsi,
|
|
42
|
+
sgrReset,
|
|
43
|
+
wrapLine,
|
|
44
|
+
panelLine,
|
|
45
|
+
panelBar,
|
|
46
|
+
effectBorderSgr,
|
|
47
|
+
effectBar,
|
|
48
|
+
fmtTokens,
|
|
49
|
+
ramp,
|
|
50
|
+
sinceCompactStr,
|
|
51
|
+
} from "./widget-ansi.js";
|
|
37
52
|
import type { WidgetData } from "./widget-types.js";
|
|
38
53
|
|
|
39
54
|
// ── buildWidgetLines ───────────────────────────────────────────────────────
|
|
@@ -70,13 +85,13 @@ export function buildWidgetLines(
|
|
|
70
85
|
// width guard + theme bg apply identically to the full panel. Level is shown
|
|
71
86
|
// only when game mode is on (otherwise just the cache %).
|
|
72
87
|
if (wd.tuiMode === "minimal") {
|
|
73
|
-
const lvl = wd.gameMode ? wd.level ?? 1 : undefined;
|
|
88
|
+
const lvl = wd.gameMode ? (wd.level ?? 1) : undefined;
|
|
74
89
|
const cachePct = wd.cachePct ?? 0;
|
|
75
90
|
const cacheStr = `${Math.round(cachePct * 10) / 10}%`;
|
|
76
91
|
const accent = themeAnsi(wd.theme, "accent");
|
|
77
92
|
const mega = themeAnsi(wd.theme, "mega");
|
|
78
93
|
const megaFlare =
|
|
79
|
-
wd.gameMode && wd.megaCacheFlare &&
|
|
94
|
+
wd.gameMode && wd.megaCacheFlare && (wd.megaCacheFlarePct ?? 0) >= 100
|
|
80
95
|
? ` ${mega}MEGA CACHE${sgrReset(mega)}`
|
|
81
96
|
: "";
|
|
82
97
|
const body =
|
|
@@ -100,7 +115,7 @@ export function buildWidgetLines(
|
|
|
100
115
|
? `${themeAnsi(wd.theme, "accent")}${wd.gameMode && wd.levelUpFlare ? "\x1b[5m" : ""}LVL ${wd.level ?? 1}${wd.gameMode && wd.levelUpFlare ? "\x1b[0m" : ""}${sgrReset(themeAnsi(wd.theme, "accent"))} `
|
|
101
116
|
: "";
|
|
102
117
|
const megaFlareSuffix =
|
|
103
|
-
wd.gameMode && wd.megaCacheFlare && (wd.
|
|
118
|
+
wd.gameMode && wd.megaCacheFlare && (wd.megaCacheFlarePct ?? 0) >= 100
|
|
104
119
|
? `${sep}${themeAnsi(wd.theme, "mega")}MEGA CACHE! (oops, you cached so hard the dedup caught fire)${sgrReset(themeAnsi(wd.theme, "mega"))}`
|
|
105
120
|
: "";
|
|
106
121
|
// Build one long content line — let terminal wrap it naturally
|
|
@@ -157,13 +172,21 @@ export function buildWidgetLines(
|
|
|
157
172
|
),
|
|
158
173
|
);
|
|
159
174
|
} else if (wd.pulsing) {
|
|
160
|
-
lines.push(
|
|
175
|
+
lines.push(
|
|
176
|
+
panelLine(` ${pulse}${C.teal}compacting…${C.reset}`, width, panelBg),
|
|
177
|
+
);
|
|
161
178
|
}
|
|
162
179
|
// S35: achievement-unlock toast (one-line, accent) -- fires for one render cycle.
|
|
163
180
|
if (wd.gameMode && wd.achievementFlare && wd.achievementFlareTitles?.length) {
|
|
164
181
|
const accentSgr = themeAnsi(wd.theme, "accent");
|
|
165
182
|
const titlesStr = wd.achievementFlareTitles.join(", ");
|
|
166
|
-
lines.push(
|
|
183
|
+
lines.push(
|
|
184
|
+
panelLine(
|
|
185
|
+
` ${accentSgr}🏆 Achievement unlocked: ${titlesStr}${sgrReset(accentSgr)}`,
|
|
186
|
+
width,
|
|
187
|
+
panelBg,
|
|
188
|
+
),
|
|
189
|
+
);
|
|
167
190
|
}
|
|
168
191
|
// bottom border
|
|
169
192
|
lines.push(effectBar(panelBar(width, "─", panelBg), borderSgr));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|