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