@raquezha/notrace 0.0.6 → 0.1.0

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.
@@ -87,7 +87,9 @@ function taskDisplay(taskish: any): string {
87
87
  }
88
88
 
89
89
  function resolveRepoName(data: any): string {
90
- return data?.repository?.name || data?.repositoryName || data?.repoName || "Repository";
90
+ const name = data?.repository?.name || data?.repositoryName || data?.repoName || "Repository";
91
+ const branch = data?.repository?.branch;
92
+ return branch ? `${name} @ ${branch}` : name;
91
93
  }
92
94
 
93
95
  function formatUsd(value: number | undefined): string {
@@ -211,6 +213,44 @@ function shell(title: string, body: string, script = ""): string {
211
213
  overflow: visible;
212
214
  }
213
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
+ }
214
254
  .meta {
215
255
  display: flex;
216
256
  gap: 8px;
@@ -219,7 +259,7 @@ function shell(title: string, body: string, script = ""): string {
219
259
  align-items: center;
220
260
  margin-top: 16px;
221
261
  }
222
- .pill, .workflow-pill, .sort-btn {
262
+ .pill, .workflow-pill, .sort-btn, .export-btn {
223
263
  display: inline-flex;
224
264
  align-items: center;
225
265
  gap: 6px;
@@ -232,7 +272,7 @@ function shell(title: string, body: string, script = ""): string {
232
272
  font-size: 0.86rem;
233
273
  font-weight: 600;
234
274
  }
235
- .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 16px; margin: 24px 0; }
275
+ .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(135px, 1fr)); gap: 16px; margin: 24px 0; }
236
276
  .metric-card {
237
277
  background: var(--panel-strong);
238
278
  border: 1px solid var(--border);
@@ -351,7 +391,42 @@ function shell(title: string, body: string, script = ""): string {
351
391
  .msg-role { font-size: 0.78rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
352
392
  .msg.user .msg-role { color: #8ec5ff; }
353
393
  .msg.assistant .msg-role { color: var(--accent); }
354
- .msg-content { padding: 14px; white-space: pre-wrap; word-break: break-word; }
394
+ .msg-content { padding: 14px; }
395
+ .chat-text {
396
+ white-space: pre-wrap;
397
+ word-break: break-word;
398
+ font-size: 0.95rem;
399
+ line-height: 1.6;
400
+ margin-bottom: 12px;
401
+ }
402
+ .chat-text:last-child { margin-bottom: 0; }
403
+ .chat-tool-use {
404
+ background: rgba(0,0,0,0.3);
405
+ border: 1px solid var(--border);
406
+ border-radius: 8px;
407
+ overflow: hidden;
408
+ margin-bottom: 12px;
409
+ }
410
+ .chat-tool-use:last-child { margin-bottom: 0; }
411
+ .chat-tool-header {
412
+ background: rgba(255,255,255,0.04);
413
+ padding: 8px 12px;
414
+ font-size: 0.8rem;
415
+ font-family: "SFMono-Regular", ui-monospace, Menlo, Monaco, Consolas, monospace;
416
+ color: #8ec5ff;
417
+ border-bottom: 1px solid var(--border);
418
+ display: flex;
419
+ align-items: center;
420
+ gap: 8px;
421
+ }
422
+ .chat-tool-body {
423
+ padding: 12px;
424
+ margin: 0;
425
+ background: transparent;
426
+ border: none;
427
+ max-height: 400px;
428
+ overflow-y: auto;
429
+ }
355
430
  .footer-note {
356
431
  margin-top: 22px;
357
432
  color: var(--muted);
@@ -396,7 +471,15 @@ function shell(title: string, body: string, script = ""): string {
396
471
  color: var(--text);
397
472
  border-bottom-color: rgba(236,227,218,0.45);
398
473
  }
399
- @media (max-width: 760px) {
474
+ .export-btn {
475
+ cursor: pointer;
476
+ transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
477
+ }
478
+ .export-btn:hover, .export-btn.copied {
479
+ color: var(--text);
480
+ border-color: rgba(216,132,98,0.45);
481
+ background: var(--accent-soft);
482
+ }
400
483
  .container { padding: 20px 14px 48px; }
401
484
  .hero { padding: 20px; }
402
485
  .hero-top { grid-template-columns: 1fr; }
@@ -411,13 +494,121 @@ function shell(title: string, body: string, script = ""): string {
411
494
  </html>`;
412
495
  }
413
496
 
497
+ function copyButton(value: string, label: string, className = "copy-btn"): string {
498
+ return `<button class="${escapeHtml(className)}" 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>`;
499
+ }
500
+
501
+ function exportButton(data: any): string {
502
+ const payload = JSON.stringify({
503
+ kind: "notrace-export",
504
+ traceId: data.traceId,
505
+ repository: data.repository?.name,
506
+ branch: data.repository?.branch,
507
+ task: data.task,
508
+ metrics: data.activity?.totals,
509
+ summary: data.telemetry?.extensions?.noheadroom?.summary,
510
+ events: (data.events || []).filter((e: any) => e.type === "llm_completion").map((e: any) => ({
511
+ model: e.model,
512
+ input: e.inputPayload?.messages,
513
+ output: e.outputContent
514
+ }))
515
+ });
516
+ return `<button class="export-btn" type="button" data-copy-value="${escapeHtml(payload)}" title="Copy session data for LLM/Agent context"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg><span>Export</span></button>`;
517
+ }
518
+
519
+ function copyScript(): string {
520
+ return `(() => {
521
+ document.querySelectorAll('[data-copy-value]').forEach((button) => {
522
+ button.addEventListener('click', async () => {
523
+ const value = button.getAttribute('data-copy-value') || '';
524
+ try {
525
+ if (navigator.clipboard?.writeText) {
526
+ await navigator.clipboard.writeText(value);
527
+ } else {
528
+ const textarea = document.createElement('textarea');
529
+ textarea.value = value;
530
+ textarea.style.position = 'fixed';
531
+ textarea.style.opacity = '0';
532
+ document.body.appendChild(textarea);
533
+ textarea.focus();
534
+ textarea.select();
535
+ document.execCommand('copy');
536
+ textarea.remove();
537
+ }
538
+ const previous = button.innerHTML;
539
+ button.classList.add('copied');
540
+ 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>';
541
+ setTimeout(() => {
542
+ button.classList.remove('copied');
543
+ button.innerHTML = previous;
544
+ }, 1400);
545
+ } catch {
546
+ button.textContent = 'ERR';
547
+ }
548
+ });
549
+ });
550
+ })();`;
551
+ }
552
+
414
553
  function renderJsonBlock(title: string, value: unknown): string {
415
554
  return `<section class="block"><h4>${escapeHtml(title)}</h4><pre>${escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2))}</pre></section>`;
416
555
  }
417
556
 
557
+ function renderToolUseHtml(name: string, input: any): string {
558
+ const parsedInput = typeof input === "string" ? (() => { try { return JSON.parse(input); } catch { return input; } })() : input;
559
+ return `<div class="chat-tool-use"><div class="chat-tool-header"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg> ${escapeHtml(name)}</div><pre class="chat-tool-body">${escapeHtml(typeof parsedInput === 'string' ? parsedInput : JSON.stringify(parsedInput, null, 2))}</pre></div>`;
560
+ }
561
+
562
+ function renderToolResultHtml(id: string, content: any): string {
563
+ return `<div class="chat-tool-use"><div class="chat-tool-header" style="color: var(--muted);"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path></svg> Tool Result: ${escapeHtml(id)}</div><pre class="chat-tool-body">${escapeHtml(typeof content === 'string' ? content : JSON.stringify(content, null, 2))}</pre></div>`;
564
+ }
565
+
566
+ function renderUniversalMessageContent(m: any): string {
567
+ if (!m) return "";
568
+ let html = "";
569
+
570
+ // 1. Handle string content or Anthropic/Pi blocks
571
+ if (typeof m.content === "string" && m.content.trim()) {
572
+ html += `<div class="chat-text">${escapeHtml(m.content)}</div>`;
573
+ } else if (Array.isArray(m.content)) {
574
+ html += m.content.map((block: any) => {
575
+ if (!block) return "";
576
+ if (block.type === "text") return `<div class="chat-text">${escapeHtml(block.text)}</div>`;
577
+ if (block.type === "tool_use") return renderToolUseHtml(block.name, block.input);
578
+ if (block.type === "tool_result") return renderToolResultHtml(block.tool_use_id || "unknown", block.content);
579
+ return `<pre class="chat-tool-body">${escapeHtml(JSON.stringify(block, null, 2))}</pre>`;
580
+ }).join("");
581
+ } else if (m.content && typeof m.content === "object") {
582
+ html += `<pre class="chat-tool-body">${escapeHtml(JSON.stringify(m.content, null, 2))}</pre>`;
583
+ }
584
+
585
+ // 2. Handle OpenAI/Codex tool_calls (attached to message, not in content)
586
+ if (Array.isArray(m.tool_calls)) {
587
+ html += m.tool_calls.map((tc: any) => {
588
+ if (tc.type === "function" && tc.function) {
589
+ return renderToolUseHtml(tc.function.name, tc.function.arguments);
590
+ }
591
+ return "";
592
+ }).join("");
593
+ }
594
+
595
+ // 3. Handle OpenAI legacy function_call
596
+ if (m.function_call) {
597
+ html += renderToolUseHtml(m.function_call.name, m.function_call.arguments);
598
+ }
599
+
600
+ // 4. Handle OpenAI tool result (message role is "tool")
601
+ if (m.role === "tool" && !html.includes("chat-tool-result")) {
602
+ // If it was just a string, it rendered above. Wrap it in a tool result block instead.
603
+ html = renderToolResultHtml(m.tool_call_id || m.name || "unknown", m.content);
604
+ }
605
+
606
+ return html || `<div class="empty">Empty message</div>`;
607
+ }
608
+
418
609
  function renderMessages(messages: any[] | undefined): string {
419
610
  if (!messages?.length) return "";
420
- 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>`;
611
+ 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">${renderUniversalMessageContent(m)}</div></div>`).join("")}</section>`;
421
612
  }
422
613
 
423
614
  function eventBadgeClass(ev: any): string {
@@ -440,12 +631,12 @@ function renderEventCard(ev: any): string {
440
631
  if (ev.errorMessage) {
441
632
  sections.push(renderJsonBlock("Error Message", ev.errorMessage));
442
633
  }
443
- sections.push(renderJsonBlock("Output", ev.outputContent));
634
+ sections.push(`<section class="block"><h4>Output</h4><div class="msg-content">${renderUniversalMessageContent({ content: ev.outputContent })}</div></section>`);
444
635
  if (ev.usage) sections.push(renderJsonBlock("Usage", ev.usage));
445
636
  } else if (ev.type === "tool_start") {
446
- sections.push(renderJsonBlock("Arguments", ev.args));
637
+ sections.push(`<section class="block"><h4>Arguments</h4><div class="msg-content"><div class="chat-tool-use"><div class="chat-tool-header"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg> Execution Input</div><pre class="chat-tool-body">${escapeHtml(typeof ev.args === 'string' ? ev.args : JSON.stringify(ev.args, null, 2))}</pre></div></div></section>`);
447
638
  } else if (ev.type === "tool_end") {
448
- sections.push(renderJsonBlock(ev.isError ? "Error Result" : "Result", ev.result));
639
+ sections.push(`<section class="block"><h4>${ev.isError ? "Error Result" : "Result"}</h4><div class="msg-content"><div class="chat-tool-use" style="${ev.isError ? 'border-color: rgba(239,127,127,0.3);' : ''}"><div class="chat-tool-header" style="${ev.isError ? 'color: var(--err);' : 'color: var(--muted);'}"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path></svg> Execution Output</div><pre class="chat-tool-body">${escapeHtml(typeof ev.result === 'string' ? ev.result : JSON.stringify(ev.result, null, 2))}</pre></div></div></section>`);
449
640
  } else {
450
641
  sections.push(renderJsonBlock("Event", ev));
451
642
  }
@@ -522,17 +713,19 @@ export function generateDashboardHtml(sessions: any[], options: any = {}): strin
522
713
  const reversed = sessions.slice().reverse();
523
714
  const totalCost = sessions.reduce((sum, s) => sum + Number(s.activity?.totals?.totalCostUsd || 0), 0);
524
715
  const totalTokens = sessions.reduce((sum, s) => sum + Number(s.activity?.totals?.totalTokens || 0), 0);
525
- const repositoryName = resolveRepoName(options);
526
716
  const homeHref = options?.indexHref || "index.html";
527
717
  const body = `<div class="container">
528
718
  <section class="hero">
529
- <div class="hero-top">
530
- <div>
531
- <div class="brand"><a class="brand-link" href="${escapeHtml(homeHref)}">${wordmarkSvg()}</a><p class="subtitle">Retrospective index. Repo-local session evidence.</p></div>
532
- </div>
533
- <div class="meta">
534
- <span class="pill">${escapeHtml(repositoryName)}</span>
535
- <span class="pill">${sessions.length} sessions</span>
719
+ <div class="hero-split">
720
+ <a class="brand-link" href="${escapeHtml(homeHref)}">${wordmarkSvg()}</a>
721
+ <div class="hero-right">
722
+ <div class="hero-session">
723
+ <strong style="color: var(--text); font-weight: 500;">Global Index</strong>
724
+ <span style="color: var(--muted);">Machine-wide session evidence.</span>
725
+ </div>
726
+ <div class="hero-meta">
727
+ <span class="hero-pill">${sessions.length} sessions</span>
728
+ </div>
536
729
  </div>
537
730
  </div>
538
731
  <div class="metrics">
@@ -543,14 +736,14 @@ export function generateDashboardHtml(sessions: any[], options: any = {}): strin
543
736
  </section>
544
737
  <section class="panel">
545
738
  <h2 class="section-title">Session Reports</h2>
546
- ${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>
739
+ ${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>Project</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>
547
740
  ${reversed.map((s, index) => {
548
- const link = s.artifacts.html.startsWith(".notrace/") ? s.artifacts.html.substring(9) : s.artifacts.html;
741
+ const link = s.artifacts?.html ? (s.artifacts.html.startsWith(".notrace/") ? s.artifacts.html.substring(9) : s.artifacts.html) : "#";
549
742
  const workflow = s.task?.workflow || "generic";
550
743
  const workflowLabel = workflowDisplayName(workflow);
551
744
  const tokens = Number(s.activity?.totals?.totalTokens || 0);
552
745
  const cost = Number(s.activity?.totals?.totalCostUsd || 0);
553
- 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>`;
746
+ 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="hero-pill">${escapeHtml(s.repositoryName || "Unknown")}</span></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>`;
554
747
  }).join("")}
555
748
  </tbody></table>` : `<div class="empty">No sessions yet. Run Pi with notrace enabled. New reports appear here.</div>`}
556
749
  </section>
@@ -568,12 +761,13 @@ export function generateHtmlReport(data: any): string {
568
761
  <section class="hero">
569
762
  <div class="hero-top">
570
763
  <div>
571
- <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 retrospective for ${escapeHtml(data.traceId)}</p></div>
764
+ <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>
572
765
  </div>
573
766
  <div class="meta">
574
- <span class="pill">${escapeHtml(repositoryName)}</span>
767
+ <span class="pill">${escapeHtml(resolveRepoName(data))}</span>
575
768
  <span class="pill">Started ${formatDateLong(data.session?.startedAt)}</span>
576
- <span class="pill">Capture ${escapeHtml(data.captureMode || "full")}</span>
769
+ <span class="pill">Mode: ${escapeHtml(data.captureMode || "full")}</span>
770
+ ${exportButton(data)}
577
771
  </div>
578
772
  </div>
579
773
  <div class="metrics">
@@ -612,5 +806,5 @@ export function generateHtmlReport(data: any): string {
612
806
  <div class="footer-meta"><a href="https://opensource.org/licenses/MIT">MIT</a></div>
613
807
  </footer>
614
808
  </div>`;
615
- return shell(`notrace - ${data.traceId}`, body);
809
+ return shell(`notrace - ${data.traceId}`, body, copyScript());
616
810
  }
@@ -36,6 +36,7 @@ export type NotraceHarnessInfo = {
36
36
  export type NotraceRepositoryInfo = {
37
37
  name: string;
38
38
  cwd: string;
39
+ branch?: string | null;
39
40
  };
40
41
 
41
42
  export type NotraceSessionInfo = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raquezha/notrace",
3
- "version": "0.0.6",
3
+ "version": "0.1.0",
4
4
  "description": "Zero-dependency, local-first interactive HTML Trace Viewer for the Pi Coding Agent",
5
5
  "main": "dist/notrace/index.js",
6
6
  "types": "dist/notrace/index.d.ts",
@@ -8,6 +8,8 @@
8
8
  "scripts": {
9
9
  "build": "tsc",
10
10
  "render:samples": "npm run build && node ./templates/render-samples.mjs",
11
+ "review": "node ./bin/notrace-review.mjs",
12
+ "compare": "node ./bin/notrace-compare.mjs",
11
13
  "prepare": "npm run build"
12
14
  },
13
15
  "peerDependencies": {
@@ -31,5 +33,9 @@
31
33
  "extensions": [
32
34
  "extensions"
33
35
  ]
36
+ },
37
+ "bin": {
38
+ "notrace-review": "./bin/notrace-review.mjs",
39
+ "notrace-compare": "./bin/notrace-compare.mjs"
34
40
  }
35
41
  }
@@ -68,6 +68,44 @@
68
68
  overflow: visible;
69
69
  }
70
70
  .subtitle { margin: 10px 0 0; color: var(--muted); }
71
+ .session-subtitle {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 10px;
75
+ flex-wrap: wrap;
76
+ }
77
+ .session-id-chip {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: 8px;
81
+ max-width: 100%;
82
+ padding: 6px 8px 6px 10px;
83
+ border: 1px solid var(--border);
84
+ border-radius: 999px;
85
+ background: rgba(0,0,0,0.18);
86
+ color: var(--text);
87
+ font-family: "SFMono-Regular", ui-monospace, Menlo, Monaco, Consolas, monospace;
88
+ font-size: 0.78rem;
89
+ word-break: break-all;
90
+ }
91
+ .copy-btn {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ width: 26px;
96
+ height: 26px;
97
+ border: 1px solid rgba(255,255,255,0.12);
98
+ border-radius: 999px;
99
+ background: rgba(255,255,255,0.04);
100
+ color: var(--muted);
101
+ cursor: pointer;
102
+ transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
103
+ }
104
+ .copy-btn:hover, .copy-btn.copied {
105
+ color: var(--text);
106
+ border-color: rgba(216,132,98,0.45);
107
+ background: var(--accent-soft);
108
+ }
71
109
  .meta {
72
110
  display: flex;
73
111
  gap: 8px;
@@ -76,7 +114,7 @@
76
114
  align-items: center;
77
115
  margin-top: 16px;
78
116
  }
79
- .pill, .workflow-pill, .sort-btn {
117
+ .pill, .workflow-pill, .sort-btn, .export-btn {
80
118
  display: inline-flex;
81
119
  align-items: center;
82
120
  gap: 6px;
@@ -89,7 +127,7 @@
89
127
  font-size: 0.86rem;
90
128
  font-weight: 600;
91
129
  }
92
- .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 16px; margin: 24px 0; }
130
+ .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(135px, 1fr)); gap: 16px; margin: 24px 0; }
93
131
  .metric-card {
94
132
  background: var(--panel-strong);
95
133
  border: 1px solid var(--border);
@@ -208,7 +246,40 @@
208
246
  .msg-role { font-size: 0.78rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
209
247
  .msg.user .msg-role { color: #8ec5ff; }
210
248
  .msg.assistant .msg-role { color: var(--accent); }
211
- .msg-content { padding: 14px; white-space: pre-wrap; word-break: break-word; }
249
+ .msg-content { padding: 14px; }
250
+ .chat-text {
251
+ white-space: pre-wrap;
252
+ word-break: break-word;
253
+ font-size: 0.95rem;
254
+ line-height: 1.6;
255
+ margin-bottom: 12px;
256
+ }
257
+ .chat-text:last-child { margin-bottom: 0; }
258
+ .chat-tool-use {
259
+ background: rgba(0,0,0,0.3);
260
+ border: 1px solid var(--border);
261
+ border-radius: 8px;
262
+ overflow: hidden;
263
+ margin-bottom: 12px;
264
+ }
265
+ .chat-tool-use:last-child { margin-bottom: 0; }
266
+ .chat-tool-header {
267
+ background: rgba(255,255,255,0.04);
268
+ padding: 8px 12px;
269
+ font-size: 0.8rem;
270
+ font-family: "SFMono-Regular", ui-monospace, Menlo, Monaco, Consolas, monospace;
271
+ color: #8ec5ff;
272
+ border-bottom: 1px solid var(--border);
273
+ display: flex;
274
+ align-items: center;
275
+ gap: 8px;
276
+ }
277
+ .chat-tool-body {
278
+ padding: 12px;
279
+ margin: 0;
280
+ background: transparent;
281
+ border: none;
282
+ }
212
283
  .footer-note {
213
284
  margin-top: 22px;
214
285
  color: var(--muted);
@@ -253,7 +324,15 @@
253
324
  color: var(--text);
254
325
  border-bottom-color: rgba(236,227,218,0.45);
255
326
  }
256
- @media (max-width: 760px) {
327
+ .export-btn {
328
+ cursor: pointer;
329
+ transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
330
+ }
331
+ .export-btn:hover, .export-btn.copied {
332
+ color: var(--text);
333
+ border-color: rgba(216,132,98,0.45);
334
+ background: var(--accent-soft);
335
+ }
257
336
  .container { padding: 20px 14px 48px; }
258
337
  .hero { padding: 20px; }
259
338
  .hero-top { grid-template-columns: 1fr; }
@@ -266,9 +345,8 @@
266
345
  </head>
267
346
  <body><div class="container">
268
347
  <section class="hero">
269
- <div class="hero-top">
270
- <div>
271
- <div class="brand"><a class="brand-link" href="index.html"><svg class="wordmark" viewBox="0 0 420 138" aria-label="notrace" role="img" xmlns="http://www.w3.org/2000/svg">
348
+ <div class="hero-split">
349
+ <a class="brand-link" href="index.html"><svg class="wordmark" viewBox="0 0 420 138" aria-label="notrace" role="img" xmlns="http://www.w3.org/2000/svg">
272
350
  <defs>
273
351
  <linearGradient id="fadeGrad" x1="0%" y1="0%" x2="100%" y2="0%">
274
352
  <stop offset="0%" stop-color="#E2754A"/>
@@ -285,11 +363,15 @@
285
363
  </g>
286
364
  <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>
287
365
  <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>
288
- </svg></a><p class="subtitle">Retrospective index. Repo-local session evidence.</p></div>
289
- </div>
290
- <div class="meta">
291
- <span class="pill">nothing</span>
292
- <span class="pill">3 sessions</span>
366
+ </svg></a>
367
+ <div class="hero-right">
368
+ <div class="hero-session">
369
+ <strong style="color: var(--text); font-weight: 500;">Global Index</strong>
370
+ <span style="color: var(--muted);">Machine-wide session evidence.</span>
371
+ </div>
372
+ <div class="hero-meta">
373
+ <span class="hero-pill">3 sessions</span>
374
+ </div>
293
375
  </div>
294
376
  </div>
295
377
  <div class="metrics">
@@ -300,8 +382,8 @@
300
382
  </section>
301
383
  <section class="panel">
302
384
  <h2 class="section-title">Session Reports</h2>
303
- <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>
304
- <tr data-index="3" data-workflow="Generic" data-started="1781691240000" data-tokens="5910" data-cost="0.0334"><td class="index-cell">3</td><td><a class="session-link" href="sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html"><strong>019ed2ee</strong><span class="session-sub">019ed2ee-1002-76ee-b353-000000000003</span></a></td><td><span class="workflow-pill workflow-generic">Generic</span></td><td><div class="date-cell"><strong>2026-06-17</strong><span>18:14</span></div></td><td>General session</td><td class="num-cell">5,910</td><td class="num-cell">$0.03340</td></tr><tr data-index="2" data-workflow="Research" data-started="1781689020000" data-tokens="5754" data-cost="0.0267"><td class="index-cell">2</td><td><a class="session-link" href="sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html"><strong>019ed2ee</strong><span class="session-sub">019ed2ee-1001-76ee-b353-000000000002</span></a></td><td><span class="workflow-pill workflow-research">Research</span></td><td><div class="date-cell"><strong>2026-06-17</strong><span>17:37</span></div></td><td>Branch topic-02</td><td class="num-cell">5,754</td><td class="num-cell">$0.02670</td></tr><tr data-index="1" data-workflow="RPIV" data-started="1781686800000" data-tokens="3510" data-cost="0.0204"><td class="index-cell">1</td><td><a class="session-link" href="sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html"><strong>019ed2ee</strong><span class="session-sub">019ed2ee-1000-76ee-b353-000000000001</span></a></td><td><span class="workflow-pill workflow-rpiv">RPIV</span></td><td><div class="date-cell"><strong>2026-06-17</strong><span>17:00</span></div></td><td>NR-101</td><td class="num-cell">3,510</td><td class="num-cell">$0.02040</td></tr>
385
+ <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>Project</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>
386
+ <tr data-index="3" data-workflow="Generic" data-started="1781691240000" data-tokens="5910" data-cost="0.0334"><td class="index-cell">3</td><td><a class="session-link" href="sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html"><strong>019ed2ee</strong><span class="session-sub">019ed2ee-1002-76ee-b353-000000000003</span></a></td><td><span class="hero-pill">Unknown</span></td><td><span class="workflow-pill workflow-generic">Generic</span></td><td><div class="date-cell"><strong>2026-06-17</strong><span>18:14</span></div></td><td>General session</td><td class="num-cell">5,910</td><td class="num-cell">$0.03340</td></tr><tr data-index="2" data-workflow="Research" data-started="1781689020000" data-tokens="5754" data-cost="0.0267"><td class="index-cell">2</td><td><a class="session-link" href="sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html"><strong>019ed2ee</strong><span class="session-sub">019ed2ee-1001-76ee-b353-000000000002</span></a></td><td><span class="hero-pill">Unknown</span></td><td><span class="workflow-pill workflow-research">Research</span></td><td><div class="date-cell"><strong>2026-06-17</strong><span>17:37</span></div></td><td>Branch topic-02</td><td class="num-cell">5,754</td><td class="num-cell">$0.02670</td></tr><tr data-index="1" data-workflow="RPIV" data-started="1781686800000" data-tokens="3510" data-cost="0.0204"><td class="index-cell">1</td><td><a class="session-link" href="sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html"><strong>019ed2ee</strong><span class="session-sub">019ed2ee-1000-76ee-b353-000000000001</span></a></td><td><span class="hero-pill">Unknown</span></td><td><span class="workflow-pill workflow-rpiv">RPIV</span></td><td><div class="date-cell"><strong>2026-06-17</strong><span>17:00</span></div></td><td>NR-101</td><td class="num-cell">3,510</td><td class="num-cell">$0.02040</td></tr>
305
387
  </tbody></table>
306
388
  </section>
307
389
  <footer class="footer-note minimal">notrace • raquezha 2026</footer>