@mariozechner/pi-coding-agent 0.12.5 → 0.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/README.md +86 -10
- package/dist/compaction.d.ts +51 -0
- package/dist/compaction.d.ts.map +1 -0
- package/dist/compaction.js +218 -0
- package/dist/compaction.js.map +1 -0
- package/dist/export-html.d.ts +5 -3
- package/dist/export-html.d.ts.map +1 -1
- package/dist/export-html.js +480 -1314
- package/dist/export-html.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +80 -3
- package/dist/main.js.map +1 -1
- package/dist/session-manager.d.ts +66 -1
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +175 -59
- package/dist/session-manager.js.map +1 -1
- package/dist/settings-manager.d.ts +15 -0
- package/dist/settings-manager.d.ts.map +1 -1
- package/dist/settings-manager.js +23 -0
- package/dist/settings-manager.js.map +1 -1
- package/dist/tools-manager.d.ts.map +1 -1
- package/dist/tools-manager.js +2 -2
- package/dist/tools-manager.js.map +1 -1
- package/dist/tui/compaction.d.ts +15 -0
- package/dist/tui/compaction.d.ts.map +1 -0
- package/dist/tui/compaction.js +42 -0
- package/dist/tui/compaction.js.map +1 -0
- package/dist/tui/footer.d.ts +2 -0
- package/dist/tui/footer.d.ts.map +1 -1
- package/dist/tui/footer.js +8 -3
- package/dist/tui/footer.js.map +1 -1
- package/dist/tui/tui-renderer.d.ts +8 -1
- package/dist/tui/tui-renderer.d.ts.map +1 -1
- package/dist/tui/tui-renderer.js +286 -28
- package/dist/tui/tui-renderer.js.map +1 -1
- package/package.json +4 -4
package/dist/export-html.js
CHANGED
|
@@ -1,30 +1,27 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { basename } from "path";
|
|
4
|
-
import { VERSION } from "./config.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { APP_NAME, VERSION } from "./config.js";
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Color scheme (matching TUI)
|
|
7
|
+
// ============================================================================
|
|
8
8
|
const COLORS = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
red: "rgb(239, 68, 68)", // Red for errors
|
|
22
|
-
yellow: "rgb(234, 179, 8)", // Yellow for warnings
|
|
23
|
-
italic: "rgb(161, 161, 170)", // Gray italic for thinking
|
|
9
|
+
userMessageBg: "rgb(52, 53, 65)",
|
|
10
|
+
toolPendingBg: "rgb(40, 40, 50)",
|
|
11
|
+
toolSuccessBg: "rgb(40, 50, 40)",
|
|
12
|
+
toolErrorBg: "rgb(60, 40, 40)",
|
|
13
|
+
bodyBg: "rgb(24, 24, 30)",
|
|
14
|
+
containerBg: "rgb(30, 30, 36)",
|
|
15
|
+
text: "rgb(229, 229, 231)",
|
|
16
|
+
textDim: "rgb(161, 161, 170)",
|
|
17
|
+
cyan: "rgb(103, 232, 249)",
|
|
18
|
+
green: "rgb(34, 197, 94)",
|
|
19
|
+
red: "rgb(239, 68, 68)",
|
|
20
|
+
yellow: "rgb(234, 179, 8)",
|
|
24
21
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Utility functions
|
|
24
|
+
// ============================================================================
|
|
28
25
|
function escapeHtml(text) {
|
|
29
26
|
return text
|
|
30
27
|
.replace(/&/g, "&")
|
|
@@ -33,216 +30,316 @@ function escapeHtml(text) {
|
|
|
33
30
|
.replace(/"/g, """)
|
|
34
31
|
.replace(/'/g, "'");
|
|
35
32
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Shorten path with tilde notation
|
|
38
|
-
*/
|
|
39
33
|
function shortenPath(path) {
|
|
40
34
|
const home = homedir();
|
|
41
|
-
|
|
42
|
-
return "~" + path.slice(home.length);
|
|
43
|
-
}
|
|
44
|
-
return path;
|
|
35
|
+
return path.startsWith(home) ? "~" + path.slice(home.length) : path;
|
|
45
36
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Replace tabs with 3 spaces
|
|
48
|
-
*/
|
|
49
37
|
function replaceTabs(text) {
|
|
50
38
|
return text.replace(/\t/g, " ");
|
|
51
39
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
40
|
+
function formatTimestamp(timestamp) {
|
|
41
|
+
if (!timestamp)
|
|
42
|
+
return "";
|
|
43
|
+
const date = new Date(typeof timestamp === "string" ? timestamp : timestamp);
|
|
44
|
+
return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
45
|
+
}
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Parsing functions
|
|
48
|
+
// ============================================================================
|
|
49
|
+
function parseSessionManagerFormat(lines) {
|
|
50
|
+
const data = {
|
|
51
|
+
sessionId: "unknown",
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
modelsUsed: new Set(),
|
|
54
|
+
messages: [],
|
|
55
|
+
toolResultsMap: new Map(),
|
|
56
|
+
sessionEvents: [],
|
|
57
|
+
tokenStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
58
|
+
costStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
65
59
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
let entry;
|
|
62
|
+
try {
|
|
63
|
+
entry = JSON.parse(line);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
switch (entry.type) {
|
|
69
|
+
case "session":
|
|
70
|
+
data.sessionId = entry.id || "unknown";
|
|
71
|
+
data.timestamp = entry.timestamp || data.timestamp;
|
|
72
|
+
data.systemPrompt = entry.systemPrompt;
|
|
73
|
+
if (entry.modelId) {
|
|
74
|
+
const modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : entry.modelId;
|
|
75
|
+
data.modelsUsed.add(modelInfo);
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case "message": {
|
|
79
|
+
const message = entry.message;
|
|
80
|
+
data.messages.push(message);
|
|
81
|
+
data.sessionEvents.push({
|
|
82
|
+
type: "message",
|
|
83
|
+
message,
|
|
84
|
+
timestamp: entry.timestamp,
|
|
85
|
+
});
|
|
86
|
+
if (message.role === "toolResult") {
|
|
87
|
+
const toolResult = message;
|
|
88
|
+
data.toolResultsMap.set(toolResult.toolCallId, toolResult);
|
|
92
89
|
}
|
|
93
|
-
else {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
else if (message.role === "assistant") {
|
|
91
|
+
const assistantMsg = message;
|
|
92
|
+
if (assistantMsg.usage) {
|
|
93
|
+
data.tokenStats.input += assistantMsg.usage.input || 0;
|
|
94
|
+
data.tokenStats.output += assistantMsg.usage.output || 0;
|
|
95
|
+
data.tokenStats.cacheRead += assistantMsg.usage.cacheRead || 0;
|
|
96
|
+
data.tokenStats.cacheWrite += assistantMsg.usage.cacheWrite || 0;
|
|
97
|
+
if (assistantMsg.usage.cost) {
|
|
98
|
+
data.costStats.input += assistantMsg.usage.cost.input || 0;
|
|
99
|
+
data.costStats.output += assistantMsg.usage.cost.output || 0;
|
|
100
|
+
data.costStats.cacheRead += assistantMsg.usage.cost.cacheRead || 0;
|
|
101
|
+
data.costStats.cacheWrite += assistantMsg.usage.cost.cacheWrite || 0;
|
|
102
|
+
}
|
|
98
103
|
}
|
|
99
|
-
html += "</div>";
|
|
100
104
|
}
|
|
105
|
+
break;
|
|
101
106
|
}
|
|
107
|
+
case "model_change":
|
|
108
|
+
data.sessionEvents.push({
|
|
109
|
+
type: "model_change",
|
|
110
|
+
provider: entry.provider,
|
|
111
|
+
modelId: entry.modelId,
|
|
112
|
+
timestamp: entry.timestamp,
|
|
113
|
+
});
|
|
114
|
+
if (entry.modelId) {
|
|
115
|
+
const modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : entry.modelId;
|
|
116
|
+
data.modelsUsed.add(modelInfo);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
case "compaction":
|
|
120
|
+
data.sessionEvents.push({
|
|
121
|
+
type: "compaction",
|
|
122
|
+
timestamp: entry.timestamp,
|
|
123
|
+
summary: entry.summary,
|
|
124
|
+
tokensBefore: entry.tokensBefore,
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
102
127
|
}
|
|
103
128
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
return data;
|
|
130
|
+
}
|
|
131
|
+
function parseStreamingEventFormat(lines) {
|
|
132
|
+
const data = {
|
|
133
|
+
sessionId: "unknown",
|
|
134
|
+
timestamp: new Date().toISOString(),
|
|
135
|
+
modelsUsed: new Set(),
|
|
136
|
+
messages: [],
|
|
137
|
+
toolResultsMap: new Map(),
|
|
138
|
+
sessionEvents: [],
|
|
139
|
+
tokenStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
140
|
+
costStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
141
|
+
isStreamingFormat: true,
|
|
142
|
+
};
|
|
143
|
+
let timestampSet = false;
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
let entry;
|
|
146
|
+
try {
|
|
147
|
+
entry = JSON.parse(line);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (entry.type === "message_end" && entry.message) {
|
|
153
|
+
const msg = entry.message;
|
|
154
|
+
data.messages.push(msg);
|
|
155
|
+
data.sessionEvents.push({
|
|
156
|
+
type: "message",
|
|
157
|
+
message: msg,
|
|
158
|
+
timestamp: msg.timestamp,
|
|
159
|
+
});
|
|
160
|
+
if (msg.role === "toolResult") {
|
|
161
|
+
const toolResult = msg;
|
|
162
|
+
data.toolResultsMap.set(toolResult.toolCallId, toolResult);
|
|
163
|
+
}
|
|
164
|
+
else if (msg.role === "assistant") {
|
|
165
|
+
const assistantMsg = msg;
|
|
166
|
+
if (assistantMsg.model) {
|
|
167
|
+
const modelInfo = assistantMsg.provider
|
|
168
|
+
? `${assistantMsg.provider}/${assistantMsg.model}`
|
|
169
|
+
: assistantMsg.model;
|
|
170
|
+
data.modelsUsed.add(modelInfo);
|
|
119
171
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
172
|
+
if (assistantMsg.usage) {
|
|
173
|
+
data.tokenStats.input += assistantMsg.usage.input || 0;
|
|
174
|
+
data.tokenStats.output += assistantMsg.usage.output || 0;
|
|
175
|
+
data.tokenStats.cacheRead += assistantMsg.usage.cacheRead || 0;
|
|
176
|
+
data.tokenStats.cacheWrite += assistantMsg.usage.cacheWrite || 0;
|
|
177
|
+
if (assistantMsg.usage.cost) {
|
|
178
|
+
data.costStats.input += assistantMsg.usage.cost.input || 0;
|
|
179
|
+
data.costStats.output += assistantMsg.usage.cost.output || 0;
|
|
180
|
+
data.costStats.cacheRead += assistantMsg.usage.cost.cacheRead || 0;
|
|
181
|
+
data.costStats.cacheWrite += assistantMsg.usage.cost.cacheWrite || 0;
|
|
182
|
+
}
|
|
125
183
|
}
|
|
126
|
-
html += "</div>";
|
|
127
|
-
html += "</div>";
|
|
128
184
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
for (const line of displayLines) {
|
|
133
|
-
html += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
|
134
|
-
}
|
|
135
|
-
html += "</div>";
|
|
185
|
+
if (!timestampSet && msg.timestamp) {
|
|
186
|
+
data.timestamp = new Date(msg.timestamp).toISOString();
|
|
187
|
+
timestampSet = true;
|
|
136
188
|
}
|
|
137
189
|
}
|
|
138
190
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
191
|
+
data.sessionId = `stream-${data.timestamp.replace(/[:.]/g, "-")}`;
|
|
192
|
+
return data;
|
|
193
|
+
}
|
|
194
|
+
function detectFormat(lines) {
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
try {
|
|
197
|
+
const entry = JSON.parse(line);
|
|
198
|
+
if (entry.type === "session")
|
|
199
|
+
return "session-manager";
|
|
200
|
+
if (entry.type === "agent_start" || entry.type === "message_start" || entry.type === "turn_start") {
|
|
201
|
+
return "streaming-events";
|
|
202
|
+
}
|
|
147
203
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
204
|
+
catch { }
|
|
205
|
+
}
|
|
206
|
+
return "unknown";
|
|
207
|
+
}
|
|
208
|
+
function parseSessionFile(content) {
|
|
209
|
+
const lines = content
|
|
210
|
+
.trim()
|
|
211
|
+
.split("\n")
|
|
212
|
+
.filter((l) => l.trim());
|
|
213
|
+
if (lines.length === 0) {
|
|
214
|
+
throw new Error("Empty session file");
|
|
215
|
+
}
|
|
216
|
+
const format = detectFormat(lines);
|
|
217
|
+
if (format === "unknown") {
|
|
218
|
+
throw new Error("Unknown session file format");
|
|
219
|
+
}
|
|
220
|
+
return format === "session-manager" ? parseSessionManagerFormat(lines) : parseStreamingEventFormat(lines);
|
|
221
|
+
}
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// HTML formatting functions
|
|
224
|
+
// ============================================================================
|
|
225
|
+
function formatToolExecution(toolName, args, result) {
|
|
226
|
+
let html = "";
|
|
227
|
+
const isError = result?.isError || false;
|
|
228
|
+
const bgColor = result ? (isError ? COLORS.toolErrorBg : COLORS.toolSuccessBg) : COLORS.toolPendingBg;
|
|
229
|
+
const getTextOutput = () => {
|
|
230
|
+
if (!result)
|
|
231
|
+
return "";
|
|
232
|
+
const textBlocks = result.content.filter((c) => c.type === "text");
|
|
233
|
+
return textBlocks.map((c) => c.text).join("\n");
|
|
234
|
+
};
|
|
235
|
+
const formatExpandableOutput = (lines, maxLines) => {
|
|
236
|
+
const displayLines = lines.slice(0, maxLines);
|
|
237
|
+
const remaining = lines.length - maxLines;
|
|
238
|
+
if (remaining > 0) {
|
|
239
|
+
let out = '<div class="tool-output expandable" onclick="this.classList.toggle(\'expanded\')">';
|
|
240
|
+
out += '<div class="output-preview">';
|
|
241
|
+
for (const line of displayLines) {
|
|
242
|
+
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
|
168
243
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
html += "</div>";
|
|
244
|
+
out += `<div class="expand-hint">... (${remaining} more lines) - click to expand</div>`;
|
|
245
|
+
out += "</div>";
|
|
246
|
+
out += '<div class="output-full">';
|
|
247
|
+
for (const line of lines) {
|
|
248
|
+
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
|
176
249
|
}
|
|
250
|
+
out += "</div></div>";
|
|
251
|
+
return out;
|
|
177
252
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
html += `<div class="tool-output"><div>${escapeHtml(output)}</div></div>`;
|
|
182
|
-
}
|
|
253
|
+
let out = '<div class="tool-output">';
|
|
254
|
+
for (const line of displayLines) {
|
|
255
|
+
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
|
183
256
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
html +=
|
|
195
|
-
}
|
|
196
|
-
else if (line.startsWith("-")) {
|
|
197
|
-
html += `<div class="diff-line-old">${escapeHtml(line)}</div>`;
|
|
257
|
+
out += "</div>";
|
|
258
|
+
return out;
|
|
259
|
+
};
|
|
260
|
+
switch (toolName) {
|
|
261
|
+
case "bash": {
|
|
262
|
+
const command = args?.command || "";
|
|
263
|
+
html = `<div class="tool-command">$ ${escapeHtml(command || "...")}</div>`;
|
|
264
|
+
if (result) {
|
|
265
|
+
const output = getTextOutput().trim();
|
|
266
|
+
if (output) {
|
|
267
|
+
html += formatExpandableOutput(output.split("\n"), 5);
|
|
198
268
|
}
|
|
199
|
-
|
|
200
|
-
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case "read": {
|
|
273
|
+
const path = shortenPath(args?.file_path || args?.path || "");
|
|
274
|
+
html = `<div class="tool-header"><span class="tool-name">read</span> <span class="tool-path">${escapeHtml(path || "...")}</span></div>`;
|
|
275
|
+
if (result) {
|
|
276
|
+
const output = getTextOutput();
|
|
277
|
+
if (output) {
|
|
278
|
+
html += formatExpandableOutput(output.split("\n"), 10);
|
|
201
279
|
}
|
|
202
280
|
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "write": {
|
|
284
|
+
const path = shortenPath(args?.file_path || args?.path || "");
|
|
285
|
+
const fileContent = args?.content || "";
|
|
286
|
+
const lines = fileContent ? fileContent.split("\n") : [];
|
|
287
|
+
html = `<div class="tool-header"><span class="tool-name">write</span> <span class="tool-path">${escapeHtml(path || "...")}</span>`;
|
|
288
|
+
if (lines.length > 10) {
|
|
289
|
+
html += ` <span class="line-count">(${lines.length} lines)</span>`;
|
|
290
|
+
}
|
|
203
291
|
html += "</div>";
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const output = getTextOutput().trim();
|
|
207
|
-
if (output) {
|
|
208
|
-
html += `<div class="tool-output"><div>${escapeHtml(output)}</div></div>`;
|
|
292
|
+
if (fileContent) {
|
|
293
|
+
html += formatExpandableOutput(lines, 10);
|
|
209
294
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
295
|
+
if (result) {
|
|
296
|
+
const output = getTextOutput().trim();
|
|
297
|
+
if (output) {
|
|
298
|
+
html += `<div class="tool-output"><div>${escapeHtml(output)}</div></div>`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case "edit": {
|
|
304
|
+
const path = shortenPath(args?.file_path || args?.path || "");
|
|
305
|
+
html = `<div class="tool-header"><span class="tool-name">edit</span> <span class="tool-path">${escapeHtml(path || "...")}</span></div>`;
|
|
306
|
+
if (result?.details?.diff) {
|
|
307
|
+
const diffLines = result.details.diff.split("\n");
|
|
308
|
+
html += '<div class="tool-diff">';
|
|
309
|
+
for (const line of diffLines) {
|
|
310
|
+
if (line.startsWith("+")) {
|
|
311
|
+
html += `<div class="diff-line-new">${escapeHtml(line)}</div>`;
|
|
312
|
+
}
|
|
313
|
+
else if (line.startsWith("-")) {
|
|
314
|
+
html += `<div class="diff-line-old">${escapeHtml(line)}</div>`;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
html += `<div class="diff-line-context">${escapeHtml(line)}</div>`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
html += "</div>";
|
|
321
|
+
}
|
|
322
|
+
if (result) {
|
|
323
|
+
const output = getTextOutput().trim();
|
|
324
|
+
if (output) {
|
|
325
|
+
html += `<div class="tool-output"><div>${escapeHtml(output)}</div></div>`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
default: {
|
|
331
|
+
html = `<div class="tool-header"><span class="tool-name">${escapeHtml(toolName)}</span></div>`;
|
|
332
|
+
html += `<div class="tool-output"><pre>${escapeHtml(JSON.stringify(args, null, 2))}</pre></div>`;
|
|
333
|
+
if (result) {
|
|
334
|
+
const output = getTextOutput();
|
|
335
|
+
if (output) {
|
|
336
|
+
html += `<div class="tool-output"><div>${escapeHtml(output)}</div></div>`;
|
|
337
|
+
}
|
|
220
338
|
}
|
|
221
339
|
}
|
|
222
340
|
}
|
|
223
341
|
return { html, bgColor };
|
|
224
342
|
}
|
|
225
|
-
/**
|
|
226
|
-
* Format timestamp for display
|
|
227
|
-
*/
|
|
228
|
-
function formatTimestamp(timestamp) {
|
|
229
|
-
if (!timestamp)
|
|
230
|
-
return "";
|
|
231
|
-
const date = new Date(typeof timestamp === "string" ? timestamp : timestamp);
|
|
232
|
-
return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Format model change event
|
|
236
|
-
*/
|
|
237
|
-
function formatModelChange(event) {
|
|
238
|
-
const timestamp = formatTimestamp(event.timestamp);
|
|
239
|
-
const timestampHtml = timestamp ? `<div class="message-timestamp">${timestamp}</div>` : "";
|
|
240
|
-
const modelInfo = `${event.provider}/${event.modelId}`;
|
|
241
|
-
return `<div class="model-change">${timestampHtml}<div class="model-change-text">Switched to model: <span class="model-name">${escapeHtml(modelInfo)}</span></div></div>`;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Format a message as HTML (matching TUI component styling)
|
|
245
|
-
*/
|
|
246
343
|
function formatMessage(message, toolResultsMap) {
|
|
247
344
|
let html = "";
|
|
248
345
|
const timestamp = message.timestamp;
|
|
@@ -264,7 +361,6 @@ function formatMessage(message, toolResultsMap) {
|
|
|
264
361
|
else if (message.role === "assistant") {
|
|
265
362
|
const assistantMsg = message;
|
|
266
363
|
html += timestampHtml ? `<div class="assistant-message">${timestampHtml}` : "";
|
|
267
|
-
// Render text and thinking content
|
|
268
364
|
for (const content of assistantMsg.content) {
|
|
269
365
|
if (content.type === "text" && content.text.trim()) {
|
|
270
366
|
html += `<div class="assistant-text">${escapeHtml(content.text.trim()).replace(/\n/g, "<br>")}</div>`;
|
|
@@ -273,7 +369,6 @@ function formatMessage(message, toolResultsMap) {
|
|
|
273
369
|
html += `<div class="thinking-text">${escapeHtml(content.thinking.trim()).replace(/\n/g, "<br>")}</div>`;
|
|
274
370
|
}
|
|
275
371
|
}
|
|
276
|
-
// Render tool calls with their results
|
|
277
372
|
for (const content of assistantMsg.content) {
|
|
278
373
|
if (content.type === "toolCall") {
|
|
279
374
|
const toolResult = toolResultsMap.get(content.id);
|
|
@@ -281,158 +376,121 @@ function formatMessage(message, toolResultsMap) {
|
|
|
281
376
|
html += `<div class="tool-execution" style="background-color: ${bgColor}">${toolHtml}</div>`;
|
|
282
377
|
}
|
|
283
378
|
}
|
|
284
|
-
// Show error/abort status if no tool calls
|
|
285
379
|
const hasToolCalls = assistantMsg.content.some((c) => c.type === "toolCall");
|
|
286
380
|
if (!hasToolCalls) {
|
|
287
381
|
if (assistantMsg.stopReason === "aborted") {
|
|
288
382
|
html += '<div class="error-text">Aborted</div>';
|
|
289
383
|
}
|
|
290
384
|
else if (assistantMsg.stopReason === "error") {
|
|
291
|
-
|
|
292
|
-
html += `<div class="error-text">Error: ${escapeHtml(errorMsg)}</div>`;
|
|
385
|
+
html += `<div class="error-text">Error: ${escapeHtml(assistantMsg.errorMessage || "Unknown error")}</div>`;
|
|
293
386
|
}
|
|
294
387
|
}
|
|
295
|
-
// Close the assistant message wrapper if we opened one
|
|
296
388
|
if (timestampHtml) {
|
|
297
389
|
html += "</div>";
|
|
298
390
|
}
|
|
299
391
|
}
|
|
300
392
|
return html;
|
|
301
393
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
const entry = JSON.parse(line);
|
|
337
|
-
if (entry.type === "session") {
|
|
338
|
-
sessionHeader = entry;
|
|
339
|
-
// Track initial model from session header
|
|
340
|
-
if (entry.modelId) {
|
|
341
|
-
const modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : entry.modelId;
|
|
342
|
-
modelsUsed.add(modelInfo);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
else if (entry.type === "message") {
|
|
346
|
-
messages.push(entry.message);
|
|
347
|
-
sessionEvents.push(entry);
|
|
348
|
-
// Build map of tool call ID to result
|
|
349
|
-
if (entry.message.role === "toolResult") {
|
|
350
|
-
toolResultsMap.set(entry.message.toolCallId, entry.message);
|
|
351
|
-
}
|
|
352
|
-
// Accumulate token and cost stats from assistant messages
|
|
353
|
-
if (entry.message.role === "assistant" && entry.message.usage) {
|
|
354
|
-
const usage = entry.message.usage;
|
|
355
|
-
tokenStats.input += usage.input || 0;
|
|
356
|
-
tokenStats.output += usage.output || 0;
|
|
357
|
-
tokenStats.cacheRead += usage.cacheRead || 0;
|
|
358
|
-
tokenStats.cacheWrite += usage.cacheWrite || 0;
|
|
359
|
-
if (usage.cost) {
|
|
360
|
-
costStats.input += usage.cost.input || 0;
|
|
361
|
-
costStats.output += usage.cost.output || 0;
|
|
362
|
-
costStats.cacheRead += usage.cost.cacheRead || 0;
|
|
363
|
-
costStats.cacheWrite += usage.cost.cacheWrite || 0;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
else if (entry.type === "model_change") {
|
|
368
|
-
sessionEvents.push(entry);
|
|
369
|
-
// Track model from model change event
|
|
370
|
-
if (entry.modelId) {
|
|
371
|
-
const modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : entry.modelId;
|
|
372
|
-
modelsUsed.add(modelInfo);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
catch {
|
|
377
|
-
// Skip malformed lines
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
// Calculate message stats (matching session command)
|
|
381
|
-
const userMessages = messages.filter((m) => m.role === "user").length;
|
|
382
|
-
const assistantMessages = messages.filter((m) => m.role === "assistant").length;
|
|
383
|
-
const toolResultMessages = messages.filter((m) => m.role === "toolResult").length;
|
|
384
|
-
const totalMessages = messages.length;
|
|
385
|
-
// Count tool calls from assistant messages
|
|
394
|
+
function formatModelChange(event) {
|
|
395
|
+
const timestamp = formatTimestamp(event.timestamp);
|
|
396
|
+
const timestampHtml = timestamp ? `<div class="message-timestamp">${timestamp}</div>` : "";
|
|
397
|
+
const modelInfo = `${event.provider}/${event.modelId}`;
|
|
398
|
+
return `<div class="model-change">${timestampHtml}<div class="model-change-text">Switched to model: <span class="model-name">${escapeHtml(modelInfo)}</span></div></div>`;
|
|
399
|
+
}
|
|
400
|
+
function formatCompaction(event) {
|
|
401
|
+
const timestamp = formatTimestamp(event.timestamp);
|
|
402
|
+
const timestampHtml = timestamp ? `<div class="message-timestamp">${timestamp}</div>` : "";
|
|
403
|
+
const summaryHtml = escapeHtml(event.summary).replace(/\n/g, "<br>");
|
|
404
|
+
return `<div class="compaction-container">
|
|
405
|
+
<div class="compaction-header" onclick="this.parentElement.classList.toggle('expanded')">
|
|
406
|
+
${timestampHtml}
|
|
407
|
+
<div class="compaction-header-row">
|
|
408
|
+
<span class="compaction-toggle">▶</span>
|
|
409
|
+
<span class="compaction-title">Context compacted from ${event.tokensBefore.toLocaleString()} tokens</span>
|
|
410
|
+
<span class="compaction-hint">(click to expand summary)</span>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="compaction-content">
|
|
414
|
+
<div class="compaction-summary">
|
|
415
|
+
<div class="compaction-summary-header">Summary sent to model</div>
|
|
416
|
+
<div class="compaction-summary-content">${summaryHtml}</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>`;
|
|
420
|
+
}
|
|
421
|
+
// ============================================================================
|
|
422
|
+
// HTML generation
|
|
423
|
+
// ============================================================================
|
|
424
|
+
function generateHtml(data, filename) {
|
|
425
|
+
const userMessages = data.messages.filter((m) => m.role === "user").length;
|
|
426
|
+
const assistantMessages = data.messages.filter((m) => m.role === "assistant").length;
|
|
386
427
|
let toolCallsCount = 0;
|
|
387
|
-
for (const message of messages) {
|
|
428
|
+
for (const message of data.messages) {
|
|
388
429
|
if (message.role === "assistant") {
|
|
389
|
-
|
|
390
|
-
toolCallsCount += assistantMsg.content.filter((c) => c.type === "toolCall").length;
|
|
430
|
+
toolCallsCount += message.content.filter((c) => c.type === "toolCall").length;
|
|
391
431
|
}
|
|
392
432
|
}
|
|
393
|
-
|
|
394
|
-
const lastAssistantMessage = messages
|
|
433
|
+
const lastAssistantMessage = data.messages
|
|
395
434
|
.slice()
|
|
396
435
|
.reverse()
|
|
397
436
|
.find((m) => m.role === "assistant" && m.stopReason !== "aborted");
|
|
398
|
-
// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)
|
|
399
437
|
const contextTokens = lastAssistantMessage
|
|
400
438
|
? lastAssistantMessage.usage.input +
|
|
401
439
|
lastAssistantMessage.usage.output +
|
|
402
440
|
lastAssistantMessage.usage.cacheRead +
|
|
403
441
|
lastAssistantMessage.usage.cacheWrite
|
|
404
442
|
: 0;
|
|
405
|
-
|
|
406
|
-
const lastModel = lastAssistantMessage?.model || state.model?.id || "unknown";
|
|
443
|
+
const lastModel = lastAssistantMessage?.model || "unknown";
|
|
407
444
|
const lastProvider = lastAssistantMessage?.provider || "";
|
|
408
445
|
const lastModelInfo = lastProvider ? `${lastProvider}/${lastModel}` : lastModel;
|
|
409
|
-
const contextWindow =
|
|
410
|
-
const contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) :
|
|
411
|
-
// Generate messages HTML (including model changes in chronological order)
|
|
446
|
+
const contextWindow = data.contextWindow || 0;
|
|
447
|
+
const contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : null;
|
|
412
448
|
let messagesHtml = "";
|
|
413
|
-
for (const event of sessionEvents) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
449
|
+
for (const event of data.sessionEvents) {
|
|
450
|
+
switch (event.type) {
|
|
451
|
+
case "message":
|
|
452
|
+
if (event.message.role !== "toolResult") {
|
|
453
|
+
messagesHtml += formatMessage(event.message, data.toolResultsMap);
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
case "model_change":
|
|
457
|
+
messagesHtml += formatModelChange(event);
|
|
458
|
+
break;
|
|
459
|
+
case "compaction":
|
|
460
|
+
messagesHtml += formatCompaction(event);
|
|
461
|
+
break;
|
|
420
462
|
}
|
|
421
463
|
}
|
|
422
|
-
|
|
423
|
-
|
|
464
|
+
const systemPromptHtml = data.systemPrompt
|
|
465
|
+
? `<div class="system-prompt">
|
|
466
|
+
<div class="system-prompt-header">System Prompt</div>
|
|
467
|
+
<div class="system-prompt-content">${escapeHtml(data.systemPrompt)}</div>
|
|
468
|
+
</div>`
|
|
469
|
+
: "";
|
|
470
|
+
const toolsHtml = data.tools
|
|
471
|
+
? `<div class="tools-list">
|
|
472
|
+
<div class="tools-header">Available Tools</div>
|
|
473
|
+
<div class="tools-content">
|
|
474
|
+
${data.tools.map((tool) => `<div class="tool-item"><span class="tool-item-name">${escapeHtml(tool.name)}</span> - ${escapeHtml(tool.description)}</div>`).join("")}
|
|
475
|
+
</div>
|
|
476
|
+
</div>`
|
|
477
|
+
: "";
|
|
478
|
+
const streamingNotice = data.isStreamingFormat
|
|
479
|
+
? `<div class="streaming-notice">
|
|
480
|
+
<em>Note: This session was reconstructed from raw agent event logs, which do not contain system prompt or tool definitions.</em>
|
|
481
|
+
</div>`
|
|
482
|
+
: "";
|
|
483
|
+
const contextUsageText = contextPercent
|
|
484
|
+
? `${contextTokens.toLocaleString()} / ${contextWindow.toLocaleString()} tokens (${contextPercent}%) - ${escapeHtml(lastModelInfo)}`
|
|
485
|
+
: `${contextTokens.toLocaleString()} tokens (last turn) - ${escapeHtml(lastModelInfo)}`;
|
|
486
|
+
return `<!DOCTYPE html>
|
|
424
487
|
<html lang="en">
|
|
425
488
|
<head>
|
|
426
489
|
<meta charset="UTF-8">
|
|
427
490
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
428
|
-
<title>Session Export - ${
|
|
491
|
+
<title>Session Export - ${escapeHtml(filename)}</title>
|
|
429
492
|
<style>
|
|
430
|
-
* {
|
|
431
|
-
margin: 0;
|
|
432
|
-
padding: 0;
|
|
433
|
-
box-sizing: border-box;
|
|
434
|
-
}
|
|
435
|
-
|
|
493
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
436
494
|
body {
|
|
437
495
|
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
|
438
496
|
font-size: 12px;
|
|
@@ -441,69 +499,26 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
|
|
|
441
499
|
background: ${COLORS.bodyBg};
|
|
442
500
|
padding: 24px;
|
|
443
501
|
}
|
|
444
|
-
|
|
445
|
-
.container {
|
|
446
|
-
max-width: 700px;
|
|
447
|
-
margin: 0 auto;
|
|
448
|
-
}
|
|
449
|
-
|
|
502
|
+
.container { max-width: 700px; margin: 0 auto; }
|
|
450
503
|
.header {
|
|
451
504
|
margin-bottom: 24px;
|
|
452
505
|
padding: 16px;
|
|
453
506
|
background: ${COLORS.containerBg};
|
|
454
507
|
border-radius: 4px;
|
|
455
508
|
}
|
|
456
|
-
|
|
457
509
|
.header h1 {
|
|
458
510
|
font-size: 14px;
|
|
459
511
|
font-weight: bold;
|
|
460
512
|
margin-bottom: 12px;
|
|
461
513
|
color: ${COLORS.cyan};
|
|
462
514
|
}
|
|
463
|
-
|
|
464
|
-
.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
.info-item {
|
|
472
|
-
color: ${COLORS.textDim};
|
|
473
|
-
display: flex;
|
|
474
|
-
align-items: baseline;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
.info-label {
|
|
478
|
-
font-weight: 600;
|
|
479
|
-
margin-right: 8px;
|
|
480
|
-
min-width: 100px;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
.info-value {
|
|
484
|
-
color: ${COLORS.text};
|
|
485
|
-
flex: 1;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
.info-value.cost {
|
|
489
|
-
font-family: 'SF Mono', monospace;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
.messages {
|
|
493
|
-
display: flex;
|
|
494
|
-
flex-direction: column;
|
|
495
|
-
gap: 16px;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/* Message timestamp */
|
|
499
|
-
.message-timestamp {
|
|
500
|
-
font-size: 10px;
|
|
501
|
-
color: ${COLORS.textDim};
|
|
502
|
-
margin-bottom: 4px;
|
|
503
|
-
opacity: 0.8;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/* User message - matching TUI UserMessageComponent */
|
|
515
|
+
.header-info { display: flex; flex-direction: column; gap: 3px; font-size: 11px; }
|
|
516
|
+
.info-item { color: ${COLORS.textDim}; display: flex; align-items: baseline; }
|
|
517
|
+
.info-label { font-weight: 600; margin-right: 8px; min-width: 100px; }
|
|
518
|
+
.info-value { color: ${COLORS.text}; flex: 1; }
|
|
519
|
+
.info-value.cost { font-family: 'SF Mono', monospace; }
|
|
520
|
+
.messages { display: flex; flex-direction: column; gap: 16px; }
|
|
521
|
+
.message-timestamp { font-size: 10px; color: ${COLORS.textDim}; margin-bottom: 4px; opacity: 0.8; }
|
|
507
522
|
.user-message {
|
|
508
523
|
background: ${COLORS.userMessageBg};
|
|
509
524
|
padding: 12px 16px;
|
|
@@ -513,1011 +528,162 @@ export function exportSessionToHtml(sessionManager, state, outputPath) {
|
|
|
513
528
|
overflow-wrap: break-word;
|
|
514
529
|
word-break: break-word;
|
|
515
530
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
.assistant-message {
|
|
519
|
-
padding: 0;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/* Assistant text - matching TUI AssistantMessageComponent */
|
|
523
|
-
.assistant-text {
|
|
531
|
+
.assistant-message { padding: 0; }
|
|
532
|
+
.assistant-text, .thinking-text {
|
|
524
533
|
padding: 12px 16px;
|
|
525
534
|
white-space: pre-wrap;
|
|
526
535
|
word-wrap: break-word;
|
|
527
536
|
overflow-wrap: break-word;
|
|
528
537
|
word-break: break-word;
|
|
529
538
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
.
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
539
|
+
.thinking-text { color: ${COLORS.textDim}; font-style: italic; }
|
|
540
|
+
.model-change { padding: 8px 16px; background: rgb(40, 40, 50); border-radius: 4px; }
|
|
541
|
+
.model-change-text { color: ${COLORS.textDim}; font-size: 11px; }
|
|
542
|
+
.model-name { color: ${COLORS.cyan}; font-weight: bold; }
|
|
543
|
+
.compaction-container { background: rgb(60, 55, 35); border-radius: 4px; overflow: hidden; }
|
|
544
|
+
.compaction-header { padding: 12px 16px; cursor: pointer; }
|
|
545
|
+
.compaction-header:hover { background: rgba(255, 255, 255, 0.05); }
|
|
546
|
+
.compaction-header-row { display: flex; align-items: center; gap: 8px; }
|
|
547
|
+
.compaction-toggle { color: ${COLORS.cyan}; font-size: 10px; transition: transform 0.2s; }
|
|
548
|
+
.compaction-container.expanded .compaction-toggle { transform: rotate(90deg); }
|
|
549
|
+
.compaction-title { color: ${COLORS.text}; font-weight: bold; }
|
|
550
|
+
.compaction-hint { color: ${COLORS.textDim}; font-size: 11px; }
|
|
551
|
+
.compaction-content { display: none; padding: 0 16px 16px 16px; }
|
|
552
|
+
.compaction-container.expanded .compaction-content { display: block; }
|
|
553
|
+
.compaction-summary { background: rgba(0, 0, 0, 0.2); border-radius: 4px; padding: 12px; }
|
|
554
|
+
.compaction-summary-header { font-weight: bold; color: ${COLORS.cyan}; margin-bottom: 8px; font-size: 11px; }
|
|
555
|
+
.compaction-summary-content { color: ${COLORS.text}; white-space: pre-wrap; word-wrap: break-word; }
|
|
556
|
+
.tool-execution { padding: 12px 16px; border-radius: 4px; margin-top: 8px; }
|
|
557
|
+
.tool-header, .tool-name { font-weight: bold; }
|
|
558
|
+
.tool-path { color: ${COLORS.cyan}; word-break: break-all; }
|
|
559
|
+
.line-count { color: ${COLORS.textDim}; }
|
|
560
|
+
.tool-command { font-weight: bold; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; }
|
|
561
|
+
.tool-output {
|
|
562
|
+
margin-top: 12px;
|
|
563
|
+
color: ${COLORS.textDim};
|
|
536
564
|
white-space: pre-wrap;
|
|
537
565
|
word-wrap: break-word;
|
|
538
566
|
overflow-wrap: break-word;
|
|
539
567
|
word-break: break-word;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/* Model change */
|
|
543
|
-
.model-change {
|
|
544
|
-
padding: 8px 16px;
|
|
545
|
-
background: rgb(40, 40, 50);
|
|
546
|
-
border-radius: 4px;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
.model-change-text {
|
|
550
|
-
color: ${COLORS.textDim};
|
|
551
|
-
font-size: 11px;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
.model-name {
|
|
555
|
-
color: ${COLORS.cyan};
|
|
556
|
-
font-weight: bold;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/* Tool execution - matching TUI ToolExecutionComponent */
|
|
560
|
-
.tool-execution {
|
|
561
|
-
padding: 12px 16px;
|
|
562
|
-
border-radius: 4px;
|
|
563
|
-
margin-top: 8px;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
.tool-header {
|
|
567
|
-
font-weight: bold;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
.tool-name {
|
|
571
|
-
font-weight: bold;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
.tool-path {
|
|
575
|
-
color: ${COLORS.cyan};
|
|
576
|
-
word-break: break-all;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
.line-count {
|
|
580
|
-
color: ${COLORS.textDim};
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
.tool-command {
|
|
584
|
-
font-weight: bold;
|
|
585
|
-
white-space: pre-wrap;
|
|
586
|
-
word-wrap: break-word;
|
|
587
|
-
overflow-wrap: break-word;
|
|
588
|
-
word-break: break-word;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
.tool-output {
|
|
592
|
-
margin-top: 12px;
|
|
593
|
-
color: ${COLORS.textDim};
|
|
594
|
-
white-space: pre-wrap;
|
|
595
|
-
word-wrap: break-word;
|
|
596
|
-
overflow-wrap: break-word;
|
|
597
|
-
word-break: break-word;
|
|
598
|
-
font-family: inherit;
|
|
599
|
-
overflow-x: auto;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
.tool-output > div {
|
|
603
|
-
line-height: 1.4;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
.tool-output pre {
|
|
607
|
-
margin: 0;
|
|
608
|
-
font-family: inherit;
|
|
609
|
-
color: inherit;
|
|
610
|
-
white-space: pre-wrap;
|
|
611
|
-
word-wrap: break-word;
|
|
612
|
-
overflow-wrap: break-word;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/* Expandable tool output */
|
|
616
|
-
.tool-output.expandable {
|
|
617
|
-
cursor: pointer;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
.tool-output.expandable:hover {
|
|
621
|
-
opacity: 0.9;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
.tool-output.expandable .output-full {
|
|
625
|
-
display: none;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
.tool-output.expandable.expanded .output-preview {
|
|
629
|
-
display: none;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
.tool-output.expandable.expanded .output-full {
|
|
633
|
-
display: block;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
.expand-hint {
|
|
637
|
-
color: ${COLORS.cyan};
|
|
638
|
-
font-style: italic;
|
|
639
|
-
margin-top: 4px;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/* System prompt section */
|
|
643
|
-
.system-prompt {
|
|
644
|
-
background: rgb(60, 55, 40);
|
|
645
|
-
padding: 12px 16px;
|
|
646
|
-
border-radius: 4px;
|
|
647
|
-
margin-bottom: 16px;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
.system-prompt-header {
|
|
651
|
-
font-weight: bold;
|
|
652
|
-
color: ${COLORS.yellow};
|
|
653
|
-
margin-bottom: 8px;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
.system-prompt-content {
|
|
657
|
-
color: ${COLORS.textDim};
|
|
658
|
-
white-space: pre-wrap;
|
|
659
|
-
word-wrap: break-word;
|
|
660
|
-
overflow-wrap: break-word;
|
|
661
|
-
word-break: break-word;
|
|
662
|
-
font-size: 11px;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
.tools-list {
|
|
666
|
-
background: rgb(60, 55, 40);
|
|
667
|
-
padding: 12px 16px;
|
|
668
|
-
border-radius: 4px;
|
|
669
|
-
margin-bottom: 16px;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
.tools-header {
|
|
673
|
-
font-weight: bold;
|
|
674
|
-
color: ${COLORS.yellow};
|
|
675
|
-
margin-bottom: 8px;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
.tools-content {
|
|
679
|
-
color: ${COLORS.textDim};
|
|
680
|
-
font-size: 11px;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
.tool-item {
|
|
684
|
-
margin: 4px 0;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
.tool-item-name {
|
|
688
|
-
font-weight: bold;
|
|
689
|
-
color: ${COLORS.text};
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/* Diff styling */
|
|
693
|
-
.tool-diff {
|
|
694
|
-
margin-top: 12px;
|
|
695
|
-
font-size: 11px;
|
|
696
|
-
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
|
697
|
-
overflow-x: auto;
|
|
698
|
-
max-width: 100%;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
.diff-line-old {
|
|
702
|
-
color: ${COLORS.red};
|
|
703
|
-
white-space: pre-wrap;
|
|
704
|
-
word-wrap: break-word;
|
|
705
|
-
overflow-wrap: break-word;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
.diff-line-new {
|
|
709
|
-
color: ${COLORS.green};
|
|
710
|
-
white-space: pre-wrap;
|
|
711
|
-
word-wrap: break-word;
|
|
712
|
-
overflow-wrap: break-word;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
.diff-line-context {
|
|
716
|
-
color: ${COLORS.textDim};
|
|
717
|
-
white-space: pre-wrap;
|
|
718
|
-
word-wrap: break-word;
|
|
719
|
-
overflow-wrap: break-word;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
/* Error text */
|
|
723
|
-
.error-text {
|
|
724
|
-
color: ${COLORS.red};
|
|
725
|
-
padding: 12px 16px;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
.footer {
|
|
729
|
-
margin-top: 48px;
|
|
730
|
-
padding: 20px;
|
|
731
|
-
text-align: center;
|
|
732
|
-
color: ${COLORS.textDim};
|
|
733
|
-
font-size: 10px;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
@media print {
|
|
737
|
-
body {
|
|
738
|
-
background: white;
|
|
739
|
-
color: black;
|
|
740
|
-
}
|
|
741
|
-
.tool-execution {
|
|
742
|
-
border: 1px solid #ddd;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
</style>
|
|
746
|
-
</head>
|
|
747
|
-
<body>
|
|
748
|
-
<div class="container">
|
|
749
|
-
<div class="header">
|
|
750
|
-
<h1>pi v${VERSION}</h1>
|
|
751
|
-
<div class="header-info">
|
|
752
|
-
<div class="info-item">
|
|
753
|
-
<span class="info-label">Session:</span>
|
|
754
|
-
<span class="info-value">${escapeHtml(sessionHeader?.id || "unknown")}</span>
|
|
755
|
-
</div>
|
|
756
|
-
<div class="info-item">
|
|
757
|
-
<span class="info-label">Date:</span>
|
|
758
|
-
<span class="info-value">${sessionHeader?.timestamp ? new Date(sessionHeader.timestamp).toLocaleString() : timestamp}</span>
|
|
759
|
-
</div>
|
|
760
|
-
<div class="info-item">
|
|
761
|
-
<span class="info-label">Models:</span>
|
|
762
|
-
<span class="info-value">${Array.from(modelsUsed)
|
|
763
|
-
.map((m) => escapeHtml(m))
|
|
764
|
-
.join(", ") || escapeHtml(sessionHeader?.model || state.model.id)}</span>
|
|
765
|
-
</div>
|
|
766
|
-
</div>
|
|
767
|
-
</div>
|
|
768
|
-
|
|
769
|
-
<div class="header">
|
|
770
|
-
<h1>Messages</h1>
|
|
771
|
-
<div class="header-info">
|
|
772
|
-
<div class="info-item">
|
|
773
|
-
<span class="info-label">User:</span>
|
|
774
|
-
<span class="info-value">${userMessages}</span>
|
|
775
|
-
</div>
|
|
776
|
-
<div class="info-item">
|
|
777
|
-
<span class="info-label">Assistant:</span>
|
|
778
|
-
<span class="info-value">${assistantMessages}</span>
|
|
779
|
-
</div>
|
|
780
|
-
<div class="info-item">
|
|
781
|
-
<span class="info-label">Tool Calls:</span>
|
|
782
|
-
<span class="info-value">${toolCallsCount}</span>
|
|
783
|
-
</div>
|
|
784
|
-
</div>
|
|
785
|
-
</div>
|
|
786
|
-
|
|
787
|
-
<div class="header">
|
|
788
|
-
<h1>Tokens & Cost</h1>
|
|
789
|
-
<div class="header-info">
|
|
790
|
-
<div class="info-item">
|
|
791
|
-
<span class="info-label">Input:</span>
|
|
792
|
-
<span class="info-value">${tokenStats.input.toLocaleString()} tokens</span>
|
|
793
|
-
</div>
|
|
794
|
-
<div class="info-item">
|
|
795
|
-
<span class="info-label">Output:</span>
|
|
796
|
-
<span class="info-value">${tokenStats.output.toLocaleString()} tokens</span>
|
|
797
|
-
</div>
|
|
798
|
-
<div class="info-item">
|
|
799
|
-
<span class="info-label">Cache Read:</span>
|
|
800
|
-
<span class="info-value">${tokenStats.cacheRead.toLocaleString()} tokens</span>
|
|
801
|
-
</div>
|
|
802
|
-
<div class="info-item">
|
|
803
|
-
<span class="info-label">Cache Write:</span>
|
|
804
|
-
<span class="info-value">${tokenStats.cacheWrite.toLocaleString()} tokens</span>
|
|
805
|
-
</div>
|
|
806
|
-
<div class="info-item">
|
|
807
|
-
<span class="info-label">Total:</span>
|
|
808
|
-
<span class="info-value">${(tokenStats.input + tokenStats.output + tokenStats.cacheRead + tokenStats.cacheWrite).toLocaleString()} tokens</span>
|
|
809
|
-
</div>
|
|
810
|
-
<div class="info-item">
|
|
811
|
-
<span class="info-label">Input Cost:</span>
|
|
812
|
-
<span class="info-value cost">$${costStats.input.toFixed(4)}</span>
|
|
813
|
-
</div>
|
|
814
|
-
<div class="info-item">
|
|
815
|
-
<span class="info-label">Output Cost:</span>
|
|
816
|
-
<span class="info-value cost">$${costStats.output.toFixed(4)}</span>
|
|
817
|
-
</div>
|
|
818
|
-
<div class="info-item">
|
|
819
|
-
<span class="info-label">Cache Read Cost:</span>
|
|
820
|
-
<span class="info-value cost">$${costStats.cacheRead.toFixed(4)}</span>
|
|
821
|
-
</div>
|
|
822
|
-
<div class="info-item">
|
|
823
|
-
<span class="info-label">Cache Write Cost:</span>
|
|
824
|
-
<span class="info-value cost">$${costStats.cacheWrite.toFixed(4)}</span>
|
|
825
|
-
</div>
|
|
826
|
-
<div class="info-item">
|
|
827
|
-
<span class="info-label">Total Cost:</span>
|
|
828
|
-
<span class="info-value cost"><strong>$${(costStats.input + costStats.output + costStats.cacheRead + costStats.cacheWrite).toFixed(4)}</strong></span>
|
|
829
|
-
</div>
|
|
830
|
-
<div class="info-item">
|
|
831
|
-
<span class="info-label">Context Usage:</span>
|
|
832
|
-
<span class="info-value">${contextTokens.toLocaleString()} / ${contextWindow.toLocaleString()} tokens (${contextPercent}%) - ${escapeHtml(lastModelInfo)}</span>
|
|
833
|
-
</div>
|
|
834
|
-
</div>
|
|
835
|
-
</div>
|
|
836
|
-
|
|
837
|
-
<div class="system-prompt">
|
|
838
|
-
<div class="system-prompt-header">System Prompt</div>
|
|
839
|
-
<div class="system-prompt-content">${escapeHtml(sessionHeader?.systemPrompt || state.systemPrompt)}</div>
|
|
840
|
-
</div>
|
|
841
|
-
|
|
842
|
-
<div class="tools-list">
|
|
843
|
-
<div class="tools-header">Available Tools</div>
|
|
844
|
-
<div class="tools-content">
|
|
845
|
-
${state.tools
|
|
846
|
-
.map((tool) => `<div class="tool-item"><span class="tool-item-name">${escapeHtml(tool.name)}</span> - ${escapeHtml(tool.description)}</div>`)
|
|
847
|
-
.join("")}
|
|
848
|
-
</div>
|
|
849
|
-
</div>
|
|
850
|
-
|
|
851
|
-
<div class="messages">
|
|
852
|
-
${messagesHtml}
|
|
853
|
-
</div>
|
|
854
|
-
|
|
855
|
-
<div class="footer">
|
|
856
|
-
Generated by pi coding-agent on ${new Date().toLocaleString()}
|
|
857
|
-
</div>
|
|
858
|
-
</div>
|
|
859
|
-
</body>
|
|
860
|
-
</html>`;
|
|
861
|
-
// Write HTML file
|
|
862
|
-
writeFileSync(outputPath, html, "utf8");
|
|
863
|
-
return outputPath;
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Parse session manager format (type: "session", "message", "model_change")
|
|
867
|
-
*/
|
|
868
|
-
function parseSessionManagerFormat(lines) {
|
|
869
|
-
const data = {
|
|
870
|
-
sessionId: "unknown",
|
|
871
|
-
timestamp: new Date().toISOString(),
|
|
872
|
-
modelsUsed: new Set(),
|
|
873
|
-
messages: [],
|
|
874
|
-
toolResultsMap: new Map(),
|
|
875
|
-
sessionEvents: [],
|
|
876
|
-
tokenStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
877
|
-
costStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
878
|
-
};
|
|
879
|
-
for (const line of lines) {
|
|
880
|
-
try {
|
|
881
|
-
const entry = JSON.parse(line);
|
|
882
|
-
if (entry.type === "session") {
|
|
883
|
-
data.sessionId = entry.id || "unknown";
|
|
884
|
-
data.timestamp = entry.timestamp || data.timestamp;
|
|
885
|
-
data.cwd = entry.cwd;
|
|
886
|
-
data.systemPrompt = entry.systemPrompt;
|
|
887
|
-
if (entry.modelId) {
|
|
888
|
-
const modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : entry.modelId;
|
|
889
|
-
data.modelsUsed.add(modelInfo);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
else if (entry.type === "message") {
|
|
893
|
-
data.messages.push(entry.message);
|
|
894
|
-
data.sessionEvents.push(entry);
|
|
895
|
-
if (entry.message.role === "toolResult") {
|
|
896
|
-
data.toolResultsMap.set(entry.message.toolCallId, entry.message);
|
|
897
|
-
}
|
|
898
|
-
if (entry.message.role === "assistant" && entry.message.usage) {
|
|
899
|
-
const usage = entry.message.usage;
|
|
900
|
-
data.tokenStats.input += usage.input || 0;
|
|
901
|
-
data.tokenStats.output += usage.output || 0;
|
|
902
|
-
data.tokenStats.cacheRead += usage.cacheRead || 0;
|
|
903
|
-
data.tokenStats.cacheWrite += usage.cacheWrite || 0;
|
|
904
|
-
if (usage.cost) {
|
|
905
|
-
data.costStats.input += usage.cost.input || 0;
|
|
906
|
-
data.costStats.output += usage.cost.output || 0;
|
|
907
|
-
data.costStats.cacheRead += usage.cost.cacheRead || 0;
|
|
908
|
-
data.costStats.cacheWrite += usage.cost.cacheWrite || 0;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
else if (entry.type === "model_change") {
|
|
913
|
-
data.sessionEvents.push(entry);
|
|
914
|
-
if (entry.modelId) {
|
|
915
|
-
const modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : entry.modelId;
|
|
916
|
-
data.modelsUsed.add(modelInfo);
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
catch {
|
|
921
|
-
// Skip malformed lines
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
return data;
|
|
925
|
-
}
|
|
926
|
-
/**
|
|
927
|
-
* Parse streaming event format (type: "agent_start", "message_start", "message_end", etc.)
|
|
928
|
-
*/
|
|
929
|
-
function parseStreamingEventFormat(lines) {
|
|
930
|
-
const data = {
|
|
931
|
-
sessionId: "unknown",
|
|
932
|
-
timestamp: new Date().toISOString(),
|
|
933
|
-
modelsUsed: new Set(),
|
|
934
|
-
messages: [],
|
|
935
|
-
toolResultsMap: new Map(),
|
|
936
|
-
sessionEvents: [],
|
|
937
|
-
tokenStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
938
|
-
costStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
939
|
-
isStreamingFormat: true,
|
|
940
|
-
};
|
|
941
|
-
let timestampSet = false;
|
|
942
|
-
// Track messages by collecting message_end events (which have the final state)
|
|
943
|
-
for (const line of lines) {
|
|
944
|
-
try {
|
|
945
|
-
const entry = JSON.parse(line);
|
|
946
|
-
if (entry.type === "message_end" && entry.message) {
|
|
947
|
-
const msg = entry.message;
|
|
948
|
-
data.messages.push(msg);
|
|
949
|
-
data.sessionEvents.push({ type: "message", message: msg, timestamp: msg.timestamp });
|
|
950
|
-
// Build tool results map
|
|
951
|
-
if (msg.role === "toolResult") {
|
|
952
|
-
data.toolResultsMap.set(msg.toolCallId, msg);
|
|
953
|
-
}
|
|
954
|
-
// Track models and accumulate stats from assistant messages
|
|
955
|
-
if (msg.role === "assistant") {
|
|
956
|
-
if (msg.model) {
|
|
957
|
-
const modelInfo = msg.provider ? `${msg.provider}/${msg.model}` : msg.model;
|
|
958
|
-
data.modelsUsed.add(modelInfo);
|
|
959
|
-
}
|
|
960
|
-
if (msg.usage) {
|
|
961
|
-
data.tokenStats.input += msg.usage.input || 0;
|
|
962
|
-
data.tokenStats.output += msg.usage.output || 0;
|
|
963
|
-
data.tokenStats.cacheRead += msg.usage.cacheRead || 0;
|
|
964
|
-
data.tokenStats.cacheWrite += msg.usage.cacheWrite || 0;
|
|
965
|
-
if (msg.usage.cost) {
|
|
966
|
-
data.costStats.input += msg.usage.cost.input || 0;
|
|
967
|
-
data.costStats.output += msg.usage.cost.output || 0;
|
|
968
|
-
data.costStats.cacheRead += msg.usage.cost.cacheRead || 0;
|
|
969
|
-
data.costStats.cacheWrite += msg.usage.cost.cacheWrite || 0;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
// Use first message timestamp as session timestamp
|
|
974
|
-
if (!timestampSet && msg.timestamp) {
|
|
975
|
-
data.timestamp = new Date(msg.timestamp).toISOString();
|
|
976
|
-
timestampSet = true;
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
catch {
|
|
981
|
-
// Skip malformed lines
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
// Generate a session ID from the timestamp
|
|
985
|
-
data.sessionId = `stream-${data.timestamp.replace(/[:.]/g, "-")}`;
|
|
986
|
-
return data;
|
|
987
|
-
}
|
|
988
|
-
/**
|
|
989
|
-
* Detect the format of a session file by examining the first valid JSON line
|
|
990
|
-
*/
|
|
991
|
-
function detectFormat(lines) {
|
|
992
|
-
for (const line of lines) {
|
|
993
|
-
try {
|
|
994
|
-
const entry = JSON.parse(line);
|
|
995
|
-
if (entry.type === "session")
|
|
996
|
-
return "session-manager";
|
|
997
|
-
if (entry.type === "agent_start" || entry.type === "message_start" || entry.type === "turn_start") {
|
|
998
|
-
return "streaming-events";
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
catch {
|
|
1002
|
-
// Skip malformed lines
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
return "unknown";
|
|
1006
|
-
}
|
|
1007
|
-
/**
|
|
1008
|
-
* Generate HTML from parsed session data
|
|
1009
|
-
*/
|
|
1010
|
-
function generateHtml(data, inputFilename) {
|
|
1011
|
-
// Calculate message stats
|
|
1012
|
-
const userMessages = data.messages.filter((m) => m.role === "user").length;
|
|
1013
|
-
const assistantMessages = data.messages.filter((m) => m.role === "assistant").length;
|
|
1014
|
-
// Count tool calls from assistant messages
|
|
1015
|
-
let toolCallsCount = 0;
|
|
1016
|
-
for (const message of data.messages) {
|
|
1017
|
-
if (message.role === "assistant") {
|
|
1018
|
-
const assistantMsg = message;
|
|
1019
|
-
toolCallsCount += assistantMsg.content.filter((c) => c.type === "toolCall").length;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
// Get last assistant message for context info
|
|
1023
|
-
const lastAssistantMessage = data.messages
|
|
1024
|
-
.slice()
|
|
1025
|
-
.reverse()
|
|
1026
|
-
.find((m) => m.role === "assistant" && m.stopReason !== "aborted");
|
|
1027
|
-
const contextTokens = lastAssistantMessage
|
|
1028
|
-
? lastAssistantMessage.usage.input +
|
|
1029
|
-
lastAssistantMessage.usage.output +
|
|
1030
|
-
lastAssistantMessage.usage.cacheRead +
|
|
1031
|
-
lastAssistantMessage.usage.cacheWrite
|
|
1032
|
-
: 0;
|
|
1033
|
-
const lastModel = lastAssistantMessage?.model || "unknown";
|
|
1034
|
-
const lastProvider = lastAssistantMessage?.provider || "";
|
|
1035
|
-
const lastModelInfo = lastProvider ? `${lastProvider}/${lastModel}` : lastModel;
|
|
1036
|
-
// Generate messages HTML
|
|
1037
|
-
let messagesHtml = "";
|
|
1038
|
-
for (const event of data.sessionEvents) {
|
|
1039
|
-
if (event.type === "message" && event.message.role !== "toolResult") {
|
|
1040
|
-
messagesHtml += formatMessage(event.message, data.toolResultsMap);
|
|
1041
|
-
}
|
|
1042
|
-
else if (event.type === "model_change") {
|
|
1043
|
-
messagesHtml += formatModelChange(event);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
// Tools section (only if tools info available)
|
|
1047
|
-
const toolsHtml = data.tools
|
|
1048
|
-
? `
|
|
1049
|
-
<div class="tools-list">
|
|
1050
|
-
<div class="tools-header">Available Tools</div>
|
|
1051
|
-
<div class="tools-content">
|
|
1052
|
-
${data.tools.map((tool) => `<div class="tool-item"><span class="tool-item-name">${escapeHtml(tool.name)}</span> - ${escapeHtml(tool.description)}</div>`).join("")}
|
|
1053
|
-
</div>
|
|
1054
|
-
</div>`
|
|
1055
|
-
: "";
|
|
1056
|
-
// System prompt section (only if available)
|
|
1057
|
-
const systemPromptHtml = data.systemPrompt
|
|
1058
|
-
? `
|
|
1059
|
-
<div class="system-prompt">
|
|
1060
|
-
<div class="system-prompt-header">System Prompt</div>
|
|
1061
|
-
<div class="system-prompt-content">${escapeHtml(data.systemPrompt)}</div>
|
|
1062
|
-
</div>`
|
|
1063
|
-
: "";
|
|
1064
|
-
return `<!DOCTYPE html>
|
|
1065
|
-
<html lang="en">
|
|
1066
|
-
<head>
|
|
1067
|
-
<meta charset="UTF-8">
|
|
1068
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1069
|
-
<title>Session Export - ${escapeHtml(inputFilename)}</title>
|
|
1070
|
-
<style>
|
|
1071
|
-
* {
|
|
1072
|
-
margin: 0;
|
|
1073
|
-
padding: 0;
|
|
1074
|
-
box-sizing: border-box;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
body {
|
|
1078
|
-
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
|
1079
|
-
font-size: 12px;
|
|
1080
|
-
line-height: 1.6;
|
|
1081
|
-
color: ${COLORS.text};
|
|
1082
|
-
background: ${COLORS.bodyBg};
|
|
1083
|
-
padding: 24px;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
.container {
|
|
1087
|
-
max-width: 700px;
|
|
1088
|
-
margin: 0 auto;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
.header {
|
|
1092
|
-
margin-bottom: 24px;
|
|
1093
|
-
padding: 16px;
|
|
1094
|
-
background: ${COLORS.containerBg};
|
|
1095
|
-
border-radius: 4px;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
.header h1 {
|
|
1099
|
-
font-size: 14px;
|
|
1100
|
-
font-weight: bold;
|
|
1101
|
-
margin-bottom: 12px;
|
|
1102
|
-
color: ${COLORS.cyan};
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
.header-info {
|
|
1106
|
-
display: flex;
|
|
1107
|
-
flex-direction: column;
|
|
1108
|
-
gap: 3px;
|
|
1109
|
-
font-size: 11px;
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
.info-item {
|
|
1113
|
-
color: ${COLORS.textDim};
|
|
1114
|
-
display: flex;
|
|
1115
|
-
align-items: baseline;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
.info-label {
|
|
1119
|
-
font-weight: 600;
|
|
1120
|
-
margin-right: 8px;
|
|
1121
|
-
min-width: 100px;
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
.info-value {
|
|
1125
|
-
color: ${COLORS.text};
|
|
1126
|
-
flex: 1;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
.info-value.cost {
|
|
1130
|
-
font-family: 'SF Mono', monospace;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
.messages {
|
|
1134
|
-
display: flex;
|
|
1135
|
-
flex-direction: column;
|
|
1136
|
-
gap: 16px;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
.message-timestamp {
|
|
1140
|
-
font-size: 10px;
|
|
1141
|
-
color: ${COLORS.textDim};
|
|
1142
|
-
margin-bottom: 4px;
|
|
1143
|
-
opacity: 0.8;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
.user-message {
|
|
1147
|
-
background: ${COLORS.userMessageBg};
|
|
1148
|
-
padding: 12px 16px;
|
|
1149
|
-
border-radius: 4px;
|
|
1150
|
-
white-space: pre-wrap;
|
|
1151
|
-
word-wrap: break-word;
|
|
1152
|
-
overflow-wrap: break-word;
|
|
1153
|
-
word-break: break-word;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
.assistant-message {
|
|
1157
|
-
padding: 0;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
.assistant-text {
|
|
1161
|
-
padding: 12px 16px;
|
|
1162
|
-
white-space: pre-wrap;
|
|
1163
|
-
word-wrap: break-word;
|
|
1164
|
-
overflow-wrap: break-word;
|
|
1165
|
-
word-break: break-word;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
.thinking-text {
|
|
1169
|
-
padding: 12px 16px;
|
|
1170
|
-
color: ${COLORS.italic};
|
|
1171
|
-
font-style: italic;
|
|
1172
|
-
white-space: pre-wrap;
|
|
1173
|
-
word-wrap: break-word;
|
|
1174
|
-
overflow-wrap: break-word;
|
|
1175
|
-
word-break: break-word;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
.model-change {
|
|
1179
|
-
padding: 8px 16px;
|
|
1180
|
-
background: rgb(40, 40, 50);
|
|
1181
|
-
border-radius: 4px;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
.model-change-text {
|
|
1185
|
-
color: ${COLORS.textDim};
|
|
1186
|
-
font-size: 11px;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
.model-name {
|
|
1190
|
-
color: ${COLORS.cyan};
|
|
1191
|
-
font-weight: bold;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
.tool-execution {
|
|
1195
|
-
padding: 12px 16px;
|
|
1196
|
-
border-radius: 4px;
|
|
1197
|
-
margin-top: 8px;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
.tool-header {
|
|
1201
|
-
font-weight: bold;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
.tool-name {
|
|
1205
|
-
font-weight: bold;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
.tool-path {
|
|
1209
|
-
color: ${COLORS.cyan};
|
|
1210
|
-
word-break: break-all;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
.line-count {
|
|
1214
|
-
color: ${COLORS.textDim};
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
.tool-command {
|
|
1218
|
-
font-weight: bold;
|
|
1219
|
-
white-space: pre-wrap;
|
|
1220
|
-
word-wrap: break-word;
|
|
1221
|
-
overflow-wrap: break-word;
|
|
1222
|
-
word-break: break-word;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
.tool-output {
|
|
1226
|
-
margin-top: 12px;
|
|
1227
|
-
color: ${COLORS.textDim};
|
|
1228
|
-
white-space: pre-wrap;
|
|
1229
|
-
word-wrap: break-word;
|
|
1230
|
-
overflow-wrap: break-word;
|
|
1231
|
-
word-break: break-word;
|
|
1232
|
-
font-family: inherit;
|
|
1233
|
-
overflow-x: auto;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
.tool-output > div {
|
|
1237
|
-
line-height: 1.4;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
.tool-output pre {
|
|
1241
|
-
margin: 0;
|
|
1242
568
|
font-family: inherit;
|
|
1243
|
-
color: inherit;
|
|
1244
|
-
white-space: pre-wrap;
|
|
1245
|
-
word-wrap: break-word;
|
|
1246
|
-
overflow-wrap: break-word;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
.tool-output.expandable {
|
|
1250
|
-
cursor: pointer;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
.tool-output.expandable:hover {
|
|
1254
|
-
opacity: 0.9;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
.tool-output.expandable .output-full {
|
|
1258
|
-
display: none;
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
.tool-output.expandable.expanded .output-preview {
|
|
1262
|
-
display: none;
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
.tool-output.expandable.expanded .output-full {
|
|
1266
|
-
display: block;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
.expand-hint {
|
|
1270
|
-
color: ${COLORS.cyan};
|
|
1271
|
-
font-style: italic;
|
|
1272
|
-
margin-top: 4px;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
.system-prompt {
|
|
1276
|
-
background: rgb(60, 55, 40);
|
|
1277
|
-
padding: 12px 16px;
|
|
1278
|
-
border-radius: 4px;
|
|
1279
|
-
margin-bottom: 16px;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
.system-prompt-header {
|
|
1283
|
-
font-weight: bold;
|
|
1284
|
-
color: ${COLORS.yellow};
|
|
1285
|
-
margin-bottom: 8px;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
.system-prompt-content {
|
|
1289
|
-
color: ${COLORS.textDim};
|
|
1290
|
-
white-space: pre-wrap;
|
|
1291
|
-
word-wrap: break-word;
|
|
1292
|
-
overflow-wrap: break-word;
|
|
1293
|
-
word-break: break-word;
|
|
1294
|
-
font-size: 11px;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
.tools-list {
|
|
1298
|
-
background: rgb(60, 55, 40);
|
|
1299
|
-
padding: 12px 16px;
|
|
1300
|
-
border-radius: 4px;
|
|
1301
|
-
margin-bottom: 16px;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
.tools-header {
|
|
1305
|
-
font-weight: bold;
|
|
1306
|
-
color: ${COLORS.yellow};
|
|
1307
|
-
margin-bottom: 8px;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
.tools-content {
|
|
1311
|
-
color: ${COLORS.textDim};
|
|
1312
|
-
font-size: 11px;
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
.tool-item {
|
|
1316
|
-
margin: 4px 0;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
.tool-item-name {
|
|
1320
|
-
font-weight: bold;
|
|
1321
|
-
color: ${COLORS.text};
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
.tool-diff {
|
|
1325
|
-
margin-top: 12px;
|
|
1326
|
-
font-size: 11px;
|
|
1327
|
-
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
|
1328
569
|
overflow-x: auto;
|
|
1329
|
-
max-width: 100%;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
.diff-line-old {
|
|
1333
|
-
color: ${COLORS.red};
|
|
1334
|
-
white-space: pre-wrap;
|
|
1335
|
-
word-wrap: break-word;
|
|
1336
|
-
overflow-wrap: break-word;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
.diff-line-new {
|
|
1340
|
-
color: ${COLORS.green};
|
|
1341
|
-
white-space: pre-wrap;
|
|
1342
|
-
word-wrap: break-word;
|
|
1343
|
-
overflow-wrap: break-word;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
.diff-line-context {
|
|
1347
|
-
color: ${COLORS.textDim};
|
|
1348
|
-
white-space: pre-wrap;
|
|
1349
|
-
word-wrap: break-word;
|
|
1350
|
-
overflow-wrap: break-word;
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
.error-text {
|
|
1354
|
-
color: ${COLORS.red};
|
|
1355
|
-
padding: 12px 16px;
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
.footer {
|
|
1359
|
-
margin-top: 48px;
|
|
1360
|
-
padding: 20px;
|
|
1361
|
-
text-align: center;
|
|
1362
|
-
color: ${COLORS.textDim};
|
|
1363
|
-
font-size: 10px;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
.streaming-notice {
|
|
1367
|
-
background: rgb(50, 45, 35);
|
|
1368
|
-
padding: 12px 16px;
|
|
1369
|
-
border-radius: 4px;
|
|
1370
|
-
margin-bottom: 16px;
|
|
1371
|
-
color: ${COLORS.textDim};
|
|
1372
|
-
font-size: 11px;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
@media print {
|
|
1376
|
-
body {
|
|
1377
|
-
background: white;
|
|
1378
|
-
color: black;
|
|
1379
|
-
}
|
|
1380
|
-
.tool-execution {
|
|
1381
|
-
border: 1px solid #ddd;
|
|
1382
|
-
}
|
|
1383
570
|
}
|
|
571
|
+
.tool-output > div { line-height: 1.4; }
|
|
572
|
+
.tool-output pre { margin: 0; font-family: inherit; color: inherit; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
|
573
|
+
.tool-output.expandable { cursor: pointer; }
|
|
574
|
+
.tool-output.expandable:hover { opacity: 0.9; }
|
|
575
|
+
.tool-output.expandable .output-full { display: none; }
|
|
576
|
+
.tool-output.expandable.expanded .output-preview { display: none; }
|
|
577
|
+
.tool-output.expandable.expanded .output-full { display: block; }
|
|
578
|
+
.expand-hint { color: ${COLORS.cyan}; font-style: italic; margin-top: 4px; }
|
|
579
|
+
.system-prompt, .tools-list { background: rgb(60, 55, 40); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; }
|
|
580
|
+
.system-prompt-header, .tools-header { font-weight: bold; color: ${COLORS.yellow}; margin-bottom: 8px; }
|
|
581
|
+
.system-prompt-content, .tools-content { color: ${COLORS.textDim}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; font-size: 11px; }
|
|
582
|
+
.tool-item { margin: 4px 0; }
|
|
583
|
+
.tool-item-name { font-weight: bold; color: ${COLORS.text}; }
|
|
584
|
+
.tool-diff { margin-top: 12px; font-size: 11px; font-family: inherit; overflow-x: auto; max-width: 100%; }
|
|
585
|
+
.diff-line-old { color: ${COLORS.red}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
|
586
|
+
.diff-line-new { color: ${COLORS.green}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
|
587
|
+
.diff-line-context { color: ${COLORS.textDim}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
|
588
|
+
.error-text { color: ${COLORS.red}; padding: 12px 16px; }
|
|
589
|
+
.footer { margin-top: 48px; padding: 20px; text-align: center; color: ${COLORS.textDim}; font-size: 10px; }
|
|
590
|
+
.streaming-notice { background: rgb(50, 45, 35); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; color: ${COLORS.textDim}; font-size: 11px; }
|
|
591
|
+
@media print { body { background: white; color: black; } .tool-execution { border: 1px solid #ddd; } }
|
|
1384
592
|
</style>
|
|
1385
593
|
</head>
|
|
1386
594
|
<body>
|
|
1387
595
|
<div class="container">
|
|
1388
596
|
<div class="header">
|
|
1389
|
-
<h1
|
|
597
|
+
<h1>${APP_NAME} v${VERSION}</h1>
|
|
1390
598
|
<div class="header-info">
|
|
1391
|
-
<div class="info-item">
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
</div>
|
|
1395
|
-
<div class="info-item">
|
|
1396
|
-
<span class="info-label">Date:</span>
|
|
1397
|
-
<span class="info-value">${new Date(data.timestamp).toLocaleString()}</span>
|
|
1398
|
-
</div>
|
|
1399
|
-
<div class="info-item">
|
|
1400
|
-
<span class="info-label">Models:</span>
|
|
1401
|
-
<span class="info-value">${Array.from(data.modelsUsed)
|
|
599
|
+
<div class="info-item"><span class="info-label">Session:</span><span class="info-value">${escapeHtml(data.sessionId)}</span></div>
|
|
600
|
+
<div class="info-item"><span class="info-label">Date:</span><span class="info-value">${new Date(data.timestamp).toLocaleString()}</span></div>
|
|
601
|
+
<div class="info-item"><span class="info-label">Models:</span><span class="info-value">${Array.from(data.modelsUsed)
|
|
1402
602
|
.map((m) => escapeHtml(m))
|
|
1403
|
-
.join(", ") || "unknown"}</span>
|
|
1404
|
-
</div>
|
|
603
|
+
.join(", ") || "unknown"}</span></div>
|
|
1405
604
|
</div>
|
|
1406
605
|
</div>
|
|
1407
606
|
|
|
1408
607
|
<div class="header">
|
|
1409
608
|
<h1>Messages</h1>
|
|
1410
609
|
<div class="header-info">
|
|
1411
|
-
<div class="info-item">
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
</div>
|
|
1415
|
-
<div class="info-item">
|
|
1416
|
-
<span class="info-label">Assistant:</span>
|
|
1417
|
-
<span class="info-value">${assistantMessages}</span>
|
|
1418
|
-
</div>
|
|
1419
|
-
<div class="info-item">
|
|
1420
|
-
<span class="info-label">Tool Calls:</span>
|
|
1421
|
-
<span class="info-value">${toolCallsCount}</span>
|
|
1422
|
-
</div>
|
|
610
|
+
<div class="info-item"><span class="info-label">User:</span><span class="info-value">${userMessages}</span></div>
|
|
611
|
+
<div class="info-item"><span class="info-label">Assistant:</span><span class="info-value">${assistantMessages}</span></div>
|
|
612
|
+
<div class="info-item"><span class="info-label">Tool Calls:</span><span class="info-value">${toolCallsCount}</span></div>
|
|
1423
613
|
</div>
|
|
1424
614
|
</div>
|
|
1425
615
|
|
|
1426
616
|
<div class="header">
|
|
1427
617
|
<h1>Tokens & Cost</h1>
|
|
1428
618
|
<div class="header-info">
|
|
1429
|
-
<div class="info-item">
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
</div>
|
|
1433
|
-
<div class="info-item">
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
</div>
|
|
1437
|
-
<div class="info-item">
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
</div>
|
|
1441
|
-
<div class="info-item">
|
|
1442
|
-
<span class="info-label">Cache Write:</span>
|
|
1443
|
-
<span class="info-value">${data.tokenStats.cacheWrite.toLocaleString()} tokens</span>
|
|
1444
|
-
</div>
|
|
1445
|
-
<div class="info-item">
|
|
1446
|
-
<span class="info-label">Total:</span>
|
|
1447
|
-
<span class="info-value">${(data.tokenStats.input + data.tokenStats.output + data.tokenStats.cacheRead + data.tokenStats.cacheWrite).toLocaleString()} tokens</span>
|
|
1448
|
-
</div>
|
|
1449
|
-
<div class="info-item">
|
|
1450
|
-
<span class="info-label">Input Cost:</span>
|
|
1451
|
-
<span class="info-value cost">$${data.costStats.input.toFixed(4)}</span>
|
|
1452
|
-
</div>
|
|
1453
|
-
<div class="info-item">
|
|
1454
|
-
<span class="info-label">Output Cost:</span>
|
|
1455
|
-
<span class="info-value cost">$${data.costStats.output.toFixed(4)}</span>
|
|
1456
|
-
</div>
|
|
1457
|
-
<div class="info-item">
|
|
1458
|
-
<span class="info-label">Cache Read Cost:</span>
|
|
1459
|
-
<span class="info-value cost">$${data.costStats.cacheRead.toFixed(4)}</span>
|
|
1460
|
-
</div>
|
|
1461
|
-
<div class="info-item">
|
|
1462
|
-
<span class="info-label">Cache Write Cost:</span>
|
|
1463
|
-
<span class="info-value cost">$${data.costStats.cacheWrite.toFixed(4)}</span>
|
|
1464
|
-
</div>
|
|
1465
|
-
<div class="info-item">
|
|
1466
|
-
<span class="info-label">Total Cost:</span>
|
|
1467
|
-
<span class="info-value cost"><strong>$${(data.costStats.input + data.costStats.output + data.costStats.cacheRead + data.costStats.cacheWrite).toFixed(4)}</strong></span>
|
|
1468
|
-
</div>
|
|
1469
|
-
<div class="info-item">
|
|
1470
|
-
<span class="info-label">Context Usage:</span>
|
|
1471
|
-
<span class="info-value">${contextTokens.toLocaleString()} tokens (last turn) - ${escapeHtml(lastModelInfo)}</span>
|
|
1472
|
-
</div>
|
|
619
|
+
<div class="info-item"><span class="info-label">Input:</span><span class="info-value">${data.tokenStats.input.toLocaleString()} tokens</span></div>
|
|
620
|
+
<div class="info-item"><span class="info-label">Output:</span><span class="info-value">${data.tokenStats.output.toLocaleString()} tokens</span></div>
|
|
621
|
+
<div class="info-item"><span class="info-label">Cache Read:</span><span class="info-value">${data.tokenStats.cacheRead.toLocaleString()} tokens</span></div>
|
|
622
|
+
<div class="info-item"><span class="info-label">Cache Write:</span><span class="info-value">${data.tokenStats.cacheWrite.toLocaleString()} tokens</span></div>
|
|
623
|
+
<div class="info-item"><span class="info-label">Total:</span><span class="info-value">${(data.tokenStats.input + data.tokenStats.output + data.tokenStats.cacheRead + data.tokenStats.cacheWrite).toLocaleString()} tokens</span></div>
|
|
624
|
+
<div class="info-item"><span class="info-label">Input Cost:</span><span class="info-value cost">$${data.costStats.input.toFixed(4)}</span></div>
|
|
625
|
+
<div class="info-item"><span class="info-label">Output Cost:</span><span class="info-value cost">$${data.costStats.output.toFixed(4)}</span></div>
|
|
626
|
+
<div class="info-item"><span class="info-label">Cache Read Cost:</span><span class="info-value cost">$${data.costStats.cacheRead.toFixed(4)}</span></div>
|
|
627
|
+
<div class="info-item"><span class="info-label">Cache Write Cost:</span><span class="info-value cost">$${data.costStats.cacheWrite.toFixed(4)}</span></div>
|
|
628
|
+
<div class="info-item"><span class="info-label">Total Cost:</span><span class="info-value cost"><strong>$${(data.costStats.input + data.costStats.output + data.costStats.cacheRead + data.costStats.cacheWrite).toFixed(4)}</strong></span></div>
|
|
629
|
+
<div class="info-item"><span class="info-label">Context Usage:</span><span class="info-value">${contextUsageText}</span></div>
|
|
1473
630
|
</div>
|
|
1474
631
|
</div>
|
|
1475
632
|
|
|
1476
633
|
${systemPromptHtml}
|
|
1477
634
|
${toolsHtml}
|
|
1478
|
-
|
|
1479
|
-
${data.isStreamingFormat
|
|
1480
|
-
? `<div class="streaming-notice">
|
|
1481
|
-
<em>Note: This session was reconstructed from raw agent event logs, which do not contain system prompt or tool definitions.</em>
|
|
1482
|
-
</div>`
|
|
1483
|
-
: ""}
|
|
635
|
+
${streamingNotice}
|
|
1484
636
|
|
|
1485
637
|
<div class="messages">
|
|
1486
638
|
${messagesHtml}
|
|
1487
639
|
</div>
|
|
1488
640
|
|
|
1489
641
|
<div class="footer">
|
|
1490
|
-
Generated by
|
|
642
|
+
Generated by ${APP_NAME} coding-agent on ${new Date().toLocaleString()}
|
|
1491
643
|
</div>
|
|
1492
644
|
</div>
|
|
1493
645
|
</body>
|
|
1494
646
|
</html>`;
|
|
1495
647
|
}
|
|
648
|
+
// ============================================================================
|
|
649
|
+
// Public API
|
|
650
|
+
// ============================================================================
|
|
651
|
+
/**
|
|
652
|
+
* Export session to HTML using SessionManager and AgentState.
|
|
653
|
+
* Used by TUI's /export command.
|
|
654
|
+
*/
|
|
655
|
+
export function exportSessionToHtml(sessionManager, state, outputPath) {
|
|
656
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
657
|
+
const content = readFileSync(sessionFile, "utf8");
|
|
658
|
+
const data = parseSessionFile(content);
|
|
659
|
+
// Enrich with data from AgentState (tools, context window)
|
|
660
|
+
data.tools = state.tools.map((t) => ({ name: t.name, description: t.description }));
|
|
661
|
+
data.contextWindow = state.model?.contextWindow;
|
|
662
|
+
if (!data.systemPrompt) {
|
|
663
|
+
data.systemPrompt = state.systemPrompt;
|
|
664
|
+
}
|
|
665
|
+
if (!outputPath) {
|
|
666
|
+
const sessionBasename = basename(sessionFile, ".jsonl");
|
|
667
|
+
outputPath = `${APP_NAME}-session-${sessionBasename}.html`;
|
|
668
|
+
}
|
|
669
|
+
const html = generateHtml(data, basename(sessionFile));
|
|
670
|
+
writeFileSync(outputPath, html, "utf8");
|
|
671
|
+
return outputPath;
|
|
672
|
+
}
|
|
1496
673
|
/**
|
|
1497
|
-
* Export
|
|
1498
|
-
* Auto-detects format: session manager format or streaming event format
|
|
674
|
+
* Export session file to HTML (standalone, without AgentState).
|
|
675
|
+
* Auto-detects format: session manager format or streaming event format.
|
|
676
|
+
* Used by CLI for exporting arbitrary session files.
|
|
1499
677
|
*/
|
|
1500
678
|
export function exportFromFile(inputPath, outputPath) {
|
|
1501
679
|
if (!existsSync(inputPath)) {
|
|
1502
680
|
throw new Error(`File not found: ${inputPath}`);
|
|
1503
681
|
}
|
|
1504
682
|
const content = readFileSync(inputPath, "utf8");
|
|
1505
|
-
const
|
|
1506
|
-
.trim()
|
|
1507
|
-
.split("\n")
|
|
1508
|
-
.filter((l) => l.trim());
|
|
1509
|
-
if (lines.length === 0) {
|
|
1510
|
-
throw new Error(`Empty file: ${inputPath}`);
|
|
1511
|
-
}
|
|
1512
|
-
const format = detectFormat(lines);
|
|
1513
|
-
if (format === "unknown") {
|
|
1514
|
-
throw new Error(`Unknown session file format: ${inputPath}`);
|
|
1515
|
-
}
|
|
1516
|
-
const data = format === "session-manager" ? parseSessionManagerFormat(lines) : parseStreamingEventFormat(lines);
|
|
1517
|
-
// Generate output path if not provided
|
|
683
|
+
const data = parseSessionFile(content);
|
|
1518
684
|
if (!outputPath) {
|
|
1519
685
|
const inputBasename = basename(inputPath, ".jsonl");
|
|
1520
|
-
outputPath =
|
|
686
|
+
outputPath = `${APP_NAME}-session-${inputBasename}.html`;
|
|
1521
687
|
}
|
|
1522
688
|
const html = generateHtml(data, basename(inputPath));
|
|
1523
689
|
writeFileSync(outputPath, html, "utf8");
|