@lim324/my-claude-code-viewer 0.0.1

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.
@@ -0,0 +1,737 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = require("express");
7
+ const fuse_js_1 = __importDefault(require("fuse.js"));
8
+ const escape_html_1 = __importDefault(require("escape-html"));
9
+ const project_scanner_1 = require("../server/project-scanner");
10
+ const session_analyzer_1 = require("../server/session-analyzer");
11
+ const router = (0, express_1.Router)();
12
+ // Helper function to format currency
13
+ function formatCurrency(amount) {
14
+ return `$${amount.toFixed(4)}`;
15
+ }
16
+ // Helper function to format number
17
+ function formatNumber(num) {
18
+ return num.toLocaleString();
19
+ }
20
+ // Helper function to format date
21
+ function formatDate(date) {
22
+ return new Date(date).toLocaleString();
23
+ }
24
+ // Helper function to escape HTML to prevent XSS
25
+ function e(text) {
26
+ if (text === null || text === undefined)
27
+ return "";
28
+ return (0, escape_html_1.default)(String(text));
29
+ }
30
+ // Helper function to highlight search matches (with XSS protection)
31
+ function highlightMatch(text, query) {
32
+ if (!query)
33
+ return e(text);
34
+ const safeText = e(text);
35
+ const safeQuery = e(query);
36
+ // Use a simple case-insensitive replace for highlighting
37
+ const regex = new RegExp(`(${escapeRegExp(safeQuery)})`, 'gi');
38
+ return safeText.replace(regex, '<span class="highlight">$1</span>');
39
+ }
40
+ // Helper function to escape special regex characters
41
+ function escapeRegExp(string) {
42
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
43
+ }
44
+ // HTML layout template
45
+ function renderLayout(title, content) {
46
+ 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>
53
+ <style>
54
+ * {
55
+ box-sizing: border-box;
56
+ margin: 0;
57
+ padding: 0;
58
+ }
59
+ body {
60
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
61
+ background: #0d1117;
62
+ color: #c9d1d9;
63
+ line-height: 1.6;
64
+ }
65
+ .container {
66
+ max-width: 1200px;
67
+ margin: 0 auto;
68
+ padding: 20px;
69
+ }
70
+ header {
71
+ background: #161b22;
72
+ border-bottom: 1px solid #30363d;
73
+ padding: 20px 0;
74
+ margin-bottom: 30px;
75
+ }
76
+ header h1 {
77
+ color: #58a6ff;
78
+ font-size: 1.8rem;
79
+ }
80
+ header h1 a {
81
+ color: inherit;
82
+ text-decoration: none;
83
+ }
84
+ .stats-grid {
85
+ display: grid;
86
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
87
+ gap: 20px;
88
+ margin-bottom: 30px;
89
+ }
90
+ .stat-card {
91
+ background: #161b22;
92
+ border: 1px solid #30363d;
93
+ border-radius: 8px;
94
+ padding: 20px;
95
+ text-align: center;
96
+ }
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
+ }
109
+ .section {
110
+ background: #161b22;
111
+ border: 1px solid #30363d;
112
+ border-radius: 8px;
113
+ padding: 20px;
114
+ margin-bottom: 20px;
115
+ }
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;
136
+ text-transform: uppercase;
137
+ }
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;
186
+ }
187
+ .token-bar {
188
+ display: flex;
189
+ height: 20px;
190
+ border-radius: 4px;
191
+ overflow: hidden;
192
+ margin-top: 8px;
193
+ }
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;
205
+ overflow-y: auto;
206
+ }
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;
245
+ }
246
+ .cost-item {
247
+ 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
+ }
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;
266
+ 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
+ }
291
+ </style>
292
+ </head>
293
+ <body>
294
+ <header>
295
+ <div class="container">
296
+ <h1><a href="/">🔍 Claude Code Viewer</a></h1>
297
+ </div>
298
+ </header>
299
+ <div class="container">
300
+ ${content}
301
+ </div>
302
+ </body>
303
+ </html>
304
+ `;
305
+ }
306
+ /**
307
+ * GET /
308
+ * Dashboard with all projects
309
+ */
310
+ router.get("/", (req, res) => {
311
+ try {
312
+ const stats = (0, project_scanner_1.getGlobalStats)();
313
+ const projects = (0, project_scanner_1.scanProjects)();
314
+ const searchQuery = req.query.search || "";
315
+ // Setup Fuse.js for fuzzy search
316
+ let filteredProjects = projects;
317
+ if (searchQuery) {
318
+ const fuse = new fuse_js_1.default(projects, {
319
+ keys: ["name", "id"],
320
+ threshold: 0.4,
321
+ includeScore: true,
322
+ });
323
+ const results = fuse.search(searchQuery);
324
+ filteredProjects = results.map((r) => r.item);
325
+ }
326
+ const content = `
327
+ <div class="stats-grid">
328
+ <div class="stat-card">
329
+ <h3>Total Projects</h3>
330
+ <div class="value">${formatNumber(stats.totalProjects)}</div>
331
+ </div>
332
+ <div class="stat-card">
333
+ <h3>Total Sessions</h3>
334
+ <div class="value">${formatNumber(stats.totalSessions)}</div>
335
+ </div>
336
+ <div class="stat-card">
337
+ <h3>Total Messages</h3>
338
+ <div class="value">${formatNumber(stats.totalMessages)}</div>
339
+ </div>
340
+ <div class="stat-card">
341
+ <h3>Total Cost</h3>
342
+ <div class="value cost">${formatCurrency(stats.totalCost)}</div>
343
+ </div>
344
+ </div>
345
+
346
+ <div class="section">
347
+ <h2>Projects</h2>
348
+ <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
+ />
357
+ ${searchQuery ? `<div class="search-results-info">Showing ${filteredProjects.length} of ${projects.length} projects</div>` : ""}
358
+ </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>`}
389
+ </div>
390
+
391
+ <script>
392
+ // Client-side search for instant feedback
393
+ const searchInput = document.getElementById('project-search');
394
+ const projectsTable = document.getElementById('projects-table');
395
+
396
+ if (searchInput && projectsTable) {
397
+ const rows = projectsTable.querySelectorAll('tbody tr');
398
+
399
+ searchInput.addEventListener('input', (e) => {
400
+ const query = e.target.value.toLowerCase().trim();
401
+
402
+ rows.forEach(row => {
403
+ const name = row.getAttribute('data-project-name');
404
+ 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
+ }
411
+ });
412
+
413
+ // Update URL without page reload
414
+ const url = new URL(window.location);
415
+ if (query) {
416
+ url.searchParams.set('search', query);
417
+ } else {
418
+ url.searchParams.delete('search');
419
+ }
420
+ window.history.replaceState({}, '', url);
421
+ });
422
+
423
+ // Handle Enter key to submit search
424
+ searchInput.addEventListener('keypress', (e) => {
425
+ if (e.key === 'Enter') {
426
+ const url = new URL(window.location);
427
+ const query = e.target.value.trim();
428
+ if (query) {
429
+ url.searchParams.set('search', query);
430
+ } else {
431
+ url.searchParams.delete('search');
432
+ }
433
+ window.location.href = url.toString();
434
+ }
435
+ });
436
+ }
437
+ </script>
438
+ `;
439
+ res.send(renderLayout("Dashboard", content));
440
+ }
441
+ catch (error) {
442
+ res.status(500).send(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
443
+ }
444
+ });
445
+ /**
446
+ * GET /projects/:id
447
+ * Project detail with sessions
448
+ */
449
+ router.get("/projects/:id", (req, res) => {
450
+ try {
451
+ const project = (0, project_scanner_1.getProjectDetail)(req.params.id);
452
+ if (!project) {
453
+ return res.status(404).send("Project not found");
454
+ }
455
+ const searchQuery = req.query.search || "";
456
+ // Setup Fuse.js for fuzzy search on sessionId
457
+ let filteredSessions = project.sessions;
458
+ if (searchQuery) {
459
+ const fuse = new fuse_js_1.default(project.sessions, {
460
+ keys: ["id"],
461
+ threshold: 0.4,
462
+ includeScore: true,
463
+ });
464
+ const results = fuse.search(searchQuery);
465
+ filteredSessions = results.map((r) => r.item);
466
+ }
467
+ const content = `
468
+ <div class="breadcrumb">
469
+ <a href="/">← Back to Dashboard</a>
470
+ </div>
471
+
472
+ <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>
489
+ </div>
490
+
491
+ <div class="section">
492
+ <h2>Sessions</h2>
493
+ <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
+ />
502
+ ${searchQuery ? `<div class="search-results-info">Showing ${filteredSessions.length} of ${project.sessions.length} sessions</div>` : ""}
503
+ </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>
517
+ </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>`}
537
+ </div>
538
+
539
+ <script>
540
+ // Client-side search for instant feedback
541
+ const searchInput = document.getElementById('session-search');
542
+ const sessionsTable = document.getElementById('sessions-table');
543
+
544
+ if (searchInput && sessionsTable) {
545
+ const rows = sessionsTable.querySelectorAll('tbody tr');
546
+
547
+ searchInput.addEventListener('input', (e) => {
548
+ const query = e.target.value.toLowerCase().trim();
549
+
550
+ 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
+ }
558
+ });
559
+
560
+ // Update URL without page reload
561
+ const url = new URL(window.location);
562
+ if (query) {
563
+ url.searchParams.set('search', query);
564
+ } else {
565
+ url.searchParams.delete('search');
566
+ }
567
+ window.history.replaceState({}, '', url);
568
+ });
569
+
570
+ // Handle Enter key to submit search
571
+ searchInput.addEventListener('keypress', (e) => {
572
+ if (e.key === 'Enter') {
573
+ const url = new URL(window.location);
574
+ const query = e.target.value.trim();
575
+ if (query) {
576
+ url.searchParams.set('search', query);
577
+ } else {
578
+ url.searchParams.delete('search');
579
+ }
580
+ window.location.href = url.toString();
581
+ }
582
+ });
583
+ }
584
+ </script>
585
+ `;
586
+ res.send(renderLayout(project.name, content));
587
+ }
588
+ catch (error) {
589
+ res.status(500).send(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
590
+ }
591
+ });
592
+ /**
593
+ * GET /projects/:projectId/sessions/:sessionId
594
+ * Session detail view
595
+ */
596
+ router.get("/projects/:projectId/sessions/:sessionId", (req, res) => {
597
+ try {
598
+ const project = (0, project_scanner_1.getProjectDetail)(req.params.projectId);
599
+ if (!project) {
600
+ return res.status(404).send("Project not found");
601
+ }
602
+ const projectPath = (0, project_scanner_1.getSafeProjectPath)(req.params.projectId);
603
+ if (!projectPath) {
604
+ return res.status(404).send("Invalid project path");
605
+ }
606
+ const session = (0, session_analyzer_1.getSessionDetail)(req.params.projectId, req.params.sessionId, projectPath);
607
+ if (!session) {
608
+ 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;
614
+ 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>
620
+
621
+ <div class="stats-grid">
622
+ <div class="stat-card">
623
+ <h3>Session</h3>
624
+ <div class="value" style="font-size: 1rem; word-break: break-all;">${e(session.id)}</div>
625
+ </div>
626
+ <div class="stat-card">
627
+ <h3>Total Messages</h3>
628
+ <div class="value">${formatNumber(session.meta.messageCount)}</div>
629
+ </div>
630
+ <div class="stat-card">
631
+ <h3>Total Tokens</h3>
632
+ <div class="value">${formatNumber(totalTokens)}</div>
633
+ </div>
634
+ <div class="stat-card">
635
+ <h3>Total Cost</h3>
636
+ <div class="value cost">${formatCurrency(session.meta.cost.totalUsd)}</div>
637
+ </div>
638
+ </div>
639
+
640
+ <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>
663
+ </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
+ </div>
675
+
676
+ <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>
695
+ </div>
696
+ </div>
697
+
698
+ <div class="section">
699
+ <h2>First User Message</h2>
700
+ <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"}
702
+ </div>
703
+ </div>
704
+
705
+ <div class="section">
706
+ <h2>Conversations (${session.conversations.length})</h2>
707
+ <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";
716
+ return `
717
+ <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
+ : ""}
723
+ </div>
724
+ `;
725
+ })
726
+ .join("")}
727
+ </div>
728
+ </div>
729
+ `;
730
+ res.send(renderLayout(`Session: ${session.id}`, content));
731
+ }
732
+ catch (error) {
733
+ res.status(500).send(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
734
+ }
735
+ });
736
+ exports.default = router;
737
+ //# sourceMappingURL=views.js.map