Skip to content

Commit 1a4a975

Browse files
Myoontyeeclaude
andcommitted
feat: trim creates new session "...(trim)" instead of overwriting (v0.8.7)
- Trim no longer touches the original session file at all - Creates a new .jsonl with fresh UUID alongside the original - New session's customTitle = original title + " (trim)" - All UUIDs/parentUUIDs remapped to keep linked-list chain valid - Both sessions visible in the tree after refresh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a0e153c commit 1a4a975

File tree

2 files changed

+53
-21
lines changed

2 files changed

+53
-21
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "claude-code-exporter",
33
"displayName": "Claude Code Exporter — Claude Code, Codex & Cursor Conversation History",
44
"description": "Export AI coding agent conversations to Markdown. Supports Claude Code (→ .claude-code-history/), OpenAI Codex (→ .codex-history/), and Cursor Composer (→ .cursor-history/). Features: auto-watch, session inject/import for claude --resume, and repair of thinking-block signature errors when switching API providers.",
5-
"version": "0.8.6",
5+
"version": "0.8.7",
66
"publisher": "myoontyee",
77
"engines": {
88
"vscode": "^1.85.0"

src/extension.ts

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -540,8 +540,8 @@ export function activate(context: vscode.ExtensionContext): void {
540540
try {
541541
const r = trimSession(picked.value, Number(keepInput));
542542
vscode.window.showInformationMessage(
543-
`✓ 裁剪完成 / Trimmed: ${r.originalLines}${r.keptLines} 行 (${r.sizeMb.toFixed(1)} MB → ${r.trimmedSizeMb.toFixed(1)} MB)\n\n` +
544-
`备份位置 / Backup: ${r.backupPath}\n\n关闭并重新打开 CC 对话面板以生效 / Close and reopen CC chat to reload.`,
543+
`✓ 裁剪副本已创建 / Trim copy created: ${r.originalLines}${r.keptLines} 行 (${r.sizeMb.toFixed(1)} MB → ${r.trimmedSizeMb.toFixed(1)} MB)\n\n` +
544+
`原对话不变,裁剪版标题为 "...(trim)",刷新列表即可看到 / Original unchanged. Trimmed copy titled "...(trim)" — refresh to see it.`,
545545
{ modal: true }
546546
);
547547
} catch (err) {
@@ -611,8 +611,8 @@ export function activate(context: vscode.ExtensionContext): void {
611611
try {
612612
const r = trimSession(jsonlPath, Number(keepInput));
613613
vscode.window.showInformationMessage(
614-
`✓ 裁剪完成 / Trimmed: ${r.originalLines}${r.keptLines} 行 / lines (${r.sizeMb.toFixed(1)} MB → ${r.trimmedSizeMb.toFixed(1)} MB)\n\n` +
615-
`备份位置 / Backup: ${r.backupPath}\n\n关闭并重新打开 CC 对话面板以生效 / Close and reopen CC chat panel to reload.`,
614+
`✓ 裁剪副本已创建 / Trim copy created: ${r.originalLines}${r.keptLines} 行 / lines (${r.sizeMb.toFixed(1)} MB → ${r.trimmedSizeMb.toFixed(1)} MB)\n\n` +
615+
`原对话不变,裁剪版标题为 "...(trim)",刷新列表即可看到 / Original unchanged. Trimmed copy titled "...(trim)" — refresh to see it.`,
616616
{ modal: true }
617617
);
618618
} catch (err) {
@@ -1334,7 +1334,7 @@ function getSessionDisplayTitle(jsonlPath: string): string {
13341334
function trimSession(
13351335
jsonlPath: string,
13361336
keepRecentMessages: number = 400
1337-
): { keptLines: number; originalLines: number; sizeMb: number; trimmedSizeMb: number; backupPath: string } {
1337+
): { keptLines: number; originalLines: number; sizeMb: number; trimmedSizeMb: number; newFilePath: string; newSessionId: string } {
13381338
const raw = fs.readFileSync(jsonlPath, 'utf8');
13391339
const lines = raw.split('\n');
13401340
const originalLines = lines.filter((l) => l.trim()).length;
@@ -1346,8 +1346,8 @@ function trimSession(
13461346
});
13471347

13481348
// Classify each non-empty line
1349-
const msgLines: number[] = []; // indices of user/assistant message lines
1350-
const initLines: number[] = []; // everything else (system, summaries, etc.)
1349+
const msgLines: number[] = [];
1350+
const initLines: number[] = [];
13511351

13521352
for (let i = 0; i < parsed.length; i++) {
13531353
const obj = parsed[i].obj;
@@ -1360,7 +1360,7 @@ function trimSession(
13601360
}
13611361
}
13621362

1363-
// Keep last keepRecentMessages message lines, ensuring we don't start with assistant
1363+
// Keep last keepRecentMessages message lines, start from a user turn
13641364
let recent = msgLines.slice(-keepRecentMessages);
13651365
while (recent.length > 0) {
13661366
const firstObj = parsed[recent[0]].obj;
@@ -1369,23 +1369,55 @@ function trimSession(
13691369
recent = recent.slice(1);
13701370
}
13711371

1372-
// Build output: init lines + recent message lines, preserving original order
1372+
// Build selected lines preserving original order
13731373
const keepSet = new Set([...initLines, ...recent]);
1374-
const outLines: string[] = [];
1374+
const selectedLines: Record<string, unknown>[] = [];
13751375
for (let i = 0; i < parsed.length; i++) {
1376-
if (!parsed[i].raw.trim()) continue; // skip blank
1377-
if (keepSet.has(i)) outLines.push(parsed[i].raw);
1376+
if (!parsed[i].raw.trim()) continue;
1377+
if (keepSet.has(i) && parsed[i].obj) selectedLines.push(parsed[i].obj!);
13781378
}
13791379

1380-
// Backup original
1381-
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
1382-
const backupPath = `${jsonlPath}.bak-${ts}`;
1383-
fs.copyFileSync(jsonlPath, backupPath);
1380+
// Generate new session ID and remap all UUIDs (same approach as injectSession)
1381+
const crypto = require('crypto') as typeof import('crypto');
1382+
const newSessionId: string = crypto.randomUUID();
1383+
const uuidMap = new Map<string, string>();
1384+
1385+
const outLines: string[] = [];
1386+
let titleSet = false;
1387+
1388+
for (const obj of selectedLines) {
1389+
const cloned = { ...obj } as Record<string, unknown>;
1390+
1391+
// Remap uuid
1392+
const oldUuid = cloned.uuid as string | undefined;
1393+
const newUuid: string = crypto.randomUUID();
1394+
if (oldUuid) uuidMap.set(oldUuid, newUuid);
1395+
cloned.uuid = newUuid;
1396+
1397+
// Remap parentUuid
1398+
const oldParent = cloned.parentUuid as string | null | undefined;
1399+
if (typeof oldParent === 'string' && uuidMap.has(oldParent)) {
1400+
cloned.parentUuid = uuidMap.get(oldParent)!;
1401+
}
1402+
1403+
// Stamp new sessionId
1404+
cloned.sessionId = newSessionId;
1405+
1406+
// Set customTitle on first line that has one (or first line overall)
1407+
if (!titleSet) {
1408+
const originalTitle = (cloned.customTitle as string | undefined) || getSessionDisplayTitle(jsonlPath);
1409+
cloned.customTitle = `${originalTitle} (trim)`;
1410+
titleSet = true;
1411+
}
1412+
1413+
outLines.push(JSON.stringify(cloned));
1414+
}
13841415

1385-
// Overwrite in-place
1386-
fs.writeFileSync(jsonlPath, outLines.join('\n') + '\n', 'utf8');
1387-
const trimmedSizeMb = fs.statSync(jsonlPath).size / 1024 / 1024;
1416+
// Write new file alongside original (same project dir, new UUID filename)
1417+
const newFilePath = path.join(path.dirname(jsonlPath), `${newSessionId}.jsonl`);
1418+
fs.writeFileSync(newFilePath, outLines.join('\n') + '\n', 'utf8');
1419+
const trimmedSizeMb = fs.statSync(newFilePath).size / 1024 / 1024;
13881420

1389-
return { keptLines: outLines.length, originalLines, sizeMb, trimmedSizeMb, backupPath };
1421+
return { keptLines: outLines.length, originalLines, sizeMb, trimmedSizeMb, newFilePath, newSessionId };
13901422
}
13911423

0 commit comments

Comments
 (0)