Skip to content

Commit 7690dc2

Browse files
vakovalskiiclaude
andcommitted
v1.1.0: Grid/List toggle, Codex support fix, project navigation
- Fix Codex history parsing (session_id/ts/text format) - Scan ~/.codex/sessions/ for session files - Add Grid/List layout toggle (persisted in localStorage) - Fix checkbox positioning inside cards - Add openProject() to navigate from Projects view to filtered sessions - Fix html.js template injection (split/join instead of replace) - Add list view CSS styles Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9d22f5f commit 7690dc2

6 files changed

Lines changed: 202 additions & 34 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codedash-app",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "Termius-style browser dashboard for Claude Code sessions. View, search, resume, and delete sessions with a dark-themed UI.",
55
"bin": {
66
"codedash": "./bin/cli.js"

src/data.js

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,20 @@ function scanCodexSessions() {
2020
for (const line of lines) {
2121
try {
2222
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;
2425
if (!sid) continue;
26+
const ts = d.ts ? d.ts * 1000 : (d.timestamp || Date.now());
2527
if (!sessions.find(s => s.id === sid)) {
2628
sessions.push({
2729
id: sid,
2830
tool: 'codex',
2931
project: d.project || d.cwd || '',
3032
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,
3335
messages: 1,
34-
first_message: d.display || d.prompt || '',
36+
first_message: d.text || d.display || d.prompt || '',
3537
has_detail: false,
3638
file_size: 0,
3739
detail_messages: 0,
@@ -40,6 +42,52 @@ function scanCodexSessions() {
4042
} catch {}
4143
}
4244
}
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+
4391
return sessions;
4492
}
4593

src/frontend/app.js

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ let allSessions = [];
77
let filteredSessions = [];
88
let currentView = 'sessions'; // sessions, projects, timeline, activity, starred
99
let grouped = true;
10+
let layout = localStorage.getItem('codedash-layout') || 'grid'; // 'grid' or 'list'
1011
let searchQuery = '';
1112
let toolFilter = null; // null, 'claude', 'codex'
1213
let tagFilter = '';
@@ -325,6 +326,42 @@ function renderCard(s, idx) {
325326
return html;
326327
}
327328

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+
328365
function onCardClick(id, event) {
329366
if (selectMode) {
330367
toggleSelect(id, event);
@@ -384,15 +421,18 @@ function render() {
384421
return;
385422
}
386423

424+
var renderFn = layout === 'list' ? renderListCard : renderCard;
387425
if (grouped) {
388-
renderGrouped(content, sessions);
426+
renderGrouped(content, sessions, renderFn);
389427
} else {
390428
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>';
392431
}
393432
}
394433

395-
function renderGrouped(container, sessions) {
434+
function renderGrouped(container, sessions, renderFn) {
435+
renderFn = renderFn || renderCard;
396436
var groups = {};
397437
sessions.forEach(function(s) {
398438
var key = getProjectName(s.project);
@@ -415,9 +455,10 @@ function renderGrouped(container, sessions) {
415455
html += '<span class="group-count">' + groups[key].length + '</span>';
416456
html += '<span class="group-chevron">&#9660;</span>';
417457
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 + '">';
419460
groups[key].forEach(function(s) {
420-
html += renderCard(s, globalIdx++);
461+
html += renderFn(s, globalIdx++);
421462
});
422463
html += '</div></div>';
423464
});
@@ -479,7 +520,7 @@ function renderProjects(container, sessions) {
479520
var totalSize = info.sessions.reduce(function(sum, s) { return sum + (s.file_size || 0); }, 0);
480521
var latest = info.sessions[0];
481522

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, "\\'") + '\')">';
483524
html += '<div class="project-card-header">';
484525
html += '<span class="group-dot" style="background:' + color + '"></span>';
485526
html += '<span class="project-card-name">' + escHtml(name) + '</span>';
@@ -898,6 +939,18 @@ async function bulkDelete() {
898939
}
899940
}
900941

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+
901954
// ── Themes ─────────────────────────────────────────────────────
902955

903956
function setTheme(theme) {

src/frontend/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@
7272
<input type="date" class="date-input" id="dateFrom" onchange="onDateFilter()" title="From date">
7373
<input type="date" class="date-input" id="dateTo" onchange="onDateFilter()" title="To date">
7474
<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>
7578
<button class="toolbar-btn" onclick="toggleSelectMode()" id="selectBtn">Select</button>
7679
<button class="toolbar-btn" onclick="refreshData()">Refresh</button>
7780
<span class="stats" id="stats"></span>

src/frontend/styles.css

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -335,30 +335,16 @@ body {
335335
/* ── Card Checkbox (bulk select) ───────────────────────────────── */
336336

337337
.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;
346340
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;
353344
}
354345
.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;
362348
}
363349

364350
/* ── Card Actions ──────────────────────────────────────────────── */
@@ -1383,3 +1369,81 @@ body {
13831369
background: var(--accent-red);
13841370
color: #fff;
13851371
}
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+
}

src/html.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ function buildHTML() {
1010
const script = fs.readFileSync(path.join(FRONTEND_DIR, 'app.js'), 'utf8');
1111

1212
return template
13-
.replace('{{STYLES}}', styles)
14-
.replace('{{SCRIPT}}', script);
13+
.split('{{STYLES}}').join(styles)
14+
.split('{{SCRIPT}}').join(script);
1515
}
1616

1717
// Cache in production

0 commit comments

Comments
 (0)