codedash-app 4.1.0 → 4.2.1
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 +139 -1
- package/src/frontend/app.js +4 -1
- package/src/frontend/index.html +4 -0
- package/src/frontend/styles.css +11 -0
package/package.json
CHANGED
package/src/data.js
CHANGED
|
@@ -7,11 +7,120 @@ const { execSync } = require('child_process');
|
|
|
7
7
|
|
|
8
8
|
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
9
9
|
const CODEX_DIR = path.join(os.homedir(), '.codex');
|
|
10
|
+
const OPENCODE_DB = path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
|
|
10
11
|
const HISTORY_FILE = path.join(CLAUDE_DIR, 'history.jsonl');
|
|
11
12
|
const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|
12
13
|
|
|
13
14
|
// ── Helpers ────────────────────────────────────────────────
|
|
14
15
|
|
|
16
|
+
function scanOpenCodeSessions() {
|
|
17
|
+
const sessions = [];
|
|
18
|
+
if (!fs.existsSync(OPENCODE_DB)) return sessions;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Use sqlite3 CLI to avoid Node version dependency
|
|
22
|
+
const rows = execSync(
|
|
23
|
+
`sqlite3 "${OPENCODE_DB}" "SELECT s.id, s.title, s.directory, s.time_created, s.time_updated, COUNT(m.id) as msg_count FROM session s LEFT JOIN message m ON m.session_id = s.id GROUP BY s.id ORDER BY s.time_updated DESC"`,
|
|
24
|
+
{ encoding: 'utf8', timeout: 5000 }
|
|
25
|
+
).trim();
|
|
26
|
+
|
|
27
|
+
if (!rows) return sessions;
|
|
28
|
+
|
|
29
|
+
for (const row of rows.split('\n')) {
|
|
30
|
+
const parts = row.split('|');
|
|
31
|
+
if (parts.length < 6) continue;
|
|
32
|
+
const [id, title, directory, timeCreated, timeUpdated, msgCount] = parts;
|
|
33
|
+
|
|
34
|
+
sessions.push({
|
|
35
|
+
id: id,
|
|
36
|
+
tool: 'opencode',
|
|
37
|
+
project: directory || '',
|
|
38
|
+
project_short: (directory || '').replace(os.homedir(), '~'),
|
|
39
|
+
first_ts: parseInt(timeCreated) || Date.now(),
|
|
40
|
+
last_ts: parseInt(timeUpdated) || Date.now(),
|
|
41
|
+
messages: parseInt(msgCount) || 0,
|
|
42
|
+
first_message: title || '',
|
|
43
|
+
has_detail: true,
|
|
44
|
+
file_size: 0,
|
|
45
|
+
detail_messages: parseInt(msgCount) || 0,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
|
|
50
|
+
return sessions;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function loadOpenCodeDetail(sessionId) {
|
|
54
|
+
if (!fs.existsSync(OPENCODE_DB)) return { messages: [] };
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Get messages with parts joined
|
|
58
|
+
const rows = execSync(
|
|
59
|
+
`sqlite3 "${OPENCODE_DB}" "SELECT m.data, GROUP_CONCAT(p.data, '|||') FROM message m LEFT JOIN part p ON p.message_id = m.id WHERE m.session_id = '${sessionId.replace(/'/g, "''")}' GROUP BY m.id ORDER BY m.time_created"`,
|
|
60
|
+
{ encoding: 'utf8', timeout: 10000 }
|
|
61
|
+
).trim();
|
|
62
|
+
|
|
63
|
+
if (!rows) return { messages: [] };
|
|
64
|
+
|
|
65
|
+
const messages = [];
|
|
66
|
+
for (const row of rows.split('\n')) {
|
|
67
|
+
const sepIdx = row.indexOf('|');
|
|
68
|
+
if (sepIdx < 0) continue;
|
|
69
|
+
|
|
70
|
+
// Parse message data (first column)
|
|
71
|
+
// Find the JSON boundary - message data ends where part data starts
|
|
72
|
+
let msgJson, partsRaw;
|
|
73
|
+
try {
|
|
74
|
+
// Try to find where message JSON ends
|
|
75
|
+
let braceCount = 0;
|
|
76
|
+
let jsonEnd = 0;
|
|
77
|
+
for (let i = 0; i < row.length; i++) {
|
|
78
|
+
if (row[i] === '{') braceCount++;
|
|
79
|
+
if (row[i] === '}') { braceCount--; if (braceCount === 0) { jsonEnd = i + 1; break; } }
|
|
80
|
+
}
|
|
81
|
+
msgJson = row.slice(0, jsonEnd);
|
|
82
|
+
partsRaw = row.slice(jsonEnd + 1); // skip |
|
|
83
|
+
} catch { continue; }
|
|
84
|
+
|
|
85
|
+
let msgData;
|
|
86
|
+
try { msgData = JSON.parse(msgJson); } catch { continue; }
|
|
87
|
+
|
|
88
|
+
const role = msgData.role;
|
|
89
|
+
if (role !== 'user' && role !== 'assistant') continue;
|
|
90
|
+
|
|
91
|
+
// Extract text from parts
|
|
92
|
+
let content = '';
|
|
93
|
+
if (partsRaw) {
|
|
94
|
+
for (const partStr of partsRaw.split('|||')) {
|
|
95
|
+
try {
|
|
96
|
+
const part = JSON.parse(partStr);
|
|
97
|
+
if (part.type === 'text' && part.text) {
|
|
98
|
+
content += part.text + '\n';
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
content = content.trim();
|
|
105
|
+
if (!content) continue;
|
|
106
|
+
|
|
107
|
+
const tokens = msgData.tokens || {};
|
|
108
|
+
|
|
109
|
+
messages.push({
|
|
110
|
+
role: role,
|
|
111
|
+
content: content.slice(0, 2000),
|
|
112
|
+
uuid: '',
|
|
113
|
+
model: msgData.modelID || msgData.model?.modelID || '',
|
|
114
|
+
tokens: tokens,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { messages: messages.slice(0, 200) };
|
|
119
|
+
} catch {
|
|
120
|
+
return { messages: [] };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
15
124
|
function scanCodexSessions() {
|
|
16
125
|
const sessions = [];
|
|
17
126
|
const codexHistory = path.join(CODEX_DIR, 'history.jsonl');
|
|
@@ -154,6 +263,14 @@ function loadSessions() {
|
|
|
154
263
|
} catch {}
|
|
155
264
|
}
|
|
156
265
|
|
|
266
|
+
// Load OpenCode sessions
|
|
267
|
+
try {
|
|
268
|
+
const opencodeSessions = scanOpenCodeSessions();
|
|
269
|
+
for (const ocs of opencodeSessions) {
|
|
270
|
+
sessions[ocs.id] = ocs;
|
|
271
|
+
}
|
|
272
|
+
} catch {}
|
|
273
|
+
|
|
157
274
|
// Enrich Claude sessions with detail file info
|
|
158
275
|
for (const [sid, s] of Object.entries(sessions)) {
|
|
159
276
|
if (s.tool !== 'claude') continue;
|
|
@@ -185,7 +302,8 @@ function loadSessions() {
|
|
|
185
302
|
for (const s of result) {
|
|
186
303
|
s.first_time = new Date(s.first_ts).toLocaleString('sv-SE').slice(0, 16);
|
|
187
304
|
s.last_time = new Date(s.last_ts).toLocaleString('sv-SE').slice(0, 16);
|
|
188
|
-
|
|
305
|
+
const dt = new Date(s.last_ts);
|
|
306
|
+
s.date = dt.getFullYear() + '-' + String(dt.getMonth()+1).padStart(2,'0') + '-' + String(dt.getDate()).padStart(2,'0');
|
|
189
307
|
}
|
|
190
308
|
|
|
191
309
|
return result;
|
|
@@ -195,6 +313,11 @@ function loadSessionDetail(sessionId, project) {
|
|
|
195
313
|
const found = findSessionFile(sessionId, project);
|
|
196
314
|
if (!found) return { error: 'Session file not found', messages: [] };
|
|
197
315
|
|
|
316
|
+
// OpenCode uses SQLite
|
|
317
|
+
if (found.format === 'opencode') {
|
|
318
|
+
return loadOpenCodeDetail(sessionId);
|
|
319
|
+
}
|
|
320
|
+
|
|
198
321
|
const messages = [];
|
|
199
322
|
const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
|
|
200
323
|
|
|
@@ -367,6 +490,11 @@ function findSessionFile(sessionId, project) {
|
|
|
367
490
|
if (codexFile) return { file: codexFile, format: 'codex' };
|
|
368
491
|
}
|
|
369
492
|
|
|
493
|
+
// Try OpenCode (SQLite — return special marker)
|
|
494
|
+
if (fs.existsSync(OPENCODE_DB) && sessionId.startsWith('ses_')) {
|
|
495
|
+
return { file: OPENCODE_DB, format: 'opencode', sessionId: sessionId };
|
|
496
|
+
}
|
|
497
|
+
|
|
370
498
|
return null;
|
|
371
499
|
}
|
|
372
500
|
|
|
@@ -402,6 +530,14 @@ function getSessionPreview(sessionId, project, limit) {
|
|
|
402
530
|
const found = findSessionFile(sessionId, project);
|
|
403
531
|
if (!found) return [];
|
|
404
532
|
|
|
533
|
+
// OpenCode: use loadOpenCodeDetail and slice
|
|
534
|
+
if (found.format === 'opencode') {
|
|
535
|
+
const detail = loadOpenCodeDetail(sessionId);
|
|
536
|
+
return detail.messages.slice(0, limit).map(function(m) {
|
|
537
|
+
return { role: m.role, content: m.content.slice(0, 300) };
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
405
541
|
const messages = [];
|
|
406
542
|
const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
|
|
407
543
|
|
|
@@ -832,8 +968,10 @@ module.exports = {
|
|
|
832
968
|
findSessionFile,
|
|
833
969
|
extractContent,
|
|
834
970
|
isSystemMessage,
|
|
971
|
+
loadOpenCodeDetail,
|
|
835
972
|
CLAUDE_DIR,
|
|
836
973
|
CODEX_DIR,
|
|
974
|
+
OPENCODE_DB,
|
|
837
975
|
HISTORY_FILE,
|
|
838
976
|
PROJECTS_DIR,
|
|
839
977
|
};
|
package/src/frontend/app.js
CHANGED
|
@@ -413,6 +413,9 @@ function setView(view) {
|
|
|
413
413
|
} else if (view === 'codex-only') {
|
|
414
414
|
toolFilter = toolFilter === 'codex' ? null : 'codex';
|
|
415
415
|
currentView = 'sessions';
|
|
416
|
+
} else if (view === 'opencode-only') {
|
|
417
|
+
toolFilter = toolFilter === 'opencode' ? null : 'opencode';
|
|
418
|
+
currentView = 'sessions';
|
|
416
419
|
} else {
|
|
417
420
|
toolFilter = null;
|
|
418
421
|
currentView = view;
|
|
@@ -444,7 +447,7 @@ function renderCard(s, idx) {
|
|
|
444
447
|
var costStr = cost > 0 ? '~$' + cost.toFixed(2) : '';
|
|
445
448
|
var projName = getProjectName(s.project);
|
|
446
449
|
var projColor = getProjectColor(projName);
|
|
447
|
-
var toolClass = s.tool === 'codex' ? 'tool-codex' : 'tool-claude';
|
|
450
|
+
var toolClass = s.tool === 'codex' ? 'tool-codex' : s.tool === 'opencode' ? 'tool-opencode' : 'tool-claude';
|
|
448
451
|
|
|
449
452
|
var classes = 'card';
|
|
450
453
|
if (isSelected) classes += ' selected';
|
package/src/frontend/index.html
CHANGED
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
<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>
|
|
53
53
|
Codex
|
|
54
54
|
</div>
|
|
55
|
+
<div class="sidebar-item" data-view="opencode-only">
|
|
56
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
|
57
|
+
OpenCode
|
|
58
|
+
</div>
|
|
55
59
|
<div class="sidebar-divider"></div>
|
|
56
60
|
<div class="sidebar-section">Install Agents</div>
|
|
57
61
|
<div class="sidebar-item small" onclick="installAgent('claude')">
|
package/src/frontend/styles.css
CHANGED
|
@@ -1041,6 +1041,11 @@ body {
|
|
|
1041
1041
|
color: var(--accent-cyan);
|
|
1042
1042
|
}
|
|
1043
1043
|
|
|
1044
|
+
.tool-opencode {
|
|
1045
|
+
background: rgba(192, 132, 252, 0.15);
|
|
1046
|
+
color: var(--accent-purple);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1044
1049
|
/* ── Groups ─────────────────────────────────────────────────── */
|
|
1045
1050
|
|
|
1046
1051
|
.group {
|
|
@@ -1096,6 +1101,12 @@ body {
|
|
|
1096
1101
|
|
|
1097
1102
|
.heatmap-container {
|
|
1098
1103
|
padding: 20px;
|
|
1104
|
+
overflow-x: auto;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.heatmap-container .heatmap-row,
|
|
1108
|
+
.heatmap-container .heatmap-months {
|
|
1109
|
+
min-width: max-content;
|
|
1099
1110
|
}
|
|
1100
1111
|
|
|
1101
1112
|
.heatmap-title {
|