codedash-app 1.4.0 → 1.5.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.4.0",
3
+ "version": "1.5.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
@@ -330,6 +330,99 @@ function exportSessionMarkdown(sessionId, project) {
330
330
  return parts.join('');
331
331
  }
332
332
 
333
+ // ── Session Preview (first N messages, lightweight) ────────
334
+
335
+ function getSessionPreview(sessionId, project, limit) {
336
+ 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 [];
341
+
342
+ const messages = [];
343
+ const lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
344
+
345
+ for (const line of lines) {
346
+ if (messages.length >= limit) break;
347
+ try {
348
+ 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');
357
+ }
358
+ messages.push({
359
+ role: entry.type,
360
+ content: content.slice(0, 300), // short preview
361
+ });
362
+ }
363
+ } catch {}
364
+ }
365
+
366
+ return messages;
367
+ }
368
+
369
+ // ── Full-text search across all sessions ──────────────────
370
+
371
+ function searchFullText(query, sessions) {
372
+ if (!query || query.length < 2) return [];
373
+ const q = query.toLowerCase();
374
+ const results = [];
375
+
376
+ for (const s of sessions) {
377
+ if (s.tool !== 'claude' || !s.has_detail) continue;
378
+
379
+ const projectKey = s.project.replace(/\//g, '-').replace(/^-/, '');
380
+ const sessionFile = path.join(PROJECTS_DIR, projectKey, `${s.id}.jsonl`);
381
+ if (!fs.existsSync(sessionFile)) continue;
382
+
383
+ try {
384
+ const data = fs.readFileSync(sessionFile, 'utf8');
385
+ // Quick check before parsing
386
+ if (data.toLowerCase().indexOf(q) === -1) continue;
387
+
388
+ // Find matching messages
389
+ const lines = data.split('\n').filter(Boolean);
390
+ const matches = [];
391
+ for (const line of lines) {
392
+ if (matches.length >= 3) break; // max 3 matches per session
393
+ try {
394
+ const entry = JSON.parse(line);
395
+ if (entry.type !== 'user' && entry.type !== 'assistant') continue;
396
+ const msg = entry.message || {};
397
+ let content = msg.content || '';
398
+ if (Array.isArray(content)) {
399
+ content = content
400
+ .map(b => (typeof b === 'string' ? b : (b.type === 'text' ? b.text : '')))
401
+ .filter(Boolean)
402
+ .join('\n');
403
+ }
404
+ if (content.toLowerCase().indexOf(q) >= 0) {
405
+ // Extract snippet around match
406
+ const idx = content.toLowerCase().indexOf(q);
407
+ const start = Math.max(0, idx - 50);
408
+ const end = Math.min(content.length, idx + q.length + 50);
409
+ matches.push({
410
+ role: entry.type,
411
+ snippet: (start > 0 ? '...' : '') + content.slice(start, end) + (end < content.length ? '...' : ''),
412
+ });
413
+ }
414
+ } catch {}
415
+ }
416
+
417
+ if (matches.length > 0) {
418
+ results.push({ sessionId: s.id, matches });
419
+ }
420
+ } catch {}
421
+ }
422
+
423
+ return results;
424
+ }
425
+
333
426
  // ── Exports ────────────────────────────────────────────────
334
427
 
335
428
  module.exports = {
@@ -338,7 +431,8 @@ module.exports = {
338
431
  deleteSession,
339
432
  getGitCommits,
340
433
  exportSessionMarkdown,
341
- // Expose constants for consumers that need them
434
+ getSessionPreview,
435
+ searchFullText,
342
436
  CLAUDE_DIR,
343
437
  CODEX_DIR,
344
438
  HISTORY_FILE,
@@ -288,6 +288,12 @@ function applyFilters() {
288
288
  function onSearch(val) {
289
289
  searchQuery = val;
290
290
  applyFilters();
291
+
292
+ // Trigger deep search after debounce
293
+ clearTimeout(deepSearchTimeout);
294
+ if (val && val.length >= 3) {
295
+ deepSearchTimeout = setTimeout(function() { deepSearch(val); }, 600);
296
+ }
291
297
  }
292
298
 
293
299
  function onTagFilter(val) {
@@ -379,12 +385,15 @@ function renderCard(s, idx) {
379
385
  html += '<span class="card-meta">' + escHtml(s.last_time || '') + '</span>';
380
386
  html += '<span class="card-id">' + s.id.slice(0, 8) + '</span>';
381
387
  // Tags
382
- if (tagHtml || true) {
383
- html += '<span class="card-tags">' + tagHtml;
384
- html += '<button class="tag-add-btn" onclick="showTagDropdown(event, \'' + s.id + '\')" title="Add tag">+</button>';
385
- html += '</span>';
388
+ html += '<span class="card-tags">' + tagHtml;
389
+ html += '<button class="tag-add-btn" onclick="showTagDropdown(event, \'' + s.id + '\')" title="Add tag">+</button>';
390
+ html += '</span>';
391
+ if (s.has_detail) {
392
+ html += '<button class="card-expand-btn" onclick="event.stopPropagation();toggleExpand(\'' + s.id + '\',\'' + escHtml(s.project || '').replace(/'/g, "\\'") + '\',this)" title="Preview messages">&#9662;</button>';
386
393
  }
387
394
  html += '</div>';
395
+ // Expandable preview area (hidden by default)
396
+ html += '<div class="card-preview-area" id="preview-' + s.id + '"></div>';
388
397
  html += '</div>';
389
398
  return html;
390
399
  }
@@ -425,6 +434,177 @@ function renderListCard(s, idx) {
425
434
  return html;
426
435
  }
427
436
 
437
+ // ── Card expand (inline preview) ──────────────────────────────
438
+
439
+ async function toggleExpand(sessionId, project, btn) {
440
+ var area = document.getElementById('preview-' + sessionId);
441
+ if (!area) return;
442
+
443
+ if (area.classList.contains('open')) {
444
+ area.classList.remove('open');
445
+ area.innerHTML = '';
446
+ btn.innerHTML = '&#9662;';
447
+ return;
448
+ }
449
+
450
+ btn.innerHTML = '&#8987;';
451
+ area.innerHTML = '<div class="loading">Loading...</div>';
452
+ area.classList.add('open');
453
+
454
+ try {
455
+ var resp = await fetch('/api/preview/' + sessionId + '?project=' + encodeURIComponent(project) + '&limit=10');
456
+ var messages = await resp.json();
457
+
458
+ if (messages.length === 0) {
459
+ area.innerHTML = '<div class="preview-empty">No messages</div>';
460
+ } else {
461
+ var html = '';
462
+ messages.forEach(function(m) {
463
+ var cls = m.role === 'user' ? 'preview-user' : 'preview-assistant';
464
+ var label = m.role === 'user' ? 'You' : 'AI';
465
+ html += '<div class="preview-msg ' + cls + '">';
466
+ html += '<span class="preview-role">' + label + '</span> ';
467
+ html += escHtml(m.content);
468
+ html += '</div>';
469
+ });
470
+ area.innerHTML = html;
471
+ }
472
+ btn.innerHTML = '&#9652;';
473
+ } catch (e) {
474
+ area.innerHTML = '<div class="preview-empty">Failed to load</div>';
475
+ btn.innerHTML = '&#9662;';
476
+ }
477
+ }
478
+
479
+ // ── Hover tooltip (show first messages on hover) ──────────────
480
+
481
+ var hoverTimer = null;
482
+ var hoverTooltip = null;
483
+
484
+ function initHoverPreview() {
485
+ document.addEventListener('mouseover', function(e) {
486
+ var card = e.target.closest('.card');
487
+ if (!card) { hideHoverTooltip(); return; }
488
+
489
+ var id = card.getAttribute('data-id');
490
+ if (!id) return;
491
+
492
+ clearTimeout(hoverTimer);
493
+ hoverTimer = setTimeout(function() {
494
+ var s = allSessions.find(function(x) { return x.id === id; });
495
+ if (!s || !s.has_detail) return;
496
+ showHoverTooltip(card, s);
497
+ }, 400); // 400ms delay
498
+ });
499
+
500
+ document.addEventListener('mouseout', function(e) {
501
+ var card = e.target.closest('.card');
502
+ if (!card) { clearTimeout(hoverTimer); hideHoverTooltip(); }
503
+ });
504
+ }
505
+
506
+ async function showHoverTooltip(card, session) {
507
+ hideHoverTooltip();
508
+
509
+ try {
510
+ var resp = await fetch('/api/preview/' + session.id + '?project=' + encodeURIComponent(session.project || '') + '&limit=6');
511
+ var messages = await resp.json();
512
+ if (messages.length === 0) return;
513
+
514
+ var tip = document.createElement('div');
515
+ tip.className = 'hover-tooltip';
516
+
517
+ var html = '';
518
+ messages.forEach(function(m) {
519
+ var label = m.role === 'user' ? 'You' : 'AI';
520
+ var cls = m.role === 'user' ? 'preview-user' : 'preview-assistant';
521
+ html += '<div class="preview-msg ' + cls + '">';
522
+ html += '<span class="preview-role">' + label + '</span> ';
523
+ html += escHtml(m.content.slice(0, 150));
524
+ if (m.content.length > 150) html += '...';
525
+ html += '</div>';
526
+ });
527
+ tip.innerHTML = html;
528
+
529
+ document.body.appendChild(tip);
530
+ hoverTooltip = tip;
531
+
532
+ // Position near card
533
+ var rect = card.getBoundingClientRect();
534
+ tip.style.top = Math.min(rect.bottom + 4, window.innerHeight - tip.offsetHeight - 8) + 'px';
535
+ tip.style.left = Math.max(8, rect.left) + 'px';
536
+ tip.style.maxWidth = Math.min(500, window.innerWidth - rect.left - 20) + 'px';
537
+
538
+ requestAnimationFrame(function() { tip.classList.add('visible'); });
539
+ } catch {}
540
+ }
541
+
542
+ function hideHoverTooltip() {
543
+ if (hoverTooltip) {
544
+ hoverTooltip.remove();
545
+ hoverTooltip = null;
546
+ }
547
+ }
548
+
549
+ // ── Deep search (full-text across session content) ────────────
550
+
551
+ var deepSearchCache = {};
552
+ var deepSearchTimeout = null;
553
+
554
+ async function deepSearch(query) {
555
+ if (!query || query.length < 3) return;
556
+ if (deepSearchCache[query]) {
557
+ applyDeepSearchResults(deepSearchCache[query]);
558
+ return;
559
+ }
560
+
561
+ try {
562
+ var resp = await fetch('/api/search?q=' + encodeURIComponent(query));
563
+ var results = await resp.json();
564
+ deepSearchCache[query] = results;
565
+ applyDeepSearchResults(results);
566
+ } catch {}
567
+ }
568
+
569
+ function applyDeepSearchResults(results) {
570
+ if (!results || results.length === 0) return;
571
+
572
+ // Highlight matching session IDs in filtered list
573
+ var matchIds = results.map(function(r) { return r.sessionId; });
574
+
575
+ // Boost matching sessions to top if not already visible
576
+ var boosted = [];
577
+ var rest = [];
578
+ filteredSessions.forEach(function(s) {
579
+ if (matchIds.indexOf(s.id) >= 0) {
580
+ s._deepMatch = results.find(function(r) { return r.sessionId === s.id; });
581
+ boosted.push(s);
582
+ } else {
583
+ rest.push(s);
584
+ }
585
+ });
586
+
587
+ // Also add sessions that weren't in filteredSessions but match
588
+ matchIds.forEach(function(id) {
589
+ if (!boosted.find(function(s) { return s.id === id; }) && !rest.find(function(s) { return s.id === id; })) {
590
+ var s = allSessions.find(function(x) { return x.id === id; });
591
+ if (s) {
592
+ s._deepMatch = results.find(function(r) { return r.sessionId === id; });
593
+ boosted.push(s);
594
+ }
595
+ }
596
+ });
597
+
598
+ filteredSessions = boosted.concat(rest);
599
+ render();
600
+
601
+ // Show deep search indicator
602
+ var stats = document.getElementById('stats');
603
+ if (stats && boosted.length > 0) {
604
+ stats.textContent += ' | ' + boosted.length + ' deep matches';
605
+ }
606
+ }
607
+
428
608
  function onCardClick(id, event) {
429
609
  if (selectMode) {
430
610
  toggleSelect(id, event);
@@ -1196,6 +1376,7 @@ function dismissUpdate() {
1196
1376
  loadSessions();
1197
1377
  loadTerminals();
1198
1378
  checkForUpdates();
1379
+ initHoverPreview();
1199
1380
 
1200
1381
  // Apply saved theme
1201
1382
  var savedTheme = localStorage.getItem('codedash-theme') || 'dark';
@@ -1370,6 +1370,104 @@ body {
1370
1370
  color: #fff;
1371
1371
  }
1372
1372
 
1373
+ /* ── Card expand preview ────────────────────────────────────── */
1374
+
1375
+ .card-expand-btn {
1376
+ background: none;
1377
+ border: 1px solid var(--border);
1378
+ border-radius: 4px;
1379
+ color: var(--text-muted);
1380
+ font-size: 12px;
1381
+ cursor: pointer;
1382
+ padding: 1px 6px;
1383
+ margin-left: auto;
1384
+ transition: all 0.15s;
1385
+ }
1386
+ .card-expand-btn:hover {
1387
+ color: var(--text-primary);
1388
+ border-color: var(--accent-blue);
1389
+ }
1390
+
1391
+ .card-preview-area {
1392
+ display: none;
1393
+ border-top: 1px solid var(--border);
1394
+ margin-top: 10px;
1395
+ padding-top: 10px;
1396
+ max-height: 300px;
1397
+ overflow-y: auto;
1398
+ animation: fadeIn 0.2s ease;
1399
+ }
1400
+
1401
+ .card-preview-area.open {
1402
+ display: block;
1403
+ }
1404
+
1405
+ @keyframes fadeIn {
1406
+ from { opacity: 0; transform: translateY(-4px); }
1407
+ to { opacity: 1; transform: translateY(0); }
1408
+ }
1409
+
1410
+ .preview-msg {
1411
+ font-size: 12px;
1412
+ line-height: 1.5;
1413
+ padding: 4px 8px;
1414
+ margin-bottom: 4px;
1415
+ border-radius: 6px;
1416
+ word-break: break-word;
1417
+ white-space: pre-wrap;
1418
+ }
1419
+
1420
+ .preview-user {
1421
+ background: rgba(96, 165, 250, 0.08);
1422
+ }
1423
+
1424
+ .preview-assistant {
1425
+ background: rgba(74, 222, 128, 0.06);
1426
+ }
1427
+
1428
+ .preview-role {
1429
+ font-weight: 600;
1430
+ font-size: 10px;
1431
+ text-transform: uppercase;
1432
+ letter-spacing: 0.3px;
1433
+ }
1434
+
1435
+ .preview-user .preview-role { color: var(--accent-blue); }
1436
+ .preview-assistant .preview-role { color: var(--accent-green); }
1437
+
1438
+ .preview-empty {
1439
+ color: var(--text-muted);
1440
+ font-size: 12px;
1441
+ padding: 8px 0;
1442
+ }
1443
+
1444
+ /* ── Hover tooltip ──────────────────────────────────────────── */
1445
+
1446
+ .hover-tooltip {
1447
+ position: fixed;
1448
+ background: var(--bg-secondary);
1449
+ border: 1px solid var(--border);
1450
+ border-radius: 10px;
1451
+ padding: 12px;
1452
+ z-index: 150;
1453
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
1454
+ max-height: 280px;
1455
+ overflow-y: auto;
1456
+ opacity: 0;
1457
+ transform: translateY(4px);
1458
+ transition: opacity 0.15s, transform 0.15s;
1459
+ pointer-events: none;
1460
+ }
1461
+
1462
+ .hover-tooltip.visible {
1463
+ opacity: 1;
1464
+ transform: translateY(0);
1465
+ }
1466
+
1467
+ [data-theme="light"] .hover-tooltip {
1468
+ box-shadow: 0 8px 32px rgba(0,0,0,0.12);
1469
+ }
1470
+
1373
1471
  /* ── Update banner ──────────────────────────────────────────── */
1374
1472
 
1375
1473
  .update-banner {
package/src/server.js CHANGED
@@ -3,7 +3,7 @@ const http = require('http');
3
3
  const https = require('https');
4
4
  const { URL } = require('url');
5
5
  const { exec } = require('child_process');
6
- const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown } = require('./data');
6
+ const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText } = require('./data');
7
7
  const { detectTerminals, openInTerminal } = require('./terminals');
8
8
  const { getHTML } = require('./html');
9
9
 
@@ -104,6 +104,23 @@ function startServer(port, openBrowser = true) {
104
104
  json(res, commits);
105
105
  }
106
106
 
107
+ // ── Session preview ─────────────────────
108
+ else if (req.method === 'GET' && pathname.startsWith('/api/preview/')) {
109
+ const sessionId = pathname.split('/').pop();
110
+ const project = parsed.searchParams.get('project') || '';
111
+ const limit = parseInt(parsed.searchParams.get('limit') || '10');
112
+ const messages = getSessionPreview(sessionId, project, limit);
113
+ json(res, messages);
114
+ }
115
+
116
+ // ── Full-text search ──────────────────────
117
+ else if (req.method === 'GET' && pathname === '/api/search') {
118
+ const q = parsed.searchParams.get('q') || '';
119
+ const sessions = loadSessions();
120
+ const results = searchFullText(q, sessions);
121
+ json(res, results);
122
+ }
123
+
107
124
  // ── Version check ────────────────────────
108
125
  else if (req.method === 'GET' && pathname === '/api/version') {
109
126
  const pkg = require('../package.json');