codedash-app 1.6.0 → 1.9.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/bin/cli.js CHANGED
@@ -57,6 +57,53 @@ switch (command) {
57
57
  break;
58
58
  }
59
59
 
60
+ case 'update':
61
+ case 'upgrade': {
62
+ const { execSync: execU } = require('child_process');
63
+ console.log('\n \x1b[36m\x1b[1mUpdating codedash-app...\x1b[0m\n');
64
+ try {
65
+ execU('npm i -g codedash-app@latest', { stdio: 'inherit' });
66
+ const newPkg = require('../package.json');
67
+ console.log(`\n \x1b[32mUpdated to v${newPkg.version}!\x1b[0m`);
68
+ console.log(' Run \x1b[2mcodedash restart\x1b[0m to apply.\n');
69
+ } catch (e) {
70
+ console.error(' \x1b[31mUpdate failed.\x1b[0m Try: npm i -g codedash-app@latest\n');
71
+ }
72
+ break;
73
+ }
74
+
75
+ case 'restart': {
76
+ const { execSync } = require('child_process');
77
+ const portArg = args.find(a => a.startsWith('--port='));
78
+ const port = portArg ? parseInt(portArg.split('=')[1]) : DEFAULT_PORT;
79
+ console.log(`\n Stopping codedash on port ${port}...`);
80
+ try {
81
+ execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
82
+ console.log(' Stopped.');
83
+ } catch {
84
+ console.log(' No running instance found.');
85
+ }
86
+ setTimeout(() => {
87
+ console.log(' Starting...\n');
88
+ const noBrowser = args.includes('--no-browser');
89
+ startServer(port, !noBrowser);
90
+ }, 500);
91
+ break;
92
+ }
93
+
94
+ case 'stop': {
95
+ const { execSync: execS } = require('child_process');
96
+ const pArg = args.find(a => a.startsWith('--port='));
97
+ const p = pArg ? parseInt(pArg.split('=')[1]) : DEFAULT_PORT;
98
+ try {
99
+ execS(`lsof -ti:${p} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
100
+ console.log(`\n codedash stopped (port ${p})\n`);
101
+ } catch {
102
+ console.log(`\n No codedash running on port ${p}\n`);
103
+ }
104
+ break;
105
+ }
106
+
60
107
  case 'export': {
61
108
  const outPath = args[1] || `codedash-export-${new Date().toISOString().slice(0,10)}.tar.gz`;
62
109
  exportArchive(outPath);
@@ -90,6 +137,9 @@ switch (command) {
90
137
 
91
138
  \x1b[1mUsage:\x1b[0m
92
139
  codedash run [port] [--no-browser] Start the dashboard server
140
+ codedash update Update to latest version
141
+ codedash restart [--port=N] Restart the server
142
+ codedash stop [--port=N] Stop the server
93
143
  codedash list [limit] List sessions in terminal
94
144
  codedash stats Show session statistics
95
145
  codedash export [file.tar.gz] Export all sessions to archive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codedash-app",
3
- "version": "1.6.0",
3
+ "version": "1.9.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
@@ -214,7 +214,7 @@ function loadSessionDetail(sessionId, project) {
214
214
  const role = entry.payload.role;
215
215
  if (role === 'user' || role === 'assistant') {
216
216
  const content = extractContent(entry.payload.content);
217
- if (content && !content.startsWith('<permissions') && !content.startsWith('<environment_context') && !content.startsWith('<collaboration_mode')) {
217
+ if (content && !isSystemMessage(content)) {
218
218
  messages.push({ role: role, content: content.slice(0, 2000), uuid: '' });
219
219
  }
220
220
  }
@@ -370,6 +370,21 @@ function findSessionFile(sessionId, project) {
370
370
  return null;
371
371
  }
372
372
 
373
+ function isSystemMessage(text) {
374
+ if (!text) return true;
375
+ var t = text.trim();
376
+ if (t === 'exit' || t === 'quit' || t === '/exit') return true;
377
+ if (t.startsWith('<permissions')) return true;
378
+ if (t.startsWith('<environment_context')) return true;
379
+ if (t.startsWith('<collaboration_mode')) return true;
380
+ if (t.startsWith('# AGENTS.md')) return true;
381
+ if (t.startsWith('<INSTRUCTIONS>')) return true;
382
+ // Codex developer role system prompts
383
+ if (t.startsWith('You are Codex')) return true;
384
+ if (t.startsWith('Filesystem sandboxing')) return true;
385
+ return false;
386
+ }
387
+
373
388
  function extractContent(raw) {
374
389
  if (!raw) return '';
375
390
  if (typeof raw === 'string') return raw;
@@ -410,7 +425,7 @@ function getSessionPreview(sessionId, project, limit) {
410
425
  if (role === 'user' || role === 'assistant') {
411
426
  const content = extractContent(entry.payload.content);
412
427
  // Skip system-like messages
413
- if (content && !content.startsWith('<permissions') && !content.startsWith('<environment_context') && !content.startsWith('<collaboration_mode')) {
428
+ if (content && !isSystemMessage(content)) {
414
429
  messages.push({ role: role, content: content.slice(0, 300) });
415
430
  }
416
431
  }
@@ -422,60 +437,103 @@ function getSessionPreview(sessionId, project, limit) {
422
437
  return messages;
423
438
  }
424
439
 
425
- // ── Full-text search across all sessions ──────────────────
440
+ // ── Full-text search index ─────────────────────────────────
441
+ //
442
+ // Built once on first search, then cached in memory.
443
+ // Each entry: { sessionId, texts: [{role, content}] }
444
+ // Total text is kept lowercase for fast substring matching.
426
445
 
427
- function searchFullText(query, sessions) {
428
- if (!query || query.length < 2) return [];
429
- const q = query.toLowerCase();
430
- const results = [];
446
+ let searchIndex = null;
447
+ let searchIndexBuiltAt = 0;
448
+ const INDEX_TTL = 60000; // rebuild every 60s
449
+
450
+ function buildSearchIndex(sessions) {
451
+ const startMs = Date.now();
452
+ const index = [];
431
453
 
432
454
  for (const s of sessions) {
433
- if (s.tool !== 'claude' || !s.has_detail) continue;
455
+ if (!s.has_detail) continue;
434
456
 
435
- const projectKey = s.project.replace(/[\/\.]/g, '-');
436
- const sessionFile = path.join(PROJECTS_DIR, projectKey, `${s.id}.jsonl`);
437
- if (!fs.existsSync(sessionFile)) continue;
457
+ const found = findSessionFile(s.id, s.project);
458
+ if (!found) continue;
438
459
 
439
460
  try {
440
- const data = fs.readFileSync(sessionFile, 'utf8');
441
- // Quick check before parsing
442
- if (data.toLowerCase().indexOf(q) === -1) continue;
461
+ const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
462
+ const texts = [];
443
463
 
444
- // Find matching messages
445
- const lines = data.split('\n').filter(Boolean);
446
- const matches = [];
447
464
  for (const line of lines) {
448
- if (matches.length >= 3) break; // max 3 matches per session
449
465
  try {
450
466
  const entry = JSON.parse(line);
451
- if (entry.type !== 'user' && entry.type !== 'assistant') continue;
452
- const msg = entry.message || {};
453
- let content = msg.content || '';
454
- if (Array.isArray(content)) {
455
- content = content
456
- .map(b => (typeof b === 'string' ? b : (b.type === 'text' ? b.text : '')))
457
- .filter(Boolean)
458
- .join('\n');
467
+ let role, content;
468
+
469
+ if (found.format === 'claude') {
470
+ if (entry.type !== 'user' && entry.type !== 'assistant') continue;
471
+ role = entry.type;
472
+ content = extractContent((entry.message || {}).content);
473
+ } else {
474
+ if (entry.type !== 'response_item' || !entry.payload) continue;
475
+ role = entry.payload.role;
476
+ if (role !== 'user' && role !== 'assistant') continue;
477
+ content = extractContent(entry.payload.content);
459
478
  }
460
- if (content.toLowerCase().indexOf(q) >= 0) {
461
- // Extract snippet around match
462
- const idx = content.toLowerCase().indexOf(q);
463
- const start = Math.max(0, idx - 50);
464
- const end = Math.min(content.length, idx + q.length + 50);
465
- matches.push({
466
- role: entry.type,
467
- snippet: (start > 0 ? '...' : '') + content.slice(start, end) + (end < content.length ? '...' : ''),
468
- });
479
+
480
+ if (content && !isSystemMessage(content)) {
481
+ texts.push({ role, content: content.slice(0, 500) });
469
482
  }
470
483
  } catch {}
471
484
  }
472
485
 
473
- if (matches.length > 0) {
474
- results.push({ sessionId: s.id, matches });
486
+ if (texts.length > 0) {
487
+ // Pre-compute lowercase full text for fast matching
488
+ const fullText = texts.map(t => t.content).join(' ').toLowerCase();
489
+ index.push({ sessionId: s.id, texts, fullText });
475
490
  }
476
491
  } catch {}
477
492
  }
478
493
 
494
+ const elapsed = Date.now() - startMs;
495
+ console.log(` \x1b[2mSearch index: ${index.length} sessions, ${elapsed}ms\x1b[0m`);
496
+ return index;
497
+ }
498
+
499
+ function getSearchIndex(sessions) {
500
+ const now = Date.now();
501
+ if (!searchIndex || (now - searchIndexBuiltAt) > INDEX_TTL) {
502
+ searchIndex = buildSearchIndex(sessions);
503
+ searchIndexBuiltAt = now;
504
+ }
505
+ return searchIndex;
506
+ }
507
+
508
+ function searchFullText(query, sessions) {
509
+ if (!query || query.length < 2) return [];
510
+ const q = query.toLowerCase();
511
+ const index = getSearchIndex(sessions);
512
+ const results = [];
513
+
514
+ for (const entry of index) {
515
+ if (entry.fullText.indexOf(q) === -1) continue;
516
+
517
+ // Find matching messages with snippets
518
+ const matches = [];
519
+ for (const t of entry.texts) {
520
+ if (matches.length >= 3) break;
521
+ const idx = t.content.toLowerCase().indexOf(q);
522
+ if (idx >= 0) {
523
+ const start = Math.max(0, idx - 50);
524
+ const end = Math.min(t.content.length, idx + q.length + 50);
525
+ matches.push({
526
+ role: t.role,
527
+ snippet: (start > 0 ? '...' : '') + t.content.slice(start, end) + (end < t.content.length ? '...' : ''),
528
+ });
529
+ }
530
+ }
531
+
532
+ if (matches.length > 0) {
533
+ results.push({ sessionId: entry.sessionId, matches });
534
+ }
535
+ }
536
+
479
537
  return results;
480
538
  }
481
539
 
@@ -104,10 +104,20 @@ function showTagDropdown(event, sessionId) {
104
104
  document.querySelectorAll('.tag-dropdown').forEach(function(el) { el.remove(); });
105
105
  var dd = document.createElement('div');
106
106
  dd.className = 'tag-dropdown';
107
+ var existingTags = tags[sessionId] || [];
107
108
  dd.innerHTML = TAG_OPTIONS.map(function(t) {
108
- return '<div class="tag-dropdown-item" onclick="event.stopPropagation();addTag(\'' + sessionId + '\',\'' + t + '\')">' + t + '</div>';
109
+ var has = existingTags.indexOf(t) >= 0;
110
+ return '<div class="tag-dropdown-item" onclick="event.stopPropagation();' +
111
+ (has ? 'removeTag' : 'addTag') + '(\'' + sessionId + '\',\'' + t + '\')">' +
112
+ (has ? '&#10003; ' : '') + t + '</div>';
109
113
  }).join('');
110
- event.target.parentElement.appendChild(dd);
114
+
115
+ // Position near the button
116
+ var rect = event.target.getBoundingClientRect();
117
+ dd.style.top = (rect.bottom + 4) + 'px';
118
+ dd.style.left = rect.left + 'px';
119
+
120
+ document.body.appendChild(dd);
111
121
  setTimeout(function() {
112
122
  document.addEventListener('click', function() { dd.remove(); }, { once: true });
113
123
  }, 0);
@@ -1338,6 +1348,30 @@ document.addEventListener('keydown', function(e) {
1338
1348
  }
1339
1349
  });
1340
1350
 
1351
+ // ── Export/Import dialog ──────────────────────────────────────
1352
+
1353
+ function showExportDialog() {
1354
+ var overlay = document.getElementById('confirmOverlay');
1355
+ document.getElementById('confirmTitle').textContent = 'Export / Import Sessions';
1356
+ document.getElementById('confirmText').innerHTML =
1357
+ '<strong>Export</strong> all sessions to migrate to another PC:<br>' +
1358
+ '<code style="display:block;margin:8px 0;padding:8px;background:var(--bg-card);border-radius:6px;font-size:12px">codedash export</code>' +
1359
+ 'Creates a tar.gz with all Claude &amp; Codex session data.<br><br>' +
1360
+ '<strong>Import</strong> on the new machine:<br>' +
1361
+ '<code style="display:block;margin:8px 0;padding:8px;background:var(--bg-card);border-radius:6px;font-size:12px">codedash import &lt;file.tar.gz&gt;</code>' +
1362
+ '<br><em style="color:var(--text-muted);font-size:12px">Don\'t forget to clone your git repos separately.</em>';
1363
+ document.getElementById('confirmId').textContent = '';
1364
+ document.getElementById('confirmAction').textContent = 'Copy Export Command';
1365
+ document.getElementById('confirmAction').className = 'launch-btn btn-primary';
1366
+ document.getElementById('confirmAction').onclick = function() {
1367
+ navigator.clipboard.writeText('codedash export').then(function() {
1368
+ showToast('Copied: codedash export');
1369
+ });
1370
+ closeConfirm();
1371
+ };
1372
+ if (overlay) overlay.style.display = 'flex';
1373
+ }
1374
+
1341
1375
  // ── Update check ──────────────────────────────────────────────
1342
1376
 
1343
1377
  async function checkForUpdates() {
@@ -1373,10 +1407,9 @@ async function checkForUpdates() {
1373
1407
  }
1374
1408
 
1375
1409
  function copyUpdate() {
1376
- var banner = document.getElementById('updateBanner');
1377
- var cmd = banner ? banner.dataset.cmd : 'npm update -g codedash-app';
1410
+ var cmd = 'codedash update && codedash restart';
1378
1411
  navigator.clipboard.writeText(cmd).then(function() {
1379
- showToast('Copied: ' + cmd);
1412
+ showToast('Copied: ' + cmd + ' (run in terminal)');
1380
1413
  });
1381
1414
  }
1382
1415
 
@@ -44,6 +44,11 @@
44
44
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
45
45
  Codex
46
46
  </div>
47
+ <div class="sidebar-divider"></div>
48
+ <div class="sidebar-item" onclick="showExportDialog()">
49
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
50
+ Export / Import
51
+ </div>
47
52
  <div class="sidebar-settings">
48
53
  <label>Terminal</label>
49
54
  <select id="terminalSelect" onchange="saveTerminalPref(this.value)">
@@ -484,20 +484,15 @@ body {
484
484
  }
485
485
 
486
486
  .tag-dropdown {
487
- position: absolute;
488
- top: 100%;
489
- left: 0;
490
- margin-top: 4px;
487
+ position: fixed;
491
488
  background: var(--bg-secondary);
492
489
  border: 1px solid var(--border);
493
490
  border-radius: 8px;
494
491
  padding: 6px;
495
492
  min-width: 140px;
496
493
  box-shadow: 0 8px 24px rgba(0,0,0,0.3);
497
- z-index: 50;
498
- display: none;
494
+ z-index: 150;
499
495
  }
500
- .tag-dropdown.open { display: block; }
501
496
 
502
497
  .tag-dropdown-item {
503
498
  display: flex;