From 3491ef2125d2ca3866cf6483740ada61f909a225 Mon Sep 17 00:00:00 2001 From: Johnny Huynh <27847622+johnnyhuy@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:19:55 +0900 Subject: [PATCH] feat: add recovery summary to session history export --- src/renderer/export-session.ts | 29 ++++++++++++++++++++++++++++- tests/unit/export-session.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/renderer/export-session.ts b/src/renderer/export-session.ts index 41a7908..26eade8 100644 --- a/src/renderer/export-session.ts +++ b/src/renderer/export-session.ts @@ -47,6 +47,30 @@ function handoffSection(cwd: string): string { ].join('\n'); } +function summarySection(entries: Entry[]): string { + const users = entries.filter((e) => e.kind === 'user').length; + const assistants = entries.filter((e) => e.kind === 'assistant').length; + const messages = users + assistants; + const tools = entries.filter( + (e): e is Extract => e.kind === 'tool', + ); + const lines = [ + '## Summary', + '', + `- ${messages} ${messages === 1 ? 'message' : 'messages'}: ${users} from you, ${assistants} from Pi.`, + ]; + if (tools.length > 0) { + const done = tools.filter((e) => !e.running && e.ok).length; + const failed = tools.filter((e) => !e.running && !e.ok).length; + const running = tools.filter((e) => e.running).length; + lines.push( + `- ${tools.length} ${tools.length === 1 ? 'tool call' : 'tool calls'}: ${done} done, ${failed} failed, ${running} running.`, + `- Tools used: ${[...new Set(tools.map((e) => e.tool))].join(', ')}.`, + ); + } + return lines.join('\n'); +} + export function sessionExportMarkdown(input: SessionExportInput): string { const parts: string[] = [ `# ${input.title.trim() || 'Untitled session'}`, @@ -54,7 +78,10 @@ export function sessionExportMarkdown(input: SessionExportInput): string { ]; if (input.cwd.trim()) parts.push(`It ran in \`${input.cwd.trim()}\`.`); if (input.model.trim()) parts.push(`It used \`${input.model.trim()}\`.`); - if (input.entries.length > 0) parts.push(handoffSection(input.cwd)); + if (input.entries.length > 0) { + parts.push(summarySection(input.entries)); + parts.push(handoffSection(input.cwd)); + } if (input.entries.length === 0) { parts.push('No messages in this selection.'); return `${parts.join('\n\n')}\n`; diff --git a/tests/unit/export-session.test.ts b/tests/unit/export-session.test.ts index 4a8da57..2222f21 100644 --- a/tests/unit/export-session.test.ts +++ b/tests/unit/export-session.test.ts @@ -48,6 +48,12 @@ describe('sessionExportMarkdown', () => { '', 'It used `anthropic/claude-test`.', '', + '## Summary', + '', + '- 2 messages: 1 from you, 1 from Pi.', + '- 1 tool call: 1 done, 0 failed, 0 running.', + '- Tools used: bash.', + '', '## Continue elsewhere', '', 'This file is portable context, not a restorable Pi session archive.', @@ -158,6 +164,28 @@ describe('sessionExportMarkdown', () => { expect(markdown).not.toContain('Same folder (`'); }); + it('summarises tool outcomes and distinct tools in first-use order', () => { + const markdown = sessionExportMarkdown({ + title: 'T', + cwd: '', + model: '', + exportedAt, + entries: [ + { key: 'u1', kind: 'user', text: 'hi' }, + { ...bashDone, key: 't1' }, + { ...bashDone, key: 't2', tool: 'read', ok: false, output: 'boom' }, + { ...bashDone, key: 't3', tool: 'bash', running: true, output: '' }, + ], + }); + expect(markdown).toContain( + '- 1 message: 1 from you, 0 from Pi.', + ); + expect(markdown).toContain( + '- 3 tool calls: 1 done, 1 failed, 1 running.', + ); + expect(markdown).toContain('- Tools used: bash, read.'); + }); + it('handles an empty selection and a blank title', () => { const markdown = sessionExportMarkdown({ title: ' ', @@ -171,6 +199,7 @@ describe('sessionExportMarkdown', () => { expect(markdown).not.toContain('It ran in'); expect(markdown).not.toContain('It used'); expect(markdown).not.toContain('Continue elsewhere'); + expect(markdown).not.toContain('## Summary'); }); });