Skip to content

Commit a0e153c

Browse files
Myoontyeeclaude
andcommitted
fix: trim session UX — show title not UUID, show backup path (v0.8.6)
- Fix: trimSession command now reads sessionFilePath from tree item correctly (was using resourceUri which doesn't exist on SessionItem) - Fix: session picker shows human-readable title + short UUID, not raw UUID - Fix: confirmation modal now shows exact backup file path - Add: getSessionDisplayTitle() helper reads customTitle or first user message - Add: UI strings are bilingual (中文 / English) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent be19fe9 commit a0e153c

File tree

2 files changed

+55
-28
lines changed

2 files changed

+55
-28
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.5",
5+
"version": "0.8.6",
66
"publisher": "myoontyee",
77
"engines": {
88
"vscode": "^1.85.0"

src/extension.ts

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -502,51 +502,50 @@ export function activate(context: vscode.ExtensionContext): void {
502502
vscode.window.showWarningMessage('No Claude Code sessions found for the current workspace.');
503503
return;
504504
}
505+
506+
// Pick from current workspace sessions sorted by size
505507
const trimJsonls = fs.readdirSync(claudeProjectDir)
506508
.filter((f) => f.endsWith('.jsonl'))
507509
.map((f) => path.join(claudeProjectDir, f));
508510

509511
if (trimJsonls.length === 0) {
510-
vscode.window.showInformationMessage('No sessions found.');
512+
vscode.window.showInformationMessage('未找到会话文件 / No sessions found.');
511513
return;
512514
}
513515

514-
// Sort by size descending, let user pick
515516
const sizedFiles = trimJsonls
516517
.map((f) => ({ f, sizeMb: fs.statSync(f).size / 1024 / 1024 }))
517518
.sort((a, b) => b.sizeMb - a.sizeMb);
518519

519520
const picks = sizedFiles.map(({ f, sizeMb }) => ({
520-
label: `$(file) ${path.basename(f, '.jsonl')}`,
521-
description: `${sizeMb.toFixed(1)} MB`,
521+
label: `$(file) ${getSessionDisplayTitle(f)}`,
522+
description: `${sizeMb.toFixed(1)} MB [${path.basename(f, '.jsonl').slice(0, 8)}]`,
522523
value: f,
523524
large: sizeMb > 10,
524525
}));
525526

526527
const picked = await vscode.window.showQuickPick(picks, {
527-
placeHolder: 'Select session to trim (original will be backed up)',
528-
title: 'Trim Large Session',
528+
placeHolder: '选择要裁剪的会话(原文件将自动备份)/ Select session to trim (original will be backed up)',
529+
title: '裁剪大型会话 / Trim Large Session',
529530
});
530531
if (!picked) return;
531532

532533
const keepInput = await vscode.window.showInputBox({
533-
prompt: 'How many recent messages to keep? (each user/assistant turn counts as 1)',
534+
prompt: '保留最近多少条消息?每条用户/助手发言计为1条 / How many recent messages to keep? (each user/assistant turn counts as 1)',
534535
value: '400',
535-
validateInput: (v) => (isNaN(Number(v)) || Number(v) < 10) ? 'Enter a number ≥ 10' : undefined,
536+
validateInput: (v) => (isNaN(Number(v)) || Number(v) < 10) ? '请输入 ≥ 10 的数字 / Enter a number ≥ 10' : undefined,
536537
});
537538
if (!keepInput) return;
538539

539540
try {
540541
const r = trimSession(picked.value, Number(keepInput));
541542
vscode.window.showInformationMessage(
542-
`✓ Trimmed session from ${r.originalLines}${r.keptLines} lines ` +
543-
`(${r.sizeMb.toFixed(1)} MB → ${r.trimmedSizeMb.toFixed(1)} MB).\n\n` +
544-
`Original backed up to ${path.basename(picked.value)}.bak-*\n\n` +
545-
`Close and reopen your CC chat to reload the trimmed session.`,
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.`,
546545
{ modal: true }
547546
);
548547
} catch (err) {
549-
vscode.window.showErrorMessage(`Trim failed: ${err}`);
548+
vscode.window.showErrorMessage(`裁剪失败 / Trim failed: ${err}`);
550549
}
551550
return;
552551
}
@@ -574,49 +573,50 @@ export function activate(context: vscode.ExtensionContext): void {
574573
vscode.commands.registerCommand('claudeCodeExporter.trimSession', async (item?: SessionItem) => {
575574
let jsonlPath: string | undefined;
576575

577-
if (item?.resourceUri?.fsPath?.endsWith('.jsonl')) {
578-
jsonlPath = item.resourceUri.fsPath;
576+
// If invoked from a tree item, use its path directly — no picker needed
577+
if (item?.sessionFilePath?.endsWith('.jsonl')) {
578+
jsonlPath = item.sessionFilePath;
579579
} else if (claudeProjectDir) {
580-
// Pick from current workspace sessions sorted by size
580+
// Invoked from command palette: let user pick
581581
const jsonls = fs.readdirSync(claudeProjectDir)
582582
.filter((f) => f.endsWith('.jsonl'))
583583
.map((f) => ({ f: path.join(claudeProjectDir, f), sizeMb: fs.statSync(path.join(claudeProjectDir, f)).size / 1024 / 1024 }))
584584
.sort((a, b) => b.sizeMb - a.sizeMb);
585585

586586
const picked = await vscode.window.showQuickPick(
587587
jsonls.map(({ f, sizeMb }) => ({
588-
label: `$(file) ${path.basename(f, '.jsonl')}`,
589-
description: `${sizeMb.toFixed(1)} MB`,
588+
label: `$(file) ${getSessionDisplayTitle(f)}`,
589+
description: `${sizeMb.toFixed(1)} MB [${path.basename(f, '.jsonl').slice(0, 8)}]`,
590590
value: f,
591591
})),
592-
{ placeHolder: 'Select session to trim', title: 'Trim Large Session' }
592+
{ placeHolder: '选择要裁剪的会话 / Select session to trim', title: '裁剪大型会话 / Trim Large Session' }
593593
);
594594
if (!picked) return;
595595
jsonlPath = picked.value;
596596
}
597597

598598
if (!jsonlPath) {
599-
vscode.window.showWarningMessage('No session selected.');
599+
vscode.window.showWarningMessage('未选择会话 / No session selected.');
600600
return;
601601
}
602602

603603
const sizeMb = fs.statSync(jsonlPath).size / 1024 / 1024;
604604
const keepInput = await vscode.window.showInputBox({
605-
prompt: `Session is ${sizeMb.toFixed(1)} MB. How many recent messages to keep?`,
605+
prompt: `当前会话 ${sizeMb.toFixed(1)} MB,保留最近多少条消息? / Session is ${sizeMb.toFixed(1)} MB — how many recent messages to keep?`,
606606
value: '400',
607-
validateInput: (v) => (isNaN(Number(v)) || Number(v) < 10) ? 'Enter a number ≥ 10' : undefined,
607+
validateInput: (v) => (isNaN(Number(v)) || Number(v) < 10) ? '请输入 ≥ 10 的数字 / Enter a number ≥ 10' : undefined,
608608
});
609609
if (!keepInput) return;
610610

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-
`Original backed up. Close and reopen your CC chat to reload.`,
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.`,
616616
{ modal: true }
617617
);
618618
} catch (err) {
619-
vscode.window.showErrorMessage(`Trim failed: ${err}`);
619+
vscode.window.showErrorMessage(`裁剪失败 / Trim failed: ${err}`);
620620
}
621621
}),
622622

@@ -1304,10 +1304,37 @@ function repairThinkingSignatures(jsonlPath: string): number {
13041304
// Backs up original to .jsonl.bak-TIMESTAMP, then overwrites in-place so the
13051305
// session ID and CC entry point are unchanged.
13061306
// Returns { keptLines, originalLines, sizeMb, trimmedSizeMb }
1307+
1308+
function getSessionDisplayTitle(jsonlPath: string): string {
1309+
try {
1310+
const content = fs.readFileSync(jsonlPath, 'utf8');
1311+
const lines = content.split('\n').filter((l) => l.trim());
1312+
for (const line of lines) {
1313+
try {
1314+
const obj = JSON.parse(line) as Record<string, unknown>;
1315+
if (obj.customTitle && typeof obj.customTitle === 'string') return obj.customTitle;
1316+
const msg = obj.message as Record<string, unknown> | undefined;
1317+
if (msg?.role === 'user') {
1318+
const blocks = msg.content;
1319+
if (Array.isArray(blocks)) {
1320+
for (const b of blocks) {
1321+
if ((b as Record<string, unknown>).type === 'text') {
1322+
const t = ((b as Record<string, unknown>).text as string || '').trim();
1323+
if (t) return t.slice(0, 50);
1324+
}
1325+
}
1326+
}
1327+
if (typeof blocks === 'string' && blocks.trim()) return blocks.trim().slice(0, 50);
1328+
}
1329+
} catch { /* skip bad line */ }
1330+
}
1331+
} catch { /* unreadable */ }
1332+
return path.basename(jsonlPath, '.jsonl').slice(0, 8);
1333+
}
13071334
function trimSession(
13081335
jsonlPath: string,
13091336
keepRecentMessages: number = 400
1310-
): { keptLines: number; originalLines: number; sizeMb: number; trimmedSizeMb: number } {
1337+
): { keptLines: number; originalLines: number; sizeMb: number; trimmedSizeMb: number; backupPath: string } {
13111338
const raw = fs.readFileSync(jsonlPath, 'utf8');
13121339
const lines = raw.split('\n');
13131340
const originalLines = lines.filter((l) => l.trim()).length;
@@ -1359,6 +1386,6 @@ function trimSession(
13591386
fs.writeFileSync(jsonlPath, outLines.join('\n') + '\n', 'utf8');
13601387
const trimmedSizeMb = fs.statSync(jsonlPath).size / 1024 / 1024;
13611388

1362-
return { keptLines: outLines.length, originalLines, sizeMb, trimmedSizeMb };
1389+
return { keptLines: outLines.length, originalLines, sizeMb, trimmedSizeMb, backupPath };
13631390
}
13641391

0 commit comments

Comments
 (0)