pi-context-map 0.7.1 → 0.7.3
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 -0
- package/extensions/analyzer.ts +32 -11
- package/extensions/generator.ts +40 -24
- package/extensions/index.ts +1 -1
- package/extensions/insights.ts +14 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.3] - 2026-06-15
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
- **Fixed insights context window**: Removed hardcoded "128k" from all insight messages. Now shows actual window size (e.g., "your 200k context window").
|
|
6
|
+
- **Fixed stale file count**: Insight now counts both "legacy" AND "stale" files (all inactive), matching what the files section shows.
|
|
7
|
+
- **Fixed composition bar**: Used largest-remainder method to ensure segment percentages always sum to exactly 100%. No more gaps.
|
|
8
|
+
|
|
9
|
+
## [0.7.2] - 2026-06-15
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
- **Fixed insights context window**: Now uses the actual context window from Pi (e.g., 200k) instead of hardcoded 128k. Critical insight no longer shows wrong percentages.
|
|
12
|
+
- **Fixed file filter**: Changed from direct `addEventListener` to event delegation (`document.addEventListener('change', ...)`). Filter now works after SSE body replacement.
|
|
13
|
+
- **Fixed file search**: Changed to event delegation (`document.addEventListener('input', ...)`). Search now works in real-time and survives SSE body replacement.
|
|
14
|
+
- **Added search icon**: SVG magnifying glass icon inside the search input for better UX.
|
|
15
|
+
|
|
3
16
|
## [0.7.1] - 2026-06-15
|
|
4
17
|
### Bug Fixes
|
|
5
18
|
- **Fixed insights dropdown**: Changed from direct `addEventListener` to event delegation (`document.addEventListener('click', ...)` with `e.target.closest('.insight-header')`). The dropdown now works after SSE body replacement.
|
package/extensions/analyzer.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface ContextComposition {
|
|
|
50
50
|
export class ContextAnalyzer {
|
|
51
51
|
public analyzeByType(
|
|
52
52
|
messages: any[],
|
|
53
|
-
|
|
53
|
+
_currentTurn: number,
|
|
54
54
|
systemPrompt?: string,
|
|
55
55
|
): ContextComposition {
|
|
56
56
|
const fileRegistry = new Map<string, FileContext>();
|
|
@@ -243,9 +243,30 @@ export class ContextAnalyzer {
|
|
|
243
243
|
const totalTokens =
|
|
244
244
|
systemTokens + toolTokens + historyTokens + fileTokens + summaryTokens;
|
|
245
245
|
|
|
246
|
-
|
|
246
|
+
// Calculate exact percentages, then use largest-remainder to ensure sum = 100
|
|
247
|
+
const raw = [
|
|
248
|
+
{ key: "system", tokens: systemTokens },
|
|
249
|
+
{ key: "tools", tokens: toolTokens },
|
|
250
|
+
{ key: "history", tokens: historyTokens },
|
|
251
|
+
{ key: "files", tokens: fileTokens },
|
|
252
|
+
{ key: "summaries", tokens: summaryTokens },
|
|
253
|
+
];
|
|
254
|
+
const floored = raw.map((r) => ({
|
|
255
|
+
...r,
|
|
256
|
+
pct: totalTokens > 0 ? Math.floor((r.tokens / totalTokens) * 100) : 0,
|
|
257
|
+
remainder: totalTokens > 0 ? ((r.tokens / totalTokens) * 100) % 1 : 0,
|
|
258
|
+
}));
|
|
259
|
+
let sum = floored.reduce((s, r) => s + r.pct, 0);
|
|
260
|
+
const sorted = [...floored].sort((a, b) => b.remainder - a.remainder);
|
|
261
|
+
for (let i = 0; sum < 100 && i < sorted.length; i++) {
|
|
262
|
+
sorted[i].pct++;
|
|
263
|
+
sum++;
|
|
264
|
+
}
|
|
265
|
+
const pctMap = new Map(floored.map((r) => [r.key, r.pct]));
|
|
266
|
+
|
|
267
|
+
const mk = (key: string, tokens: number): ContextSlice => ({
|
|
247
268
|
tokens: Math.ceil(tokens),
|
|
248
|
-
percent:
|
|
269
|
+
percent: pctMap.get(key) || 0,
|
|
249
270
|
});
|
|
250
271
|
|
|
251
272
|
const files_detail = Array.from(fileRegistry.values())
|
|
@@ -253,19 +274,19 @@ export class ContextAnalyzer {
|
|
|
253
274
|
.slice(0, 100);
|
|
254
275
|
|
|
255
276
|
return {
|
|
256
|
-
system: mk(systemTokens),
|
|
257
|
-
tools: mk(toolTokens),
|
|
258
|
-
history: mk(historyTokens),
|
|
259
|
-
files: mk(fileTokens),
|
|
260
|
-
summaries: mk(summaryTokens),
|
|
261
|
-
total:
|
|
277
|
+
system: mk("system", systemTokens),
|
|
278
|
+
tools: mk("tools", toolTokens),
|
|
279
|
+
history: mk("history", historyTokens),
|
|
280
|
+
files: mk("files", fileTokens),
|
|
281
|
+
summaries: mk("summaries", summaryTokens),
|
|
282
|
+
total: { tokens: Math.ceil(totalTokens), percent: 100 },
|
|
262
283
|
files_detail,
|
|
263
284
|
};
|
|
264
285
|
}
|
|
265
286
|
|
|
266
287
|
/** Backward-compatible wrapper. */
|
|
267
|
-
public analyze(messages: any[],
|
|
268
|
-
return this.analyzeByType(messages,
|
|
288
|
+
public analyze(messages: any[], _currentTurn: number): ContextComposition {
|
|
289
|
+
return this.analyzeByType(messages, _currentTurn);
|
|
269
290
|
}
|
|
270
291
|
|
|
271
292
|
private extractPath(toolName: string, args: any): string | null {
|
package/extensions/generator.ts
CHANGED
|
@@ -312,11 +312,23 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
312
312
|
align-items: center;
|
|
313
313
|
flex-wrap: wrap;
|
|
314
314
|
}
|
|
315
|
-
.file-search {
|
|
315
|
+
.file-search-wrap {
|
|
316
316
|
flex: 1;
|
|
317
317
|
min-width: 220px;
|
|
318
|
+
position: relative;
|
|
319
|
+
}
|
|
320
|
+
.file-search-icon {
|
|
321
|
+
position: absolute;
|
|
322
|
+
left: 16px;
|
|
323
|
+
top: 50%;
|
|
324
|
+
transform: translateY(-50%);
|
|
325
|
+
color: var(--ink-quaternary);
|
|
326
|
+
pointer-events: none;
|
|
327
|
+
}
|
|
328
|
+
.file-search {
|
|
329
|
+
width: 100%;
|
|
318
330
|
height: 44px;
|
|
319
|
-
padding: 0 20px;
|
|
331
|
+
padding: 0 20px 0 40px;
|
|
320
332
|
border: 1px solid rgba(0,0,0,0.08);
|
|
321
333
|
border-radius: 22px;
|
|
322
334
|
background: var(--surface);
|
|
@@ -579,7 +591,10 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
579
591
|
<section>
|
|
580
592
|
<h2>Files <span style="font-size:14px;font-weight:400;color:var(--ink-tertiary);letter-spacing:0;">(${composition.files_detail.length})</span></h2>
|
|
581
593
|
<div class="file-controls">
|
|
582
|
-
<
|
|
594
|
+
<div class="file-search-wrap">
|
|
595
|
+
<svg class="file-search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
596
|
+
<input type="text" class="file-search" id="fileSearch" placeholder="Search files..." aria-label="Search files">
|
|
597
|
+
</div>
|
|
583
598
|
<select class="file-filter" id="fileFilter" aria-label="Filter by status">
|
|
584
599
|
<option value="all">All</option>
|
|
585
600
|
<option value="active">Active</option>
|
|
@@ -598,17 +613,19 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
598
613
|
|
|
599
614
|
<script>
|
|
600
615
|
(function() {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
var empty = document.getElementById('emptyState');
|
|
606
|
-
var cards = grid ? Array.from(grid.querySelectorAll('.file-card')) : [];
|
|
607
|
-
var total = cards.length;
|
|
616
|
+
function getCards() {
|
|
617
|
+
var grid = document.getElementById('fileGrid');
|
|
618
|
+
return grid ? Array.from(grid.querySelectorAll('.file-card')) : [];
|
|
619
|
+
}
|
|
608
620
|
|
|
609
621
|
function update() {
|
|
610
|
-
var
|
|
611
|
-
var
|
|
622
|
+
var search = document.getElementById('fileSearch');
|
|
623
|
+
var filter = document.getElementById('fileFilter');
|
|
624
|
+
var count = document.getElementById('fileCount');
|
|
625
|
+
var empty = document.getElementById('emptyState');
|
|
626
|
+
var cards = getCards();
|
|
627
|
+
var q = (search ? search.value : '').toLowerCase();
|
|
628
|
+
var s = filter ? filter.value : 'all';
|
|
612
629
|
var v = 0;
|
|
613
630
|
for (var i = 0; i < cards.length; i++) {
|
|
614
631
|
var c = cards[i];
|
|
@@ -619,11 +636,17 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
619
636
|
if (mq && ms) { c.classList.remove('hidden'); v++; }
|
|
620
637
|
else { c.classList.add('hidden'); }
|
|
621
638
|
}
|
|
622
|
-
count.textContent = v
|
|
623
|
-
empty.style.display = v === 0 ? '' : 'none';
|
|
639
|
+
if (count) count.textContent = v + ' of ' + cards.length;
|
|
640
|
+
if (empty) empty.style.display = v === 0 ? '' : 'none';
|
|
624
641
|
}
|
|
625
|
-
|
|
626
|
-
|
|
642
|
+
|
|
643
|
+
// Event delegation — survives SSE body replacement
|
|
644
|
+
document.addEventListener('input', function(e) {
|
|
645
|
+
if (e.target.id === 'fileSearch') update();
|
|
646
|
+
});
|
|
647
|
+
document.addEventListener('change', function(e) {
|
|
648
|
+
if (e.target.id === 'fileFilter') update();
|
|
649
|
+
});
|
|
627
650
|
update();
|
|
628
651
|
|
|
629
652
|
// Theme toggle
|
|
@@ -680,14 +703,7 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
680
703
|
}
|
|
681
704
|
// Restore theme after body replacement
|
|
682
705
|
applyTheme(currentTheme);
|
|
683
|
-
//
|
|
684
|
-
var ns = document.getElementById('fileSearch');
|
|
685
|
-
var nf = document.getElementById('fileFilter');
|
|
686
|
-
if (ns) ns.addEventListener('input', update);
|
|
687
|
-
if (nf) nf.addEventListener('change', update);
|
|
688
|
-
// Re-query cards after body replacement
|
|
689
|
-
cards = Array.from(document.getElementById('fileGrid').querySelectorAll('.file-card'));
|
|
690
|
-
total = cards.length;
|
|
706
|
+
// Search/filter use event delegation — no re-binding needed
|
|
691
707
|
update();
|
|
692
708
|
}
|
|
693
709
|
} catch(_) {}
|
package/extensions/index.ts
CHANGED
|
@@ -163,7 +163,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
const insights = InsightEngine.generate(composition);
|
|
166
|
+
const insights = InsightEngine.generate(composition, contextWindow);
|
|
167
167
|
const html = ReportGenerator.generateHTML(
|
|
168
168
|
composition,
|
|
169
169
|
insights,
|
package/extensions/insights.ts
CHANGED
|
@@ -14,15 +14,17 @@ export interface Insight {
|
|
|
14
14
|
command?: string; // Suggested slash command
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const TYPICAL_WINDOW = 128_000; // Default context window size (Claude Sonnet/Opus)
|
|
18
|
-
|
|
19
17
|
export class InsightEngine {
|
|
20
18
|
/**
|
|
21
19
|
* Generate a list of insights based on the composition.
|
|
22
20
|
*/
|
|
23
|
-
public static generate(
|
|
21
|
+
public static generate(
|
|
22
|
+
composition: ContextComposition,
|
|
23
|
+
contextWindow?: number,
|
|
24
|
+
): Insight[] {
|
|
24
25
|
const insights: Insight[] = [];
|
|
25
|
-
const { system, tools,
|
|
26
|
+
const { system, tools, files, summaries, total } = composition;
|
|
27
|
+
const windowSize = contextWindow || 128_000;
|
|
26
28
|
|
|
27
29
|
// Rule 1: Tool bloat
|
|
28
30
|
if (tools.percent > 40) {
|
|
@@ -35,28 +37,28 @@ export class InsightEngine {
|
|
|
35
37
|
});
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
// Rule 2: Stale files
|
|
40
|
+
// Rule 2: Stale/legacy files
|
|
39
41
|
const staleFiles = composition.files_detail.filter(
|
|
40
|
-
(f) => f.status === "legacy",
|
|
42
|
+
(f) => f.status === "legacy" || f.status === "stale",
|
|
41
43
|
);
|
|
42
44
|
if (staleFiles.length > 0) {
|
|
43
45
|
const totalStaleTokens = staleFiles.reduce((sum, f) => sum + f.weight, 0);
|
|
44
46
|
insights.push({
|
|
45
47
|
id: "stale-files",
|
|
46
48
|
severity: staleFiles.length > 5 ? "warning" : "info",
|
|
47
|
-
title: `${staleFiles.length}
|
|
48
|
-
message:
|
|
49
|
+
title: `${staleFiles.length} inactive file(s) in context`,
|
|
50
|
+
message: `${staleFiles.length} file(s) accessed earlier in the session are still in context (~${totalStaleTokens.toLocaleString()} tokens). They are unlikely to be needed.`,
|
|
49
51
|
});
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// Rule 3: High overall usage
|
|
53
|
-
const usagePercent = Math.round((total.tokens /
|
|
55
|
+
const usagePercent = Math.round((total.tokens / windowSize) * 100);
|
|
54
56
|
if (usagePercent > 80) {
|
|
55
57
|
insights.push({
|
|
56
58
|
id: "high-usage",
|
|
57
59
|
severity: "critical",
|
|
58
60
|
title: "Context window nearly full",
|
|
59
|
-
message: `You are at ${usagePercent}% of
|
|
61
|
+
message: `You are at ${usagePercent}% of your ${(windowSize / 1000).toFixed(0)}k context window. Compaction or summarization is strongly recommended.`,
|
|
60
62
|
command: "/ultra-compact",
|
|
61
63
|
});
|
|
62
64
|
} else if (usagePercent > 60) {
|
|
@@ -64,7 +66,7 @@ export class InsightEngine {
|
|
|
64
66
|
id: "moderate-usage",
|
|
65
67
|
severity: "warning",
|
|
66
68
|
title: "Context usage is high",
|
|
67
|
-
message: `You are at ${usagePercent}% of
|
|
69
|
+
message: `You are at ${usagePercent}% of your ${(windowSize / 1000).toFixed(0)}k context window. Plan to compact before adding more files.`,
|
|
68
70
|
});
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -105,7 +107,7 @@ export class InsightEngine {
|
|
|
105
107
|
id: "healthy-context",
|
|
106
108
|
severity: "info",
|
|
107
109
|
title: "Context looks healthy",
|
|
108
|
-
message: `Your context composition is balanced and under ${usagePercent}% of
|
|
110
|
+
message: `Your context composition is balanced and under ${usagePercent}% of your ${(windowSize / 1000).toFixed(0)}k window.`,
|
|
109
111
|
});
|
|
110
112
|
}
|
|
111
113
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-map",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|