@raquezha/notrace 0.0.4 → 0.0.6

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 (95) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +123 -23
  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/dist/notrace/adapters.d.ts +32 -0
  7. package/dist/notrace/adapters.js +83 -0
  8. package/dist/notrace/index.d.ts +2 -0
  9. package/dist/notrace/index.js +295 -0
  10. package/dist/notrace/renderer.d.ts +4 -0
  11. package/dist/notrace/renderer.js +605 -0
  12. package/dist/notrace/types.d.ts +93 -0
  13. package/dist/notrace/types.js +1 -0
  14. package/extensions/notrace/adapters.ts +88 -0
  15. package/extensions/notrace/index.ts +359 -0
  16. package/extensions/notrace/renderer.ts +616 -0
  17. package/extensions/notrace/types.ts +108 -0
  18. package/package.json +4 -3
  19. package/templates/README.md +24 -0
  20. package/templates/dashboard.sample.html +361 -0
  21. package/templates/dashboard.sample.json +113 -0
  22. package/templates/notrace-logo.preview.png +0 -0
  23. package/templates/render-samples.mjs +44 -0
  24. package/templates/session.sample.html +431 -0
  25. package/templates/session.sample.json +127 -0
  26. package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.html +313 -0
  27. package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.json +129 -0
  28. package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.html +313 -0
  29. package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.json +129 -0
  30. package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html +426 -0
  31. package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.json +134 -0
  32. package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html +425 -0
  33. package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.json +133 -0
  34. package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html +426 -0
  35. package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.json +134 -0
  36. package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.html +423 -0
  37. package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.json +130 -0
  38. package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.html +423 -0
  39. package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.json +130 -0
  40. package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.html +423 -0
  41. package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.json +130 -0
  42. package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.html +423 -0
  43. package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.json +130 -0
  44. package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.html +423 -0
  45. package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.json +130 -0
  46. package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.html +423 -0
  47. package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.json +130 -0
  48. package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.html +423 -0
  49. package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.json +130 -0
  50. package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.html +423 -0
  51. package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.json +130 -0
  52. package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.html +423 -0
  53. package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.json +130 -0
  54. package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.html +423 -0
  55. package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.json +130 -0
  56. package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.html +423 -0
  57. package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.json +130 -0
  58. package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.html +423 -0
  59. package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.json +130 -0
  60. package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.html +423 -0
  61. package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.json +130 -0
  62. package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.html +423 -0
  63. package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.json +130 -0
  64. package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.html +423 -0
  65. package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.json +130 -0
  66. package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.html +423 -0
  67. package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.json +130 -0
  68. package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.html +423 -0
  69. package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.json +130 -0
  70. package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.html +423 -0
  71. package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.json +130 -0
  72. package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.html +423 -0
  73. package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.json +130 -0
  74. package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.html +423 -0
  75. package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.json +130 -0
  76. package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.html +423 -0
  77. package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.json +130 -0
  78. package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.html +423 -0
  79. package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.json +130 -0
  80. package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.html +423 -0
  81. package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.json +130 -0
  82. package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.html +423 -0
  83. package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.json +130 -0
  84. package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.html +423 -0
  85. package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.json +130 -0
  86. package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.html +423 -0
  87. package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.json +130 -0
  88. package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.html +423 -0
  89. package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.json +130 -0
  90. package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.html +313 -0
  91. package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.json +129 -0
  92. package/tsconfig.json +1 -1
  93. package/dist/notrace.d.ts +0 -9
  94. package/dist/notrace.js +0 -785
  95. package/extensions/notrace.ts +0 -802
@@ -1,802 +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
- return SENSITIVE_KEY_RE.test(key.replace(/[^a-z0-9]/gi, ""));
24
- }
25
-
26
- function redactString(value: string): string {
27
- const redacted = value.replace(SENSITIVE_VALUE_RE, REDACTED);
28
- if (redacted.length <= MAX_STRING_LENGTH) return redacted;
29
- return `${redacted.slice(0, MAX_STRING_LENGTH)}\n…[truncated ${redacted.length - MAX_STRING_LENGTH} chars by notrace]`;
30
- }
31
-
32
- function sanitizeTraceValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
33
- if (getCaptureMode() === "full") return value;
34
- if (value == null || typeof value === "number" || typeof value === "boolean") return value;
35
- if (typeof value === "string") return redactString(value);
36
- if (typeof value === "bigint") return value.toString();
37
- if (typeof value === "function" || typeof value === "symbol") return `[${typeof value}]`;
38
- if (depth >= MAX_DEPTH) return "[Max depth reached by notrace]";
39
- if (typeof value !== "object") return String(value);
40
- if (seen.has(value)) return "[Circular]";
41
- seen.add(value);
42
-
43
- if (Array.isArray(value)) {
44
- const items = value.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitizeTraceValue(item, depth + 1, seen));
45
- if (value.length > MAX_ARRAY_ITEMS) items.push(`…[truncated ${value.length - MAX_ARRAY_ITEMS} items by notrace]`);
46
- return items;
47
- }
48
-
49
- const output: Record<string, unknown> = {};
50
- const entries = Object.entries(value as Record<string, unknown>);
51
- for (const [key, item] of entries.slice(0, MAX_OBJECT_KEYS)) {
52
- output[key] = isSensitiveKey(key) ? REDACTED : sanitizeTraceValue(item, depth + 1, seen);
53
- }
54
- if (entries.length > MAX_OBJECT_KEYS) output.__notrace_truncated__ = `${entries.length - MAX_OBJECT_KEYS} keys`;
55
- return output;
56
- }
57
-
58
- function safeResolveUnder(baseDir: string, candidate: string): string | null {
59
- const base = path.resolve(baseDir);
60
- const resolved = path.resolve(base, candidate);
61
- const relative = path.relative(base, resolved);
62
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ? resolved : null;
63
- }
64
-
65
- function escapeHtml(value: unknown): string {
66
- return String(value).replace(/[&<>'"]/g, (char) => {
67
- switch (char) {
68
- case "&": return "&amp;";
69
- case "<": return "&lt;";
70
- case ">": return "&gt;";
71
- case "'": return "&#39;";
72
- case '"': return "&quot;";
73
- default: return char;
74
- }
75
- });
76
- }
77
-
78
- /**
79
- * html-observability extension
80
- *
81
- * Captures execution traces from the pi coding agent sessions and writes
82
- * a self-contained, interactive, beautiful HTML report to the active task's
83
- * workspace at the end of the session.
84
- */
85
- export default function (pi: ExtensionAPI) {
86
- const events: any[] = [];
87
- const sessionStartTime = Date.now();
88
- let traceId = "";
89
- let activeLlmPayload: any = null;
90
- let llmStartTime = 0;
91
- const activeToolTimes: Record<string, number> = {};
92
-
93
- // Helper to extract active task path from .workflow/active_task.json.
94
- // Reports are constrained to cwd/.workflow to avoid task metadata causing writes elsewhere.
95
- function getActiveTaskDir(cwd: string): string {
96
- const workflowDir = path.resolve(cwd, ".workflow");
97
- try {
98
- const activeTaskJsonPath = path.join(workflowDir, "active_task.json");
99
- if (existsSync(activeTaskJsonPath)) {
100
- const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
101
- const candidate = typeof content.taskPath === "string"
102
- ? safeResolveUnder(cwd, content.taskPath)
103
- : typeof content.active_task === "string"
104
- ? safeResolveUnder(workflowDir, path.join("tasks", content.active_task))
105
- : null;
106
-
107
- if (candidate && safeResolveUnder(workflowDir, path.relative(workflowDir, candidate))) {
108
- return candidate;
109
- }
110
- }
111
- } catch {
112
- // fallback
113
- }
114
- if (!existsSync(workflowDir)) {
115
- try { mkdirSync(workflowDir, { recursive: true, mode: 0o700 }); } catch {}
116
- }
117
- return workflowDir;
118
- }
119
-
120
- // 1. Session start
121
- pi.on("session_start", async (event, ctx) => {
122
- traceId = ctx.sessionManager.getSessionId() || `session-${Date.now()}`;
123
- events.push({
124
- type: "session_start",
125
- timestamp: Date.now(),
126
- reason: event.reason,
127
- sessionFile: ctx.sessionManager.getSessionFile() || ""
128
- });
129
- });
130
-
131
- // 2. Turn start
132
- pi.on("turn_start", async (event, ctx) => {
133
- events.push({
134
- type: "turn_start",
135
- timestamp: Date.now()
136
- });
137
- });
138
-
139
- // 3. Turn end
140
- pi.on("turn_end", async (event, ctx) => {
141
- events.push({
142
- type: "turn_end",
143
- timestamp: Date.now()
144
- });
145
- });
146
-
147
- // 4. Tool start
148
- pi.on("tool_execution_start", async (event, ctx) => {
149
- const { toolCallId, toolName, args } = event;
150
- activeToolTimes[toolCallId] = Date.now();
151
- events.push({
152
- type: "tool_start",
153
- toolCallId,
154
- toolName,
155
- args: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(args),
156
- timestamp: Date.now()
157
- });
158
- });
159
-
160
- // 5. Tool end
161
- pi.on("tool_execution_end", async (event, ctx) => {
162
- const { toolCallId, toolName, result, isError } = event;
163
- const startTime = activeToolTimes[toolCallId] || Date.now();
164
- const durationMs = Date.now() - startTime;
165
-
166
- events.push({
167
- type: "tool_end",
168
- toolCallId,
169
- toolName,
170
- result: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(result),
171
- isError,
172
- durationMs,
173
- timestamp: Date.now()
174
- });
175
- delete activeToolTimes[toolCallId];
176
- });
177
-
178
- // 6. LLM call start (capture payload)
179
- pi.on("before_provider_request", async (event, ctx) => {
180
- activeLlmPayload = getCaptureMode() === "metadata" ? null : sanitizeTraceValue(event.payload);
181
- llmStartTime = Date.now();
182
- });
183
-
184
- // 7. LLM call end (capture generation / usage)
185
- pi.on("message_end", async (event, ctx) => {
186
- const { message } = event;
187
- if (message.role !== "assistant") return;
188
-
189
- const durationMs = llmStartTime > 0 ? Date.now() - llmStartTime : 0;
190
-
191
- events.push({
192
- type: "llm_completion",
193
- model: message.model || "unknown",
194
- provider: message.provider || "unknown",
195
- inputPayload: activeLlmPayload,
196
- outputContent: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(message.content),
197
- usage: sanitizeTraceValue(message.usage),
198
- durationMs,
199
- timestamp: Date.now()
200
- });
201
-
202
- activeLlmPayload = null;
203
- llmStartTime = 0;
204
- });
205
-
206
- // 8. Shutdown: Compile the HTML report and write to disk
207
- pi.on("session_shutdown", async (event, ctx) => {
208
- const sessionEndTime = Date.now();
209
- const totalDurationMs = sessionEndTime - sessionStartTime;
210
-
211
- const taskDir = getActiveTaskDir(ctx.cwd);
212
- const reportPath = path.join(taskDir, "notrace.html");
213
-
214
- // Gather statistics
215
- let totalTokens = 0;
216
- let inputTokens = 0;
217
- let outputTokens = 0;
218
- let totalCost = 0;
219
- let toolCallCount = 0;
220
- let llmCallCount = 0;
221
-
222
- events.forEach((e) => {
223
- if (e.type === "llm_completion") {
224
- llmCallCount++;
225
- if (e.usage) {
226
- inputTokens += e.usage.input || 0;
227
- outputTokens += e.usage.output || 0;
228
- totalTokens += e.usage.totalTokens || 0;
229
- totalCost += e.usage.cost?.total || 0;
230
- }
231
- } else if (e.type === "tool_start") {
232
- toolCallCount++;
233
- }
234
- });
235
-
236
- const htmlContent = generateHtmlReport({
237
- traceId,
238
- projectName: process.env.PHOENIX_PROJECT_NAME || "pi-coding-agent",
239
- startTime: new Date(sessionStartTime).toISOString(),
240
- endTime: new Date(sessionEndTime).toISOString(),
241
- durationMs: totalDurationMs,
242
- metrics: {
243
- totalTokens,
244
- inputTokens,
245
- outputTokens,
246
- totalCost: totalCost.toFixed(5),
247
- toolCallCount,
248
- llmCallCount
249
- },
250
- events
251
- });
252
-
253
- try {
254
- mkdirSync(taskDir, { recursive: true, mode: 0o700 });
255
- writeFileSync(reportPath, htmlContent, { encoding: "utf-8", mode: 0o600 });
256
- // Output a nice clickable file:// link to the console for the user
257
- console.log(`\n📊 [notrace] Observability report generated:`);
258
- console.log(`👉 \x1b[36mfile://${reportPath}\x1b[0m\n`);
259
- } catch (err: any) {
260
- console.warn(`[notrace] Failed to write HTML report: ${err?.message || err}`);
261
- }
262
- });
263
- }
264
-
265
- function safeJsonForScript(value: any): string {
266
- return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (char) => {
267
- switch (char) {
268
- case "<": return "\\u003c";
269
- case ">": return "\\u003e";
270
- case "&": return "\\u0026";
271
- case "\u2028": return "\\u2028";
272
- case "\u2029": return "\\u2029";
273
- default: return char;
274
- }
275
- });
276
- }
277
-
278
- // Returns a self-contained premium HTML template incorporating the design tokens
279
- function generateHtmlReport(data: any): string {
280
- const serializedData = safeJsonForScript(data);
281
- const escapedTraceId = escapeHtml(data.traceId);
282
-
283
- return `<!DOCTYPE html>
284
- <html lang="en">
285
- <head>
286
- <meta charset="UTF-8">
287
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
288
- <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'">
289
- <meta name="referrer" content="no-referrer">
290
- <title>notrace - ${escapedTraceId}</title>
291
- <style>
292
- :root {
293
- --bg: #0b0b0e;
294
- --bg-panel: rgba(22, 22, 28, 0.45);
295
- --bg-card: rgba(30, 30, 38, 0.6);
296
- --border: rgba(255, 255, 255, 0.08);
297
- --border-hover: rgba(255, 255, 255, 0.15);
298
- --text: #e2e2e9;
299
- --text-muted: #9f9fa9;
300
- --accent: #8b5cf6;
301
- --accent-gradient: linear-gradient(135deg, #a78bfa, #8b5cf6);
302
- --success: #10b981;
303
- --error: #ef4444;
304
- --warning: #f59e0b;
305
- --info: #3b82f6;
306
- }
307
-
308
- * {
309
- box-sizing: border-box;
310
- margin: 0;
311
- padding: 0;
312
- }
313
-
314
- body {
315
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
316
- background-color: var(--bg);
317
- color: var(--text);
318
- line-height: 1.5;
319
- padding: 2rem;
320
- min-height: 100vh;
321
- background-image:
322
- radial-gradient(circle at 10% 20%, rgba(139, 92, 246, 0.08) 0%, transparent 40%),
323
- radial-gradient(circle at 90% 80%, rgba(59, 130, 246, 0.06) 0%, transparent 40%);
324
- background-attachment: fixed;
325
- }
326
-
327
- .container {
328
- max-width: 1100px;
329
- margin: 0 auto;
330
- }
331
-
332
- header {
333
- display: flex;
334
- justify-content: space-between;
335
- align-items: center;
336
- margin-bottom: 2rem;
337
- padding-bottom: 1.5rem;
338
- border-bottom: 1px solid var(--border);
339
- }
340
-
341
- .brand {
342
- display: flex;
343
- align-items: center;
344
- gap: 0.75rem;
345
- }
346
-
347
- .brand h1 {
348
- font-size: 1.75rem;
349
- font-weight: 700;
350
- background: var(--accent-gradient);
351
- -webkit-background-clip: text;
352
- -webkit-text-fill-color: transparent;
353
- }
354
-
355
- .brand-tag {
356
- font-size: 0.75rem;
357
- text-transform: uppercase;
358
- letter-spacing: 0.05em;
359
- background: rgba(139, 92, 246, 0.15);
360
- color: #c084fc;
361
- padding: 0.2rem 0.5rem;
362
- border-radius: 4px;
363
- border: 1px solid rgba(167, 139, 250, 0.3);
364
- }
365
-
366
- .meta-time {
367
- font-size: 0.875rem;
368
- color: var(--text-muted);
369
- text-align: right;
370
- }
371
-
372
- .metrics-grid {
373
- display: grid;
374
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
375
- gap: 1rem;
376
- margin-bottom: 2.5rem;
377
- }
378
-
379
- .metric-card {
380
- background: var(--bg-panel);
381
- border: 1px solid var(--border);
382
- padding: 1.25rem;
383
- border-radius: 12px;
384
- backdrop-filter: blur(12px);
385
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
386
- }
387
-
388
- .metric-card:hover {
389
- border-color: var(--border-hover);
390
- transform: translateY(-2px);
391
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
392
- }
393
-
394
- .metric-label {
395
- font-size: 0.75rem;
396
- text-transform: uppercase;
397
- letter-spacing: 0.05em;
398
- color: var(--text-muted);
399
- margin-bottom: 0.5rem;
400
- }
401
-
402
- .metric-value {
403
- font-size: 1.5rem;
404
- font-weight: 600;
405
- color: #fff;
406
- }
407
-
408
- .timeline {
409
- position: relative;
410
- padding-left: 2rem;
411
- border-left: 2px solid var(--border);
412
- margin-left: 1rem;
413
- }
414
-
415
- .timeline-event {
416
- position: relative;
417
- margin-bottom: 2rem;
418
- animation: slideIn 0.4s ease-out;
419
- }
420
-
421
- @keyframes slideIn {
422
- from { opacity: 0; transform: translateX(-10px); }
423
- to { opacity: 1; transform: translateX(0); }
424
- }
425
-
426
- .timeline-dot {
427
- position: absolute;
428
- left: calc(-2rem - 6px);
429
- top: 1.25rem;
430
- width: 10px;
431
- height: 10px;
432
- border-radius: 50%;
433
- background: var(--text-muted);
434
- box-shadow: 0 0 0 4px var(--bg);
435
- transition: all 0.3s ease;
436
- }
437
-
438
- /* Event color mappings */
439
- .timeline-event.session-start .timeline-dot { background: var(--info); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--info); }
440
- .timeline-event.turn-start .timeline-dot { background: var(--accent); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--accent); }
441
- .timeline-event.tool-call .timeline-dot { background: var(--warning); box-shadow: 0 0 0 4px var(--bg); }
442
- .timeline-event.tool-call.error .timeline-dot { background: var(--error); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--error); }
443
- .timeline-event.llm-call .timeline-dot { background: var(--success); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--success); }
444
-
445
- .card {
446
- background: var(--bg-panel);
447
- border: 1px solid var(--border);
448
- border-radius: 12px;
449
- padding: 1.25rem;
450
- backdrop-filter: blur(12px);
451
- transition: border-color 0.2s;
452
- }
453
-
454
- .card:hover {
455
- border-color: var(--border-hover);
456
- }
457
-
458
- .card-header {
459
- display: flex;
460
- justify-content: space-between;
461
- align-items: center;
462
- cursor: pointer;
463
- user-select: none;
464
- }
465
-
466
- .card-title {
467
- display: flex;
468
- align-items: center;
469
- gap: 0.75rem;
470
- font-weight: 600;
471
- }
472
-
473
- .card-badge {
474
- font-size: 0.75rem;
475
- font-weight: 500;
476
- padding: 0.15rem 0.5rem;
477
- border-radius: 4px;
478
- }
479
-
480
- .badge-session { background: rgba(59, 130, 246, 0.15); color: #60a5fa; }
481
- .badge-turn { background: rgba(139, 92, 246, 0.15); color: #c084fc; }
482
- .badge-tool { background: rgba(245, 158, 11, 0.15); color: #fbbf24; }
483
- .badge-tool.error { background: rgba(239, 68, 68, 0.15); color: #f87171; }
484
- .badge-llm { background: rgba(16, 185, 129, 0.15); color: #34d399; }
485
-
486
- .card-time {
487
- font-size: 0.8rem;
488
- color: var(--text-muted);
489
- display: flex;
490
- align-items: center;
491
- gap: 0.5rem;
492
- }
493
-
494
- .arrow-icon {
495
- width: 16px;
496
- height: 16px;
497
- fill: none;
498
- stroke: currentColor;
499
- stroke-width: 2;
500
- stroke-linecap: round;
501
- stroke-linejoin: round;
502
- transition: transform 0.2s;
503
- }
504
-
505
- .expanded .arrow-icon {
506
- transform: rotate(90deg);
507
- }
508
-
509
- .card-body {
510
- margin-top: 1rem;
511
- padding-top: 1rem;
512
- border-top: 1px solid var(--border);
513
- display: none;
514
- }
515
-
516
- .expanded .card-body {
517
- display: block;
518
- }
519
-
520
- .code-block {
521
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
522
- font-size: 0.875rem;
523
- background: rgba(0, 0, 0, 0.35);
524
- border: 1px solid var(--border);
525
- padding: 1rem;
526
- border-radius: 8px;
527
- overflow-x: auto;
528
- margin-top: 0.5rem;
529
- color: #e2e2e9;
530
- white-space: pre-wrap;
531
- }
532
-
533
- .messages-container {
534
- display: flex;
535
- flex-direction: column;
536
- gap: 0.75rem;
537
- }
538
-
539
- .msg-row {
540
- display: flex;
541
- flex-direction: column;
542
- gap: 0.25rem;
543
- padding: 0.75rem;
544
- background: rgba(255, 255, 255, 0.02);
545
- border-radius: 6px;
546
- border-left: 3px solid var(--border);
547
- }
548
-
549
- .msg-row.user { border-left-color: var(--info); }
550
- .msg-row.assistant { border-left-color: var(--success); }
551
- .msg-row.system { border-left-color: var(--accent); }
552
-
553
- .msg-role {
554
- font-size: 0.75rem;
555
- font-weight: 600;
556
- text-transform: uppercase;
557
- letter-spacing: 0.05em;
558
- }
559
-
560
- .msg-row.user .msg-role { color: #60a5fa; }
561
- .msg-row.assistant .msg-role { color: #34d399; }
562
- .msg-row.system .msg-role { color: #c084fc; }
563
-
564
- .msg-text {
565
- font-size: 0.9rem;
566
- white-space: pre-wrap;
567
- }
568
-
569
- .duration-pill {
570
- font-size: 0.75rem;
571
- background: rgba(255, 255, 255, 0.06);
572
- padding: 0.1rem 0.4rem;
573
- border-radius: 4px;
574
- color: var(--text-muted);
575
- }
576
- </style>
577
- </head>
578
- <body>
579
- <div class="container">
580
- <header>
581
- <div class="brand">
582
- <h1>notrace</h1>
583
- <span class="brand-tag">Local Observability</span>
584
- </div>
585
- <div class="meta-time">
586
- <div>Session: <span id="sess-id"></span></div>
587
- <div id="sess-time" style="font-size: 0.8rem;"></div>
588
- </div>
589
- </header>
590
-
591
- <div class="metrics-grid">
592
- <div class="metric-card">
593
- <div class="metric-label">Duration</div>
594
- <div class="metric-value" id="val-duration">-</div>
595
- </div>
596
- <div class="metric-card">
597
- <div class="metric-label">Total Tokens</div>
598
- <div class="metric-value" id="val-tokens">-</div>
599
- </div>
600
- <div class="metric-card">
601
- <div class="metric-label">LLM Calls</div>
602
- <div class="metric-value" id="val-llms">-</div>
603
- </div>
604
- <div class="metric-card">
605
- <div class="metric-label">Tool Calls</div>
606
- <div class="metric-value" id="val-tools">-</div>
607
- </div>
608
- <div class="metric-card">
609
- <div class="metric-label">Cost (USD)</div>
610
- <div class="metric-value" id="val-cost">-</div>
611
- </div>
612
- </div>
613
-
614
- <h2 style="margin-bottom: 1.5rem; font-weight: 600; font-size: 1.25rem; color: #fff;">Activity Flow</h2>
615
- <div class="timeline" id="timeline-container">
616
- <!-- Injected by JS -->
617
- </div>
618
- </div>
619
-
620
- <script>
621
- const traceData = ${serializedData};
622
-
623
- function escapeHtml(value) {
624
- return String(value ?? "").replace(/[&<>'"]/g, (char) => ({
625
- "&": "&amp;",
626
- "<": "&lt;",
627
- ">": "&gt;",
628
- "'": "&#39;",
629
- '"': "&quot;"
630
- }[char]));
631
- }
632
-
633
- function safeClassName(value, fallback = "unknown") {
634
- const normalized = String(value ?? fallback).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
635
- return normalized || fallback;
636
- }
637
-
638
- function jsonText(value) {
639
- return escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2));
640
- }
641
-
642
- // Render Metrics
643
- document.getElementById("sess-id").textContent = traceData.traceId;
644
- document.getElementById("sess-time").textContent = new Date(traceData.startTime).toLocaleString();
645
- document.getElementById("val-duration").textContent = (traceData.durationMs / 1000).toFixed(2) + "s";
646
- document.getElementById("val-tokens").textContent = traceData.metrics.totalTokens.toLocaleString();
647
- document.getElementById("val-llms").textContent = traceData.metrics.llmCallCount;
648
- document.getElementById("val-tools").textContent = traceData.metrics.toolCallCount;
649
- document.getElementById("val-cost").textContent = "$" + traceData.metrics.totalCost;
650
-
651
- // Process & Render Timeline
652
- const container = document.getElementById("timeline-container");
653
- const events = traceData.events;
654
-
655
- // Group start and end of tools to display them as single cards
656
- const toolExecutions = {};
657
- const renderedEvents = [];
658
-
659
- events.forEach(e => {
660
- if (e.type === "session_start") {
661
- renderedEvents.push({
662
- type: "session",
663
- title: "Session Initialized",
664
- time: new Date(e.timestamp).toLocaleTimeString(),
665
- body: \`Reason: \${e.reason}\\nFile: \${e.sessionFile}\`
666
- });
667
- } else if (e.type === "turn_start") {
668
- renderedEvents.push({
669
- type: "turn",
670
- title: "User Turn Started",
671
- time: new Date(e.timestamp).toLocaleTimeString(),
672
- body: "Agent waiting for query execution loop."
673
- });
674
- } else if (e.type === "tool_start") {
675
- toolExecutions[e.toolCallId] = {
676
- type: "tool",
677
- title: \`Tool Call: \${e.toolName}\`,
678
- toolName: e.toolName,
679
- time: new Date(e.timestamp).toLocaleTimeString(),
680
- args: e.args,
681
- startTime: e.timestamp
682
- };
683
- } else if (e.type === "tool_end") {
684
- const start = toolExecutions[e.toolCallId];
685
- if (start) {
686
- start.result = e.result;
687
- start.isError = e.isError;
688
- start.durationMs = e.durationMs;
689
- renderedEvents.push(start);
690
- delete toolExecutions[e.toolCallId];
691
- }
692
- } else if (e.type === "llm_completion") {
693
- renderedEvents.push({
694
- type: "llm",
695
- title: \`LLM Call: \${e.model}\`,
696
- model: e.model,
697
- provider: e.provider,
698
- time: new Date(e.timestamp).toLocaleTimeString(),
699
- durationMs: e.durationMs,
700
- usage: e.usage,
701
- payload: e.inputPayload,
702
- output: e.outputContent
703
- });
704
- }
705
- });
706
-
707
- // Render cards
708
- renderedEvents.forEach((ev, index) => {
709
- const evDiv = document.createElement("div");
710
- const eventType = safeClassName(ev.type);
711
- evDiv.className = \`timeline-event \${eventType}-start \${ev.isError ? "error" : ""}\`;
712
-
713
- let cardHtml = \`
714
- <div class="timeline-dot"></div>
715
- <div class="card" id="card-\${index}">
716
- <div class="card-header" onclick="toggleCard(\${index})">
717
- <div class="card-title">
718
- <span class="card-badge badge-\${eventType} \${ev.isError ? "error" : ""}">\${escapeHtml(eventType.toUpperCase())}</span>
719
- <span>\${escapeHtml(ev.title)}</span>
720
- \${ev.durationMs ? \`<span class="duration-pill">\${escapeHtml((ev.durationMs / 1000).toFixed(2))}s</span>\` : ""}
721
- </div>
722
- <div class="card-time">
723
- <span>\${escapeHtml(ev.time)}</span>
724
- <svg class="arrow-icon" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
725
- </div>
726
- </div>
727
- <div class="card-body">
728
- \`;
729
-
730
- if (ev.type === "session" || ev.type === "turn") {
731
- cardHtml += \`<div class="code-block">\${escapeHtml(ev.body)}</div>\`;
732
- } else if (ev.type === "tool") {
733
- cardHtml += \`
734
- <strong>Arguments:</strong>
735
- <div class="code-block">\${jsonText(ev.args)}</div>
736
- <strong style="margin-top: 1rem; display: block;">Result (\${ev.isError ? "Error" : "Success"}):</strong>
737
- <div class="code-block">\${jsonText(ev.result)}</div>
738
- \`;
739
- } else if (ev.type === "llm") {
740
- // Render system prompt and input messages if present
741
- let messagesHtml = '<div class="messages-container">';
742
- if (ev.payload) {
743
- if (ev.payload.system_instruction) {
744
- const instr = typeof ev.payload.system_instruction === "string"
745
- ? ev.payload.system_instruction
746
- : JSON.stringify(ev.payload.system_instruction);
747
- messagesHtml += \`
748
- <div class="msg-row system">
749
- <span class="msg-role">System Instruction</span>
750
- <span class="msg-text">\${escapeHtml(instr)}</span>
751
- </div>
752
- \`;
753
- }
754
- if (ev.payload.messages && Array.isArray(ev.payload.messages)) {
755
- ev.payload.messages.forEach(m => {
756
- const contentText = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
757
- const role = safeClassName(m.role, "message");
758
- messagesHtml += \`
759
- <div class="msg-row \${role}">
760
- <span class="msg-role">\${escapeHtml(m.role)}</span>
761
- <span class="msg-text">\${escapeHtml(contentText)}</span>
762
- </div>
763
- \`;
764
- });
765
- }
766
- }
767
- messagesHtml += '</div>';
768
-
769
- cardHtml += \`
770
- <strong>Context Messages:</strong>
771
- \${messagesHtml}
772
- <strong style="margin-top: 1.25rem; display: block;">Generated Response:</strong>
773
- <div class="code-block">\${jsonText(ev.output)}</div>
774
- \${ev.usage ? \`
775
- <div style="margin-top: 1rem; font-size: 0.85rem; color: var(--text-muted); display: flex; gap: 1.5rem;">
776
- <span>Input Tokens: <strong>\${escapeHtml(ev.usage.input ?? 0)}</strong></span>
777
- <span>Output Tokens: <strong>\${escapeHtml(ev.usage.output ?? 0)}</strong></span>
778
- <span>Total Tokens: <strong>\${escapeHtml(ev.usage.totalTokens ?? 0)}</strong></span>
779
- <span>Cost: <strong>\$\${escapeHtml(ev.usage.cost?.total?.toFixed?.(5) || "0.00")}</strong></span>
780
- </div>
781
- \` : ""}
782
- \`;
783
- }
784
-
785
- cardHtml += \`
786
- </div>
787
- </div>
788
- \`;
789
-
790
- evDiv.innerHTML = cardHtml;
791
- container.appendChild(evDiv);
792
- });
793
-
794
- // Expand/Collapse controller
795
- function toggleCard(index) {
796
- const card = document.getElementById(\`card-\${index}\`);
797
- card?.classList.toggle("expanded");
798
- }
799
- </script>
800
- </body>
801
- </html>`;
802
- }