jev-devtools 0.5.3 → 0.5.5

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.
package/README.md CHANGED
@@ -1,157 +1,163 @@
1
1
  # Jev DevTools
2
2
 
3
- [![npm version 0.5.3](https://img.shields.io/badge/npm-v0.5.3-E551BA?labelColor=0D0A0F&logo=npm)](https://www.npmjs.com/package/jev-devtools/v/0.5.3)
3
+ [![npm version 0.5.5](https://img.shields.io/badge/npm-v0.5.5-E551BA?labelColor=0D0A0F&logo=npm)](https://www.npmjs.com/package/jev-devtools/v/0.5.5)
4
4
 
5
- A React drawer and Node adapter for inspecting TypeSafe Jev requests with animated diagrams. Includes request history, context, questions, typed answers, transport attempts, and raw snapshots.
5
+ A React inspector for TypeSafe Jev requests captured through a Node server adapter. Inspect request inputs, returned answers and probabilities, retries, and errors. Use the optional Flow API to connect requests and application steps in a diagram.
6
6
 
7
- **Package version: 0.5.3.** [See release notes for verified publication status.](https://github.com/diegoag64/jev-devtools/releases/tag/v0.5.3) Check the [release history](https://github.com/diegoag64/jev-devtools/blob/main/CHANGELOG.md) for compatibility notes. To try newer source changes, install the local archive below. **Experimental (pre-1.0).** React 18/19 and Node 22.12+ are supported. APIs may change before 1.0. Verification uses sample responses through the real TypeSafe SDK, not live inference.
8
-
9
- [Website](https://jevtools.dev/) · [Documentation](https://docs.jevtools.dev/) · [Quick start](https://docs.jevtools.dev/getting-started/) · [API reference](https://docs.jevtools.dev/api/reference/) · [Release history](https://docs.jevtools.dev/changelog/)
10
-
11
- Hosted docs update separately; use the installed declarations and bundled skill for your exact version, and the [repository changelog](https://github.com/diegoag64/jev-devtools/blob/main/CHANGELOG.md) for release-candidate status.
7
+ [Website](https://jevtools.dev/) · [Documentation](https://docs.jevtools.dev/) · [API reference](https://docs.jevtools.dev/api/reference/) · [For AI agents](#for-ai-agents)
12
8
 
13
9
  ## Demo
14
10
 
15
- ![Full 28-second Jev DevTools demo showing sample results, the request drawer, and animated diagrams.](https://raw.githubusercontent.com/diegoag64/jev-devtools/main/docs/assets/jev-devtools-preview.gif)
11
+ ![Jev DevTools sample recording showing the request drawer and diagrams.](https://raw.githubusercontent.com/diegoag64/jev-devtools/main/docs/assets/jev-devtools-preview.gif)
12
+
13
+ The recording uses sample responses. Try the [sample apps and cookbooks](https://docs.jevtools.dev/examples/) without an API key. Cookbook application code and fixtures live in the repository and are not bundled in npm.
16
14
 
17
- The full sample recording loops above.
15
+ ## Install
18
16
 
19
- ## Set up with your coding agent
17
+ Use matching React and React DOM **18.x or 19.x**, Node **22.12+**, TypeSafe SDK **0.7.x**, and ESM imports. TypeScript consumers need **5.4+** and compatible React and Node types. React and React DOM are required peers even in a server-only workspace. See [compatibility and public imports](https://github.com/diegoag64/jev-devtools/blob/main/docs/integration.md#compatibility-and-public-imports) for exact ranges and compiler settings.
20
18
 
21
- In your application's coding-agent chat, paste:
19
+ Install in each workspace that imports the package:
22
20
 
23
- ```text
24
- Set up jev-devtools in this React app. Read and follow:
25
- https://raw.githubusercontent.com/diegoag64/jev-devtools/main/packages/jev-devtools/skills/setup-jev-devtools/SKILL.md
26
- Choose the recipe for this app's framework, wire the server capture and root
27
- drawer, and verify with a labeled sample request. Preserve existing app behavior.
21
+ ```sh
22
+ npm install jev-devtools @typesafe-ai/sdk
28
23
  ```
29
24
 
30
- After installing, the version-matched instructions are available locally at `node_modules/jev-devtools/skills/setup-jev-devtools/SKILL.md` with all references included. In a workspace, read the package installed for the consuming app. Agents can follow these files without web access. Tools with skill support can install/copy the entire `setup-jev-devtools` folder into their supported skills location; npm does not register it automatically.
25
+ Connect one React drawer, one shared Node adapter, and an instrumented client created with `inspector.createClient()`. Only calls through that client are captured. The drawer and adapter default to disabled. Enable both for development and keep API keys on the server.
31
26
 
32
- The [setup skill](https://github.com/diegoag64/jev-devtools/blob/main/packages/jev-devtools/skills/setup-jev-devtools/SKILL.md) covers React/Node/Vite and Next.js App Router, with no-key sample verification and troubleshooting. A Node backend is required for live calls; browser-only apps can use a local Vite sample server. [Agent documentation index](https://raw.githubusercontent.com/diegoag64/jev-devtools/main/llms.txt).
27
+ Choose the setup guide for your app:
33
28
 
34
- ## Install
29
+ - [React with Node](https://docs.jevtools.dev/getting-started/) connects an existing Express server and a Vite React app, with proxy configuration and capture checks.
30
+ - [Next.js App Router](https://docs.jevtools.dev/guides/nextjs/) uses `getJevDevtools` for a shared adapter, `handleNodeRoute` in an optional catch-all route, and a client component for the drawer. Externalize `@typesafe-ai/sdk` and keep `jev-devtools` bundled. The package does not require a Next.js dependency.
35
31
 
36
- Install from npm:
32
+ Mount the drawer once near your app's root. It supplies its own styles inside a Shadow DOM boundary and requires no CSS import, Tailwind, or shadcn. The browser entry is safe for server rendering. Pan the Flow canvas by dragging, zoom by scrolling or pinching, or select **Fit**.
37
33
 
38
- ```sh
39
- pnpm add jev-devtools @typesafe-ai/sdk
40
- ```
34
+ ### Runtime and access
41
35
 
42
- Use ESM imports, matching React/React DOM **18.x or 19.x**, Node **22.12+**, and TypeSafe SDK **0.7.x**. TypeScript consumers need **5.4+** and compatible React/Node types. React and React DOM are required package peers even for server-only installations. See [compatibility and all public imports](https://github.com/diegoag64/jev-devtools/blob/main/docs/integration.md#compatibility-and-public-imports); `/server` and `/flow` run only on Node.
36
+ The inspector routes and application client must share one adapter in the same Node process. Keep `jev-devtools/server` and `jev-devtools/flow` on Node. Edge runtimes and history aggregation across distributed or serverless instances are unsupported.
43
37
 
44
- To try local changes before a future release:
38
+ The drawer receives same-origin server-sent events, starting with a snapshot and then changed traces. Older client and server combinations can use full snapshots. Keep both on the same package version to inspect all supported flow steps. History is bounded to 50 calls in memory by default.
45
39
 
46
- ```sh
47
- # From the jev-devtools repository root:
48
- npm ci
49
- npm run pack:devtools
50
- # From the consuming app:
51
- npm install /absolute/path/to/jev-devtools-0.5.3.tgz
52
- ```
40
+ Bind local development servers to `127.0.0.1`. Without `authorize`, Node middleware accepts only loopback connections. Web Request handlers cannot inspect the connection's address. Shared access and tunnels require real [authorization and access controls](https://github.com/diegoag64/jev-devtools/blob/main/docs/integration.md#history-and-access). Match the adapter's `origin` to the browser URL, and proxy the inspector endpoints when the frontend and backend use different ports.
53
41
 
54
- Mount once. The drawer loads its own styles; no CSS import is needed:
42
+ ### Capture privacy
55
43
 
56
- ```tsx
57
- import { JevDevtools } from "jev-devtools";
44
+ Captures contain bounded, redacted copies of request context and answers. Application and SDK data remain unchanged. Diagrams show captured data and application-defined routing, not hidden model reasoning.
58
45
 
59
- <JevDevtools enabled={import.meta.env.DEV} defaultOpen={false} />;
60
- ```
46
+ Known credential fields and custom server redactions cannot be revealed in the drawer. Headers and client options are not captured. Arbitrary secrets in prose or unusual field names still need application-specific server redaction. See the [redaction policy and limits](https://github.com/diegoag64/jev-devtools/blob/main/docs/integration.md#redaction-policy).
61
47
 
62
- Create one shared Node adapter and instrumented client:
48
+ `redactAdditionalSensitiveData` defaults to `true` and masks supported email, phone, postal address, SSN, and card-number fields in the drawer. Set it to `false` on `JevDevtools` to disable that display masking, including in existing views. **Display masking does not remove captured data.** The underlying values remain in capture, stream, and browser memory. Use server-side redaction for data that must not reach the browser.
63
49
 
64
- ```ts
65
- import { createJevDevtools } from "jev-devtools/server";
50
+ ### Versions and local builds
66
51
 
67
- const inspector = createJevDevtools({
68
- enabled: process.env.NODE_ENV === "development",
69
- origin: "http://localhost:3000",
70
- });
71
- const jev = inspector.createClient({ apiKey: process.env.TYPESAFE_API_KEY });
72
- app.use(inspector.middleware); // Mount at root, before fallback handlers.
73
- // Call jev.systemOne(...) in your application's handlers.
74
- // Call inspector.dispose() on shutdown.
75
- ```
52
+ **Package version: 0.5.5.** [See release notes for verified publication status](https://github.com/diegoag64/jev-devtools/releases/tag/v0.5.5). APIs remain experimental before 1.0. Check the [changelog](https://github.com/diegoag64/jev-devtools/blob/main/CHANGELOG.md) for compatibility and release status. Verification uses sample responses through the real TypeSafe SDK, not live inference.
76
53
 
77
- Only calls through that client are captured. The React component subscribes via same-origin SSE, receiving an initial snapshot followed by changed traces. Older client/server combinations remain compatible through full snapshots. Both sides default to disabled. API keys remain server-side. Browser code is SSR-safe. The drawer uses a Shadow DOM boundary and its own stylesheet so host page selectors cannot alter its internal layout. No Tailwind or shadcn installation is required. Drag the Flow canvas to pan, scroll/pinch to zoom, or use Fit.
54
+ Hosted docs update separately. Prefer installed declarations and the bundled setup skill when the site's version differs from your package.
78
55
 
79
- History is bounded to 50 calls in memory by default. API routes and inspector endpoints must share an adapter instance; distributed processes and edge runtimes are not supported. Without `authorize`, the middleware accepts only loopback connections; Web Request handlers can't see the connection's address, so run those servers on `127.0.0.1`. Captures include bounded/redacted context and answers, not hidden model reasoning.
56
+ <details>
57
+ <summary>Try changes from a local checkout</summary>
80
58
 
81
- Known credential fields are always redacted from DevTools captures; SDK and runtime data remain unchanged. The optional `<JevDevtools redactAdditionalSensitiveData={false} />` prop controls **display-only** masking of supported email, phone, postal address, SSN and card-number fields. It defaults to `true` and updates existing views immediately. These additional values still exist in capture/stream/browser memory while masked. Custom server redaction remains active, and neither credentials nor custom-redacted values can be revealed. Headers/options are not captured. See the [exact policy and limitations](https://github.com/diegoag64/jev-devtools/blob/main/docs/integration.md#redaction-policy); arbitrary secrets in prose and unusual field names still need application-specific redaction.
59
+ Pack the Jev DevTools repository, then install the archive in your app:
82
60
 
83
- For Next.js App Router, use `getJevDevtools(appKey, optionsFactory)` from `/server` to share an adapter across server reloads, expose `handleNodeRoute` through an optional catch-all Route Handler, and mount the root component in a client boundary. See the [Next.js guide](https://docs.jevtools.dev/guides/nextjs/) and runnable example. No Next dependency is required by this package.
61
+ ```sh
62
+ # From the jev-devtools repository root:
63
+ npm ci
64
+ npm run pack:devtools
65
+
66
+ # From the consuming app:
67
+ npm install /absolute/path/to/jev-devtools/jev-devtools-0.5.5.tgz
68
+ ```
84
69
 
85
- [Quick start](https://docs.jevtools.dev/getting-started/) · [Detailed integration and access controls](https://github.com/diegoag64/jev-devtools/blob/main/docs/integration.md) · [Repository and playground](https://github.com/diegoag64/jev-devtools)
70
+ Keep the drawer and server adapter on the same build.
86
71
 
87
- Jev DevTools is an independent, open-source community project. It is not affiliated with TypeSafe AI.
72
+ </details>
88
73
 
89
- MIT for project code. See bundled LICENSE and THIRD_PARTY_NOTICES.txt for original design attribution and TypeSafe branding.
74
+ <a id="connected-jev-flows-with-jev-devtoolsflow"></a>
90
75
 
91
- ## Connected Jev flows with `jev-devtools/flow`
76
+ ## Connect requests with flows
92
77
 
93
- Define calls, conditions, parallel checks, and named outcomes with `jev-devtools/flow`. The builder supports server function steps (`task`), runtime request factories, bounded collections (`map`), returning conditionals (`if`), stateful iteration (`loop`), and strict numeric `.gt()` comparisons. The drawer connects request diagrams, function results, selected paths, collection items, and loop iterations on one canvas. Existing single-request integrations remain supported.
78
+ Define a flow on Node, then run it for each input. Typed answer references can feed later requests, conditions, and outputs. The API also supports server tasks, parallel steps, bounded collections, conditional paths that rejoin, and stateful loops.
94
79
 
95
- On the server, reuse the instrumented `jev` client and `inspector` from the setup above:
80
+ This example asks whether a message is ready for review. An application-defined threshold selects an outcome. Reuse the instrumented `jev` client and shared `inspector` from your server integration. Running this example with a live client makes an inference request:
96
81
 
97
82
  ```ts
83
+ import { noul } from "@typesafe-ai/sdk";
98
84
  import { defineJevFlow } from "jev-devtools/flow";
99
85
 
100
86
  const review = defineJevFlow<{ message: string }>("review", (flow) => {
101
- const decision = flow.jev(
102
- "readiness",
103
- {
104
- state: flow.input,
105
- questions: {
106
- ready: { type: "noul", instructions: "Is this ready for review?" },
107
- },
108
- },
109
- { title: "Assess readiness" },
110
- );
111
-
112
- return flow.branch(
113
- "route",
114
- {
115
- when: decision.answer("ready").noul.gte(0.9), // Your application's policy.
116
- then: (path) =>
117
- path.output(
118
- "ready",
119
- { probability: decision.answer("ready").noul },
120
- {
121
- title: "Ready for review",
122
- description: "Return readiness to the application.",
123
- },
124
- ),
125
- else: (path) =>
126
- path.output("manual-review", undefined, {
127
- title: "Needs manual review",
128
- }),
87
+ const readiness = flow.jev("readiness", {
88
+ state: flow.input,
89
+ questions: {
90
+ ready: noul("Does this message include enough detail to review?"),
129
91
  },
130
- { title: "Require at least 90% readiness" },
131
- );
92
+ });
93
+
94
+ return flow.branch("route", {
95
+ // Example application policy, not a threshold supplied by Jev.
96
+ when: readiness.answer("ready").noul.gte(0.9),
97
+ then: (path) => path.output("ready"),
98
+ else: (path) => path.output("needs-detail"),
99
+ });
132
100
  });
133
101
 
134
102
  const result = await review.run(
135
- { message: "Please review my ticket." },
136
- {
137
- client: jev,
138
- devtools: inspector,
139
- },
103
+ { message: "Please review the attached proposal." },
104
+ { client: jev, devtools: inspector },
140
105
  );
141
- // Handle result.outcome in your app. The flow does not execute business actions.
106
+ console.log(result.outcome); // "ready" or "needs-detail", depending on the answer.
142
107
  ```
143
108
 
144
- Define once in a Node module and run per request. Use `flow.parallel` for concurrent named lanes; downstream calls wait for every lane to succeed. `flow.branch` is terminal in its block; use `flow.if` when selected values feed shared downstream work. Tasks execute application code on Node with JSON-compatible inputs/results; maps and loops have explicit bounds. Runs have SDK retries and cooperative cancellation, with bounded in-memory inspector history. They do not provide durable or distributed execution. Keep the server and drawer on the same package version to inspect every supported step kind.
109
+ Open **Jev DevTools**, select **Flows**, and open the run. Inspect the returned probability and the condition that selected the path. Untaken paths remain visible. The flow returns an outcome. Your application decides what to do with it.
110
+
111
+ Runs support SDK retries and cancellation, with bounded in-memory inspector history. They do not provide durable workflow execution. Learn the API in [Flows from scratch](https://docs.jevtools.dev/guides/flow-basics/) or look up contracts in the [flow reference](https://github.com/diegoag64/jev-devtools/blob/main/docs/flows.md).
112
+
113
+ ## Replay and compare runs
114
+
115
+ Replay and comparison require **0.5.0 or later**. Choose **Copy fixture** on a captured run to save its answers as JSON. `replayClient(fixture)` from `jev-devtools/flow` supplies those answers without Jev inference or an API key. Tasks run again and may call outside services.
116
+
117
+ `compareWithFixture` compares answers, routing, and item steps with a saved fixture. It runs the flow with the supplied client and can make live calls. See the [Flow API reference](https://docs.jevtools.dev/api/flow/) for fixture requirements and complete examples.
118
+
119
+ <a id="set-up-with-your-coding-agent"></a>
120
+
121
+ ## For AI agents
145
122
 
146
- **Added in 0.5.0.** **Copy fixture** in the drawer saves a run as JSON, and `replayClient(fixture)` from `jev-devtools/flow` replays its Jev calls in tests with no network access or API key; tasks run again. `compareWithFixture` from `jev-devtools/flow` compares a live run's answers, routing and item steps with a saved fixture.
123
+ Start with the `jev-devtools` version installed in the consuming app. Follow that app's project instructions and use its existing package manager. Choose guidance for the task:
147
124
 
148
- [Beginner flow guide](https://docs.jevtools.dev/guides/flow-basics/) · [Flow API reference](https://docs.jevtools.dev/api/flow/) · [Detailed flow guide](https://github.com/diegoag64/jev-devtools/blob/main/docs/flows.md) · [AI flow recipe](https://github.com/diegoag64/jev-devtools/blob/main/packages/jev-devtools/skills/setup-jev-devtools/references/flows.md)
125
+ | Task | Instructions |
126
+ | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127
+ | Install or troubleshoot the inspector | Read `node_modules/jev-devtools/skills/setup-jev-devtools/SKILL.md`. The [setup skill](https://github.com/diegoag64/jev-devtools/blob/main/packages/jev-devtools/skills/setup-jev-devtools/SKILL.md) selects the framework recipe and capture checks. |
128
+ | Build a flow | Read `references/flows.md` inside the installed setup skill. The [flow recipe](https://github.com/diegoag64/jev-devtools/blob/main/packages/jev-devtools/skills/setup-jev-devtools/references/flows.md) covers builders, execution, capture, replay, and limits. |
129
+ | Convert existing Jev logic or check whether it fits Flow | Read [port-jev-to-flow](https://github.com/diegoag64/jev-devtools/blob/main/.agents/skills/port-jev-to-flow/SKILL.md) and its referenced guidance. It maps source behavior to public Flow APIs and records adaptations or unsupported behavior. |
130
+ | Look up an API contract | Read `lib/src/index.d.ts`, `lib/server/index.d.ts`, or `lib/flow/index.d.ts` inside the installed package. Use the [API reference](https://docs.jevtools.dev/api/reference/) for explanations and examples. |
149
131
 
150
- To have your coding agent add a flow after installation:
132
+ The setup skill and its references ship in npm. **The porting skill is repository-only.** Read it at `.agents/skills/port-jev-to-flow/SKILL.md` in a Jev DevTools checkout, or use its [raw Markdown](https://raw.githubusercontent.com/diegoag64/jev-devtools/main/.agents/skills/port-jev-to-flow/SKILL.md). Resolve its `references/` links beside that file. For offline use, copy the whole `port-jev-to-flow` folder, including its references. Cookbook paths in the skill refer to the Jev DevTools repository.
133
+
134
+ Check online guidance against the installed declarations before using an API. Read declaration files from disk, but import application code only through `jev-devtools`, `jev-devtools/server`, and `jev-devtools/flow`. The latter two run on Node. Internal files and `jev-devtools/package.json` are not application import paths.
135
+
136
+ Use this prompt to set up an app after installing the package:
151
137
 
152
138
  ```text
153
- Read node_modules/jev-devtools/skills/setup-jev-devtools/SKILL.md and references/flows.md.
154
- Build a flow for the scenario I describe using the shared server client/inspector.
155
- Preserve my routing policies, add clear step titles/descriptions, and verify with
156
- labeled samples before using live inference. Ask for any missing decision rules.
139
+ Read node_modules/jev-devtools/skills/setup-jev-devtools/SKILL.md
140
+ from the package installed for this app, then follow its framework recipe.
141
+ Preserve authentication, redaction, SDK options, and application behavior.
142
+ Keep credentials on the server. Share one adapter between application calls
143
+ and inspector routes in the same Node process.
144
+ Verify a labeled sample in the drawer, run the app's typecheck and build,
145
+ and check that capture and display are disabled in production.
146
+ Report what passed, whether inference was sample or live, and any gaps.
157
147
  ```
148
+
149
+ For a port, provide the source code or recipe and ask the agent to use `port-jev-to-flow`. Preserve question wording, request order and concurrency, thresholds, and external effects. Report unsupported behavior before changing it. Verify with labeled samples first. Live inference, publication, and deployment require separate authorization.
150
+
151
+ npm does not register skills with coding tools automatically. Point the agent to the files explicitly, or install each complete skill folder using the tool's supported mechanism. Use [llms.txt](https://raw.githubusercontent.com/diegoag64/jev-devtools/main/llms.txt) to find repository guidance and [the hosted Markdown index](https://docs.jevtools.dev/llms.txt) for docs pages. The [contributor instructions](https://github.com/diegoag64/jev-devtools/blob/main/AGENTS.md) apply when editing Jev DevTools itself.
152
+
153
+ ## Documentation and contributing
154
+
155
+ Use the [documentation](https://docs.jevtools.dev/) for guides and API examples, and the [repository](https://github.com/diegoag64/jev-devtools/) for sample apps and cookbooks. See [Contributing](https://github.com/diegoag64/jev-devtools/blob/main/CONTRIBUTING.md) for repository setup, verification, and the pull request workflow. Merging, npm publication, and deployment each require explicit authorization.
156
+
157
+ ## Credits and license
158
+
159
+ Inspired by the TanStack Query DevTools workflow, with a custom drawer and TanStack Virtual for the Questions list. The TanStack DevTools shell is not included. Diagram styling is based on [Cathryn Lavery's diagram-design](https://github.com/cathrynlavery/diagram-design).
160
+
161
+ Jev DevTools is an independent, open-source community project. It is not affiliated with TypeSafe AI. TypeSafe branding belongs to its respective owner.
162
+
163
+ Project code is MIT licensed. See the bundled `LICENSE` and `THIRD_PARTY_NOTICES.txt`, or read the repository's [license](https://github.com/diegoag64/jev-devtools/blob/main/LICENSE) and [third-party notices](https://github.com/diegoag64/jev-devtools/blob/main/THIRD_PARTY_NOTICES.txt).
@@ -21,14 +21,17 @@ type Ast = {
21
21
  args: Ast[];
22
22
  };
23
23
  declare const valueTypeBrand: unique symbol;
24
+ type Scalar = string | number | boolean | null;
25
+ type ExpressionFields<T> = T extends readonly unknown[] ? Pick<T, number | "length" | Extract<keyof T, `${number}`>> : T;
24
26
  /** Inspectable expression, not an application callback. Use get() for input fields. */
25
27
  export declare class FlowExpression<T> {
26
28
  readonly ast: Ast;
27
29
  protected readonly [valueTypeBrand]: T;
28
30
  constructor(ast: Ast, owner?: object);
29
- /** Select an own field from an object or array; primitive properties are unsupported. */
30
- get<K extends keyof T & (string | number)>(this: FlowExpression<object>, key: K): FlowExpression<T[K]>;
31
- eq(value: T | FlowExpression<T>): FlowExpression<boolean>;
31
+ /** Select an own object field, array index, or array length. Missing values fail at runtime. */
32
+ get<K extends keyof T & keyof ExpressionFields<T> & (string | number)>(this: FlowExpression<object>, key: K): FlowExpression<T[K]>;
33
+ /** Compare matching scalar values; numbers must be finite at runtime. */
34
+ eq(this: FlowExpression<Scalar>, value: T | FlowExpression<T>): FlowExpression<boolean>;
32
35
  lt(this: FlowExpression<number>, value: number | FlowExpression<number>): FlowExpression<boolean>;
33
36
  gt(this: FlowExpression<number>, value: number | FlowExpression<number>): FlowExpression<boolean>;
34
37
  gte(this: FlowExpression<number>, value: number | FlowExpression<number>): FlowExpression<boolean>;
package/lib/flow/index.js CHANGED
@@ -49,10 +49,11 @@ export class FlowExpression {
49
49
  freezeAst(ast);
50
50
  Object.freeze(this);
51
51
  }
52
- /** Select an own field from an object or array; primitive properties are unsupported. */
52
+ /** Select an own object field, array index, or array length. Missing values fail at runtime. */
53
53
  get(key) {
54
54
  return expression({ op: "get", from: this.ast, key: String(key) }, [this]);
55
55
  }
56
+ /** Compare matching scalar values; numbers must be finite at runtime. */
56
57
  eq(value) {
57
58
  return operation("eq", [this, value]);
58
59
  }
@@ -214,7 +215,7 @@ function conditionRule(ast, requests, tasks) {
214
215
  ? path.length === 3 && ["noul", "choice", "score"].includes(path[2])
215
216
  ? path[1]
216
217
  : path.slice(1).join(" ")
217
- : path.at(-1) ?? "Input",
218
+ : (path.at(-1) ?? "Input"),
218
219
  ...(base.op === "ref" && requests.has(base.id)
219
220
  ? { stepId: base.id, path }
220
221
  : base.op === "ref" && tasks.has(base.id)
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { Image } from "@typesafe-ai/sdk";
4
+ import { projectTraceCapture } from "../src/capture-view.js";
4
5
  // Whole field names only: tokenCount, sessionId, apiKeyName, etc. stay useful.
5
6
  // Keep the documented lists in docs/integration.md aligned with these policies.
6
7
  const credentialKeys = new Set([
@@ -227,14 +228,10 @@ export class JevRecorder {
227
228
  captured = capture({ ...request, model: request.model ?? client.defaultModel }, warnings, this.options.redact);
228
229
  }
229
230
  catch {
230
- captured = {
231
- model: client.defaultModel,
232
- state: "[capture unavailable]",
233
- questions: {},
234
- };
231
+ captured = null;
235
232
  warnings.add("Input snapshot failed; request continues normally.");
236
233
  }
237
- this.traces.set(id, {
234
+ this.traces.set(id, projectTraceCapture({
238
235
  version: 1,
239
236
  id,
240
237
  ...(typeof meta === "function" ? meta() : meta),
@@ -244,7 +241,7 @@ export class JevRecorder {
244
241
  request: captured,
245
242
  attempts: [],
246
243
  captureWarnings: [...warnings],
247
- });
244
+ }));
248
245
  const trace = this.traces.get(id);
249
246
  try {
250
247
  this.flowContext.getStore()?.onTrace(id);
@@ -291,8 +288,13 @@ export class JevRecorder {
291
288
  t.durationMs = performance.now() - started;
292
289
  t.requestId =
293
290
  response.headers.get("x-typesafe-request-id") ?? undefined;
294
- t.result = result;
295
- t.captureWarnings = [...warnings];
291
+ Object.assign(t, projectTraceCapture({
292
+ ...t,
293
+ result,
294
+ captureWarnings: [
295
+ ...new Set([...t.captureWarnings, ...warnings]),
296
+ ],
297
+ }));
296
298
  });
297
299
  }, fail)
298
300
  .catch(() => {
@@ -0,0 +1,11 @@
1
+ import type { Trace } from "./protocol.js";
2
+ /** Missing paths come from shape checks, never from redaction marker strings. */
3
+ export declare function CaptureFields({ trace, path, }: {
4
+ trace: Trace;
5
+ path: readonly string[];
6
+ }): import("react").JSX.Element;
7
+ /** A partial box has readable coordinates but cannot define an overlay. */
8
+ export declare function PartialBox({ trace, name }: {
9
+ trace: Trace;
10
+ name: string;
11
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,42 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { capturedValue, captureFieldUnavailable, unavailableCaptureFields, } from "./capture-view.js";
3
+ const labels = {
4
+ request: "Request",
5
+ result: "Response",
6
+ questions: "Questions",
7
+ answers: "Answers",
8
+ score: "Score",
9
+ noul: "Probability of true",
10
+ confidence: "Confidence",
11
+ choice: "Choice",
12
+ probabilities: "Probabilities",
13
+ legend: "Legend",
14
+ box: "Box",
15
+ type: "Type",
16
+ model: "Model",
17
+ state: "Context",
18
+ };
19
+ const label = (parts) => parts
20
+ .map((part) => (Object.hasOwn(labels, part) ? labels[part] : part))
21
+ .join(" · ");
22
+ /** Missing paths come from shape checks, never from redaction marker strings. */
23
+ export function CaptureFields({ trace, path, }) {
24
+ const fields = captureFieldUnavailable(trace, path)
25
+ ? [path]
26
+ : unavailableCaptureFields(trace, path);
27
+ return (_jsx(_Fragment, { children: fields.map((field) => (_jsxs("p", { className: "muted", children: [label(field.slice(path.length).length
28
+ ? field.slice(path.length)
29
+ : path.slice(-1)), " ", "unavailable in this capture."] }, JSON.stringify(field)))) }));
30
+ }
31
+ /** A partial box has readable coordinates but cannot define an overlay. */
32
+ export function PartialBox({ trace, name }) {
33
+ if (trace.result?.answers[name]?.box)
34
+ return null;
35
+ const value = capturedValue(trace, ["result", "answers", name, "box"]);
36
+ if (!value || typeof value !== "object" || Array.isArray(value))
37
+ return null;
38
+ const coordinates = Object.entries(value).flatMap(([key, value]) => typeof value === "number" && Number.isFinite(value)
39
+ ? [`${key} ${value.toFixed(2)}`]
40
+ : []);
41
+ return coordinates.length ? (_jsxs("p", { className: "muted", children: [coordinates.join(" · "), " \u00B7 Box overlay unavailable"] })) : null;
42
+ }
@@ -1,4 +1,6 @@
1
+ import { type ReactNode } from "react";
1
2
  import type { AnswerView } from "./protocol.js";
2
- export declare function ChoiceProbabilities({ answer }: {
3
+ export declare function ChoiceProbabilities({ answer, captureNotice, }: {
3
4
  answer: AnswerView;
5
+ captureNotice?: ReactNode;
4
6
  }): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from "react";
3
- export function ChoiceProbabilities({ answer }) {
3
+ export function ChoiceProbabilities({ answer, captureNotice, }) {
4
4
  const [search, setSearch] = useState("");
5
5
  const [page, setPage] = useState(0);
6
6
  const rows = useMemo(() => Object.entries(answer.probabilities ?? {}).sort((a, b) => b[1] - a[1]), [answer.probabilities]);
@@ -13,7 +13,7 @@ export function ChoiceProbabilities({ answer }) {
13
13
  return (_jsxs("div", { className: "lab-choice-browser", role: "region", "aria-label": "All Choice probabilities", "data-diagram-scroll": "", children: [_jsxs("div", { className: "lab-choice-controls", children: [_jsxs("label", { children: ["Search categories", _jsx("input", { autoFocus: true, type: "search", value: search, onChange: (event) => {
14
14
  setSearch(event.target.value);
15
15
  setPage(0);
16
- } })] }), _jsxs("span", { role: "status", children: [filtered.length, " of ", rows.length, " categories \u00B7 highest probability first"] })] }), _jsxs("div", { className: "lab-choice-table-scroll", tabIndex: 0, "aria-label": "Probability table", children: [_jsxs("table", { children: [_jsxs("caption", { children: ["Captured probabilities. Values are not renormalized. Returned choice: ", answer.choice ?? "unavailable", "."] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Category" }), _jsx("th", { scope: "col", children: "Probability" }), _jsx("th", { scope: "col", children: "Exact value (0\u20131)" })] }) }), _jsx("tbody", { children: filtered
16
+ } })] }), _jsxs("span", { role: "status", children: [filtered.length, " of ", rows.length, " categories \u00B7 highest probability first"] })] }), _jsxs("div", { className: "lab-choice-table-scroll", tabIndex: 0, "aria-label": "Probability table", children: [_jsxs("table", { children: [_jsxs("caption", { children: [captureNotice, "Captured probabilities. Values are not renormalized. Returned choice: ", answer.choice ?? "unavailable", "."] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Category" }), _jsx("th", { scope: "col", children: "Probability" }), _jsx("th", { scope: "col", children: "Exact value (0\u20131)" })] }) }), _jsx("tbody", { children: filtered
17
17
  .slice(current * 20, (current + 1) * 20)
18
18
  .map(([label, value]) => (_jsxs("tr", { children: [_jsxs("th", { scope: "row", children: [label, label === answer.choice && (_jsx("span", { className: "lab-choice-returned", children: "Returned choice" }))] }), _jsxs("td", { children: [Number((value * 100).toPrecision(6)), "%"] }), _jsx("td", { children: _jsx("code", { children: String(value) }) })] }, label))) })] }), !filtered.length && _jsx("p", { children: "No matching categories." })] }), _jsxs("div", { className: "lab-choice-pagination", children: [_jsx("button", { disabled: current === 0, onClick: () => setPage(current - 1), children: "Previous categories" }), _jsxs("span", { "aria-live": "polite", children: ["Page ", current + 1, " of ", pages] }), _jsx("button", { disabled: current + 1 >= pages, onClick: () => setPage(current + 1), children: "Next categories" })] })] }));
19
19
  }
@@ -3,7 +3,7 @@ import type { Trace } from "./protocol.js";
3
3
  export declare const RequestDetail: import("react").MemoExoticComponent<({ trace, }: {
4
4
  trace?: Trace;
5
5
  }) => import("react").JSX.Element>;
6
- export declare function JevPanel({ traces, flows, connection, onClear, }: {
6
+ export declare function JevPanel({ traces: incomingTraces, flows, connection, onClear, }: {
7
7
  traces: Trace[];
8
8
  flows?: FlowHistory;
9
9
  connection: string;
@@ -1,4 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CaptureFields } from "./CaptureFields.js";
3
+ import { capturedValue, captureFieldUnavailable, traceCaptureView, } from "./capture-view.js";
2
4
  import { emptyFlows } from "./flow-stream.js";
3
5
  import { lazy, memo, startTransition, Suspense, useCallback, useEffect, useMemo, useRef, useState, } from "react";
4
6
  import { TraceDiagram } from "./TraceDiagram.js";
@@ -18,24 +20,37 @@ const RequestRow = memo(function RequestRow({ trace, selected, onSelect, }) {
18
20
  return (_jsxs("button", { className: `request-row ${selected ? "selected" : ""}`, "aria-pressed": selected, onClick: () => onSelect(trace.id), children: [_jsx("span", { className: `status-dot ${trace.status}` }), _jsxs("span", { children: [_jsx("strong", { children: trace.label }), _jsxs("small", { children: [new Date(trace.startedAt).toLocaleTimeString(), " \u00B7 ", trace.source, " \u00B7", " ", ms(trace.durationMs)] })] }), _jsx("span", { className: "row-status", children: trace.status })] }));
19
21
  });
20
22
  export const RequestDetail = memo(function RequestDetail({ trace, }) {
23
+ trace = trace && traceCaptureView(trace);
24
+ const tokens = (field) => {
25
+ if (!trace)
26
+ return "—";
27
+ const value = capturedValue(trace, ["result", "usage", field]);
28
+ return typeof value === "number" && Number.isFinite(value)
29
+ ? value
30
+ : captureFieldUnavailable(trace, ["result", "usage", field])
31
+ ? "Unavailable"
32
+ : "—";
33
+ };
21
34
  // Keep tab selection across request switches and clear, as before. Local tab
22
35
  // changes no longer rerender the history list.
23
36
  const [tab, setTab] = useState("Flow");
24
- return (_jsx("section", { className: "request-detail", children: trace ? (_jsxs(_Fragment, { children: [_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: [trace.source, " response \u00B7 ", trace.status] }), _jsx("h2", { children: trace.label })] }), _jsxs("div", { className: "metrics", children: [_jsxs("span", { children: [ms(trace.durationMs), _jsx("small", { children: "Total duration" })] }), _jsxs("span", { children: [trace.attempts.length, _jsx("small", { children: "Attempts" })] }), _jsxs("span", { children: [trace.result?.usage
25
- ? `${trace.result.usage.input_tokens} / ${trace.result.usage.output_tokens}`
26
- : "—", _jsx("small", { children: "Tokens in / out" })] })] })] }), _jsxs("div", { className: "detail-meta", children: [_jsx("code", { children: trace.result?.model || trace.request.model }), _jsx("code", { children: trace.requestId ||
37
+ return (_jsx("section", { className: "request-detail", children: trace ? (_jsxs(_Fragment, { children: [_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: [trace.source, " response \u00B7 ", trace.status] }), _jsx("h2", { children: trace.label })] }), _jsxs("div", { className: "metrics", children: [_jsxs("span", { children: [ms(trace.durationMs), _jsx("small", { children: "Total duration" })] }), _jsxs("span", { children: [trace.attempts.length, _jsx("small", { children: "Attempts" })] }), _jsxs("span", { children: [`${tokens("input_tokens")} / ${tokens("output_tokens")}`, _jsx("small", { children: "Tokens in / out" })] })] })] }), _jsxs("div", { className: "detail-meta", children: [_jsx("code", { children: trace.result?.model ||
38
+ trace.request.model ||
39
+ "Model unavailable in this capture" }), _jsx("code", { children: trace.requestId ||
27
40
  (trace.status === "pending"
28
41
  ? "Request ID pending"
29
- : "No request ID") })] }), _jsx("nav", { className: "detail-tabs", "aria-label": "Request views", children: ["Flow", "Questions", "Context", "Transport", "Raw"].map((name) => (_jsx("button", { "aria-pressed": tab === name, onClick: () => {
42
+ : "No request ID") }), ["request", "result"].map((section) => !captureFieldUnavailable(trace, [section]) &&
43
+ captureFieldUnavailable(trace, [section, "model"]) ? (_jsxs("p", { className: "muted", children: [section === "request" ? "Request" : "Response", " model unavailable in this capture."] }, section)) : null)] }), _jsx("nav", { className: "detail-tabs", "aria-label": "Request views", children: ["Flow", "Questions", "Context", "Transport", "Raw"].map((name) => (_jsx("button", { "aria-pressed": tab === name, onClick: () => {
30
44
  // Keep the current view while its chunk loads, but don't
31
45
  // defer subsequent opens once the component is available.
32
46
  if (name === "Questions" && !questionsLoaded)
33
47
  startTransition(() => setTab(name));
34
48
  else
35
49
  setTab(name);
36
- }, children: name }, name))) }), _jsx("div", { className: `detail-body ${tab === "Flow" ? "detail-body-flow" : tab === "Questions" ? "detail-body-questions" : ""}`, children: _jsxs(Suspense, { fallback: _jsx("p", { className: "muted", role: "status", children: "Loading questions\u2026" }), children: [trace.error && (_jsxs("div", { role: "alert", className: "error-banner", children: [_jsx("strong", { children: trace.error.name }), " ", trace.error.message] })), trace.captureWarnings.map((w) => (_jsx("p", { className: "warning", children: w }, w))), tab === "Flow" && _jsx(TraceDiagram, { trace: trace }, trace.id), tab === "Questions" && (_jsx(Questions, { trace: trace }, trace.id)), tab === "Context" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "muted", children: "Captured before dispatch without changing SDK data. Known credential fields remain redacted; additional display masking follows the component prop. Captures are size limited." }), findImage(trace.request.state) && (_jsx("img", { className: "context-image", src: findImage(trace.request.state), alt: "Captured context" })), _jsx(JsonBlock, { value: trace.request.state }), trace.request.assets && (_jsx(JsonBlock, { value: trace.request.assets }))] })), tab === "Transport" && (_jsxs(_Fragment, { children: [_jsx("h3", { children: "HTTP attempts" }), _jsx("p", { className: "muted", children: "Attempt duration ends at response headers. Total duration includes SDK processing and retry waits." }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Attempt" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Duration" })] }) }), _jsx("tbody", { children: trace.attempts.map((a) => (_jsxs("tr", { children: [_jsx("td", { children: a.number }), _jsx("td", { children: a.error || a.status || "Pending" }), _jsx("td", { children: ms(a.durationMs) })] }, a.number))) })] }), !trace.attempts.length && _jsx("p", { children: "No HTTP attempt recorded." })] })), tab === "Raw" && _jsx(JsonBlock, { value: trace })] }) })] })) : (_jsxs("div", { className: "empty-detail", children: [_jsx("span", { className: "empty-symbol", children: "\u21B3" }), _jsx("h2", { children: "Follow a Jev request" }), _jsxs("p", { children: ["Context, parallel questions, typed answers.", _jsx("br", {}), "Run a sample above to see the complete flow."] })] })) }));
50
+ }, children: name }, name))) }), _jsx("div", { className: `detail-body ${tab === "Flow" ? "detail-body-flow" : tab === "Questions" ? "detail-body-questions" : ""}`, children: _jsxs(Suspense, { fallback: _jsx("p", { className: "muted", role: "status", children: "Loading questions\u2026" }), children: [trace.error && (_jsxs("div", { role: "alert", className: "error-banner", children: [_jsx("strong", { children: trace.error.name }), " ", trace.error.message] })), trace.captureWarnings.map((w) => (_jsx("p", { className: "warning", children: w }, w))), tab === "Flow" && _jsx(TraceDiagram, { trace: trace }, trace.id), tab === "Questions" && (_jsx(Questions, { trace: trace }, trace.id)), tab === "Context" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "muted", children: "Captured before dispatch without changing SDK data. Known credential fields remain redacted; additional display masking follows the component prop. Captures are size limited." }), _jsx(CaptureFields, { trace: trace, path: ["request", "state"] }), !captureFieldUnavailable(trace, ["request", "state"]) && (_jsxs(_Fragment, { children: [findImage(trace.request.state) && (_jsx("img", { className: "context-image", src: findImage(trace.request.state), alt: "Captured context" })), _jsx(JsonBlock, { value: trace.request.state })] })), trace.request.assets && (_jsx(JsonBlock, { value: trace.request.assets }))] })), tab === "Transport" && (_jsxs(_Fragment, { children: [_jsx("h3", { children: "HTTP attempts" }), _jsx("p", { className: "muted", children: "Attempt duration ends at response headers. Total duration includes SDK processing and retry waits." }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Attempt" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Duration" })] }) }), _jsx("tbody", { children: trace.attempts.map((a) => (_jsxs("tr", { children: [_jsx("td", { children: a.number }), _jsx("td", { children: a.error || a.status || "Pending" }), _jsx("td", { children: ms(a.durationMs) })] }, a.number))) })] }), !trace.attempts.length && _jsx("p", { children: "No HTTP attempt recorded." })] })), tab === "Raw" && _jsx(JsonBlock, { value: trace })] }) })] })) : (_jsxs("div", { className: "empty-detail", children: [_jsx("span", { className: "empty-symbol", children: "\u21B3" }), _jsx("h2", { children: "Follow a Jev request" }), _jsxs("p", { children: ["Context, parallel questions, typed answers.", _jsx("br", {}), "Run a sample above to see the complete flow."] })] })) }));
37
51
  });
38
- export function JevPanel({ traces, flows = emptyFlows, connection, onClear, }) {
52
+ export function JevPanel({ traces: incomingTraces, flows = emptyFlows, connection, onClear, }) {
53
+ const traces = useMemo(() => incomingTraces.map(traceCaptureView), [incomingTraces]);
39
54
  const [view, setView] = useState("requests");
40
55
  const [selectedRun, setSelectedRun] = useState();
41
56
  const run = flows.runs.find((r) => r.id === selectedRun) ?? flows.runs[0];
@@ -1,14 +1,17 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CaptureFields, PartialBox } from "./CaptureFields.js";
3
+ import { captureFieldUnavailable, captureQuestionEntries, capturedValue, traceCaptureView, } from "./capture-view.js";
2
4
  import { memo, useCallback, useEffect, useLayoutEffect, useId, useMemo, useRef, useState, } from "react";
3
5
  import { defaultRangeExtractor, measureElement, observeElementRect, useVirtualizer, } from "@tanstack/react-virtual";
4
6
  import { Answer, findImage, JsonBlock } from "./TraceContent.js";
5
- const QuestionContent = memo(function QuestionContent({ name, question, answer, image, legendOpen, onLegendChange, }) {
6
- return (_jsxs(_Fragment, { children: [_jsxs("header", { children: [_jsx("h3", { children: name }), _jsx("span", { className: "type", children: question.type })] }), _jsx(JsonBlock, { value: question }), _jsx(Answer, { answer: answer, image: image, legendOpen: legendOpen, onLegendToggle: (open) => onLegendChange(name, open) })] }));
7
+ const QuestionContent = memo(function QuestionContent({ trace, name, question, answer, image, legendOpen, onLegendChange, }) {
8
+ return (_jsxs(_Fragment, { children: [_jsxs("header", { children: [_jsx("h3", { children: name }), _jsx("span", { className: "type", children: question.type })] }), _jsx(CaptureFields, { trace: trace, path: ["request", "questions", name] }), _jsx(JsonBlock, { value: capturedValue(trace, ["request", "questions", name]) }), _jsx(CaptureFields, { trace: trace, path: ["result", "answers", name] }), _jsx(PartialBox, { trace: trace, name: name }), !captureFieldUnavailable(trace, ["result", "answers", name]) && (_jsx(Answer, { answer: answer, image: image, legendOpen: legendOpen, onLegendToggle: (open) => onLegendChange(name, open) }))] }));
7
9
  });
8
10
  /** Variable-height rows retain full answers; only offscreen DOM is omitted. */
9
11
  export function Questions({ trace }) {
12
+ trace = traceCaptureView(trace);
10
13
  const helpId = useId();
11
- const entries = useMemo(() => Object.entries(trace.request.questions ?? {}).filter(([, q]) => q && typeof q === "object" && typeof q.type === "string"), [trace.request.questions]);
14
+ const entries = useMemo(() => captureQuestionEntries(trace), [trace]);
12
15
  const image = useMemo(() => findImage(trace.request.state), [trace.request.state]);
13
16
  const viewport = useRef(null);
14
17
  const sizes = useRef(new Map());
@@ -96,7 +99,9 @@ export function Questions({ trace }) {
96
99
  // Scroll-range updates must not rebuild the full question picker.
97
100
  const navigation = useMemo(() => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "questions-navigation", children: [_jsxs("label", { children: ["Jump to question", _jsx("select", { "aria-label": "Jump to question", value: Math.min(destination, entries.length - 1), onChange: (event) => goTo(Number(event.target.value)), children: entries.map(([name, q], index) => (_jsxs("option", { value: index, children: [index + 1, ". ", name, " \u00B7 ", q.type] }, name))) })] }), _jsx("button", { "aria-label": "Previous question", disabled: destination === 0, onClick: () => goTo(destination - 1), children: "Previous" }), _jsx("button", { "aria-label": "Next question", disabled: destination >= entries.length - 1, onClick: () => goTo(destination + 1), children: "Next" }), _jsxs("span", { children: [entries.length, " question", entries.length === 1 ? "" : "s"] })] }), _jsx("p", { className: "questions-help muted", id: helpId, children: "\u2191 / \u2193 previous / next \u00B7 Home / End first / last when the list is focused." })] })), [entries, destination, goTo, helpId]);
98
101
  if (!entries.length)
99
- return _jsx("p", { className: "muted", children: "No questions available in this capture." });
102
+ return captureFieldUnavailable(trace, ["request", "questions"]) ? (_jsx(CaptureFields, { trace: trace, path: captureFieldUnavailable(trace, ["request"])
103
+ ? ["request"]
104
+ : ["request", "questions"] })) : (_jsx("p", { className: "muted", children: "No questions available in this capture." }));
100
105
  return (_jsxs("div", { className: "questions-list", children: [navigation, _jsx("div", { className: "questions-viewport", ref: viewport, tabIndex: 0, role: "region", "aria-label": "Questions and answers", "aria-describedby": helpId, onScroll: (event) => {
101
106
  const offset = event.currentTarget.scrollTop;
102
107
  const time = performance.now();
@@ -169,6 +174,6 @@ export function Questions({ trace }) {
169
174
  const preview = seeking && focused !== item.index;
170
175
  return (_jsx("div", { "data-index": item.index, "data-question-key": name, "data-preview": preview || undefined, style: preview
171
176
  ? { height: sizes.current.get(name) || 600 }
172
- : undefined, ref: virtualizer.measureElement, className: "questions-row", role: "listitem", "aria-posinset": item.index + 1, "aria-setsize": entries.length, children: _jsx("article", { className: "question-node standalone", tabIndex: -1, "aria-busy": preview || undefined, "aria-label": `Question ${item.index + 1}: ${name}`, children: preview ? (_jsxs("div", { className: "question-scroll-preview", children: [_jsxs("header", { children: [_jsx("h3", { children: name }), _jsx("span", { className: "type", children: question.type })] }), _jsx("p", { className: "muted", children: "Pause scrolling to inspect this answer." })] })) : (_jsx(QuestionContent, { name: name, question: question, answer: trace.result?.answers[name], image: image, legendOpen: !!legends.get(name), onLegendChange: onLegendChange })) }) }, item.key));
177
+ : undefined, ref: virtualizer.measureElement, className: "questions-row", role: "listitem", "aria-posinset": item.index + 1, "aria-setsize": entries.length, children: _jsx("article", { className: "question-node standalone", tabIndex: -1, "aria-busy": preview || undefined, "aria-label": `Question ${item.index + 1}: ${name}`, children: preview ? (_jsxs("div", { className: "question-scroll-preview", children: [_jsxs("header", { children: [_jsx("h3", { children: name }), _jsx("span", { className: "type", children: question.type })] }), _jsx("p", { className: "muted", children: "Pause scrolling to inspect this answer." })] })) : (_jsx(QuestionContent, { trace: trace, name: name, question: question, answer: trace.result?.answers[name], image: image, legendOpen: !!legends.get(name), onLegendChange: onLegendChange })) }) }, item.key));
173
178
  }) }) })] }));
174
179
  }