@lim324/my-claude-code-viewer 0.0.1 → 0.0.3

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.
@@ -9,78 +9,148 @@ const escape_html_1 = __importDefault(require("escape-html"));
9
9
  const project_scanner_1 = require("../server/project-scanner");
10
10
  const session_analyzer_1 = require("../server/session-analyzer");
11
11
  const router = (0, express_1.Router)();
12
- // Helper function to format currency
12
+ // Helper functions
13
13
  function formatCurrency(amount) {
14
14
  return `$${amount.toFixed(4)}`;
15
15
  }
16
- // Helper function to format number
17
16
  function formatNumber(num) {
18
17
  return num.toLocaleString();
19
18
  }
20
- // Helper function to format date
21
19
  function formatDate(date) {
22
20
  return new Date(date).toLocaleString();
23
21
  }
24
- // Helper function to escape HTML to prevent XSS
25
22
  function e(text) {
26
23
  if (text === null || text === undefined)
27
24
  return "";
28
25
  return (0, escape_html_1.default)(String(text));
29
26
  }
30
- // Helper function to highlight search matches (with XSS protection)
31
27
  function highlightMatch(text, query) {
32
28
  if (!query)
33
29
  return e(text);
34
30
  const safeText = e(text);
35
31
  const safeQuery = e(query);
36
- // Use a simple case-insensitive replace for highlighting
37
32
  const regex = new RegExp(`(${escapeRegExp(safeQuery)})`, 'gi');
38
33
  return safeText.replace(regex, '<span class="highlight">$1</span>');
39
34
  }
40
- // Helper function to escape special regex characters
41
35
  function escapeRegExp(string) {
42
36
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
43
37
  }
44
- // HTML layout template
45
- function renderLayout(title, content) {
38
+ // Render message content based on type
39
+ function renderMessageContent(message) {
40
+ if (!message || !message.content)
41
+ return '';
42
+ const content = message.content;
43
+ // Handle plain string (user messages)
44
+ if (typeof content === 'string') {
45
+ return renderTextBlock(content);
46
+ }
47
+ // Handle array of content blocks
48
+ if (Array.isArray(content)) {
49
+ return content.map((block) => {
50
+ switch (block.type) {
51
+ case 'text':
52
+ return renderTextBlock(block.text);
53
+ case 'thinking':
54
+ return renderThinkingBlock(block.thinking);
55
+ case 'tool_use':
56
+ return renderToolUseBlock(block);
57
+ case 'tool_result':
58
+ return renderToolResultBlock(block);
59
+ default:
60
+ return `<div class="unknown-block">Unknown block type: ${e(block.type)}</div>`;
61
+ }
62
+ }).join('');
63
+ }
64
+ return '';
65
+ }
66
+ function renderTextBlock(text) {
67
+ if (!text)
68
+ return '';
69
+ // Handle long text with collapse
70
+ const maxLength = 2000;
71
+ if (text.length > maxLength) {
72
+ const id = 'text-' + Math.random().toString(36).substr(2, 9);
73
+ return `
74
+ <div class="message-text">
75
+ <div class="text-collapsed" id="${id}-collapsed">${e(text.substring(0, maxLength))}...</div>
76
+ <div class="text-expanded" id="${id}-expanded" style="display:none;">${e(text)}</div>
77
+ <button class="toggle-btn" onclick="toggleText('${id}')">展开 ▼</button>
78
+ </div>
79
+ `;
80
+ }
81
+ return `<div class="message-text">${e(text)}</div>`;
82
+ }
83
+ function renderThinkingBlock(thinking) {
84
+ if (!thinking)
85
+ return '';
86
+ const id = 'thinking-' + Math.random().toString(36).substr(2, 9);
46
87
  return `
47
- <!DOCTYPE html>
48
- <html lang="en">
49
- <head>
50
- <meta charset="UTF-8">
51
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
52
- <title>${e(title)} | Claude Code Viewer</title>
88
+ <div class="thinking-block">
89
+ <div class="thinking-header" onclick="toggleThinking('${id}')">
90
+ <span class="thinking-icon">🧠</span>
91
+ <span>Thinking...</span>
92
+ <span class="thinking-toggle" id="${id}-toggle">▼</span>
93
+ </div>
94
+ <div class="thinking-content" id="${id}">${e(thinking)}</div>
95
+ </div>
96
+ `;
97
+ }
98
+ function renderToolUseBlock(block) {
99
+ const inputJson = JSON.stringify(block.input, null, 2);
100
+ return `
101
+ <div class="tool-use-block">
102
+ <div class="tool-header">
103
+ <span class="tool-icon">🔧</span>
104
+ <span class="tool-name">${e(block.name)}</span>
105
+ <span class="tool-id">${e(block.id?.slice(0, 8) || '')}</span>
106
+ </div>
107
+ <div class="tool-input">
108
+ <pre><code>${e(inputJson)}</code></pre>
109
+ </div>
110
+ </div>
111
+ `;
112
+ }
113
+ function renderToolResultBlock(block) {
114
+ let content = block.content;
115
+ // Handle array content in tool_result
116
+ if (Array.isArray(content)) {
117
+ content = content.map((c) => c.text || c.content || JSON.stringify(c)).join('\n');
118
+ }
119
+ const maxLength = 1000;
120
+ const displayContent = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
121
+ const truncated = displayContent.length > maxLength;
122
+ return `
123
+ <div class="tool-result-block">
124
+ <div class="tool-result-header">
125
+ <span class="tool-icon">📤</span>
126
+ <span>Tool Result</span>
127
+ <span class="tool-id">${e(block.tool_use_id?.slice(0, 8) || '')}</span>
128
+ </div>
129
+ <div class="tool-result-content">
130
+ ${truncated ? e(displayContent.substring(0, maxLength)) + '...' : e(displayContent)}
131
+ </div>
132
+ </div>
133
+ `;
134
+ }
135
+ // CSS styles
136
+ const styles = `
53
137
  <style>
54
- * {
55
- box-sizing: border-box;
56
- margin: 0;
57
- padding: 0;
58
- }
138
+ * { box-sizing: border-box; margin: 0; padding: 0; }
59
139
  body {
60
140
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
61
141
  background: #0d1117;
62
142
  color: #c9d1d9;
63
143
  line-height: 1.6;
64
144
  }
65
- .container {
66
- max-width: 1200px;
67
- margin: 0 auto;
68
- padding: 20px;
69
- }
145
+ .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
70
146
  header {
71
147
  background: #161b22;
72
148
  border-bottom: 1px solid #30363d;
73
149
  padding: 20px 0;
74
150
  margin-bottom: 30px;
75
151
  }
76
- header h1 {
77
- color: #58a6ff;
78
- font-size: 1.8rem;
79
- }
80
- header h1 a {
81
- color: inherit;
82
- text-decoration: none;
83
- }
152
+ header h1 { color: #58a6ff; font-size: 1.8rem; }
153
+ header h1 a { color: inherit; text-decoration: none; }
84
154
  .stats-grid {
85
155
  display: grid;
86
156
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
@@ -94,18 +164,8 @@ function renderLayout(title, content) {
94
164
  padding: 20px;
95
165
  text-align: center;
96
166
  }
97
- .stat-card h3 {
98
- color: #8b949e;
99
- font-size: 0.9rem;
100
- font-weight: normal;
101
- margin-bottom: 8px;
102
- text-transform: uppercase;
103
- }
104
- .stat-card .value {
105
- color: #58a6ff;
106
- font-size: 2rem;
107
- font-weight: bold;
108
- }
167
+ .stat-card h3 { color: #8b949e; font-size: 0.9rem; font-weight: normal; margin-bottom: 8px; text-transform: uppercase; }
168
+ .stat-card .value { color: #58a6ff; font-size: 2rem; font-weight: bold; }
109
169
  .section {
110
170
  background: #161b22;
111
171
  border: 1px solid #30363d;
@@ -113,182 +173,218 @@ function renderLayout(title, content) {
113
173
  padding: 20px;
114
174
  margin-bottom: 20px;
115
175
  }
116
- .section h2 {
117
- color: #e6edf3;
118
- font-size: 1.3rem;
119
- margin-bottom: 15px;
120
- padding-bottom: 10px;
121
- border-bottom: 1px solid #30363d;
122
- }
123
- table {
124
- width: 100%;
125
- border-collapse: collapse;
126
- }
127
- th, td {
128
- padding: 12px;
129
- text-align: left;
130
- border-bottom: 1px solid #30363d;
131
- }
132
- th {
133
- color: #8b949e;
134
- font-weight: 600;
135
- font-size: 0.85rem;
176
+ .section h2 { color: #e6edf3; font-size: 1.3rem; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #30363d; }
177
+ table { width: 100%; border-collapse: collapse; }
178
+ th, td { padding: 12px; text-align: left; border-bottom: 1px solid #30363d; }
179
+ th { color: #8b949e; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; }
180
+ tr:hover { background: #1c2128; }
181
+ a { color: #58a6ff; text-decoration: none; }
182
+ a:hover { text-decoration: underline; }
183
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.8rem; font-weight: 500; }
184
+ .badge-blue { background: rgba(56, 139, 253, 0.15); color: #58a6ff; }
185
+ .badge-green { background: rgba(35, 197, 94, 0.15); color: #3fb950; }
186
+ .badge-purple { background: rgba(163, 113, 247, 0.15); color: #a371f7; }
187
+ .cost { color: #f0883e; font-weight: 600; }
188
+ .empty-state { text-align: center; padding: 40px; color: #8b949e; }
189
+ .breadcrumb { margin-bottom: 20px; color: #8b949e; }
190
+ .breadcrumb a { color: #58a6ff; }
191
+ .message-preview { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #8b949e; font-size: 0.9rem; }
192
+ .token-bar { display: flex; height: 20px; border-radius: 4px; overflow: hidden; margin-top: 8px; }
193
+ .token-bar-input { background: #58a6ff; }
194
+ .token-bar-output { background: #3fb950; }
195
+ .token-bar-cache-creation { background: #d29922; }
196
+ .token-bar-cache-read { background: #a371f7; }
197
+ .conversation-list { max-height: 600px; overflow-y: auto; }
198
+ .conversation-item {
199
+ padding: 15px;
200
+ border-bottom: 1px solid #30363d;
201
+ background: #0d1117;
202
+ margin-bottom: 10px;
203
+ border-radius: 6px;
204
+ }
205
+ .conversation-item:last-child { border-bottom: none; }
206
+ .conversation-header {
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 10px;
210
+ margin-bottom: 12px;
211
+ padding-bottom: 8px;
212
+ border-bottom: 1px solid #21262d;
213
+ }
214
+ .conversation-type {
215
+ display: inline-block;
216
+ padding: 3px 10px;
217
+ border-radius: 4px;
218
+ font-size: 0.75rem;
219
+ font-weight: 600;
136
220
  text-transform: uppercase;
137
221
  }
138
- tr:hover {
139
- background: #1c2128;
140
- }
141
- a {
142
- color: #58a6ff;
143
- text-decoration: none;
144
- }
145
- a:hover {
146
- text-decoration: underline;
147
- }
148
- .badge {
149
- display: inline-block;
150
- padding: 2px 8px;
151
- border-radius: 12px;
152
- font-size: 0.8rem;
153
- font-weight: 500;
154
- }
155
- .badge-blue {
156
- background: rgba(56, 139, 253, 0.15);
157
- color: #58a6ff;
158
- }
159
- .badge-green {
160
- background: rgba(35, 197, 94, 0.15);
161
- color: #3fb950;
162
- }
163
- .cost {
164
- color: #f0883e;
165
- font-weight: 600;
166
- }
167
- .empty-state {
168
- text-align: center;
169
- padding: 40px;
170
- color: #8b949e;
171
- }
172
- .breadcrumb {
173
- margin-bottom: 20px;
174
- color: #8b949e;
175
- }
176
- .breadcrumb a {
177
- color: #58a6ff;
178
- }
179
- .message-preview {
180
- max-width: 300px;
181
- overflow: hidden;
182
- text-overflow: ellipsis;
183
- white-space: nowrap;
184
- color: #8b949e;
185
- font-size: 0.9rem;
222
+ .type-user { background: rgba(35, 197, 94, 0.2); color: #3fb950; }
223
+ .type-assistant { background: rgba(56, 139, 253, 0.2); color: #58a6ff; }
224
+ .type-system { background: rgba(139, 148, 158, 0.2); color: #8b949e; }
225
+ .timestamp { color: #6e7681; font-size: 0.8rem; }
226
+
227
+ /* Message Content Styles */
228
+ .message-content { margin-top: 10px; }
229
+ .message-text {
230
+ white-space: pre-wrap;
231
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
232
+ line-height: 1.6;
233
+ color: #c9d1d9;
186
234
  }
187
- .token-bar {
188
- display: flex;
189
- height: 20px;
190
- border-radius: 4px;
235
+
236
+ /* Thinking Block */
237
+ .thinking-block {
238
+ margin: 10px 0;
239
+ border: 1px solid #30363d;
240
+ border-radius: 6px;
191
241
  overflow: hidden;
192
- margin-top: 8px;
193
242
  }
194
- .token-bar-input {
195
- background: #58a6ff;
196
- }
197
- .token-bar-output {
198
- background: #3fb950;
199
- }
200
- .token-bar-cache {
201
- background: #a371f7;
202
- }
203
- .conversation-list {
204
- max-height: 500px;
243
+ .thinking-header {
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 8px;
247
+ padding: 10px 15px;
248
+ background: #21262d;
249
+ cursor: pointer;
250
+ user-select: none;
251
+ }
252
+ .thinking-header:hover { background: #30363d; }
253
+ .thinking-icon { font-size: 1rem; }
254
+ .thinking-toggle { margin-left: auto; font-size: 0.8rem; color: #8b949e; }
255
+ .thinking-content {
256
+ display: none;
257
+ padding: 15px;
258
+ background: #161b22;
259
+ color: #8b949e;
260
+ font-style: italic;
261
+ white-space: pre-wrap;
262
+ font-size: 0.9rem;
263
+ max-height: 400px;
205
264
  overflow-y: auto;
206
265
  }
207
- .conversation-item {
208
- padding: 15px;
209
- border-bottom: 1px solid #30363d;
210
- }
211
- .conversation-item:last-child {
212
- border-bottom: none;
213
- }
214
- .conversation-type {
215
- display: inline-block;
216
- padding: 2px 8px;
217
- border-radius: 4px;
218
- font-size: 0.75rem;
219
- font-weight: 600;
220
- text-transform: uppercase;
221
- margin-bottom: 8px;
222
- }
223
- .type-user {
224
- background: rgba(35, 197, 94, 0.15);
225
- color: #3fb950;
226
- }
227
- .type-assistant {
228
- background: rgba(56, 139, 253, 0.15);
229
- color: #58a6ff;
230
- }
231
- .type-system {
232
- background: rgba(139, 148, 158, 0.15);
233
- color: #8b949e;
234
- }
235
- .timestamp {
236
- color: #6e7681;
237
- font-size: 0.8rem;
238
- margin-left: 10px;
239
- }
240
- .cost-breakdown {
241
- display: grid;
242
- grid-template-columns: repeat(2, 1fr);
243
- gap: 10px;
244
- margin-top: 10px;
266
+ .thinking-content.expanded { display: block; }
267
+
268
+ /* Tool Use/Result Blocks */
269
+ .tool-use-block, .tool-result-block {
270
+ margin: 10px 0;
271
+ border: 1px solid #30363d;
272
+ border-radius: 6px;
273
+ overflow: hidden;
245
274
  }
246
- .cost-item {
275
+ .tool-header, .tool-result-header {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 8px;
279
+ padding: 10px 15px;
280
+ background: #21262d;
281
+ }
282
+ .tool-icon { font-size: 1rem; }
283
+ .tool-name { font-weight: 600; color: #e6edf3; }
284
+ .tool-id { margin-left: auto; font-size: 0.75rem; color: #6e7681; font-family: monospace; }
285
+ .tool-input, .tool-result-content {
286
+ padding: 12px 15px;
247
287
  background: #0d1117;
248
- padding: 10px;
249
- border-radius: 4px;
250
- font-size: 0.9rem;
251
- }
252
- .cost-item-label {
253
- color: #8b949e;
254
- font-size: 0.8rem;
255
288
  }
256
- .search-container {
257
- margin-bottom: 20px;
258
- }
259
- .search-input {
260
- width: 100%;
261
- padding: 12px 16px;
262
- font-size: 1rem;
263
- background: #0d1117;
264
- border: 1px solid #30363d;
265
- border-radius: 8px;
289
+ .tool-input pre, .tool-result-content pre {
290
+ margin: 0;
291
+ white-space: pre-wrap;
292
+ word-break: break-all;
293
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
294
+ font-size: 0.85rem;
266
295
  color: #c9d1d9;
267
- outline: none;
268
- transition: border-color 0.2s;
269
- }
270
- .search-input:focus {
271
- border-color: #58a6ff;
272
- }
273
- .search-input::placeholder {
274
- color: #6e7681;
275
- }
276
- .search-results-info {
277
- color: #8b949e;
278
- font-size: 0.9rem;
279
- margin-top: 10px;
280
- }
281
- .highlight {
282
- background: rgba(56, 139, 253, 0.3);
283
- padding: 0 2px;
284
- border-radius: 2px;
285
- }
286
- .no-results {
287
- text-align: center;
288
- padding: 40px;
289
- color: #8b949e;
290
296
  }
297
+ .tool-result-block { border-color: #238636; }
298
+ .tool-result-header { background: rgba(35, 134, 54, 0.15); }
299
+
300
+ /* Toggle Button */
301
+ .toggle-btn {
302
+ background: transparent;
303
+ border: none;
304
+ color: #58a6ff;
305
+ cursor: pointer;
306
+ font-size: 0.85rem;
307
+ padding: 5px 0;
308
+ margin-top: 5px;
309
+ }
310
+ .toggle-btn:hover { text-decoration: underline; }
311
+ .text-expanded { white-space: pre-wrap; }
312
+ .text-collapsed { white-space: pre-wrap; }
313
+ .cost-breakdown { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 10px; }
314
+ .cost-item { background: #0d1117; padding: 10px; border-radius: 4px; font-size: 0.9rem; }
315
+ .cost-item-label { color: #8b949e; font-size: 0.8rem; }
316
+ .cost-card { background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 15px; margin-bottom: 15px; }
317
+ .cost-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
318
+ .cost-card-title { font-size: 1rem; font-weight: 600; color: #e6edf3; }
319
+ .cost-card-amount { font-size: 1.2rem; font-weight: bold; color: #f0883e; }
320
+ .cost-card-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; font-size: 0.85rem; }
321
+ .cost-card-detail { display: flex; justify-content: space-between; color: #8b949e; }
322
+ .cost-card-detail span:last-child { color: #c9d1d9; }
323
+ .subagent-list { margin-top: 15px; }
324
+ .subagent-item { display: flex; align-items: center; gap: 10px; padding: 10px; background: #0d1117; border-radius: 4px; margin-bottom: 8px; transition: background 0.2s; }
325
+ .subagent-item:hover { background: #1c2128; }
326
+ .subagent-icon { width: 32px; height: 32px; background: rgba(163, 113, 247, 0.15); border-radius: 6px; display: flex; align-items: center; justify-content: center; }
327
+ .subagent-icon svg { width: 16px; height: 16px; color: #a371f7; }
328
+ .subagent-info { flex: 1; min-width: 0; }
329
+ .subagent-name { font-weight: 500; color: #e6edf3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
330
+ .subagent-id { font-size: 0.75rem; color: #6e7681; font-family: monospace; }
331
+ .subagent-cost { text-align: right; }
332
+ .subagent-cost-amount { font-weight: 600; color: #f0883e; }
333
+ .subagent-cost-tokens { font-size: 0.75rem; color: #8b949e; }
334
+ .search-container { margin-bottom: 20px; }
335
+ .search-input { width: 100%; padding: 12px 16px; font-size: 1rem; background: #0d1117; border: 1px solid #30363d; border-radius: 8px; color: #c9d1d9; outline: none; transition: border-color 0.2s; }
336
+ .search-input:focus { border-color: #58a6ff; }
337
+ .search-input::placeholder { color: #6e7681; }
338
+ .search-results-info { color: #8b949e; font-size: 0.9rem; margin-top: 10px; }
339
+ .highlight { background: rgba(56, 139, 253, 0.3); padding: 0 2px; border-radius: 2px; }
340
+ .no-results { text-align: center; padding: 40px; color: #8b949e; }
341
+ .back-link { display: inline-flex; align-items: center; gap: 6px; margin-bottom: 20px; color: #58a6ff; }
342
+ .back-link:hover { text-decoration: none; }
291
343
  </style>
344
+ `;
345
+ // JavaScript for interactive features
346
+ const scripts = `
347
+ <script>
348
+ // Toggle thinking block visibility
349
+ function toggleThinking(id) {
350
+ const content = document.getElementById(id);
351
+ const toggle = document.getElementById(id + '-toggle');
352
+ if (content) {
353
+ content.classList.toggle('expanded');
354
+ if (toggle) {
355
+ toggle.textContent = content.classList.contains('expanded') ? '▲' : '▼';
356
+ }
357
+ }
358
+ }
359
+
360
+ // Toggle text expansion
361
+ function toggleText(id) {
362
+ const collapsed = document.getElementById(id + '-collapsed');
363
+ const expanded = document.getElementById(id + '-expanded');
364
+ const btn = event.target;
365
+ if (collapsed && expanded) {
366
+ if (expanded.style.display === 'none') {
367
+ collapsed.style.display = 'none';
368
+ expanded.style.display = 'block';
369
+ btn.textContent = '收起 ▲';
370
+ } else {
371
+ collapsed.style.display = 'block';
372
+ expanded.style.display = 'none';
373
+ btn.textContent = '展开 ▼';
374
+ }
375
+ }
376
+ }
377
+ </script>
378
+ `;
379
+ function renderLayout(title, content) {
380
+ return `
381
+ <!DOCTYPE html>
382
+ <html lang="en">
383
+ <head>
384
+ <meta charset="UTF-8">
385
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
386
+ <title>${e(title)} | Claude Code Viewer</title>
387
+ ${styles}
292
388
  </head>
293
389
  <body>
294
390
  <header>
@@ -299,10 +395,50 @@ function renderLayout(title, content) {
299
395
  <div class="container">
300
396
  ${content}
301
397
  </div>
398
+ ${scripts}
302
399
  </body>
303
400
  </html>
304
401
  `;
305
402
  }
403
+ // Helper to render cost card
404
+ function renderCostCard(title, cost, isTotal = false) {
405
+ const totalTokens = cost.tokenUsage.inputTokens + cost.tokenUsage.outputTokens +
406
+ cost.tokenUsage.cacheCreationTokens + cost.tokenUsage.cacheReadTokens;
407
+ return `
408
+ <div class="cost-card" style="${isTotal ? 'border-color: #58a6ff;' : ''}">
409
+ <div class="cost-card-header">
410
+ <span class="cost-card-title">${e(title)}</span>
411
+ <span class="cost-card-amount">${formatCurrency(cost.totalUsd)}</span>
412
+ </div>
413
+ <div class="cost-card-details">
414
+ <div class="cost-card-detail">
415
+ <span>Input:</span>
416
+ <span>${formatNumber(cost.tokenUsage.inputTokens)} tokens</span>
417
+ </div>
418
+ <div class="cost-card-detail">
419
+ <span>Output:</span>
420
+ <span>${formatNumber(cost.tokenUsage.outputTokens)} tokens</span>
421
+ </div>
422
+ ${cost.tokenUsage.cacheCreationTokens > 0 ? `
423
+ <div class="cost-card-detail">
424
+ <span>Cache Creation:</span>
425
+ <span>${formatNumber(cost.tokenUsage.cacheCreationTokens)} tokens</span>
426
+ </div>
427
+ ` : ''}
428
+ ${cost.tokenUsage.cacheReadTokens > 0 ? `
429
+ <div class="cost-card-detail">
430
+ <span>Cache Read:</span>
431
+ <span>${formatNumber(cost.tokenUsage.cacheReadTokens)} tokens</span>
432
+ </div>
433
+ ` : ''}
434
+ <div class="cost-card-detail">
435
+ <span>Total Tokens:</span>
436
+ <span>${formatNumber(totalTokens)}</span>
437
+ </div>
438
+ </div>
439
+ </div>
440
+ `;
441
+ }
306
442
  /**
307
443
  * GET /
308
444
  * Dashboard with all projects
@@ -312,7 +448,6 @@ router.get("/", (req, res) => {
312
448
  const stats = (0, project_scanner_1.getGlobalStats)();
313
449
  const projects = (0, project_scanner_1.scanProjects)();
314
450
  const searchQuery = req.query.search || "";
315
- // Setup Fuse.js for fuzzy search
316
451
  let filteredProjects = projects;
317
452
  if (searchQuery) {
318
453
  const fuse = new fuse_js_1.default(projects, {
@@ -346,90 +481,52 @@ router.get("/", (req, res) => {
346
481
  <div class="section">
347
482
  <h2>Projects</h2>
348
483
  <div class="search-container">
349
- <input
350
- type="text"
351
- id="project-search"
352
- class="search-input"
353
- placeholder="🔍 Search projects by name..."
354
- value="${e(searchQuery)}"
355
- autocomplete="off"
356
- />
484
+ <input type="text" id="project-search" class="search-input" placeholder="🔍 Search projects by name..." value="${e(searchQuery)}" autocomplete="off" />
357
485
  ${searchQuery ? `<div class="search-results-info">Showing ${filteredProjects.length} of ${projects.length} projects</div>` : ""}
358
486
  </div>
359
- ${projects.length === 0
360
- ? `<div class="empty-state">
361
- No projects found in ${e(stats.projectsPath)}
362
- </div>`
363
- : filteredProjects.length === 0
364
- ? `<div class="no-results">No projects match "${e(searchQuery)}"</div>`
365
- : `<table id="projects-table">
366
- <thead>
367
- <tr>
368
- <th>Project Name</th>
369
- <th>Sessions</th>
370
- <th>Messages</th>
371
- <th>Total Cost</th>
372
- <th>Last Modified</th>
373
- </tr>
374
- </thead>
375
- <tbody>
376
- ${filteredProjects
377
- .map((p) => `
378
- <tr data-project-name="${e(p.name.toLowerCase())}" data-project-id="${e(p.id.toLowerCase())}">
379
- <td><a href="/projects/${e(p.id)}">${highlightMatch(p.name, searchQuery)}</a></td>
380
- <td><span class="badge badge-blue">${formatNumber(p.sessionCount)}</span></td>
381
- <td>${formatNumber(p.totalMessageCount)}</td>
382
- <td class="cost">${formatCurrency(p.totalCost)}</td>
383
- <td>${formatDate(p.lastModifiedAt)}</td>
384
- </tr>
385
- `)
386
- .join("")}
387
- </tbody>
388
- </table>`}
487
+ ${projects.length === 0 ? `<div class="empty-state">No projects found in ${e(stats.projectsPath)}</div>` :
488
+ filteredProjects.length === 0 ? `<div class="no-results">No projects match "${e(searchQuery)}"</div>` :
489
+ `<table id="projects-table">
490
+ <thead>
491
+ <tr><th>Project Name</th><th>Sessions</th><th>Messages</th><th>Total Cost</th><th>Last Modified</th></tr>
492
+ </thead>
493
+ <tbody>
494
+ ${filteredProjects.map((p) => `
495
+ <tr data-project-name="${e(p.name.toLowerCase())}" data-project-id="${e(p.id.toLowerCase())}">
496
+ <td><a href="/projects/${e(p.id)}">${highlightMatch(p.name, searchQuery)}</a></td>
497
+ <td><span class="badge badge-blue">${formatNumber(p.sessionCount)}</span></td>
498
+ <td>${formatNumber(p.totalMessageCount)}</td>
499
+ <td class="cost">${formatCurrency(p.totalCost)}</td>
500
+ <td>${formatDate(p.lastModifiedAt)}</td>
501
+ </tr>
502
+ `).join("")}
503
+ </tbody>
504
+ </table>`}
389
505
  </div>
390
506
 
391
507
  <script>
392
- // Client-side search for instant feedback
393
508
  const searchInput = document.getElementById('project-search');
394
509
  const projectsTable = document.getElementById('projects-table');
395
-
396
510
  if (searchInput && projectsTable) {
397
511
  const rows = projectsTable.querySelectorAll('tbody tr');
398
-
399
512
  searchInput.addEventListener('input', (e) => {
400
513
  const query = e.target.value.toLowerCase().trim();
401
-
402
514
  rows.forEach(row => {
403
515
  const name = row.getAttribute('data-project-name');
404
516
  const id = row.getAttribute('data-project-id');
405
-
406
- if (!query || name.includes(query) || id.includes(query)) {
407
- row.style.display = '';
408
- } else {
409
- row.style.display = 'none';
410
- }
517
+ row.style.display = (!query || name.includes(query) || id.includes(query)) ? '' : 'none';
411
518
  });
412
-
413
- // Update URL without page reload
414
519
  const url = new URL(window.location);
415
- if (query) {
416
- url.searchParams.set('search', query);
417
- } else {
418
- url.searchParams.delete('search');
419
- }
520
+ if (query) url.searchParams.set('search', query);
521
+ else url.searchParams.delete('search');
420
522
  window.history.replaceState({}, '', url);
421
523
  });
422
-
423
- // Handle Enter key to submit search
424
524
  searchInput.addEventListener('keypress', (e) => {
425
525
  if (e.key === 'Enter') {
426
526
  const url = new URL(window.location);
427
527
  const query = e.target.value.trim();
428
- if (query) {
429
- url.searchParams.set('search', query);
430
- } else {
431
- url.searchParams.delete('search');
432
- }
528
+ if (query) url.searchParams.set('search', query);
529
+ else url.searchParams.delete('search');
433
530
  window.location.href = url.toString();
434
531
  }
435
532
  });
@@ -449,18 +546,12 @@ router.get("/", (req, res) => {
449
546
  router.get("/projects/:id", (req, res) => {
450
547
  try {
451
548
  const project = (0, project_scanner_1.getProjectDetail)(req.params.id);
452
- if (!project) {
549
+ if (!project)
453
550
  return res.status(404).send("Project not found");
454
- }
455
551
  const searchQuery = req.query.search || "";
456
- // Setup Fuse.js for fuzzy search on sessionId
457
552
  let filteredSessions = project.sessions;
458
553
  if (searchQuery) {
459
- const fuse = new fuse_js_1.default(project.sessions, {
460
- keys: ["id"],
461
- threshold: 0.4,
462
- includeScore: true,
463
- });
554
+ const fuse = new fuse_js_1.default(project.sessions, { keys: ["id"], threshold: 0.4, includeScore: true });
464
555
  const results = fuse.search(searchQuery);
465
556
  filteredSessions = results.map((r) => r.item);
466
557
  }
@@ -470,113 +561,66 @@ router.get("/projects/:id", (req, res) => {
470
561
  </div>
471
562
 
472
563
  <div class="stats-grid">
473
- <div class="stat-card">
474
- <h3>Project</h3>
475
- <div class="value">${e(project.name)}</div>
476
- </div>
477
- <div class="stat-card">
478
- <h3>Sessions</h3>
479
- <div class="value">${formatNumber(project.sessionCount)}</div>
480
- </div>
481
- <div class="stat-card">
482
- <h3>Messages</h3>
483
- <div class="value">${formatNumber(project.totalMessageCount)}</div>
484
- </div>
485
- <div class="stat-card">
486
- <h3>Total Cost</h3>
487
- <div class="value cost">${formatCurrency(project.totalCost)}</div>
488
- </div>
564
+ <div class="stat-card"><h3>Project</h3><div class="value">${e(project.name)}</div></div>
565
+ <div class="stat-card"><h3>Sessions</h3><div class="value">${formatNumber(project.sessionCount)}</div></div>
566
+ <div class="stat-card"><h3>Messages</h3><div class="value">${formatNumber(project.totalMessageCount)}</div></div>
567
+ <div class="stat-card"><h3>Total Cost</h3><div class="value cost">${formatCurrency(project.totalCost)}</div></div>
489
568
  </div>
490
569
 
491
570
  <div class="section">
492
571
  <h2>Sessions</h2>
493
572
  <div class="search-container">
494
- <input
495
- type="text"
496
- id="session-search"
497
- class="search-input"
498
- placeholder="🔍 Search sessions by ID..."
499
- value="${e(searchQuery)}"
500
- autocomplete="off"
501
- />
573
+ <input type="text" id="session-search" class="search-input" placeholder="🔍 Search sessions by ID..." value="${e(searchQuery)}" autocomplete="off" />
502
574
  ${searchQuery ? `<div class="search-results-info">Showing ${filteredSessions.length} of ${project.sessions.length} sessions</div>` : ""}
503
575
  </div>
504
- ${project.sessions.length === 0
505
- ? `<div class="empty-state">No sessions found</div>`
506
- : filteredSessions.length === 0
507
- ? `<div class="no-results">No sessions match "${e(searchQuery)}"</div>`
508
- : `<table id="sessions-table">
509
- <thead>
510
- <tr>
511
- <th>Session ID</th>
512
- <th>Messages</th>
513
- <th>Tokens</th>
514
- <th>Cost</th>
515
- <th>First Message</th>
516
- <th>Last Modified</th>
576
+ ${project.sessions.length === 0 ? `<div class="empty-state">No sessions found</div>` :
577
+ filteredSessions.length === 0 ? `<div class="no-results">No sessions match "${e(searchQuery)}"</div>` :
578
+ `<table id="sessions-table">
579
+ <thead>
580
+ <tr><th>Session ID</th><th>Messages</th><th>Tokens</th><th>Cost</th><th>Subagents</th><th>First Message</th><th>Last Modified</th></tr>
581
+ </thead>
582
+ <tbody>
583
+ ${filteredSessions.map((s) => {
584
+ const totalTokens = s.meta.cost.tokenUsage.inputTokens + s.meta.cost.tokenUsage.outputTokens +
585
+ s.meta.cost.tokenUsage.cacheCreationTokens + s.meta.cost.tokenUsage.cacheReadTokens;
586
+ const subagentCount = s.meta.subagents?.length || 0;
587
+ return `
588
+ <tr data-session-id="${e(s.id.toLowerCase())}">
589
+ <td><a href="/projects/${e(project.id)}/sessions/${e(s.id)}">${highlightMatch(s.id, searchQuery)}</a></td>
590
+ <td><span class="badge badge-blue">${formatNumber(s.meta.messageCount)}</span></td>
591
+ <td>${formatNumber(totalTokens)}</td>
592
+ <td class="cost">${formatCurrency(s.meta.cost.totalUsd)}</td>
593
+ <td>${subagentCount > 0 ? `<span class="badge badge-purple">${subagentCount}</span>` : '-'}</td>
594
+ <td><div class="message-preview">${s.meta.firstUserMessage ? e(s.meta.firstUserMessage.substring(0, 100)) : "N/A"}</div></td>
595
+ <td>${formatDate(s.lastModifiedAt)}</td>
517
596
  </tr>
518
- </thead>
519
- <tbody>
520
- ${filteredSessions
521
- .map((s) => `
522
- <tr data-session-id="${e(s.id.toLowerCase())}">
523
- <td><a href="/projects/${e(project.id)}/sessions/${e(s.id)}">${highlightMatch(s.id, searchQuery)}</a></td>
524
- <td><span class="badge badge-blue">${formatNumber(s.meta.messageCount)}</span></td>
525
- <td>${formatNumber(s.meta.cost.tokenUsage.inputTokens +
526
- s.meta.cost.tokenUsage.outputTokens +
527
- s.meta.cost.tokenUsage.cacheCreationTokens +
528
- s.meta.cost.tokenUsage.cacheReadTokens)}</td>
529
- <td class="cost">${formatCurrency(s.meta.cost.totalUsd)}</td>
530
- <td><div class="message-preview">${s.meta.firstUserMessage ? e(s.meta.firstUserMessage.substring(0, 100)) : "N/A"}</div></td>
531
- <td>${formatDate(s.lastModifiedAt)}</td>
532
- </tr>
533
- `)
534
- .join("")}
535
- </tbody>
536
- </table>`}
597
+ `;
598
+ }).join("")}
599
+ </tbody>
600
+ </table>`}
537
601
  </div>
538
602
 
539
603
  <script>
540
- // Client-side search for instant feedback
541
604
  const searchInput = document.getElementById('session-search');
542
605
  const sessionsTable = document.getElementById('sessions-table');
543
-
544
606
  if (searchInput && sessionsTable) {
545
607
  const rows = sessionsTable.querySelectorAll('tbody tr');
546
-
547
608
  searchInput.addEventListener('input', (e) => {
548
609
  const query = e.target.value.toLowerCase().trim();
549
-
550
610
  rows.forEach(row => {
551
- const sessionId = row.getAttribute('data-session-id');
552
-
553
- if (!query || sessionId.includes(query)) {
554
- row.style.display = '';
555
- } else {
556
- row.style.display = 'none';
557
- }
611
+ row.style.display = (!query || row.getAttribute('data-session-id').includes(query)) ? '' : 'none';
558
612
  });
559
-
560
- // Update URL without page reload
561
613
  const url = new URL(window.location);
562
- if (query) {
563
- url.searchParams.set('search', query);
564
- } else {
565
- url.searchParams.delete('search');
566
- }
614
+ if (query) url.searchParams.set('search', query);
615
+ else url.searchParams.delete('search');
567
616
  window.history.replaceState({}, '', url);
568
617
  });
569
-
570
- // Handle Enter key to submit search
571
618
  searchInput.addEventListener('keypress', (e) => {
572
619
  if (e.key === 'Enter') {
573
620
  const url = new URL(window.location);
574
621
  const query = e.target.value.trim();
575
- if (query) {
576
- url.searchParams.set('search', query);
577
- } else {
578
- url.searchParams.delete('search');
579
- }
622
+ if (query) url.searchParams.set('search', query);
623
+ else url.searchParams.delete('search');
580
624
  window.location.href = url.toString();
581
625
  }
582
626
  });
@@ -591,32 +635,24 @@ router.get("/projects/:id", (req, res) => {
591
635
  });
592
636
  /**
593
637
  * GET /projects/:projectId/sessions/:sessionId
594
- * Session detail view
638
+ * Session detail view with cost breakdown
595
639
  */
596
640
  router.get("/projects/:projectId/sessions/:sessionId", (req, res) => {
597
641
  try {
598
642
  const project = (0, project_scanner_1.getProjectDetail)(req.params.projectId);
599
- if (!project) {
643
+ if (!project)
600
644
  return res.status(404).send("Project not found");
601
- }
602
645
  const projectPath = (0, project_scanner_1.getSafeProjectPath)(req.params.projectId);
603
- if (!projectPath) {
646
+ if (!projectPath)
604
647
  return res.status(404).send("Invalid project path");
605
- }
606
648
  const session = (0, session_analyzer_1.getSessionDetail)(req.params.projectId, req.params.sessionId, projectPath);
607
- if (!session) {
649
+ if (!session)
608
650
  return res.status(404).send("Session not found");
609
- }
610
- const totalTokens = session.meta.cost.tokenUsage.inputTokens +
611
- session.meta.cost.tokenUsage.outputTokens +
612
- session.meta.cost.tokenUsage.cacheCreationTokens +
613
- session.meta.cost.tokenUsage.cacheReadTokens;
651
+ const costBreakdown = session.meta.costBreakdown;
652
+ const subagents = session.meta.subagents || [];
653
+ const hasSubagents = subagents.length > 0;
614
654
  const content = `
615
- <div class="breadcrumb">
616
- <a href="/">Dashboard</a> /
617
- <a href="/projects/${e(project.id)}">${e(project.name)}</a> /
618
- Session
619
- </div>
655
+ <a href="/projects/${e(project.id)}" class="back-link">← Back to ${e(project.name)}</a>
620
656
 
621
657
  <div class="stats-grid">
622
658
  <div class="stat-card">
@@ -627,107 +663,154 @@ router.get("/projects/:projectId/sessions/:sessionId", (req, res) => {
627
663
  <h3>Total Messages</h3>
628
664
  <div class="value">${formatNumber(session.meta.messageCount)}</div>
629
665
  </div>
630
- <div class="stat-card">
631
- <h3>Total Tokens</h3>
632
- <div class="value">${formatNumber(totalTokens)}</div>
633
- </div>
634
666
  <div class="stat-card">
635
667
  <h3>Total Cost</h3>
636
668
  <div class="value cost">${formatCurrency(session.meta.cost.totalUsd)}</div>
637
669
  </div>
670
+ <div class="stat-card">
671
+ <h3>Subagents</h3>
672
+ <div class="value">${subagents.length}</div>
673
+ </div>
674
+ </div>
675
+
676
+ <div class="section">
677
+ <h2>💰 Cost Breakdown</h2>
678
+ ${renderCostCard("🟰 Total (Main + Subagents)", costBreakdown.total, true)}
679
+ ${renderCostCard("📄 Main Session", costBreakdown.main)}
680
+ ${hasSubagents ? renderCostCard("🤖 Subagents (Total)", costBreakdown.subagents) : ""}
638
681
  </div>
639
682
 
683
+ ${hasSubagents ? `
640
684
  <div class="section">
641
- <h2>Token Usage Breakdown</h2>
642
- <div class="cost-breakdown">
643
- <div class="cost-item">
644
- <div class="cost-item-label">Input Tokens</div>
645
- <div>${formatNumber(session.meta.cost.tokenUsage.inputTokens)}</div>
646
- <div class="cost">${formatCurrency(session.meta.cost.breakdown.inputTokensUsd)}</div>
647
- </div>
648
- <div class="cost-item">
649
- <div class="cost-item-label">Output Tokens</div>
650
- <div>${formatNumber(session.meta.cost.tokenUsage.outputTokens)}</div>
651
- <div class="cost">${formatCurrency(session.meta.cost.breakdown.outputTokensUsd)}</div>
652
- </div>
653
- <div class="cost-item">
654
- <div class="cost-item-label">Cache Creation</div>
655
- <div>${formatNumber(session.meta.cost.tokenUsage.cacheCreationTokens)}</div>
656
- <div class="cost">${formatCurrency(session.meta.cost.breakdown.cacheCreationUsd)}</div>
657
- </div>
658
- <div class="cost-item">
659
- <div class="cost-item-label">Cache Read</div>
660
- <div>${formatNumber(session.meta.cost.tokenUsage.cacheReadTokens)}</div>
661
- <div class="cost">${formatCurrency(session.meta.cost.breakdown.cacheReadUsd)}</div>
662
- </div>
685
+ <h2>🤖 Subagents (${subagents.length})</h2>
686
+ <div class="subagent-list">
687
+ ${subagents.map((subagent) => {
688
+ const totalTokens = subagent.cost.tokenUsage.inputTokens + subagent.cost.tokenUsage.outputTokens +
689
+ subagent.cost.tokenUsage.cacheCreationTokens + subagent.cost.tokenUsage.cacheReadTokens;
690
+ return `
691
+ <a href="/projects/${e(project.id)}/sessions/${e(session.id)}/subagents/${e(subagent.agentId)}" class="subagent-item">
692
+ <div class="subagent-icon">
693
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
694
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
695
+ </svg>
696
+ </div>
697
+ <div class="subagent-info">
698
+ <div class="subagent-name">${subagent.firstMessage ? e(subagent.firstMessage.substring(0, 60)) : `Agent ${subagent.agentId.slice(0, 8)}`}</div>
699
+ <div class="subagent-id">${e(subagent.agentId)}</div>
700
+ </div>
701
+ <div class="subagent-cost">
702
+ <div class="subagent-cost-amount">${formatCurrency(subagent.cost.totalUsd)}</div>
703
+ <div class="subagent-cost-tokens">${formatNumber(totalTokens)} tokens</div>
704
+ </div>
705
+ </a>
706
+ `;
707
+ }).join("")}
663
708
  </div>
664
- ${totalTokens > 0
665
- ? `<div class="token-bar">
666
- <div class="token-bar-input" style="width: ${(session.meta.cost.tokenUsage.inputTokens / totalTokens) * 100}%"></div>
667
- <div class="token-bar-output" style="width: ${(session.meta.cost.tokenUsage.outputTokens / totalTokens) * 100}%"></div>
668
- <div class="token-bar-cache" style="width: ${((session.meta.cost.tokenUsage.cacheCreationTokens +
669
- session.meta.cost.tokenUsage.cacheReadTokens) /
670
- totalTokens) *
671
- 100}%"></div>
672
- </div>`
673
- : ""}
674
709
  </div>
710
+ ` : ""}
675
711
 
676
712
  <div class="section">
677
- <h2>Message Counts</h2>
678
- <div class="cost-breakdown">
679
- <div class="cost-item">
680
- <div class="cost-item-label">User Messages</div>
681
- <div>${formatNumber(session.userMessageCount)}</div>
682
- </div>
683
- <div class="cost-item">
684
- <div class="cost-item-label">Assistant Messages</div>
685
- <div>${formatNumber(session.assistantMessageCount)}</div>
686
- </div>
687
- <div class="cost-item">
688
- <div class="cost-item-label">System Messages</div>
689
- <div>${formatNumber(session.systemMessageCount)}</div>
690
- </div>
691
- <div class="cost-item">
692
- <div class="cost-item-label">Model</div>
693
- <div>${e(session.meta.modelName)}</div>
694
- </div>
713
+ <h2>💬 Conversations (${session.conversations.length})</h2>
714
+ <div class="conversation-list">
715
+ ${session.conversations.filter((c) => c.type !== "x-error").map((c) => {
716
+ const typeClass = c.type === "user" ? "type-user" : c.type === "assistant" ? "type-assistant" : "type-system";
717
+ return `
718
+ <div class="conversation-item">
719
+ <div class="conversation-header">
720
+ <span class="conversation-type ${typeClass}">${e(c.type)}</span>
721
+ <span class="timestamp">${e(new Date(c.timestamp).toLocaleString())}</span>
722
+ ${c.message?.usage ? `<span class="badge badge-green">${c.message.usage.input_tokens + c.message.usage.output_tokens} tokens</span>` : ""}
723
+ </div>
724
+ <div class="message-content">
725
+ ${renderMessageContent(c.message)}
726
+ </div>
727
+ </div>
728
+ `;
729
+ }).join("")}
730
+ </div>
731
+ </div>
732
+ `;
733
+ res.send(renderLayout(`Session: ${session.id}`, content));
734
+ }
735
+ catch (error) {
736
+ res.status(500).send(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
737
+ }
738
+ });
739
+ /**
740
+ * GET /projects/:projectId/sessions/:sessionId/subagents/:agentId
741
+ * Subagent detail view
742
+ */
743
+ router.get("/projects/:projectId/sessions/:sessionId/subagents/:agentId", (req, res) => {
744
+ try {
745
+ const project = (0, project_scanner_1.getProjectDetail)(req.params.projectId);
746
+ if (!project)
747
+ return res.status(404).send("Project not found");
748
+ const projectPath = (0, project_scanner_1.getSafeProjectPath)(req.params.projectId);
749
+ if (!projectPath)
750
+ return res.status(404).send("Invalid project path");
751
+ const subagent = (0, session_analyzer_1.getSubagentDetail)(req.params.projectId, req.params.sessionId, req.params.agentId, projectPath);
752
+ if (!subagent)
753
+ return res.status(404).send("Subagent not found");
754
+ const totalTokens = subagent.cost.tokenUsage.inputTokens + subagent.cost.tokenUsage.outputTokens +
755
+ subagent.cost.tokenUsage.cacheCreationTokens + subagent.cost.tokenUsage.cacheReadTokens;
756
+ const content = `
757
+ <a href="/projects/${e(project.id)}/sessions/${e(req.params.sessionId)}" class="back-link">← Back to Session</a>
758
+
759
+ <div class="stats-grid">
760
+ <div class="stat-card">
761
+ <h3>Subagent</h3>
762
+ <div class="value" style="font-size: 1rem; word-break: break-all;">${e(subagent.agentId)}</div>
763
+ </div>
764
+ <div class="stat-card">
765
+ <h3>Messages</h3>
766
+ <div class="value">${formatNumber(subagent.conversations.length)}</div>
767
+ </div>
768
+ <div class="stat-card">
769
+ <h3>Total Tokens</h3>
770
+ <div class="value">${formatNumber(totalTokens)}</div>
695
771
  </div>
772
+ <div class="stat-card">
773
+ <h3>Cost</h3>
774
+ <div class="value cost">${formatCurrency(subagent.cost.totalUsd)}</div>
775
+ </div>
776
+ </div>
777
+
778
+ <div class="section">
779
+ <h2>💰 Cost Details</h2>
780
+ ${renderCostCard("🤖 Subagent Cost", subagent.cost, true)}
696
781
  </div>
697
782
 
783
+ ${subagent.firstMessage ? `
698
784
  <div class="section">
699
- <h2>First User Message</h2>
785
+ <h2>📝 First Message</h2>
700
786
  <div style="background: #0d1117; padding: 15px; border-radius: 4px; font-family: monospace; white-space: pre-wrap;">
701
- ${session.meta.firstUserMessage ? e(session.meta.firstUserMessage) : "No user message found"}
787
+ ${e(subagent.firstMessage)}
702
788
  </div>
703
789
  </div>
790
+ ` : ""}
704
791
 
705
792
  <div class="section">
706
- <h2>Conversations (${session.conversations.length})</h2>
793
+ <h2>💬 Conversations (${subagent.conversations.length})</h2>
707
794
  <div class="conversation-list">
708
- ${session.conversations
709
- .filter((c) => c.type !== "x-error")
710
- .map((c) => {
711
- const typeClass = c.type === "user"
712
- ? "type-user"
713
- : c.type === "assistant"
714
- ? "type-assistant"
715
- : "type-system";
795
+ ${subagent.conversations.filter((c) => c.type !== "x-error").map((c) => {
796
+ const typeClass = c.type === "user" ? "type-user" : c.type === "assistant" ? "type-assistant" : "type-system";
716
797
  return `
717
798
  <div class="conversation-item">
718
- <span class="conversation-type ${typeClass}">${e(c.type)}</span>
719
- <span class="timestamp">${e(new Date(c.timestamp).toLocaleString())}</span>
720
- ${c.message?.usage
721
- ? `<span class="badge badge-green" style="margin-left: 10px;">${c.message.usage.input_tokens + c.message.usage.output_tokens} tokens</span>`
722
- : ""}
799
+ <div class="conversation-header">
800
+ <span class="conversation-type ${typeClass}">${e(c.type)}</span>
801
+ <span class="timestamp">${e(new Date(c.timestamp).toLocaleString())}</span>
802
+ ${c.message?.usage ? `<span class="badge badge-green">${c.message.usage.input_tokens + c.message.usage.output_tokens} tokens</span>` : ""}
803
+ </div>
804
+ <div class="message-content">
805
+ ${renderMessageContent(c.message)}
806
+ </div>
723
807
  </div>
724
808
  `;
725
- })
726
- .join("")}
809
+ }).join("")}
727
810
  </div>
728
811
  </div>
729
812
  `;
730
- res.send(renderLayout(`Session: ${session.id}`, content));
813
+ res.send(renderLayout(`Subagent: ${subagent.agentId}`, content));
731
814
  }
732
815
  catch (error) {
733
816
  res.status(500).send(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);