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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codedash-app",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Termius-style browser dashboard for Claude Code sessions. View, search, resume, and delete sessions with a dark-themed UI.",
5
5
  "bin": {
6
6
  "codedash": "./bin/cli.js"
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
- const sid = d.sessionId || d.id;
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: d.timestamp || Date.now(),
32
- last_ts: d.timestamp || Date.now(),
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
 
@@ -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 + '\')">&#9733;</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
- content.innerHTML = sessions.map(function(s) { return renderCard(s, idx2++); }).join('');
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">&#9660;</span>';
417
457
  html += '</div>';
418
- html += '<div class="group-body">';
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 += renderCard(s, globalIdx++);
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="onSearch(\'' + escHtml(name) + '\');document.querySelector(\'.search-box\').value=\'' + escHtml(name) + '\'">';
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) {
@@ -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>
@@ -335,30 +335,16 @@ body {
335
335
  /* ── Card Checkbox (bulk select) ───────────────────────────────── */
336
336
 
337
337
  .card-checkbox {
338
- position: absolute;
339
- top: 10px;
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
- opacity: 0;
348
- transition: opacity 0.15s, background 0.15s;
349
- display: flex;
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
- .select-mode .card-checkbox {
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
- .replace('{{STYLES}}', styles)
14
- .replace('{{SCRIPT}}', script);
13
+ .split('{{STYLES}}').join(styles)
14
+ .split('{{SCRIPT}}').join(script);
15
15
  }
16
16
 
17
17
  // Cache in production