@maxiaochao/pi-toolkit 0.2.2 → 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 +13 -1
- package/README.md +18 -0
- package/extensions/cache-export/index.ts +1 -1
- package/extensions/cache-export/render.ts +160 -21
- package/extensions/cache-export/tests/e2e-browser.mjs +295 -0
- package/extensions/cache-export/tests/run-tests.mjs +29 -1
- package/global/AGENTS.md +3 -13
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2026-09-09 - v0.2.4
|
|
2
|
+
|
|
3
|
+
- fix cache export: hover tooltips now work across segment breaks; context segments also break on model changes; add headless-browser e2e tests
|
|
4
|
+
|
|
5
|
+
## 2026-09-08 - v0.2.3
|
|
6
|
+
|
|
7
|
+
- add cache export Context segment views
|
|
8
|
+
|
|
1
9
|
## 2026-09-03 - v0.2.2
|
|
2
10
|
|
|
3
11
|
- let the focused `/btw` overlay scroll its full transcript with Up/Down and PageUp/PageDown
|
|
@@ -51,7 +59,11 @@
|
|
|
51
59
|
|
|
52
60
|
- tbd
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
## Unreleased
|
|
63
|
+
|
|
64
|
+
- let `/cache_export` switch between all history and individual compaction-defined Context segments, defaulting to the latest segment; cache-hit cumulative lines reset at compaction or model boundaries
|
|
65
|
+
- add the independent `@maxiaochao/pi-codex-edit` package with model-aware Codex-style `apply_patch`
|
|
66
|
+
- include the benchmark summary and interactive HTML architecture explainer
|
|
55
67
|
|
|
56
68
|
## 2026-08-18 - v0.1.0
|
|
57
69
|
|
package/README.md
CHANGED
|
@@ -40,6 +40,8 @@ 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 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
|
+
|
|
43
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.
|
|
44
46
|
|
|
45
47
|
The bundled `web-browser` skill provides reusable scripts for starting Chrome/Chromium with remote debugging, navigating tabs, evaluating JavaScript, emulating devices, taking screenshots, dismissing cookie dialogs, and inspecting browser logs. It auto-detects common Chrome/Chromium installations on macOS and Linux, including Windows Chrome when Pi runs under WSL; set `BROWSER_BIN` when the binary is elsewhere.
|
|
@@ -68,6 +70,22 @@ pi update npm:@maxiaochao/pi-tiny-subagent
|
|
|
68
70
|
|
|
69
71
|
See [packages/tiny-subagent/README.md](packages/tiny-subagent/README.md) for personas, model selection, child isolation, live transcript rendering, and diagnostics.
|
|
70
72
|
|
|
73
|
+
## Codex Edit
|
|
74
|
+
|
|
75
|
+
The Codex edit package is a separate npm package. Install it when you want GPT/Codex models to use the freeform `apply_patch` protocol while other models keep Pi's native `edit` tool:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pi install npm:@maxiaochao/pi-codex-edit
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
It includes the extension implementation plus the benchmark summary and interactive architecture explainer. Its versions, release workflow, and tag are independent from the root package and tiny subagent:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pi update npm:@maxiaochao/pi-codex-edit
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
See [packages/codex-edit/README.md](packages/codex-edit/README.md) for the routing rules and [packages/codex-edit/summary.md](packages/codex-edit/summary.md) for the measured tradeoffs.
|
|
88
|
+
|
|
71
89
|
## Development
|
|
72
90
|
|
|
73
91
|
Repository development and release rules are documented in [AGENTS.md](AGENTS.md).
|
|
@@ -61,7 +61,7 @@ function defaultOutputPath(ctx) {
|
|
|
61
61
|
|
|
62
62
|
export default function (pi) {
|
|
63
63
|
pi.registerCommand("cache_export", {
|
|
64
|
-
description: "Export
|
|
64
|
+
description: "Export an interactive cache dashboard (latest Context segment by default, all-history switch, per-request charts)",
|
|
65
65
|
handler: async (args, ctx) => {
|
|
66
66
|
const sessionManager = ctx.sessionManager;
|
|
67
67
|
const entries = typeof sessionManager.getBranch === "function"
|
|
@@ -71,10 +71,12 @@ export function parseEntries(entries) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// pi stores tool calls inside assistant content blocks
|
|
74
|
+
const calledTools = [];
|
|
74
75
|
for (const blk of m.content || []) {
|
|
75
76
|
if (blk && typeof blk === "object" && blk.type === "toolCall") {
|
|
76
77
|
const name = blk.tool || blk.name || "?";
|
|
77
78
|
toolCounts[name] = (toolCounts[name] || 0) + 1;
|
|
79
|
+
calledTools.push(name);
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -129,6 +131,7 @@ export function parseEntries(entries) {
|
|
|
129
131
|
stopReason: m.stopReason || "",
|
|
130
132
|
estimatedCost: cost,
|
|
131
133
|
gapTools,
|
|
134
|
+
calledTools,
|
|
132
135
|
prevOutput: requests.length ? requests[requests.length - 1].output : 0,
|
|
133
136
|
});
|
|
134
137
|
gapTools = [];
|
|
@@ -184,11 +187,12 @@ const fmtPct1 = (x) => (x * 100).toFixed(1) + "%";
|
|
|
184
187
|
// ---------------------------------------------------------------------------
|
|
185
188
|
|
|
186
189
|
function lineChart(title, series, opts = {}) {
|
|
187
|
-
const { yMax = null, percent = false, timestamps = null, events = [] } = opts;
|
|
190
|
+
const { yMax = null, percent = false, timestamps = null, events = [], breakBefore = [], requestNumbers = null } = opts;
|
|
188
191
|
const width = 900, height = 330, left = 68, right = 20, top = 42, bottom = 52;
|
|
189
192
|
const plotWidth = width - left - right, plotHeight = height - top - bottom;
|
|
190
193
|
const count = Math.max(0, ...series.map(([, v]) => v.length));
|
|
191
|
-
const observedMax = Math.max(0, ...series.
|
|
194
|
+
const observedMax = Math.max(0, ...series.flatMap(([, values]) => values.filter(Number.isFinite)));
|
|
195
|
+
const breaks = new Set(breakBefore);
|
|
192
196
|
let maximum = yMax || Math.max(observedMax, 1);
|
|
193
197
|
if (!percent) {
|
|
194
198
|
const magnitude = 10 ** Math.max(0, String(Math.trunc(maximum)).length - 1);
|
|
@@ -217,7 +221,7 @@ function lineChart(title, series, opts = {}) {
|
|
|
217
221
|
for (let index = 0; index < count; index++) {
|
|
218
222
|
if (count <= 20 || index % Math.max(1, Math.trunc(count / 12)) === 0 || index === count - 1) {
|
|
219
223
|
const [x] = point(index, 0);
|
|
220
|
-
parts.push(`<text class="tick" x="${x.toFixed(1)}" y="${height - 22}" text-anchor="middle">${index + 1}</text>`);
|
|
224
|
+
parts.push(`<text class="tick" x="${x.toFixed(1)}" y="${height - 22}" text-anchor="middle">${requestNumbers?.[index] ?? index + 1}</text>`);
|
|
221
225
|
}
|
|
222
226
|
}
|
|
223
227
|
parts.push(
|
|
@@ -240,11 +244,31 @@ function lineChart(title, series, opts = {}) {
|
|
|
240
244
|
});
|
|
241
245
|
series.forEach(([name, values], seriesIndex) => {
|
|
242
246
|
const [dark, light] = seriesColor(name);
|
|
243
|
-
const
|
|
244
|
-
|
|
247
|
+
const runs = [];
|
|
248
|
+
let run = [];
|
|
249
|
+
values.forEach((value, i) => {
|
|
250
|
+
if (breaks.has(i) || !Number.isFinite(value)) {
|
|
251
|
+
if (run.length) runs.push(run);
|
|
252
|
+
run = [];
|
|
253
|
+
}
|
|
254
|
+
if (Number.isFinite(value)) run.push(point(i, value).map((c) => c.toFixed(1)).join(","));
|
|
255
|
+
});
|
|
256
|
+
if (run.length) runs.push(run);
|
|
257
|
+
const broken = runs.map((points) =>
|
|
258
|
+
`<polyline class="series-line" data-dark="${dark}" data-light="${light}" points="${points.join(" ")}" fill="none" stroke="${dark}" stroke-width="3"/>`,
|
|
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(" ")}"/>`;
|
|
267
|
+
const labels = values.map((v) => !Number.isFinite(v) ? "N/A" : (percent ? `${(v * 100).toFixed(1)}%` : fmtInt(Math.trunc(v)))).join("|");
|
|
245
268
|
parts.push(
|
|
246
269
|
`<g class="series" data-series-id="${seriesIndex}" data-name="${esc(name, true)}" data-labels="${esc(labels, true)}">` +
|
|
247
|
-
|
|
270
|
+
full +
|
|
271
|
+
broken +
|
|
248
272
|
`<circle class="hover-point" data-dark="${dark}" data-light="${light}" r="5" fill="${dark}" visibility="hidden"/></g>`,
|
|
249
273
|
);
|
|
250
274
|
});
|
|
@@ -488,60 +512,121 @@ function renderMissPanel(miss) {
|
|
|
488
512
|
// render_usage_dashboard port
|
|
489
513
|
// ---------------------------------------------------------------------------
|
|
490
514
|
|
|
491
|
-
export function
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
515
|
+
export function buildContextSegments(requests, events) {
|
|
516
|
+
const starts = new Set(events
|
|
517
|
+
.filter((event) => (event.kind === "compaction" || event.kind === "model") && event.requestNumber >= 1 && event.requestNumber <= requests.length)
|
|
518
|
+
.map((event) => event.requestNumber));
|
|
519
|
+
const segments = [];
|
|
520
|
+
let start = 0;
|
|
521
|
+
for (let index = 1; index < requests.length; index++) {
|
|
522
|
+
if (!starts.has(requests[index].number)) continue;
|
|
523
|
+
segments.push({ number: segments.length + 1, requests: requests.slice(start, index) });
|
|
524
|
+
start = index;
|
|
495
525
|
}
|
|
526
|
+
segments.push({ number: segments.length + 1, requests: requests.slice(start) });
|
|
527
|
+
return segments;
|
|
528
|
+
}
|
|
496
529
|
|
|
530
|
+
function countToolCalls(requests) {
|
|
531
|
+
const counts = new Map();
|
|
532
|
+
for (const request of requests) {
|
|
533
|
+
for (const name of request.calledTools || []) counts.set(name, (counts.get(name) || 0) + 1);
|
|
534
|
+
}
|
|
535
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function eventsForRequests(events, requests) {
|
|
539
|
+
if (!requests.length) return [];
|
|
540
|
+
const from = requests[0].number;
|
|
541
|
+
const to = requests[requests.length - 1].number;
|
|
542
|
+
return events.filter((event) => event.requestNumber >= from && event.requestNumber <= to);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function chartEvents(events, requests) {
|
|
546
|
+
if (!requests.length) return [];
|
|
547
|
+
const first = requests[0].number;
|
|
548
|
+
return events.map((event) => ({ ...event, requestNumber: event.requestNumber - first + 1 }));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function renderDashboardView(requests, events, toolCalls, compactions, scopeLabel) {
|
|
497
552
|
const timestamps = requests.map((r) => r.timestamp);
|
|
553
|
+
const requestNumbers = requests.map((r) => r.number);
|
|
498
554
|
const cached = requests.map((r) => r.cached);
|
|
499
555
|
const cacheWrites = requests.map((r) => r.cacheWrite);
|
|
500
556
|
const fresh = requests.map((r) => r.fresh);
|
|
501
557
|
const outputs = requests.map((r) => r.output);
|
|
502
558
|
const reasoning = requests.map((r) => r.reasoning);
|
|
503
559
|
const rates = requests.map((r) => (r.fresh + r.cached + r.cacheWrite > 0 ? r.cached / (r.fresh + r.cached + r.cacheWrite) : 0));
|
|
560
|
+
const contextBreakBefore = new Set();
|
|
561
|
+
const cacheBreakBefore = new Set();
|
|
562
|
+
const resetBefore = new Set(events
|
|
563
|
+
.filter((event) => event.kind === "compaction" || event.kind === "model")
|
|
564
|
+
.map((event) => event.requestNumber));
|
|
504
565
|
const cumulativeRates = [];
|
|
505
566
|
let runCached = 0, runPrompt = 0;
|
|
506
|
-
|
|
567
|
+
let previousKey = null;
|
|
568
|
+
let cacheWindows = 1;
|
|
569
|
+
let currentWindowStart = 0;
|
|
570
|
+
for (let index = 0; index < requests.length; index++) {
|
|
571
|
+
const r = requests[index];
|
|
572
|
+
const key = `${r.provider}/${r.model}`;
|
|
573
|
+
const contextReset = index > 0 && events.some((event) => (event.kind === "compaction" || event.kind === "model") && event.requestNumber === r.number);
|
|
574
|
+
const cacheReset = index > 0 && (resetBefore.has(r.number) || (previousKey !== null && key !== previousKey));
|
|
575
|
+
if (contextReset) contextBreakBefore.add(index);
|
|
576
|
+
if (cacheReset) {
|
|
577
|
+
cacheBreakBefore.add(index);
|
|
578
|
+
cacheWindows += 1;
|
|
579
|
+
currentWindowStart = index;
|
|
580
|
+
runCached = 0;
|
|
581
|
+
runPrompt = 0;
|
|
582
|
+
}
|
|
507
583
|
runCached += r.cached;
|
|
508
584
|
runPrompt += r.fresh + r.cached + r.cacheWrite;
|
|
509
585
|
cumulativeRates.push(runPrompt > 0 ? runCached / runPrompt : 0);
|
|
586
|
+
previousKey = key;
|
|
510
587
|
}
|
|
511
588
|
|
|
512
589
|
const tot = (f) => requests.reduce((s, r) => s + f(r), 0);
|
|
513
590
|
const totalFresh = tot((r) => r.fresh), totalCached = tot((r) => r.cached),
|
|
514
591
|
totalWrite = tot((r) => r.cacheWrite), totalPrompt = totalFresh + totalCached + totalWrite,
|
|
515
592
|
totalOutput = tot((r) => r.output);
|
|
516
|
-
|
|
517
593
|
const cacheHitRate = totalPrompt > 0 && (totalCached > 0 || totalWrite > 0) ? totalCached / totalPrompt : null;
|
|
594
|
+
const windowRequests = requests.slice(currentWindowStart);
|
|
595
|
+
const windowPrompt = windowRequests.reduce((sum, r) => sum + r.fresh + r.cached + r.cacheWrite, 0);
|
|
596
|
+
const windowCached = windowRequests.reduce((sum, r) => sum + r.cached, 0);
|
|
597
|
+
const windowHitRate = windowPrompt > 0 && (windowCached > 0 || windowRequests.some((r) => r.cacheWrite > 0))
|
|
598
|
+
? windowCached / windowPrompt
|
|
599
|
+
: null;
|
|
518
600
|
|
|
519
601
|
const cards = [
|
|
602
|
+
["Scope", scopeLabel],
|
|
520
603
|
["Model requests", fmtInt(requests.length)],
|
|
521
|
-
["
|
|
604
|
+
["View cache hit rate", cacheHitRate !== null ? fmtPct1(cacheHitRate) : "N/A"],
|
|
605
|
+
["Latest cache window", `#${cacheWindows} · ${windowHitRate !== null ? fmtPct1(windowHitRate) : "N/A"}`],
|
|
522
606
|
["Cached input", fmtInt(totalCached)],
|
|
523
607
|
["Reported cache writes", fmtInt(totalWrite)],
|
|
524
608
|
["Fresh input", fmtInt(totalFresh)],
|
|
525
609
|
["Total prompt input", fmtInt(totalPrompt)],
|
|
526
610
|
["Output tokens", fmtInt(totalOutput)],
|
|
527
|
-
["Compactions", String(compactions)],
|
|
611
|
+
["Compactions in view", String(compactions)],
|
|
528
612
|
];
|
|
529
613
|
const cardsHtml = cards
|
|
530
614
|
.map(([l, v]) => `<div class="usage-card"><span>${esc(l)}</span><strong>${esc(v)}</strong></div>`)
|
|
531
615
|
.join("");
|
|
532
616
|
|
|
617
|
+
const eventMarkers = chartEvents(events, requests);
|
|
533
618
|
const charts = [
|
|
534
619
|
figure(lineChart("Prompt input by request",
|
|
535
620
|
[["cached", cached], ["cache writes", cacheWrites], ["fresh", fresh]],
|
|
536
|
-
{ timestamps, events })),
|
|
621
|
+
{ timestamps, requestNumbers, events: eventMarkers, breakBefore: contextBreakBefore })),
|
|
537
622
|
];
|
|
538
623
|
if (cacheHitRate !== null) {
|
|
539
624
|
charts.push(figure(lineChart("Cache hit rate",
|
|
540
625
|
[["request", rates], ["cumulative", cumulativeRates]],
|
|
541
|
-
{ yMax: 1.0, percent: true, timestamps })));
|
|
626
|
+
{ yMax: 1.0, percent: true, timestamps, requestNumbers, events: eventMarkers, breakBefore: cacheBreakBefore })));
|
|
542
627
|
}
|
|
543
628
|
charts.push(figure(lineChart("Output and reasoning tokens",
|
|
544
|
-
[["output", outputs], ["reasoning", reasoning]], { timestamps })));
|
|
629
|
+
[["output", outputs], ["reasoning", reasoning]], { timestamps, requestNumbers, events: eventMarkers, breakBefore: contextBreakBefore })));
|
|
545
630
|
|
|
546
631
|
const showHit = cacheHitRate !== null;
|
|
547
632
|
const rows = requests
|
|
@@ -561,9 +646,8 @@ export function renderDashboard(data) {
|
|
|
561
646
|
|
|
562
647
|
return (
|
|
563
648
|
`<div class="usage-cards">${cardsHtml}</div>` +
|
|
564
|
-
`<p class="usage-note">
|
|
565
|
-
`
|
|
566
|
-
`compactions, model or thinking changes. Some providers report cache reads but not cache writes.</p>` +
|
|
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. ` +
|
|
650
|
+
`TTL and service interruptions are shown only when recorded or inferred by the miss analysis; they do not create a Context segment.</p>` +
|
|
567
651
|
`<div class="usage-charts">${charts.join("")}</div>` +
|
|
568
652
|
renderMissPanel(analyzeMisses(requests, events)) +
|
|
569
653
|
renderStreakPanel(analyzeStreaks(requests, events)) +
|
|
@@ -577,6 +661,48 @@ export function renderDashboard(data) {
|
|
|
577
661
|
);
|
|
578
662
|
}
|
|
579
663
|
|
|
664
|
+
export function renderDashboard(data) {
|
|
665
|
+
const { requests, events, toolCalls, compactions } = data;
|
|
666
|
+
if (!requests.length) {
|
|
667
|
+
return `<p class="empty">No assistant responses with token usage were found in this session.</p>`;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const segments = buildContextSegments(requests, events);
|
|
671
|
+
const latest = segments[segments.length - 1];
|
|
672
|
+
const allView = renderDashboardView(requests, events, toolCalls, compactions, `All ${segments.length} context segment${segments.length === 1 ? "" : "s"}`);
|
|
673
|
+
const segmentViews = segments.map((segment) => {
|
|
674
|
+
const segmentEvents = eventsForRequests(events, segment.requests);
|
|
675
|
+
const startsWithCompaction = segmentEvents.some((event) => event.kind === "compaction" && event.requestNumber === segment.requests[0].number);
|
|
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)" : ""}`;
|
|
679
|
+
return {
|
|
680
|
+
id: `segment-${segment.number}`,
|
|
681
|
+
label,
|
|
682
|
+
html: renderDashboardView(
|
|
683
|
+
segment.requests,
|
|
684
|
+
segmentEvents,
|
|
685
|
+
countToolCalls(segment.requests),
|
|
686
|
+
startsWithCompaction ? 1 : 0,
|
|
687
|
+
label,
|
|
688
|
+
),
|
|
689
|
+
};
|
|
690
|
+
});
|
|
691
|
+
const options = [
|
|
692
|
+
`<option value="all">All context segments</option>`,
|
|
693
|
+
...segmentViews.map((view) => `<option value="${view.id}"${view.id === `segment-${latest.number}` ? " selected" : ""}>${esc(view.label)}</option>`),
|
|
694
|
+
].join("");
|
|
695
|
+
const views = [
|
|
696
|
+
`<section class="usage-context-view" data-context-view="all" hidden>${allView}</section>`,
|
|
697
|
+
...segmentViews.map((view) => `<section class="usage-context-view" data-context-view="${view.id}"${view.id === `segment-${latest.number}` ? "" : " hidden"}>${view.html}</section>`),
|
|
698
|
+
].join("");
|
|
699
|
+
|
|
700
|
+
return `<div class="usage-view-controls"><label for="context-segment-select">View</label>` +
|
|
701
|
+
`<select id="context-segment-select" data-context-segment-select>${options}</select>` +
|
|
702
|
+
`<span>Default: latest Context segment. All-history values remain available; cache metrics reset where compaction rewrites the prefix or the model changes.</span></div>` +
|
|
703
|
+
views;
|
|
704
|
+
}
|
|
705
|
+
|
|
580
706
|
// ---------------------------------------------------------------------------
|
|
581
707
|
// Full page
|
|
582
708
|
// ---------------------------------------------------------------------------
|
|
@@ -602,6 +728,10 @@ main { max-width: 1080px; margin: 0 auto; padding: 40px 24px 80px; }
|
|
|
602
728
|
h1 { color: var(--bright); font-size: 1.6rem; font-weight: 500; margin: 6px 0 4px; }
|
|
603
729
|
.sub { color: var(--muted); font-size: .78rem; margin: 0 0 30px; }
|
|
604
730
|
.usage-shell { margin-top: 18px; }
|
|
731
|
+
.usage-view-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 9px 12px; margin-bottom: 18px; color: var(--muted); font-size: .78rem; }
|
|
732
|
+
.usage-view-controls label { color: var(--bright); }
|
|
733
|
+
.usage-view-controls select { background: var(--surface-2); border: 1px solid var(--line-strong); border-radius: 4px; color: var(--text); font: inherit; padding: 5px 8px; }
|
|
734
|
+
.usage-context-view[hidden] { display: none; }
|
|
605
735
|
/* keep hover points visible on dark page chrome for both chart palettes */
|
|
606
736
|
.hover-point { stroke: #ffffff; stroke-width: 2; }
|
|
607
737
|
${USAGE_STYLES}
|
|
@@ -611,6 +741,15 @@ ${USAGE_STYLES}
|
|
|
611
741
|
<h1>${esc(title)}</h1>
|
|
612
742
|
<p class="sub">tau-style dashboard rendered by pi-cache-dashboard · ${n} requests · times in ${esc(tzLabel())}${opts.sessionId ? " · session " + esc(opts.sessionId) : ""}</p>
|
|
613
743
|
<section class="usage-shell" id="panel-usage">${renderDashboard(data)}</section>
|
|
614
|
-
<script>${USAGE_SCRIPT}
|
|
744
|
+
<script>${USAGE_SCRIPT}
|
|
745
|
+
for (const select of document.querySelectorAll("[data-context-segment-select]")) {
|
|
746
|
+
const shell = select.closest(".usage-shell");
|
|
747
|
+
const show = () => shell.querySelectorAll("[data-context-view]").forEach((view) => {
|
|
748
|
+
view.hidden = view.dataset.contextView !== select.value;
|
|
749
|
+
});
|
|
750
|
+
select.addEventListener("change", show);
|
|
751
|
+
show();
|
|
752
|
+
}
|
|
753
|
+
</script>
|
|
615
754
|
</main></body></html>`;
|
|
616
755
|
}
|
|
@@ -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);
|
|
@@ -4,7 +4,7 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import registerCacheExport from "../index.ts";
|
|
7
|
-
import { parseEntries, analyzeStreaks, analyzeMisses, lineChartForTest } from "../render.ts";
|
|
7
|
+
import { parseEntries, analyzeStreaks, analyzeMisses, buildContextSegments, lineChartForTest, renderDashboard } from "../render.ts";
|
|
8
8
|
|
|
9
9
|
let pass = 0, fail = 0;
|
|
10
10
|
function check(name, cond, detail = "") {
|
|
@@ -318,6 +318,34 @@ 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 break at compaction AND model switches -------------
|
|
322
|
+
{
|
|
323
|
+
const entries = [
|
|
324
|
+
req({ cached: 90000, fresh: 10000, min: 1, model: "a" }),
|
|
325
|
+
{ type: "model_change", timestamp: "2026-01-01T00:01:30Z", provider: "p", modelId: "b" },
|
|
326
|
+
req({ cached: 0, fresh: 100000, min: 2, model: "b" }),
|
|
327
|
+
{ type: "compaction", timestamp: "2026-01-01T00:02:30Z" },
|
|
328
|
+
req({ cached: 0, fresh: 20000, min: 3, model: "b" }),
|
|
329
|
+
];
|
|
330
|
+
const d = parseEntries(entries);
|
|
331
|
+
const segments = buildContextSegments(d.requests, d.events);
|
|
332
|
+
check("P2 model switch and compaction each create a Context segment",
|
|
333
|
+
segments.length === 3 && segments.every((segment) => segment.requests.length === 1),
|
|
334
|
+
JSON.stringify(segments.map((segment) => segment.requests.map((request) => request.number))));
|
|
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));
|
|
343
|
+
const dashboard = renderDashboard(d);
|
|
344
|
+
check("P2 dashboard defaults to latest segment and keeps an all-history view",
|
|
345
|
+
dashboard.includes('value="all"') && dashboard.includes('value="segment-3" selected') && dashboard.includes('data-context-view="all" hidden'),
|
|
346
|
+
dashboard.slice(0, 500));
|
|
347
|
+
}
|
|
348
|
+
|
|
321
349
|
// Q. subagent results remain visible as main-session tool input -----------
|
|
322
350
|
{
|
|
323
351
|
const entries = [
|
package/global/AGENTS.md
CHANGED
|
@@ -74,17 +74,7 @@ Before adding code to any file:
|
|
|
74
74
|
- Check shared utilities in the same module.
|
|
75
75
|
- If you're unsure why code is structured a certain way, ask before writing.
|
|
76
76
|
|
|
77
|
-
## 6.
|
|
78
|
-
|
|
79
|
-
**Multi-step work needs checkpoints. Don't keep going from a state you can't describe.**
|
|
80
|
-
|
|
81
|
-
After each significant step:
|
|
82
|
-
- Summarize what was done.
|
|
83
|
-
- State what was verified.
|
|
84
|
-
- State what remains.
|
|
85
|
-
- If you can't describe the current state back, stop and restate instead of continuing.
|
|
86
|
-
|
|
87
|
-
## 7. Fail Visibly, Not Silently
|
|
77
|
+
## 6. Fail Visibly, Not Silently
|
|
88
78
|
|
|
89
79
|
**Silent skips are failures. Surface uncertainty instead of hiding it.**
|
|
90
80
|
|
|
@@ -93,14 +83,14 @@ After each significant step:
|
|
|
93
83
|
- Default to surfacing uncertainty. When in doubt, over-communicate what you're unsure about.
|
|
94
84
|
- If a required input, file, or tool is missing (or a step is impossible under the stated constraints), do NOT fabricate it to make a check pass. Report what is missing and what you need.
|
|
95
85
|
|
|
96
|
-
##
|
|
86
|
+
## 7. Reuse Before Writing
|
|
97
87
|
|
|
98
88
|
**Before writing new code, check in order:**
|
|
99
89
|
1. Does it already exist in this codebase? Reuse it - do not re-implement it yourself, even with the standard library. (If the existing helper is itself the bug, fix it once - see Rule 3.)
|
|
100
90
|
2. Does the standard library cover it? Use it.
|
|
101
91
|
3. Only then: write the minimum code that works.
|
|
102
92
|
|
|
103
|
-
##
|
|
93
|
+
## 8. Use Available Editing Tools
|
|
104
94
|
|
|
105
95
|
**Follow the current harness's tool surface; do not assume a shell patch helper exists.**
|
|
106
96
|
|
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",
|