pi-context-map 0.5.0 → 0.6.1
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 +103 -54
- package/extensions/generator.ts +20 -26
- package/extensions/index.ts +32 -35
- package/extensions/live-server.ts +9 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.1] - 2026-06-15
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
- **Fixed libuv assertion on Windows**: Removed `process.on('exit')` handler and `process.exit(0)` calls that left server handles open. Server now closes synchronously via `closeAllConnections()`.
|
|
6
|
+
- **Synchronous stop()**: `isRunning` returns `false` immediately after `stop()` instead of after async callback.
|
|
7
|
+
|
|
8
|
+
## [0.6.0] - 2026-06-15
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
- **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%.
|
|
11
|
+
- **File attachment detection**: Now detects images (type: "image") and file paths in user messages, not just assistant tool_use blocks.
|
|
12
|
+
- **Compaction summary detection**: Improved detection of Pi compaction entries via `customType` field.
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
- **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.
|
|
16
|
+
- **Auto-report path on session start**: New session gets a fresh report path automatically.
|
|
17
|
+
|
|
18
|
+
## [0.5.1] - 2026-06-15
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
- **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.
|
|
21
|
+
- **Toggle moved to corner**: Button is now fixed in the top-right corner of the page, not inline with the live badge.
|
|
22
|
+
- **Event delegation**: Theme toggle click handler uses event delegation so it survives SSE body replacement without re-binding.
|
|
23
|
+
|
|
3
24
|
## [0.5.0] - 2026-06-15
|
|
4
25
|
### Features
|
|
5
26
|
- **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.
|
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,66 +46,117 @@ 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 = block.source?.url || block.image_url?.url || "[image]";
|
|
85
|
+
const w = TokenCounter.count(JSON.stringify(block));
|
|
86
|
+
fileTokens += w;
|
|
87
|
+
if (!fileRegistry.has(p)) {
|
|
88
|
+
fileRegistry.set(p, {
|
|
89
|
+
path: p,
|
|
90
|
+
weight: w,
|
|
91
|
+
lastOp: {
|
|
92
|
+
type: "read",
|
|
93
|
+
turn,
|
|
94
|
+
timestamp: msg.timestamp || Date.now(),
|
|
95
|
+
},
|
|
96
|
+
status: this.calculateStatus(turn, currentTurn),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
101
|
+
const matches = block.text.match(
|
|
102
|
+
/(?:\/|[A-Z]:\\)[\w./\\-]+\.\w+/g,
|
|
103
|
+
);
|
|
104
|
+
if (matches) {
|
|
105
|
+
for (const m of matches) {
|
|
106
|
+
if (!fileRegistry.has(m)) {
|
|
107
|
+
fileRegistry.set(m, {
|
|
108
|
+
path: m,
|
|
109
|
+
weight: TokenCounter.count(m),
|
|
110
|
+
lastOp: {
|
|
111
|
+
type: "read",
|
|
112
|
+
turn,
|
|
113
|
+
timestamp: msg.timestamp || Date.now(),
|
|
114
|
+
},
|
|
115
|
+
status: this.calculateStatus(turn, currentTurn),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
83
124
|
}
|
|
84
125
|
|
|
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
|
-
|
|
126
|
+
// 5. Assistant messages — track tool_use blocks
|
|
127
|
+
if (role === "assistant") {
|
|
128
|
+
historyTokens += TokenCounter.countMessage(msg);
|
|
129
|
+
if (Array.isArray(msg.content)) {
|
|
130
|
+
for (const block of msg.content) {
|
|
131
|
+
if (block.type === "tool_use") {
|
|
132
|
+
const input = block.input as Record<string, any>;
|
|
133
|
+
const p = this.extractPath(block.name, input);
|
|
134
|
+
if (p) {
|
|
135
|
+
const opType = this.getOpType(block.name);
|
|
136
|
+
const result = this.findToolResult(messages, index, block.id);
|
|
137
|
+
const content = result?.content || "";
|
|
138
|
+
const w = TokenCounter.count(String(content));
|
|
139
|
+
fileTokens += w;
|
|
140
|
+
fileRegistry.set(p, {
|
|
141
|
+
path: p,
|
|
142
|
+
weight: w,
|
|
143
|
+
lastOp: {
|
|
144
|
+
type: opType,
|
|
145
|
+
turn,
|
|
146
|
+
timestamp: msg.timestamp || Date.now(),
|
|
147
|
+
},
|
|
148
|
+
status: this.calculateStatus(turn, currentTurn),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
109
151
|
}
|
|
110
152
|
}
|
|
111
153
|
}
|
|
154
|
+
continue;
|
|
112
155
|
}
|
|
113
|
-
|
|
156
|
+
|
|
157
|
+
// 6. Everything else
|
|
158
|
+
historyTokens += TokenCounter.countMessage(msg);
|
|
159
|
+
}
|
|
114
160
|
|
|
115
161
|
const totalTokens =
|
|
116
162
|
systemTokens + toolTokens + historyTokens + fileTokens + summaryTokens;
|
|
@@ -182,7 +228,10 @@ export class ContextAnalyzer {
|
|
|
182
228
|
toolId: string,
|
|
183
229
|
): any {
|
|
184
230
|
for (let i = toolTurnIndex + 1; i < messages.length; i++) {
|
|
185
|
-
if (
|
|
231
|
+
if (
|
|
232
|
+
messages[i].role === "toolResult" &&
|
|
233
|
+
messages[i].tool_call_id === toolId
|
|
234
|
+
) {
|
|
186
235
|
return messages[i];
|
|
187
236
|
}
|
|
188
237
|
if (messages[i].role === "assistant") break;
|
package/extensions/generator.ts
CHANGED
|
@@ -17,7 +17,8 @@ export class ReportGenerator {
|
|
|
17
17
|
contextWindow: number = 128_000,
|
|
18
18
|
): string {
|
|
19
19
|
const total = composition.total.tokens;
|
|
20
|
-
const usagePercent =
|
|
20
|
+
const usagePercent =
|
|
21
|
+
total > 0 ? Math.round((total / contextWindow) * 100) : 0;
|
|
21
22
|
|
|
22
23
|
const fileCards = composition.files_detail
|
|
23
24
|
.map(
|
|
@@ -503,14 +504,12 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
503
504
|
<div class="container">
|
|
504
505
|
|
|
505
506
|
<header>
|
|
506
|
-
<
|
|
507
|
-
<
|
|
508
|
-
<
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
</button>
|
|
513
|
-
</div>
|
|
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>
|
|
514
513
|
<h1>Context Profiler</h1>
|
|
515
514
|
<p class="subtitle">Session context window breakdown with actionable recommendations</p>
|
|
516
515
|
|
|
@@ -623,20 +622,23 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
623
622
|
update();
|
|
624
623
|
|
|
625
624
|
// 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
625
|
function applyTheme(t) {
|
|
631
626
|
document.documentElement.setAttribute('data-theme', t);
|
|
632
627
|
localStorage.setItem('context-map-theme', t);
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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' : '';
|
|
636
635
|
}
|
|
637
636
|
var saved = localStorage.getItem('context-map-theme');
|
|
638
637
|
applyTheme(saved || 'light');
|
|
639
|
-
|
|
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;
|
|
640
642
|
var cur = document.documentElement.getAttribute('data-theme');
|
|
641
643
|
applyTheme(cur === 'dark' ? 'light' : 'dark');
|
|
642
644
|
});
|
|
@@ -673,15 +675,7 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
673
675
|
}
|
|
674
676
|
// Restore theme after body replacement
|
|
675
677
|
applyTheme(currentTheme);
|
|
676
|
-
// Re-bind
|
|
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
|
|
678
|
+
// Re-bind file search/filter (direct listeners, not delegated)
|
|
685
679
|
var ns = document.getElementById('fileSearch');
|
|
686
680
|
var nf = document.getElementById('fileFilter');
|
|
687
681
|
if (ns) ns.addEventListener('input', update);
|
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,46 +14,55 @@ 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").replace(/[^\w.-]/g, "_").slice(0, 40);
|
|
29
|
+
const filename = `${date}_${time}_${safe}.html`;
|
|
30
|
+
return path.join(dir, filename);
|
|
31
|
+
}
|
|
25
32
|
|
|
26
33
|
export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
27
34
|
const analyzer = new ContextAnalyzer();
|
|
28
35
|
const liveServer = new LiveReportServer();
|
|
29
36
|
|
|
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
37
|
let sessionMessages: AgentMessage[] = [];
|
|
33
38
|
let currentTurn = 0;
|
|
34
|
-
let contextWindow = 128_000;
|
|
39
|
+
let contextWindow = 128_000;
|
|
40
|
+
let currentReportPath = makeReportPath();
|
|
35
41
|
|
|
36
|
-
// Capture messages and context
|
|
42
|
+
// Capture messages and context window from Pi system
|
|
37
43
|
pi.on("context", (event: any, ctx: any) => {
|
|
38
44
|
if (event?.messages && Array.isArray(event.messages)) {
|
|
39
45
|
sessionMessages = event.messages;
|
|
40
46
|
}
|
|
41
|
-
// Fetch actual context window from Pi system
|
|
42
47
|
try {
|
|
43
48
|
const usage = ctx?.getContextUsage?.();
|
|
44
49
|
if (usage?.contextWindow && usage.contextWindow > 0) {
|
|
45
50
|
contextWindow = usage.contextWindow;
|
|
46
51
|
}
|
|
47
52
|
} catch {
|
|
48
|
-
//
|
|
53
|
+
// Keep fallback
|
|
49
54
|
}
|
|
50
55
|
});
|
|
51
56
|
|
|
52
|
-
// Track turns
|
|
53
57
|
pi.on("turn_start", () => {
|
|
54
58
|
currentTurn++;
|
|
55
59
|
});
|
|
56
60
|
|
|
61
|
+
// Update report path when session changes
|
|
62
|
+
pi.on("session_start", () => {
|
|
63
|
+
currentReportPath = makeReportPath();
|
|
64
|
+
});
|
|
65
|
+
|
|
57
66
|
async function runAnalysis(): Promise<{
|
|
58
67
|
composition: ReturnType<typeof analyzer.analyzeByType>;
|
|
59
68
|
insights: ReturnType<typeof InsightEngine.generate>;
|
|
@@ -68,30 +77,25 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
68
77
|
contextWindow,
|
|
69
78
|
);
|
|
70
79
|
|
|
71
|
-
// Write to disk
|
|
72
80
|
try {
|
|
73
|
-
const
|
|
74
|
-
const dir = path.dirname(REPORT_PATH);
|
|
81
|
+
const dir = path.dirname(currentReportPath);
|
|
75
82
|
if (!fs.existsSync(dir)) {
|
|
76
83
|
fs.mkdirSync(dir, { recursive: true });
|
|
77
84
|
}
|
|
78
|
-
fs.writeFileSync(
|
|
85
|
+
fs.writeFileSync(currentReportPath, html, "utf8");
|
|
79
86
|
} catch (err: any) {
|
|
80
87
|
console.error(`[pi-context-map] Failed to write report: ${err.message}`);
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
// Push to live server if running
|
|
84
90
|
if (liveServer.isRunning) {
|
|
85
|
-
liveServer.update(html,
|
|
91
|
+
liveServer.update(html, currentReportPath);
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
return { composition, insights, reportPath:
|
|
94
|
+
return { composition, insights, reportPath: currentReportPath };
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
// Start live server
|
|
92
97
|
const serverUrl = await liveServer.start();
|
|
93
98
|
|
|
94
|
-
// Register /context-map command
|
|
95
99
|
pi.registerCommand("context-map", {
|
|
96
100
|
description:
|
|
97
101
|
"Generate a visual context map with actionable insights. Use 'stop' to terminate the live server.",
|
|
@@ -129,7 +133,6 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
129
133
|
},
|
|
130
134
|
});
|
|
131
135
|
|
|
132
|
-
// Register the tool for agent use
|
|
133
136
|
pi.registerTool({
|
|
134
137
|
name: "context-map",
|
|
135
138
|
label: "Context Map",
|
|
@@ -146,7 +149,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
146
149
|
_ctx: any,
|
|
147
150
|
) {
|
|
148
151
|
try {
|
|
149
|
-
const { composition, insights } = await runAnalysis();
|
|
152
|
+
const { composition, insights, reportPath } = await runAnalysis();
|
|
150
153
|
const usagePercent =
|
|
151
154
|
composition.total.tokens > 0
|
|
152
155
|
? Math.round((composition.total.tokens / contextWindow) * 100)
|
|
@@ -166,7 +169,8 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
166
169
|
...insights.map(
|
|
167
170
|
(i) => `[${i.severity.toUpperCase()}] ${i.title}: ${i.message}`,
|
|
168
171
|
),
|
|
169
|
-
|
|
172
|
+
`Report: ${reportPath}`,
|
|
173
|
+
serverUrl ? `Live: ${serverUrl}` : "",
|
|
170
174
|
]
|
|
171
175
|
.filter(Boolean)
|
|
172
176
|
.join("\n"),
|
|
@@ -181,41 +185,34 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
181
185
|
},
|
|
182
186
|
});
|
|
183
187
|
|
|
184
|
-
// Auto-warning on high context before compaction
|
|
185
188
|
pi.on("session_before_compact", (_event: any, ctx: any) => {
|
|
186
189
|
const tokens = _event?.preparation?.tokensBefore;
|
|
187
190
|
if (tokens && tokens > 100_000) {
|
|
188
191
|
ctx.ui.notify(
|
|
189
|
-
`High context load
|
|
192
|
+
`High context load (${(tokens / 1000).toFixed(1)}k tokens). Try /context-map to see what's consuming space.`,
|
|
190
193
|
"info",
|
|
191
194
|
);
|
|
192
195
|
}
|
|
193
196
|
});
|
|
194
197
|
|
|
195
|
-
// Auto-refresh after each assistant message if server is running
|
|
196
198
|
pi.on("message_end", async (_event: any) => {
|
|
197
199
|
if (_event?.message?.role === "assistant" && liveServer.isRunning) {
|
|
198
200
|
try {
|
|
199
201
|
await runAnalysis();
|
|
200
202
|
} catch {
|
|
201
|
-
//
|
|
203
|
+
// Ignore auto-refresh failures
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
});
|
|
205
207
|
|
|
206
|
-
// Graceful shutdown
|
|
207
208
|
pi.on("session_shutdown", () => {
|
|
208
209
|
liveServer.stop();
|
|
209
210
|
});
|
|
210
211
|
|
|
211
|
-
// Kill server when process exits
|
|
212
|
-
process.on("exit", () => liveServer.stop());
|
|
213
212
|
process.on("SIGINT", () => {
|
|
214
213
|
liveServer.stop();
|
|
215
|
-
process.exit(0);
|
|
216
214
|
});
|
|
217
215
|
process.on("SIGTERM", () => {
|
|
218
216
|
liveServer.stop();
|
|
219
|
-
process.exit(0);
|
|
220
217
|
});
|
|
221
218
|
}
|
|
@@ -94,18 +94,19 @@ export class LiveReportServer {
|
|
|
94
94
|
for (const client of this.clients) {
|
|
95
95
|
try {
|
|
96
96
|
client.end();
|
|
97
|
-
} catch
|
|
98
|
-
// Ignore
|
|
97
|
+
} catch {
|
|
98
|
+
// Ignore
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
this.clients.clear();
|
|
102
102
|
|
|
103
|
-
//
|
|
104
|
-
this.server.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
// Force-close all connections synchronously (Node 18.2+)
|
|
104
|
+
if (typeof this.server.closeAllConnections === "function") {
|
|
105
|
+
this.server.closeAllConnections();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Close server and reset state synchronously
|
|
109
|
+
this.server.close();
|
|
109
110
|
this.server = null;
|
|
110
111
|
this.port = 0;
|
|
111
112
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-map",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|