chat-scroll-fixture.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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. CallId,
  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. turn,
  71. step,
  72. message: createAssistantMessage({
  73. content: text(body),
  74. source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
  75. }),
  76. usage: {
  77. inputTokens: 2_000 + turn * 7,
  78. outputTokens: 180 + step * 20,
  79. },
  80. }, { surfaceOp: 'append' })
  81. }
  82. function codeBlock(turn: number): string {
  83. if (turn % CODE_INTERVAL !== 0) return ''
  84. const lines = Array.from(
  85. { length: 30 },
  86. (_, index) => `const scroll_case_${suffix(turn)}_${String(index).padStart(2, '0')} = ${String(turn + index)}`,
  87. )
  88. return `\n\n\`\`\`ts\n${lines.join('\n')}\n\`\`\``
  89. }
  90. function appendToolStep(
  91. session: Session,
  92. markers: ChatScrollMarkers,
  93. turn: number,
  94. ): void {
  95. const calls = [1, 2].map((index) => {
  96. const marker = markers.tool(turn, index)
  97. const callId = CallId(`chat-scroll-${suffix(turn)}-${String(index)}`)
  98. const args = JSON.stringify({
  99. command: `printf '${marker}\\n'`,
  100. description: marker,
  101. })
  102. return { args, callId, marker }
  103. })
  104. session.append('assistant/message', {
  105. turn,
  106. step: 1,
  107. message: createAssistantMessage({
  108. content: [
  109. { type: 'reasoning', text: `Inspecting two scroll fixtures for turn ${String(turn)}.` },
  110. ...calls.map(call => ({
  111. type: 'tool-call' as const,
  112. id: call.callId,
  113. name: 'bash',
  114. arguments: call.args,
  115. })),
  116. ],
  117. source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
  118. }),
  119. usage: { inputTokens: 2_000 + turn * 7, outputTokens: 240, reasoningTokens: 30 },
  120. }, { surfaceOp: 'append' })
  121. for (const call of calls) {
  122. const source = session.append('tool/call', {
  123. turn,
  124. step: 1,
  125. callId: call.callId,
  126. name: 'bash',
  127. arguments: call.args,
  128. })
  129. session.append('tool/result', {
  130. turn,
  131. step: 1,
  132. message: createToolResultMessage({
  133. callId: call.callId,
  134. content: text(Array.from(
  135. { length: 12 },
  136. (_, line) => `${call.marker} output line ${String(line + 1).padStart(2, '0')}`,
  137. ).join('\n')),
  138. isError: false,
  139. }),
  140. }, { surfaceOp: 'append', sourceEventSeqs: [source.seq] })
  141. }
  142. }
  143. function fixtureLog(session: Session): string {
  144. return [
  145. JSON.stringify({
  146. type: 'session',
  147. version: SESSION_FORMAT_VERSION,
  148. id: '{{sessionId}}',
  149. createdAt: Date.now() - 60_000,
  150. cwd: '{{cwd}}',
  151. delegationDepth: 0,
  152. }),
  153. ...session.events.map(event => JSON.stringify(event)),
  154. '',
  155. ].join('\n')
  156. }
  157. /**
  158. * Build a multi-page conversation with prose, fenced code, and paired bash
  159. * calls/results. Every turn is closed, so cold resume cannot repair or mutate
  160. * the seed before the browser observes it.
  161. * @param options - Fixture identity and optional turn count.
  162. * @returns Canonical JSONL and semantic marker helpers.
  163. */
  164. export function createChatScrollFixture(options: ChatScrollFixtureOptions): ChatScrollFixture {
  165. const turns = options.turns ?? DEFAULT_TURNS
  166. const markers = markerHelpers(options.markerPrefix)
  167. const session = Session.create(SessionId(`chat-scroll-${options.markerPrefix.toLowerCase()}-template`))
  168. for (let turn = 1; turn <= turns; turn += 1) {
  169. session.append('turn/start', {
  170. turn,
  171. })
  172. const user = session.append('user/message', createUserMessage({
  173. content: text(
  174. `${markers.user(turn)} Review the long-running conversation state for turn ${String(turn)}. `
  175. + 'Keep the visible message stable while history, tools, and new output change around it.',
  176. ),
  177. source: { kind: 'user' },
  178. }), { surfaceOp: 'append' })
  179. if (turn === 1) {
  180. session.append('session/title', {
  181. title: options.title,
  182. messageSeqs: [user.seq],
  183. source: { kind: 'fallback' },
  184. })
  185. }
  186. session.append('step/start', { turn, step: 1 })
  187. appendRequestHeader(session, turn, 1)
  188. if (turn % TOOL_INTERVAL === 0) {
  189. appendToolStep(session, markers, turn)
  190. session.append('step/end', { turn, step: 1 })
  191. session.append('step/start', { turn, step: 2 })
  192. appendRequestHeader(session, turn, 2)
  193. appendAssistant(
  194. session,
  195. turn,
  196. 2,
  197. `${markers.assistant(turn)} Both tool results are accounted for. `
  198. + `This settled response keeps turn ${String(turn)} identifiable after paging.${codeBlock(turn)}`,
  199. )
  200. session.append('step/end', { turn, step: 2 })
  201. } else {
  202. appendAssistant(
  203. session,
  204. turn,
  205. 1,
  206. `${markers.assistant(turn)} The conversation remains readable after several paragraphs.\n\n`
  207. + `Turn ${String(turn)} deliberately carries enough prose to wrap at narrower viewport widths. `
  208. + 'The semantic marker stays near the start so geometry probes can find the same rendered row.\n\n'
  209. + `The closing paragraph makes this a realistic assistant response rather than a one-line list item.${codeBlock(turn)}`,
  210. )
  211. session.append('step/end', { turn, step: 1 })
  212. }
  213. session.append('turn/end', { turn, reason: { kind: 'completed' } })
  214. }
  215. return { log: fixtureLog(session), markers, title: options.title, turns }
  216. }