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 CHANGED
@@ -1,60 +1,71 @@
1
- # claude-sessions-dash
1
+ # CodeDash
2
2
 
3
- Termius-style browser dashboard for your Claude Code (and Codex) sessions.
3
+ Browser dashboard for Claude Code & Codex sessions. View, search, resume, and manage all your AI coding sessions.
4
4
 
5
- ![Dashboard](https://img.shields.io/badge/UI-Dark%20Theme-1a1d23?style=flat-square) ![Node](https://img.shields.io/badge/node-%3E%3D16-green?style=flat-square) ![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)
5
+ https://github.com/user-attachments/assets/15c45659-365b-49f8-86a3-9005fa155ca6
6
+
7
+ ![npm](https://img.shields.io/npm/v/codedash-app?style=flat-square) ![Node](https://img.shields.io/badge/node-%3E%3D16-green?style=flat-square) ![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)
6
8
 
7
9
  ## Quick Start
8
10
 
9
11
  ```bash
10
- npx claude-sessions-dash
12
+ npx codedash-app run
11
13
  ```
12
14
 
13
- Opens `http://localhost:3847` with your sessions dashboard.
14
-
15
- Custom port:
15
+ Opens `http://localhost:3847` in your browser.
16
16
 
17
17
  ```bash
18
- npx claude-sessions-dash 4000
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
- - View all Claude Code and Codex sessions in a card grid
25
- - Group by project, view as timeline, or filter by tool
26
- - Full-text search across session names and projects
27
- - Preview conversation history in a side panel
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 any session directly in your terminal (iTerm2, Terminal.app, Warp, Kitty, Alacritty)
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
- - Confirmation dialog to prevent accidents
39
- - Refresh data without restarting
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
- - `/` Focus search
43
- - `Escape` Close panels
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 with timestamps and projects
49
- - `projects/*/\<session-id\>.jsonl` — full conversation data
50
- - `session-env/` — session environment files
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. Single Node.js file. Everything runs on `localhost`.
63
+ Zero dependencies. Everything runs on `localhost`.
53
64
 
54
65
  ## Requirements
55
66
 
56
67
  - Node.js >= 16
57
- - Claude Code installed (`~/.claude/` directory exists)
68
+ - Claude Code or Codex CLI installed
58
69
  - macOS / Linux / Windows
59
70
 
60
71
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codedash-app",
3
- "version": "1.1.1",
3
+ "version": "1.3.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"
@@ -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
- filteredSessions = allSessions.filter(function(s) {
193
- // Tool filter
194
- if (toolFilter && s.tool !== toolFilter) return false;
246
+ var scored = [];
247
+ for (var i = 0; i < allSessions.length; i++) {
248
+ var s = allSessions[i];
195
249
 
196
- // Search
197
- if (searchQuery) {
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) return false;
256
+ if (sessionTags.indexOf(tagFilter) === -1) continue;
213
257
  }
214
258
 
215
259
  // Date range
216
- if (dateFrom && s.date < dateFrom) return false;
217
- if (dateTo && s.date > dateTo) return false;
260
+ if (dateFrom && s.date < dateFrom) continue;
261
+ if (dateTo && s.date > dateTo) continue;
218
262
 
219
- return true;
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
- // 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;
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.last_ts - a.last_ts;
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 --resume ' + sessionId
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';
@@ -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()">&times;</button>
118
+ </div>
119
+
114
120
  <script>{{SCRIPT}}</script>
115
121
  </body>
116
122
  </html>
@@ -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 --resume ${sessionId}`;
73
+ cmd = `codex resume ${sessionId}`;
74
74
  } else {
75
75
  cmd = `claude --resume ${sessionId}`;
76
76
  if (skipPerms) cmd += ' --dangerously-skip-permissions';