@nanobpm/bojtos-react 0.5.0 → 0.6.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
@@ -10,6 +10,15 @@ React binding for the **Bojtos** in-browser BPMN demo framework
10
10
  - **`<BpmnRuntimeView xml activeIds incidentIds />`** — the live diagram: it
11
11
  imports the XML once and updates token (`nano-active`) / incident
12
12
  (`nano-incident`) markers in place, so zoom/scroll survive stepping.
13
+ - **`<TraceTimeline rows />`** — the shared activity log: it renders the
14
+ framework-agnostic trace model from [`@nanobpm/bojtos-kit`](../bojtos-kit) as a
15
+ turn-by-turn story (consecutive same-`turn` rows fold into one card; rows with
16
+ no `turn` render as plain lines). Feed it a kit adapter —
17
+ `foldEngineEvents(run.events)` for a plain engine run, or
18
+ `traceEntriesToRows(entries)` for handler-emitted agent/tool/turn entries. It
19
+ imports **only** the kit and React, so a trace-only import tree-shakes `bpmn-js`
20
+ out (the package is `sideEffects: false`); a test walks the built module graph
21
+ to pin that.
13
22
 
14
23
  ## Install
15
24
 
@@ -42,6 +51,26 @@ function Demo({ bpmn }: { bpmn: string }) {
42
51
  }
43
52
  ```
44
53
 
54
+ ## Trace timeline
55
+
56
+ ```tsx
57
+ import { useBojtos, TraceTimeline, foldEngineEvents } from "@nanobpm/bojtos-react";
58
+
59
+ function RunLog({ bpmn }: { bpmn: string }) {
60
+ const run = useBojtos({ bpmn });
61
+ // Engine-event fold — the non-agentic / test-view case.
62
+ return <TraceTimeline rows={foldEngineEvents(run.events)} />;
63
+ }
64
+ ```
65
+
66
+ For an agentic run, emit `TraceEntry` lines from your handlers (with the additive
67
+ `turn` / `elementId` / `args` / `result` fields) and pass
68
+ `traceEntriesToRows(entries)` instead — same component, turn-grouped card view.
69
+ `TraceTimeline` keeps the class names (`timeline`, `timeline-turn`,
70
+ `log-line log-<kind>`, …) the demo stylesheet already targets, so your CSS applies
71
+ unchanged. It never imports `bpmn-js`, so importing it alone won't pull the
72
+ diagram bundle in.
73
+
45
74
  ## Peer requirements
46
75
 
47
76
  `react` and `bpmn-js` are peer dependencies (the consumer already has them). The
@@ -0,0 +1,50 @@
1
+ import { type ElementStatDto, type IncidentDto, type TraceRow } from "@nanobpm/bojtos-kit";
2
+ /**
3
+ * The shared activity log — the run told as a story rather than a flat stack of
4
+ * lines. It is the single component that retired the two drifted, forked
5
+ * `TraceTimeline` copies (nanobpm/bojtos#9): the web-demo framework's agent/tool/
6
+ * turn view and the console test-view's engine-event fold.
7
+ *
8
+ * It renders the framework-agnostic {@link TraceRow} model from
9
+ * `@nanobpm/bojtos-kit` — feed it whichever adapter matches your source:
10
+ *
11
+ * - `foldEngineEvents(useBojtos().events)` for the non-agentic / test-view case, or
12
+ * - `traceEntriesToRows(entries)` for handler-emitted agent/tool/turn entries.
13
+ *
14
+ * Consecutive rows sharing a `turn` fold into one card (the model's raw LLM reply,
15
+ * each tool it activated with its arguments, and — once it lands — what that tool
16
+ * returned); rows with no `turn` render as plain lines in order, so a non-agentic
17
+ * run looks exactly like the flat log it replaces.
18
+ *
19
+ * This module deliberately imports **only** the kit and React — never
20
+ * `./BpmnRuntimeView` or `bpmn-js` — so a trace-only import tree-shakes the
21
+ * diagram renderer out (the package is `sideEffects: false`). See
22
+ * `test/trace.timeline.test.ts`, which walks the built module graph to pin that.
23
+ *
24
+ * The markup keeps the class names the forked copies' CSS already targets
25
+ * (`timeline`, `timeline-turn`, `timeline-tool`, `log-line log-<kind>`, …) so a
26
+ * consumer's existing stylesheet applies unchanged; no design-system dependency
27
+ * is pulled in.
28
+ */
29
+ export interface TraceTimelineProps {
30
+ /**
31
+ * The normalized rows to render. Produce them with a kit adapter
32
+ * (`foldEngineEvents` / `traceEntriesToRows`) or your own {@link TraceRow[]}.
33
+ */
34
+ rows: TraceRow[];
35
+ /** `snapshot.elementStats` — per-element completion/active counts, engine-side. */
36
+ elementStats?: ElementStatDto[];
37
+ /** Incidents on the current snapshot, with their reason. */
38
+ incidents?: IncidentDto[];
39
+ /** BPMN element id → human label, for both the timeline and the panels below. */
40
+ labelFor?: (elementId: string) => string;
41
+ /** Card heading. Defaults to "Activity". */
42
+ title?: string;
43
+ /** Sub-heading under the title. */
44
+ description?: string;
45
+ /** Shown when there are no rows yet. Defaults to "Press Run to start.". */
46
+ emptyText?: string;
47
+ /** Optional class for the outer container. */
48
+ className?: string;
49
+ }
50
+ export declare function TraceTimeline({ rows, elementStats, incidents, labelFor, title, description, emptyText, className, }: TraceTimelineProps): import("react").JSX.Element;
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { buildTraceItems, isTraceTurnGroup, } from "@nanobpm/bojtos-kit";
4
+ function safeStringify(value, space) {
5
+ // `JSON.stringify(undefined)` returns the JS value `undefined` (not a string),
6
+ // which would render as nothing. Emit the literal "undefined" so a handler/tool
7
+ // that actually returned `undefined` is shown explicitly rather than vanishing.
8
+ if (value === undefined)
9
+ return "undefined";
10
+ try {
11
+ // `JSON.stringify` throws on BigInt and circular structures; a replacer
12
+ // renders BigInt losslessly as its decimal string so trace payloads that
13
+ // carry engine-native BigInts don't crash serialization.
14
+ return JSON.stringify(value, (_key, val) => (typeof val === "bigint" ? val.toString() : val), space);
15
+ }
16
+ catch {
17
+ return "[unserializable value]";
18
+ }
19
+ }
20
+ function ToolStep({ activation, result, labelFor, }) {
21
+ const elementId = activation.elementId ?? "";
22
+ return (_jsxs("div", { className: "timeline-tool", children: [_jsxs("div", { className: "timeline-tool-head", children: [_jsx("span", { className: "timeline-badge timeline-badge-info", children: "tool" }), _jsx("strong", { children: labelFor(elementId) || elementId }), _jsx("code", { children: elementId })] }), activation.args !== undefined &&
23
+ Object.keys(activation.args).length > 0 && (_jsxs("div", { className: "timeline-kv", children: [_jsx("span", { className: "timeline-kv-label", children: "arguments" }), _jsx("code", { children: safeStringify(activation.args) })] })), _jsxs("div", { className: "timeline-kv", children: [_jsx("span", { className: "timeline-kv-label", children: "returned" }), _jsx("code", { children: result
24
+ ? safeStringify(result.result)
25
+ : "— waiting for the job to complete —" })] })] }));
26
+ }
27
+ function TurnCard({ group, labelFor, }) {
28
+ const reply = group.rows.find((e) => e.kind === "llm");
29
+ const activations = group.rows.filter((e) => e.kind === "agent" && e.elementId);
30
+ const results = group.rows.filter((e) => e.kind === "vars" && e.elementId);
31
+ const decisions = group.rows.filter((e) => e.kind === "agent" && !e.elementId);
32
+ const errors = group.rows.filter((e) => e.kind === "error");
33
+ // Entries a handler's own trace call emits (kind "tool") and any "vars" result
34
+ // that never paired with an activation above would otherwise vanish once
35
+ // stamped with a turn — render them as plain lines within the card, in order.
36
+ const activatedElementIds = new Set(activations.map((a) => a.elementId));
37
+ const loose = group.rows
38
+ .filter((e) => e.kind === "tool" ||
39
+ (e.kind === "vars" &&
40
+ e.elementId &&
41
+ !activatedElementIds.has(e.elementId)))
42
+ .sort((a, b) => a.id - b.id);
43
+ return (_jsxs("div", { className: "timeline-turn", children: [_jsxs("div", { className: "timeline-turn-head", children: [_jsxs("span", { className: `timeline-badge ${reply?.pending ? "timeline-badge-warning" : "timeline-badge-neutral"}`, children: ["Turn ", group.turn] }), reply?.pending && _jsx("span", { className: "timeline-pending", children: "thinking\u2026" })] }), reply && _jsx("blockquote", { className: "timeline-reply", children: reply.text }), decisions.map((d) => (_jsx("div", { className: "timeline-note", children: d.text }, d.key ?? d.id))), activations.map((a) => (_jsx(ToolStep, { activation: a, result: results.find((r) => r.elementId === a.elementId), labelFor: labelFor }, a.key ?? a.id))), loose.map((e) => (_jsxs("div", { className: `log-line log-${e.kind}`, children: [e.pending ? "⏳ " : "", e.text] }, e.key ?? e.id))), errors.map((e) => (_jsxs("div", { className: "timeline-error", children: ["\u26A0 ", e.text] }, e.id)))] }));
44
+ }
45
+ export function TraceTimeline({ rows, elementStats = [], incidents = [], labelFor = (id) => id, title = "Activity", description = "Agent turns, model replies, and tool calls — read top to bottom as a story.", emptyText = "Press Run to start.", className, }) {
46
+ const items = useMemo(() => buildTraceItems(rows), [rows]);
47
+ const [copied, setCopied] = useState(false);
48
+ const scrollRef = useRef(null);
49
+ // Keep the newest step in view as the run grows, same as the flat logs this
50
+ // replaces.
51
+ useEffect(() => {
52
+ const el = scrollRef.current;
53
+ if (el)
54
+ el.scrollTop = el.scrollHeight;
55
+ }, [items]);
56
+ const copyJson = () => {
57
+ const payload = {
58
+ log: rows.map(({ id: _id, ...rest }) => rest),
59
+ elementStats,
60
+ incidents,
61
+ };
62
+ const text = safeStringify(payload, 2);
63
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
64
+ navigator.clipboard
65
+ .writeText(text)
66
+ .then(() => {
67
+ setCopied(true);
68
+ setTimeout(() => setCopied(false), 1500);
69
+ })
70
+ .catch(() => {
71
+ // Clipboard access can be denied (permissions policy, insecure
72
+ // context, an embed iframe without a clipboard-write allowance) —
73
+ // fail quietly rather than surfacing an error for a convenience
74
+ // action, not a run-blocking one.
75
+ });
76
+ }
77
+ };
78
+ return (_jsxs("div", { className: className ? `timeline-panel ${className}` : "timeline-panel", children: [_jsxs("div", { className: "timeline-header", children: [_jsx("div", { className: "timeline-title", children: title }), description && (_jsx("div", { className: "timeline-description", children: description }))] }), _jsx("div", { className: "timeline-toolbar", children: _jsx("button", { type: "button", onClick: copyJson, children: copied ? "Copied!" : "Copy run as JSON" }) }), _jsx("div", { className: "timeline", ref: scrollRef, children: items.length === 0 ? (_jsx("div", { className: "log-empty", children: emptyText })) : (items.map((item) => isTraceTurnGroup(item) ? (_jsx(TurnCard, { group: item, labelFor: labelFor }, `turn-${item.turn}-${item.rows[0].key ?? item.rows[0].id}`)) : (_jsxs("div", { className: `log-line log-${item.kind}`, children: [item.pending ? "⏳ " : "", item.text] }, item.key ?? item.id)))) }), (elementStats.length > 0 || incidents.length > 0) && (_jsxs("div", { className: "timeline-engine-view", children: [elementStats.length > 0 && (_jsxs("div", { className: "timeline-stats", children: [_jsx("span", { className: "timeline-kv-label", children: "Element completion" }), _jsx("ul", { children: elementStats
79
+ .filter((s) => s.completed > 0 || (s.active ?? 0) > 0)
80
+ .map((s) => (_jsxs("li", { children: [_jsx("code", { children: labelFor(s.elementId) || s.elementId }), " ", "completed ", s.completed, s.active ? `, ${s.active} active` : ""] }, s.elementId))) })] })), incidents.length > 0 && (_jsxs("div", { className: "timeline-incidents", children: [_jsx("span", { className: "timeline-kv-label", children: "Incidents" }), _jsx("ul", { children: incidents.map((inc, i) => (_jsxs("li", { children: [_jsx("code", { children: labelFor(inc.elementId) || inc.elementId }), " \u2014", " ", inc.reason] }, `${inc.elementId}-${i}`))) })] }))] }))] }));
81
+ }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { useBojtos, type UseBojtosOptions, type BojtosControls, type BojtosPhase
2
2
  export { Bojtos, type BojtosProps, type TraceEvent } from "./Bojtos.js";
3
3
  export { OrderFulfillmentDemo, ORDER_FULFILLMENT_BPMN, orderFulfillmentWorkers, } from "./examples/orderFulfillment.js";
4
4
  export { BpmnRuntimeView, type BpmnRuntimeViewProps, } from "./BpmnRuntimeView.js";
5
+ export { TraceTimeline, type TraceTimelineProps } from "./TraceTimeline.js";
5
6
  export { describeRunState, markerKey, bpmnKey, resourceList, capEvents, } from "./runState.js";
6
- export { JobFailure, settleReason, unhandledJobTypes, type JobHandler, type JobResult, type AgentHandler, type DispatchOptions, type DispatchResult, type RoundResult, type SettleReason, } from "@nanobpm/bojtos-kit";
7
- export type { BojtosSession, Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, UserTaskDto, MessageSubscriptionDto, SignalSubscriptionDto, ElementStatDto, SequenceFlowDto, DecisionInstanceDto, ActiveEl, ActivateInstruction, AgentActivation, AgentResult, WasmEvent, } from "@nanobpm/bojtos-kit";
7
+ export { JobFailure, settleReason, unhandledJobTypes, buildTraceItems, isTraceTurnGroup, foldEngineEvents, traceEntriesToRows, type JobHandler, type JobResult, type AgentHandler, type DispatchOptions, type DispatchResult, type RoundResult, type SettleReason, } from "@nanobpm/bojtos-kit";
8
+ export type { BojtosSession, Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, UserTaskDto, MessageSubscriptionDto, SignalSubscriptionDto, ElementStatDto, SequenceFlowDto, DecisionInstanceDto, ActiveEl, ActivateInstruction, AgentActivation, AgentResult, WasmEvent, TraceRowKind, TraceEntry, TraceRow, TraceTurnGroup, TraceItem, TraceAdapter, } from "@nanobpm/bojtos-kit";
package/dist/index.js CHANGED
@@ -7,5 +7,8 @@ export { useBojtos, } from "./useBojtos.js";
7
7
  export { Bojtos } from "./Bojtos.js";
8
8
  export { OrderFulfillmentDemo, ORDER_FULFILLMENT_BPMN, orderFulfillmentWorkers, } from "./examples/orderFulfillment.js";
9
9
  export { BpmnRuntimeView, } from "./BpmnRuntimeView.js";
10
+ // The shared activity log (#9). Trace-only imports tree-shake bpmn-js out —
11
+ // TraceTimeline imports only the kit + React, never BpmnRuntimeView.
12
+ export { TraceTimeline } from "./TraceTimeline.js";
10
13
  export { describeRunState, markerKey, bpmnKey, resourceList, capEvents, } from "./runState.js";
11
- export { JobFailure, settleReason, unhandledJobTypes, } from "@nanobpm/bojtos-kit";
14
+ export { JobFailure, settleReason, unhandledJobTypes, buildTraceItems, isTraceTurnGroup, foldEngineEvents, traceEntriesToRows, } from "@nanobpm/bojtos-kit";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/bojtos-react",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "React binding for the Bojtos in-browser BPMN demo framework (ADR 0043): the useBojtos hook (owns the engine session + reactive snapshot/event state) and the <BpmnRuntimeView> live token/incident diagram. Built on @nanobpm/bojtos-kit; the console test-run panel is its first consumer.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -32,7 +32,7 @@
32
32
  "test:ci": "node --experimental-strip-types --test test/*.test.ts"
33
33
  },
34
34
  "dependencies": {
35
- "@nanobpm/bojtos-kit": "^0.5.0"
35
+ "@nanobpm/bojtos-kit": "^0.6.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "bpmn-js": ">=17",
@@ -0,0 +1,303 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import {
3
+ buildTraceItems,
4
+ isTraceTurnGroup,
5
+ type ElementStatDto,
6
+ type IncidentDto,
7
+ type TraceItem,
8
+ type TraceRow,
9
+ type TraceTurnGroup,
10
+ } from "@nanobpm/bojtos-kit";
11
+
12
+ /**
13
+ * The shared activity log — the run told as a story rather than a flat stack of
14
+ * lines. It is the single component that retired the two drifted, forked
15
+ * `TraceTimeline` copies (nanobpm/bojtos#9): the web-demo framework's agent/tool/
16
+ * turn view and the console test-view's engine-event fold.
17
+ *
18
+ * It renders the framework-agnostic {@link TraceRow} model from
19
+ * `@nanobpm/bojtos-kit` — feed it whichever adapter matches your source:
20
+ *
21
+ * - `foldEngineEvents(useBojtos().events)` for the non-agentic / test-view case, or
22
+ * - `traceEntriesToRows(entries)` for handler-emitted agent/tool/turn entries.
23
+ *
24
+ * Consecutive rows sharing a `turn` fold into one card (the model's raw LLM reply,
25
+ * each tool it activated with its arguments, and — once it lands — what that tool
26
+ * returned); rows with no `turn` render as plain lines in order, so a non-agentic
27
+ * run looks exactly like the flat log it replaces.
28
+ *
29
+ * This module deliberately imports **only** the kit and React — never
30
+ * `./BpmnRuntimeView` or `bpmn-js` — so a trace-only import tree-shakes the
31
+ * diagram renderer out (the package is `sideEffects: false`). See
32
+ * `test/trace.timeline.test.ts`, which walks the built module graph to pin that.
33
+ *
34
+ * The markup keeps the class names the forked copies' CSS already targets
35
+ * (`timeline`, `timeline-turn`, `timeline-tool`, `log-line log-<kind>`, …) so a
36
+ * consumer's existing stylesheet applies unchanged; no design-system dependency
37
+ * is pulled in.
38
+ */
39
+ export interface TraceTimelineProps {
40
+ /**
41
+ * The normalized rows to render. Produce them with a kit adapter
42
+ * (`foldEngineEvents` / `traceEntriesToRows`) or your own {@link TraceRow[]}.
43
+ */
44
+ rows: TraceRow[];
45
+ /** `snapshot.elementStats` — per-element completion/active counts, engine-side. */
46
+ elementStats?: ElementStatDto[];
47
+ /** Incidents on the current snapshot, with their reason. */
48
+ incidents?: IncidentDto[];
49
+ /** BPMN element id → human label, for both the timeline and the panels below. */
50
+ labelFor?: (elementId: string) => string;
51
+ /** Card heading. Defaults to "Activity". */
52
+ title?: string;
53
+ /** Sub-heading under the title. */
54
+ description?: string;
55
+ /** Shown when there are no rows yet. Defaults to "Press Run to start.". */
56
+ emptyText?: string;
57
+ /** Optional class for the outer container. */
58
+ className?: string;
59
+ }
60
+
61
+ function safeStringify(value: unknown, space?: number): string {
62
+ // `JSON.stringify(undefined)` returns the JS value `undefined` (not a string),
63
+ // which would render as nothing. Emit the literal "undefined" so a handler/tool
64
+ // that actually returned `undefined` is shown explicitly rather than vanishing.
65
+ if (value === undefined) return "undefined";
66
+ try {
67
+ // `JSON.stringify` throws on BigInt and circular structures; a replacer
68
+ // renders BigInt losslessly as its decimal string so trace payloads that
69
+ // carry engine-native BigInts don't crash serialization.
70
+ return JSON.stringify(
71
+ value,
72
+ (_key, val) => (typeof val === "bigint" ? val.toString() : val),
73
+ space,
74
+ );
75
+ } catch {
76
+ return "[unserializable value]";
77
+ }
78
+ }
79
+
80
+ function ToolStep({
81
+ activation,
82
+ result,
83
+ labelFor,
84
+ }: {
85
+ activation: TraceRow;
86
+ result: TraceRow | undefined;
87
+ labelFor: (elementId: string) => string;
88
+ }) {
89
+ const elementId = activation.elementId ?? "";
90
+ return (
91
+ <div className="timeline-tool">
92
+ <div className="timeline-tool-head">
93
+ <span className="timeline-badge timeline-badge-info">tool</span>
94
+ <strong>{labelFor(elementId) || elementId}</strong>
95
+ <code>{elementId}</code>
96
+ </div>
97
+ {activation.args !== undefined &&
98
+ Object.keys(activation.args).length > 0 && (
99
+ <div className="timeline-kv">
100
+ <span className="timeline-kv-label">arguments</span>
101
+ <code>{safeStringify(activation.args)}</code>
102
+ </div>
103
+ )}
104
+ <div className="timeline-kv">
105
+ <span className="timeline-kv-label">returned</span>
106
+ <code>
107
+ {result
108
+ ? safeStringify(result.result)
109
+ : "— waiting for the job to complete —"}
110
+ </code>
111
+ </div>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ function TurnCard({
117
+ group,
118
+ labelFor,
119
+ }: {
120
+ group: TraceTurnGroup;
121
+ labelFor: (elementId: string) => string;
122
+ }) {
123
+ const reply = group.rows.find((e) => e.kind === "llm");
124
+ const activations = group.rows.filter((e) => e.kind === "agent" && e.elementId);
125
+ const results = group.rows.filter((e) => e.kind === "vars" && e.elementId);
126
+ const decisions = group.rows.filter((e) => e.kind === "agent" && !e.elementId);
127
+ const errors = group.rows.filter((e) => e.kind === "error");
128
+ // Entries a handler's own trace call emits (kind "tool") and any "vars" result
129
+ // that never paired with an activation above would otherwise vanish once
130
+ // stamped with a turn — render them as plain lines within the card, in order.
131
+ const activatedElementIds = new Set(activations.map((a) => a.elementId));
132
+ const loose = group.rows
133
+ .filter(
134
+ (e) =>
135
+ e.kind === "tool" ||
136
+ (e.kind === "vars" &&
137
+ e.elementId &&
138
+ !activatedElementIds.has(e.elementId)),
139
+ )
140
+ .sort((a, b) => a.id - b.id);
141
+
142
+ return (
143
+ <div className="timeline-turn">
144
+ <div className="timeline-turn-head">
145
+ <span
146
+ className={`timeline-badge ${
147
+ reply?.pending ? "timeline-badge-warning" : "timeline-badge-neutral"
148
+ }`}
149
+ >
150
+ Turn {group.turn}
151
+ </span>
152
+ {reply?.pending && <span className="timeline-pending">thinking…</span>}
153
+ </div>
154
+
155
+ {reply && <blockquote className="timeline-reply">{reply.text}</blockquote>}
156
+
157
+ {decisions.map((d) => (
158
+ <div key={d.key ?? d.id} className="timeline-note">
159
+ {d.text}
160
+ </div>
161
+ ))}
162
+
163
+ {activations.map((a) => (
164
+ <ToolStep
165
+ key={a.key ?? a.id}
166
+ activation={a}
167
+ result={results.find((r) => r.elementId === a.elementId)}
168
+ labelFor={labelFor}
169
+ />
170
+ ))}
171
+
172
+ {loose.map((e) => (
173
+ <div key={e.key ?? e.id} className={`log-line log-${e.kind}`}>
174
+ {e.pending ? "⏳ " : ""}
175
+ {e.text}
176
+ </div>
177
+ ))}
178
+
179
+ {errors.map((e) => (
180
+ <div key={e.id} className="timeline-error">
181
+ ⚠ {e.text}
182
+ </div>
183
+ ))}
184
+ </div>
185
+ );
186
+ }
187
+
188
+ export function TraceTimeline({
189
+ rows,
190
+ elementStats = [],
191
+ incidents = [],
192
+ labelFor = (id) => id,
193
+ title = "Activity",
194
+ description = "Agent turns, model replies, and tool calls — read top to bottom as a story.",
195
+ emptyText = "Press Run to start.",
196
+ className,
197
+ }: TraceTimelineProps) {
198
+ const items: TraceItem[] = useMemo(() => buildTraceItems(rows), [rows]);
199
+ const [copied, setCopied] = useState(false);
200
+ const scrollRef = useRef<HTMLDivElement>(null);
201
+
202
+ // Keep the newest step in view as the run grows, same as the flat logs this
203
+ // replaces.
204
+ useEffect(() => {
205
+ const el = scrollRef.current;
206
+ if (el) el.scrollTop = el.scrollHeight;
207
+ }, [items]);
208
+
209
+ const copyJson = () => {
210
+ const payload = {
211
+ log: rows.map(({ id: _id, ...rest }) => rest),
212
+ elementStats,
213
+ incidents,
214
+ };
215
+ const text = safeStringify(payload, 2);
216
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
217
+ navigator.clipboard
218
+ .writeText(text)
219
+ .then(() => {
220
+ setCopied(true);
221
+ setTimeout(() => setCopied(false), 1500);
222
+ })
223
+ .catch(() => {
224
+ // Clipboard access can be denied (permissions policy, insecure
225
+ // context, an embed iframe without a clipboard-write allowance) —
226
+ // fail quietly rather than surfacing an error for a convenience
227
+ // action, not a run-blocking one.
228
+ });
229
+ }
230
+ };
231
+
232
+ return (
233
+ <div className={className ? `timeline-panel ${className}` : "timeline-panel"}>
234
+ <div className="timeline-header">
235
+ <div className="timeline-title">{title}</div>
236
+ {description && (
237
+ <div className="timeline-description">{description}</div>
238
+ )}
239
+ </div>
240
+
241
+ <div className="timeline-toolbar">
242
+ <button type="button" onClick={copyJson}>
243
+ {copied ? "Copied!" : "Copy run as JSON"}
244
+ </button>
245
+ </div>
246
+
247
+ <div className="timeline" ref={scrollRef}>
248
+ {items.length === 0 ? (
249
+ <div className="log-empty">{emptyText}</div>
250
+ ) : (
251
+ items.map((item) =>
252
+ isTraceTurnGroup(item) ? (
253
+ <TurnCard
254
+ key={`turn-${item.turn}-${item.rows[0].key ?? item.rows[0].id}`}
255
+ group={item}
256
+ labelFor={labelFor}
257
+ />
258
+ ) : (
259
+ <div key={item.key ?? item.id} className={`log-line log-${item.kind}`}>
260
+ {item.pending ? "⏳ " : ""}
261
+ {item.text}
262
+ </div>
263
+ ),
264
+ )
265
+ )}
266
+ </div>
267
+
268
+ {(elementStats.length > 0 || incidents.length > 0) && (
269
+ <div className="timeline-engine-view">
270
+ {elementStats.length > 0 && (
271
+ <div className="timeline-stats">
272
+ <span className="timeline-kv-label">Element completion</span>
273
+ <ul>
274
+ {elementStats
275
+ .filter((s) => s.completed > 0 || (s.active ?? 0) > 0)
276
+ .map((s) => (
277
+ <li key={s.elementId}>
278
+ <code>{labelFor(s.elementId) || s.elementId}</code>{" "}
279
+ completed {s.completed}
280
+ {s.active ? `, ${s.active} active` : ""}
281
+ </li>
282
+ ))}
283
+ </ul>
284
+ </div>
285
+ )}
286
+ {incidents.length > 0 && (
287
+ <div className="timeline-incidents">
288
+ <span className="timeline-kv-label">Incidents</span>
289
+ <ul>
290
+ {incidents.map((inc, i) => (
291
+ <li key={`${inc.elementId}-${i}`}>
292
+ <code>{labelFor(inc.elementId) || inc.elementId}</code> —{" "}
293
+ {inc.reason}
294
+ </li>
295
+ ))}
296
+ </ul>
297
+ </div>
298
+ )}
299
+ </div>
300
+ )}
301
+ </div>
302
+ );
303
+ }
package/src/index.ts CHANGED
@@ -20,6 +20,9 @@ export {
20
20
  BpmnRuntimeView,
21
21
  type BpmnRuntimeViewProps,
22
22
  } from "./BpmnRuntimeView.js";
23
+ // The shared activity log (#9). Trace-only imports tree-shake bpmn-js out —
24
+ // TraceTimeline imports only the kit + React, never BpmnRuntimeView.
25
+ export { TraceTimeline, type TraceTimelineProps } from "./TraceTimeline.js";
23
26
  export {
24
27
  describeRunState,
25
28
  markerKey,
@@ -31,6 +34,10 @@ export {
31
34
  JobFailure,
32
35
  settleReason,
33
36
  unhandledJobTypes,
37
+ buildTraceItems,
38
+ isTraceTurnGroup,
39
+ foldEngineEvents,
40
+ traceEntriesToRows,
34
41
  type JobHandler,
35
42
  type JobResult,
36
43
  type AgentHandler,
@@ -58,4 +65,10 @@ export type {
58
65
  AgentActivation,
59
66
  AgentResult,
60
67
  WasmEvent,
68
+ TraceRowKind,
69
+ TraceEntry,
70
+ TraceRow,
71
+ TraceTurnGroup,
72
+ TraceItem,
73
+ TraceAdapter,
61
74
  } from "@nanobpm/bojtos-kit";