@raquezha/notrace 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/notrace/index.js +2 -1
- package/dist/notrace/report-app/__tests__/analytics.test.d.ts +1 -0
- package/dist/notrace/report-app/__tests__/analytics.test.js +35 -0
- package/dist/notrace/report-app/__tests__/card.test.d.ts +1 -0
- package/dist/notrace/report-app/__tests__/card.test.js +26 -0
- package/dist/notrace/report-app/__tests__/event.test.d.ts +1 -0
- package/dist/notrace/report-app/__tests__/event.test.js +20 -0
- package/dist/notrace/report-app/__tests__/format.test.d.ts +1 -0
- package/dist/notrace/report-app/__tests__/format.test.js +41 -0
- package/dist/notrace/report-app/__tests__/report.test.d.ts +1 -0
- package/dist/notrace/report-app/__tests__/report.test.js +31 -0
- package/dist/notrace/report-app/analytics.d.ts +3 -0
- package/dist/notrace/report-app/analytics.js +78 -0
- package/dist/notrace/report-app/client.d.ts +2 -0
- package/dist/notrace/report-app/client.js +105 -0
- package/dist/notrace/report-app/components/card.d.ts +4 -0
- package/dist/notrace/report-app/components/card.js +36 -0
- package/dist/notrace/report-app/components/dashboard.d.ts +1 -0
- package/dist/notrace/report-app/components/dashboard.js +16 -0
- package/dist/notrace/report-app/components/event.d.ts +5 -0
- package/dist/notrace/report-app/components/event.js +42 -0
- package/dist/notrace/report-app/components/message.d.ts +2 -0
- package/dist/notrace/report-app/components/message.js +43 -0
- package/dist/notrace/report-app/dashboard-report.d.ts +1 -0
- package/dist/notrace/report-app/dashboard-report.js +6 -0
- package/dist/notrace/report-app/escape.d.ts +1 -0
- package/dist/notrace/report-app/escape.js +10 -0
- package/dist/notrace/report-app/format.d.ts +13 -0
- package/dist/notrace/report-app/format.js +102 -0
- package/dist/notrace/report-app/report.d.ts +1 -0
- package/dist/notrace/report-app/report.js +29 -0
- package/dist/notrace/report-app/shell.d.ts +5 -0
- package/dist/notrace/report-app/shell.js +19 -0
- package/dist/notrace/report-app/styles.d.ts +1 -0
- package/dist/notrace/report-app/styles.js +431 -0
- package/dist/notrace/report-app/types.d.ts +28 -0
- package/dist/notrace/report-app/types.js +1 -0
- package/extensions/notrace/index.ts +2 -1
- package/extensions/notrace/report-app/__tests__/analytics.test.ts +41 -0
- package/extensions/notrace/report-app/__tests__/card.test.ts +29 -0
- package/extensions/notrace/report-app/__tests__/event.test.ts +23 -0
- package/extensions/notrace/report-app/__tests__/format.test.ts +46 -0
- package/extensions/notrace/report-app/__tests__/report.test.ts +33 -0
- package/extensions/notrace/report-app/analytics.ts +79 -0
- package/extensions/notrace/report-app/client.ts +106 -0
- package/extensions/notrace/report-app/components/card.ts +38 -0
- package/extensions/notrace/report-app/components/dashboard.ts +17 -0
- package/extensions/notrace/report-app/components/event.ts +39 -0
- package/extensions/notrace/report-app/components/message.ts +39 -0
- package/extensions/notrace/report-app/dashboard-report.ts +7 -0
- package/extensions/notrace/report-app/escape.ts +10 -0
- package/extensions/notrace/report-app/format.ts +107 -0
- package/extensions/notrace/report-app/report.ts +33 -0
- package/extensions/notrace/report-app/shell.ts +24 -0
- package/extensions/notrace/report-app/styles.ts +431 -0
- package/extensions/notrace/report-app/types.ts +35 -0
- package/package.json +1 -1
- package/templates/dashboard.sample.html +103 -63
- package/templates/dashboard.sample.json +73 -10
- package/templates/render-samples.mjs +119 -1
- package/templates/session.sample.html +125 -168
- package/templates/session.sample.json +66 -7
- package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html +125 -163
- package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.json +50 -0
- package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html +125 -162
- package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.json +50 -0
- package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html +125 -163
- package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.json +50 -0
- package/templates/sessions/019ed2ee-massive/notrace.html +498 -0
- package/templates/sessions/019ed2ee-massive/notrace.json +14660 -0
- package/tsconfig.json +1 -1
- package/dist/notrace/renderer.d.ts +0 -4
- package/dist/notrace/renderer.js +0 -800
- package/extensions/notrace/renderer.ts +0 -810
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @raquezha/notrace
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 382b624: Add model usage breakdowns, switch insights, lazy timeline rendering, and refreshed report samples.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 04d574e: Refactor notrace report rendering into modular report-app files and add focused regression coverage without changing package dependencies or entrypoints.
|
|
12
|
+
|
|
3
13
|
## 0.1.2
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/dist/notrace/index.js
CHANGED
|
@@ -3,7 +3,8 @@ import * as path from "node:path";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
import { getActiveAdapter } from "./adapters.js";
|
|
6
|
-
import { generateHtmlReport
|
|
6
|
+
import { generateHtmlReport } from "./report-app/report.js";
|
|
7
|
+
import { generateDashboardHtml } from "./report-app/dashboard-report.js";
|
|
7
8
|
const REDACTED = "[REDACTED by notrace]";
|
|
8
9
|
const SENSITIVE_VALUE_RE = /(bearer\s+[a-z0-9._~+/=-]{12,}|sk-[a-z0-9_-]{16,}|gh[pousr]_[a-z0-9_]{16,}|AKIA[0-9A-Z]{16})/gi;
|
|
9
10
|
const TELEMETRY_CHANNEL = "notrace.telemetry.extension";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildModelSummary, buildModelSwitches, groupByModel } from "../analytics.js";
|
|
3
|
+
const events = [
|
|
4
|
+
{ type: "llm_completion", model: "a", provider: "x", timestamp: 1000, usage: { input: 10, output: 5, totalTokens: 15, cost: { total: 1 } } },
|
|
5
|
+
{ type: "llm_completion", model: "a", provider: "x", timestamp: "2026-06-17T17:00:02.000Z", usage: { input: 20, output: 10, totalTokens: 30, cost: { total: 2 } }, errorMessage: "oops" },
|
|
6
|
+
{ type: "llm_completion", model: "b", provider: "y", timestamp: "2026-06-17T17:00:03.000Z", usage: { input: 40, output: 20, cost: { total: 3 } } }, // missing totalTokens
|
|
7
|
+
];
|
|
8
|
+
describe("report-app analytics", () => {
|
|
9
|
+
it("groups model usage and falls back when totalTokens is missing", () => {
|
|
10
|
+
const grouped = groupByModel(events);
|
|
11
|
+
expect(grouped.a.count).toBe(2);
|
|
12
|
+
expect(grouped.a.inputTokens).toBe(30);
|
|
13
|
+
expect(grouped.a.errors).toBe(1);
|
|
14
|
+
// b is missing totalTokens, should fallback to input + output (40 + 20)
|
|
15
|
+
expect(grouped.b.totalTokens).toBe(60);
|
|
16
|
+
});
|
|
17
|
+
it("tracks model switches and normalizes string timestamps", () => {
|
|
18
|
+
const switches = buildModelSwitches(events);
|
|
19
|
+
expect(switches).toHaveLength(1);
|
|
20
|
+
expect(switches[0].from).toBe("a");
|
|
21
|
+
expect(switches[0].to).toBe("b");
|
|
22
|
+
expect(switches[0].providerChanged).toBe(true);
|
|
23
|
+
// timestamp Delta between "2026-06-17T17:00:02.000Z" and "2026-06-17T17:00:03.000Z" is 1000ms
|
|
24
|
+
expect(switches[0].timeDelta).toBe(1000);
|
|
25
|
+
// falls back to input + output when totalTokens is missing
|
|
26
|
+
expect(switches[0].tokens).toBe(60);
|
|
27
|
+
});
|
|
28
|
+
it("builds summary", () => {
|
|
29
|
+
const summary = buildModelSummary(events);
|
|
30
|
+
expect(summary.firstModel).toBe("a");
|
|
31
|
+
expect(summary.finalModel).toBe("b");
|
|
32
|
+
expect(summary.switchCount).toBe(1);
|
|
33
|
+
expect(summary.uniqueModels).toBe(2);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderCollapsibleSection, renderKeyValueList, renderToolResultHtml, renderToolUseHtml } from "../components/card.js";
|
|
3
|
+
describe("report-app card", () => {
|
|
4
|
+
it("renders tool use and result cards", () => {
|
|
5
|
+
const useHtml = renderToolUseHtml("bash", { command: "echo hi" });
|
|
6
|
+
const resultHtml = renderToolResultHtml("tool-1", { stdout: "ok" }, true);
|
|
7
|
+
expect(useHtml).toContain("chat-tool-use");
|
|
8
|
+
expect(useHtml).toContain("bash");
|
|
9
|
+
expect(resultHtml).toContain("Tool Result: tool-1");
|
|
10
|
+
expect(resultHtml).toContain("color: var(--err);");
|
|
11
|
+
});
|
|
12
|
+
it("escapes injected content", () => {
|
|
13
|
+
const html = renderToolUseHtml("<script>alert(1)</script>", "<script>alert(1)</script>");
|
|
14
|
+
expect(html).toContain("<script>alert(1)</script>");
|
|
15
|
+
expect(html).not.toContain("<script>alert(1)</script>");
|
|
16
|
+
});
|
|
17
|
+
it("renders collapsible sections and key-value lists", () => {
|
|
18
|
+
const section = renderCollapsibleSection("Models", "<div>body</div>", true);
|
|
19
|
+
const kv = renderKeyValueList([["A", "B"], ["Empty", ""]]);
|
|
20
|
+
expect(section).toContain("panel collapsible");
|
|
21
|
+
expect(section).toContain(" open");
|
|
22
|
+
expect(kv).toContain("kv-list");
|
|
23
|
+
expect(kv).toContain("Empty");
|
|
24
|
+
expect(kv).toContain(">-<");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderEventCard } from "../components/event.js";
|
|
3
|
+
describe("report-app event", () => {
|
|
4
|
+
it("escapes hostile content in lazy event body attributes", () => {
|
|
5
|
+
const hostileEvent = {
|
|
6
|
+
type: "llm_completion",
|
|
7
|
+
outputContent: '<img src=x onerror=alert(1)><script>alert(1)</script>" autofocus onfocus=alert(1)',
|
|
8
|
+
timestamp: 1000
|
|
9
|
+
};
|
|
10
|
+
const html = renderEventCard(hostileEvent);
|
|
11
|
+
// The data-lazy-event-body attribute should contain the URI-encoded then HTML-escaped string.
|
|
12
|
+
// It must NOT contain the raw dangerous strings.
|
|
13
|
+
expect(html).not.toContain("<script>alert(1)</script>");
|
|
14
|
+
expect(html).not.toContain('onerror=alert(1)');
|
|
15
|
+
expect(html).toContain("data-lazy-event-body=");
|
|
16
|
+
// Double check that the encoded payload still holds the data safely.
|
|
17
|
+
expect(html).toContain("alert(1)"); // uri encoded form might just be alert(1) but the < > and " will be %3C %3E %22
|
|
18
|
+
expect(html).toContain("%26lt%3Bscript%26gt%3Balert(1)%26lt%3B%2Fscript%26gt%3B");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatDateDay, formatMs, formatTelemetryStatus, formatTimeMinutes, formatTimeSeconds, formatTokens, formatUsd, parseDate, taskDisplay, workflowClassName, workflowDisplayName } from "../format.js";
|
|
3
|
+
describe("report-app format", () => {
|
|
4
|
+
it("parses valid dates and rejects invalid ones", () => {
|
|
5
|
+
expect(parseDate("2026-06-17T17:00:00Z")?.toISOString()).toBe("2026-06-17T17:00:00.000Z");
|
|
6
|
+
expect(parseDate("nope")).toBeNull();
|
|
7
|
+
});
|
|
8
|
+
it("formats date and time pieces", () => {
|
|
9
|
+
expect(formatDateDay("2026-06-17T17:00:00Z")).toBe("2026-06-17");
|
|
10
|
+
expect(formatTimeMinutes("2026-06-17T17:00:00Z")).toMatch(/^17:00|\d{2}:\d{2}$/);
|
|
11
|
+
expect(formatTimeSeconds("2026-06-17T17:00:00Z")).toMatch(/^17:00:00|\d{2}:\d{2}:\d{2}$/);
|
|
12
|
+
});
|
|
13
|
+
it("formats workflow labels and classes", () => {
|
|
14
|
+
expect(workflowDisplayName("norpiv")).toBe("RPIV");
|
|
15
|
+
expect(workflowDisplayName("research")).toBe("Research");
|
|
16
|
+
expect(workflowDisplayName(undefined)).toBe("Generic");
|
|
17
|
+
expect(workflowClassName("norpiv")).toBe("workflow-rpiv");
|
|
18
|
+
expect(workflowClassName("research")).toBe("workflow-research");
|
|
19
|
+
expect(workflowClassName(undefined)).toBe("workflow-generic");
|
|
20
|
+
});
|
|
21
|
+
it("preserves old taskDisplay semantics for direct and nested shapes", () => {
|
|
22
|
+
expect(taskDisplay({ workflow: "research", id: "branch:main" })).toBe("Branch main");
|
|
23
|
+
expect(taskDisplay({ task: { workflow: "norpiv", id: "NR-101" } })).toBe("NR-101");
|
|
24
|
+
expect(taskDisplay({ workflow: "generic" })).toBe("General session");
|
|
25
|
+
expect(taskDisplay({ workflow: "weird" })).toBe("No active task");
|
|
26
|
+
});
|
|
27
|
+
it("formats usd, tokens, durations, and telemetry status", () => {
|
|
28
|
+
expect(formatUsd(0)).toBe("$0");
|
|
29
|
+
expect(formatUsd(1.234567)).toBe("$1.23457");
|
|
30
|
+
expect(formatTokens(999)).toBe("999");
|
|
31
|
+
expect(formatTokens(1500)).toBe("1.5k");
|
|
32
|
+
expect(formatTokens(2_500_000)).toBe("2.50M");
|
|
33
|
+
expect(formatMs(0)).toBe("-");
|
|
34
|
+
expect(formatMs(15_000)).toBe("15s");
|
|
35
|
+
expect(formatMs(125_000)).toBe("2m 5s");
|
|
36
|
+
expect(formatMs(3_780_000)).toBe("1h 3m");
|
|
37
|
+
expect(formatTelemetryStatus("active")).toBe("Active");
|
|
38
|
+
expect(formatTelemetryStatus("loaded-disabled")).toBe("Loaded disabled");
|
|
39
|
+
expect(formatTelemetryStatus(undefined)).toBe("Unknown");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { generateHtmlReport } from "../report.js";
|
|
3
|
+
const record = {
|
|
4
|
+
traceId: "session-1",
|
|
5
|
+
repository: { name: "nothing", branch: "main" },
|
|
6
|
+
session: { id: "session-1", startedAt: "2026-06-17T17:00:00Z", durationMs: 15000 },
|
|
7
|
+
task: { workflow: "research", id: "branch:main" },
|
|
8
|
+
captureMode: "full",
|
|
9
|
+
conditions: { providers: ["anthropic"] },
|
|
10
|
+
activity: {
|
|
11
|
+
llmCallCount: 1,
|
|
12
|
+
toolCallCount: 1,
|
|
13
|
+
toolErrorCount: 0,
|
|
14
|
+
durationMs: 15000,
|
|
15
|
+
totals: { inputTokens: 10, outputTokens: 5, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 15, totalCostUsd: 0.01 },
|
|
16
|
+
},
|
|
17
|
+
telemetry: { extensions: {} },
|
|
18
|
+
events: [
|
|
19
|
+
{ type: "llm_completion", model: "claude", provider: "anthropic", timestamp: 1710000000000, inputPayload: { messages: [{ role: "user", content: "hi" }] }, outputContent: "hello", usage: { totalTokens: 15, cost: { total: 0.01 } } },
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
describe("report-app report", () => {
|
|
23
|
+
it("renders structural markers", () => {
|
|
24
|
+
const html = generateHtmlReport(record);
|
|
25
|
+
expect(html).toContain("Session retrospective");
|
|
26
|
+
expect(html).toContain("Run Summary");
|
|
27
|
+
expect(html).toContain("Timeline");
|
|
28
|
+
expect(html).toContain("Branch main");
|
|
29
|
+
expect(html).toContain("claude");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export function groupByModel(events) {
|
|
2
|
+
const models = {};
|
|
3
|
+
for (const ev of events) {
|
|
4
|
+
if (ev.type !== "llm_completion")
|
|
5
|
+
continue;
|
|
6
|
+
const name = ev.model || "unknown";
|
|
7
|
+
if (!models[name]) {
|
|
8
|
+
models[name] = { count: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0, cacheRead: 0, cacheWrite: 0, errors: 0 };
|
|
9
|
+
}
|
|
10
|
+
const m = models[name];
|
|
11
|
+
m.count++;
|
|
12
|
+
if (ev.usage) {
|
|
13
|
+
const input = Number(ev.usage.inputTokens || ev.usage.input || 0);
|
|
14
|
+
const output = Number(ev.usage.outputTokens || ev.usage.output || 0);
|
|
15
|
+
const cacheR = Number(ev.usage.cacheReadTokens || ev.usage.cacheRead || 0);
|
|
16
|
+
const cacheW = Number(ev.usage.cacheWriteTokens || ev.usage.cacheWrite || 0);
|
|
17
|
+
m.inputTokens += input;
|
|
18
|
+
m.outputTokens += output;
|
|
19
|
+
m.cacheRead += cacheR;
|
|
20
|
+
m.cacheWrite += cacheW;
|
|
21
|
+
m.totalTokens += Number(ev.usage.totalTokens || (input + output + cacheR + cacheW));
|
|
22
|
+
m.cost += Number(ev.usage.cost?.total || 0);
|
|
23
|
+
}
|
|
24
|
+
if (ev.errorMessage)
|
|
25
|
+
m.errors++;
|
|
26
|
+
}
|
|
27
|
+
return models;
|
|
28
|
+
}
|
|
29
|
+
export function buildModelSwitches(events) {
|
|
30
|
+
const switches = [];
|
|
31
|
+
let lastModel = null;
|
|
32
|
+
let lastProvider = null;
|
|
33
|
+
let lastTime = null;
|
|
34
|
+
let completionIndex = 0;
|
|
35
|
+
for (const ev of events) {
|
|
36
|
+
if (ev.type !== "llm_completion")
|
|
37
|
+
continue;
|
|
38
|
+
completionIndex++;
|
|
39
|
+
const currentModel = ev.model || "unknown";
|
|
40
|
+
const currentProvider = ev.provider || "unknown";
|
|
41
|
+
const currentTimestamp = ev.timestamp ? new Date(ev.timestamp).getTime() : 0;
|
|
42
|
+
const validTimestamp = Number.isNaN(currentTimestamp) ? 0 : currentTimestamp;
|
|
43
|
+
if (lastModel && lastModel !== currentModel) {
|
|
44
|
+
const input = Number(ev.usage?.inputTokens || ev.usage?.input || 0);
|
|
45
|
+
const output = Number(ev.usage?.outputTokens || ev.usage?.output || 0);
|
|
46
|
+
const cacheR = Number(ev.usage?.cacheReadTokens || ev.usage?.cacheRead || 0);
|
|
47
|
+
const cacheW = Number(ev.usage?.cacheWriteTokens || ev.usage?.cacheWrite || 0);
|
|
48
|
+
switches.push({
|
|
49
|
+
index: completionIndex,
|
|
50
|
+
from: lastModel,
|
|
51
|
+
to: currentModel,
|
|
52
|
+
fromProvider: lastProvider || "unknown",
|
|
53
|
+
toProvider: currentProvider,
|
|
54
|
+
providerChanged: (lastProvider || "unknown") !== currentProvider,
|
|
55
|
+
timestamp: validTimestamp,
|
|
56
|
+
timeDelta: lastTime ? validTimestamp - lastTime : 0,
|
|
57
|
+
cost: Number(ev.usage?.cost?.total || 0),
|
|
58
|
+
tokens: Number(ev.usage?.totalTokens || (input + output + cacheR + cacheW))
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
lastModel = currentModel;
|
|
62
|
+
lastProvider = currentProvider;
|
|
63
|
+
lastTime = validTimestamp;
|
|
64
|
+
}
|
|
65
|
+
return switches;
|
|
66
|
+
}
|
|
67
|
+
export function buildModelSummary(events) {
|
|
68
|
+
const completions = events.filter((ev) => ev.type === "llm_completion");
|
|
69
|
+
if (!completions.length)
|
|
70
|
+
return null;
|
|
71
|
+
const uniqueModels = new Set(completions.map((ev) => ev.model || "unknown"));
|
|
72
|
+
return {
|
|
73
|
+
firstModel: completions[0]?.model || "unknown",
|
|
74
|
+
finalModel: completions[completions.length - 1]?.model || "unknown",
|
|
75
|
+
switchCount: buildModelSwitches(events).length,
|
|
76
|
+
uniqueModels: uniqueModels.size,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const COPY_SCRIPT = "(() => {\n document.querySelectorAll('[data-copy-value]').forEach((button) => {\n button.addEventListener('click', async () => {\n const value = button.getAttribute('data-copy-value') || '';\n try {\n if (navigator.clipboard?.writeText) {\n await navigator.clipboard.writeText(value);\n } else {\n const textarea = document.createElement('textarea');\n textarea.value = value;\n textarea.style.position = 'fixed';\n textarea.style.opacity = '0';\n document.body.appendChild(textarea);\n textarea.focus();\n textarea.select();\n document.execCommand('copy');\n textarea.remove();\n }\n const previous = button.innerHTML;\n button.classList.add('copied');\n 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>';\n setTimeout(() => {\n button.classList.remove('copied');\n button.innerHTML = previous;\n }, 1400);\n } catch {\n button.textContent = 'ERR';\n }\n });\n });\n\n document.querySelectorAll('details[data-lazy-event-body]').forEach((details) => {\n details.addEventListener('toggle', () => {\n if (!details.open) return;\n if (details.querySelector('.event-body')) return;\n const html = decodeURIComponent(details.getAttribute('data-lazy-event-body') || '');\n details.insertAdjacentHTML('beforeend', html);\n }, { once: false });\n });\n\n const topBtn = document.querySelector('.back-to-top');\n if (topBtn) {\n const syncTopButton = () => {\n const scrollable = document.documentElement.scrollHeight > window.innerHeight + 24;\n const show = scrollable && window.scrollY > 200;\n topBtn.classList.toggle('visible', show);\n };\n window.addEventListener('scroll', syncTopButton, { passive: true });\n window.addEventListener('resize', syncTopButton);\n syncTopButton();\n }\n })();";
|
|
2
|
+
export declare const DASHBOARD_SORT_SCRIPT = "(() => {\n const table = document.querySelector('[data-dashboard-table]');\n if (!table) return;\n const tbody = table.querySelector('tbody');\n if (!tbody) return;\n const buttons = Array.from(document.querySelectorAll('[data-sort-key]'));\n let currentKey = 'index';\n let currentDir = 'desc';\n\n function icon(dir) {\n return dir === 'asc' ? '\u2191' : '\u2193';\n }\n\n function updateState() {\n buttons.forEach(btn => {\n const key = btn.getAttribute('data-sort-key');\n const state = btn.querySelector('.sort-state');\n if (!state) return;\n state.textContent = key === currentKey ? icon(currentDir) : '';\n });\n }\n\n function compare(a, b, key) {\n if (key === 'index' || key === 'started' || key === 'tokens' || key === 'cost') {\n return Number(a.dataset[key] || 0) - Number(b.dataset[key] || 0);\n }\n return String(a.dataset[key] || '').localeCompare(String(b.dataset[key] || ''));\n }\n\n function sortBy(key) {\n const rows = Array.from(tbody.querySelectorAll('tr'));\n rows.sort((a, b) => {\n const result = compare(a, b, key);\n return currentDir === 'asc' ? result : -result;\n });\n rows.forEach(row => tbody.appendChild(row));\n updateState();\n }\n\n buttons.forEach(btn => {\n btn.addEventListener('click', () => {\n const key = btn.getAttribute('data-sort-key') || 'index';\n if (currentKey === key) currentDir = currentDir === 'asc' ? 'desc' : 'asc';\n else {\n currentKey = key;\n currentDir = key === 'workflow' ? 'asc' : 'desc';\n }\n sortBy(currentKey);\n });\n });\n\n sortBy(currentKey);\n })();";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export const COPY_SCRIPT = `(() => {
|
|
2
|
+
document.querySelectorAll('[data-copy-value]').forEach((button) => {
|
|
3
|
+
button.addEventListener('click', async () => {
|
|
4
|
+
const value = button.getAttribute('data-copy-value') || '';
|
|
5
|
+
try {
|
|
6
|
+
if (navigator.clipboard?.writeText) {
|
|
7
|
+
await navigator.clipboard.writeText(value);
|
|
8
|
+
} else {
|
|
9
|
+
const textarea = document.createElement('textarea');
|
|
10
|
+
textarea.value = value;
|
|
11
|
+
textarea.style.position = 'fixed';
|
|
12
|
+
textarea.style.opacity = '0';
|
|
13
|
+
document.body.appendChild(textarea);
|
|
14
|
+
textarea.focus();
|
|
15
|
+
textarea.select();
|
|
16
|
+
document.execCommand('copy');
|
|
17
|
+
textarea.remove();
|
|
18
|
+
}
|
|
19
|
+
const previous = button.innerHTML;
|
|
20
|
+
button.classList.add('copied');
|
|
21
|
+
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>';
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
button.classList.remove('copied');
|
|
24
|
+
button.innerHTML = previous;
|
|
25
|
+
}, 1400);
|
|
26
|
+
} catch {
|
|
27
|
+
button.textContent = 'ERR';
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
document.querySelectorAll('details[data-lazy-event-body]').forEach((details) => {
|
|
33
|
+
details.addEventListener('toggle', () => {
|
|
34
|
+
if (!details.open) return;
|
|
35
|
+
if (details.querySelector('.event-body')) return;
|
|
36
|
+
const html = decodeURIComponent(details.getAttribute('data-lazy-event-body') || '');
|
|
37
|
+
details.insertAdjacentHTML('beforeend', html);
|
|
38
|
+
}, { once: false });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const topBtn = document.querySelector('.back-to-top');
|
|
42
|
+
if (topBtn) {
|
|
43
|
+
const syncTopButton = () => {
|
|
44
|
+
const scrollable = document.documentElement.scrollHeight > window.innerHeight + 24;
|
|
45
|
+
const show = scrollable && window.scrollY > 200;
|
|
46
|
+
topBtn.classList.toggle('visible', show);
|
|
47
|
+
};
|
|
48
|
+
window.addEventListener('scroll', syncTopButton, { passive: true });
|
|
49
|
+
window.addEventListener('resize', syncTopButton);
|
|
50
|
+
syncTopButton();
|
|
51
|
+
}
|
|
52
|
+
})();`;
|
|
53
|
+
export const DASHBOARD_SORT_SCRIPT = `(() => {
|
|
54
|
+
const table = document.querySelector('[data-dashboard-table]');
|
|
55
|
+
if (!table) return;
|
|
56
|
+
const tbody = table.querySelector('tbody');
|
|
57
|
+
if (!tbody) return;
|
|
58
|
+
const buttons = Array.from(document.querySelectorAll('[data-sort-key]'));
|
|
59
|
+
let currentKey = 'index';
|
|
60
|
+
let currentDir = 'desc';
|
|
61
|
+
|
|
62
|
+
function icon(dir) {
|
|
63
|
+
return dir === 'asc' ? '↑' : '↓';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function updateState() {
|
|
67
|
+
buttons.forEach(btn => {
|
|
68
|
+
const key = btn.getAttribute('data-sort-key');
|
|
69
|
+
const state = btn.querySelector('.sort-state');
|
|
70
|
+
if (!state) return;
|
|
71
|
+
state.textContent = key === currentKey ? icon(currentDir) : '';
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function compare(a, b, key) {
|
|
76
|
+
if (key === 'index' || key === 'started' || key === 'tokens' || key === 'cost') {
|
|
77
|
+
return Number(a.dataset[key] || 0) - Number(b.dataset[key] || 0);
|
|
78
|
+
}
|
|
79
|
+
return String(a.dataset[key] || '').localeCompare(String(b.dataset[key] || ''));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sortBy(key) {
|
|
83
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
84
|
+
rows.sort((a, b) => {
|
|
85
|
+
const result = compare(a, b, key);
|
|
86
|
+
return currentDir === 'asc' ? result : -result;
|
|
87
|
+
});
|
|
88
|
+
rows.forEach(row => tbody.appendChild(row));
|
|
89
|
+
updateState();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
buttons.forEach(btn => {
|
|
93
|
+
btn.addEventListener('click', () => {
|
|
94
|
+
const key = btn.getAttribute('data-sort-key') || 'index';
|
|
95
|
+
if (currentKey === key) currentDir = currentDir === 'asc' ? 'desc' : 'asc';
|
|
96
|
+
else {
|
|
97
|
+
currentKey = key;
|
|
98
|
+
currentDir = key === 'workflow' ? 'asc' : 'desc';
|
|
99
|
+
}
|
|
100
|
+
sortBy(currentKey);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
sortBy(currentKey);
|
|
105
|
+
})();`;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function renderToolUseHtml(name: string, input: any): string;
|
|
2
|
+
export declare function renderToolResultHtml(id: string, content: any, isError?: boolean): string;
|
|
3
|
+
export declare function renderCollapsibleSection(title: string, body: string, open?: boolean): string;
|
|
4
|
+
export declare function renderKeyValueList(items: Array<[string, unknown]>): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { escapeHtml } from "../escape.js";
|
|
2
|
+
const TOOL_USE_ICON = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>`;
|
|
3
|
+
const TOOL_RESULT_ICON = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path></svg>`;
|
|
4
|
+
function normalizeContent(value) {
|
|
5
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
6
|
+
}
|
|
7
|
+
function parseMaybeJson(value) {
|
|
8
|
+
if (typeof value !== "string")
|
|
9
|
+
return value;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(value);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function renderToolCard(label, icon, content, options = {}) {
|
|
18
|
+
const headerStyle = options.headerStyle ? ` style="${options.headerStyle}"` : "";
|
|
19
|
+
const cardStyle = options.cardStyle ? ` style="${options.cardStyle}"` : "";
|
|
20
|
+
return `<div class="chat-tool-use"${cardStyle}><div class="chat-tool-header"${headerStyle}>${icon} ${escapeHtml(label)}</div><pre class="chat-tool-body">${escapeHtml(normalizeContent(content))}</pre></div>`;
|
|
21
|
+
}
|
|
22
|
+
export function renderToolUseHtml(name, input) {
|
|
23
|
+
return renderToolCard(name, TOOL_USE_ICON, parseMaybeJson(input));
|
|
24
|
+
}
|
|
25
|
+
export function renderToolResultHtml(id, content, isError = false) {
|
|
26
|
+
return renderToolCard(`Tool Result: ${id}`, TOOL_RESULT_ICON, content, {
|
|
27
|
+
headerStyle: isError ? "color: var(--err);" : "color: var(--muted);",
|
|
28
|
+
cardStyle: isError ? "border-color: rgba(239,127,127,0.3);" : undefined,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function renderCollapsibleSection(title, body, open = false) {
|
|
32
|
+
return `<details class="panel collapsible"${open ? " open" : ""}><summary>${escapeHtml(title)}</summary><div>${body}</div></details>`;
|
|
33
|
+
}
|
|
34
|
+
export function renderKeyValueList(items) {
|
|
35
|
+
return `<div class="kv-list">${items.map(([k, v]) => `<div class="kv-row"><span class="kv-key">${escapeHtml(k)}</span><strong class="kv-value">${escapeHtml(v == null || v === "" ? "-" : v)}</strong></div>`).join("")}</div>`;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderDashboardBody(sessions: any[], options?: any): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { escapeHtml } from "../escape.js";
|
|
2
|
+
import { formatDateDay, formatTimeMinutes, formatUsd, parseDate, taskDisplay, workflowClassName, workflowDisplayName } from "../format.js";
|
|
3
|
+
import { wordmarkSvg } from "../shell.js";
|
|
4
|
+
function renderDateCell(value) {
|
|
5
|
+
const date = parseDate(value);
|
|
6
|
+
if (!date)
|
|
7
|
+
return `<span>${escapeHtml(value)}</span>`;
|
|
8
|
+
return `<div class="date-cell"><strong>${escapeHtml(formatDateDay(value))}</strong><span>${escapeHtml(formatTimeMinutes(value))}</span></div>`;
|
|
9
|
+
}
|
|
10
|
+
export function renderDashboardBody(sessions, options = {}) {
|
|
11
|
+
const reversed = sessions.slice().reverse();
|
|
12
|
+
const totalCost = sessions.reduce((sum, s) => sum + Number(s.activity?.totals?.totalCostUsd || 0), 0);
|
|
13
|
+
const totalTokens = sessions.reduce((sum, s) => sum + Number(s.activity?.totals?.totalTokens || 0), 0);
|
|
14
|
+
const homeHref = options?.indexHref || "index.html";
|
|
15
|
+
return `<div class="container"><section class="hero"><div class="hero-split"><a class="brand-link" href="${escapeHtml(homeHref)}">${wordmarkSvg()}</a><div class="hero-right"><div class="hero-session"><strong style="color: var(--text); font-weight: 500;">Global Index</strong><span style="color: var(--muted);">Machine-wide session evidence.</span></div><div class="hero-meta"><span class="hero-pill">${sessions.length} sessions</span></div></div></div><div class="metrics"><div class="metric-card"><small>Sessions</small><strong>${sessions.length}</strong></div><div class="metric-card"><small>Total Tokens</small><strong>${totalTokens.toLocaleString()}</strong></div><div class="metric-card"><small>Total Cost</small><strong>${formatUsd(totalCost)}</strong></div></div></section><section class="panel"><h2 class="section-title">Session Reports</h2>${reversed.length ? `<table data-dashboard-table><thead><tr><th class="col-index sortable-head"><button class="sort-btn" data-sort-key="index"><span class="sort-label">#</span><span class="sort-state">↓</span></button></th><th>Session</th><th>Project</th><th class="sortable-head"><button class="sort-btn" data-sort-key="workflow"><span class="sort-label">Workflow</span><span class="sort-state"></span></button></th><th class="sortable-head"><button class="sort-btn" data-sort-key="started"><span class="sort-label">Started</span><span class="sort-state"></span></button></th><th>Task</th><th class="sortable-head num-cell"><button class="sort-btn" data-sort-key="tokens"><span class="sort-label">Tokens</span><span class="sort-state"></span></button></th><th class="sortable-head num-cell"><button class="sort-btn" data-sort-key="cost"><span class="sort-label">Cost</span><span class="sort-state"></span></button></th></tr></thead><tbody>${reversed.map((s, index) => { const link = s.artifacts?.html ? (s.artifacts.html.startsWith(".notrace/") ? s.artifacts.html.substring(9) : s.artifacts.html) : "#"; const workflow = s.task?.workflow || "generic"; const workflowLabel = workflowDisplayName(workflow); const tokens = Number(s.activity?.totals?.totalTokens || 0); const cost = Number(s.activity?.totals?.totalCostUsd || 0); return `<tr data-index="${reversed.length - index}" data-workflow="${escapeHtml(workflowLabel)}" data-started="${parseDate(s.startedAt)?.getTime() || 0}" data-tokens="${tokens}" data-cost="${cost}"><td class="index-cell">${reversed.length - index}</td><td><a class="session-link" href="${escapeHtml(link)}"><strong>${escapeHtml(String(s.sessionId).slice(0, 8))}</strong><span class="session-sub">${escapeHtml(String(s.sessionId))}</span></a></td><td><span class="hero-pill">${escapeHtml(s.repositoryName || "Unknown")}</span></td><td><span class="workflow-pill ${workflowClassName(workflow)}">${escapeHtml(workflowLabel)}</span></td><td>${renderDateCell(s.startedAt)}</td><td>${escapeHtml(taskDisplay(s))}</td><td class="num-cell">${tokens.toLocaleString()}</td><td class="num-cell">${formatUsd(cost)}</td></tr>`; }).join("")}</tbody></table>` : `<div class="empty">No sessions yet. Run Pi with notrace enabled. New reports appear here.</div>`}</section><footer class="footer-note minimal">notrace • raquezha 2026</footer></div>`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function renderJsonBlock(title: string, value: unknown): string;
|
|
2
|
+
export declare function eventBadgeClass(ev: any): string;
|
|
3
|
+
export declare function eventTitle(ev: any): string;
|
|
4
|
+
export declare function renderEventBody(ev: any): string;
|
|
5
|
+
export declare function renderEventCard(ev: any): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { escapeHtml } from "../escape.js";
|
|
2
|
+
import { formatTimeSeconds } from "../format.js";
|
|
3
|
+
import { renderMessages, renderUniversalMessageContent } from "./message.js";
|
|
4
|
+
export function renderJsonBlock(title, value) {
|
|
5
|
+
return `<section class="block"><h4>${escapeHtml(title)}</h4><pre>${escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2))}</pre></section>`;
|
|
6
|
+
}
|
|
7
|
+
export function eventBadgeClass(ev) {
|
|
8
|
+
if (ev.type === "llm_completion")
|
|
9
|
+
return "badge badge-llm";
|
|
10
|
+
if (ev.type === "tool_start" || ev.type === "tool_end")
|
|
11
|
+
return ev.isError ? "badge badge-error" : "badge badge-tool";
|
|
12
|
+
return "badge badge-system";
|
|
13
|
+
}
|
|
14
|
+
export function eventTitle(ev) {
|
|
15
|
+
return ev.model || ev.toolName || ev.type;
|
|
16
|
+
}
|
|
17
|
+
export function renderEventBody(ev) {
|
|
18
|
+
const sections = [];
|
|
19
|
+
if (ev.type === "llm_completion") {
|
|
20
|
+
sections.push(renderMessages(ev.inputPayload?.messages));
|
|
21
|
+
if (ev.stopReason && ev.stopReason !== "stop" && ev.stopReason !== "toolUse")
|
|
22
|
+
sections.push(renderJsonBlock("Stop Reason", ev.stopReason));
|
|
23
|
+
if (ev.errorMessage)
|
|
24
|
+
sections.push(renderJsonBlock("Error Message", ev.errorMessage));
|
|
25
|
+
sections.push(`<section class="block"><h4>Output</h4><div class="msg-content">${renderUniversalMessageContent({ content: ev.outputContent })}</div></section>`);
|
|
26
|
+
if (ev.usage)
|
|
27
|
+
sections.push(renderJsonBlock("Usage", ev.usage));
|
|
28
|
+
}
|
|
29
|
+
else if (ev.type === "tool_start") {
|
|
30
|
+
sections.push(`<section class="block"><h4>Arguments</h4><div class="msg-content"><div class="chat-tool-use"><div class="chat-tool-header"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg> Execution Input</div><pre class="chat-tool-body">${escapeHtml(typeof ev.args === 'string' ? ev.args : JSON.stringify(ev.args, null, 2))}</pre></div></div></section>`);
|
|
31
|
+
}
|
|
32
|
+
else if (ev.type === "tool_end") {
|
|
33
|
+
sections.push(`<section class="block"><h4>${ev.isError ? "Error Result" : "Result"}</h4><div class="msg-content"><div class="chat-tool-use" style="${ev.isError ? 'border-color: rgba(239,127,127,0.3);' : ''}"><div class="chat-tool-header" style="${ev.isError ? 'color: var(--err);' : 'color: var(--muted);'}"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path></svg> Execution Output</div><pre class="chat-tool-body">${escapeHtml(typeof ev.result === 'string' ? ev.result : JSON.stringify(ev.result, null, 2))}</pre></div></div></section>`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
sections.push(renderJsonBlock("Event", ev));
|
|
37
|
+
}
|
|
38
|
+
return `<div class="event-body"><div class="stack">${sections.join("")}</div></div>`;
|
|
39
|
+
}
|
|
40
|
+
export function renderEventCard(ev) {
|
|
41
|
+
return `<details class="event" data-lazy-event-body="${escapeHtml(encodeURIComponent(renderEventBody(ev)))}"><summary><div class="event-main"><span class="${eventBadgeClass(ev)}">${escapeHtml(ev.type)}</span><span class="event-title">${escapeHtml(eventTitle(ev))}</span></div><span class="event-time">${escapeHtml(formatTimeSeconds(ev.timestamp))}</span></summary></details>`;
|
|
42
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { escapeHtml } from "../escape.js";
|
|
2
|
+
import { renderToolResultHtml, renderToolUseHtml } from "./card.js";
|
|
3
|
+
export function renderUniversalMessageContent(m) {
|
|
4
|
+
if (!m)
|
|
5
|
+
return "";
|
|
6
|
+
let html = "";
|
|
7
|
+
if (typeof m.content === "string" && m.content.trim()) {
|
|
8
|
+
html += `<div class="chat-text">${escapeHtml(m.content)}</div>`;
|
|
9
|
+
}
|
|
10
|
+
else if (Array.isArray(m.content)) {
|
|
11
|
+
html += m.content.map((block) => {
|
|
12
|
+
if (!block)
|
|
13
|
+
return "";
|
|
14
|
+
if (block.type === "text")
|
|
15
|
+
return `<div class="chat-text">${escapeHtml(block.text)}</div>`;
|
|
16
|
+
if (block.type === "tool_use")
|
|
17
|
+
return renderToolUseHtml(block.name, block.input);
|
|
18
|
+
if (block.type === "tool_result")
|
|
19
|
+
return renderToolResultHtml(block.tool_use_id || "unknown", block.content, block.is_error === true);
|
|
20
|
+
return `<pre class="chat-tool-body">${escapeHtml(JSON.stringify(block, null, 2))}</pre>`;
|
|
21
|
+
}).join("");
|
|
22
|
+
}
|
|
23
|
+
else if (m.content && typeof m.content === "object") {
|
|
24
|
+
html += `<pre class="chat-tool-body">${escapeHtml(JSON.stringify(m.content, null, 2))}</pre>`;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(m.tool_calls)) {
|
|
27
|
+
html += m.tool_calls.map((tc) => {
|
|
28
|
+
if (tc.type === "function" && tc.function)
|
|
29
|
+
return renderToolUseHtml(tc.function.name, tc.function.arguments);
|
|
30
|
+
return "";
|
|
31
|
+
}).join("");
|
|
32
|
+
}
|
|
33
|
+
if (m.function_call)
|
|
34
|
+
html += renderToolUseHtml(m.function_call.name, m.function_call.arguments);
|
|
35
|
+
if (m.role === "tool")
|
|
36
|
+
html = renderToolResultHtml(m.tool_call_id || m.name || "unknown", m.content, m.is_error === true);
|
|
37
|
+
return html || `<div class="empty">Empty message</div>`;
|
|
38
|
+
}
|
|
39
|
+
export function renderMessages(messages) {
|
|
40
|
+
if (!messages?.length)
|
|
41
|
+
return "";
|
|
42
|
+
return `<section class="block"><h4>Input Messages</h4>${messages.map(m => `<div class="msg ${escapeHtml(m?.role || "unknown")} "><div class="msg-head"><span class="msg-role">${escapeHtml(m?.role || "unknown")}</span></div><div class="msg-content">${renderUniversalMessageContent(m)}</div></div>`).join("")}</section>`;
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateDashboardHtml(sessions: any[], options?: any): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DASHBOARD_SORT_SCRIPT } from "./client.js";
|
|
2
|
+
import { renderDashboardBody } from "./components/dashboard.js";
|
|
3
|
+
import { shell } from "./shell.js";
|
|
4
|
+
export function generateDashboardHtml(sessions, options = {}) {
|
|
5
|
+
return shell("notrace", renderDashboardBody(sessions, options), DASHBOARD_SORT_SCRIPT);
|
|
6
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function escapeHtml(v: unknown): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DateValue, MaybeNumber, MaybeString, Repoish, Taskish } from "./types.js";
|
|
2
|
+
export declare function parseDate(value: DateValue): Date | null;
|
|
3
|
+
export declare function formatDateDay(value: DateValue): string;
|
|
4
|
+
export declare function formatTimeMinutes(value: DateValue): string;
|
|
5
|
+
export declare function formatTimeSeconds(value: DateValue): string;
|
|
6
|
+
export declare function workflowDisplayName(workflow: MaybeString): string;
|
|
7
|
+
export declare function workflowClassName(workflow: MaybeString): string;
|
|
8
|
+
export declare function taskDisplay(taskish: Taskish): string;
|
|
9
|
+
export declare function resolveRepoName(data: Repoish): string;
|
|
10
|
+
export declare function formatUsd(value: MaybeNumber): string;
|
|
11
|
+
export declare function formatTokens(value: MaybeNumber): string;
|
|
12
|
+
export declare function formatMs(value: MaybeNumber): string;
|
|
13
|
+
export declare function formatTelemetryStatus(value: MaybeString): string;
|