pi-context-map 0.6.1 → 0.7.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 +22 -0
- package/extensions/analyzer.ts +160 -37
- package/extensions/generator.ts +5 -1
- package/extensions/index.ts +122 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.0] - 2026-06-15
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
- **Fixed token accuracy**: Now uses Pi's actual token count from `ctx.getContextUsage()` instead of heuristic estimation. The usage percentage now matches Pi's terminal display.
|
|
6
|
+
- **Fixed compaction summary detection**: Now detects `role: "compactionSummary"` (Pi's actual format) with the `summary` field. Summaries no longer show 0%.
|
|
7
|
+
- **Fixed file status calculation**: Changed from turn-based to position-based. Files in the last 30% of messages are "active", middle 40% are "stale", first 30% are "legacy".
|
|
8
|
+
- **Removed error spam**: Silent error handling instead of console.error for non-critical failures.
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- **Auto-open browser**: Report automatically opens in default browser on first `/context-map` invocation.
|
|
12
|
+
- **Pi actual tokens in HTML**: Generator accepts and displays Pi's real token count.
|
|
13
|
+
|
|
14
|
+
## [0.6.2] - 2026-06-15
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
- **Fixed Pi message format**: Now uses `type: "toolCall"` (not `"tool_use"`) and `toolCallId` (not `tool_call_id`) to match Pi's actual `@mariozechner/pi-ai` types.
|
|
17
|
+
- **System prompt detection**: Now accepts `systemPrompt` parameter from Pi's `ctx.getSystemPrompt()`. System slice no longer shows 0%.
|
|
18
|
+
- **Tool results detection**: Changed to `role === "toolResult"` to match Pi's actual message format.
|
|
19
|
+
- **File tracking from tool results**: Now extracts file paths from `toolResult` messages (read/write/edit tool results).
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
- **Message persistence**: Messages are saved via `appendEntry` on compaction to survive session reloads.
|
|
23
|
+
- **Enhanced diagnostics**: `/context-map` command now shows message count, system tokens, and tool tokens in the notification.
|
|
24
|
+
|
|
3
25
|
## [0.6.1] - 2026-06-15
|
|
4
26
|
### Bug Fixes
|
|
5
27
|
- **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()`.
|
package/extensions/analyzer.ts
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
* ContextAnalyzer
|
|
3
3
|
* Parses Pi session messages to identify the active working set of files,
|
|
4
4
|
* their token weights, and their temporal status.
|
|
5
|
+
*
|
|
6
|
+
* Pi message format (from @mariozechner/pi-ai + pi-coding-agent):
|
|
7
|
+
* UserMessage: { role: "user", content: string | (TextContent | ImageContent)[] }
|
|
8
|
+
* AssistantMessage: { role: "assistant", content: (TextContent | ThinkingContent | ToolCall)[] }
|
|
9
|
+
* ToolResultMessage: { role: "toolResult", toolCallId, toolName, content }
|
|
10
|
+
* CompactionSummaryMessage:{ role: "compactionSummary", summary: string, tokensBefore: number }
|
|
11
|
+
* BranchSummaryMessage: { role: "branchSummary", summary: string }
|
|
12
|
+
* BashExecutionMessage: { role: "bashExecution", command, output }
|
|
13
|
+
* CustomMessage: { role: "custom", customType, content }
|
|
14
|
+
*
|
|
15
|
+
* ToolCall: { type: "toolCall", id, name, arguments }
|
|
5
16
|
*/
|
|
6
17
|
import { TokenCounter } from "./token-counter";
|
|
7
18
|
|
|
@@ -31,12 +42,16 @@ export interface ContextComposition {
|
|
|
31
42
|
summaries: ContextSlice;
|
|
32
43
|
total: ContextSlice;
|
|
33
44
|
files_detail: FileContext[];
|
|
45
|
+
/** Pi's actual token count from ctx.getContextUsage() — may differ from heuristic total */
|
|
46
|
+
actualTokens?: number | null;
|
|
47
|
+
actualPercent?: number | null;
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
export class ContextAnalyzer {
|
|
37
51
|
public analyzeByType(
|
|
38
52
|
messages: any[],
|
|
39
53
|
currentTurn: number,
|
|
54
|
+
systemPrompt?: string,
|
|
40
55
|
): ContextComposition {
|
|
41
56
|
const fileRegistry = new Map<string, FileContext>();
|
|
42
57
|
|
|
@@ -46,42 +61,112 @@ export class ContextAnalyzer {
|
|
|
46
61
|
let fileTokens = 0;
|
|
47
62
|
let summaryTokens = 0;
|
|
48
63
|
|
|
64
|
+
// Count system prompt tokens if provided
|
|
65
|
+
if (systemPrompt && systemPrompt.length > 0) {
|
|
66
|
+
systemTokens += TokenCounter.count(systemPrompt);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Track message indices for status calculation
|
|
70
|
+
const totalMessages = messages.length;
|
|
71
|
+
|
|
49
72
|
for (let index = 0; index < messages.length; index++) {
|
|
50
73
|
const msg = messages[index];
|
|
51
74
|
const turn = index + 1;
|
|
75
|
+
|
|
76
|
+
// Normalize role — Pi may use different role strings
|
|
52
77
|
const role = msg.role || "";
|
|
53
|
-
const msgType = msg.type || "";
|
|
54
78
|
|
|
55
|
-
// 1. Compaction summaries
|
|
79
|
+
// 1. Compaction summaries (Pi uses role="compactionSummary" with summary field)
|
|
56
80
|
if (
|
|
81
|
+
role === "compactionSummary" ||
|
|
57
82
|
role === "compaction" ||
|
|
58
|
-
|
|
83
|
+
msg.type === "compaction" ||
|
|
59
84
|
msg.customType === "compaction" ||
|
|
60
85
|
msg.compactionEntry
|
|
61
86
|
) {
|
|
62
|
-
|
|
87
|
+
// Use the summary field if available, otherwise fall back to content
|
|
88
|
+
const summaryText =
|
|
89
|
+
typeof msg.summary === "string"
|
|
90
|
+
? msg.summary
|
|
91
|
+
: typeof msg.content === "string"
|
|
92
|
+
? msg.content
|
|
93
|
+
: JSON.stringify(msg.content || msg);
|
|
94
|
+
summaryTokens += TokenCounter.count(summaryText);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. Branch summaries
|
|
99
|
+
if (role === "branchSummary") {
|
|
100
|
+
const summaryText =
|
|
101
|
+
typeof msg.summary === "string" ? msg.summary : JSON.stringify(msg);
|
|
102
|
+
summaryTokens += TokenCounter.count(summaryText);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Bash executions
|
|
107
|
+
if (role === "bashExecution") {
|
|
108
|
+
toolTokens += TokenCounter.countMessage(msg);
|
|
63
109
|
continue;
|
|
64
110
|
}
|
|
65
111
|
|
|
66
|
-
//
|
|
67
|
-
if (role === "
|
|
68
|
-
|
|
112
|
+
// 4. Custom messages (extensions)
|
|
113
|
+
if (role === "custom") {
|
|
114
|
+
// Categorize based on customType
|
|
115
|
+
const customType = msg.customType || "";
|
|
116
|
+
if (
|
|
117
|
+
customType.includes("compaction") ||
|
|
118
|
+
customType.includes("summary")
|
|
119
|
+
) {
|
|
120
|
+
summaryTokens += TokenCounter.countMessage(msg);
|
|
121
|
+
} else {
|
|
122
|
+
historyTokens += TokenCounter.countMessage(msg);
|
|
123
|
+
}
|
|
69
124
|
continue;
|
|
70
125
|
}
|
|
71
126
|
|
|
72
|
-
//
|
|
73
|
-
if (role === "toolResult"
|
|
127
|
+
// 5. Tool results (Pi uses role="toolResult")
|
|
128
|
+
if (role === "toolResult") {
|
|
74
129
|
toolTokens += TokenCounter.countMessage(msg);
|
|
130
|
+
// Track file content from tool results
|
|
131
|
+
const toolName = msg.toolName || "";
|
|
132
|
+
if (
|
|
133
|
+
toolName === "read" ||
|
|
134
|
+
toolName === "write" ||
|
|
135
|
+
toolName === "edit"
|
|
136
|
+
) {
|
|
137
|
+
const content = msg.content;
|
|
138
|
+
const path = this.extractPathFromToolResult(content);
|
|
139
|
+
if (path) {
|
|
140
|
+
const w = TokenCounter.countMessage(msg);
|
|
141
|
+
fileTokens += w;
|
|
142
|
+
if (!fileRegistry.has(path)) {
|
|
143
|
+
fileRegistry.set(path, {
|
|
144
|
+
path,
|
|
145
|
+
weight: w,
|
|
146
|
+
lastOp: {
|
|
147
|
+
type: this.getOpType(toolName),
|
|
148
|
+
turn,
|
|
149
|
+
timestamp: msg.timestamp || Date.now(),
|
|
150
|
+
},
|
|
151
|
+
status: this.calculateStatus(
|
|
152
|
+
index,
|
|
153
|
+
totalMessages,
|
|
154
|
+
),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
75
159
|
continue;
|
|
76
160
|
}
|
|
77
161
|
|
|
78
|
-
//
|
|
162
|
+
// 6. User messages
|
|
79
163
|
if (role === "user") {
|
|
80
164
|
historyTokens += TokenCounter.countMessage(msg);
|
|
165
|
+
// Track file attachments (images, file paths in text)
|
|
81
166
|
if (Array.isArray(msg.content)) {
|
|
82
167
|
for (const block of msg.content) {
|
|
83
|
-
if (block.type === "image"
|
|
84
|
-
const p =
|
|
168
|
+
if (block.type === "image") {
|
|
169
|
+
const p = "[image]";
|
|
85
170
|
const w = TokenCounter.count(JSON.stringify(block));
|
|
86
171
|
fileTokens += w;
|
|
87
172
|
if (!fileRegistry.has(p)) {
|
|
@@ -93,7 +178,10 @@ export class ContextAnalyzer {
|
|
|
93
178
|
turn,
|
|
94
179
|
timestamp: msg.timestamp || Date.now(),
|
|
95
180
|
},
|
|
96
|
-
status: this.calculateStatus(
|
|
181
|
+
status: this.calculateStatus(
|
|
182
|
+
index,
|
|
183
|
+
totalMessages,
|
|
184
|
+
),
|
|
97
185
|
});
|
|
98
186
|
}
|
|
99
187
|
}
|
|
@@ -112,7 +200,10 @@ export class ContextAnalyzer {
|
|
|
112
200
|
turn,
|
|
113
201
|
timestamp: msg.timestamp || Date.now(),
|
|
114
202
|
},
|
|
115
|
-
status: this.calculateStatus(
|
|
203
|
+
status: this.calculateStatus(
|
|
204
|
+
index,
|
|
205
|
+
totalMessages,
|
|
206
|
+
),
|
|
116
207
|
});
|
|
117
208
|
}
|
|
118
209
|
}
|
|
@@ -123,19 +214,25 @@ export class ContextAnalyzer {
|
|
|
123
214
|
continue;
|
|
124
215
|
}
|
|
125
216
|
|
|
126
|
-
//
|
|
217
|
+
// 7. Assistant messages — track toolCall blocks
|
|
127
218
|
if (role === "assistant") {
|
|
128
219
|
historyTokens += TokenCounter.countMessage(msg);
|
|
129
220
|
if (Array.isArray(msg.content)) {
|
|
130
221
|
for (const block of msg.content) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const p = this.extractPath(block.name,
|
|
222
|
+
// Pi uses type="toolCall" with id, name, arguments
|
|
223
|
+
if (block.type === "toolCall") {
|
|
224
|
+
const p = this.extractPath(block.name, block.arguments);
|
|
134
225
|
if (p) {
|
|
135
226
|
const opType = this.getOpType(block.name);
|
|
136
|
-
const result = this.findToolResult(
|
|
227
|
+
const result = this.findToolResult(
|
|
228
|
+
messages,
|
|
229
|
+
index,
|
|
230
|
+
block.id,
|
|
231
|
+
);
|
|
137
232
|
const content = result?.content || "";
|
|
138
|
-
const w = TokenCounter.count(
|
|
233
|
+
const w = TokenCounter.count(
|
|
234
|
+
String(JSON.stringify(content)),
|
|
235
|
+
);
|
|
139
236
|
fileTokens += w;
|
|
140
237
|
fileRegistry.set(p, {
|
|
141
238
|
path: p,
|
|
@@ -145,7 +242,10 @@ export class ContextAnalyzer {
|
|
|
145
242
|
turn,
|
|
146
243
|
timestamp: msg.timestamp || Date.now(),
|
|
147
244
|
},
|
|
148
|
-
status: this.calculateStatus(
|
|
245
|
+
status: this.calculateStatus(
|
|
246
|
+
index,
|
|
247
|
+
totalMessages,
|
|
248
|
+
),
|
|
149
249
|
});
|
|
150
250
|
}
|
|
151
251
|
}
|
|
@@ -154,7 +254,7 @@ export class ContextAnalyzer {
|
|
|
154
254
|
continue;
|
|
155
255
|
}
|
|
156
256
|
|
|
157
|
-
//
|
|
257
|
+
// 8. Everything else
|
|
158
258
|
historyTokens += TokenCounter.countMessage(msg);
|
|
159
259
|
}
|
|
160
260
|
|
|
@@ -163,7 +263,8 @@ export class ContextAnalyzer {
|
|
|
163
263
|
|
|
164
264
|
const mk = (tokens: number): ContextSlice => ({
|
|
165
265
|
tokens: Math.ceil(tokens),
|
|
166
|
-
percent:
|
|
266
|
+
percent:
|
|
267
|
+
totalTokens > 0 ? Math.round((tokens / totalTokens) * 100) : 0,
|
|
167
268
|
});
|
|
168
269
|
|
|
169
270
|
const files_detail = Array.from(fileRegistry.values())
|
|
@@ -186,12 +287,13 @@ export class ContextAnalyzer {
|
|
|
186
287
|
return this.analyzeByType(messages, currentTurn);
|
|
187
288
|
}
|
|
188
289
|
|
|
189
|
-
private extractPath(toolName: string,
|
|
290
|
+
private extractPath(toolName: string, args: any): string | null {
|
|
291
|
+
if (!args || typeof args !== "object") return null;
|
|
190
292
|
if (toolName === "read" || toolName === "write" || toolName === "edit") {
|
|
191
|
-
return typeof
|
|
293
|
+
return typeof args.path === "string" ? args.path : null;
|
|
192
294
|
}
|
|
193
295
|
if (toolName === "bash") {
|
|
194
|
-
const match =
|
|
296
|
+
const match = args.command?.match(
|
|
195
297
|
/(?:cat|ls|rm|mv|cp|vi|nano)\s+([^\s;]+)/,
|
|
196
298
|
);
|
|
197
299
|
return match ? match[1] : null;
|
|
@@ -199,6 +301,21 @@ export class ContextAnalyzer {
|
|
|
199
301
|
return null;
|
|
200
302
|
}
|
|
201
303
|
|
|
304
|
+
private extractPathFromToolResult(content: any): string | null {
|
|
305
|
+
if (typeof content === "string") return null;
|
|
306
|
+
if (Array.isArray(content)) {
|
|
307
|
+
for (const block of content) {
|
|
308
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
309
|
+
const match = block.text.match(
|
|
310
|
+
/(?:\/|[A-Z]:\\)[\w./\\-]+\.\w+/,
|
|
311
|
+
);
|
|
312
|
+
if (match) return match[0];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
202
319
|
private getOpType(toolName: string): FileOp["type"] {
|
|
203
320
|
switch (toolName) {
|
|
204
321
|
case "write":
|
|
@@ -212,13 +329,20 @@ export class ContextAnalyzer {
|
|
|
212
329
|
}
|
|
213
330
|
}
|
|
214
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Calculate file status based on position in message array.
|
|
334
|
+
* Files near the end are "active", middle are "stale", beginning are "legacy".
|
|
335
|
+
* This is more reliable than turn-based calculation since the context event
|
|
336
|
+
* replaces all messages at once.
|
|
337
|
+
*/
|
|
215
338
|
private calculateStatus(
|
|
216
|
-
|
|
217
|
-
|
|
339
|
+
messageIndex: number,
|
|
340
|
+
totalMessages: number,
|
|
218
341
|
): FileContext["status"] {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (
|
|
342
|
+
if (totalMessages === 0) return "legacy";
|
|
343
|
+
const ratio = messageIndex / totalMessages;
|
|
344
|
+
if (ratio >= 0.7) return "active";
|
|
345
|
+
if (ratio >= 0.3) return "stale";
|
|
222
346
|
return "legacy";
|
|
223
347
|
}
|
|
224
348
|
|
|
@@ -228,13 +352,12 @@ export class ContextAnalyzer {
|
|
|
228
352
|
toolId: string,
|
|
229
353
|
): any {
|
|
230
354
|
for (let i = toolTurnIndex + 1; i < messages.length; i++) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return messages[i];
|
|
355
|
+
const m = messages[i];
|
|
356
|
+
// Pi uses role="toolResult" and toolCallId (not tool_call_id)
|
|
357
|
+
if (m.role === "toolResult" && m.toolCallId === toolId) {
|
|
358
|
+
return m;
|
|
236
359
|
}
|
|
237
|
-
if (
|
|
360
|
+
if (m.role === "assistant") break;
|
|
238
361
|
}
|
|
239
362
|
return null;
|
|
240
363
|
}
|
package/extensions/generator.ts
CHANGED
|
@@ -15,8 +15,12 @@ export class ReportGenerator {
|
|
|
15
15
|
composition: ContextComposition,
|
|
16
16
|
insights: Insight[],
|
|
17
17
|
contextWindow: number = 128_000,
|
|
18
|
+
actualTokens?: number | null,
|
|
18
19
|
): string {
|
|
19
|
-
|
|
20
|
+
// Use Pi's actual token count when available
|
|
21
|
+
const total = actualTokens != null && actualTokens > 0
|
|
22
|
+
? actualTokens
|
|
23
|
+
: composition.total.tokens;
|
|
20
24
|
const usagePercent =
|
|
21
25
|
total > 0 ? Math.round((total / contextWindow) * 100) : 0;
|
|
22
26
|
|
package/extensions/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-context-map
|
|
3
3
|
* Professional Context Profiler for Pi.
|
|
4
|
-
* v0.
|
|
4
|
+
* v0.7.0 — Fixed token accuracy (uses Pi's actual count), compactionSummary detection,
|
|
5
|
+
* auto-open browser, position-based file status, error cleanup.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type {
|
|
@@ -16,6 +17,7 @@ import { LiveReportServer } from "./live-server";
|
|
|
16
17
|
import * as path from "node:path";
|
|
17
18
|
import * as fs from "node:fs";
|
|
18
19
|
import * as os from "node:os";
|
|
20
|
+
import { exec } from "node:child_process";
|
|
19
21
|
|
|
20
22
|
function makeReportPath(sessionName?: string): string {
|
|
21
23
|
const dir = path.join(os.homedir(), ".pi", "context-map");
|
|
@@ -25,11 +27,24 @@ function makeReportPath(sessionName?: string): string {
|
|
|
25
27
|
const now = new Date();
|
|
26
28
|
const date = now.toISOString().split("T")[0];
|
|
27
29
|
const time = now.toTimeString().split(" ")[0].replace(/:/g, "-");
|
|
28
|
-
const safe = (sessionName || "session")
|
|
30
|
+
const safe = (sessionName || "session")
|
|
31
|
+
.replace(/[^\w.-]/g, "_")
|
|
32
|
+
.slice(0, 40);
|
|
29
33
|
const filename = `${date}_${time}_${safe}.html`;
|
|
30
34
|
return path.join(dir, filename);
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
function openBrowser(url: string): void {
|
|
38
|
+
const platform = process.platform;
|
|
39
|
+
if (platform === "win32") {
|
|
40
|
+
exec(`start "" "${url}"`);
|
|
41
|
+
} else if (platform === "darwin") {
|
|
42
|
+
exec(`open "${url}"`);
|
|
43
|
+
} else {
|
|
44
|
+
exec(`xdg-open "${url}"`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
34
49
|
const analyzer = new ContextAnalyzer();
|
|
35
50
|
const liveServer = new LiveReportServer();
|
|
@@ -37,21 +52,43 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
37
52
|
let sessionMessages: AgentMessage[] = [];
|
|
38
53
|
let currentTurn = 0;
|
|
39
54
|
let contextWindow = 128_000;
|
|
55
|
+
let actualTokens: number | null = null;
|
|
56
|
+
let actualPercent: number | null = null;
|
|
57
|
+
let systemPrompt = "";
|
|
40
58
|
let currentReportPath = makeReportPath();
|
|
59
|
+
let isFirstRun = true;
|
|
41
60
|
|
|
42
|
-
// Capture messages
|
|
61
|
+
// Capture messages, context window, system prompt, and actual token count from Pi
|
|
43
62
|
pi.on("context", (event: any, ctx: any) => {
|
|
44
63
|
if (event?.messages && Array.isArray(event.messages)) {
|
|
45
64
|
sessionMessages = event.messages;
|
|
46
65
|
}
|
|
47
66
|
try {
|
|
48
67
|
const usage = ctx?.getContextUsage?.();
|
|
49
|
-
if (usage
|
|
50
|
-
contextWindow
|
|
68
|
+
if (usage) {
|
|
69
|
+
if (usage.contextWindow && usage.contextWindow > 0) {
|
|
70
|
+
contextWindow = usage.contextWindow;
|
|
71
|
+
}
|
|
72
|
+
// Use Pi's actual token count — this is the real value
|
|
73
|
+
if (usage.tokens != null && usage.tokens > 0) {
|
|
74
|
+
actualTokens = usage.tokens;
|
|
75
|
+
}
|
|
76
|
+
if (usage.percent != null && usage.percent > 0) {
|
|
77
|
+
actualPercent = usage.percent;
|
|
78
|
+
}
|
|
51
79
|
}
|
|
52
80
|
} catch {
|
|
53
81
|
// Keep fallback
|
|
54
82
|
}
|
|
83
|
+
// Get system prompt from Pi
|
|
84
|
+
try {
|
|
85
|
+
const sp = ctx?.getSystemPrompt?.();
|
|
86
|
+
if (sp && typeof sp === "string") {
|
|
87
|
+
systemPrompt = sp;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// Keep empty
|
|
91
|
+
}
|
|
55
92
|
});
|
|
56
93
|
|
|
57
94
|
pi.on("turn_start", () => {
|
|
@@ -61,6 +98,22 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
61
98
|
// Update report path when session changes
|
|
62
99
|
pi.on("session_start", () => {
|
|
63
100
|
currentReportPath = makeReportPath();
|
|
101
|
+
isFirstRun = true;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Persist messages on compaction so they survive reload
|
|
105
|
+
pi.on("session_compact", (event: any) => {
|
|
106
|
+
if (event?.compactionEntry) {
|
|
107
|
+
try {
|
|
108
|
+
pi.appendEntry("context-map-snapshot", {
|
|
109
|
+
messages: sessionMessages.slice(-50),
|
|
110
|
+
turn: currentTurn,
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore persistence errors
|
|
115
|
+
}
|
|
116
|
+
}
|
|
64
117
|
});
|
|
65
118
|
|
|
66
119
|
async function runAnalysis(): Promise<{
|
|
@@ -69,12 +122,45 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
69
122
|
reportPath: string;
|
|
70
123
|
}> {
|
|
71
124
|
const messages = sessionMessages.length > 0 ? sessionMessages : [];
|
|
72
|
-
const composition = analyzer.analyzeByType(
|
|
125
|
+
const composition = analyzer.analyzeByType(
|
|
126
|
+
messages,
|
|
127
|
+
currentTurn,
|
|
128
|
+
systemPrompt,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Override with Pi's actual token count when available
|
|
132
|
+
if (actualTokens != null && actualTokens > 0) {
|
|
133
|
+
composition.actualTokens = actualTokens;
|
|
134
|
+
composition.actualPercent = actualPercent;
|
|
135
|
+
// Recalculate percentages relative to actual total
|
|
136
|
+
const total = actualTokens;
|
|
137
|
+
if (total > 0) {
|
|
138
|
+
composition.system.percent = Math.round(
|
|
139
|
+
(composition.system.tokens / total) * 100,
|
|
140
|
+
);
|
|
141
|
+
composition.tools.percent = Math.round(
|
|
142
|
+
(composition.tools.tokens / total) * 100,
|
|
143
|
+
);
|
|
144
|
+
composition.history.percent = Math.round(
|
|
145
|
+
(composition.history.tokens / total) * 100,
|
|
146
|
+
);
|
|
147
|
+
composition.files.percent = Math.round(
|
|
148
|
+
(composition.files.tokens / total) * 100,
|
|
149
|
+
);
|
|
150
|
+
composition.summaries.percent = Math.round(
|
|
151
|
+
(composition.summaries.tokens / total) * 100,
|
|
152
|
+
);
|
|
153
|
+
// Use Pi's actual total for the usage calculation
|
|
154
|
+
composition.total.tokens = total;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
73
158
|
const insights = InsightEngine.generate(composition);
|
|
74
159
|
const html = ReportGenerator.generateHTML(
|
|
75
160
|
composition,
|
|
76
161
|
insights,
|
|
77
162
|
contextWindow,
|
|
163
|
+
actualTokens,
|
|
78
164
|
);
|
|
79
165
|
|
|
80
166
|
try {
|
|
@@ -84,7 +170,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
84
170
|
}
|
|
85
171
|
fs.writeFileSync(currentReportPath, html, "utf8");
|
|
86
172
|
} catch (err: any) {
|
|
87
|
-
|
|
173
|
+
// Silent — don't spam console
|
|
88
174
|
}
|
|
89
175
|
|
|
90
176
|
if (liveServer.isRunning) {
|
|
@@ -108,7 +194,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
108
194
|
|
|
109
195
|
ctx.ui.notify("Analyzing session context...", "info");
|
|
110
196
|
try {
|
|
111
|
-
const { insights, reportPath } = await runAnalysis();
|
|
197
|
+
const { composition, insights, reportPath } = await runAnalysis();
|
|
112
198
|
const criticalCount = insights.filter(
|
|
113
199
|
(i) => i.severity === "critical",
|
|
114
200
|
).length;
|
|
@@ -116,14 +202,27 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
116
202
|
criticalCount > 0
|
|
117
203
|
? `Context map generated. ${criticalCount} critical insight(s) found.`
|
|
118
204
|
: "Context map generated successfully.";
|
|
119
|
-
|
|
205
|
+
|
|
206
|
+
// Use Pi's actual percentage when available
|
|
207
|
+
const usageDisplay =
|
|
208
|
+
actualPercent != null
|
|
209
|
+
? `${actualPercent.toFixed(1)}%`
|
|
210
|
+
: `${composition.total.percent}%`;
|
|
211
|
+
|
|
212
|
+
let details = `Usage: ${usageDisplay} of ${(contextWindow / 1000).toFixed(0)}k`;
|
|
120
213
|
if (serverUrl) {
|
|
121
|
-
details += ` |
|
|
214
|
+
details += ` | ${serverUrl}`;
|
|
122
215
|
}
|
|
123
216
|
ctx.ui.notify(
|
|
124
217
|
`${summary} ${details}`,
|
|
125
218
|
criticalCount > 0 ? "warning" : "success",
|
|
126
219
|
);
|
|
220
|
+
|
|
221
|
+
// Auto-open browser on first run
|
|
222
|
+
if (isFirstRun && serverUrl) {
|
|
223
|
+
openBrowser(serverUrl);
|
|
224
|
+
isFirstRun = false;
|
|
225
|
+
}
|
|
127
226
|
} catch (error: any) {
|
|
128
227
|
ctx.ui.notify(
|
|
129
228
|
`Failed to generate context map: ${error.message}`,
|
|
@@ -137,7 +236,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
137
236
|
name: "context-map",
|
|
138
237
|
label: "Context Map",
|
|
139
238
|
description:
|
|
140
|
-
"Analyze the current session context composition and return actionable insights.
|
|
239
|
+
"Analyze the current session context composition and return actionable insights.",
|
|
141
240
|
parameters: {
|
|
142
241
|
type: "object",
|
|
143
242
|
properties: {},
|
|
@@ -151,23 +250,28 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
151
250
|
try {
|
|
152
251
|
const { composition, insights, reportPath } = await runAnalysis();
|
|
153
252
|
const usagePercent =
|
|
154
|
-
|
|
155
|
-
?
|
|
156
|
-
: 0
|
|
253
|
+
actualPercent != null
|
|
254
|
+
? actualPercent
|
|
255
|
+
: composition.total.tokens > 0
|
|
256
|
+
? Math.round(
|
|
257
|
+
(composition.total.tokens / contextWindow) * 100,
|
|
258
|
+
)
|
|
259
|
+
: 0;
|
|
157
260
|
const summary =
|
|
158
|
-
`Context: ${composition.total.tokens.toLocaleString()} tokens
|
|
261
|
+
`Context: ${composition.total.tokens.toLocaleString()} tokens (${usagePercent.toFixed(1)}% of ${(contextWindow / 1000).toFixed(0)}k). ` +
|
|
159
262
|
`System ${composition.system.percent}%, Tools ${composition.tools.percent}%, ` +
|
|
160
263
|
`History ${composition.history.percent}%, Files ${composition.files.percent}%, ` +
|
|
161
264
|
`Summaries ${composition.summaries.percent}%. ` +
|
|
162
|
-
`
|
|
163
|
-
`${insights.length} insight(s)
|
|
265
|
+
`Messages: ${sessionMessages.length}. ` +
|
|
266
|
+
`${insights.length} insight(s).`;
|
|
164
267
|
return {
|
|
165
268
|
type: "text" as const,
|
|
166
269
|
content: [
|
|
167
270
|
summary,
|
|
168
271
|
"",
|
|
169
272
|
...insights.map(
|
|
170
|
-
(i) =>
|
|
273
|
+
(i) =>
|
|
274
|
+
`[${i.severity.toUpperCase()}] ${i.title}: ${i.message}`,
|
|
171
275
|
),
|
|
172
276
|
`Report: ${reportPath}`,
|
|
173
277
|
serverUrl ? `Live: ${serverUrl}` : "",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-map",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|