pi-context-map 0.3.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 CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## [0.3.0] - 2026-06-14
4
12
  ### Professional Context Profiler
5
13
  - **Code-Aware Token Counting**: New `TokenCounter` module applies multipliers for code blocks (1.3x) and JSON (1.5x) for more accurate estimation.
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 token budget bar and a file-weight grid.
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
 
@@ -17,7 +17,7 @@ export class ReportGenerator {
17
17
  const fileCards = composition.files_detail
18
18
  .map(
19
19
  (file) => `
20
- <div class="file-card ${file.status}">
20
+ <div class="file-card ${file.status}" data-path="${ReportGenerator.escapeHtml(file.path)}" data-status="${file.status}">
21
21
  <div class="file-header">
22
22
  <span class="file-path">${ReportGenerator.escapeHtml(file.path)}</span>
23
23
  <span class="file-weight">${file.weight.toLocaleString()} tokens</span>
@@ -36,18 +36,23 @@ export class ReportGenerator {
36
36
  .join("");
37
37
 
38
38
  const insightCards = insights
39
- .map(
40
- (insight) => `
41
- <div class="insight-card ${insight.severity}">
42
- <div class="insight-header">
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>
43
46
  <span class="insight-severity">${insight.severity.toUpperCase()}</span>
44
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>` : ""}
45
52
  </div>
46
- <div class="insight-body">${ReportGenerator.escapeHtml(insight.message)}</div>
47
- ${insight.command ? `<div class="insight-command">Suggested: <code>${insight.command}</code></div>` : ""}
48
53
  </div>
49
- `,
50
- )
54
+ `;
55
+ })
51
56
  .join("");
52
57
 
53
58
  return `
@@ -56,19 +61,333 @@ export class ReportGenerator {
56
61
  <head>
57
62
  <meta charset="UTF-8">
58
63
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
59
- <title>Pi Context Map</title>
64
+ <title>Pi Context Profiler</title>
60
65
  <style>
66
+ /* ============================================
67
+ pi-context-map Report — Design Tokens
68
+ Based on Linear design system + shadcn/ui card patterns
69
+ ============================================ */
61
70
  :root {
62
- --bg: #0f172a;
63
- --card-bg: #1e293b;
64
- --text: #f1f5f9;
65
- --text-dim: #94a3b8;
66
- --primary: #38bdf8;
67
- --active: #22c55e;
68
- --stale: #eab308;
69
- --legacy: #ef4444;
70
- --border: #334155;
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;
71
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;
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>
72
391
  body {
73
392
  background: var(--bg);
74
393
  color: var(--text);
@@ -275,10 +594,70 @@ export class ReportGenerator {
275
594
  ${insightCards}
276
595
  </section>
277
596
 
278
- <div class="file-grid">
279
- ${fileCards}
280
- </div>
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>
281
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>
282
661
  </body>
283
662
  </html>
284
663
  `;
@@ -294,11 +673,16 @@ export class ReportGenerator {
294
673
 
295
674
  private static getOpIcon(type: string): string {
296
675
  switch (type) {
297
- case "read": return "READ";
298
- case "write": return "WRITE";
299
- case "edit": return "EDIT";
300
- case "delete": return "DELETE";
301
- default: return "FILE";
676
+ case "read":
677
+ return "READ";
678
+ case "write":
679
+ return "WRITE";
680
+ case "edit":
681
+ return "EDIT";
682
+ case "delete":
683
+ return "DELETE";
684
+ default:
685
+ return "FILE";
302
686
  }
303
687
  }
304
688
 
@@ -27,11 +27,17 @@ export default async function piContextMap(pi: ExtensionAPI) {
27
27
  ctx.ui.notify("Analyzing session context...", "info");
28
28
  try {
29
29
  const { reportPath, insights } = await runAnalysis();
30
- const criticalCount = insights.filter((i) => i.severity === "critical").length;
31
- const summary = criticalCount > 0
32
- ? `Context map generated. ${criticalCount} critical insight(s) found.`
33
- : `Context map generated successfully.`;
34
- ctx.ui.notify(`${summary} Path: ${reportPath}`, criticalCount > 0 ? "warning" : "success");
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.`;
37
+ ctx.ui.notify(
38
+ `${summary} Path: ${reportPath}`,
39
+ criticalCount > 0 ? "warning" : "success",
40
+ );
35
41
  } catch (error) {
36
42
  const message = error instanceof Error ? error.message : String(error);
37
43
  ctx.ui.notify(`Failed to generate context map: ${message}`, "error");
@@ -41,7 +47,8 @@ export default async function piContextMap(pi: ExtensionAPI) {
41
47
 
42
48
  pi.registerTool({
43
49
  name: "context-map",
44
- description: "Analyze the current session context composition and return actionable insights.",
50
+ description:
51
+ "Analyze the current session context composition and return actionable insights.",
45
52
  parameters: {
46
53
  type: "object",
47
54
  properties: {},
@@ -49,7 +56,8 @@ export default async function piContextMap(pi: ExtensionAPI) {
49
56
  handler: async (_ctx: any, _args: any) => {
50
57
  try {
51
58
  const { composition, insights } = await runAnalysis();
52
- const summary = `Context: ${composition.total.tokens.toLocaleString()} tokens total. ` +
59
+ const summary =
60
+ `Context: ${composition.total.tokens.toLocaleString()} tokens total. ` +
53
61
  `System ${composition.system.percent}%, Tools ${composition.tools.percent}%, ` +
54
62
  `History ${composition.history.percent}%, Files ${composition.files.percent}%, ` +
55
63
  `Summaries ${composition.summaries.percent}%. ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-context-map",
3
- "version": "0.3.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",