agentfootprint-lens 0.36.0 → 0.37.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.
package/README.md CHANGED
@@ -561,6 +561,105 @@ a Vue or CLI shell.
561
561
 
562
562
  ---
563
563
 
564
+ ## Driving the cursor from your app
565
+
566
+ **Omitting these props keeps the lens self-driving.** It holds the position
567
+ itself, follows the live run, and needs nothing from you — that is the default
568
+ and it has not changed.
569
+
570
+ Pass `step` and you own the cursor. That is the difference between a lens that
571
+ happens to be on your page and a lens that is part of your app: two tabs can
572
+ show the same moment, a "jump to the failure" button in your own UI can move it,
573
+ and switching tabs no longer loses the position.
574
+
575
+ ```tsx
576
+ function Debugger({ recorder, runner }) {
577
+ // ONE cursor, held by you. Both lenses show the same moment.
578
+ const [step, setStep] = useState(0);
579
+
580
+ return (
581
+ <>
582
+ <Lens recorder={recorder} runner={runner} step={step} onStepChange={setStep} />
583
+ <Lens recorder={recorder} runner={runner} step={step} onStepChange={setStep} view="analyst" />
584
+ <button onClick={() => setStep(0)}>Back to the start</button>
585
+ </>
586
+ );
587
+ }
588
+ ```
589
+
590
+ Just want to *watch* the cursor? Pass `onStepChange` alone. The lens keeps
591
+ owning the state and calls you on every move — the same contract
592
+ `<TraceExplorerShell onSelectionChange>` has in `footprint-explainable-ui`.
593
+
594
+ ### The unit is a step, and the callback carries the rest
595
+
596
+ A **step** is one stop on the lens's scrub axis — the same number the transport
597
+ counts ("3 / 12"), the same one the WHAT HAPPENED rail dots. Valid values are
598
+ `0 … totalSteps - 1`, and `totalSteps` **grows** while a run is live.
599
+
600
+ Every call hands you the other two units of the same position, so you never
601
+ invert the mapping yourself:
602
+
603
+ ```ts
604
+ onStepChange(step, at)
605
+ // at = { step, totalSteps, runtimeStageId, commitIdx, label, kind?, clamped }
606
+ ```
607
+
608
+ - `at.runtimeStageId` — footprintjs's address, the string
609
+ `<TraceExplorerShell selectedRuntimeStageId>` and `<RunSlider cursorRuntimeStageId>` take.
610
+ - `at.commitIdx` — the commit-log index the position anchors to.
611
+
612
+ Why the step is the controlled unit and not one of those: it is the only one
613
+ that is **one-to-one** with a position the lens can show. A group's start and
614
+ its end are the same group, so "Run · start" and "Run · end" are both
615
+ `__root__#0`; a parallel fork's branches open at the same commit. Address the
616
+ cursor by either and the second of every such pair becomes unreachable — half
617
+ the positions silently unselectable, which is the exact failure this prop
618
+ exists to prevent.
619
+
620
+ Move it out of range and the lens **clamps and says so**: it renders the nearest
621
+ real position, calls `onStepChange(clamped, { clamped: true })`, and warns once
622
+ on the console. It clamps rather than refuses because the axis grows under you —
623
+ a step you stored from a finished run is a perfectly good value that the same
624
+ run, earlier, does not have yet. Store what the callback hands back and the two
625
+ cursors agree again.
626
+
627
+ Every mover reports: the step strip, ◀ ▶ ⟳Live, the arrow keys, a chart node
628
+ click, a WHAT HAPPENED moment, a provenance jump, and the auto-advance that
629
+ follows a live run. One cursor, one funnel — a mover that moved without telling
630
+ you would be a second cursor wearing the first one's clothes.
631
+
632
+ ---
633
+
634
+ ## Rendering your own detail pane
635
+
636
+ `slots.detail` replaces the CONTENT of the shipped right column. The column
637
+ itself — its width, its border, its collapse pill, its cursor — is unchanged.
638
+ Omit `slots` and the built-in timeline renders exactly as before.
639
+
640
+ ```tsx
641
+ const Detail: React.FC<LensDetailSlotProps> = ({ step, cursorRuntimeStageId, node, onNavigate }) => (
642
+ <div>
643
+ <h3>{node?.label ?? 'nothing focused'}</h3>
644
+ <code>{cursorRuntimeStageId}</code>
645
+ <button onClick={() => onNavigate(0)}>rewind</button>
646
+ </div>
647
+ );
648
+
649
+ <Lens recorder={recorder} runner={runner} slots={{ detail: Detail }} />
650
+ ```
651
+
652
+ The slot receives the cursor in every unit (`step`, `totalSteps`,
653
+ `cursorRuntimeStageId`, `commitIdx`, `label`, `kind`), the `StepNode` it sits on
654
+ and the ones that ran inside its scope (`node`, `relatedNodes`), the `recorder`
655
+ for anything else, and `onNavigate` — the same funnel every built-in mover uses,
656
+ so your pane moves the ONE cursor rather than starting a second one.
657
+
658
+ Keep the slots object stable across renders (module scope or `useMemo`), same as
659
+ `<TraceExplorerShell slots>`.
660
+
661
+ ---
662
+
564
663
  ## Theming
565
664
 
566
665
  **Lens inherits theme tokens from your app via CSS variables.** Set `--fp-*`
@@ -637,9 +736,22 @@ if a strict CSP blocks the automatic injection.
637
736
 
638
737
  ## Responsive
639
738
 
640
- Lens resizes to whatever space you give it. Below ~640px wide it stacks panels
641
- vertically (like `<ExplainableShell>` does). Drop it in a splitter, a drawer, or
642
- a full-screen tab — no config needed.
739
+ Lens resizes to whatever space you give it. The engineer view is a chart column
740
+ beside an inspector with a 300px minimum, and below **`LENS_NARROW_BREAKPOINT`
741
+ (690px of available row width)** that pair stops fitting — so the columns
742
+ **stack** instead of clipping. Nothing is hidden and nothing is cut off; the
743
+ same panes are read top to bottom instead of left to right. A split panel
744
+ dragged down to 392px gets a readable lens, not a sliver of one.
745
+
746
+ The threshold is exported, so a shell that lays out around Lens can use the same
747
+ number rather than guessing it:
748
+
749
+ ```ts
750
+ import { LENS_NARROW_BREAKPOINT, isNarrowRow } from 'agentfootprint-lens';
751
+ ```
752
+
753
+ Drop it in a splitter, a drawer, or a full-screen tab — no config needed, both
754
+ themes.
643
755
 
644
756
  ---
645
757
 
@@ -673,6 +785,9 @@ graph in one call. Returns an unsubscribe. Call it once per run.
673
785
  | `stepGraph` | `StepGraph?` | Bring your own step graph; by default Lens uses the recorder's. |
674
786
  | `toolChoice` | `ToolChoiceSource?` | Mount the per-iteration tool-choice panel. |
675
787
  | `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). |
788
+ | `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). |
789
+ | `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`. |
790
+ | `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). |
676
791
 
677
792
  ### `<LensFlow>` — the chart canvas on its own
678
793