@raquezha/notrace 0.0.7 → 0.1.1
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/dist/notrace/adapters.js +4 -1
- package/dist/notrace/index.js +41 -13
- package/dist/notrace/renderer.js +140 -21
- package/extensions/notrace/adapters.ts +4 -1
- package/extensions/notrace/index.ts +38 -15
- package/extensions/notrace/renderer.ts +137 -21
- package/package.json +1 -1
- package/templates/dashboard.sample.html +58 -14
- package/templates/dashboard.sample.json +1 -1
- package/templates/session.sample.html +51 -9
- package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html +51 -9
- package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html +51 -9
- package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html +52 -10
- package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @raquezha/notrace
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 13be706: refactor: split antigravity monolith and implement dynamic model routing, validated toolConfig, interleaved thinking headers, and empty stream retries
|
|
8
|
+
|
|
9
|
+
docs: replace stale public model IDs in notrace sample templates
|
|
10
|
+
|
|
11
|
+
## 0.1.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- a2fc3cb: Implement machine-global observability dashboard and Mistral-style timeline parser.
|
|
16
|
+
- Storage migrated from `.notrace/` in the local working directory to a machine-wide `~/.notrace/` directory to prevent repository pollution and enable global insights.
|
|
17
|
+
- Dashboard updated with a new `Project` column for multi-repo tracking.
|
|
18
|
+
- Timeline parser overhauled to render LLM arrays, tool execution cards, and code blocks beautifully instead of raw JSON dumps.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- 8f31379: fix(noheadroom): match lowercase footer casing
|
|
23
|
+
feat(notrace): add session export to HTML retrospective
|
|
24
|
+
|
|
3
25
|
## 0.0.7
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/dist/notrace/adapters.js
CHANGED
|
@@ -22,8 +22,11 @@ function appendWorkLogEntry(taskDir, message) {
|
|
|
22
22
|
}
|
|
23
23
|
const before = lines.slice(0, nextSection);
|
|
24
24
|
const after = lines.slice(nextSection);
|
|
25
|
+
while (before.length > logIndex + 1 && before[before.length - 1]?.trim() === "") {
|
|
26
|
+
before.pop();
|
|
27
|
+
}
|
|
25
28
|
before.push(entry);
|
|
26
|
-
writeFileSync(workMd, `${before.join("\n")
|
|
29
|
+
writeFileSync(workMd, `${[...before, ...after].join("\n").replace(/\n*$/, "\n")}`);
|
|
27
30
|
}
|
|
28
31
|
catch { }
|
|
29
32
|
}
|
package/dist/notrace/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, chmodSync } from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
3
4
|
import { execSync } from "node:child_process";
|
|
4
5
|
import { getActiveAdapter } from "./adapters.js";
|
|
5
6
|
import { generateHtmlReport, generateDashboardHtml } from "./renderer.js";
|
|
@@ -147,9 +148,10 @@ function toTaskInfo(context) {
|
|
|
147
148
|
dir: context.taskDir,
|
|
148
149
|
};
|
|
149
150
|
}
|
|
150
|
-
function createIndexEntry(record,
|
|
151
|
+
function createIndexEntry(record, htmlPath, recordPath) {
|
|
151
152
|
return {
|
|
152
153
|
sessionId: record.traceId,
|
|
154
|
+
repositoryName: record.repository.name,
|
|
153
155
|
startedAt: record.session.startedAt,
|
|
154
156
|
endedAt: record.session.endedAt,
|
|
155
157
|
captureMode: record.captureMode,
|
|
@@ -157,8 +159,8 @@ function createIndexEntry(record, cwd, htmlPath, recordPath) {
|
|
|
157
159
|
conditions: record.conditions,
|
|
158
160
|
activity: record.activity,
|
|
159
161
|
artifacts: {
|
|
160
|
-
html:
|
|
161
|
-
record:
|
|
162
|
+
html: htmlPath,
|
|
163
|
+
record: recordPath,
|
|
162
164
|
},
|
|
163
165
|
};
|
|
164
166
|
}
|
|
@@ -251,13 +253,13 @@ export default function (pi) {
|
|
|
251
253
|
const endedAt = Date.now();
|
|
252
254
|
const adapter = getActiveAdapter(ctx.cwd);
|
|
253
255
|
const context = adapter.getContext(ctx.cwd);
|
|
254
|
-
const notraceDir = path.
|
|
256
|
+
const notraceDir = process.env.NOTRACE_DIR || path.join(os.homedir(), ".notrace");
|
|
255
257
|
const finalTraceId = ctx.sessionManager?.getSessionId?.() || traceId;
|
|
256
258
|
const outputDir = path.join(notraceDir, "sessions", finalTraceId.replace(/[^a-z0-9]/gi, "-"));
|
|
257
259
|
const repositoryName = path.basename(ctx.cwd);
|
|
258
260
|
let branchName = null;
|
|
259
261
|
try {
|
|
260
|
-
branchName = execSync("git branch --show-current", { cwd: ctx.cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim() || null;
|
|
262
|
+
branchName = execSync("git branch --show-current", { cwd: ctx.cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 1000 }).trim() || null;
|
|
261
263
|
}
|
|
262
264
|
catch {
|
|
263
265
|
// not a git repo or no commits yet
|
|
@@ -317,17 +319,43 @@ export default function (pi) {
|
|
|
317
319
|
writePrivateFileAtomic(htmlPath, html);
|
|
318
320
|
writePrivateFileAtomic(recordPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
319
321
|
const indexPath = path.join(notraceDir, "index.json");
|
|
320
|
-
const
|
|
321
|
-
let
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
const lockPath = `${indexPath}.lock`;
|
|
323
|
+
let lockAcquired = false;
|
|
324
|
+
for (let i = 0; i < 20; i++) {
|
|
325
|
+
try {
|
|
326
|
+
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
|
327
|
+
lockAcquired = true;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
const t = Date.now();
|
|
332
|
+
while (Date.now() - t < 50) { } // busy wait 50ms
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const existing = readJsonFile(indexPath, { sessions: [] });
|
|
337
|
+
let sessions = Array.isArray(existing.sessions) ? existing.sessions.filter((s) => s.sessionId !== finalTraceId) : [];
|
|
338
|
+
if (!isGhostSession) {
|
|
339
|
+
sessions.push(createIndexEntry(record, htmlPath, recordPath));
|
|
340
|
+
}
|
|
341
|
+
writePrivateFileAtomic(indexPath, `${JSON.stringify({ sessions }, null, 2)}\n`);
|
|
342
|
+
writePrivateFileAtomic(path.join(notraceDir, "index.html"), generateDashboardHtml(sessions, {}));
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
if (lockAcquired && existsSync(lockPath)) {
|
|
346
|
+
try {
|
|
347
|
+
import("node:fs").then(fs => fs.rmSync ? fs.rmSync(lockPath) : fs.unlinkSync(lockPath));
|
|
348
|
+
}
|
|
349
|
+
catch { }
|
|
350
|
+
}
|
|
324
351
|
}
|
|
325
|
-
writePrivateFileAtomic(indexPath, `${JSON.stringify({ repositoryName, sessions }, null, 2)}\n`);
|
|
326
|
-
writePrivateFileAtomic(path.join(notraceDir, "index.html"), generateDashboardHtml(sessions, { repositoryName }));
|
|
327
352
|
if (context) {
|
|
353
|
+
const displayPath = htmlPath.startsWith(os.homedir())
|
|
354
|
+
? `~${htmlPath.slice(os.homedir().length)}`
|
|
355
|
+
: htmlPath;
|
|
328
356
|
adapter.attach(context, {
|
|
329
|
-
html:
|
|
330
|
-
record:
|
|
357
|
+
html: displayPath,
|
|
358
|
+
record: recordPath
|
|
331
359
|
});
|
|
332
360
|
}
|
|
333
361
|
console.log(`\n\x1b[1m\x1b[38;5;208m[notrace] Session Retrospective: file://${htmlPath}\x1b[0m\n`);
|
package/dist/notrace/renderer.js
CHANGED
|
@@ -250,7 +250,7 @@ function shell(title, body, script = "") {
|
|
|
250
250
|
align-items: center;
|
|
251
251
|
margin-top: 16px;
|
|
252
252
|
}
|
|
253
|
-
.pill, .workflow-pill, .sort-btn {
|
|
253
|
+
.pill, .workflow-pill, .sort-btn, .export-btn {
|
|
254
254
|
display: inline-flex;
|
|
255
255
|
align-items: center;
|
|
256
256
|
gap: 6px;
|
|
@@ -263,7 +263,7 @@ function shell(title, body, script = "") {
|
|
|
263
263
|
font-size: 0.86rem;
|
|
264
264
|
font-weight: 600;
|
|
265
265
|
}
|
|
266
|
-
.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; }
|
|
267
267
|
.metric-card {
|
|
268
268
|
background: var(--panel-strong);
|
|
269
269
|
border: 1px solid var(--border);
|
|
@@ -382,7 +382,42 @@ function shell(title, body, script = "") {
|
|
|
382
382
|
.msg-role { font-size: 0.78rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
383
383
|
.msg.user .msg-role { color: #8ec5ff; }
|
|
384
384
|
.msg.assistant .msg-role { color: var(--accent); }
|
|
385
|
-
.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
|
+
}
|
|
386
421
|
.footer-note {
|
|
387
422
|
margin-top: 22px;
|
|
388
423
|
color: var(--muted);
|
|
@@ -427,7 +462,15 @@ function shell(title, body, script = "") {
|
|
|
427
462
|
color: var(--text);
|
|
428
463
|
border-bottom-color: rgba(236,227,218,0.45);
|
|
429
464
|
}
|
|
430
|
-
|
|
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
|
+
}
|
|
431
474
|
.container { padding: 20px 14px 48px; }
|
|
432
475
|
.hero { padding: 20px; }
|
|
433
476
|
.hero-top { grid-template-columns: 1fr; }
|
|
@@ -441,8 +484,25 @@ function shell(title, body, script = "") {
|
|
|
441
484
|
<body>${body}${script ? `<script>${script}</script>` : ""}</body>
|
|
442
485
|
</html>`;
|
|
443
486
|
}
|
|
444
|
-
function copyButton(value, label) {
|
|
445
|
-
return `<button class="
|
|
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>`;
|
|
446
506
|
}
|
|
447
507
|
function copyScript() {
|
|
448
508
|
return `(() => {
|
|
@@ -480,10 +540,66 @@ function copyScript() {
|
|
|
480
540
|
function renderJsonBlock(title, value) {
|
|
481
541
|
return `<section class="block"><h4>${escapeHtml(title)}</h4><pre>${escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2))}</pre></section>`;
|
|
482
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
|
+
}
|
|
483
599
|
function renderMessages(messages) {
|
|
484
600
|
if (!messages?.length)
|
|
485
601
|
return "";
|
|
486
|
-
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>`;
|
|
487
603
|
}
|
|
488
604
|
function eventBadgeClass(ev) {
|
|
489
605
|
if (ev.type === "llm_completion")
|
|
@@ -505,15 +621,15 @@ function renderEventCard(ev) {
|
|
|
505
621
|
if (ev.errorMessage) {
|
|
506
622
|
sections.push(renderJsonBlock("Error Message", ev.errorMessage));
|
|
507
623
|
}
|
|
508
|
-
sections.push(
|
|
624
|
+
sections.push(`<section class="block"><h4>Output</h4><div class="msg-content">${renderUniversalMessageContent({ content: ev.outputContent })}</div></section>`);
|
|
509
625
|
if (ev.usage)
|
|
510
626
|
sections.push(renderJsonBlock("Usage", ev.usage));
|
|
511
627
|
}
|
|
512
628
|
else if (ev.type === "tool_start") {
|
|
513
|
-
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>`);
|
|
514
630
|
}
|
|
515
631
|
else if (ev.type === "tool_end") {
|
|
516
|
-
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>`);
|
|
517
633
|
}
|
|
518
634
|
else {
|
|
519
635
|
sections.push(renderJsonBlock("Event", ev));
|
|
@@ -588,17 +704,19 @@ export function generateDashboardHtml(sessions, options = {}) {
|
|
|
588
704
|
const reversed = sessions.slice().reverse();
|
|
589
705
|
const totalCost = sessions.reduce((sum, s) => sum + Number(s.activity?.totals?.totalCostUsd || 0), 0);
|
|
590
706
|
const totalTokens = sessions.reduce((sum, s) => sum + Number(s.activity?.totals?.totalTokens || 0), 0);
|
|
591
|
-
const repositoryName = resolveRepoName(options);
|
|
592
707
|
const homeHref = options?.indexHref || "index.html";
|
|
593
708
|
const body = `<div class="container">
|
|
594
709
|
<section class="hero">
|
|
595
|
-
<div class="hero-
|
|
596
|
-
<
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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>
|
|
602
720
|
</div>
|
|
603
721
|
</div>
|
|
604
722
|
<div class="metrics">
|
|
@@ -609,14 +727,14 @@ export function generateDashboardHtml(sessions, options = {}) {
|
|
|
609
727
|
</section>
|
|
610
728
|
<section class="panel">
|
|
611
729
|
<h2 class="section-title">Session Reports</h2>
|
|
612
|
-
${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>
|
|
613
731
|
${reversed.map((s, index) => {
|
|
614
|
-
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) : "#";
|
|
615
733
|
const workflow = s.task?.workflow || "generic";
|
|
616
734
|
const workflowLabel = workflowDisplayName(workflow);
|
|
617
735
|
const tokens = Number(s.activity?.totals?.totalTokens || 0);
|
|
618
736
|
const cost = Number(s.activity?.totals?.totalCostUsd || 0);
|
|
619
|
-
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>`;
|
|
620
738
|
}).join("")}
|
|
621
739
|
</tbody></table>` : `<div class="empty">No sessions yet. Run Pi with notrace enabled. New reports appear here.</div>`}
|
|
622
740
|
</section>
|
|
@@ -639,6 +757,7 @@ export function generateHtmlReport(data) {
|
|
|
639
757
|
<span class="pill">${escapeHtml(resolveRepoName(data))}</span>
|
|
640
758
|
<span class="pill">Started ${formatDateLong(data.session?.startedAt)}</span>
|
|
641
759
|
<span class="pill">Mode: ${escapeHtml(data.captureMode || "full")}</span>
|
|
760
|
+
${exportButton(data)}
|
|
642
761
|
</div>
|
|
643
762
|
</div>
|
|
644
763
|
<div class="metrics">
|
|
@@ -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,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
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";
|
|
4
5
|
import { execSync } from "node:child_process";
|
|
5
6
|
import type {
|
|
6
7
|
NotraceActivity,
|
|
@@ -185,9 +186,10 @@ function toTaskInfo(context: WorkflowContext | null): NotraceRunRecord["task"] {
|
|
|
185
186
|
};
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
function createIndexEntry(record: NotraceRunRecord,
|
|
189
|
+
function createIndexEntry(record: NotraceRunRecord, htmlPath: string, recordPath: string): Record<string, unknown> {
|
|
189
190
|
return {
|
|
190
191
|
sessionId: record.traceId,
|
|
192
|
+
repositoryName: record.repository.name,
|
|
191
193
|
startedAt: record.session.startedAt,
|
|
192
194
|
endedAt: record.session.endedAt,
|
|
193
195
|
captureMode: record.captureMode,
|
|
@@ -195,8 +197,8 @@ function createIndexEntry(record: NotraceRunRecord, cwd: string, htmlPath: strin
|
|
|
195
197
|
conditions: record.conditions,
|
|
196
198
|
activity: record.activity,
|
|
197
199
|
artifacts: {
|
|
198
|
-
html:
|
|
199
|
-
record:
|
|
200
|
+
html: htmlPath,
|
|
201
|
+
record: recordPath,
|
|
200
202
|
},
|
|
201
203
|
};
|
|
202
204
|
}
|
|
@@ -299,13 +301,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
299
301
|
const endedAt = Date.now();
|
|
300
302
|
const adapter = getActiveAdapter(ctx.cwd);
|
|
301
303
|
const context = adapter.getContext(ctx.cwd);
|
|
302
|
-
const notraceDir = path.
|
|
304
|
+
const notraceDir = process.env.NOTRACE_DIR || path.join(os.homedir(), ".notrace");
|
|
303
305
|
const finalTraceId = ctx.sessionManager?.getSessionId?.() || traceId;
|
|
304
306
|
const outputDir = path.join(notraceDir, "sessions", finalTraceId.replace(/[^a-z0-9]/gi, "-"));
|
|
305
307
|
const repositoryName = path.basename(ctx.cwd);
|
|
306
308
|
let branchName: string | null = null;
|
|
307
309
|
try {
|
|
308
|
-
branchName = execSync("git branch --show-current", { cwd: ctx.cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim() || null;
|
|
310
|
+
branchName = execSync("git branch --show-current", { cwd: ctx.cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 1000 }).trim() || null;
|
|
309
311
|
} catch {
|
|
310
312
|
// not a git repo or no commits yet
|
|
311
313
|
}
|
|
@@ -371,20 +373,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
371
373
|
writePrivateFileAtomic(recordPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
372
374
|
|
|
373
375
|
const indexPath = path.join(notraceDir, "index.json");
|
|
374
|
-
const
|
|
375
|
-
let
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
}
|
|
379
402
|
}
|
|
380
|
-
|
|
381
|
-
writePrivateFileAtomic(indexPath, `${JSON.stringify({ repositoryName, sessions }, null, 2)}\n`);
|
|
382
|
-
writePrivateFileAtomic(path.join(notraceDir, "index.html"), generateDashboardHtml(sessions, { repositoryName }));
|
|
383
403
|
|
|
384
404
|
if (context) {
|
|
405
|
+
const displayPath = htmlPath.startsWith(os.homedir())
|
|
406
|
+
? `~${htmlPath.slice(os.homedir().length)}`
|
|
407
|
+
: htmlPath;
|
|
385
408
|
adapter.attach(context, {
|
|
386
|
-
html:
|
|
387
|
-
record:
|
|
409
|
+
html: displayPath,
|
|
410
|
+
record: recordPath
|
|
388
411
|
});
|
|
389
412
|
}
|
|
390
413
|
|