@raquezha/notrace 0.0.5 → 0.0.7

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.
Files changed (97) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +122 -52
  3. package/assets/notrace-logo.svg +20 -0
  4. package/assets/notrace-mark.svg +18 -0
  5. package/assets/notrace-wordmark.svg +4 -0
  6. package/bin/notrace-compare.mjs +153 -0
  7. package/bin/notrace-review.mjs +123 -0
  8. package/dist/notrace/adapters.d.ts +32 -0
  9. package/dist/notrace/adapters.js +83 -0
  10. package/dist/notrace/index.d.ts +2 -0
  11. package/dist/notrace/index.js +335 -0
  12. package/dist/notrace/renderer.d.ts +4 -0
  13. package/dist/notrace/renderer.js +681 -0
  14. package/dist/notrace/types.d.ts +94 -0
  15. package/dist/notrace/types.js +1 -0
  16. package/extensions/notrace/adapters.ts +88 -0
  17. package/extensions/notrace/index.ts +393 -0
  18. package/extensions/notrace/renderer.ts +694 -0
  19. package/extensions/notrace/types.ts +109 -0
  20. package/package.json +10 -3
  21. package/templates/README.md +24 -0
  22. package/templates/dashboard.sample.html +399 -0
  23. package/templates/dashboard.sample.json +113 -0
  24. package/templates/notrace-logo.preview.png +0 -0
  25. package/templates/render-samples.mjs +44 -0
  26. package/templates/session.sample.html +499 -0
  27. package/templates/session.sample.json +127 -0
  28. package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.html +313 -0
  29. package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.json +129 -0
  30. package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.html +313 -0
  31. package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.json +129 -0
  32. package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html +494 -0
  33. package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.json +134 -0
  34. package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html +493 -0
  35. package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.json +133 -0
  36. package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html +494 -0
  37. package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.json +134 -0
  38. package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.html +423 -0
  39. package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.json +130 -0
  40. package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.html +423 -0
  41. package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.json +130 -0
  42. package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.html +423 -0
  43. package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.json +130 -0
  44. package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.html +423 -0
  45. package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.json +130 -0
  46. package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.html +423 -0
  47. package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.json +130 -0
  48. package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.html +423 -0
  49. package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.json +130 -0
  50. package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.html +423 -0
  51. package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.json +130 -0
  52. package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.html +423 -0
  53. package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.json +130 -0
  54. package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.html +423 -0
  55. package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.json +130 -0
  56. package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.html +423 -0
  57. package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.json +130 -0
  58. package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.html +423 -0
  59. package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.json +130 -0
  60. package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.html +423 -0
  61. package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.json +130 -0
  62. package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.html +423 -0
  63. package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.json +130 -0
  64. package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.html +423 -0
  65. package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.json +130 -0
  66. package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.html +423 -0
  67. package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.json +130 -0
  68. package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.html +423 -0
  69. package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.json +130 -0
  70. package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.html +423 -0
  71. package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.json +130 -0
  72. package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.html +423 -0
  73. package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.json +130 -0
  74. package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.html +423 -0
  75. package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.json +130 -0
  76. package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.html +423 -0
  77. package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.json +130 -0
  78. package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.html +423 -0
  79. package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.json +130 -0
  80. package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.html +423 -0
  81. package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.json +130 -0
  82. package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.html +423 -0
  83. package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.json +130 -0
  84. package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.html +423 -0
  85. package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.json +130 -0
  86. package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.html +423 -0
  87. package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.json +130 -0
  88. package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.html +423 -0
  89. package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.json +130 -0
  90. package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.html +423 -0
  91. package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.json +130 -0
  92. package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.html +313 -0
  93. package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.json +129 -0
  94. package/tsconfig.json +1 -1
  95. package/dist/notrace.d.ts +0 -9
  96. package/dist/notrace.js +0 -914
  97. package/extensions/notrace.ts +0 -965
@@ -0,0 +1,694 @@
1
+ export function escapeHtml(v: unknown): string {
2
+ const map: Record<string, string> = {
3
+ "&": "&amp;",
4
+ "<": "&lt;",
5
+ ">": "&gt;",
6
+ "'": "&#39;",
7
+ '"': "&quot;"
8
+ };
9
+ return String(v ?? "").replace(/[&<>'"]/g, c => map[c]);
10
+ }
11
+
12
+ export function safeJsonForScript(v: any): string {
13
+ const map: Record<string, string> = {
14
+ "<": "\\u003c",
15
+ ">": "\\u003e",
16
+ "&": "\\u0026",
17
+ "\u2028": "\\u2028",
18
+ "\u2029": "\\u2029"
19
+ };
20
+ return JSON.stringify(v).replace(/[<>&\u2028\u2029]/g, c => map[c]);
21
+ }
22
+
23
+ function parseDate(value: string | number): Date | null {
24
+ const date = new Date(value);
25
+ return Number.isNaN(date.getTime()) ? null : date;
26
+ }
27
+
28
+ function formatDateCell(value: string | number): string {
29
+ const date = parseDate(value);
30
+ if (!date) return `<span>${escapeHtml(value)}</span>`;
31
+ const day = date.toISOString().slice(0, 10);
32
+ const time = date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });
33
+ return `<div class="date-cell"><strong>${escapeHtml(day)}</strong><span>${escapeHtml(time)}</span></div>`;
34
+ }
35
+
36
+ function formatDateLong(value: string | number): string {
37
+ const date = parseDate(value);
38
+ if (!date) return escapeHtml(value);
39
+ const day = date.toISOString().slice(0, 10);
40
+ const time = date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });
41
+ return `${escapeHtml(day)} ${escapeHtml(time)}`;
42
+ }
43
+
44
+ function formatTime(value: string | number): string {
45
+ const date = parseDate(value);
46
+ if (!date) return escapeHtml(value);
47
+ return escapeHtml(date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }));
48
+ }
49
+
50
+ function workflowDisplayName(workflow: string | null | undefined): string {
51
+ switch (workflow) {
52
+ case "norpiv":
53
+ return "RPIV";
54
+ case "research":
55
+ return "Research";
56
+ case "generic":
57
+ default:
58
+ return "Generic";
59
+ }
60
+ }
61
+
62
+ function workflowClassName(workflow: string | null | undefined): string {
63
+ switch (workflow) {
64
+ case "norpiv":
65
+ return "workflow-rpiv";
66
+ case "research":
67
+ return "workflow-research";
68
+ case "generic":
69
+ default:
70
+ return "workflow-generic";
71
+ }
72
+ }
73
+
74
+ function taskDisplay(taskish: any): string {
75
+ const task = taskish?.task || taskish;
76
+ const workflow = task?.workflow || taskish?.workflow || "generic";
77
+ const taskId = task?.id ?? taskish?.taskId;
78
+ if (taskId) {
79
+ if (workflow === "research" && String(taskId).startsWith("branch:")) {
80
+ return `Branch ${String(taskId).slice(7)}`;
81
+ }
82
+ return String(taskId);
83
+ }
84
+ if (workflow === "research") return "Open research";
85
+ if (workflow === "generic") return "General session";
86
+ return "No active task";
87
+ }
88
+
89
+ function resolveRepoName(data: any): string {
90
+ const name = data?.repository?.name || data?.repositoryName || data?.repoName || "Repository";
91
+ const branch = data?.repository?.branch;
92
+ return branch ? `${name} @ ${branch}` : name;
93
+ }
94
+
95
+ function formatUsd(value: number | undefined): string {
96
+ const num = Number(value || 0);
97
+ if (num === 0) return "$0";
98
+ return `$${num.toFixed(5)}`;
99
+ }
100
+
101
+ function formatTelemetryStatus(value: string | undefined): string {
102
+ switch (value) {
103
+ case "active": return "Active";
104
+ case "loaded-disabled": return "Loaded disabled";
105
+ case "loaded-inactive": return "Loaded inactive";
106
+ case "absent": return "Absent";
107
+ case "unknown":
108
+ default:
109
+ return "Unknown";
110
+ }
111
+ }
112
+
113
+ function summarizeEventCount(data: any): number {
114
+ return Array.isArray(data?.events)
115
+ ? data.events.filter((ev: any) => ev?.type !== "session_start" && ev?.type !== "turn_start").length
116
+ : 0;
117
+ }
118
+
119
+ function wordmarkSvg(className = "wordmark"): string {
120
+ return `<svg class="${escapeHtml(className)}" viewBox="0 0 420 138" aria-label="notrace" role="img" xmlns="http://www.w3.org/2000/svg">
121
+ <defs>
122
+ <linearGradient id="fadeGrad" x1="0%" y1="0%" x2="100%" y2="0%">
123
+ <stop offset="0%" stop-color="#E2754A"/>
124
+ <stop offset="100%" stop-color="#EDE2D2"/>
125
+ </linearGradient>
126
+ </defs>
127
+ <g id="trace-icon" transform="translate(1 -18) scale(0.93)">
128
+ <path d="M6,50 C16,18 26,18 36,50 C46,82 54,82 60,50 C64,30 68,30 71,50"
129
+ fill="none" stroke="url(#fadeGrad)" stroke-width="4" stroke-linecap="round"/>
130
+ <line x1="74" y1="50" x2="79" y2="50" stroke="#D9C9B5" stroke-width="4" stroke-linecap="round" stroke-opacity="0.6"/>
131
+ <circle cx="85" cy="50" r="2.2" fill="#D9C9B5" opacity="0.5"/>
132
+ <circle cx="90" cy="50" r="1.4" fill="#EDE2D2" opacity="0.32"/>
133
+ <circle cx="94" cy="50" r="0.9" fill="#EDE2D2" opacity="0.15"/>
134
+ </g>
135
+ <text x="0" y="114" fill="#ECE3DA" style="fill:#ECE3DA" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif" font-size="96" font-weight="900" letter-spacing="-7">no</text>
136
+ <text x="82" y="114" fill="#d88462" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif" font-size="96" font-weight="900" letter-spacing="-7">trace</text>
137
+ </svg>`;
138
+ }
139
+
140
+ function faviconHref(): string {
141
+ const svg = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" role="img"><title>notrace logo mark</title><desc>A wave that smooths into a flat line, then fades into dots — color shifts from trace orange to no cream as it dissolves.</desc><defs><linearGradient id="fadeGrad" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#E2754A"/><stop offset="100%" stop-color="#EDE2D2"/></linearGradient></defs><g id="trace-icon"><path d="M6,50 C16,18 26,18 36,50 C46,82 54,82 60,50 C64,30 68,30 71,50" fill="none" stroke="url(#fadeGrad)" stroke-width="4" stroke-linecap="round"/><line x1="74" y1="50" x2="79" y2="50" stroke="#D9C9B5" stroke-width="4" stroke-linecap="round" stroke-opacity="0.6"/><circle cx="85" cy="50" r="2.2" fill="#D9C9B5" opacity="0.5"/><circle cx="90" cy="50" r="1.4" fill="#EDE2D2" opacity="0.32"/><circle cx="94" cy="50" r="0.9" fill="#EDE2D2" opacity="0.15"/></g></svg>`;
142
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
143
+ }
144
+
145
+ function shell(title: string, body: string, script = ""): string {
146
+ return `<!DOCTYPE html>
147
+ <html lang="en">
148
+ <head>
149
+ <meta charset="UTF-8">
150
+ <meta name="viewport" content="width=device-width, initial-scale=1">
151
+ <title>${escapeHtml(title)}</title>
152
+ <link rel="icon" href="${faviconHref()}">
153
+ <style>
154
+ :root {
155
+ --bg: #0c0b0a;
156
+ --panel: rgba(255,255,255,0.04);
157
+ --panel-strong: rgba(255,255,255,0.06);
158
+ --text: #ece3da;
159
+ --muted: rgba(236,227,218,0.68);
160
+ --accent: #d88462;
161
+ --accent-soft: rgba(216,132,98,0.12);
162
+ --border: rgba(255,255,255,0.08);
163
+ --shadow: 0 20px 50px rgba(0,0,0,0.45);
164
+ --code: #090807;
165
+ --err: #ef7f7f;
166
+ --rpiv-fg: #f3be8a;
167
+ --rpiv-bg: rgba(243,190,138,0.12);
168
+ --rpiv-border: rgba(243,190,138,0.26);
169
+ --research-fg: #8ec5ff;
170
+ --research-bg: rgba(142,197,255,0.12);
171
+ --research-border: rgba(142,197,255,0.24);
172
+ --generic-fg: #b9b4ae;
173
+ --generic-bg: rgba(185,180,174,0.12);
174
+ --generic-border: rgba(185,180,174,0.2);
175
+ }
176
+ * { box-sizing: border-box; }
177
+ html { color-scheme: dark; }
178
+ body {
179
+ margin: 0;
180
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
181
+ background: var(--bg);
182
+ color: var(--text);
183
+ line-height: 1.5;
184
+ background-image: radial-gradient(circle at 50% -10%, rgba(216,132,98,0.14), transparent 45%);
185
+ background-attachment: fixed;
186
+ }
187
+ a { color: inherit; }
188
+ .container { max-width: 1120px; margin: 0 auto; padding: 32px 20px 64px; }
189
+ .hero, .panel {
190
+ background: var(--panel);
191
+ border: 1px solid var(--border);
192
+ border-radius: 24px;
193
+ box-shadow: var(--shadow);
194
+ backdrop-filter: blur(10px);
195
+ }
196
+ .hero { padding: 28px; margin-bottom: 24px; }
197
+ .hero-top {
198
+ display: grid;
199
+ grid-template-columns: minmax(0, 1fr) auto;
200
+ gap: 16px;
201
+ align-items: start;
202
+ }
203
+ .brand { margin-bottom: 10px; }
204
+ .brand-link {
205
+ display: inline-flex;
206
+ align-items: flex-start;
207
+ text-decoration: none;
208
+ }
209
+ .wordmark {
210
+ width: 340px;
211
+ height: 112px;
212
+ display: block;
213
+ overflow: visible;
214
+ }
215
+ .subtitle { margin: 10px 0 0; color: var(--muted); }
216
+ .session-subtitle {
217
+ display: flex;
218
+ align-items: center;
219
+ gap: 10px;
220
+ flex-wrap: wrap;
221
+ }
222
+ .session-id-chip {
223
+ display: inline-flex;
224
+ align-items: center;
225
+ gap: 8px;
226
+ max-width: 100%;
227
+ padding: 6px 8px 6px 10px;
228
+ border: 1px solid var(--border);
229
+ border-radius: 999px;
230
+ background: rgba(0,0,0,0.18);
231
+ color: var(--text);
232
+ font-family: "SFMono-Regular", ui-monospace, Menlo, Monaco, Consolas, monospace;
233
+ font-size: 0.78rem;
234
+ word-break: break-all;
235
+ }
236
+ .copy-btn {
237
+ display: inline-flex;
238
+ align-items: center;
239
+ justify-content: center;
240
+ width: 26px;
241
+ height: 26px;
242
+ border: 1px solid rgba(255,255,255,0.12);
243
+ border-radius: 999px;
244
+ background: rgba(255,255,255,0.04);
245
+ color: var(--muted);
246
+ cursor: pointer;
247
+ transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
248
+ }
249
+ .copy-btn:hover, .copy-btn.copied {
250
+ color: var(--text);
251
+ border-color: rgba(216,132,98,0.45);
252
+ background: var(--accent-soft);
253
+ }
254
+ .meta {
255
+ display: flex;
256
+ gap: 8px;
257
+ flex-wrap: wrap;
258
+ justify-content: flex-end;
259
+ align-items: center;
260
+ margin-top: 16px;
261
+ }
262
+ .pill, .workflow-pill, .sort-btn {
263
+ display: inline-flex;
264
+ align-items: center;
265
+ gap: 6px;
266
+ text-decoration: none;
267
+ padding: 8px 12px;
268
+ border: 1px solid var(--border);
269
+ border-radius: 999px;
270
+ background: rgba(255,255,255,0.03);
271
+ color: var(--muted);
272
+ font-size: 0.86rem;
273
+ font-weight: 600;
274
+ }
275
+ .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 16px; margin: 24px 0; }
276
+ .metric-card {
277
+ background: var(--panel-strong);
278
+ border: 1px solid var(--border);
279
+ border-radius: 18px;
280
+ padding: 18px;
281
+ }
282
+ .metric-card small { display: block; color: var(--accent); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.72rem; font-weight: 700; }
283
+ .metric-card strong { display: block; margin-top: 8px; font-size: 1.55rem; }
284
+ .panel { padding: 0; overflow: hidden; }
285
+ .section-title { margin: 0; padding: 20px 22px; border-bottom: 1px solid var(--border); font-size: 1rem; }
286
+ .empty { padding: 32px 22px; color: var(--muted); }
287
+ table { width: 100%; border-collapse: collapse; }
288
+ th, td { padding: 14px 18px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; }
289
+ th { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.08em; }
290
+ .num-cell { text-align: right; }
291
+ tr:last-child td { border-bottom: 0; }
292
+ .col-index { width: 64px; }
293
+ .sortable-head { padding: 10px 18px; }
294
+ .sort-btn {
295
+ padding: 0;
296
+ border: 0;
297
+ border-radius: 0;
298
+ background: transparent;
299
+ font: inherit;
300
+ text-transform: inherit;
301
+ letter-spacing: inherit;
302
+ cursor: pointer;
303
+ }
304
+ .sort-label { color: inherit; }
305
+ .sort-state { color: var(--accent); font-size: 0.9rem; min-width: 16px; text-align: left; line-height: 1; }
306
+ .index-cell { color: var(--muted); font-variant-numeric: tabular-nums; }
307
+ .session-link { text-decoration: none; }
308
+ .session-link strong { display: block; }
309
+ .session-sub { display: block; margin-top: 2px; color: var(--muted); font-size: 0.8rem; }
310
+ .workflow-pill { padding: 6px 10px; font-size: 0.78rem; }
311
+ .workflow-rpiv { color: var(--rpiv-fg); background: var(--rpiv-bg); border-color: var(--rpiv-border); }
312
+ .workflow-research { color: var(--research-fg); background: var(--research-bg); border-color: var(--research-border); }
313
+ .workflow-generic { color: var(--generic-fg); background: var(--generic-bg); border-color: var(--generic-border); }
314
+ .date-cell { display: grid; gap: 2px; }
315
+ .date-cell strong { font-size: 0.92rem; }
316
+ .date-cell span { color: var(--muted); font-size: 0.82rem; }
317
+ .timeline { display: grid; gap: 14px; }
318
+ .event {
319
+ background: var(--panel);
320
+ border: 1px solid var(--border);
321
+ border-radius: 18px;
322
+ overflow: hidden;
323
+ }
324
+ .event summary {
325
+ list-style: none;
326
+ cursor: pointer;
327
+ display: flex;
328
+ justify-content: space-between;
329
+ gap: 14px;
330
+ align-items: center;
331
+ padding: 16px 18px;
332
+ }
333
+ .event summary::-webkit-details-marker { display: none; }
334
+ .event summary:hover { background: rgba(255,255,255,0.02); }
335
+ .event-main { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
336
+ .badge {
337
+ display: inline-flex;
338
+ align-items: center;
339
+ padding: 4px 8px;
340
+ border-radius: 999px;
341
+ font-size: 0.72rem;
342
+ font-weight: 800;
343
+ text-transform: uppercase;
344
+ letter-spacing: 0.06em;
345
+ border: 1px solid var(--border);
346
+ background: rgba(255,255,255,0.03);
347
+ }
348
+ .badge-llm { color: var(--accent); background: var(--accent-soft); border-color: rgba(216,132,98,0.24); }
349
+ .badge-tool { color: #8ec5ff; background: rgba(142,197,255,0.1); border-color: rgba(142,197,255,0.22); }
350
+ .badge-system { color: var(--muted); }
351
+ .badge-error { color: var(--err); background: rgba(239,127,127,0.12); border-color: rgba(239,127,127,0.24); }
352
+ .event-title { font-weight: 700; }
353
+ .event-time { color: var(--muted); font-size: 0.9rem; white-space: nowrap; }
354
+ .event-body { padding: 0 18px 18px; }
355
+ .stack { display: grid; gap: 12px; }
356
+ .block {
357
+ background: rgba(0,0,0,0.18);
358
+ border: 1px solid var(--border);
359
+ border-radius: 14px;
360
+ overflow: hidden;
361
+ }
362
+ .block h4 {
363
+ margin: 0;
364
+ padding: 10px 12px;
365
+ border-bottom: 1px solid var(--border);
366
+ color: var(--muted);
367
+ font-size: 0.8rem;
368
+ text-transform: uppercase;
369
+ letter-spacing: 0.08em;
370
+ }
371
+ pre {
372
+ margin: 0;
373
+ padding: 14px;
374
+ overflow-x: auto;
375
+ white-space: pre-wrap;
376
+ word-break: break-word;
377
+ font-family: "SFMono-Regular", ui-monospace, Menlo, Monaco, Consolas, monospace;
378
+ font-size: 0.84rem;
379
+ background: var(--code);
380
+ }
381
+ .msg { border-bottom: 1px solid var(--border); }
382
+ .msg:last-child { border-bottom: 0; }
383
+ .msg-head {
384
+ display: flex;
385
+ justify-content: space-between;
386
+ gap: 12px;
387
+ padding: 10px 12px;
388
+ border-bottom: 1px solid var(--border);
389
+ background: rgba(255,255,255,0.02);
390
+ }
391
+ .msg-role { font-size: 0.78rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
392
+ .msg.user .msg-role { color: #8ec5ff; }
393
+ .msg.assistant .msg-role { color: var(--accent); }
394
+ .msg-content { padding: 14px; white-space: pre-wrap; word-break: break-word; }
395
+ .footer-note {
396
+ margin-top: 22px;
397
+ color: var(--muted);
398
+ text-align: center;
399
+ padding: 10px 0 0;
400
+ font-family: inherit;
401
+ }
402
+ .footer-note.minimal {
403
+ font-size: 0.84rem;
404
+ font-variant-caps: all-small-caps;
405
+ letter-spacing: 0.14em;
406
+ line-height: 1;
407
+ }
408
+ .footer-note.stack {
409
+ display: grid;
410
+ gap: 6px;
411
+ line-height: 1.2;
412
+ }
413
+ .footer-brand {
414
+ color: var(--text);
415
+ font-size: 0.88rem;
416
+ font-weight: 700;
417
+ font-variant-caps: all-small-caps;
418
+ letter-spacing: 0.16em;
419
+ }
420
+ .footer-tagline {
421
+ font-size: 0.78rem;
422
+ letter-spacing: 0.08em;
423
+ font-variant-caps: all-small-caps;
424
+ }
425
+ .footer-meta {
426
+ font-size: 0.76rem;
427
+ font-variant-caps: all-small-caps;
428
+ letter-spacing: 0.14em;
429
+ }
430
+ .footer-meta a {
431
+ color: inherit;
432
+ text-decoration: none;
433
+ border-bottom: 1px solid rgba(236,227,218,0.22);
434
+ }
435
+ .footer-meta a:hover {
436
+ color: var(--text);
437
+ border-bottom-color: rgba(236,227,218,0.45);
438
+ }
439
+ @media (max-width: 760px) {
440
+ .container { padding: 20px 14px 48px; }
441
+ .hero { padding: 20px; }
442
+ .hero-top { grid-template-columns: 1fr; }
443
+ .meta { justify-content: flex-start; margin-top: 8px; }
444
+ .wordmark { width: 280px; height: 92px; }
445
+ th:nth-child(5), td:nth-child(5) { display: none; }
446
+ .event summary { align-items: flex-start; }
447
+ }
448
+ </style>
449
+ </head>
450
+ <body>${body}${script ? `<script>${script}</script>` : ""}</body>
451
+ </html>`;
452
+ }
453
+
454
+ function copyButton(value: string, label: string): string {
455
+ return `<button class="copy-btn" type="button" data-copy-value="${escapeHtml(value)}" aria-label="Copy ${escapeHtml(label)}" title="Copy ${escapeHtml(label)}"><svg width="14" height="14" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button>`;
456
+ }
457
+
458
+ function copyScript(): string {
459
+ return `(() => {
460
+ document.querySelectorAll('[data-copy-value]').forEach((button) => {
461
+ button.addEventListener('click', async () => {
462
+ const value = button.getAttribute('data-copy-value') || '';
463
+ try {
464
+ if (navigator.clipboard?.writeText) {
465
+ await navigator.clipboard.writeText(value);
466
+ } else {
467
+ const textarea = document.createElement('textarea');
468
+ textarea.value = value;
469
+ textarea.style.position = 'fixed';
470
+ textarea.style.opacity = '0';
471
+ document.body.appendChild(textarea);
472
+ textarea.focus();
473
+ textarea.select();
474
+ document.execCommand('copy');
475
+ textarea.remove();
476
+ }
477
+ const previous = button.innerHTML;
478
+ button.classList.add('copied');
479
+ button.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg>';
480
+ setTimeout(() => {
481
+ button.classList.remove('copied');
482
+ button.innerHTML = previous;
483
+ }, 1400);
484
+ } catch {
485
+ button.textContent = 'ERR';
486
+ }
487
+ });
488
+ });
489
+ })();`;
490
+ }
491
+
492
+ function renderJsonBlock(title: string, value: unknown): string {
493
+ return `<section class="block"><h4>${escapeHtml(title)}</h4><pre>${escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2))}</pre></section>`;
494
+ }
495
+
496
+ function renderMessages(messages: any[] | undefined): string {
497
+ if (!messages?.length) return "";
498
+ return `<section class="block"><h4>Input Messages</h4>${messages.map(m => `<div class="msg ${escapeHtml(m?.role || "unknown")}"><div class="msg-head"><span class="msg-role">${escapeHtml(m?.role || "unknown")}</span></div><div class="msg-content">${escapeHtml(m?.content ?? "")}</div></div>`).join("")}</section>`;
499
+ }
500
+
501
+ function eventBadgeClass(ev: any): string {
502
+ if (ev.type === "llm_completion") return "badge badge-llm";
503
+ if (ev.type === "tool_start" || ev.type === "tool_end") return ev.isError ? "badge badge-error" : "badge badge-tool";
504
+ return "badge badge-system";
505
+ }
506
+
507
+ function eventTitle(ev: any): string {
508
+ return ev.model || ev.toolName || ev.type;
509
+ }
510
+
511
+ function renderEventCard(ev: any): string {
512
+ const sections: string[] = [];
513
+ if (ev.type === "llm_completion") {
514
+ sections.push(renderMessages(ev.inputPayload?.messages));
515
+ if (ev.stopReason && ev.stopReason !== "stop" && ev.stopReason !== "toolUse") {
516
+ sections.push(renderJsonBlock("Stop Reason", ev.stopReason));
517
+ }
518
+ if (ev.errorMessage) {
519
+ sections.push(renderJsonBlock("Error Message", ev.errorMessage));
520
+ }
521
+ sections.push(renderJsonBlock("Output", ev.outputContent));
522
+ if (ev.usage) sections.push(renderJsonBlock("Usage", ev.usage));
523
+ } else if (ev.type === "tool_start") {
524
+ sections.push(renderJsonBlock("Arguments", ev.args));
525
+ } else if (ev.type === "tool_end") {
526
+ sections.push(renderJsonBlock(ev.isError ? "Error Result" : "Result", ev.result));
527
+ } else {
528
+ sections.push(renderJsonBlock("Event", ev));
529
+ }
530
+
531
+ return `<details class="event">
532
+ <summary>
533
+ <div class="event-main">
534
+ <span class="${eventBadgeClass(ev)}">${escapeHtml(ev.type)}</span>
535
+ <span class="event-title">${escapeHtml(eventTitle(ev))}</span>
536
+ </div>
537
+ <span class="event-time">${formatTime(ev.timestamp)}</span>
538
+ </summary>
539
+ <div class="event-body"><div class="stack">${sections.join("")}</div></div>
540
+ </details>`;
541
+ }
542
+
543
+ function dashboardSortScript(): string {
544
+ return `(() => {
545
+ const table = document.querySelector('[data-dashboard-table]');
546
+ if (!table) return;
547
+ const tbody = table.querySelector('tbody');
548
+ if (!tbody) return;
549
+ const buttons = Array.from(document.querySelectorAll('[data-sort-key]'));
550
+ let currentKey = 'index';
551
+ let currentDir = 'desc';
552
+
553
+ function icon(dir) {
554
+ return dir === 'asc' ? '↑' : '↓';
555
+ }
556
+
557
+ function updateState() {
558
+ buttons.forEach(btn => {
559
+ const key = btn.getAttribute('data-sort-key');
560
+ const state = btn.querySelector('.sort-state');
561
+ if (!state) return;
562
+ state.textContent = key === currentKey ? icon(currentDir) : '';
563
+ });
564
+ }
565
+
566
+ function compare(a, b, key) {
567
+ if (key === 'index' || key === 'started' || key === 'tokens' || key === 'cost') {
568
+ return Number(a.dataset[key] || 0) - Number(b.dataset[key] || 0);
569
+ }
570
+ return String(a.dataset[key] || '').localeCompare(String(b.dataset[key] || ''));
571
+ }
572
+
573
+ function sortBy(key) {
574
+ const rows = Array.from(tbody.querySelectorAll('tr'));
575
+ rows.sort((a, b) => {
576
+ const result = compare(a, b, key);
577
+ return currentDir === 'asc' ? result : -result;
578
+ });
579
+ rows.forEach(row => tbody.appendChild(row));
580
+ updateState();
581
+ }
582
+
583
+ buttons.forEach(btn => {
584
+ btn.addEventListener('click', () => {
585
+ const key = btn.getAttribute('data-sort-key') || 'index';
586
+ if (currentKey === key) currentDir = currentDir === 'asc' ? 'desc' : 'asc';
587
+ else {
588
+ currentKey = key;
589
+ currentDir = key === 'workflow' ? 'asc' : 'desc';
590
+ }
591
+ sortBy(currentKey);
592
+ });
593
+ });
594
+
595
+ sortBy(currentKey);
596
+ })();`;
597
+ }
598
+
599
+ export function generateDashboardHtml(sessions: any[], options: any = {}): string {
600
+ const reversed = sessions.slice().reverse();
601
+ const totalCost = sessions.reduce((sum, s) => sum + Number(s.activity?.totals?.totalCostUsd || 0), 0);
602
+ const totalTokens = sessions.reduce((sum, s) => sum + Number(s.activity?.totals?.totalTokens || 0), 0);
603
+ const repositoryName = resolveRepoName(options);
604
+ const homeHref = options?.indexHref || "index.html";
605
+ const body = `<div class="container">
606
+ <section class="hero">
607
+ <div class="hero-top">
608
+ <div>
609
+ <div class="brand"><a class="brand-link" href="${escapeHtml(homeHref)}">${wordmarkSvg()}</a><p class="subtitle">Retrospective index. Repo-local session evidence.</p></div>
610
+ </div>
611
+ <div class="meta">
612
+ <span class="pill">${escapeHtml(repositoryName)}</span>
613
+ <span class="pill">${sessions.length} sessions</span>
614
+ </div>
615
+ </div>
616
+ <div class="metrics">
617
+ <div class="metric-card"><small>Sessions</small><strong>${sessions.length}</strong></div>
618
+ <div class="metric-card"><small>Total Tokens</small><strong>${totalTokens.toLocaleString()}</strong></div>
619
+ <div class="metric-card"><small>Total Cost</small><strong>${formatUsd(totalCost)}</strong></div>
620
+ </div>
621
+ </section>
622
+ <section class="panel">
623
+ <h2 class="section-title">Session Reports</h2>
624
+ ${reversed.length ? `<table data-dashboard-table><thead><tr><th class="col-index sortable-head"><button class="sort-btn" data-sort-key="index"><span class="sort-label">#</span><span class="sort-state">↓</span></button></th><th>Session</th><th class="sortable-head"><button class="sort-btn" data-sort-key="workflow"><span class="sort-label">Workflow</span><span class="sort-state"></span></button></th><th class="sortable-head"><button class="sort-btn" data-sort-key="started"><span class="sort-label">Started</span><span class="sort-state"></span></button></th><th>Task</th><th class="sortable-head num-cell"><button class="sort-btn" data-sort-key="tokens"><span class="sort-label">Tokens</span><span class="sort-state"></span></button></th><th class="sortable-head num-cell"><button class="sort-btn" data-sort-key="cost"><span class="sort-label">Cost</span><span class="sort-state"></span></button></th></tr></thead><tbody>
625
+ ${reversed.map((s, index) => {
626
+ const link = s.artifacts.html.startsWith(".notrace/") ? s.artifacts.html.substring(9) : s.artifacts.html;
627
+ const workflow = s.task?.workflow || "generic";
628
+ const workflowLabel = workflowDisplayName(workflow);
629
+ const tokens = Number(s.activity?.totals?.totalTokens || 0);
630
+ const cost = Number(s.activity?.totals?.totalCostUsd || 0);
631
+ return `<tr data-index="${reversed.length - index}" data-workflow="${escapeHtml(workflowLabel)}" data-started="${parseDate(s.startedAt)?.getTime() || 0}" data-tokens="${tokens}" data-cost="${cost}"><td class="index-cell">${reversed.length - index}</td><td><a class="session-link" href="${escapeHtml(link)}"><strong>${escapeHtml(String(s.sessionId).slice(0, 8))}</strong><span class="session-sub">${escapeHtml(String(s.sessionId))}</span></a></td><td><span class="workflow-pill ${workflowClassName(workflow)}">${escapeHtml(workflowLabel)}</span></td><td>${formatDateCell(s.startedAt)}</td><td>${escapeHtml(taskDisplay(s))}</td><td class="num-cell">${tokens.toLocaleString()}</td><td class="num-cell">${formatUsd(cost)}</td></tr>`;
632
+ }).join("")}
633
+ </tbody></table>` : `<div class="empty">No sessions yet. Run Pi with notrace enabled. New reports appear here.</div>`}
634
+ </section>
635
+ <footer class="footer-note minimal">notrace • raquezha 2026</footer>
636
+ </div>`;
637
+ return shell("notrace", body, dashboardSortScript());
638
+ }
639
+
640
+ export function generateHtmlReport(data: any): string {
641
+ const visibleEvents = (data.events || []).filter((ev: any) => ev.type !== "session_start" && ev.type !== "turn_start");
642
+ const indexHref = data?.navigation?.indexHref || "../../index.html";
643
+ const repositoryName = resolveRepoName(data);
644
+ const task = data.task;
645
+ const body = `<div class="container">
646
+ <section class="hero">
647
+ <div class="hero-top">
648
+ <div>
649
+ <div class="brand"><a class="brand-link" href="${escapeHtml(indexHref)}" onclick="if (window.history.length > 1) { window.history.back(); return false; }">${wordmarkSvg()}</a><p class="subtitle session-subtitle"><span>Session retrospective</span><span class="session-id-chip"><span>${escapeHtml(data.traceId)}</span>${copyButton(String(data.traceId || ""), "session ID")}</span></p></div>
650
+ </div>
651
+ <div class="meta">
652
+ <span class="pill">${escapeHtml(resolveRepoName(data))}</span>
653
+ <span class="pill">Started ${formatDateLong(data.session?.startedAt)}</span>
654
+ <span class="pill">Mode: ${escapeHtml(data.captureMode || "full")}</span>
655
+ </div>
656
+ </div>
657
+ <div class="metrics">
658
+ <div class="metric-card"><small>Total Cost</small><strong>${formatUsd(data.activity?.totals?.totalCostUsd)}</strong></div>
659
+ <div class="metric-card"><small>Total Tokens</small><strong>${Number(data.activity?.totals?.totalTokens || 0).toLocaleString()}</strong></div>
660
+ <div class="metric-card"><small>Input Tokens</small><strong>${Number(data.activity?.totals?.inputTokens || 0).toLocaleString()}</strong></div>
661
+ <div class="metric-card"><small>Output Tokens</small><strong>${Number(data.activity?.totals?.outputTokens || 0).toLocaleString()}</strong></div>
662
+ <div class="metric-card"><small>Tool Calls</small><strong>${Number(data.activity?.toolCallCount || 0)}</strong></div>
663
+ <div class="metric-card"><small>Events</small><strong>${visibleEvents.length}</strong></div>
664
+ </div>
665
+ </section>
666
+ <section class="panel">
667
+ <h2 class="section-title">Run Summary</h2>
668
+ <div style="padding: 16px;" class="stack">
669
+ ${renderJsonBlock("Session", data.session || {})}
670
+ ${renderJsonBlock("Task", task || { workflow: "generic", id: null })}
671
+ ${renderJsonBlock("Conditions", data.conditions || {})}
672
+ ${renderJsonBlock("Activity", data.activity || {})}
673
+ </div>
674
+ </section>
675
+ <section class="panel" style="margin-top: 24px;">
676
+ <h2 class="section-title">Dynamic Extension Telemetry</h2>
677
+ <div style="padding: 16px;" class="stack">
678
+ ${Object.entries(data.telemetry?.extensions || {}).length ? Object.entries(data.telemetry.extensions).map(([name, ext]: [string, any]) => renderJsonBlock(`${name} (${formatTelemetryStatus(ext?.status)})`, { summary: ext?.summary || null, ...ext?.details })).join("") : `<div class="empty">No extension telemetry captured for this run.</div>`}
679
+ </div>
680
+ </section>
681
+ <section class="panel" style="margin-top: 24px;">
682
+ <h2 class="section-title">Timeline</h2>
683
+ <div style="padding: 16px;">
684
+ <div class="timeline">${visibleEvents.map(renderEventCard).join("") || `<div class="empty">No visible events captured.</div>`}</div>
685
+ </div>
686
+ </section>
687
+ <footer class="footer-note stack">
688
+ <div class="footer-brand">notrace</div>
689
+ <div class="footer-tagline">Local-first retrospective engine</div>
690
+ <div class="footer-meta"><a href="https://opensource.org/licenses/MIT">MIT</a></div>
691
+ </footer>
692
+ </div>`;
693
+ return shell(`notrace - ${data.traceId}`, body, copyScript());
694
+ }