agentfootprint-lens 0.49.0 → 0.50.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
@@ -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,6 +1256,23 @@ 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
 
@@ -1377,7 +1406,7 @@ graph in one call. Returns an unsubscribe. Call it once per run.
1377
1406
  | `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
1407
  | `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
1408
  | `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). |
1409
+ | `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
1410
 
1382
1411
  ### `<LensFlow>` — the chart canvas on its own
1383
1412
 
@@ -634,16 +634,67 @@ interface LensProps {
634
634
  *
635
635
  * Same idea as `<TraceExplorerShell slots>`: keep the shipped layout and the
636
636
  * shipped cursor, replace what one pane renders.
637
+ *
638
+ * THE LAW: **a slot fills a PANE, never the CHROME around it.** The rail's tab
639
+ * strip, the one cursor and the collapse pill belong to the library, because
640
+ * chrome is how the library adds capability over time — a consumer that fills a
641
+ * slot must still receive every capability the rail gains later. That is why
642
+ * `detail` is ONE TAB and not a takeover, and why `detailOnly` is an opt-out
643
+ * rather than the default.
637
644
  */
638
645
  interface LensSlots {
639
646
  /**
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.
647
+ * Render the right column's FIRST TAB — your own pane, beside the library's.
648
+ *
649
+ * Your component renders inside the shipped column (its width, borders,
650
+ * collapse pill and scroll behaviour are unchanged) and inside the shipped
651
+ * rail: the tab strip stays, your pane becomes the first tab and is selected
652
+ * by default, and "What happened", "Served" and "Bookmarks" stay reachable
653
+ * beside it. Switching tabs never moves the cursor and never unmounts your
654
+ * pane — it keeps its own state.
655
+ *
656
+ * Before 0.50.0 a supplied slot replaced the whole rail, tab strip included,
657
+ * so a slot consumer could not reach anything the rail gained afterwards.
658
+ * That was the defect this shape fixes; `detailOnly` is the way back.
643
659
  *
644
660
  * Omit it and the built-in timeline renders exactly as before.
661
+ *
662
+ * ```tsx
663
+ * <Lens recorder={recorder} slots={{ detail: MyBands, detailLabel: 'Bands' }} />
664
+ * // rail: [ Bands* | What happened | Served | Bookmarks ]
665
+ * ```
645
666
  */
646
667
  readonly detail?: React__default.ComponentType<LensDetailSlotProps>;
668
+ /**
669
+ * What the `detail` tab is CALLED. Default: `"Details"`.
670
+ *
671
+ * WHY it exists: the tab strip names each reading in the reader's own words,
672
+ * and "Details" is the library's generic word for someone else's pane. An app
673
+ * whose pane is a stack of SEO bands should say so — the strip is read left to
674
+ * right and an unnamed tab makes the app's own reading the anonymous one.
675
+ *
676
+ * A plain string, rendered as the tab's text; it is the accessible name too.
677
+ * Ignored when no `detail` slot is supplied.
678
+ *
679
+ * ```tsx
680
+ * slots={{ detail: MyBands, detailLabel: 'SEO bands' }}
681
+ * ```
682
+ */
683
+ readonly detailLabel?: string;
684
+ /**
685
+ * OPT OUT of the tab strip: the `detail` slot takes the whole rail, as it did
686
+ * before 0.50.0. Default `false`.
687
+ *
688
+ * For the app with a full-height custom layout of its own — a pane that owns
689
+ * its scroll and its header and would read as a second chrome under the
690
+ * library's strip. The cost is stated plainly: nothing the rail gains later
691
+ * (Served, Bookmarks, whatever follows) can reach that consumer, and neither
692
+ * can the library's own timeline. Prefer the tabs; take this only when the
693
+ * layout genuinely demands it.
694
+ *
695
+ * Ignored when no `detail` slot is supplied.
696
+ */
697
+ readonly detailOnly?: boolean;
647
698
  }
648
699
  /**
649
700
  * What the `detail` slot receives — the cursor, in the units a detail pane
@@ -634,16 +634,67 @@ interface LensProps {
634
634
  *
635
635
  * Same idea as `<TraceExplorerShell slots>`: keep the shipped layout and the
636
636
  * shipped cursor, replace what one pane renders.
637
+ *
638
+ * THE LAW: **a slot fills a PANE, never the CHROME around it.** The rail's tab
639
+ * strip, the one cursor and the collapse pill belong to the library, because
640
+ * chrome is how the library adds capability over time — a consumer that fills a
641
+ * slot must still receive every capability the rail gains later. That is why
642
+ * `detail` is ONE TAB and not a takeover, and why `detailOnly` is an opt-out
643
+ * rather than the default.
637
644
  */
638
645
  interface LensSlots {
639
646
  /**
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.
647
+ * Render the right column's FIRST TAB — your own pane, beside the library's.
648
+ *
649
+ * Your component renders inside the shipped column (its width, borders,
650
+ * collapse pill and scroll behaviour are unchanged) and inside the shipped
651
+ * rail: the tab strip stays, your pane becomes the first tab and is selected
652
+ * by default, and "What happened", "Served" and "Bookmarks" stay reachable
653
+ * beside it. Switching tabs never moves the cursor and never unmounts your
654
+ * pane — it keeps its own state.
655
+ *
656
+ * Before 0.50.0 a supplied slot replaced the whole rail, tab strip included,
657
+ * so a slot consumer could not reach anything the rail gained afterwards.
658
+ * That was the defect this shape fixes; `detailOnly` is the way back.
643
659
  *
644
660
  * Omit it and the built-in timeline renders exactly as before.
661
+ *
662
+ * ```tsx
663
+ * <Lens recorder={recorder} slots={{ detail: MyBands, detailLabel: 'Bands' }} />
664
+ * // rail: [ Bands* | What happened | Served | Bookmarks ]
665
+ * ```
645
666
  */
646
667
  readonly detail?: React__default.ComponentType<LensDetailSlotProps>;
668
+ /**
669
+ * What the `detail` tab is CALLED. Default: `"Details"`.
670
+ *
671
+ * WHY it exists: the tab strip names each reading in the reader's own words,
672
+ * and "Details" is the library's generic word for someone else's pane. An app
673
+ * whose pane is a stack of SEO bands should say so — the strip is read left to
674
+ * right and an unnamed tab makes the app's own reading the anonymous one.
675
+ *
676
+ * A plain string, rendered as the tab's text; it is the accessible name too.
677
+ * Ignored when no `detail` slot is supplied.
678
+ *
679
+ * ```tsx
680
+ * slots={{ detail: MyBands, detailLabel: 'SEO bands' }}
681
+ * ```
682
+ */
683
+ readonly detailLabel?: string;
684
+ /**
685
+ * OPT OUT of the tab strip: the `detail` slot takes the whole rail, as it did
686
+ * before 0.50.0. Default `false`.
687
+ *
688
+ * For the app with a full-height custom layout of its own — a pane that owns
689
+ * its scroll and its header and would read as a second chrome under the
690
+ * library's strip. The cost is stated plainly: nothing the rail gains later
691
+ * (Served, Bookmarks, whatever follows) can reach that consumer, and neither
692
+ * can the library's own timeline. Prefer the tabs; take this only when the
693
+ * layout genuinely demands it.
694
+ *
695
+ * Ignored when no `detail` slot is supplied.
696
+ */
697
+ readonly detailOnly?: boolean;
647
698
  }
648
699
  /**
649
700
  * What the `detail` slot receives — the cursor, in the units a detail pane
@@ -191,7 +191,7 @@ function withGroupEmphasisAll(nodeTypes, stageNode) {
191
191
 
192
192
  // src/react/lensStyles.ts
193
193
  var STYLE_MARKER = "data-lens-styles";
194
- var STYLE_VERSION = "v5";
194
+ var STYLE_VERSION = "v6";
195
195
  var LENS_STYLESHEET = `
196
196
  /* The streaming caret's blink, and the in-flight edge's dash march. */
197
197
  @keyframes lens-blink { 50% { opacity: 0; } }
@@ -773,6 +773,16 @@ var LENS_STYLESHEET = `
773
773
  font-weight: 600;
774
774
  line-height: 1.5;
775
775
  }
776
+ /* \u2500\u2500 The right rail's tab strip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
777
+ The tabs are keyboard-reachable buttons, and this is the ring that says so.
778
+ A tab strip whose focus is invisible is a strip only a mouse can use, and
779
+ since 0.50.0 a host's own pane is one of these tabs \u2014 so the reader who
780
+ tabs to it must see where they are. */
781
+ .lens-rail-tab:focus-visible {
782
+ outline: 2px solid ${T.primary};
783
+ outline-offset: -2px;
784
+ }
785
+
776
786
  @media (prefers-reduced-motion: reduce) {
777
787
  .lens-group-boundary,
778
788
  .lens-group-node--member::after,
@@ -4809,7 +4819,11 @@ var EngineerView = ({
4809
4819
  const rowRef = useRef6(null);
4810
4820
  const stacked = useNarrowRow(rowRef);
4811
4821
  const DetailSlot = slots?.detail;
4812
- const [railTab, setRailTab] = useState8("happened");
4822
+ const detailLabel = slots?.detailLabel ?? "Details";
4823
+ const detailOnly = DetailSlot !== void 0 && slots?.detailOnly === true;
4824
+ const [railTabChoice, setRailTabChoice] = useState8(void 0);
4825
+ const defaultRailTab = DetailSlot !== void 0 ? "detail" : "happened";
4826
+ const railTab = railTabChoice !== void 0 && (railTabChoice !== "detail" || DetailSlot !== void 0) && (railTabChoice !== "bookmarks" || bookmarks !== void 0) ? railTabChoice : defaultRailTab;
4813
4827
  const servedRunner = runner ?? recorder.observedRunner();
4814
4828
  const jumpToRuntimeStageId = useCallback6(
4815
4829
  (runtimeStageId) => {
@@ -4818,6 +4832,21 @@ var EngineerView = ({
4818
4832
  },
4819
4833
  [cursorPort, focusStep, onFocusChange]
4820
4834
  );
4835
+ const detailPane = DetailSlot !== void 0 ? /* @__PURE__ */ jsx14(
4836
+ DetailSlot,
4837
+ {
4838
+ step: focusStep,
4839
+ totalSteps: total,
4840
+ cursorRuntimeStageId,
4841
+ commitIdx: cursorPositions[focusStep]?.commitIdx ?? -1,
4842
+ label: cursorPositions[focusStep]?.label ?? "",
4843
+ ...cursorPositions[focusStep]?.kind ? { kind: cursorPositions[focusStep].kind } : {},
4844
+ ...cursorFocusedNode ? { node: cursorFocusedNode } : {},
4845
+ relatedNodes: cursorRelatedNodes,
4846
+ recorder,
4847
+ onNavigate: onFocusChange
4848
+ }
4849
+ ) : null;
4821
4850
  return /* @__PURE__ */ jsxs13(
4822
4851
  "div",
4823
4852
  {
@@ -5109,22 +5138,21 @@ var EngineerView = ({
5109
5138
  borderTop: `1px solid ${T.border}`
5110
5139
  } : {}
5111
5140
  },
5112
- children: DetailSlot ? /* @__PURE__ */ jsx14(
5113
- DetailSlot,
5114
- {
5115
- step: focusStep,
5116
- totalSteps: total,
5117
- cursorRuntimeStageId,
5118
- commitIdx: cursorPositions[focusStep]?.commitIdx ?? -1,
5119
- label: cursorPositions[focusStep]?.label ?? "",
5120
- ...cursorPositions[focusStep]?.kind ? { kind: cursorPositions[focusStep].kind } : {},
5121
- ...cursorFocusedNode ? { node: cursorFocusedNode } : {},
5122
- relatedNodes: cursorRelatedNodes,
5123
- recorder,
5124
- onNavigate: onFocusChange
5125
- }
5126
- ) : /* @__PURE__ */ jsxs13(Fragment6, { children: [
5141
+ children: detailOnly ? detailPane : /* @__PURE__ */ jsxs13(Fragment6, { children: [
5127
5142
  /* @__PURE__ */ jsxs13("div", { role: "tablist", "aria-label": "Right rail", style: railTabsStyle, children: [
5143
+ DetailSlot !== void 0 && /* @__PURE__ */ jsx14(
5144
+ "button",
5145
+ {
5146
+ type: "button",
5147
+ role: "tab",
5148
+ "aria-selected": railTab === "detail",
5149
+ "data-testid": "rail-tab-detail",
5150
+ className: "lens-rail-tab",
5151
+ style: railTabStyle(railTab === "detail"),
5152
+ onClick: () => setRailTabChoice("detail"),
5153
+ children: detailLabel
5154
+ }
5155
+ ),
5128
5156
  /* @__PURE__ */ jsx14(
5129
5157
  "button",
5130
5158
  {
@@ -5132,8 +5160,9 @@ var EngineerView = ({
5132
5160
  role: "tab",
5133
5161
  "aria-selected": railTab === "happened",
5134
5162
  "data-testid": "rail-tab-happened",
5163
+ className: "lens-rail-tab",
5135
5164
  style: railTabStyle(railTab === "happened"),
5136
- onClick: () => setRailTab("happened"),
5165
+ onClick: () => setRailTabChoice("happened"),
5137
5166
  children: "What happened"
5138
5167
  }
5139
5168
  ),
@@ -5144,8 +5173,9 @@ var EngineerView = ({
5144
5173
  role: "tab",
5145
5174
  "aria-selected": railTab === "served",
5146
5175
  "data-testid": "rail-tab-served",
5176
+ className: "lens-rail-tab",
5147
5177
  style: railTabStyle(railTab === "served"),
5148
- onClick: () => setRailTab("served"),
5178
+ onClick: () => setRailTabChoice("served"),
5149
5179
  children: "Served"
5150
5180
  }
5151
5181
  ),
@@ -5156,13 +5186,28 @@ var EngineerView = ({
5156
5186
  role: "tab",
5157
5187
  "aria-selected": railTab === "bookmarks",
5158
5188
  "data-testid": "rail-tab-bookmarks",
5189
+ className: "lens-rail-tab",
5159
5190
  style: railTabStyle(railTab === "bookmarks"),
5160
- onClick: () => setRailTab("bookmarks"),
5191
+ onClick: () => setRailTabChoice("bookmarks"),
5161
5192
  children: "Bookmarks"
5162
5193
  }
5163
5194
  )
5164
5195
  ] }),
5165
- railTab === "bookmarks" && bookmarks !== void 0 ? /* @__PURE__ */ jsx14(
5196
+ DetailSlot !== void 0 && /* @__PURE__ */ jsx14(
5197
+ "div",
5198
+ {
5199
+ "data-testid": "rail-pane-detail",
5200
+ style: {
5201
+ display: railTab === "detail" ? "flex" : "none",
5202
+ flexDirection: "column",
5203
+ flex: "1 1 auto",
5204
+ minHeight: 0,
5205
+ overflow: "hidden"
5206
+ },
5207
+ children: detailPane
5208
+ }
5209
+ ),
5210
+ railTab === "detail" ? null : railTab === "bookmarks" && bookmarks !== void 0 ? /* @__PURE__ */ jsx14(
5166
5211
  BookmarksTab,
5167
5212
  {
5168
5213
  bookmarks: bookmarks.bookmarks,
@@ -6042,4 +6087,4 @@ export {
6042
6087
  useBookmarkSidecar,
6043
6088
  Lens
6044
6089
  };
6045
- //# sourceMappingURL=chunk-KS4AIWQJ.js.map
6090
+ //# sourceMappingURL=chunk-TBYKXQSA.js.map