codedash-app 1.1.1 → 1.3.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/README.md +35 -24
- package/package.json +1 -1
- package/src/frontend/app.js +113 -26
- package/src/frontend/index.html +6 -0
- package/src/frontend/styles.css +46 -0
- package/src/server.js +38 -0
- package/src/terminals.js +1 -1
package/README.md
CHANGED
|
@@ -1,60 +1,71 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CodeDash
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Browser dashboard for Claude Code & Codex sessions. View, search, resume, and manage all your AI coding sessions.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
https://github.com/user-attachments/assets/15c45659-365b-49f8-86a3-9005fa155ca6
|
|
6
|
+
|
|
7
|
+
  
|
|
6
8
|
|
|
7
9
|
## Quick Start
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
|
-
npx
|
|
12
|
+
npx codedash-app run
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
Opens `http://localhost:3847`
|
|
14
|
-
|
|
15
|
-
Custom port:
|
|
15
|
+
Opens `http://localhost:3847` in your browser.
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npx
|
|
18
|
+
npx codedash-app run --port=4000 # custom port
|
|
19
|
+
npx codedash-app run --no-browser # don't auto-open
|
|
20
|
+
npx codedash-app list # list sessions in terminal
|
|
21
|
+
npx codedash-app stats # show statistics
|
|
19
22
|
```
|
|
20
23
|
|
|
21
24
|
## Features
|
|
22
25
|
|
|
23
26
|
**Sessions**
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
27
|
+
- Grid and List view with project grouping
|
|
28
|
+
- Trigram fuzzy search across session content and projects
|
|
29
|
+
- Filter by tool (Claude/Codex), tags, date range
|
|
30
|
+
- Star/pin important sessions (always shown first)
|
|
31
|
+
- Tag sessions: bug, feature, research, infra, deploy, review
|
|
32
|
+
- Activity heatmap (GitHub-style)
|
|
33
|
+
- Cost estimation per session
|
|
28
34
|
|
|
29
35
|
**Launch**
|
|
30
|
-
- Resume
|
|
31
|
-
- One-click launch with `--dangerously-skip-permissions` option
|
|
36
|
+
- Resume sessions in iTerm2, Terminal.app, Warp, Kitty, Alacritty
|
|
32
37
|
- Auto `cd` into the correct project directory
|
|
33
38
|
- Copy resume command to clipboard
|
|
34
39
|
- Terminal preference saved between sessions
|
|
35
40
|
|
|
36
41
|
**Manage**
|
|
37
42
|
- Delete sessions (file + history + env cleanup)
|
|
38
|
-
-
|
|
39
|
-
-
|
|
43
|
+
- Bulk select and delete
|
|
44
|
+
- Export conversations as Markdown
|
|
45
|
+
- Related git commits shown per session
|
|
46
|
+
- Auto-update notifications
|
|
47
|
+
|
|
48
|
+
**Themes**
|
|
49
|
+
- Dark (default), Light, System
|
|
40
50
|
|
|
41
51
|
**Keyboard Shortcuts**
|
|
42
|
-
- `/`
|
|
43
|
-
- `
|
|
52
|
+
- `/` focus search, `j/k` navigate, `Enter` open
|
|
53
|
+
- `x` star, `d` delete, `s` select mode, `g` toggle groups
|
|
54
|
+
- `r` refresh, `Escape` close panels
|
|
44
55
|
|
|
45
56
|
## How It Works
|
|
46
57
|
|
|
47
|
-
Reads session data from `~/.claude/`:
|
|
48
|
-
- `history.jsonl` — session index
|
|
49
|
-
- `projects
|
|
50
|
-
- `
|
|
58
|
+
Reads session data from `~/.claude/` and `~/.codex/`:
|
|
59
|
+
- `history.jsonl` — session index
|
|
60
|
+
- `projects/*/<session-id>.jsonl` — conversation data
|
|
61
|
+
- `sessions/` — Codex session files
|
|
51
62
|
|
|
52
|
-
Zero dependencies.
|
|
63
|
+
Zero dependencies. Everything runs on `localhost`.
|
|
53
64
|
|
|
54
65
|
## Requirements
|
|
55
66
|
|
|
56
67
|
- Node.js >= 16
|
|
57
|
-
- Claude Code
|
|
68
|
+
- Claude Code or Codex CLI installed
|
|
58
69
|
- macOS / Linux / Windows
|
|
59
70
|
|
|
60
71
|
## License
|
package/package.json
CHANGED
package/src/frontend/app.js
CHANGED
|
@@ -186,48 +186,103 @@ function saveTerminalPref(val) {
|
|
|
186
186
|
localStorage.setItem('codedash-terminal', val);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// ── Trigram search ─────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
function trigrams(str) {
|
|
192
|
+
var s = ' ' + str.toLowerCase() + ' ';
|
|
193
|
+
var t = {};
|
|
194
|
+
for (var i = 0; i < s.length - 2; i++) {
|
|
195
|
+
var tri = s.substring(i, i + 3);
|
|
196
|
+
t[tri] = (t[tri] || 0) + 1;
|
|
197
|
+
}
|
|
198
|
+
return t;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function trigramScore(query, text) {
|
|
202
|
+
if (!query || !text) return 0;
|
|
203
|
+
var qt = trigrams(query);
|
|
204
|
+
var tt = trigrams(text);
|
|
205
|
+
var matches = 0;
|
|
206
|
+
var total = 0;
|
|
207
|
+
for (var k in qt) {
|
|
208
|
+
total += qt[k];
|
|
209
|
+
if (tt[k]) matches += Math.min(qt[k], tt[k]);
|
|
210
|
+
}
|
|
211
|
+
return total > 0 ? matches / total : 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function searchScore(query, session) {
|
|
215
|
+
var q = query.toLowerCase();
|
|
216
|
+
var fields = [
|
|
217
|
+
session.first_message || '',
|
|
218
|
+
session.project_short || '',
|
|
219
|
+
session.project || '',
|
|
220
|
+
session.id || '',
|
|
221
|
+
session.tool || ''
|
|
222
|
+
];
|
|
223
|
+
var haystack = fields.join(' ').toLowerCase();
|
|
224
|
+
|
|
225
|
+
// Exact substring match = highest score
|
|
226
|
+
if (haystack.indexOf(q) >= 0) return 1;
|
|
227
|
+
|
|
228
|
+
// Trigram fuzzy match
|
|
229
|
+
var best = 0;
|
|
230
|
+
for (var i = 0; i < fields.length; i++) {
|
|
231
|
+
var score = trigramScore(q, fields[i]);
|
|
232
|
+
if (score > best) best = score;
|
|
233
|
+
}
|
|
234
|
+
// Also score against full haystack
|
|
235
|
+
var fullScore = trigramScore(q, haystack);
|
|
236
|
+
if (fullScore > best) best = fullScore;
|
|
237
|
+
|
|
238
|
+
return best;
|
|
239
|
+
}
|
|
240
|
+
|
|
189
241
|
// ── Filtering ──────────────────────────────────────────────────
|
|
190
242
|
|
|
243
|
+
var SEARCH_THRESHOLD = 0.3;
|
|
244
|
+
|
|
191
245
|
function applyFilters() {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
246
|
+
var scored = [];
|
|
247
|
+
for (var i = 0; i < allSessions.length; i++) {
|
|
248
|
+
var s = allSessions[i];
|
|
195
249
|
|
|
196
|
-
//
|
|
197
|
-
if (
|
|
198
|
-
var q = searchQuery.toLowerCase();
|
|
199
|
-
var haystack = (
|
|
200
|
-
(s.first_message || '') + ' ' +
|
|
201
|
-
(s.project || '') + ' ' +
|
|
202
|
-
(s.project_short || '') + ' ' +
|
|
203
|
-
(s.id || '') + ' ' +
|
|
204
|
-
(s.tool || '')
|
|
205
|
-
).toLowerCase();
|
|
206
|
-
if (haystack.indexOf(q) === -1) return false;
|
|
207
|
-
}
|
|
250
|
+
// Tool filter
|
|
251
|
+
if (toolFilter && s.tool !== toolFilter) continue;
|
|
208
252
|
|
|
209
253
|
// Tag filter
|
|
210
254
|
if (tagFilter) {
|
|
211
255
|
var sessionTags = tags[s.id] || [];
|
|
212
|
-
if (sessionTags.indexOf(tagFilter) === -1)
|
|
256
|
+
if (sessionTags.indexOf(tagFilter) === -1) continue;
|
|
213
257
|
}
|
|
214
258
|
|
|
215
259
|
// Date range
|
|
216
|
-
if (dateFrom && s.date < dateFrom)
|
|
217
|
-
if (dateTo && s.date > dateTo)
|
|
260
|
+
if (dateFrom && s.date < dateFrom) continue;
|
|
261
|
+
if (dateTo && s.date > dateTo) continue;
|
|
218
262
|
|
|
219
|
-
|
|
220
|
-
|
|
263
|
+
// Search with trigram scoring
|
|
264
|
+
var score = 1;
|
|
265
|
+
if (searchQuery) {
|
|
266
|
+
score = searchScore(searchQuery, s);
|
|
267
|
+
if (score < SEARCH_THRESHOLD) continue;
|
|
268
|
+
}
|
|
221
269
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
270
|
+
scored.push({ session: s, score: score });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Sort: starred first, then by search score (if searching), then by time
|
|
274
|
+
scored.sort(function(a, b) {
|
|
275
|
+
var aStarred = stars.indexOf(a.session.id) >= 0 ? 1 : 0;
|
|
276
|
+
var bStarred = stars.indexOf(b.session.id) >= 0 ? 1 : 0;
|
|
226
277
|
if (aStarred !== bStarred) return bStarred - aStarred;
|
|
227
|
-
return b.
|
|
278
|
+
if (searchQuery && a.score !== b.score) return b.score - a.score;
|
|
279
|
+
return b.session.last_ts - a.session.last_ts;
|
|
228
280
|
});
|
|
229
281
|
|
|
282
|
+
filteredSessions = scored.map(function(x) { return x.session; });
|
|
283
|
+
|
|
230
284
|
render();
|
|
285
|
+
|
|
231
286
|
}
|
|
232
287
|
|
|
233
288
|
function onSearch(val) {
|
|
@@ -829,7 +884,7 @@ function launchSession(sessionId, tool, project) {
|
|
|
829
884
|
|
|
830
885
|
function copyResume(sessionId, tool) {
|
|
831
886
|
var cmd = tool === 'codex'
|
|
832
|
-
? 'codex
|
|
887
|
+
? 'codex resume ' + sessionId
|
|
833
888
|
: 'claude --resume ' + sessionId;
|
|
834
889
|
navigator.clipboard.writeText(cmd).then(function() {
|
|
835
890
|
showToast('Copied: ' + cmd);
|
|
@@ -1099,12 +1154,44 @@ document.addEventListener('keydown', function(e) {
|
|
|
1099
1154
|
}
|
|
1100
1155
|
});
|
|
1101
1156
|
|
|
1157
|
+
// ── Update check ──────────────────────────────────────────────
|
|
1158
|
+
|
|
1159
|
+
async function checkForUpdates() {
|
|
1160
|
+
try {
|
|
1161
|
+
var resp = await fetch('/api/version');
|
|
1162
|
+
var data = await resp.json();
|
|
1163
|
+
if (data.updateAvailable) {
|
|
1164
|
+
var banner = document.getElementById('updateBanner');
|
|
1165
|
+
var text = document.getElementById('updateText');
|
|
1166
|
+
if (banner && text) {
|
|
1167
|
+
text.textContent = 'Update available: v' + data.current + ' → v' + data.latest;
|
|
1168
|
+
banner.style.display = 'flex';
|
|
1169
|
+
banner.dataset.cmd = 'npm update -g codedash-app && codedash run';
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
} catch {}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function copyUpdate() {
|
|
1176
|
+
var banner = document.getElementById('updateBanner');
|
|
1177
|
+
var cmd = banner ? banner.dataset.cmd : 'npm update -g codedash-app';
|
|
1178
|
+
navigator.clipboard.writeText(cmd).then(function() {
|
|
1179
|
+
showToast('Copied: ' + cmd);
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function dismissUpdate() {
|
|
1184
|
+
var banner = document.getElementById('updateBanner');
|
|
1185
|
+
if (banner) banner.style.display = 'none';
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1102
1188
|
// ── Initialization ─────────────────────────────────────────────
|
|
1103
1189
|
|
|
1104
1190
|
(function init() {
|
|
1105
1191
|
// Load data
|
|
1106
1192
|
loadSessions();
|
|
1107
1193
|
loadTerminals();
|
|
1194
|
+
checkForUpdates();
|
|
1108
1195
|
|
|
1109
1196
|
// Apply saved theme
|
|
1110
1197
|
var savedTheme = localStorage.getItem('codedash-theme') || 'dark';
|
package/src/frontend/index.html
CHANGED
|
@@ -111,6 +111,12 @@
|
|
|
111
111
|
|
|
112
112
|
<div class="toast" id="toast"></div>
|
|
113
113
|
|
|
114
|
+
<div class="update-banner" id="updateBanner" style="display:none">
|
|
115
|
+
<span id="updateText"></span>
|
|
116
|
+
<button class="update-btn" onclick="copyUpdate()">Copy update command</button>
|
|
117
|
+
<button class="update-dismiss" onclick="dismissUpdate()">×</button>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
114
120
|
<script>{{SCRIPT}}</script>
|
|
115
121
|
</body>
|
|
116
122
|
</html>
|
package/src/frontend/styles.css
CHANGED
|
@@ -1370,6 +1370,52 @@ body {
|
|
|
1370
1370
|
color: #fff;
|
|
1371
1371
|
}
|
|
1372
1372
|
|
|
1373
|
+
/* ── Update banner ──────────────────────────────────────────── */
|
|
1374
|
+
|
|
1375
|
+
.update-banner {
|
|
1376
|
+
position: fixed;
|
|
1377
|
+
top: 0;
|
|
1378
|
+
left: 200px;
|
|
1379
|
+
right: 0;
|
|
1380
|
+
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
|
|
1381
|
+
color: #fff;
|
|
1382
|
+
padding: 10px 20px;
|
|
1383
|
+
display: flex;
|
|
1384
|
+
align-items: center;
|
|
1385
|
+
gap: 12px;
|
|
1386
|
+
font-size: 13px;
|
|
1387
|
+
z-index: 200;
|
|
1388
|
+
animation: slideDown 0.3s ease;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
@keyframes slideDown {
|
|
1392
|
+
from { transform: translateY(-100%); }
|
|
1393
|
+
to { transform: translateY(0); }
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
.update-btn {
|
|
1397
|
+
background: rgba(255,255,255,0.2);
|
|
1398
|
+
border: 1px solid rgba(255,255,255,0.3);
|
|
1399
|
+
color: #fff;
|
|
1400
|
+
padding: 4px 12px;
|
|
1401
|
+
border-radius: 6px;
|
|
1402
|
+
font-size: 12px;
|
|
1403
|
+
cursor: pointer;
|
|
1404
|
+
white-space: nowrap;
|
|
1405
|
+
}
|
|
1406
|
+
.update-btn:hover { background: rgba(255,255,255,0.3); }
|
|
1407
|
+
|
|
1408
|
+
.update-dismiss {
|
|
1409
|
+
background: none;
|
|
1410
|
+
border: none;
|
|
1411
|
+
color: rgba(255,255,255,0.7);
|
|
1412
|
+
font-size: 18px;
|
|
1413
|
+
cursor: pointer;
|
|
1414
|
+
margin-left: auto;
|
|
1415
|
+
padding: 0 4px;
|
|
1416
|
+
}
|
|
1417
|
+
.update-dismiss:hover { color: #fff; }
|
|
1418
|
+
|
|
1373
1419
|
/* ── List view ──────────────────────────────────────────────── */
|
|
1374
1420
|
|
|
1375
1421
|
.list-view {
|
package/src/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// HTTP server + API routes
|
|
2
2
|
const http = require('http');
|
|
3
|
+
const https = require('https');
|
|
3
4
|
const { URL } = require('url');
|
|
4
5
|
const { exec } = require('child_process');
|
|
5
6
|
const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown } = require('./data');
|
|
@@ -103,6 +104,18 @@ function startServer(port, openBrowser = true) {
|
|
|
103
104
|
json(res, commits);
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
// ── Version check ────────────────────────
|
|
108
|
+
else if (req.method === 'GET' && pathname === '/api/version') {
|
|
109
|
+
const pkg = require('../package.json');
|
|
110
|
+
const current = pkg.version;
|
|
111
|
+
// Fetch latest from npm registry
|
|
112
|
+
fetchLatestVersion(pkg.name).then(latest => {
|
|
113
|
+
json(res, { current, latest, updateAvailable: latest && latest !== current && isNewer(latest, current) });
|
|
114
|
+
}).catch(() => {
|
|
115
|
+
json(res, { current, latest: null, updateAvailable: false });
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
106
119
|
// ── 404 ─────────────────────────────────
|
|
107
120
|
else {
|
|
108
121
|
res.writeHead(404);
|
|
@@ -139,4 +152,29 @@ function readBody(req, cb) {
|
|
|
139
152
|
req.on('end', () => cb(body));
|
|
140
153
|
}
|
|
141
154
|
|
|
155
|
+
// ── npm version check ───────────────────
|
|
156
|
+
function fetchLatestVersion(packageName) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
https.get(`https://registry.npmjs.org/${packageName}/latest`, { timeout: 5000 }, (res) => {
|
|
159
|
+
let data = '';
|
|
160
|
+
res.on('data', chunk => data += chunk);
|
|
161
|
+
res.on('end', () => {
|
|
162
|
+
try {
|
|
163
|
+
resolve(JSON.parse(data).version);
|
|
164
|
+
} catch { reject(); }
|
|
165
|
+
});
|
|
166
|
+
}).on('error', reject);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isNewer(latest, current) {
|
|
171
|
+
const l = latest.split('.').map(Number);
|
|
172
|
+
const c = current.split('.').map(Number);
|
|
173
|
+
for (let i = 0; i < 3; i++) {
|
|
174
|
+
if ((l[i] || 0) > (c[i] || 0)) return true;
|
|
175
|
+
if ((l[i] || 0) < (c[i] || 0)) return false;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
142
180
|
module.exports = { startServer };
|
package/src/terminals.js
CHANGED
|
@@ -70,7 +70,7 @@ function openInTerminal(sessionId, tool, flags, projectDir, terminalId) {
|
|
|
70
70
|
let cmd;
|
|
71
71
|
|
|
72
72
|
if (tool === 'codex') {
|
|
73
|
-
cmd = `codex
|
|
73
|
+
cmd = `codex resume ${sessionId}`;
|
|
74
74
|
} else {
|
|
75
75
|
cmd = `claude --resume ${sessionId}`;
|
|
76
76
|
if (skipPerms) cmd += ' --dangerously-skip-permissions';
|