@raquezha/notrace 0.0.5 → 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.
Files changed (97) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +122 -52
  3. package/assets/notrace-logo.svg +20 -0
  4. package/assets/notrace-mark.svg +18 -0
  5. package/assets/notrace-wordmark.svg +4 -0
  6. package/bin/notrace-compare.mjs +153 -0
  7. package/bin/notrace-review.mjs +123 -0
  8. package/dist/notrace/adapters.d.ts +32 -0
  9. package/dist/notrace/adapters.js +83 -0
  10. package/dist/notrace/index.d.ts +2 -0
  11. package/dist/notrace/index.js +335 -0
  12. package/dist/notrace/renderer.d.ts +4 -0
  13. package/dist/notrace/renderer.js +681 -0
  14. package/dist/notrace/types.d.ts +94 -0
  15. package/dist/notrace/types.js +1 -0
  16. package/extensions/notrace/adapters.ts +88 -0
  17. package/extensions/notrace/index.ts +393 -0
  18. package/extensions/notrace/renderer.ts +694 -0
  19. package/extensions/notrace/types.ts +109 -0
  20. package/package.json +10 -3
  21. package/templates/README.md +24 -0
  22. package/templates/dashboard.sample.html +399 -0
  23. package/templates/dashboard.sample.json +113 -0
  24. package/templates/notrace-logo.preview.png +0 -0
  25. package/templates/render-samples.mjs +44 -0
  26. package/templates/session.sample.html +499 -0
  27. package/templates/session.sample.json +127 -0
  28. package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.html +313 -0
  29. package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.json +129 -0
  30. package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.html +313 -0
  31. package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.json +129 -0
  32. package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html +494 -0
  33. package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.json +134 -0
  34. package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html +493 -0
  35. package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.json +133 -0
  36. package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html +494 -0
  37. package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.json +134 -0
  38. package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.html +423 -0
  39. package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.json +130 -0
  40. package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.html +423 -0
  41. package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.json +130 -0
  42. package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.html +423 -0
  43. package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.json +130 -0
  44. package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.html +423 -0
  45. package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.json +130 -0
  46. package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.html +423 -0
  47. package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.json +130 -0
  48. package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.html +423 -0
  49. package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.json +130 -0
  50. package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.html +423 -0
  51. package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.json +130 -0
  52. package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.html +423 -0
  53. package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.json +130 -0
  54. package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.html +423 -0
  55. package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.json +130 -0
  56. package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.html +423 -0
  57. package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.json +130 -0
  58. package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.html +423 -0
  59. package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.json +130 -0
  60. package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.html +423 -0
  61. package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.json +130 -0
  62. package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.html +423 -0
  63. package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.json +130 -0
  64. package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.html +423 -0
  65. package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.json +130 -0
  66. package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.html +423 -0
  67. package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.json +130 -0
  68. package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.html +423 -0
  69. package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.json +130 -0
  70. package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.html +423 -0
  71. package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.json +130 -0
  72. package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.html +423 -0
  73. package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.json +130 -0
  74. package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.html +423 -0
  75. package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.json +130 -0
  76. package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.html +423 -0
  77. package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.json +130 -0
  78. package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.html +423 -0
  79. package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.json +130 -0
  80. package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.html +423 -0
  81. package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.json +130 -0
  82. package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.html +423 -0
  83. package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.json +130 -0
  84. package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.html +423 -0
  85. package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.json +130 -0
  86. package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.html +423 -0
  87. package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.json +130 -0
  88. package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.html +423 -0
  89. package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.json +130 -0
  90. package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.html +423 -0
  91. package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.json +130 -0
  92. package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.html +313 -0
  93. package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.json +129 -0
  94. package/tsconfig.json +1 -1
  95. package/dist/notrace.d.ts +0 -9
  96. package/dist/notrace.js +0 -914
  97. package/extensions/notrace.ts +0 -965
@@ -1,965 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
3
- import * as path from "node:path";
4
-
5
- const REDACTED = "[REDACTED by notrace]";
6
- const MAX_STRING_LENGTH = 20_000;
7
- const MAX_ARRAY_ITEMS = 200;
8
- const MAX_OBJECT_KEYS = 200;
9
- const MAX_DEPTH = 8;
10
-
11
- const SENSITIVE_KEY_RE = /(authorization|cookie|setcookie|password|passwd|pwd|secret|token|apikey|accesskey|accesskeyid|accessid|accesstoken|privatekey|session|credential|refreshtoken|idtoken)/i;
12
- const SENSITIVE_VALUE_RE = /(bearer\s+[a-z0-9._~+/=-]{12,}|sk-[a-z0-9_-]{16,}|gh[pousr]_[a-z0-9_]{16,}|xox[baprs]-[a-z0-9-]{16,}|AKIA[0-9A-Z]{16})/gi;
13
-
14
- type CaptureMode = "metadata" | "redacted" | "full";
15
-
16
- function getCaptureMode(): CaptureMode {
17
- const mode = process.env.NOTRACE_CAPTURE?.toLowerCase();
18
- if (mode === "metadata" || mode === "full") return mode;
19
- return "redacted";
20
- }
21
-
22
- function isSensitiveKey(key: string): boolean {
23
- const normalized = key.replace(/[^a-z0-9]/gi, "");
24
- if (/^(inputtokens|outputtokens|totaltokens|prompttokens|completiontokens|reasoningtokens|cachedtokens|cachecreationinputtokens|cachereadinputtokens|cost|total|input|output|prompt|completion|reasoning|read|write)$/i.test(normalized)) {
25
- return false;
26
- }
27
- return SENSITIVE_KEY_RE.test(normalized);
28
- }
29
-
30
- function sanitizeUsageValue(usage: unknown): unknown {
31
- if (getCaptureMode() === "full") return usage;
32
- if (!usage || typeof usage !== "object") return sanitizeTraceValue(usage);
33
-
34
- const source = usage as Record<string, unknown>;
35
- const output: Record<string, unknown> = {};
36
-
37
- for (const [key, value] of Object.entries(source)) {
38
- if (typeof value === "number" || typeof value === "boolean" || value == null) {
39
- output[key] = value;
40
- } else if (typeof value === "object" && value) {
41
- output[key] = sanitizeUsageValue(value);
42
- } else {
43
- output[key] = sanitizeTraceValue(value);
44
- }
45
- }
46
-
47
- return output;
48
- }
49
-
50
- function redactString(value: string): string {
51
- const redacted = value.replace(SENSITIVE_VALUE_RE, REDACTED);
52
- if (redacted.length <= MAX_STRING_LENGTH) return redacted;
53
- return `${redacted.slice(0, MAX_STRING_LENGTH)}\n…[truncated ${redacted.length - MAX_STRING_LENGTH} chars by notrace]`;
54
- }
55
-
56
- function sanitizeTraceValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
57
- if (getCaptureMode() === "full") return value;
58
- if (value == null || typeof value === "number" || typeof value === "boolean") return value;
59
- if (typeof value === "string") return redactString(value);
60
- if (typeof value === "bigint") return value.toString();
61
- if (typeof value === "function" || typeof value === "symbol") return `[${typeof value}]`;
62
- if (depth >= MAX_DEPTH) return "[Max depth reached by notrace]";
63
- if (typeof value !== "object") return String(value);
64
- if (seen.has(value)) return "[Circular]";
65
- seen.add(value);
66
-
67
- if (Array.isArray(value)) {
68
- const items = value.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitizeTraceValue(item, depth + 1, seen));
69
- if (value.length > MAX_ARRAY_ITEMS) items.push(`…[truncated ${value.length - MAX_ARRAY_ITEMS} items by notrace]`);
70
- return items;
71
- }
72
-
73
- const output: Record<string, unknown> = {};
74
- const entries = Object.entries(value as Record<string, unknown>);
75
- for (const [key, item] of entries.slice(0, MAX_OBJECT_KEYS)) {
76
- output[key] = isSensitiveKey(key) ? REDACTED : sanitizeTraceValue(item, depth + 1, seen);
77
- }
78
- if (entries.length > MAX_OBJECT_KEYS) output.__notrace_truncated__ = `${entries.length - MAX_OBJECT_KEYS} keys`;
79
- return output;
80
- }
81
-
82
- function safeResolveUnder(baseDir: string, candidate: string): string | null {
83
- const base = path.resolve(baseDir);
84
- const resolved = path.resolve(base, candidate);
85
- const relative = path.relative(base, resolved);
86
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ? resolved : null;
87
- }
88
-
89
- function escapeHtml(value: unknown): string {
90
- return String(value).replace(/[&<>'"]/g, (char) => {
91
- switch (char) {
92
- case "&": return "&amp;";
93
- case "<": return "&lt;";
94
- case ">": return "&gt;";
95
- case "'": return "&#39;";
96
- case '"': return "&quot;";
97
- default: return char;
98
- }
99
- });
100
- }
101
-
102
- type NotraceLocation = {
103
- workflowDir: string;
104
- notraceDir: string;
105
- outputDir: string;
106
- taskDir: string | null;
107
- taskId: string | null;
108
- taskPath: string | null;
109
- };
110
-
111
- type NotraceMetrics = {
112
- totalTokens: number;
113
- inputTokens: number;
114
- outputTokens: number;
115
- totalCost: number;
116
- toolCallCount: number;
117
- toolErrorCount: number;
118
- llmCallCount: number;
119
- turnCount: number;
120
- models: string[];
121
- providers: string[];
122
- };
123
-
124
- function ensureWorkflowDir(workflowDir: string): void {
125
- if (existsSync(workflowDir)) return;
126
- try {
127
- mkdirSync(workflowDir, { recursive: true, mode: 0o700 });
128
- } catch {}
129
- }
130
-
131
- function safePathSegment(value: string): string {
132
- return value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 160) || `session-${Date.now()}`;
133
- }
134
-
135
- function getNotraceLocation(cwd: string, sessionId: string): NotraceLocation {
136
- const workflowDir = path.resolve(cwd, ".workflow");
137
- const notraceDir = path.resolve(cwd, ".notrace");
138
- const outputDir = path.join(notraceDir, "sessions", safePathSegment(sessionId));
139
- ensureWorkflowDir(workflowDir);
140
-
141
- try {
142
- const activeTaskJsonPath = path.join(workflowDir, "active_task.json");
143
- if (existsSync(activeTaskJsonPath)) {
144
- const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
145
- const candidate = typeof content.taskPath === "string"
146
- ? safeResolveUnder(cwd, content.taskPath)
147
- : typeof content.active_task === "string"
148
- ? safeResolveUnder(workflowDir, path.join("tasks", content.active_task))
149
- : null;
150
-
151
- if (candidate && safeResolveUnder(workflowDir, path.relative(workflowDir, candidate))) {
152
- return {
153
- workflowDir,
154
- notraceDir,
155
- outputDir,
156
- taskDir: candidate,
157
- taskId: typeof content.active_task === "string" ? content.active_task : path.basename(candidate),
158
- taskPath: path.relative(cwd, candidate)
159
- };
160
- }
161
- }
162
- } catch {
163
- // fallback
164
- }
165
-
166
- return {
167
- workflowDir,
168
- notraceDir,
169
- outputDir,
170
- taskDir: null,
171
- taskId: null,
172
- taskPath: null
173
- };
174
- }
175
-
176
- function updateNotraceIndex(cwd: string, location: NotraceLocation, entry: Record<string, unknown>): void {
177
- const indexPath = path.join(location.notraceDir, "index.json");
178
- const base = {
179
- schemaVersion: 1,
180
- kind: "notrace-index",
181
- workdir: ".",
182
- sessions: [] as Record<string, unknown>[]
183
- };
184
-
185
- try {
186
- mkdirSync(location.notraceDir, { recursive: true, mode: 0o700 });
187
- const existing = existsSync(indexPath)
188
- ? JSON.parse(readFileSync(indexPath, "utf-8"))
189
- : base;
190
- const sessions = Array.isArray(existing.sessions) ? existing.sessions : [];
191
- const sessionId = entry.sessionId;
192
- const nextSessions = sessions.filter((item: any) => item?.sessionId !== sessionId);
193
- nextSessions.push(entry);
194
- const next = {
195
- ...base,
196
- ...existing,
197
- schemaVersion: 1,
198
- kind: "notrace-index",
199
- workdir: ".",
200
- sessions: nextSessions
201
- };
202
- writeFileSync(indexPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
203
- } catch {
204
- // keep notrace non-fatal if index update fails
205
- }
206
- }
207
-
208
- function appendWorkLogEntry(taskDir: string, message: string): void {
209
- const workMd = path.join(taskDir, "WORK.md");
210
- if (!existsSync(workMd)) return;
211
-
212
- try {
213
- const text = readFileSync(workMd, "utf-8");
214
- const entry = `- ${new Date().toISOString()}: ${message}`;
215
-
216
- if (!/^(## )?\[LOG\]\s*$/m.test(text)) {
217
- writeFileSync(workMd, `${text.trimEnd()}\n\n## [LOG]\n${entry}\n`, { encoding: "utf-8" });
218
- return;
219
- }
220
-
221
- const lines = text.split("\n");
222
- const logIndex = lines.findIndex((line) => /^(## )?\[LOG\]\s*$/.test(line));
223
- if (logIndex === -1) return;
224
-
225
- let nextSectionIndex = lines.length;
226
- for (let i = logIndex + 1; i < lines.length; i++) {
227
- if (/^(## )?\[[A-Z0-9_-]+\]\s*$/.test(lines[i])) {
228
- nextSectionIndex = i;
229
- break;
230
- }
231
- }
232
-
233
- const before = lines.slice(0, nextSectionIndex);
234
- const after = lines.slice(nextSectionIndex);
235
- while (before.length > logIndex + 1 && before[before.length - 1]?.trim() === "") {
236
- before.pop();
237
- }
238
- before.push(entry);
239
-
240
- writeFileSync(workMd, `${[...before, ...after].join("\n").replace(/\n*$/, "\n")}`, { encoding: "utf-8" });
241
- } catch {
242
- // keep notrace non-fatal if WORK.md append fails
243
- }
244
- }
245
-
246
- function collectMetrics(events: any[]): NotraceMetrics {
247
- let totalTokens = 0;
248
- let inputTokens = 0;
249
- let outputTokens = 0;
250
- let totalCost = 0;
251
- let toolCallCount = 0;
252
- let toolErrorCount = 0;
253
- let llmCallCount = 0;
254
- let turnCount = 0;
255
- const models = new Set<string>();
256
- const providers = new Set<string>();
257
-
258
- for (const event of events) {
259
- if (event.type === "turn_start") {
260
- turnCount++;
261
- continue;
262
- }
263
-
264
- if (event.type === "tool_start") {
265
- toolCallCount++;
266
- continue;
267
- }
268
-
269
- if (event.type === "tool_end") {
270
- if (event.isError) toolErrorCount++;
271
- continue;
272
- }
273
-
274
- if (event.type === "llm_completion") {
275
- llmCallCount++;
276
- if (typeof event.model === "string" && event.model) models.add(event.model);
277
- if (typeof event.provider === "string" && event.provider) providers.add(event.provider);
278
- if (event.usage) {
279
- inputTokens += event.usage.input || 0;
280
- outputTokens += event.usage.output || 0;
281
- totalTokens += event.usage.totalTokens || 0;
282
- totalCost += event.usage.cost?.total || 0;
283
- }
284
- }
285
- }
286
-
287
- return {
288
- totalTokens,
289
- inputTokens,
290
- outputTokens,
291
- totalCost,
292
- toolCallCount,
293
- toolErrorCount,
294
- llmCallCount,
295
- turnCount,
296
- models: [...models],
297
- providers: [...providers]
298
- };
299
- }
300
-
301
- /**
302
- * html-observability extension
303
- *
304
- * Captures execution traces from the pi coding agent sessions and writes
305
- * a self-contained, interactive, beautiful HTML report to the active task's
306
- * workspace at the end of the session.
307
- */
308
- export default function (pi: ExtensionAPI) {
309
- const events: any[] = [];
310
- const sessionStartTime = Date.now();
311
- let traceId = "";
312
- let activeLlmPayload: any = null;
313
- let llmStartTime = 0;
314
- const activeToolTimes: Record<string, number> = {};
315
-
316
- // 1. Session start
317
- pi.on("session_start", async (event, ctx) => {
318
- traceId = ctx.sessionManager.getSessionId() || `session-${Date.now()}`;
319
- events.push({
320
- type: "session_start",
321
- timestamp: Date.now(),
322
- reason: event.reason,
323
- sessionFile: ctx.sessionManager.getSessionFile() || ""
324
- });
325
- });
326
-
327
- // 2. Turn start
328
- pi.on("turn_start", async (event, ctx) => {
329
- events.push({
330
- type: "turn_start",
331
- timestamp: Date.now()
332
- });
333
- });
334
-
335
- // 3. Turn end
336
- pi.on("turn_end", async (event, ctx) => {
337
- events.push({
338
- type: "turn_end",
339
- timestamp: Date.now()
340
- });
341
- });
342
-
343
- // 4. Tool start
344
- pi.on("tool_execution_start", async (event, ctx) => {
345
- const { toolCallId, toolName, args } = event;
346
- activeToolTimes[toolCallId] = Date.now();
347
- events.push({
348
- type: "tool_start",
349
- toolCallId,
350
- toolName,
351
- args: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(args),
352
- timestamp: Date.now()
353
- });
354
- });
355
-
356
- // 5. Tool end
357
- pi.on("tool_execution_end", async (event, ctx) => {
358
- const { toolCallId, toolName, result, isError } = event;
359
- const startTime = activeToolTimes[toolCallId] || Date.now();
360
- const durationMs = Date.now() - startTime;
361
-
362
- events.push({
363
- type: "tool_end",
364
- toolCallId,
365
- toolName,
366
- result: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(result),
367
- isError,
368
- durationMs,
369
- timestamp: Date.now()
370
- });
371
- delete activeToolTimes[toolCallId];
372
- });
373
-
374
- // 6. LLM call start (capture payload)
375
- pi.on("before_provider_request", async (event, ctx) => {
376
- activeLlmPayload = getCaptureMode() === "metadata" ? null : sanitizeTraceValue(event.payload);
377
- llmStartTime = Date.now();
378
- });
379
-
380
- // 7. LLM call end (capture generation / usage)
381
- pi.on("message_end", async (event, ctx) => {
382
- const { message } = event;
383
- if (message.role !== "assistant") return;
384
-
385
- const durationMs = llmStartTime > 0 ? Date.now() - llmStartTime : 0;
386
-
387
- events.push({
388
- type: "llm_completion",
389
- model: message.model || "unknown",
390
- provider: message.provider || "unknown",
391
- inputPayload: activeLlmPayload,
392
- outputContent: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(message.content),
393
- usage: sanitizeUsageValue(message.usage),
394
- durationMs,
395
- timestamp: Date.now()
396
- });
397
-
398
- activeLlmPayload = null;
399
- llmStartTime = 0;
400
- });
401
-
402
- // 8. Shutdown: Compile the HTML report and write to disk
403
- pi.on("session_shutdown", async (event, ctx) => {
404
- const sessionEndTime = Date.now();
405
- const totalDurationMs = sessionEndTime - sessionStartTime;
406
- const projectName = process.env.PHOENIX_PROJECT_NAME || "pi-coding-agent";
407
- const captureMode = getCaptureMode();
408
- const location = getNotraceLocation(ctx.cwd, traceId);
409
- const reportPath = path.join(location.outputDir, "notrace.html");
410
- const recordPath = path.join(location.outputDir, "notrace.json");
411
- const reviewPath = path.join(location.outputDir, "notrace.review.json");
412
- const metrics = collectMetrics(events);
413
- const sessionFile = ctx.sessionManager.getSessionFile() || null;
414
-
415
- const htmlContent = generateHtmlReport({
416
- traceId,
417
- projectName,
418
- startTime: new Date(sessionStartTime).toISOString(),
419
- endTime: new Date(sessionEndTime).toISOString(),
420
- durationMs: totalDurationMs,
421
- metrics: {
422
- totalTokens: metrics.totalTokens,
423
- inputTokens: metrics.inputTokens,
424
- outputTokens: metrics.outputTokens,
425
- totalCost: metrics.totalCost.toFixed(5),
426
- toolCallCount: metrics.toolCallCount,
427
- llmCallCount: metrics.llmCallCount
428
- },
429
- events
430
- });
431
-
432
- const runRecord = {
433
- schemaVersion: 1,
434
- kind: "notrace-run",
435
- traceId,
436
- runtime: "pi",
437
- projectName,
438
- captureMode,
439
- session: {
440
- id: traceId,
441
- cwd: ctx.cwd,
442
- file: sessionFile
443
- },
444
- task: location.taskId || location.taskPath
445
- ? {
446
- id: location.taskId,
447
- path: location.taskPath
448
- }
449
- : null,
450
- conditions: {
451
- models: metrics.models,
452
- providers: metrics.providers
453
- },
454
- activity: {
455
- startTime: new Date(sessionStartTime).toISOString(),
456
- endTime: new Date(sessionEndTime).toISOString(),
457
- durationMs: totalDurationMs,
458
- turnCount: metrics.turnCount,
459
- llmCallCount: metrics.llmCallCount,
460
- toolCallCount: metrics.toolCallCount,
461
- toolErrorCount: metrics.toolErrorCount,
462
- totals: {
463
- totalTokens: metrics.totalTokens,
464
- inputTokens: metrics.inputTokens,
465
- outputTokens: metrics.outputTokens,
466
- totalCostUsd: Number(metrics.totalCost.toFixed(5))
467
- }
468
- },
469
- artifacts: {
470
- htmlReportPath: path.relative(ctx.cwd, reportPath),
471
- recordPath: path.relative(ctx.cwd, recordPath),
472
- reviewPath: path.relative(ctx.cwd, reviewPath)
473
- },
474
- evidence: {
475
- events
476
- }
477
- };
478
-
479
- try {
480
- mkdirSync(location.outputDir, { recursive: true, mode: 0o700 });
481
- writeFileSync(reportPath, htmlContent, { encoding: "utf-8", mode: 0o600 });
482
- writeFileSync(recordPath, `${JSON.stringify(runRecord, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
483
- updateNotraceIndex(ctx.cwd, location, {
484
- sessionId: traceId,
485
- startedAt: new Date(sessionStartTime).toISOString(),
486
- endedAt: new Date(sessionEndTime).toISOString(),
487
- taskId: location.taskId,
488
- taskPath: location.taskPath,
489
- artifacts: {
490
- record: path.relative(ctx.cwd, recordPath),
491
- html: path.relative(ctx.cwd, reportPath),
492
- review: path.relative(ctx.cwd, reviewPath)
493
- }
494
- });
495
- if (location.taskDir && (location.taskId || location.taskPath)) {
496
- appendWorkLogEntry(location.taskDir, `notrace captured artifacts: ${path.relative(location.taskDir, reportPath)}, ${path.relative(location.taskDir, recordPath)}`);
497
- }
498
- console.log(`\n📊 [notrace] Observability artifacts generated:`);
499
- console.log(`👉 \x1b[36mfile://${reportPath}\x1b[0m`);
500
- console.log(`🧾 \x1b[36mfile://${recordPath}\x1b[0m\n`);
501
- } catch (err: any) {
502
- console.warn(`[notrace] Failed to write notrace artifacts: ${err?.message || err}`);
503
- }
504
- });
505
- }
506
-
507
- function safeJsonForScript(value: any): string {
508
- return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (char) => {
509
- switch (char) {
510
- case "<": return "\\u003c";
511
- case ">": return "\\u003e";
512
- case "&": return "\\u0026";
513
- case "\u2028": return "\\u2028";
514
- case "\u2029": return "\\u2029";
515
- default: return char;
516
- }
517
- });
518
- }
519
-
520
- // Returns a self-contained premium HTML template incorporating the design tokens
521
- function generateHtmlReport(data: any): string {
522
- const serializedData = safeJsonForScript(data);
523
- const escapedTraceId = escapeHtml(data.traceId);
524
-
525
- return `<!DOCTYPE html>
526
- <html lang="en">
527
- <head>
528
- <meta charset="UTF-8">
529
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
530
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'; connect-src 'none'">
531
- <meta name="referrer" content="no-referrer">
532
- <title>notrace - ${escapedTraceId}</title>
533
- <style>
534
- :root {
535
- --bg: #191613;
536
- --bg-glow: #2a221c;
537
- --panel: rgba(244, 237, 228, 0.055);
538
- --panel-strong: rgba(244, 237, 228, 0.075);
539
- --paper: #f6efe7;
540
- --paper-soft: #e7dbce;
541
- --text: #f5eee7;
542
- --text-soft: #ddd0c2;
543
- --text-muted: #b9ab9d;
544
- --border: rgba(255, 255, 255, 0.09);
545
- --border-strong: rgba(255, 255, 255, 0.16);
546
- --accent: #d88462;
547
- --accent-soft: rgba(216, 132, 98, 0.14);
548
- --session: #8ab7ff;
549
- --turn: #e0b46b;
550
- --tool: #86cca4;
551
- --error: #f08e8e;
552
- --shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
553
- --card-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
554
- }
555
-
556
- * { box-sizing: border-box; margin: 0; padding: 0; }
557
-
558
- body {
559
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
560
- background:
561
- radial-gradient(900px 420px at 50% -10%, rgba(216, 132, 98, 0.16), transparent 60%),
562
- radial-gradient(680px 360px at 10% 0%, rgba(255, 255, 255, 0.03), transparent 60%),
563
- linear-gradient(180deg, #171411 0%, #1b1714 100%);
564
- color: var(--text);
565
- line-height: 1.65;
566
- padding: 14px 12px 40px;
567
- min-height: 100vh;
568
- letter-spacing: 0.01em;
569
- }
570
-
571
- .container { max-width: 920px; margin: 0 auto; }
572
- header { margin-bottom: 24px; }
573
-
574
- .hero {
575
- position: relative;
576
- overflow: hidden;
577
- background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.025));
578
- border: 1px solid var(--border);
579
- border-radius: 22px;
580
- padding: 18px 16px 16px;
581
- box-shadow: var(--shadow);
582
- backdrop-filter: blur(10px);
583
- }
584
-
585
- .hero::after {
586
- content: "";
587
- position: absolute;
588
- inset: auto -10% -30% auto;
589
- width: 260px;
590
- height: 260px;
591
- background: radial-gradient(circle, rgba(216,132,98,0.14), transparent 70%);
592
- pointer-events: none;
593
- }
594
-
595
- .brand { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; position: relative; z-index: 1; }
596
- .brand h1 { font-size: 2rem; line-height: 1; font-weight: 680; color: var(--text); letter-spacing: -0.03em; }
597
- .brand-tag { font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; color: #f4ccb9; background: var(--accent-soft); border: 1px solid rgba(216, 132, 98, 0.24); padding: 0.35rem 0.6rem; border-radius: 999px; }
598
- .hero-subtitle { position: relative; z-index: 1; color: var(--text-soft); margin-bottom: 20px; max-width: 720px; font-size: 1rem; }
599
- .hero-meta { position: relative; z-index: 1; display: flex; flex-wrap: wrap; gap: 10px; }
600
- .meta-pill { display: inline-flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.04); border: 1px solid var(--border); border-radius: 999px; padding: 0.48rem 0.82rem; font-size: 0.86rem; color: var(--text-muted); }
601
- .meta-pill strong { color: var(--text); font-weight: 570; }
602
-
603
- .metrics-grid { display: grid; grid-template-columns: 1fr; gap: 10px; margin: 16px 0 24px; }
604
- .metric-card {
605
- background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.028));
606
- border: 1px solid var(--border);
607
- border-radius: 20px;
608
- padding: 18px 16px;
609
- box-shadow: var(--card-shadow);
610
- transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
611
- }
612
- .metric-card:hover { border-color: var(--border-strong); transform: translateY(-1px); background: linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0.03)); }
613
- .metric-label { font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); margin-bottom: 7px; }
614
- .metric-value { font-size: 1.42rem; font-weight: 640; color: var(--text); letter-spacing: -0.03em; }
615
-
616
- .section-title {
617
- font-size: 0.95rem;
618
- font-weight: 620;
619
- color: var(--paper-soft);
620
- margin-bottom: 16px;
621
- padding-left: 2px;
622
- letter-spacing: 0.04em;
623
- text-transform: uppercase;
624
- }
625
-
626
- .timeline { display: flex; flex-direction: column; gap: 16px; }
627
- .timeline-event { position: relative; animation: slideIn 0.28s ease-out; }
628
- @keyframes slideIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
629
- .timeline-dot { display: none; }
630
-
631
- .card {
632
- position: relative;
633
- overflow: hidden;
634
- background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.024));
635
- border: 1px solid var(--border);
636
- border-radius: 18px;
637
- padding: 14px 14px 13px;
638
- box-shadow: var(--card-shadow);
639
- transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
640
- }
641
- .card::before {
642
- content: "";
643
- position: absolute;
644
- left: 0;
645
- top: 0;
646
- bottom: 0;
647
- width: 3px;
648
- background: rgba(255,255,255,0.1);
649
- }
650
- .timeline-event.session-start .card::before { background: var(--session); }
651
- .timeline-event.turn-start .card::before { background: var(--turn); }
652
- .timeline-event.tool-start .card::before { background: var(--tool); }
653
- .timeline-event.tool-start.error .card::before { background: var(--error); }
654
- .timeline-event.llm-start .card::before { background: var(--accent); }
655
- .card:hover { border-color: var(--border-strong); background: linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0.028)); transform: translateY(-1px); }
656
-
657
- .card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; cursor: pointer; user-select: none; }
658
- .card-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-weight: 600; color: var(--text); }
659
- .card-title span:last-child { font-size: 1rem; letter-spacing: -0.01em; }
660
- .card-badge { font-size: 0.69rem; font-weight: 650; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.34rem 0.62rem; border-radius: 999px; border: 1px solid transparent; }
661
- .badge-session { background: rgba(138, 183, 255, 0.14); color: var(--session); border-color: rgba(138, 183, 255, 0.24); }
662
- .badge-turn { background: rgba(224, 180, 107, 0.14); color: var(--turn); border-color: rgba(224, 180, 107, 0.22); }
663
- .badge-tool { background: rgba(134, 204, 164, 0.14); color: var(--tool); border-color: rgba(134, 204, 164, 0.22); }
664
- .badge-tool.error { background: rgba(240, 142, 142, 0.15); color: var(--error); border-color: rgba(240, 142, 142, 0.24); }
665
- .badge-llm { background: rgba(216, 132, 98, 0.14); color: #f2c2ae; border-color: rgba(216, 132, 98, 0.22); }
666
- .card-time { flex-shrink: 0; font-size: 0.82rem; color: var(--text-muted); display: flex; align-items: center; gap: 8px; padding-top: 3px; }
667
- .arrow-icon { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: transform 0.2s; }
668
- .expanded .arrow-icon { transform: rotate(90deg); }
669
- .card-body { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); display: none; }
670
- .expanded .card-body { display: block; }
671
-
672
- .detail-label { font-size: 0.76rem; font-weight: 650; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; margin: 14px 0 8px; display: block; }
673
- .code-block {
674
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
675
- font-size: 0.84rem;
676
- line-height: 1.62;
677
- background: rgba(16, 14, 12, 0.64);
678
- border: 1px solid rgba(255, 255, 255, 0.06);
679
- padding: 14px;
680
- border-radius: 16px;
681
- overflow-x: auto;
682
- margin-top: 0.35rem;
683
- color: #f1e9df;
684
- white-space: pre-wrap;
685
- }
686
-
687
- .messages-container { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
688
- .msg-row {
689
- display: flex;
690
- flex-direction: column;
691
- gap: 4px;
692
- padding: 12px 14px;
693
- background: rgba(255,255,255,0.038);
694
- border: 1px solid rgba(255,255,255,0.06);
695
- border-radius: 18px;
696
- }
697
- .msg-row.user { background: rgba(138, 183, 255, 0.08); }
698
- .msg-row.assistant { background: rgba(216, 132, 98, 0.08); }
699
- .msg-row.system { background: rgba(224, 180, 107, 0.08); }
700
- .msg-role { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
701
- .msg-text { font-size: 0.93rem; color: var(--text-soft); white-space: pre-wrap; }
702
-
703
- .usage-row {
704
- margin-top: 12px;
705
- font-size: 0.84rem;
706
- color: var(--text-muted);
707
- display: flex;
708
- flex-wrap: wrap;
709
- gap: 12px;
710
- padding: 10px 12px;
711
- border-radius: 14px;
712
- background: rgba(255,255,255,0.03);
713
- border: 1px solid rgba(255,255,255,0.05);
714
- }
715
- .usage-row strong { color: var(--text); font-weight: 620; }
716
- .duration-pill { font-size: 0.74rem; background: rgba(255,255,255,0.06); padding: 0.2rem 0.46rem; border-radius: 999px; color: var(--text-muted); border: 1px solid rgba(255,255,255,0.06); }
717
-
718
- @media (min-width: 640px) {
719
- body { padding: 28px 18px 56px; }
720
- .hero { border-radius: 28px; padding: 28px 28px 24px; }
721
- .metrics-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 18px 0 30px; }
722
- .card { border-radius: 22px; padding: 18px 18px 16px; }
723
- }
724
-
725
- @media (max-width: 720px) {
726
- .hero, .card, .metric-card { border-radius: 18px; }
727
- .card-header { flex-direction: column; }
728
- .card-time { width: 100%; justify-content: space-between; }
729
- .brand { align-items: flex-start; flex-direction: column; gap: 8px; }
730
- .brand h1 { font-size: 1.8rem; }
731
- .hero-meta { flex-direction: column; align-items: stretch; }
732
- .meta-pill { width: 100%; justify-content: space-between; }
733
- .usage-row { flex-direction: column; gap: 6px; }
734
- .code-block { font-size: 0.8rem; padding: 12px; }
735
- }
736
- </style>
737
- </head>
738
- <body>
739
- <div class="container">
740
- <header>
741
- <div class="hero">
742
- <div class="brand">
743
- <h1>notrace</h1>
744
- <span class="brand-tag">retrospective</span>
745
- </div>
746
- <p class="hero-subtitle">A local evidence view of the run: messages, tools, model calls, timing, and token cost.</p>
747
- <div class="hero-meta">
748
- <span class="meta-pill">Session <strong id="sess-id"></strong></span>
749
- <span class="meta-pill">Started <strong id="sess-time"></strong></span>
750
- </div>
751
- </div>
752
- </header>
753
-
754
- <div class="metrics-grid">
755
- <div class="metric-card">
756
- <div class="metric-label">Duration</div>
757
- <div class="metric-value" id="val-duration">-</div>
758
- </div>
759
- <div class="metric-card">
760
- <div class="metric-label">Total Tokens</div>
761
- <div class="metric-value" id="val-tokens">-</div>
762
- </div>
763
- <div class="metric-card">
764
- <div class="metric-label">LLM Calls</div>
765
- <div class="metric-value" id="val-llms">-</div>
766
- </div>
767
- <div class="metric-card">
768
- <div class="metric-label">Tool Calls</div>
769
- <div class="metric-value" id="val-tools">-</div>
770
- </div>
771
- <div class="metric-card">
772
- <div class="metric-label">Cost (USD)</div>
773
- <div class="metric-value" id="val-cost">-</div>
774
- </div>
775
- </div>
776
-
777
- <h2 class="section-title">Activity flow</h2>
778
- <div class="timeline" id="timeline-container">
779
- <!-- Injected by JS -->
780
- </div>
781
- </div>
782
-
783
- <script>
784
- const traceData = ${serializedData};
785
-
786
- function escapeHtml(value) {
787
- return String(value ?? "").replace(/[&<>'"]/g, (char) => ({
788
- "&": "&amp;",
789
- "<": "&lt;",
790
- ">": "&gt;",
791
- "'": "&#39;",
792
- '"': "&quot;"
793
- }[char]));
794
- }
795
-
796
- function safeClassName(value, fallback = "unknown") {
797
- const normalized = String(value ?? fallback).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
798
- return normalized || fallback;
799
- }
800
-
801
- function jsonText(value) {
802
- return escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2));
803
- }
804
-
805
- // Render Metrics
806
- document.getElementById("sess-id").textContent = traceData.traceId;
807
- document.getElementById("sess-time").textContent = new Date(traceData.startTime).toLocaleString();
808
- document.getElementById("val-duration").textContent = (traceData.durationMs / 1000).toFixed(2) + "s";
809
- document.getElementById("val-tokens").textContent = traceData.metrics.totalTokens.toLocaleString();
810
- document.getElementById("val-llms").textContent = traceData.metrics.llmCallCount;
811
- document.getElementById("val-tools").textContent = traceData.metrics.toolCallCount;
812
- document.getElementById("val-cost").textContent = "$" + traceData.metrics.totalCost;
813
-
814
- // Process & Render Timeline
815
- const container = document.getElementById("timeline-container");
816
- const events = traceData.events;
817
-
818
- // Group start and end of tools to display them as single cards
819
- const toolExecutions = {};
820
- const renderedEvents = [];
821
-
822
- events.forEach(e => {
823
- if (e.type === "session_start") {
824
- renderedEvents.push({
825
- type: "session",
826
- title: "Session Initialized",
827
- time: new Date(e.timestamp).toLocaleTimeString(),
828
- body: \`Reason: \${e.reason}\\nFile: \${e.sessionFile}\`
829
- });
830
- } else if (e.type === "turn_start") {
831
- renderedEvents.push({
832
- type: "turn",
833
- title: "User Turn Started",
834
- time: new Date(e.timestamp).toLocaleTimeString(),
835
- body: "Agent waiting for query execution loop."
836
- });
837
- } else if (e.type === "tool_start") {
838
- toolExecutions[e.toolCallId] = {
839
- type: "tool",
840
- title: \`Tool Call: \${e.toolName}\`,
841
- toolName: e.toolName,
842
- time: new Date(e.timestamp).toLocaleTimeString(),
843
- args: e.args,
844
- startTime: e.timestamp
845
- };
846
- } else if (e.type === "tool_end") {
847
- const start = toolExecutions[e.toolCallId];
848
- if (start) {
849
- start.result = e.result;
850
- start.isError = e.isError;
851
- start.durationMs = e.durationMs;
852
- renderedEvents.push(start);
853
- delete toolExecutions[e.toolCallId];
854
- }
855
- } else if (e.type === "llm_completion") {
856
- renderedEvents.push({
857
- type: "llm",
858
- title: \`LLM Call: \${e.model}\`,
859
- model: e.model,
860
- provider: e.provider,
861
- time: new Date(e.timestamp).toLocaleTimeString(),
862
- durationMs: e.durationMs,
863
- usage: e.usage,
864
- payload: e.inputPayload,
865
- output: e.outputContent
866
- });
867
- }
868
- });
869
-
870
- // Render cards
871
- renderedEvents.forEach((ev, index) => {
872
- const evDiv = document.createElement("div");
873
- const eventType = safeClassName(ev.type);
874
- evDiv.className = \`timeline-event \${eventType}-start \${ev.isError ? "error" : ""}\`;
875
-
876
- let cardHtml = \`
877
- <div class="timeline-dot"></div>
878
- <div class="card" id="card-\${index}">
879
- <div class="card-header" onclick="toggleCard(\${index})">
880
- <div class="card-title">
881
- <span class="card-badge badge-\${eventType} \${ev.isError ? "error" : ""}">\${escapeHtml(eventType.toUpperCase())}</span>
882
- <span>\${escapeHtml(ev.title)}</span>
883
- \${ev.durationMs ? \`<span class="duration-pill">\${escapeHtml((ev.durationMs / 1000).toFixed(2))}s</span>\` : ""}
884
- </div>
885
- <div class="card-time">
886
- <span>\${escapeHtml(ev.time)}</span>
887
- <svg class="arrow-icon" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
888
- </div>
889
- </div>
890
- <div class="card-body">
891
- \`;
892
-
893
- if (ev.type === "session" || ev.type === "turn") {
894
- cardHtml += \`<div class="code-block">\${escapeHtml(ev.body)}</div>\`;
895
- } else if (ev.type === "tool") {
896
- cardHtml += \`
897
- <span class="detail-label">Arguments</span>
898
- <div class="code-block">\${jsonText(ev.args)}</div>
899
- <span class="detail-label">Result (\${ev.isError ? "Error" : "Success"})</span>
900
- <div class="code-block">\${jsonText(ev.result)}</div>
901
- \`;
902
- } else if (ev.type === "llm") {
903
- // Render system prompt and input messages if present
904
- let messagesHtml = '<div class="messages-container">';
905
- if (ev.payload) {
906
- if (ev.payload.system_instruction) {
907
- const instr = typeof ev.payload.system_instruction === "string"
908
- ? ev.payload.system_instruction
909
- : JSON.stringify(ev.payload.system_instruction);
910
- messagesHtml += \`
911
- <div class="msg-row system">
912
- <span class="msg-role">System Instruction</span>
913
- <span class="msg-text">\${escapeHtml(instr)}</span>
914
- </div>
915
- \`;
916
- }
917
- if (ev.payload.messages && Array.isArray(ev.payload.messages)) {
918
- ev.payload.messages.forEach(m => {
919
- const contentText = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
920
- const role = safeClassName(m.role, "message");
921
- messagesHtml += \`
922
- <div class="msg-row \${role}">
923
- <span class="msg-role">\${escapeHtml(m.role)}</span>
924
- <span class="msg-text">\${escapeHtml(contentText)}</span>
925
- </div>
926
- \`;
927
- });
928
- }
929
- }
930
- messagesHtml += '</div>';
931
-
932
- cardHtml += \`
933
- <span class="detail-label">Context messages</span>
934
- \${messagesHtml}
935
- <span class="detail-label">Generated response</span>
936
- <div class="code-block">\${jsonText(ev.output)}</div>
937
- \${ev.usage ? \`
938
- <div class="usage-row">
939
- <span>Input tokens <strong>\${escapeHtml(ev.usage.input ?? 0)}</strong></span>
940
- <span>Output tokens <strong>\${escapeHtml(ev.usage.output ?? 0)}</strong></span>
941
- <span>Total tokens <strong>\${escapeHtml(ev.usage.totalTokens ?? 0)}</strong></span>
942
- <span>Cost <strong>\$\${escapeHtml(ev.usage.cost?.total?.toFixed?.(5) || "0.00")}</strong></span>
943
- </div>
944
- \` : ""}
945
- \`;
946
- }
947
-
948
- cardHtml += \`
949
- </div>
950
- </div>
951
- \`;
952
-
953
- evDiv.innerHTML = cardHtml;
954
- container.appendChild(evDiv);
955
- });
956
-
957
- // Expand/Collapse controller
958
- function toggleCard(index) {
959
- const card = document.getElementById(\`card-\${index}\`);
960
- card?.classList.toggle("expanded");
961
- }
962
- </script>
963
- </body>
964
- </html>`;
965
- }