codedash-app 1.0.0 → 1.1.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/package.json +1 -1
- package/src/data.js +52 -4
- package/src/frontend/app.js +59 -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,52 @@ 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
|
+
const existing = sessions.find(s => s.id === sid);
|
|
69
|
+
if (existing) {
|
|
70
|
+
existing.has_detail = true;
|
|
71
|
+
existing.file_size = stat.size;
|
|
72
|
+
} else {
|
|
73
|
+
sessions.push({
|
|
74
|
+
id: sid,
|
|
75
|
+
tool: 'codex',
|
|
76
|
+
project: '',
|
|
77
|
+
project_short: '',
|
|
78
|
+
first_ts: stat.mtimeMs,
|
|
79
|
+
last_ts: stat.mtimeMs,
|
|
80
|
+
messages: 0,
|
|
81
|
+
first_message: '',
|
|
82
|
+
has_detail: true,
|
|
83
|
+
file_size: stat.size,
|
|
84
|
+
detail_messages: 0,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
|
|
43
91
|
return sessions;
|
|
44
92
|
}
|
|
45
93
|
|
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 = '';
|
|
@@ -325,6 +326,42 @@ function renderCard(s, idx) {
|
|
|
325
326
|
return html;
|
|
326
327
|
}
|
|
327
328
|
|
|
329
|
+
function toggleLayout() {
|
|
330
|
+
layout = layout === 'grid' ? 'list' : 'grid';
|
|
331
|
+
localStorage.setItem('codedash-layout', layout);
|
|
332
|
+
var btn = document.getElementById('layoutBtn');
|
|
333
|
+
if (btn) btn.classList.toggle('active', layout === 'list');
|
|
334
|
+
var icon = document.getElementById('layoutIcon');
|
|
335
|
+
if (icon) {
|
|
336
|
+
icon.innerHTML = layout === 'list'
|
|
337
|
+
? '<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"/>'
|
|
338
|
+
: '<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"/>';
|
|
339
|
+
}
|
|
340
|
+
render();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function renderListCard(s, idx) {
|
|
344
|
+
var isStarred = stars.indexOf(s.id) >= 0;
|
|
345
|
+
var isSelected = selectedIds.has(s.id);
|
|
346
|
+
var isFocused = focusedIndex === idx;
|
|
347
|
+
var projName = getProjectName(s.project);
|
|
348
|
+
var projColor = getProjectColor(projName);
|
|
349
|
+
|
|
350
|
+
var classes = 'list-row';
|
|
351
|
+
if (isSelected) classes += ' selected';
|
|
352
|
+
if (isFocused) classes += ' focused';
|
|
353
|
+
|
|
354
|
+
var html = '<div class="' + classes + '" data-id="' + s.id + '" onclick="onCardClick(\'' + s.id + '\', event)">';
|
|
355
|
+
html += '<span class="tool-badge tool-' + s.tool + '">' + escHtml(s.tool) + '</span>';
|
|
356
|
+
html += '<span class="list-project" style="color:' + projColor + '">' + escHtml(projName) + '</span>';
|
|
357
|
+
html += '<span class="list-msg">' + escHtml((s.first_message || '').slice(0, 80)) + '</span>';
|
|
358
|
+
html += '<span class="list-meta">' + s.messages + ' msgs</span>';
|
|
359
|
+
html += '<span class="list-time">' + timeAgo(s.last_ts) + '</span>';
|
|
360
|
+
html += '<button class="star-btn' + (isStarred ? ' active' : '') + '" onclick="event.stopPropagation();toggleStar(\'' + s.id + '\')">★</button>';
|
|
361
|
+
html += '</div>';
|
|
362
|
+
return html;
|
|
363
|
+
}
|
|
364
|
+
|
|
328
365
|
function onCardClick(id, event) {
|
|
329
366
|
if (selectMode) {
|
|
330
367
|
toggleSelect(id, event);
|
|
@@ -384,15 +421,18 @@ function render() {
|
|
|
384
421
|
return;
|
|
385
422
|
}
|
|
386
423
|
|
|
424
|
+
var renderFn = layout === 'list' ? renderListCard : renderCard;
|
|
387
425
|
if (grouped) {
|
|
388
|
-
renderGrouped(content, sessions);
|
|
426
|
+
renderGrouped(content, sessions, renderFn);
|
|
389
427
|
} else {
|
|
390
428
|
var idx2 = 0;
|
|
391
|
-
|
|
429
|
+
var wrapClass = layout === 'list' ? 'list-view' : 'grid-view';
|
|
430
|
+
content.innerHTML = '<div class="' + wrapClass + '">' + sessions.map(function(s) { return renderFn(s, idx2++); }).join('') + '</div>';
|
|
392
431
|
}
|
|
393
432
|
}
|
|
394
433
|
|
|
395
|
-
function renderGrouped(container, sessions) {
|
|
434
|
+
function renderGrouped(container, sessions, renderFn) {
|
|
435
|
+
renderFn = renderFn || renderCard;
|
|
396
436
|
var groups = {};
|
|
397
437
|
sessions.forEach(function(s) {
|
|
398
438
|
var key = getProjectName(s.project);
|
|
@@ -415,9 +455,10 @@ function renderGrouped(container, sessions) {
|
|
|
415
455
|
html += '<span class="group-count">' + groups[key].length + '</span>';
|
|
416
456
|
html += '<span class="group-chevron">▼</span>';
|
|
417
457
|
html += '</div>';
|
|
418
|
-
|
|
458
|
+
var bodyClass = layout === 'list' ? 'group-body group-body-list' : 'group-body';
|
|
459
|
+
html += '<div class="' + bodyClass + '">';
|
|
419
460
|
groups[key].forEach(function(s) {
|
|
420
|
-
html +=
|
|
461
|
+
html += renderFn(s, globalIdx++);
|
|
421
462
|
});
|
|
422
463
|
html += '</div></div>';
|
|
423
464
|
});
|
|
@@ -479,7 +520,7 @@ function renderProjects(container, sessions) {
|
|
|
479
520
|
var totalSize = info.sessions.reduce(function(sum, s) { return sum + (s.file_size || 0); }, 0);
|
|
480
521
|
var latest = info.sessions[0];
|
|
481
522
|
|
|
482
|
-
html += '<div class="project-card" onclick="
|
|
523
|
+
html += '<div class="project-card" onclick="openProject(\'' + escHtml(name).replace(/'/g, "\\'") + '\')">';
|
|
483
524
|
html += '<div class="project-card-header">';
|
|
484
525
|
html += '<span class="group-dot" style="background:' + color + '"></span>';
|
|
485
526
|
html += '<span class="project-card-name">' + escHtml(name) + '</span>';
|
|
@@ -898,6 +939,18 @@ async function bulkDelete() {
|
|
|
898
939
|
}
|
|
899
940
|
}
|
|
900
941
|
|
|
942
|
+
// ── Project actions ────────────────────────────────────────────
|
|
943
|
+
|
|
944
|
+
function openProject(name) {
|
|
945
|
+
currentView = 'sessions';
|
|
946
|
+
searchQuery = name;
|
|
947
|
+
document.querySelector('.search-box').value = name;
|
|
948
|
+
document.querySelectorAll('.sidebar-item').forEach(function(el) {
|
|
949
|
+
el.classList.toggle('active', el.getAttribute('data-view') === 'sessions');
|
|
950
|
+
});
|
|
951
|
+
applyFilters();
|
|
952
|
+
}
|
|
953
|
+
|
|
901
954
|
// ── Themes ─────────────────────────────────────────────────────
|
|
902
955
|
|
|
903
956
|
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
|