session-timeline.fixture.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. const words = [
  2. "alpha",
  3. "bravo",
  4. "charlie",
  5. "delta",
  6. "echo",
  7. "foxtrot",
  8. "golf",
  9. "hotel",
  10. "india",
  11. "juliet",
  12. "kilo",
  13. "lima",
  14. "metro",
  15. "nova",
  16. "orbit",
  17. "pixel",
  18. "quartz",
  19. "river",
  20. "signal",
  21. "vector",
  22. ]
  23. const sourceID = "ses_smoke_source"
  24. const targetID = "ses_smoke_target"
  25. const directory = "C:/OpenCode/SmokeProject"
  26. const projectID = "proj_smoke_timeline"
  27. const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
  28. type MessageInfo = Record<string, unknown> & { id: string; role: "user" | "assistant" }
  29. type MessagePart = Record<string, unknown> & { id: string; type: string; text?: string; tool?: string }
  30. type Message = { info: MessageInfo; parts: MessagePart[] }
  31. function lorem(seed: number, length: number) {
  32. let out = ""
  33. let i = seed
  34. while (out.length < length) {
  35. const word = words[i % words.length]
  36. out += (out ? " " : "") + word
  37. if (i % 17 === 0) out += ".\n\n"
  38. i += 7
  39. }
  40. return out.slice(0, length)
  41. }
  42. function id(prefix: string, value: number) {
  43. return `${prefix}_smoke_${String(value).padStart(4, "0")}`
  44. }
  45. function userMessage(sessionID: string, index: number, textLength: number, diffs: unknown[] = []): Message {
  46. const messageID = id("msg_user", index)
  47. return {
  48. info: {
  49. id: messageID,
  50. sessionID,
  51. role: "user",
  52. time: { created: 1700000000000 + index * 10_000 },
  53. summary: { diffs },
  54. agent: "build",
  55. model,
  56. },
  57. parts: [
  58. {
  59. id: id("prt_user_text", index),
  60. sessionID,
  61. messageID,
  62. type: "text",
  63. text: lorem(index, textLength),
  64. },
  65. ],
  66. }
  67. }
  68. function assistantMessage(sessionID: string, index: number, parentID: string, parts: MessagePart[]): Message {
  69. const messageID = id("msg_assistant", index)
  70. return {
  71. info: {
  72. id: messageID,
  73. sessionID,
  74. role: "assistant",
  75. time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 8_000 },
  76. parentID,
  77. modelID: model.modelID,
  78. providerID: model.providerID,
  79. mode: "build",
  80. agent: "build",
  81. path: { cwd: directory, root: directory },
  82. cost: 0.01,
  83. tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
  84. variant: "max",
  85. finish: "stop",
  86. },
  87. parts: parts.map((part) => ({
  88. ...part,
  89. sessionID,
  90. messageID,
  91. })),
  92. }
  93. }
  94. function textPart(index: number, partIndex: number, length: number): MessagePart {
  95. return { id: id(`prt_text_${partIndex}`, index), type: "text", text: lorem(index * 13 + partIndex, length) }
  96. }
  97. function reasoningPart(index: number, partIndex: number, length: number): MessagePart {
  98. return {
  99. id: id(`prt_reasoning_${partIndex}`, index),
  100. type: "reasoning",
  101. text: lorem(index * 19 + partIndex, length),
  102. time: { start: 1700000000000 + index * 10_000, end: 1700000000000 + index * 10_000 + 500 },
  103. }
  104. }
  105. function toolPart(
  106. index: number,
  107. partIndex: number,
  108. tool: string,
  109. input: Record<string, unknown>,
  110. outputLength = 160,
  111. ): MessagePart {
  112. const metadata =
  113. tool === "apply_patch"
  114. ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
  115. : tool === "edit" || tool === "write"
  116. ? {
  117. filediff: fileDiff(String(input.filePath ?? `src/generated/file-${index}.ts`), index),
  118. diff: patch(index, outputLength),
  119. preview: patch(index + 1, 420),
  120. }
  121. : tool === "question"
  122. ? { answers: [["Proceed"], ["Keep sample output"]] }
  123. : {}
  124. return {
  125. id: id(`prt_tool_${tool}_${partIndex}`, index),
  126. type: "tool",
  127. callID: id("call", index * 10 + partIndex),
  128. tool,
  129. state: {
  130. status: "completed",
  131. input,
  132. output: lorem(index * 23 + partIndex, outputLength),
  133. title: tool === "bash" ? "Verify generated output" : input.filePath || input.path || input.pattern || "completed",
  134. metadata,
  135. time: { start: 1700000000000 + index * 10_000, end: 1700000000000 + index * 10_000 + 400 },
  136. },
  137. }
  138. }
  139. function patchFile(seed: number, type: "add" | "update" | "delete") {
  140. return {
  141. filePath: `src/generated/patch-${seed}.ts`,
  142. relativePath: `src/generated/patch-${seed}.ts`,
  143. type,
  144. additions: (seed % 7) + 1,
  145. deletions: type === "add" ? 0 : seed % 4,
  146. patch: patch(seed, 520),
  147. before: type === "add" ? undefined : code(seed, 18),
  148. after: type === "delete" ? undefined : code(seed + 1, 24),
  149. }
  150. }
  151. function fileDiff(file: string, seed: number) {
  152. return {
  153. file,
  154. additions: (seed % 9) + 1,
  155. deletions: seed % 4,
  156. before: code(seed, 32),
  157. after: code(seed + 1, 38),
  158. }
  159. }
  160. function patch(seed: number, length: number) {
  161. return `diff --git a/src/generated/file-${seed}.ts b/src/generated/file-${seed}.ts\n+${lorem(seed, length).replace(/\n/g, "\n+")}`
  162. }
  163. function code(seed: number, lines: number) {
  164. return Array.from({ length: lines }, (_, index) => `export const value${index} = "${lorem(seed + index, 32)}"`).join(
  165. "\n",
  166. )
  167. }
  168. function turn(index: number): Message[] {
  169. const diff = index % 9 === 0 ? [fileDiff(`src/generated/summary-${index}.ts`, index)] : []
  170. const user = userMessage(targetID, index, 100 + (index % 4) * 80, diff)
  171. const parts = [
  172. ...(index % 5 === 0 ? [reasoningPart(index, 0, 420)] : []),
  173. ...(index % 3 === 0
  174. ? [
  175. toolPart(index, 0, "read", { filePath: `src/generated/file-${index}.ts`, offset: 0, limit: 80 }, 220),
  176. toolPart(index, 5, "glob", { path: directory, pattern: `**/*sample-${index}*.ts` }, 140),
  177. toolPart(index, 1, "grep", { path: directory, pattern: `sample-${index}`, include: "*.ts" }, 180),
  178. toolPart(index, 6, "list", { path: `src/generated/${index}` }, 120),
  179. ]
  180. : []),
  181. textPart(index, 2, 160 + (index % 6) * 90),
  182. ...(index % 4 === 0 ? [toolPart(index, 3, "edit", { filePath: `src/generated/file-${index}.ts` }, 700)] : []),
  183. ...(index % 6 === 0
  184. ? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
  185. : []),
  186. ...(index % 8 === 0
  187. ? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
  188. : []),
  189. ...(index % 7 === 0
  190. ? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
  191. : []),
  192. ...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
  193. ...(index % 11 === 0 ? [toolPart(index, 10, "websearch", { query: "sample movement notes" }, 240)] : []),
  194. ...(index % 13 === 0
  195. ? [
  196. toolPart(
  197. index,
  198. 11,
  199. "question",
  200. { questions: [{ question: "Use generated fixture?" }, { question: "Keep same row shape?" }] },
  201. 120,
  202. ),
  203. ]
  204. : []),
  205. ...(index % 17 === 0
  206. ? [toolPart(index, 12, "task", { description: "Inspect generated fixture", subagent_type: "explore" }, 160)]
  207. : []),
  208. ]
  209. return [user, assistantMessage(targetID, index, user.info.id, parts)]
  210. }
  211. const targetMessages = Array.from({ length: 72 }, (_, index) => turn(index)).flat()
  212. const sourceMessages = Array.from({ length: 12 }, (_, index) => [
  213. userMessage(sourceID, index + 1000, 120),
  214. assistantMessage(sourceID, index + 1000, id("msg_user", index + 1000), [textPart(index + 1000, 0, 240)]),
  215. ]).flat()
  216. function renderable(part: MessagePart) {
  217. if (part.type === "tool" && part.tool === "todowrite") return false
  218. if (part.type === "text") return !!part.text.trim()
  219. if (part.type === "reasoning") return !!part.text.trim()
  220. return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch"
  221. }
  222. function orderedParts(message: Message) {
  223. return message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
  224. }
  225. export const fixture = {
  226. directory,
  227. project: {
  228. id: projectID,
  229. worktree: directory,
  230. vcs: "git",
  231. name: "smoke-project",
  232. time: { created: 1700000000000, updated: 1700000000000 },
  233. sandboxes: [],
  234. },
  235. provider: {
  236. all: [
  237. {
  238. id: "opencode",
  239. name: "OpenCode",
  240. models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } },
  241. },
  242. ],
  243. connected: ["opencode"],
  244. default: { providerID: "opencode", modelID: "claude-opus-4-6" },
  245. },
  246. sessions: [
  247. {
  248. id: sourceID,
  249. slug: "source",
  250. projectID,
  251. directory,
  252. title: "Uncommitted changes inquiry",
  253. version: "dev",
  254. time: { created: 1700000000000, updated: 1700000000000 },
  255. },
  256. {
  257. id: targetID,
  258. slug: "target",
  259. projectID,
  260. directory,
  261. title: "Example Game: sample jump movement & sample physics analysis",
  262. version: "dev",
  263. time: { created: 1700000001000, updated: 1700000001000 },
  264. },
  265. ],
  266. sourceID,
  267. targetID,
  268. messages: { [sourceID]: sourceMessages, [targetID]: targetMessages },
  269. expected: {
  270. sourceTitle: "Uncommitted changes inquiry",
  271. targetTitle: "Example Game: sample jump movement & sample physics analysis",
  272. targetMessageIDs: targetMessages
  273. .filter((message) => message.info.role === "user")
  274. .map((message) => message.info.id),
  275. targetPartIDs: targetMessages.flatMap((message) =>
  276. orderedParts(message)
  277. .filter(renderable)
  278. .map((part) => part.id),
  279. ),
  280. },
  281. }
  282. export function pageMessages(sessionID: string, limit: number, before?: string) {
  283. const messages = fixture.messages[sessionID as keyof typeof fixture.messages] ?? []
  284. const end = before
  285. ? Math.max(
  286. 0,
  287. messages.findIndex((message) => message.info.id === before),
  288. )
  289. : messages.length
  290. const start = Math.max(0, end - limit)
  291. return {
  292. items: messages.slice(start, end),
  293. cursor: start > 0 ? messages[start]!.info.id : undefined,
  294. }
  295. }