ccakashic 0.1.1 → 0.2.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.
- package/README.md +11 -4
- package/lib/html-generator.js +77 -10
- package/lib/parser.js +160 -12
- package/lib/template-assets.js +65 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
An Akashic Record of your Claude Code sessions — browse Claude Code session logs (`~/.claude/projects/`) as beautiful HTML in your browser.
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
5
9
|
## Usage
|
|
6
10
|
|
|
7
11
|
### npx
|
|
@@ -23,13 +27,16 @@ A local HTTP server starts and your browser opens automatically.
|
|
|
23
27
|
## Features
|
|
24
28
|
|
|
25
29
|
- **Fully browser-based** — Project list → Session list → Conversation detail
|
|
26
|
-
- **Chat-style layout** — User / assistant messages in bubbles
|
|
27
|
-
- **Collapsible tool calls** — Bash, Read, Edit, and other tool invocations
|
|
30
|
+
- **Chat-style layout** — User / assistant messages in chat bubbles
|
|
31
|
+
- **Collapsible tool calls** — Bash, Read, Edit, and other tool invocations collapsed by default
|
|
28
32
|
- **Diff view** — File edits shown with red/green line highlights
|
|
29
33
|
- **Date navigation** — Side nav and sticky headers to jump between dates
|
|
30
|
-
- **
|
|
34
|
+
- **Cost estimation** — Per-turn and per-message USD cost based on Claude Opus 4 pricing (input / output / cache read / cache write breakdown)
|
|
35
|
+
- **Elapsed time** — Per-turn duration and per-tool execution time derived from timestamps
|
|
36
|
+
- **Local command display** — `!` shell commands rendered with prompt and output
|
|
31
37
|
- **Inline subagent conversations** — Subagent dialogues nested inside the Agent tool_use that spawned them
|
|
32
|
-
- **Permalinks** — Click any message timestamp to get a shareable URL
|
|
38
|
+
- **Permalinks** — Click any message timestamp to get a shareable URL (`#t20260416103045`)
|
|
39
|
+
- **Session-level stats** — Estimated cost, turns, token breakdown, cache hit rate, and duration in the header
|
|
33
40
|
- **Dark mode** — Follows `prefers-color-scheme` automatically
|
|
34
41
|
- **Filter search** — Incremental filtering on list pages
|
|
35
42
|
- **Keyboard navigation** — `j` / `k` to move between messages
|
package/lib/html-generator.js
CHANGED
|
@@ -132,12 +132,40 @@ function renderMessage(msg) {
|
|
|
132
132
|
const id = msgId(msg.timestamp);
|
|
133
133
|
const time = `<a class="timestamp" href="#${id}">${formatTime(msg.timestamp)}</a>`;
|
|
134
134
|
|
|
135
|
+
const turnBadge = msg.turnUsage
|
|
136
|
+
? (() => {
|
|
137
|
+
const c = calcCost(msg.turnUsage.input, msg.turnUsage.output, msg.turnUsage.cacheRead, msg.turnUsage.cacheCreate);
|
|
138
|
+
const dur = msg.turnUsage.durationMs ? ` | ${formatDuration(msg.turnUsage.durationMs)}` : '';
|
|
139
|
+
return `<span class="turn-usage">⚡ Turn: ${c.totalStr}${dur} <span class="usage-detail">(in:${c.inStr} out:${c.outStr} cache-r:${c.crStr} cache-w:${c.cwStr})</span></span>`;
|
|
140
|
+
})()
|
|
141
|
+
: '';
|
|
142
|
+
|
|
143
|
+
function makeItemBadge(m) {
|
|
144
|
+
const parts = [];
|
|
145
|
+
const c = m.usage
|
|
146
|
+
? calcCost(m.usage.input_tokens || 0, m.usage.output_tokens || 0, m.usage.cache_read_input_tokens || 0, m.usage.cache_creation_input_tokens || 0)
|
|
147
|
+
: null;
|
|
148
|
+
if (c) {
|
|
149
|
+
parts.push(`${c.totalStr}`);
|
|
150
|
+
}
|
|
151
|
+
if (m.execMs) {
|
|
152
|
+
parts.push(`${formatDuration(m.execMs)}`);
|
|
153
|
+
} else if (m.elapsedMs && m.elapsedMs >= 1000) {
|
|
154
|
+
parts.push(`${formatDuration(m.elapsedMs)}`);
|
|
155
|
+
}
|
|
156
|
+
if (!parts.length) return '';
|
|
157
|
+
const detail = c ? ` <span class="usage-detail">(in:${c.inStr} out:${c.outStr} cache-r:${c.crStr} cache-w:${c.cwStr})</span>` : '';
|
|
158
|
+
return `<span class="item-usage">${parts.join(' | ')}${detail}</span>`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const itemBadge = makeItemBadge(msg);
|
|
162
|
+
|
|
135
163
|
switch (msg.type) {
|
|
136
164
|
case 'user':
|
|
137
|
-
return `<div class="msg msg-user" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div
|
|
165
|
+
return `<div class="msg msg-user" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div>${turnBadge}</div>`;
|
|
138
166
|
|
|
139
167
|
case 'assistant':
|
|
140
|
-
return `<div class="msg msg-assistant" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div
|
|
168
|
+
return `<div class="msg msg-assistant" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div>${itemBadge ? `<div>${itemBadge}</div>` : ''}</div>`;
|
|
141
169
|
|
|
142
170
|
case 'thinking':
|
|
143
171
|
return `<div class="msg msg-thinking" id="${id}">${time}<span class="thinking-indicator">Thinking...</span></div>`;
|
|
@@ -148,16 +176,23 @@ function renderMessage(msg) {
|
|
|
148
176
|
const subagentHtml = msg.subagentMessages
|
|
149
177
|
? `<div class="subagent-inline"><details><summary><span class="tool-summary">Subagent conversation</span></summary><div class="subagent-content">${msg.subagentMessages.map(renderMessage).join('\n')}</div></details></div>`
|
|
150
178
|
: '';
|
|
151
|
-
return `<div class="msg msg-tool" id="${id}"><details><summary>${time}<span class="tool-summary">${summary}</span></summary><div class="tool-details">${renderToolInput(msg)}${resultHtml}${subagentHtml}</div></details
|
|
179
|
+
return `<div class="msg msg-tool" id="${id}"><details><summary>${time}<span class="tool-summary">${summary}</span></summary><div class="tool-details">${renderToolInput(msg)}${resultHtml}${subagentHtml}</div></details>${itemBadge ? `<div class="tool-usage-row">${itemBadge}</div>` : ''}</div>`;
|
|
152
180
|
}
|
|
153
181
|
|
|
154
182
|
case 'tool_result':
|
|
155
183
|
// Unpaired tool result (shouldn't happen often)
|
|
156
184
|
return `<div class="msg msg-tool" id="${id}"><div class="tool-output"><pre><code>${escapeHtml((msg.content || '').slice(0, 2000))}</code></pre></div></div>`;
|
|
157
185
|
|
|
186
|
+
case 'local_command': {
|
|
187
|
+
const cmd = msg.command ? `<div class="local-cmd-input"><span class="local-cmd-prompt">$</span> ${escapeHtml(msg.command)}</div>` : '';
|
|
188
|
+
const stdout = msg.stdout && msg.stdout.trim() ? `<pre class="local-cmd-output"><code>${escapeHtml(msg.stdout)}</code></pre>` : '';
|
|
189
|
+
const stderr = msg.stderr && msg.stderr.trim() ? `<pre class="local-cmd-output local-cmd-stderr"><code>${escapeHtml(msg.stderr)}</code></pre>` : '';
|
|
190
|
+
return `<div class="msg msg-local-cmd" id="${id}">${time}${cmd}${stdout}${stderr}${turnBadge}</div>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
158
193
|
case 'system':
|
|
159
194
|
if (msg.subtype === 'turn_duration') {
|
|
160
|
-
return
|
|
195
|
+
return ''; // Now shown in turn usage badge
|
|
161
196
|
}
|
|
162
197
|
return `<div class="msg msg-system" id="${id}">${escapeHtml(msg.content || '')}</div>`;
|
|
163
198
|
|
|
@@ -214,6 +249,40 @@ function groupMessagesByDate(messages) {
|
|
|
214
249
|
return groups;
|
|
215
250
|
}
|
|
216
251
|
|
|
252
|
+
// Cost per 1M tokens (USD) - Claude Opus 4 pricing
|
|
253
|
+
const COST_PER_M = {
|
|
254
|
+
input: 15,
|
|
255
|
+
output: 75,
|
|
256
|
+
cacheRead: 1.5,
|
|
257
|
+
cacheWrite: 18.75,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
function tokenCostUsd(tokens, rate) {
|
|
261
|
+
return (tokens / 1_000_000) * rate;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function formatCost(usd) {
|
|
265
|
+
if (usd < 0.001) return '<$0.01';
|
|
266
|
+
if (usd < 0.01) return `$${usd.toFixed(3)}`;
|
|
267
|
+
if (usd < 1) return `$${usd.toFixed(2)}`;
|
|
268
|
+
return `$${usd.toFixed(2)}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function calcCost(input, output, cacheRead, cacheWrite) {
|
|
272
|
+
const inCost = tokenCostUsd(input, COST_PER_M.input);
|
|
273
|
+
const outCost = tokenCostUsd(output, COST_PER_M.output);
|
|
274
|
+
const crCost = tokenCostUsd(cacheRead, COST_PER_M.cacheRead);
|
|
275
|
+
const cwCost = tokenCostUsd(cacheWrite, COST_PER_M.cacheWrite);
|
|
276
|
+
const total = inCost + outCost + crCost + cwCost;
|
|
277
|
+
return {
|
|
278
|
+
totalStr: formatCost(total),
|
|
279
|
+
inStr: formatTokens(input),
|
|
280
|
+
outStr: formatTokens(output),
|
|
281
|
+
crStr: formatTokens(cacheRead),
|
|
282
|
+
cwStr: formatTokens(cacheWrite),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
217
286
|
function formatTokens(n) {
|
|
218
287
|
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
219
288
|
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
@@ -233,20 +302,18 @@ function formatDurationLong(ms) {
|
|
|
233
302
|
function renderStats(stats) {
|
|
234
303
|
if (!stats || !stats.turns) return '';
|
|
235
304
|
|
|
305
|
+
const totalCost = calcCost(stats.inputTokens, stats.outputTokens, stats.cacheRead, stats.cacheCreation);
|
|
306
|
+
|
|
236
307
|
const items = [];
|
|
308
|
+
items.push(`<span class="stat-item"><span class="stat-label">Est. Cost</span><span class="stat-value">${totalCost.totalStr}</span></span>`);
|
|
237
309
|
items.push(`<span class="stat-item"><span class="stat-label">Turns</span><span class="stat-value">${stats.turns}</span></span>`);
|
|
238
310
|
items.push(`<span class="stat-item"><span class="stat-label">Input</span><span class="stat-value">${formatTokens(stats.inputTokens)}</span></span>`);
|
|
239
311
|
items.push(`<span class="stat-item"><span class="stat-label">Output</span><span class="stat-value">${formatTokens(stats.outputTokens)}</span></span>`);
|
|
240
|
-
items.push(`<span class="stat-item"><span class="stat-label">Cache Create</span><span class="stat-value">${formatTokens(stats.cacheCreation)}</span></span>`);
|
|
241
312
|
items.push(`<span class="stat-item"><span class="stat-label">Cache Read</span><span class="stat-value">${formatTokens(stats.cacheRead)}</span></span>`);
|
|
242
|
-
items.push(`<span class="stat-item"><span class="stat-label">
|
|
313
|
+
items.push(`<span class="stat-item"><span class="stat-label">Cache Write</span><span class="stat-value">${formatTokens(stats.cacheCreation)}</span></span>`);
|
|
243
314
|
items.push(`<span class="stat-item"><span class="stat-label">Cache Hit</span><span class="stat-value">${(stats.cacheHitRate * 100).toFixed(0)}%</span></span>`);
|
|
244
315
|
if (stats.durationMs) {
|
|
245
316
|
items.push(`<span class="stat-item"><span class="stat-label">Duration</span><span class="stat-value">${formatDurationLong(stats.durationMs)}</span></span>`);
|
|
246
|
-
if (stats.outputTokens && stats.durationMs > 0) {
|
|
247
|
-
const tokPerMin = Math.round(stats.outputTokens / (stats.durationMs / 60000));
|
|
248
|
-
items.push(`<span class="stat-item"><span class="stat-label">Output/min</span><span class="stat-value">${formatTokens(tokPerMin)}</span></span>`);
|
|
249
|
-
}
|
|
250
317
|
}
|
|
251
318
|
|
|
252
319
|
return `<div class="stats-bar">${items.join('')}</div>`;
|
package/lib/parser.js
CHANGED
|
@@ -142,21 +142,97 @@ function buildConversation(lines) {
|
|
|
142
142
|
// Pair tool_use with tool_result
|
|
143
143
|
pairToolMessages(messages);
|
|
144
144
|
|
|
145
|
+
// Calculate per-turn token usage (tokens between each user input)
|
|
146
|
+
calculateTurnUsage(messages);
|
|
147
|
+
|
|
148
|
+
// Calculate elapsed time for each message (time since previous message)
|
|
149
|
+
calculateElapsed(messages);
|
|
150
|
+
|
|
145
151
|
return messages;
|
|
146
152
|
}
|
|
147
153
|
|
|
154
|
+
function parseLocalCommand(text) {
|
|
155
|
+
// Parse <bash-input>, <bash-stdout>, <bash-stderr> tags
|
|
156
|
+
const inputMatch = text.match(/<bash-input>([\s\S]*?)<\/bash-input>/);
|
|
157
|
+
const stdoutMatch = text.match(/<bash-stdout>([\s\S]*?)<\/bash-stdout>/);
|
|
158
|
+
const stderrMatch = text.match(/<bash-stderr>([\s\S]*?)<\/bash-stderr>/);
|
|
159
|
+
|
|
160
|
+
if (inputMatch) {
|
|
161
|
+
return { type: 'local_command', subtype: 'input', command: inputMatch[1] };
|
|
162
|
+
}
|
|
163
|
+
if (stdoutMatch || stderrMatch) {
|
|
164
|
+
return {
|
|
165
|
+
type: 'local_command',
|
|
166
|
+
subtype: 'output',
|
|
167
|
+
stdout: stdoutMatch ? stdoutMatch[1] : '',
|
|
168
|
+
stderr: stderrMatch ? stderrMatch[1] : '',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function processUserText(text, line, messages) {
|
|
175
|
+
// Skip local-command-caveat (system instruction, not user input)
|
|
176
|
+
if (text.match(/^<local-command-caveat>/)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Parse local command input/output
|
|
181
|
+
const localCmd = parseLocalCommand(text);
|
|
182
|
+
if (localCmd) {
|
|
183
|
+
if (localCmd.subtype === 'input') {
|
|
184
|
+
messages.push({
|
|
185
|
+
type: 'local_command',
|
|
186
|
+
command: localCmd.command,
|
|
187
|
+
timestamp: line.timestamp,
|
|
188
|
+
uuid: line.uuid,
|
|
189
|
+
});
|
|
190
|
+
} else if (localCmd.subtype === 'output') {
|
|
191
|
+
// Attach to previous local_command if exists
|
|
192
|
+
const prev = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
193
|
+
if (prev && prev.type === 'local_command' && !prev.stdout) {
|
|
194
|
+
prev.stdout = localCmd.stdout;
|
|
195
|
+
prev.stderr = localCmd.stderr;
|
|
196
|
+
} else {
|
|
197
|
+
messages.push({
|
|
198
|
+
type: 'local_command',
|
|
199
|
+
command: '',
|
|
200
|
+
stdout: localCmd.stdout,
|
|
201
|
+
stderr: localCmd.stderr,
|
|
202
|
+
timestamp: line.timestamp,
|
|
203
|
+
uuid: line.uuid,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Strip system-reminder tags but keep surrounding user text
|
|
211
|
+
const stripped = text
|
|
212
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
|
|
213
|
+
.replace(/<command-name>[\s\S]*?<\/command-name>/g, '')
|
|
214
|
+
.replace(/<command-message>[\s\S]*?<\/command-message>/g, '')
|
|
215
|
+
.replace(/<command-args>[\s\S]*?<\/command-args>/g, '')
|
|
216
|
+
.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, '')
|
|
217
|
+
.trim();
|
|
218
|
+
|
|
219
|
+
if (stripped) {
|
|
220
|
+
messages.push({
|
|
221
|
+
type: 'user',
|
|
222
|
+
text: stripped,
|
|
223
|
+
timestamp: line.timestamp,
|
|
224
|
+
uuid: line.uuid,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
148
229
|
function processUserMessage(line, messages) {
|
|
149
230
|
const content = line.message?.content;
|
|
150
231
|
if (!content) return;
|
|
151
232
|
|
|
152
233
|
if (typeof content === 'string') {
|
|
153
234
|
if (content.trim()) {
|
|
154
|
-
messages
|
|
155
|
-
type: 'user',
|
|
156
|
-
text: content,
|
|
157
|
-
timestamp: line.timestamp,
|
|
158
|
-
uuid: line.uuid,
|
|
159
|
-
});
|
|
235
|
+
processUserText(content, line, messages);
|
|
160
236
|
}
|
|
161
237
|
return;
|
|
162
238
|
}
|
|
@@ -164,12 +240,7 @@ function processUserMessage(line, messages) {
|
|
|
164
240
|
if (Array.isArray(content)) {
|
|
165
241
|
for (const block of content) {
|
|
166
242
|
if (block.type === 'text' && block.text?.trim()) {
|
|
167
|
-
messages
|
|
168
|
-
type: 'user',
|
|
169
|
-
text: block.text,
|
|
170
|
-
timestamp: line.timestamp,
|
|
171
|
-
uuid: line.uuid,
|
|
172
|
-
});
|
|
243
|
+
processUserText(block.text, line, messages);
|
|
173
244
|
} else if (block.type === 'tool_result') {
|
|
174
245
|
const toolResult = {
|
|
175
246
|
type: 'tool_result',
|
|
@@ -218,6 +289,9 @@ function processAssistantMessage(line, messages) {
|
|
|
218
289
|
const content = line.message?.content;
|
|
219
290
|
if (!content || !Array.isArray(content)) return;
|
|
220
291
|
|
|
292
|
+
const usage = line.message?.usage || null;
|
|
293
|
+
let usageAssigned = false;
|
|
294
|
+
|
|
221
295
|
for (const block of content) {
|
|
222
296
|
if (block.type === 'thinking') {
|
|
223
297
|
messages.push({
|
|
@@ -232,7 +306,9 @@ function processAssistantMessage(line, messages) {
|
|
|
232
306
|
model: line.message.model,
|
|
233
307
|
timestamp: line.timestamp,
|
|
234
308
|
uuid: line.uuid,
|
|
309
|
+
usage: !usageAssigned ? usage : null,
|
|
235
310
|
});
|
|
311
|
+
usageAssigned = true;
|
|
236
312
|
} else if (block.type === 'tool_use') {
|
|
237
313
|
messages.push({
|
|
238
314
|
type: 'tool_use',
|
|
@@ -241,7 +317,79 @@ function processAssistantMessage(line, messages) {
|
|
|
241
317
|
input: block.input,
|
|
242
318
|
timestamp: line.timestamp,
|
|
243
319
|
uuid: line.uuid,
|
|
320
|
+
usage: !usageAssigned ? usage : null,
|
|
244
321
|
});
|
|
322
|
+
usageAssigned = true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function calculateTurnUsage(messages) {
|
|
328
|
+
// Walk backwards from each user message, summing usage of preceding assistant messages
|
|
329
|
+
// A "turn" = user input → all assistant responses until next user input
|
|
330
|
+
for (let i = 0; i < messages.length; i++) {
|
|
331
|
+
if (messages[i].type !== 'user' && messages[i].type !== 'local_command') continue;
|
|
332
|
+
|
|
333
|
+
// Sum usage of all following assistant/tool messages until next user message
|
|
334
|
+
let totalInput = 0;
|
|
335
|
+
let totalOutput = 0;
|
|
336
|
+
let totalCacheRead = 0;
|
|
337
|
+
let totalCacheCreate = 0;
|
|
338
|
+
const seenUsageIds = new Set(); // Dedupe (multiple blocks from same API response)
|
|
339
|
+
|
|
340
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
341
|
+
const m = messages[j];
|
|
342
|
+
if (m.type === 'user' || m.type === 'local_command') break;
|
|
343
|
+
if (m.usage && !seenUsageIds.has(m.uuid + (m.usage.input_tokens || 0))) {
|
|
344
|
+
seenUsageIds.add(m.uuid + (m.usage.input_tokens || 0));
|
|
345
|
+
totalInput += m.usage.input_tokens || 0;
|
|
346
|
+
totalOutput += m.usage.output_tokens || 0;
|
|
347
|
+
totalCacheRead += m.usage.cache_read_input_tokens || 0;
|
|
348
|
+
totalCacheCreate += m.usage.cache_creation_input_tokens || 0;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Find turn_duration in the following messages
|
|
353
|
+
let turnDurationMs = 0;
|
|
354
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
355
|
+
const m = messages[j];
|
|
356
|
+
if (m.type === 'user' || m.type === 'local_command') break;
|
|
357
|
+
if (m.type === 'system' && m.subtype === 'turn_duration') {
|
|
358
|
+
turnDurationMs = m.durationMs || 0;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const total = totalInput + totalOutput + totalCacheRead + totalCacheCreate;
|
|
364
|
+
if (total > 0 || turnDurationMs > 0) {
|
|
365
|
+
messages[i].turnUsage = {
|
|
366
|
+
input: totalInput,
|
|
367
|
+
output: totalOutput,
|
|
368
|
+
cacheRead: totalCacheRead,
|
|
369
|
+
cacheCreate: totalCacheCreate,
|
|
370
|
+
total,
|
|
371
|
+
durationMs: turnDurationMs,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function calculateElapsed(messages) {
|
|
378
|
+
for (let i = 1; i < messages.length; i++) {
|
|
379
|
+
const prev = messages[i - 1];
|
|
380
|
+
const curr = messages[i];
|
|
381
|
+
if (prev.timestamp && curr.timestamp) {
|
|
382
|
+
const elapsed = new Date(curr.timestamp) - new Date(prev.timestamp);
|
|
383
|
+
if (elapsed > 0) {
|
|
384
|
+
curr.elapsedMs = elapsed;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// For tool_use with paired result, calculate execution time
|
|
388
|
+
if (curr.type === 'tool_use' && curr.result?.timestamp && curr.timestamp) {
|
|
389
|
+
const execTime = new Date(curr.result.timestamp) - new Date(curr.timestamp);
|
|
390
|
+
if (execTime > 0) {
|
|
391
|
+
curr.execMs = execTime;
|
|
392
|
+
}
|
|
245
393
|
}
|
|
246
394
|
}
|
|
247
395
|
}
|
package/lib/template-assets.js
CHANGED
|
@@ -131,6 +131,71 @@ body {
|
|
|
131
131
|
position: relative;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/* Local command (user ! commands) */
|
|
135
|
+
.msg-local-cmd {
|
|
136
|
+
align-self: flex-end;
|
|
137
|
+
margin-left: 20%;
|
|
138
|
+
background: var(--bg-secondary);
|
|
139
|
+
border: 1px solid var(--border);
|
|
140
|
+
border-radius: 8px;
|
|
141
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
142
|
+
font-size: 0.82rem;
|
|
143
|
+
}
|
|
144
|
+
.local-cmd-input {
|
|
145
|
+
padding: 2px 0;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
}
|
|
148
|
+
.local-cmd-prompt {
|
|
149
|
+
color: var(--text-muted);
|
|
150
|
+
margin-right: 4px;
|
|
151
|
+
}
|
|
152
|
+
.local-cmd-output {
|
|
153
|
+
margin-top: 4px;
|
|
154
|
+
padding: 6px 8px;
|
|
155
|
+
background: var(--code-block-bg);
|
|
156
|
+
color: var(--code-block-text);
|
|
157
|
+
border-radius: 4px;
|
|
158
|
+
overflow-x: auto;
|
|
159
|
+
font-size: 0.78rem;
|
|
160
|
+
max-height: 200px;
|
|
161
|
+
overflow-y: auto;
|
|
162
|
+
}
|
|
163
|
+
.local-cmd-output code {
|
|
164
|
+
background: none;
|
|
165
|
+
padding: 0;
|
|
166
|
+
color: inherit;
|
|
167
|
+
}
|
|
168
|
+
.local-cmd-stderr {
|
|
169
|
+
border-left: 3px solid #ef4444;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Token usage badges */
|
|
173
|
+
.turn-usage, .item-usage {
|
|
174
|
+
display: inline-block;
|
|
175
|
+
margin-top: 6px;
|
|
176
|
+
padding: 2px 8px;
|
|
177
|
+
font-size: 0.68rem;
|
|
178
|
+
font-variant-numeric: tabular-nums;
|
|
179
|
+
color: var(--text-muted);
|
|
180
|
+
background: var(--bg-secondary);
|
|
181
|
+
border: 1px solid var(--border);
|
|
182
|
+
border-radius: 4px;
|
|
183
|
+
}
|
|
184
|
+
.turn-usage {
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
}
|
|
187
|
+
.item-usage {
|
|
188
|
+
font-weight: 400;
|
|
189
|
+
opacity: 0.8;
|
|
190
|
+
}
|
|
191
|
+
.tool-usage-row {
|
|
192
|
+
padding: 4px 14px 6px;
|
|
193
|
+
}
|
|
194
|
+
.usage-detail {
|
|
195
|
+
font-size: 0.6rem;
|
|
196
|
+
opacity: 0.7;
|
|
197
|
+
}
|
|
198
|
+
|
|
134
199
|
.msg .timestamp {
|
|
135
200
|
display: block;
|
|
136
201
|
font-size: 0.7rem;
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccakashic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Browse Claude Code session logs (~/.claude/projects/) as beautiful HTML in your browser — an Akashic Record of your Claude Code sessions",
|
|
5
5
|
"bin": {
|
|
6
|
-
"ccakashic": "
|
|
6
|
+
"ccakashic": "bin/ccakashic.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin",
|