codedash-app 3.4.0 → 4.1.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/bin/cli.js +40 -0
- package/package.json +1 -1
- package/src/changelog.js +161 -0
- package/src/convert.js +273 -0
- package/src/data.js +3 -0
- package/src/frontend/app.js +73 -0
- package/src/frontend/index.html +4 -0
- package/src/frontend/styles.css +87 -0
- package/src/server.js +20 -0
package/bin/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { loadSessions, searchFullText, getSessionPreview, computeSessionCost } = require('../src/data');
|
|
4
4
|
const { startServer } = require('../src/server');
|
|
5
5
|
const { exportArchive, importArchive } = require('../src/migrate');
|
|
6
|
+
const { convertSession } = require('../src/convert');
|
|
6
7
|
|
|
7
8
|
const DEFAULT_PORT = 3847;
|
|
8
9
|
const args = process.argv.slice(2);
|
|
@@ -130,6 +131,44 @@ switch (command) {
|
|
|
130
131
|
break;
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
case 'convert': {
|
|
135
|
+
const sid = args[1];
|
|
136
|
+
const target = args[2]; // 'claude' or 'codex'
|
|
137
|
+
if (!sid || !target) {
|
|
138
|
+
console.log(`
|
|
139
|
+
\x1b[36m\x1b[1mConvert session between agents\x1b[0m
|
|
140
|
+
|
|
141
|
+
Usage: codedash convert <session-id> <target-format>
|
|
142
|
+
|
|
143
|
+
Formats: claude, codex
|
|
144
|
+
|
|
145
|
+
Examples:
|
|
146
|
+
codedash convert 019d54ed codex Convert Claude session to Codex
|
|
147
|
+
codedash convert 13ae5748 claude Convert Codex session to Claude
|
|
148
|
+
`);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
// Find full session ID
|
|
152
|
+
const allConv = loadSessions();
|
|
153
|
+
const match = allConv.find(s => s.id === sid || s.id.startsWith(sid));
|
|
154
|
+
if (!match) {
|
|
155
|
+
console.error(` Session not found: ${sid}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
console.log(`\n Converting ${match.tool} session \x1b[1m${match.id.slice(0, 12)}\x1b[0m → ${target}...`);
|
|
159
|
+
const result = convertSession(match.id, match.project, target);
|
|
160
|
+
if (!result.ok) {
|
|
161
|
+
console.error(` \x1b[31mError:\x1b[0m ${result.error}\n`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
console.log(` \x1b[32mDone!\x1b[0m`);
|
|
165
|
+
console.log(` New session: ${result.target.sessionId}`);
|
|
166
|
+
console.log(` Messages: ${result.target.messages}`);
|
|
167
|
+
console.log(` File: ${result.target.file}`);
|
|
168
|
+
console.log(` Resume: \x1b[2m${result.target.resumeCmd}\x1b[0m\n`);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
133
172
|
case 'update':
|
|
134
173
|
case 'upgrade': {
|
|
135
174
|
const { execSync: execU } = require('child_process');
|
|
@@ -214,6 +253,7 @@ switch (command) {
|
|
|
214
253
|
codedash show <session-id> Show session details + messages
|
|
215
254
|
codedash list [limit] List sessions in terminal
|
|
216
255
|
codedash stats Show session statistics
|
|
256
|
+
codedash convert <id> <format> Convert session (claude/codex)
|
|
217
257
|
codedash export [file.tar.gz] Export all sessions to archive
|
|
218
258
|
codedash import <file.tar.gz> Import sessions from archive
|
|
219
259
|
codedash update Update to latest version
|
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/convert.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { findSessionFile, extractContent, isSystemMessage } = require('./data');
|
|
8
|
+
|
|
9
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
10
|
+
const CODEX_DIR = path.join(os.homedir(), '.codex');
|
|
11
|
+
|
|
12
|
+
// ── Read session into canonical format ────────────────────
|
|
13
|
+
|
|
14
|
+
function readSession(sessionId, project) {
|
|
15
|
+
const found = findSessionFile(sessionId, project);
|
|
16
|
+
if (!found) return null;
|
|
17
|
+
|
|
18
|
+
const messages = [];
|
|
19
|
+
const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
|
|
20
|
+
let sessionMeta = {};
|
|
21
|
+
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
try {
|
|
24
|
+
const entry = JSON.parse(line);
|
|
25
|
+
|
|
26
|
+
if (found.format === 'claude') {
|
|
27
|
+
if (entry.type === 'permission-mode') {
|
|
28
|
+
sessionMeta.permissionMode = entry.permissionMode;
|
|
29
|
+
sessionMeta.originalSessionId = entry.sessionId;
|
|
30
|
+
}
|
|
31
|
+
if (entry.type === 'user' || entry.type === 'assistant') {
|
|
32
|
+
const msg = entry.message || {};
|
|
33
|
+
let content = '';
|
|
34
|
+
if (typeof msg.content === 'string') {
|
|
35
|
+
content = msg.content;
|
|
36
|
+
} else if (Array.isArray(msg.content)) {
|
|
37
|
+
content = msg.content
|
|
38
|
+
.filter(b => b.type === 'text')
|
|
39
|
+
.map(b => b.text)
|
|
40
|
+
.join('\n');
|
|
41
|
+
}
|
|
42
|
+
if (!content || isSystemMessage(content)) continue;
|
|
43
|
+
|
|
44
|
+
messages.push({
|
|
45
|
+
role: entry.type === 'user' ? 'user' : 'assistant',
|
|
46
|
+
content: content,
|
|
47
|
+
timestamp: entry.timestamp || '',
|
|
48
|
+
model: msg.model || '',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Codex
|
|
53
|
+
if (entry.type === 'session_meta' && entry.payload) {
|
|
54
|
+
sessionMeta.cwd = entry.payload.cwd;
|
|
55
|
+
sessionMeta.originalSessionId = entry.payload.id;
|
|
56
|
+
}
|
|
57
|
+
if (entry.type === 'response_item' && entry.payload) {
|
|
58
|
+
const role = entry.payload.role;
|
|
59
|
+
if (role !== 'user' && role !== 'assistant') continue;
|
|
60
|
+
const content = extractContent(entry.payload.content);
|
|
61
|
+
if (!content || isSystemMessage(content)) continue;
|
|
62
|
+
|
|
63
|
+
messages.push({
|
|
64
|
+
role: role,
|
|
65
|
+
content: content,
|
|
66
|
+
timestamp: entry.timestamp || '',
|
|
67
|
+
model: '',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
sourceFormat: found.format,
|
|
76
|
+
sourceFile: found.file,
|
|
77
|
+
sessionId: sessionId,
|
|
78
|
+
meta: sessionMeta,
|
|
79
|
+
messages: messages,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Write as Claude Code session ──────────────────────────
|
|
84
|
+
|
|
85
|
+
function writeClaude(canonical, targetProject) {
|
|
86
|
+
const newSessionId = crypto.randomUUID();
|
|
87
|
+
const projectKey = (targetProject || os.homedir()).replace(/[\/\.]/g, '-');
|
|
88
|
+
const projectDir = path.join(CLAUDE_DIR, 'projects', projectKey);
|
|
89
|
+
|
|
90
|
+
if (!fs.existsSync(projectDir)) {
|
|
91
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const outFile = path.join(projectDir, `${newSessionId}.jsonl`);
|
|
95
|
+
const cwd = targetProject || canonical.meta.cwd || os.homedir();
|
|
96
|
+
const lines = [];
|
|
97
|
+
|
|
98
|
+
// Permission mode entry
|
|
99
|
+
lines.push(JSON.stringify({
|
|
100
|
+
type: 'permission-mode',
|
|
101
|
+
permissionMode: 'default',
|
|
102
|
+
sessionId: newSessionId,
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
let prevUuid = null;
|
|
106
|
+
|
|
107
|
+
for (const msg of canonical.messages) {
|
|
108
|
+
const uuid = crypto.randomUUID();
|
|
109
|
+
const entry = {
|
|
110
|
+
parentUuid: prevUuid,
|
|
111
|
+
isSidechain: false,
|
|
112
|
+
type: msg.role === 'user' ? 'user' : 'assistant',
|
|
113
|
+
uuid: uuid,
|
|
114
|
+
timestamp: msg.timestamp || new Date().toISOString(),
|
|
115
|
+
userType: 'external',
|
|
116
|
+
entrypoint: 'cli',
|
|
117
|
+
cwd: cwd,
|
|
118
|
+
sessionId: newSessionId,
|
|
119
|
+
version: '2.1.91',
|
|
120
|
+
gitBranch: 'main',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (msg.role === 'user') {
|
|
124
|
+
entry.message = { role: 'user', content: msg.content };
|
|
125
|
+
entry.promptId = crypto.randomUUID();
|
|
126
|
+
} else {
|
|
127
|
+
entry.message = {
|
|
128
|
+
model: msg.model || 'claude-sonnet-4-6',
|
|
129
|
+
id: 'msg_converted_' + uuid.slice(0, 8),
|
|
130
|
+
type: 'message',
|
|
131
|
+
role: 'assistant',
|
|
132
|
+
content: [{ type: 'text', text: msg.content }],
|
|
133
|
+
stop_reason: 'end_turn',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
lines.push(JSON.stringify(entry));
|
|
138
|
+
prevUuid = uuid;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Write atomically
|
|
142
|
+
const tmpFile = outFile + '.tmp';
|
|
143
|
+
fs.writeFileSync(tmpFile, lines.join('\n') + '\n');
|
|
144
|
+
fs.renameSync(tmpFile, outFile);
|
|
145
|
+
|
|
146
|
+
// Add to history.jsonl
|
|
147
|
+
const historyFile = path.join(CLAUDE_DIR, 'history.jsonl');
|
|
148
|
+
const historyEntry = JSON.stringify({
|
|
149
|
+
sessionId: newSessionId,
|
|
150
|
+
project: cwd,
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
display: `[Converted from ${canonical.sourceFormat}] ${canonical.messages[0]?.content?.slice(0, 100) || ''}`,
|
|
153
|
+
pastedContents: {},
|
|
154
|
+
});
|
|
155
|
+
fs.appendFileSync(historyFile, historyEntry + '\n');
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
sessionId: newSessionId,
|
|
159
|
+
file: outFile,
|
|
160
|
+
format: 'claude',
|
|
161
|
+
messages: canonical.messages.length,
|
|
162
|
+
resumeCmd: `claude --resume ${newSessionId}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Write as Codex session ────────────────────────────────
|
|
167
|
+
|
|
168
|
+
function writeCodex(canonical, targetProject) {
|
|
169
|
+
const newSessionId = crypto.randomUUID();
|
|
170
|
+
const now = new Date();
|
|
171
|
+
const dateDir = path.join(
|
|
172
|
+
CODEX_DIR, 'sessions',
|
|
173
|
+
String(now.getFullYear()),
|
|
174
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
175
|
+
String(now.getDate()).padStart(2, '0')
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!fs.existsSync(dateDir)) {
|
|
179
|
+
fs.mkdirSync(dateDir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const fileName = `rollout-${now.toISOString().replace(/[:.]/g, '-').slice(0, 19)}-${newSessionId}.jsonl`;
|
|
183
|
+
const outFile = path.join(dateDir, fileName);
|
|
184
|
+
const cwd = targetProject || canonical.meta.cwd || os.homedir();
|
|
185
|
+
const lines = [];
|
|
186
|
+
|
|
187
|
+
// Session meta
|
|
188
|
+
lines.push(JSON.stringify({
|
|
189
|
+
timestamp: now.toISOString(),
|
|
190
|
+
type: 'session_meta',
|
|
191
|
+
payload: {
|
|
192
|
+
id: newSessionId,
|
|
193
|
+
timestamp: now.toISOString(),
|
|
194
|
+
cwd: cwd,
|
|
195
|
+
originator: 'codex_cli_rs',
|
|
196
|
+
cli_version: '0.101.0',
|
|
197
|
+
source: 'cli',
|
|
198
|
+
model_provider: 'openai',
|
|
199
|
+
},
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
// Messages
|
|
203
|
+
for (const msg of canonical.messages) {
|
|
204
|
+
lines.push(JSON.stringify({
|
|
205
|
+
timestamp: msg.timestamp || now.toISOString(),
|
|
206
|
+
type: 'response_item',
|
|
207
|
+
payload: {
|
|
208
|
+
type: 'message',
|
|
209
|
+
role: msg.role,
|
|
210
|
+
content: [{ type: 'input_text', text: msg.content }],
|
|
211
|
+
},
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Write atomically
|
|
216
|
+
const tmpFile = outFile + '.tmp';
|
|
217
|
+
fs.writeFileSync(tmpFile, lines.join('\n') + '\n');
|
|
218
|
+
fs.renameSync(tmpFile, outFile);
|
|
219
|
+
|
|
220
|
+
// Add to codex history
|
|
221
|
+
const historyFile = path.join(CODEX_DIR, 'history.jsonl');
|
|
222
|
+
if (!fs.existsSync(path.dirname(historyFile))) {
|
|
223
|
+
fs.mkdirSync(path.dirname(historyFile), { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
const historyEntry = JSON.stringify({
|
|
226
|
+
session_id: newSessionId,
|
|
227
|
+
ts: Math.floor(Date.now() / 1000),
|
|
228
|
+
text: `[Converted from ${canonical.sourceFormat}] ${canonical.messages[0]?.content?.slice(0, 100) || ''}`,
|
|
229
|
+
});
|
|
230
|
+
fs.appendFileSync(historyFile, historyEntry + '\n');
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
sessionId: newSessionId,
|
|
234
|
+
file: outFile,
|
|
235
|
+
format: 'codex',
|
|
236
|
+
messages: canonical.messages.length,
|
|
237
|
+
resumeCmd: `codex resume ${newSessionId}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Main convert function ─────────────────────────────────
|
|
242
|
+
|
|
243
|
+
function convertSession(sessionId, project, targetFormat) {
|
|
244
|
+
const canonical = readSession(sessionId, project);
|
|
245
|
+
if (!canonical) {
|
|
246
|
+
return { ok: false, error: 'Session not found' };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (canonical.sourceFormat === targetFormat) {
|
|
250
|
+
return { ok: false, error: `Session is already in ${targetFormat} format` };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (canonical.messages.length === 0) {
|
|
254
|
+
return { ok: false, error: 'Session has no messages to convert' };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let result;
|
|
258
|
+
if (targetFormat === 'claude') {
|
|
259
|
+
result = writeClaude(canonical, project);
|
|
260
|
+
} else if (targetFormat === 'codex') {
|
|
261
|
+
result = writeCodex(canonical, project);
|
|
262
|
+
} else {
|
|
263
|
+
return { ok: false, error: `Unknown target format: ${targetFormat}` };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
source: { format: canonical.sourceFormat, sessionId: sessionId, messages: canonical.messages.length },
|
|
269
|
+
target: result,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = { convertSession, readSession };
|
package/src/data.js
CHANGED
package/src/frontend/app.js
CHANGED
|
@@ -732,6 +732,11 @@ function render() {
|
|
|
732
732
|
return;
|
|
733
733
|
}
|
|
734
734
|
|
|
735
|
+
if (currentView === 'changelog') {
|
|
736
|
+
renderChangelog(content);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
735
740
|
if (currentView === 'running') {
|
|
736
741
|
renderRunning(content, sessions);
|
|
737
742
|
return;
|
|
@@ -1081,6 +1086,8 @@ async function openDetail(s) {
|
|
|
1081
1086
|
if (s.has_detail) {
|
|
1082
1087
|
infoHtml += '<button class="launch-btn btn-secondary" onclick="closeDetail();openReplay(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Replay</button>';
|
|
1083
1088
|
infoHtml += '<button class="launch-btn btn-secondary" onclick="exportMd(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Export MD</button>';
|
|
1089
|
+
var convertTarget = s.tool === 'codex' ? 'claude' : 'codex';
|
|
1090
|
+
infoHtml += '<button class="launch-btn btn-secondary" onclick="convertTo(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\',\'' + convertTarget + '\')">Convert to ' + convertTarget + '</button>';
|
|
1084
1091
|
}
|
|
1085
1092
|
infoHtml += '<button class="star-btn detail-star' + (isStarred ? ' active' : '') + '" onclick="toggleStar(\'' + s.id + '\')">★ ' + (isStarred ? 'Starred' : 'Star') + '</button>';
|
|
1086
1093
|
infoHtml += '<button class="launch-btn btn-delete" onclick="showDeleteConfirm(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Delete</button>';
|
|
@@ -1744,6 +1751,65 @@ function focusSession(sessionId) {
|
|
|
1744
1751
|
});
|
|
1745
1752
|
}
|
|
1746
1753
|
|
|
1754
|
+
// ── Changelog view ────────────────────────────────────────────
|
|
1755
|
+
|
|
1756
|
+
async function renderChangelog(container) {
|
|
1757
|
+
container.innerHTML = '<div class="loading">Loading changelog...</div>';
|
|
1758
|
+
try {
|
|
1759
|
+
var resp = await fetch('/api/changelog');
|
|
1760
|
+
var log = await resp.json();
|
|
1761
|
+
|
|
1762
|
+
var html = '<div class="changelog-container">';
|
|
1763
|
+
html += '<h2 class="heatmap-title">Changelog</h2>';
|
|
1764
|
+
|
|
1765
|
+
log.forEach(function(entry, i) {
|
|
1766
|
+
var isNew = i === 0;
|
|
1767
|
+
html += '<div class="changelog-entry' + (isNew ? ' changelog-latest' : '') + '">';
|
|
1768
|
+
html += '<div class="changelog-header">';
|
|
1769
|
+
html += '<span class="changelog-version">v' + escHtml(entry.version) + '</span>';
|
|
1770
|
+
if (isNew) html += '<span class="changelog-new">NEW</span>';
|
|
1771
|
+
html += '<span class="changelog-date">' + escHtml(entry.date) + '</span>';
|
|
1772
|
+
html += '</div>';
|
|
1773
|
+
html += '<div class="changelog-title">' + escHtml(entry.title) + '</div>';
|
|
1774
|
+
html += '<ul class="changelog-list">';
|
|
1775
|
+
entry.changes.forEach(function(c) {
|
|
1776
|
+
html += '<li>' + escHtml(c) + '</li>';
|
|
1777
|
+
});
|
|
1778
|
+
html += '</ul></div>';
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
html += '</div>';
|
|
1782
|
+
container.innerHTML = html;
|
|
1783
|
+
} catch (e) {
|
|
1784
|
+
container.innerHTML = '<div class="empty-state">Failed to load changelog.</div>';
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// ── Convert session ───────────────────────────────────────────
|
|
1789
|
+
|
|
1790
|
+
async function convertTo(sessionId, project, targetFormat) {
|
|
1791
|
+
if (!confirm('Convert this session to ' + targetFormat + '? A new session will be created.')) return;
|
|
1792
|
+
showToast('Converting...');
|
|
1793
|
+
try {
|
|
1794
|
+
var resp = await fetch('/api/convert', {
|
|
1795
|
+
method: 'POST',
|
|
1796
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1797
|
+
body: JSON.stringify({ sessionId: sessionId, project: project, targetFormat: targetFormat }),
|
|
1798
|
+
});
|
|
1799
|
+
var data = await resp.json();
|
|
1800
|
+
if (data.ok) {
|
|
1801
|
+
showToast('Converted! New session: ' + data.target.sessionId.slice(0, 12));
|
|
1802
|
+
// Refresh to show new session
|
|
1803
|
+
await loadSessions();
|
|
1804
|
+
closeDetail();
|
|
1805
|
+
} else {
|
|
1806
|
+
showToast('Error: ' + (data.error || 'unknown'));
|
|
1807
|
+
}
|
|
1808
|
+
} catch (e) {
|
|
1809
|
+
showToast('Convert failed: ' + e.message);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1747
1813
|
// ── Install agents ────────────────────────────────────────────
|
|
1748
1814
|
|
|
1749
1815
|
var AGENT_INSTALL = {
|
|
@@ -1833,6 +1899,13 @@ async function checkForUpdates() {
|
|
|
1833
1899
|
badge.textContent = 'v' + data.current;
|
|
1834
1900
|
}
|
|
1835
1901
|
|
|
1902
|
+
// Show "what's new" if version changed since last visit
|
|
1903
|
+
var lastSeenVersion = localStorage.getItem('codedash-last-version');
|
|
1904
|
+
if (lastSeenVersion && lastSeenVersion !== data.current) {
|
|
1905
|
+
showToast('Updated to v' + data.current + ' — check Changelog!');
|
|
1906
|
+
}
|
|
1907
|
+
localStorage.setItem('codedash-last-version', data.current);
|
|
1908
|
+
|
|
1836
1909
|
if (data.updateAvailable) {
|
|
1837
1910
|
if (badge) {
|
|
1838
1911
|
badge.textContent = 'v' + data.current + ' → v' + data.latest;
|
package/src/frontend/index.html
CHANGED
|
@@ -75,6 +75,10 @@
|
|
|
75
75
|
<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
76
|
Export / Import
|
|
77
77
|
</div>
|
|
78
|
+
<div class="sidebar-item small" data-view="changelog">
|
|
79
|
+
<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>
|
|
80
|
+
Changelog
|
|
81
|
+
</div>
|
|
78
82
|
<div class="sidebar-author">
|
|
79
83
|
Made by <a href="https://t.me/neuraldeep" target="_blank">Valerii Kovalskii</a>
|
|
80
84
|
</div>
|
package/src/frontend/styles.css
CHANGED
|
@@ -1596,6 +1596,93 @@ body {
|
|
|
1596
1596
|
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
|
1597
1597
|
}
|
|
1598
1598
|
|
|
1599
|
+
/* ── Changelog ──────────────────────────────────────────────── */
|
|
1600
|
+
|
|
1601
|
+
.changelog-container { padding: 20px; max-width: 700px; }
|
|
1602
|
+
|
|
1603
|
+
.changelog-entry {
|
|
1604
|
+
border-left: 2px solid var(--border);
|
|
1605
|
+
padding: 0 0 24px 20px;
|
|
1606
|
+
margin-left: 8px;
|
|
1607
|
+
position: relative;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
.changelog-entry::before {
|
|
1611
|
+
content: '';
|
|
1612
|
+
position: absolute;
|
|
1613
|
+
left: -5px;
|
|
1614
|
+
top: 4px;
|
|
1615
|
+
width: 8px;
|
|
1616
|
+
height: 8px;
|
|
1617
|
+
border-radius: 50%;
|
|
1618
|
+
background: var(--text-muted);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
.changelog-latest {
|
|
1622
|
+
border-left-color: var(--accent-green);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
.changelog-latest::before {
|
|
1626
|
+
background: var(--accent-green);
|
|
1627
|
+
box-shadow: 0 0 6px var(--accent-green);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
.changelog-header {
|
|
1631
|
+
display: flex;
|
|
1632
|
+
align-items: center;
|
|
1633
|
+
gap: 8px;
|
|
1634
|
+
margin-bottom: 4px;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
.changelog-version {
|
|
1638
|
+
font-size: 15px;
|
|
1639
|
+
font-weight: 700;
|
|
1640
|
+
color: var(--text-primary);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
.changelog-new {
|
|
1644
|
+
font-size: 9px;
|
|
1645
|
+
font-weight: 700;
|
|
1646
|
+
letter-spacing: 0.5px;
|
|
1647
|
+
padding: 2px 6px;
|
|
1648
|
+
border-radius: 4px;
|
|
1649
|
+
background: rgba(74, 222, 128, 0.2);
|
|
1650
|
+
color: var(--accent-green);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
.changelog-date {
|
|
1654
|
+
font-size: 12px;
|
|
1655
|
+
color: var(--text-muted);
|
|
1656
|
+
margin-left: auto;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
.changelog-title {
|
|
1660
|
+
font-size: 14px;
|
|
1661
|
+
font-weight: 600;
|
|
1662
|
+
color: var(--text-secondary);
|
|
1663
|
+
margin-bottom: 6px;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
.changelog-list {
|
|
1667
|
+
list-style: none;
|
|
1668
|
+
padding: 0;
|
|
1669
|
+
margin: 0;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
.changelog-list li {
|
|
1673
|
+
font-size: 13px;
|
|
1674
|
+
color: var(--text-secondary);
|
|
1675
|
+
padding: 2px 0;
|
|
1676
|
+
line-height: 1.5;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
.changelog-list li::before {
|
|
1680
|
+
content: '+';
|
|
1681
|
+
color: var(--accent-green);
|
|
1682
|
+
font-weight: 700;
|
|
1683
|
+
margin-right: 8px;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1599
1686
|
/* ── Running Sessions View ──────────────────────────────────── */
|
|
1600
1687
|
|
|
1601
1688
|
.running-container { padding: 20px; }
|
package/src/server.js
CHANGED
|
@@ -5,6 +5,8 @@ const { URL } = require('url');
|
|
|
5
5
|
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
|
+
const { convertSession } = require('./convert');
|
|
9
|
+
const { CHANGELOG } = require('./changelog');
|
|
8
10
|
const { getHTML } = require('./html');
|
|
9
11
|
|
|
10
12
|
function startServer(port, openBrowser = true) {
|
|
@@ -110,6 +112,19 @@ function startServer(port, openBrowser = true) {
|
|
|
110
112
|
json(res, active);
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
// ── Convert session ─────────────────────
|
|
116
|
+
else if (req.method === 'POST' && pathname === '/api/convert') {
|
|
117
|
+
readBody(req, body => {
|
|
118
|
+
try {
|
|
119
|
+
const { sessionId, project, targetFormat } = JSON.parse(body);
|
|
120
|
+
const result = convertSession(sessionId, project || '', targetFormat);
|
|
121
|
+
json(res, result);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
json(res, { ok: false, error: e.message }, 400);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
113
128
|
// ── Focus terminal ──────────────────────
|
|
114
129
|
else if (req.method === 'POST' && pathname === '/api/focus') {
|
|
115
130
|
readBody(req, body => {
|
|
@@ -163,6 +178,11 @@ function startServer(port, openBrowser = true) {
|
|
|
163
178
|
json(res, data);
|
|
164
179
|
}
|
|
165
180
|
|
|
181
|
+
// ── Changelog ─────────────────────────────
|
|
182
|
+
else if (req.method === 'GET' && pathname === '/api/changelog') {
|
|
183
|
+
json(res, CHANGELOG);
|
|
184
|
+
}
|
|
185
|
+
|
|
166
186
|
// ── Version check ────────────────────────
|
|
167
187
|
else if (req.method === 'GET' && pathname === '/api/version') {
|
|
168
188
|
const pkg = require('../package.json');
|