codedash-app 1.4.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 +1 -1
- package/src/data.js +174 -24
- package/src/frontend/app.js +203 -6
- package/src/frontend/index.html +1 -0
- package/src/frontend/styles.css +118 -0
- package/src/server.js +18 -1
package/package.json
CHANGED
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(
|
|
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
|
|
196
|
-
|
|
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(
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
content
|
|
213
|
-
.
|
|
214
|
-
|
|
215
|
-
|
|
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(
|
|
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(
|
|
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)) {
|
|
@@ -330,6 +330,155 @@ function exportSessionMarkdown(sessionId, project) {
|
|
|
330
330
|
return parts.join('');
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
// ── Session Preview (first N messages, lightweight) ────────
|
|
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
|
+
|
|
385
|
+
function getSessionPreview(sessionId, project, limit) {
|
|
386
|
+
limit = limit || 10;
|
|
387
|
+
const found = findSessionFile(sessionId, project);
|
|
388
|
+
if (!found) return [];
|
|
389
|
+
|
|
390
|
+
const messages = [];
|
|
391
|
+
const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
|
|
392
|
+
|
|
393
|
+
for (const line of lines) {
|
|
394
|
+
if (messages.length >= limit) break;
|
|
395
|
+
try {
|
|
396
|
+
const entry = JSON.parse(line);
|
|
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
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} catch {}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return messages;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── Full-text search across all sessions ──────────────────
|
|
426
|
+
|
|
427
|
+
function searchFullText(query, sessions) {
|
|
428
|
+
if (!query || query.length < 2) return [];
|
|
429
|
+
const q = query.toLowerCase();
|
|
430
|
+
const results = [];
|
|
431
|
+
|
|
432
|
+
for (const s of sessions) {
|
|
433
|
+
if (s.tool !== 'claude' || !s.has_detail) continue;
|
|
434
|
+
|
|
435
|
+
const projectKey = s.project.replace(/[\/\.]/g, '-');
|
|
436
|
+
const sessionFile = path.join(PROJECTS_DIR, projectKey, `${s.id}.jsonl`);
|
|
437
|
+
if (!fs.existsSync(sessionFile)) continue;
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const data = fs.readFileSync(sessionFile, 'utf8');
|
|
441
|
+
// Quick check before parsing
|
|
442
|
+
if (data.toLowerCase().indexOf(q) === -1) continue;
|
|
443
|
+
|
|
444
|
+
// Find matching messages
|
|
445
|
+
const lines = data.split('\n').filter(Boolean);
|
|
446
|
+
const matches = [];
|
|
447
|
+
for (const line of lines) {
|
|
448
|
+
if (matches.length >= 3) break; // max 3 matches per session
|
|
449
|
+
try {
|
|
450
|
+
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');
|
|
459
|
+
}
|
|
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
|
+
});
|
|
469
|
+
}
|
|
470
|
+
} catch {}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (matches.length > 0) {
|
|
474
|
+
results.push({ sessionId: s.id, matches });
|
|
475
|
+
}
|
|
476
|
+
} catch {}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return results;
|
|
480
|
+
}
|
|
481
|
+
|
|
333
482
|
// ── Exports ────────────────────────────────────────────────
|
|
334
483
|
|
|
335
484
|
module.exports = {
|
|
@@ -338,7 +487,8 @@ module.exports = {
|
|
|
338
487
|
deleteSession,
|
|
339
488
|
getGitCommits,
|
|
340
489
|
exportSessionMarkdown,
|
|
341
|
-
|
|
490
|
+
getSessionPreview,
|
|
491
|
+
searchFullText,
|
|
342
492
|
CLAUDE_DIR,
|
|
343
493
|
CODEX_DIR,
|
|
344
494
|
HISTORY_FILE,
|
package/src/frontend/app.js
CHANGED
|
@@ -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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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">▾</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 = '▾';
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
btn.innerHTML = '⌛';
|
|
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 = '▴';
|
|
473
|
+
} catch (e) {
|
|
474
|
+
area.innerHTML = '<div class="preview-empty">Failed to load</div>';
|
|
475
|
+
btn.innerHTML = '▾';
|
|
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);
|
|
@@ -1164,13 +1344,29 @@ async function checkForUpdates() {
|
|
|
1164
1344
|
try {
|
|
1165
1345
|
var resp = await fetch('/api/version');
|
|
1166
1346
|
var data = await resp.json();
|
|
1347
|
+
var badge = document.getElementById('versionBadge');
|
|
1348
|
+
|
|
1349
|
+
if (badge) {
|
|
1350
|
+
badge.textContent = 'v' + data.current;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1167
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
|
+
}
|
|
1168
1364
|
var banner = document.getElementById('updateBanner');
|
|
1169
1365
|
var text = document.getElementById('updateText');
|
|
1170
1366
|
if (banner && text) {
|
|
1171
|
-
text.textContent = '
|
|
1367
|
+
text.textContent = 'v' + data.latest + ' available — run: npm i -g codedash-app@latest';
|
|
1172
1368
|
banner.style.display = 'flex';
|
|
1173
|
-
banner.dataset.cmd = 'npm
|
|
1369
|
+
banner.dataset.cmd = 'npm i -g codedash-app@latest';
|
|
1174
1370
|
}
|
|
1175
1371
|
}
|
|
1176
1372
|
} catch {}
|
|
@@ -1196,6 +1392,7 @@ function dismissUpdate() {
|
|
|
1196
1392
|
loadSessions();
|
|
1197
1393
|
loadTerminals();
|
|
1198
1394
|
checkForUpdates();
|
|
1395
|
+
initHoverPreview();
|
|
1199
1396
|
|
|
1200
1397
|
// Apply saved theme
|
|
1201
1398
|
var savedTheme = localStorage.getItem('codedash-theme') || 'dark';
|
package/src/frontend/index.html
CHANGED
|
@@ -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>
|
package/src/frontend/styles.css
CHANGED
|
@@ -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 {
|
|
@@ -1370,6 +1390,104 @@ body {
|
|
|
1370
1390
|
color: #fff;
|
|
1371
1391
|
}
|
|
1372
1392
|
|
|
1393
|
+
/* ── Card expand preview ────────────────────────────────────── */
|
|
1394
|
+
|
|
1395
|
+
.card-expand-btn {
|
|
1396
|
+
background: none;
|
|
1397
|
+
border: 1px solid var(--border);
|
|
1398
|
+
border-radius: 4px;
|
|
1399
|
+
color: var(--text-muted);
|
|
1400
|
+
font-size: 12px;
|
|
1401
|
+
cursor: pointer;
|
|
1402
|
+
padding: 1px 6px;
|
|
1403
|
+
margin-left: auto;
|
|
1404
|
+
transition: all 0.15s;
|
|
1405
|
+
}
|
|
1406
|
+
.card-expand-btn:hover {
|
|
1407
|
+
color: var(--text-primary);
|
|
1408
|
+
border-color: var(--accent-blue);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.card-preview-area {
|
|
1412
|
+
display: none;
|
|
1413
|
+
border-top: 1px solid var(--border);
|
|
1414
|
+
margin-top: 10px;
|
|
1415
|
+
padding-top: 10px;
|
|
1416
|
+
max-height: 300px;
|
|
1417
|
+
overflow-y: auto;
|
|
1418
|
+
animation: fadeIn 0.2s ease;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.card-preview-area.open {
|
|
1422
|
+
display: block;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
@keyframes fadeIn {
|
|
1426
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
1427
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
.preview-msg {
|
|
1431
|
+
font-size: 12px;
|
|
1432
|
+
line-height: 1.5;
|
|
1433
|
+
padding: 4px 8px;
|
|
1434
|
+
margin-bottom: 4px;
|
|
1435
|
+
border-radius: 6px;
|
|
1436
|
+
word-break: break-word;
|
|
1437
|
+
white-space: pre-wrap;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
.preview-user {
|
|
1441
|
+
background: rgba(96, 165, 250, 0.08);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
.preview-assistant {
|
|
1445
|
+
background: rgba(74, 222, 128, 0.06);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
.preview-role {
|
|
1449
|
+
font-weight: 600;
|
|
1450
|
+
font-size: 10px;
|
|
1451
|
+
text-transform: uppercase;
|
|
1452
|
+
letter-spacing: 0.3px;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
.preview-user .preview-role { color: var(--accent-blue); }
|
|
1456
|
+
.preview-assistant .preview-role { color: var(--accent-green); }
|
|
1457
|
+
|
|
1458
|
+
.preview-empty {
|
|
1459
|
+
color: var(--text-muted);
|
|
1460
|
+
font-size: 12px;
|
|
1461
|
+
padding: 8px 0;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/* ── Hover tooltip ──────────────────────────────────────────── */
|
|
1465
|
+
|
|
1466
|
+
.hover-tooltip {
|
|
1467
|
+
position: fixed;
|
|
1468
|
+
background: var(--bg-secondary);
|
|
1469
|
+
border: 1px solid var(--border);
|
|
1470
|
+
border-radius: 10px;
|
|
1471
|
+
padding: 12px;
|
|
1472
|
+
z-index: 150;
|
|
1473
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
1474
|
+
max-height: 280px;
|
|
1475
|
+
overflow-y: auto;
|
|
1476
|
+
opacity: 0;
|
|
1477
|
+
transform: translateY(4px);
|
|
1478
|
+
transition: opacity 0.15s, transform 0.15s;
|
|
1479
|
+
pointer-events: none;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
.hover-tooltip.visible {
|
|
1483
|
+
opacity: 1;
|
|
1484
|
+
transform: translateY(0);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
[data-theme="light"] .hover-tooltip {
|
|
1488
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1373
1491
|
/* ── Update banner ──────────────────────────────────────────── */
|
|
1374
1492
|
|
|
1375
1493
|
.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');
|