/** * The automatic-maintenance settings panel inside the session-archive * section: independent auto-archive / auto-delete switches with day * thresholds (validated locally before save), candidate previews, manual * run-now buttons, and the persisted last-run/next-check status. * @module @linxin666/dsh-session-archive/client/AutoSettings */ import { useMemo, useState, useSyncExternalStore, type ReactNode } from 'react' import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client' import { AUTO_ARCHIVE_DAYS_MAX, AUTO_ARCHIVE_DAYS_MIN, AUTO_DELETE_DAYS_MAX, AUTO_DELETE_DAYS_MIN, type SessionArchiveConfig, } from '../core/config.ts' import type { AutoStateView } from '../core/types.ts' import type { ArchiveController } from './archive-controller.ts' import { formatBytes, formatTime } from './dialogs.tsx' import { t } from './locales.ts' import styles from './archive.module.css' /** One validated day-threshold input; invalid values are never saved. */ function DaysInput(props: { value: number | undefined min: number max: number label: string onSave: (value: number) => void }): ReactNode { const [text, setText] = useState(null) const effective = text ?? (props.value === undefined ? '' : String(props.value)) const parsed = Number(effective) const valid = Number.isFinite(parsed) && Number.isInteger(parsed) && parsed >= props.min && parsed <= props.max return ( ) } export function AutoSettingsPanel(props: { settings: ConfigForm controller: ArchiveController auto?: AutoStateView }): ReactNode { // Subscribe: the shared form replaces its snapshot after each accepted write; // without this subscription the controlled checkboxes never re-render and // appear stuck. The form's subscribe/getSnapshot are prototype methods of the // official ConfigForm controller, and useSyncExternalStore invokes both as // bare functions — they must be bound to the form first or `this.store` reads // undefined and the slot crashes. const settings = props.settings const subscribe = useMemo(() => settings.subscribe.bind(settings), [settings]) const getSnapshot = useMemo(() => settings.getSnapshot.bind(settings), [settings]) const snapshot = useSyncExternalStore(subscribe, getSnapshot) const value: SessionArchiveConfig = snapshot.value ?? {} const ui = useSyncExternalStore(props.controller.store.subscribe, props.controller.store.getSnapshot) const autoPreview = ui.autoPreview const autoPreviewLoading = ui.autoPreviewLoading const cycleRunning = props.auto?.cycleRunning === true // The form answers every write with whether the Host accepted it: `false` // covers a refusal and a skipped write, and a rejected transport throws. Both // are a failed save, so the note stays visible until a write lands — a // refused setting must never look saved. const [saveFailed, setSaveFailed] = useState(false) const write = (field: keyof SessionArchiveConfig, value: unknown): void => { void props.settings.set(field, value).then( (accepted) => { setSaveFailed(!accepted) }, () => { setSaveFailed(true) }, ) } const runStatsLine = (stats: AutoStateView['lastArchiveRun'], key: 'arch.auto.lastArchive' | 'arch.auto.lastDelete'): ReactNode => { if (stats === undefined) return {t('arch.auto.neverRun')} return {t(key, { time: formatTime(stats.at), total: stats.total, ok: stats.ok, skipped: stats.skipped, failed: stats.failed })} } return (
{t('arch.auto.title')}
{t('arch.auto.defaultOff')}
{ write('autoArchiveDays', value) }} />
{ write('autoDeleteDays', value) }} />
{t('arch.auto.recordManualNote')}
{saveFailed && (
{t('arch.auto.saveFailed')}
)}
{cycleRunning &&
{t('arch.auto.cycleRunning')}
} {autoPreview !== null && (
{t('arch.auto.preview.archive', { n: autoPreview.archiveCandidates.length })}
{t('arch.auto.preview.delete', { n: autoPreview.deleteCandidates.length, size: formatBytes(autoPreview.deleteBytes) })}
{autoPreview.archiveCandidates.length === 0 && autoPreview.deleteCandidates.length === 0 && (
{t('arch.auto.preview.none')}
)}
)}
{runStatsLine(props.auto?.lastArchiveRun, 'arch.auto.lastArchive')} {runStatsLine(props.auto?.lastDeleteRun, 'arch.auto.lastDelete')} {props.auto?.nextCheckAt !== undefined && ( {t('arch.auto.nextRun', { time: formatTime(props.auto.nextCheckAt) })} )}
{t('arch.auto.basis')}
) }