@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.
- package/dist/lib/cost-breakdown.d.ts +42 -0
- package/dist/lib/cost-breakdown.d.ts.map +1 -0
- package/dist/lib/cost-breakdown.js +146 -0
- package/dist/lib/cost-breakdown.js.map +1 -0
- package/dist/lib/cost-calculator.d.ts.map +1 -1
- package/dist/lib/cost-calculator.js +13 -0
- package/dist/lib/cost-calculator.js.map +1 -1
- package/dist/lib/jsonl-parser.d.ts +1 -0
- package/dist/lib/jsonl-parser.d.ts.map +1 -1
- package/dist/lib/jsonl-parser.js.map +1 -1
- package/dist/lib/pricing.d.ts +2 -0
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/pricing.js +106 -15
- package/dist/lib/pricing.js.map +1 -1
- package/dist/routes/api.d.ts.map +1 -1
- package/dist/routes/api.js +32 -0
- package/dist/routes/api.js.map +1 -1
- package/dist/routes/views.d.ts.map +1 -1
- package/dist/routes/views.js +532 -449
- package/dist/routes/views.js.map +1 -1
- package/dist/server/session-analyzer.d.ts +14 -2
- package/dist/server/session-analyzer.d.ts.map +1 -1
- package/dist/server/session-analyzer.js +141 -39
- package/dist/server/session-analyzer.js.map +1 -1
- package/package.json +1 -1
package/dist/routes/views.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
45
|
-
function
|
|
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
|
-
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
.
|
|
204
|
-
|
|
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
|
-
.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
border
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
257
|
-
margin
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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="
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
475
|
-
|
|
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="
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
611
|
-
|
|
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
|
-
<
|
|
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
|
|
642
|
-
<div class="
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
678
|
-
<div class="
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
|
785
|
+
<h2>📝 First Message</h2>
|
|
700
786
|
<div style="background: #0d1117; padding: 15px; border-radius: 4px; font-family: monospace; white-space: pre-wrap;">
|
|
701
|
-
${
|
|
787
|
+
${e(subagent.firstMessage)}
|
|
702
788
|
</div>
|
|
703
789
|
</div>
|
|
790
|
+
` : ""}
|
|
704
791
|
|
|
705
792
|
<div class="section">
|
|
706
|
-
<h2
|
|
793
|
+
<h2>💬 Conversations (${subagent.conversations.length})</h2>
|
|
707
794
|
<div class="conversation-list">
|
|
708
|
-
${
|
|
709
|
-
.
|
|
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
|
-
<
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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(`
|
|
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"}`);
|