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