1
0

chat-scroll-fixture.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. // Synthetic long-chat history for browser behavior contracts. The fixture is
  2. // generated through Session so pagination exercises the same event shapes as
  3. // persisted conversations, while unique markers identify semantic rows
  4. // without depending on CSS-module names or virtualizer DOM positions.
  5. import {
  6. ToolCallId,
  7. createAssistantMessage,
  8. createSystemMessage,
  9. createToolResultMessage,
  10. createUserMessage,
  11. } from '@deepseek-ai/dsh-llm'
  12. import {
  13. SESSION_FORMAT_VERSION,
  14. Session,
  15. SessionId,
  16. } from '@deepseek-ai/dsh-session'
  17. // Carries the session/title event declaration into this fixture builder.
  18. import type {} from '@deepseek-ai/dsh-session-title'
  19. /** Options for one deterministic long-chat fixture. */
  20. export interface ChatScrollFixtureOptions {
  21. /** Marker namespace, used when two sessions share one browser world. */
  22. readonly markerPrefix: string
  23. /** Searchable title projected into the sidebar. */
  24. readonly title: string
  25. /** Number of closed turns to generate. */
  26. readonly turns?: number
  27. }
  28. /** Semantic marker helpers returned with a generated fixture. */
  29. interface ChatScrollMarkers {
  30. /** Marker painted in the human message for a turn. */
  31. user(turn: number): string
  32. /** Marker painted in the final assistant message for a turn. */
  33. assistant(turn: number): string
  34. /** Marker painted in one seeded bash call and result. */
  35. tool(turn: number, index: number): string
  36. }
  37. /** Generated JSONL plus the stable facts browser scenarios assert. */
  38. export interface ChatScrollFixture {
  39. readonly log: string
  40. readonly markers: ChatScrollMarkers
  41. readonly title: string
  42. readonly turns: number
  43. }
  44. const DEFAULT_TURNS = 88
  45. const TOOL_INTERVAL = 8
  46. const CODE_INTERVAL = 11
  47. function text(value: string): { type: 'text'; text: string }[] {
  48. return [{ type: 'text', text: value }]
  49. }
  50. function suffix(turn: number): string {
  51. return String(turn).padStart(3, '0')
  52. }
  53. function markerHelpers(prefix: string): ChatScrollMarkers {
  54. return {
  55. user: turn => `CHAT_SCROLL_${prefix}_USER_${suffix(turn)}`,
  56. assistant: turn => `CHAT_SCROLL_${prefix}_ASSISTANT_${suffix(turn)}`,
  57. tool: (turn, index) => `CHAT_SCROLL_${prefix}_TOOL_${suffix(turn)}_${String(index)}`,
  58. }
  59. }
  60. function appendSystemPrompt(session: Session, turn: number, step: number): void {
  61. session.append('system/message', {
  62. turn,
  63. step,
  64. message: createSystemMessage(
  65. 'Synthetic chat-scroll system prompt.',
  66. '@deepseek-ai/dsh-system-prompt',
  67. ),
  68. }, { surfaceOp: 'append' })
  69. }
  70. function appendRequestHeader(session: Session, turn: number, step: number): void {
  71. session.append('request/header', {
  72. header: {
  73. config: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
  74. },
  75. reason: turn === 1 && step === 1 ? 'initial' : 'change',
  76. })
  77. }
  78. function appendAssistant(session: Session, turn: number, step: number, body: string): void {
  79. session.append('assistant/message', {
  80. stream: [],
  81. turn,
  82. step,
  83. message: createAssistantMessage({
  84. content: text(body),
  85. source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
  86. }),
  87. usage: {
  88. inputTokens: 2_000 + turn * 7,
  89. outputTokens: 180 + step * 20,
  90. },
  91. }, { surfaceOp: 'append' })
  92. }
  93. function codeBlock(turn: number): string {
  94. if (turn % CODE_INTERVAL !== 0) return ''
  95. const lines = Array.from(
  96. { length: 30 },
  97. (_, index) => `const scroll_case_${suffix(turn)}_${String(index).padStart(2, '0')} = ${String(turn + index)}`,
  98. )
  99. return `\n\n\`\`\`ts\n${lines.join('\n')}\n\`\`\``
  100. }
  101. function appendToolStep(
  102. session: Session,
  103. markers: ChatScrollMarkers,
  104. turn: number,
  105. ): void {
  106. const calls = [1, 2].map((index) => {
  107. const marker = markers.tool(turn, index)
  108. const callId = ToolCallId(`chat-scroll-${suffix(turn)}-${String(index)}`)
  109. const args = JSON.stringify({
  110. command: `printf '${marker}\\n'`,
  111. description: marker,
  112. })
  113. return { args, callId, marker }
  114. })
  115. session.append('assistant/message', {
  116. stream: [],
  117. turn,
  118. step: 1,
  119. message: createAssistantMessage({
  120. content: [
  121. { type: 'reasoning', text: `Inspecting two scroll fixtures for turn ${String(turn)}.` },
  122. ...calls.map(call => ({
  123. type: 'tool-call' as const,
  124. id: call.callId,
  125. name: 'bash',
  126. arguments: call.args,
  127. })),
  128. ],
  129. source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
  130. }),
  131. usage: { inputTokens: 2_000 + turn * 7, outputTokens: 240, reasoningTokens: 30 },
  132. }, { surfaceOp: 'append' })
  133. for (const call of calls) {
  134. const source = session.append('tool/call', {
  135. turn,
  136. step: 1,
  137. callId: call.callId,
  138. name: 'bash',
  139. arguments: call.args,
  140. })
  141. session.append('tool/result', {
  142. turn,
  143. step: 1,
  144. message: createToolResultMessage({
  145. callId: call.callId,
  146. content: text(Array.from(
  147. { length: 12 },
  148. (_, line) => `${call.marker} output line ${String(line + 1).padStart(2, '0')}`,
  149. ).join('\n')),
  150. isError: false,
  151. }),
  152. }, { surfaceOp: 'append', sourceEventSeqs: [source.seq] })
  153. }
  154. }
  155. function fixtureLog(session: Session): string {
  156. return [
  157. JSON.stringify({
  158. type: 'session',
  159. version: SESSION_FORMAT_VERSION,
  160. id: '{{sessionId}}',
  161. createdAt: Date.now() - 60_000,
  162. cwd: '{{cwd}}',
  163. isSeeded: false,
  164. delegationDepth: 0,
  165. }),
  166. ...session.snapshotEvents().map(event => JSON.stringify(event)),
  167. '',
  168. ].join('\n')
  169. }
  170. /**
  171. * Build a multi-page conversation with prose, fenced code, and paired bash
  172. * calls/results. Every turn is closed, so cold resume cannot repair or mutate
  173. * the seed before the browser observes it.
  174. * @param options - Fixture identity and optional turn count.
  175. * @returns Canonical JSONL and semantic marker helpers.
  176. */
  177. export function createChatScrollFixture(options: ChatScrollFixtureOptions): ChatScrollFixture {
  178. const turns = options.turns ?? DEFAULT_TURNS
  179. const markers = markerHelpers(options.markerPrefix)
  180. const session = Session.create(SessionId(`chat-scroll-${options.markerPrefix.toLowerCase()}-template`))
  181. for (let turn = 1; turn <= turns; turn += 1) {
  182. session.append('turn/start', {
  183. turn,
  184. })
  185. session.append('step/start', { turn, step: 1 })
  186. // Native V3 installs the protected system head before any user surface.
  187. if (turn === 1) appendSystemPrompt(session, turn, 1)
  188. const user = session.append('user/message', createUserMessage({
  189. content: text(
  190. `${markers.user(turn)} Review the long-running conversation state for turn ${String(turn)}. `
  191. + 'Keep the visible message stable while history, tools, and new output change around it.',
  192. ),
  193. source: { kind: 'user' },
  194. }), { surfaceOp: 'append' })
  195. if (turn === 1) {
  196. session.append('session/title', {
  197. title: options.title,
  198. messageSeqs: [user.seq],
  199. source: { kind: 'fallback' },
  200. })
  201. }
  202. appendRequestHeader(session, turn, 1)
  203. if (turn % TOOL_INTERVAL === 0) {
  204. appendToolStep(session, markers, turn)
  205. session.append('step/end', { turn, step: 1 })
  206. session.append('step/start', { turn, step: 2 })
  207. appendRequestHeader(session, turn, 2)
  208. appendAssistant(
  209. session,
  210. turn,
  211. 2,
  212. `${markers.assistant(turn)} Both tool results are accounted for. `
  213. + `This settled response keeps turn ${String(turn)} identifiable after paging.${codeBlock(turn)}`,
  214. )
  215. session.append('step/end', { turn, step: 2 })
  216. } else {
  217. appendAssistant(
  218. session,
  219. turn,
  220. 1,
  221. `${markers.assistant(turn)} The conversation remains readable after several paragraphs.\n\n`
  222. + `Turn ${String(turn)} deliberately carries enough prose to wrap at narrower viewport widths. `
  223. + 'The semantic marker stays near the start so geometry probes can find the same rendered row.\n\n'
  224. + `The closing paragraph makes this a realistic assistant response rather than a one-line list item.${codeBlock(turn)}`,
  225. )
  226. session.append('step/end', { turn, step: 1 })
  227. }
  228. session.append('turn/end', { turn, reason: { kind: 'completed' } })
  229. }
  230. return { log: fixtureLog(session), markers, title: options.title, turns }
  231. }