codedash-app 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/data.js +66 -4
- package/src/frontend/app.js +78 -6
- package/src/frontend/index.html +3 -0
- package/src/frontend/styles.css +85 -21
- package/src/html.js +2 -2
package/package.json
CHANGED
package/src/data.js
CHANGED
|
@@ -20,18 +20,20 @@ function scanCodexSessions() {
|
|
|
20
20
|
for (const line of lines) {
|
|
21
21
|
try {
|
|
22
22
|
const d = JSON.parse(line);
|
|
23
|
-
|
|
23
|
+
// Codex uses session_id, ts (seconds), text
|
|
24
|
+
const sid = d.session_id || d.sessionId || d.id;
|
|
24
25
|
if (!sid) continue;
|
|
26
|
+
const ts = d.ts ? d.ts * 1000 : (d.timestamp || Date.now());
|
|
25
27
|
if (!sessions.find(s => s.id === sid)) {
|
|
26
28
|
sessions.push({
|
|
27
29
|
id: sid,
|
|
28
30
|
tool: 'codex',
|
|
29
31
|
project: d.project || d.cwd || '',
|
|
30
32
|
project_short: (d.project || d.cwd || '').replace(os.homedir(), '~'),
|
|
31
|
-
first_ts:
|
|
32
|
-
last_ts:
|
|
33
|
+
first_ts: ts,
|
|
34
|
+
last_ts: ts,
|
|
33
35
|
messages: 1,
|
|
34
|
-
first_message: d.display || d.prompt || '',
|
|
36
|
+
first_message: d.text || d.display || d.prompt || '',
|
|
35
37
|
has_detail: false,
|
|
36
38
|
file_size: 0,
|
|
37
39
|
detail_messages: 0,
|
|
@@ -40,6 +42,66 @@ function scanCodexSessions() {
|
|
|
40
42
|
} catch {}
|
|
41
43
|
}
|
|
42
44
|
}
|
|
45
|
+
|
|
46
|
+
// Enrich with session files from ~/.codex/sessions/
|
|
47
|
+
const codexSessionsDir = path.join(CODEX_DIR, 'sessions');
|
|
48
|
+
if (fs.existsSync(codexSessionsDir)) {
|
|
49
|
+
try {
|
|
50
|
+
// Walk year/month/day directories
|
|
51
|
+
const files = [];
|
|
52
|
+
const walkDir = (dir) => {
|
|
53
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
54
|
+
const full = path.join(dir, entry.name);
|
|
55
|
+
if (entry.isDirectory()) walkDir(full);
|
|
56
|
+
else if (entry.name.endsWith('.jsonl')) files.push(full);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walkDir(codexSessionsDir);
|
|
60
|
+
|
|
61
|
+
for (const f of files) {
|
|
62
|
+
const stat = fs.statSync(f);
|
|
63
|
+
// Extract session ID from filename (rollout-DATE-UUID.jsonl)
|
|
64
|
+
const basename = path.basename(f, '.jsonl');
|
|
65
|
+
const uuidMatch = basename.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
|
|
66
|
+
if (!uuidMatch) continue;
|
|
67
|
+
const sid = uuidMatch[1];
|
|
68
|
+
// Try to extract cwd from session_meta
|
|
69
|
+
let cwd = '';
|
|
70
|
+
try {
|
|
71
|
+
const firstLine = fs.readFileSync(f, 'utf8').split('\n')[0];
|
|
72
|
+
const meta = JSON.parse(firstLine);
|
|
73
|
+
if (meta.type === 'session_meta' && meta.payload && meta.payload.cwd) {
|
|
74
|
+
cwd = meta.payload.cwd;
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
|
|
78
|
+
const existing = sessions.find(s => s.id === sid);
|
|
79
|
+
if (existing) {
|
|
80
|
+
existing.has_detail = true;
|
|
81
|
+
existing.file_size = stat.size;
|
|
82
|
+
if (cwd && !existing.project) {
|
|
83
|
+
existing.project = cwd;
|
|
84
|
+
existing.project_short = cwd.replace(os.homedir(), '~');
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
sessions.push({
|
|
88
|
+
id: sid,
|
|
89
|
+
tool: 'codex',
|
|
90
|
+
project: cwd,
|
|
91
|
+
project_short: cwd ? cwd.replace(os.homedir(), '~') : '',
|
|
92
|
+
first_ts: stat.mtimeMs,
|
|
93
|
+
last_ts: stat.mtimeMs,
|
|
94
|
+
messages: 0,
|
|
95
|
+
first_message: '',
|
|
96
|
+
has_detail: true,
|
|
97
|
+
file_size: stat.size,
|
|
98
|
+
detail_messages: 0,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
|
|
43
105
|
return sessions;
|
|
44
106
|
}
|
|
45
107
|
|
package/src/frontend/app.js
CHANGED
|
@@ -7,6 +7,7 @@ let allSessions = [];
|
|
|
7
7
|
let filteredSessions = [];
|
|
8
8
|
let currentView = 'sessions'; // sessions, projects, timeline, activity, starred
|
|
9
9
|
let grouped = true;
|
|
10
|
+
let layout = localStorage.getItem('codedash-layout') || 'grid'; // 'grid' or 'list'
|
|
10
11
|
let searchQuery = '';
|
|
11
12
|
let toolFilter = null; // null, 'claude', 'codex'
|
|
12
13
|
let tagFilter = '';
|
|
@@ -218,6 +219,14 @@ function applyFilters() {
|
|
|
218
219
|
return true;
|
|
219
220
|
});
|
|
220
221
|
|
|
222
|
+
// Starred sessions first
|
|
223
|
+
filteredSessions.sort(function(a, b) {
|
|
224
|
+
var aStarred = stars.indexOf(a.id) >= 0 ? 1 : 0;
|
|
225
|
+
var bStarred = stars.indexOf(b.id) >= 0 ? 1 : 0;
|
|
226
|
+
if (aStarred !== bStarred) return bStarred - aStarred;
|
|
227
|
+
return b.last_ts - a.last_ts;
|
|
228
|
+
});
|
|
229
|
+
|
|
221
230
|
render();
|
|
222
231
|
}
|
|
223
232
|
|
|
@@ -325,6 +334,42 @@ function renderCard(s, idx) {
|
|
|
325
334
|
return html;
|
|
326
335
|
}
|
|
327
336
|
|
|
337
|
+
function toggleLayout() {
|
|
338
|
+
layout = layout === 'grid' ? 'list' : 'grid';
|
|
339
|
+
localStorage.setItem('codedash-layout', layout);
|
|
340
|
+
var btn = document.getElementById('layoutBtn');
|
|
341
|
+
if (btn) btn.classList.toggle('active', layout === 'list');
|
|
342
|
+
var icon = document.getElementById('layoutIcon');
|
|
343
|
+
if (icon) {
|
|
344
|
+
icon.innerHTML = layout === 'list'
|
|
345
|
+
? '<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>'
|
|
346
|
+
: '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>';
|
|
347
|
+
}
|
|
348
|
+
render();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function renderListCard(s, idx) {
|
|
352
|
+
var isStarred = stars.indexOf(s.id) >= 0;
|
|
353
|
+
var isSelected = selectedIds.has(s.id);
|
|
354
|
+
var isFocused = focusedIndex === idx;
|
|
355
|
+
var projName = getProjectName(s.project);
|
|
356
|
+
var projColor = getProjectColor(projName);
|
|
357
|
+
|
|
358
|
+
var classes = 'list-row';
|
|
359
|
+
if (isSelected) classes += ' selected';
|
|
360
|
+
if (isFocused) classes += ' focused';
|
|
361
|
+
|
|
362
|
+
var html = '<div class="' + classes + '" data-id="' + s.id + '" onclick="onCardClick(\'' + s.id + '\', event)">';
|
|
363
|
+
html += '<span class="tool-badge tool-' + s.tool + '">' + escHtml(s.tool) + '</span>';
|
|
364
|
+
html += '<span class="list-project" style="color:' + projColor + '">' + escHtml(projName) + '</span>';
|
|
365
|
+
html += '<span class="list-msg">' + escHtml((s.first_message || '').slice(0, 80)) + '</span>';
|
|
366
|
+
html += '<span class="list-meta">' + s.messages + ' msgs</span>';
|
|
367
|
+
html += '<span class="list-time">' + timeAgo(s.last_ts) + '</span>';
|
|
368
|
+
html += '<button class="star-btn' + (isStarred ? ' active' : '') + '" onclick="event.stopPropagation();toggleStar(\'' + s.id + '\')">★</button>';
|
|
369
|
+
html += '</div>';
|
|
370
|
+
return html;
|
|
371
|
+
}
|
|
372
|
+
|
|
328
373
|
function onCardClick(id, event) {
|
|
329
374
|
if (selectMode) {
|
|
330
375
|
toggleSelect(id, event);
|
|
@@ -384,15 +429,18 @@ function render() {
|
|
|
384
429
|
return;
|
|
385
430
|
}
|
|
386
431
|
|
|
432
|
+
var renderFn = layout === 'list' ? renderListCard : renderCard;
|
|
387
433
|
if (grouped) {
|
|
388
|
-
renderGrouped(content, sessions);
|
|
434
|
+
renderGrouped(content, sessions, renderFn);
|
|
389
435
|
} else {
|
|
390
436
|
var idx2 = 0;
|
|
391
|
-
|
|
437
|
+
var wrapClass = layout === 'list' ? 'list-view' : 'grid-view';
|
|
438
|
+
content.innerHTML = '<div class="' + wrapClass + '">' + sessions.map(function(s) { return renderFn(s, idx2++); }).join('') + '</div>';
|
|
392
439
|
}
|
|
393
440
|
}
|
|
394
441
|
|
|
395
|
-
function renderGrouped(container, sessions) {
|
|
442
|
+
function renderGrouped(container, sessions, renderFn) {
|
|
443
|
+
renderFn = renderFn || renderCard;
|
|
396
444
|
var groups = {};
|
|
397
445
|
sessions.forEach(function(s) {
|
|
398
446
|
var key = getProjectName(s.project);
|
|
@@ -415,9 +463,10 @@ function renderGrouped(container, sessions) {
|
|
|
415
463
|
html += '<span class="group-count">' + groups[key].length + '</span>';
|
|
416
464
|
html += '<span class="group-chevron">▼</span>';
|
|
417
465
|
html += '</div>';
|
|
418
|
-
|
|
466
|
+
var bodyClass = layout === 'list' ? 'group-body group-body-list' : 'group-body';
|
|
467
|
+
html += '<div class="' + bodyClass + '">';
|
|
419
468
|
groups[key].forEach(function(s) {
|
|
420
|
-
html +=
|
|
469
|
+
html += renderFn(s, globalIdx++);
|
|
421
470
|
});
|
|
422
471
|
html += '</div></div>';
|
|
423
472
|
});
|
|
@@ -479,7 +528,7 @@ function renderProjects(container, sessions) {
|
|
|
479
528
|
var totalSize = info.sessions.reduce(function(sum, s) { return sum + (s.file_size || 0); }, 0);
|
|
480
529
|
var latest = info.sessions[0];
|
|
481
530
|
|
|
482
|
-
html += '<div class="project-card" onclick="
|
|
531
|
+
html += '<div class="project-card" onclick="openProject(\'' + escHtml(name).replace(/'/g, "\\'") + '\')">';
|
|
483
532
|
html += '<div class="project-card-header">';
|
|
484
533
|
html += '<span class="group-dot" style="background:' + color + '"></span>';
|
|
485
534
|
html += '<span class="project-card-name">' + escHtml(name) + '</span>';
|
|
@@ -822,6 +871,17 @@ async function confirmDelete() {
|
|
|
822
871
|
if (data.ok) {
|
|
823
872
|
showToast('Session deleted');
|
|
824
873
|
allSessions = allSessions.filter(function(s) { return s.id !== pendingDelete.id; });
|
|
874
|
+
// Clear search if no more results
|
|
875
|
+
if (searchQuery) {
|
|
876
|
+
var remaining = allSessions.filter(function(s) {
|
|
877
|
+
return (s.project || '').toLowerCase().indexOf(searchQuery.toLowerCase()) >= 0 ||
|
|
878
|
+
(s.first_message || '').toLowerCase().indexOf(searchQuery.toLowerCase()) >= 0;
|
|
879
|
+
});
|
|
880
|
+
if (remaining.length === 0) {
|
|
881
|
+
searchQuery = '';
|
|
882
|
+
document.querySelector('.search-box').value = '';
|
|
883
|
+
}
|
|
884
|
+
}
|
|
825
885
|
closeConfirm();
|
|
826
886
|
closeDetail();
|
|
827
887
|
applyFilters();
|
|
@@ -898,6 +958,18 @@ async function bulkDelete() {
|
|
|
898
958
|
}
|
|
899
959
|
}
|
|
900
960
|
|
|
961
|
+
// ── Project actions ────────────────────────────────────────────
|
|
962
|
+
|
|
963
|
+
function openProject(name) {
|
|
964
|
+
currentView = 'sessions';
|
|
965
|
+
searchQuery = name;
|
|
966
|
+
document.querySelector('.search-box').value = name;
|
|
967
|
+
document.querySelectorAll('.sidebar-item').forEach(function(el) {
|
|
968
|
+
el.classList.toggle('active', el.getAttribute('data-view') === 'sessions');
|
|
969
|
+
});
|
|
970
|
+
applyFilters();
|
|
971
|
+
}
|
|
972
|
+
|
|
901
973
|
// ── Themes ─────────────────────────────────────────────────────
|
|
902
974
|
|
|
903
975
|
function setTheme(theme) {
|
package/src/frontend/index.html
CHANGED
|
@@ -72,6 +72,9 @@
|
|
|
72
72
|
<input type="date" class="date-input" id="dateFrom" onchange="onDateFilter()" title="From date">
|
|
73
73
|
<input type="date" class="date-input" id="dateTo" onchange="onDateFilter()" title="To date">
|
|
74
74
|
<button class="toolbar-btn" onclick="toggleGroup()" id="groupBtn">Group</button>
|
|
75
|
+
<button class="toolbar-btn" onclick="toggleLayout()" id="layoutBtn" title="Grid / List">
|
|
76
|
+
<svg id="layoutIcon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
|
77
|
+
</button>
|
|
75
78
|
<button class="toolbar-btn" onclick="toggleSelectMode()" id="selectBtn">Select</button>
|
|
76
79
|
<button class="toolbar-btn" onclick="refreshData()">Refresh</button>
|
|
77
80
|
<span class="stats" id="stats"></span>
|
package/src/frontend/styles.css
CHANGED
|
@@ -335,30 +335,16 @@ body {
|
|
|
335
335
|
/* ── Card Checkbox (bulk select) ───────────────────────────────── */
|
|
336
336
|
|
|
337
337
|
.card-checkbox {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
left: -6px;
|
|
341
|
-
width: 18px;
|
|
342
|
-
height: 18px;
|
|
343
|
-
border-radius: 4px;
|
|
344
|
-
border: 2px solid var(--border);
|
|
345
|
-
background: var(--bg-secondary);
|
|
338
|
+
width: 16px;
|
|
339
|
+
height: 16px;
|
|
346
340
|
cursor: pointer;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
display:
|
|
350
|
-
align-items: center;
|
|
351
|
-
justify-content: center;
|
|
352
|
-
z-index: 2;
|
|
341
|
+
accent-color: var(--accent-blue);
|
|
342
|
+
flex-shrink: 0;
|
|
343
|
+
display: none;
|
|
353
344
|
}
|
|
354
345
|
.card:hover .card-checkbox,
|
|
355
|
-
.card.selected .card-checkbox
|
|
356
|
-
|
|
357
|
-
opacity: 1;
|
|
358
|
-
}
|
|
359
|
-
.card-checkbox.checked {
|
|
360
|
-
background: var(--accent-blue);
|
|
361
|
-
border-color: var(--accent-blue);
|
|
346
|
+
.card.selected .card-checkbox {
|
|
347
|
+
display: inline-block;
|
|
362
348
|
}
|
|
363
349
|
|
|
364
350
|
/* ── Card Actions ──────────────────────────────────────────────── */
|
|
@@ -1383,3 +1369,81 @@ body {
|
|
|
1383
1369
|
background: var(--accent-red);
|
|
1384
1370
|
color: #fff;
|
|
1385
1371
|
}
|
|
1372
|
+
|
|
1373
|
+
/* ── List view ──────────────────────────────────────────────── */
|
|
1374
|
+
|
|
1375
|
+
.list-view {
|
|
1376
|
+
display: flex;
|
|
1377
|
+
flex-direction: column;
|
|
1378
|
+
gap: 2px;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
.list-row {
|
|
1382
|
+
display: flex;
|
|
1383
|
+
align-items: center;
|
|
1384
|
+
gap: 12px;
|
|
1385
|
+
padding: 10px 14px;
|
|
1386
|
+
background: var(--bg-card);
|
|
1387
|
+
border: 1px solid var(--border);
|
|
1388
|
+
border-radius: 8px;
|
|
1389
|
+
cursor: pointer;
|
|
1390
|
+
transition: background 0.15s;
|
|
1391
|
+
font-size: 13px;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.list-row:hover {
|
|
1395
|
+
background: var(--bg-card-hover);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
.list-row.selected {
|
|
1399
|
+
border-color: var(--accent-blue);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
.list-row.focused {
|
|
1403
|
+
outline: 2px solid var(--accent-blue);
|
|
1404
|
+
outline-offset: -2px;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
.list-project {
|
|
1408
|
+
font-weight: 600;
|
|
1409
|
+
font-size: 12px;
|
|
1410
|
+
width: 100px;
|
|
1411
|
+
flex-shrink: 0;
|
|
1412
|
+
overflow: hidden;
|
|
1413
|
+
text-overflow: ellipsis;
|
|
1414
|
+
white-space: nowrap;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
.list-msg {
|
|
1418
|
+
flex: 1;
|
|
1419
|
+
overflow: hidden;
|
|
1420
|
+
text-overflow: ellipsis;
|
|
1421
|
+
white-space: nowrap;
|
|
1422
|
+
color: var(--text-primary);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
.list-meta {
|
|
1426
|
+
color: var(--text-muted);
|
|
1427
|
+
font-size: 12px;
|
|
1428
|
+
flex-shrink: 0;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
.list-time {
|
|
1432
|
+
color: var(--text-muted);
|
|
1433
|
+
font-size: 12px;
|
|
1434
|
+
flex-shrink: 0;
|
|
1435
|
+
width: 60px;
|
|
1436
|
+
text-align: right;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.grid-view {
|
|
1440
|
+
display: grid;
|
|
1441
|
+
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
|
1442
|
+
gap: 10px;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
.group-body-list {
|
|
1446
|
+
display: flex;
|
|
1447
|
+
flex-direction: column;
|
|
1448
|
+
gap: 2px;
|
|
1449
|
+
}
|
package/src/html.js
CHANGED
|
@@ -10,8 +10,8 @@ function buildHTML() {
|
|
|
10
10
|
const script = fs.readFileSync(path.join(FRONTEND_DIR, 'app.js'), 'utf8');
|
|
11
11
|
|
|
12
12
|
return template
|
|
13
|
-
.
|
|
14
|
-
.
|
|
13
|
+
.split('{{STYLES}}').join(styles)
|
|
14
|
+
.split('{{SCRIPT}}').join(script);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// Cache in production
|