@maxiaochao/pi-toolkit 0.2.3 → 0.2.4
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
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ bash scripts/install.sh
|
|
|
40
40
|
|
|
41
41
|
The `/btw` side chat is a separate agent session. It can inspect the main conversation and repository, and its thread stays out of the main context until you close it and choose to inject a summary. Use `/btw` to open it or `/btw <question>` to ask directly. While the BTW overlay has focus, use `Up` / `Down` to scroll its transcript one line at a time and `PageUp` / `PageDown` to move by one transcript page.
|
|
42
42
|
|
|
43
|
-
Use `/cache_export [path]` to write an interactive per-request cache dashboard. It defaults to the latest
|
|
43
|
+
Use `/cache_export [path]` to write an interactive per-request cache dashboard. It defaults to the latest Context segment for current-state diagnosis, and the View selector can show any earlier segment or all session history. Compaction and model changes both break context charts into segments; cache-hit accumulation also resets at model changes, while TTL and other inferred interruptions remain diagnostic events rather than segment boundaries.
|
|
44
44
|
|
|
45
45
|
The bundled `nightowl` theme is available to Pi through the package manifest and is selected automatically on npm installation only when no theme is already configured.
|
|
46
46
|
|
|
@@ -254,13 +254,21 @@ function lineChart(title, series, opts = {}) {
|
|
|
254
254
|
if (Number.isFinite(value)) run.push(point(i, value).map((c) => c.toFixed(1)).join(","));
|
|
255
255
|
});
|
|
256
256
|
if (run.length) runs.push(run);
|
|
257
|
-
const
|
|
257
|
+
const broken = runs.map((points) =>
|
|
258
258
|
`<polyline class="series-line" data-dark="${dark}" data-light="${light}" points="${points.join(" ")}" fill="none" stroke="${dark}" stroke-width="3"/>`,
|
|
259
259
|
).join("");
|
|
260
|
+
// tau's tooltip script reads `series.querySelector(".series-line").points.getItem(i)`,
|
|
261
|
+
// which returns only the FIRST polyline; with break splits that one is short and
|
|
262
|
+
// getItem(i) throws IndexSizeError past the first run, killing the tooltip. Keep a
|
|
263
|
+
// hidden full-coverage `.series-line` first in the group so the tooltip works at
|
|
264
|
+
// every index while the visible broken polylines still draw the gaps.
|
|
265
|
+
const full = `<polyline class="series-line" data-dark="${dark}" data-light="${light}" visibility="hidden" fill="none" stroke="${dark}" stroke-width="3" ` +
|
|
266
|
+
`points="${values.map((v, i) => point(i, Number.isFinite(v) ? v : 0).map((c) => c.toFixed(1)).join(",")).join(" ")}"/>`;
|
|
260
267
|
const labels = values.map((v) => !Number.isFinite(v) ? "N/A" : (percent ? `${(v * 100).toFixed(1)}%` : fmtInt(Math.trunc(v)))).join("|");
|
|
261
268
|
parts.push(
|
|
262
269
|
`<g class="series" data-series-id="${seriesIndex}" data-name="${esc(name, true)}" data-labels="${esc(labels, true)}">` +
|
|
263
|
-
|
|
270
|
+
full +
|
|
271
|
+
broken +
|
|
264
272
|
`<circle class="hover-point" data-dark="${dark}" data-light="${light}" r="5" fill="${dark}" visibility="hidden"/></g>`,
|
|
265
273
|
);
|
|
266
274
|
});
|
|
@@ -506,7 +514,7 @@ function renderMissPanel(miss) {
|
|
|
506
514
|
|
|
507
515
|
export function buildContextSegments(requests, events) {
|
|
508
516
|
const starts = new Set(events
|
|
509
|
-
.filter((event) => event.kind === "compaction" && event.requestNumber >= 1 && event.requestNumber <= requests.length)
|
|
517
|
+
.filter((event) => (event.kind === "compaction" || event.kind === "model") && event.requestNumber >= 1 && event.requestNumber <= requests.length)
|
|
510
518
|
.map((event) => event.requestNumber));
|
|
511
519
|
const segments = [];
|
|
512
520
|
let start = 0;
|
|
@@ -562,7 +570,7 @@ function renderDashboardView(requests, events, toolCalls, compactions, scopeLabe
|
|
|
562
570
|
for (let index = 0; index < requests.length; index++) {
|
|
563
571
|
const r = requests[index];
|
|
564
572
|
const key = `${r.provider}/${r.model}`;
|
|
565
|
-
const contextReset = index > 0 && events.some((event) => event.kind === "compaction" && event.requestNumber === r.number);
|
|
573
|
+
const contextReset = index > 0 && events.some((event) => (event.kind === "compaction" || event.kind === "model") && event.requestNumber === r.number);
|
|
566
574
|
const cacheReset = index > 0 && (resetBefore.has(r.number) || (previousKey !== null && key !== previousKey));
|
|
567
575
|
if (contextReset) contextBreakBefore.add(index);
|
|
568
576
|
if (cacheReset) {
|
|
@@ -638,7 +646,7 @@ function renderDashboardView(requests, events, toolCalls, compactions, scopeLabe
|
|
|
638
646
|
|
|
639
647
|
return (
|
|
640
648
|
`<div class="usage-cards">${cardsHtml}</div>` +
|
|
641
|
-
`<p class="usage-note">Prompt and output lines break at a Context segment boundary (compaction). Cache-window cumulative hit rate also resets at a model change. ` +
|
|
649
|
+
`<p class="usage-note">Prompt and output lines break at a Context segment boundary (compaction or model change). Cache-window cumulative hit rate also resets at a model change. ` +
|
|
642
650
|
`TTL and service interruptions are shown only when recorded or inferred by the miss analysis; they do not create a Context segment.</p>` +
|
|
643
651
|
`<div class="usage-charts">${charts.join("")}</div>` +
|
|
644
652
|
renderMissPanel(analyzeMisses(requests, events)) +
|
|
@@ -665,7 +673,9 @@ export function renderDashboard(data) {
|
|
|
665
673
|
const segmentViews = segments.map((segment) => {
|
|
666
674
|
const segmentEvents = eventsForRequests(events, segment.requests);
|
|
667
675
|
const startsWithCompaction = segmentEvents.some((event) => event.kind === "compaction" && event.requestNumber === segment.requests[0].number);
|
|
668
|
-
const
|
|
676
|
+
const first = segment.requests[0];
|
|
677
|
+
const model = first ? `${first.provider}/${first.model}` : "";
|
|
678
|
+
const label = `Context segment ${segment.number}${model ? ` · ${model}` : ""}${segment.number === latest.number ? " (latest)" : ""}`;
|
|
669
679
|
return {
|
|
670
680
|
id: `segment-${segment.number}`,
|
|
671
681
|
label,
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// Real-browser end-to-end test for the cache dashboard.
|
|
2
|
+
// Drives actual pointer events in headless Chromium via raw CDP (Node 22
|
|
3
|
+
// built-ins only: fetch + WebSocket) and asserts the hover tooltip keeps
|
|
4
|
+
// working across polyline break boundaries and inside segment views.
|
|
5
|
+
//
|
|
6
|
+
// Chrome discovery mirrors the web-browser skill (BROWSER_BIN, common Linux
|
|
7
|
+
// paths, Playwright cache, WSL Windows Chrome). If no browser is found the
|
|
8
|
+
// test prints SKIP and exits 0 so CI without Chrome still passes; if a
|
|
9
|
+
// browser IS found, failing assertions exit 1.
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/e2e-browser.mjs (from extensions/cache-export)
|
|
12
|
+
|
|
13
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
14
|
+
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
15
|
+
import net from "node:net";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { parseEntries, buildCacheExportHtml } from "../render.ts";
|
|
19
|
+
|
|
20
|
+
let pass = 0, fail = 0, skipped = 0;
|
|
21
|
+
function check(name, cond, detail = "") {
|
|
22
|
+
if (cond) { pass++; console.log(` PASS ${name}`); }
|
|
23
|
+
else { fail++; console.log(` FAIL ${name} ${detail}`); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Chrome discovery (mirrors the web-browser skill) ---------------------
|
|
27
|
+
|
|
28
|
+
const IS_WSL = Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
|
|
29
|
+
|
|
30
|
+
function commandOutput(command, args) {
|
|
31
|
+
return execFileSync(command, args, { encoding: "utf8" }).trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function findChrome() {
|
|
35
|
+
if (process.env.BROWSER_BIN && existsSync(process.env.BROWSER_BIN)) return process.env.BROWSER_BIN;
|
|
36
|
+
// Prefer Linux binaries (also run fine under WSL2 and CI). Windows Chrome is
|
|
37
|
+
// a last resort: its launch interacts with any already-running Chrome
|
|
38
|
+
// singleton and can silently skip binding the debugging port.
|
|
39
|
+
const candidates = [
|
|
40
|
+
"/usr/bin/google-chrome", "/usr/bin/google-chrome-stable",
|
|
41
|
+
"/usr/bin/chromium", "/usr/bin/chromium-browser",
|
|
42
|
+
];
|
|
43
|
+
for (const p of candidates) if (existsSync(p)) return p;
|
|
44
|
+
// Playwright-managed Chromium builds
|
|
45
|
+
const pwRoot = join(process.env.HOME || tmpdir(), ".cache", "ms-playwright");
|
|
46
|
+
if (existsSync(pwRoot)) {
|
|
47
|
+
for (const dir of readdirSync(pwRoot)) {
|
|
48
|
+
const p = join(pwRoot, dir, "chrome-linux", "chrome");
|
|
49
|
+
if (existsSync(p)) return p;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const p of [
|
|
53
|
+
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
54
|
+
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
55
|
+
]) if (existsSync(p)) return p;
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- tiny CDP client ------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
async function cdpConnect(wsUrl, timeoutMs = 5000) {
|
|
62
|
+
const ws = new WebSocket(wsUrl);
|
|
63
|
+
const pending = new Map();
|
|
64
|
+
let nextId = 1;
|
|
65
|
+
await new Promise((resolve, reject) => {
|
|
66
|
+
const timer = setTimeout(() => reject(new Error("ws connect timeout")), timeoutMs);
|
|
67
|
+
ws.onopen = () => { clearTimeout(timer); resolve(); };
|
|
68
|
+
ws.onerror = () => { clearTimeout(timer); reject(new Error("ws connect error")); };
|
|
69
|
+
});
|
|
70
|
+
ws.onmessage = (event) => {
|
|
71
|
+
const msg = JSON.parse(event.data);
|
|
72
|
+
if (msg.id && pending.has(msg.id)) {
|
|
73
|
+
const { resolve, reject } = pending.get(msg.id);
|
|
74
|
+
pending.delete(msg.id);
|
|
75
|
+
if (msg.error) reject(new Error(msg.error.message));
|
|
76
|
+
else resolve(msg.result);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
send(method, params = {}) {
|
|
81
|
+
const id = nextId++;
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
pending.set(id, { resolve, reject });
|
|
84
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
close() { ws.close(); },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function freePort() {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
const srv = net.createServer();
|
|
94
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
95
|
+
const port = srv.address().port;
|
|
96
|
+
srv.close(() => resolve(port));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function waitForEndpoint(port, timeoutMs = 15000) {
|
|
102
|
+
const deadline = Date.now() + timeoutMs;
|
|
103
|
+
while (Date.now() < deadline) {
|
|
104
|
+
try {
|
|
105
|
+
const resp = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
106
|
+
if (resp.ok) return true;
|
|
107
|
+
} catch { /* not up yet */ }
|
|
108
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- fixture --------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
function entry(i, model) {
|
|
116
|
+
const mm = String(i % 60).padStart(2, "0");
|
|
117
|
+
const ss = String(Math.floor(i / 60)).padStart(2, "0");
|
|
118
|
+
return {
|
|
119
|
+
type: "message",
|
|
120
|
+
timestamp: `2026-02-01T00:${mm}:${ss}Z`,
|
|
121
|
+
message: {
|
|
122
|
+
role: "assistant", provider: "p", model, api: "openai-responses",
|
|
123
|
+
stopReason: "stop", content: [],
|
|
124
|
+
usage: { input: 2000, cacheRead: i % 7 === 0 ? 3000 : 90000, cacheWrite: 0, output: 200 },
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function fixtureEntries() {
|
|
130
|
+
const entries = [];
|
|
131
|
+
for (let i = 1; i <= 24; i++) entries.push(entry(i, "m1"));
|
|
132
|
+
entries.push({ type: "model_change", timestamp: "2026-02-01T00:24:30Z", provider: "p", modelId: "m2" });
|
|
133
|
+
entries.push({ type: "compaction", timestamp: "2026-02-01T00:24:45Z" });
|
|
134
|
+
for (let i = 25; i <= 32; i++) entries.push(entry(i, "m2"));
|
|
135
|
+
entries.push({ type: "model_change", timestamp: "2026-02-01T00:32:30Z", provider: "p", modelId: "m3" });
|
|
136
|
+
for (let i = 33; i <= 39; i++) entries.push(entry(i, "m3"));
|
|
137
|
+
entries.push({ type: "compaction", timestamp: "2026-02-01T00:39:30Z" });
|
|
138
|
+
for (let i = 40; i <= 45; i++) entries.push(entry(i, "m3"));
|
|
139
|
+
return entries;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const HOVER_SCRIPT = `
|
|
143
|
+
(() => {
|
|
144
|
+
window.__errs = [];
|
|
145
|
+
window.addEventListener("error", (e) => __errs.push(String(e.message)));
|
|
146
|
+
const sel = document.querySelector("[data-context-segment-select]");
|
|
147
|
+
function setView(value) {
|
|
148
|
+
sel.value = value; sel.dispatchEvent(new Event("change"));
|
|
149
|
+
}
|
|
150
|
+
function hover(label, frac) {
|
|
151
|
+
// Only the chart inside the currently visible context view can be hovered.
|
|
152
|
+
const chart = [...document.querySelectorAll("svg.usage-chart")]
|
|
153
|
+
.find((s) => s.getAttribute("aria-label") === label &&
|
|
154
|
+
s.closest(".usage-context-view") && !s.closest(".usage-context-view").hidden);
|
|
155
|
+
if (!chart) return { visible: false, text: "no visible chart" };
|
|
156
|
+
const r = chart.getBoundingClientRect();
|
|
157
|
+
chart.dispatchEvent(new PointerEvent("pointermove", {
|
|
158
|
+
clientX: r.left + 30 + r.width * frac, clientY: r.top + r.height / 2, bubbles: true,
|
|
159
|
+
}));
|
|
160
|
+
const t = document.querySelector(".usage-tooltip");
|
|
161
|
+
return { visible: getComputedStyle(t).display !== "none", text: t.textContent || "" };
|
|
162
|
+
}
|
|
163
|
+
const requestOf = (r) => {
|
|
164
|
+
const m = (r.text || "").match(/Request (\\d+)/);
|
|
165
|
+
return m ? Number(m[1]) : null;
|
|
166
|
+
};
|
|
167
|
+
window.__probe = { setView, hover, requestOf, errs: () => window.__errs };
|
|
168
|
+
return "ready";
|
|
169
|
+
})();
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
async function run() {
|
|
173
|
+
const html = buildCacheExportHtml(parseEntries(fixtureEntries()), { title: "e2e fixture", sessionId: "e2e" });
|
|
174
|
+
|
|
175
|
+
const chrome = findChrome();
|
|
176
|
+
if (!chrome) {
|
|
177
|
+
console.log(" SKIP browser e2e (no Chrome/Chromium found; set BROWSER_BIN to enable)");
|
|
178
|
+
skipped++;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const usesWindowsChrome = IS_WSL && /\.exe$/i.test(chrome);
|
|
182
|
+
let windowsPid = 0;
|
|
183
|
+
const port = await freePort();
|
|
184
|
+
const userDataDir = usesWindowsChrome
|
|
185
|
+
? commandOutput("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
|
|
186
|
+
"[Environment]::GetFolderPath('LocalApplicationData')"]) + `\\agent-web\\e2e-${process.pid}`
|
|
187
|
+
: mkdtempSync(join(tmpdir(), "cache-export-e2e-"));
|
|
188
|
+
|
|
189
|
+
const chromeArgs = [
|
|
190
|
+
`--remote-debugging-port=${port}`,
|
|
191
|
+
`--user-data-dir=${userDataDir}`,
|
|
192
|
+
"--headless", "--disable-graphics", "--no-sandbox", "--disable-gpu",
|
|
193
|
+
"--no-first-run", "--no-default-browser-check", "--disable-extensions",
|
|
194
|
+
];
|
|
195
|
+
if (usesWindowsChrome) {
|
|
196
|
+
const winPath = commandOutput("wslpath", ["-w", chrome]);
|
|
197
|
+
const quoted = chromeArgs.map((a) => `'${a.replaceAll("'", "''")}'`).join(", ");
|
|
198
|
+
const out = commandOutput("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
|
|
199
|
+
`$p = Start-Process -FilePath '${winPath}' -ArgumentList @(${quoted}) -PassThru; $p.Id`]);
|
|
200
|
+
windowsPid = Number(out.trim());
|
|
201
|
+
} else {
|
|
202
|
+
spawn(chrome, chromeArgs, { stdio: "ignore", detached: true }).unref();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
if (!(await waitForEndpoint(port))) {
|
|
207
|
+
check("browser endpoint comes up", false, "chrome did not expose :" + port);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json();
|
|
211
|
+
const page = list.find((t) => t.type === "page");
|
|
212
|
+
const cdp = await cdpConnect(page.webSocketDebuggerUrl);
|
|
213
|
+
await cdp.send("Page.enable");
|
|
214
|
+
await cdp.send("Emulation.setDeviceMetricsOverride", { width: 1600, height: 1200, deviceScaleFactor: 1, mobile: false });
|
|
215
|
+
await cdp.send("Page.navigate", { url: "data:text/html;charset=utf-8," + encodeURIComponent(html) });
|
|
216
|
+
await cdp.send("Runtime.enable");
|
|
217
|
+
|
|
218
|
+
const evalJs = async (expression) => {
|
|
219
|
+
const res = await cdp.send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
|
|
220
|
+
if (res.exceptionDetails) throw new Error(res.exceptionDetails.text || "eval failed");
|
|
221
|
+
return res.result.value;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// wait for load + the dashboard IIFE
|
|
225
|
+
for (let i = 0; i < 40; i++) {
|
|
226
|
+
if (await evalJs(`document.readyState === "complete" && !!document.querySelector(".usage-chart")`)) break;
|
|
227
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
228
|
+
}
|
|
229
|
+
await evalJs(HOVER_SCRIPT);
|
|
230
|
+
|
|
231
|
+
// 1. segmentation: model switches create context segments
|
|
232
|
+
const segInfo = await evalJs(`
|
|
233
|
+
(() => {
|
|
234
|
+
const opts = [...document.querySelectorAll("[data-context-segment-select] option")]
|
|
235
|
+
.map((o) => ({ value: o.value, text: o.textContent }));
|
|
236
|
+
return { count: opts.length - 1, selected: document.querySelector("[data-context-segment-select]").value, opts };
|
|
237
|
+
})()
|
|
238
|
+
`);
|
|
239
|
+
check("e2e segments split at compactions AND model switches (4 expected)",
|
|
240
|
+
segInfo.count === 4, JSON.stringify(segInfo.opts.map((o) => o.text)));
|
|
241
|
+
check("e2e segment labels carry the model", segInfo.opts.some((o) => o.text.includes("· p/m2")), JSON.stringify(segInfo.opts));
|
|
242
|
+
check("e2e defaults to the latest segment", segInfo.selected === "segment-4", segInfo.selected);
|
|
243
|
+
|
|
244
|
+
// 2. All view: tooltip must work past every break, on every chart
|
|
245
|
+
await evalJs(`window.__probe.setView("all")`);
|
|
246
|
+
const allCharts = await evalJs(`
|
|
247
|
+
(() => {
|
|
248
|
+
const labels = [...document.querySelectorAll("svg.usage-chart")].map((s) => s.getAttribute("aria-label"));
|
|
249
|
+
const out = {};
|
|
250
|
+
for (const label of labels) {
|
|
251
|
+
const near = window.__probe.hover(label, 0.05);
|
|
252
|
+
const far = window.__probe.hover(label, 0.9);
|
|
253
|
+
out[label] = { near, far, nearReq: window.__probe.requestOf(near), farReq: window.__probe.requestOf(far) };
|
|
254
|
+
}
|
|
255
|
+
return { out, errs: window.__probe.errs() };
|
|
256
|
+
})()
|
|
257
|
+
`);
|
|
258
|
+
const labels = Object.keys(allCharts.out);
|
|
259
|
+
check("e2e All view has 3 charts", labels.length === 3, labels.join(","));
|
|
260
|
+
for (const label of labels) {
|
|
261
|
+
const { near, far, nearReq, farReq } = allCharts.out[label];
|
|
262
|
+
check(`e2e All "${label}" tooltip visible near start`, near.visible && nearReq !== null, JSON.stringify(near));
|
|
263
|
+
check(`e2e All "${label}" tooltip works past the last break`, far.visible && farReq !== null && farReq > 39, JSON.stringify(far));
|
|
264
|
+
check(`e2e All "${label}" tooltip tracks the cursor across breaks`, farReq !== nearReq, `${farReq} vs ${nearReq}`);
|
|
265
|
+
}
|
|
266
|
+
check("e2e no IndexSizeError / uncaught errors in All view",
|
|
267
|
+
!allCharts.errs.some((e) => /IndexSizeError|Uncaught/i.test(e)), JSON.stringify(allCharts.errs));
|
|
268
|
+
|
|
269
|
+
// 3. latest segment view (no internal breaks): full-width tooltips
|
|
270
|
+
await evalJs(`window.__probe.setView("segment-4")`);
|
|
271
|
+
const seg4 = await evalJs(`
|
|
272
|
+
(() => {
|
|
273
|
+
const near = window.__probe.hover("Prompt input by request", 0.05);
|
|
274
|
+
const far = window.__probe.hover("Prompt input by request", 0.9);
|
|
275
|
+
return { near, far, nearReq: window.__probe.requestOf(near), farReq: window.__probe.requestOf(far), errs: window.__probe.errs() };
|
|
276
|
+
})()
|
|
277
|
+
`);
|
|
278
|
+
check("e2e segment-4 tooltip works at start and end",
|
|
279
|
+
seg4.near.visible && seg4.far.visible && seg4.farReq !== seg4.nearReq,
|
|
280
|
+
JSON.stringify(seg4));
|
|
281
|
+
check("e2e segment view has no uncaught errors",
|
|
282
|
+
!seg4.errs.some((e) => /IndexSizeError|Uncaught/i.test(e)), JSON.stringify(seg4.errs));
|
|
283
|
+
|
|
284
|
+
cdp.close();
|
|
285
|
+
if (usesWindowsChrome && windowsPid) {
|
|
286
|
+
try { execFileSync("taskkill.exe", ["/PID", String(windowsPid), "/T", "/F"]); } catch { /* best effort */ }
|
|
287
|
+
}
|
|
288
|
+
} finally {
|
|
289
|
+
if (!usesWindowsChrome) rmSync(userDataDir, { recursive: true, force: true });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await run();
|
|
294
|
+
console.log(`\n${pass} passed, ${fail} failed, ${skipped} skipped (browser e2e)`);
|
|
295
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -318,7 +318,7 @@ const hitOf = (r) => r.cached / (r.fresh + r.cached + r.cacheWrite);
|
|
|
318
318
|
s.length === 2 && s.every((x) => x.hits.length === 3), JSON.stringify(s.map((x) => [x.from, x.to])));
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
// P2. Context segments
|
|
321
|
+
// P2. Context segments break at compaction AND model switches -------------
|
|
322
322
|
{
|
|
323
323
|
const entries = [
|
|
324
324
|
req({ cached: 90000, fresh: 10000, min: 1, model: "a" }),
|
|
@@ -329,14 +329,20 @@ const hitOf = (r) => r.cached / (r.fresh + r.cached + r.cacheWrite);
|
|
|
329
329
|
];
|
|
330
330
|
const d = parseEntries(entries);
|
|
331
331
|
const segments = buildContextSegments(d.requests, d.events);
|
|
332
|
-
check("P2 model switch
|
|
333
|
-
segments.length ===
|
|
332
|
+
check("P2 model switch and compaction each create a Context segment",
|
|
333
|
+
segments.length === 3 && segments.every((segment) => segment.requests.length === 1),
|
|
334
334
|
JSON.stringify(segments.map((segment) => segment.requests.map((request) => request.number))));
|
|
335
|
-
const chart = lineChartForTest("segmented", [["cached", [10, 20, 30]]], { breakBefore: new Set([2]) });
|
|
336
|
-
|
|
335
|
+
const chart = lineChartForTest("segmented", [["cached", [10, 20, 30, 40]]], { breakBefore: new Set([2]) });
|
|
336
|
+
const polys = [...chart.matchAll(/<polyline ([^>]*)points="([^"]*)"/g)].map((m) => [m[1], m[2]]);
|
|
337
|
+
check("P2 chart boundary splits the visible polyline and keeps a hidden full one first (tooltip fix)",
|
|
338
|
+
polys.length === 3 &&
|
|
339
|
+
polys[0][0].includes('visibility="hidden"') && polys[0][1].split(" ").length === 4 &&
|
|
340
|
+
!polys[1][0].includes("visibility") && polys[1][1].split(" ").length === 2 &&
|
|
341
|
+
!polys[2][0].includes("visibility") && polys[2][1].split(" ").length === 2,
|
|
342
|
+
chart.slice(0, 900));
|
|
337
343
|
const dashboard = renderDashboard(d);
|
|
338
344
|
check("P2 dashboard defaults to latest segment and keeps an all-history view",
|
|
339
|
-
dashboard.includes('value="all"') && dashboard.includes('value="segment-
|
|
345
|
+
dashboard.includes('value="all"') && dashboard.includes('value="segment-3" selected') && dashboard.includes('data-context-view="all" hidden'),
|
|
340
346
|
dashboard.slice(0, 500));
|
|
341
347
|
}
|
|
342
348
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxiaochao/pi-toolkit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Personal pi coding agent toolkit: /todos, /btw, /cache_export extensions, browser automation, and bundled theme/skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"ws": "^8.19.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
|
-
"test": "cd extensions/cache-export && node --experimental-strip-types tests/run-tests.mjs && node --experimental-strip-types ../tests/btw-history.mjs",
|
|
46
|
+
"test": "cd extensions/cache-export && node --experimental-strip-types tests/run-tests.mjs && node --experimental-strip-types ../tests/btw-history.mjs && node tests/e2e-browser.mjs",
|
|
47
47
|
"load-test": "pi -ne -e ./extensions/todo.ts && pi -ne -e ./extensions/cache-export/index.ts && pi -ne -e ./extensions/btw.ts",
|
|
48
48
|
"eval:agents": "node evaluations/agents-md/run.mjs",
|
|
49
49
|
"eval:agents:test": "node evaluations/agents-md/tests/run-tests.mjs",
|