pi-context-map 0.4.4 → 0.6.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 +21 -0
- package/extensions/analyzer.ts +110 -55
- package/extensions/generator.ts +68 -6
- package/extensions/index.ts +49 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0] - 2026-06-15
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
- **Fixed composition analysis**: Changed `role === "tool"` to `role === "toolResult"` to match Pi's actual message format. Tools and files now show correct percentages instead of 0%.
|
|
6
|
+
- **File attachment detection**: Now detects images (type: "image") and file paths in user messages, not just assistant tool_use blocks.
|
|
7
|
+
- **Compaction summary detection**: Improved detection of Pi compaction entries via `customType` field.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
- **Session-unique reports**: Each report is saved with date, time, and session name (e.g., `2026-06-15_14-30-00_my-session.html`). Old reports are preserved for history.
|
|
11
|
+
- **Auto-report path on session start**: New session gets a fresh report path automatically.
|
|
12
|
+
|
|
13
|
+
## [0.5.1] - 2026-06-15
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
- **Fixed toggle symbol**: Dark mode now correctly shows sun icon + "Light" label. Light mode shows moon icon + "Dark" label. Uses fresh DOM queries to survive SSE body replacement.
|
|
16
|
+
- **Toggle moved to corner**: Button is now fixed in the top-right corner of the page, not inline with the live badge.
|
|
17
|
+
- **Event delegation**: Theme toggle click handler uses event delegation so it survives SSE body replacement without re-binding.
|
|
18
|
+
|
|
19
|
+
## [0.5.0] - 2026-06-15
|
|
20
|
+
### Features
|
|
21
|
+
- **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.
|
|
22
|
+
- **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.).
|
|
23
|
+
|
|
3
24
|
## [0.4.4] - 2026-06-15
|
|
4
25
|
### Bug Fixes
|
|
5
26
|
- **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.
|
package/extensions/analyzer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ContextAnalyzer
|
|
3
|
-
*
|
|
4
|
-
* their token weights, and their temporal status.
|
|
3
|
+
* Parses Pi session messages to identify the active working set of files,
|
|
4
|
+
* their token weights, and their temporal status.
|
|
5
5
|
*/
|
|
6
6
|
import { TokenCounter } from "./token-counter";
|
|
7
7
|
|
|
@@ -13,7 +13,7 @@ export interface FileOp {
|
|
|
13
13
|
|
|
14
14
|
export interface FileContext {
|
|
15
15
|
path: string;
|
|
16
|
-
weight: number;
|
|
16
|
+
weight: number;
|
|
17
17
|
lastOp: FileOp;
|
|
18
18
|
status: "active" | "stale" | "legacy";
|
|
19
19
|
}
|
|
@@ -34,11 +34,6 @@ export interface ContextComposition {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export class ContextAnalyzer {
|
|
37
|
-
/**
|
|
38
|
-
* Analyze session messages to produce a structured ContextComposition.
|
|
39
|
-
* @param messages The full session conversation history.
|
|
40
|
-
* @param currentTurn The current turn number.
|
|
41
|
-
*/
|
|
42
37
|
public analyzeByType(
|
|
43
38
|
messages: any[],
|
|
44
39
|
currentTurn: number,
|
|
@@ -51,73 +46,130 @@ export class ContextAnalyzer {
|
|
|
51
46
|
let fileTokens = 0;
|
|
52
47
|
let summaryTokens = 0;
|
|
53
48
|
|
|
54
|
-
messages.
|
|
49
|
+
for (let index = 0; index < messages.length; index++) {
|
|
50
|
+
const msg = messages[index];
|
|
55
51
|
const turn = index + 1;
|
|
52
|
+
const role = msg.role || "";
|
|
53
|
+
const msgType = msg.type || "";
|
|
56
54
|
|
|
57
|
-
// 1.
|
|
58
|
-
if (msg.role === "system") {
|
|
59
|
-
systemTokens += TokenCounter.countMessage(msg);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (msg.role === "tool") {
|
|
64
|
-
toolTokens += TokenCounter.countMessage(msg);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Detect compaction summaries (Pi uses customType or specific role)
|
|
55
|
+
// 1. Compaction summaries
|
|
69
56
|
if (
|
|
70
|
-
|
|
71
|
-
|
|
57
|
+
role === "compaction" ||
|
|
58
|
+
msgType === "compaction" ||
|
|
59
|
+
msg.customType === "compaction" ||
|
|
72
60
|
msg.compactionEntry
|
|
73
61
|
) {
|
|
74
62
|
summaryTokens += TokenCounter.countMessage(msg);
|
|
75
|
-
|
|
63
|
+
continue;
|
|
76
64
|
}
|
|
77
65
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
66
|
+
// 2. System messages
|
|
67
|
+
if (role === "system" || msgType === "system") {
|
|
68
|
+
systemTokens += TokenCounter.countMessage(msg);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. Tool results (Pi uses "toolResult")
|
|
73
|
+
if (role === "toolResult" || role === "tool") {
|
|
74
|
+
toolTokens += TokenCounter.countMessage(msg);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 4. User messages — track file attachments
|
|
79
|
+
if (role === "user") {
|
|
82
80
|
historyTokens += TokenCounter.countMessage(msg);
|
|
81
|
+
if (Array.isArray(msg.content)) {
|
|
82
|
+
for (const block of msg.content) {
|
|
83
|
+
if (block.type === "image" || block.type === "image_url") {
|
|
84
|
+
const p =
|
|
85
|
+
block.source?.url || block.image_url?.url || "[image]";
|
|
86
|
+
const w = TokenCounter.count(JSON.stringify(block));
|
|
87
|
+
fileTokens += w;
|
|
88
|
+
if (!fileRegistry.has(p)) {
|
|
89
|
+
fileRegistry.set(p, {
|
|
90
|
+
path: p,
|
|
91
|
+
weight: w,
|
|
92
|
+
lastOp: {
|
|
93
|
+
type: "read",
|
|
94
|
+
turn,
|
|
95
|
+
timestamp: msg.timestamp || Date.now(),
|
|
96
|
+
},
|
|
97
|
+
status: this.calculateStatus(turn, currentTurn),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
102
|
+
const matches = block.text.match(
|
|
103
|
+
/(?:\/|[A-Z]:\\)[\w./\\-]+\.\w+/g,
|
|
104
|
+
);
|
|
105
|
+
if (matches) {
|
|
106
|
+
for (const m of matches) {
|
|
107
|
+
if (!fileRegistry.has(m)) {
|
|
108
|
+
fileRegistry.set(m, {
|
|
109
|
+
path: m,
|
|
110
|
+
weight: TokenCounter.count(m),
|
|
111
|
+
lastOp: {
|
|
112
|
+
type: "read",
|
|
113
|
+
turn,
|
|
114
|
+
timestamp: msg.timestamp || Date.now(),
|
|
115
|
+
},
|
|
116
|
+
status: this.calculateStatus(turn, currentTurn),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
83
125
|
}
|
|
84
126
|
|
|
85
|
-
//
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
// 5. Assistant messages — track tool_use blocks
|
|
128
|
+
if (role === "assistant") {
|
|
129
|
+
historyTokens += TokenCounter.countMessage(msg);
|
|
130
|
+
if (Array.isArray(msg.content)) {
|
|
131
|
+
for (const block of msg.content) {
|
|
132
|
+
if (block.type === "tool_use") {
|
|
133
|
+
const input = block.input as Record<string, any>;
|
|
134
|
+
const p = this.extractPath(block.name, input);
|
|
135
|
+
if (p) {
|
|
136
|
+
const opType = this.getOpType(block.name);
|
|
137
|
+
const result = this.findToolResult(
|
|
138
|
+
messages,
|
|
139
|
+
index,
|
|
140
|
+
block.id,
|
|
141
|
+
);
|
|
142
|
+
const content = result?.content || "";
|
|
143
|
+
const w = TokenCounter.count(String(content));
|
|
144
|
+
fileTokens += w;
|
|
145
|
+
fileRegistry.set(p, {
|
|
146
|
+
path: p,
|
|
147
|
+
weight: w,
|
|
148
|
+
lastOp: {
|
|
149
|
+
type: opType,
|
|
150
|
+
turn,
|
|
151
|
+
timestamp: msg.timestamp || Date.now(),
|
|
152
|
+
},
|
|
153
|
+
status: this.calculateStatus(turn, currentTurn),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
109
156
|
}
|
|
110
157
|
}
|
|
111
158
|
}
|
|
159
|
+
continue;
|
|
112
160
|
}
|
|
113
|
-
|
|
161
|
+
|
|
162
|
+
// 6. Everything else
|
|
163
|
+
historyTokens += TokenCounter.countMessage(msg);
|
|
164
|
+
}
|
|
114
165
|
|
|
115
166
|
const totalTokens =
|
|
116
167
|
systemTokens + toolTokens + historyTokens + fileTokens + summaryTokens;
|
|
117
168
|
|
|
118
169
|
const mk = (tokens: number): ContextSlice => ({
|
|
119
170
|
tokens: Math.ceil(tokens),
|
|
120
|
-
percent:
|
|
171
|
+
percent:
|
|
172
|
+
totalTokens > 0 ? Math.round((tokens / totalTokens) * 100) : 0,
|
|
121
173
|
});
|
|
122
174
|
|
|
123
175
|
const files_detail = Array.from(fileRegistry.values())
|
|
@@ -182,7 +234,10 @@ export class ContextAnalyzer {
|
|
|
182
234
|
toolId: string,
|
|
183
235
|
): any {
|
|
184
236
|
for (let i = toolTurnIndex + 1; i < messages.length; i++) {
|
|
185
|
-
if (
|
|
237
|
+
if (
|
|
238
|
+
messages[i].role === "toolResult" &&
|
|
239
|
+
messages[i].tool_call_id === toolId
|
|
240
|
+
) {
|
|
186
241
|
return messages[i];
|
|
187
242
|
}
|
|
188
243
|
if (messages[i].role === "assistant") break;
|
package/extensions/generator.ts
CHANGED
|
@@ -14,9 +14,11 @@ 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
|
+
const usagePercent =
|
|
21
|
+
total > 0 ? Math.round((total / contextWindow) * 100) : 0;
|
|
20
22
|
|
|
21
23
|
const fileCards = composition.files_detail
|
|
22
24
|
.map(
|
|
@@ -90,6 +92,26 @@ export class ReportGenerator {
|
|
|
90
92
|
--seg-files: #007aff;
|
|
91
93
|
--seg-summaries: #34c759;
|
|
92
94
|
}
|
|
95
|
+
[data-theme="dark"] {
|
|
96
|
+
--canvas: #0a0a0b;
|
|
97
|
+
--canvas-alt: #141415;
|
|
98
|
+
--surface: #1a1a1c;
|
|
99
|
+
--hairline: #2c2c2e;
|
|
100
|
+
--hairline-soft: rgba(255,255,255,0.06);
|
|
101
|
+
--ink: #f5f5f7;
|
|
102
|
+
--ink-secondary: #a1a1a6;
|
|
103
|
+
--ink-tertiary: #6e6e73;
|
|
104
|
+
--ink-quaternary: #48484a;
|
|
105
|
+
--accent: #2997ff;
|
|
106
|
+
--accent-hover: #40a9ff;
|
|
107
|
+
--accent-soft: rgba(41,151,255,0.12);
|
|
108
|
+
--success: #30d158;
|
|
109
|
+
--success-soft: rgba(48,209,88,0.12);
|
|
110
|
+
--warning: #ff9f0a;
|
|
111
|
+
--warning-soft: rgba(255,159,10,0.12);
|
|
112
|
+
--danger: #ff453a;
|
|
113
|
+
--danger-soft: rgba(255,69,58,0.12);
|
|
114
|
+
}
|
|
93
115
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
94
116
|
body {
|
|
95
117
|
background: var(--canvas);
|
|
@@ -482,8 +504,13 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
482
504
|
<div class="container">
|
|
483
505
|
|
|
484
506
|
<header>
|
|
485
|
-
|
|
486
|
-
|
|
507
|
+
<button id="themeToggle" style="position:fixed;top:24px;right:24px;z-index:100;display:inline-flex;align-items:center;gap:6px;padding:8px 16px;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;box-shadow:0 2px 8px rgba(0,0,0,0.08);" aria-label="Toggle theme">
|
|
508
|
+
<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>
|
|
509
|
+
<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>
|
|
510
|
+
<span id="themeLabel">Dark</span>
|
|
511
|
+
</button>
|
|
512
|
+
<div class="live-badge"><span class="dot"></span>Live</div>
|
|
513
|
+
<h1>Context Profiler</h1>
|
|
487
514
|
<p class="subtitle">Session context window breakdown with actionable recommendations</p>
|
|
488
515
|
|
|
489
516
|
<div class="stats">
|
|
@@ -500,8 +527,8 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
500
527
|
<span class="stat-label">Alerts</span>
|
|
501
528
|
</div>
|
|
502
529
|
<div class="stat">
|
|
503
|
-
<span class="stat-value">${
|
|
504
|
-
<span class="stat-label">
|
|
530
|
+
<span class="stat-value">${(contextWindow / 1000).toFixed(0)}k</span>
|
|
531
|
+
<span class="stat-label">Context Window</span>
|
|
505
532
|
</div>
|
|
506
533
|
</div>
|
|
507
534
|
|
|
@@ -515,7 +542,7 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
515
542
|
</svg>
|
|
516
543
|
</div>
|
|
517
544
|
<div class="usage-label">
|
|
518
|
-
${usagePercent}% of
|
|
545
|
+
${usagePercent}% of ${(contextWindow / 1000).toFixed(0)}k window
|
|
519
546
|
<small>${usagePercent > 80 ? "Compaction recommended" : usagePercent > 60 ? "Monitor usage" : "Healthy"}</small>
|
|
520
547
|
</div>
|
|
521
548
|
</div>
|
|
@@ -594,6 +621,28 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
594
621
|
if (filter) filter.addEventListener('change', update);
|
|
595
622
|
update();
|
|
596
623
|
|
|
624
|
+
// Theme toggle
|
|
625
|
+
function applyTheme(t) {
|
|
626
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
627
|
+
localStorage.setItem('context-map-theme', t);
|
|
628
|
+
// Query fresh each time (DOM may have been replaced by SSE)
|
|
629
|
+
var lbl = document.getElementById('themeLabel');
|
|
630
|
+
var sun = document.getElementById('themeIconSun');
|
|
631
|
+
var moon = document.getElementById('themeIconMoon');
|
|
632
|
+
if (lbl) lbl.textContent = t === 'dark' ? 'Light' : 'Dark';
|
|
633
|
+
if (sun) sun.style.display = t === 'dark' ? '' : 'none';
|
|
634
|
+
if (moon) moon.style.display = t === 'dark' ? 'none' : '';
|
|
635
|
+
}
|
|
636
|
+
var saved = localStorage.getItem('context-map-theme');
|
|
637
|
+
applyTheme(saved || 'light');
|
|
638
|
+
// Use event delegation on document so clicks survive body replacement
|
|
639
|
+
document.addEventListener('click', function(e) {
|
|
640
|
+
var btn = e.target.closest('#themeToggle');
|
|
641
|
+
if (!btn) return;
|
|
642
|
+
var cur = document.documentElement.getAttribute('data-theme');
|
|
643
|
+
applyTheme(cur === 'dark' ? 'light' : 'dark');
|
|
644
|
+
});
|
|
645
|
+
|
|
597
646
|
// Insight toggles
|
|
598
647
|
var btns = document.querySelectorAll('.insight-header');
|
|
599
648
|
for (var j = 0; j < btns.length; j++) {
|
|
@@ -612,6 +661,8 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
612
661
|
try {
|
|
613
662
|
var p = JSON.parse(e.data);
|
|
614
663
|
if (p.html) {
|
|
664
|
+
// Preserve current theme before replacing body
|
|
665
|
+
var currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
|
|
615
666
|
// Only replace the body content to preserve <style> and <script>
|
|
616
667
|
var d = new DOMParser().parseFromString(p.html, 'text/html');
|
|
617
668
|
var newBody = d.querySelector('body');
|
|
@@ -622,6 +673,17 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
622
673
|
if (newTitle) {
|
|
623
674
|
document.title = newTitle.textContent || document.title;
|
|
624
675
|
}
|
|
676
|
+
// Restore theme after body replacement
|
|
677
|
+
applyTheme(currentTheme);
|
|
678
|
+
// Re-bind file search/filter (direct listeners, not delegated)
|
|
679
|
+
var ns = document.getElementById('fileSearch');
|
|
680
|
+
var nf = document.getElementById('fileFilter');
|
|
681
|
+
if (ns) ns.addEventListener('input', update);
|
|
682
|
+
if (nf) nf.addEventListener('change', update);
|
|
683
|
+
// Re-query cards after body replacement
|
|
684
|
+
cards = Array.from(document.getElementById('fileGrid').querySelectorAll('.file-card'));
|
|
685
|
+
total = cards.length;
|
|
686
|
+
update();
|
|
625
687
|
}
|
|
626
688
|
} catch(_) {}
|
|
627
689
|
};
|
package/extensions/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-context-map
|
|
3
3
|
* Professional Context Profiler for Pi.
|
|
4
|
-
* v0.
|
|
4
|
+
* v0.5.1 — Dynamic context window, dark mode, session-unique reports.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {
|
|
@@ -14,36 +14,57 @@ import { ReportGenerator } from "./generator";
|
|
|
14
14
|
import { InsightEngine } from "./insights";
|
|
15
15
|
import { LiveReportServer } from "./live-server";
|
|
16
16
|
import * as path from "node:path";
|
|
17
|
+
import * as fs from "node:fs";
|
|
17
18
|
import * as os from "node:os";
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
os.homedir(),
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
);
|
|
20
|
+
function makeReportPath(sessionName?: string): string {
|
|
21
|
+
const dir = path.join(os.homedir(), ".pi", "context-map");
|
|
22
|
+
if (!fs.existsSync(dir)) {
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const date = now.toISOString().split("T")[0];
|
|
27
|
+
const time = now.toTimeString().split(" ")[0].replace(/:/g, "-");
|
|
28
|
+
const safe = (sessionName || "session")
|
|
29
|
+
.replace(/[^\w.-]/g, "_")
|
|
30
|
+
.slice(0, 40);
|
|
31
|
+
const filename = `${date}_${time}_${safe}.html`;
|
|
32
|
+
return path.join(dir, filename);
|
|
33
|
+
}
|
|
25
34
|
|
|
26
35
|
export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
27
36
|
const analyzer = new ContextAnalyzer();
|
|
28
37
|
const liveServer = new LiveReportServer();
|
|
29
38
|
|
|
30
|
-
// Accumulate messages from events — this is the correct way to access
|
|
31
|
-
// session messages in Pi. (pi as any).session?.messages does NOT exist.
|
|
32
39
|
let sessionMessages: AgentMessage[] = [];
|
|
33
40
|
let currentTurn = 0;
|
|
41
|
+
let contextWindow = 128_000;
|
|
42
|
+
let currentReportPath = makeReportPath();
|
|
34
43
|
|
|
35
|
-
// Capture messages
|
|
36
|
-
pi.on("context", (event: any) => {
|
|
44
|
+
// Capture messages and context window from Pi system
|
|
45
|
+
pi.on("context", (event: any, ctx: any) => {
|
|
37
46
|
if (event?.messages && Array.isArray(event.messages)) {
|
|
38
47
|
sessionMessages = event.messages;
|
|
39
48
|
}
|
|
49
|
+
try {
|
|
50
|
+
const usage = ctx?.getContextUsage?.();
|
|
51
|
+
if (usage?.contextWindow && usage.contextWindow > 0) {
|
|
52
|
+
contextWindow = usage.contextWindow;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Keep fallback
|
|
56
|
+
}
|
|
40
57
|
});
|
|
41
58
|
|
|
42
|
-
// Track turns
|
|
43
59
|
pi.on("turn_start", () => {
|
|
44
60
|
currentTurn++;
|
|
45
61
|
});
|
|
46
62
|
|
|
63
|
+
// Update report path when session changes
|
|
64
|
+
pi.on("session_start", () => {
|
|
65
|
+
currentReportPath = makeReportPath();
|
|
66
|
+
});
|
|
67
|
+
|
|
47
68
|
async function runAnalysis(): Promise<{
|
|
48
69
|
composition: ReturnType<typeof analyzer.analyzeByType>;
|
|
49
70
|
insights: ReturnType<typeof InsightEngine.generate>;
|
|
@@ -52,32 +73,31 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
52
73
|
const messages = sessionMessages.length > 0 ? sessionMessages : [];
|
|
53
74
|
const composition = analyzer.analyzeByType(messages, currentTurn);
|
|
54
75
|
const insights = InsightEngine.generate(composition);
|
|
55
|
-
const html = ReportGenerator.generateHTML(
|
|
76
|
+
const html = ReportGenerator.generateHTML(
|
|
77
|
+
composition,
|
|
78
|
+
insights,
|
|
79
|
+
contextWindow,
|
|
80
|
+
);
|
|
56
81
|
|
|
57
|
-
// Write to disk
|
|
58
82
|
try {
|
|
59
|
-
const
|
|
60
|
-
const dir = path.dirname(REPORT_PATH);
|
|
83
|
+
const dir = path.dirname(currentReportPath);
|
|
61
84
|
if (!fs.existsSync(dir)) {
|
|
62
85
|
fs.mkdirSync(dir, { recursive: true });
|
|
63
86
|
}
|
|
64
|
-
fs.writeFileSync(
|
|
87
|
+
fs.writeFileSync(currentReportPath, html, "utf8");
|
|
65
88
|
} catch (err: any) {
|
|
66
89
|
console.error(`[pi-context-map] Failed to write report: ${err.message}`);
|
|
67
90
|
}
|
|
68
91
|
|
|
69
|
-
// Push to live server if running
|
|
70
92
|
if (liveServer.isRunning) {
|
|
71
|
-
liveServer.update(html,
|
|
93
|
+
liveServer.update(html, currentReportPath);
|
|
72
94
|
}
|
|
73
95
|
|
|
74
|
-
return { composition, insights, reportPath:
|
|
96
|
+
return { composition, insights, reportPath: currentReportPath };
|
|
75
97
|
}
|
|
76
98
|
|
|
77
|
-
// Start live server
|
|
78
99
|
const serverUrl = await liveServer.start();
|
|
79
100
|
|
|
80
|
-
// Register /context-map command
|
|
81
101
|
pi.registerCommand("context-map", {
|
|
82
102
|
description:
|
|
83
103
|
"Generate a visual context map with actionable insights. Use 'stop' to terminate the live server.",
|
|
@@ -115,7 +135,6 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
115
135
|
},
|
|
116
136
|
});
|
|
117
137
|
|
|
118
|
-
// Register the tool for agent use
|
|
119
138
|
pi.registerTool({
|
|
120
139
|
name: "context-map",
|
|
121
140
|
label: "Context Map",
|
|
@@ -132,17 +151,17 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
132
151
|
_ctx: any,
|
|
133
152
|
) {
|
|
134
153
|
try {
|
|
135
|
-
const { composition, insights } = await runAnalysis();
|
|
154
|
+
const { composition, insights, reportPath } = await runAnalysis();
|
|
136
155
|
const usagePercent =
|
|
137
156
|
composition.total.tokens > 0
|
|
138
|
-
? Math.round((composition.total.tokens /
|
|
157
|
+
? Math.round((composition.total.tokens / contextWindow) * 100)
|
|
139
158
|
: 0;
|
|
140
159
|
const summary =
|
|
141
160
|
`Context: ${composition.total.tokens.toLocaleString()} tokens total. ` +
|
|
142
161
|
`System ${composition.system.percent}%, Tools ${composition.tools.percent}%, ` +
|
|
143
162
|
`History ${composition.history.percent}%, Files ${composition.files.percent}%, ` +
|
|
144
163
|
`Summaries ${composition.summaries.percent}%. ` +
|
|
145
|
-
`Usage: ${usagePercent}% of
|
|
164
|
+
`Usage: ${usagePercent}% of ${(contextWindow / 1000).toFixed(0)}k window. ` +
|
|
146
165
|
`${insights.length} insight(s) generated.`;
|
|
147
166
|
return {
|
|
148
167
|
type: "text" as const,
|
|
@@ -152,7 +171,8 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
152
171
|
...insights.map(
|
|
153
172
|
(i) => `[${i.severity.toUpperCase()}] ${i.title}: ${i.message}`,
|
|
154
173
|
),
|
|
155
|
-
|
|
174
|
+
`Report: ${reportPath}`,
|
|
175
|
+
serverUrl ? `Live: ${serverUrl}` : "",
|
|
156
176
|
]
|
|
157
177
|
.filter(Boolean)
|
|
158
178
|
.join("\n"),
|
|
@@ -167,34 +187,30 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
167
187
|
},
|
|
168
188
|
});
|
|
169
189
|
|
|
170
|
-
// Auto-warning on high context before compaction
|
|
171
190
|
pi.on("session_before_compact", (_event: any, ctx: any) => {
|
|
172
191
|
const tokens = _event?.preparation?.tokensBefore;
|
|
173
192
|
if (tokens && tokens > 100_000) {
|
|
174
193
|
ctx.ui.notify(
|
|
175
|
-
`High context load
|
|
194
|
+
`High context load (${(tokens / 1000).toFixed(1)}k tokens). Try /context-map to see what's consuming space.`,
|
|
176
195
|
"info",
|
|
177
196
|
);
|
|
178
197
|
}
|
|
179
198
|
});
|
|
180
199
|
|
|
181
|
-
// Auto-refresh after each assistant message if server is running
|
|
182
200
|
pi.on("message_end", async (_event: any) => {
|
|
183
201
|
if (_event?.message?.role === "assistant" && liveServer.isRunning) {
|
|
184
202
|
try {
|
|
185
203
|
await runAnalysis();
|
|
186
204
|
} catch {
|
|
187
|
-
//
|
|
205
|
+
// Ignore auto-refresh failures
|
|
188
206
|
}
|
|
189
207
|
}
|
|
190
208
|
});
|
|
191
209
|
|
|
192
|
-
// Graceful shutdown
|
|
193
210
|
pi.on("session_shutdown", () => {
|
|
194
211
|
liveServer.stop();
|
|
195
212
|
});
|
|
196
213
|
|
|
197
|
-
// Kill server when process exits
|
|
198
214
|
process.on("exit", () => liveServer.stop());
|
|
199
215
|
process.on("SIGINT", () => {
|
|
200
216
|
liveServer.stop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-map",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|