agentfootprint-lens 0.41.0 → 0.43.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 (47) hide show
  1. package/README.md +78 -2
  2. package/dist/{useCursorPositions-CF9RDUw0.d.cts → Lens-B039-UGw.d.cts} +98 -48
  3. package/dist/{useCursorPositions-B1HJiarC.d.ts → Lens-ey1EgN9i.d.ts} +98 -48
  4. package/dist/{chunk-36MMC2GY.js → chunk-CWKOGPOJ.js} +219 -203
  5. package/dist/chunk-CWKOGPOJ.js.map +1 -0
  6. package/dist/{chunk-OZUS4IAS.js → chunk-F473UD5M.js} +3 -3
  7. package/dist/{chunk-OZUS4IAS.js.map → chunk-F473UD5M.js.map} +1 -1
  8. package/dist/{chunk-5E2N7IP7.js → chunk-FFQ5EARP.js} +2 -2
  9. package/dist/{chunk-HBVKOHFU.js → chunk-UWN5VKQ2.js} +2 -2
  10. package/dist/{chunk-XO5VNJYM.js → chunk-VBMVWQUO.js} +134 -77
  11. package/dist/chunk-VBMVWQUO.js.map +1 -0
  12. package/dist/{chunk-6RXMOK2E.js → chunk-VBRT372Q.js} +63 -8
  13. package/dist/chunk-VBRT372Q.js.map +1 -0
  14. package/dist/core.cjs +198 -82
  15. package/dist/core.cjs.map +1 -1
  16. package/dist/core.d.cts +5 -5
  17. package/dist/core.d.ts +5 -5
  18. package/dist/core.js +7 -3
  19. package/dist/index.cjs +424 -291
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +8 -8
  22. package/dist/index.d.ts +8 -8
  23. package/dist/index.js +11 -7
  24. package/dist/index.js.map +1 -1
  25. package/dist/{stepForRuntimeStageId-Bl6cG9wB.d.ts → resolveNavigation-o4E4cM_1.d.cts} +128 -1
  26. package/dist/{stepForRuntimeStageId-Bl6cG9wB.d.cts → resolveNavigation-o4E4cM_1.d.ts} +128 -1
  27. package/dist/{stepForCommitIdx-feXP5i-2.d.cts → scrubAxisFor-CNceBlz-.d.cts} +48 -2
  28. package/dist/{stepForCommitIdx-PnR_Kael.d.ts → scrubAxisFor-Cz214OHj.d.ts} +48 -2
  29. package/dist/{selectSkillFrameContext-AX9VtcCf.d.ts → selectSkillFrameContext-CT_EKu4u.d.cts} +1 -1
  30. package/dist/{selectSkillFrameContext-C19Sqv3W.d.cts → selectSkillFrameContext-D0xQV_px.d.ts} +1 -1
  31. package/dist/skillgraph.cjs +63 -7
  32. package/dist/skillgraph.cjs.map +1 -1
  33. package/dist/skillgraph.d.cts +15 -11
  34. package/dist/skillgraph.d.ts +15 -11
  35. package/dist/skillgraph.js +5 -3
  36. package/dist/why.cjs +259 -128
  37. package/dist/why.cjs.map +1 -1
  38. package/dist/why.d.cts +5 -5
  39. package/dist/why.d.ts +5 -5
  40. package/dist/why.js +8 -6
  41. package/dist/why.js.map +1 -1
  42. package/package.json +3 -2
  43. package/dist/chunk-36MMC2GY.js.map +0 -1
  44. package/dist/chunk-6RXMOK2E.js.map +0 -1
  45. package/dist/chunk-XO5VNJYM.js.map +0 -1
  46. /package/dist/{chunk-5E2N7IP7.js.map → chunk-FFQ5EARP.js.map} +0 -0
  47. /package/dist/{chunk-HBVKOHFU.js.map → chunk-UWN5VKQ2.js.map} +0 -0
package/README.md CHANGED
@@ -76,8 +76,8 @@ changes.
76
76
 
77
77
  | import | the job |
78
78
  |---|---|
79
- | `agentfootprint-lens/why` | Replay a recording as the agent's own milestones. `<WhyLens recording={...} />` takes the recording straight (`recordRun()`'s `{ snapshot, events, structure }`, or the `persistRecording` envelope), validates it at mount, and mounts the shipped `<Lens>` shell on the milestone axis — plus the axis helpers (`scrubAxisFor`, `stepForCommitIdx`, `stepForRuntimeStageId`) for hosts holding one cursor across views. |
80
- | `agentfootprint-lens/skillgraph` | Debug how a run routed through its skills. `<SkillGraphDebugger recorder={...} />` plus its headless selectors (`selectSkillRoute`, `selectSkillBeats`, `selectSkillTopology`, `selectSkillFrameContext`, `stepForRuntimeStageId`). |
79
+ | `agentfootprint-lens/why` | Replay a recording as the agent's own milestones. `<WhyLens recording={...} />` takes the recording straight (`recordRun()`'s `{ snapshot, events, structure }`, or the `persistRecording` envelope), validates it at mount, and mounts the shipped `<Lens>` shell on the milestone axis — plus the axis helpers (`scrubAxisFor`, `stepForCommitIdx`, `stepForRuntimeStageId`, `resolveNavigation`) for hosts holding one cursor across views. |
80
+ | `agentfootprint-lens/skillgraph` | Debug how a run routed through its skills. `<SkillGraphDebugger recorder={...} />` plus its headless selectors (`selectSkillRoute`, `selectSkillBeats`, `selectSkillTopology`, `selectSkillFrameContext`, `stepForRuntimeStageId`, `resolveNavigation`). |
81
81
 
82
82
  The axis model in three sentences: one run leaves one causal trace, and each
83
83
  lens replays one AXIS of it — the Why reading scrubs the milestones, the Flow
@@ -111,6 +111,20 @@ Lens watches agentfootprint's typed event stream — `agentfootprint.agent.*`,
111
111
  of the 65-type registry. You never wire events yourself; `recorder.observe()`
112
112
  subscribes to all of them.
113
113
 
114
+ **A long tool call is no longer a silence.** When a tool reports from inside
115
+ its own `execute` with `ctx.progress(payload)` (agentfootprint 9.52+), each
116
+ report lands in the stream between that call's start and its end:
117
+
118
+ ```
119
+ `walk_graph` reported progress (iteration 1): {"hop":1,"of":3,"node":"svc-a"}
120
+ ```
121
+
122
+ The three identity fields are stamped by the framework, so the line states them
123
+ as facts. The payload is the tool author's own data — the Lens shows a preview
124
+ of it rather than guessing at a shape it was never promised, and says so when
125
+ it had to cut one short. A tool that never calls `ctx.progress` files nothing,
126
+ and nothing here invents a report it did not make.
127
+
114
128
  ---
115
129
 
116
130
  ## Multiple watchers, one agent
@@ -654,6 +668,67 @@ click, a WHAT HAPPENED moment, a provenance jump, and the auto-advance that
654
668
  follows a live run. One cursor, one funnel — a mover that moved without telling
655
669
  you would be a second cursor wearing the first one's clothes.
656
670
 
671
+ ### Pointing at a step: `navigatorRef`
672
+
673
+ `step` lets you *own* the cursor. `navigatorRef` lets you *say where* — when the
674
+ only thing you know is a stage's address. That is what a chat answer has: it can
675
+ tell you the tool call went wrong, and now it can point at the exact step it
676
+ means.
677
+
678
+ ```tsx
679
+ import { Lens, type LensNavigator } from 'agentfootprint-lens';
680
+
681
+ const nav = useRef<LensNavigator>(null);
682
+
683
+ <Lens recorder={recorder} navigatorRef={nav} />
684
+
685
+ // Anywhere — a chat message, a dashboard row, a deep link:
686
+ const to = nav.current?.navigateTo('call-llm#18');
687
+ // → { ok: true, step: 7, runtimeStageId: 'call-llm#18', match: 'exact', label: 'call-llm 1' }
688
+ ```
689
+
690
+ It resolves against **the ruler you are actually scrubbing**, so the same
691
+ address is step 7 under `granularity="step"` and step 3 under
692
+ `granularity="group"` — and then it goes through the *same funnel* a click on
693
+ that stop uses. Uncontrolled, the lens moves and reports. Controlled, it reports
694
+ and your `step` lands it. Nothing new holds a position.
695
+
696
+ **A miss is answered, never guessed.** Three rungs, in order:
697
+
698
+ | | what happened | you get |
699
+ |---|---|---|
700
+ | `match: 'exact'` | a stop **is** that address | `{ ok: true, step, label }` |
701
+ | `match: 'enclosing'` | no stop is that address, but one **contains** it (a stage inside a subflow, on a ruler that stops at whole subflows) | `{ ok: true, step, runtimeStageId }` — the stop's own address, so you always know where it went |
702
+ | — | nothing holds it | `{ ok: false, reason, message, nearest? }` — **the cursor did not move** |
703
+
704
+ `nearest` is the stop just *before* the address, handed back as data. It is an
705
+ **offer, not a jump**: a cursor that silently lands somewhere the person did not
706
+ ask for is worse than one that stays put. Take it when you want to — one more
707
+ call, and it is an exact hit by construction:
708
+
709
+ ```tsx
710
+ const to = nav.current?.navigateTo(stageId);
711
+ if (to?.ok) {
712
+ // landed
713
+ } else if (to?.nearest) {
714
+ // "That step isn't on this ruler. Go to Iteration 1 instead?"
715
+ onConfirm(() => nav.current?.navigateTo(to.nearest.runtimeStageId));
716
+ } else {
717
+ show(to?.message); // says exactly why, in a sentence you can print
718
+ }
719
+ ```
720
+
721
+ **No component? Same answer.** The resolver is pure and ships headlessly, so a
722
+ server-rendered link or a CLI can compute the step with nothing mounted:
723
+
724
+ ```ts
725
+ import { scrubAxisFor, resolveNavigation } from 'agentfootprint-lens/core';
726
+
727
+ const positions = scrubAxisFor(recorder, 'step'); // the ruler, as data
728
+ const to = resolveNavigation(positions, 'call-llm#18');
729
+ const href = to.ok ? `/runs/${runId}?step=${to.step}` : undefined;
730
+ ```
731
+
657
732
  ---
658
733
 
659
734
  ## Rendering your own detail pane
@@ -812,6 +887,7 @@ graph in one call. Returns an unsubscribe. Call it once per run.
812
887
  | `granularity` | `'step' \| 'group'?` | Which ruler is scrubbing the chart. `'group'` paints the cursor's group as a named place. Default `'step'`. See [Scrubbing by group](#scrubbing-by-group--the-active-group-is-a-named-place). |
813
888
  | `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). |
814
889
  | `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`. |
890
+ | `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). |
815
891
  | `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). |
816
892
 
817
893
  ### `<LensFlow>` — the chart canvas on its own
@@ -2,9 +2,9 @@ import * as agentfootprint_observe from 'agentfootprint/observe';
2
2
  import { ToolChoiceCall, ToolChoiceSummary } from 'agentfootprint/observe';
3
3
  import * as agentfootprint from 'agentfootprint';
4
4
  import { CommentaryTemplates } from 'agentfootprint';
5
- import React__default from 'react';
6
- import { L as LensRecorder, C as CursorPosition } from './stepForRuntimeStageId-Bl6cG9wB.cjs';
7
- import { C as ChartGroupHighlight, H as Humanizer } from './stepForCommitIdx-feXP5i-2.cjs';
5
+ import React__default, { Ref } from 'react';
6
+ import { d as NavigationResult, C as CursorPosition, L as LensRecorder } from './resolveNavigation-o4E4cM_1.cjs';
7
+ import { C as ChartGroupHighlight, H as Humanizer } from './scrubAxisFor-CNceBlz-.cjs';
8
8
  import { NodeTypes } from '@xyflow/react';
9
9
  import { TraceGraph, TraceFlowLayout, RuntimeOverlay } from 'footprint-explainable-ui/flowchart';
10
10
 
@@ -217,6 +217,73 @@ interface UseLensCursorResult {
217
217
  declare function clampStep(n: number, maxStep: number): number;
218
218
  declare function useLensCursor({ controlledStep, onStepChange, maxStep, describe, }: UseLensCursorArgs): UseLensCursorResult;
219
219
 
220
+ /**
221
+ * useLensNavigator — move the ONE cursor to a stage by its address.
222
+ *
223
+ * `<Lens step onStepChange>` lets a host OWN the cursor. This is the other
224
+ * half a pointing host needs: a way to SAY WHERE, when the only thing it knows
225
+ * is a `runtimeStageId` — a chat answer citing the step it means, a dashboard
226
+ * row, a deep link.
227
+ *
228
+ * THE ONE-CURSOR LAW, kept: this adds NO state and NO second channel. It is a
229
+ * RESOLUTION (`resolveNavigation`, the shipped ladder) followed by the SAME
230
+ * `moveTo` funnel every internal mover already goes through — the step strip,
231
+ * the ◀ ▶ buttons, the arrow keys, a chart click, the live auto-advance. So it
232
+ * behaves exactly like a person clicking that stop:
233
+ *
234
+ * - **Uncontrolled** — the cursor moves, and `onStepChange` reports it.
235
+ * - **Controlled** — nothing moves on its own; `onStepChange` fires with the
236
+ * step and the host's own `step` prop lands it. Two owners never fight,
237
+ * because there is still only one owner.
238
+ * - **Already there** — no move, no event (a non-move is not a change), and
239
+ * still `{ ok: true }` with the step the cursor is standing on.
240
+ *
241
+ * HONEST MISSES: an address the axis cannot hold comes back `{ ok: false }`
242
+ * with the nearest-previous stop OFFERED as data, never taken. See
243
+ * `resolveNavigation` for the ladder.
244
+ *
245
+ * @example The handle, on the component.
246
+ * ```tsx
247
+ * const nav = useRef<LensNavigator>(null);
248
+ *
249
+ * <Lens recorder={recorder} navigatorRef={nav} />
250
+ *
251
+ * // Later — a chat answer points at its evidence:
252
+ * const to = nav.current?.navigateTo('llm#7');
253
+ * if (to?.ok) show(`Jumped to ${to.label}`);
254
+ * else if (to?.nearest) offer(`Not on this ruler. Go to ${to.nearest.label}?`);
255
+ * // Taking the offer is one more call — `nearest.runtimeStageId` is an exact
256
+ * // hit by construction:
257
+ * // nav.current?.navigateTo(to.nearest.runtimeStageId);
258
+ * ```
259
+ */
260
+
261
+ /**
262
+ * The imperative handle `<Lens navigatorRef>` fills in — the lens's cursor,
263
+ * addressable by stage.
264
+ */
265
+ interface LensNavigator {
266
+ /**
267
+ * Move the ONE cursor to the stop that holds `runtimeStageId`, on whatever
268
+ * axis the lens is currently scrubbing (`granularity="step"` → the commit
269
+ * axis, `granularity="group"` → the milestone axis).
270
+ *
271
+ * Returns where it went — or, on a miss, why it did not and what was nearby.
272
+ * Never throws, and never moves on a miss.
273
+ */
274
+ navigateTo(runtimeStageId: string): NavigationResult;
275
+ }
276
+ interface UseLensNavigatorArgs {
277
+ /** The ACTIVE scrub axis — the same list the lens renders its ruler from. */
278
+ readonly positions: readonly CursorPosition[];
279
+ /** The one cursor funnel (`useLensCursor`'s `moveTo`). */
280
+ readonly moveTo: (step: number) => void;
281
+ /** The host's ref, filled with the handle. Omit and the hook still returns
282
+ * the navigator for in-tree use. */
283
+ readonly navigatorRef?: Ref<LensNavigator> | undefined;
284
+ }
285
+ declare function useLensNavigator({ positions, moveTo, navigatorRef, }: UseLensNavigatorArgs): LensNavigator;
286
+
220
287
  /**
221
288
  * useToolChoice — async reader for the agentfootprint/observe
222
289
  * `toolChoiceRecorder` handle (RFC-002 block C7).
@@ -498,6 +565,33 @@ interface LensProps {
498
565
  * Memoize via `useCallback` to avoid downstream re-renders.
499
566
  */
500
567
  readonly onStepChange?: (step: number, at: LensCursorAt) => void;
568
+ /**
569
+ * Move the cursor to a STAGE, by its address — the pointing half of the
570
+ * cursor API. `step` / `onStepChange` let a host OWN the cursor; this lets
571
+ * one SAY WHERE when all it knows is a `runtimeStageId` (a chat answer
572
+ * citing the step it means, a dashboard row, a deep link).
573
+ *
574
+ * ```tsx
575
+ * const nav = useRef<LensNavigator>(null);
576
+ * <Lens recorder={recorder} navigatorRef={nav} />
577
+ *
578
+ * const to = nav.current?.navigateTo('llm#7');
579
+ * // { ok: true, step: 7, runtimeStageId: 'llm#7', match: 'exact', label: … }
580
+ * ```
581
+ *
582
+ * It is a RESOLUTION plus the cursor channel you already have — not a second
583
+ * channel. The address is resolved against the ACTIVE axis (`granularity`),
584
+ * then handed to the same funnel a click on that stop uses: uncontrolled,
585
+ * the lens moves and reports; controlled, it reports and your `step` lands
586
+ * it. Nothing here holds a position.
587
+ *
588
+ * An address the axis cannot hold comes back `{ ok: false, reason, message }`
589
+ * with the nearest earlier stop OFFERED as `nearest` — never taken. The
590
+ * caller decides. See `resolveNavigation` for the ladder (exact → enclosing
591
+ * → nearest-previous-as-an-offer), which is also exported headlessly for
592
+ * hosts with nothing mounted.
593
+ */
594
+ readonly navigatorRef?: React__default.Ref<LensNavigator>;
501
595
  /**
502
596
  * Optional slot overrides. Should be stable across renders (define at module
503
597
  * scope or `useMemo`). Omit and the shipped panes render unchanged.
@@ -548,48 +642,4 @@ interface LensDetailSlotProps {
548
642
  }
549
643
  declare const Lens: React__default.FC<LensProps>;
550
644
 
551
- /**
552
- * useCursorPositions — React hook exposing the slider's valid positions
553
- * at the current drill level, on the requested AXIS.
554
- *
555
- * Layer 2 / Tier B / Lens v0.1.
556
- *
557
- * TWO AXES, ONE CURSOR (the 0.39.0 correction):
558
- *
559
- * `'commit'` — one stop per executed stage, straight off the commit log.
560
- * The Flow Lens's axis (`granularity="step"`): every stage
561
- * is a stop, nothing skippable, and the ruler's count is the
562
- * commit log's count by construction.
563
- * `'milestone'` — one stop per agent-meaningful moment (iteration → context
564
- * → LLM turn → route → tool call), classified by the
565
- * domain's `milestoneFor`. The Why Lens's axis
566
- * (`granularity="group"`), which `stepBands` bands by
567
- * iteration.
568
- *
569
- * The cursor type is still `runtimeStageId` either way. Only the SET of valid
570
- * positions differs — the same compound-time-axis rule that already scaled the
571
- * set by drill depth now also scales it by reading.
572
- *
573
- * Lifecycle
574
- * ─────────
575
- * Reactive to BOTH the recorder (events advance the commit log /
576
- * open new groups) and to `drillPath` changes (user drill-in /
577
- * drill-out). Memoized so a stable drillPath + recorder = stable
578
- * array identity.
579
- */
580
-
581
- /** Which projection of the run the scrub axis stops at. */
582
- type ScrubAxis = 'commit' | 'milestone';
583
- /**
584
- * The scrub axis for a recording, as a PURE function — the same positions the
585
- * mounted `<Lens>` scrubs at that granularity, computable outside React.
586
- *
587
- * This is the export a HOST holding the cursor across two granularities needs:
588
- * to carry a position from one axis to the other, build the target axis here
589
- * and resolve the commit with `stepForCommitIdx` (or an address with
590
- * `stepForRuntimeStageId`). Returns `[]` when the recording has no commits yet
591
- * (or on any read error — same posture as the hook).
592
- */
593
- declare function scrubAxisFor(recorder: LensRecorder, granularity: 'step' | 'group', drillPath?: readonly string[]): readonly CursorPosition[];
594
-
595
- export { type LensProps as L, type ScrubAxis as S, type ToolChoiceSource as T, type UseLensCursorArgs as U, Lens as a, type LensCursorAt as b, type LensDetailSlotProps as c, type LensSlots as d, type LensTheme as e, type LensView as f, type LensCursorPlace as g, LensFlow as h, type LensFlowProps as i, type UseLensCursorResult as j, type UseToolChoiceResult as k, clampStep as l, useToolChoice as m, scrubAxisFor as s, useLensCursor as u };
645
+ export { type LensProps as L, type ToolChoiceSource as T, type UseLensCursorArgs as U, Lens as a, type LensCursorAt as b, type LensDetailSlotProps as c, type LensSlots as d, type LensTheme as e, type LensView as f, type LensCursorPlace as g, LensFlow as h, type LensFlowProps as i, type LensNavigator as j, type UseLensCursorResult as k, type UseLensNavigatorArgs as l, type UseToolChoiceResult as m, clampStep as n, useLensNavigator as o, useToolChoice as p, useLensCursor as u };
@@ -2,9 +2,9 @@ import * as agentfootprint_observe from 'agentfootprint/observe';
2
2
  import { ToolChoiceCall, ToolChoiceSummary } from 'agentfootprint/observe';
3
3
  import * as agentfootprint from 'agentfootprint';
4
4
  import { CommentaryTemplates } from 'agentfootprint';
5
- import React__default from 'react';
6
- import { L as LensRecorder, C as CursorPosition } from './stepForRuntimeStageId-Bl6cG9wB.js';
7
- import { C as ChartGroupHighlight, H as Humanizer } from './stepForCommitIdx-PnR_Kael.js';
5
+ import React__default, { Ref } from 'react';
6
+ import { d as NavigationResult, C as CursorPosition, L as LensRecorder } from './resolveNavigation-o4E4cM_1.js';
7
+ import { C as ChartGroupHighlight, H as Humanizer } from './scrubAxisFor-Cz214OHj.js';
8
8
  import { NodeTypes } from '@xyflow/react';
9
9
  import { TraceGraph, TraceFlowLayout, RuntimeOverlay } from 'footprint-explainable-ui/flowchart';
10
10
 
@@ -217,6 +217,73 @@ interface UseLensCursorResult {
217
217
  declare function clampStep(n: number, maxStep: number): number;
218
218
  declare function useLensCursor({ controlledStep, onStepChange, maxStep, describe, }: UseLensCursorArgs): UseLensCursorResult;
219
219
 
220
+ /**
221
+ * useLensNavigator — move the ONE cursor to a stage by its address.
222
+ *
223
+ * `<Lens step onStepChange>` lets a host OWN the cursor. This is the other
224
+ * half a pointing host needs: a way to SAY WHERE, when the only thing it knows
225
+ * is a `runtimeStageId` — a chat answer citing the step it means, a dashboard
226
+ * row, a deep link.
227
+ *
228
+ * THE ONE-CURSOR LAW, kept: this adds NO state and NO second channel. It is a
229
+ * RESOLUTION (`resolveNavigation`, the shipped ladder) followed by the SAME
230
+ * `moveTo` funnel every internal mover already goes through — the step strip,
231
+ * the ◀ ▶ buttons, the arrow keys, a chart click, the live auto-advance. So it
232
+ * behaves exactly like a person clicking that stop:
233
+ *
234
+ * - **Uncontrolled** — the cursor moves, and `onStepChange` reports it.
235
+ * - **Controlled** — nothing moves on its own; `onStepChange` fires with the
236
+ * step and the host's own `step` prop lands it. Two owners never fight,
237
+ * because there is still only one owner.
238
+ * - **Already there** — no move, no event (a non-move is not a change), and
239
+ * still `{ ok: true }` with the step the cursor is standing on.
240
+ *
241
+ * HONEST MISSES: an address the axis cannot hold comes back `{ ok: false }`
242
+ * with the nearest-previous stop OFFERED as data, never taken. See
243
+ * `resolveNavigation` for the ladder.
244
+ *
245
+ * @example The handle, on the component.
246
+ * ```tsx
247
+ * const nav = useRef<LensNavigator>(null);
248
+ *
249
+ * <Lens recorder={recorder} navigatorRef={nav} />
250
+ *
251
+ * // Later — a chat answer points at its evidence:
252
+ * const to = nav.current?.navigateTo('llm#7');
253
+ * if (to?.ok) show(`Jumped to ${to.label}`);
254
+ * else if (to?.nearest) offer(`Not on this ruler. Go to ${to.nearest.label}?`);
255
+ * // Taking the offer is one more call — `nearest.runtimeStageId` is an exact
256
+ * // hit by construction:
257
+ * // nav.current?.navigateTo(to.nearest.runtimeStageId);
258
+ * ```
259
+ */
260
+
261
+ /**
262
+ * The imperative handle `<Lens navigatorRef>` fills in — the lens's cursor,
263
+ * addressable by stage.
264
+ */
265
+ interface LensNavigator {
266
+ /**
267
+ * Move the ONE cursor to the stop that holds `runtimeStageId`, on whatever
268
+ * axis the lens is currently scrubbing (`granularity="step"` → the commit
269
+ * axis, `granularity="group"` → the milestone axis).
270
+ *
271
+ * Returns where it went — or, on a miss, why it did not and what was nearby.
272
+ * Never throws, and never moves on a miss.
273
+ */
274
+ navigateTo(runtimeStageId: string): NavigationResult;
275
+ }
276
+ interface UseLensNavigatorArgs {
277
+ /** The ACTIVE scrub axis — the same list the lens renders its ruler from. */
278
+ readonly positions: readonly CursorPosition[];
279
+ /** The one cursor funnel (`useLensCursor`'s `moveTo`). */
280
+ readonly moveTo: (step: number) => void;
281
+ /** The host's ref, filled with the handle. Omit and the hook still returns
282
+ * the navigator for in-tree use. */
283
+ readonly navigatorRef?: Ref<LensNavigator> | undefined;
284
+ }
285
+ declare function useLensNavigator({ positions, moveTo, navigatorRef, }: UseLensNavigatorArgs): LensNavigator;
286
+
220
287
  /**
221
288
  * useToolChoice — async reader for the agentfootprint/observe
222
289
  * `toolChoiceRecorder` handle (RFC-002 block C7).
@@ -498,6 +565,33 @@ interface LensProps {
498
565
  * Memoize via `useCallback` to avoid downstream re-renders.
499
566
  */
500
567
  readonly onStepChange?: (step: number, at: LensCursorAt) => void;
568
+ /**
569
+ * Move the cursor to a STAGE, by its address — the pointing half of the
570
+ * cursor API. `step` / `onStepChange` let a host OWN the cursor; this lets
571
+ * one SAY WHERE when all it knows is a `runtimeStageId` (a chat answer
572
+ * citing the step it means, a dashboard row, a deep link).
573
+ *
574
+ * ```tsx
575
+ * const nav = useRef<LensNavigator>(null);
576
+ * <Lens recorder={recorder} navigatorRef={nav} />
577
+ *
578
+ * const to = nav.current?.navigateTo('llm#7');
579
+ * // { ok: true, step: 7, runtimeStageId: 'llm#7', match: 'exact', label: … }
580
+ * ```
581
+ *
582
+ * It is a RESOLUTION plus the cursor channel you already have — not a second
583
+ * channel. The address is resolved against the ACTIVE axis (`granularity`),
584
+ * then handed to the same funnel a click on that stop uses: uncontrolled,
585
+ * the lens moves and reports; controlled, it reports and your `step` lands
586
+ * it. Nothing here holds a position.
587
+ *
588
+ * An address the axis cannot hold comes back `{ ok: false, reason, message }`
589
+ * with the nearest earlier stop OFFERED as `nearest` — never taken. The
590
+ * caller decides. See `resolveNavigation` for the ladder (exact → enclosing
591
+ * → nearest-previous-as-an-offer), which is also exported headlessly for
592
+ * hosts with nothing mounted.
593
+ */
594
+ readonly navigatorRef?: React__default.Ref<LensNavigator>;
501
595
  /**
502
596
  * Optional slot overrides. Should be stable across renders (define at module
503
597
  * scope or `useMemo`). Omit and the shipped panes render unchanged.
@@ -548,48 +642,4 @@ interface LensDetailSlotProps {
548
642
  }
549
643
  declare const Lens: React__default.FC<LensProps>;
550
644
 
551
- /**
552
- * useCursorPositions — React hook exposing the slider's valid positions
553
- * at the current drill level, on the requested AXIS.
554
- *
555
- * Layer 2 / Tier B / Lens v0.1.
556
- *
557
- * TWO AXES, ONE CURSOR (the 0.39.0 correction):
558
- *
559
- * `'commit'` — one stop per executed stage, straight off the commit log.
560
- * The Flow Lens's axis (`granularity="step"`): every stage
561
- * is a stop, nothing skippable, and the ruler's count is the
562
- * commit log's count by construction.
563
- * `'milestone'` — one stop per agent-meaningful moment (iteration → context
564
- * → LLM turn → route → tool call), classified by the
565
- * domain's `milestoneFor`. The Why Lens's axis
566
- * (`granularity="group"`), which `stepBands` bands by
567
- * iteration.
568
- *
569
- * The cursor type is still `runtimeStageId` either way. Only the SET of valid
570
- * positions differs — the same compound-time-axis rule that already scaled the
571
- * set by drill depth now also scales it by reading.
572
- *
573
- * Lifecycle
574
- * ─────────
575
- * Reactive to BOTH the recorder (events advance the commit log /
576
- * open new groups) and to `drillPath` changes (user drill-in /
577
- * drill-out). Memoized so a stable drillPath + recorder = stable
578
- * array identity.
579
- */
580
-
581
- /** Which projection of the run the scrub axis stops at. */
582
- type ScrubAxis = 'commit' | 'milestone';
583
- /**
584
- * The scrub axis for a recording, as a PURE function — the same positions the
585
- * mounted `<Lens>` scrubs at that granularity, computable outside React.
586
- *
587
- * This is the export a HOST holding the cursor across two granularities needs:
588
- * to carry a position from one axis to the other, build the target axis here
589
- * and resolve the commit with `stepForCommitIdx` (or an address with
590
- * `stepForRuntimeStageId`). Returns `[]` when the recording has no commits yet
591
- * (or on any read error — same posture as the hook).
592
- */
593
- declare function scrubAxisFor(recorder: LensRecorder, granularity: 'step' | 'group', drillPath?: readonly string[]): readonly CursorPosition[];
594
-
595
- export { type LensProps as L, type ScrubAxis as S, type ToolChoiceSource as T, type UseLensCursorArgs as U, Lens as a, type LensCursorAt as b, type LensDetailSlotProps as c, type LensSlots as d, type LensTheme as e, type LensView as f, type LensCursorPlace as g, LensFlow as h, type LensFlowProps as i, type UseLensCursorResult as j, type UseToolChoiceResult as k, clampStep as l, useToolChoice as m, scrubAxisFor as s, useLensCursor as u };
645
+ export { type LensProps as L, type ToolChoiceSource as T, type UseLensCursorArgs as U, Lens as a, type LensCursorAt as b, type LensDetailSlotProps as c, type LensSlots as d, type LensTheme as e, type LensView as f, type LensCursorPlace as g, LensFlow as h, type LensFlowProps as i, type LensNavigator as j, type UseLensCursorResult as k, type UseLensNavigatorArgs as l, type UseToolChoiceResult as m, clampStep as n, useLensNavigator as o, useToolChoice as p, useLensCursor as u };