ccakashic 0.2.4 → 0.2.6
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 +6 -2
- package/dist/bin/ccakashic.js +248 -0
- package/dist/discover.js +230 -0
- package/dist/html-generator.js +554 -0
- package/{lib → dist}/pages.js +65 -80
- package/dist/parser.js +465 -0
- package/{lib → dist}/template-assets.js +6 -7
- package/package.json +14 -5
- package/bin/ccakashic.js +0 -225
- package/lib/discover.js +0 -221
- package/lib/html-generator.js +0 -586
- package/lib/parser.js +0 -496
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generate = generate;
|
|
4
|
+
const template_assets_1 = require("./template-assets");
|
|
5
|
+
function escapeHtml(str) {
|
|
6
|
+
return String(str)
|
|
7
|
+
.replace(/&/g, '&')
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"');
|
|
11
|
+
}
|
|
12
|
+
function formatTime(ts) {
|
|
13
|
+
if (!ts)
|
|
14
|
+
return '';
|
|
15
|
+
const d = new Date(ts);
|
|
16
|
+
return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
17
|
+
}
|
|
18
|
+
function formatDuration(ms) {
|
|
19
|
+
if (!ms)
|
|
20
|
+
return '';
|
|
21
|
+
if (ms < 1000)
|
|
22
|
+
return `${ms}ms`;
|
|
23
|
+
const s = Math.round(ms / 1000);
|
|
24
|
+
if (s < 60)
|
|
25
|
+
return `${s}s`;
|
|
26
|
+
const m = Math.floor(s / 60);
|
|
27
|
+
const rs = s % 60;
|
|
28
|
+
return `${m}m ${rs}s`;
|
|
29
|
+
}
|
|
30
|
+
function toolUseSummary(msg) {
|
|
31
|
+
const name = msg.toolName;
|
|
32
|
+
const input = msg.input || {};
|
|
33
|
+
switch (name) {
|
|
34
|
+
case 'Bash':
|
|
35
|
+
return `Bash: <code>${escapeHtml((input.command || '').slice(0, 120))}</code>`;
|
|
36
|
+
case 'Read':
|
|
37
|
+
return `Read: <code>${escapeHtml(input.file_path || '')}</code>`;
|
|
38
|
+
case 'Write':
|
|
39
|
+
return `Write: <code>${escapeHtml(input.file_path || '')}</code>`;
|
|
40
|
+
case 'Edit':
|
|
41
|
+
return `Edit: <code>${escapeHtml(input.file_path || '')}</code>`;
|
|
42
|
+
case 'Grep':
|
|
43
|
+
return `Grep: <code>${escapeHtml(input.pattern || '')}</code>`;
|
|
44
|
+
case 'Glob':
|
|
45
|
+
return `Glob: <code>${escapeHtml(input.pattern || '')}</code>`;
|
|
46
|
+
case 'Agent':
|
|
47
|
+
return `Agent: ${escapeHtml(input.description || input.subagent_type || '')}`;
|
|
48
|
+
default:
|
|
49
|
+
return escapeHtml(name);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function renderToolResult(msg) {
|
|
53
|
+
if (!msg.result)
|
|
54
|
+
return '';
|
|
55
|
+
const result = msg.result;
|
|
56
|
+
const rich = result.richResult;
|
|
57
|
+
const parts = [];
|
|
58
|
+
if (rich) {
|
|
59
|
+
// Bash result
|
|
60
|
+
if (rich.stdout !== undefined) {
|
|
61
|
+
if (rich.stdout) {
|
|
62
|
+
parts.push(`<div class="tool-output"><pre><code>${escapeHtml(rich.stdout)}</code></pre></div>`);
|
|
63
|
+
}
|
|
64
|
+
if (rich.stderr) {
|
|
65
|
+
parts.push(`<div class="tool-output stderr"><pre><code>${escapeHtml(rich.stderr)}</code></pre></div>`);
|
|
66
|
+
}
|
|
67
|
+
return parts.join('');
|
|
68
|
+
}
|
|
69
|
+
// File read result
|
|
70
|
+
if (rich.type === 'text' && rich.file) {
|
|
71
|
+
const f = rich.file;
|
|
72
|
+
parts.push(`<div class="tool-meta">${escapeHtml(f.filePath)} (lines ${f.startLine}-${f.startLine + f.numLines - 1} of ${f.totalLines})</div>`);
|
|
73
|
+
if (f.content) {
|
|
74
|
+
parts.push(`<div class="tool-output"><pre><code>${escapeHtml(f.content.slice(0, 3000))}</code></pre></div>`);
|
|
75
|
+
}
|
|
76
|
+
return parts.join('');
|
|
77
|
+
}
|
|
78
|
+
// File edit/create with structuredPatch
|
|
79
|
+
if ((rich.type === 'update' || rich.type === 'create') && rich.filePath) {
|
|
80
|
+
parts.push(`<div class="tool-meta">${escapeHtml(rich.filePath)}</div>`);
|
|
81
|
+
if (rich.structuredPatch && rich.structuredPatch.length > 0) {
|
|
82
|
+
parts.push(renderDiff(rich.structuredPatch));
|
|
83
|
+
}
|
|
84
|
+
else if (rich.content) {
|
|
85
|
+
parts.push(`<div class="tool-output"><pre><code>${escapeHtml(rich.content.slice(0, 3000))}</code></pre></div>`);
|
|
86
|
+
}
|
|
87
|
+
return parts.join('');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Fallback: raw content
|
|
91
|
+
const content = result.fullContent || result.content;
|
|
92
|
+
if (content) {
|
|
93
|
+
parts.push(`<div class="tool-output"><pre><code>${escapeHtml(content.slice(0, 5000))}</code></pre></div>`);
|
|
94
|
+
}
|
|
95
|
+
return parts.join('');
|
|
96
|
+
}
|
|
97
|
+
function renderDiff(patches) {
|
|
98
|
+
const lines = [];
|
|
99
|
+
lines.push('<div class="diff">');
|
|
100
|
+
for (const patch of patches) {
|
|
101
|
+
lines.push(`<div class="diff-hunk">@@ -${patch.oldStart},${patch.oldLines} +${patch.newStart},${patch.newLines} @@</div>`);
|
|
102
|
+
for (const line of (patch.lines || [])) {
|
|
103
|
+
const ch = line[0];
|
|
104
|
+
const text = line.slice(1);
|
|
105
|
+
if (ch === '+') {
|
|
106
|
+
lines.push(`<div class="diff-add">+${escapeHtml(text)}</div>`);
|
|
107
|
+
}
|
|
108
|
+
else if (ch === '-') {
|
|
109
|
+
lines.push(`<div class="diff-del">-${escapeHtml(text)}</div>`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
lines.push(`<div class="diff-ctx"> ${escapeHtml(text)}</div>`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
lines.push('</div>');
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
function msgId(ts) {
|
|
120
|
+
if (!ts)
|
|
121
|
+
return `msg-${Date.now()}${Math.random().toString(36).slice(2, 5)}`;
|
|
122
|
+
const d = new Date(ts);
|
|
123
|
+
const pad = (n, len = 2) => String(n).padStart(len, '0');
|
|
124
|
+
return `t${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
125
|
+
}
|
|
126
|
+
function renderMessage(msg) {
|
|
127
|
+
const id = msgId(msg.timestamp);
|
|
128
|
+
const time = `<a class="timestamp" href="#${id}">${formatTime(msg.timestamp)}</a>`;
|
|
129
|
+
const turnBadge = msg.turnUsage
|
|
130
|
+
? (() => {
|
|
131
|
+
const c = calcCost(msg.turnUsage.input, msg.turnUsage.output, msg.turnUsage.cacheRead, msg.turnUsage.cacheCreate);
|
|
132
|
+
const dur = msg.turnUsage.durationMs ? ` | ${formatDuration(msg.turnUsage.durationMs)}` : '';
|
|
133
|
+
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>`;
|
|
134
|
+
})()
|
|
135
|
+
: '';
|
|
136
|
+
function makeItemBadge(m) {
|
|
137
|
+
const parts = [];
|
|
138
|
+
const c = m.usage
|
|
139
|
+
? 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)
|
|
140
|
+
: null;
|
|
141
|
+
if (c) {
|
|
142
|
+
parts.push(`${c.totalStr}`);
|
|
143
|
+
}
|
|
144
|
+
if (m.execMs) {
|
|
145
|
+
parts.push(`${formatDuration(m.execMs)}`);
|
|
146
|
+
}
|
|
147
|
+
else if (m.elapsedMs && m.elapsedMs >= 1000) {
|
|
148
|
+
parts.push(`${formatDuration(m.elapsedMs)}`);
|
|
149
|
+
}
|
|
150
|
+
if (!parts.length)
|
|
151
|
+
return '';
|
|
152
|
+
const detail = c ? ` <span class="usage-detail">(in:${c.inStr} out:${c.outStr} cache-r:${c.crStr} cache-w:${c.cwStr})</span>` : '';
|
|
153
|
+
return `<span class="item-usage">${parts.join(' | ')}${detail}</span>`;
|
|
154
|
+
}
|
|
155
|
+
const itemBadge = makeItemBadge(msg);
|
|
156
|
+
switch (msg.type) {
|
|
157
|
+
case 'user':
|
|
158
|
+
return `<div class="msg msg-user" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div>${turnBadge}</div>`;
|
|
159
|
+
case 'assistant':
|
|
160
|
+
return `<div class="msg msg-assistant" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div>${itemBadge ? `<div>${itemBadge}</div>` : ''}</div>`;
|
|
161
|
+
case 'thinking':
|
|
162
|
+
return `<div class="msg msg-thinking" id="${id}">${time}<span class="thinking-indicator">Thinking...</span></div>`;
|
|
163
|
+
case 'tool_use': {
|
|
164
|
+
const summary = toolUseSummary(msg);
|
|
165
|
+
const resultHtml = renderToolResult(msg);
|
|
166
|
+
const subagentHtml = msg.subagentMessages
|
|
167
|
+
? `<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>`
|
|
168
|
+
: '';
|
|
169
|
+
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>`;
|
|
170
|
+
}
|
|
171
|
+
case 'tool_result':
|
|
172
|
+
// Unpaired tool result (shouldn't happen often)
|
|
173
|
+
return `<div class="msg msg-tool" id="${id}"><div class="tool-output"><pre><code>${escapeHtml((msg.content || '').slice(0, 2000))}</code></pre></div></div>`;
|
|
174
|
+
case 'local_command': {
|
|
175
|
+
const cmd = msg.command ? `<div class="local-cmd-input"><span class="local-cmd-prompt">$</span> ${escapeHtml(msg.command)}</div>` : '';
|
|
176
|
+
const stdout = msg.stdout && msg.stdout.trim() ? `<pre class="local-cmd-output"><code>${escapeHtml(msg.stdout)}</code></pre>` : '';
|
|
177
|
+
const stderr = msg.stderr && msg.stderr.trim() ? `<pre class="local-cmd-output local-cmd-stderr"><code>${escapeHtml(msg.stderr)}</code></pre>` : '';
|
|
178
|
+
return `<div class="msg msg-local-cmd" id="${id}">${time}${cmd}${stdout}${stderr}${turnBadge}</div>`;
|
|
179
|
+
}
|
|
180
|
+
case 'system':
|
|
181
|
+
if (msg.subtype === 'turn_duration') {
|
|
182
|
+
return ''; // Now shown in turn usage badge
|
|
183
|
+
}
|
|
184
|
+
return `<div class="msg msg-system" id="${id}">${escapeHtml(msg.content || '')}</div>`;
|
|
185
|
+
default:
|
|
186
|
+
return '';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function renderToolInput(msg) {
|
|
190
|
+
const input = msg.input || {};
|
|
191
|
+
const name = msg.toolName;
|
|
192
|
+
// Show command for Bash
|
|
193
|
+
if (name === 'Bash' && input.command) {
|
|
194
|
+
return `<div class="tool-input"><div class="tool-input-label">Command:</div><pre><code>${escapeHtml(input.command)}</code></pre></div>`;
|
|
195
|
+
}
|
|
196
|
+
// Show old_string/new_string for Edit
|
|
197
|
+
if (name === 'Edit' && input.old_string) {
|
|
198
|
+
return `<div class="tool-input"><div class="tool-input-label">Edit:</div><div class="diff"><div class="diff-del">${escapeHtml(input.old_string)}</div><div class="diff-add">${escapeHtml(input.new_string || '')}</div></div></div>`;
|
|
199
|
+
}
|
|
200
|
+
// For other tools, show input as JSON if small enough
|
|
201
|
+
const json = JSON.stringify(input, null, 2);
|
|
202
|
+
if (json.length > 500)
|
|
203
|
+
return '';
|
|
204
|
+
return `<div class="tool-input"><pre><code>${escapeHtml(json)}</code></pre></div>`;
|
|
205
|
+
}
|
|
206
|
+
function renderSubagent(agentId, messages) {
|
|
207
|
+
const agentHtml = messages.map(renderMessage).join('\n');
|
|
208
|
+
return `<div class="msg msg-subagent"><details><summary><span class="tool-summary">Subagent: ${escapeHtml(agentId)}</span></summary><div class="subagent-content">${agentHtml}</div></details></div>`;
|
|
209
|
+
}
|
|
210
|
+
function formatDateOnly(ts) {
|
|
211
|
+
if (!ts)
|
|
212
|
+
return '';
|
|
213
|
+
const d = new Date(ts);
|
|
214
|
+
return d.toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
215
|
+
}
|
|
216
|
+
function groupMessagesByDate(messages) {
|
|
217
|
+
const groups = [];
|
|
218
|
+
let currentDate = null;
|
|
219
|
+
for (const msg of messages) {
|
|
220
|
+
const dateStr = formatDateOnly(msg.timestamp);
|
|
221
|
+
if (dateStr && dateStr !== currentDate) {
|
|
222
|
+
currentDate = dateStr;
|
|
223
|
+
groups.push({ date: dateStr, messages: [] });
|
|
224
|
+
}
|
|
225
|
+
if (groups.length === 0) {
|
|
226
|
+
groups.push({ date: 'unknown', messages: [] });
|
|
227
|
+
}
|
|
228
|
+
groups[groups.length - 1].messages.push(msg);
|
|
229
|
+
}
|
|
230
|
+
return groups;
|
|
231
|
+
}
|
|
232
|
+
// Cost per 1M tokens (USD) - Claude Opus 4 pricing
|
|
233
|
+
const COST_PER_M = {
|
|
234
|
+
input: 15,
|
|
235
|
+
output: 75,
|
|
236
|
+
cacheRead: 1.5,
|
|
237
|
+
cacheWrite: 18.75,
|
|
238
|
+
};
|
|
239
|
+
function tokenCostUsd(tokens, rate) {
|
|
240
|
+
return (tokens / 1_000_000) * rate;
|
|
241
|
+
}
|
|
242
|
+
function formatCost(usd) {
|
|
243
|
+
if (usd < 0.001)
|
|
244
|
+
return '<$0.01';
|
|
245
|
+
if (usd < 0.01)
|
|
246
|
+
return `$${usd.toFixed(3)}`;
|
|
247
|
+
if (usd < 1)
|
|
248
|
+
return `$${usd.toFixed(2)}`;
|
|
249
|
+
return `$${usd.toFixed(2)}`;
|
|
250
|
+
}
|
|
251
|
+
function calcCost(input, output, cacheRead, cacheWrite) {
|
|
252
|
+
const inCost = tokenCostUsd(input, COST_PER_M.input);
|
|
253
|
+
const outCost = tokenCostUsd(output, COST_PER_M.output);
|
|
254
|
+
const crCost = tokenCostUsd(cacheRead, COST_PER_M.cacheRead);
|
|
255
|
+
const cwCost = tokenCostUsd(cacheWrite, COST_PER_M.cacheWrite);
|
|
256
|
+
const total = inCost + outCost + crCost + cwCost;
|
|
257
|
+
return {
|
|
258
|
+
totalStr: formatCost(total),
|
|
259
|
+
inStr: formatTokens(input),
|
|
260
|
+
outStr: formatTokens(output),
|
|
261
|
+
crStr: formatTokens(cacheRead),
|
|
262
|
+
cwStr: formatTokens(cacheWrite),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function formatTokens(n) {
|
|
266
|
+
if (n >= 1000000)
|
|
267
|
+
return (n / 1000000).toFixed(1) + 'M';
|
|
268
|
+
if (n >= 1000)
|
|
269
|
+
return (n / 1000).toFixed(1) + 'K';
|
|
270
|
+
return String(n);
|
|
271
|
+
}
|
|
272
|
+
function formatDurationLong(ms) {
|
|
273
|
+
if (!ms)
|
|
274
|
+
return '';
|
|
275
|
+
const s = Math.floor(ms / 1000);
|
|
276
|
+
if (s < 60)
|
|
277
|
+
return `${s}s`;
|
|
278
|
+
const m = Math.floor(s / 60);
|
|
279
|
+
const h = Math.floor(m / 60);
|
|
280
|
+
if (h > 0)
|
|
281
|
+
return `${h}h ${m % 60}m`;
|
|
282
|
+
return `${m}m`;
|
|
283
|
+
}
|
|
284
|
+
function renderStats(stats) {
|
|
285
|
+
if (!stats || !stats.turns)
|
|
286
|
+
return '';
|
|
287
|
+
const totalCost = calcCost(stats.inputTokens, stats.outputTokens, stats.cacheRead, stats.cacheCreation);
|
|
288
|
+
const items = [];
|
|
289
|
+
items.push(`<span class="stat-item"><span class="stat-label">Est. Cost</span><span class="stat-value">${totalCost.totalStr}</span></span>`);
|
|
290
|
+
items.push(`<span class="stat-item"><span class="stat-label">Turns</span><span class="stat-value">${stats.turns}</span></span>`);
|
|
291
|
+
items.push(`<span class="stat-item"><span class="stat-label">Input</span><span class="stat-value">${formatTokens(stats.inputTokens)}</span></span>`);
|
|
292
|
+
items.push(`<span class="stat-item"><span class="stat-label">Output</span><span class="stat-value">${formatTokens(stats.outputTokens)}</span></span>`);
|
|
293
|
+
items.push(`<span class="stat-item"><span class="stat-label">Cache Read</span><span class="stat-value">${formatTokens(stats.cacheRead)}</span></span>`);
|
|
294
|
+
items.push(`<span class="stat-item"><span class="stat-label">Cache Write</span><span class="stat-value">${formatTokens(stats.cacheCreation)}</span></span>`);
|
|
295
|
+
items.push(`<span class="stat-item"><span class="stat-label">Cache Hit</span><span class="stat-value">${(stats.cacheHitRate * 100).toFixed(0)}%</span></span>`);
|
|
296
|
+
if (stats.durationMs) {
|
|
297
|
+
items.push(`<span class="stat-item"><span class="stat-label">Duration</span><span class="stat-value">${formatDurationLong(stats.durationMs)}</span></span>`);
|
|
298
|
+
}
|
|
299
|
+
return `<div class="stats-bar">${items.join('')}</div>`;
|
|
300
|
+
}
|
|
301
|
+
function generate(parsed, options = {}) {
|
|
302
|
+
const { projectName, session, backUrl } = options;
|
|
303
|
+
const title = session?.slug || session?.id || 'Session';
|
|
304
|
+
const date = session?.timestamp
|
|
305
|
+
? new Date(session.timestamp).toLocaleDateString('en-CA')
|
|
306
|
+
: '';
|
|
307
|
+
// Group messages by date
|
|
308
|
+
const dateGroups = groupMessagesByDate(parsed.messages);
|
|
309
|
+
// Build grouped HTML with date anchors
|
|
310
|
+
const groupsHtml = dateGroups.map(g => {
|
|
311
|
+
const msgsHtml = g.messages.map(renderMessage).join('\n');
|
|
312
|
+
return `<div class="detail-date-group" id="date-${g.date}" data-date="${escapeHtml(g.date)}">
|
|
313
|
+
<div class="detail-date-heading">${escapeHtml(g.date)}</div>
|
|
314
|
+
${msgsHtml}
|
|
315
|
+
</div>`;
|
|
316
|
+
}).join('\n');
|
|
317
|
+
// Side nav for dates
|
|
318
|
+
const sideNavItems = dateGroups
|
|
319
|
+
.filter(g => g.date !== 'unknown')
|
|
320
|
+
.map(g => `<a class="detail-sidenav-item" href="#date-${g.date}" data-date="${escapeHtml(g.date)}">${escapeHtml(g.date)}</a>`).join('\n');
|
|
321
|
+
const backLink = backUrl
|
|
322
|
+
? `<div style="font-size:0.8rem;margin-bottom:8px"><a href="${escapeHtml(backUrl)}" style="color:var(--link);text-decoration:none">← Back to sessions</a> | <a href="/" style="color:var(--link);text-decoration:none">All projects</a></div>`
|
|
323
|
+
: '';
|
|
324
|
+
return `<!DOCTYPE html>
|
|
325
|
+
<html lang="en">
|
|
326
|
+
<head>
|
|
327
|
+
<meta charset="utf-8">
|
|
328
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
329
|
+
<title>${escapeHtml(title)} — ${escapeHtml(date)}</title>
|
|
330
|
+
<style>${(0, template_assets_1.getCSS)()}
|
|
331
|
+
${detailLayoutCSS()}
|
|
332
|
+
</style>
|
|
333
|
+
</head>
|
|
334
|
+
<body>
|
|
335
|
+
<a href="https://github.com/ashimon83/ccakashic" class="github-corner" aria-label="View source on GitHub" target="_blank" rel="noopener"><svg width="70" height="70" viewBox="0 0 250 250" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
|
|
336
|
+
<div class="detail-sticky-bar" id="detailStickyBar"></div>
|
|
337
|
+
<header class="session-header">
|
|
338
|
+
${backLink}
|
|
339
|
+
<h1>${escapeHtml(title)}</h1>
|
|
340
|
+
<div class="session-meta">
|
|
341
|
+
${projectName ? `<span class="meta-item">Project: ${escapeHtml(projectName)}</span>` : ''}
|
|
342
|
+
${date ? `<span class="meta-item">Date: ${escapeHtml(date)}</span>` : ''}
|
|
343
|
+
${session?.gitBranch ? `<span class="meta-item">Branch: ${escapeHtml(session.gitBranch)}</span>` : ''}
|
|
344
|
+
${session?.model ? `<span class="meta-item">Model: ${escapeHtml(session.model)}</span>` : ''}
|
|
345
|
+
</div>
|
|
346
|
+
${renderStats(parsed.stats)}
|
|
347
|
+
</header>
|
|
348
|
+
<div class="detail-layout">
|
|
349
|
+
<nav class="detail-sidenav" id="detailSidenav">
|
|
350
|
+
<div class="detail-sidenav-title">Dates</div>
|
|
351
|
+
${sideNavItems}
|
|
352
|
+
<div class="detail-sidenav-jump">
|
|
353
|
+
<a class="detail-sidenav-item" href="#session-bottom">↓ Bottom</a>
|
|
354
|
+
<a class="detail-sidenav-item" href="#session-top">↑ Top</a>
|
|
355
|
+
</div>
|
|
356
|
+
</nav>
|
|
357
|
+
<main class="chat-container" id="session-top">
|
|
358
|
+
${groupsHtml}
|
|
359
|
+
<div class="load-more-row">
|
|
360
|
+
<button class="load-more-btn" id="loadMoreBtn" type="button">Load more ↻</button>
|
|
361
|
+
</div>
|
|
362
|
+
<div id="session-bottom"></div>
|
|
363
|
+
</main>
|
|
364
|
+
</div>
|
|
365
|
+
<script>${(0, template_assets_1.getAppJS)()}
|
|
366
|
+
${detailNavJS()}
|
|
367
|
+
</script>
|
|
368
|
+
</body>
|
|
369
|
+
</html>`;
|
|
370
|
+
}
|
|
371
|
+
function detailLayoutCSS() {
|
|
372
|
+
return `
|
|
373
|
+
.detail-layout {
|
|
374
|
+
display: flex;
|
|
375
|
+
max-width: 1100px;
|
|
376
|
+
margin: 0 auto;
|
|
377
|
+
gap: 0;
|
|
378
|
+
}
|
|
379
|
+
.detail-layout .chat-container {
|
|
380
|
+
flex: 1;
|
|
381
|
+
min-width: 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* Load more (reload) button at the bottom of the thread */
|
|
385
|
+
.load-more-row {
|
|
386
|
+
display: flex;
|
|
387
|
+
justify-content: center;
|
|
388
|
+
margin: 24px 0 12px;
|
|
389
|
+
}
|
|
390
|
+
.load-more-btn {
|
|
391
|
+
padding: 8px 18px;
|
|
392
|
+
font-size: 0.85rem;
|
|
393
|
+
color: var(--text);
|
|
394
|
+
background: var(--bg-secondary);
|
|
395
|
+
border: 1px solid var(--border);
|
|
396
|
+
border-radius: 6px;
|
|
397
|
+
cursor: pointer;
|
|
398
|
+
transition: background 0.1s;
|
|
399
|
+
}
|
|
400
|
+
.load-more-btn:hover {
|
|
401
|
+
background: var(--bg-hover, var(--border));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* Stats bar */
|
|
405
|
+
.stats-bar {
|
|
406
|
+
display: flex;
|
|
407
|
+
flex-wrap: wrap;
|
|
408
|
+
gap: 4px 12px;
|
|
409
|
+
margin-top: 12px;
|
|
410
|
+
padding: 10px 14px;
|
|
411
|
+
background: var(--bg-secondary);
|
|
412
|
+
border: 1px solid var(--border);
|
|
413
|
+
border-radius: 8px;
|
|
414
|
+
}
|
|
415
|
+
.stat-item {
|
|
416
|
+
display: flex;
|
|
417
|
+
flex-direction: column;
|
|
418
|
+
align-items: center;
|
|
419
|
+
min-width: 60px;
|
|
420
|
+
}
|
|
421
|
+
.stat-label {
|
|
422
|
+
font-size: 0.65rem;
|
|
423
|
+
color: var(--text-muted);
|
|
424
|
+
text-transform: uppercase;
|
|
425
|
+
letter-spacing: 0.04em;
|
|
426
|
+
}
|
|
427
|
+
.stat-value {
|
|
428
|
+
font-size: 0.9rem;
|
|
429
|
+
font-weight: 700;
|
|
430
|
+
font-variant-numeric: tabular-nums;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* Detail side nav */
|
|
434
|
+
.detail-sidenav {
|
|
435
|
+
width: 140px;
|
|
436
|
+
flex-shrink: 0;
|
|
437
|
+
position: sticky;
|
|
438
|
+
top: 48px;
|
|
439
|
+
align-self: flex-start;
|
|
440
|
+
max-height: calc(100vh - 60px);
|
|
441
|
+
overflow-y: auto;
|
|
442
|
+
padding: 16px 8px 16px 16px;
|
|
443
|
+
border-right: 1px solid var(--border);
|
|
444
|
+
}
|
|
445
|
+
.detail-sidenav-title {
|
|
446
|
+
font-size: 0.7rem;
|
|
447
|
+
text-transform: uppercase;
|
|
448
|
+
letter-spacing: 0.08em;
|
|
449
|
+
color: var(--text-muted);
|
|
450
|
+
margin-bottom: 8px;
|
|
451
|
+
font-weight: 600;
|
|
452
|
+
}
|
|
453
|
+
.detail-sidenav-jump {
|
|
454
|
+
margin-top: 12px;
|
|
455
|
+
padding-top: 8px;
|
|
456
|
+
border-top: 1px solid var(--border);
|
|
457
|
+
}
|
|
458
|
+
.detail-sidenav-item {
|
|
459
|
+
display: block;
|
|
460
|
+
padding: 4px 8px;
|
|
461
|
+
font-size: 0.78rem;
|
|
462
|
+
color: var(--text-muted);
|
|
463
|
+
text-decoration: none;
|
|
464
|
+
border-radius: 4px;
|
|
465
|
+
margin-bottom: 2px;
|
|
466
|
+
transition: background 0.15s, color 0.15s;
|
|
467
|
+
}
|
|
468
|
+
.detail-sidenav-item:hover {
|
|
469
|
+
background: var(--bg-secondary);
|
|
470
|
+
color: var(--text);
|
|
471
|
+
}
|
|
472
|
+
.detail-sidenav-item.active {
|
|
473
|
+
background: var(--user-bg);
|
|
474
|
+
color: var(--text);
|
|
475
|
+
font-weight: 600;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* Date group headings in detail */
|
|
479
|
+
.detail-date-heading {
|
|
480
|
+
font-size: 0.85rem;
|
|
481
|
+
font-weight: 700;
|
|
482
|
+
color: var(--text-muted);
|
|
483
|
+
padding: 14px 0 6px;
|
|
484
|
+
border-bottom: 2px solid var(--border);
|
|
485
|
+
margin-bottom: 12px;
|
|
486
|
+
position: sticky;
|
|
487
|
+
top: 44px;
|
|
488
|
+
background: var(--bg);
|
|
489
|
+
z-index: 5;
|
|
490
|
+
}
|
|
491
|
+
.detail-date-group {
|
|
492
|
+
margin-bottom: 8px;
|
|
493
|
+
display: flex;
|
|
494
|
+
flex-direction: column;
|
|
495
|
+
gap: 28px;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/* Sticky date bar */
|
|
499
|
+
.detail-sticky-bar {
|
|
500
|
+
position: fixed;
|
|
501
|
+
top: 0;
|
|
502
|
+
left: 0;
|
|
503
|
+
right: 0;
|
|
504
|
+
z-index: 100;
|
|
505
|
+
background: var(--bg-secondary);
|
|
506
|
+
border-bottom: 1px solid var(--border);
|
|
507
|
+
padding: 8px 24px;
|
|
508
|
+
font-size: 0.85rem;
|
|
509
|
+
font-weight: 700;
|
|
510
|
+
color: var(--text);
|
|
511
|
+
transform: translateY(-100%);
|
|
512
|
+
transition: transform 0.2s;
|
|
513
|
+
}
|
|
514
|
+
.detail-sticky-bar.visible {
|
|
515
|
+
transform: translateY(0);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
@media (max-width: 768px) {
|
|
519
|
+
.detail-sidenav { display: none; }
|
|
520
|
+
.detail-layout { display: block; }
|
|
521
|
+
}
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
function detailNavJS() {
|
|
525
|
+
return `
|
|
526
|
+
(function() {
|
|
527
|
+
var groups = Array.from(document.querySelectorAll('.detail-date-group'));
|
|
528
|
+
var bar = document.getElementById('detailStickyBar');
|
|
529
|
+
var navItems = Array.from(document.querySelectorAll('.detail-sidenav-item'));
|
|
530
|
+
if (!groups.length) return;
|
|
531
|
+
|
|
532
|
+
function update() {
|
|
533
|
+
var scrollY = window.scrollY + 60;
|
|
534
|
+
var current = null;
|
|
535
|
+
for (var i = 0; i < groups.length; i++) {
|
|
536
|
+
if (groups[i].offsetTop <= scrollY) current = groups[i];
|
|
537
|
+
}
|
|
538
|
+
var date = current ? current.dataset.date : '';
|
|
539
|
+
if (date) {
|
|
540
|
+
bar.textContent = date;
|
|
541
|
+
bar.classList.add('visible');
|
|
542
|
+
} else {
|
|
543
|
+
bar.classList.remove('visible');
|
|
544
|
+
}
|
|
545
|
+
navItems.forEach(function(a) {
|
|
546
|
+
a.classList.toggle('active', a.dataset.date === date);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
window.addEventListener('scroll', update, { passive: true });
|
|
551
|
+
update();
|
|
552
|
+
})();
|
|
553
|
+
`;
|
|
554
|
+
}
|