codedash-app 1.7.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/package.json +1 -1
- package/src/data.js +78 -35
- package/src/frontend/app.js +36 -2
- package/src/frontend/index.html +5 -0
- package/src/frontend/styles.css +2 -7
package/package.json
CHANGED
package/src/data.js
CHANGED
|
@@ -437,60 +437,103 @@ function getSessionPreview(sessionId, project, limit) {
|
|
|
437
437
|
return messages;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
// ── Full-text search
|
|
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.
|
|
441
445
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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 = [];
|
|
446
453
|
|
|
447
454
|
for (const s of sessions) {
|
|
448
|
-
if (
|
|
455
|
+
if (!s.has_detail) continue;
|
|
449
456
|
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
if (!fs.existsSync(sessionFile)) continue;
|
|
457
|
+
const found = findSessionFile(s.id, s.project);
|
|
458
|
+
if (!found) continue;
|
|
453
459
|
|
|
454
460
|
try {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
if (data.toLowerCase().indexOf(q) === -1) continue;
|
|
461
|
+
const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
|
|
462
|
+
const texts = [];
|
|
458
463
|
|
|
459
|
-
// Find matching messages
|
|
460
|
-
const lines = data.split('\n').filter(Boolean);
|
|
461
|
-
const matches = [];
|
|
462
464
|
for (const line of lines) {
|
|
463
|
-
if (matches.length >= 3) break; // max 3 matches per session
|
|
464
465
|
try {
|
|
465
466
|
const entry = JSON.parse(line);
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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);
|
|
474
478
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const start = Math.max(0, idx - 50);
|
|
479
|
-
const end = Math.min(content.length, idx + q.length + 50);
|
|
480
|
-
matches.push({
|
|
481
|
-
role: entry.type,
|
|
482
|
-
snippet: (start > 0 ? '...' : '') + content.slice(start, end) + (end < content.length ? '...' : ''),
|
|
483
|
-
});
|
|
479
|
+
|
|
480
|
+
if (content && !isSystemMessage(content)) {
|
|
481
|
+
texts.push({ role, content: content.slice(0, 500) });
|
|
484
482
|
}
|
|
485
483
|
} catch {}
|
|
486
484
|
}
|
|
487
485
|
|
|
488
|
-
if (
|
|
489
|
-
|
|
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 });
|
|
490
490
|
}
|
|
491
491
|
} catch {}
|
|
492
492
|
}
|
|
493
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
|
+
|
|
494
537
|
return results;
|
|
495
538
|
}
|
|
496
539
|
|
package/src/frontend/app.js
CHANGED
|
@@ -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
|
-
|
|
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 ? '✓ ' : '') + t + '</div>';
|
|
109
113
|
}).join('');
|
|
110
|
-
|
|
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 & 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 <file.tar.gz></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() {
|
package/src/frontend/index.html
CHANGED
|
@@ -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)">
|
package/src/frontend/styles.css
CHANGED
|
@@ -484,20 +484,15 @@ body {
|
|
|
484
484
|
}
|
|
485
485
|
|
|
486
486
|
.tag-dropdown {
|
|
487
|
-
position:
|
|
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:
|
|
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;
|