pi-context-map 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/extensions/generator.ts +90 -11
- package/extensions/index.ts +19 -5
- package/extensions/live-server.ts +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-06-15
|
|
4
|
+
### Features
|
|
5
|
+
- **Dark mode toggle**: Light/dark theme switcher in the report header. Preference saved to localStorage and restored on next load. Theme persists across SSE live updates.
|
|
6
|
+
- **Dynamic context window**: Replaced hardcoded 128k with actual context window size from Pi system via `ctx.getContextUsage()`. Now accurately reflects your model's real context limit (200k, 128k, etc.).
|
|
7
|
+
|
|
8
|
+
## [0.4.4] - 2026-06-15
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
- **Fixed SSE rendering**: Changed `document.replaceChild(document.documentElement)` to `document.body.innerHTML` replacement. CSS and JavaScript no longer render as visible text on the page.
|
|
11
|
+
- **Singleton server**: `start()` now kills any pre-existing server instance before starting a new one. Only one localhost server runs at a time — no duplicate ports.
|
|
12
|
+
|
|
3
13
|
## [0.4.3] - 2026-06-15
|
|
4
14
|
### Apple-Inspired HTML Redesign
|
|
5
15
|
- **Complete visual overhaul**: Report now uses Apple design language — white canvas, Inter font (SF Pro substitute), Action Blue (#0066cc) accent, 18px card radius, pill-shaped inputs.
|
package/extensions/generator.ts
CHANGED
|
@@ -14,10 +14,10 @@ export class ReportGenerator {
|
|
|
14
14
|
public static generateHTML(
|
|
15
15
|
composition: ContextComposition,
|
|
16
16
|
insights: Insight[],
|
|
17
|
+
contextWindow: number = 128_000,
|
|
17
18
|
): string {
|
|
18
19
|
const total = composition.total.tokens;
|
|
19
|
-
const usagePercent =
|
|
20
|
-
total > 0 ? Math.round((total / 128_000) * 100) : 0;
|
|
20
|
+
const usagePercent = total > 0 ? Math.round((total / contextWindow) * 100) : 0;
|
|
21
21
|
|
|
22
22
|
const fileCards = composition.files_detail
|
|
23
23
|
.map(
|
|
@@ -91,6 +91,26 @@ export class ReportGenerator {
|
|
|
91
91
|
--seg-files: #007aff;
|
|
92
92
|
--seg-summaries: #34c759;
|
|
93
93
|
}
|
|
94
|
+
[data-theme="dark"] {
|
|
95
|
+
--canvas: #0a0a0b;
|
|
96
|
+
--canvas-alt: #141415;
|
|
97
|
+
--surface: #1a1a1c;
|
|
98
|
+
--hairline: #2c2c2e;
|
|
99
|
+
--hairline-soft: rgba(255,255,255,0.06);
|
|
100
|
+
--ink: #f5f5f7;
|
|
101
|
+
--ink-secondary: #a1a1a6;
|
|
102
|
+
--ink-tertiary: #6e6e73;
|
|
103
|
+
--ink-quaternary: #48484a;
|
|
104
|
+
--accent: #2997ff;
|
|
105
|
+
--accent-hover: #40a9ff;
|
|
106
|
+
--accent-soft: rgba(41,151,255,0.12);
|
|
107
|
+
--success: #30d158;
|
|
108
|
+
--success-soft: rgba(48,209,88,0.12);
|
|
109
|
+
--warning: #ff9f0a;
|
|
110
|
+
--warning-soft: rgba(255,159,10,0.12);
|
|
111
|
+
--danger: #ff453a;
|
|
112
|
+
--danger-soft: rgba(255,69,58,0.12);
|
|
113
|
+
}
|
|
94
114
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
95
115
|
body {
|
|
96
116
|
background: var(--canvas);
|
|
@@ -483,8 +503,15 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
483
503
|
<div class="container">
|
|
484
504
|
|
|
485
505
|
<header>
|
|
486
|
-
|
|
487
|
-
|
|
506
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;flex-wrap:wrap;">
|
|
507
|
+
<div class="live-badge"><span class="dot"></span>Live</div>
|
|
508
|
+
<button id="themeToggle" style="display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border:1px solid var(--hairline);border-radius:20px;background:var(--surface);color:var(--ink-secondary);font:inherit;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s;white-space:nowrap;" aria-label="Toggle theme">
|
|
509
|
+
<svg id="themeIconSun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
|
510
|
+
<svg id="themeIconMoon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
511
|
+
<span id="themeLabel">Dark</span>
|
|
512
|
+
</button>
|
|
513
|
+
</div>
|
|
514
|
+
<h1>Context Profiler</h1>
|
|
488
515
|
<p class="subtitle">Session context window breakdown with actionable recommendations</p>
|
|
489
516
|
|
|
490
517
|
<div class="stats">
|
|
@@ -497,12 +524,12 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
497
524
|
<span class="stat-label">Files</span>
|
|
498
525
|
</div>
|
|
499
526
|
<div class="stat">
|
|
500
|
-
<span class="stat-value">${insights.filter(i => i.severity === "warning" || i.severity === "critical").length}</span>
|
|
527
|
+
<span class="stat-value">${insights.filter((i) => i.severity === "warning" || i.severity === "critical").length}</span>
|
|
501
528
|
<span class="stat-label">Alerts</span>
|
|
502
529
|
</div>
|
|
503
530
|
<div class="stat">
|
|
504
|
-
<span class="stat-value">${
|
|
505
|
-
<span class="stat-label">
|
|
531
|
+
<span class="stat-value">${(contextWindow / 1000).toFixed(0)}k</span>
|
|
532
|
+
<span class="stat-label">Context Window</span>
|
|
506
533
|
</div>
|
|
507
534
|
</div>
|
|
508
535
|
|
|
@@ -516,7 +543,7 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
516
543
|
</svg>
|
|
517
544
|
</div>
|
|
518
545
|
<div class="usage-label">
|
|
519
|
-
${usagePercent}% of
|
|
546
|
+
${usagePercent}% of ${(contextWindow / 1000).toFixed(0)}k window
|
|
520
547
|
<small>${usagePercent > 80 ? "Compaction recommended" : usagePercent > 60 ? "Monitor usage" : "Healthy"}</small>
|
|
521
548
|
</div>
|
|
522
549
|
</div>
|
|
@@ -595,6 +622,25 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
595
622
|
if (filter) filter.addEventListener('change', update);
|
|
596
623
|
update();
|
|
597
624
|
|
|
625
|
+
// Theme toggle
|
|
626
|
+
var toggle = document.getElementById('themeToggle');
|
|
627
|
+
var label = document.getElementById('themeLabel');
|
|
628
|
+
var sunIcon = document.getElementById('themeIconSun');
|
|
629
|
+
var moonIcon = document.getElementById('themeIconMoon');
|
|
630
|
+
function applyTheme(t) {
|
|
631
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
632
|
+
localStorage.setItem('context-map-theme', t);
|
|
633
|
+
if (label) label.textContent = t === 'dark' ? 'Light' : 'Dark';
|
|
634
|
+
if (sunIcon) sunIcon.style.display = t === 'dark' ? '' : 'none';
|
|
635
|
+
if (moonIcon) moonIcon.style.display = t === 'dark' ? 'none' : '';
|
|
636
|
+
}
|
|
637
|
+
var saved = localStorage.getItem('context-map-theme');
|
|
638
|
+
applyTheme(saved || 'light');
|
|
639
|
+
if (toggle) toggle.addEventListener('click', function() {
|
|
640
|
+
var cur = document.documentElement.getAttribute('data-theme');
|
|
641
|
+
applyTheme(cur === 'dark' ? 'light' : 'dark');
|
|
642
|
+
});
|
|
643
|
+
|
|
598
644
|
// Insight toggles
|
|
599
645
|
var btns = document.querySelectorAll('.insight-header');
|
|
600
646
|
for (var j = 0; j < btns.length; j++) {
|
|
@@ -613,8 +659,37 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
613
659
|
try {
|
|
614
660
|
var p = JSON.parse(e.data);
|
|
615
661
|
if (p.html) {
|
|
662
|
+
// Preserve current theme before replacing body
|
|
663
|
+
var currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
|
|
664
|
+
// Only replace the body content to preserve <style> and <script>
|
|
616
665
|
var d = new DOMParser().parseFromString(p.html, 'text/html');
|
|
617
|
-
|
|
666
|
+
var newBody = d.querySelector('body');
|
|
667
|
+
var newTitle = d.querySelector('title');
|
|
668
|
+
if (newBody) {
|
|
669
|
+
document.body.innerHTML = newBody.innerHTML;
|
|
670
|
+
}
|
|
671
|
+
if (newTitle) {
|
|
672
|
+
document.title = newTitle.textContent || document.title;
|
|
673
|
+
}
|
|
674
|
+
// Restore theme after body replacement
|
|
675
|
+
applyTheme(currentTheme);
|
|
676
|
+
// Re-bind new theme toggle button
|
|
677
|
+
var newToggle = document.getElementById('themeToggle');
|
|
678
|
+
if (newToggle) {
|
|
679
|
+
newToggle.addEventListener('click', function() {
|
|
680
|
+
var cur = document.documentElement.getAttribute('data-theme');
|
|
681
|
+
applyTheme(cur === 'dark' ? 'light' : 'dark');
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
// Re-bind new file search/filter
|
|
685
|
+
var ns = document.getElementById('fileSearch');
|
|
686
|
+
var nf = document.getElementById('fileFilter');
|
|
687
|
+
if (ns) ns.addEventListener('input', update);
|
|
688
|
+
if (nf) nf.addEventListener('change', update);
|
|
689
|
+
// Re-query cards after body replacement
|
|
690
|
+
cards = Array.from(document.getElementById('fileGrid').querySelectorAll('.file-card'));
|
|
691
|
+
total = cards.length;
|
|
692
|
+
update();
|
|
618
693
|
}
|
|
619
694
|
} catch(_) {}
|
|
620
695
|
};
|
|
@@ -657,7 +732,11 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
657
732
|
}
|
|
658
733
|
|
|
659
734
|
private static escapeHtml(text: string): string {
|
|
660
|
-
return text
|
|
661
|
-
.replace(
|
|
735
|
+
return text
|
|
736
|
+
.replace(/&/g, "&")
|
|
737
|
+
.replace(/</g, "<")
|
|
738
|
+
.replace(/>/g, ">")
|
|
739
|
+
.replace(/"/g, """)
|
|
740
|
+
.replace(/'/g, "'");
|
|
662
741
|
}
|
|
663
742
|
}
|
package/extensions/index.ts
CHANGED
|
@@ -31,12 +31,22 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
31
31
|
// session messages in Pi. (pi as any).session?.messages does NOT exist.
|
|
32
32
|
let sessionMessages: AgentMessage[] = [];
|
|
33
33
|
let currentTurn = 0;
|
|
34
|
+
let contextWindow = 128_000; // fallback, updated from Pi system
|
|
34
35
|
|
|
35
|
-
// Capture messages before each LLM call
|
|
36
|
-
pi.on("context", (event: any) => {
|
|
36
|
+
// Capture messages and context info before each LLM call
|
|
37
|
+
pi.on("context", (event: any, ctx: any) => {
|
|
37
38
|
if (event?.messages && Array.isArray(event.messages)) {
|
|
38
39
|
sessionMessages = event.messages;
|
|
39
40
|
}
|
|
41
|
+
// Fetch actual context window from Pi system
|
|
42
|
+
try {
|
|
43
|
+
const usage = ctx?.getContextUsage?.();
|
|
44
|
+
if (usage?.contextWindow && usage.contextWindow > 0) {
|
|
45
|
+
contextWindow = usage.contextWindow;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Ignore — keep fallback
|
|
49
|
+
}
|
|
40
50
|
});
|
|
41
51
|
|
|
42
52
|
// Track turns
|
|
@@ -52,7 +62,11 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
52
62
|
const messages = sessionMessages.length > 0 ? sessionMessages : [];
|
|
53
63
|
const composition = analyzer.analyzeByType(messages, currentTurn);
|
|
54
64
|
const insights = InsightEngine.generate(composition);
|
|
55
|
-
const html = ReportGenerator.generateHTML(
|
|
65
|
+
const html = ReportGenerator.generateHTML(
|
|
66
|
+
composition,
|
|
67
|
+
insights,
|
|
68
|
+
contextWindow,
|
|
69
|
+
);
|
|
56
70
|
|
|
57
71
|
// Write to disk
|
|
58
72
|
try {
|
|
@@ -135,14 +149,14 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
135
149
|
const { composition, insights } = await runAnalysis();
|
|
136
150
|
const usagePercent =
|
|
137
151
|
composition.total.tokens > 0
|
|
138
|
-
? Math.round((composition.total.tokens /
|
|
152
|
+
? Math.round((composition.total.tokens / contextWindow) * 100)
|
|
139
153
|
: 0;
|
|
140
154
|
const summary =
|
|
141
155
|
`Context: ${composition.total.tokens.toLocaleString()} tokens total. ` +
|
|
142
156
|
`System ${composition.system.percent}%, Tools ${composition.tools.percent}%, ` +
|
|
143
157
|
`History ${composition.history.percent}%, Files ${composition.files.percent}%, ` +
|
|
144
158
|
`Summaries ${composition.summaries.percent}%. ` +
|
|
145
|
-
`Usage: ${usagePercent}% of
|
|
159
|
+
`Usage: ${usagePercent}% of ${(contextWindow / 1000).toFixed(0)}k window. ` +
|
|
146
160
|
`${insights.length} insight(s) generated.`;
|
|
147
161
|
return {
|
|
148
162
|
type: "text" as const,
|
|
@@ -51,8 +51,9 @@ export class LiveReportServer {
|
|
|
51
51
|
* Start the server. Returns a Promise that resolves to the URL, or null on failure.
|
|
52
52
|
*/
|
|
53
53
|
public start(): Promise<string | null> {
|
|
54
|
+
// Kill any pre-existing server to ensure only one instance runs
|
|
54
55
|
if (this.server) {
|
|
55
|
-
|
|
56
|
+
this.stop();
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
return new Promise((resolve) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-map",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Professional context profiler for Pi that visualizes the session context window, token distribution, and integrates with Nexus packages for actionable insights.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|