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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codedash-app",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
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,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
 
@@ -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 + '\')">&#9733;</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
- content.innerHTML = sessions.map(function(s) { return renderCard(s, idx2++); }).join('');
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">&#9660;</span>';
417
465
  html += '</div>';
418
- html += '<div class="group-body">';
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 += renderCard(s, globalIdx++);
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="onSearch(\'' + escHtml(name) + '\');document.querySelector(\'.search-box\').value=\'' + escHtml(name) + '\'">';
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) {
@@ -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