ccakashic 0.2.8 → 0.3.0
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 +33 -4
- package/dist/bin/ccakashic.js +227 -5
- package/dist/cmux.js +264 -0
- package/dist/dashboard.js +330 -0
- package/dist/discover.js +78 -28
- package/dist/html-generator.js +49 -47
- package/dist/pages.js +33 -22
- package/dist/parser.js +27 -0
- package/dist/resume-ui.js +148 -0
- package/dist/template-assets.js +32 -20
- package/dist/util.js +80 -0
- package/package.json +2 -2
package/dist/html-generator.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderMessage = renderMessage;
|
|
3
4
|
exports.generate = generate;
|
|
4
5
|
const template_assets_1 = require("./template-assets");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
.replace(/&/g, '&')
|
|
8
|
-
.replace(/</g, '<')
|
|
9
|
-
.replace(/>/g, '>')
|
|
10
|
-
.replace(/"/g, '"');
|
|
11
|
-
}
|
|
6
|
+
const resume_ui_1 = require("./resume-ui");
|
|
7
|
+
const util_1 = require("./util");
|
|
12
8
|
function formatTime(ts) {
|
|
13
9
|
if (!ts)
|
|
14
10
|
return '';
|
|
@@ -32,21 +28,21 @@ function toolUseSummary(msg) {
|
|
|
32
28
|
const input = msg.input || {};
|
|
33
29
|
switch (name) {
|
|
34
30
|
case 'Bash':
|
|
35
|
-
return `Bash: <code>${escapeHtml((input.command || '').slice(0, 120))}</code>`;
|
|
31
|
+
return `Bash: <code>${(0, util_1.escapeHtml)((input.command || '').slice(0, 120))}</code>`;
|
|
36
32
|
case 'Read':
|
|
37
|
-
return `Read: <code>${escapeHtml(input.file_path || '')}</code>`;
|
|
33
|
+
return `Read: <code>${(0, util_1.escapeHtml)(input.file_path || '')}</code>`;
|
|
38
34
|
case 'Write':
|
|
39
|
-
return `Write: <code>${escapeHtml(input.file_path || '')}</code>`;
|
|
35
|
+
return `Write: <code>${(0, util_1.escapeHtml)(input.file_path || '')}</code>`;
|
|
40
36
|
case 'Edit':
|
|
41
|
-
return `Edit: <code>${escapeHtml(input.file_path || '')}</code>`;
|
|
37
|
+
return `Edit: <code>${(0, util_1.escapeHtml)(input.file_path || '')}</code>`;
|
|
42
38
|
case 'Grep':
|
|
43
|
-
return `Grep: <code>${escapeHtml(input.pattern || '')}</code>`;
|
|
39
|
+
return `Grep: <code>${(0, util_1.escapeHtml)(input.pattern || '')}</code>`;
|
|
44
40
|
case 'Glob':
|
|
45
|
-
return `Glob: <code>${escapeHtml(input.pattern || '')}</code>`;
|
|
41
|
+
return `Glob: <code>${(0, util_1.escapeHtml)(input.pattern || '')}</code>`;
|
|
46
42
|
case 'Agent':
|
|
47
|
-
return `Agent: ${escapeHtml(input.description || input.subagent_type || '')}`;
|
|
43
|
+
return `Agent: ${(0, util_1.escapeHtml)(input.description || input.subagent_type || '')}`;
|
|
48
44
|
default:
|
|
49
|
-
return escapeHtml(name);
|
|
45
|
+
return (0, util_1.escapeHtml)(name);
|
|
50
46
|
}
|
|
51
47
|
}
|
|
52
48
|
function renderToolResult(msg) {
|
|
@@ -59,30 +55,30 @@ function renderToolResult(msg) {
|
|
|
59
55
|
// Bash result
|
|
60
56
|
if (rich.stdout !== undefined) {
|
|
61
57
|
if (rich.stdout) {
|
|
62
|
-
parts.push(`<div class="tool-output"><pre><code>${escapeHtml(rich.stdout)}</code></pre></div>`);
|
|
58
|
+
parts.push(`<div class="tool-output"><pre><code>${(0, util_1.escapeHtml)(rich.stdout)}</code></pre></div>`);
|
|
63
59
|
}
|
|
64
60
|
if (rich.stderr) {
|
|
65
|
-
parts.push(`<div class="tool-output stderr"><pre><code>${escapeHtml(rich.stderr)}</code></pre></div>`);
|
|
61
|
+
parts.push(`<div class="tool-output stderr"><pre><code>${(0, util_1.escapeHtml)(rich.stderr)}</code></pre></div>`);
|
|
66
62
|
}
|
|
67
63
|
return parts.join('');
|
|
68
64
|
}
|
|
69
65
|
// File read result
|
|
70
66
|
if (rich.type === 'text' && rich.file) {
|
|
71
67
|
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>`);
|
|
68
|
+
parts.push(`<div class="tool-meta">${(0, util_1.escapeHtml)(f.filePath)} (lines ${f.startLine}-${f.startLine + f.numLines - 1} of ${f.totalLines})</div>`);
|
|
73
69
|
if (f.content) {
|
|
74
|
-
parts.push(`<div class="tool-output"><pre><code>${escapeHtml(f.content.slice(0, 3000))}</code></pre></div>`);
|
|
70
|
+
parts.push(`<div class="tool-output"><pre><code>${(0, util_1.escapeHtml)(f.content.slice(0, 3000))}</code></pre></div>`);
|
|
75
71
|
}
|
|
76
72
|
return parts.join('');
|
|
77
73
|
}
|
|
78
74
|
// File edit/create with structuredPatch
|
|
79
75
|
if ((rich.type === 'update' || rich.type === 'create') && rich.filePath) {
|
|
80
|
-
parts.push(`<div class="tool-meta">${escapeHtml(rich.filePath)}</div>`);
|
|
76
|
+
parts.push(`<div class="tool-meta">${(0, util_1.escapeHtml)(rich.filePath)}</div>`);
|
|
81
77
|
if (rich.structuredPatch && rich.structuredPatch.length > 0) {
|
|
82
78
|
parts.push(renderDiff(rich.structuredPatch));
|
|
83
79
|
}
|
|
84
80
|
else if (rich.content) {
|
|
85
|
-
parts.push(`<div class="tool-output"><pre><code>${escapeHtml(rich.content.slice(0, 3000))}</code></pre></div>`);
|
|
81
|
+
parts.push(`<div class="tool-output"><pre><code>${(0, util_1.escapeHtml)(rich.content.slice(0, 3000))}</code></pre></div>`);
|
|
86
82
|
}
|
|
87
83
|
return parts.join('');
|
|
88
84
|
}
|
|
@@ -90,7 +86,7 @@ function renderToolResult(msg) {
|
|
|
90
86
|
// Fallback: raw content
|
|
91
87
|
const content = result.fullContent || result.content;
|
|
92
88
|
if (content) {
|
|
93
|
-
parts.push(`<div class="tool-output"><pre><code>${escapeHtml(content.slice(0, 5000))}</code></pre></div>`);
|
|
89
|
+
parts.push(`<div class="tool-output"><pre><code>${(0, util_1.escapeHtml)(content.slice(0, 5000))}</code></pre></div>`);
|
|
94
90
|
}
|
|
95
91
|
return parts.join('');
|
|
96
92
|
}
|
|
@@ -103,13 +99,13 @@ function renderDiff(patches) {
|
|
|
103
99
|
const ch = line[0];
|
|
104
100
|
const text = line.slice(1);
|
|
105
101
|
if (ch === '+') {
|
|
106
|
-
lines.push(`<div class="diff-add">+${escapeHtml(text)}</div>`);
|
|
102
|
+
lines.push(`<div class="diff-add">+${(0, util_1.escapeHtml)(text)}</div>`);
|
|
107
103
|
}
|
|
108
104
|
else if (ch === '-') {
|
|
109
|
-
lines.push(`<div class="diff-del">-${escapeHtml(text)}</div>`);
|
|
105
|
+
lines.push(`<div class="diff-del">-${(0, util_1.escapeHtml)(text)}</div>`);
|
|
110
106
|
}
|
|
111
107
|
else {
|
|
112
|
-
lines.push(`<div class="diff-ctx"> ${escapeHtml(text)}</div>`);
|
|
108
|
+
lines.push(`<div class="diff-ctx"> ${(0, util_1.escapeHtml)(text)}</div>`);
|
|
113
109
|
}
|
|
114
110
|
}
|
|
115
111
|
}
|
|
@@ -155,9 +151,9 @@ function renderMessage(msg) {
|
|
|
155
151
|
const itemBadge = makeItemBadge(msg);
|
|
156
152
|
switch (msg.type) {
|
|
157
153
|
case 'user':
|
|
158
|
-
return `<div class="msg msg-user" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div>${turnBadge}</div>`;
|
|
154
|
+
return `<div class="msg msg-user" id="${id}">${time}<div class="msg-content" data-markdown>${(0, util_1.escapeHtml)(msg.text)}</div>${turnBadge}</div>`;
|
|
159
155
|
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>`;
|
|
156
|
+
return `<div class="msg msg-assistant" id="${id}">${time}<div class="msg-content" data-markdown>${(0, util_1.escapeHtml)(msg.text)}</div>${itemBadge ? `<div>${itemBadge}</div>` : ''}</div>`;
|
|
161
157
|
case 'thinking':
|
|
162
158
|
return `<div class="msg msg-thinking" id="${id}">${time}<span class="thinking-indicator">Thinking...</span></div>`;
|
|
163
159
|
case 'tool_use': {
|
|
@@ -170,18 +166,18 @@ function renderMessage(msg) {
|
|
|
170
166
|
}
|
|
171
167
|
case 'tool_result':
|
|
172
168
|
// 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>`;
|
|
169
|
+
return `<div class="msg msg-tool" id="${id}"><div class="tool-output"><pre><code>${(0, util_1.escapeHtml)((msg.content || '').slice(0, 2000))}</code></pre></div></div>`;
|
|
174
170
|
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>` : '';
|
|
171
|
+
const cmd = msg.command ? `<div class="local-cmd-input"><span class="local-cmd-prompt">$</span> ${(0, util_1.escapeHtml)(msg.command)}</div>` : '';
|
|
172
|
+
const stdout = msg.stdout && msg.stdout.trim() ? `<pre class="local-cmd-output"><code>${(0, util_1.escapeHtml)(msg.stdout)}</code></pre>` : '';
|
|
173
|
+
const stderr = msg.stderr && msg.stderr.trim() ? `<pre class="local-cmd-output local-cmd-stderr"><code>${(0, util_1.escapeHtml)(msg.stderr)}</code></pre>` : '';
|
|
178
174
|
return `<div class="msg msg-local-cmd" id="${id}">${time}${cmd}${stdout}${stderr}${turnBadge}</div>`;
|
|
179
175
|
}
|
|
180
176
|
case 'system':
|
|
181
177
|
if (msg.subtype === 'turn_duration') {
|
|
182
178
|
return ''; // Now shown in turn usage badge
|
|
183
179
|
}
|
|
184
|
-
return `<div class="msg msg-system" id="${id}">${escapeHtml(msg.content || '')}</div>`;
|
|
180
|
+
return `<div class="msg msg-system" id="${id}">${(0, util_1.escapeHtml)(msg.content || '')}</div>`;
|
|
185
181
|
default:
|
|
186
182
|
return '';
|
|
187
183
|
}
|
|
@@ -191,21 +187,21 @@ function renderToolInput(msg) {
|
|
|
191
187
|
const name = msg.toolName;
|
|
192
188
|
// Show command for Bash
|
|
193
189
|
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>`;
|
|
190
|
+
return `<div class="tool-input"><div class="tool-input-label">Command:</div><pre><code>${(0, util_1.escapeHtml)(input.command)}</code></pre></div>`;
|
|
195
191
|
}
|
|
196
192
|
// Show old_string/new_string for Edit
|
|
197
193
|
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>`;
|
|
194
|
+
return `<div class="tool-input"><div class="tool-input-label">Edit:</div><div class="diff"><div class="diff-del">${(0, util_1.escapeHtml)(input.old_string)}</div><div class="diff-add">${(0, util_1.escapeHtml)(input.new_string || '')}</div></div></div>`;
|
|
199
195
|
}
|
|
200
196
|
// For other tools, show input as JSON if small enough
|
|
201
197
|
const json = JSON.stringify(input, null, 2);
|
|
202
198
|
if (json.length > 500)
|
|
203
199
|
return '';
|
|
204
|
-
return `<div class="tool-input"><pre><code>${escapeHtml(json)}</code></pre></div>`;
|
|
200
|
+
return `<div class="tool-input"><pre><code>${(0, util_1.escapeHtml)(json)}</code></pre></div>`;
|
|
205
201
|
}
|
|
206
202
|
function renderSubagent(agentId, messages) {
|
|
207
203
|
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>`;
|
|
204
|
+
return `<div class="msg msg-subagent"><details><summary><span class="tool-summary">Subagent: ${(0, util_1.escapeHtml)(agentId)}</span></summary><div class="subagent-content">${agentHtml}</div></details></div>`;
|
|
209
205
|
}
|
|
210
206
|
function formatDateOnly(ts) {
|
|
211
207
|
if (!ts)
|
|
@@ -299,7 +295,10 @@ function renderStats(stats) {
|
|
|
299
295
|
return `<div class="stats-bar">${items.join('')}</div>`;
|
|
300
296
|
}
|
|
301
297
|
function generate(parsed, options = {}) {
|
|
302
|
-
const { projectName, session, backUrl } = options;
|
|
298
|
+
const { projectName, projectRawName, session, backUrl, resume } = options;
|
|
299
|
+
const resumeButtons = session?.id && projectRawName
|
|
300
|
+
? (0, resume_ui_1.resumeButtonsHtml)(projectRawName, { id: session.id, cwd: session.cwd ?? null, lastModified: session.lastModified ?? 0 }, resume)
|
|
301
|
+
: '';
|
|
303
302
|
const title = session?.customTitle || session?.aiTitle || session?.slug || session?.id || 'Session';
|
|
304
303
|
const date = session?.timestamp
|
|
305
304
|
? new Date(session.timestamp).toLocaleDateString('en-CA')
|
|
@@ -309,26 +308,27 @@ function generate(parsed, options = {}) {
|
|
|
309
308
|
// Build grouped HTML with date anchors
|
|
310
309
|
const groupsHtml = dateGroups.map(g => {
|
|
311
310
|
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>
|
|
311
|
+
return `<div class="detail-date-group" id="date-${g.date}" data-date="${(0, util_1.escapeHtml)(g.date)}">
|
|
312
|
+
<div class="detail-date-heading">${(0, util_1.escapeHtml)(g.date)}</div>
|
|
314
313
|
${msgsHtml}
|
|
315
314
|
</div>`;
|
|
316
315
|
}).join('\n');
|
|
317
316
|
// Side nav for dates
|
|
318
317
|
const sideNavItems = dateGroups
|
|
319
318
|
.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');
|
|
319
|
+
.map(g => `<a class="detail-sidenav-item" href="#date-${g.date}" data-date="${(0, util_1.escapeHtml)(g.date)}">${(0, util_1.escapeHtml)(g.date)}</a>`).join('\n');
|
|
321
320
|
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>`
|
|
321
|
+
? `<div style="font-size:0.8rem;margin-bottom:8px"><a href="${(0, util_1.escapeHtml)(backUrl)}" style="color:var(--link);text-decoration:none">← Back to sessions</a> | <a href="/" style="color:var(--link);text-decoration:none">Dashboard</a> | <a href="/projects" style="color:var(--link);text-decoration:none">All projects</a></div>`
|
|
323
322
|
: '';
|
|
324
323
|
return `<!DOCTYPE html>
|
|
325
324
|
<html lang="en">
|
|
326
325
|
<head>
|
|
327
326
|
<meta charset="utf-8">
|
|
328
327
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
329
|
-
<title>${escapeHtml(title)} — ${escapeHtml(date)}</title>
|
|
328
|
+
<title>${(0, util_1.escapeHtml)(title)} — ${(0, util_1.escapeHtml)(date)}</title>
|
|
330
329
|
<style>${(0, template_assets_1.getCSS)()}
|
|
331
330
|
${detailLayoutCSS()}
|
|
331
|
+
${(0, resume_ui_1.resumeCSS)()}
|
|
332
332
|
</style>
|
|
333
333
|
</head>
|
|
334
334
|
<body>
|
|
@@ -336,13 +336,14 @@ ${detailLayoutCSS()}
|
|
|
336
336
|
<div class="detail-sticky-bar" id="detailStickyBar"></div>
|
|
337
337
|
<header class="session-header">
|
|
338
338
|
${backLink}
|
|
339
|
-
<h1>${escapeHtml(title)}</h1>
|
|
339
|
+
<h1>${(0, util_1.escapeHtml)(title)}</h1>
|
|
340
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>` : ''}
|
|
341
|
+
${projectName ? `<span class="meta-item">Project: ${(0, util_1.escapeHtml)(projectName)}</span>` : ''}
|
|
342
|
+
${date ? `<span class="meta-item">Date: ${(0, util_1.escapeHtml)(date)}</span>` : ''}
|
|
343
|
+
${session?.gitBranch ? `<span class="meta-item">Branch: ${(0, util_1.escapeHtml)(session.gitBranch)}</span>` : ''}
|
|
344
|
+
${session?.model ? `<span class="meta-item">Model: ${(0, util_1.escapeHtml)(session.model)}</span>` : ''}
|
|
345
345
|
</div>
|
|
346
|
+
${resumeButtons}
|
|
346
347
|
${renderStats(parsed.stats)}
|
|
347
348
|
</header>
|
|
348
349
|
<div class="detail-layout">
|
|
@@ -364,6 +365,7 @@ ${detailLayoutCSS()}
|
|
|
364
365
|
</div>
|
|
365
366
|
<script>${(0, template_assets_1.getAppJS)()}
|
|
366
367
|
${detailNavJS()}
|
|
368
|
+
${(0, resume_ui_1.resumeJS)(resume)}
|
|
367
369
|
</script>
|
|
368
370
|
</body>
|
|
369
371
|
</html>`;
|
package/dist/pages.js
CHANGED
|
@@ -3,13 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.generateIndex = generateIndex;
|
|
4
4
|
exports.generateSessionList = generateSessionList;
|
|
5
5
|
const template_assets_1 = require("./template-assets");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.replace(/&/g, '&')
|
|
9
|
-
.replace(/</g, '<')
|
|
10
|
-
.replace(/>/g, '>')
|
|
11
|
-
.replace(/"/g, '"');
|
|
12
|
-
}
|
|
6
|
+
const resume_ui_1 = require("./resume-ui");
|
|
7
|
+
const util_1 = require("./util");
|
|
13
8
|
function formatDate(ts) {
|
|
14
9
|
if (!ts)
|
|
15
10
|
return '';
|
|
@@ -33,9 +28,10 @@ function pageShell(title, bodyHtml) {
|
|
|
33
28
|
<head>
|
|
34
29
|
<meta charset="utf-8">
|
|
35
30
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
36
|
-
<title>${escapeHtml(title)}</title>
|
|
31
|
+
<title>${(0, util_1.escapeHtml)(title)}</title>
|
|
37
32
|
<style>${(0, template_assets_1.getCSS)()}
|
|
38
33
|
${indexCSS()}
|
|
34
|
+
${(0, resume_ui_1.resumeCSS)()}
|
|
39
35
|
</style>
|
|
40
36
|
</head>
|
|
41
37
|
<body>
|
|
@@ -79,6 +75,7 @@ function indexCSS() {
|
|
|
79
75
|
}
|
|
80
76
|
.list-item {
|
|
81
77
|
display: block;
|
|
78
|
+
position: relative;
|
|
82
79
|
padding: 14px 16px;
|
|
83
80
|
border: 1px solid var(--border);
|
|
84
81
|
border-radius: 8px;
|
|
@@ -88,6 +85,12 @@ function indexCSS() {
|
|
|
88
85
|
transition: background 0.15s, border-color 0.15s;
|
|
89
86
|
cursor: pointer;
|
|
90
87
|
}
|
|
88
|
+
/* Full-cover navigation link for div-based rows (keeps <button>s out of <a>). */
|
|
89
|
+
.list-item-link {
|
|
90
|
+
position: absolute;
|
|
91
|
+
inset: 0;
|
|
92
|
+
z-index: 0;
|
|
93
|
+
}
|
|
91
94
|
.list-item:hover {
|
|
92
95
|
background: var(--bg-secondary);
|
|
93
96
|
border-color: var(--link);
|
|
@@ -256,7 +259,7 @@ function generateIndex(projects) {
|
|
|
256
259
|
const items = projects.map((p) => {
|
|
257
260
|
const href = `/project/${encodeURIComponent(p.rawName)}`;
|
|
258
261
|
return `<a class="list-item" href="${href}">
|
|
259
|
-
<div class="list-item-title">${escapeHtml(p.name)}</div>
|
|
262
|
+
<div class="list-item-title">${(0, util_1.escapeHtml)(p.name)}</div>
|
|
260
263
|
<div class="list-item-meta">
|
|
261
264
|
<span>${p.sessionCount} sessions</span>
|
|
262
265
|
<span>Last: ${formatDate(p.lastModified)}</span>
|
|
@@ -265,6 +268,7 @@ function generateIndex(projects) {
|
|
|
265
268
|
}).join('\n');
|
|
266
269
|
return pageShell('ccakashic', `
|
|
267
270
|
<div class="page-header">
|
|
271
|
+
<div class="breadcrumb"><a href="/">← dashboard</a></div>
|
|
268
272
|
<h1>ccakashic</h1>
|
|
269
273
|
<div class="subtitle">Claude Code Session Logs</div>
|
|
270
274
|
</div>
|
|
@@ -293,7 +297,7 @@ function formatTimeOnly(ts) {
|
|
|
293
297
|
const d = ts instanceof Date ? ts : new Date(ts);
|
|
294
298
|
return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
295
299
|
}
|
|
296
|
-
function generateSessionList(project, sessions) {
|
|
300
|
+
function generateSessionList(project, sessions, resume) {
|
|
297
301
|
const dateGroups = [];
|
|
298
302
|
let currentDate = null;
|
|
299
303
|
for (const s of sessions) {
|
|
@@ -305,20 +309,20 @@ function generateSessionList(project, sessions) {
|
|
|
305
309
|
dateGroups[dateGroups.length - 1].sessions.push(s);
|
|
306
310
|
}
|
|
307
311
|
// Build side nav
|
|
308
|
-
const sideNavItems = dateGroups.map(g => `<a class="sidenav-item" href="#date-${g.date}" data-date="${escapeHtml(g.date)}">${escapeHtml(g.date)} <span class="sidenav-count">(${g.sessions.length})</span></a>`).join('\n');
|
|
312
|
+
const sideNavItems = dateGroups.map(g => `<a class="sidenav-item" href="#date-${g.date}" data-date="${(0, util_1.escapeHtml)(g.date)}">${(0, util_1.escapeHtml)(g.date)} <span class="sidenav-count">(${g.sessions.length})</span></a>`).join('\n');
|
|
309
313
|
// Build session items grouped by date
|
|
310
314
|
const groupsHtml = dateGroups.map(g => {
|
|
311
315
|
const items = g.sessions.map(s => {
|
|
312
316
|
const href = `/project/${encodeURIComponent(project.rawName)}/session/${encodeURIComponent(s.id)}`;
|
|
313
317
|
const lastModTime = formatTimeOnly(new Date(s.lastModified));
|
|
314
318
|
const startedTime = formatDate(s.timestamp);
|
|
315
|
-
const slug = s.slug ? `<span class="badge">${escapeHtml(s.slug)}</span>` : '';
|
|
319
|
+
const slug = s.slug ? `<span class="badge">${(0, util_1.escapeHtml)(s.slug)}</span>` : '';
|
|
316
320
|
const sub = s.hasSubagents ? '<span class="badge">subagents</span>' : '';
|
|
317
|
-
const model = s.model ? `<span>${escapeHtml(s.model)}</span>` : '';
|
|
318
|
-
const branch = s.gitBranch && s.gitBranch !== 'HEAD' ? `<span>branch: ${escapeHtml(s.gitBranch)}</span>` : '';
|
|
321
|
+
const model = s.model ? `<span>${(0, util_1.escapeHtml)(s.model)}</span>` : '';
|
|
322
|
+
const branch = s.gitBranch && s.gitBranch !== 'HEAD' ? `<span>branch: ${(0, util_1.escapeHtml)(s.gitBranch)}</span>` : '';
|
|
319
323
|
const tokens = s.totalTokens ? `<span>${formatTokens(s.totalTokens)} tokens</span>` : '';
|
|
320
324
|
const outTok = s.outputTokens ? `<span>out: ${formatTokens(s.outputTokens)}</span>` : '';
|
|
321
|
-
const preview = s.preview ? `<div class="list-item-preview">${escapeHtml(s.preview)}</div>` : '';
|
|
325
|
+
const preview = s.preview ? `<div class="list-item-preview">${(0, util_1.escapeHtml)(s.preview)}</div>` : '';
|
|
322
326
|
// Prefer a human/AI-given name as the headline; renamed sessions get a
|
|
323
327
|
// distinct treatment so they're easy to spot in the list.
|
|
324
328
|
const displayName = s.customTitle || s.aiTitle;
|
|
@@ -328,25 +332,31 @@ function generateSessionList(project, sessions) {
|
|
|
328
332
|
? '<span class="badge badge-ai">AI</span>'
|
|
329
333
|
: '';
|
|
330
334
|
const titleRow = displayName
|
|
331
|
-
? `<div class="list-item-title"><span class="list-item-name${s.customTitle ? ' is-named' : ''}">${escapeHtml(displayName)}</span> ${nameBadge} ${slug} ${sub}</div>
|
|
335
|
+
? `<div class="list-item-title"><span class="list-item-name${s.customTitle ? ' is-named' : ''}">${(0, util_1.escapeHtml)(displayName)}</span> ${nameBadge} ${slug} ${sub}</div>
|
|
332
336
|
<div class="list-item-meta"><span>${lastModTime}</span><span>started: ${startedTime}</span>${model}${branch}${tokens}${outTok}</div>`
|
|
333
337
|
: `<div class="list-item-title">${lastModTime} ${slug} ${sub}</div>
|
|
334
338
|
<div class="list-item-meta"><span>started: ${startedTime}</span>${model}${branch}${tokens}${outTok}</div>`;
|
|
335
|
-
|
|
339
|
+
// Stretched-link pattern: the row is a <div> (not an <a>) so the resume
|
|
340
|
+
// <button>s aren't nested inside an anchor (invalid HTML). A full-cover
|
|
341
|
+
// link handles row navigation; the buttons sit above it via z-index.
|
|
342
|
+
const label = (0, util_1.escapeHtml)(displayName || s.id);
|
|
343
|
+
return `<div class="list-item">
|
|
344
|
+
<a class="list-item-link" href="${href}" aria-label="${label}"></a>
|
|
336
345
|
${titleRow}
|
|
337
346
|
${preview}
|
|
338
|
-
|
|
347
|
+
${(0, resume_ui_1.resumeButtonsHtml)(project.rawName, s, resume)}
|
|
348
|
+
</div>`;
|
|
339
349
|
}).join('\n');
|
|
340
|
-
return `<div class="date-group" id="date-${g.date}" data-date="${escapeHtml(g.date)}">
|
|
341
|
-
<div class="date-heading">${escapeHtml(g.date)}</div>
|
|
350
|
+
return `<div class="date-group" id="date-${g.date}" data-date="${(0, util_1.escapeHtml)(g.date)}">
|
|
351
|
+
<div class="date-heading">${(0, util_1.escapeHtml)(g.date)}</div>
|
|
342
352
|
${items}
|
|
343
353
|
</div>`;
|
|
344
354
|
}).join('\n');
|
|
345
355
|
return pageShell(`${project.name} — ccakashic`, `
|
|
346
356
|
<div class="sticky-date-bar" id="stickyDateBar"></div>
|
|
347
357
|
<div class="page-header">
|
|
348
|
-
<div class="breadcrumb"><a href="/">
|
|
349
|
-
<h1>${escapeHtml(project.name)}</h1>
|
|
358
|
+
<div class="breadcrumb"><a href="/">dashboard</a> / <a href="/projects">projects</a> / ${(0, util_1.escapeHtml)(project.name)}</div>
|
|
359
|
+
<h1>${(0, util_1.escapeHtml)(project.name)}</h1>
|
|
350
360
|
<div class="subtitle">${sessions.length} sessions</div>
|
|
351
361
|
</div>
|
|
352
362
|
<div class="session-layout">
|
|
@@ -402,5 +412,6 @@ function generateSessionList(project, sessions) {
|
|
|
402
412
|
window.addEventListener('scroll', updateCurrentDate, { passive: true });
|
|
403
413
|
updateCurrentDate();
|
|
404
414
|
})();
|
|
415
|
+
${(0, resume_ui_1.resumeJS)(resume)}
|
|
405
416
|
</script>`);
|
|
406
417
|
}
|
package/dist/parser.js
CHANGED
|
@@ -33,10 +33,37 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseSessionCached = parseSessionCached;
|
|
36
37
|
exports.parseSession = parseSession;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const readline = __importStar(require("readline"));
|
|
41
|
+
// Small LRU cache keyed on (path, mtime). The dashboard's server-side render
|
|
42
|
+
// and the /api/pane poll can ask for the same (unchanged) session, and several
|
|
43
|
+
// panes/clients may show the same session; this avoids re-reading and
|
|
44
|
+
// re-parsing the whole file for an identical mtime. NOTE: an actively-changing
|
|
45
|
+
// session gets a new mtime each poll (cache miss) and is still fully re-parsed;
|
|
46
|
+
// a future optimization could read only the bytes appended past the last offset.
|
|
47
|
+
const parseCache = new Map();
|
|
48
|
+
const PARSE_CACHE_MAX = 32;
|
|
49
|
+
async function parseSessionCached(sessionPath, mtimeMs) {
|
|
50
|
+
const key = sessionPath;
|
|
51
|
+
const hit = parseCache.get(key);
|
|
52
|
+
if (hit && hit.mtimeMs === mtimeMs) {
|
|
53
|
+
// Refresh LRU recency.
|
|
54
|
+
parseCache.delete(key);
|
|
55
|
+
parseCache.set(key, hit);
|
|
56
|
+
return hit.parsed;
|
|
57
|
+
}
|
|
58
|
+
const parsed = await parseSession(sessionPath);
|
|
59
|
+
parseCache.set(key, { mtimeMs, parsed });
|
|
60
|
+
if (parseCache.size > PARSE_CACHE_MAX) {
|
|
61
|
+
const oldest = parseCache.keys().next().value;
|
|
62
|
+
if (oldest !== undefined)
|
|
63
|
+
parseCache.delete(oldest);
|
|
64
|
+
}
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
40
67
|
async function parseSession(sessionPath) {
|
|
41
68
|
const lines = await readJsonlLines(sessionPath);
|
|
42
69
|
const messages = buildConversation(lines);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resumeButtonsHtml = resumeButtonsHtml;
|
|
4
|
+
exports.resumeCSS = resumeCSS;
|
|
5
|
+
exports.resumeJS = resumeJS;
|
|
6
|
+
const cmux_1 = require("./cmux");
|
|
7
|
+
const util_1 = require("./util");
|
|
8
|
+
function resumeButtonsHtml(projectRawName, session, ctx) {
|
|
9
|
+
if (!ctx || !session.cwd)
|
|
10
|
+
return '';
|
|
11
|
+
const isOpen = ctx.openSessionIds.has(session.id);
|
|
12
|
+
const isActive = Date.now() - session.lastModified < util_1.ACTIVE_THRESHOLD_MS;
|
|
13
|
+
const resumeBtn = ctx.cmuxAvailable
|
|
14
|
+
? `<button type="button" class="resume-btn${isOpen ? ' is-open' : ''}"
|
|
15
|
+
data-project="${(0, util_1.escapeHtml)(projectRawName)}"
|
|
16
|
+
data-session="${(0, util_1.escapeHtml)(session.id)}"
|
|
17
|
+
data-open="${isOpen ? '1' : '0'}"
|
|
18
|
+
data-active="${isActive ? '1' : '0'}"
|
|
19
|
+
title="${isOpen ? 'Jump to the open cmux workspace' : 'Resume in a new cmux workspace (Alt-click: open in background)'}">${isOpen ? '↪ Jump' : '▶ Resume'}</button>`
|
|
20
|
+
: '';
|
|
21
|
+
const copyBtn = `<button type="button" class="copy-cmd-btn"
|
|
22
|
+
data-cmd="${(0, util_1.escapeHtml)((0, cmux_1.buildResumeCommand)(session.cwd, session.id))}"
|
|
23
|
+
title="Copy the cd + claude --resume command">📋 Copy</button>`;
|
|
24
|
+
return `<div class="resume-actions">${resumeBtn}${copyBtn}</div>`;
|
|
25
|
+
}
|
|
26
|
+
function resumeCSS() {
|
|
27
|
+
return `
|
|
28
|
+
.resume-actions {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
gap: 6px;
|
|
31
|
+
margin-top: 6px;
|
|
32
|
+
/* Sit above the stretched .list-item-link so the buttons stay clickable. */
|
|
33
|
+
position: relative;
|
|
34
|
+
z-index: 1;
|
|
35
|
+
}
|
|
36
|
+
.resume-btn, .copy-cmd-btn {
|
|
37
|
+
font-size: 0.72rem;
|
|
38
|
+
font-weight: 600;
|
|
39
|
+
padding: 3px 10px;
|
|
40
|
+
border-radius: 5px;
|
|
41
|
+
border: 1px solid var(--border);
|
|
42
|
+
background: var(--tool-bg);
|
|
43
|
+
color: var(--text);
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
transition: background 0.15s, border-color 0.15s;
|
|
46
|
+
}
|
|
47
|
+
.resume-btn:hover, .copy-cmd-btn:hover {
|
|
48
|
+
border-color: var(--link);
|
|
49
|
+
background: var(--bg-secondary);
|
|
50
|
+
}
|
|
51
|
+
.resume-btn.is-open {
|
|
52
|
+
color: var(--link);
|
|
53
|
+
border-color: var(--link);
|
|
54
|
+
}
|
|
55
|
+
.resume-btn:disabled, .copy-cmd-btn:disabled { opacity: 0.5; cursor: wait; }
|
|
56
|
+
.resume-toast {
|
|
57
|
+
position: fixed;
|
|
58
|
+
bottom: 24px;
|
|
59
|
+
left: 50%;
|
|
60
|
+
transform: translateX(-50%);
|
|
61
|
+
background: var(--text);
|
|
62
|
+
color: var(--bg);
|
|
63
|
+
padding: 8px 18px;
|
|
64
|
+
border-radius: 8px;
|
|
65
|
+
font-size: 0.85rem;
|
|
66
|
+
z-index: 1000;
|
|
67
|
+
opacity: 0;
|
|
68
|
+
transition: opacity 0.2s;
|
|
69
|
+
pointer-events: none;
|
|
70
|
+
}
|
|
71
|
+
.resume-toast.visible { opacity: 1; }
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
function resumeJS(ctx) {
|
|
75
|
+
if (!ctx)
|
|
76
|
+
return '';
|
|
77
|
+
return `
|
|
78
|
+
(function() {
|
|
79
|
+
var TOKEN = ${JSON.stringify(ctx.token)};
|
|
80
|
+
var toastEl = null;
|
|
81
|
+
function toast(msg) {
|
|
82
|
+
if (!toastEl) {
|
|
83
|
+
toastEl = document.createElement('div');
|
|
84
|
+
toastEl.className = 'resume-toast';
|
|
85
|
+
document.body.appendChild(toastEl);
|
|
86
|
+
}
|
|
87
|
+
toastEl.textContent = msg;
|
|
88
|
+
toastEl.classList.add('visible');
|
|
89
|
+
clearTimeout(toastEl._t);
|
|
90
|
+
toastEl._t = setTimeout(function() { toastEl.classList.remove('visible'); }, 2500);
|
|
91
|
+
}
|
|
92
|
+
function copy(text) {
|
|
93
|
+
navigator.clipboard.writeText(text).then(
|
|
94
|
+
function() { toast('Command copied'); },
|
|
95
|
+
function() { toast('Copy failed'); }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
document.addEventListener('click', function(e) {
|
|
99
|
+
var btn = e.target.closest ? e.target.closest('.resume-btn, .copy-cmd-btn') : null;
|
|
100
|
+
if (!btn) return;
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
|
|
104
|
+
if (btn.classList.contains('copy-cmd-btn')) {
|
|
105
|
+
copy(btn.dataset.cmd);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Resuming a session that still looks active forks the conversation;
|
|
110
|
+
// require a second click to confirm.
|
|
111
|
+
if (btn.dataset.active === '1' && btn.dataset.open !== '1' && !btn.dataset.confirm) {
|
|
112
|
+
btn.dataset.confirm = '1';
|
|
113
|
+
var orig = btn.innerHTML;
|
|
114
|
+
btn.innerHTML = '⚠ Fork? click again';
|
|
115
|
+
setTimeout(function() { delete btn.dataset.confirm; btn.innerHTML = orig; }, 3000);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
delete btn.dataset.confirm;
|
|
119
|
+
|
|
120
|
+
var mode = e.altKey ? 'background' : 'jump';
|
|
121
|
+
btn.disabled = true;
|
|
122
|
+
fetch('/api/resume', {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json', 'X-Ccakashic-Token': TOKEN },
|
|
125
|
+
body: JSON.stringify({ project: btn.dataset.project, session: btn.dataset.session, mode: mode })
|
|
126
|
+
}).then(function(res) { return res.json(); }).then(function(data) {
|
|
127
|
+
btn.disabled = false;
|
|
128
|
+
if (data.action === 'jumped') {
|
|
129
|
+
toast('Jumped to the open cmux workspace');
|
|
130
|
+
} else if (data.action === 'resumed') {
|
|
131
|
+
toast(mode === 'background' ? 'Resumed in a background cmux workspace' : 'Resumed in a new cmux workspace');
|
|
132
|
+
btn.dataset.open = '1';
|
|
133
|
+
btn.dataset.active = '1';
|
|
134
|
+
btn.classList.add('is-open');
|
|
135
|
+
btn.innerHTML = '↪ Jump';
|
|
136
|
+
} else if (data.action === 'unavailable' && data.command) {
|
|
137
|
+
copy(data.command);
|
|
138
|
+
} else {
|
|
139
|
+
toast(data.message || 'Resume failed');
|
|
140
|
+
}
|
|
141
|
+
}).catch(function() {
|
|
142
|
+
btn.disabled = false;
|
|
143
|
+
toast('Resume failed: server unreachable');
|
|
144
|
+
});
|
|
145
|
+
}, true);
|
|
146
|
+
})();
|
|
147
|
+
`;
|
|
148
|
+
}
|