@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.
- package/CHANGELOG.md +22 -0
- package/README.md +24 -1
- package/bin/notrace-compare.mjs +153 -0
- package/bin/notrace-review.mjs +123 -0
- package/dist/notrace/adapters.js +4 -1
- package/dist/notrace/index.js +84 -16
- package/dist/notrace/renderer.js +219 -24
- package/dist/notrace/types.d.ts +1 -0
- package/extensions/notrace/adapters.ts +4 -1
- package/extensions/notrace/index.ts +75 -18
- package/extensions/notrace/renderer.ts +218 -24
- package/extensions/notrace/types.ts +1 -0
- package/package.json +7 -1
- package/templates/dashboard.sample.html +96 -14
- package/templates/session.sample.html +122 -12
- package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html +122 -12
- package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html +122 -12
- package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html +122 -12
package/dist/notrace/renderer.js
CHANGED
|
@@ -83,7 +83,9 @@ function taskDisplay(taskish) {
|
|
|
83
83
|
return "No active task";
|
|
84
84
|
}
|
|
85
85
|
function resolveRepoName(data) {
|
|
86
|
-
|
|
86
|
+
const name = data?.repository?.name || data?.repositoryName || data?.repoName || "Repository";
|
|
87
|
+
const branch = data?.repository?.branch;
|
|
88
|
+
return branch ? `${name} @ ${branch}` : name;
|
|
87
89
|
}
|
|
88
90
|
function formatUsd(value) {
|
|
89
91
|
const num = Number(value || 0);
|
|
@@ -202,6 +204,44 @@ function shell(title, body, script = "") {
|
|
|
202
204
|
overflow: visible;
|
|
203
205
|
}
|
|
204
206
|
.subtitle { margin: 10px 0 0; color: var(--muted); }
|
|
207
|
+
.session-subtitle {
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 10px;
|
|
211
|
+
flex-wrap: wrap;
|
|
212
|
+
}
|
|
213
|
+
.session-id-chip {
|
|
214
|
+
display: inline-flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: 8px;
|
|
217
|
+
max-width: 100%;
|
|
218
|
+
padding: 6px 8px 6px 10px;
|
|
219
|
+
border: 1px solid var(--border);
|
|
220
|
+
border-radius: 999px;
|
|
221
|
+
background: rgba(0,0,0,0.18);
|
|
222
|
+
color: var(--text);
|
|
223
|
+
font-family: "SFMono-Regular", ui-monospace, Menlo, Monaco, Consolas, monospace;
|
|
224
|
+
font-size: 0.78rem;
|
|
225
|
+
word-break: break-all;
|
|
226
|
+
}
|
|
227
|
+
.copy-btn {
|
|
228
|
+
display: inline-flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
justify-content: center;
|
|
231
|
+
width: 26px;
|
|
232
|
+
height: 26px;
|
|
233
|
+
border: 1px solid rgba(255,255,255,0.12);
|
|
234
|
+
border-radius: 999px;
|
|
235
|
+
background: rgba(255,255,255,0.04);
|
|
236
|
+
color: var(--muted);
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
|
|
239
|
+
}
|
|
240
|
+
.copy-btn:hover, .copy-btn.copied {
|
|
241
|
+
color: var(--text);
|
|
242
|
+
border-color: rgba(216,132,98,0.45);
|
|
243
|
+
background: var(--accent-soft);
|
|
244
|
+
}
|
|
205
245
|
.meta {
|
|
206
246
|
display: flex;
|
|
207
247
|
gap: 8px;
|
|
@@ -210,7 +250,7 @@ function shell(title, body, script = "") {
|
|
|
210
250
|
align-items: center;
|
|
211
251
|
margin-top: 16px;
|
|
212
252
|
}
|
|
213
|
-
.pill, .workflow-pill, .sort-btn {
|
|
253
|
+
.pill, .workflow-pill, .sort-btn, .export-btn {
|
|
214
254
|
display: inline-flex;
|
|
215
255
|
align-items: center;
|
|
216
256
|
gap: 6px;
|
|
@@ -223,7 +263,7 @@ function shell(title, body, script = "") {
|
|
|
223
263
|
font-size: 0.86rem;
|
|
224
264
|
font-weight: 600;
|
|
225
265
|
}
|
|
226
|
-
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(
|
|
266
|
+
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(135px, 1fr)); gap: 16px; margin: 24px 0; }
|
|
227
267
|
.metric-card {
|
|
228
268
|
background: var(--panel-strong);
|
|
229
269
|
border: 1px solid var(--border);
|
|
@@ -342,7 +382,42 @@ function shell(title, body, script = "") {
|
|
|
342
382
|
.msg-role { font-size: 0.78rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
343
383
|
.msg.user .msg-role { color: #8ec5ff; }
|
|
344
384
|
.msg.assistant .msg-role { color: var(--accent); }
|
|
345
|
-
.msg-content { padding: 14px;
|
|
385
|
+
.msg-content { padding: 14px; }
|
|
386
|
+
.chat-text {
|
|
387
|
+
white-space: pre-wrap;
|
|
388
|
+
word-break: break-word;
|
|
389
|
+
font-size: 0.95rem;
|
|
390
|
+
line-height: 1.6;
|
|
391
|
+
margin-bottom: 12px;
|
|
392
|
+
}
|
|
393
|
+
.chat-text:last-child { margin-bottom: 0; }
|
|
394
|
+
.chat-tool-use {
|
|
395
|
+
background: rgba(0,0,0,0.3);
|
|
396
|
+
border: 1px solid var(--border);
|
|
397
|
+
border-radius: 8px;
|
|
398
|
+
overflow: hidden;
|
|
399
|
+
margin-bottom: 12px;
|
|
400
|
+
}
|
|
401
|
+
.chat-tool-use:last-child { margin-bottom: 0; }
|
|
402
|
+
.chat-tool-header {
|
|
403
|
+
background: rgba(255,255,255,0.04);
|
|
404
|
+
padding: 8px 12px;
|
|
405
|
+
font-size: 0.8rem;
|
|
406
|
+
font-family: "SFMono-Regular", ui-monospace, Menlo, Monaco, Consolas, monospace;
|
|
407
|
+
color: #8ec5ff;
|
|
408
|
+
border-bottom: 1px solid var(--border);
|
|
409
|
+
display: flex;
|
|
410
|
+
align-items: center;
|
|
411
|
+
gap: 8px;
|
|
412
|
+
}
|
|
413
|
+
.chat-tool-body {
|
|
414
|
+
padding: 12px;
|
|
415
|
+
margin: 0;
|
|
416
|
+
background: transparent;
|
|
417
|
+
border: none;
|
|
418
|
+
max-height: 400px;
|
|
419
|
+
overflow-y: auto;
|
|
420
|
+
}
|
|
346
421
|
.footer-note {
|
|
347
422
|
margin-top: 22px;
|
|
348
423
|
color: var(--muted);
|
|
@@ -387,7 +462,15 @@ function shell(title, body, script = "") {
|
|
|
387
462
|
color: var(--text);
|
|
388
463
|
border-bottom-color: rgba(236,227,218,0.45);
|
|
389
464
|
}
|
|
390
|
-
|
|
465
|
+
.export-btn {
|
|
466
|
+
cursor: pointer;
|
|
467
|
+
transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
|
|
468
|
+
}
|
|
469
|
+
.export-btn:hover, .export-btn.copied {
|
|
470
|
+
color: var(--text);
|
|
471
|
+
border-color: rgba(216,132,98,0.45);
|
|
472
|
+
background: var(--accent-soft);
|
|
473
|
+
}
|
|
391
474
|
.container { padding: 20px 14px 48px; }
|
|
392
475
|
.hero { padding: 20px; }
|
|
393
476
|
.hero-top { grid-template-columns: 1fr; }
|
|
@@ -401,13 +484,122 @@ function shell(title, body, script = "") {
|
|
|
401
484
|
<body>${body}${script ? `<script>${script}</script>` : ""}</body>
|
|
402
485
|
</html>`;
|
|
403
486
|
}
|
|
487
|
+
function copyButton(value, label, className = "copy-btn") {
|
|
488
|
+
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>`;
|
|
489
|
+
}
|
|
490
|
+
function exportButton(data) {
|
|
491
|
+
const payload = JSON.stringify({
|
|
492
|
+
kind: "notrace-export",
|
|
493
|
+
traceId: data.traceId,
|
|
494
|
+
repository: data.repository?.name,
|
|
495
|
+
branch: data.repository?.branch,
|
|
496
|
+
task: data.task,
|
|
497
|
+
metrics: data.activity?.totals,
|
|
498
|
+
summary: data.telemetry?.extensions?.noheadroom?.summary,
|
|
499
|
+
events: (data.events || []).filter((e) => e.type === "llm_completion").map((e) => ({
|
|
500
|
+
model: e.model,
|
|
501
|
+
input: e.inputPayload?.messages,
|
|
502
|
+
output: e.outputContent
|
|
503
|
+
}))
|
|
504
|
+
});
|
|
505
|
+
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>`;
|
|
506
|
+
}
|
|
507
|
+
function copyScript() {
|
|
508
|
+
return `(() => {
|
|
509
|
+
document.querySelectorAll('[data-copy-value]').forEach((button) => {
|
|
510
|
+
button.addEventListener('click', async () => {
|
|
511
|
+
const value = button.getAttribute('data-copy-value') || '';
|
|
512
|
+
try {
|
|
513
|
+
if (navigator.clipboard?.writeText) {
|
|
514
|
+
await navigator.clipboard.writeText(value);
|
|
515
|
+
} else {
|
|
516
|
+
const textarea = document.createElement('textarea');
|
|
517
|
+
textarea.value = value;
|
|
518
|
+
textarea.style.position = 'fixed';
|
|
519
|
+
textarea.style.opacity = '0';
|
|
520
|
+
document.body.appendChild(textarea);
|
|
521
|
+
textarea.focus();
|
|
522
|
+
textarea.select();
|
|
523
|
+
document.execCommand('copy');
|
|
524
|
+
textarea.remove();
|
|
525
|
+
}
|
|
526
|
+
const previous = button.innerHTML;
|
|
527
|
+
button.classList.add('copied');
|
|
528
|
+
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>';
|
|
529
|
+
setTimeout(() => {
|
|
530
|
+
button.classList.remove('copied');
|
|
531
|
+
button.innerHTML = previous;
|
|
532
|
+
}, 1400);
|
|
533
|
+
} catch {
|
|
534
|
+
button.textContent = 'ERR';
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
})();`;
|
|
539
|
+
}
|
|
404
540
|
function renderJsonBlock(title, value) {
|
|
405
541
|
return `<section class="block"><h4>${escapeHtml(title)}</h4><pre>${escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2))}</pre></section>`;
|
|
406
542
|
}
|
|
543
|
+
function renderToolUseHtml(name, input) {
|
|
544
|
+
const parsedInput = typeof input === "string" ? (() => { try {
|
|
545
|
+
return JSON.parse(input);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return input;
|
|
549
|
+
} })() : input;
|
|
550
|
+
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>`;
|
|
551
|
+
}
|
|
552
|
+
function renderToolResultHtml(id, content) {
|
|
553
|
+
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>`;
|
|
554
|
+
}
|
|
555
|
+
function renderUniversalMessageContent(m) {
|
|
556
|
+
if (!m)
|
|
557
|
+
return "";
|
|
558
|
+
let html = "";
|
|
559
|
+
// 1. Handle string content or Anthropic/Pi blocks
|
|
560
|
+
if (typeof m.content === "string" && m.content.trim()) {
|
|
561
|
+
html += `<div class="chat-text">${escapeHtml(m.content)}</div>`;
|
|
562
|
+
}
|
|
563
|
+
else if (Array.isArray(m.content)) {
|
|
564
|
+
html += m.content.map((block) => {
|
|
565
|
+
if (!block)
|
|
566
|
+
return "";
|
|
567
|
+
if (block.type === "text")
|
|
568
|
+
return `<div class="chat-text">${escapeHtml(block.text)}</div>`;
|
|
569
|
+
if (block.type === "tool_use")
|
|
570
|
+
return renderToolUseHtml(block.name, block.input);
|
|
571
|
+
if (block.type === "tool_result")
|
|
572
|
+
return renderToolResultHtml(block.tool_use_id || "unknown", block.content);
|
|
573
|
+
return `<pre class="chat-tool-body">${escapeHtml(JSON.stringify(block, null, 2))}</pre>`;
|
|
574
|
+
}).join("");
|
|
575
|
+
}
|
|
576
|
+
else if (m.content && typeof m.content === "object") {
|
|
577
|
+
html += `<pre class="chat-tool-body">${escapeHtml(JSON.stringify(m.content, null, 2))}</pre>`;
|
|
578
|
+
}
|
|
579
|
+
// 2. Handle OpenAI/Codex tool_calls (attached to message, not in content)
|
|
580
|
+
if (Array.isArray(m.tool_calls)) {
|
|
581
|
+
html += m.tool_calls.map((tc) => {
|
|
582
|
+
if (tc.type === "function" && tc.function) {
|
|
583
|
+
return renderToolUseHtml(tc.function.name, tc.function.arguments);
|
|
584
|
+
}
|
|
585
|
+
return "";
|
|
586
|
+
}).join("");
|
|
587
|
+
}
|
|
588
|
+
// 3. Handle OpenAI legacy function_call
|
|
589
|
+
if (m.function_call) {
|
|
590
|
+
html += renderToolUseHtml(m.function_call.name, m.function_call.arguments);
|
|
591
|
+
}
|
|
592
|
+
// 4. Handle OpenAI tool result (message role is "tool")
|
|
593
|
+
if (m.role === "tool" && !html.includes("chat-tool-result")) {
|
|
594
|
+
// If it was just a string, it rendered above. Wrap it in a tool result block instead.
|
|
595
|
+
html = renderToolResultHtml(m.tool_call_id || m.name || "unknown", m.content);
|
|
596
|
+
}
|
|
597
|
+
return html || `<div class="empty">Empty message</div>`;
|
|
598
|
+
}
|
|
407
599
|
function renderMessages(messages) {
|
|
408
600
|
if (!messages?.length)
|
|
409
601
|
return "";
|
|
410
|
-
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">${
|
|
602
|
+
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>`;
|
|
411
603
|
}
|
|
412
604
|
function eventBadgeClass(ev) {
|
|
413
605
|
if (ev.type === "llm_completion")
|
|
@@ -429,15 +621,15 @@ function renderEventCard(ev) {
|
|
|
429
621
|
if (ev.errorMessage) {
|
|
430
622
|
sections.push(renderJsonBlock("Error Message", ev.errorMessage));
|
|
431
623
|
}
|
|
432
|
-
sections.push(
|
|
624
|
+
sections.push(`<section class="block"><h4>Output</h4><div class="msg-content">${renderUniversalMessageContent({ content: ev.outputContent })}</div></section>`);
|
|
433
625
|
if (ev.usage)
|
|
434
626
|
sections.push(renderJsonBlock("Usage", ev.usage));
|
|
435
627
|
}
|
|
436
628
|
else if (ev.type === "tool_start") {
|
|
437
|
-
sections.push(
|
|
629
|
+
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>`);
|
|
438
630
|
}
|
|
439
631
|
else if (ev.type === "tool_end") {
|
|
440
|
-
sections.push(
|
|
632
|
+
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>`);
|
|
441
633
|
}
|
|
442
634
|
else {
|
|
443
635
|
sections.push(renderJsonBlock("Event", ev));
|
|
@@ -512,17 +704,19 @@ export function generateDashboardHtml(sessions, options = {}) {
|
|
|
512
704
|
const reversed = sessions.slice().reverse();
|
|
513
705
|
const totalCost = sessions.reduce((sum, s) => sum + Number(s.activity?.totals?.totalCostUsd || 0), 0);
|
|
514
706
|
const totalTokens = sessions.reduce((sum, s) => sum + Number(s.activity?.totals?.totalTokens || 0), 0);
|
|
515
|
-
const repositoryName = resolveRepoName(options);
|
|
516
707
|
const homeHref = options?.indexHref || "index.html";
|
|
517
708
|
const body = `<div class="container">
|
|
518
709
|
<section class="hero">
|
|
519
|
-
<div class="hero-
|
|
520
|
-
<
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
710
|
+
<div class="hero-split">
|
|
711
|
+
<a class="brand-link" href="${escapeHtml(homeHref)}">${wordmarkSvg()}</a>
|
|
712
|
+
<div class="hero-right">
|
|
713
|
+
<div class="hero-session">
|
|
714
|
+
<strong style="color: var(--text); font-weight: 500;">Global Index</strong>
|
|
715
|
+
<span style="color: var(--muted);">Machine-wide session evidence.</span>
|
|
716
|
+
</div>
|
|
717
|
+
<div class="hero-meta">
|
|
718
|
+
<span class="hero-pill">${sessions.length} sessions</span>
|
|
719
|
+
</div>
|
|
526
720
|
</div>
|
|
527
721
|
</div>
|
|
528
722
|
<div class="metrics">
|
|
@@ -533,14 +727,14 @@ export function generateDashboardHtml(sessions, options = {}) {
|
|
|
533
727
|
</section>
|
|
534
728
|
<section class="panel">
|
|
535
729
|
<h2 class="section-title">Session Reports</h2>
|
|
536
|
-
${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>
|
|
730
|
+
${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>
|
|
537
731
|
${reversed.map((s, index) => {
|
|
538
|
-
const link = s.artifacts.html.startsWith(".notrace/") ? s.artifacts.html.substring(9) : s.artifacts.html;
|
|
732
|
+
const link = s.artifacts?.html ? (s.artifacts.html.startsWith(".notrace/") ? s.artifacts.html.substring(9) : s.artifacts.html) : "#";
|
|
539
733
|
const workflow = s.task?.workflow || "generic";
|
|
540
734
|
const workflowLabel = workflowDisplayName(workflow);
|
|
541
735
|
const tokens = Number(s.activity?.totals?.totalTokens || 0);
|
|
542
736
|
const cost = Number(s.activity?.totals?.totalCostUsd || 0);
|
|
543
|
-
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>`;
|
|
737
|
+
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>`;
|
|
544
738
|
}).join("")}
|
|
545
739
|
</tbody></table>` : `<div class="empty">No sessions yet. Run Pi with notrace enabled. New reports appear here.</div>`}
|
|
546
740
|
</section>
|
|
@@ -557,12 +751,13 @@ export function generateHtmlReport(data) {
|
|
|
557
751
|
<section class="hero">
|
|
558
752
|
<div class="hero-top">
|
|
559
753
|
<div>
|
|
560
|
-
<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
|
|
754
|
+
<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>
|
|
561
755
|
</div>
|
|
562
756
|
<div class="meta">
|
|
563
|
-
<span class="pill">${escapeHtml(
|
|
757
|
+
<span class="pill">${escapeHtml(resolveRepoName(data))}</span>
|
|
564
758
|
<span class="pill">Started ${formatDateLong(data.session?.startedAt)}</span>
|
|
565
|
-
<span class="pill">
|
|
759
|
+
<span class="pill">Mode: ${escapeHtml(data.captureMode || "full")}</span>
|
|
760
|
+
${exportButton(data)}
|
|
566
761
|
</div>
|
|
567
762
|
</div>
|
|
568
763
|
<div class="metrics">
|
|
@@ -601,5 +796,5 @@ export function generateHtmlReport(data) {
|
|
|
601
796
|
<div class="footer-meta"><a href="https://opensource.org/licenses/MIT">MIT</a></div>
|
|
602
797
|
</footer>
|
|
603
798
|
</div>`;
|
|
604
|
-
return shell(`notrace - ${data.traceId}`, body);
|
|
799
|
+
return shell(`notrace - ${data.traceId}`, body, copyScript());
|
|
605
800
|
}
|
package/dist/notrace/types.d.ts
CHANGED
|
@@ -30,8 +30,11 @@ function appendWorkLogEntry(taskDir: string, message: string): void {
|
|
|
30
30
|
}
|
|
31
31
|
const before = lines.slice(0, nextSection);
|
|
32
32
|
const after = lines.slice(nextSection);
|
|
33
|
+
while (before.length > logIndex + 1 && before[before.length - 1]?.trim() === "") {
|
|
34
|
+
before.pop();
|
|
35
|
+
}
|
|
33
36
|
before.push(entry);
|
|
34
|
-
writeFileSync(workMd, `${before.join("\n")
|
|
37
|
+
writeFileSync(workMd, `${[...before, ...after].join("\n").replace(/\n*$/, "\n")}`);
|
|
35
38
|
} catch { }
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, chmodSync } from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
4
6
|
import type {
|
|
5
7
|
NotraceActivity,
|
|
6
8
|
NotraceCaptureMode,
|
|
@@ -64,6 +66,7 @@ function isSensitiveKey(key: string): boolean {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
function sanitizeTraceValue(value: unknown): unknown {
|
|
69
|
+
if (currentMode === "metadata") return { omitted: true, reason: "metadata-capture" };
|
|
67
70
|
if (currentMode === "full") return value;
|
|
68
71
|
if (value == null || typeof value !== "object") {
|
|
69
72
|
return typeof value === "string" ? value.replace(SENSITIVE_VALUE_RE, REDACTED).slice(0, 10000) : value;
|
|
@@ -90,6 +93,30 @@ function normalizeUsage(raw: unknown): Required<Pick<UsageLike, "inputTokens" |
|
|
|
90
93
|
};
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
function readJsonFile<T>(filePath: string, fallback: T): T {
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(readFileSync(filePath, "utf-8")) as T;
|
|
99
|
+
} catch {
|
|
100
|
+
return fallback;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function writePrivateFileAtomic(filePath: string, content: string): void {
|
|
105
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
106
|
+
writeFileSync(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
107
|
+
chmodSync(tmpPath, 0o600);
|
|
108
|
+
renameSync(tmpPath, filePath);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateRunRecord(record: NotraceRunRecord): void {
|
|
112
|
+
if (record.kind !== "notrace-run") throw new Error("notrace record validation failed: invalid kind");
|
|
113
|
+
if (record.schemaVersion !== SCHEMA_VERSION) throw new Error("notrace record validation failed: invalid schemaVersion");
|
|
114
|
+
if (!record.traceId || !record.session?.id) throw new Error("notrace record validation failed: missing session id");
|
|
115
|
+
if (!record.repository?.cwd) throw new Error("notrace record validation failed: missing repository cwd");
|
|
116
|
+
if (!record.activity?.totals) throw new Error("notrace record validation failed: missing activity totals");
|
|
117
|
+
if (!Array.isArray(record.events)) throw new Error("notrace record validation failed: events must be an array");
|
|
118
|
+
}
|
|
119
|
+
|
|
93
120
|
function collectActivity(events: NotraceEvent[], startedAt: number, endedAt: number): NotraceActivity {
|
|
94
121
|
const activity: NotraceActivity = {
|
|
95
122
|
turnCount: 0,
|
|
@@ -159,9 +186,10 @@ function toTaskInfo(context: WorkflowContext | null): NotraceRunRecord["task"] {
|
|
|
159
186
|
};
|
|
160
187
|
}
|
|
161
188
|
|
|
162
|
-
function createIndexEntry(record: NotraceRunRecord,
|
|
189
|
+
function createIndexEntry(record: NotraceRunRecord, htmlPath: string, recordPath: string): Record<string, unknown> {
|
|
163
190
|
return {
|
|
164
191
|
sessionId: record.traceId,
|
|
192
|
+
repositoryName: record.repository.name,
|
|
165
193
|
startedAt: record.session.startedAt,
|
|
166
194
|
endedAt: record.session.endedAt,
|
|
167
195
|
captureMode: record.captureMode,
|
|
@@ -169,8 +197,8 @@ function createIndexEntry(record: NotraceRunRecord, cwd: string, htmlPath: strin
|
|
|
169
197
|
conditions: record.conditions,
|
|
170
198
|
activity: record.activity,
|
|
171
199
|
artifacts: {
|
|
172
|
-
html:
|
|
173
|
-
record:
|
|
200
|
+
html: htmlPath,
|
|
201
|
+
record: recordPath,
|
|
174
202
|
},
|
|
175
203
|
};
|
|
176
204
|
}
|
|
@@ -273,10 +301,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
273
301
|
const endedAt = Date.now();
|
|
274
302
|
const adapter = getActiveAdapter(ctx.cwd);
|
|
275
303
|
const context = adapter.getContext(ctx.cwd);
|
|
276
|
-
const notraceDir = path.
|
|
304
|
+
const notraceDir = process.env.NOTRACE_DIR || path.join(os.homedir(), ".notrace");
|
|
277
305
|
const finalTraceId = ctx.sessionManager?.getSessionId?.() || traceId;
|
|
278
306
|
const outputDir = path.join(notraceDir, "sessions", finalTraceId.replace(/[^a-z0-9]/gi, "-"));
|
|
279
307
|
const repositoryName = path.basename(ctx.cwd);
|
|
308
|
+
let branchName: string | null = null;
|
|
309
|
+
try {
|
|
310
|
+
branchName = execSync("git branch --show-current", { cwd: ctx.cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 1000 }).trim() || null;
|
|
311
|
+
} catch {
|
|
312
|
+
// not a git repo or no commits yet
|
|
313
|
+
}
|
|
280
314
|
const recordPath = path.join(outputDir, "notrace.json");
|
|
281
315
|
|
|
282
316
|
let mergedEvents = events;
|
|
@@ -284,7 +318,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
284
318
|
let originalTask: any = null;
|
|
285
319
|
if (existsSync(recordPath)) {
|
|
286
320
|
try {
|
|
287
|
-
const oldRecord =
|
|
321
|
+
const oldRecord = readJsonFile<any>(recordPath, null);
|
|
288
322
|
if (Array.isArray(oldRecord.events)) {
|
|
289
323
|
mergedEvents = [...oldRecord.events, ...events];
|
|
290
324
|
}
|
|
@@ -313,6 +347,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
313
347
|
repository: {
|
|
314
348
|
name: repositoryName,
|
|
315
349
|
cwd: ctx.cwd,
|
|
350
|
+
branch: branchName,
|
|
316
351
|
},
|
|
317
352
|
session: {
|
|
318
353
|
id: finalTraceId,
|
|
@@ -329,28 +364,50 @@ export default function (pi: ExtensionAPI) {
|
|
|
329
364
|
events: mergedEvents,
|
|
330
365
|
};
|
|
331
366
|
|
|
367
|
+
validateRunRecord(record);
|
|
332
368
|
const html = generateHtmlReport(record);
|
|
333
369
|
|
|
334
370
|
mkdirSync(outputDir, { recursive: true });
|
|
335
371
|
const htmlPath = path.join(outputDir, "notrace.html");
|
|
336
|
-
|
|
337
|
-
|
|
372
|
+
writePrivateFileAtomic(htmlPath, html);
|
|
373
|
+
writePrivateFileAtomic(recordPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
338
374
|
|
|
339
375
|
const indexPath = path.join(notraceDir, "index.json");
|
|
340
|
-
const
|
|
341
|
-
let
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
376
|
+
const lockPath = `${indexPath}.lock`;
|
|
377
|
+
let lockAcquired = false;
|
|
378
|
+
for (let i = 0; i < 20; i++) {
|
|
379
|
+
try {
|
|
380
|
+
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
|
381
|
+
lockAcquired = true;
|
|
382
|
+
break;
|
|
383
|
+
} catch {
|
|
384
|
+
const t = Date.now(); while (Date.now() - t < 50) {} // busy wait 50ms
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const existing = readJsonFile<any>(indexPath, { sessions: [] });
|
|
390
|
+
let sessions = Array.isArray(existing.sessions) ? existing.sessions.filter((s: any) => s.sessionId !== finalTraceId) : [];
|
|
391
|
+
|
|
392
|
+
if (!isGhostSession) {
|
|
393
|
+
sessions.push(createIndexEntry(record, htmlPath, recordPath));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
writePrivateFileAtomic(indexPath, `${JSON.stringify({ sessions }, null, 2)}\n`);
|
|
397
|
+
writePrivateFileAtomic(path.join(notraceDir, "index.html"), generateDashboardHtml(sessions, {}));
|
|
398
|
+
} finally {
|
|
399
|
+
if (lockAcquired && existsSync(lockPath)) {
|
|
400
|
+
try { import("node:fs").then(fs => fs.rmSync ? fs.rmSync(lockPath) : fs.unlinkSync(lockPath)); } catch {}
|
|
401
|
+
}
|
|
345
402
|
}
|
|
346
|
-
|
|
347
|
-
writeFileSync(indexPath, `${JSON.stringify({ repositoryName, sessions }, null, 2)}\n`);
|
|
348
|
-
writeFileSync(path.join(notraceDir, "index.html"), generateDashboardHtml(sessions, { repositoryName }));
|
|
349
403
|
|
|
350
404
|
if (context) {
|
|
405
|
+
const displayPath = htmlPath.startsWith(os.homedir())
|
|
406
|
+
? `~${htmlPath.slice(os.homedir().length)}`
|
|
407
|
+
: htmlPath;
|
|
351
408
|
adapter.attach(context, {
|
|
352
|
-
html:
|
|
353
|
-
record:
|
|
409
|
+
html: displayPath,
|
|
410
|
+
record: recordPath
|
|
354
411
|
});
|
|
355
412
|
}
|
|
356
413
|
|