pi-context-map 0.2.0 → 0.3.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 +17 -0
- package/README.md +5 -1
- package/extensions/analyzer.ts +78 -50
- package/extensions/generator.ts +493 -58
- package/extensions/index.ts +67 -37
- package/extensions/insights.ts +114 -0
- package/extensions/token-counter.ts +149 -0
- package/extensions/types/pi-coding-agent.d.ts +1 -29
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2026-06-14
|
|
4
|
+
### Design & Interactivity Upgrade
|
|
5
|
+
- **Linear Design System**: Refactored CSS to use the Linear design tokens (canvas #010102, accent #5e6ad2) for a professional, near-black aesthetic.
|
|
6
|
+
- **shadcn/ui Card Patterns**: Insight cards now follow shadcn conventions (hairline borders, gradient backgrounds for severity).
|
|
7
|
+
- **Collapsible Insights**: Critical and warning insights are expanded by default; info insights are collapsed. Click to toggle.
|
|
8
|
+
- **File Search & Filter**: Added a real-time search input and status filter dropdown above the file grid. Shows match count and empty state.
|
|
9
|
+
- **Design Doc**: Added `docs/design.md` documenting the visual language, layout, and accessibility decisions.
|
|
10
|
+
|
|
11
|
+
## [0.3.0] - 2026-06-14
|
|
12
|
+
### Professional Context Profiler
|
|
13
|
+
- **Code-Aware Token Counting**: New `TokenCounter` module applies multipliers for code blocks (1.3x) and JSON (1.5x) for more accurate estimation.
|
|
14
|
+
- **Context Composition**: Refactored analyzer to break down context into System, Tools, History, Files, and Summaries slices.
|
|
15
|
+
- **Actionable Insights Engine**: New `InsightEngine` generates 6 built-in rules (tool bloat, stale files, high usage, file-heavy, summaries, system overhead).
|
|
16
|
+
- **Interactive HTML Report**: Stacked composition bar, color-coded insights section, and improved file cards.
|
|
17
|
+
- **Tool + Command**: Now registers as both a slash command (`/context-map`) and a tool for programmatic agent access.
|
|
18
|
+
- **Async Factory**: Updated to modern async pattern.
|
|
19
|
+
|
|
3
20
|
## [0.2.0] - 2026-06-14
|
|
4
21
|
### Professional Context Profiler
|
|
5
22
|
- **Architectural Modernization**: Migrated to source-shipping (`./extensions`) and async factory function pattern.
|
package/README.md
CHANGED
|
@@ -46,7 +46,11 @@ The extension categorizes files to help you manage context bloat:
|
|
|
46
46
|
1. **Scanning**: The analyzer iterates through the session history, identifying all `tool_use` calls involving file operations.
|
|
47
47
|
2. **Weighting**: It extracts the content length of tool results and applies a token heuristic (approx. 4 chars/token).
|
|
48
48
|
3. **Categorization**: It calculates the temporal distance between the current turn and the last file access.
|
|
49
|
-
4. **Visualization**: It generates a standalone HTML dashboard featuring a
|
|
49
|
+
4. **Visualization**: It generates a standalone HTML dashboard featuring a stacked composition bar, a file-weight grid with search/filter, and an interactive insights section.
|
|
50
|
+
|
|
51
|
+
## Design
|
|
52
|
+
|
|
53
|
+
The report uses the **Linear design system** (canvas `#010102`, accent `#5e6ad2`) with **shadcn/ui card patterns**. See `docs/design.md` for the full specification. The output is a single self-contained HTML file with no external dependencies.
|
|
50
54
|
|
|
51
55
|
## Compatibility
|
|
52
56
|
|
package/extensions/analyzer.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ContextAnalyzer
|
|
3
3
|
* Responsible for parsing Pi session messages to identify the active working set of files,
|
|
4
|
-
* their token weights, and their temporal status.
|
|
4
|
+
* their token weights, and their temporal status. Uses the code-aware TokenCounter.
|
|
5
5
|
*/
|
|
6
|
+
import { TokenCounter } from "./token-counter";
|
|
6
7
|
|
|
7
8
|
export interface FileOp {
|
|
8
9
|
type: "read" | "write" | "edit" | "delete";
|
|
@@ -17,45 +18,71 @@ export interface FileContext {
|
|
|
17
18
|
status: "active" | "stale" | "legacy";
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export interface
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
systemTokens: number;
|
|
24
|
-
historyTokens: number;
|
|
25
|
-
fileTokens: number;
|
|
26
|
-
toolTokens: number;
|
|
21
|
+
export interface ContextSlice {
|
|
22
|
+
tokens: number;
|
|
23
|
+
percent: number;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
export interface ContextComposition {
|
|
27
|
+
system: ContextSlice;
|
|
28
|
+
tools: ContextSlice;
|
|
29
|
+
history: ContextSlice;
|
|
30
|
+
files: ContextSlice;
|
|
31
|
+
summaries: ContextSlice;
|
|
32
|
+
total: ContextSlice;
|
|
33
|
+
files_detail: FileContext[];
|
|
34
|
+
}
|
|
34
35
|
|
|
36
|
+
export class ContextAnalyzer {
|
|
35
37
|
/**
|
|
36
|
-
* Analyze session messages to produce a
|
|
38
|
+
* Analyze session messages to produce a structured ContextComposition.
|
|
37
39
|
* @param messages The full session conversation history.
|
|
38
40
|
* @param currentTurn The current turn number.
|
|
39
41
|
*/
|
|
40
|
-
public
|
|
42
|
+
public analyzeByType(
|
|
43
|
+
messages: any[],
|
|
44
|
+
currentTurn: number,
|
|
45
|
+
): ContextComposition {
|
|
41
46
|
const fileRegistry = new Map<string, FileContext>();
|
|
42
|
-
|
|
43
|
-
let
|
|
47
|
+
|
|
48
|
+
let systemTokens = 0;
|
|
44
49
|
let toolTokens = 0;
|
|
50
|
+
let historyTokens = 0;
|
|
51
|
+
let fileTokens = 0;
|
|
52
|
+
let summaryTokens = 0;
|
|
45
53
|
|
|
46
54
|
messages.forEach((msg, index) => {
|
|
47
55
|
const turn = index + 1;
|
|
48
56
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const msgTokens = Math.ceil(
|
|
55
|
-
msgText.length / ContextAnalyzer.TOKEN_HEURISTIC,
|
|
56
|
-
);
|
|
57
|
-
totalTokens += msgTokens;
|
|
57
|
+
// 1. Categorize and count
|
|
58
|
+
if (msg.role === "system") {
|
|
59
|
+
systemTokens += TokenCounter.countMessage(msg);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
58
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)
|
|
69
|
+
if (
|
|
70
|
+
msg.role === "compaction" ||
|
|
71
|
+
msg.type === "compaction" ||
|
|
72
|
+
msg.compactionEntry
|
|
73
|
+
) {
|
|
74
|
+
summaryTokens += TokenCounter.countMessage(msg);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
79
|
+
historyTokens += TokenCounter.countMessage(msg);
|
|
80
|
+
} else {
|
|
81
|
+
// Default to history
|
|
82
|
+
historyTokens += TokenCounter.countMessage(msg);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. File tracking (only on assistant tool_use blocks)
|
|
59
86
|
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
60
87
|
for (const block of msg.content) {
|
|
61
88
|
if (block.type === "tool_use") {
|
|
@@ -64,15 +91,10 @@ export class ContextAnalyzer {
|
|
|
64
91
|
|
|
65
92
|
if (path) {
|
|
66
93
|
const opType = this.getOpType(block.name);
|
|
67
|
-
|
|
68
|
-
// If the file is already tracked, update it
|
|
69
|
-
|
|
70
|
-
// Find the tool result for this tool use to get actual content length
|
|
71
94
|
const result = this.findToolResult(messages, index, block.id);
|
|
72
95
|
const content = result?.content || "";
|
|
73
|
-
const weight =
|
|
74
|
-
|
|
75
|
-
);
|
|
96
|
+
const weight = TokenCounter.count(String(content));
|
|
97
|
+
fileTokens += weight;
|
|
76
98
|
|
|
77
99
|
fileRegistry.set(path, {
|
|
78
100
|
path,
|
|
@@ -88,33 +110,41 @@ export class ContextAnalyzer {
|
|
|
88
110
|
}
|
|
89
111
|
}
|
|
90
112
|
}
|
|
113
|
+
});
|
|
91
114
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
115
|
+
const totalTokens =
|
|
116
|
+
systemTokens + toolTokens + historyTokens + fileTokens + summaryTokens;
|
|
117
|
+
|
|
118
|
+
const mk = (tokens: number): ContextSlice => ({
|
|
119
|
+
tokens: Math.ceil(tokens),
|
|
120
|
+
percent: totalTokens > 0 ? Math.round((tokens / totalTokens) * 100) : 0,
|
|
97
121
|
});
|
|
98
122
|
|
|
99
|
-
const
|
|
100
|
-
|
|
123
|
+
const files_detail = Array.from(fileRegistry.values())
|
|
124
|
+
.sort((a, b) => b.weight - a.weight)
|
|
125
|
+
.slice(0, 100);
|
|
101
126
|
|
|
102
127
|
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
system: mk(systemTokens),
|
|
129
|
+
tools: mk(toolTokens),
|
|
130
|
+
history: mk(historyTokens),
|
|
131
|
+
files: mk(fileTokens),
|
|
132
|
+
summaries: mk(summaryTokens),
|
|
133
|
+
total: mk(totalTokens),
|
|
134
|
+
files_detail,
|
|
109
135
|
};
|
|
110
136
|
}
|
|
111
137
|
|
|
138
|
+
/** Backward-compatible wrapper. */
|
|
139
|
+
public analyze(messages: any[], currentTurn: number): ContextComposition {
|
|
140
|
+
return this.analyzeByType(messages, currentTurn);
|
|
141
|
+
}
|
|
142
|
+
|
|
112
143
|
private extractPath(toolName: string, input: any): string | null {
|
|
113
144
|
if (toolName === "read" || toolName === "write" || toolName === "edit") {
|
|
114
145
|
return typeof input.path === "string" ? input.path : null;
|
|
115
146
|
}
|
|
116
147
|
if (toolName === "bash") {
|
|
117
|
-
// Simple regex for paths in bash commands (e.g., cat path/to/file)
|
|
118
148
|
const match = input.command?.match(
|
|
119
149
|
/(?:cat|ls|rm|mv|cp|vi|nano)\s+([^\s;]+)/,
|
|
120
150
|
);
|
|
@@ -130,7 +160,7 @@ export class ContextAnalyzer {
|
|
|
130
160
|
case "edit":
|
|
131
161
|
return "edit";
|
|
132
162
|
case "bash":
|
|
133
|
-
return "delete";
|
|
163
|
+
return "delete";
|
|
134
164
|
default:
|
|
135
165
|
return "read";
|
|
136
166
|
}
|
|
@@ -151,12 +181,10 @@ export class ContextAnalyzer {
|
|
|
151
181
|
toolTurnIndex: number,
|
|
152
182
|
toolId: string,
|
|
153
183
|
): any {
|
|
154
|
-
// Look for the tool result immediately following the tool use
|
|
155
184
|
for (let i = toolTurnIndex + 1; i < messages.length; i++) {
|
|
156
185
|
if (messages[i].role === "tool" && messages[i].tool_call_id === toolId) {
|
|
157
186
|
return messages[i];
|
|
158
187
|
}
|
|
159
|
-
// If we hit another assistant turn, the result for this specific call is likely gone/compacted
|
|
160
188
|
if (messages[i].role === "assistant") break;
|
|
161
189
|
}
|
|
162
190
|
return null;
|
package/extensions/generator.ts
CHANGED
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
* Generates a visual HTML dashboard based on the ContextMap.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { ContextComposition } from "./analyzer";
|
|
7
|
+
import type { Insight } from "./insights";
|
|
7
8
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
import { homedir } from "node:os";
|
|
10
11
|
|
|
11
12
|
export class ReportGenerator {
|
|
12
|
-
public static generateHTML(
|
|
13
|
-
|
|
13
|
+
public static generateHTML(
|
|
14
|
+
composition: ContextComposition,
|
|
15
|
+
insights: Insight[],
|
|
16
|
+
): string {
|
|
17
|
+
const fileCards = composition.files_detail
|
|
14
18
|
.map(
|
|
15
19
|
(file) => `
|
|
16
|
-
<div class="file-card ${file.status}">
|
|
20
|
+
<div class="file-card ${file.status}" data-path="${ReportGenerator.escapeHtml(file.path)}" data-status="${file.status}">
|
|
17
21
|
<div class="file-header">
|
|
18
22
|
<span class="file-path">${ReportGenerator.escapeHtml(file.path)}</span>
|
|
19
23
|
<span class="file-weight">${file.weight.toLocaleString()} tokens</span>
|
|
@@ -31,7 +35,25 @@ export class ReportGenerator {
|
|
|
31
35
|
)
|
|
32
36
|
.join("");
|
|
33
37
|
|
|
34
|
-
const
|
|
38
|
+
const insightCards = insights
|
|
39
|
+
.map((insight, i) => {
|
|
40
|
+
// Critical and warning are expanded by default; info is collapsed
|
|
41
|
+
const isCollapsed = insight.severity === "info" ? " collapsed" : "";
|
|
42
|
+
return `
|
|
43
|
+
<div class="insight-card ${insight.severity}${isCollapsed}">
|
|
44
|
+
<button class="insight-header" data-toggle="insight-${i}" aria-expanded="${isCollapsed ? "false" : "true"}">
|
|
45
|
+
<svg class="insight-chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4,6 8,10 12,6"/></svg>
|
|
46
|
+
<span class="insight-severity">${insight.severity.toUpperCase()}</span>
|
|
47
|
+
<span class="insight-title">${ReportGenerator.escapeHtml(insight.title)}</span>
|
|
48
|
+
</button>
|
|
49
|
+
<div class="insight-body">
|
|
50
|
+
${ReportGenerator.escapeHtml(insight.message)}
|
|
51
|
+
${insight.command ? `<div class="insight-command">Suggested: <code>${insight.command}</code></div>` : ""}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
`;
|
|
55
|
+
})
|
|
56
|
+
.join("");
|
|
35
57
|
|
|
36
58
|
return `
|
|
37
59
|
<!DOCTYPE html>
|
|
@@ -39,19 +61,333 @@ export class ReportGenerator {
|
|
|
39
61
|
<head>
|
|
40
62
|
<meta charset="UTF-8">
|
|
41
63
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
42
|
-
<title>Pi Context
|
|
64
|
+
<title>Pi Context Profiler</title>
|
|
43
65
|
<style>
|
|
66
|
+
/* ============================================
|
|
67
|
+
pi-context-map Report — Design Tokens
|
|
68
|
+
Based on Linear design system + shadcn/ui card patterns
|
|
69
|
+
============================================ */
|
|
44
70
|
:root {
|
|
45
|
-
|
|
46
|
-
--
|
|
47
|
-
--
|
|
48
|
-
--
|
|
49
|
-
--
|
|
50
|
-
--
|
|
51
|
-
--
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
/* Surfaces */
|
|
72
|
+
--canvas: #010102;
|
|
73
|
+
--surface-1: #0f1011;
|
|
74
|
+
--surface-2: #141516;
|
|
75
|
+
--surface-3: #18191a;
|
|
76
|
+
--hairline: #23252a;
|
|
77
|
+
--hairline-strong: #34343a;
|
|
78
|
+
|
|
79
|
+
/* Text */
|
|
80
|
+
--ink: #f7f8f8;
|
|
81
|
+
--ink-muted: #d0d6e0;
|
|
82
|
+
--ink-subtle: #8a8f98;
|
|
83
|
+
--ink-tertiary: #62666d;
|
|
84
|
+
|
|
85
|
+
/* Accent */
|
|
86
|
+
--accent: #5e6ad2;
|
|
87
|
+
--accent-hover: #828fff;
|
|
88
|
+
--accent-soft: rgba(94, 106, 210, 0.12);
|
|
89
|
+
|
|
90
|
+
/* Semantic */
|
|
91
|
+
--success: #27a644;
|
|
92
|
+
--warning: #eab308;
|
|
93
|
+
--danger: #ef4444;
|
|
94
|
+
--warning-soft: rgba(234, 179, 8, 0.10);
|
|
95
|
+
--danger-soft: rgba(239, 68, 68, 0.10);
|
|
96
|
+
|
|
97
|
+
/* Composition segments */
|
|
98
|
+
--seg-system: #6366f1;
|
|
99
|
+
--seg-tools: #ec4899;
|
|
100
|
+
--seg-history: #a855f7;
|
|
101
|
+
--seg-files: #38bdf8;
|
|
102
|
+
--seg-summaries: #14b8a6;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
106
|
+
|
|
107
|
+
body {
|
|
108
|
+
background: var(--canvas);
|
|
109
|
+
color: var(--ink);
|
|
110
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
111
|
+
font-size: 14px;
|
|
112
|
+
line-height: 1.5;
|
|
113
|
+
-webkit-font-smoothing: antialiased;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 48px 32px; }
|
|
117
|
+
|
|
118
|
+
/* ===== Header ===== */
|
|
119
|
+
header { margin-bottom: 48px; }
|
|
120
|
+
h1 {
|
|
121
|
+
font-size: 32px;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
letter-spacing: -0.8px;
|
|
124
|
+
margin-bottom: 8px;
|
|
125
|
+
color: var(--ink);
|
|
126
|
+
}
|
|
127
|
+
.subtitle { color: var(--ink-subtle); font-size: 14px; margin-bottom: 32px; }
|
|
128
|
+
|
|
129
|
+
.stats-grid {
|
|
130
|
+
display: grid;
|
|
131
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
132
|
+
gap: 16px;
|
|
133
|
+
}
|
|
134
|
+
.stat-card {
|
|
135
|
+
background: var(--surface-1);
|
|
136
|
+
border: 1px solid var(--hairline);
|
|
137
|
+
border-radius: 6px;
|
|
138
|
+
padding: 20px;
|
|
139
|
+
text-align: left;
|
|
140
|
+
}
|
|
141
|
+
.stat-value {
|
|
142
|
+
font-size: 24px;
|
|
143
|
+
font-weight: 600;
|
|
144
|
+
color: var(--ink);
|
|
145
|
+
display: block;
|
|
146
|
+
font-variant-numeric: tabular-nums;
|
|
147
|
+
}
|
|
148
|
+
.stat-label {
|
|
149
|
+
color: var(--ink-subtle);
|
|
150
|
+
font-size: 12px;
|
|
151
|
+
text-transform: uppercase;
|
|
152
|
+
letter-spacing: 0.5px;
|
|
153
|
+
margin-top: 4px;
|
|
154
|
+
display: block;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ===== Composition Bar ===== */
|
|
158
|
+
.composition-container {
|
|
159
|
+
background: var(--surface-1);
|
|
160
|
+
border: 1px solid var(--hairline);
|
|
161
|
+
border-radius: 6px;
|
|
162
|
+
padding: 20px;
|
|
163
|
+
margin-top: 24px;
|
|
164
|
+
}
|
|
165
|
+
.composition-bar {
|
|
166
|
+
height: 32px;
|
|
167
|
+
background: var(--surface-3);
|
|
168
|
+
border-radius: 4px;
|
|
169
|
+
display: flex;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
margin-bottom: 12px;
|
|
172
|
+
}
|
|
173
|
+
.composition-segment {
|
|
174
|
+
height: 100%;
|
|
175
|
+
transition: opacity 0.2s ease;
|
|
176
|
+
cursor: default;
|
|
177
|
+
}
|
|
178
|
+
.composition-segment:hover { opacity: 0.85; }
|
|
179
|
+
.seg-system { background: var(--seg-system); }
|
|
180
|
+
.seg-tools { background: var(--seg-tools); }
|
|
181
|
+
.seg-history { background: var(--seg-history); }
|
|
182
|
+
.seg-files { background: var(--seg-files); }
|
|
183
|
+
.seg-summaries { background: var(--seg-summaries); }
|
|
184
|
+
|
|
185
|
+
.composition-legend {
|
|
186
|
+
display: grid;
|
|
187
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
188
|
+
gap: 8px;
|
|
189
|
+
font-size: 12px;
|
|
190
|
+
}
|
|
191
|
+
.legend-item {
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 8px;
|
|
195
|
+
color: var(--ink-muted);
|
|
196
|
+
font-variant-numeric: tabular-nums;
|
|
197
|
+
}
|
|
198
|
+
.dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }
|
|
199
|
+
|
|
200
|
+
/* ===== Sections ===== */
|
|
201
|
+
h2 {
|
|
202
|
+
font-size: 20px;
|
|
203
|
+
font-weight: 600;
|
|
204
|
+
color: var(--ink);
|
|
205
|
+
margin: 48px 0 16px;
|
|
206
|
+
letter-spacing: -0.3px;
|
|
207
|
+
}
|
|
208
|
+
h3 {
|
|
209
|
+
font-size: 12px;
|
|
210
|
+
font-weight: 500;
|
|
211
|
+
color: var(--ink-subtle);
|
|
212
|
+
text-transform: uppercase;
|
|
213
|
+
letter-spacing: 0.8px;
|
|
214
|
+
margin-bottom: 12px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* ===== Insights (shadcn-style cards) ===== */
|
|
218
|
+
.insight-card {
|
|
219
|
+
background: var(--surface-1);
|
|
220
|
+
border: 1px solid var(--hairline);
|
|
221
|
+
border-left: 3px solid var(--accent);
|
|
222
|
+
border-radius: 6px;
|
|
223
|
+
margin-bottom: 8px;
|
|
224
|
+
overflow: hidden;
|
|
225
|
+
}
|
|
226
|
+
.insight-card.critical { border-left-color: var(--danger); background: linear-gradient(90deg, var(--danger-soft) 0%, var(--surface-1) 100%); }
|
|
227
|
+
.insight-card.warning { border-left-color: var(--warning); background: linear-gradient(90deg, var(--warning-soft) 0%, var(--surface-1) 100%); }
|
|
228
|
+
.insight-card.info { border-left-color: var(--accent); }
|
|
229
|
+
|
|
230
|
+
.insight-header {
|
|
231
|
+
display: flex;
|
|
232
|
+
align-items: center;
|
|
233
|
+
gap: 12px;
|
|
234
|
+
padding: 14px 16px;
|
|
235
|
+
cursor: pointer;
|
|
236
|
+
user-select: none;
|
|
237
|
+
background: none;
|
|
238
|
+
border: none;
|
|
239
|
+
width: 100%;
|
|
240
|
+
text-align: left;
|
|
241
|
+
color: inherit;
|
|
242
|
+
font: inherit;
|
|
243
|
+
}
|
|
244
|
+
.insight-header:hover { background: var(--surface-2); }
|
|
245
|
+
.insight-chevron {
|
|
246
|
+
width: 16px;
|
|
247
|
+
height: 16px;
|
|
248
|
+
transition: transform 0.2s ease;
|
|
249
|
+
color: var(--ink-subtle);
|
|
250
|
+
flex-shrink: 0;
|
|
251
|
+
}
|
|
252
|
+
.insight-card.collapsed .insight-chevron { transform: rotate(-90deg); }
|
|
253
|
+
.insight-severity {
|
|
254
|
+
font-size: 10px;
|
|
255
|
+
font-weight: 700;
|
|
256
|
+
padding: 3px 8px;
|
|
257
|
+
border-radius: 3px;
|
|
258
|
+
background: var(--surface-3);
|
|
259
|
+
color: var(--ink-muted);
|
|
260
|
+
letter-spacing: 0.5px;
|
|
261
|
+
flex-shrink: 0;
|
|
262
|
+
}
|
|
263
|
+
.insight-card.critical .insight-severity { color: var(--danger); }
|
|
264
|
+
.insight-card.warning .insight-severity { color: var(--warning); }
|
|
265
|
+
.insight-card.info .insight-severity { color: var(--accent); }
|
|
266
|
+
.insight-title { font-weight: 600; color: var(--ink); font-size: 14px; }
|
|
267
|
+
.insight-body {
|
|
268
|
+
padding: 0 16px 14px 44px;
|
|
269
|
+
color: var(--ink-muted);
|
|
270
|
+
font-size: 13px;
|
|
271
|
+
line-height: 1.6;
|
|
272
|
+
}
|
|
273
|
+
.insight-card.collapsed .insight-body { display: none; }
|
|
274
|
+
.insight-command {
|
|
275
|
+
margin-top: 8px;
|
|
276
|
+
font-size: 12px;
|
|
277
|
+
color: var(--ink-subtle);
|
|
278
|
+
}
|
|
279
|
+
.insight-command code {
|
|
280
|
+
background: var(--surface-3);
|
|
281
|
+
color: var(--accent-hover);
|
|
282
|
+
padding: 2px 6px;
|
|
283
|
+
border-radius: 3px;
|
|
284
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
285
|
+
font-size: 12px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* ===== File Controls ===== */
|
|
289
|
+
.file-controls {
|
|
290
|
+
display: flex;
|
|
291
|
+
gap: 12px;
|
|
292
|
+
margin-bottom: 16px;
|
|
293
|
+
flex-wrap: wrap;
|
|
294
|
+
}
|
|
295
|
+
.file-search, .file-filter {
|
|
296
|
+
background: var(--surface-1);
|
|
297
|
+
border: 1px solid var(--hairline);
|
|
298
|
+
border-radius: 6px;
|
|
299
|
+
padding: 8px 12px;
|
|
300
|
+
color: var(--ink);
|
|
301
|
+
font: inherit;
|
|
302
|
+
font-size: 13px;
|
|
303
|
+
outline: none;
|
|
304
|
+
transition: border-color 0.15s ease;
|
|
305
|
+
}
|
|
306
|
+
.file-search:focus, .file-filter:focus { border-color: var(--accent); }
|
|
307
|
+
.file-search { flex: 1; min-width: 200px; }
|
|
308
|
+
.file-search::placeholder { color: var(--ink-tertiary); }
|
|
309
|
+
.file-filter { cursor: pointer; }
|
|
310
|
+
.file-count { color: var(--ink-subtle); font-size: 12px; padding: 8px 0; align-self: center; }
|
|
311
|
+
|
|
312
|
+
/* ===== File Grid ===== */
|
|
313
|
+
.file-grid {
|
|
314
|
+
display: grid;
|
|
315
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
316
|
+
gap: 12px;
|
|
54
317
|
}
|
|
318
|
+
.file-card {
|
|
319
|
+
background: var(--surface-1);
|
|
320
|
+
border: 1px solid var(--hairline);
|
|
321
|
+
border-radius: 6px;
|
|
322
|
+
padding: 14px 16px;
|
|
323
|
+
transition: border-color 0.15s ease;
|
|
324
|
+
}
|
|
325
|
+
.file-card:hover { border-color: var(--hairline-strong); }
|
|
326
|
+
.file-card.hidden { display: none; }
|
|
327
|
+
.file-header {
|
|
328
|
+
display: flex;
|
|
329
|
+
justify-content: space-between;
|
|
330
|
+
align-items: flex-start;
|
|
331
|
+
gap: 8px;
|
|
332
|
+
margin-bottom: 10px;
|
|
333
|
+
}
|
|
334
|
+
.file-path {
|
|
335
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
336
|
+
font-size: 12px;
|
|
337
|
+
color: var(--ink);
|
|
338
|
+
word-break: break-all;
|
|
339
|
+
line-height: 1.4;
|
|
340
|
+
}
|
|
341
|
+
.file-weight {
|
|
342
|
+
font-size: 11px;
|
|
343
|
+
color: var(--ink-subtle);
|
|
344
|
+
white-space: nowrap;
|
|
345
|
+
font-variant-numeric: tabular-nums;
|
|
346
|
+
flex-shrink: 0;
|
|
347
|
+
}
|
|
348
|
+
.file-footer {
|
|
349
|
+
display: flex;
|
|
350
|
+
justify-content: space-between;
|
|
351
|
+
align-items: center;
|
|
352
|
+
font-size: 11px;
|
|
353
|
+
color: var(--ink-subtle);
|
|
354
|
+
text-transform: uppercase;
|
|
355
|
+
letter-spacing: 0.5px;
|
|
356
|
+
}
|
|
357
|
+
.op-badge {
|
|
358
|
+
background: var(--surface-3);
|
|
359
|
+
padding: 2px 6px;
|
|
360
|
+
border-radius: 3px;
|
|
361
|
+
color: var(--ink-muted);
|
|
362
|
+
}
|
|
363
|
+
.status-text { font-weight: 700; }
|
|
364
|
+
.file-card.active { border-left: 3px solid var(--success); }
|
|
365
|
+
.file-card.active .status-text { color: var(--success); }
|
|
366
|
+
.file-card.stale { border-left: 3px solid var(--warning); }
|
|
367
|
+
.file-card.stale .status-text { color: var(--warning); }
|
|
368
|
+
.file-card.legacy { border-left: 3px solid var(--danger); }
|
|
369
|
+
.file-card.legacy .status-text { color: var(--danger); }
|
|
370
|
+
|
|
371
|
+
.weight-bar {
|
|
372
|
+
height: 3px;
|
|
373
|
+
background: var(--surface-3);
|
|
374
|
+
border-radius: 2px;
|
|
375
|
+
margin-top: 10px;
|
|
376
|
+
overflow: hidden;
|
|
377
|
+
}
|
|
378
|
+
.weight-fill {
|
|
379
|
+
height: 100%;
|
|
380
|
+
background: var(--accent);
|
|
381
|
+
transition: width 0.3s ease;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.empty-state {
|
|
385
|
+
text-align: center;
|
|
386
|
+
padding: 48px 16px;
|
|
387
|
+
color: var(--ink-subtle);
|
|
388
|
+
font-size: 13px;
|
|
389
|
+
}
|
|
390
|
+
</style>
|
|
55
391
|
body {
|
|
56
392
|
background: var(--bg);
|
|
57
393
|
color: var(--text);
|
|
@@ -79,36 +415,67 @@ export class ReportGenerator {
|
|
|
79
415
|
.stat-value { font-size: 1.5rem; font-weight: bold; display: block; }
|
|
80
416
|
.stat-label { color: var(--text-dim); font-size: 0.875rem; text-transform: uppercase; }
|
|
81
417
|
|
|
82
|
-
.
|
|
418
|
+
.composition-container {
|
|
83
419
|
margin: 2rem 0;
|
|
84
420
|
background: var(--card-bg);
|
|
85
|
-
padding:
|
|
421
|
+
padding: 1.5rem;
|
|
86
422
|
border-radius: 12px;
|
|
87
423
|
border: 1px solid var(--border);
|
|
88
424
|
}
|
|
89
|
-
.
|
|
90
|
-
height:
|
|
425
|
+
.composition-bar {
|
|
426
|
+
height: 32px;
|
|
91
427
|
background: #020617;
|
|
92
|
-
border-radius:
|
|
428
|
+
border-radius: 8px;
|
|
93
429
|
display: flex;
|
|
94
430
|
overflow: hidden;
|
|
95
|
-
margin-bottom:
|
|
431
|
+
margin-bottom: 1rem;
|
|
96
432
|
}
|
|
97
|
-
.
|
|
433
|
+
.composition-segment { height: 100%; transition: width 0.3s ease; }
|
|
98
434
|
.seg-system { background: #6366f1; }
|
|
435
|
+
.seg-tools { background: #ec4899; }
|
|
99
436
|
.seg-history { background: #a855f7; }
|
|
100
437
|
.seg-files { background: var(--primary); }
|
|
101
|
-
.seg-
|
|
102
|
-
|
|
103
|
-
.
|
|
104
|
-
display:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
font-size: 0.
|
|
438
|
+
.seg-summaries { background: #14b8a6; }
|
|
439
|
+
|
|
440
|
+
.composition-legend {
|
|
441
|
+
display: grid;
|
|
442
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
443
|
+
gap: 0.75rem;
|
|
444
|
+
font-size: 0.8rem;
|
|
108
445
|
color: var(--text-dim);
|
|
109
446
|
}
|
|
110
447
|
.legend-item { display: flex; align-items: center; gap: 0.5rem; }
|
|
111
|
-
.dot { width:
|
|
448
|
+
.dot { width: 10px; height: 10px; border-radius: 50%; }
|
|
449
|
+
|
|
450
|
+
.insights-section { margin: 2rem 0; }
|
|
451
|
+
.insight-card {
|
|
452
|
+
background: var(--card-bg);
|
|
453
|
+
border: 1px solid var(--border);
|
|
454
|
+
border-left: 4px solid var(--primary);
|
|
455
|
+
border-radius: 8px;
|
|
456
|
+
padding: 1rem 1.25rem;
|
|
457
|
+
margin-bottom: 0.75rem;
|
|
458
|
+
}
|
|
459
|
+
.insight-card.info { border-left-color: var(--primary); }
|
|
460
|
+
.insight-card.warning { border-left-color: var(--stale); }
|
|
461
|
+
.insight-card.critical { border-left-color: var(--legacy); }
|
|
462
|
+
.insight-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
|
|
463
|
+
.insight-severity {
|
|
464
|
+
font-size: 0.7rem;
|
|
465
|
+
font-weight: bold;
|
|
466
|
+
padding: 2px 8px;
|
|
467
|
+
border-radius: 4px;
|
|
468
|
+
background: rgba(255,255,255,0.1);
|
|
469
|
+
}
|
|
470
|
+
.insight-title { font-weight: 600; }
|
|
471
|
+
.insight-body { color: var(--text); font-size: 0.9rem; }
|
|
472
|
+
.insight-command { margin-top: 0.5rem; font-size: 0.8rem; color: var(--text-dim); }
|
|
473
|
+
.insight-command code {
|
|
474
|
+
background: rgba(0,0,0,0.3);
|
|
475
|
+
padding: 2px 6px;
|
|
476
|
+
border-radius: 4px;
|
|
477
|
+
font-family: 'Fira Code', monospace;
|
|
478
|
+
}
|
|
112
479
|
|
|
113
480
|
.file-grid {
|
|
114
481
|
display: grid;
|
|
@@ -181,48 +548,116 @@ export class ReportGenerator {
|
|
|
181
548
|
<body>
|
|
182
549
|
<div class="container">
|
|
183
550
|
<header>
|
|
184
|
-
<h1>Pi Context
|
|
185
|
-
<p style="color: var(--text-dim)">
|
|
186
|
-
|
|
551
|
+
<h1>Pi Context Profiler</h1>
|
|
552
|
+
<p style="color: var(--text-dim)">Professional session context window analysis with actionable insights.</p>
|
|
553
|
+
|
|
187
554
|
<div class="stats-grid">
|
|
188
555
|
<div class="stat-card">
|
|
189
|
-
<span class="stat-value">${
|
|
556
|
+
<span class="stat-value">${composition.total.tokens.toLocaleString()}</span>
|
|
190
557
|
<span class="stat-label">Total Tokens</span>
|
|
191
558
|
</div>
|
|
192
559
|
<div class="stat-card">
|
|
193
|
-
<span class="stat-value">${
|
|
560
|
+
<span class="stat-value">${composition.files_detail.length}</span>
|
|
194
561
|
<span class="stat-label">Files in Context</span>
|
|
195
562
|
</div>
|
|
196
563
|
<div class="stat-card">
|
|
197
|
-
<span class="stat-value">${
|
|
198
|
-
<span class="stat-label">
|
|
564
|
+
<span class="stat-value">${composition.tools.tokens.toLocaleString()}</span>
|
|
565
|
+
<span class="stat-label">Tool Tokens</span>
|
|
199
566
|
</div>
|
|
200
567
|
<div class="stat-card">
|
|
201
|
-
<span class="stat-value">${Math.round(
|
|
202
|
-
<span class="stat-label">
|
|
568
|
+
<span class="stat-value">${Math.round((composition.total.tokens / 128000) * 100)}%</span>
|
|
569
|
+
<span class="stat-label">Of 128k Window</span>
|
|
203
570
|
</div>
|
|
204
571
|
</div>
|
|
205
572
|
|
|
206
|
-
<div class="
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
<div class="
|
|
210
|
-
<div class="
|
|
211
|
-
<div class="
|
|
573
|
+
<div class="composition-container">
|
|
574
|
+
<h3 style="margin-top: 0; color: var(--text-dim); font-size: 0.9rem; text-transform: uppercase;">Context Composition</h3>
|
|
575
|
+
<div class="composition-bar">
|
|
576
|
+
<div class="composition-segment seg-system" style="width: ${composition.system.percent}%" title="System: ${composition.system.percent}%"></div>
|
|
577
|
+
<div class="composition-segment seg-tools" style="width: ${composition.tools.percent}%" title="Tools: ${composition.tools.percent}%"></div>
|
|
578
|
+
<div class="composition-segment seg-history" style="width: ${composition.history.percent}%" title="History: ${composition.history.percent}%"></div>
|
|
579
|
+
<div class="composition-segment seg-files" style="width: ${composition.files.percent}%" title="Files: ${composition.files.percent}%"></div>
|
|
580
|
+
<div class="composition-segment seg-summaries" style="width: ${composition.summaries.percent}%" title="Summaries: ${composition.summaries.percent}%"></div>
|
|
212
581
|
</div>
|
|
213
|
-
<div class="
|
|
214
|
-
<div class="legend-item"><span class="dot seg-system"></span> System</div>
|
|
215
|
-
<div class="legend-item"><span class="dot seg-
|
|
216
|
-
<div class="legend-item"><span class="dot seg-
|
|
217
|
-
<div class="legend-item"><span class="dot seg-
|
|
582
|
+
<div class="composition-legend">
|
|
583
|
+
<div class="legend-item"><span class="dot seg-system"></span> System (${composition.system.percent}%)</div>
|
|
584
|
+
<div class="legend-item"><span class="dot seg-tools"></span> Tools (${composition.tools.percent}%)</div>
|
|
585
|
+
<div class="legend-item"><span class="dot seg-history"></span> History (${composition.history.percent}%)</div>
|
|
586
|
+
<div class="legend-item"><span class="dot seg-files"></span> Files (${composition.files.percent}%)</div>
|
|
587
|
+
<div class="legend-item"><span class="dot seg-summaries"></span> Summaries (${composition.summaries.percent}%)</div>
|
|
218
588
|
</div>
|
|
219
589
|
</div>
|
|
220
590
|
</header>
|
|
221
591
|
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
592
|
+
<section class="insights-section">
|
|
593
|
+
<h2>Actionable Insights</h2>
|
|
594
|
+
${insightCards}
|
|
595
|
+
</section>
|
|
596
|
+
|
|
597
|
+
<section>
|
|
598
|
+
<h2>Files in Context</h2>
|
|
599
|
+
<div class="file-controls">
|
|
600
|
+
<input type="text" class="file-search" id="fileSearch" placeholder="Search files by path..." aria-label="Search files" />
|
|
601
|
+
<select class="file-filter" id="fileFilter" aria-label="Filter by status">
|
|
602
|
+
<option value="all">All statuses</option>
|
|
603
|
+
<option value="active">Active</option>
|
|
604
|
+
<option value="stale">Stale</option>
|
|
605
|
+
<option value="legacy">Legacy</option>
|
|
606
|
+
</select>
|
|
607
|
+
<span class="file-count" id="fileCount"></span>
|
|
608
|
+
</div>
|
|
609
|
+
<div class="file-grid" id="fileGrid">
|
|
610
|
+
${fileCards}
|
|
611
|
+
</div>
|
|
612
|
+
<div class="empty-state" id="emptyState" style="display: none;">No files match your search.</div>
|
|
613
|
+
</section>
|
|
225
614
|
</div>
|
|
615
|
+
|
|
616
|
+
<script>
|
|
617
|
+
(function() {
|
|
618
|
+
// ===== Insight collapse/expand =====
|
|
619
|
+
document.querySelectorAll('.insight-header[data-toggle]').forEach(function(btn) {
|
|
620
|
+
btn.addEventListener('click', function() {
|
|
621
|
+
var card = btn.closest('.insight-card');
|
|
622
|
+
var isCollapsed = card.classList.toggle('collapsed');
|
|
623
|
+
btn.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// ===== File search & filter =====
|
|
628
|
+
var search = document.getElementById('fileSearch');
|
|
629
|
+
var filter = document.getElementById('fileFilter');
|
|
630
|
+
var grid = document.getElementById('fileGrid');
|
|
631
|
+
var count = document.getElementById('fileCount');
|
|
632
|
+
var empty = document.getElementById('emptyState');
|
|
633
|
+
var cards = Array.prototype.slice.call(grid.querySelectorAll('.file-card'));
|
|
634
|
+
var total = cards.length;
|
|
635
|
+
|
|
636
|
+
function applyFilters() {
|
|
637
|
+
var query = (search.value || '').toLowerCase();
|
|
638
|
+
var status = filter.value;
|
|
639
|
+
var visible = 0;
|
|
640
|
+
cards.forEach(function(card) {
|
|
641
|
+
var path = (card.getAttribute('data-path') || '').toLowerCase();
|
|
642
|
+
var cardStatus = card.getAttribute('data-status') || '';
|
|
643
|
+
var matchQuery = !query || path.indexOf(query) !== -1;
|
|
644
|
+
var matchStatus = status === 'all' || cardStatus === status;
|
|
645
|
+
if (matchQuery && matchStatus) {
|
|
646
|
+
card.classList.remove('hidden');
|
|
647
|
+
visible++;
|
|
648
|
+
} else {
|
|
649
|
+
card.classList.add('hidden');
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
count.textContent = visible === total ? total + ' files' : visible + ' of ' + total + ' files';
|
|
653
|
+
empty.style.display = visible === 0 ? 'block' : 'none';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (search) search.addEventListener('input', applyFilters);
|
|
657
|
+
if (filter) filter.addEventListener('change', applyFilters);
|
|
658
|
+
applyFilters();
|
|
659
|
+
})();
|
|
660
|
+
</script>
|
|
226
661
|
</body>
|
|
227
662
|
</html>
|
|
228
663
|
`;
|
|
@@ -239,15 +674,15 @@ export class ReportGenerator {
|
|
|
239
674
|
private static getOpIcon(type: string): string {
|
|
240
675
|
switch (type) {
|
|
241
676
|
case "read":
|
|
242
|
-
return "
|
|
677
|
+
return "READ";
|
|
243
678
|
case "write":
|
|
244
|
-
return "
|
|
679
|
+
return "WRITE";
|
|
245
680
|
case "edit":
|
|
246
|
-
return "
|
|
681
|
+
return "EDIT";
|
|
247
682
|
case "delete":
|
|
248
|
-
return "
|
|
683
|
+
return "DELETE";
|
|
249
684
|
default:
|
|
250
|
-
return "
|
|
685
|
+
return "FILE";
|
|
251
686
|
}
|
|
252
687
|
}
|
|
253
688
|
|
package/extensions/index.ts
CHANGED
|
@@ -1,49 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-context-map
|
|
3
|
-
*
|
|
3
|
+
* Professional Context Profiler for Pi.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { ContextAnalyzer } from "./analyzer";
|
|
8
8
|
import { ReportGenerator } from "./generator";
|
|
9
|
+
import { InsightEngine } from "./insights";
|
|
9
10
|
|
|
10
|
-
export default function (pi: ExtensionAPI) {
|
|
11
|
+
export default async function piContextMap(pi: ExtensionAPI) {
|
|
11
12
|
const analyzer = new ContextAnalyzer();
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
async function runAnalysis() {
|
|
15
|
+
const messages = (pi as any).session?.messages || [];
|
|
16
|
+
const currentTurn = messages.length;
|
|
17
|
+
const composition = analyzer.analyzeByType(messages, currentTurn);
|
|
18
|
+
const insights = InsightEngine.generate(composition);
|
|
19
|
+
const html = ReportGenerator.generateHTML(composition, insights);
|
|
20
|
+
const reportPath = ReportGenerator.writeReport(html);
|
|
21
|
+
return { composition, insights, reportPath };
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
pi.registerCommand("context-map", {
|
|
15
|
-
description: "Generate a visual map
|
|
16
|
-
handler: (_args, ctx) => {
|
|
25
|
+
description: "Generate a visual context map with actionable insights.",
|
|
26
|
+
handler: async (_args: any, ctx: any) => {
|
|
17
27
|
ctx.ui.notify("Analyzing session context...", "info");
|
|
18
|
-
|
|
19
28
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// 2. Analyze context
|
|
32
|
-
const map = analyzer.analyze(messages, currentTurn);
|
|
33
|
-
|
|
34
|
-
// 3. Generate HTML Report
|
|
35
|
-
const html = ReportGenerator.generateHTML(map);
|
|
36
|
-
const reportPath = ReportGenerator.writeReport(html);
|
|
37
|
-
|
|
38
|
-
ctx.ui.notify(
|
|
39
|
-
`Context map generated successfully! \nPath: ${reportPath}`,
|
|
40
|
-
"success",
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
// Providing a link or instruction to open the report
|
|
29
|
+
const { reportPath, insights } = await runAnalysis();
|
|
30
|
+
const criticalCount = insights.filter(
|
|
31
|
+
(i) => i.severity === "critical",
|
|
32
|
+
).length;
|
|
33
|
+
const summary =
|
|
34
|
+
criticalCount > 0
|
|
35
|
+
? `Context map generated. ${criticalCount} critical insight(s) found.`
|
|
36
|
+
: `Context map generated successfully.`;
|
|
44
37
|
ctx.ui.notify(
|
|
45
|
-
|
|
46
|
-
"
|
|
38
|
+
`${summary} Path: ${reportPath}`,
|
|
39
|
+
criticalCount > 0 ? "warning" : "success",
|
|
47
40
|
);
|
|
48
41
|
} catch (error) {
|
|
49
42
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -52,12 +45,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
52
45
|
},
|
|
53
46
|
});
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
pi.registerTool({
|
|
49
|
+
name: "context-map",
|
|
50
|
+
description:
|
|
51
|
+
"Analyze the current session context composition and return actionable insights.",
|
|
52
|
+
parameters: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {},
|
|
55
|
+
},
|
|
56
|
+
handler: async (_ctx: any, _args: any) => {
|
|
57
|
+
try {
|
|
58
|
+
const { composition, insights } = await runAnalysis();
|
|
59
|
+
const summary =
|
|
60
|
+
`Context: ${composition.total.tokens.toLocaleString()} tokens total. ` +
|
|
61
|
+
`System ${composition.system.percent}%, Tools ${composition.tools.percent}%, ` +
|
|
62
|
+
`History ${composition.history.percent}%, Files ${composition.files.percent}%, ` +
|
|
63
|
+
`Summaries ${composition.summaries.percent}%. ` +
|
|
64
|
+
`${insights.length} insight(s) generated.`;
|
|
65
|
+
return {
|
|
66
|
+
summary,
|
|
67
|
+
composition: {
|
|
68
|
+
system: composition.system.tokens,
|
|
69
|
+
tools: composition.tools.tokens,
|
|
70
|
+
history: composition.history.tokens,
|
|
71
|
+
files: composition.files.tokens,
|
|
72
|
+
summaries: composition.summaries.tokens,
|
|
73
|
+
total: composition.total.tokens,
|
|
74
|
+
},
|
|
75
|
+
insights: insights.map((i) => ({
|
|
76
|
+
severity: i.severity,
|
|
77
|
+
title: i.title,
|
|
78
|
+
message: i.message,
|
|
79
|
+
command: i.command,
|
|
80
|
+
})),
|
|
81
|
+
};
|
|
82
|
+
} catch (error: any) {
|
|
83
|
+
return { error: error.message };
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
59
87
|
|
|
60
|
-
|
|
88
|
+
pi.on("session_before_compact", (event: any, ctx: any) => {
|
|
89
|
+
const tokens = (event as any).preparation?.tokensBefore;
|
|
90
|
+
if (tokens && tokens > 100_000) {
|
|
61
91
|
ctx.ui.notify(
|
|
62
92
|
`High context load detected (${(tokens / 1000).toFixed(1)}k tokens). Try /context-map to see what's consuming space.`,
|
|
63
93
|
"info",
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InsightEngine
|
|
3
|
+
* Generates actionable recommendations based on the ContextComposition.
|
|
4
|
+
*/
|
|
5
|
+
import type { ContextComposition } from "./analyzer";
|
|
6
|
+
|
|
7
|
+
export type InsightSeverity = "info" | "warning" | "critical";
|
|
8
|
+
|
|
9
|
+
export interface Insight {
|
|
10
|
+
id: string;
|
|
11
|
+
severity: InsightSeverity;
|
|
12
|
+
title: string;
|
|
13
|
+
message: string;
|
|
14
|
+
command?: string; // Suggested slash command
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TYPICAL_WINDOW = 128_000; // Default context window size (Claude Sonnet/Opus)
|
|
18
|
+
|
|
19
|
+
export class InsightEngine {
|
|
20
|
+
/**
|
|
21
|
+
* Generate a list of insights based on the composition.
|
|
22
|
+
*/
|
|
23
|
+
public static generate(composition: ContextComposition): Insight[] {
|
|
24
|
+
const insights: Insight[] = [];
|
|
25
|
+
const { system, tools, history, files, summaries, total } = composition;
|
|
26
|
+
|
|
27
|
+
// Rule 1: Tool bloat
|
|
28
|
+
if (tools.percent > 40) {
|
|
29
|
+
insights.push({
|
|
30
|
+
id: "tool-bloat",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
title: "Tool results dominate context",
|
|
33
|
+
message: `Tool results account for ${tools.percent}% of your context (${tools.tokens.toLocaleString()} tokens). Consider compacting or trimming verbose tool outputs.`,
|
|
34
|
+
command: "/ultra-compact",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Rule 2: Stale files
|
|
39
|
+
const staleFiles = composition.files_detail.filter(
|
|
40
|
+
(f) => f.status === "legacy",
|
|
41
|
+
);
|
|
42
|
+
if (staleFiles.length > 0) {
|
|
43
|
+
const totalStaleTokens = staleFiles.reduce((sum, f) => sum + f.weight, 0);
|
|
44
|
+
insights.push({
|
|
45
|
+
id: "stale-files",
|
|
46
|
+
severity: staleFiles.length > 5 ? "warning" : "info",
|
|
47
|
+
title: `${staleFiles.length} stale file(s) in context`,
|
|
48
|
+
message: `Files accessed more than 10 turns ago are still in context (~${totalStaleTokens.toLocaleString()} tokens). They are unlikely to be needed.`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Rule 3: High overall usage
|
|
53
|
+
const usagePercent = Math.round((total.tokens / TYPICAL_WINDOW) * 100);
|
|
54
|
+
if (usagePercent > 80) {
|
|
55
|
+
insights.push({
|
|
56
|
+
id: "high-usage",
|
|
57
|
+
severity: "critical",
|
|
58
|
+
title: "Context window nearly full",
|
|
59
|
+
message: `You are at ${usagePercent}% of a typical 128k context window. Compaction or summarization is strongly recommended.`,
|
|
60
|
+
command: "/ultra-compact",
|
|
61
|
+
});
|
|
62
|
+
} else if (usagePercent > 60) {
|
|
63
|
+
insights.push({
|
|
64
|
+
id: "moderate-usage",
|
|
65
|
+
severity: "warning",
|
|
66
|
+
title: "Context usage is high",
|
|
67
|
+
message: `You are at ${usagePercent}% of a typical 128k context window. Plan to compact before adding more files.`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Rule 4: File-heavy context
|
|
72
|
+
if (files.percent > 30) {
|
|
73
|
+
insights.push({
|
|
74
|
+
id: "file-heavy",
|
|
75
|
+
severity: "info",
|
|
76
|
+
title: "Many files loaded",
|
|
77
|
+
message: `Files account for ${files.percent}% of context (${files.tokens.toLocaleString()} tokens). Consider using smart-read to load only the relevant symbols.`,
|
|
78
|
+
command: "/smart-read",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Rule 5: Compaction summaries present
|
|
83
|
+
if (summaries.tokens > 0) {
|
|
84
|
+
insights.push({
|
|
85
|
+
id: "summaries-present",
|
|
86
|
+
severity: "info",
|
|
87
|
+
title: "Compaction summaries detected",
|
|
88
|
+
message: `${summaries.tokens.toLocaleString()} tokens are from prior compaction summaries. Original detail has been compressed.`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Rule 6: System prompt overhead
|
|
93
|
+
if (system.percent > 25) {
|
|
94
|
+
insights.push({
|
|
95
|
+
id: "system-overhead",
|
|
96
|
+
severity: "info",
|
|
97
|
+
title: "Large system prompt",
|
|
98
|
+
message: `System prompt uses ${system.percent}% of context. This is normal for agents with extensive tool definitions.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If everything looks good, add a positive insight
|
|
103
|
+
if (insights.length === 0) {
|
|
104
|
+
insights.push({
|
|
105
|
+
id: "healthy-context",
|
|
106
|
+
severity: "info",
|
|
107
|
+
title: "Context looks healthy",
|
|
108
|
+
message: `Your context composition is balanced and under ${usagePercent}% of a typical window.`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return insights;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TokenCounter
|
|
3
|
+
* Code-aware token estimation. Avoids heavy external tokenizers (e.g., tiktoken)
|
|
4
|
+
* by applying multipliers based on content structure.
|
|
5
|
+
*
|
|
6
|
+
* Heuristic base: 4 characters per token (the standard for English text).
|
|
7
|
+
* Adjustments:
|
|
8
|
+
* - Code blocks (```...```) are denser in tokens (identifiers, symbols).
|
|
9
|
+
* - JSON payloads have more structural overhead.
|
|
10
|
+
* - Pure whitespace is under-weighted.
|
|
11
|
+
*/
|
|
12
|
+
export class TokenCounter {
|
|
13
|
+
/** Base heuristic: average English is ~4 characters per token. */
|
|
14
|
+
private static BASE_CHARS_PER_TOKEN = 4;
|
|
15
|
+
|
|
16
|
+
/** Multiplier for fenced code blocks (```` ``` ````). */
|
|
17
|
+
private static CODE_MULTIPLIER = 1.3;
|
|
18
|
+
|
|
19
|
+
/** Multiplier for JSON-like structures. */
|
|
20
|
+
private static JSON_MULTIPLIER = 1.5;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Count estimated tokens for a raw string of text.
|
|
24
|
+
*/
|
|
25
|
+
public static count(text: string): number {
|
|
26
|
+
if (!text) return 0;
|
|
27
|
+
|
|
28
|
+
let total = 0;
|
|
29
|
+
let cursor = 0;
|
|
30
|
+
const len = text.length;
|
|
31
|
+
|
|
32
|
+
while (cursor < len) {
|
|
33
|
+
// Detect fenced code blocks
|
|
34
|
+
const fenceStart = text.indexOf("```", cursor);
|
|
35
|
+
if (fenceStart !== -1) {
|
|
36
|
+
// Count everything up to the fence as regular text
|
|
37
|
+
total += TokenCounter.regularChunk(text.substring(cursor, fenceStart));
|
|
38
|
+
// Find the closing fence
|
|
39
|
+
const fenceEnd = text.indexOf("```", fenceStart + 3);
|
|
40
|
+
if (fenceEnd === -1) {
|
|
41
|
+
// Unclosed fence — treat the rest as code
|
|
42
|
+
total += TokenCounter.codeChunk(text.substring(fenceStart));
|
|
43
|
+
cursor = len;
|
|
44
|
+
} else {
|
|
45
|
+
total += TokenCounter.codeChunk(
|
|
46
|
+
text.substring(fenceStart, fenceEnd + 3),
|
|
47
|
+
);
|
|
48
|
+
cursor = fenceEnd + 3;
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Detect JSON-like content (starts with { or [ and has balanced structure)
|
|
54
|
+
const trimmed = text.substring(cursor).trimStart();
|
|
55
|
+
const firstChar = trimmed.charAt(0);
|
|
56
|
+
if (
|
|
57
|
+
(firstChar === "{" || firstChar === "[") &&
|
|
58
|
+
TokenCounter.looksLikeJson(trimmed)
|
|
59
|
+
) {
|
|
60
|
+
const jsonLen = TokenCounter.extractJsonLength(trimmed);
|
|
61
|
+
if (jsonLen > 0) {
|
|
62
|
+
total += TokenCounter.jsonChunk(trimmed.substring(0, jsonLen));
|
|
63
|
+
cursor += text.substring(cursor).indexOf(trimmed) + jsonLen;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Default: regular text
|
|
69
|
+
total += TokenCounter.regularChunk(text.substring(cursor));
|
|
70
|
+
cursor = len;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return Math.ceil(total);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convenience: count tokens for any message shape Pi uses.
|
|
78
|
+
*/
|
|
79
|
+
public static countMessage(msg: any): number {
|
|
80
|
+
if (!msg) return 0;
|
|
81
|
+
if (typeof msg.content === "string") {
|
|
82
|
+
return TokenCounter.count(msg.content);
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(msg.content)) {
|
|
85
|
+
return msg.content.reduce((sum: number, block: any) => {
|
|
86
|
+
if (typeof block === "string") return sum + TokenCounter.count(block);
|
|
87
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
88
|
+
return sum + TokenCounter.count(block.text);
|
|
89
|
+
}
|
|
90
|
+
if (block.type === "tool_use" || block.type === "tool_result") {
|
|
91
|
+
return sum + TokenCounter.count(JSON.stringify(block));
|
|
92
|
+
}
|
|
93
|
+
return sum + TokenCounter.count(JSON.stringify(block));
|
|
94
|
+
}, 0);
|
|
95
|
+
}
|
|
96
|
+
return TokenCounter.count(JSON.stringify(msg));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private static regularChunk(text: string): number {
|
|
100
|
+
return text.length / TokenCounter.BASE_CHARS_PER_TOKEN;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private static codeChunk(text: string): number {
|
|
104
|
+
return (
|
|
105
|
+
(text.length / TokenCounter.BASE_CHARS_PER_TOKEN) *
|
|
106
|
+
TokenCounter.CODE_MULTIPLIER
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private static jsonChunk(text: string): number {
|
|
111
|
+
return (
|
|
112
|
+
(text.length / TokenCounter.BASE_CHARS_PER_TOKEN) *
|
|
113
|
+
TokenCounter.JSON_MULTIPLIER
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private static looksLikeJson(text: string): boolean {
|
|
118
|
+
// Quick heuristic: contains quoted keys and structural punctuation
|
|
119
|
+
return /"[^"]+"\s*:/i.test(text) && /[{}[\],]/.test(text);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private static extractJsonLength(text: string): number {
|
|
123
|
+
let depth = 0;
|
|
124
|
+
let inString = false;
|
|
125
|
+
let escape = false;
|
|
126
|
+
for (let i = 0; i < text.length; i++) {
|
|
127
|
+
const ch = text[i];
|
|
128
|
+
if (escape) {
|
|
129
|
+
escape = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ch === "\\") {
|
|
133
|
+
escape = true;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (ch === '"') {
|
|
137
|
+
inString = !inString;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (inString) continue;
|
|
141
|
+
if (ch === "{" || ch === "[") depth++;
|
|
142
|
+
else if (ch === "}" || ch === "]") {
|
|
143
|
+
depth--;
|
|
144
|
+
if (depth === 0) return i + 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,31 +1,3 @@
|
|
|
1
1
|
declare module "@earendil-works/pi-coding-agent" {
|
|
2
|
-
export
|
|
3
|
-
registerCommand(
|
|
4
|
-
name: string,
|
|
5
|
-
options: {
|
|
6
|
-
description: string;
|
|
7
|
-
handler: (
|
|
8
|
-
args: string | undefined,
|
|
9
|
-
ctx: ExtensionContext,
|
|
10
|
-
) => Promise<void> | void;
|
|
11
|
-
},
|
|
12
|
-
): void;
|
|
13
|
-
on(
|
|
14
|
-
event: string,
|
|
15
|
-
handler: (event: any, ctx: ExtensionContext) => Promise<void> | void,
|
|
16
|
-
): void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ExtensionContext {
|
|
20
|
-
ui: {
|
|
21
|
-
notify(
|
|
22
|
-
message: string,
|
|
23
|
-
level: "info" | "success" | "warning" | "error",
|
|
24
|
-
): void;
|
|
25
|
-
};
|
|
26
|
-
session: {
|
|
27
|
-
messages: any[];
|
|
28
|
-
};
|
|
29
|
-
modelRegistry: any;
|
|
30
|
-
}
|
|
2
|
+
export type ExtensionAPI = any;
|
|
31
3
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-map",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|