feat: add session export with full and compressed modes (#507)

Add export functionality that allows users to download session data
as JSON or plain text, with optional LLM-based context compression
for long conversations. Includes UI controls in chat panel, session
list, and history view, plus i18n strings for all 8 locales.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-05-07 13:49:57 +08:00
committed by GitHub
parent c0ad8c907b
commit 173307ef28
18 changed files with 554 additions and 14 deletions
@@ -11,7 +11,7 @@ import { copyToClipboard } from '@/utils/clipboard'
import FolderPicker from '@/components/hermes/chat/FolderPicker.vue'
import HistoryMessageList from '@/components/hermes/chat/HistoryMessageList.vue'
import SessionListItem from '@/components/hermes/chat/SessionListItem.vue'
import { renameSession, setSessionWorkspace, fetchHermesSessions, fetchHermesSession, type SessionSummary } from '@/api/hermes/sessions'
import { renameSession, setSessionWorkspace, fetchHermesSessions, fetchHermesSession, exportSession, type SessionSummary } from '@/api/hermes/sessions'
const chatStore = useChatStore()
const appStore = useAppStore()
@@ -281,6 +281,28 @@ const contextMenuOptions = computed(() => [
{ label: t(contextSessionPinned.value ? 'chat.unpin' : 'chat.pin'), key: 'pin' },
{ label: t('chat.rename'), key: 'rename' },
{ label: t('chat.setWorkspace'), key: 'workspace' },
{
label: t('chat.export'),
key: 'export',
children: [
{
label: t('chat.exportFull'),
key: 'export-full',
children: [
{ label: 'JSON', key: 'export-full-json' },
{ label: 'TXT', key: 'export-full-txt' },
],
},
{
label: t('chat.exportCompressed'),
key: 'export-compressed',
children: [
{ label: 'JSON', key: 'export-compressed-json' },
{ label: 'TXT', key: 'export-compressed-txt' },
],
},
],
},
{ label: t('chat.copySessionId'), key: 'copy-id' },
])
@@ -296,7 +318,15 @@ const showContextMenu = ref(false)
const contextMenuX = ref(0)
const contextMenuY = ref(0)
function handleContextMenuSelect(key: string) {
function parseExportKey(key: string): { mode: 'full' | 'compressed'; ext: 'json' | 'txt' } | null {
if (key === 'export-full-json') return { mode: 'full', ext: 'json' }
if (key === 'export-full-txt') return { mode: 'full', ext: 'txt' }
if (key === 'export-compressed-json') return { mode: 'compressed', ext: 'json' }
if (key === 'export-compressed-txt') return { mode: 'compressed', ext: 'txt' }
return null
}
async function handleContextMenuSelect(key: string) {
showContextMenu.value = false
if (!contextSessionId.value) return
if (key === 'pin') {
@@ -305,6 +335,17 @@ function handleContextMenuSelect(key: string) {
}
if (key === 'copy-id') {
copySessionId(contextSessionId.value)
} else if (parseExportKey(key)) {
const { mode, ext } = parseExportKey(key)!
const loadingMsg = mode === 'compressed' ? message.loading(t('chat.exportCompressing'), { duration: 0 }) : null
try {
await exportSession(contextSessionId.value, mode, ext)
loadingMsg?.destroy()
message.success(t('chat.exportSuccess'))
} catch {
loadingMsg?.destroy()
message.error(t('chat.exportFailed'))
}
} else if (key === 'workspace') {
const session = historySessions.value.find(s => s.id === contextSessionId.value)
workspaceSessionId.value = contextSessionId.value