pi-context-map 0.4.1 → 0.4.3

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.
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * ReportGenerator
3
- * Generates a visual HTML dashboard based on the ContextMap.
3
+ * Generates a visual HTML dashboard based on the ContextComposition.
4
+ * Apple-inspired design: clean whitespace, SF Pro typography, single blue accent.
4
5
  */
5
6
 
6
7
  import type { ContextComposition } from "./analyzer";
@@ -14,684 +15,616 @@ export class ReportGenerator {
14
15
  composition: ContextComposition,
15
16
  insights: Insight[],
16
17
  ): string {
18
+ const total = composition.total.tokens;
19
+ const usagePercent =
20
+ total > 0 ? Math.round((total / 128_000) * 100) : 0;
21
+
17
22
  const fileCards = composition.files_detail
18
23
  .map(
19
24
  (file) => `
20
- <div class="file-card ${file.status}" data-path="${ReportGenerator.escapeHtml(file.path)}" data-status="${file.status}">
21
- <div class="file-header">
25
+ <div class="file-card" data-path="${ReportGenerator.escapeHtml(file.path)}" data-status="${file.status}">
26
+ <div class="file-card-top">
22
27
  <span class="file-path">${ReportGenerator.escapeHtml(file.path)}</span>
23
- <span class="file-weight">${file.weight.toLocaleString()} tokens</span>
28
+ <span class="file-weight">${file.weight.toLocaleString()}</span>
24
29
  </div>
25
- <div class="file-footer">
26
- <span class="op-badge">${ReportGenerator.getOpIcon(file.lastOp.type)} ${file.lastOp.type}</span>
27
- <span class="turn-badge">Turn ${file.lastOp.turn}</span>
28
- <span class="status-text">${file.status.toUpperCase()}</span>
30
+ <div class="file-card-bottom">
31
+ <span class="op-tag">${ReportGenerator.getOpIcon(file.lastOp.type)} &middot; Turn ${file.lastOp.turn}</span>
32
+ <span class="status-chip ${file.status}">${file.status}</span>
29
33
  </div>
30
- <div class="weight-bar">
31
- <div class="weight-fill" style="width: ${Math.min(100, (file.weight / 1000) * 100)}%"></div>
34
+ <div class="file-bar">
35
+ <div class="file-bar-fill" style="width: ${Math.min(100, (file.weight / Math.max(1, total)) * 100 * 3)}%"></div>
32
36
  </div>
33
- </div>
34
- `,
37
+ </div>`,
35
38
  )
36
39
  .join("");
37
40
 
38
41
  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>
42
+ .map(
43
+ (insight, i) => `
44
+ <div class="insight-card ${insight.severity}${insight.severity === "info" ? " collapsed" : ""}">
45
+ <button class="insight-header" data-target="i${i}" aria-expanded="${insight.severity !== "info"}">
46
+ <svg class="insight-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4 3l4 3-4 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
47
+ <span class="insight-severity">${insight.severity}</span>
47
48
  <span class="insight-title">${ReportGenerator.escapeHtml(insight.title)}</span>
48
49
  </button>
49
50
  <div class="insight-body">
50
- ${ReportGenerator.escapeHtml(insight.message)}
51
- ${insight.command ? `<div class="insight-command">Suggested: <code>${insight.command}</code></div>` : ""}
51
+ <p>${ReportGenerator.escapeHtml(insight.message)}</p>
52
+ ${insight.command ? `<div class="insight-action"><code>${insight.command}</code></div>` : ""}
52
53
  </div>
53
- </div>
54
- `;
55
- })
54
+ </div>`,
55
+ )
56
56
  .join("");
57
57
 
58
- return `
59
- <!DOCTYPE html>
58
+ return `<!DOCTYPE html>
60
59
  <html lang="en">
61
60
  <head>
62
- <meta charset="UTF-8">
63
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
64
- <title>Pi Context Profiler</title>
65
- <style>
66
- /* ============================================
67
- pi-context-map Report Design Tokens
68
- Based on Linear design system + shadcn/ui card patterns
69
- ============================================ */
70
- :root {
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;
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>
391
- body {
392
- background: var(--bg);
393
- color: var(--text);
394
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
395
- margin: 0;
396
- padding: 2rem;
397
- line-height: 1.5;
398
- }
399
- .container { max-width: 1200px; margin: 0 auto; }
400
- header { margin-bottom: 3rem; border-bottom: 1px solid var(--border); padding-bottom: 2rem; }
401
- h1 { font-size: 2rem; margin: 0; color: var(--primary); }
402
- .stats-grid {
403
- display: grid;
404
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
405
- gap: 1.5rem;
406
- margin-top: 2rem;
407
- }
408
- .stat-card {
409
- background: var(--card-bg);
410
- padding: 1.5rem;
411
- border-radius: 12px;
412
- border: 1px solid var(--border);
413
- text-align: center;
414
- }
415
- .stat-value { font-size: 1.5rem; font-weight: bold; display: block; }
416
- .stat-label { color: var(--text-dim); font-size: 0.875rem; text-transform: uppercase; }
417
-
418
- .composition-container {
419
- margin: 2rem 0;
420
- background: var(--card-bg);
421
- padding: 1.5rem;
422
- border-radius: 12px;
423
- border: 1px solid var(--border);
424
- }
425
- .composition-bar {
426
- height: 32px;
427
- background: #020617;
428
- border-radius: 8px;
429
- display: flex;
430
- overflow: hidden;
431
- margin-bottom: 1rem;
432
- }
433
- .composition-segment { height: 100%; transition: width 0.3s ease; }
434
- .seg-system { background: #6366f1; }
435
- .seg-tools { background: #ec4899; }
436
- .seg-history { background: #a855f7; }
437
- .seg-files { background: var(--primary); }
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;
445
- color: var(--text-dim);
446
- }
447
- .legend-item { display: flex; align-items: center; gap: 0.5rem; }
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
- }
479
-
480
- .file-grid {
481
- display: grid;
482
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
483
- gap: 1rem;
484
- }
485
- .file-card {
486
- background: var(--card-bg);
487
- border: 1px solid var(--border);
488
- border-radius: 12px;
489
- padding: 1rem;
490
- transition: transform 0.2s ease, border-color 0.2s ease;
491
- display: flex;
492
- flex-direction: column;
493
- justify-content: space-between;
494
- }
495
- .file-card:hover { transform: translateY(-4px); border-color: var(--primary); }
496
- .file-header {
497
- display: flex;
498
- justify-content: space-between;
499
- align-items: flex-start;
500
- margin-bottom: 1rem;
501
- }
502
- .file-path {
503
- font-family: 'Fira Code', monospace;
504
- font-size: 0.875rem;
505
- word-break: break-all;
506
- margin-right: 1rem;
507
- color: var(--text);
508
- }
509
- .file-weight { font-size: 0.75rem; color: var(--text-dim); white-space: nowrap; }
510
- .file-footer {
511
- display: flex;
512
- justify-content: space-between;
513
- align-items: center;
514
- margin-top: 1rem;
515
- font-size: 0.75rem;
516
- }
517
- .op-badge {
518
- background: #0f172a;
519
- padding: 2px 6px;
520
- border-radius: 4px;
521
- color: var(--text-dim);
522
- }
523
- .turn-badge { color: var(--text-dim); }
524
- .status-text { font-weight: bold; text-transform: uppercase; }
525
-
526
- /* Status Colors */
527
- .active { border-left: 4px solid var(--active); }
528
- .active .status-text { color: var(--active); }
529
- .stale { border-left: 4px solid var(--stale); }
530
- .stale .status-text { color: var(--stale); }
531
- .legacy { border-left: 4px solid var(--legacy); }
532
- .legacy .status-text { color: var(--legacy); }
533
-
534
- .weight-bar {
535
- height: 4px;
536
- background: #020617;
537
- border-radius: 2px;
538
- margin-top: 1rem;
539
- overflow: hidden;
540
- }
541
- .weight-fill {
542
- height: 100%;
543
- background: var(--primary);
544
- transition: width 0.3s ease;
545
- }
546
- </style>
61
+ <meta charset="UTF-8">
62
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
63
+ <meta name="context-map-token" content="{{TOKEN}}">
64
+ <title>Context Profiler</title>
65
+ <link rel="preconnect" href="https://fonts.googleapis.com">
66
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
67
+ <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet">
68
+ <style>
69
+ :root {
70
+ --canvas: #ffffff;
71
+ --canvas-alt: #f5f5f7;
72
+ --surface: #ffffff;
73
+ --hairline: #e0e0e0;
74
+ --hairline-soft: rgba(0,0,0,0.06);
75
+ --ink: #1d1d1f;
76
+ --ink-secondary: #6e6e73;
77
+ --ink-tertiary: #86868b;
78
+ --ink-quaternary: #a1a1a6;
79
+ --accent: #0066cc;
80
+ --accent-hover: #0071e3;
81
+ --accent-soft: rgba(0,102,204,0.08);
82
+ --success: #30d158;
83
+ --success-soft: rgba(48,209,88,0.10);
84
+ --warning: #ff9f0a;
85
+ --warning-soft: rgba(255,159,10,0.10);
86
+ --danger: #ff453a;
87
+ --danger-soft: rgba(255,69,58,0.10);
88
+ --seg-system: #5e5ce6;
89
+ --seg-tools: #ff375f;
90
+ --seg-history: #bf5af2;
91
+ --seg-files: #007aff;
92
+ --seg-summaries: #34c759;
93
+ }
94
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
95
+ body {
96
+ background: var(--canvas);
97
+ color: var(--ink);
98
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
99
+ font-size: 17px;
100
+ line-height: 1.47;
101
+ letter-spacing: -0.374px;
102
+ -webkit-font-smoothing: antialiased;
103
+ -moz-osx-font-smoothing: grayscale;
104
+ }
105
+ .container { max-width: 980px; margin: 0 auto; padding: 80px 24px; }
106
+
107
+ /* Header */
108
+ header { margin-bottom: 64px; }
109
+ h1 {
110
+ font-size: 40px;
111
+ font-weight: 600;
112
+ line-height: 1.1;
113
+ letter-spacing: 0;
114
+ margin-bottom: 4px;
115
+ }
116
+ .subtitle { color: var(--ink-secondary); font-size: 17px; font-weight: 400; margin-bottom: 40px; }
117
+
118
+ /* Stat tiles */
119
+ .stats {
120
+ display: grid;
121
+ grid-template-columns: repeat(4, 1fr);
122
+ gap: 1px;
123
+ background: var(--hairline);
124
+ border: 1px solid var(--hairline);
125
+ border-radius: 18px;
126
+ overflow: hidden;
127
+ margin-bottom: 24px;
128
+ }
129
+ .stat {
130
+ background: var(--surface);
131
+ padding: 24px 20px;
132
+ text-align: center;
133
+ }
134
+ .stat:not(:last-child) { border-right: 1px solid var(--hairline); }
135
+ .stat-value {
136
+ font-size: 28px;
137
+ font-weight: 600;
138
+ letter-spacing: -0.374px;
139
+ display: block;
140
+ font-variant-numeric: tabular-nums;
141
+ }
142
+ .stat-label {
143
+ font-size: 12px;
144
+ font-weight: 400;
145
+ color: var(--ink-tertiary);
146
+ text-transform: uppercase;
147
+ letter-spacing: 0.5px;
148
+ margin-top: 4px;
149
+ display: block;
150
+ }
151
+
152
+ /* Composition card */
153
+ .composition-card {
154
+ background: var(--canvas-alt);
155
+ border-radius: 18px;
156
+ padding: 32px;
157
+ }
158
+ .composition-card h3 {
159
+ font-size: 12px;
160
+ font-weight: 600;
161
+ color: var(--ink-tertiary);
162
+ text-transform: uppercase;
163
+ letter-spacing: 0.8px;
164
+ margin-bottom: 20px;
165
+ }
166
+ .bar {
167
+ height: 8px;
168
+ background: rgba(0,0,0,0.06);
169
+ border-radius: 4px;
170
+ display: flex;
171
+ overflow: hidden;
172
+ margin-bottom: 16px;
173
+ }
174
+ .bar-seg { height: 100%; transition: width 0.4s ease; }
175
+ .bar-seg.seg-system { background: var(--seg-system); }
176
+ .bar-seg.seg-tools { background: var(--seg-tools); }
177
+ .bar-seg.seg-history { background: var(--seg-history); }
178
+ .bar-seg.seg-files { background: var(--seg-files); }
179
+ .bar-seg.seg-summaries { background: var(--seg-summaries); }
180
+
181
+ .legend {
182
+ display: grid;
183
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
184
+ gap: 8px 16px;
185
+ }
186
+ .legend-item {
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 8px;
190
+ font-size: 13px;
191
+ color: var(--ink-secondary);
192
+ font-variant-numeric: tabular-nums;
193
+ }
194
+ .legend-dot { width: 8px; height: 8px; border-radius: 4px; flex-shrink: 0; }
195
+ .legend-dot.sys { background: var(--seg-system); }
196
+ .legend-dot.tools { background: var(--seg-tools); }
197
+ .legend-dot.hist { background: var(--seg-history); }
198
+ .legend-dot.files { background: var(--seg-files); }
199
+ .legend-dot.summ { background: var(--seg-summaries); }
200
+
201
+ /* Section titles */
202
+ h2 {
203
+ font-size: 28px;
204
+ font-weight: 600;
205
+ letter-spacing: 0.196px;
206
+ margin: 64px 0 24px;
207
+ }
208
+ h2:first-of-type { margin-top: 48px; }
209
+
210
+ /* Insight cards */
211
+ .insight-card {
212
+ background: var(--surface);
213
+ border: 1px solid var(--hairline);
214
+ border-left: 3px solid var(--accent);
215
+ border-radius: 14px;
216
+ margin-bottom: 8px;
217
+ overflow: hidden;
218
+ transition: box-shadow 0.2s;
219
+ }
220
+ .insight-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
221
+ .insight-card.critical { border-left-color: var(--danger); background: linear-gradient(90deg, var(--danger-soft) 0%, var(--surface) 100%); }
222
+ .insight-card.warning { border-left-color: var(--warning); background: linear-gradient(90deg, var(--warning-soft) 0%, var(--surface) 100%); }
223
+ .insight-card.success { border-left-color: var(--success); background: linear-gradient(90deg, var(--success-soft) 0%, var(--surface) 100%); }
224
+
225
+ .insight-header {
226
+ display: flex;
227
+ align-items: center;
228
+ gap: 10px;
229
+ width: 100%;
230
+ padding: 14px 16px;
231
+ background: none;
232
+ border: none;
233
+ cursor: pointer;
234
+ font: inherit;
235
+ text-align: left;
236
+ color: inherit;
237
+ -webkit-tap-highlight-color: transparent;
238
+ }
239
+ .insight-header:hover { background: var(--hairline-soft); }
240
+ .insight-chevron {
241
+ flex-shrink: 0;
242
+ color: var(--ink-quaternary);
243
+ transition: transform 0.2s ease;
244
+ }
245
+ .collapsed .insight-chevron { transform: rotate(-90deg); }
246
+
247
+ .insight-severity {
248
+ font-size: 11px;
249
+ font-weight: 600;
250
+ padding: 3px 8px;
251
+ border-radius: 6px;
252
+ text-transform: uppercase;
253
+ letter-spacing: 0.5px;
254
+ background: var(--accent-soft);
255
+ color: var(--accent);
256
+ flex-shrink: 0;
257
+ }
258
+ .insight-card.critical .insight-severity { background: var(--danger-soft); color: var(--danger); }
259
+ .insight-card.warning .insight-severity { background: var(--warning-soft); color: var(--warning); }
260
+
261
+ .insight-title { font-size: 14px; font-weight: 600; color: var(--ink); }
262
+ .insight-body {
263
+ padding: 0 16px 14px 48px;
264
+ font-size: 14px;
265
+ color: var(--ink-secondary);
266
+ line-height: 1.6;
267
+ }
268
+ .collapsed .insight-body { display: none; }
269
+ .insight-body p { margin-bottom: 8px; }
270
+ .insight-action code {
271
+ display: inline-block;
272
+ background: var(--canvas-alt);
273
+ color: var(--accent);
274
+ font-family: "SF Mono", ui-monospace, "Cascadia Code", monospace;
275
+ font-size: 12px;
276
+ padding: 4px 10px;
277
+ border-radius: 8px;
278
+ border: 1px solid var(--hairline);
279
+ }
280
+
281
+ /* File controls */
282
+ .file-controls {
283
+ display: flex;
284
+ gap: 12px;
285
+ margin-bottom: 20px;
286
+ align-items: center;
287
+ flex-wrap: wrap;
288
+ }
289
+ .file-search {
290
+ flex: 1;
291
+ min-width: 220px;
292
+ height: 44px;
293
+ padding: 0 20px;
294
+ border: 1px solid rgba(0,0,0,0.08);
295
+ border-radius: 22px;
296
+ background: var(--surface);
297
+ font: inherit;
298
+ font-size: 14px;
299
+ color: var(--ink);
300
+ outline: none;
301
+ transition: border-color 0.2s;
302
+ }
303
+ .file-search:focus { border-color: var(--accent); }
304
+ .file-search::placeholder { color: var(--ink-quaternary); }
305
+ .file-filter {
306
+ height: 44px;
307
+ padding: 0 16px;
308
+ border: 1px solid rgba(0,0,0,0.08);
309
+ border-radius: 22px;
310
+ background: var(--surface);
311
+ font: inherit;
312
+ font-size: 13px;
313
+ color: var(--ink-secondary);
314
+ outline: none;
315
+ cursor: pointer;
316
+ transition: border-color 0.2s;
317
+ }
318
+ .file-filter:focus { border-color: var(--accent); }
319
+ .file-count { font-size: 13px; color: var(--ink-tertiary); margin-left: auto; }
320
+
321
+ /* File grid */
322
+ .file-grid {
323
+ display: grid;
324
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
325
+ gap: 12px;
326
+ }
327
+ .file-card {
328
+ background: var(--surface);
329
+ border: 1px solid var(--hairline);
330
+ border-radius: 14px;
331
+ padding: 16px;
332
+ transition: border-color 0.2s, box-shadow 0.2s;
333
+ display: flex;
334
+ flex-direction: column;
335
+ gap: 10px;
336
+ }
337
+ .file-card:hover {
338
+ border-color: var(--accent);
339
+ box-shadow: 0 2px 12px rgba(0,102,204,0.08);
340
+ }
341
+ .file-card.hidden { display: none; }
342
+ .file-card-top {
343
+ display: flex;
344
+ justify-content: space-between;
345
+ align-items: flex-start;
346
+ gap: 8px;
347
+ }
348
+ .file-path {
349
+ font-family: "SF Mono", ui-monospace, "Cascadia Code", monospace;
350
+ font-size: 12px;
351
+ color: var(--ink);
352
+ word-break: break-all;
353
+ line-height: 1.5;
354
+ }
355
+ .file-weight {
356
+ font-size: 11px;
357
+ color: var(--ink-tertiary);
358
+ white-space: nowrap;
359
+ font-variant-numeric: tabular-nums;
360
+ flex-shrink: 0;
361
+ }
362
+ .file-card-bottom {
363
+ display: flex;
364
+ justify-content: space-between;
365
+ align-items: center;
366
+ }
367
+ .op-tag {
368
+ font-size: 11px;
369
+ color: var(--ink-tertiary);
370
+ }
371
+ .status-chip {
372
+ font-size: 10px;
373
+ font-weight: 600;
374
+ text-transform: uppercase;
375
+ letter-spacing: 0.5px;
376
+ padding: 2px 8px;
377
+ border-radius: 6px;
378
+ }
379
+ .status-chip.active { background: var(--success-soft); color: #248a3d; }
380
+ .status-chip.stale { background: var(--warning-soft); color: #b87503; }
381
+ .status-chip.legacy { background: var(--danger-soft); color: #cc3a30; }
382
+
383
+ .file-bar {
384
+ height: 3px;
385
+ background: rgba(0,0,0,0.06);
386
+ border-radius: 2px;
387
+ overflow: hidden;
388
+ }
389
+ .file-bar-fill {
390
+ height: 100%;
391
+ background: var(--accent);
392
+ border-radius: 2px;
393
+ transition: width 0.3s ease;
394
+ }
395
+
396
+ .empty-state {
397
+ text-align: center;
398
+ padding: 60px 20px;
399
+ color: var(--ink-tertiary);
400
+ font-size: 15px;
401
+ grid-column: 1 / -1;
402
+ }
403
+
404
+ /* Live status badge */
405
+ .live-badge {
406
+ display: inline-flex;
407
+ align-items: center;
408
+ gap: 6px;
409
+ padding: 4px 12px;
410
+ border-radius: 20px;
411
+ font-size: 11px;
412
+ font-weight: 500;
413
+ background: var(--success-soft);
414
+ color: #248a3d;
415
+ margin-bottom: 20px;
416
+ }
417
+ .live-badge .dot {
418
+ width: 6px;
419
+ height: 6px;
420
+ border-radius: 50%;
421
+ background: var(--success);
422
+ animation: pulse 2s infinite;
423
+ }
424
+ @keyframes pulse {
425
+ 0%, 100% { opacity: 1; }
426
+ 50% { opacity: 0.4; }
427
+ }
428
+
429
+ /* Usage ring */
430
+ .usage-container {
431
+ display: flex;
432
+ gap: 24px;
433
+ align-items: center;
434
+ margin-bottom: 24px;
435
+ }
436
+ .usage-ring {
437
+ width: 80px;
438
+ height: 80px;
439
+ border-radius: 50%;
440
+ position: relative;
441
+ flex-shrink: 0;
442
+ }
443
+ .usage-ring svg { transform: rotate(-90deg); }
444
+ .usage-ring .bg { fill: none; stroke: rgba(0,0,0,0.06); stroke-width: 6; }
445
+ .usage-ring .fg {
446
+ fill: none;
447
+ stroke: var(--accent);
448
+ stroke-width: 6;
449
+ stroke-linecap: round;
450
+ transition: stroke-dashoffset 0.6s ease;
451
+ }
452
+ .usage-ring.critical .fg { stroke: var(--danger); }
453
+ .usage-ring.warning .fg { stroke: var(--warning); }
454
+ .usage-label {
455
+ font-size: 18px;
456
+ font-weight: 600;
457
+ letter-spacing: -0.2px;
458
+ }
459
+ .usage-label small {
460
+ font-size: 13px;
461
+ font-weight: 400;
462
+ color: var(--ink-secondary);
463
+ display: block;
464
+ margin-top: 2px;
465
+ }
466
+
467
+ /* Responsive */
468
+ @media (max-width: 700px) {
469
+ .container { padding: 40px 16px; }
470
+ h1 { font-size: 32px; }
471
+ .stats { grid-template-columns: repeat(2, 1fr); }
472
+ .stat:nth-child(2) { border-right: none; }
473
+ .stat:nth-child(3), .stat:nth-child(4) { border-top: 1px solid var(--hairline); }
474
+ .stat-value { font-size: 22px; }
475
+ .file-grid { grid-template-columns: 1fr; }
476
+ .legend { grid-template-columns: repeat(2, 1fr); }
477
+ .composition-card { padding: 20px; }
478
+ .usage-container { flex-direction: column; align-items: flex-start; }
479
+ }
480
+ </style>
547
481
  </head>
548
482
  <body>
549
- <div class="container">
550
- <header>
551
- <h1>Pi Context Profiler</h1>
552
- <p style="color: var(--text-dim)">Professional session context window analysis with actionable insights.</p>
553
-
554
- <div class="stats-grid">
555
- <div class="stat-card">
556
- <span class="stat-value">${composition.total.tokens.toLocaleString()}</span>
557
- <span class="stat-label">Total Tokens</span>
558
- </div>
559
- <div class="stat-card">
560
- <span class="stat-value">${composition.files_detail.length}</span>
561
- <span class="stat-label">Files in Context</span>
562
- </div>
563
- <div class="stat-card">
564
- <span class="stat-value">${composition.tools.tokens.toLocaleString()}</span>
565
- <span class="stat-label">Tool Tokens</span>
566
- </div>
567
- <div class="stat-card">
568
- <span class="stat-value">${Math.round((composition.total.tokens / 128000) * 100)}%</span>
569
- <span class="stat-label">Of 128k Window</span>
570
- </div>
571
- </div>
572
-
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>
581
- </div>
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>
588
- </div>
589
- </div>
590
- </header>
591
-
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>
614
- </div>
615
-
616
- <script>
617
- (function() {
618
- // ===== Live update via Server-Sent Events =====
619
- // Token is injected into the script via a meta tag (set by the server).
620
- // Connect to /events?token=...; when the server pushes a new html payload, replace the document.
621
- try {
622
- var tokenMeta = document.querySelector('meta[name="context-map-token"]');
623
- var token = tokenMeta ? tokenMeta.getAttribute('content') : '';
624
- var evtSource = new EventSource('/events?token=' + encodeURIComponent(token));
625
- evtSource.onmessage = function(e) {
626
- try {
627
- var payload = JSON.parse(e.data);
628
- if (payload.html) {
629
- // Replace the document body with the new HTML
630
- var parser = new DOMParser();
631
- var newDoc = parser.parseFromString(payload.html, 'text/html');
632
- document.documentElement.replaceChild(
633
- document.importNode(newDoc.documentElement, true),
634
- document.documentElement
635
- );
636
- }
637
- } catch (err) {
638
- console.warn('Failed to apply live update:', err);
639
- }
640
- };
641
- evtSource.onerror = function() {
642
- // Silently close on error; user can refresh the page to reconnect
643
- evtSource.close();
644
- };
645
- } catch (err) {
646
- // EventSource not available; fall back to manual refresh
647
- }
648
-
649
- // ===== Insight collapse/expand =====
650
- document.querySelectorAll('.insight-header[data-toggle]').forEach(function(btn) {
651
- btn.addEventListener('click', function() {
652
- var card = btn.closest('.insight-card');
653
- var isCollapsed = card.classList.toggle('collapsed');
654
- btn.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
655
- });
656
- });
657
-
658
- // ===== File search & filter =====
659
- var search = document.getElementById('fileSearch');
660
- var filter = document.getElementById('fileFilter');
661
- var grid = document.getElementById('fileGrid');
662
- var count = document.getElementById('fileCount');
663
- var empty = document.getElementById('emptyState');
664
- var cards = grid ? Array.prototype.slice.call(grid.querySelectorAll('.file-card')) : [];
665
- var total = cards.length;
666
-
667
- function applyFilters() {
668
- var query = (search.value || '').toLowerCase();
669
- var status = filter.value;
670
- var visible = 0;
671
- cards.forEach(function(card) {
672
- var path = (card.getAttribute('data-path') || '').toLowerCase();
673
- var cardStatus = card.getAttribute('data-status') || '';
674
- var matchQuery = !query || path.indexOf(query) !== -1;
675
- var matchStatus = status === 'all' || cardStatus === status;
676
- if (matchQuery && matchStatus) {
677
- card.classList.remove('hidden');
678
- visible++;
679
- } else {
680
- card.classList.add('hidden');
681
- }
682
- });
683
- count.textContent = visible === total ? total + ' files' : visible + ' of ' + total + ' files';
684
- empty.style.display = visible === 0 ? 'block' : 'none';
685
- }
686
-
687
- if (search) search.addEventListener('input', applyFilters);
688
- if (filter) filter.addEventListener('change', applyFilters);
689
- applyFilters();
690
- })();
691
- </script>
483
+ <div class="container">
484
+
485
+ <header>
486
+ <div class="live-badge"><span class="dot"></span>Live</div>
487
+ <h1>Context Profiler</h1>
488
+ <p class="subtitle">Session context window breakdown with actionable recommendations</p>
489
+
490
+ <div class="stats">
491
+ <div class="stat">
492
+ <span class="stat-value">${total.toLocaleString()}</span>
493
+ <span class="stat-label">Total Tokens</span>
494
+ </div>
495
+ <div class="stat">
496
+ <span class="stat-value">${composition.files_detail.length}</span>
497
+ <span class="stat-label">Files</span>
498
+ </div>
499
+ <div class="stat">
500
+ <span class="stat-value">${insights.filter(i => i.severity === "warning" || i.severity === "critical").length}</span>
501
+ <span class="stat-label">Alerts</span>
502
+ </div>
503
+ <div class="stat">
504
+ <span class="stat-value">${usagePercent}<span style="font-size:14px;color:var(--ink-tertiary)">%</span></span>
505
+ <span class="stat-label">of 128k Window</span>
506
+ </div>
507
+ </div>
508
+
509
+ <div class="usage-container">
510
+ <div class="usage-ring${usagePercent > 80 ? " critical" : usagePercent > 60 ? " warning" : ""}">
511
+ <svg width="80" height="80" viewBox="0 0 80 80">
512
+ <circle class="bg" cx="40" cy="40" r="34"/>
513
+ <circle class="fg" cx="40" cy="40" r="34"
514
+ stroke-dasharray="${2 * Math.PI * 34}"
515
+ stroke-dashoffset="${2 * Math.PI * 34 * (1 - usagePercent / 100)}"/>
516
+ </svg>
517
+ </div>
518
+ <div class="usage-label">
519
+ ${usagePercent}% of 128k window
520
+ <small>${usagePercent > 80 ? "Compaction recommended" : usagePercent > 60 ? "Monitor usage" : "Healthy"}</small>
521
+ </div>
522
+ </div>
523
+
524
+ <div class="composition-card">
525
+ <h3>Context Composition</h3>
526
+ <div class="bar">
527
+ ${ReportGenerator.seg("seg-system", composition.system.percent)}
528
+ ${ReportGenerator.seg("seg-tools", composition.tools.percent)}
529
+ ${ReportGenerator.seg("seg-history", composition.history.percent)}
530
+ ${ReportGenerator.seg("seg-files", composition.files.percent)}
531
+ ${ReportGenerator.seg("seg-summaries", composition.summaries.percent)}
532
+ </div>
533
+ <div class="legend">
534
+ <div class="legend-item"><span class="legend-dot sys"></span> System &mdash; ${composition.system.tokens.toLocaleString()} (${composition.system.percent}%)</div>
535
+ <div class="legend-item"><span class="legend-dot tools"></span> Tools &mdash; ${composition.tools.tokens.toLocaleString()} (${composition.tools.percent}%)</div>
536
+ <div class="legend-item"><span class="legend-dot hist"></span> History &mdash; ${composition.history.tokens.toLocaleString()} (${composition.history.percent}%)</div>
537
+ <div class="legend-item"><span class="legend-dot files"></span> Files &mdash; ${composition.files.tokens.toLocaleString()} (${composition.files.percent}%)</div>
538
+ <div class="legend-item"><span class="legend-dot summ"></span> Summaries &mdash; ${composition.summaries.tokens.toLocaleString()} (${composition.summaries.percent}%)</div>
539
+ </div>
540
+ </div>
541
+ </header>
542
+
543
+ <section>
544
+ <h2>Insights</h2>
545
+ ${insightCards || `<p style="color:var(--ink-tertiary);font-size:15px;">No insights available yet &mdash; the context composition is balanced.</p>`}
546
+ </section>
547
+
548
+ <section>
549
+ <h2>Files <span style="font-size:14px;font-weight:400;color:var(--ink-tertiary);letter-spacing:0;">(${composition.files_detail.length})</span></h2>
550
+ <div class="file-controls">
551
+ <input type="text" class="file-search" id="fileSearch" placeholder="Search files" aria-label="Search files">
552
+ <select class="file-filter" id="fileFilter" aria-label="Filter by status">
553
+ <option value="all">All</option>
554
+ <option value="active">Active</option>
555
+ <option value="stale">Stale</option>
556
+ <option value="legacy">Legacy</option>
557
+ </select>
558
+ <span class="file-count" id="fileCount"></span>
559
+ </div>
560
+ <div class="file-grid" id="fileGrid">
561
+ ${fileCards || '<div class="empty-state">No files tracked in the current session context.</div>'}
562
+ </div>
563
+ <div class="empty-state" id="emptyState" style="display:none">No files match your current filters.</div>
564
+ </section>
565
+
566
+ </div>
567
+
568
+ <script>
569
+ (function() {
570
+ var search = document.getElementById('fileSearch');
571
+ var filter = document.getElementById('fileFilter');
572
+ var grid = document.getElementById('fileGrid');
573
+ var count = document.getElementById('fileCount');
574
+ var empty = document.getElementById('emptyState');
575
+ var cards = grid ? Array.from(grid.querySelectorAll('.file-card')) : [];
576
+ var total = cards.length;
577
+
578
+ function update() {
579
+ var q = (search.value || '').toLowerCase();
580
+ var s = filter.value;
581
+ var v = 0;
582
+ for (var i = 0; i < cards.length; i++) {
583
+ var c = cards[i];
584
+ var p = (c.getAttribute('data-path') || '').toLowerCase();
585
+ var st = c.getAttribute('data-status') || '';
586
+ var mq = !q || p.indexOf(q) !== -1;
587
+ var ms = s === 'all' || st === s;
588
+ if (mq && ms) { c.classList.remove('hidden'); v++; }
589
+ else { c.classList.add('hidden'); }
590
+ }
591
+ count.textContent = v === total ? total + ' files' : v + ' of ' + total;
592
+ empty.style.display = v === 0 ? '' : 'none';
593
+ }
594
+ if (search) search.addEventListener('input', update);
595
+ if (filter) filter.addEventListener('change', update);
596
+ update();
597
+
598
+ // Insight toggles
599
+ var btns = document.querySelectorAll('.insight-header');
600
+ for (var j = 0; j < btns.length; j++) {
601
+ btns[j].addEventListener('click', function() {
602
+ var card = this.closest('.insight-card');
603
+ var was = card.classList.toggle('collapsed');
604
+ this.setAttribute('aria-expanded', was ? 'false' : 'true');
605
+ });
606
+ }
607
+
608
+ // Live SSE
609
+ try {
610
+ var token = (document.querySelector('meta[name="context-map-token"]') || {}).getAttribute('content') || '';
611
+ var es = new EventSource('/events?token=' + encodeURIComponent(token));
612
+ es.onmessage = function(e) {
613
+ try {
614
+ var p = JSON.parse(e.data);
615
+ if (p.html) {
616
+ var d = new DOMParser().parseFromString(p.html, 'text/html');
617
+ document.replaceChild(document.importNode(d.documentElement, true), document.documentElement);
618
+ }
619
+ } catch(_) {}
620
+ };
621
+ es.onerror = function() { es.close(); };
622
+ } catch(_) {}
623
+ })();
624
+ </script>
625
+
692
626
  </body>
693
- </html>
694
- `;
627
+ </html>`;
695
628
  }
696
629
 
697
630
  public static writeReport(html: string): string {
@@ -702,29 +635,29 @@ export class ReportGenerator {
702
635
  return reportPath;
703
636
  }
704
637
 
638
+ private static seg(cls: string, pct: number): string {
639
+ return pct > 0
640
+ ? `<div class="bar-seg ${cls}" style="width:${pct}%"></div>`
641
+ : "";
642
+ }
643
+
705
644
  private static getOpIcon(type: string): string {
706
645
  switch (type) {
707
646
  case "read":
708
- return "READ";
647
+ return "Read";
709
648
  case "write":
710
- return "WRITE";
649
+ return "Write";
711
650
  case "edit":
712
- return "EDIT";
651
+ return "Edit";
713
652
  case "delete":
714
- return "DELETE";
653
+ return "Delete";
715
654
  default:
716
- return "FILE";
655
+ return "Access";
717
656
  }
718
657
  }
719
658
 
720
659
  private static escapeHtml(text: string): string {
721
- const map = {
722
- "&": "&amp;",
723
- "<": "&lt;",
724
- ">": "&gt;",
725
- '"': "&quot;",
726
- "'": "&#039;",
727
- };
728
- return text.replace(/[&<>"']/g, (m) => map[m as keyof typeof map]);
660
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
661
+ .replace(/"/g, "&quot;").replace(/'/g, "&#039;");
729
662
  }
730
663
  }