1
0

chat-scroll-fixture.ts 7.8 KB

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