codedash-app 4.0.0 → 4.2.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/changelog.js +161 -0
- package/src/data.js +137 -0
- package/src/frontend/app.js +50 -1
- package/src/frontend/index.html +8 -0
- package/src/frontend/styles.css +92 -0
- package/src/server.js +6 -0
package/package.json
CHANGED
package/src/changelog.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CHANGELOG = [
|
|
4
|
+
{
|
|
5
|
+
version: '4.0.0',
|
|
6
|
+
date: '2026-04-04',
|
|
7
|
+
title: 'Cross-agent session conversion',
|
|
8
|
+
changes: [
|
|
9
|
+
'Convert sessions between Claude Code and Codex CLI',
|
|
10
|
+
'CLI: codedash convert <id> claude/codex',
|
|
11
|
+
'Convert button in session detail panel',
|
|
12
|
+
'Atomic writes for safety',
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
version: '3.4.0',
|
|
17
|
+
date: '2026-04-04',
|
|
18
|
+
title: 'CLI search, show, docs',
|
|
19
|
+
changes: [
|
|
20
|
+
'codedash search <query> — full-text search from terminal',
|
|
21
|
+
'codedash show <id> — session details with cost and messages',
|
|
22
|
+
'CLAUDE.md and architecture documentation',
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
version: '3.3.0',
|
|
27
|
+
date: '2026-04-04',
|
|
28
|
+
title: 'Install Agents, author credit',
|
|
29
|
+
changes: [
|
|
30
|
+
'Install Agents section: Claude Code, Codex, Kiro, OpenCode',
|
|
31
|
+
'One-click copy install commands',
|
|
32
|
+
'Author credit in sidebar',
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
version: '3.2.0',
|
|
37
|
+
date: '2026-04-04',
|
|
38
|
+
title: 'Real cost calculation',
|
|
39
|
+
changes: [
|
|
40
|
+
'Real cost from actual token usage (not file size estimates)',
|
|
41
|
+
'Model-specific pricing: Opus, Sonnet, Haiku, Codex, GPT-5',
|
|
42
|
+
'Cache pricing: cache_read 90% discount, cache_create 25% premium',
|
|
43
|
+
'Detail panel shows real cost with model and token breakdown',
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
version: '3.1.0',
|
|
48
|
+
date: '2026-04-04',
|
|
49
|
+
title: 'Running sessions view',
|
|
50
|
+
changes: [
|
|
51
|
+
'New "Running" sidebar view with grid layout',
|
|
52
|
+
'CPU, Memory, PID, Uptime for each active session',
|
|
53
|
+
'Focus, Details, Replay buttons',
|
|
54
|
+
'Recently inactive sessions shown below',
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
version: '3.0.0',
|
|
59
|
+
date: '2026-04-04',
|
|
60
|
+
title: 'Session Replay, Cost Analytics',
|
|
61
|
+
changes: [
|
|
62
|
+
'Session Replay: timeline slider, play/pause, progressive messages',
|
|
63
|
+
'Cost Analytics dashboard: daily chart, project bars, top sessions',
|
|
64
|
+
'Focus Terminal button for active sessions',
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
version: '2.1.0',
|
|
69
|
+
date: '2026-04-04',
|
|
70
|
+
title: 'Animated border on live cards',
|
|
71
|
+
changes: [
|
|
72
|
+
'Conic gradient border spins around LIVE cards',
|
|
73
|
+
'WAITING cards: static border',
|
|
74
|
+
'Pulsing dot + LIVE/WAITING badges',
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
version: '2.0.0',
|
|
79
|
+
date: '2026-04-04',
|
|
80
|
+
title: 'Live session detection',
|
|
81
|
+
changes: [
|
|
82
|
+
'Detect running Claude/Codex processes via PID files',
|
|
83
|
+
'LIVE (green) and WAITING (yellow) badges with pulse animation',
|
|
84
|
+
'CPU%, Memory, PID shown on hover',
|
|
85
|
+
'Polling every 5 seconds',
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
version: '1.9.0',
|
|
90
|
+
date: '2026-04-03',
|
|
91
|
+
title: 'Tags fix, search index, Export/Import UI',
|
|
92
|
+
changes: [
|
|
93
|
+
'Fixed tag dropdown positioning',
|
|
94
|
+
'In-memory search index (263ms build, 60s cache)',
|
|
95
|
+
'Export/Import dialog in sidebar',
|
|
96
|
+
'codedash update/restart/stop commands',
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
version: '1.6.0',
|
|
101
|
+
date: '2026-04-03',
|
|
102
|
+
title: 'Message extraction fix',
|
|
103
|
+
changes: [
|
|
104
|
+
'Fixed message extraction for both Claude and Codex',
|
|
105
|
+
'Hover preview and expand cards working',
|
|
106
|
+
'Version badge in sidebar',
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
version: '1.5.0',
|
|
111
|
+
date: '2026-04-03',
|
|
112
|
+
title: 'Deep search, hover preview, expandable cards',
|
|
113
|
+
changes: [
|
|
114
|
+
'Full-text search across all session messages',
|
|
115
|
+
'Hover tooltip with first 6 messages',
|
|
116
|
+
'Expand cards inline with first 10 messages',
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
version: '1.4.0',
|
|
121
|
+
date: '2026-04-03',
|
|
122
|
+
title: 'Export/Import for PC migration',
|
|
123
|
+
changes: [
|
|
124
|
+
'codedash export — archive all sessions as tar.gz',
|
|
125
|
+
'codedash import — restore on new machine',
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
version: '1.3.0',
|
|
130
|
+
date: '2026-04-03',
|
|
131
|
+
title: 'Trigram fuzzy search',
|
|
132
|
+
changes: [
|
|
133
|
+
'Fuzzy search with trigram scoring',
|
|
134
|
+
'Results ranked by relevance',
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
version: '1.1.0',
|
|
139
|
+
date: '2026-04-03',
|
|
140
|
+
title: 'Grid/List toggle, Codex support',
|
|
141
|
+
changes: [
|
|
142
|
+
'Grid/List layout switcher',
|
|
143
|
+
'Codex session parsing fixed',
|
|
144
|
+
'Project navigation from Projects view',
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
version: '1.0.0',
|
|
149
|
+
date: '2026-04-03',
|
|
150
|
+
title: 'Initial release',
|
|
151
|
+
changes: [
|
|
152
|
+
'Session dashboard with dark theme',
|
|
153
|
+
'Group by project, timeline, activity heatmap',
|
|
154
|
+
'Star, tag, delete sessions',
|
|
155
|
+
'Resume in iTerm2/Terminal.app',
|
|
156
|
+
'Terminal selector, theme switcher',
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
module.exports = { CHANGELOG };
|
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;
|
|
@@ -195,6 +312,11 @@ function loadSessionDetail(sessionId, project) {
|
|
|
195
312
|
const found = findSessionFile(sessionId, project);
|
|
196
313
|
if (!found) return { error: 'Session file not found', messages: [] };
|
|
197
314
|
|
|
315
|
+
// OpenCode uses SQLite
|
|
316
|
+
if (found.format === 'opencode') {
|
|
317
|
+
return loadOpenCodeDetail(sessionId);
|
|
318
|
+
}
|
|
319
|
+
|
|
198
320
|
const messages = [];
|
|
199
321
|
const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
|
|
200
322
|
|
|
@@ -367,6 +489,11 @@ function findSessionFile(sessionId, project) {
|
|
|
367
489
|
if (codexFile) return { file: codexFile, format: 'codex' };
|
|
368
490
|
}
|
|
369
491
|
|
|
492
|
+
// Try OpenCode (SQLite — return special marker)
|
|
493
|
+
if (fs.existsSync(OPENCODE_DB) && sessionId.startsWith('ses_')) {
|
|
494
|
+
return { file: OPENCODE_DB, format: 'opencode', sessionId: sessionId };
|
|
495
|
+
}
|
|
496
|
+
|
|
370
497
|
return null;
|
|
371
498
|
}
|
|
372
499
|
|
|
@@ -402,6 +529,14 @@ function getSessionPreview(sessionId, project, limit) {
|
|
|
402
529
|
const found = findSessionFile(sessionId, project);
|
|
403
530
|
if (!found) return [];
|
|
404
531
|
|
|
532
|
+
// OpenCode: use loadOpenCodeDetail and slice
|
|
533
|
+
if (found.format === 'opencode') {
|
|
534
|
+
const detail = loadOpenCodeDetail(sessionId);
|
|
535
|
+
return detail.messages.slice(0, limit).map(function(m) {
|
|
536
|
+
return { role: m.role, content: m.content.slice(0, 300) };
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
405
540
|
const messages = [];
|
|
406
541
|
const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
|
|
407
542
|
|
|
@@ -832,8 +967,10 @@ module.exports = {
|
|
|
832
967
|
findSessionFile,
|
|
833
968
|
extractContent,
|
|
834
969
|
isSystemMessage,
|
|
970
|
+
loadOpenCodeDetail,
|
|
835
971
|
CLAUDE_DIR,
|
|
836
972
|
CODEX_DIR,
|
|
973
|
+
OPENCODE_DB,
|
|
837
974
|
HISTORY_FILE,
|
|
838
975
|
PROJECTS_DIR,
|
|
839
976
|
};
|
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';
|
|
@@ -732,6 +735,11 @@ function render() {
|
|
|
732
735
|
return;
|
|
733
736
|
}
|
|
734
737
|
|
|
738
|
+
if (currentView === 'changelog') {
|
|
739
|
+
renderChangelog(content);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
735
743
|
if (currentView === 'running') {
|
|
736
744
|
renderRunning(content, sessions);
|
|
737
745
|
return;
|
|
@@ -1746,6 +1754,40 @@ function focusSession(sessionId) {
|
|
|
1746
1754
|
});
|
|
1747
1755
|
}
|
|
1748
1756
|
|
|
1757
|
+
// ── Changelog view ────────────────────────────────────────────
|
|
1758
|
+
|
|
1759
|
+
async function renderChangelog(container) {
|
|
1760
|
+
container.innerHTML = '<div class="loading">Loading changelog...</div>';
|
|
1761
|
+
try {
|
|
1762
|
+
var resp = await fetch('/api/changelog');
|
|
1763
|
+
var log = await resp.json();
|
|
1764
|
+
|
|
1765
|
+
var html = '<div class="changelog-container">';
|
|
1766
|
+
html += '<h2 class="heatmap-title">Changelog</h2>';
|
|
1767
|
+
|
|
1768
|
+
log.forEach(function(entry, i) {
|
|
1769
|
+
var isNew = i === 0;
|
|
1770
|
+
html += '<div class="changelog-entry' + (isNew ? ' changelog-latest' : '') + '">';
|
|
1771
|
+
html += '<div class="changelog-header">';
|
|
1772
|
+
html += '<span class="changelog-version">v' + escHtml(entry.version) + '</span>';
|
|
1773
|
+
if (isNew) html += '<span class="changelog-new">NEW</span>';
|
|
1774
|
+
html += '<span class="changelog-date">' + escHtml(entry.date) + '</span>';
|
|
1775
|
+
html += '</div>';
|
|
1776
|
+
html += '<div class="changelog-title">' + escHtml(entry.title) + '</div>';
|
|
1777
|
+
html += '<ul class="changelog-list">';
|
|
1778
|
+
entry.changes.forEach(function(c) {
|
|
1779
|
+
html += '<li>' + escHtml(c) + '</li>';
|
|
1780
|
+
});
|
|
1781
|
+
html += '</ul></div>';
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
html += '</div>';
|
|
1785
|
+
container.innerHTML = html;
|
|
1786
|
+
} catch (e) {
|
|
1787
|
+
container.innerHTML = '<div class="empty-state">Failed to load changelog.</div>';
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1749
1791
|
// ── Convert session ───────────────────────────────────────────
|
|
1750
1792
|
|
|
1751
1793
|
async function convertTo(sessionId, project, targetFormat) {
|
|
@@ -1860,6 +1902,13 @@ async function checkForUpdates() {
|
|
|
1860
1902
|
badge.textContent = 'v' + data.current;
|
|
1861
1903
|
}
|
|
1862
1904
|
|
|
1905
|
+
// Show "what's new" if version changed since last visit
|
|
1906
|
+
var lastSeenVersion = localStorage.getItem('codedash-last-version');
|
|
1907
|
+
if (lastSeenVersion && lastSeenVersion !== data.current) {
|
|
1908
|
+
showToast('Updated to v' + data.current + ' — check Changelog!');
|
|
1909
|
+
}
|
|
1910
|
+
localStorage.setItem('codedash-last-version', data.current);
|
|
1911
|
+
|
|
1863
1912
|
if (data.updateAvailable) {
|
|
1864
1913
|
if (badge) {
|
|
1865
1914
|
badge.textContent = 'v' + data.current + ' → v' + data.latest;
|
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')">
|
|
@@ -75,6 +79,10 @@
|
|
|
75
79
|
<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>
|
|
76
80
|
Export / Import
|
|
77
81
|
</div>
|
|
82
|
+
<div class="sidebar-item small" data-view="changelog">
|
|
83
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
|
84
|
+
Changelog
|
|
85
|
+
</div>
|
|
78
86
|
<div class="sidebar-author">
|
|
79
87
|
Made by <a href="https://t.me/neuraldeep" target="_blank">Valerii Kovalskii</a>
|
|
80
88
|
</div>
|
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 {
|
|
@@ -1596,6 +1601,93 @@ body {
|
|
|
1596
1601
|
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
|
1597
1602
|
}
|
|
1598
1603
|
|
|
1604
|
+
/* ── Changelog ──────────────────────────────────────────────── */
|
|
1605
|
+
|
|
1606
|
+
.changelog-container { padding: 20px; max-width: 700px; }
|
|
1607
|
+
|
|
1608
|
+
.changelog-entry {
|
|
1609
|
+
border-left: 2px solid var(--border);
|
|
1610
|
+
padding: 0 0 24px 20px;
|
|
1611
|
+
margin-left: 8px;
|
|
1612
|
+
position: relative;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
.changelog-entry::before {
|
|
1616
|
+
content: '';
|
|
1617
|
+
position: absolute;
|
|
1618
|
+
left: -5px;
|
|
1619
|
+
top: 4px;
|
|
1620
|
+
width: 8px;
|
|
1621
|
+
height: 8px;
|
|
1622
|
+
border-radius: 50%;
|
|
1623
|
+
background: var(--text-muted);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
.changelog-latest {
|
|
1627
|
+
border-left-color: var(--accent-green);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
.changelog-latest::before {
|
|
1631
|
+
background: var(--accent-green);
|
|
1632
|
+
box-shadow: 0 0 6px var(--accent-green);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
.changelog-header {
|
|
1636
|
+
display: flex;
|
|
1637
|
+
align-items: center;
|
|
1638
|
+
gap: 8px;
|
|
1639
|
+
margin-bottom: 4px;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
.changelog-version {
|
|
1643
|
+
font-size: 15px;
|
|
1644
|
+
font-weight: 700;
|
|
1645
|
+
color: var(--text-primary);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
.changelog-new {
|
|
1649
|
+
font-size: 9px;
|
|
1650
|
+
font-weight: 700;
|
|
1651
|
+
letter-spacing: 0.5px;
|
|
1652
|
+
padding: 2px 6px;
|
|
1653
|
+
border-radius: 4px;
|
|
1654
|
+
background: rgba(74, 222, 128, 0.2);
|
|
1655
|
+
color: var(--accent-green);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
.changelog-date {
|
|
1659
|
+
font-size: 12px;
|
|
1660
|
+
color: var(--text-muted);
|
|
1661
|
+
margin-left: auto;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
.changelog-title {
|
|
1665
|
+
font-size: 14px;
|
|
1666
|
+
font-weight: 600;
|
|
1667
|
+
color: var(--text-secondary);
|
|
1668
|
+
margin-bottom: 6px;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
.changelog-list {
|
|
1672
|
+
list-style: none;
|
|
1673
|
+
padding: 0;
|
|
1674
|
+
margin: 0;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
.changelog-list li {
|
|
1678
|
+
font-size: 13px;
|
|
1679
|
+
color: var(--text-secondary);
|
|
1680
|
+
padding: 2px 0;
|
|
1681
|
+
line-height: 1.5;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
.changelog-list li::before {
|
|
1685
|
+
content: '+';
|
|
1686
|
+
color: var(--accent-green);
|
|
1687
|
+
font-weight: 700;
|
|
1688
|
+
margin-right: 8px;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1599
1691
|
/* ── Running Sessions View ──────────────────────────────────── */
|
|
1600
1692
|
|
|
1601
1693
|
.running-container { padding: 20px; }
|
package/src/server.js
CHANGED
|
@@ -6,6 +6,7 @@ const { exec } = require('child_process');
|
|
|
6
6
|
const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText, getActiveSessions, getSessionReplay, getCostAnalytics, computeSessionCost } = require('./data');
|
|
7
7
|
const { detectTerminals, openInTerminal, focusTerminalByPid } = require('./terminals');
|
|
8
8
|
const { convertSession } = require('./convert');
|
|
9
|
+
const { CHANGELOG } = require('./changelog');
|
|
9
10
|
const { getHTML } = require('./html');
|
|
10
11
|
|
|
11
12
|
function startServer(port, openBrowser = true) {
|
|
@@ -177,6 +178,11 @@ function startServer(port, openBrowser = true) {
|
|
|
177
178
|
json(res, data);
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
// ── Changelog ─────────────────────────────
|
|
182
|
+
else if (req.method === 'GET' && pathname === '/api/changelog') {
|
|
183
|
+
json(res, CHANGELOG);
|
|
184
|
+
}
|
|
185
|
+
|
|
180
186
|
// ── Version check ────────────────────────
|
|
181
187
|
else if (req.method === 'GET' && pathname === '/api/version') {
|
|
182
188
|
const pkg = require('../package.json');
|