From d9d1c3e079da98c1319c9ceeb1a75056aec62bcc Mon Sep 17 00:00:00 2001 From: _Kerman Date: Fri, 31 Jul 2026 14:09:56 +0800 Subject: [PATCH] fix(trajectory): use runtime preference persistence --- packages/client/ui-trajectory/package.json | 2 + .../src/client/TrajectoryView.tsx | 36 +++------- .../src/client/duration-store.ts | 13 ++++ .../client/ui-trajectory/src/client/index.ts | 5 +- .../client/ui-trajectory/tests/views.spec.tsx | 69 ++++++++++++++++--- 5 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 packages/client/ui-trajectory/src/client/duration-store.ts diff --git a/packages/client/ui-trajectory/package.json b/packages/client/ui-trajectory/package.json index fc853d19d1..8da6559866 100644 --- a/packages/client/ui-trajectory/package.json +++ b/packages/client/ui-trajectory/package.json @@ -24,6 +24,7 @@ }, "dshClient": { "inject": [ + "@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-ui-conversation" ], "platform": "web" @@ -37,6 +38,7 @@ "diff": "^9.0.0" }, "peerDependencies": { + "@deepseek-ai/dsh-client-runtime": "^0.0.1", "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-client-ui-primitives": "^0.0.1", "cordis": "^4.0.0-rc.7", diff --git a/packages/client/ui-trajectory/src/client/TrajectoryView.tsx b/packages/client/ui-trajectory/src/client/TrajectoryView.tsx index e2c86cc1f6..01839cbf91 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryView.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryView.tsx @@ -5,7 +5,7 @@ import type { ConvViewProps } from '@deepseek-ai/dsh-client-ui-conversation/clie import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots' import type { AssistantMessageNode, ConversationContext, - SessionHistoryFace, + SessionHistoryFace, SnapshotStore, } from '@deepseek-ai/dsh-client-runtime/client' import { deriveTrajectoryContextBranches, trajectoryBranchContainsRequest, @@ -26,34 +26,15 @@ import { import css from './views.module.css' const EMPTY_IDS: ReadonlySet = new Set() -const DURATION_STORAGE_KEY = 'dsh.trajectory.duration' - -/** Restore the browser-wide duration preference; absent or unreadable storage defaults off. */ -function restoreActualDuration(): boolean { - if (typeof localStorage === 'undefined') return false - try { - return localStorage.getItem(DURATION_STORAGE_KEY) === 'true' - } catch { - // Storage access can throw in privacy mode; the default remains usable. - return false - } -} - -/** Persist the browser-wide duration preference without making storage availability fatal. */ -function persistActualDuration(actualDuration: boolean): void { - if (typeof localStorage === 'undefined') return - try { - localStorage.setItem(DURATION_STORAGE_KEY, String(actualDuration)) - } catch { - // Storage access can throw in privacy mode or at quota; this mount still - // keeps the selected preference in React state. - } -} /** Session-history paging needed by the event-complete trajectory view. */ export interface TrajectoryViewInjected { - hooks: { history: SessionHistoryFace } + hooks: { + history: SessionHistoryFace + duration: SnapshotStore + } loadAllHistory: (signal: AbortSignal) => Promise + setActualDuration: (actualDuration: boolean) => void } interface UsageLike { @@ -157,7 +138,7 @@ function searchMatches( } export function TrajectoryView({ - useHistory, loadAllHistory, inspect, onInspectDone, + useHistory, useDuration, loadAllHistory, setActualDuration, inspect, onInspectDone, }: ConvViewProps & InjectFace) { const [collapsedTurns, setCollapsedTurns] = useState>(EMPTY_IDS) const [collapsedAssistants, setCollapsedAssistants] = @@ -166,7 +147,7 @@ export function TrajectoryView({ branchId: number range: TrajectoryTimeRange } | null>(null) - const [actualDuration, setActualDuration] = useState(restoreActualDuration) + const actualDuration = useDuration(value => value) const [actualTime, setActualTime] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [selectedTimelineIndex, setSelectedTimelineIndex] = useState(null) @@ -480,7 +461,6 @@ export function TrajectoryView({ { - persistActualDuration(nextActualDuration) setActualDuration(nextActualDuration) setTimelineSelection(null) }} diff --git a/packages/client/ui-trajectory/src/client/duration-store.ts b/packages/client/ui-trajectory/src/client/duration-store.ts new file mode 100644 index 0000000000..f965dff2d8 --- /dev/null +++ b/packages/client/ui-trajectory/src/client/duration-store.ts @@ -0,0 +1,13 @@ +import { + createSnapshotStore, type SnapshotStore, +} from '@deepseek-ai/dsh-client-runtime/client' + +/** + * Create the browser-wide trajectory duration preference source. + * @returns a persisted source shared by every session view in one plugin lifecycle. + */ +export function createTrajectoryDurationStore(): SnapshotStore { + return createSnapshotStore(false, { + persist: { name: 'dsh.trajectory.duration' }, + }) +} diff --git a/packages/client/ui-trajectory/src/client/index.ts b/packages/client/ui-trajectory/src/client/index.ts index 347b5c9cec..94f512a9a8 100644 --- a/packages/client/ui-trajectory/src/client/index.ts +++ b/packages/client/ui-trajectory/src/client/index.ts @@ -7,6 +7,7 @@ import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client' // Type-only: the 'conversation.view' SlotMap row (declared by the slot's // 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 { TrajectoryView, type TrajectoryViewInjected } from './TrajectoryView.tsx' /** @@ -24,6 +25,7 @@ export const inject = ['slots', 'conversation', 'sessionHistory'] * @param ctx - client root context. */ export function apply(ctx: Context): void { + const duration = createTrajectoryDurationStore() ctx.slots.register({ name: 'conversation.view', id: 'trajectory', @@ -32,8 +34,9 @@ export function apply(ctx: Context): void { inject: (sessionId: SessionId): TrajectoryViewInjected => { const history = ctx.sessionHistory.source(sessionId) return { - hooks: { history }, + hooks: { history, duration }, loadAllHistory: signal => history.loadAll(signal), + setActualDuration: (value) => { duration.set(value) }, } }, }, TrajectoryView) diff --git a/packages/client/ui-trajectory/tests/views.spec.tsx b/packages/client/ui-trajectory/tests/views.spec.tsx index 5dd7805b70..a3360efa5d 100644 --- a/packages/client/ui-trajectory/tests/views.spec.tsx +++ b/packages/client/ui-trajectory/tests/views.spec.tsx @@ -31,6 +31,7 @@ import { TrajectoryTimeline } from '../src/client/TrajectoryTimeline.tsx' import { TrajectoryView, type TrajectoryViewInjected, } from '../src/client/TrajectoryView.tsx' +import { createTrajectoryDurationStore } from '../src/client/duration-store.ts' import { deriveTrajectoryTimeline } from '../src/client/timeline.ts' const SID = 's1' as SessionId @@ -96,6 +97,16 @@ function standaloneHistory( } } +function standaloneDuration(): Pick< + ComponentProps, 'useDuration' | 'setActualDuration' +> { + const duration = createSnapshotStore(false) + return { + useDuration: bindSnapshotSelector(duration), + setActualDuration: (value) => { duration.set(value) }, + } +} + function fakeSession(nodes: ConversationSnapshot['nodes']) { const store = createSnapshotStore({ nodes, pending: [], partial: null, @@ -187,12 +198,15 @@ function mount(slots: SlotsService, nodes: ConversationSnapshot['nodes'] = NODES ? {} : injectEntry(SID) const injectedProps = 'hooks' in injected - ? { - loadAllHistory: (injected as TrajectoryViewInjected).loadAllHistory, - useHistory: bindSnapshotSelector( - (injected as TrajectoryViewInjected).hooks.history, - ), - } + ? (() => { + const trajectory = injected as TrajectoryViewInjected + return { + loadAllHistory: trajectory.loadAllHistory, + setActualDuration: trajectory.setActualDuration, + useHistory: bindSnapshotSelector(trajectory.hooks.history), + useDuration: bindSnapshotSelector(trajectory.hooks.duration), + } + })() : injected return ( { await b.fiber.dispose() expect(tabsOf(b.slots).map(v => v.id)).toEqual(['chat']) }) + + it('shares one browser-wide duration preference across session injections', async () => { + const b = await bench() + const entry = b.slots.entries('conversation.view') + .find(candidate => candidate.options.id === 'trajectory') + expect(entry).toBeDefined() + const injectEntry = entry!.inject as unknown as ( + sessionId: SessionId, + ) => TrajectoryViewInjected + const first = injectEntry(SID) + const second = injectEntry('s2' as SessionId) + + expect(second.hooks.duration).toBe(first.hooks.duration) + first.setActualDuration(true) + expect(second.hooks.duration.getSnapshot()).toBe(true) + expect(localStorage.getItem('dsh.trajectory.duration')).toBe('true') + expect(localStorage.getItem(`dsh.trajectory.duration.${SID}`)).toBeNull() + }) }) describe('tab switching in ConversationRoot', () => { @@ -653,6 +685,7 @@ describe('timeline projection', () => { { ...standaloneProps([]), ...standaloneHistory(historySnapshot([])), + ...standaloneDuration(), }, )) expect(screen.getByRole('toolbar', { name: 'Trajectory toolbar' })).toBeTruthy() @@ -661,12 +694,19 @@ describe('timeline projection', () => { }) describe('TrajectoryView branches', () => { - it('persists the duration preference across trajectory view mounts', () => { - const props = { + it('persists the duration preference through the runtime snapshot-store seam', () => { + const firstDuration = createTrajectoryDurationStore() + const commonProps = { ...standaloneProps(NODES), ...standaloneHistory(historySnapshot(NODES)), } - const first = render() + const first = render( + { firstDuration.set(value) }} + />, + ) const duration = screen.getByRole('button', { name: 'Use actual duration' }) expect(duration.getAttribute('aria-pressed')).toBe('false') @@ -674,7 +714,14 @@ describe('TrajectoryView branches', () => { expect(localStorage.getItem('dsh.trajectory.duration')).toBe('true') first.unmount() - render() + const restoredDuration = createTrajectoryDurationStore() + render( + { restoredDuration.set(value) }} + />, + ) expect(screen.getByRole('button', { name: 'Use actual duration' }).getAttribute('aria-pressed')) .toBe('true') }) @@ -734,6 +781,7 @@ describe('TrajectoryView branches', () => { const view = render( Promise.resolve())} />, @@ -776,6 +824,7 @@ describe('TrajectoryView branches', () => { render( Promise.resolve())} />,