okengine 0.22.0 → 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 (262) hide show
  1. package/AGENTS.md +7 -0
  2. package/manifest.v1.schema.json +44 -0
  3. package/package.json +32 -19
  4. package/site/content/docs/ai/mcp.mdx +13 -9
  5. package/site/content/docs/ai/skills.mdx +1 -1
  6. package/site/content/docs/elements/ai/agents.mdx +61 -13
  7. package/site/content/docs/elements/ai/decide.mdx +536 -0
  8. package/site/content/docs/elements/ai/events.mdx +373 -0
  9. package/site/content/docs/elements/ai/index.mdx +9 -3
  10. package/site/content/docs/elements/ai/meta.json +1 -1
  11. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  12. package/site/content/docs/elements/gate/auth.mdx +4 -1
  13. package/site/content/docs/elements/store/index.mdx +1 -1
  14. package/site/content/docs/elements/store/kv.mdx +5 -5
  15. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  16. package/site/content/docs/recipes/index.mdx +6 -6
  17. package/site/content/docs/recipes/postgres.mdx +12 -8
  18. package/site/content/docs/recipes/redis.mdx +4 -4
  19. package/site/content/docs/reference/cli.mdx +2 -1
  20. package/site/content/docs/reference/configuration.mdx +9 -7
  21. package/site/content/docs/reference/fx.mdx +7 -6
  22. package/src/auth/api-key-sql.test.ts +44 -0
  23. package/src/auth/api-key-sql.ts +1 -0
  24. package/src/auth/api-keys.ts +30 -12
  25. package/src/auth/config.ts +6 -2
  26. package/src/auth/gate-auth.test.ts +20 -0
  27. package/src/cli/agents-md.test.ts +25 -0
  28. package/src/cli/ask-vault-gaps.test.ts +38 -1
  29. package/src/cli/ask-vault-gaps.ts +35 -2
  30. package/src/cli/decide.ts +174 -0
  31. package/src/cli/decision-lock-watch.test.ts +45 -0
  32. package/src/cli/decision-lock-watch.ts +51 -0
  33. package/src/cli/dev-app-runner.ts +34 -8
  34. package/src/cli/dev-auth-secret.test.ts +49 -0
  35. package/src/cli/dev-auth-secret.ts +49 -0
  36. package/src/cli/dev-hot.test.ts +41 -0
  37. package/src/cli/dev-hot.ts +48 -0
  38. package/src/cli/dev.test.ts +1 -1
  39. package/src/cli/dev.ts +24 -6
  40. package/src/cli/eval.ts +142 -1
  41. package/src/cli/index.ts +5 -0
  42. package/src/cli/load-config.images.test.ts +2 -2
  43. package/src/cli/load-config.ts +1 -1
  44. package/src/cli/mcp-from-console.ts +48 -0
  45. package/src/cli/registry.ts +35 -1
  46. package/src/client/agent.ts +307 -0
  47. package/src/client/transport.test.ts +20 -0
  48. package/src/client/transport.ts +2 -1
  49. package/src/client/types.ts +1 -1
  50. package/src/client-react/index.ts +7 -1
  51. package/src/client-react/use-agent-run.test.ts +271 -0
  52. package/src/client-react/use-agent-run.ts +193 -0
  53. package/src/compiler/ai-approval.test.ts +36 -0
  54. package/src/compiler/ai-repair.test.ts +22 -0
  55. package/src/compiler/decisions.extract.test.ts +164 -0
  56. package/src/compiler/effects-infer.ts +27 -1
  57. package/src/compiler/extract.ts +268 -12
  58. package/src/compiler/response.ts +35 -2
  59. package/src/config/index.ts +21 -3
  60. package/src/console/server/ai-runs-flows.ts +269 -0
  61. package/src/console/server/ai-runs.test.ts +323 -0
  62. package/src/console/server/ai-runs.ts +562 -0
  63. package/src/console/server/ai.ts +12 -1
  64. package/src/console/server/app.ts +10 -1
  65. package/src/console/server/decisions-flows.ts +157 -0
  66. package/src/console/server/decisions.test.ts +66 -0
  67. package/src/console/server/decisions.ts +229 -0
  68. package/src/console/server/flows.ts +50 -2
  69. package/src/console/server/invoke-user-flow.ts +9 -0
  70. package/src/console/server/runs-ingest.ts +16 -0
  71. package/src/console/server/serve.ts +5 -0
  72. package/src/console/server/state.ts +25 -1
  73. package/src/console/server/store-stats.test.ts +47 -0
  74. package/src/console/server/store-stats.ts +92 -18
  75. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  76. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
  77. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
  78. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
  79. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.js} +1 -1
  80. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  81. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  82. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  83. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  84. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  85. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  86. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  87. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  88. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  89. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  90. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
  91. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
  92. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  93. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  94. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  95. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
  96. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  97. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  98. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  99. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  100. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  101. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  102. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  103. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  104. package/src/console/ui-next/dist/index.html +4 -3
  105. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  106. package/src/console/ui-next/src/client.ts +224 -1
  107. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  108. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  109. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  110. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  111. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  112. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  113. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  114. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  115. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  116. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  117. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  118. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  119. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  120. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  121. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  122. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  123. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  124. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  125. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  126. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  127. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  128. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  129. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  130. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  131. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  132. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  133. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  134. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  135. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  137. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  138. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  139. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  140. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  141. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  142. package/src/console/ui-next/src/router.tsx +17 -0
  143. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  144. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  145. package/src/console/ui-next/vite.config.ts +9 -0
  146. package/src/docker/recipes/dragonfly.ts +3 -2
  147. package/src/docker/recipes/redis.ts +1 -1
  148. package/src/drivers/ai-anthropic.ts +212 -21
  149. package/src/drivers/ai-mock.ts +34 -1
  150. package/src/drivers/ai-openai-compatible.ts +67 -26
  151. package/src/drivers/ai-preconnect.ts +18 -0
  152. package/src/drivers/ai-stream.test.ts +62 -0
  153. package/src/drivers/ai-types.ts +20 -0
  154. package/src/drivers/journal-postgres.ts +289 -2
  155. package/src/drivers/memory-ddl.test.ts +18 -0
  156. package/src/drivers/memory.ts +21 -1
  157. package/src/drivers/pg-rls.test.ts +31 -0
  158. package/src/drivers/pg-rls.ts +15 -0
  159. package/src/drivers/postgres.test.ts +9 -0
  160. package/src/drivers/postgres.ts +49 -11
  161. package/src/elements/ai/agent-event-slot.ts +27 -0
  162. package/src/elements/ai/approval-http.ts +207 -0
  163. package/src/elements/ai/approval.test.ts +741 -0
  164. package/src/elements/ai/approval.ts +228 -0
  165. package/src/elements/ai/decisions/bind.ts +163 -0
  166. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  167. package/src/elements/ai/decisions/certificate.ts +377 -0
  168. package/src/elements/ai/decisions/certify.ts +314 -0
  169. package/src/elements/ai/decisions/decide.test.ts +953 -0
  170. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  171. package/src/elements/ai/decisions/export.test.ts +120 -0
  172. package/src/elements/ai/decisions/export.ts +146 -0
  173. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  174. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  175. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  176. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  177. package/src/elements/ai/decisions/http.test.ts +154 -0
  178. package/src/elements/ai/decisions/http.ts +240 -0
  179. package/src/elements/ai/decisions/labels.test.ts +282 -0
  180. package/src/elements/ai/decisions/labels.ts +257 -0
  181. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  182. package/src/elements/ai/decisions/openrouter.ts +36 -0
  183. package/src/elements/ai/decisions/provider.ts +102 -0
  184. package/src/elements/ai/decisions/typesafe.ts +36 -0
  185. package/src/elements/ai/declare.ts +252 -1
  186. package/src/elements/ai/events.test.ts +379 -0
  187. package/src/elements/ai/events.ts +138 -0
  188. package/src/elements/ai/run-events.test.ts +461 -0
  189. package/src/elements/ai/run-events.ts +522 -0
  190. package/src/elements/ai/runtime.ts +1186 -105
  191. package/src/elements/ai/stream-turn.ts +214 -0
  192. package/src/elements/ai/stream.live.test.ts +76 -0
  193. package/src/elements/ai/subagent.test.ts +136 -0
  194. package/src/elements/ai.test.ts +497 -3
  195. package/src/elements/ai.ts +16 -1
  196. package/src/elements/clock/durable.ts +27 -1
  197. package/src/elements/store/sql-session.test.ts +51 -0
  198. package/src/elements/store/sql-session.ts +111 -1
  199. package/src/i18n/catalogs/ar.ts +5 -0
  200. package/src/i18n/catalogs/en.ts +5 -0
  201. package/src/index.ts +2 -0
  202. package/src/kernel/agent-event-store.ts +448 -0
  203. package/src/kernel/app.ts +103 -1
  204. package/src/kernel/boot-bind/ai.ts +3 -0
  205. package/src/kernel/boot-bind/store.test.ts +26 -0
  206. package/src/kernel/boot-bind/store.ts +82 -6
  207. package/src/kernel/boot.ts +15 -4
  208. package/src/kernel/builtin-errors.ts +2 -0
  209. package/src/kernel/capability.ts +3 -0
  210. package/src/kernel/decision-label-store.ts +441 -0
  211. package/src/kernel/effects.test.ts +2 -1
  212. package/src/kernel/effects.ts +13 -1
  213. package/src/kernel/element-registries.ts +3 -0
  214. package/src/kernel/errors-text.ts +4 -0
  215. package/src/kernel/errors.ts +2 -0
  216. package/src/kernel/flow.test.ts +2 -3
  217. package/src/kernel/fx-decide.ts +761 -0
  218. package/src/kernel/fx.test.ts +12 -5
  219. package/src/kernel/fx.ts +312 -29
  220. package/src/kernel/http-frame.test.ts +94 -0
  221. package/src/kernel/http-frame.ts +203 -0
  222. package/src/kernel/journal.ts +104 -0
  223. package/src/kernel/json-result.ts +10 -0
  224. package/src/kernel/pipeline.test.ts +45 -0
  225. package/src/kernel/sse-id.ts +32 -0
  226. package/src/manifest/diff.ts +1 -0
  227. package/src/manifest/types.ts +29 -2
  228. package/src/mcp/ai-tools.test.ts +202 -0
  229. package/src/mcp/authorization.ts +66 -0
  230. package/src/mcp/session.ts +3 -0
  231. package/src/mcp/tools.ts +168 -0
  232. package/src/release/http-graph.test.ts +2 -0
  233. package/src/release/http-graph.ts +5 -0
  234. package/src/release/measure.ts +39 -14
  235. package/src/runs/collect.ts +4 -1
  236. package/src/runs/parquet.test.ts +27 -1
  237. package/src/runs/parquet.ts +19 -0
  238. package/src/runs/types.ts +31 -0
  239. package/src/runtime/json-code-block.test.ts +106 -47
  240. package/src/runtime/json-code-block.ts +1209 -46
  241. package/src/term.test.ts +13 -0
  242. package/src/term.ts +81 -49
  243. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  244. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  245. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  246. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  247. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  248. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  249. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  250. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  251. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  252. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  253. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  254. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  255. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  256. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  257. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  258. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  259. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  260. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  261. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  262. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -28,12 +28,16 @@ import { HighlightedJson } from "@/components/highlighted-json";
28
28
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
29
29
  import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
30
30
  import { cn } from "@/lib/utils";
31
+ import type { RunHttpFrame } from "@/client.ts";
31
32
  import { httpMethodBadgeClass, httpMethodRailClass } from "./http-method.ts";
33
+ import { RequestClientCard } from "./request-client-card.tsx";
34
+ import { httpStatusRailClass, httpStatusReason, httpStatusTextClass } from "./http-status.ts";
32
35
  import {
33
36
  fieldCopyText,
34
37
  inputByteLabel,
35
38
  inputFieldRows,
36
39
  inputShapeHint,
40
+ payloadHasContent,
37
41
  type InputFieldKind,
38
42
  type InputFieldRow,
39
43
  } from "./request-input-view.ts";
@@ -50,6 +54,11 @@ export type TraceRequestSectionProps = {
50
54
  readonly input: unknown;
51
55
  /** Projected run output snapshot. */
52
56
  readonly output: unknown;
57
+ /**
58
+ * HTTP wire frame. `null` for non-HTTP runs and rows recorded before the
59
+ * frame existed — those still show the body only.
60
+ */
61
+ readonly http: RunHttpFrame | null;
53
62
  /** Declared / framework error code when the run failed. */
54
63
  readonly error: string | null;
55
64
  /** Optional human message paired with {@link error}. */
@@ -81,6 +90,7 @@ export function TraceRequestSection({
81
90
  headline,
82
91
  input,
83
92
  output,
93
+ http,
84
94
  error,
85
95
  errorMessage,
86
96
  inputOpen,
@@ -88,10 +98,19 @@ export function TraceRequestSection({
88
98
  outputOpen,
89
99
  onOutputOpenChange,
90
100
  }: TraceRequestSectionProps): JSX.Element {
91
- const endpoint = method && path ? `${method} ${path}` : headline;
101
+ const shownMethod = http?.request.method ?? method;
102
+ const shownPath = http?.request.path ?? path;
103
+ const endpoint = shownMethod && shownPath ? `${shownMethod} ${shownPath}` : headline;
104
+ const query = http?.request.query ?? {};
105
+ const requestHeaders = http?.request.headers ?? {};
106
+ const hasQuery = Object.keys(query).length > 0;
107
+ const hasRequestHeaders = Object.keys(requestHeaders).length > 0;
108
+ const responseStatus = http?.response?.status;
109
+ const responseHeaders = http?.response?.headers ?? {};
110
+ const hasResponseHeaders = Object.keys(responseHeaders).length > 0;
92
111
  const failed = error !== null;
93
- const hasInput = input !== null && input !== undefined;
94
- const hasOutput = output !== null && output !== undefined;
112
+ const hasInput = payloadHasContent(input);
113
+ const hasOutput = payloadHasContent(output);
95
114
  const responseFills = outputOpen && !failed && hasOutput;
96
115
  const requestFills = inputOpen && hasInput && !responseFills;
97
116
 
@@ -119,22 +138,55 @@ export function TraceRequestSection({
119
138
 
120
139
  <div data-slot="trace-request-frame" className={requestFills ? FILL_COL_CLASS : undefined}>
121
140
  <div className={cn("flex min-w-0", requestFills && "min-h-0 flex-1")}>
122
- <MethodRail method={method} />
141
+ <MethodRail method={shownMethod} />
123
142
  <div className={cn("flex min-w-0 flex-1 flex-col", requestFills && "min-h-0")}>
124
- <RequestEndpoint method={method} path={path} headline={headline} />
125
- <PayloadPanel
126
- value={input}
127
- open={inputOpen}
128
- onOpenChange={onInputOpenChange}
129
- label="Body"
130
- empty="No stored input — replay uses an empty body when the ledger has none."
131
- copyLabel="Copy body JSON"
132
- copySlot="trace-request-copy-input"
133
- toggleSlot="trace-input-toggle"
134
- fieldsSlot="trace-request-fields"
135
- jsonSlot="trace-input-json"
136
- fill={requestFills}
137
- />
143
+ <RequestEndpoint method={shownMethod} path={shownPath} headline={headline} />
144
+ {hasRequestHeaders ? <RequestClientCard headers={requestHeaders} /> : null}
145
+ {hasQuery ? (
146
+ <PayloadPanel
147
+ value={query}
148
+ open
149
+ onOpenChange={() => undefined}
150
+ label="Query"
151
+ empty=""
152
+ copyLabel="Copy query JSON"
153
+ copySlot="trace-request-copy-query"
154
+ toggleSlot="trace-request-query-toggle"
155
+ fieldsSlot="trace-request-query"
156
+ jsonSlot="trace-request-query-json"
157
+ localOpen
158
+ />
159
+ ) : null}
160
+ {hasRequestHeaders ? (
161
+ <PayloadPanel
162
+ value={requestHeaders}
163
+ open
164
+ onOpenChange={() => undefined}
165
+ label="Headers"
166
+ empty=""
167
+ copyLabel="Copy request headers"
168
+ copySlot="trace-request-copy-headers"
169
+ toggleSlot="trace-request-headers-toggle"
170
+ fieldsSlot="trace-request-headers"
171
+ jsonSlot="trace-request-headers-json"
172
+ localOpen
173
+ />
174
+ ) : null}
175
+ {hasInput ? (
176
+ <PayloadPanel
177
+ value={input}
178
+ open={inputOpen}
179
+ onOpenChange={onInputOpenChange}
180
+ label="Body"
181
+ empty=""
182
+ copyLabel="Copy body JSON"
183
+ copySlot="trace-request-copy-input"
184
+ toggleSlot="trace-input-toggle"
185
+ fieldsSlot="trace-request-fields"
186
+ jsonSlot="trace-input-json"
187
+ fill={requestFills}
188
+ />
189
+ ) : null}
138
190
  </div>
139
191
  </div>
140
192
  </div>
@@ -150,7 +202,7 @@ export function TraceRequestSection({
150
202
  <div className={EXPLORER_STRIP_CLASS}>
151
203
  <h3 className={cn(SECTION_HEAD_CLASS, "flex items-center px-2")}>Response</h3>
152
204
  <div className="ml-auto flex h-full items-stretch">
153
- {!failed && output !== null && output !== undefined ? (
205
+ {!failed && hasOutput ? (
154
206
  <CopyIconButton
155
207
  label="Copy response JSON"
156
208
  text={JSON.stringify(output, null, 2)}
@@ -167,33 +219,8 @@ export function TraceRequestSection({
167
219
  </div>
168
220
  </div>
169
221
 
170
- {failed ? (
171
- <div data-slot="trace-response-error" role="alert">
172
- <div className="flex min-w-0">
173
- <div
174
- className="w-1 shrink-0 self-stretch bg-destructive"
175
- aria-hidden
176
- data-slot="trace-response-rail"
177
- />
178
- <div className="flex min-w-0 flex-1 flex-col gap-1 px-2.5 py-2.5">
179
- <div className="flex items-center gap-1.5">
180
- <HugeiconsIcon
181
- icon={Alert02Icon}
182
- className="size-3.5 shrink-0 text-destructive"
183
- aria-hidden
184
- />
185
- <span className="font-mono text-xs font-semibold text-destructive">{error}</span>
186
- </div>
187
- {errorMessage ? (
188
- <p className="text-[11px] leading-snug text-destructive/90">{errorMessage}</p>
189
- ) : (
190
- <p className="text-[11px] text-muted-foreground">
191
- Run failed with this error code — no return value was stored.
192
- </p>
193
- )}
194
- </div>
195
- </div>
196
- </div>
222
+ {failed && responseStatus === undefined ? (
223
+ <ResponseError error={error} errorMessage={errorMessage} />
197
224
  ) : (
198
225
  <div
199
226
  data-slot="trace-response-frame"
@@ -201,24 +228,49 @@ export function TraceRequestSection({
201
228
  >
202
229
  <div className={cn("flex min-w-0", responseFills && "min-h-0 flex-1")}>
203
230
  <div
204
- className="w-1 shrink-0 self-stretch bg-emerald-500"
231
+ className={cn(
232
+ "w-1 shrink-0 self-stretch",
233
+ responseStatus !== undefined
234
+ ? httpStatusRailClass(responseStatus)
235
+ : "bg-emerald-500",
236
+ )}
205
237
  aria-hidden
206
238
  data-slot="trace-response-rail"
207
239
  />
208
240
  <div className={cn("flex min-w-0 flex-1 flex-col", responseFills && "min-h-0")}>
209
- <PayloadPanel
210
- value={output}
211
- open={outputOpen}
212
- onOpenChange={onOutputOpenChange}
213
- label="Body"
214
- empty="No stored response — this run completed without a return value."
215
- copyLabel="Copy response JSON"
216
- copySlot="trace-response-copy-body"
217
- toggleSlot="trace-response-toggle"
218
- fieldsSlot="trace-response-fields"
219
- jsonSlot="trace-response-json"
220
- fill={responseFills}
221
- />
241
+ {responseStatus !== undefined ? <ResponseStatus status={responseStatus} /> : null}
242
+ {hasResponseHeaders ? (
243
+ <PayloadPanel
244
+ value={responseHeaders}
245
+ open
246
+ onOpenChange={() => undefined}
247
+ label="Headers"
248
+ empty=""
249
+ copyLabel="Copy response headers"
250
+ copySlot="trace-response-copy-headers"
251
+ toggleSlot="trace-response-headers-toggle"
252
+ fieldsSlot="trace-response-headers"
253
+ jsonSlot="trace-response-headers-json"
254
+ localOpen
255
+ />
256
+ ) : null}
257
+ {failed ? (
258
+ <ResponseError error={error} errorMessage={errorMessage} bare />
259
+ ) : hasOutput ? (
260
+ <PayloadPanel
261
+ value={output}
262
+ open={outputOpen}
263
+ onOpenChange={onOutputOpenChange}
264
+ label="Body"
265
+ empty=""
266
+ copyLabel="Copy response JSON"
267
+ copySlot="trace-response-copy-body"
268
+ toggleSlot="trace-response-toggle"
269
+ fieldsSlot="trace-response-fields"
270
+ jsonSlot="trace-response-json"
271
+ fill={responseFills}
272
+ />
273
+ ) : null}
222
274
  </div>
223
275
  </div>
224
276
  </div>
@@ -228,6 +280,82 @@ export function TraceRequestSection({
228
280
  );
229
281
  }
230
282
 
283
+ /**
284
+ * Status line for an HTTP response frame.
285
+ *
286
+ * @param props - Status code
287
+ */
288
+ function ResponseStatus({ status }: { readonly status: number }): JSX.Element {
289
+ const reason = httpStatusReason(status);
290
+ return (
291
+ <div className="flex min-w-0 items-center gap-2 px-2.5 py-2" data-slot="trace-response-status">
292
+ <span
293
+ className={cn(
294
+ "shrink-0 font-mono text-[10px] font-semibold tabular-nums",
295
+ httpStatusTextClass(status),
296
+ )}
297
+ >
298
+ {status}
299
+ </span>
300
+ {reason ? <span className="font-mono text-xs text-foreground">{reason}</span> : null}
301
+ </div>
302
+ );
303
+ }
304
+
305
+ /**
306
+ * Failed-run alert. `bare` sits inside a frame that already has a rail.
307
+ *
308
+ * @param props - Error code and optional message
309
+ */
310
+ function ResponseError({
311
+ error,
312
+ errorMessage,
313
+ bare = false,
314
+ }: {
315
+ readonly error: string | null;
316
+ readonly errorMessage: string | null;
317
+ readonly bare?: boolean;
318
+ }): JSX.Element {
319
+ const body = (
320
+ <div className="flex min-w-0 flex-1 flex-col gap-1 px-2.5 py-2.5">
321
+ <div className="flex items-center gap-1.5">
322
+ <HugeiconsIcon
323
+ icon={Alert02Icon}
324
+ className="size-3.5 shrink-0 text-destructive"
325
+ aria-hidden
326
+ />
327
+ <span className="font-mono text-xs font-semibold text-destructive">{error}</span>
328
+ </div>
329
+ {errorMessage ? (
330
+ <p className="text-[11px] leading-snug text-destructive/90">{errorMessage}</p>
331
+ ) : (
332
+ <p className="text-[11px] text-muted-foreground">
333
+ Run failed with this error code — no return value was stored.
334
+ </p>
335
+ )}
336
+ </div>
337
+ );
338
+ if (bare) {
339
+ return (
340
+ <div data-slot="trace-response-error" role="alert" className="border-t border-border/60">
341
+ {body}
342
+ </div>
343
+ );
344
+ }
345
+ return (
346
+ <div data-slot="trace-response-error" role="alert">
347
+ <div className="flex min-w-0">
348
+ <div
349
+ className="w-1 shrink-0 self-stretch bg-destructive"
350
+ aria-hidden
351
+ data-slot="trace-response-rail"
352
+ />
353
+ {body}
354
+ </div>
355
+ </div>
356
+ );
357
+ }
358
+
231
359
  /**
232
360
  * Collapsible Fields/Raw payload panel shared by Request and Output.
233
361
  *
@@ -245,6 +373,7 @@ function PayloadPanel({
245
373
  fieldsSlot,
246
374
  jsonSlot,
247
375
  fill = false,
376
+ localOpen = false,
248
377
  }: {
249
378
  readonly value: unknown;
250
379
  readonly open: boolean;
@@ -258,6 +387,8 @@ function PayloadPanel({
258
387
  readonly jsonSlot: string;
259
388
  /** Grow to fill leftover sheet height instead of capping at 14rem. */
260
389
  readonly fill?: boolean;
390
+ /** Own the open state, starting expanded. Used for query and headers. */
391
+ readonly localOpen?: boolean;
261
392
  }): JSX.Element {
262
393
  const hasValue = value !== null && value !== undefined;
263
394
  const json = useMemo(() => (hasValue ? JSON.stringify(value, null, 2) : ""), [hasValue, value]);
@@ -265,6 +396,9 @@ function PayloadPanel({
265
396
  const shapeHint = useMemo(() => (hasValue ? inputShapeHint(value) : null), [hasValue, value]);
266
397
  const byteLabel = useMemo(() => (hasValue ? inputByteLabel(json) : null), [hasValue, json]);
267
398
  const [view, setView] = useState<BodyView>(rows ? "fields" : "raw");
399
+ const [ownOpen, setOwnOpen] = useState(true);
400
+ const panelOpen = localOpen ? ownOpen : open;
401
+ const setPanelOpen = localOpen ? setOwnOpen : onOpenChange;
268
402
 
269
403
  useEffect(() => {
270
404
  setView(rows ? "fields" : "raw");
@@ -280,9 +414,9 @@ function PayloadPanel({
280
414
 
281
415
  return (
282
416
  <Collapsible
283
- open={open}
284
- onOpenChange={onOpenChange}
285
- className={fill ? FILL_COL_CLASS : undefined}
417
+ open={panelOpen}
418
+ onOpenChange={setPanelOpen}
419
+ className={fill ? FILL_COL_CLASS : "shrink-0"}
286
420
  >
287
421
  <div className={cn(EXPLORER_STRIP_CLASS, "border-t")}>
288
422
  <CollapsibleTrigger
@@ -291,7 +425,7 @@ function PayloadPanel({
291
425
  >
292
426
  <HugeiconsIcon
293
427
  icon={ArrowDown01Icon}
294
- className={cn(EXPLORER_CHEVRON_CLASS, !open && "-rotate-90")}
428
+ className={cn(EXPLORER_CHEVRON_CLASS, !panelOpen && "-rotate-90")}
295
429
  />
296
430
  <span>{label}</span>
297
431
  {shapeHint ? (
@@ -453,7 +587,7 @@ function ViewToggleButton({
453
587
  }
454
588
 
455
589
  /**
456
- * Interactive key / value table for flat object bodies.
590
+ * Interactive key / value tree. Objects and arrays expand into child fields.
457
591
  *
458
592
  * @param props - Projected field rows
459
593
  */
@@ -467,42 +601,87 @@ function FieldsTable({
467
601
  return (
468
602
  <ul data-slot={dataSlot}>
469
603
  {rows.map((row) => (
470
- <li
471
- key={row.key}
472
- className={cn(EXPLORER_ROW_CLASS, "group/field items-start")}
473
- data-slot="trace-payload-field"
474
- >
475
- <div className="flex min-w-0 flex-1 flex-col gap-0.5 sm:flex-row sm:items-baseline sm:gap-3">
476
- <span className="w-[7.5rem] shrink-0 truncate font-mono text-[11px] font-medium text-sky-600 dark:text-sky-400">
477
- {row.key}
478
- </span>
479
- <span
480
- className={cn(
481
- "min-w-0 flex-1 break-all font-mono text-[11px] leading-snug",
482
- valueToneClass(row.kind),
483
- )}
484
- title={row.display}
485
- >
486
- {row.display}
487
- </span>
488
- </div>
489
- <span className="mt-0.5 shrink-0 font-mono text-[9px] tracking-wide text-muted-foreground uppercase opacity-70">
490
- {row.kind}
491
- </span>
492
- <div className="opacity-0 transition-opacity group-hover/field:opacity-100 group-focus-within/field:opacity-100">
493
- <CopyIconButton
494
- label={`Copy ${row.key}`}
495
- text={fieldCopyText(row.value)}
496
- dataSlot="trace-payload-copy-field"
497
- bare
498
- />
499
- </div>
500
- </li>
604
+ <PayloadFieldRow key={row.key} row={row} depth={0} />
501
605
  ))}
502
606
  </ul>
503
607
  );
504
608
  }
505
609
 
610
+ /**
611
+ * One payload field. Containers toggle a nested field list.
612
+ *
613
+ * @param props - Row + indent depth
614
+ */
615
+ function PayloadFieldRow({
616
+ row,
617
+ depth,
618
+ }: {
619
+ readonly row: InputFieldRow;
620
+ readonly depth: number;
621
+ }): JSX.Element {
622
+ const expandable = row.children !== null;
623
+ const [open, setOpen] = useState(false);
624
+
625
+ return (
626
+ <li data-slot="trace-payload-field" data-depth={depth}>
627
+ <div
628
+ className={cn(EXPLORER_ROW_CLASS, "group/field items-start")}
629
+ style={depth > 0 ? { paddingLeft: `${10 + depth * 14}px` } : undefined}
630
+ >
631
+ {expandable ? (
632
+ <button
633
+ type="button"
634
+ className="mt-0.5 flex size-3.5 shrink-0 items-center justify-center text-muted-foreground"
635
+ aria-expanded={open}
636
+ aria-label={`${open ? "Collapse" : "Expand"} ${row.key}`}
637
+ data-slot="trace-payload-expand"
638
+ onClick={() => setOpen((next) => !next)}
639
+ >
640
+ <HugeiconsIcon
641
+ icon={ArrowDown01Icon}
642
+ className={cn(EXPLORER_CHEVRON_CLASS, !open && "-rotate-90")}
643
+ />
644
+ </button>
645
+ ) : (
646
+ <span className="mt-0.5 size-3.5 shrink-0" aria-hidden />
647
+ )}
648
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5 sm:flex-row sm:items-baseline sm:gap-3">
649
+ <span className="w-[7.5rem] shrink-0 truncate font-mono text-[11px] font-medium text-sky-600 dark:text-sky-400">
650
+ {row.key}
651
+ </span>
652
+ <span
653
+ className={cn(
654
+ "min-w-0 flex-1 break-all font-mono text-[11px] leading-snug",
655
+ valueToneClass(row.kind),
656
+ )}
657
+ title={row.display}
658
+ >
659
+ {row.display}
660
+ </span>
661
+ </div>
662
+ <span className="mt-0.5 shrink-0 font-mono text-[9px] tracking-wide text-muted-foreground uppercase opacity-70">
663
+ {row.kind}
664
+ </span>
665
+ <div className="opacity-0 transition-opacity group-hover/field:opacity-100 group-focus-within/field:opacity-100">
666
+ <CopyIconButton
667
+ label={`Copy ${row.key}`}
668
+ text={fieldCopyText(row.value)}
669
+ dataSlot="trace-payload-copy-field"
670
+ bare
671
+ />
672
+ </div>
673
+ </div>
674
+ {expandable && open ? (
675
+ <ul data-slot="trace-payload-children">
676
+ {row.children?.map((child) => (
677
+ <PayloadFieldRow key={child.key} row={child} depth={depth + 1} />
678
+ ))}
679
+ </ul>
680
+ ) : null}
681
+ </li>
682
+ );
683
+ }
684
+
506
685
  function valueToneClass(kind: InputFieldKind): string {
507
686
  switch (kind) {
508
687
  case "string":
@@ -2,7 +2,7 @@
2
2
  * Scoped Traces pane (start side of the Flow split-view).
3
3
  */
4
4
 
5
- import { useEffect, useMemo, useState } from "react";
5
+ import { useEffect, useMemo, useRef, useState } from "react";
6
6
  import { Alert02Icon, FilterHorizontalIcon, Menu01Icon } from "@hugeicons/core-free-icons";
7
7
  import { HugeiconsIcon } from "@hugeicons/react";
8
8
  import type { Manifest } from "../../../../../../manifest/types.ts";
@@ -100,6 +100,11 @@ export function TracesPane({
100
100
  return filterRunsByGraph(base, graphFilter, manifest);
101
101
  }, [runs, filters, graphFilter, manifest]);
102
102
 
103
+ const navigationRuns = useMemo(() => {
104
+ if (!selectedRunId) return visible;
105
+ return visible.some((run) => run.id === selectedRunId) ? visible : runs;
106
+ }, [visible, runs, selectedRunId]);
107
+
103
108
  const selectedRun = useMemo(
104
109
  () =>
105
110
  selectedRunProp !== undefined
@@ -123,6 +128,13 @@ export function TracesPane({
123
128
  setFilters((prev) => ({ ...prev, advanced }));
124
129
  };
125
130
 
131
+ const listRef = useRef<HTMLDivElement>(null);
132
+ useEffect(() => {
133
+ if (!selectedRunId || !listRef.current) return;
134
+ const row = listRef.current.querySelector(`[data-run-id="${CSS.escape(selectedRunId)}"]`);
135
+ if (row instanceof HTMLElement) row.scrollIntoView({ block: "nearest" });
136
+ }, [selectedRunId]);
137
+
126
138
  return (
127
139
  <div className="flex h-full min-h-0 flex-col overflow-hidden" data-slot="traces-pane">
128
140
  <div className="shrink-0 border-b border-border/60">
@@ -249,7 +261,7 @@ export function TracesPane({
249
261
  </AgentDisclosure>
250
262
  ) : null}
251
263
 
252
- <div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
264
+ <div ref={listRef} className="flex min-h-0 flex-1 flex-col overflow-y-auto">
253
265
  {runs.length === 0 ? (
254
266
  <Empty className="border-none">
255
267
  <EmptyHeader>
@@ -290,6 +302,8 @@ export function TracesPane({
290
302
  onFocusEffectChange={onFocusEffectChange}
291
303
  playbackKey={playbackKey}
292
304
  onReplayStart={onReplayStart}
305
+ navigationRuns={navigationRuns}
306
+ onSelectRun={(id) => onSelect(id)}
293
307
  />
294
308
  </div>
295
309
  );