okengine 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/AGENTS.md +11 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +54 -0
  6. package/package.json +53 -38
  7. package/site/content/docs/ai/mcp.mdx +13 -9
  8. package/site/content/docs/ai/skills.mdx +2 -2
  9. package/site/content/docs/client/calling.mdx +27 -19
  10. package/site/content/docs/client/index.mdx +1 -1
  11. package/site/content/docs/elements/ai/agents.mdx +61 -13
  12. package/site/content/docs/elements/ai/decide.mdx +536 -0
  13. package/site/content/docs/elements/ai/events.mdx +373 -0
  14. package/site/content/docs/elements/ai/index.mdx +9 -3
  15. package/site/content/docs/elements/ai/meta.json +1 -1
  16. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  17. package/site/content/docs/elements/channel/email.mdx +1 -1
  18. package/site/content/docs/elements/channel/index.mdx +1 -1
  19. package/site/content/docs/elements/flow/http.mdx +4 -4
  20. package/site/content/docs/elements/flow/index.mdx +3 -3
  21. package/site/content/docs/elements/flow/meta.json +1 -1
  22. package/site/content/docs/elements/gate/auth.mdx +4 -1
  23. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  24. package/site/content/docs/elements/store/index.mdx +1 -1
  25. package/site/content/docs/elements/store/kv.mdx +5 -5
  26. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  27. package/site/content/docs/plugins/otp.mdx +31 -31
  28. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  29. package/site/content/docs/recipes/index.mdx +6 -6
  30. package/site/content/docs/recipes/mailpit.mdx +1 -1
  31. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  32. package/site/content/docs/recipes/pgdog.mdx +1 -1
  33. package/site/content/docs/recipes/postgres.mdx +12 -8
  34. package/site/content/docs/recipes/redis.mdx +4 -4
  35. package/site/content/docs/reference/cli.mdx +2 -1
  36. package/site/content/docs/reference/configuration.mdx +11 -9
  37. package/site/content/docs/reference/errors.mdx +12 -2
  38. package/site/content/docs/reference/fx.mdx +35 -21
  39. package/site/content/docs/reference/idempotency.mdx +192 -0
  40. package/site/content/docs/reference/index.mdx +6 -1
  41. package/site/content/docs/reference/meta.json +2 -1
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  44. package/site/content/docs/understand/the-architecture.mdx +14 -12
  45. package/site/content/docs/understand/try-it.mdx +3 -3
  46. package/src/auth/api-key-sql.test.ts +44 -0
  47. package/src/auth/api-key-sql.ts +1 -0
  48. package/src/auth/api-keys.ts +30 -12
  49. package/src/auth/config.ts +6 -2
  50. package/src/auth/gate-auth.test.ts +20 -0
  51. package/src/cli/agents-md.test.ts +25 -0
  52. package/src/cli/ask-vault-gaps.test.ts +38 -1
  53. package/src/cli/ask-vault-gaps.ts +35 -2
  54. package/src/cli/competitor-mention-removal.test.ts +3 -3
  55. package/src/cli/decide.ts +174 -0
  56. package/src/cli/decision-lock-watch.test.ts +45 -0
  57. package/src/cli/decision-lock-watch.ts +51 -0
  58. package/src/cli/dev-app-runner.ts +34 -8
  59. package/src/cli/dev-auth-secret.test.ts +49 -0
  60. package/src/cli/dev-auth-secret.ts +49 -0
  61. package/src/cli/dev-hot.test.ts +41 -0
  62. package/src/cli/dev-hot.ts +48 -0
  63. package/src/cli/dev.test.ts +1 -1
  64. package/src/cli/dev.ts +24 -6
  65. package/src/cli/docker-cli.test.ts +1 -1
  66. package/src/cli/eval.ts +142 -1
  67. package/src/cli/index.ts +14 -1
  68. package/src/cli/load-config.images.test.ts +12 -12
  69. package/src/cli/load-config.ts +2 -2
  70. package/src/cli/mcp-from-console.ts +48 -0
  71. package/src/cli/registry.ts +35 -1
  72. package/src/cli/vault-cmd.test.ts +23 -0
  73. package/src/cli/vault-cmd.ts +7 -0
  74. package/src/client/agent.ts +307 -0
  75. package/src/client/create.ts +18 -1
  76. package/src/client/transport.test.ts +245 -0
  77. package/src/client/transport.ts +133 -19
  78. package/src/client/types.ts +29 -3
  79. package/src/client-react/index.ts +7 -1
  80. package/src/client-react/use-agent-run.test.ts +271 -0
  81. package/src/client-react/use-agent-run.ts +193 -0
  82. package/src/compiler/ai-approval.test.ts +36 -0
  83. package/src/compiler/ai-repair.test.ts +22 -0
  84. package/src/compiler/decisions.extract.test.ts +164 -0
  85. package/src/compiler/effects-infer.ts +753 -17
  86. package/src/compiler/extract.test.ts +37 -0
  87. package/src/compiler/extract.ts +381 -24
  88. package/src/compiler/fixtures/skyport.expected.json +24 -0
  89. package/src/compiler/fx-follow.test.ts +164 -0
  90. package/src/compiler/fx-index.ts +399 -0
  91. package/src/compiler/response.ts +35 -2
  92. package/src/config/index.ts +21 -3
  93. package/src/console/server/ai-runs-flows.ts +269 -0
  94. package/src/console/server/ai-runs.test.ts +323 -0
  95. package/src/console/server/ai-runs.ts +562 -0
  96. package/src/console/server/ai.ts +12 -1
  97. package/src/console/server/app.ts +10 -1
  98. package/src/console/server/decisions-flows.ts +157 -0
  99. package/src/console/server/decisions.test.ts +66 -0
  100. package/src/console/server/decisions.ts +229 -0
  101. package/src/console/server/flows-invoke.test.ts +2 -2
  102. package/src/console/server/flows.ts +50 -2
  103. package/src/console/server/invoke-user-flow.ts +9 -0
  104. package/src/console/server/runs-ingest.ts +16 -0
  105. package/src/console/server/serve.ts +5 -0
  106. package/src/console/server/state.ts +25 -1
  107. package/src/console/server/store-stats.test.ts +47 -0
  108. package/src/console/server/store-stats.ts +92 -18
  109. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  110. package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
  111. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
  112. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
  113. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
  114. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  115. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  116. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  117. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  118. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  119. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  120. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  121. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  122. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  123. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  124. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
  125. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
  126. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  127. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  128. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  129. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
  130. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  131. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  132. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  133. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  134. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  135. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  136. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  137. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  138. package/src/console/ui-next/dist/index.html +4 -3
  139. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  140. package/src/console/ui-next/src/client.ts +224 -1
  141. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  142. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  143. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  144. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  145. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  146. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  147. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  148. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  149. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  150. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  151. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  152. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  153. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  154. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  155. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  156. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  157. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  158. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  159. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  160. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  161. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  162. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  163. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  164. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  165. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  166. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  167. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  168. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  169. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  171. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
  172. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  173. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  174. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  175. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  176. package/src/console/ui-next/src/router.tsx +17 -0
  177. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  178. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  179. package/src/console/ui-next/vite.config.ts +9 -0
  180. package/src/docker/docker.test.ts +13 -13
  181. package/src/docker/images-config.test.ts +12 -12
  182. package/src/docker/recipes/dragonfly.ts +3 -2
  183. package/src/docker/recipes/redis.ts +1 -1
  184. package/src/docker/stack-id.test.ts +1 -1
  185. package/src/drivers/ai-anthropic.ts +212 -21
  186. package/src/drivers/ai-mock.ts +34 -1
  187. package/src/drivers/ai-openai-compatible.ts +67 -26
  188. package/src/drivers/ai-preconnect.ts +18 -0
  189. package/src/drivers/ai-stream.test.ts +62 -0
  190. package/src/drivers/ai-types.ts +20 -0
  191. package/src/drivers/journal-postgres.ts +455 -3
  192. package/src/drivers/meilisearch.ts +2 -0
  193. package/src/drivers/memory-ddl.test.ts +18 -0
  194. package/src/drivers/memory.ts +21 -1
  195. package/src/drivers/pg-rls.test.ts +31 -0
  196. package/src/drivers/pg-rls.ts +15 -0
  197. package/src/drivers/postgres.test.ts +9 -0
  198. package/src/drivers/postgres.ts +49 -11
  199. package/src/drivers/vault-builtin.test.ts +10 -0
  200. package/src/drivers/vault-builtin.ts +13 -1
  201. package/src/drivers/vault-remote-bag.ts +5 -1
  202. package/src/drivers/vault-types.ts +6 -1
  203. package/src/elements/ai/agent-event-slot.ts +27 -0
  204. package/src/elements/ai/approval-http.ts +207 -0
  205. package/src/elements/ai/approval.test.ts +741 -0
  206. package/src/elements/ai/approval.ts +228 -0
  207. package/src/elements/ai/decisions/bind.ts +163 -0
  208. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  209. package/src/elements/ai/decisions/certificate.ts +377 -0
  210. package/src/elements/ai/decisions/certify.ts +314 -0
  211. package/src/elements/ai/decisions/decide.test.ts +953 -0
  212. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  213. package/src/elements/ai/decisions/export.test.ts +120 -0
  214. package/src/elements/ai/decisions/export.ts +146 -0
  215. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  216. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  217. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  218. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  219. package/src/elements/ai/decisions/http.test.ts +154 -0
  220. package/src/elements/ai/decisions/http.ts +240 -0
  221. package/src/elements/ai/decisions/labels.test.ts +282 -0
  222. package/src/elements/ai/decisions/labels.ts +257 -0
  223. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  224. package/src/elements/ai/decisions/openrouter.ts +36 -0
  225. package/src/elements/ai/decisions/provider.ts +102 -0
  226. package/src/elements/ai/decisions/typesafe.ts +36 -0
  227. package/src/elements/ai/declare.ts +252 -1
  228. package/src/elements/ai/events.test.ts +379 -0
  229. package/src/elements/ai/events.ts +138 -0
  230. package/src/elements/ai/run-events.test.ts +461 -0
  231. package/src/elements/ai/run-events.ts +522 -0
  232. package/src/elements/ai/runtime.ts +1186 -105
  233. package/src/elements/ai/stream-turn.ts +214 -0
  234. package/src/elements/ai/stream.live.test.ts +76 -0
  235. package/src/elements/ai/subagent.test.ts +136 -0
  236. package/src/elements/ai.test.ts +497 -3
  237. package/src/elements/ai.ts +16 -1
  238. package/src/elements/clock/durable.ts +27 -1
  239. package/src/elements/store/emit-drizzle.ts +2 -2
  240. package/src/elements/store/live-default.test.ts +2 -0
  241. package/src/elements/store/live-http.test.ts +1 -0
  242. package/src/elements/store/resource-list-docs.test.ts +2 -2
  243. package/src/elements/store/resource.test.ts +3 -3
  244. package/src/elements/store/schema-decl.test.ts +1 -0
  245. package/src/elements/store/sql-condition.ts +24 -7
  246. package/src/elements/store/sql-session.test.ts +51 -0
  247. package/src/elements/store/sql-session.ts +111 -1
  248. package/src/elements/store/upsert-app.test.ts +1 -1
  249. package/src/elements/vault/audit.test.ts +137 -0
  250. package/src/elements/vault/audit.ts +106 -13
  251. package/src/elements/vault/boot-chain.ts +9 -1
  252. package/src/elements/vault/builtin-adapter.ts +33 -3
  253. package/src/i18n/catalogs/ar.ts +9 -0
  254. package/src/i18n/catalogs/en.ts +9 -0
  255. package/src/index.ts +2 -0
  256. package/src/kernel/abort-scope.ts +33 -2
  257. package/src/kernel/agent-event-store.ts +448 -0
  258. package/src/kernel/app.ts +294 -19
  259. package/src/kernel/auto-cache.test.ts +6 -6
  260. package/src/kernel/boot-bind/ai.ts +3 -0
  261. package/src/kernel/boot-bind/store.test.ts +26 -0
  262. package/src/kernel/boot-bind/store.ts +82 -6
  263. package/src/kernel/boot-bind/vault.ts +1 -0
  264. package/src/kernel/boot.ts +34 -5
  265. package/src/kernel/builtin-errors.ts +10 -0
  266. package/src/kernel/capability.ts +3 -0
  267. package/src/kernel/client-descriptor.ts +1 -0
  268. package/src/kernel/concurrency.test.ts +33 -0
  269. package/src/kernel/decision-label-store.ts +441 -0
  270. package/src/kernel/effects-stamping.test.ts +2 -2
  271. package/src/kernel/effects.test.ts +2 -1
  272. package/src/kernel/effects.ts +13 -1
  273. package/src/kernel/element-registries.ts +3 -0
  274. package/src/kernel/errors-compiler.ts +20 -0
  275. package/src/kernel/errors-text.ts +4 -0
  276. package/src/kernel/errors.ts +2 -0
  277. package/src/kernel/external-effects.test.ts +36 -0
  278. package/src/kernel/flow.test.ts +2 -3
  279. package/src/kernel/flow.ts +24 -0
  280. package/src/kernel/fx-decide.ts +761 -0
  281. package/src/kernel/fx-fetch.ts +5 -1
  282. package/src/kernel/fx.test.ts +12 -5
  283. package/src/kernel/fx.ts +316 -29
  284. package/src/kernel/http-frame.test.ts +94 -0
  285. package/src/kernel/http-frame.ts +203 -0
  286. package/src/kernel/idempotency-store.ts +578 -0
  287. package/src/kernel/idempotency.test.ts +556 -0
  288. package/src/kernel/idempotency.ts +304 -0
  289. package/src/kernel/index.ts +1 -0
  290. package/src/kernel/journal.ts +160 -0
  291. package/src/kernel/json-result.ts +10 -0
  292. package/src/kernel/pipeline.test.ts +45 -0
  293. package/src/kernel/sse-id.ts +32 -0
  294. package/src/manifest/diff.ts +1 -0
  295. package/src/manifest/types.ts +37 -2
  296. package/src/mcp/ai-tools.test.ts +202 -0
  297. package/src/mcp/authorization.ts +66 -0
  298. package/src/mcp/session.ts +3 -0
  299. package/src/mcp/tools.ts +168 -0
  300. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  301. package/src/plugins/otp.test.ts +65 -1
  302. package/src/plugins/otp.ts +59 -7
  303. package/src/release/absolute-regression.test.ts +63 -0
  304. package/src/release/http-graph.test.ts +27 -0
  305. package/src/release/http-graph.ts +95 -0
  306. package/src/release/index.ts +3 -0
  307. package/src/release/limits.ts +17 -2
  308. package/src/release/measure.ts +144 -28
  309. package/src/runs/collect.ts +4 -1
  310. package/src/runs/parquet.test.ts +27 -1
  311. package/src/runs/parquet.ts +19 -0
  312. package/src/runs/types.ts +31 -0
  313. package/src/runtime/json-code-block.test.ts +106 -47
  314. package/src/runtime/json-code-block.ts +1209 -46
  315. package/src/term.test.ts +13 -0
  316. package/src/term.ts +81 -49
  317. package/src/test/create-test-app.test.ts +3 -1
  318. package/src/test/create-test-app.ts +40 -0
  319. package/src/test/live-signals.test.ts +1 -0
  320. package/src/test/provisions.integration.test.ts +1 -0
  321. package/src/test/tenant-isolation.test.ts +1 -0
  322. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  323. package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
  324. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  325. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
  326. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
  327. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
  328. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  329. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  330. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
  331. package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
  332. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  333. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  334. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  335. package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
  336. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  337. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  338. package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
  339. package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
  340. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
  341. package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
  342. package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
@@ -10,13 +10,14 @@ import {
10
10
  ArrowLeft01Icon,
11
11
  ArrowReloadHorizontalIcon,
12
12
  ArrowRight01Icon,
13
+ ArrowUp01Icon,
13
14
  LeftToRightListBulletIcon,
14
15
  MinusSignIcon,
15
16
  PlusSignIcon,
16
17
  Timer01Icon,
17
18
  } from "@hugeicons/core-free-icons";
18
19
  import { HugeiconsIcon } from "@hugeicons/react";
19
- import { tracesReplay, type RunRow } from "@/client.ts";
20
+ import { tracesReplay, type RunEffect, type RunRow } from "@/client.ts";
20
21
  import {
21
22
  AnimatePresence,
22
23
  animate,
@@ -29,6 +30,7 @@ import {
29
30
  import {
30
31
  EXPLORER_CHEVRON_CLASS,
31
32
  EXPLORER_COUNT_CLASS,
33
+ EXPLORER_GROUP_ROW_CLASS,
32
34
  EXPLORER_ICON_BUTTON_CLASS,
33
35
  EXPLORER_ICON_CLASS,
34
36
  EXPLORER_RAIL_ACTIVE_CLASS,
@@ -39,6 +41,7 @@ import {
39
41
  EXPLORER_STRIP_TOKEN_CLASS,
40
42
  SECTION_HEAD_CLASS,
41
43
  } from "@/components/explorer/explorer-chrome.ts";
44
+ import { CopyInlineButton } from "@/components/explorer/copy-inline-button.tsx";
42
45
  import { Badge } from "@/components/ui/badge";
43
46
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
44
47
  import {
@@ -56,6 +59,7 @@ import { useManifest } from "../data/use-manifest.ts";
56
59
  import { NODE_ACCENT } from "../graph/flow-graph-theme.ts";
57
60
  import { effectBarColor, effectKindIcon, type RunEffectKind } from "./effect-kind.ts";
58
61
  import {
62
+ effectAiHref,
59
63
  effectEventLabel,
60
64
  effectSummaryChips,
61
65
  type EffectSummaryChip,
@@ -67,9 +71,11 @@ import { durationClassName } from "./duration-tone.ts";
67
71
  import { traceRequestMeta } from "./request-meta.ts";
68
72
  import { TraceRequestSection } from "./trace-request-section.tsx";
69
73
  import { executeTraceReplay } from "./trace-actions.ts";
74
+ import { traceIdentityLines, traceNav } from "./trace-nav.ts";
70
75
  import { traceGateInfos } from "./trace-gates.ts";
71
76
  import { triggerIconSpec } from "./trigger-icon.ts";
72
77
  import { playbackDurationMs } from "./replay-playback.ts";
78
+ import { traceIsDense, traceLanes, type TraceLane } from "./trace-lanes.ts";
73
79
  import { waterfallBars, type WaterfallBar } from "./waterfall-bars.ts";
74
80
  import {
75
81
  mapToViewport,
@@ -99,6 +105,13 @@ export type TraceDetailSheetProps = {
99
105
  readonly playbackKey?: number;
100
106
  /** Called when Replay succeeds so the page can pulse the graph chain. */
101
107
  readonly onReplayStart?: () => void;
108
+ /**
109
+ * Ordered traces the header can step through. Previous is the row above
110
+ * the open trace; next is the row below.
111
+ */
112
+ readonly navigationRuns?: readonly RunRow[];
113
+ /** Open another trace from {@link navigationRuns}. */
114
+ readonly onSelectRun?: (id: string) => void;
102
115
  };
103
116
 
104
117
  const sectionClassName = "border-b border-border/60 last:border-b-0";
@@ -118,6 +131,8 @@ export function TraceDetailSheet({
118
131
  onFocusEffectChange,
119
132
  playbackKey = 0,
120
133
  onReplayStart,
134
+ navigationRuns,
135
+ onSelectRun,
121
136
  }: TraceDetailSheetProps) {
122
137
  const manifest = useManifest();
123
138
  const reduceMotion = useReducedMotion();
@@ -132,7 +147,14 @@ export function TraceDetailSheet({
132
147
  () => (run ? waterfallBars(run.effects, run.startedAt, run.durationMs) : []),
133
148
  [run],
134
149
  );
150
+ const dense = traceIsDense(bars.length);
151
+ const lanes = useMemo(() => (dense ? traceLanes(bars) : []), [dense, bars]);
135
152
  const gaps = useMemo(() => (run ? waterfallGaps(bars, run.durationMs) : []), [run, bars]);
153
+ const identity = useMemo(() => (run ? traceIdentityLines(run) : []), [run]);
154
+ const nav = useMemo(
155
+ () => (run && navigationRuns ? traceNav(navigationRuns, run.id) : null),
156
+ [run, navigationRuns],
157
+ );
136
158
  const requestMeta = useMemo(
137
159
  () =>
138
160
  run
@@ -151,6 +173,7 @@ export function TraceDetailSheet({
151
173
  const [zoom, setZoom] = useState(1);
152
174
  const [viewStart, setViewStart] = useState(0);
153
175
  const [playing, setPlaying] = useState(false);
176
+ const [openLanes, setOpenLanes] = useState<ReadonlySet<string>>(() => new Set());
154
177
 
155
178
  const progress = useMotionValue(0);
156
179
  // Hero duration counter — counts up on open, tracks the playhead on Replay.
@@ -176,6 +199,7 @@ export function TraceDetailSheet({
176
199
  setZoom(1);
177
200
  setViewStart(0);
178
201
  setPlaying(false);
202
+ setOpenLanes(new Set());
179
203
  progress.set(0);
180
204
  if (!run) return;
181
205
  if (reduceMotion) {
@@ -220,6 +244,24 @@ export function TraceDetailSheet({
220
244
  // eslint-disable-next-line react-hooks/exhaustive-deps
221
245
  }, [playbackKey]);
222
246
 
247
+ useEffect(() => {
248
+ if (focusEffectIndex == null) return;
249
+ const lane = lanes.find((item) => item.bars.some((bar) => bar.index === focusEffectIndex));
250
+ if (!lane || lane.bars.length < 2) return;
251
+ setOpenLanes((prev) => {
252
+ if (prev.has(lane.key)) return prev;
253
+ const next = new Set(prev);
254
+ next.add(lane.key);
255
+ return next;
256
+ });
257
+ }, [focusEffectIndex, lanes]);
258
+
259
+ const stepTo = (id: string | null) => {
260
+ if (!id || !onSelectRun) return;
261
+ onFocusEffectChange?.(null);
262
+ onSelectRun(id);
263
+ };
264
+
223
265
  const onReplay = async (e: MouseEvent) => {
224
266
  e.stopPropagation();
225
267
  if (!run || busy) return;
@@ -263,7 +305,12 @@ export function TraceDetailSheet({
263
305
  >
264
306
  {run && trigger ? (
265
307
  <>
266
- <SheetHeader className="gap-1.5 border-b border-border/60 px-2 py-2 pr-12">
308
+ <SheetHeader
309
+ className={cn(
310
+ "gap-1.5 border-b border-border/60 px-2 py-2",
311
+ nav && nav.index >= 0 && onSelectRun ? "pr-28" : "pr-12",
312
+ )}
313
+ >
267
314
  <div className="flex items-start gap-2">
268
315
  <span className="mt-0.5 text-muted-foreground" title={trigger.label} aria-hidden>
269
316
  <HugeiconsIcon icon={trigger.icon} className={EXPLORER_ICON_CLASS} />
@@ -289,7 +336,79 @@ export function TraceDetailSheet({
289
336
  </>
290
337
  ) : null}
291
338
  </SheetDescription>
339
+ <div className="mt-1 flex min-w-0 flex-col gap-0.5" data-slot="trace-sheet-ids">
340
+ {identity.map((line) => (
341
+ <div
342
+ key={line.key}
343
+ className="flex min-w-0 items-center gap-1.5"
344
+ data-slot="trace-sheet-id"
345
+ data-id={line.key}
346
+ >
347
+ <span className="w-12 shrink-0 text-[10px] font-semibold tracking-[0.08em] text-muted-foreground uppercase">
348
+ {line.label}
349
+ </span>
350
+ <code
351
+ className="min-w-0 truncate font-mono text-[11px] leading-none text-foreground/80"
352
+ title={line.value}
353
+ >
354
+ {line.value}
355
+ </code>
356
+ <CopyInlineButton value={line.value} label={`Copy ${line.label} ID`} />
357
+ </div>
358
+ ))}
359
+ </div>
292
360
  </div>
361
+ {nav && nav.index >= 0 && onSelectRun ? (
362
+ <div
363
+ className="absolute top-3 right-12 flex items-center"
364
+ data-slot="trace-sheet-nav"
365
+ role="group"
366
+ aria-label="Trace navigation"
367
+ >
368
+ <Tooltip>
369
+ <TooltipTrigger
370
+ render={(props) => (
371
+ <button
372
+ {...props}
373
+ type="button"
374
+ className="inline-flex size-7 items-center justify-center text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-40"
375
+ aria-label="Previous trace"
376
+ data-slot="trace-sheet-previous"
377
+ disabled={nav.previousId === null}
378
+ onClick={(event) => {
379
+ props.onClick?.(event);
380
+ stepTo(nav.previousId);
381
+ }}
382
+ >
383
+ <HugeiconsIcon icon={ArrowUp01Icon} className="size-5" />
384
+ </button>
385
+ )}
386
+ />
387
+ <TooltipContent side="bottom">Previous trace</TooltipContent>
388
+ </Tooltip>
389
+ <Tooltip>
390
+ <TooltipTrigger
391
+ render={(props) => (
392
+ <button
393
+ {...props}
394
+ type="button"
395
+ className="inline-flex size-7 items-center justify-center text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-40"
396
+ aria-label="Next trace"
397
+ data-slot="trace-sheet-next"
398
+ disabled={nav.nextId === null}
399
+ onClick={(event) => {
400
+ props.onClick?.(event);
401
+ stepTo(nav.nextId);
402
+ }}
403
+ >
404
+ <HugeiconsIcon icon={ArrowDown01Icon} className="size-5" />
405
+ </button>
406
+ )}
407
+ />
408
+ <TooltipContent side="bottom">Next trace</TooltipContent>
409
+ </Tooltip>
410
+ </div>
411
+ ) : null}
293
412
  </div>
294
413
  <AnimatePresence mode="wait">
295
414
  {hint ? (
@@ -335,7 +454,14 @@ export function TraceDetailSheet({
335
454
 
336
455
  <section className={sectionClassName} data-slot="trace-waterfall">
337
456
  <div className={EXPLORER_STRIP_CLASS}>
338
- <h3 className={cn(SECTION_HEAD_CLASS, "flex items-center px-2")}>Waterfall</h3>
457
+ <h3 className={cn(SECTION_HEAD_CLASS, "flex items-center gap-2 px-2")}>
458
+ Waterfall
459
+ {dense ? (
460
+ <span className={EXPLORER_COUNT_CLASS}>
461
+ {lanes.length} {lanes.length === 1 ? "lane" : "lanes"}
462
+ </span>
463
+ ) : null}
464
+ </h3>
339
465
  <div
340
466
  className="ml-auto flex h-full items-stretch"
341
467
  data-slot="trace-waterfall-zoom"
@@ -414,21 +540,44 @@ export function TraceDetailSheet({
414
540
  }
415
541
  focusIndex={focusEffectIndex}
416
542
  />
417
- {bars.map((bar) => (
418
- <WaterfallBarRow
419
- key={bar.index}
420
- bar={bar}
421
- view={view}
422
- dimmed={hoverIndex !== null && hoverIndex !== bar.index}
423
- focused={focusEffectIndex === bar.index}
424
- progress={progress}
425
- playing={playing}
426
- onHover={setHoverIndex}
427
- onSelect={(index) =>
428
- onFocusEffectChange?.(focusEffectIndex === index ? null : index)
429
- }
430
- />
431
- ))}
543
+ {dense ? (
544
+ <div
545
+ className="flex max-h-72 flex-col overflow-y-auto"
546
+ data-slot="trace-waterfall-lanes"
547
+ >
548
+ {lanes.map((lane) => (
549
+ <WaterfallLaneRow
550
+ key={lane.key}
551
+ lane={lane}
552
+ view={view}
553
+ hoverIndex={hoverIndex}
554
+ focusIndex={focusEffectIndex}
555
+ progress={progress}
556
+ playing={playing}
557
+ onHover={setHoverIndex}
558
+ onSelect={(index) =>
559
+ onFocusEffectChange?.(focusEffectIndex === index ? null : index)
560
+ }
561
+ />
562
+ ))}
563
+ </div>
564
+ ) : (
565
+ bars.map((bar) => (
566
+ <WaterfallBarRow
567
+ key={bar.index}
568
+ bar={bar}
569
+ view={view}
570
+ dimmed={hoverIndex !== null && hoverIndex !== bar.index}
571
+ focused={focusEffectIndex === bar.index}
572
+ progress={progress}
573
+ playing={playing}
574
+ onHover={setHoverIndex}
575
+ onSelect={(index) =>
576
+ onFocusEffectChange?.(focusEffectIndex === index ? null : index)
577
+ }
578
+ />
579
+ ))
580
+ )}
432
581
  {playing ? (
433
582
  <motion.div
434
583
  className="pointer-events-none absolute top-4 bottom-0 w-px bg-foreground/70"
@@ -475,69 +624,47 @@ export function TraceDetailSheet({
475
624
  </div>
476
625
  <CollapsibleContent>
477
626
  <ul className="flex flex-col" data-slot="trace-events">
478
- {run.effects.map((effect, index) => {
479
- const bar = bars[index];
480
- const focused = focusEffectIndex === index;
481
- return (
482
- <li
483
- key={`${effect.kind}:${effect.resource}:${index}`}
484
- data-slot="trace-event-row"
485
- data-index={index}
486
- data-focused={focused ? "true" : "false"}
487
- data-external={effect.external ? "true" : undefined}
488
- className={cn(
489
- EXPLORER_ROW_CLASS,
490
- "cursor-pointer flex-col items-stretch gap-1 text-[11px]",
491
- focused && EXPLORER_ROW_SELECTED_CLASS,
492
- hoverIndex === index && !focused && "bg-muted/50",
493
- )}
494
- onClick={() =>
495
- onFocusEffectChange?.(focusEffectIndex === index ? null : index)
496
- }
497
- onMouseEnter={() => setHoverIndex(index)}
498
- onMouseLeave={() => setHoverIndex(null)}
499
- >
500
- <span
501
- aria-hidden
502
- className={cn(
503
- EXPLORER_RAIL_CLASS,
504
- focused && EXPLORER_RAIL_ACTIVE_CLASS,
505
- )}
627
+ {dense
628
+ ? lanes.map((lane) => (
629
+ <EventLane
630
+ key={lane.key}
631
+ lane={lane}
632
+ effects={run.effects}
633
+ open={openLanes.has(lane.key)}
634
+ onToggle={() =>
635
+ setOpenLanes((prev) => {
636
+ const next = new Set(prev);
637
+ if (next.has(lane.key)) next.delete(lane.key);
638
+ else next.add(lane.key);
639
+ return next;
640
+ })
641
+ }
642
+ focusIndex={focusEffectIndex}
643
+ hoverIndex={hoverIndex}
644
+ progress={progress}
645
+ playing={playing}
646
+ onHover={setHoverIndex}
647
+ onSelect={(index) =>
648
+ onFocusEffectChange?.(focusEffectIndex === index ? null : index)
649
+ }
650
+ />
651
+ ))
652
+ : run.effects.map((effect, index) => (
653
+ <EventRow
654
+ key={`${effect.kind}:${effect.resource}:${index}`}
655
+ effect={effect}
656
+ index={index}
657
+ bar={bars[index]}
658
+ focused={focusEffectIndex === index}
659
+ hovered={hoverIndex === index}
660
+ progress={progress}
661
+ playing={playing}
662
+ onHover={setHoverIndex}
663
+ onSelect={(next) =>
664
+ onFocusEffectChange?.(focusEffectIndex === next ? null : next)
665
+ }
506
666
  />
507
- <div className="flex items-center gap-2">
508
- <EffectKindGlyph kind={effect.kind} resource={effect.resource} />
509
- <span className="w-[4.5rem] shrink-0 font-medium text-foreground/90">
510
- {effectEventLabel(effect)}
511
- </span>
512
- <span className="min-w-0 flex-1 truncate font-mono text-muted-foreground">
513
- {effect.resource}
514
- {effect.external
515
- ? ` · ↗ ${effect.external.host}${effect.external.kind ? ` (${effect.external.kind})` : ""}`
516
- : ""}
517
- </span>
518
- <span className="shrink-0 tabular-nums text-muted-foreground">
519
- {formatDuration(effect.duration)}
520
- </span>
521
- </div>
522
- {bar ? (
523
- <div
524
- className="relative ml-7 h-1.5 w-[calc(100%-1.75rem)] rounded-full bg-muted/70"
525
- data-slot="trace-event-bar-track"
526
- aria-hidden
527
- >
528
- <PlaybackBarFill
529
- bar={bar}
530
- left={bar.offsetRatio}
531
- width={bar.widthRatio}
532
- progress={progress}
533
- playing={playing}
534
- slot="trace-event-bar"
535
- />
536
- </div>
537
- ) : null}
538
- </li>
539
- );
540
- })}
667
+ ))}
541
668
  </ul>
542
669
  </CollapsibleContent>
543
670
  </Collapsible>
@@ -549,6 +676,7 @@ export function TraceDetailSheet({
549
676
  headline={requestMeta.headline}
550
677
  input={run.input}
551
678
  output={run.output}
679
+ http={run.http ?? null}
552
680
  error={run.error}
553
681
  errorMessage={run.errorMessage}
554
682
  inputOpen={inputOpen}
@@ -569,6 +697,200 @@ export function TraceDetailSheet({
569
697
  *
570
698
  * @param props - Effect kind
571
699
  */
700
+ function effectExternalSuffix(external: WaterfallBar["external"]): string {
701
+ if (!external) return "";
702
+ return ` · ↗ ${external.host}${external.kind ? ` (${external.kind})` : ""}`;
703
+ }
704
+
705
+ /**
706
+ * One effect row in Event Details — shared by flat runs and expanded lanes.
707
+ */
708
+ function EffectResource({
709
+ effect,
710
+ }: {
711
+ readonly effect: Pick<RunEffect, "kind" | "resource">;
712
+ }): JSX.Element {
713
+ const manifest = useManifest();
714
+ const agents = new Set(Object.keys(manifest.data?.ai?.agents ?? {}));
715
+ const href = effectAiHref(effect, agents);
716
+ if (!href) return <>{effect.resource}</>;
717
+ return (
718
+ <a href={href} data-slot="trace-effect-link" onClick={(event) => event.stopPropagation()}>
719
+ {effect.resource}
720
+ </a>
721
+ );
722
+ }
723
+
724
+ function EventRow({
725
+ effect,
726
+ index,
727
+ bar,
728
+ focused,
729
+ hovered,
730
+ progress,
731
+ playing,
732
+ onHover,
733
+ onSelect,
734
+ }: {
735
+ readonly effect: RunEffect;
736
+ readonly index: number;
737
+ readonly bar: WaterfallBar | undefined;
738
+ readonly focused: boolean;
739
+ readonly hovered: boolean;
740
+ readonly progress: MotionValue<number>;
741
+ readonly playing: boolean;
742
+ readonly onHover: (index: number | null) => void;
743
+ readonly onSelect: (index: number) => void;
744
+ }): JSX.Element {
745
+ return (
746
+ <li
747
+ data-slot="trace-event-row"
748
+ data-index={index}
749
+ data-focused={focused ? "true" : "false"}
750
+ data-external={effect.external ? "true" : undefined}
751
+ className={cn(
752
+ EXPLORER_ROW_CLASS,
753
+ "cursor-pointer flex-col items-stretch gap-1 text-[11px]",
754
+ focused && EXPLORER_ROW_SELECTED_CLASS,
755
+ hovered && !focused && "bg-muted/50",
756
+ )}
757
+ onClick={() => onSelect(index)}
758
+ onMouseEnter={() => onHover(index)}
759
+ onMouseLeave={() => onHover(null)}
760
+ >
761
+ <span
762
+ aria-hidden
763
+ className={cn(EXPLORER_RAIL_CLASS, focused && EXPLORER_RAIL_ACTIVE_CLASS)}
764
+ />
765
+ <div className="flex items-center gap-2">
766
+ <EffectKindGlyph kind={effect.kind} resource={effect.resource} />
767
+ <span className="w-[4.5rem] shrink-0 font-medium text-foreground/90">
768
+ {effectEventLabel(effect)}
769
+ </span>
770
+ <span className="min-w-0 flex-1 truncate font-mono text-muted-foreground">
771
+ <EffectResource effect={effect} />
772
+ {effectExternalSuffix(effect.external)}
773
+ </span>
774
+ <span className="shrink-0 tabular-nums text-muted-foreground">
775
+ {formatDuration(effect.duration)}
776
+ </span>
777
+ </div>
778
+ {bar ? (
779
+ <div
780
+ className="relative ml-7 h-1.5 w-[calc(100%-1.75rem)] rounded-full bg-muted/70"
781
+ data-slot="trace-event-bar-track"
782
+ aria-hidden
783
+ >
784
+ <PlaybackBarFill
785
+ bar={bar}
786
+ left={bar.offsetRatio}
787
+ width={bar.widthRatio}
788
+ progress={progress}
789
+ playing={playing}
790
+ slot="trace-event-bar"
791
+ />
792
+ </div>
793
+ ) : null}
794
+ </li>
795
+ );
796
+ }
797
+
798
+ /**
799
+ * Dense event list — one row per repeated operation, expanded to the ledger.
800
+ */
801
+ function EventLane({
802
+ lane,
803
+ effects,
804
+ open,
805
+ onToggle,
806
+ focusIndex,
807
+ hoverIndex,
808
+ progress,
809
+ playing,
810
+ onHover,
811
+ onSelect,
812
+ }: {
813
+ readonly lane: TraceLane;
814
+ readonly effects: readonly RunEffect[];
815
+ readonly open: boolean;
816
+ readonly onToggle: () => void;
817
+ readonly focusIndex: number | null;
818
+ readonly hoverIndex: number | null;
819
+ readonly progress: MotionValue<number>;
820
+ readonly playing: boolean;
821
+ readonly onHover: (index: number | null) => void;
822
+ readonly onSelect: (index: number) => void;
823
+ }): JSX.Element | null {
824
+ const only = lane.bars.length === 1 ? lane.bars[0] : undefined;
825
+ if (only) {
826
+ const effect = effects[only.index];
827
+ if (!effect) return null;
828
+ return (
829
+ <EventRow
830
+ effect={effect}
831
+ index={only.index}
832
+ bar={only}
833
+ focused={focusIndex === only.index}
834
+ hovered={hoverIndex === only.index}
835
+ progress={progress}
836
+ playing={playing}
837
+ onHover={onHover}
838
+ onSelect={onSelect}
839
+ />
840
+ );
841
+ }
842
+
843
+ const focused = focusIndex !== null && lane.bars.some((bar) => bar.index === focusIndex);
844
+ return (
845
+ <li data-slot="trace-event-lane" data-open={open ? "true" : "false"}>
846
+ <button
847
+ type="button"
848
+ className={cn(EXPLORER_GROUP_ROW_CLASS, focused && EXPLORER_ROW_SELECTED_CLASS)}
849
+ onClick={onToggle}
850
+ >
851
+ <HugeiconsIcon
852
+ icon={ArrowDown01Icon}
853
+ className={cn(EXPLORER_CHEVRON_CLASS, !open && "-rotate-90")}
854
+ />
855
+ <EffectKindGlyph kind={lane.kind} resource={lane.resource} />
856
+ <span className="w-[4.5rem] shrink-0 font-medium text-foreground/90">
857
+ {effectEventLabel(lane)}
858
+ </span>
859
+ <span className="min-w-0 flex-1 truncate font-mono text-muted-foreground">
860
+ <EffectResource effect={lane} />
861
+ {effectExternalSuffix(lane.external)}
862
+ </span>
863
+ <span className={EXPLORER_COUNT_CLASS}>×{lane.bars.length}</span>
864
+ <span className="shrink-0 tabular-nums text-muted-foreground">
865
+ {formatDuration(lane.totalDurationMs)}
866
+ </span>
867
+ </button>
868
+ {open ? (
869
+ <ul className="max-h-64 overflow-y-auto" data-slot="trace-event-lane-items">
870
+ {lane.bars.map((bar) => {
871
+ const effect = effects[bar.index];
872
+ if (!effect) return null;
873
+ return (
874
+ <EventRow
875
+ key={bar.index}
876
+ effect={effect}
877
+ index={bar.index}
878
+ bar={bar}
879
+ focused={focusIndex === bar.index}
880
+ hovered={hoverIndex === bar.index}
881
+ progress={progress}
882
+ playing={playing}
883
+ onHover={onHover}
884
+ onSelect={onSelect}
885
+ />
886
+ );
887
+ })}
888
+ </ul>
889
+ ) : null}
890
+ </li>
891
+ );
892
+ }
893
+
572
894
  function EffectKindGlyph({
573
895
  kind,
574
896
  resource,
@@ -895,6 +1217,109 @@ function GapSegment({
895
1217
  );
896
1218
  }
897
1219
 
1220
+ /**
1221
+ * One labeled timeline for every occurrence of the same kind and resource.
1222
+ */
1223
+ function WaterfallLaneRow({
1224
+ lane,
1225
+ view,
1226
+ hoverIndex,
1227
+ focusIndex,
1228
+ progress,
1229
+ playing,
1230
+ onHover,
1231
+ onSelect,
1232
+ }: {
1233
+ readonly lane: TraceLane;
1234
+ readonly view: TimelineView;
1235
+ readonly hoverIndex: number | null;
1236
+ readonly focusIndex: number | null;
1237
+ readonly progress: MotionValue<number>;
1238
+ readonly playing: boolean;
1239
+ readonly onHover: (index: number | null) => void;
1240
+ readonly onSelect?: (index: number) => void;
1241
+ }): JSX.Element {
1242
+ const hot =
1243
+ (hoverIndex !== null && lane.bars.some((bar) => bar.index === hoverIndex)) ||
1244
+ (focusIndex !== null && lane.bars.some((bar) => bar.index === focusIndex));
1245
+ return (
1246
+ <div
1247
+ className={cn(
1248
+ "flex flex-col gap-1 border-b border-border/60 px-2 py-1.5 last:border-b-0",
1249
+ hot && "bg-muted/50",
1250
+ )}
1251
+ data-slot="trace-waterfall-lane"
1252
+ >
1253
+ <div className="flex items-center gap-2 text-[11px]">
1254
+ <EffectKindGlyph kind={lane.kind} resource={lane.resource} />
1255
+ <span className="w-[4.5rem] shrink-0 font-medium text-foreground/90">
1256
+ {effectEventLabel(lane)}
1257
+ </span>
1258
+ <span className="min-w-0 flex-1 truncate font-mono text-muted-foreground">
1259
+ <EffectResource effect={lane} />
1260
+ {effectExternalSuffix(lane.external)}
1261
+ </span>
1262
+ <span className={EXPLORER_COUNT_CLASS}>×{lane.bars.length}</span>
1263
+ <span className="shrink-0 tabular-nums text-muted-foreground">
1264
+ {formatDuration(lane.totalDurationMs)}
1265
+ </span>
1266
+ </div>
1267
+ <div
1268
+ className="relative ml-7 h-2 w-[calc(100%-1.75rem)] rounded-full bg-muted/70"
1269
+ data-slot="trace-waterfall-lane-track"
1270
+ >
1271
+ {lane.bars.map((bar) => {
1272
+ const mapped = mapToViewport(bar.offsetRatio, bar.widthRatio, view);
1273
+ if (!mapped || mapped.width <= 0) return null;
1274
+ const focused = focusIndex === bar.index;
1275
+ return (
1276
+ <Tooltip key={bar.index}>
1277
+ <TooltipTrigger
1278
+ render={(props) => (
1279
+ <div
1280
+ {...props}
1281
+ className="absolute inset-y-0 cursor-pointer"
1282
+ style={{
1283
+ left: `${mapped.left * 100}%`,
1284
+ width: `${Math.max(mapped.width * 100, 0.4)}%`,
1285
+ }}
1286
+ onClick={(event) => {
1287
+ event.stopPropagation();
1288
+ onSelect?.(bar.index);
1289
+ }}
1290
+ onMouseEnter={(event) => {
1291
+ props.onMouseEnter?.(event);
1292
+ onHover(bar.index);
1293
+ }}
1294
+ onMouseLeave={(event) => {
1295
+ props.onMouseLeave?.(event);
1296
+ onHover(null);
1297
+ }}
1298
+ >
1299
+ <PlaybackBarFill
1300
+ bar={bar}
1301
+ left={0}
1302
+ width={1}
1303
+ progress={progress}
1304
+ playing={playing}
1305
+ dimmed={hoverIndex !== null && hoverIndex !== bar.index && !focused}
1306
+ focused={focused}
1307
+ slot="trace-waterfall-bar"
1308
+ />
1309
+ </div>
1310
+ )}
1311
+ />
1312
+ <TooltipContent side="top" className="max-w-xs font-mono text-[11px]">
1313
+ {waterfallBarTooltip(bar)}
1314
+ </TooltipContent>
1315
+ </Tooltip>
1316
+ );
1317
+ })}
1318
+ </div>
1319
+ </div>
1320
+ );
1321
+ }
1322
+
898
1323
  /**
899
1324
  * One waterfall track with a real tooltip (viewport-aware).
900
1325
  *