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 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.
@@ -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
- <div class="live-badge"><span class="dot"></span>Live</div>
487
- <h1>Context Profiler</h1>
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">${usagePercent}<span style="font-size:14px;color:var(--ink-tertiary)">%</span></span>
505
- <span class="stat-label">of 128k Window</span>
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 128k window
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
- document.replaceChild(document.importNode(d.documentElement, true), document.documentElement);
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.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
661
- .replace(/"/g, "&quot;").replace(/'/g, "&#039;");
735
+ return text
736
+ .replace(/&/g, "&amp;")
737
+ .replace(/</g, "&lt;")
738
+ .replace(/>/g, "&gt;")
739
+ .replace(/"/g, "&quot;")
740
+ .replace(/'/g, "&#039;");
662
741
  }
663
742
  }
@@ -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(composition, insights);
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 / 128_000) * 100)
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 typical 128k window. ` +
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
- return Promise.resolve(this.url);
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.4.3",
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",