agentfootprint-lens 0.40.0 → 0.42.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 (48) hide show
  1. package/README.md +64 -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-S7FLEXJ3.js → chunk-F473UD5M.js} +61 -12
  5. package/dist/chunk-F473UD5M.js.map +1 -0
  6. package/dist/{chunk-EBQCTQQU.js → chunk-FFQ5EARP.js} +2 -2
  7. package/dist/{chunk-CHFRN2WV.js → chunk-JE37MDVV.js} +95 -77
  8. package/dist/chunk-JE37MDVV.js.map +1 -0
  9. package/dist/{chunk-EYCTYBSV.js → chunk-VBRT372Q.js} +143 -18
  10. package/dist/chunk-VBRT372Q.js.map +1 -0
  11. package/dist/{chunk-P4F3RGXR.js → chunk-VKBQYD6I.js} +2 -2
  12. package/dist/{chunk-L2Z6HTJI.js → chunk-YQXVXNXK.js} +219 -203
  13. package/dist/chunk-YQXVXNXK.js.map +1 -0
  14. package/dist/core.cjs +239 -92
  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 +523 -310
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +30 -20
  22. package/dist/index.d.ts +30 -20
  23. package/dist/index.js +11 -7
  24. package/dist/index.js.map +1 -1
  25. package/dist/{stepForRuntimeStageId-Bl6cG9wB.d.cts → resolveNavigation-o4E4cM_1.d.cts} +128 -1
  26. package/dist/{stepForRuntimeStageId-Bl6cG9wB.d.ts → 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-DdObTJz3.d.cts → selectSkillFrameContext-CT_EKu4u.d.cts} +98 -22
  30. package/dist/{selectSkillFrameContext-Hjjf75t9.d.ts → selectSkillFrameContext-D0xQV_px.d.ts} +98 -22
  31. package/dist/skillgraph.cjs +201 -26
  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 +220 -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 +2 -2
  43. package/dist/chunk-CHFRN2WV.js.map +0 -1
  44. package/dist/chunk-EYCTYBSV.js.map +0 -1
  45. package/dist/chunk-L2Z6HTJI.js.map +0 -1
  46. package/dist/chunk-S7FLEXJ3.js.map +0 -1
  47. /package/dist/{chunk-EBQCTQQU.js.map → chunk-FFQ5EARP.js.map} +0 -0
  48. /package/dist/{chunk-P4F3RGXR.js.map → chunk-VKBQYD6I.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
@@ -654,6 +654,67 @@ click, a WHAT HAPPENED moment, a provenance jump, and the auto-advance that
654
654
  follows a live run. One cursor, one funnel — a mover that moved without telling
655
655
  you would be a second cursor wearing the first one's clothes.
656
656
 
657
+ ### Pointing at a step: `navigatorRef`
658
+
659
+ `step` lets you *own* the cursor. `navigatorRef` lets you *say where* — when the
660
+ only thing you know is a stage's address. That is what a chat answer has: it can
661
+ tell you the tool call went wrong, and now it can point at the exact step it
662
+ means.
663
+
664
+ ```tsx
665
+ import { Lens, type LensNavigator } from 'agentfootprint-lens';
666
+
667
+ const nav = useRef<LensNavigator>(null);
668
+
669
+ <Lens recorder={recorder} navigatorRef={nav} />
670
+
671
+ // Anywhere — a chat message, a dashboard row, a deep link:
672
+ const to = nav.current?.navigateTo('call-llm#18');
673
+ // → { ok: true, step: 7, runtimeStageId: 'call-llm#18', match: 'exact', label: 'call-llm 1' }
674
+ ```
675
+
676
+ It resolves against **the ruler you are actually scrubbing**, so the same
677
+ address is step 7 under `granularity="step"` and step 3 under
678
+ `granularity="group"` — and then it goes through the *same funnel* a click on
679
+ that stop uses. Uncontrolled, the lens moves and reports. Controlled, it reports
680
+ and your `step` lands it. Nothing new holds a position.
681
+
682
+ **A miss is answered, never guessed.** Three rungs, in order:
683
+
684
+ | | what happened | you get |
685
+ |---|---|---|
686
+ | `match: 'exact'` | a stop **is** that address | `{ ok: true, step, label }` |
687
+ | `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 |
688
+ | — | nothing holds it | `{ ok: false, reason, message, nearest? }` — **the cursor did not move** |
689
+
690
+ `nearest` is the stop just *before* the address, handed back as data. It is an
691
+ **offer, not a jump**: a cursor that silently lands somewhere the person did not
692
+ ask for is worse than one that stays put. Take it when you want to — one more
693
+ call, and it is an exact hit by construction:
694
+
695
+ ```tsx
696
+ const to = nav.current?.navigateTo(stageId);
697
+ if (to?.ok) {
698
+ // landed
699
+ } else if (to?.nearest) {
700
+ // "That step isn't on this ruler. Go to Iteration 1 instead?"
701
+ onConfirm(() => nav.current?.navigateTo(to.nearest.runtimeStageId));
702
+ } else {
703
+ show(to?.message); // says exactly why, in a sentence you can print
704
+ }
705
+ ```
706
+
707
+ **No component? Same answer.** The resolver is pure and ships headlessly, so a
708
+ server-rendered link or a CLI can compute the step with nothing mounted:
709
+
710
+ ```ts
711
+ import { scrubAxisFor, resolveNavigation } from 'agentfootprint-lens/core';
712
+
713
+ const positions = scrubAxisFor(recorder, 'step'); // the ruler, as data
714
+ const to = resolveNavigation(positions, 'call-llm#18');
715
+ const href = to.ok ? `/runs/${runId}?step=${to.step}` : undefined;
716
+ ```
717
+
657
718
  ---
658
719
 
659
720
  ## Rendering your own detail pane
@@ -812,6 +873,7 @@ graph in one call. Returns an unsubscribe. Call it once per run.
812
873
  | `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
874
  | `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
875
  | `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`. |
876
+ | `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
877
  | `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
878
 
817
879
  ### `<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 };
@@ -6,14 +6,14 @@ import {
6
6
  describeReceived,
7
7
  refusalDestinationFor,
8
8
  useNarrowRow
9
- } from "./chunk-EBQCTQQU.js";
9
+ } from "./chunk-FFQ5EARP.js";
10
10
  import {
11
11
  selectSkillBeatAt,
12
12
  selectSkillBeats,
13
13
  selectSkillFrameContext,
14
14
  selectSkillRoute,
15
15
  selectSkillTopology
16
- } from "./chunk-EYCTYBSV.js";
16
+ } from "./chunk-VBRT372Q.js";
17
17
 
18
18
  // src/react/skillGraphFlowLayout.ts
19
19
  import dagre from "dagre";
@@ -237,6 +237,8 @@ function FrameFactsPanel({
237
237
  return /* @__PURE__ */ jsx2("div", { style: panelStyle, "data-testid": "frame-facts", children: /* @__PURE__ */ jsx2("p", { style: { margin: 0, fontSize: 12, color: T.textMuted }, children: "Nothing has been sent to the model yet at this cursor position." }) });
238
238
  }
239
239
  const hop = beat.hop;
240
+ const promptOnRecord = hop.systemPromptText !== void 0;
241
+ const reachableAsData = beat.reachable?.source === "cursor-move";
240
242
  return /* @__PURE__ */ jsxs2("div", { style: panelStyle, "data-testid": "frame-facts", children: [
241
243
  /* @__PURE__ */ jsx2(
242
244
  Header,
@@ -245,6 +247,14 @@ function FrameFactsPanel({
245
247
  subtitle: `${beat.label} \xB7 the call this stop prepared`
246
248
  }
247
249
  ),
250
+ hop.systemPromptText !== void 0 && /* @__PURE__ */ jsxs2(Section, { title: "system prompt, as sent", count: 1, children: [
251
+ /* @__PURE__ */ jsxs2("div", { style: { fontSize: 10, color: T.textMuted, marginBottom: 4 }, children: [
252
+ "the assembled prompt, byte-for-byte as the provider received it (the producer opted in with ",
253
+ /* @__PURE__ */ jsx2("code", { children: "recordSystemPrompt: true" }),
254
+ ")"
255
+ ] }),
256
+ /* @__PURE__ */ jsx2(Mono, { testId: "system-prompt-text", children: hop.systemPromptText })
257
+ ] }),
248
258
  /* @__PURE__ */ jsx2(Section, { title: "read_skill, as sent", count: hop.readSkillDescription !== void 0 ? 1 : 0, children: hop.readSkillDescription !== void 0 ? /* @__PURE__ */ jsx2(Mono, { testId: "read-skill-description", children: hop.readSkillDescription }) : /* @__PURE__ */ jsxs2(Absent, { children: [
249
259
  "This iteration's recording carries no ",
250
260
  /* @__PURE__ */ jsx2("code", { children: "read_skill" }),
@@ -321,7 +331,7 @@ function FrameFactsPanel({
321
331
  u.shape !== void 0 ? ` (${u.shape})` : ""
322
332
  ] }, i))
323
333
  ] }),
324
- /* @__PURE__ */ jsxs2(
334
+ (!promptOnRecord || !reachableAsData) && /* @__PURE__ */ jsxs2(
325
335
  "div",
326
336
  {
327
337
  "data-testid": "frame-absence",
@@ -337,9 +347,20 @@ function FrameFactsPanel({
337
347
  },
338
348
  children: [
339
349
  /* @__PURE__ */ jsx2("strong", { style: { color: T.textSecondary }, children: "Not in this recording." }),
340
- " The system prompt as one assembled string is not recorded \u2014 the injections above are the pieces it was composed from. The reachable set is not recorded as data either: it appears as prose inside ",
341
- /* @__PURE__ */ jsx2("code", { children: "read_skill" }),
342
- "'s description, and as a typed list only when the gate refused a pick. Neither is reconstructed here."
350
+ !promptOnRecord && /* @__PURE__ */ jsxs2(Fragment, { children: [
351
+ " ",
352
+ "The system prompt as one assembled string is not recorded \u2014 the injections above are the pieces it was composed from. Recording the assembled prompt is an explicit opt-in (",
353
+ /* @__PURE__ */ jsx2("code", { children: "recordSystemPrompt: true" }),
354
+ "); the default keeps it out of every recording, because it carries everything injected into it."
355
+ ] }),
356
+ !reachableAsData && /* @__PURE__ */ jsxs2(Fragment, { children: [
357
+ " ",
358
+ "The reachable set is not recorded as data in this era's recording: it appears as prose inside ",
359
+ /* @__PURE__ */ jsx2("code", { children: "read_skill" }),
360
+ "'s description, and as a typed list only when the gate refused a pick."
361
+ ] }),
362
+ " ",
363
+ "Nothing absent is reconstructed here."
343
364
  ]
344
365
  }
345
366
  )
@@ -647,7 +668,7 @@ function RouteDecisionCard({ beat, lens }) {
647
668
  "\u201D"
648
669
  ] })
649
670
  ] }),
650
- beat.reachable !== void 0 && /* @__PURE__ */ jsx4(Evidence, { label: `reachable (${beat.reachable.source})`, children: beat.reachable.ids.join(", ") }),
671
+ beat.reachable !== void 0 && /* @__PURE__ */ jsx4(Evidence, { label: `reachable (${beat.reachable.source})`, children: beat.reachable.ids.length > 0 ? beat.reachable.ids.join(", ") : "none \u2014 a dead end: no skill was admissible from this cursor" }),
651
672
  beat.hop.offered !== void 0 && /* @__PURE__ */ jsx4(Evidence, { label: "menu offered", children: beat.hop.offered.join(", ") }),
652
673
  beat.hop.refusals.map((r, i) => /* @__PURE__ */ jsxs4(
653
674
  "div",
@@ -800,7 +821,10 @@ var SkillStateNode = ({ data }) => {
800
821
  {
801
822
  "data-testid": `skill-node-${d.label}`,
802
823
  "data-state": d.state,
803
- title: d.jumpable ? `Jump the cursor to "${d.label}"` : `The cursor never stood in "${d.label}"`,
824
+ title: [
825
+ d.description,
826
+ d.jumpable ? `Jump the cursor to "${d.label}"` : `The cursor never stood in "${d.label}"`
827
+ ].filter((line) => line !== void 0).join("\n\n"),
804
828
  style: {
805
829
  width: 192,
806
830
  minHeight: 56,
@@ -859,6 +883,21 @@ var SkillStateNode = ({ data }) => {
859
883
  },
860
884
  children: [
861
885
  /* @__PURE__ */ jsx5("span", { children: d.stateLabel }),
886
+ d.isEntry && /* @__PURE__ */ jsx5(
887
+ "span",
888
+ {
889
+ style: {
890
+ padding: "1px 5px",
891
+ borderRadius: 5,
892
+ background: `${T.bgPrimary}`,
893
+ border: `1px solid ${T.textMuted}`,
894
+ color: T.textMuted,
895
+ textTransform: "none",
896
+ letterSpacing: 0
897
+ },
898
+ children: "entry"
899
+ }
900
+ ),
862
901
  d.pickedByModel && /* @__PURE__ */ jsx5(
863
902
  "span",
864
903
  {
@@ -910,11 +949,13 @@ function SkillTopologyCanvas({
910
949
  draggable: false,
911
950
  selectable: true,
912
951
  data: {
913
- label: n.id,
952
+ label: n.label ?? n.id,
914
953
  state: n.state,
915
954
  stateLabel: lens === "product" ? STATE_STYLE[n.state].product : STATE_STYLE[n.state].developer,
916
955
  pickedByModel: n.pickedByModel,
917
- jumpable: jumpable.has(n.id)
956
+ jumpable: jumpable.has(n.id),
957
+ ...n.description !== void 0 ? { description: n.description } : {},
958
+ isEntry: n.isEntry
918
959
  }
919
960
  };
920
961
  }),
@@ -1019,7 +1060,15 @@ function Legend({
1019
1060
  /* @__PURE__ */ jsx5("span", { children: "\u2014 solid: an edge the author declared" }),
1020
1061
  /* @__PURE__ */ jsx5("span", { children: "\u2504 dashed: a hop the run took, never declared on the record" })
1021
1062
  ] }),
1022
- topology.declaredSource !== "graph" && /* @__PURE__ */ jsx5("div", { style: { flex: "1 1 100%", color: T.warning, lineHeight: 1.35 }, children: topology.declaredSource === "recording" ? "Declared edges shown are only the ones this recording named \u2014 a recording names an edge once it fires, so the author may have drawn more." : "This recording named no declared edges; every line here is a hop that was observed." })
1063
+ topology.declaredSource === "recording-declared" && /* @__PURE__ */ jsx5(
1064
+ "div",
1065
+ {
1066
+ "data-testid": "declared-complete-note",
1067
+ style: { flex: "1 1 100%", color: T.textMuted, lineHeight: 1.35 },
1068
+ children: "The author's complete declared map \u2014 this recording carries it (skill.graph_declared)."
1069
+ }
1070
+ ),
1071
+ !topology.declaredComplete && /* @__PURE__ */ jsx5("div", { style: { flex: "1 1 100%", color: T.warning, lineHeight: 1.35 }, children: topology.declaredSource === "recording" ? "Declared edges shown are only the ones this recording named \u2014 a recording names an edge once it fires, so the author may have drawn more." : "This recording named no declared edges; every line here is a hop that was observed." })
1023
1072
  ]
1024
1073
  }
1025
1074
  );
@@ -1326,4 +1375,4 @@ export {
1326
1375
  SKILL_GRAPH_READS,
1327
1376
  SkillGraphDebugger
1328
1377
  };
1329
- //# sourceMappingURL=chunk-S7FLEXJ3.js.map
1378
+ //# sourceMappingURL=chunk-F473UD5M.js.map