@raquezha/notrace 0.0.4 → 0.0.5

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.
@@ -20,7 +20,31 @@ function getCaptureMode(): CaptureMode {
20
20
  }
21
21
 
22
22
  function isSensitiveKey(key: string): boolean {
23
- return SENSITIVE_KEY_RE.test(key.replace(/[^a-z0-9]/gi, ""));
23
+ const normalized = key.replace(/[^a-z0-9]/gi, "");
24
+ if (/^(inputtokens|outputtokens|totaltokens|prompttokens|completiontokens|reasoningtokens|cachedtokens|cachecreationinputtokens|cachereadinputtokens|cost|total|input|output|prompt|completion|reasoning|read|write)$/i.test(normalized)) {
25
+ return false;
26
+ }
27
+ return SENSITIVE_KEY_RE.test(normalized);
28
+ }
29
+
30
+ function sanitizeUsageValue(usage: unknown): unknown {
31
+ if (getCaptureMode() === "full") return usage;
32
+ if (!usage || typeof usage !== "object") return sanitizeTraceValue(usage);
33
+
34
+ const source = usage as Record<string, unknown>;
35
+ const output: Record<string, unknown> = {};
36
+
37
+ for (const [key, value] of Object.entries(source)) {
38
+ if (typeof value === "number" || typeof value === "boolean" || value == null) {
39
+ output[key] = value;
40
+ } else if (typeof value === "object" && value) {
41
+ output[key] = sanitizeUsageValue(value);
42
+ } else {
43
+ output[key] = sanitizeTraceValue(value);
44
+ }
45
+ }
46
+
47
+ return output;
24
48
  }
25
49
 
26
50
  function redactString(value: string): string {
@@ -75,6 +99,205 @@ function escapeHtml(value: unknown): string {
75
99
  });
76
100
  }
77
101
 
102
+ type NotraceLocation = {
103
+ workflowDir: string;
104
+ notraceDir: string;
105
+ outputDir: string;
106
+ taskDir: string | null;
107
+ taskId: string | null;
108
+ taskPath: string | null;
109
+ };
110
+
111
+ type NotraceMetrics = {
112
+ totalTokens: number;
113
+ inputTokens: number;
114
+ outputTokens: number;
115
+ totalCost: number;
116
+ toolCallCount: number;
117
+ toolErrorCount: number;
118
+ llmCallCount: number;
119
+ turnCount: number;
120
+ models: string[];
121
+ providers: string[];
122
+ };
123
+
124
+ function ensureWorkflowDir(workflowDir: string): void {
125
+ if (existsSync(workflowDir)) return;
126
+ try {
127
+ mkdirSync(workflowDir, { recursive: true, mode: 0o700 });
128
+ } catch {}
129
+ }
130
+
131
+ function safePathSegment(value: string): string {
132
+ return value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 160) || `session-${Date.now()}`;
133
+ }
134
+
135
+ function getNotraceLocation(cwd: string, sessionId: string): NotraceLocation {
136
+ const workflowDir = path.resolve(cwd, ".workflow");
137
+ const notraceDir = path.resolve(cwd, ".notrace");
138
+ const outputDir = path.join(notraceDir, "sessions", safePathSegment(sessionId));
139
+ ensureWorkflowDir(workflowDir);
140
+
141
+ try {
142
+ const activeTaskJsonPath = path.join(workflowDir, "active_task.json");
143
+ if (existsSync(activeTaskJsonPath)) {
144
+ const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
145
+ const candidate = typeof content.taskPath === "string"
146
+ ? safeResolveUnder(cwd, content.taskPath)
147
+ : typeof content.active_task === "string"
148
+ ? safeResolveUnder(workflowDir, path.join("tasks", content.active_task))
149
+ : null;
150
+
151
+ if (candidate && safeResolveUnder(workflowDir, path.relative(workflowDir, candidate))) {
152
+ return {
153
+ workflowDir,
154
+ notraceDir,
155
+ outputDir,
156
+ taskDir: candidate,
157
+ taskId: typeof content.active_task === "string" ? content.active_task : path.basename(candidate),
158
+ taskPath: path.relative(cwd, candidate)
159
+ };
160
+ }
161
+ }
162
+ } catch {
163
+ // fallback
164
+ }
165
+
166
+ return {
167
+ workflowDir,
168
+ notraceDir,
169
+ outputDir,
170
+ taskDir: null,
171
+ taskId: null,
172
+ taskPath: null
173
+ };
174
+ }
175
+
176
+ function updateNotraceIndex(cwd: string, location: NotraceLocation, entry: Record<string, unknown>): void {
177
+ const indexPath = path.join(location.notraceDir, "index.json");
178
+ const base = {
179
+ schemaVersion: 1,
180
+ kind: "notrace-index",
181
+ workdir: ".",
182
+ sessions: [] as Record<string, unknown>[]
183
+ };
184
+
185
+ try {
186
+ mkdirSync(location.notraceDir, { recursive: true, mode: 0o700 });
187
+ const existing = existsSync(indexPath)
188
+ ? JSON.parse(readFileSync(indexPath, "utf-8"))
189
+ : base;
190
+ const sessions = Array.isArray(existing.sessions) ? existing.sessions : [];
191
+ const sessionId = entry.sessionId;
192
+ const nextSessions = sessions.filter((item: any) => item?.sessionId !== sessionId);
193
+ nextSessions.push(entry);
194
+ const next = {
195
+ ...base,
196
+ ...existing,
197
+ schemaVersion: 1,
198
+ kind: "notrace-index",
199
+ workdir: ".",
200
+ sessions: nextSessions
201
+ };
202
+ writeFileSync(indexPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
203
+ } catch {
204
+ // keep notrace non-fatal if index update fails
205
+ }
206
+ }
207
+
208
+ function appendWorkLogEntry(taskDir: string, message: string): void {
209
+ const workMd = path.join(taskDir, "WORK.md");
210
+ if (!existsSync(workMd)) return;
211
+
212
+ try {
213
+ const text = readFileSync(workMd, "utf-8");
214
+ const entry = `- ${new Date().toISOString()}: ${message}`;
215
+
216
+ if (!/^(## )?\[LOG\]\s*$/m.test(text)) {
217
+ writeFileSync(workMd, `${text.trimEnd()}\n\n## [LOG]\n${entry}\n`, { encoding: "utf-8" });
218
+ return;
219
+ }
220
+
221
+ const lines = text.split("\n");
222
+ const logIndex = lines.findIndex((line) => /^(## )?\[LOG\]\s*$/.test(line));
223
+ if (logIndex === -1) return;
224
+
225
+ let nextSectionIndex = lines.length;
226
+ for (let i = logIndex + 1; i < lines.length; i++) {
227
+ if (/^(## )?\[[A-Z0-9_-]+\]\s*$/.test(lines[i])) {
228
+ nextSectionIndex = i;
229
+ break;
230
+ }
231
+ }
232
+
233
+ const before = lines.slice(0, nextSectionIndex);
234
+ const after = lines.slice(nextSectionIndex);
235
+ while (before.length > logIndex + 1 && before[before.length - 1]?.trim() === "") {
236
+ before.pop();
237
+ }
238
+ before.push(entry);
239
+
240
+ writeFileSync(workMd, `${[...before, ...after].join("\n").replace(/\n*$/, "\n")}`, { encoding: "utf-8" });
241
+ } catch {
242
+ // keep notrace non-fatal if WORK.md append fails
243
+ }
244
+ }
245
+
246
+ function collectMetrics(events: any[]): NotraceMetrics {
247
+ let totalTokens = 0;
248
+ let inputTokens = 0;
249
+ let outputTokens = 0;
250
+ let totalCost = 0;
251
+ let toolCallCount = 0;
252
+ let toolErrorCount = 0;
253
+ let llmCallCount = 0;
254
+ let turnCount = 0;
255
+ const models = new Set<string>();
256
+ const providers = new Set<string>();
257
+
258
+ for (const event of events) {
259
+ if (event.type === "turn_start") {
260
+ turnCount++;
261
+ continue;
262
+ }
263
+
264
+ if (event.type === "tool_start") {
265
+ toolCallCount++;
266
+ continue;
267
+ }
268
+
269
+ if (event.type === "tool_end") {
270
+ if (event.isError) toolErrorCount++;
271
+ continue;
272
+ }
273
+
274
+ if (event.type === "llm_completion") {
275
+ llmCallCount++;
276
+ if (typeof event.model === "string" && event.model) models.add(event.model);
277
+ if (typeof event.provider === "string" && event.provider) providers.add(event.provider);
278
+ if (event.usage) {
279
+ inputTokens += event.usage.input || 0;
280
+ outputTokens += event.usage.output || 0;
281
+ totalTokens += event.usage.totalTokens || 0;
282
+ totalCost += event.usage.cost?.total || 0;
283
+ }
284
+ }
285
+ }
286
+
287
+ return {
288
+ totalTokens,
289
+ inputTokens,
290
+ outputTokens,
291
+ totalCost,
292
+ toolCallCount,
293
+ toolErrorCount,
294
+ llmCallCount,
295
+ turnCount,
296
+ models: [...models],
297
+ providers: [...providers]
298
+ };
299
+ }
300
+
78
301
  /**
79
302
  * html-observability extension
80
303
  *
@@ -90,33 +313,6 @@ export default function (pi: ExtensionAPI) {
90
313
  let llmStartTime = 0;
91
314
  const activeToolTimes: Record<string, number> = {};
92
315
 
93
- // Helper to extract active task path from .workflow/active_task.json.
94
- // Reports are constrained to cwd/.workflow to avoid task metadata causing writes elsewhere.
95
- function getActiveTaskDir(cwd: string): string {
96
- const workflowDir = path.resolve(cwd, ".workflow");
97
- try {
98
- const activeTaskJsonPath = path.join(workflowDir, "active_task.json");
99
- if (existsSync(activeTaskJsonPath)) {
100
- const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
101
- const candidate = typeof content.taskPath === "string"
102
- ? safeResolveUnder(cwd, content.taskPath)
103
- : typeof content.active_task === "string"
104
- ? safeResolveUnder(workflowDir, path.join("tasks", content.active_task))
105
- : null;
106
-
107
- if (candidate && safeResolveUnder(workflowDir, path.relative(workflowDir, candidate))) {
108
- return candidate;
109
- }
110
- }
111
- } catch {
112
- // fallback
113
- }
114
- if (!existsSync(workflowDir)) {
115
- try { mkdirSync(workflowDir, { recursive: true, mode: 0o700 }); } catch {}
116
- }
117
- return workflowDir;
118
- }
119
-
120
316
  // 1. Session start
121
317
  pi.on("session_start", async (event, ctx) => {
122
318
  traceId = ctx.sessionManager.getSessionId() || `session-${Date.now()}`;
@@ -194,7 +390,7 @@ export default function (pi: ExtensionAPI) {
194
390
  provider: message.provider || "unknown",
195
391
  inputPayload: activeLlmPayload,
196
392
  outputContent: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(message.content),
197
- usage: sanitizeTraceValue(message.usage),
393
+ usage: sanitizeUsageValue(message.usage),
198
394
  durationMs,
199
395
  timestamp: Date.now()
200
396
  });
@@ -207,57 +403,103 @@ export default function (pi: ExtensionAPI) {
207
403
  pi.on("session_shutdown", async (event, ctx) => {
208
404
  const sessionEndTime = Date.now();
209
405
  const totalDurationMs = sessionEndTime - sessionStartTime;
210
-
211
- const taskDir = getActiveTaskDir(ctx.cwd);
212
- const reportPath = path.join(taskDir, "notrace.html");
213
-
214
- // Gather statistics
215
- let totalTokens = 0;
216
- let inputTokens = 0;
217
- let outputTokens = 0;
218
- let totalCost = 0;
219
- let toolCallCount = 0;
220
- let llmCallCount = 0;
221
-
222
- events.forEach((e) => {
223
- if (e.type === "llm_completion") {
224
- llmCallCount++;
225
- if (e.usage) {
226
- inputTokens += e.usage.input || 0;
227
- outputTokens += e.usage.output || 0;
228
- totalTokens += e.usage.totalTokens || 0;
229
- totalCost += e.usage.cost?.total || 0;
230
- }
231
- } else if (e.type === "tool_start") {
232
- toolCallCount++;
233
- }
234
- });
406
+ const projectName = process.env.PHOENIX_PROJECT_NAME || "pi-coding-agent";
407
+ const captureMode = getCaptureMode();
408
+ const location = getNotraceLocation(ctx.cwd, traceId);
409
+ const reportPath = path.join(location.outputDir, "notrace.html");
410
+ const recordPath = path.join(location.outputDir, "notrace.json");
411
+ const reviewPath = path.join(location.outputDir, "notrace.review.json");
412
+ const metrics = collectMetrics(events);
413
+ const sessionFile = ctx.sessionManager.getSessionFile() || null;
235
414
 
236
415
  const htmlContent = generateHtmlReport({
237
416
  traceId,
238
- projectName: process.env.PHOENIX_PROJECT_NAME || "pi-coding-agent",
417
+ projectName,
239
418
  startTime: new Date(sessionStartTime).toISOString(),
240
419
  endTime: new Date(sessionEndTime).toISOString(),
241
420
  durationMs: totalDurationMs,
242
421
  metrics: {
243
- totalTokens,
244
- inputTokens,
245
- outputTokens,
246
- totalCost: totalCost.toFixed(5),
247
- toolCallCount,
248
- llmCallCount
422
+ totalTokens: metrics.totalTokens,
423
+ inputTokens: metrics.inputTokens,
424
+ outputTokens: metrics.outputTokens,
425
+ totalCost: metrics.totalCost.toFixed(5),
426
+ toolCallCount: metrics.toolCallCount,
427
+ llmCallCount: metrics.llmCallCount
249
428
  },
250
429
  events
251
430
  });
252
431
 
432
+ const runRecord = {
433
+ schemaVersion: 1,
434
+ kind: "notrace-run",
435
+ traceId,
436
+ runtime: "pi",
437
+ projectName,
438
+ captureMode,
439
+ session: {
440
+ id: traceId,
441
+ cwd: ctx.cwd,
442
+ file: sessionFile
443
+ },
444
+ task: location.taskId || location.taskPath
445
+ ? {
446
+ id: location.taskId,
447
+ path: location.taskPath
448
+ }
449
+ : null,
450
+ conditions: {
451
+ models: metrics.models,
452
+ providers: metrics.providers
453
+ },
454
+ activity: {
455
+ startTime: new Date(sessionStartTime).toISOString(),
456
+ endTime: new Date(sessionEndTime).toISOString(),
457
+ durationMs: totalDurationMs,
458
+ turnCount: metrics.turnCount,
459
+ llmCallCount: metrics.llmCallCount,
460
+ toolCallCount: metrics.toolCallCount,
461
+ toolErrorCount: metrics.toolErrorCount,
462
+ totals: {
463
+ totalTokens: metrics.totalTokens,
464
+ inputTokens: metrics.inputTokens,
465
+ outputTokens: metrics.outputTokens,
466
+ totalCostUsd: Number(metrics.totalCost.toFixed(5))
467
+ }
468
+ },
469
+ artifacts: {
470
+ htmlReportPath: path.relative(ctx.cwd, reportPath),
471
+ recordPath: path.relative(ctx.cwd, recordPath),
472
+ reviewPath: path.relative(ctx.cwd, reviewPath)
473
+ },
474
+ evidence: {
475
+ events
476
+ }
477
+ };
478
+
253
479
  try {
254
- mkdirSync(taskDir, { recursive: true, mode: 0o700 });
480
+ mkdirSync(location.outputDir, { recursive: true, mode: 0o700 });
255
481
  writeFileSync(reportPath, htmlContent, { encoding: "utf-8", mode: 0o600 });
256
- // Output a nice clickable file:// link to the console for the user
257
- console.log(`\n📊 [notrace] Observability report generated:`);
258
- console.log(`👉 \x1b[36mfile://${reportPath}\x1b[0m\n`);
482
+ writeFileSync(recordPath, `${JSON.stringify(runRecord, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
483
+ updateNotraceIndex(ctx.cwd, location, {
484
+ sessionId: traceId,
485
+ startedAt: new Date(sessionStartTime).toISOString(),
486
+ endedAt: new Date(sessionEndTime).toISOString(),
487
+ taskId: location.taskId,
488
+ taskPath: location.taskPath,
489
+ artifacts: {
490
+ record: path.relative(ctx.cwd, recordPath),
491
+ html: path.relative(ctx.cwd, reportPath),
492
+ review: path.relative(ctx.cwd, reviewPath)
493
+ }
494
+ });
495
+ if (location.taskDir && (location.taskId || location.taskPath)) {
496
+ appendWorkLogEntry(location.taskDir, `notrace captured artifacts: ${path.relative(location.taskDir, reportPath)}, ${path.relative(location.taskDir, recordPath)}`);
497
+ }
498
+ console.log(`\n📊 [notrace] Observability artifacts generated:`);
499
+ console.log(`👉 \x1b[36mfile://${reportPath}\x1b[0m`);
500
+ console.log(`🧾 \x1b[36mfile://${recordPath}\x1b[0m\n`);
259
501
  } catch (err: any) {
260
- console.warn(`[notrace] Failed to write HTML report: ${err?.message || err}`);
502
+ console.warn(`[notrace] Failed to write notrace artifacts: ${err?.message || err}`);
261
503
  }
262
504
  });
263
505
  }
@@ -290,301 +532,222 @@ function generateHtmlReport(data: any): string {
290
532
  <title>notrace - ${escapedTraceId}</title>
291
533
  <style>
292
534
  :root {
293
- --bg: #0b0b0e;
294
- --bg-panel: rgba(22, 22, 28, 0.45);
295
- --bg-card: rgba(30, 30, 38, 0.6);
296
- --border: rgba(255, 255, 255, 0.08);
297
- --border-hover: rgba(255, 255, 255, 0.15);
298
- --text: #e2e2e9;
299
- --text-muted: #9f9fa9;
300
- --accent: #8b5cf6;
301
- --accent-gradient: linear-gradient(135deg, #a78bfa, #8b5cf6);
302
- --success: #10b981;
303
- --error: #ef4444;
304
- --warning: #f59e0b;
305
- --info: #3b82f6;
306
- }
307
-
308
- * {
309
- box-sizing: border-box;
310
- margin: 0;
311
- padding: 0;
312
- }
535
+ --bg: #191613;
536
+ --bg-glow: #2a221c;
537
+ --panel: rgba(244, 237, 228, 0.055);
538
+ --panel-strong: rgba(244, 237, 228, 0.075);
539
+ --paper: #f6efe7;
540
+ --paper-soft: #e7dbce;
541
+ --text: #f5eee7;
542
+ --text-soft: #ddd0c2;
543
+ --text-muted: #b9ab9d;
544
+ --border: rgba(255, 255, 255, 0.09);
545
+ --border-strong: rgba(255, 255, 255, 0.16);
546
+ --accent: #d88462;
547
+ --accent-soft: rgba(216, 132, 98, 0.14);
548
+ --session: #8ab7ff;
549
+ --turn: #e0b46b;
550
+ --tool: #86cca4;
551
+ --error: #f08e8e;
552
+ --shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
553
+ --card-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
554
+ }
555
+
556
+ * { box-sizing: border-box; margin: 0; padding: 0; }
313
557
 
314
558
  body {
315
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
316
- background-color: var(--bg);
559
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
560
+ background:
561
+ radial-gradient(900px 420px at 50% -10%, rgba(216, 132, 98, 0.16), transparent 60%),
562
+ radial-gradient(680px 360px at 10% 0%, rgba(255, 255, 255, 0.03), transparent 60%),
563
+ linear-gradient(180deg, #171411 0%, #1b1714 100%);
317
564
  color: var(--text);
318
- line-height: 1.5;
319
- padding: 2rem;
565
+ line-height: 1.65;
566
+ padding: 14px 12px 40px;
320
567
  min-height: 100vh;
321
- background-image:
322
- radial-gradient(circle at 10% 20%, rgba(139, 92, 246, 0.08) 0%, transparent 40%),
323
- radial-gradient(circle at 90% 80%, rgba(59, 130, 246, 0.06) 0%, transparent 40%);
324
- background-attachment: fixed;
325
- }
326
-
327
- .container {
328
- max-width: 1100px;
329
- margin: 0 auto;
330
- }
331
-
332
- header {
333
- display: flex;
334
- justify-content: space-between;
335
- align-items: center;
336
- margin-bottom: 2rem;
337
- padding-bottom: 1.5rem;
338
- border-bottom: 1px solid var(--border);
339
- }
340
-
341
- .brand {
342
- display: flex;
343
- align-items: center;
344
- gap: 0.75rem;
345
- }
346
-
347
- .brand h1 {
348
- font-size: 1.75rem;
349
- font-weight: 700;
350
- background: var(--accent-gradient);
351
- -webkit-background-clip: text;
352
- -webkit-text-fill-color: transparent;
353
- }
354
-
355
- .brand-tag {
356
- font-size: 0.75rem;
357
- text-transform: uppercase;
358
- letter-spacing: 0.05em;
359
- background: rgba(139, 92, 246, 0.15);
360
- color: #c084fc;
361
- padding: 0.2rem 0.5rem;
362
- border-radius: 4px;
363
- border: 1px solid rgba(167, 139, 250, 0.3);
568
+ letter-spacing: 0.01em;
364
569
  }
365
570
 
366
- .meta-time {
367
- font-size: 0.875rem;
368
- color: var(--text-muted);
369
- text-align: right;
370
- }
571
+ .container { max-width: 920px; margin: 0 auto; }
572
+ header { margin-bottom: 24px; }
371
573
 
372
- .metrics-grid {
373
- display: grid;
374
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
375
- gap: 1rem;
376
- margin-bottom: 2.5rem;
574
+ .hero {
575
+ position: relative;
576
+ overflow: hidden;
577
+ background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.025));
578
+ border: 1px solid var(--border);
579
+ border-radius: 22px;
580
+ padding: 18px 16px 16px;
581
+ box-shadow: var(--shadow);
582
+ backdrop-filter: blur(10px);
377
583
  }
378
584
 
585
+ .hero::after {
586
+ content: "";
587
+ position: absolute;
588
+ inset: auto -10% -30% auto;
589
+ width: 260px;
590
+ height: 260px;
591
+ background: radial-gradient(circle, rgba(216,132,98,0.14), transparent 70%);
592
+ pointer-events: none;
593
+ }
594
+
595
+ .brand { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; position: relative; z-index: 1; }
596
+ .brand h1 { font-size: 2rem; line-height: 1; font-weight: 680; color: var(--text); letter-spacing: -0.03em; }
597
+ .brand-tag { font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; color: #f4ccb9; background: var(--accent-soft); border: 1px solid rgba(216, 132, 98, 0.24); padding: 0.35rem 0.6rem; border-radius: 999px; }
598
+ .hero-subtitle { position: relative; z-index: 1; color: var(--text-soft); margin-bottom: 20px; max-width: 720px; font-size: 1rem; }
599
+ .hero-meta { position: relative; z-index: 1; display: flex; flex-wrap: wrap; gap: 10px; }
600
+ .meta-pill { display: inline-flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.04); border: 1px solid var(--border); border-radius: 999px; padding: 0.48rem 0.82rem; font-size: 0.86rem; color: var(--text-muted); }
601
+ .meta-pill strong { color: var(--text); font-weight: 570; }
602
+
603
+ .metrics-grid { display: grid; grid-template-columns: 1fr; gap: 10px; margin: 16px 0 24px; }
379
604
  .metric-card {
380
- background: var(--bg-panel);
605
+ background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.028));
381
606
  border: 1px solid var(--border);
382
- padding: 1.25rem;
383
- border-radius: 12px;
384
- backdrop-filter: blur(12px);
385
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
386
- }
387
-
388
- .metric-card:hover {
389
- border-color: var(--border-hover);
390
- transform: translateY(-2px);
391
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
392
- }
393
-
394
- .metric-label {
395
- font-size: 0.75rem;
607
+ border-radius: 20px;
608
+ padding: 18px 16px;
609
+ box-shadow: var(--card-shadow);
610
+ transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
611
+ }
612
+ .metric-card:hover { border-color: var(--border-strong); transform: translateY(-1px); background: linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0.03)); }
613
+ .metric-label { font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); margin-bottom: 7px; }
614
+ .metric-value { font-size: 1.42rem; font-weight: 640; color: var(--text); letter-spacing: -0.03em; }
615
+
616
+ .section-title {
617
+ font-size: 0.95rem;
618
+ font-weight: 620;
619
+ color: var(--paper-soft);
620
+ margin-bottom: 16px;
621
+ padding-left: 2px;
622
+ letter-spacing: 0.04em;
396
623
  text-transform: uppercase;
397
- letter-spacing: 0.05em;
398
- color: var(--text-muted);
399
- margin-bottom: 0.5rem;
400
- }
401
-
402
- .metric-value {
403
- font-size: 1.5rem;
404
- font-weight: 600;
405
- color: #fff;
406
624
  }
407
625
 
408
- .timeline {
409
- position: relative;
410
- padding-left: 2rem;
411
- border-left: 2px solid var(--border);
412
- margin-left: 1rem;
413
- }
414
-
415
- .timeline-event {
416
- position: relative;
417
- margin-bottom: 2rem;
418
- animation: slideIn 0.4s ease-out;
419
- }
420
-
421
- @keyframes slideIn {
422
- from { opacity: 0; transform: translateX(-10px); }
423
- to { opacity: 1; transform: translateX(0); }
424
- }
425
-
426
- .timeline-dot {
427
- position: absolute;
428
- left: calc(-2rem - 6px);
429
- top: 1.25rem;
430
- width: 10px;
431
- height: 10px;
432
- border-radius: 50%;
433
- background: var(--text-muted);
434
- box-shadow: 0 0 0 4px var(--bg);
435
- transition: all 0.3s ease;
436
- }
437
-
438
- /* Event color mappings */
439
- .timeline-event.session-start .timeline-dot { background: var(--info); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--info); }
440
- .timeline-event.turn-start .timeline-dot { background: var(--accent); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--accent); }
441
- .timeline-event.tool-call .timeline-dot { background: var(--warning); box-shadow: 0 0 0 4px var(--bg); }
442
- .timeline-event.tool-call.error .timeline-dot { background: var(--error); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--error); }
443
- .timeline-event.llm-call .timeline-dot { background: var(--success); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--success); }
626
+ .timeline { display: flex; flex-direction: column; gap: 16px; }
627
+ .timeline-event { position: relative; animation: slideIn 0.28s ease-out; }
628
+ @keyframes slideIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
629
+ .timeline-dot { display: none; }
444
630
 
445
631
  .card {
446
- background: var(--bg-panel);
632
+ position: relative;
633
+ overflow: hidden;
634
+ background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.024));
447
635
  border: 1px solid var(--border);
448
- border-radius: 12px;
449
- padding: 1.25rem;
450
- backdrop-filter: blur(12px);
451
- transition: border-color 0.2s;
636
+ border-radius: 18px;
637
+ padding: 14px 14px 13px;
638
+ box-shadow: var(--card-shadow);
639
+ transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
452
640
  }
453
-
454
- .card:hover {
455
- border-color: var(--border-hover);
456
- }
457
-
458
- .card-header {
459
- display: flex;
460
- justify-content: space-between;
461
- align-items: center;
462
- cursor: pointer;
463
- user-select: none;
464
- }
465
-
466
- .card-title {
467
- display: flex;
468
- align-items: center;
469
- gap: 0.75rem;
470
- font-weight: 600;
471
- }
472
-
473
- .card-badge {
474
- font-size: 0.75rem;
475
- font-weight: 500;
476
- padding: 0.15rem 0.5rem;
477
- border-radius: 4px;
478
- }
479
-
480
- .badge-session { background: rgba(59, 130, 246, 0.15); color: #60a5fa; }
481
- .badge-turn { background: rgba(139, 92, 246, 0.15); color: #c084fc; }
482
- .badge-tool { background: rgba(245, 158, 11, 0.15); color: #fbbf24; }
483
- .badge-tool.error { background: rgba(239, 68, 68, 0.15); color: #f87171; }
484
- .badge-llm { background: rgba(16, 185, 129, 0.15); color: #34d399; }
485
-
486
- .card-time {
487
- font-size: 0.8rem;
488
- color: var(--text-muted);
489
- display: flex;
490
- align-items: center;
491
- gap: 0.5rem;
492
- }
493
-
494
- .arrow-icon {
495
- width: 16px;
496
- height: 16px;
497
- fill: none;
498
- stroke: currentColor;
499
- stroke-width: 2;
500
- stroke-linecap: round;
501
- stroke-linejoin: round;
502
- transition: transform 0.2s;
503
- }
504
-
505
- .expanded .arrow-icon {
506
- transform: rotate(90deg);
507
- }
508
-
509
- .card-body {
510
- margin-top: 1rem;
511
- padding-top: 1rem;
512
- border-top: 1px solid var(--border);
513
- display: none;
514
- }
515
-
516
- .expanded .card-body {
517
- display: block;
518
- }
519
-
641
+ .card::before {
642
+ content: "";
643
+ position: absolute;
644
+ left: 0;
645
+ top: 0;
646
+ bottom: 0;
647
+ width: 3px;
648
+ background: rgba(255,255,255,0.1);
649
+ }
650
+ .timeline-event.session-start .card::before { background: var(--session); }
651
+ .timeline-event.turn-start .card::before { background: var(--turn); }
652
+ .timeline-event.tool-start .card::before { background: var(--tool); }
653
+ .timeline-event.tool-start.error .card::before { background: var(--error); }
654
+ .timeline-event.llm-start .card::before { background: var(--accent); }
655
+ .card:hover { border-color: var(--border-strong); background: linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0.028)); transform: translateY(-1px); }
656
+
657
+ .card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; cursor: pointer; user-select: none; }
658
+ .card-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-weight: 600; color: var(--text); }
659
+ .card-title span:last-child { font-size: 1rem; letter-spacing: -0.01em; }
660
+ .card-badge { font-size: 0.69rem; font-weight: 650; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.34rem 0.62rem; border-radius: 999px; border: 1px solid transparent; }
661
+ .badge-session { background: rgba(138, 183, 255, 0.14); color: var(--session); border-color: rgba(138, 183, 255, 0.24); }
662
+ .badge-turn { background: rgba(224, 180, 107, 0.14); color: var(--turn); border-color: rgba(224, 180, 107, 0.22); }
663
+ .badge-tool { background: rgba(134, 204, 164, 0.14); color: var(--tool); border-color: rgba(134, 204, 164, 0.22); }
664
+ .badge-tool.error { background: rgba(240, 142, 142, 0.15); color: var(--error); border-color: rgba(240, 142, 142, 0.24); }
665
+ .badge-llm { background: rgba(216, 132, 98, 0.14); color: #f2c2ae; border-color: rgba(216, 132, 98, 0.22); }
666
+ .card-time { flex-shrink: 0; font-size: 0.82rem; color: var(--text-muted); display: flex; align-items: center; gap: 8px; padding-top: 3px; }
667
+ .arrow-icon { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: transform 0.2s; }
668
+ .expanded .arrow-icon { transform: rotate(90deg); }
669
+ .card-body { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); display: none; }
670
+ .expanded .card-body { display: block; }
671
+
672
+ .detail-label { font-size: 0.76rem; font-weight: 650; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; margin: 14px 0 8px; display: block; }
520
673
  .code-block {
521
674
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
522
- font-size: 0.875rem;
523
- background: rgba(0, 0, 0, 0.35);
524
- border: 1px solid var(--border);
525
- padding: 1rem;
526
- border-radius: 8px;
675
+ font-size: 0.84rem;
676
+ line-height: 1.62;
677
+ background: rgba(16, 14, 12, 0.64);
678
+ border: 1px solid rgba(255, 255, 255, 0.06);
679
+ padding: 14px;
680
+ border-radius: 16px;
527
681
  overflow-x: auto;
528
- margin-top: 0.5rem;
529
- color: #e2e2e9;
682
+ margin-top: 0.35rem;
683
+ color: #f1e9df;
530
684
  white-space: pre-wrap;
531
685
  }
532
686
 
533
- .messages-container {
534
- display: flex;
535
- flex-direction: column;
536
- gap: 0.75rem;
537
- }
538
-
687
+ .messages-container { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
539
688
  .msg-row {
540
689
  display: flex;
541
690
  flex-direction: column;
542
- gap: 0.25rem;
543
- padding: 0.75rem;
544
- background: rgba(255, 255, 255, 0.02);
545
- border-radius: 6px;
546
- border-left: 3px solid var(--border);
547
- }
548
-
549
- .msg-row.user { border-left-color: var(--info); }
550
- .msg-row.assistant { border-left-color: var(--success); }
551
- .msg-row.system { border-left-color: var(--accent); }
552
-
553
- .msg-role {
554
- font-size: 0.75rem;
555
- font-weight: 600;
556
- text-transform: uppercase;
557
- letter-spacing: 0.05em;
558
- }
559
-
560
- .msg-row.user .msg-role { color: #60a5fa; }
561
- .msg-row.assistant .msg-role { color: #34d399; }
562
- .msg-row.system .msg-role { color: #c084fc; }
563
-
564
- .msg-text {
565
- font-size: 0.9rem;
566
- white-space: pre-wrap;
567
- }
568
-
569
- .duration-pill {
570
- font-size: 0.75rem;
571
- background: rgba(255, 255, 255, 0.06);
572
- padding: 0.1rem 0.4rem;
573
- border-radius: 4px;
691
+ gap: 4px;
692
+ padding: 12px 14px;
693
+ background: rgba(255,255,255,0.038);
694
+ border: 1px solid rgba(255,255,255,0.06);
695
+ border-radius: 18px;
696
+ }
697
+ .msg-row.user { background: rgba(138, 183, 255, 0.08); }
698
+ .msg-row.assistant { background: rgba(216, 132, 98, 0.08); }
699
+ .msg-row.system { background: rgba(224, 180, 107, 0.08); }
700
+ .msg-role { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
701
+ .msg-text { font-size: 0.93rem; color: var(--text-soft); white-space: pre-wrap; }
702
+
703
+ .usage-row {
704
+ margin-top: 12px;
705
+ font-size: 0.84rem;
574
706
  color: var(--text-muted);
707
+ display: flex;
708
+ flex-wrap: wrap;
709
+ gap: 12px;
710
+ padding: 10px 12px;
711
+ border-radius: 14px;
712
+ background: rgba(255,255,255,0.03);
713
+ border: 1px solid rgba(255,255,255,0.05);
714
+ }
715
+ .usage-row strong { color: var(--text); font-weight: 620; }
716
+ .duration-pill { font-size: 0.74rem; background: rgba(255,255,255,0.06); padding: 0.2rem 0.46rem; border-radius: 999px; color: var(--text-muted); border: 1px solid rgba(255,255,255,0.06); }
717
+
718
+ @media (min-width: 640px) {
719
+ body { padding: 28px 18px 56px; }
720
+ .hero { border-radius: 28px; padding: 28px 28px 24px; }
721
+ .metrics-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 18px 0 30px; }
722
+ .card { border-radius: 22px; padding: 18px 18px 16px; }
723
+ }
724
+
725
+ @media (max-width: 720px) {
726
+ .hero, .card, .metric-card { border-radius: 18px; }
727
+ .card-header { flex-direction: column; }
728
+ .card-time { width: 100%; justify-content: space-between; }
729
+ .brand { align-items: flex-start; flex-direction: column; gap: 8px; }
730
+ .brand h1 { font-size: 1.8rem; }
731
+ .hero-meta { flex-direction: column; align-items: stretch; }
732
+ .meta-pill { width: 100%; justify-content: space-between; }
733
+ .usage-row { flex-direction: column; gap: 6px; }
734
+ .code-block { font-size: 0.8rem; padding: 12px; }
575
735
  }
576
736
  </style>
577
737
  </head>
578
738
  <body>
579
739
  <div class="container">
580
740
  <header>
581
- <div class="brand">
582
- <h1>notrace</h1>
583
- <span class="brand-tag">Local Observability</span>
584
- </div>
585
- <div class="meta-time">
586
- <div>Session: <span id="sess-id"></span></div>
587
- <div id="sess-time" style="font-size: 0.8rem;"></div>
741
+ <div class="hero">
742
+ <div class="brand">
743
+ <h1>notrace</h1>
744
+ <span class="brand-tag">retrospective</span>
745
+ </div>
746
+ <p class="hero-subtitle">A local evidence view of the run: messages, tools, model calls, timing, and token cost.</p>
747
+ <div class="hero-meta">
748
+ <span class="meta-pill">Session <strong id="sess-id"></strong></span>
749
+ <span class="meta-pill">Started <strong id="sess-time"></strong></span>
750
+ </div>
588
751
  </div>
589
752
  </header>
590
753
 
@@ -611,7 +774,7 @@ function generateHtmlReport(data: any): string {
611
774
  </div>
612
775
  </div>
613
776
 
614
- <h2 style="margin-bottom: 1.5rem; font-weight: 600; font-size: 1.25rem; color: #fff;">Activity Flow</h2>
777
+ <h2 class="section-title">Activity flow</h2>
615
778
  <div class="timeline" id="timeline-container">
616
779
  <!-- Injected by JS -->
617
780
  </div>
@@ -731,9 +894,9 @@ function generateHtmlReport(data: any): string {
731
894
  cardHtml += \`<div class="code-block">\${escapeHtml(ev.body)}</div>\`;
732
895
  } else if (ev.type === "tool") {
733
896
  cardHtml += \`
734
- <strong>Arguments:</strong>
897
+ <span class="detail-label">Arguments</span>
735
898
  <div class="code-block">\${jsonText(ev.args)}</div>
736
- <strong style="margin-top: 1rem; display: block;">Result (\${ev.isError ? "Error" : "Success"}):</strong>
899
+ <span class="detail-label">Result (\${ev.isError ? "Error" : "Success"})</span>
737
900
  <div class="code-block">\${jsonText(ev.result)}</div>
738
901
  \`;
739
902
  } else if (ev.type === "llm") {
@@ -767,16 +930,16 @@ function generateHtmlReport(data: any): string {
767
930
  messagesHtml += '</div>';
768
931
 
769
932
  cardHtml += \`
770
- <strong>Context Messages:</strong>
933
+ <span class="detail-label">Context messages</span>
771
934
  \${messagesHtml}
772
- <strong style="margin-top: 1.25rem; display: block;">Generated Response:</strong>
935
+ <span class="detail-label">Generated response</span>
773
936
  <div class="code-block">\${jsonText(ev.output)}</div>
774
937
  \${ev.usage ? \`
775
- <div style="margin-top: 1rem; font-size: 0.85rem; color: var(--text-muted); display: flex; gap: 1.5rem;">
776
- <span>Input Tokens: <strong>\${escapeHtml(ev.usage.input ?? 0)}</strong></span>
777
- <span>Output Tokens: <strong>\${escapeHtml(ev.usage.output ?? 0)}</strong></span>
778
- <span>Total Tokens: <strong>\${escapeHtml(ev.usage.totalTokens ?? 0)}</strong></span>
779
- <span>Cost: <strong>\$\${escapeHtml(ev.usage.cost?.total?.toFixed?.(5) || "0.00")}</strong></span>
938
+ <div class="usage-row">
939
+ <span>Input tokens <strong>\${escapeHtml(ev.usage.input ?? 0)}</strong></span>
940
+ <span>Output tokens <strong>\${escapeHtml(ev.usage.output ?? 0)}</strong></span>
941
+ <span>Total tokens <strong>\${escapeHtml(ev.usage.totalTokens ?? 0)}</strong></span>
942
+ <span>Cost <strong>\$\${escapeHtml(ev.usage.cost?.total?.toFixed?.(5) || "0.00")}</strong></span>
780
943
  </div>
781
944
  \` : ""}
782
945
  \`;