feat(session-export): add command and Header action
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user