agentfootprint-lens 0.1.0 → 0.2.1

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
@@ -22,11 +22,13 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  AgentLens: () => AgentLens,
24
24
  IterationStrip: () => IterationStrip,
25
+ LiveTimelineBuilder: () => LiveTimelineBuilder,
25
26
  MessagesPanel: () => MessagesPanel,
26
27
  ToolCallInspector: () => ToolCallInspector,
27
28
  fromAgentSnapshot: () => fromAgentSnapshot,
28
29
  resolveLensTheme: () => resolve,
29
- useLensTheme: () => useLensTheme
30
+ useLensTheme: () => useLensTheme,
31
+ useLiveTimeline: () => useLiveTimeline
30
32
  });
31
33
  module.exports = __toCommonJS(src_exports);
32
34
 
@@ -273,12 +275,26 @@ var import_jsx_runtime = require("react/jsx-runtime");
273
275
  function MessagesPanel({
274
276
  timeline,
275
277
  onToolCallClick,
276
- systemPrompt
278
+ systemPrompt,
279
+ selectedIterKey
277
280
  }) {
278
281
  const t = useLensTheme();
282
+ const scrollRef = (0, import_react.useRef)(null);
283
+ (0, import_react.useEffect)(() => {
284
+ if (!selectedIterKey || !scrollRef.current) return;
285
+ const target = scrollRef.current.querySelector(
286
+ `[data-iter-key="${CSS.escape(selectedIterKey)}"]`
287
+ );
288
+ if (!target) return;
289
+ target.scrollIntoView({ block: "start", behavior: "smooth" });
290
+ target.setAttribute("data-iter-selected", "true");
291
+ const h = window.setTimeout(() => target.removeAttribute("data-iter-selected"), 1200);
292
+ return () => window.clearTimeout(h);
293
+ }, [selectedIterKey]);
279
294
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
280
295
  "div",
281
296
  {
297
+ ref: scrollRef,
282
298
  "data-fp-lens": "messages-panel",
283
299
  style: {
284
300
  display: "flex",
@@ -365,7 +381,15 @@ function TurnBlock({
365
381
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
366
382
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TurnHeader, { turn }),
367
383
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(UserBubble, { text: turn.userPrompt }),
368
- turn.iterations.map((iter) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IterationBlock, { iter, onToolCallClick }, iter.index)),
384
+ turn.iterations.map((iter) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
385
+ IterationBlock,
386
+ {
387
+ iter,
388
+ turnIndex: turn.index,
389
+ onToolCallClick
390
+ },
391
+ iter.index
392
+ )),
369
393
  turn.finalContent && turn.iterations.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 11, color: t.textSubtle, textAlign: "center" }, children: [
370
394
  "turn ",
371
395
  turn.index + 1,
@@ -427,27 +451,51 @@ function UserBubble({ text }) {
427
451
  }
428
452
  function IterationBlock({
429
453
  iter,
454
+ turnIndex,
430
455
  onToolCallClick
431
456
  }) {
432
457
  const t = useLensTheme();
433
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
434
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IterationBadge, { iter }),
435
- iter.assistantContent && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
436
- "div",
437
- {
438
- style: {
439
- background: t.bgElev,
440
- border: `1px solid ${t.border}`,
441
- borderRadius: `2px ${t.radius} ${t.radius} ${t.radius}`,
442
- padding: "10px 14px",
443
- maxWidth: 820,
444
- whiteSpace: "pre-wrap"
445
- },
446
- children: iter.assistantContent
447
- }
448
- ),
449
- iter.toolCalls.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6, paddingLeft: 12 }, children: iter.toolCalls.map((tc) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolCallCard, { invocation: tc, onClick: onToolCallClick }, tc.id)) })
450
- ] });
458
+ const key = `${turnIndex}.${iter.index}`;
459
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
460
+ "div",
461
+ {
462
+ "data-iter-key": key,
463
+ "data-turn-index": turnIndex,
464
+ "data-iter-index": iter.index,
465
+ style: {
466
+ display: "flex",
467
+ flexDirection: "column",
468
+ gap: 6,
469
+ // When the parent adds data-iter-selected (via IterationStrip
470
+ // click), pulse a soft ring using the accent color. Subtle so
471
+ // the chat itself stays readable.
472
+ padding: 8,
473
+ margin: -8,
474
+ borderRadius: t.radius,
475
+ outline: "2px solid transparent",
476
+ outlineOffset: 2,
477
+ transition: "outline-color 180ms ease, background 180ms ease"
478
+ },
479
+ children: [
480
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IterationBadge, { iter }),
481
+ iter.assistantContent && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
482
+ "div",
483
+ {
484
+ style: {
485
+ background: t.bgElev,
486
+ border: `1px solid ${t.border}`,
487
+ borderRadius: `2px ${t.radius} ${t.radius} ${t.radius}`,
488
+ padding: "10px 14px",
489
+ maxWidth: 820,
490
+ whiteSpace: "pre-wrap"
491
+ },
492
+ children: iter.assistantContent
493
+ }
494
+ ),
495
+ iter.toolCalls.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6, paddingLeft: 12 }, children: iter.toolCalls.map((tc) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolCallCard, { invocation: tc, onClick: onToolCallClick }, tc.id)) })
496
+ ]
497
+ }
498
+ );
451
499
  }
452
500
  function IterationBadge({ iter }) {
453
501
  const t = useLensTheme();
@@ -882,6 +930,12 @@ function AgentLens({
882
930
  fontFamily: t.fontSans
883
931
  },
884
932
  children: [
933
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `
934
+ [data-iter-selected="true"] {
935
+ outline-color: currentColor !important;
936
+ background: color-mix(in srgb, currentColor 8%, transparent);
937
+ }
938
+ ` }),
885
939
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { gridArea: "strip" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
886
940
  IterationStrip,
887
941
  {
@@ -895,6 +949,7 @@ function AgentLens({
895
949
  {
896
950
  timeline,
897
951
  onToolCallClick: handleToolClick,
952
+ selectedIterKey,
898
953
  ...derivedSystemPrompt && { systemPrompt: derivedSystemPrompt }
899
954
  }
900
955
  ) }),
@@ -921,14 +976,244 @@ function AgentLens({
921
976
  }
922
977
  );
923
978
  }
979
+
980
+ // src/adapters/LiveTimelineBuilder.ts
981
+ var LiveTimelineBuilder = class {
982
+ constructor() {
983
+ this.turns = [];
984
+ this.currentTurn = null;
985
+ this.currentIter = null;
986
+ this.toolByCallId = /* @__PURE__ */ new Map();
987
+ this.messages = [];
988
+ this.finalDecision = {};
989
+ }
990
+ /**
991
+ * Begin a new turn. Call this BEFORE `agent.run(userPrompt)` so the
992
+ * user's prompt appears alongside the iterations it produced. Safe to
993
+ * call even mid-conversation — it closes the previous turn cleanly.
994
+ */
995
+ startTurn(userPrompt) {
996
+ if (this.currentTurn) this.commitCurrentTurn();
997
+ this.currentTurn = {
998
+ index: this.turns.length,
999
+ userPrompt,
1000
+ iterations: [],
1001
+ finalContent: "",
1002
+ totalInputTokens: 0,
1003
+ totalOutputTokens: 0,
1004
+ totalDurationMs: 0,
1005
+ startMs: Date.now()
1006
+ };
1007
+ this.messages.push({ role: "user", content: userPrompt });
1008
+ }
1009
+ /**
1010
+ * Optional — attach the system prompt so MessagesPanel can render it
1011
+ * in the collapsible preamble. Usually set once at agent construction.
1012
+ */
1013
+ setSystemPrompt(prompt) {
1014
+ this.systemPrompt = prompt;
1015
+ }
1016
+ /**
1017
+ * Feed one agent stream event. Safe to call for unknown events; we
1018
+ * ignore anything we don't recognize.
1019
+ */
1020
+ ingest(event) {
1021
+ const e = event;
1022
+ if (!e || typeof e.type !== "string") return;
1023
+ switch (e.type) {
1024
+ case "llm_start":
1025
+ case "agentfootprint.stream.llm_start":
1026
+ this.onLLMStart(e);
1027
+ return;
1028
+ case "llm_end":
1029
+ case "agentfootprint.stream.llm_end":
1030
+ this.onLLMEnd(e);
1031
+ return;
1032
+ case "tool_start":
1033
+ case "agentfootprint.stream.tool_start":
1034
+ this.onToolStart(e);
1035
+ return;
1036
+ case "tool_end":
1037
+ case "agentfootprint.stream.tool_end":
1038
+ this.onToolEnd(e);
1039
+ return;
1040
+ case "turn_end":
1041
+ case "agentfootprint.agent.turn_complete":
1042
+ this.commitCurrentTurn();
1043
+ return;
1044
+ default:
1045
+ return;
1046
+ }
1047
+ }
1048
+ onLLMStart(e) {
1049
+ if (!this.currentTurn) return;
1050
+ const iterNum = e.iteration ?? this.currentTurn.iterations.length + 1;
1051
+ this.currentIter = {
1052
+ index: iterNum,
1053
+ assistantContent: "",
1054
+ toolCalls: [],
1055
+ decisionAtStart: {},
1056
+ visibleTools: [],
1057
+ startMs: Date.now()
1058
+ };
1059
+ this.currentTurn.iterations.push(this.currentIter);
1060
+ }
1061
+ onLLMEnd(e) {
1062
+ if (!this.currentIter || !this.currentTurn) return;
1063
+ this.currentIter.assistantContent = e.content ?? this.currentIter.assistantContent;
1064
+ if (e.model) this.currentIter.model = e.model;
1065
+ if (e.usage?.inputTokens !== void 0) this.currentIter.inputTokens = e.usage.inputTokens;
1066
+ if (e.usage?.outputTokens !== void 0) this.currentIter.outputTokens = e.usage.outputTokens;
1067
+ if (e.stopReason) this.currentIter.stopReason = e.stopReason;
1068
+ this.currentIter.durationMs = e.durationMs ?? Date.now() - this.currentIter.startMs;
1069
+ this.currentTurn.totalInputTokens += this.currentIter.inputTokens ?? 0;
1070
+ this.currentTurn.totalOutputTokens += this.currentIter.outputTokens ?? 0;
1071
+ this.currentTurn.totalDurationMs += this.currentIter.durationMs ?? 0;
1072
+ if (this.currentIter.assistantContent) {
1073
+ this.messages.push({ role: "assistant", content: this.currentIter.assistantContent });
1074
+ }
1075
+ if ((e.toolCallCount ?? 0) === 0) {
1076
+ this.currentTurn.finalContent = this.currentIter.assistantContent;
1077
+ }
1078
+ }
1079
+ onToolStart(e) {
1080
+ if (!this.currentIter || !this.currentTurn) return;
1081
+ const tool = {
1082
+ id: e.toolCallId ?? `tool-${this.currentIter.toolCalls.length}`,
1083
+ name: e.toolName ?? "unknown",
1084
+ arguments: e.args ?? {},
1085
+ result: "",
1086
+ iterationIndex: this.currentIter.index,
1087
+ turnIndex: this.currentTurn.index,
1088
+ startMs: Date.now()
1089
+ };
1090
+ this.currentIter.toolCalls.push(tool);
1091
+ this.toolByCallId.set(tool.id, tool);
1092
+ }
1093
+ onToolEnd(e) {
1094
+ const tool = this.toolByCallId.get(e.toolCallId ?? "");
1095
+ if (!tool) return;
1096
+ const r = e.result;
1097
+ if (typeof r === "string") {
1098
+ tool.result = r;
1099
+ } else if (r && typeof r === "object") {
1100
+ tool.result = r.content ?? "";
1101
+ if (r.error === true) tool.error = true;
1102
+ }
1103
+ tool.durationMs = e.durationMs ?? Date.now() - tool.startMs;
1104
+ this.messages.push({ role: "tool", content: tool.result, toolCallId: tool.id });
1105
+ }
1106
+ commitCurrentTurn() {
1107
+ if (!this.currentTurn) return;
1108
+ this.turns.push(this.currentTurn);
1109
+ this.currentTurn = null;
1110
+ this.currentIter = null;
1111
+ }
1112
+ /**
1113
+ * Snapshot the current state as an immutable `AgentTimeline`. Safe to
1114
+ * call at any point — mid-run gives you the partial state so Lens can
1115
+ * live-update.
1116
+ */
1117
+ getTimeline() {
1118
+ const allTurns = [...this.turns];
1119
+ if (this.currentTurn) allTurns.push(this.currentTurn);
1120
+ const tools = [];
1121
+ const frozenTurns = allTurns.map((t) => {
1122
+ const iterations = t.iterations.map((i) => {
1123
+ const tcs = i.toolCalls.map((tc) => ({ ...tc }));
1124
+ tools.push(...tcs);
1125
+ return { ...i, toolCalls: tcs };
1126
+ });
1127
+ return { ...t, iterations };
1128
+ });
1129
+ return {
1130
+ turns: frozenTurns,
1131
+ messages: [...this.messages],
1132
+ tools,
1133
+ finalDecision: { ...this.finalDecision },
1134
+ rawSnapshot: null
1135
+ };
1136
+ }
1137
+ /**
1138
+ * Fold in a final `decision` scope after the run — useful when
1139
+ * the consumer wants the Decision Ribbon (phase-2) to reflect the
1140
+ * post-run state. Safe no-op during the run itself.
1141
+ */
1142
+ setFinalDecision(decision) {
1143
+ this.finalDecision = decision;
1144
+ }
1145
+ /** Get the optional system prompt for the Messages preamble. */
1146
+ getSystemPrompt() {
1147
+ return this.systemPrompt;
1148
+ }
1149
+ /** Wipe state. Useful when the consumer starts a fresh conversation. */
1150
+ reset() {
1151
+ this.turns = [];
1152
+ this.currentTurn = null;
1153
+ this.currentIter = null;
1154
+ this.toolByCallId.clear();
1155
+ this.messages = [];
1156
+ this.finalDecision = {};
1157
+ }
1158
+ };
1159
+
1160
+ // src/adapters/useLiveTimeline.ts
1161
+ var import_react3 = require("react");
1162
+ function useLiveTimeline() {
1163
+ const builderRef = (0, import_react3.useRef)(null);
1164
+ if (!builderRef.current) builderRef.current = new LiveTimelineBuilder();
1165
+ const builder = builderRef.current;
1166
+ const [timeline, setTimeline] = (0, import_react3.useState)(() => builder.getTimeline());
1167
+ const sync = (0, import_react3.useCallback)(() => {
1168
+ setTimeline(builder.getTimeline());
1169
+ }, [builder]);
1170
+ const ingest = (0, import_react3.useCallback)(
1171
+ (event) => {
1172
+ builder.ingest(event);
1173
+ sync();
1174
+ },
1175
+ [builder, sync]
1176
+ );
1177
+ const startTurn = (0, import_react3.useCallback)(
1178
+ (userPrompt) => {
1179
+ builder.startTurn(userPrompt);
1180
+ sync();
1181
+ },
1182
+ [builder, sync]
1183
+ );
1184
+ const setSystemPrompt = (0, import_react3.useCallback)(
1185
+ (prompt) => {
1186
+ builder.setSystemPrompt(prompt);
1187
+ sync();
1188
+ },
1189
+ [builder, sync]
1190
+ );
1191
+ const setFinalDecision = (0, import_react3.useCallback)(
1192
+ (decision) => {
1193
+ builder.setFinalDecision(decision);
1194
+ sync();
1195
+ },
1196
+ [builder, sync]
1197
+ );
1198
+ const reset = (0, import_react3.useCallback)(() => {
1199
+ builder.reset();
1200
+ sync();
1201
+ }, [builder, sync]);
1202
+ return (0, import_react3.useMemo)(
1203
+ () => ({ timeline, ingest, startTurn, setSystemPrompt, setFinalDecision, reset, builder }),
1204
+ [timeline, ingest, startTurn, setSystemPrompt, setFinalDecision, reset, builder]
1205
+ );
1206
+ }
924
1207
  // Annotate the CommonJS export names for ESM import in node:
925
1208
  0 && (module.exports = {
926
1209
  AgentLens,
927
1210
  IterationStrip,
1211
+ LiveTimelineBuilder,
928
1212
  MessagesPanel,
929
1213
  ToolCallInspector,
930
1214
  fromAgentSnapshot,
931
1215
  resolveLensTheme,
932
- useLensTheme
1216
+ useLensTheme,
1217
+ useLiveTimeline
933
1218
  });
934
1219
  //# sourceMappingURL=index.cjs.map