@raquezha/notrace 0.0.6 → 0.0.7
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 +8 -0
- package/README.md +24 -1
- package/bin/notrace-compare.mjs +153 -0
- package/bin/notrace-review.mjs +123 -0
- package/dist/notrace/index.js +47 -7
- package/dist/notrace/renderer.js +81 -5
- package/dist/notrace/types.d.ts +1 -0
- package/extensions/notrace/index.ts +41 -7
- package/extensions/notrace/renderer.ts +83 -5
- package/extensions/notrace/types.ts +1 -0
- package/package.json +7 -1
- package/templates/dashboard.sample.html +38 -0
- package/templates/session.sample.html +71 -3
- package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html +71 -3
- package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html +71 -3
- package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html +71 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @raquezha/notrace
|
|
2
2
|
|
|
3
|
+
## 0.0.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5a3e563: Improve session reports by rendering the session ID as a copyable chip under the notrace logo.
|
|
8
|
+
- 5a3e563: Enhance the trace header to include the active git branch alongside the repository name, and clarify the capture setting label.
|
|
9
|
+
- 7664e50: Polish notrace reliability and installed-package ergonomics: add review/compare package CLIs, validate run records before writing, atomically write private artifacts, recover from corrupt index JSON, and verify capture modes.
|
|
10
|
+
|
|
3
11
|
## 0.0.6
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -103,10 +103,12 @@ Mode meanings:
|
|
|
103
103
|
- `redacted`: captured payloads with common secret-like values redacted
|
|
104
104
|
- `metadata`: minimal capture, no prompt/tool bodies
|
|
105
105
|
|
|
106
|
-
**Security warning:**
|
|
106
|
+
**Security warning:** `full` reports can contain prompts, tool arguments, tool outputs, local paths, model payloads, and secrets returned by tools. `redacted` mode removes common secret-shaped values and sensitive keys, but redaction is best-effort and can miss project-specific secrets. `metadata` mode is safest for sharing because prompt/tool bodies are omitted, but reports can still reveal repository names, paths, timing, models, providers, and workflow metadata. Do not publish generated reports without review.
|
|
107
107
|
|
|
108
108
|
## Review
|
|
109
109
|
|
|
110
|
+
From this monorepo:
|
|
111
|
+
|
|
110
112
|
```bash
|
|
111
113
|
npm run review:notrace -- \
|
|
112
114
|
.notrace/sessions/<id>/notrace.json \
|
|
@@ -116,6 +118,17 @@ npm run review:notrace -- \
|
|
|
116
118
|
--next-change "Try same task with RepoScry enabled."
|
|
117
119
|
```
|
|
118
120
|
|
|
121
|
+
From an installed package:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npx -p @raquezha/notrace notrace-review \
|
|
125
|
+
.notrace/sessions/<id>/notrace.json \
|
|
126
|
+
--outcome partial \
|
|
127
|
+
--friction high \
|
|
128
|
+
--lesson "Headroom reduced tokens but needed manual steering." \
|
|
129
|
+
--next-change "Try same task with RepoScry enabled."
|
|
130
|
+
```
|
|
131
|
+
|
|
119
132
|
Review fields:
|
|
120
133
|
- `outcome`: `success`, `partial`, `failed`, `abandoned`, `inconclusive`
|
|
121
134
|
- `friction`: `low`, `medium`, `high`
|
|
@@ -124,12 +137,22 @@ Review fields:
|
|
|
124
137
|
|
|
125
138
|
## Compare
|
|
126
139
|
|
|
140
|
+
From this monorepo:
|
|
141
|
+
|
|
127
142
|
```bash
|
|
128
143
|
npm run compare:notrace -- \
|
|
129
144
|
.notrace/sessions/<baseline-id>/notrace.json \
|
|
130
145
|
.notrace/sessions/<candidate-id>/notrace.json
|
|
131
146
|
```
|
|
132
147
|
|
|
148
|
+
From an installed package:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npx -p @raquezha/notrace notrace-compare \
|
|
152
|
+
.notrace/sessions/<baseline-id>/notrace.json \
|
|
153
|
+
.notrace/sessions/<candidate-id>/notrace.json
|
|
154
|
+
```
|
|
155
|
+
|
|
133
156
|
## Templates
|
|
134
157
|
|
|
135
158
|
HTML source-of-truth lives in `templates/`:
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolve, relative, dirname, basename, extname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
function usage() {
|
|
6
|
+
console.error("Usage: notrace-compare <baseline-notrace.json> <candidate-notrace.json>");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const [, , baselineArg, candidateArg] = process.argv;
|
|
11
|
+
if (!baselineArg || !candidateArg) usage();
|
|
12
|
+
|
|
13
|
+
function loadReview(runPath) {
|
|
14
|
+
const reviewPath = join(dirname(runPath), `${basename(runPath, extname(runPath))}.review.json`);
|
|
15
|
+
try {
|
|
16
|
+
const data = JSON.parse(readFileSync(reviewPath, "utf8"));
|
|
17
|
+
if (data?.kind !== "notrace-review") return null;
|
|
18
|
+
return data;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadRun(filePath) {
|
|
25
|
+
const absolutePath = resolve(filePath);
|
|
26
|
+
const data = JSON.parse(readFileSync(absolutePath, "utf8"));
|
|
27
|
+
if (data?.kind !== "notrace-run") {
|
|
28
|
+
throw new Error(`${filePath} is not a notrace run record`);
|
|
29
|
+
}
|
|
30
|
+
return { path: absolutePath, data, review: loadReview(absolutePath) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fmtNumber(value) {
|
|
34
|
+
return Number(value || 0).toLocaleString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fmtUsd(value) {
|
|
38
|
+
return `$${Number(value || 0).toFixed(5)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fmtMs(value) {
|
|
42
|
+
const ms = Number(value || 0);
|
|
43
|
+
if (ms < 1000) return `${ms}ms`;
|
|
44
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fmtDelta(delta, formatter = fmtNumber, invertGood = false) {
|
|
48
|
+
const good = invertGood ? delta > 0 : delta < 0;
|
|
49
|
+
const bad = invertGood ? delta < 0 : delta > 0;
|
|
50
|
+
const sign = delta > 0 ? "+" : "";
|
|
51
|
+
const text = `${sign}${formatter(delta)}`;
|
|
52
|
+
if (good) return `${text} better`;
|
|
53
|
+
if (bad) return `${text} worse`;
|
|
54
|
+
return `${text} same`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function list(value) {
|
|
58
|
+
return Array.isArray(value) && value.length ? value.join(", ") : "-";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const baseline = loadRun(baselineArg);
|
|
62
|
+
const candidate = loadRun(candidateArg);
|
|
63
|
+
|
|
64
|
+
const a = baseline.data;
|
|
65
|
+
const b = candidate.data;
|
|
66
|
+
const aReview = baseline.review;
|
|
67
|
+
const bReview = candidate.review;
|
|
68
|
+
const aActivity = a.activity || {};
|
|
69
|
+
const bActivity = b.activity || {};
|
|
70
|
+
const aTotals = aActivity.totals || {};
|
|
71
|
+
const bTotals = bActivity.totals || {};
|
|
72
|
+
|
|
73
|
+
const rows = [
|
|
74
|
+
{
|
|
75
|
+
label: "Total tokens",
|
|
76
|
+
baseline: fmtNumber(aTotals.totalTokens),
|
|
77
|
+
candidate: fmtNumber(bTotals.totalTokens),
|
|
78
|
+
delta: fmtDelta((bTotals.totalTokens || 0) - (aTotals.totalTokens || 0))
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
label: "Input tokens",
|
|
82
|
+
baseline: fmtNumber(aTotals.inputTokens),
|
|
83
|
+
candidate: fmtNumber(bTotals.inputTokens),
|
|
84
|
+
delta: fmtDelta((bTotals.inputTokens || 0) - (aTotals.inputTokens || 0))
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: "Output tokens",
|
|
88
|
+
baseline: fmtNumber(aTotals.outputTokens),
|
|
89
|
+
candidate: fmtNumber(bTotals.outputTokens),
|
|
90
|
+
delta: fmtDelta((bTotals.outputTokens || 0) - (aTotals.outputTokens || 0))
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
label: "Duration",
|
|
94
|
+
baseline: fmtMs(aActivity.durationMs),
|
|
95
|
+
candidate: fmtMs(bActivity.durationMs),
|
|
96
|
+
delta: fmtDelta((bActivity.durationMs || 0) - (aActivity.durationMs || 0), fmtMs)
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
label: "LLM calls",
|
|
100
|
+
baseline: fmtNumber(aActivity.llmCallCount),
|
|
101
|
+
candidate: fmtNumber(bActivity.llmCallCount),
|
|
102
|
+
delta: fmtDelta((bActivity.llmCallCount || 0) - (aActivity.llmCallCount || 0))
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
label: "Tool calls",
|
|
106
|
+
baseline: fmtNumber(aActivity.toolCallCount),
|
|
107
|
+
candidate: fmtNumber(bActivity.toolCallCount),
|
|
108
|
+
delta: fmtDelta((bActivity.toolCallCount || 0) - (aActivity.toolCallCount || 0))
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
label: "Tool errors",
|
|
112
|
+
baseline: fmtNumber(aActivity.toolErrorCount),
|
|
113
|
+
candidate: fmtNumber(bActivity.toolErrorCount),
|
|
114
|
+
delta: fmtDelta((bActivity.toolErrorCount || 0) - (aActivity.toolErrorCount || 0))
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: "Cost (USD)",
|
|
118
|
+
baseline: fmtUsd(aTotals.totalCostUsd),
|
|
119
|
+
candidate: fmtUsd(bTotals.totalCostUsd),
|
|
120
|
+
delta: fmtDelta((bTotals.totalCostUsd || 0) - (aTotals.totalCostUsd || 0), fmtUsd)
|
|
121
|
+
}
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const labelWidth = Math.max(...rows.map((row) => row.label.length));
|
|
125
|
+
const baselineWidth = Math.max("Baseline".length, ...rows.map((row) => row.baseline.length));
|
|
126
|
+
const candidateWidth = Math.max("Candidate".length, ...rows.map((row) => row.candidate.length));
|
|
127
|
+
|
|
128
|
+
function pad(value, width) {
|
|
129
|
+
return String(value).padEnd(width);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log("notrace compare\n");
|
|
133
|
+
console.log(`Baseline : ${relative(process.cwd(), baseline.path) || baseline.path}`);
|
|
134
|
+
console.log(`Candidate: ${relative(process.cwd(), candidate.path) || candidate.path}`);
|
|
135
|
+
console.log("");
|
|
136
|
+
console.log(`Task : ${b.task?.id || a.task?.id || "(none)"}`);
|
|
137
|
+
console.log(`Capture : ${a.captureMode} -> ${b.captureMode}`);
|
|
138
|
+
console.log(`Models : ${list(a.conditions?.models)} -> ${list(b.conditions?.models)}`);
|
|
139
|
+
console.log(`Providers : ${list(a.conditions?.providers)} -> ${list(b.conditions?.providers)}`);
|
|
140
|
+
console.log("");
|
|
141
|
+
console.log(`Review : ${(aReview?.outcome || "-")}/${(aReview?.friction || "-")} -> ${(bReview?.outcome || "-")}/${(bReview?.friction || "-")}`);
|
|
142
|
+
if (aReview?.lesson || bReview?.lesson) {
|
|
143
|
+
console.log(`Lessons : ${(aReview?.lesson || "-")} -> ${(bReview?.lesson || "-")}`);
|
|
144
|
+
}
|
|
145
|
+
if (aReview?.nextChange || bReview?.nextChange) {
|
|
146
|
+
console.log(`Next : ${(aReview?.nextChange || "-")} -> ${(bReview?.nextChange || "-")}`);
|
|
147
|
+
}
|
|
148
|
+
console.log("");
|
|
149
|
+
console.log(`${pad("Metric", labelWidth)} | ${pad("Baseline", baselineWidth)} | ${pad("Candidate", candidateWidth)} | Delta`);
|
|
150
|
+
console.log(`${"-".repeat(labelWidth)}-+-${"-".repeat(baselineWidth)}-+-${"-".repeat(candidateWidth)}-+-${"-".repeat(24)}`);
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
console.log(`${pad(row.label, labelWidth)} | ${pad(row.baseline, baselineWidth)} | ${pad(row.candidate, candidateWidth)} | ${row.delta}`);
|
|
153
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve, relative, dirname, basename, extname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const VALID_OUTCOMES = new Set(["success", "partial", "failed", "abandoned", "inconclusive"]);
|
|
6
|
+
const VALID_FRICTION = new Set(["low", "medium", "high"]);
|
|
7
|
+
|
|
8
|
+
function appendWorkLogEntry(taskDir, message) {
|
|
9
|
+
const workMd = join(taskDir, "WORK.md");
|
|
10
|
+
if (!existsSync(workMd)) return;
|
|
11
|
+
|
|
12
|
+
const text = readFileSync(workMd, "utf8");
|
|
13
|
+
const entry = `- ${new Date().toISOString()}: ${message}`;
|
|
14
|
+
|
|
15
|
+
if (!/^(## )?\[LOG\]\s*$/m.test(text)) {
|
|
16
|
+
writeFileSync(workMd, `${text.trimEnd()}\n\n## [LOG]\n${entry}\n`, { encoding: "utf8" });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lines = text.split("\n");
|
|
21
|
+
const logIndex = lines.findIndex((line) => /^(## )?\[LOG\]\s*$/.test(line));
|
|
22
|
+
if (logIndex === -1) return;
|
|
23
|
+
|
|
24
|
+
let nextSectionIndex = lines.length;
|
|
25
|
+
for (let i = logIndex + 1; i < lines.length; i++) {
|
|
26
|
+
if (/^(## )?\[[A-Z0-9_-]+\]\s*$/.test(lines[i])) {
|
|
27
|
+
nextSectionIndex = i;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const before = lines.slice(0, nextSectionIndex);
|
|
33
|
+
const after = lines.slice(nextSectionIndex);
|
|
34
|
+
while (before.length > logIndex + 1 && before[before.length - 1]?.trim() === "") {
|
|
35
|
+
before.pop();
|
|
36
|
+
}
|
|
37
|
+
before.push(entry);
|
|
38
|
+
|
|
39
|
+
writeFileSync(workMd, `${[...before, ...after].join("\n").replace(/\n*$/, "\n")}`, { encoding: "utf8" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function usage() {
|
|
43
|
+
console.error("Usage: notrace-review <notrace.json> [--outcome value] [--friction value] [--lesson text] [--next-change text]");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const args = process.argv.slice(2);
|
|
48
|
+
if (!args.length) usage();
|
|
49
|
+
|
|
50
|
+
const runPath = resolve(args[0]);
|
|
51
|
+
const flags = args.slice(1);
|
|
52
|
+
|
|
53
|
+
function takeFlag(name) {
|
|
54
|
+
const index = flags.indexOf(name);
|
|
55
|
+
if (index === -1) return undefined;
|
|
56
|
+
const value = flags[index + 1];
|
|
57
|
+
if (!value || value.startsWith("--")) {
|
|
58
|
+
throw new Error(`Missing value for ${name}`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const outcome = takeFlag("--outcome");
|
|
64
|
+
const friction = takeFlag("--friction");
|
|
65
|
+
const lesson = takeFlag("--lesson");
|
|
66
|
+
const nextChange = takeFlag("--next-change");
|
|
67
|
+
|
|
68
|
+
if (!existsSync(runPath)) {
|
|
69
|
+
throw new Error(`Run record not found: ${runPath}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const run = JSON.parse(readFileSync(runPath, "utf8"));
|
|
73
|
+
if (run?.kind !== "notrace-run") {
|
|
74
|
+
throw new Error(`Not a notrace run record: ${runPath}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (outcome && !VALID_OUTCOMES.has(outcome)) {
|
|
78
|
+
throw new Error(`Invalid outcome: ${outcome}`);
|
|
79
|
+
}
|
|
80
|
+
if (friction && !VALID_FRICTION.has(friction)) {
|
|
81
|
+
throw new Error(`Invalid friction: ${friction}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const reviewPath = join(dirname(runPath), `${basename(runPath, extname(runPath))}.review.json`);
|
|
85
|
+
const existing = existsSync(reviewPath)
|
|
86
|
+
? JSON.parse(readFileSync(reviewPath, "utf8"))
|
|
87
|
+
: {
|
|
88
|
+
schemaVersion: 1,
|
|
89
|
+
kind: "notrace-review",
|
|
90
|
+
traceId: run.traceId,
|
|
91
|
+
runRecord: basename(runPath),
|
|
92
|
+
outcome: null,
|
|
93
|
+
friction: null,
|
|
94
|
+
lesson: "",
|
|
95
|
+
nextChange: ""
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const review = {
|
|
99
|
+
...existing,
|
|
100
|
+
traceId: run.traceId,
|
|
101
|
+
runRecord: basename(runPath),
|
|
102
|
+
outcome: outcome ?? existing.outcome ?? null,
|
|
103
|
+
friction: friction ?? existing.friction ?? null,
|
|
104
|
+
lesson: lesson ?? existing.lesson ?? "",
|
|
105
|
+
nextChange: nextChange ?? existing.nextChange ?? ""
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
writeFileSync(reviewPath, `${JSON.stringify(review, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
109
|
+
|
|
110
|
+
if (run.repository?.cwd && (run.task?.dir || run.task?.path)) {
|
|
111
|
+
const taskDir = run.task?.dir
|
|
112
|
+
? resolve(run.task.dir)
|
|
113
|
+
: resolve(run.repository.cwd, run.task.path);
|
|
114
|
+
appendWorkLogEntry(taskDir, `notrace review recorded: outcome=${review.outcome ?? "-"}, friction=${review.friction ?? "-"}, review=${relative(taskDir, reviewPath)}`);
|
|
115
|
+
} else {
|
|
116
|
+
appendWorkLogEntry(dirname(runPath), `notrace review recorded: outcome=${review.outcome ?? "-"}, friction=${review.friction ?? "-"}, review=${basename(reviewPath)}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(`notrace review ✓ ${reviewPath}`);
|
|
120
|
+
console.log(` outcome : ${review.outcome ?? "-"}`);
|
|
121
|
+
console.log(` friction : ${review.friction ?? "-"}`);
|
|
122
|
+
console.log(` lesson : ${review.lesson || "-"}`);
|
|
123
|
+
console.log(` nextChange: ${review.nextChange || "-"}`);
|
package/dist/notrace/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, chmodSync } from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
3
4
|
import { getActiveAdapter } from "./adapters.js";
|
|
4
5
|
import { generateHtmlReport, generateDashboardHtml } from "./renderer.js";
|
|
5
6
|
const REDACTED = "[REDACTED by notrace]";
|
|
@@ -21,6 +22,8 @@ function isSensitiveKey(key) {
|
|
|
21
22
|
return SENSITIVE_KEY_RE.test(normalized);
|
|
22
23
|
}
|
|
23
24
|
function sanitizeTraceValue(value) {
|
|
25
|
+
if (currentMode === "metadata")
|
|
26
|
+
return { omitted: true, reason: "metadata-capture" };
|
|
24
27
|
if (currentMode === "full")
|
|
25
28
|
return value;
|
|
26
29
|
if (value == null || typeof value !== "object") {
|
|
@@ -47,6 +50,34 @@ function normalizeUsage(raw) {
|
|
|
47
50
|
totalCostUsd: asNumber(usage.cost?.total),
|
|
48
51
|
};
|
|
49
52
|
}
|
|
53
|
+
function readJsonFile(filePath, fallback) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function writePrivateFileAtomic(filePath, content) {
|
|
62
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
63
|
+
writeFileSync(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
64
|
+
chmodSync(tmpPath, 0o600);
|
|
65
|
+
renameSync(tmpPath, filePath);
|
|
66
|
+
}
|
|
67
|
+
function validateRunRecord(record) {
|
|
68
|
+
if (record.kind !== "notrace-run")
|
|
69
|
+
throw new Error("notrace record validation failed: invalid kind");
|
|
70
|
+
if (record.schemaVersion !== SCHEMA_VERSION)
|
|
71
|
+
throw new Error("notrace record validation failed: invalid schemaVersion");
|
|
72
|
+
if (!record.traceId || !record.session?.id)
|
|
73
|
+
throw new Error("notrace record validation failed: missing session id");
|
|
74
|
+
if (!record.repository?.cwd)
|
|
75
|
+
throw new Error("notrace record validation failed: missing repository cwd");
|
|
76
|
+
if (!record.activity?.totals)
|
|
77
|
+
throw new Error("notrace record validation failed: missing activity totals");
|
|
78
|
+
if (!Array.isArray(record.events))
|
|
79
|
+
throw new Error("notrace record validation failed: events must be an array");
|
|
80
|
+
}
|
|
50
81
|
function collectActivity(events, startedAt, endedAt) {
|
|
51
82
|
const activity = {
|
|
52
83
|
turnCount: 0,
|
|
@@ -224,13 +255,20 @@ export default function (pi) {
|
|
|
224
255
|
const finalTraceId = ctx.sessionManager?.getSessionId?.() || traceId;
|
|
225
256
|
const outputDir = path.join(notraceDir, "sessions", finalTraceId.replace(/[^a-z0-9]/gi, "-"));
|
|
226
257
|
const repositoryName = path.basename(ctx.cwd);
|
|
258
|
+
let branchName = null;
|
|
259
|
+
try {
|
|
260
|
+
branchName = execSync("git branch --show-current", { cwd: ctx.cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim() || null;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// not a git repo or no commits yet
|
|
264
|
+
}
|
|
227
265
|
const recordPath = path.join(outputDir, "notrace.json");
|
|
228
266
|
let mergedEvents = events;
|
|
229
267
|
let originalStartedAt = startTime;
|
|
230
268
|
let originalTask = null;
|
|
231
269
|
if (existsSync(recordPath)) {
|
|
232
270
|
try {
|
|
233
|
-
const oldRecord =
|
|
271
|
+
const oldRecord = readJsonFile(recordPath, null);
|
|
234
272
|
if (Array.isArray(oldRecord.events)) {
|
|
235
273
|
mergedEvents = [...oldRecord.events, ...events];
|
|
236
274
|
}
|
|
@@ -256,6 +294,7 @@ export default function (pi) {
|
|
|
256
294
|
repository: {
|
|
257
295
|
name: repositoryName,
|
|
258
296
|
cwd: ctx.cwd,
|
|
297
|
+
branch: branchName,
|
|
259
298
|
},
|
|
260
299
|
session: {
|
|
261
300
|
id: finalTraceId,
|
|
@@ -271,19 +310,20 @@ export default function (pi) {
|
|
|
271
310
|
telemetry: { extensions: telemetry },
|
|
272
311
|
events: mergedEvents,
|
|
273
312
|
};
|
|
313
|
+
validateRunRecord(record);
|
|
274
314
|
const html = generateHtmlReport(record);
|
|
275
315
|
mkdirSync(outputDir, { recursive: true });
|
|
276
316
|
const htmlPath = path.join(outputDir, "notrace.html");
|
|
277
|
-
|
|
278
|
-
|
|
317
|
+
writePrivateFileAtomic(htmlPath, html);
|
|
318
|
+
writePrivateFileAtomic(recordPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
279
319
|
const indexPath = path.join(notraceDir, "index.json");
|
|
280
|
-
const existing =
|
|
320
|
+
const existing = readJsonFile(indexPath, { repositoryName, sessions: [] });
|
|
281
321
|
let sessions = Array.isArray(existing.sessions) ? existing.sessions.filter((s) => s.sessionId !== finalTraceId) : [];
|
|
282
322
|
if (!isGhostSession) {
|
|
283
323
|
sessions.push(createIndexEntry(record, ctx.cwd, htmlPath, recordPath));
|
|
284
324
|
}
|
|
285
|
-
|
|
286
|
-
|
|
325
|
+
writePrivateFileAtomic(indexPath, `${JSON.stringify({ repositoryName, sessions }, null, 2)}\n`);
|
|
326
|
+
writePrivateFileAtomic(path.join(notraceDir, "index.html"), generateDashboardHtml(sessions, { repositoryName }));
|
|
287
327
|
if (context) {
|
|
288
328
|
adapter.attach(context, {
|
|
289
329
|
html: path.relative(ctx.cwd, htmlPath),
|
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;
|
|
@@ -401,6 +441,42 @@ function shell(title, body, script = "") {
|
|
|
401
441
|
<body>${body}${script ? `<script>${script}</script>` : ""}</body>
|
|
402
442
|
</html>`;
|
|
403
443
|
}
|
|
444
|
+
function copyButton(value, label) {
|
|
445
|
+
return `<button class="copy-btn" 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>`;
|
|
446
|
+
}
|
|
447
|
+
function copyScript() {
|
|
448
|
+
return `(() => {
|
|
449
|
+
document.querySelectorAll('[data-copy-value]').forEach((button) => {
|
|
450
|
+
button.addEventListener('click', async () => {
|
|
451
|
+
const value = button.getAttribute('data-copy-value') || '';
|
|
452
|
+
try {
|
|
453
|
+
if (navigator.clipboard?.writeText) {
|
|
454
|
+
await navigator.clipboard.writeText(value);
|
|
455
|
+
} else {
|
|
456
|
+
const textarea = document.createElement('textarea');
|
|
457
|
+
textarea.value = value;
|
|
458
|
+
textarea.style.position = 'fixed';
|
|
459
|
+
textarea.style.opacity = '0';
|
|
460
|
+
document.body.appendChild(textarea);
|
|
461
|
+
textarea.focus();
|
|
462
|
+
textarea.select();
|
|
463
|
+
document.execCommand('copy');
|
|
464
|
+
textarea.remove();
|
|
465
|
+
}
|
|
466
|
+
const previous = button.innerHTML;
|
|
467
|
+
button.classList.add('copied');
|
|
468
|
+
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>';
|
|
469
|
+
setTimeout(() => {
|
|
470
|
+
button.classList.remove('copied');
|
|
471
|
+
button.innerHTML = previous;
|
|
472
|
+
}, 1400);
|
|
473
|
+
} catch {
|
|
474
|
+
button.textContent = 'ERR';
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
})();`;
|
|
479
|
+
}
|
|
404
480
|
function renderJsonBlock(title, value) {
|
|
405
481
|
return `<section class="block"><h4>${escapeHtml(title)}</h4><pre>${escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2))}</pre></section>`;
|
|
406
482
|
}
|
|
@@ -557,12 +633,12 @@ export function generateHtmlReport(data) {
|
|
|
557
633
|
<section class="hero">
|
|
558
634
|
<div class="hero-top">
|
|
559
635
|
<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
|
|
636
|
+
<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
637
|
</div>
|
|
562
638
|
<div class="meta">
|
|
563
|
-
<span class="pill">${escapeHtml(
|
|
639
|
+
<span class="pill">${escapeHtml(resolveRepoName(data))}</span>
|
|
564
640
|
<span class="pill">Started ${formatDateLong(data.session?.startedAt)}</span>
|
|
565
|
-
<span class="pill">
|
|
641
|
+
<span class="pill">Mode: ${escapeHtml(data.captureMode || "full")}</span>
|
|
566
642
|
</div>
|
|
567
643
|
</div>
|
|
568
644
|
<div class="metrics">
|
|
@@ -601,5 +677,5 @@ export function generateHtmlReport(data) {
|
|
|
601
677
|
<div class="footer-meta"><a href="https://opensource.org/licenses/MIT">MIT</a></div>
|
|
602
678
|
</footer>
|
|
603
679
|
</div>`;
|
|
604
|
-
return shell(`notrace - ${data.traceId}`, body);
|
|
680
|
+
return shell(`notrace - ${data.traceId}`, body, copyScript());
|
|
605
681
|
}
|
package/dist/notrace/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 { execSync } from "node:child_process";
|
|
4
5
|
import type {
|
|
5
6
|
NotraceActivity,
|
|
6
7
|
NotraceCaptureMode,
|
|
@@ -64,6 +65,7 @@ function isSensitiveKey(key: string): boolean {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function sanitizeTraceValue(value: unknown): unknown {
|
|
68
|
+
if (currentMode === "metadata") return { omitted: true, reason: "metadata-capture" };
|
|
67
69
|
if (currentMode === "full") return value;
|
|
68
70
|
if (value == null || typeof value !== "object") {
|
|
69
71
|
return typeof value === "string" ? value.replace(SENSITIVE_VALUE_RE, REDACTED).slice(0, 10000) : value;
|
|
@@ -90,6 +92,30 @@ function normalizeUsage(raw: unknown): Required<Pick<UsageLike, "inputTokens" |
|
|
|
90
92
|
};
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
function readJsonFile<T>(filePath: string, fallback: T): T {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(readFileSync(filePath, "utf-8")) as T;
|
|
98
|
+
} catch {
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writePrivateFileAtomic(filePath: string, content: string): void {
|
|
104
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
105
|
+
writeFileSync(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
106
|
+
chmodSync(tmpPath, 0o600);
|
|
107
|
+
renameSync(tmpPath, filePath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateRunRecord(record: NotraceRunRecord): void {
|
|
111
|
+
if (record.kind !== "notrace-run") throw new Error("notrace record validation failed: invalid kind");
|
|
112
|
+
if (record.schemaVersion !== SCHEMA_VERSION) throw new Error("notrace record validation failed: invalid schemaVersion");
|
|
113
|
+
if (!record.traceId || !record.session?.id) throw new Error("notrace record validation failed: missing session id");
|
|
114
|
+
if (!record.repository?.cwd) throw new Error("notrace record validation failed: missing repository cwd");
|
|
115
|
+
if (!record.activity?.totals) throw new Error("notrace record validation failed: missing activity totals");
|
|
116
|
+
if (!Array.isArray(record.events)) throw new Error("notrace record validation failed: events must be an array");
|
|
117
|
+
}
|
|
118
|
+
|
|
93
119
|
function collectActivity(events: NotraceEvent[], startedAt: number, endedAt: number): NotraceActivity {
|
|
94
120
|
const activity: NotraceActivity = {
|
|
95
121
|
turnCount: 0,
|
|
@@ -277,6 +303,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
277
303
|
const finalTraceId = ctx.sessionManager?.getSessionId?.() || traceId;
|
|
278
304
|
const outputDir = path.join(notraceDir, "sessions", finalTraceId.replace(/[^a-z0-9]/gi, "-"));
|
|
279
305
|
const repositoryName = path.basename(ctx.cwd);
|
|
306
|
+
let branchName: string | null = null;
|
|
307
|
+
try {
|
|
308
|
+
branchName = execSync("git branch --show-current", { cwd: ctx.cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim() || null;
|
|
309
|
+
} catch {
|
|
310
|
+
// not a git repo or no commits yet
|
|
311
|
+
}
|
|
280
312
|
const recordPath = path.join(outputDir, "notrace.json");
|
|
281
313
|
|
|
282
314
|
let mergedEvents = events;
|
|
@@ -284,7 +316,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
284
316
|
let originalTask: any = null;
|
|
285
317
|
if (existsSync(recordPath)) {
|
|
286
318
|
try {
|
|
287
|
-
const oldRecord =
|
|
319
|
+
const oldRecord = readJsonFile<any>(recordPath, null);
|
|
288
320
|
if (Array.isArray(oldRecord.events)) {
|
|
289
321
|
mergedEvents = [...oldRecord.events, ...events];
|
|
290
322
|
}
|
|
@@ -313,6 +345,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
313
345
|
repository: {
|
|
314
346
|
name: repositoryName,
|
|
315
347
|
cwd: ctx.cwd,
|
|
348
|
+
branch: branchName,
|
|
316
349
|
},
|
|
317
350
|
session: {
|
|
318
351
|
id: finalTraceId,
|
|
@@ -329,23 +362,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
329
362
|
events: mergedEvents,
|
|
330
363
|
};
|
|
331
364
|
|
|
365
|
+
validateRunRecord(record);
|
|
332
366
|
const html = generateHtmlReport(record);
|
|
333
367
|
|
|
334
368
|
mkdirSync(outputDir, { recursive: true });
|
|
335
369
|
const htmlPath = path.join(outputDir, "notrace.html");
|
|
336
|
-
|
|
337
|
-
|
|
370
|
+
writePrivateFileAtomic(htmlPath, html);
|
|
371
|
+
writePrivateFileAtomic(recordPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
338
372
|
|
|
339
373
|
const indexPath = path.join(notraceDir, "index.json");
|
|
340
|
-
const existing =
|
|
374
|
+
const existing = readJsonFile<any>(indexPath, { repositoryName, sessions: [] });
|
|
341
375
|
let sessions = Array.isArray(existing.sessions) ? existing.sessions.filter((s: any) => s.sessionId !== finalTraceId) : [];
|
|
342
376
|
|
|
343
377
|
if (!isGhostSession) {
|
|
344
378
|
sessions.push(createIndexEntry(record, ctx.cwd, htmlPath, recordPath));
|
|
345
379
|
}
|
|
346
380
|
|
|
347
|
-
|
|
348
|
-
|
|
381
|
+
writePrivateFileAtomic(indexPath, `${JSON.stringify({ repositoryName, sessions }, null, 2)}\n`);
|
|
382
|
+
writePrivateFileAtomic(path.join(notraceDir, "index.html"), generateDashboardHtml(sessions, { repositoryName }));
|
|
349
383
|
|
|
350
384
|
if (context) {
|
|
351
385
|
adapter.attach(context, {
|
|
@@ -87,7 +87,9 @@ function taskDisplay(taskish: any): string {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
function resolveRepoName(data: any): string {
|
|
90
|
-
|
|
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;
|
|
@@ -411,6 +451,44 @@ function shell(title: string, body: string, script = ""): string {
|
|
|
411
451
|
</html>`;
|
|
412
452
|
}
|
|
413
453
|
|
|
454
|
+
function copyButton(value: string, label: string): string {
|
|
455
|
+
return `<button class="copy-btn" 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>`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function copyScript(): string {
|
|
459
|
+
return `(() => {
|
|
460
|
+
document.querySelectorAll('[data-copy-value]').forEach((button) => {
|
|
461
|
+
button.addEventListener('click', async () => {
|
|
462
|
+
const value = button.getAttribute('data-copy-value') || '';
|
|
463
|
+
try {
|
|
464
|
+
if (navigator.clipboard?.writeText) {
|
|
465
|
+
await navigator.clipboard.writeText(value);
|
|
466
|
+
} else {
|
|
467
|
+
const textarea = document.createElement('textarea');
|
|
468
|
+
textarea.value = value;
|
|
469
|
+
textarea.style.position = 'fixed';
|
|
470
|
+
textarea.style.opacity = '0';
|
|
471
|
+
document.body.appendChild(textarea);
|
|
472
|
+
textarea.focus();
|
|
473
|
+
textarea.select();
|
|
474
|
+
document.execCommand('copy');
|
|
475
|
+
textarea.remove();
|
|
476
|
+
}
|
|
477
|
+
const previous = button.innerHTML;
|
|
478
|
+
button.classList.add('copied');
|
|
479
|
+
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>';
|
|
480
|
+
setTimeout(() => {
|
|
481
|
+
button.classList.remove('copied');
|
|
482
|
+
button.innerHTML = previous;
|
|
483
|
+
}, 1400);
|
|
484
|
+
} catch {
|
|
485
|
+
button.textContent = 'ERR';
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
})();`;
|
|
490
|
+
}
|
|
491
|
+
|
|
414
492
|
function renderJsonBlock(title: string, value: unknown): string {
|
|
415
493
|
return `<section class="block"><h4>${escapeHtml(title)}</h4><pre>${escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2))}</pre></section>`;
|
|
416
494
|
}
|
|
@@ -568,12 +646,12 @@ export function generateHtmlReport(data: any): string {
|
|
|
568
646
|
<section class="hero">
|
|
569
647
|
<div class="hero-top">
|
|
570
648
|
<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
|
|
649
|
+
<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
650
|
</div>
|
|
573
651
|
<div class="meta">
|
|
574
|
-
<span class="pill">${escapeHtml(
|
|
652
|
+
<span class="pill">${escapeHtml(resolveRepoName(data))}</span>
|
|
575
653
|
<span class="pill">Started ${formatDateLong(data.session?.startedAt)}</span>
|
|
576
|
-
<span class="pill">
|
|
654
|
+
<span class="pill">Mode: ${escapeHtml(data.captureMode || "full")}</span>
|
|
577
655
|
</div>
|
|
578
656
|
</div>
|
|
579
657
|
<div class="metrics">
|
|
@@ -612,5 +690,5 @@ export function generateHtmlReport(data: any): string {
|
|
|
612
690
|
<div class="footer-meta"><a href="https://opensource.org/licenses/MIT">MIT</a></div>
|
|
613
691
|
</footer>
|
|
614
692
|
</div>`;
|
|
615
|
-
return shell(`notrace - ${data.traceId}`, body);
|
|
693
|
+
return shell(`notrace - ${data.traceId}`, body, copyScript());
|
|
616
694
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raquezha/notrace",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
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;
|
|
@@ -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;
|
|
@@ -285,12 +323,12 @@
|
|
|
285
323
|
</g>
|
|
286
324
|
<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
325
|
<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">Session retrospective
|
|
326
|
+
</svg></a><p class="subtitle session-subtitle"><span>Session retrospective</span><span class="session-id-chip"><span>019ed2ee-5252-76ee-b353-ad925a6bad31</span><button class="copy-btn" type="button" data-copy-value="019ed2ee-5252-76ee-b353-ad925a6bad31" aria-label="Copy session ID" title="Copy session ID"><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></span></p></div>
|
|
289
327
|
</div>
|
|
290
328
|
<div class="meta">
|
|
291
329
|
<span class="pill">nothing</span>
|
|
292
330
|
<span class="pill">Started 2026-06-17 22:05</span>
|
|
293
|
-
<span class="pill">
|
|
331
|
+
<span class="pill">Mode: full</span>
|
|
294
332
|
</div>
|
|
295
333
|
</div>
|
|
296
334
|
<div class="metrics">
|
|
@@ -427,5 +465,35 @@
|
|
|
427
465
|
<div class="footer-tagline">Local-first retrospective engine</div>
|
|
428
466
|
<div class="footer-meta"><a href="https://opensource.org/licenses/MIT">MIT</a></div>
|
|
429
467
|
</footer>
|
|
430
|
-
</div
|
|
468
|
+
</div><script>(() => {
|
|
469
|
+
document.querySelectorAll('[data-copy-value]').forEach((button) => {
|
|
470
|
+
button.addEventListener('click', async () => {
|
|
471
|
+
const value = button.getAttribute('data-copy-value') || '';
|
|
472
|
+
try {
|
|
473
|
+
if (navigator.clipboard?.writeText) {
|
|
474
|
+
await navigator.clipboard.writeText(value);
|
|
475
|
+
} else {
|
|
476
|
+
const textarea = document.createElement('textarea');
|
|
477
|
+
textarea.value = value;
|
|
478
|
+
textarea.style.position = 'fixed';
|
|
479
|
+
textarea.style.opacity = '0';
|
|
480
|
+
document.body.appendChild(textarea);
|
|
481
|
+
textarea.focus();
|
|
482
|
+
textarea.select();
|
|
483
|
+
document.execCommand('copy');
|
|
484
|
+
textarea.remove();
|
|
485
|
+
}
|
|
486
|
+
const previous = button.innerHTML;
|
|
487
|
+
button.classList.add('copied');
|
|
488
|
+
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>';
|
|
489
|
+
setTimeout(() => {
|
|
490
|
+
button.classList.remove('copied');
|
|
491
|
+
button.innerHTML = previous;
|
|
492
|
+
}, 1400);
|
|
493
|
+
} catch {
|
|
494
|
+
button.textContent = 'ERR';
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
})();</script></body>
|
|
431
499
|
</html>
|
|
@@ -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;
|
|
@@ -285,12 +323,12 @@
|
|
|
285
323
|
</g>
|
|
286
324
|
<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
325
|
<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">Session retrospective
|
|
326
|
+
</svg></a><p class="subtitle session-subtitle"><span>Session retrospective</span><span class="session-id-chip"><span>019ed2ee-1000-76ee-b353-000000000001</span><button class="copy-btn" type="button" data-copy-value="019ed2ee-1000-76ee-b353-000000000001" aria-label="Copy session ID" title="Copy session ID"><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></span></p></div>
|
|
289
327
|
</div>
|
|
290
328
|
<div class="meta">
|
|
291
329
|
<span class="pill">nothing</span>
|
|
292
330
|
<span class="pill">Started 2026-06-17 17:00</span>
|
|
293
|
-
<span class="pill">
|
|
331
|
+
<span class="pill">Mode: full</span>
|
|
294
332
|
</div>
|
|
295
333
|
</div>
|
|
296
334
|
<div class="metrics">
|
|
@@ -422,5 +460,35 @@
|
|
|
422
460
|
<div class="footer-tagline">Local-first retrospective engine</div>
|
|
423
461
|
<div class="footer-meta"><a href="https://opensource.org/licenses/MIT">MIT</a></div>
|
|
424
462
|
</footer>
|
|
425
|
-
</div
|
|
463
|
+
</div><script>(() => {
|
|
464
|
+
document.querySelectorAll('[data-copy-value]').forEach((button) => {
|
|
465
|
+
button.addEventListener('click', async () => {
|
|
466
|
+
const value = button.getAttribute('data-copy-value') || '';
|
|
467
|
+
try {
|
|
468
|
+
if (navigator.clipboard?.writeText) {
|
|
469
|
+
await navigator.clipboard.writeText(value);
|
|
470
|
+
} else {
|
|
471
|
+
const textarea = document.createElement('textarea');
|
|
472
|
+
textarea.value = value;
|
|
473
|
+
textarea.style.position = 'fixed';
|
|
474
|
+
textarea.style.opacity = '0';
|
|
475
|
+
document.body.appendChild(textarea);
|
|
476
|
+
textarea.focus();
|
|
477
|
+
textarea.select();
|
|
478
|
+
document.execCommand('copy');
|
|
479
|
+
textarea.remove();
|
|
480
|
+
}
|
|
481
|
+
const previous = button.innerHTML;
|
|
482
|
+
button.classList.add('copied');
|
|
483
|
+
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>';
|
|
484
|
+
setTimeout(() => {
|
|
485
|
+
button.classList.remove('copied');
|
|
486
|
+
button.innerHTML = previous;
|
|
487
|
+
}, 1400);
|
|
488
|
+
} catch {
|
|
489
|
+
button.textContent = 'ERR';
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
})();</script></body>
|
|
426
494
|
</html>
|
|
@@ -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;
|
|
@@ -285,12 +323,12 @@
|
|
|
285
323
|
</g>
|
|
286
324
|
<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
325
|
<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">Session retrospective
|
|
326
|
+
</svg></a><p class="subtitle session-subtitle"><span>Session retrospective</span><span class="session-id-chip"><span>019ed2ee-1001-76ee-b353-000000000002</span><button class="copy-btn" type="button" data-copy-value="019ed2ee-1001-76ee-b353-000000000002" aria-label="Copy session ID" title="Copy session ID"><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></span></p></div>
|
|
289
327
|
</div>
|
|
290
328
|
<div class="meta">
|
|
291
329
|
<span class="pill">nothing</span>
|
|
292
330
|
<span class="pill">Started 2026-06-17 17:37</span>
|
|
293
|
-
<span class="pill">
|
|
331
|
+
<span class="pill">Mode: redacted</span>
|
|
294
332
|
</div>
|
|
295
333
|
</div>
|
|
296
334
|
<div class="metrics">
|
|
@@ -421,5 +459,35 @@
|
|
|
421
459
|
<div class="footer-tagline">Local-first retrospective engine</div>
|
|
422
460
|
<div class="footer-meta"><a href="https://opensource.org/licenses/MIT">MIT</a></div>
|
|
423
461
|
</footer>
|
|
424
|
-
</div
|
|
462
|
+
</div><script>(() => {
|
|
463
|
+
document.querySelectorAll('[data-copy-value]').forEach((button) => {
|
|
464
|
+
button.addEventListener('click', async () => {
|
|
465
|
+
const value = button.getAttribute('data-copy-value') || '';
|
|
466
|
+
try {
|
|
467
|
+
if (navigator.clipboard?.writeText) {
|
|
468
|
+
await navigator.clipboard.writeText(value);
|
|
469
|
+
} else {
|
|
470
|
+
const textarea = document.createElement('textarea');
|
|
471
|
+
textarea.value = value;
|
|
472
|
+
textarea.style.position = 'fixed';
|
|
473
|
+
textarea.style.opacity = '0';
|
|
474
|
+
document.body.appendChild(textarea);
|
|
475
|
+
textarea.focus();
|
|
476
|
+
textarea.select();
|
|
477
|
+
document.execCommand('copy');
|
|
478
|
+
textarea.remove();
|
|
479
|
+
}
|
|
480
|
+
const previous = button.innerHTML;
|
|
481
|
+
button.classList.add('copied');
|
|
482
|
+
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>';
|
|
483
|
+
setTimeout(() => {
|
|
484
|
+
button.classList.remove('copied');
|
|
485
|
+
button.innerHTML = previous;
|
|
486
|
+
}, 1400);
|
|
487
|
+
} catch {
|
|
488
|
+
button.textContent = 'ERR';
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
})();</script></body>
|
|
425
493
|
</html>
|
|
@@ -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;
|
|
@@ -285,12 +323,12 @@
|
|
|
285
323
|
</g>
|
|
286
324
|
<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
325
|
<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">Session retrospective
|
|
326
|
+
</svg></a><p class="subtitle session-subtitle"><span>Session retrospective</span><span class="session-id-chip"><span>019ed2ee-1002-76ee-b353-000000000003</span><button class="copy-btn" type="button" data-copy-value="019ed2ee-1002-76ee-b353-000000000003" aria-label="Copy session ID" title="Copy session ID"><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></span></p></div>
|
|
289
327
|
</div>
|
|
290
328
|
<div class="meta">
|
|
291
329
|
<span class="pill">nothing</span>
|
|
292
330
|
<span class="pill">Started 2026-06-17 18:14</span>
|
|
293
|
-
<span class="pill">
|
|
331
|
+
<span class="pill">Mode: metadata</span>
|
|
294
332
|
</div>
|
|
295
333
|
</div>
|
|
296
334
|
<div class="metrics">
|
|
@@ -422,5 +460,35 @@
|
|
|
422
460
|
<div class="footer-tagline">Local-first retrospective engine</div>
|
|
423
461
|
<div class="footer-meta"><a href="https://opensource.org/licenses/MIT">MIT</a></div>
|
|
424
462
|
</footer>
|
|
425
|
-
</div
|
|
463
|
+
</div><script>(() => {
|
|
464
|
+
document.querySelectorAll('[data-copy-value]').forEach((button) => {
|
|
465
|
+
button.addEventListener('click', async () => {
|
|
466
|
+
const value = button.getAttribute('data-copy-value') || '';
|
|
467
|
+
try {
|
|
468
|
+
if (navigator.clipboard?.writeText) {
|
|
469
|
+
await navigator.clipboard.writeText(value);
|
|
470
|
+
} else {
|
|
471
|
+
const textarea = document.createElement('textarea');
|
|
472
|
+
textarea.value = value;
|
|
473
|
+
textarea.style.position = 'fixed';
|
|
474
|
+
textarea.style.opacity = '0';
|
|
475
|
+
document.body.appendChild(textarea);
|
|
476
|
+
textarea.focus();
|
|
477
|
+
textarea.select();
|
|
478
|
+
document.execCommand('copy');
|
|
479
|
+
textarea.remove();
|
|
480
|
+
}
|
|
481
|
+
const previous = button.innerHTML;
|
|
482
|
+
button.classList.add('copied');
|
|
483
|
+
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>';
|
|
484
|
+
setTimeout(() => {
|
|
485
|
+
button.classList.remove('copied');
|
|
486
|
+
button.innerHTML = previous;
|
|
487
|
+
}, 1400);
|
|
488
|
+
} catch {
|
|
489
|
+
button.textContent = 'ERR';
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
})();</script></body>
|
|
426
494
|
</html>
|