mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(trajectory): use runtime preference persistence
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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<number> = 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<boolean>
|
||||
}
|
||||
loadAllHistory: (signal: AbortSignal) => Promise<void>
|
||||
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<TrajectoryViewInjected>) {
|
||||
const [collapsedTurns, setCollapsedTurns] = useState<ReadonlySet<number>>(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<number | null>(null)
|
||||
@@ -480,7 +461,6 @@ export function TrajectoryView({
|
||||
<TrajectoryToolbar
|
||||
actualDuration={actualDuration}
|
||||
onActualDurationChange={(nextActualDuration) => {
|
||||
persistActualDuration(nextActualDuration)
|
||||
setActualDuration(nextActualDuration)
|
||||
setTimelineSelection(null)
|
||||
}}
|
||||
|
||||
@@ -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<boolean> {
|
||||
return createSnapshotStore(false, {
|
||||
persist: { name: 'dsh.trajectory.duration' },
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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<typeof TrajectoryView>, '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 (
|
||||
<View
|
||||
@@ -241,6 +255,24 @@ describe('plugin registration', () => {
|
||||
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(<TrajectoryView {...props} />)
|
||||
const first = render(
|
||||
<TrajectoryView
|
||||
{...commonProps}
|
||||
useDuration={bindSnapshotSelector(firstDuration)}
|
||||
setActualDuration={(value) => { 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(<TrajectoryView {...props} />)
|
||||
const restoredDuration = createTrajectoryDurationStore()
|
||||
render(
|
||||
<TrajectoryView
|
||||
{...commonProps}
|
||||
useDuration={bindSnapshotSelector(restoredDuration)}
|
||||
setActualDuration={(value) => { 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(
|
||||
<TrajectoryView
|
||||
{...standaloneProps([])}
|
||||
{...standaloneDuration()}
|
||||
useHistory={bindSnapshotSelector(store)}
|
||||
loadAllHistory={vi.fn(() => Promise.resolve())}
|
||||
/>,
|
||||
@@ -776,6 +824,7 @@ describe('TrajectoryView branches', () => {
|
||||
render(
|
||||
<TrajectoryView
|
||||
{...standaloneProps([])}
|
||||
{...standaloneDuration()}
|
||||
useHistory={bindSnapshotSelector(store)}
|
||||
loadAllHistory={vi.fn(() => Promise.resolve())}
|
||||
/>,
|
||||
|
||||
Reference in New Issue
Block a user