// Synthetic long-chat history for browser behavior contracts. The fixture is // generated through Session so pagination exercises the same event shapes as // persisted conversations, while unique markers identify semantic rows // without depending on CSS-module names or virtualizer DOM positions. import { CallId, createAssistantMessage, createToolResultMessage, createUserMessage, } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, Session, SessionId, } from '@deepseek-ai/dsh-session' // Carries the session/title event declaration into this fixture builder. import type {} from '@deepseek-ai/dsh-session-title' /** Options for one deterministic long-chat fixture. */ export interface ChatScrollFixtureOptions { /** Marker namespace, used when two sessions share one browser world. */ readonly markerPrefix: string /** Searchable title projected into the sidebar. */ readonly title: string /** Number of closed turns to generate. */ readonly turns?: number } /** Semantic marker helpers returned with a generated fixture. */ interface ChatScrollMarkers { /** Marker painted in the human message for a turn. */ user(turn: number): string /** Marker painted in the final assistant message for a turn. */ assistant(turn: number): string /** Marker painted in one seeded bash call and result. */ tool(turn: number, index: number): string } /** Generated JSONL plus the stable facts browser scenarios assert. */ export interface ChatScrollFixture { readonly log: string readonly markers: ChatScrollMarkers readonly title: string readonly turns: number } const DEFAULT_TURNS = 88 const TOOL_INTERVAL = 8 const CODE_INTERVAL = 11 function text(value: string): { type: 'text'; text: string }[] { return [{ type: 'text', text: value }] } function suffix(turn: number): string { return String(turn).padStart(3, '0') } function markerHelpers(prefix: string): ChatScrollMarkers { return { user: turn => `CHAT_SCROLL_${prefix}_USER_${suffix(turn)}`, assistant: turn => `CHAT_SCROLL_${prefix}_ASSISTANT_${suffix(turn)}`, tool: (turn, index) => `CHAT_SCROLL_${prefix}_TOOL_${suffix(turn)}_${String(index)}`, } } function appendRequestHeader(session: Session, turn: number, step: number): void { session.append('request/header', { header: { config: { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, system: `Synthetic chat-scroll request for turn ${String(turn)}, step ${String(step)}.`, }, reason: turn === 1 && step === 1 ? 'initial' : 'change', }) } function appendAssistant(session: Session, turn: number, step: number, body: string): void { session.append('assistant/message', { turn, step, message: createAssistantMessage({ content: text(body), source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, }), usage: { inputTokens: 2_000 + turn * 7, outputTokens: 180 + step * 20, }, }, { surfaceOp: 'append' }) } function codeBlock(turn: number): string { if (turn % CODE_INTERVAL !== 0) return '' const lines = Array.from( { length: 30 }, (_, index) => `const scroll_case_${suffix(turn)}_${String(index).padStart(2, '0')} = ${String(turn + index)}`, ) return `\n\n\`\`\`ts\n${lines.join('\n')}\n\`\`\`` } function appendToolStep( session: Session, markers: ChatScrollMarkers, turn: number, ): void { const calls = [1, 2].map((index) => { const marker = markers.tool(turn, index) const callId = CallId(`chat-scroll-${suffix(turn)}-${String(index)}`) const args = JSON.stringify({ command: `printf '${marker}\\n'`, description: marker, }) return { args, callId, marker } }) session.append('assistant/message', { turn, step: 1, message: createAssistantMessage({ content: [ { type: 'reasoning', text: `Inspecting two scroll fixtures for turn ${String(turn)}.` }, ...calls.map(call => ({ type: 'tool-call' as const, id: call.callId, name: 'bash', arguments: call.args, })), ], source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, }), usage: { inputTokens: 2_000 + turn * 7, outputTokens: 240, reasoningTokens: 30 }, }, { surfaceOp: 'append' }) for (const call of calls) { const source = session.append('tool/call', { turn, step: 1, callId: call.callId, name: 'bash', arguments: call.args, }) session.append('tool/result', { turn, step: 1, message: createToolResultMessage({ callId: call.callId, content: text(Array.from( { length: 12 }, (_, line) => `${call.marker} output line ${String(line + 1).padStart(2, '0')}`, ).join('\n')), isError: false, }), }, { surfaceOp: 'append', sourceEventSeqs: [source.seq] }) } } function fixtureLog(session: Session): string { return [ JSON.stringify({ type: 'session', version: SESSION_FORMAT_VERSION, id: '{{sessionId}}', createdAt: Date.now() - 60_000, cwd: '{{cwd}}', delegationDepth: 0, }), ...session.events.map(event => JSON.stringify(event)), '', ].join('\n') } /** * Build a multi-page conversation with prose, fenced code, and paired bash * calls/results. Every turn is closed, so cold resume cannot repair or mutate * the seed before the browser observes it. * @param options - Fixture identity and optional turn count. * @returns Canonical JSONL and semantic marker helpers. */ export function createChatScrollFixture(options: ChatScrollFixtureOptions): ChatScrollFixture { const turns = options.turns ?? DEFAULT_TURNS const markers = markerHelpers(options.markerPrefix) const session = Session.create(SessionId(`chat-scroll-${options.markerPrefix.toLowerCase()}-template`)) for (let turn = 1; turn <= turns; turn += 1) { session.append('turn/start', { turn, }) const user = session.append('user/message', createUserMessage({ content: text( `${markers.user(turn)} Review the long-running conversation state for turn ${String(turn)}. ` + 'Keep the visible message stable while history, tools, and new output change around it.', ), source: { kind: 'user' }, }), { surfaceOp: 'append' }) if (turn === 1) { session.append('session/title', { title: options.title, messageSeqs: [user.seq], source: { kind: 'fallback' }, }) } session.append('step/start', { turn, step: 1 }) appendRequestHeader(session, turn, 1) if (turn % TOOL_INTERVAL === 0) { appendToolStep(session, markers, turn) session.append('step/end', { turn, step: 1 }) session.append('step/start', { turn, step: 2 }) appendRequestHeader(session, turn, 2) appendAssistant( session, turn, 2, `${markers.assistant(turn)} Both tool results are accounted for. ` + `This settled response keeps turn ${String(turn)} identifiable after paging.${codeBlock(turn)}`, ) session.append('step/end', { turn, step: 2 }) } else { appendAssistant( session, turn, 1, `${markers.assistant(turn)} The conversation remains readable after several paragraphs.\n\n` + `Turn ${String(turn)} deliberately carries enough prose to wrap at narrower viewport widths. ` + 'The semantic marker stays near the start so geometry probes can find the same rendered row.\n\n' + `The closing paragraph makes this a realistic assistant response rather than a one-line list item.${codeBlock(turn)}`, ) session.append('step/end', { turn, step: 1 }) } session.append('turn/end', { turn, reason: { kind: 'completed' } }) } return { log: fixtureLog(session), markers, title: options.title, turns } }