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 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()`.
@@ -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
- msgType === "compaction" ||
83
+ msg.type === "compaction" ||
59
84
  msg.customType === "compaction" ||
60
85
  msg.compactionEntry
61
86
  ) {
62
- summaryTokens += TokenCounter.countMessage(msg);
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
- // 2. System messages
67
- if (role === "system" || msgType === "system") {
68
- systemTokens += TokenCounter.countMessage(msg);
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
- // 3. Tool results (Pi uses "toolResult")
73
- if (role === "toolResult" || role === "tool") {
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
- // 4. User messages — track file attachments
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" || block.type === "image_url") {
84
- const p = block.source?.url || block.image_url?.url || "[image]";
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(turn, currentTurn),
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(turn, currentTurn),
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
- // 5. Assistant messages — track tool_use blocks
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
- if (block.type === "tool_use") {
132
- const input = block.input as Record<string, any>;
133
- const p = this.extractPath(block.name, input);
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(messages, index, block.id);
227
+ const result = this.findToolResult(
228
+ messages,
229
+ index,
230
+ block.id,
231
+ );
137
232
  const content = result?.content || "";
138
- const w = TokenCounter.count(String(content));
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(turn, currentTurn),
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
- // 6. Everything else
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: totalTokens > 0 ? Math.round((tokens / totalTokens) * 100) : 0,
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, input: any): string | null {
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 input.path === "string" ? input.path : null;
293
+ return typeof args.path === "string" ? args.path : null;
192
294
  }
193
295
  if (toolName === "bash") {
194
- const match = input.command?.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
- turn: number,
217
- currentTurn: number,
339
+ messageIndex: number,
340
+ totalMessages: number,
218
341
  ): FileContext["status"] {
219
- const diff = currentTurn - turn;
220
- if (diff <= 3) return "active";
221
- if (diff <= 10) return "stale";
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
- if (
232
- messages[i].role === "toolResult" &&
233
- messages[i].tool_call_id === toolId
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 (messages[i].role === "assistant") break;
360
+ if (m.role === "assistant") break;
238
361
  }
239
362
  return null;
240
363
  }
@@ -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
- const total = composition.total.tokens;
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
 
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * pi-context-map
3
3
  * Professional Context Profiler for Pi.
4
- * v0.5.1Dynamic context window, dark mode, session-unique reports.
4
+ * v0.7.0Fixed 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").replace(/[^\w.-]/g, "_").slice(0, 40);
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 and context window from Pi system
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?.contextWindow && usage.contextWindow > 0) {
50
- contextWindow = usage.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(messages, currentTurn);
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
- console.error(`[pi-context-map] Failed to write report: ${err.message}`);
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
- let details = `File: ${reportPath}`;
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 += ` | Live: ${serverUrl}`;
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. The live localhost report will auto-update.",
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
- composition.total.tokens > 0
155
- ? Math.round((composition.total.tokens / contextWindow) * 100)
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 total. ` +
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
- `Usage: ${usagePercent}% of ${(contextWindow / 1000).toFixed(0)}k window. ` +
163
- `${insights.length} insight(s) generated.`;
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) => `[${i.severity.toUpperCase()}] ${i.title}: ${i.message}`,
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.6.1",
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",