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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codedash-app",
3
- "version": "4.1.0",
3
+ "version": "4.2.1",
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
@@ -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
- s.date = new Date(s.last_ts).toISOString().slice(0, 10);
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
  };
@@ -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';
@@ -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')">
@@ -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 {