ccakashic 0.2.7 → 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 +88 -28
- package/dist/html-generator.js +50 -48
- package/dist/pages.js +62 -23
- 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,8 +295,11 @@ 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;
|
|
303
|
-
const
|
|
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
|
+
: '';
|
|
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')
|
|
306
305
|
: '';
|
|
@@ -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);
|
|
@@ -97,6 +100,12 @@ function indexCSS() {
|
|
|
97
100
|
font-size: 0.95rem;
|
|
98
101
|
margin-bottom: 4px;
|
|
99
102
|
}
|
|
103
|
+
.list-item-name {
|
|
104
|
+
font-size: 1.02rem;
|
|
105
|
+
}
|
|
106
|
+
.list-item-name.is-named {
|
|
107
|
+
color: var(--link);
|
|
108
|
+
}
|
|
100
109
|
.list-item-meta {
|
|
101
110
|
font-size: 0.8rem;
|
|
102
111
|
color: var(--text-muted);
|
|
@@ -121,6 +130,16 @@ function indexCSS() {
|
|
|
121
130
|
background: var(--tool-bg);
|
|
122
131
|
border: 1px solid var(--border);
|
|
123
132
|
}
|
|
133
|
+
.badge-named {
|
|
134
|
+
background: var(--link);
|
|
135
|
+
border-color: var(--link);
|
|
136
|
+
color: #fff;
|
|
137
|
+
}
|
|
138
|
+
.badge-ai {
|
|
139
|
+
background: transparent;
|
|
140
|
+
border-color: var(--link);
|
|
141
|
+
color: var(--link);
|
|
142
|
+
}
|
|
124
143
|
.search-box {
|
|
125
144
|
width: 100%;
|
|
126
145
|
padding: 10px 14px;
|
|
@@ -240,7 +259,7 @@ function generateIndex(projects) {
|
|
|
240
259
|
const items = projects.map((p) => {
|
|
241
260
|
const href = `/project/${encodeURIComponent(p.rawName)}`;
|
|
242
261
|
return `<a class="list-item" href="${href}">
|
|
243
|
-
<div class="list-item-title">${escapeHtml(p.name)}</div>
|
|
262
|
+
<div class="list-item-title">${(0, util_1.escapeHtml)(p.name)}</div>
|
|
244
263
|
<div class="list-item-meta">
|
|
245
264
|
<span>${p.sessionCount} sessions</span>
|
|
246
265
|
<span>Last: ${formatDate(p.lastModified)}</span>
|
|
@@ -249,6 +268,7 @@ function generateIndex(projects) {
|
|
|
249
268
|
}).join('\n');
|
|
250
269
|
return pageShell('ccakashic', `
|
|
251
270
|
<div class="page-header">
|
|
271
|
+
<div class="breadcrumb"><a href="/">← dashboard</a></div>
|
|
252
272
|
<h1>ccakashic</h1>
|
|
253
273
|
<div class="subtitle">Claude Code Session Logs</div>
|
|
254
274
|
</div>
|
|
@@ -277,7 +297,7 @@ function formatTimeOnly(ts) {
|
|
|
277
297
|
const d = ts instanceof Date ? ts : new Date(ts);
|
|
278
298
|
return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
279
299
|
}
|
|
280
|
-
function generateSessionList(project, sessions) {
|
|
300
|
+
function generateSessionList(project, sessions, resume) {
|
|
281
301
|
const dateGroups = [];
|
|
282
302
|
let currentDate = null;
|
|
283
303
|
for (const s of sessions) {
|
|
@@ -289,36 +309,54 @@ function generateSessionList(project, sessions) {
|
|
|
289
309
|
dateGroups[dateGroups.length - 1].sessions.push(s);
|
|
290
310
|
}
|
|
291
311
|
// Build side nav
|
|
292
|
-
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');
|
|
293
313
|
// Build session items grouped by date
|
|
294
314
|
const groupsHtml = dateGroups.map(g => {
|
|
295
315
|
const items = g.sessions.map(s => {
|
|
296
316
|
const href = `/project/${encodeURIComponent(project.rawName)}/session/${encodeURIComponent(s.id)}`;
|
|
297
317
|
const lastModTime = formatTimeOnly(new Date(s.lastModified));
|
|
298
318
|
const startedTime = formatDate(s.timestamp);
|
|
299
|
-
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>` : '';
|
|
300
320
|
const sub = s.hasSubagents ? '<span class="badge">subagents</span>' : '';
|
|
301
|
-
const model = s.model ? `<span>${escapeHtml(s.model)}</span>` : '';
|
|
302
|
-
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>` : '';
|
|
303
323
|
const tokens = s.totalTokens ? `<span>${formatTokens(s.totalTokens)} tokens</span>` : '';
|
|
304
324
|
const outTok = s.outputTokens ? `<span>out: ${formatTokens(s.outputTokens)}</span>` : '';
|
|
305
|
-
const preview = s.preview ? `<div class="list-item-preview">${escapeHtml(s.preview)}</div>` : '';
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
325
|
+
const preview = s.preview ? `<div class="list-item-preview">${(0, util_1.escapeHtml)(s.preview)}</div>` : '';
|
|
326
|
+
// Prefer a human/AI-given name as the headline; renamed sessions get a
|
|
327
|
+
// distinct treatment so they're easy to spot in the list.
|
|
328
|
+
const displayName = s.customTitle || s.aiTitle;
|
|
329
|
+
const nameBadge = s.customTitle
|
|
330
|
+
? '<span class="badge badge-named">named</span>'
|
|
331
|
+
: s.aiTitle
|
|
332
|
+
? '<span class="badge badge-ai">AI</span>'
|
|
333
|
+
: '';
|
|
334
|
+
const titleRow = displayName
|
|
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>
|
|
336
|
+
<div class="list-item-meta"><span>${lastModTime}</span><span>started: ${startedTime}</span>${model}${branch}${tokens}${outTok}</div>`
|
|
337
|
+
: `<div class="list-item-title">${lastModTime} ${slug} ${sub}</div>
|
|
338
|
+
<div class="list-item-meta"><span>started: ${startedTime}</span>${model}${branch}${tokens}${outTok}</div>`;
|
|
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>
|
|
345
|
+
${titleRow}
|
|
309
346
|
${preview}
|
|
310
|
-
|
|
347
|
+
${(0, resume_ui_1.resumeButtonsHtml)(project.rawName, s, resume)}
|
|
348
|
+
</div>`;
|
|
311
349
|
}).join('\n');
|
|
312
|
-
return `<div class="date-group" id="date-${g.date}" data-date="${escapeHtml(g.date)}">
|
|
313
|
-
<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>
|
|
314
352
|
${items}
|
|
315
353
|
</div>`;
|
|
316
354
|
}).join('\n');
|
|
317
355
|
return pageShell(`${project.name} — ccakashic`, `
|
|
318
356
|
<div class="sticky-date-bar" id="stickyDateBar"></div>
|
|
319
357
|
<div class="page-header">
|
|
320
|
-
<div class="breadcrumb"><a href="/">
|
|
321
|
-
<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>
|
|
322
360
|
<div class="subtitle">${sessions.length} sessions</div>
|
|
323
361
|
</div>
|
|
324
362
|
<div class="session-layout">
|
|
@@ -374,5 +412,6 @@ function generateSessionList(project, sessions) {
|
|
|
374
412
|
window.addEventListener('scroll', updateCurrentDate, { passive: true });
|
|
375
413
|
updateCurrentDate();
|
|
376
414
|
})();
|
|
415
|
+
${(0, resume_ui_1.resumeJS)(resume)}
|
|
377
416
|
</script>`);
|
|
378
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
|
+
}
|