feat(sidebar): add batch export from project context menu (#81)

- Project context menu gains 'Export sessions…' entry that enters
  batch mode with all project sessions pre-selected.
- Batch toolbar adds 'Export selected (N)' button alongside the
  existing select-all and delete buttons (grid now 3-col).
- handleBatchExport loops over selected sessions, calling the
  existing per-session JSONL export, then exits batch mode.
This commit is contained in:
你的姓名 2026-06-19 16:51:29 +08:00
parent 0f2729adcd
commit 5a5997e50a
6 changed files with 46 additions and 2 deletions

View File

@ -1,5 +1,5 @@
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
import { Folder, FolderOpen, MoreHorizontal, Pin, PinOff, RefreshCw, RotateCcw, SquarePen, Trash2, Upload, X } from 'lucide-react'
import { Download, Folder, FolderOpen, MoreHorizontal, Pin, PinOff, RefreshCw, RotateCcw, SquarePen, Trash2, Upload, X } from 'lucide-react'
import { useSessionStore } from '../../stores/sessionStore'
import { useUIStore } from '../../stores/uiStore'
import { useTranslation } from '../../i18n'
@ -631,6 +631,19 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
setPendingBatchDeleteSessionIds(null)
}, [exitBatchMode])
const handleBatchExport = useCallback(async () => {
const ids = [...selectedSessionIds]
if (ids.length === 0) return
let successCount = 0
for (const sessionId of ids) {
await handleExportSession(sessionId)
successCount++
}
if (successCount > 0) {
handleExitBatchMode()
}
}, [handleExportSession, handleExitBatchMode, selectedSessionIds])
const requestBatchDelete = useCallback((ids: string[]) => {
if (ids.length === 0) return
setPendingBatchDeleteSessionIds([...new Set(ids)])
@ -920,7 +933,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
<span className="material-symbols-outlined text-[17px]">close</span>
</button>
</div>
<div className="mt-2 grid grid-cols-2 gap-1.5">
<div className="mt-2 grid grid-cols-3 gap-1.5">
<button
type="button"
onClick={() => {
@ -937,6 +950,14 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
? t('sidebar.batchDeselectAll')
: t('sidebar.batchSelectAll')}
</button>
<button
type="button"
onClick={() => void handleBatchExport()}
disabled={selectedCount === 0}
className="rounded-md border border-[var(--color-border)] px-2 py-1.5 text-xs font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] disabled:opacity-50"
>
{t('sidebar.batchExportSelected', { count: selectedCount })}
</button>
<button
type="button"
onClick={() => requestBatchDelete([...selectedSessionIds])}
@ -1292,6 +1313,19 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
>
{t('sidebar.importSession')}
</ProjectMenuItem>
<ProjectMenuItem
icon={<Download size={18} aria-hidden="true" />}
onClick={() => {
setProjectContextMenu(null)
enterBatchMode()
const projectSessionIds = sessions
.filter((s) => (s.workDir || s.projectRoot || s.projectPath) === project.workDir)
.map((s) => s.id)
selectSessions(projectSessionIds)
}}
>
{t('sidebar.exportProjectSessions')}
</ProjectMenuItem>
<ProjectMenuItem
icon={hidden ? <RotateCcw size={18} aria-hidden="true" /> : <X size={18} aria-hidden="true" />}
onClick={() => toggleHiddenProject(project)}

View File

@ -92,6 +92,8 @@ export const en = {
'sidebar.batchDeselectAll': 'Deselect all',
'sidebar.batchSelectGroup': 'Select {group}',
'sidebar.batchDeleteSelected': 'Delete selected ({count})',
'sidebar.batchExportSelected': 'Export selected ({count})',
'sidebar.exportProjectSessions': 'Export sessions…',
'sidebar.batchDeleteConfirm': 'Delete {count} sessions? This cannot be undone.',
'sidebar.batchDeleteConfirmBody': 'The following sessions will be deleted:',
'sidebar.batchDeleteMore': '...and {count} more',

View File

@ -94,6 +94,8 @@ export const jp: Record<TranslationKey, string> = {
'sidebar.batchDeselectAll': 'すべて選択解除',
'sidebar.batchSelectGroup': '{group} を選択',
'sidebar.batchDeleteSelected': '選択項目を削除 ({count})',
'sidebar.batchExportSelected': '選択項目をエクスポート ({count})',
'sidebar.exportProjectSessions': 'セッションをエクスポート…',
'sidebar.batchDeleteConfirm': '{count} 件のセッションを削除しますか?この操作は取り消せません。',
'sidebar.batchDeleteConfirmBody': '以下のセッションが削除されます:',
'sidebar.batchDeleteMore': '...他 {count} 件',

View File

@ -94,6 +94,8 @@ export const kr: Record<TranslationKey, string> = {
'sidebar.batchDeselectAll': '모두 선택 해제',
'sidebar.batchSelectGroup': '{group} 선택',
'sidebar.batchDeleteSelected': '선택 항목 삭제 ({count})',
'sidebar.batchExportSelected': '선택 항목 내보내기 ({count})',
'sidebar.exportProjectSessions': '세션 내보내기…',
'sidebar.batchDeleteConfirm': '{count}개의 세션을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.',
'sidebar.batchDeleteConfirmBody': '다음 세션이 삭제됩니다:',
'sidebar.batchDeleteMore': '...외 {count}개',

View File

@ -94,6 +94,8 @@ export const zh: Record<TranslationKey, string> = {
'sidebar.batchDeselectAll': '取消全選',
'sidebar.batchSelectGroup': '選擇{group}',
'sidebar.batchDeleteSelected': '刪除已選 ({count})',
'sidebar.batchExportSelected': '匯出已選 ({count})',
'sidebar.exportProjectSessions': '匯出會話…',
'sidebar.batchDeleteConfirm': '確定要刪除 {count} 個會話嗎?此操作不可撤銷。',
'sidebar.batchDeleteConfirmBody': '以下會話將被刪除:',
'sidebar.batchDeleteMore': '...還有 {count} 條',

View File

@ -94,6 +94,8 @@ export const zh: Record<TranslationKey, string> = {
'sidebar.batchDeselectAll': '取消全选',
'sidebar.batchSelectGroup': '选择{group}',
'sidebar.batchDeleteSelected': '删除已选 ({count})',
'sidebar.batchExportSelected': '导出已选 ({count})',
'sidebar.exportProjectSessions': '导出会话…',
'sidebar.batchDeleteConfirm': '确定要删除 {count} 个会话吗?此操作不可撤销。',
'sidebar.batchDeleteConfirmBody': '以下会话将被删除:',
'sidebar.batchDeleteMore': '...还有 {count} 条',