agentfootprint-lens 0.17.0 → 0.18.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/dist/index.cjs CHANGED
@@ -395,6 +395,8 @@ __export(src_exports, {
395
395
  LensRecorder: () => LensRecorder,
396
396
  LensSnapshotRecorder: () => LensSnapshotRecorder,
397
397
  RunTreeView: () => RunTreeView,
398
+ SKILL_GRAPH_START_ID: () => SKILL_GRAPH_START_ID,
399
+ SkillGraphFlow: () => SkillGraphFlow,
398
400
  SummaryCard: () => SummaryCard,
399
401
  TimeTravel: () => TimeTravel,
400
402
  buildLLMText: () => buildLLMText,
@@ -405,6 +407,7 @@ __export(src_exports, {
405
407
  humanizeWith: () => humanizeWith,
406
408
  isContextEngineering: () => isContextEngineering,
407
409
  layoutLensGraph: () => layoutLensGraph,
410
+ layoutSkillGraph: () => layoutSkillGraph,
408
411
  lensGroupTranslator: () => lensGroupTranslator,
409
412
  lensRecorder: () => lensRecorder,
410
413
  lensSnapshotRecorder: () => lensSnapshotRecorder,
@@ -5811,12 +5814,432 @@ function shortType(type) {
5811
5814
  return type.replace(/^agentfootprint\./, "");
5812
5815
  }
5813
5816
 
5814
- // src/react/hooks/useStepFocus.ts
5817
+ // src/react/SkillGraphFlow.tsx
5815
5818
  var import_react13 = require("react");
5819
+ var import_react14 = require("@xyflow/react");
5820
+ var import_style2 = require("@xyflow/react/dist/style.css");
5821
+
5822
+ // src/react/skillGraphFlowLayout.ts
5823
+ var import_dagre2 = __toESM(require("dagre"), 1);
5824
+ var SKILL_GRAPH_START_ID = "__start__";
5825
+ var SIZES = {
5826
+ start: { width: 104, height: 40 },
5827
+ predicate: { width: 188, height: 96 },
5828
+ skill: { width: 192, height: 56 }
5829
+ };
5830
+ function sizeFor(kind) {
5831
+ return SIZES[kind];
5832
+ }
5833
+ function layoutSkillGraph(graph, opts = {}) {
5834
+ const showStart = opts.showStart ?? true;
5835
+ const g = new import_dagre2.default.graphlib.Graph({ multigraph: true });
5836
+ g.setGraph({
5837
+ rankdir: "TB",
5838
+ ranksep: opts.rankSep ?? 64,
5839
+ nodesep: opts.nodeSep ?? 40,
5840
+ marginx: 8,
5841
+ marginy: 8
5842
+ });
5843
+ g.setDefaultEdgeLabel(() => ({}));
5844
+ const usesStart = showStart && graph.edges.some((e) => e.from === null);
5845
+ if (usesStart) g.setNode(SKILL_GRAPH_START_ID, { ...SIZES.start });
5846
+ for (const n of graph.nodes) g.setNode(n.id, { ...SIZES[n.kind] });
5847
+ const flowEdges = [];
5848
+ let i = 0;
5849
+ for (const e of graph.edges) {
5850
+ if (e.from === null && !usesStart) continue;
5851
+ const source = e.from === null ? SKILL_GRAPH_START_ID : e.from;
5852
+ if (!g.hasNode(source) || !g.hasNode(e.to)) continue;
5853
+ const id = `sge${i++}:${source}->${e.to}`;
5854
+ g.setEdge(source, e.to, {}, id);
5855
+ flowEdges.push({ id, source, target: e.to, label: e.label, dashed: e.kind === "model" });
5856
+ }
5857
+ import_dagre2.default.layout(g);
5858
+ const toFlow = (id, kind, label) => {
5859
+ const p = g.node(id);
5860
+ return {
5861
+ id,
5862
+ kind,
5863
+ label,
5864
+ x: p.x - p.width / 2,
5865
+ // dagre centers; xyflow positions by top-left
5866
+ y: p.y - p.height / 2,
5867
+ width: p.width,
5868
+ height: p.height
5869
+ };
5870
+ };
5871
+ const nodes = [];
5872
+ if (usesStart) nodes.push(toFlow(SKILL_GRAPH_START_ID, "start", "\u25B6 start"));
5873
+ for (const n of graph.nodes) nodes.push(toFlow(n.id, n.kind, n.label ?? n.id));
5874
+ return { nodes, edges: flowEdges };
5875
+ }
5876
+
5877
+ // src/react/SkillGraphFlow.tsx
5878
+ var import_jsx_runtime10 = require("react/jsx-runtime");
5879
+ var HANDLE_STYLE = {
5880
+ opacity: 0,
5881
+ width: 1,
5882
+ height: 1,
5883
+ border: "none",
5884
+ background: "transparent",
5885
+ pointerEvents: "none"
5886
+ };
5887
+ var StartNode = ({ data }) => {
5888
+ const d = data;
5889
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
5890
+ "div",
5891
+ {
5892
+ style: {
5893
+ ...sizeFor("start"),
5894
+ display: "flex",
5895
+ alignItems: "center",
5896
+ justifyContent: "center",
5897
+ borderRadius: 999,
5898
+ background: T.bgTertiary,
5899
+ color: T.textSecondary,
5900
+ border: `1px solid ${T.border}`,
5901
+ fontFamily: T.fontSans,
5902
+ fontSize: 12,
5903
+ fontWeight: 600,
5904
+ boxSizing: "border-box"
5905
+ },
5906
+ children: [
5907
+ d.label,
5908
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react14.Handle, { type: "source", position: import_react14.Position.Bottom, style: HANDLE_STYLE, isConnectable: false })
5909
+ ]
5910
+ }
5911
+ );
5912
+ };
5913
+ var SkillBoxNode = ({ data }) => {
5914
+ const d = data;
5915
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
5916
+ "div",
5917
+ {
5918
+ style: {
5919
+ ...sizeFor("skill"),
5920
+ display: "flex",
5921
+ alignItems: "center",
5922
+ gap: 8,
5923
+ padding: "0 12px",
5924
+ borderRadius: 10,
5925
+ background: T.bgSecondary,
5926
+ color: T.textPrimary,
5927
+ border: `${d.isSelected ? 2 : 1}px solid ${d.isSelected ? T.srcSkill : T.border}`,
5928
+ boxShadow: d.isSelected ? `0 0 0 3px ${T.srcSkill}33` : "none",
5929
+ fontFamily: T.fontSans,
5930
+ fontSize: 13,
5931
+ fontWeight: 600,
5932
+ boxSizing: "border-box",
5933
+ cursor: "pointer"
5934
+ },
5935
+ children: [
5936
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react14.Handle, { type: "target", position: import_react14.Position.Top, style: HANDLE_STYLE, isConnectable: false }),
5937
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
5938
+ "span",
5939
+ {
5940
+ "aria-hidden": true,
5941
+ style: {
5942
+ flex: "0 0 auto",
5943
+ width: 8,
5944
+ height: 8,
5945
+ borderRadius: 2,
5946
+ background: T.srcSkill
5947
+ }
5948
+ }
5949
+ ),
5950
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: d.label }),
5951
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react14.Handle, { type: "source", position: import_react14.Position.Bottom, style: HANDLE_STYLE, isConnectable: false })
5952
+ ]
5953
+ }
5954
+ );
5955
+ };
5956
+ var PredicateNode = ({ data }) => {
5957
+ const d = data;
5958
+ const { width, height } = sizeFor("predicate");
5959
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { width, height, position: "relative", cursor: "pointer" }, children: [
5960
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react14.Handle, { type: "target", position: import_react14.Position.Top, style: HANDLE_STYLE, isConnectable: false }),
5961
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
5962
+ "div",
5963
+ {
5964
+ style: {
5965
+ position: "absolute",
5966
+ inset: 0,
5967
+ background: T.bgSecondary,
5968
+ border: `${d.isSelected ? 2 : 1}px solid ${d.isSelected ? T.edgeDecision : T.border}`,
5969
+ boxShadow: d.isSelected ? `0 0 0 3px ${T.edgeDecision}33` : "none",
5970
+ clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)"
5971
+ }
5972
+ }
5973
+ ),
5974
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
5975
+ "div",
5976
+ {
5977
+ style: {
5978
+ position: "absolute",
5979
+ inset: 0,
5980
+ display: "flex",
5981
+ alignItems: "center",
5982
+ justifyContent: "center",
5983
+ padding: "0 32px",
5984
+ textAlign: "center",
5985
+ color: T.textPrimary,
5986
+ fontFamily: T.fontSans,
5987
+ fontSize: 12,
5988
+ fontWeight: 600,
5989
+ lineHeight: 1.2,
5990
+ boxSizing: "border-box"
5991
+ },
5992
+ children: d.label
5993
+ }
5994
+ ),
5995
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react14.Handle, { type: "source", position: import_react14.Position.Bottom, style: HANDLE_STYLE, isConnectable: false })
5996
+ ] });
5997
+ };
5998
+ var NODE_TYPES = {
5999
+ sgStart: StartNode,
6000
+ sgPredicate: PredicateNode,
6001
+ sgSkill: SkillBoxNode
6002
+ };
6003
+ function DetailPanel({
6004
+ node,
6005
+ detail,
6006
+ width
6007
+ }) {
6008
+ const panel = {
6009
+ width,
6010
+ flex: `0 0 ${width}px`,
6011
+ borderLeft: `1px solid ${T.border}`,
6012
+ background: T.bgPrimary,
6013
+ color: T.textPrimary,
6014
+ fontFamily: T.fontSans,
6015
+ padding: 16,
6016
+ overflow: "auto",
6017
+ boxSizing: "border-box"
6018
+ };
6019
+ if (!node) {
6020
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("aside", { style: panel, "data-testid": "skill-graph-detail", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: { color: T.textMuted, fontSize: 13, margin: 0 }, children: "Click a node to inspect it. Diamonds are decision predicates; boxes are skills that load just-in-time when their path is chosen." }) });
6021
+ }
6022
+ const isPredicate = node.kind === "predicate";
6023
+ const accent = isPredicate ? T.edgeDecision : T.srcSkill;
6024
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("aside", { style: panel, "data-testid": "skill-graph-detail", children: [
6025
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
6026
+ "span",
6027
+ {
6028
+ style: {
6029
+ fontSize: 11,
6030
+ fontWeight: 700,
6031
+ textTransform: "uppercase",
6032
+ letterSpacing: 0.5,
6033
+ color: accent
6034
+ },
6035
+ children: isPredicate ? "\u25C7 Decision" : "\u25A2 Skill"
6036
+ }
6037
+ ) }),
6038
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h3", { style: { margin: "0 0 12px", fontSize: 16 }, children: detail?.title ?? node.label ?? node.id }),
6039
+ isPredicate && !detail && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("p", { style: { color: T.textSecondary, fontSize: 13, margin: 0 }, children: [
6040
+ "Routes to its ",
6041
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: "yes" }),
6042
+ " / ",
6043
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: "no" }),
6044
+ " subtree based on this predicate, evaluated every iteration."
6045
+ ] }),
6046
+ detail?.description && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: { color: T.textSecondary, fontSize: 13, margin: "0 0 12px" }, children: detail.description }),
6047
+ detail?.meta?.map((row) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { fontSize: 12, marginBottom: 6 }, children: [
6048
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { style: { color: T.textMuted }, children: [
6049
+ row.label,
6050
+ ": "
6051
+ ] }),
6052
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: T.textSecondary }, children: row.value })
6053
+ ] }, row.label)),
6054
+ detail?.tools && detail.tools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { marginTop: 12 }, children: [
6055
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { fontSize: 11, color: T.textMuted, marginBottom: 6 }, children: [
6056
+ "UNLOCKS ",
6057
+ detail.tools.length,
6058
+ " TOOL",
6059
+ detail.tools.length === 1 ? "" : "S"
6060
+ ] }),
6061
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: detail.tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
6062
+ "span",
6063
+ {
6064
+ style: {
6065
+ fontFamily: T.fontMono,
6066
+ fontSize: 11,
6067
+ padding: "2px 6px",
6068
+ borderRadius: 6,
6069
+ background: T.bgTertiary,
6070
+ color: T.textSecondary
6071
+ },
6072
+ children: tool
6073
+ },
6074
+ tool
6075
+ )) })
6076
+ ] }),
6077
+ detail?.body && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
6078
+ "pre",
6079
+ {
6080
+ style: {
6081
+ marginTop: 12,
6082
+ padding: 10,
6083
+ borderRadius: 8,
6084
+ background: T.bgSecondary,
6085
+ color: T.textSecondary,
6086
+ fontFamily: T.fontMono,
6087
+ fontSize: 11.5,
6088
+ lineHeight: 1.45,
6089
+ whiteSpace: "pre-wrap",
6090
+ wordBreak: "break-word",
6091
+ overflow: "auto"
6092
+ },
6093
+ children: detail.body
6094
+ }
6095
+ )
6096
+ ] });
6097
+ }
6098
+ var SkillGraphFlow = ({
6099
+ graph,
6100
+ detailFor,
6101
+ selectedId: selectedIdProp,
6102
+ defaultSelectedId = null,
6103
+ onSelectNode,
6104
+ showStart = true,
6105
+ hideDetailPanel = false,
6106
+ defaultPanelWidth = 320,
6107
+ height = "100%",
6108
+ className,
6109
+ style
6110
+ }) => {
6111
+ const isControlled = selectedIdProp !== void 0;
6112
+ const [internalSelected, setInternalSelected] = (0, import_react13.useState)(defaultSelectedId);
6113
+ const selectedId = isControlled ? selectedIdProp : internalSelected;
6114
+ const select = (0, import_react13.useCallback)(
6115
+ (id) => {
6116
+ if (!isControlled) setInternalSelected(id);
6117
+ onSelectNode?.(id);
6118
+ },
6119
+ [isControlled, onSelectNode]
6120
+ );
6121
+ const containerRef = (0, import_react13.useRef)(null);
6122
+ const [panelWidth, setPanelWidth] = (0, import_react13.useState)(defaultPanelWidth);
6123
+ const [dragging, setDragging] = (0, import_react13.useState)(false);
6124
+ const startResize = (0, import_react13.useCallback)((e) => {
6125
+ e.preventDefault();
6126
+ setDragging(true);
6127
+ const onMove = (ev) => {
6128
+ const rect = containerRef.current?.getBoundingClientRect();
6129
+ if (!rect) return;
6130
+ const next = rect.right - ev.clientX;
6131
+ const max = Math.max(240, rect.width - 240);
6132
+ setPanelWidth(Math.min(max, Math.max(220, next)));
6133
+ };
6134
+ const onUp = () => {
6135
+ setDragging(false);
6136
+ window.removeEventListener("mousemove", onMove);
6137
+ window.removeEventListener("mouseup", onUp);
6138
+ };
6139
+ window.addEventListener("mousemove", onMove);
6140
+ window.addEventListener("mouseup", onUp);
6141
+ }, []);
6142
+ const { rfNodes, rfEdges } = (0, import_react13.useMemo)(() => {
6143
+ const laid = layoutSkillGraph(graph, { showStart });
6144
+ const rfNodes2 = laid.nodes.map((n) => ({
6145
+ id: n.id,
6146
+ type: n.kind === "start" ? "sgStart" : n.kind === "predicate" ? "sgPredicate" : "sgSkill",
6147
+ position: { x: n.x, y: n.y },
6148
+ width: n.width,
6149
+ height: n.height,
6150
+ draggable: false,
6151
+ selectable: n.kind !== "start",
6152
+ data: { label: n.label, isSelected: n.id === selectedId }
6153
+ }));
6154
+ const rfEdges2 = laid.edges.map((e) => ({
6155
+ id: e.id,
6156
+ source: e.source,
6157
+ target: e.target,
6158
+ label: e.label,
6159
+ style: {
6160
+ stroke: T.edgeDefault,
6161
+ strokeWidth: 1.5,
6162
+ strokeDasharray: e.dashed ? "5 4" : void 0
6163
+ },
6164
+ labelStyle: { fill: T.textMuted, fontFamily: T.fontSans, fontSize: 11 },
6165
+ labelBgStyle: { fill: T.bgPrimary, fillOpacity: 0.85 },
6166
+ markerEnd: { type: import_react14.MarkerType.ArrowClosed, color: T.edgeDefault, width: 16, height: 16 }
6167
+ }));
6168
+ return { rfNodes: rfNodes2, rfEdges: rfEdges2 };
6169
+ }, [graph, showStart, selectedId]);
6170
+ const selectedNode = (0, import_react13.useMemo)(
6171
+ () => selectedId ? graph.nodes.find((n) => n.id === selectedId) ?? null : null,
6172
+ [graph.nodes, selectedId]
6173
+ );
6174
+ const detail = selectedNode && detailFor ? detailFor(selectedNode) : void 0;
6175
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
6176
+ "div",
6177
+ {
6178
+ ref: containerRef,
6179
+ className,
6180
+ style: {
6181
+ display: "flex",
6182
+ height,
6183
+ width: "100%",
6184
+ background: T.bgPrimary,
6185
+ // While dragging, suppress text selection + let the divider own the cursor.
6186
+ userSelect: dragging ? "none" : void 0,
6187
+ cursor: dragging ? "col-resize" : void 0,
6188
+ ...style
6189
+ },
6190
+ children: [
6191
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { flex: 1, minWidth: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react14.ReactFlowProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
6192
+ import_react14.ReactFlow,
6193
+ {
6194
+ nodes: rfNodes,
6195
+ edges: rfEdges,
6196
+ nodeTypes: NODE_TYPES,
6197
+ onNodeClick: (_, node) => select(node.id),
6198
+ onPaneClick: () => select(null),
6199
+ nodesDraggable: false,
6200
+ nodesConnectable: false,
6201
+ elementsSelectable: true,
6202
+ fitView: true,
6203
+ fitViewOptions: { padding: 0.2 },
6204
+ minZoom: 0.1,
6205
+ maxZoom: 1.5,
6206
+ proOptions: { hideAttribution: true },
6207
+ children: [
6208
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react14.Background, { color: T.border, gap: 20 }),
6209
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react14.Controls, { showInteractive: false })
6210
+ ]
6211
+ }
6212
+ ) }) }),
6213
+ !hideDetailPanel && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
6214
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
6215
+ "div",
6216
+ {
6217
+ role: "separator",
6218
+ "aria-orientation": "vertical",
6219
+ "data-testid": "skill-graph-resizer",
6220
+ onMouseDown: startResize,
6221
+ title: "Drag to resize",
6222
+ style: {
6223
+ flex: "0 0 6px",
6224
+ cursor: "col-resize",
6225
+ background: dragging ? T.srcSkill : T.border,
6226
+ transition: dragging ? void 0 : "background 120ms"
6227
+ }
6228
+ }
6229
+ ),
6230
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DetailPanel, { node: selectedNode, detail, width: panelWidth })
6231
+ ] })
6232
+ ]
6233
+ }
6234
+ );
6235
+ };
6236
+
6237
+ // src/react/hooks/useStepFocus.ts
6238
+ var import_react15 = require("react");
5816
6239
  function useStepFocus(max) {
5817
- const [focus, setFocus] = (0, import_react13.useState)(Math.max(0, max));
5818
- const prevMax = (0, import_react13.useRef)(max);
5819
- (0, import_react13.useEffect)(() => {
6240
+ const [focus, setFocus] = (0, import_react15.useState)(Math.max(0, max));
6241
+ const prevMax = (0, import_react15.useRef)(max);
6242
+ (0, import_react15.useEffect)(() => {
5820
6243
  const wasLiveBefore = focus >= prevMax.current;
5821
6244
  if (wasLiveBefore) setFocus(max);
5822
6245
  prevMax.current = max;
@@ -5829,32 +6252,32 @@ function useStepFocus(max) {
5829
6252
  }
5830
6253
 
5831
6254
  // src/react/hooks/useStepView.ts
5832
- var import_react14 = require("react");
6255
+ var import_react16 = require("react");
5833
6256
  function useStepView(graph, log, focusIndex, drillPath) {
5834
- return (0, import_react14.useMemo)(
6257
+ return (0, import_react16.useMemo)(
5835
6258
  () => selectStepView({ graph, log, focusIndex, drillPath }),
5836
6259
  [graph, log, focusIndex, drillPath]
5837
6260
  );
5838
6261
  }
5839
6262
 
5840
6263
  // src/react/hooks/useCommentarySlider.ts
5841
- var import_react15 = require("react");
5842
- var import_react16 = require("react");
6264
+ var import_react17 = require("react");
6265
+ var import_react18 = require("react");
5843
6266
  function useCommentarySlider(recorder, initialMode = "commentary") {
5844
- const version = (0, import_react16.useSyncExternalStore)(
6267
+ const version = (0, import_react18.useSyncExternalStore)(
5845
6268
  (listener) => recorder.subscribe(listener),
5846
6269
  () => recorder.getVersion(),
5847
6270
  () => recorder.getVersion()
5848
6271
  );
5849
- const [commitIdx, setCommitIdxRaw] = (0, import_react15.useState)(0);
5850
- const [mode, setMode] = (0, import_react15.useState)(initialMode);
5851
- const [drill, setDrill] = (0, import_react15.useState)(void 0);
5852
- const ranges = (0, import_react15.useMemo)(
6272
+ const [commitIdx, setCommitIdxRaw] = (0, import_react17.useState)(0);
6273
+ const [mode, setMode] = (0, import_react17.useState)(initialMode);
6274
+ const [drill, setDrill] = (0, import_react17.useState)(void 0);
6275
+ const ranges = (0, import_react17.useMemo)(
5853
6276
  () => selectCommentaryRanges(recorder.boundary),
5854
6277
  // version captured at render time — re-derives on every notify.
5855
6278
  [recorder, version]
5856
6279
  );
5857
- const totalCommits = (0, import_react15.useMemo)(() => {
6280
+ const totalCommits = (0, import_react17.useMemo)(() => {
5858
6281
  const live = recorder.getCommitCount();
5859
6282
  if (live > 0) return live;
5860
6283
  if (ranges.length === 0) return 0;
@@ -5865,11 +6288,11 @@ function useCommentarySlider(recorder, initialMode = "commentary") {
5865
6288
  }
5866
6289
  return max + 1;
5867
6290
  }, [recorder, ranges, version]);
5868
- const snapPoints = (0, import_react15.useMemo)(() => {
6291
+ const snapPoints = (0, import_react17.useMemo)(() => {
5869
6292
  if (mode === "commit") return [];
5870
6293
  return ranges.map((r) => r.startIdx);
5871
6294
  }, [mode, ranges]);
5872
- const setCommitIdx = (0, import_react15.useCallback)(
6295
+ const setCommitIdx = (0, import_react17.useCallback)(
5873
6296
  (idx) => {
5874
6297
  if (!Number.isFinite(idx)) return;
5875
6298
  const max = Math.max(0, totalCommits - 1);
@@ -5880,7 +6303,7 @@ function useCommentarySlider(recorder, initialMode = "commentary") {
5880
6303
  },
5881
6304
  [totalCommits, drill]
5882
6305
  );
5883
- const drillInto = (0, import_react15.useCallback)(
6306
+ const drillInto = (0, import_react17.useCallback)(
5884
6307
  (range) => {
5885
6308
  if (!Number.isFinite(range.startIdx) || range.startIdx < 0) return;
5886
6309
  if (range.endIdx !== void 0 && !Number.isFinite(range.endIdx)) return;
@@ -5900,16 +6323,16 @@ function useCommentarySlider(recorder, initialMode = "commentary") {
5900
6323
  },
5901
6324
  [totalCommits]
5902
6325
  );
5903
- (0, import_react15.useEffect)(() => {
6326
+ (0, import_react17.useEffect)(() => {
5904
6327
  if (!drill) return;
5905
6328
  const inRange = commitIdx >= drill.startIdx && (drill.endIdx === void 0 || commitIdx <= drill.endIdx);
5906
6329
  if (!inRange) setDrill(void 0);
5907
6330
  }, [commitIdx, drill]);
5908
- const active = (0, import_react15.useMemo)(
6331
+ const active = (0, import_react17.useMemo)(
5909
6332
  () => selectCommentaryAt(recorder.boundary, commitIdx),
5910
6333
  [recorder, commitIdx, version]
5911
6334
  );
5912
- const drillRange = (0, import_react15.useMemo)(() => {
6335
+ const drillRange = (0, import_react17.useMemo)(() => {
5913
6336
  if (!drill) return void 0;
5914
6337
  return ranges.find(
5915
6338
  (r) => r.label.runtimeStageId === drill.runtimeStageId
@@ -5930,9 +6353,9 @@ function useCommentarySlider(recorder, initialMode = "commentary") {
5930
6353
  }
5931
6354
 
5932
6355
  // src/react/hooks/useLensRenderGraph.ts
5933
- var import_react17 = require("react");
6356
+ var import_react19 = require("react");
5934
6357
  function useLensRenderGraph(runner, options = {}) {
5935
- return (0, import_react17.useMemo)(() => {
6358
+ return (0, import_react19.useMemo)(() => {
5936
6359
  const output = runner.getUIGroupWith(lensGroupTranslator);
5937
6360
  if (output === void 0) {
5938
6361
  throw new Error(
@@ -5971,6 +6394,8 @@ function assertLensGroupOutput(value) {
5971
6394
  LensRecorder,
5972
6395
  LensSnapshotRecorder,
5973
6396
  RunTreeView,
6397
+ SKILL_GRAPH_START_ID,
6398
+ SkillGraphFlow,
5974
6399
  SummaryCard,
5975
6400
  TimeTravel,
5976
6401
  buildLLMText,
@@ -5981,6 +6406,7 @@ function assertLensGroupOutput(value) {
5981
6406
  humanizeWith,
5982
6407
  isContextEngineering,
5983
6408
  layoutLensGraph,
6409
+ layoutSkillGraph,
5984
6410
  lensGroupTranslator,
5985
6411
  lensRecorder,
5986
6412
  lensSnapshotRecorder,