agentfootprint-lens 0.49.0 → 0.51.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 (46) hide show
  1. package/README.md +138 -7
  2. package/dist/{Lens-_hExvOXu.d.ts → Lens-Bw1em67x.d.cts} +103 -25
  3. package/dist/{Lens-_IjGLEGt.d.cts → Lens-CDsSUiHJ.d.ts} +103 -25
  4. package/dist/{chunk-KS4AIWQJ.js → chunk-7QSK7EJW.js} +152 -86
  5. package/dist/chunk-7QSK7EJW.js.map +1 -0
  6. package/dist/{chunk-AUGRQKRL.js → chunk-BJR32J6H.js} +2 -2
  7. package/dist/{chunk-K4CTXAB7.js → chunk-H63KQNY3.js} +2 -2
  8. package/dist/{chunk-2UOQEE4U.js → chunk-MS5XW7GH.js} +23 -13
  9. package/dist/{chunk-2UOQEE4U.js.map → chunk-MS5XW7GH.js.map} +1 -1
  10. package/dist/{chunk-LCIEJYZG.js → chunk-OXPEMAMG.js} +29 -2
  11. package/dist/chunk-OXPEMAMG.js.map +1 -0
  12. package/dist/{chunk-AIZIILHJ.js → chunk-ZZJ2OIQP.js} +2 -2
  13. package/dist/core.cjs +28 -0
  14. package/dist/core.cjs.map +1 -1
  15. package/dist/core.d.cts +5 -5
  16. package/dist/core.d.ts +5 -5
  17. package/dist/core.js +5 -3
  18. package/dist/index.cjs +149 -46
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +34 -16
  21. package/dist/index.d.ts +34 -16
  22. package/dist/index.js +8 -6
  23. package/dist/index.js.map +1 -1
  24. package/dist/{resolveNavigation-Ckr2nXHY.d.ts → lensCursor-B8HFFZgR.d.cts} +162 -1
  25. package/dist/{resolveNavigation-Ckr2nXHY.d.cts → lensCursor-B8HFFZgR.d.ts} +162 -1
  26. package/dist/{selectSkillFrameContext-DEk_qpXr.d.ts → selectSkillFrameContext-BE7lCVcB.d.cts} +1 -1
  27. package/dist/{selectSkillFrameContext-DjZDveZw.d.cts → selectSkillFrameContext-D2uHzhJJ.d.ts} +1 -1
  28. package/dist/skillgraph.cjs +48 -10
  29. package/dist/skillgraph.cjs.map +1 -1
  30. package/dist/skillgraph.d.cts +45 -10
  31. package/dist/skillgraph.d.ts +45 -10
  32. package/dist/skillgraph.js +5 -3
  33. package/dist/{tagAxis-C__DMHiD.d.ts → tagAxis-BUrsQ1fX.d.cts} +1 -1
  34. package/dist/{tagAxis-B598GxMJ.d.cts → tagAxis-Cqom3DPf.d.ts} +1 -1
  35. package/dist/why.cjs +221 -128
  36. package/dist/why.cjs.map +1 -1
  37. package/dist/why.d.cts +5 -5
  38. package/dist/why.d.ts +5 -5
  39. package/dist/why.js +6 -4
  40. package/dist/why.js.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/chunk-KS4AIWQJ.js.map +0 -1
  43. package/dist/chunk-LCIEJYZG.js.map +0 -1
  44. /package/dist/{chunk-AUGRQKRL.js.map → chunk-BJR32J6H.js.map} +0 -0
  45. /package/dist/{chunk-K4CTXAB7.js.map → chunk-H63KQNY3.js.map} +0 -0
  46. /package/dist/{chunk-AIZIILHJ.js.map → chunk-ZZJ2OIQP.js.map} +0 -0
package/README.md CHANGED
@@ -940,7 +940,9 @@ not answer, since it takes an exact stop.
940
940
  ## The Served tab
941
941
 
942
942
  **At every LLM call, exactly what the model was served — provable from the log.**
943
- The Why Lens's right rail has a second tab, **Served**, mounted for every run.
943
+ The Why Lens's right rail has a second tab, **Served**, mounted for every run —
944
+ including for a host that supplies `slots.detail`, whose own pane is the tab
945
+ beside it (0.50.0).
944
946
  Stand on an LLM turn and it shows the request that call went out with: the system
945
947
  prompt (piece by piece), the messages as sent, the tools as sent, the dials, the
946
948
  cache breakpoints — and beside every field, what the record can PROVE about it.
@@ -1108,7 +1110,8 @@ likely to rot — and the question above is answered without it.
1108
1110
  ## Bookmarks, and scrubbing by declared tag
1109
1111
 
1110
1112
  **The reader's marks, kept beside the recording — and the author's tags, as a
1111
- ruler.** Since 0.48.0 the Why Lens's right rail has a third tab, **Bookmarks**,
1113
+ ruler.** Since 0.48.0 the Why Lens's right rail has a third tab, **Bookmarks**
1114
+ (a fourth when a host fills `slots.detail`),
1112
1115
  and the grouped ruler carries a **Tags** strip.
1113
1116
 
1114
1117
  ### Why
@@ -1222,9 +1225,17 @@ unavailable*, and nothing else changes. The peer ranges are untouched.
1222
1225
 
1223
1226
  ## Rendering your own detail pane
1224
1227
 
1225
- `slots.detail` replaces the CONTENT of the shipped right column. The column
1226
- itself — its width, its border, its collapse pill, its cursor — is unchanged.
1227
- Omit `slots` and the built-in timeline renders exactly as before.
1228
+ **A slot fills a PANE, never the CHROME around it.** The right rail's tab strip,
1229
+ the one cursor and the collapse pill belong to the library, because chrome is how
1230
+ the library adds capability over time — a consumer that fills a slot must still
1231
+ receive every reading the rail gains later. That is why `slots.detail` is ONE TAB
1232
+ and not a takeover, and why `detailOnly` is an opt-out rather than the default.
1233
+
1234
+ `slots.detail` renders the right column's FIRST tab. The column itself — its
1235
+ width, its border, its collapse pill, its cursor — is unchanged, and so is the
1236
+ strip: your pane is selected by default, with *What happened*, *Served* and
1237
+ *Bookmarks* one click away beside it. Omit `slots` and the built-in timeline
1238
+ renders exactly as before.
1228
1239
 
1229
1240
  ```tsx
1230
1241
  const Detail: React.FC<LensDetailSlotProps> = ({ step, cursorRuntimeStageId, node, onNavigate }) => (
@@ -1235,7 +1246,8 @@ const Detail: React.FC<LensDetailSlotProps> = ({ step, cursorRuntimeStageId, nod
1235
1246
  </div>
1236
1247
  );
1237
1248
 
1238
- <Lens recorder={recorder} runner={runner} slots={{ detail: Detail }} />
1249
+ <Lens recorder={recorder} runner={runner} slots={{ detail: Detail, detailLabel: 'My bands' }} />
1250
+ // rail: [ My bands* | What happened | Served | Bookmarks ]
1239
1251
  ```
1240
1252
 
1241
1253
  The slot receives the cursor in every unit (`step`, `totalSteps`,
@@ -1244,11 +1256,130 @@ and the ones that ran inside its scope (`node`, `relatedNodes`), the `recorder`
1244
1256
  for anything else, and `onNavigate` — the same funnel every built-in mover uses,
1245
1257
  so your pane moves the ONE cursor rather than starting a second one.
1246
1258
 
1259
+ | key | what it does |
1260
+ |---|---|
1261
+ | `detail` | your pane, as the rail's first tab, selected by default |
1262
+ | `detailLabel` | what that tab is CALLED. Default `"Details"` — the library's generic word for someone else's pane. Name it and the strip reads in your reader's words. |
1263
+ | `detailOnly` | opt OUT of the strip: your pane takes the whole rail, as it did before 0.50.0. For a full-height custom layout that would read as a second chrome under the library's strip. The cost: nothing the rail gains later can reach you. |
1264
+
1265
+ **Switching tabs moves nothing and remounts nothing.** The cursor stays where it
1266
+ was, and your pane is hidden rather than unmounted — a reader who glances at
1267
+ *Served* and comes back finds their bands open and their scroll where they left
1268
+ it.
1269
+
1270
+ **The worked example (0.50.0).** An app supplies `detail` + `detailLabel` and
1271
+ changes nothing else: it sees its own pane first and selected, exactly as before,
1272
+ and it GAINS Served, the Served graph and Bookmarks on the version bump. Before
1273
+ 0.50.0 that same app lost the entire rail — the tab strip included — and could
1274
+ not reach any of them.
1275
+
1247
1276
  Keep the slots object stable across renders (module scope or `useMemo`), same as
1248
1277
  `<TraceExplorerShell slots>`.
1249
1278
 
1250
1279
  ---
1251
1280
 
1281
+ ## One address, one cursor — for every view
1282
+
1283
+ > **A stage id is an ADDRESS, not a POSITION.** It says WHICH stage, never WHERE
1284
+ > on an axis. Only an axis can answer that, and it may honestly answer *"not
1285
+ > here"*.
1286
+
1287
+ Until 0.51.0 the same one cursor reached three views in three vocabularies, and
1288
+ every new view invented a fourth:
1289
+
1290
+ | view | handed | reads |
1291
+ |---|---|---|
1292
+ | skill graph | `cursorRuntimeStageId` + `onJumpTo` | the EVENT record |
1293
+ | a consumer's data graph | `{ step, total, stepOf(id), onStep }` | the COMMIT record |
1294
+ | Served tab / graph | `cursorRuntimeStageId` + `commitIdx` | the COMMIT record |
1295
+
1296
+ `LensCursor` is their superset, and every view now gets exactly it. It is three
1297
+ things and deliberately nothing more:
1298
+
1299
+ ```ts
1300
+ interface LensCursor {
1301
+ readonly at: LensCursorReading; // step · totalSteps · runtimeStageId · commitIdx · label · kind
1302
+ readonly total: number;
1303
+ resolve(runtimeStageId: string): NavigationResult; // an ADDRESS, answered honestly
1304
+ moveTo(step: number): void; // the ONE funnel
1305
+ }
1306
+ ```
1307
+
1308
+ **It holds no position of its own**, and no view may gain one: `at` is derived
1309
+ from the axis and the step on every render, `resolve` is a pure query, and
1310
+ `moveTo` is the same funnel the step strip, the ◀ ▶ buttons and the live
1311
+ auto-advance go through. Read it; never store it.
1312
+
1313
+ ### Adopting it — one line
1314
+
1315
+ ```tsx
1316
+ <Lens
1317
+ recorder={recorder}
1318
+ slots={{ detail: (p) => <SkillGraphDebugger recorder={p.recorder} cursor={p.cursor} /> }}
1319
+ />
1320
+ ```
1321
+
1322
+ `cursor` arrives on `LensDetailSlotProps` beside every prop that was already
1323
+ there, and `<SkillGraphDebugger cursor>` / `<ServedTab cursor>` are the narrow
1324
+ form of the props they already took. Nothing was removed; an explicit prop still
1325
+ wins wherever both are passed.
1326
+
1327
+ ### Placing an element by address
1328
+
1329
+ This is the behaviour the packet generalises. A real consumer's data-graph view
1330
+ took `stepOf(id)` and drew an element `unplaced` when the answer came back below
1331
+ zero — a number every view had to invent a meaning for. Written against
1332
+ `resolve`, the meaning *and the words* come from the library:
1333
+
1334
+ ```tsx
1335
+ function Element({ cursor, runtimeStageId, name }) {
1336
+ const to = cursor.resolve(runtimeStageId);
1337
+ if (!to.ok) {
1338
+ // Drawn, never hidden — omit never deny — and the sentence is the library's.
1339
+ return <li data-unplaced title={to.message}>{name}</li>;
1340
+ }
1341
+ return (
1342
+ <li data-enclosing={to.match === 'enclosing'} onClick={() => cursor.moveTo(to.step)}>
1343
+ {to.label}
1344
+ </li>
1345
+ );
1346
+ }
1347
+ ```
1348
+
1349
+ ### The three cases the contract names
1350
+
1351
+ Each is a real run, and each has a test measured on a frozen fixture:
1352
+
1353
+ 1. **The axis does not stop there.** A filtered or tag axis over an untagged
1354
+ stage. `resolve` refuses `'not-on-axis'`, carries a printable `message`, and
1355
+ OFFERS the nearest earlier stop in `nearest` — an offer, never a move.
1356
+ 2. **The id belongs to an inner log.** A subflow's stages commit into their own
1357
+ isolated log, so the axis holds the MOUNT rather than the stage. The ladder's
1358
+ `'enclosing'` rung answers `{ ok: true, match: 'enclosing' }` whose
1359
+ `runtimeStageId` is the mount's — a true landing at the granularity this axis
1360
+ has. Branch on `match` when you must tell "on it" from "inside it", and offer
1361
+ a drill to go deeper.
1362
+ 3. **The event has no stage at all.** Run start, run end. There is no id;
1363
+ `resolve('')` refuses `'no-id'` and your view says so rather than inventing
1364
+ an address.
1365
+
1366
+ ### Headless
1367
+
1368
+ ```ts
1369
+ import { scrubAxisFor, lensCursorFrom } from 'agentfootprint-lens/core';
1370
+
1371
+ const positions = scrubAxisFor(recorder, 'step');
1372
+ const cursor = lensCursorFrom(positions, 0, (step) => setStep(step));
1373
+ const to = cursor.resolve('llm#7');
1374
+ const href = to.ok ? `/run/${runId}?step=${to.step}` : undefined;
1375
+ ```
1376
+
1377
+ `stepForRuntimeStageId` still ships and still behaves byte for byte; it is
1378
+ **deprecated** in favour of `resolveNavigation` / `cursor.resolve`, the same
1379
+ ladder with its rungs named instead of flattened to `-1`.
1380
+
1381
+ ---
1382
+
1252
1383
  ## Theming
1253
1384
 
1254
1385
  **Lens inherits theme tokens from your app via CSS variables.** Set `--fp-*`
@@ -1377,7 +1508,7 @@ graph in one call. Returns an unsubscribe. Call it once per run.
1377
1508
  | `step` | `number?` | Controlled cursor. **Omit it and the lens is self-driving, exactly as before.** Pass it and you own the position; out-of-range values are clamped and reported. See [Driving the cursor](#driving-the-cursor-from-your-app). |
1378
1509
  | `onStepChange` | `(step, at) => void?` | Fires on every cursor move — required for movement in controlled mode, an observation hook otherwise. `at` carries `runtimeStageId`, `commitIdx`, `label`, `kind` and `clamped`. |
1379
1510
  | `navigatorRef` | `Ref<LensNavigator>?` | Move the cursor to a stage **by its `runtimeStageId`**. `ref.current.navigateTo(id)` returns `{ ok: true, step, match, label }` or `{ ok: false, reason, message, nearest? }` — a miss never moves. See [Pointing at a step](#pointing-at-a-step-navigatorref). |
1380
- | `slots` | `LensSlots?` | Slot overrides. `slots.detail` renders your content in the shipped right column. Omit for the built-in timeline. See [Rendering your own detail pane](#rendering-your-own-detail-pane). |
1511
+ | `slots` | `LensSlots?` | Slot overrides. `slots.detail` renders your content as the right rail's FIRST TAB (`slots.detailLabel` names it; `slots.detailOnly` takes the whole rail instead). The strip stays the library's. Omit for the built-in timeline. See [Rendering your own detail pane](#rendering-your-own-detail-pane). |
1381
1512
 
1382
1513
  ### `<LensFlow>` — the chart canvas on its own
1383
1514
 
@@ -3,8 +3,8 @@ import { ToolChoiceCall, ToolChoiceSummary } from 'agentfootprint/observe';
3
3
  import * as agentfootprint from 'agentfootprint';
4
4
  import { CommentaryTemplates } from 'agentfootprint';
5
5
  import React__default, { Ref } from 'react';
6
- import { d as NavigationResult, C as CursorPosition, L as LensRecorder } from './resolveNavigation-Ckr2nXHY.js';
7
- import { C as ChartGroupHighlight, c as LensCursorPort, H as Humanizer, b as BookmarkStore } from './tagAxis-C__DMHiD.js';
6
+ import { a as LensCursorReading, C as CursorPosition, L as LensCursor, e as NavigationResult, h as LensRecorder } from './lensCursor-B8HFFZgR.cjs';
7
+ import { C as ChartGroupHighlight, c as LensCursorPort, H as Humanizer, b as BookmarkStore } from './tagAxis-BUrsQ1fX.cjs';
8
8
  import { NodeTypes } from '@xyflow/react';
9
9
  import { TraceGraph, TraceFlowLayout, RuntimeOverlay } from 'footprint-explainable-ui/flowchart';
10
10
 
@@ -158,26 +158,12 @@ declare const LensFlow: React__default.FC<LensFlowProps>;
158
158
  * Where the cursor landed, in all three units the lens knows — handed to
159
159
  * `onStepChange` alongside the step so a host never has to reverse-engineer
160
160
  * the lens's axis.
161
+ *
162
+ * It is a `LensCursorReading` (`/core`) plus the one thing a MOVE REPORT needs
163
+ * and a reading does not: `clamped`. One declaration of the reading's fields,
164
+ * so the report and the `LensCursor` every view is handed cannot drift.
161
165
  */
162
- interface LensCursorAt {
163
- /** The new cursor position, on the lens's step axis. Same number the
164
- * `step` prop takes. */
165
- readonly step: number;
166
- /** How many positions the axis has right now. Valid steps are
167
- * `0 … totalSteps - 1`. GROWS during a live run. */
168
- readonly totalSteps: number;
169
- /** The cursor's address in footprintjs's address space
170
- * (`[subflowPath/]stageId#executionIndex`) — the same string
171
- * `<TraceExplorerShell>` and `<RunSlider>` call `selectedRuntimeStageId` /
172
- * `cursorRuntimeStageId`. `''` when the axis is empty. */
173
- readonly runtimeStageId: string;
174
- /** The commit-log index this position anchors to. `-1` when unknown. */
175
- readonly commitIdx: number;
176
- /** The position's human label, as the step strip and the timeline spell it
177
- * ("Iteration 2", "Context 3", "Run · start"). */
178
- readonly label: string;
179
- /** What kind of stop this is. Absent when the axis is empty. */
180
- readonly kind?: string;
166
+ interface LensCursorAt extends LensCursorReading {
181
167
  /**
182
168
  * `true` when this call is Lens CORRECTING a step that is not a position on
183
169
  * the axis it is now read against, not a move someone made. That is an
@@ -218,6 +204,18 @@ interface UseLensCursorArgs {
218
204
  * owns before every question and remembers nothing between them.
219
205
  */
220
206
  readonly port?: LensCursorPort | undefined;
207
+ /**
208
+ * The ACTIVE scrub axis — the same list the lens draws its ruler from.
209
+ *
210
+ * Supplied only so this hook can also hand back the `LensCursor` every view
211
+ * receives (`cursor` below), which needs the axis to answer an ADDRESS. It
212
+ * is never consulted for movement: `maxStep`, `describe` and `port` decide
213
+ * that, exactly as they did before 0.51.0.
214
+ *
215
+ * Omit it and `cursor` is a cursor over an EMPTY axis — `resolve` refuses
216
+ * `'empty-axis'`, which is the honest answer for a caller that gave none.
217
+ */
218
+ readonly positions?: readonly CursorPosition[] | undefined;
221
219
  }
222
220
  interface UseLensCursorResult {
223
221
  /** The cursor to render. */
@@ -226,13 +224,22 @@ interface UseLensCursorResult {
226
224
  readonly isLive: boolean;
227
225
  /** The ONE way anything inside the lens moves the cursor. */
228
226
  readonly moveTo: (n: number) => void;
227
+ /**
228
+ * The same one cursor in the shape EVERY view is handed (0.51.0): a READING
229
+ * (`at`), the honest address query (`resolve`) and this very `moveTo`.
230
+ *
231
+ * Added BESIDE the three fields above — nothing moved, nothing was removed.
232
+ * It holds no position: it is rebuilt from (`positions`, `step`) on every
233
+ * render, so a view cannot end up rendering a stale copy.
234
+ */
235
+ readonly cursor: LensCursor;
229
236
  }
230
237
  /**
231
238
  * Snap a host-supplied step onto the axis. Non-finite and fractional values
232
239
  * are not positions, so they clamp too — and the clamp is always reported.
233
240
  */
234
241
  declare function clampStep(n: number, maxStep: number): number;
235
- declare function useLensCursor({ controlledStep, onStepChange, maxStep, describe, port, }: UseLensCursorArgs): UseLensCursorResult;
242
+ declare function useLensCursor({ controlledStep, onStepChange, maxStep, describe, port, positions, }: UseLensCursorArgs): UseLensCursorResult;
236
243
 
237
244
  /**
238
245
  * useLensNavigator — move the ONE cursor to a stage by its address.
@@ -634,16 +641,67 @@ interface LensProps {
634
641
  *
635
642
  * Same idea as `<TraceExplorerShell slots>`: keep the shipped layout and the
636
643
  * shipped cursor, replace what one pane renders.
644
+ *
645
+ * THE LAW: **a slot fills a PANE, never the CHROME around it.** The rail's tab
646
+ * strip, the one cursor and the collapse pill belong to the library, because
647
+ * chrome is how the library adds capability over time — a consumer that fills a
648
+ * slot must still receive every capability the rail gains later. That is why
649
+ * `detail` is ONE TAB and not a takeover, and why `detailOnly` is an opt-out
650
+ * rather than the default.
637
651
  */
638
652
  interface LensSlots {
639
653
  /**
640
- * Override the RIGHT column — the "what happened" timeline. Your component
641
- * renders inside the shipped column (its width, borders, collapse pill and
642
- * scroll behaviour are unchanged); only the content is yours.
654
+ * Render the right column's FIRST TAB — your own pane, beside the library's.
655
+ *
656
+ * Your component renders inside the shipped column (its width, borders,
657
+ * collapse pill and scroll behaviour are unchanged) and inside the shipped
658
+ * rail: the tab strip stays, your pane becomes the first tab and is selected
659
+ * by default, and "What happened", "Served" and "Bookmarks" stay reachable
660
+ * beside it. Switching tabs never moves the cursor and never unmounts your
661
+ * pane — it keeps its own state.
662
+ *
663
+ * Before 0.50.0 a supplied slot replaced the whole rail, tab strip included,
664
+ * so a slot consumer could not reach anything the rail gained afterwards.
665
+ * That was the defect this shape fixes; `detailOnly` is the way back.
643
666
  *
644
667
  * Omit it and the built-in timeline renders exactly as before.
668
+ *
669
+ * ```tsx
670
+ * <Lens recorder={recorder} slots={{ detail: MyBands, detailLabel: 'Bands' }} />
671
+ * // rail: [ Bands* | What happened | Served | Bookmarks ]
672
+ * ```
645
673
  */
646
674
  readonly detail?: React__default.ComponentType<LensDetailSlotProps>;
675
+ /**
676
+ * What the `detail` tab is CALLED. Default: `"Details"`.
677
+ *
678
+ * WHY it exists: the tab strip names each reading in the reader's own words,
679
+ * and "Details" is the library's generic word for someone else's pane. An app
680
+ * whose pane is a stack of SEO bands should say so — the strip is read left to
681
+ * right and an unnamed tab makes the app's own reading the anonymous one.
682
+ *
683
+ * A plain string, rendered as the tab's text; it is the accessible name too.
684
+ * Ignored when no `detail` slot is supplied.
685
+ *
686
+ * ```tsx
687
+ * slots={{ detail: MyBands, detailLabel: 'SEO bands' }}
688
+ * ```
689
+ */
690
+ readonly detailLabel?: string;
691
+ /**
692
+ * OPT OUT of the tab strip: the `detail` slot takes the whole rail, as it did
693
+ * before 0.50.0. Default `false`.
694
+ *
695
+ * For the app with a full-height custom layout of its own — a pane that owns
696
+ * its scroll and its header and would read as a second chrome under the
697
+ * library's strip. The cost is stated plainly: nothing the rail gains later
698
+ * (Served, Bookmarks, whatever follows) can reach that consumer, and neither
699
+ * can the library's own timeline. Prefer the tabs; take this only when the
700
+ * layout genuinely demands it.
701
+ *
702
+ * Ignored when no `detail` slot is supplied.
703
+ */
704
+ readonly detailOnly?: boolean;
647
705
  }
648
706
  /**
649
707
  * What the `detail` slot receives — the cursor, in the units a detail pane
@@ -670,6 +728,26 @@ interface LensDetailSlotProps {
670
728
  readonly recorder: LensRecorder;
671
729
  /** Move the ONE cursor — same funnel every built-in mover uses. */
672
730
  readonly onNavigate: (step: number) => void;
731
+ /**
732
+ * THE CURSOR, in the one vocabulary every view now shares (0.51.0) — the
733
+ * reading above (`cursor.at`), the axis's honest answer to an ADDRESS
734
+ * (`cursor.resolve(id)`), and the same funnel as `onNavigate`
735
+ * (`cursor.moveTo`).
736
+ *
737
+ * Everything else on this interface stays exactly as it was; this is the
738
+ * shape to reach for when your pane places elements BY ADDRESS, because the
739
+ * axis may honestly not stop where an address points:
740
+ *
741
+ * ```tsx
742
+ * const to = p.cursor.resolve(node.runtimeStageId);
743
+ * if (!to.ok) return <li data-unplaced title={to.message}>{node.name}</li>;
744
+ * return <li onClick={() => p.cursor.moveTo(to.step)}>{to.label}</li>;
745
+ * ```
746
+ *
747
+ * It holds no position of its own — it is rebuilt every render. Read it;
748
+ * never store it.
749
+ */
750
+ readonly cursor: LensCursor;
673
751
  }
674
752
  declare const Lens: React__default.FC<LensProps>;
675
753
 
@@ -3,8 +3,8 @@ import { ToolChoiceCall, ToolChoiceSummary } from 'agentfootprint/observe';
3
3
  import * as agentfootprint from 'agentfootprint';
4
4
  import { CommentaryTemplates } from 'agentfootprint';
5
5
  import React__default, { Ref } from 'react';
6
- import { d as NavigationResult, C as CursorPosition, L as LensRecorder } from './resolveNavigation-Ckr2nXHY.cjs';
7
- import { C as ChartGroupHighlight, c as LensCursorPort, H as Humanizer, b as BookmarkStore } from './tagAxis-B598GxMJ.cjs';
6
+ import { a as LensCursorReading, C as CursorPosition, L as LensCursor, e as NavigationResult, h as LensRecorder } from './lensCursor-B8HFFZgR.js';
7
+ import { C as ChartGroupHighlight, c as LensCursorPort, H as Humanizer, b as BookmarkStore } from './tagAxis-Cqom3DPf.js';
8
8
  import { NodeTypes } from '@xyflow/react';
9
9
  import { TraceGraph, TraceFlowLayout, RuntimeOverlay } from 'footprint-explainable-ui/flowchart';
10
10
 
@@ -158,26 +158,12 @@ declare const LensFlow: React__default.FC<LensFlowProps>;
158
158
  * Where the cursor landed, in all three units the lens knows — handed to
159
159
  * `onStepChange` alongside the step so a host never has to reverse-engineer
160
160
  * the lens's axis.
161
+ *
162
+ * It is a `LensCursorReading` (`/core`) plus the one thing a MOVE REPORT needs
163
+ * and a reading does not: `clamped`. One declaration of the reading's fields,
164
+ * so the report and the `LensCursor` every view is handed cannot drift.
161
165
  */
162
- interface LensCursorAt {
163
- /** The new cursor position, on the lens's step axis. Same number the
164
- * `step` prop takes. */
165
- readonly step: number;
166
- /** How many positions the axis has right now. Valid steps are
167
- * `0 … totalSteps - 1`. GROWS during a live run. */
168
- readonly totalSteps: number;
169
- /** The cursor's address in footprintjs's address space
170
- * (`[subflowPath/]stageId#executionIndex`) — the same string
171
- * `<TraceExplorerShell>` and `<RunSlider>` call `selectedRuntimeStageId` /
172
- * `cursorRuntimeStageId`. `''` when the axis is empty. */
173
- readonly runtimeStageId: string;
174
- /** The commit-log index this position anchors to. `-1` when unknown. */
175
- readonly commitIdx: number;
176
- /** The position's human label, as the step strip and the timeline spell it
177
- * ("Iteration 2", "Context 3", "Run · start"). */
178
- readonly label: string;
179
- /** What kind of stop this is. Absent when the axis is empty. */
180
- readonly kind?: string;
166
+ interface LensCursorAt extends LensCursorReading {
181
167
  /**
182
168
  * `true` when this call is Lens CORRECTING a step that is not a position on
183
169
  * the axis it is now read against, not a move someone made. That is an
@@ -218,6 +204,18 @@ interface UseLensCursorArgs {
218
204
  * owns before every question and remembers nothing between them.
219
205
  */
220
206
  readonly port?: LensCursorPort | undefined;
207
+ /**
208
+ * The ACTIVE scrub axis — the same list the lens draws its ruler from.
209
+ *
210
+ * Supplied only so this hook can also hand back the `LensCursor` every view
211
+ * receives (`cursor` below), which needs the axis to answer an ADDRESS. It
212
+ * is never consulted for movement: `maxStep`, `describe` and `port` decide
213
+ * that, exactly as they did before 0.51.0.
214
+ *
215
+ * Omit it and `cursor` is a cursor over an EMPTY axis — `resolve` refuses
216
+ * `'empty-axis'`, which is the honest answer for a caller that gave none.
217
+ */
218
+ readonly positions?: readonly CursorPosition[] | undefined;
221
219
  }
222
220
  interface UseLensCursorResult {
223
221
  /** The cursor to render. */
@@ -226,13 +224,22 @@ interface UseLensCursorResult {
226
224
  readonly isLive: boolean;
227
225
  /** The ONE way anything inside the lens moves the cursor. */
228
226
  readonly moveTo: (n: number) => void;
227
+ /**
228
+ * The same one cursor in the shape EVERY view is handed (0.51.0): a READING
229
+ * (`at`), the honest address query (`resolve`) and this very `moveTo`.
230
+ *
231
+ * Added BESIDE the three fields above — nothing moved, nothing was removed.
232
+ * It holds no position: it is rebuilt from (`positions`, `step`) on every
233
+ * render, so a view cannot end up rendering a stale copy.
234
+ */
235
+ readonly cursor: LensCursor;
229
236
  }
230
237
  /**
231
238
  * Snap a host-supplied step onto the axis. Non-finite and fractional values
232
239
  * are not positions, so they clamp too — and the clamp is always reported.
233
240
  */
234
241
  declare function clampStep(n: number, maxStep: number): number;
235
- declare function useLensCursor({ controlledStep, onStepChange, maxStep, describe, port, }: UseLensCursorArgs): UseLensCursorResult;
242
+ declare function useLensCursor({ controlledStep, onStepChange, maxStep, describe, port, positions, }: UseLensCursorArgs): UseLensCursorResult;
236
243
 
237
244
  /**
238
245
  * useLensNavigator — move the ONE cursor to a stage by its address.
@@ -634,16 +641,67 @@ interface LensProps {
634
641
  *
635
642
  * Same idea as `<TraceExplorerShell slots>`: keep the shipped layout and the
636
643
  * shipped cursor, replace what one pane renders.
644
+ *
645
+ * THE LAW: **a slot fills a PANE, never the CHROME around it.** The rail's tab
646
+ * strip, the one cursor and the collapse pill belong to the library, because
647
+ * chrome is how the library adds capability over time — a consumer that fills a
648
+ * slot must still receive every capability the rail gains later. That is why
649
+ * `detail` is ONE TAB and not a takeover, and why `detailOnly` is an opt-out
650
+ * rather than the default.
637
651
  */
638
652
  interface LensSlots {
639
653
  /**
640
- * Override the RIGHT column — the "what happened" timeline. Your component
641
- * renders inside the shipped column (its width, borders, collapse pill and
642
- * scroll behaviour are unchanged); only the content is yours.
654
+ * Render the right column's FIRST TAB — your own pane, beside the library's.
655
+ *
656
+ * Your component renders inside the shipped column (its width, borders,
657
+ * collapse pill and scroll behaviour are unchanged) and inside the shipped
658
+ * rail: the tab strip stays, your pane becomes the first tab and is selected
659
+ * by default, and "What happened", "Served" and "Bookmarks" stay reachable
660
+ * beside it. Switching tabs never moves the cursor and never unmounts your
661
+ * pane — it keeps its own state.
662
+ *
663
+ * Before 0.50.0 a supplied slot replaced the whole rail, tab strip included,
664
+ * so a slot consumer could not reach anything the rail gained afterwards.
665
+ * That was the defect this shape fixes; `detailOnly` is the way back.
643
666
  *
644
667
  * Omit it and the built-in timeline renders exactly as before.
668
+ *
669
+ * ```tsx
670
+ * <Lens recorder={recorder} slots={{ detail: MyBands, detailLabel: 'Bands' }} />
671
+ * // rail: [ Bands* | What happened | Served | Bookmarks ]
672
+ * ```
645
673
  */
646
674
  readonly detail?: React__default.ComponentType<LensDetailSlotProps>;
675
+ /**
676
+ * What the `detail` tab is CALLED. Default: `"Details"`.
677
+ *
678
+ * WHY it exists: the tab strip names each reading in the reader's own words,
679
+ * and "Details" is the library's generic word for someone else's pane. An app
680
+ * whose pane is a stack of SEO bands should say so — the strip is read left to
681
+ * right and an unnamed tab makes the app's own reading the anonymous one.
682
+ *
683
+ * A plain string, rendered as the tab's text; it is the accessible name too.
684
+ * Ignored when no `detail` slot is supplied.
685
+ *
686
+ * ```tsx
687
+ * slots={{ detail: MyBands, detailLabel: 'SEO bands' }}
688
+ * ```
689
+ */
690
+ readonly detailLabel?: string;
691
+ /**
692
+ * OPT OUT of the tab strip: the `detail` slot takes the whole rail, as it did
693
+ * before 0.50.0. Default `false`.
694
+ *
695
+ * For the app with a full-height custom layout of its own — a pane that owns
696
+ * its scroll and its header and would read as a second chrome under the
697
+ * library's strip. The cost is stated plainly: nothing the rail gains later
698
+ * (Served, Bookmarks, whatever follows) can reach that consumer, and neither
699
+ * can the library's own timeline. Prefer the tabs; take this only when the
700
+ * layout genuinely demands it.
701
+ *
702
+ * Ignored when no `detail` slot is supplied.
703
+ */
704
+ readonly detailOnly?: boolean;
647
705
  }
648
706
  /**
649
707
  * What the `detail` slot receives — the cursor, in the units a detail pane
@@ -670,6 +728,26 @@ interface LensDetailSlotProps {
670
728
  readonly recorder: LensRecorder;
671
729
  /** Move the ONE cursor — same funnel every built-in mover uses. */
672
730
  readonly onNavigate: (step: number) => void;
731
+ /**
732
+ * THE CURSOR, in the one vocabulary every view now shares (0.51.0) — the
733
+ * reading above (`cursor.at`), the axis's honest answer to an ADDRESS
734
+ * (`cursor.resolve(id)`), and the same funnel as `onNavigate`
735
+ * (`cursor.moveTo`).
736
+ *
737
+ * Everything else on this interface stays exactly as it was; this is the
738
+ * shape to reach for when your pane places elements BY ADDRESS, because the
739
+ * axis may honestly not stop where an address points:
740
+ *
741
+ * ```tsx
742
+ * const to = p.cursor.resolve(node.runtimeStageId);
743
+ * if (!to.ok) return <li data-unplaced title={to.message}>{node.name}</li>;
744
+ * return <li onClick={() => p.cursor.moveTo(to.step)}>{to.label}</li>;
745
+ * ```
746
+ *
747
+ * It holds no position of its own — it is rebuilt every render. Read it;
748
+ * never store it.
749
+ */
750
+ readonly cursor: LensCursor;
673
751
  }
674
752
  declare const Lens: React__default.FC<LensProps>;
675
753