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