feat(session-export): add command and Header action

This commit is contained in:
NI0317
2026-08-12 14:23:06 +08:00
parent 185a1f7da3
commit 8940282aee
114 changed files with 1533 additions and 368 deletions

View File

@@ -164,46 +164,6 @@
font: 14px/14px var(--ds-font-family-code);
}
.export {
display: inline-flex;
flex: none;
align-items: center;
height: 20px;
padding: 0 7px;
gap: 4px;
border: 0;
border-radius: 3px;
color: var(--dsw-alias-label-tertiary);
background: transparent;
cursor: pointer;
font: var(--dsw-font-xxs-12);
}
.export:hover:not(:disabled) {
color: var(--dsw-alias-label-primary);
background: var(--dsw-alias-interactive-bg-hover);
}
.export:focus-visible {
outline: 1px solid var(--dsw-alias-state-business-primary);
outline-offset: 1px;
}
.export:disabled {
color: var(--dsw-alias-label-dimmed);
cursor: wait;
}
.exportIcon {
flex: none;
width: 12px;
height: 12px;
stroke: currentColor;
stroke-width: 1.25;
stroke-linecap: round;
stroke-linejoin: round;
}
.search {
display: flex;
flex: 0 1 164px;

View File

@@ -26,12 +26,6 @@ export interface TrajectoryToolbarProps {
searchQuery: string
/** Update the live ledger search query. */
onSearchQueryChange: (query: string) => void
/** Whether the session-log export is in flight. */
exporting: boolean
/** Trigger the session-log export download. */
onExport: () => void
/** Export failure message, shown while set; null while idle or successful. */
exportError: string | null
/** Translate a toolbar dictionary key. */
t: TranslateNS<typeof NS>
}
@@ -52,9 +46,6 @@ export function TrajectoryToolbar({
onToggleAllAssistants,
searchQuery,
onSearchQueryChange,
exporting,
onExport,
exportError,
t,
}: TrajectoryToolbarProps) {
return (
@@ -119,20 +110,6 @@ export function TrajectoryToolbar({
</span>
{t('toolbar.calls')}
</button>
<button
type="button"
className={css.export}
aria-label={t('toolbar.exportAria')}
title={exportError ?? (exporting ? t('toolbar.exporting') : t('toolbar.exportTitle'))}
disabled={exporting}
onClick={onExport}
>
<svg className={css.exportIcon} viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M8 3v7m0 0 3-3m-3 3L5 7" />
<path d="M3 11.5V13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1.5" />
</svg>
{t('toolbar.export')}
</button>
</div>
<div className={css.search}>
<IconSearchOutline16 size={11} className={css.searchIcon} />

View File

@@ -71,8 +71,6 @@ export interface TrajectoryViewInjected {
}
loadOlder: () => Promise<boolean>
setActualDuration: (actualDuration: boolean) => void
/** Download the session log (including subagent logs) as a ZIP archive; rejects on failure. */
exportLog: () => Promise<void>
}
interface UsageLike {
@@ -120,7 +118,7 @@ function addUsage(
}
export function TrajectoryView({
useSession, useDuration, loadOlder, setActualDuration, exportLog,
useSession, useDuration, loadOlder, setActualDuration,
inspect, onInspectDone, t,
}: ConvViewProps & InjectFace<TrajectoryViewInjected> & PropsLocale<'trajectory'>) {
const [collapsedTurns, setCollapsedTurns] = useState<ReadonlySet<number>>(EMPTY_TURN_IDS)
@@ -130,8 +128,6 @@ export function TrajectoryView({
const actualDuration = useDuration(value => value)
const [actualTime, setActualTime] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [exporting, setExporting] = useState(false)
const [exportError, setExportError] = useState<string | null>(null)
const [searchIndex] = useState(() => new TrajectorySearchIndex())
const [searchIndexRevision, setSearchIndexRevision] = useState(0)
const searchIndexTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -447,19 +443,6 @@ export function TrajectoryView({
return loadOlder()
}, [loadOlder])
const onExport = useCallback(() => {
if (exporting) return
setExporting(true)
setExportError(null)
void exportLog().then(
() => { setExporting(false) },
(error: unknown) => {
setExportError(error instanceof Error ? error.message : String(error))
setExporting(false)
},
)
}, [exportLog, exporting])
return (
<div className={css.root} data-conversation-composer-overlay="">
<TrajectoryToolbar
@@ -479,16 +462,8 @@ export function TrajectoryView({
onToggleAllAssistants={toggleAllAssistants}
searchQuery={searchQuery}
onSearchQueryChange={setSearchQuery}
exporting={exporting}
onExport={onExport}
exportError={exportError}
t={t}
/>
{exportError !== null && (
<div className={css.exportError} role="alert">
{exportError}
</div>
)}
<TrajectoryTimeline
turns={timelineTurns}
mode={timelineMode}

View File

@@ -1,45 +0,0 @@
/**
* Session log export delivery. The host streams the archive from
* `GET /api/session.export`; this module owns the browser-native download
* handoff so the browser can stream the response directly to its download
* manager instead of buffering the ZIP in JavaScript.
* @module
*/
/**
* Collapse an untrusted session id into one safe path/filename segment.
* Distinct ids may collapse onto one segment (impossible for the host-minted
* UUIDs, so no uniqueness suffix is kept).
* @param id - the raw session id.
* @returns a filesystem-safe single segment.
*/
function safeSessionIdSegment(id: string): string {
return id.replace(/[^A-Za-z0-9_-]/g, '_')
}
/**
* The export archive filename for one session (same convention the host's
* Content-Disposition uses).
* @param sessionId - the root session id.
* @returns the download filename.
*/
export function sessionLogZipFilename(sessionId: string): string {
return `dsh-session-${safeSessionIdSegment(sessionId)}.zip`
}
/**
* Hand one host-streamed session archive to the browser download manager.
* The operation resolves after dispatching the native download; HTTP delivery
* continues outside JavaScript and is reported by the browser itself.
* @param sessionId - the root session id to export with all descendants.
* @returns a promise that rejects if the browser handoff itself fails.
*/
export function downloadSessionLog(sessionId: string): Promise<void> {
return Promise.resolve().then(() => {
const query = new URLSearchParams({ sessionId, includeDescendants: 'true' })
const anchor = document.createElement('a')
anchor.href = `/api/session.export?${query.toString()}`
anchor.download = sessionLogZipFilename(sessionId)
anchor.click()
})
}

View File

@@ -10,7 +10,6 @@ import type {} from '@deepseek-ai/dsh-client-locale/client'
// owning package) must be in the program for the register calls to type.
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import { createTrajectoryDurationStore } from './duration-store.ts'
import { downloadSessionLog } from './export-log.ts'
import { en, NS, zh } from './locales.ts'
import { registerTrajectoryAssistantDefinition } from './trajectory-assistant-definition.ts'
import { registerTrajectoryCompactionDefinitions } from './trajectory-compaction-definition.ts'
@@ -60,7 +59,6 @@ export function apply(ctx: Context): void {
return session.getSnapshot().views.get('trajectory') !== before
},
setActualDuration: (value) => { duration.set(value) },
exportLog: () => downloadSessionLog(sessionId),
}
},
}, TrajectoryView))

View File

@@ -17,10 +17,6 @@ export type TrajectoryKey =
| 'toolbar.calls'
| 'toolbar.expandCalls'
| 'toolbar.collapseCalls'
| 'toolbar.export'
| 'toolbar.exportAria'
| 'toolbar.exporting'
| 'toolbar.exportTitle'
| 'toolbar.search'
| 'toolbar.searchPlaceholder'
@@ -45,10 +41,6 @@ export const zh: Record<TrajectoryKey, string> = {
'toolbar.calls': 'Calls',
'toolbar.expandCalls': 'Expand calls',
'toolbar.collapseCalls': 'Collapse calls',
'toolbar.export': 'Export',
'toolbar.exportAria': 'Export session log',
'toolbar.exporting': 'Exporting…',
'toolbar.exportTitle': 'Export session log (ZIP, includes subagents)',
'toolbar.search': '搜索轨迹',
'toolbar.searchPlaceholder': '搜索',
}
@@ -67,10 +59,6 @@ export const en: Record<TrajectoryKey, string> = {
'toolbar.calls': 'Calls',
'toolbar.expandCalls': 'Expand calls',
'toolbar.collapseCalls': 'Collapse calls',
'toolbar.export': 'Export',
'toolbar.exportAria': 'Export session log',
'toolbar.exporting': 'Exporting…',
'toolbar.exportTitle': 'Export session log (ZIP, includes subagents)',
'toolbar.search': 'Search trajectory',
'toolbar.searchPlaceholder': 'Search',
}

View File

@@ -13,18 +13,6 @@
background: var(--dsw-alias-bg-layer-1);
}
.exportError {
box-sizing: border-box;
flex: none;
width: 100%;
padding: 4px 10px;
border-bottom: 1px solid var(--dsw-alias-border-l2);
color: var(--dsw-alias-label-danger, var(--dsw-alias-label-primary));
background: var(--dsw-alias-bg-layer-2);
font: var(--dsw-font-xxs-12);
overflow-wrap: anywhere;
}
.ledger {
position: relative;
z-index: 0;