codedash-app 1.5.0 → 1.6.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.5.0",
3
+ "version": "1.6.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
@@ -157,7 +157,7 @@ function loadSessions() {
157
157
  // Enrich Claude sessions with detail file info
158
158
  for (const [sid, s] of Object.entries(sessions)) {
159
159
  if (s.tool !== 'claude') continue;
160
- const projectKey = s.project.replace(/\//g, '-').replace(/^-/, '');
160
+ const projectKey = s.project.replace(/[\/\.]/g, '-');
161
161
  const sessionFile = path.join(PROJECTS_DIR, projectKey, `${sid}.jsonl`);
162
162
  if (fs.existsSync(sessionFile)) {
163
163
  s.has_detail = true;
@@ -192,33 +192,33 @@ function loadSessions() {
192
192
  }
193
193
 
194
194
  function loadSessionDetail(sessionId, project) {
195
- const projectKey = project.replace(/\//g, '-').replace(/^-/, '');
196
- const sessionFile = path.join(PROJECTS_DIR, projectKey, `${sessionId}.jsonl`);
197
-
198
- if (!fs.existsSync(sessionFile)) {
199
- return { error: 'Session file not found', messages: [] };
200
- }
195
+ const found = findSessionFile(sessionId, project);
196
+ if (!found) return { error: 'Session file not found', messages: [] };
201
197
 
202
198
  const messages = [];
203
- const lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
199
+ const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
204
200
 
205
201
  for (const line of lines) {
206
202
  try {
207
203
  const entry = JSON.parse(line);
208
- if (entry.type === 'user' || entry.type === 'assistant') {
209
- const msg = entry.message || {};
210
- let content = msg.content || '';
211
- if (Array.isArray(content)) {
212
- content = content
213
- .map(b => (typeof b === 'string' ? b : (b.type === 'text' ? b.text : '')))
214
- .filter(Boolean)
215
- .join('\n');
204
+
205
+ if (found.format === 'claude') {
206
+ if (entry.type === 'user' || entry.type === 'assistant') {
207
+ const content = extractContent((entry.message || {}).content);
208
+ if (content) {
209
+ messages.push({ role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' });
210
+ }
211
+ }
212
+ } else {
213
+ if (entry.type === 'response_item' && entry.payload) {
214
+ const role = entry.payload.role;
215
+ if (role === 'user' || role === 'assistant') {
216
+ const content = extractContent(entry.payload.content);
217
+ if (content && !content.startsWith('<permissions') && !content.startsWith('<environment_context') && !content.startsWith('<collaboration_mode')) {
218
+ messages.push({ role: role, content: content.slice(0, 2000), uuid: '' });
219
+ }
220
+ }
216
221
  }
217
- messages.push({
218
- role: entry.type,
219
- content: content.slice(0, 2000),
220
- uuid: entry.uuid || '',
221
- });
222
222
  }
223
223
  } catch {}
224
224
  }
@@ -230,7 +230,7 @@ function deleteSession(sessionId, project) {
230
230
  const deleted = [];
231
231
 
232
232
  // 1. Remove session JSONL file from project dir
233
- const projectKey = project.replace(/\//g, '-').replace(/^-/, '');
233
+ const projectKey = project.replace(/[\/\.]/g, '-');
234
234
  const sessionFile = path.join(PROJECTS_DIR, projectKey, `${sessionId}.jsonl`);
235
235
  if (fs.existsSync(sessionFile)) {
236
236
  fs.unlinkSync(sessionFile);
@@ -299,7 +299,7 @@ function getGitCommits(projectDir, fromTs, toTs) {
299
299
  }
300
300
 
301
301
  function exportSessionMarkdown(sessionId, project) {
302
- const projectKey = project.replace(/\//g, '-').replace(/^-/, '');
302
+ const projectKey = project.replace(/[\/\.]/g, '-');
303
303
  const sessionFile = path.join(PROJECTS_DIR, projectKey, `${sessionId}.jsonl`);
304
304
 
305
305
  if (!fs.existsSync(sessionFile)) {
@@ -332,33 +332,89 @@ function exportSessionMarkdown(sessionId, project) {
332
332
 
333
333
  // ── Session Preview (first N messages, lightweight) ────────
334
334
 
335
+ function findSessionFile(sessionId, project) {
336
+ // Try Claude projects dir
337
+ if (project) {
338
+ const projectKey = project.replace(/[\/\.]/g, '-');
339
+ const claudeFile = path.join(PROJECTS_DIR, projectKey, `${sessionId}.jsonl`);
340
+ if (fs.existsSync(claudeFile)) return { file: claudeFile, format: 'claude' };
341
+ }
342
+
343
+ // Try all Claude project dirs
344
+ if (fs.existsSync(PROJECTS_DIR)) {
345
+ for (const proj of fs.readdirSync(PROJECTS_DIR)) {
346
+ const f = path.join(PROJECTS_DIR, proj, `${sessionId}.jsonl`);
347
+ if (fs.existsSync(f)) return { file: f, format: 'claude' };
348
+ }
349
+ }
350
+
351
+ // Try Codex sessions dir (walk year/month/day)
352
+ const codexSessionsDir = path.join(CODEX_DIR, 'sessions');
353
+ if (fs.existsSync(codexSessionsDir)) {
354
+ const walkDir = (dir) => {
355
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
356
+ const full = path.join(dir, entry.name);
357
+ if (entry.isDirectory()) {
358
+ const result = walkDir(full);
359
+ if (result) return result;
360
+ } else if (entry.name.includes(sessionId) && entry.name.endsWith('.jsonl')) {
361
+ return full;
362
+ }
363
+ }
364
+ return null;
365
+ };
366
+ const codexFile = walkDir(codexSessionsDir);
367
+ if (codexFile) return { file: codexFile, format: 'codex' };
368
+ }
369
+
370
+ return null;
371
+ }
372
+
373
+ function extractContent(raw) {
374
+ if (!raw) return '';
375
+ if (typeof raw === 'string') return raw;
376
+ if (Array.isArray(raw)) {
377
+ return raw
378
+ .map(b => (typeof b === 'string' ? b : (b.text || b.input_text || '')))
379
+ .filter(Boolean)
380
+ .join('\n');
381
+ }
382
+ return String(raw);
383
+ }
384
+
335
385
  function getSessionPreview(sessionId, project, limit) {
336
386
  limit = limit || 10;
337
- const projectKey = project.replace(/\//g, '-').replace(/^-/, '');
338
- const sessionFile = path.join(PROJECTS_DIR, projectKey, `${sessionId}.jsonl`);
339
-
340
- if (!fs.existsSync(sessionFile)) return [];
387
+ const found = findSessionFile(sessionId, project);
388
+ if (!found) return [];
341
389
 
342
390
  const messages = [];
343
- const lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
391
+ const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
344
392
 
345
393
  for (const line of lines) {
346
394
  if (messages.length >= limit) break;
347
395
  try {
348
396
  const entry = JSON.parse(line);
349
- if (entry.type === 'user' || entry.type === 'assistant') {
350
- const msg = entry.message || {};
351
- let content = msg.content || '';
352
- if (Array.isArray(content)) {
353
- content = content
354
- .map(b => (typeof b === 'string' ? b : (b.type === 'text' ? b.text : '')))
355
- .filter(Boolean)
356
- .join('\n');
397
+
398
+ if (found.format === 'claude') {
399
+ // Claude: {type: "user"|"assistant", message: {content: ...}}
400
+ if (entry.type === 'user' || entry.type === 'assistant') {
401
+ const content = extractContent((entry.message || {}).content);
402
+ if (content) {
403
+ messages.push({ role: entry.type, content: content.slice(0, 300) });
404
+ }
405
+ }
406
+ } else {
407
+ // Codex: {type: "response_item", payload: {role: "user"|"assistant", content: [...]}}
408
+ if (entry.type === 'response_item' && entry.payload) {
409
+ const role = entry.payload.role;
410
+ if (role === 'user' || role === 'assistant') {
411
+ const content = extractContent(entry.payload.content);
412
+ // Skip system-like messages
413
+ if (content && !content.startsWith('<permissions') && !content.startsWith('<environment_context') && !content.startsWith('<collaboration_mode')) {
414
+ messages.push({ role: role, content: content.slice(0, 300) });
415
+ }
416
+ }
357
417
  }
358
- messages.push({
359
- role: entry.type,
360
- content: content.slice(0, 300), // short preview
361
- });
362
418
  }
363
419
  } catch {}
364
420
  }
@@ -376,7 +432,7 @@ function searchFullText(query, sessions) {
376
432
  for (const s of sessions) {
377
433
  if (s.tool !== 'claude' || !s.has_detail) continue;
378
434
 
379
- const projectKey = s.project.replace(/\//g, '-').replace(/^-/, '');
435
+ const projectKey = s.project.replace(/[\/\.]/g, '-');
380
436
  const sessionFile = path.join(PROJECTS_DIR, projectKey, `${s.id}.jsonl`);
381
437
  if (!fs.existsSync(sessionFile)) continue;
382
438
 
@@ -1344,13 +1344,29 @@ async function checkForUpdates() {
1344
1344
  try {
1345
1345
  var resp = await fetch('/api/version');
1346
1346
  var data = await resp.json();
1347
+ var badge = document.getElementById('versionBadge');
1348
+
1349
+ if (badge) {
1350
+ badge.textContent = 'v' + data.current;
1351
+ }
1352
+
1347
1353
  if (data.updateAvailable) {
1354
+ if (badge) {
1355
+ badge.textContent = 'v' + data.current + ' → v' + data.latest;
1356
+ badge.classList.add('update-available');
1357
+ badge.title = 'Click to copy update command';
1358
+ badge.onclick = function() {
1359
+ navigator.clipboard.writeText('npm i -g codedash-app@latest').then(function() {
1360
+ showToast('Copied: npm i -g codedash-app@latest');
1361
+ });
1362
+ };
1363
+ }
1348
1364
  var banner = document.getElementById('updateBanner');
1349
1365
  var text = document.getElementById('updateText');
1350
1366
  if (banner && text) {
1351
- text.textContent = 'Update available: v' + data.current + ' v' + data.latest;
1367
+ text.textContent = 'v' + data.latest + ' available run: npm i -g codedash-app@latest';
1352
1368
  banner.style.display = 'flex';
1353
- banner.dataset.cmd = 'npm update -g codedash-app && codedash run';
1369
+ banner.dataset.cmd = 'npm i -g codedash-app@latest';
1354
1370
  }
1355
1371
  }
1356
1372
  } catch {}
@@ -12,6 +12,7 @@
12
12
  <div class="sidebar-brand">
13
13
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
14
14
  codedash
15
+ <span class="version-badge" id="versionBadge"></span>
15
16
  </div>
16
17
  <div class="sidebar-item active" data-view="sessions">
17
18
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
@@ -100,6 +100,26 @@ body {
100
100
  display: flex;
101
101
  align-items: center;
102
102
  gap: 8px;
103
+ flex-wrap: wrap;
104
+ }
105
+
106
+ .version-badge {
107
+ font-size: 10px;
108
+ font-weight: 500;
109
+ color: var(--text-muted);
110
+ background: rgba(255,255,255,0.06);
111
+ padding: 1px 6px;
112
+ border-radius: 4px;
113
+ }
114
+
115
+ .version-badge.update-available {
116
+ color: var(--accent-green);
117
+ background: rgba(74, 222, 128, 0.15);
118
+ cursor: pointer;
119
+ }
120
+
121
+ .version-badge.update-available:hover {
122
+ background: rgba(74, 222, 128, 0.25);
103
123
  }
104
124
 
105
125
  .sidebar-item {