agentfootprint-lens 0.35.0 → 0.36.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
@@ -503,6 +503,64 @@ pane.
503
503
 
504
504
  ---
505
505
 
506
+ ## Scrubbing by group — the active group is a named place
507
+
508
+ One causal trace, replayed at two zoom levels. On the **per-step** ruler each
509
+ commit is a stop; on the **grouped** ruler each boundary is a stop, and ◀ ▶ moves
510
+ a whole group at a time.
511
+
512
+ The chart used to paint both the same way, and that was wrong for the second one.
513
+ A stage's styling is by TYPE — the LLM call carries a hero emphasis (accent
514
+ border, tint, glow) and the cursor's one node pulses — so landing on a group of
515
+ six nodes pulled the eye to the LLM box. The group, the thing the ruler had
516
+ actually moved by, never read as the position at all.
517
+
518
+ Pass `granularity="group"` and it does:
519
+
520
+ - **one accent for every member.** An LLM call, a tool and a context pill light
521
+ identically — same tint, same intensity. What a node IS stays legible in its
522
+ icon and its shape; how loud it is no longer depends on its type.
523
+ - **everything else recedes uniformly** — one dim, not a second ranking.
524
+ - **a boundary is drawn around the members**, from their real measured positions,
525
+ with the group's **name** on its top edge. Scrubbing group to group animates it
526
+ (and doesn't, under `prefers-reduced-motion: reduce`).
527
+
528
+ The name is `groupDisplayName` — the same spelling the WHAT HAPPENED boundary
529
+ list uses. One place, one name.
530
+
531
+ ```tsx
532
+ <Lens recorder={recorder} runner={runner} granularity="group" />
533
+ ```
534
+
535
+ Or, in a shell that owns its own canvas and its own cursor:
536
+
537
+ ```tsx
538
+ import { LensFlow, useChartGroup } from 'agentfootprint-lens';
539
+
540
+ function WhyLensChart({ recorder, chart, cursorCommitIdx }) {
541
+ // The group the cursor stands in, as chart node ids. Derived from the
542
+ // boundary ranges the grouped ruler already computes its stops from —
543
+ // no extra fetch, no second cursor.
544
+ const group = useChartGroup(recorder, cursorCommitIdx);
545
+ return <LensFlow chart={chart} granularity="group" activeGroup={group} />;
546
+ }
547
+ ```
548
+
549
+ `granularity` defaults to `'step'`, and on that path nothing changes: no classes,
550
+ no boundary, the same chart the Flow Lens has always drawn. `'group'` at a commit
551
+ no boundary encloses also renders as `'step'` — a mode with nothing to draw draws
552
+ nothing, rather than boxing the whole chart.
553
+
554
+ Restyle it with one variable (`--lens-group-accent`, falling back to
555
+ `--fp-group-accent`), or target the classes directly: `.lens-group-node--member`,
556
+ `.lens-group-node--outsider`, `.lens-group-boundary`, `.lens-group-boundary-name`.
557
+
558
+ Headless: `activeChartGroup({ groups, commits, commitIdx })` on
559
+ `agentfootprint-lens/core` is the pure function behind the hook — same answer for
560
+ a Vue or CLI shell.
561
+
562
+ ---
563
+
506
564
  ## Theming
507
565
 
508
566
  **Lens inherits theme tokens from your app via CSS variables.** Set `--fp-*`
@@ -614,6 +672,25 @@ graph in one call. Returns an unsubscribe. Call it once per run.
614
672
  | `chart` | `LensFlowProps['chart']?` | Render YOUR graph instead of the derived one. |
615
673
  | `stepGraph` | `StepGraph?` | Bring your own step graph; by default Lens uses the recorder's. |
616
674
  | `toolChoice` | `ToolChoiceSource?` | Mount the per-iteration tool-choice panel. |
675
+ | `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). |
676
+
677
+ ### `<LensFlow>` — the chart canvas on its own
678
+
679
+ The chart without the shell, for consumers who own their layout and their
680
+ cursor. Takes `chart`, the runtime overlay, the cursor, and — for the grouped
681
+ ruler — `granularity="group"` plus `activeGroup` (from `useChartGroup`). Every
682
+ other prop is unchanged by group mode.
683
+
684
+ ### `useChartGroup(recorder, commitIdx, options?)` / `activeChartGroup(...)`
685
+
686
+ The group the cursor stands in, resolved to CHART NODE IDS: the boundary's
687
+ commit range read off the recording, each commit's `runtimeStageId` stripped of
688
+ its `#executionIndex` (the id rule every chart-click and co-active highlight in
689
+ the ecosystem already uses), plus the group's own mount. Returns `undefined`
690
+ when no boundary encloses the cursor. `options.includeRoot` opts into the
691
+ synthetic Run root, which is off by default because a box around the whole chart
692
+ states nothing. `activeChartGroup` is the pure, React-free twin on
693
+ `agentfootprint-lens/core`.
617
694
 
618
695
  ### `observeRecording(recording, options?)`
619
696
 
@@ -2296,6 +2296,126 @@ function selectToolChoiceCall(calls, cursorRuntimeStageId, cursorKind) {
2296
2296
  return within ?? prev;
2297
2297
  }
2298
2298
 
2299
+ // src/core/group/groupDisplayName.ts
2300
+ function groupDisplayName(label) {
2301
+ const named = label.subflowName ?? label.compositionName ?? label.primitiveKind;
2302
+ if (named !== void 0 && named !== "") return named;
2303
+ if (label.type === "run.entry") return "Run";
2304
+ return label.runtimeStageId;
2305
+ }
2306
+ function groupDisplayNameForLabel(label) {
2307
+ return groupDisplayName(label);
2308
+ }
2309
+
2310
+ // src/core/group/Group.ts
2311
+ function groupContainsCommit(group, commitIdx) {
2312
+ if (commitIdx < group.opensAtCommitIdx) return false;
2313
+ if (group.closesAtCommitIdx === void 0) return true;
2314
+ return commitIdx <= group.closesAtCommitIdx;
2315
+ }
2316
+
2317
+ // src/core/group/activeChartGroup.ts
2318
+ function chartNodeIdOf(runtimeStageId) {
2319
+ const hash = runtimeStageId.lastIndexOf("#");
2320
+ return hash < 0 ? runtimeStageId : runtimeStageId.slice(0, hash);
2321
+ }
2322
+ function activeChartGroup(args) {
2323
+ const { groups, commits, commitIdx, includeRoot = false } = args;
2324
+ if (!Number.isFinite(commitIdx) || commitIdx < 0) return void 0;
2325
+ let best;
2326
+ for (const group of groups) {
2327
+ if (group.isRoot && !includeRoot) continue;
2328
+ if (!groupContainsCommit(group, commitIdx)) continue;
2329
+ if (best === void 0) {
2330
+ best = group;
2331
+ continue;
2332
+ }
2333
+ if (group.depth > best.depth) best = group;
2334
+ else if (group.depth === best.depth && group.opensAtCommitIdx >= best.opensAtCommitIdx) best = group;
2335
+ }
2336
+ if (best === void 0) return void 0;
2337
+ const memberNodeIds = /* @__PURE__ */ new Set();
2338
+ memberNodeIds.add(chartNodeIdOf(best.runtimeGroupId));
2339
+ const from = Math.max(0, best.opensAtCommitIdx);
2340
+ const to = Math.min(commits.length - 1, best.closesAtCommitIdx ?? commits.length - 1);
2341
+ for (let i = from; i <= to; i++) {
2342
+ const rid = commits[i]?.runtimeStageId;
2343
+ if (rid === void 0 || rid === "") continue;
2344
+ memberNodeIds.add(chartNodeIdOf(rid));
2345
+ }
2346
+ return {
2347
+ runtimeGroupId: best.runtimeGroupId,
2348
+ name: best.name,
2349
+ memberNodeIds,
2350
+ opensAtCommitIdx: best.opensAtCommitIdx,
2351
+ closesAtCommitIdx: best.closesAtCommitIdx,
2352
+ depth: best.depth
2353
+ };
2354
+ }
2355
+
2356
+ // src/core/group/buildGroups.ts
2357
+ function samePath(a, b) {
2358
+ return a.length === b.length && a.every((s, i) => s === b[i]);
2359
+ }
2360
+ function buildGroups(boundaryIndex) {
2361
+ const all = boundaryIndex.overlapping(0, Number.MAX_SAFE_INTEGER);
2362
+ if (all.length === 0) return [];
2363
+ const seen = /* @__PURE__ */ new Set();
2364
+ const result = [];
2365
+ for (let i = 0; i < all.length; i++) {
2366
+ const entry = all[i];
2367
+ const label = entry.label;
2368
+ if (seen.has(label.runtimeStageId)) continue;
2369
+ seen.add(label.runtimeStageId);
2370
+ let parentGroupId;
2371
+ if (label.type === "subflow.entry") {
2372
+ const enclosing = boundaryIndex.enclosing(entry.startIdx);
2373
+ const parentPath = label.subflowPath.slice(0, -1);
2374
+ for (let j = enclosing.length - 1; j >= 0; j--) {
2375
+ const cand = enclosing[j].label;
2376
+ if (cand.runtimeStageId === label.runtimeStageId) continue;
2377
+ if (samePath(cand.subflowPath, parentPath)) {
2378
+ parentGroupId = cand.runtimeStageId;
2379
+ break;
2380
+ }
2381
+ }
2382
+ if (parentGroupId === void 0) {
2383
+ for (let j = enclosing.length - 1; j >= 0; j--) {
2384
+ const cand = enclosing[j].label;
2385
+ if (cand.runtimeStageId === label.runtimeStageId) continue;
2386
+ parentGroupId = cand.runtimeStageId;
2387
+ break;
2388
+ }
2389
+ }
2390
+ } else if (label.type === "composition.start") {
2391
+ const enclosing = boundaryIndex.enclosing(entry.startIdx);
2392
+ for (let j = enclosing.length - 1; j >= 0; j--) {
2393
+ const cand = enclosing[j].label;
2394
+ if (cand.runtimeStageId === label.runtimeStageId) continue;
2395
+ parentGroupId = cand.runtimeStageId;
2396
+ break;
2397
+ }
2398
+ }
2399
+ const isRoot = label.type === "run.entry";
2400
+ const name = groupDisplayNameForLabel(label);
2401
+ const compositionKind = label.type === "composition.start" ? label.compositionKind : void 0;
2402
+ result.push({
2403
+ runtimeGroupId: label.runtimeStageId,
2404
+ name,
2405
+ parentGroupId,
2406
+ subflowPath: label.subflowPath,
2407
+ depth: label.depth,
2408
+ opensAtCommitIdx: entry.startIdx,
2409
+ closesAtCommitIdx: entry.endIdx,
2410
+ isRoot,
2411
+ ...compositionKind !== void 0 ? { compositionKind } : {},
2412
+ ...label.slotKind !== void 0 ? { slotKind: label.slotKind } : {},
2413
+ ...label.primitiveKind !== void 0 ? { primitiveKind: label.primitiveKind } : {}
2414
+ });
2415
+ }
2416
+ return result;
2417
+ }
2418
+
2299
2419
  // src/core/translate/helpers/makeNodeId.ts
2300
2420
  function makeRootNodeId(kind, id) {
2301
2421
  return `${kind.toLowerCase()}:${id}`;
@@ -3486,6 +3606,12 @@ export {
3486
3606
  selectCommentaryAt,
3487
3607
  selectCommentaryRanges,
3488
3608
  selectToolChoiceCall,
3609
+ groupDisplayName,
3610
+ groupDisplayNameForLabel,
3611
+ groupContainsCommit,
3612
+ chartNodeIdOf,
3613
+ activeChartGroup,
3614
+ buildGroups,
3489
3615
  makeRootNodeId,
3490
3616
  makeChildNodeId,
3491
3617
  translateAgent,
@@ -3525,4 +3651,4 @@ export {
3525
3651
  decisionSentence,
3526
3652
  isConsentDecision
3527
3653
  };
3528
- //# sourceMappingURL=chunk-PBRAJNVI.js.map
3654
+ //# sourceMappingURL=chunk-EGU4GZVH.js.map