@dxos/react-ui-trace 0.11.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/LICENSE +105 -0
- package/README.md +3 -0
- package/dist/lib/index.mjs +2868 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing.mjs +338 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/translations.mjs +19 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/types/src/components/Gantt/Gantt.d.ts +68 -0
- package/dist/types/src/components/Gantt/Gantt.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts +30 -0
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/index.d.ts +2 -0
- package/dist/types/src/components/Gantt/index.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts +32 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts +8 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/index.d.ts +2 -0
- package/dist/types/src/components/ProcessTree/index.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/Timeline.d.ts +50 -0
- package/dist/types/src/components/Timeline/Timeline.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts +24 -0
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/index.d.ts +4 -0
- package/dist/types/src/components/Timeline/index.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-options.d.ts +9 -0
- package/dist/types/src/components/Timeline/timeline-options.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-printer.d.ts +7 -0
- package/dist/types/src/components/Timeline/timeline-printer.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-printer.test.d.ts +2 -0
- package/dist/types/src/components/Timeline/timeline-printer.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/TracePanel.d.ts +28 -0
- package/dist/types/src/components/TracePanel/TracePanel.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.d.ts +19 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.test.d.ts +2 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/index.d.ts +4 -0
- package/dist/types/src/components/TracePanel/index.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/trace-filter.d.ts +39 -0
- package/dist/types/src/components/TracePanel/trace-filter.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/trace-filter.test.d.ts +2 -0
- package/dist/types/src/components/TracePanel/trace-filter.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/useTraceMenu.d.ts +15 -0
- package/dist/types/src/components/TracePanel/useTraceMenu.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +5 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/execution-graph/execution-graph.d.ts +139 -0
- package/dist/types/src/execution-graph/execution-graph.d.ts.map +1 -0
- package/dist/types/src/execution-graph/execution-graph.test.d.ts +2 -0
- package/dist/types/src/execution-graph/execution-graph.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/index.d.ts +4 -0
- package/dist/types/src/execution-graph/index.d.ts.map +1 -0
- package/dist/types/src/execution-graph/pending-block-status.d.ts +26 -0
- package/dist/types/src/execution-graph/pending-block-status.d.ts.map +1 -0
- package/dist/types/src/execution-graph/pending-block-status.test.d.ts +2 -0
- package/dist/types/src/execution-graph/pending-block-status.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts +2 -0
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/span-tree.d.ts +106 -0
- package/dist/types/src/execution-graph/span-tree.d.ts.map +1 -0
- package/dist/types/src/execution-graph/span-tree.test.d.ts +2 -0
- package/dist/types/src/execution-graph/span-tree.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/sub-agent-delegation.test.d.ts +2 -0
- package/dist/types/src/execution-graph/sub-agent-delegation.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/testing/collect-trace-events.d.ts +40 -0
- package/dist/types/src/execution-graph/testing/collect-trace-events.d.ts.map +1 -0
- package/dist/types/src/execution-graph/testing/index.d.ts +2 -0
- package/dist/types/src/execution-graph/testing/index.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +4 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useExecutionGraph.d.ts +17 -0
- package/dist/types/src/hooks/useExecutionGraph.d.ts.map +1 -0
- package/dist/types/src/hooks/useSessionTimeline.d.ts +17 -0
- package/dist/types/src/hooks/useSessionTimeline.d.ts.map +1 -0
- package/dist/types/src/hooks/useTraceMessages.d.ts +17 -0
- package/dist/types/src/hooks/useTraceMessages.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +5 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/session-timeline/index.d.ts +3 -0
- package/dist/types/src/session-timeline/index.d.ts.map +1 -0
- package/dist/types/src/session-timeline/session-timeline.d.ts +19 -0
- package/dist/types/src/session-timeline/session-timeline.d.ts.map +1 -0
- package/dist/types/src/session-timeline/session-timeline.test.d.ts +2 -0
- package/dist/types/src/session-timeline/session-timeline.test.d.ts.map +1 -0
- package/dist/types/src/session-timeline/types.d.ts +129 -0
- package/dist/types/src/session-timeline/types.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +5 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/simulated-agent.d.ts +63 -0
- package/dist/types/src/testing/simulated-agent.d.ts.map +1 -0
- package/dist/types/src/testing/snapshot-playback.d.ts +12 -0
- package/dist/types/src/testing/snapshot-playback.d.ts.map +1 -0
- package/dist/types/src/testing/test-trace.d.ts +5 -0
- package/dist/types/src/testing/test-trace.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +19 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +77 -0
- package/src/components/Gantt/Gantt.stories.tsx +241 -0
- package/src/components/Gantt/Gantt.tsx +589 -0
- package/src/components/Gantt/index.ts +5 -0
- package/src/components/ProcessTree/ProcessTree.stories.tsx +95 -0
- package/src/components/ProcessTree/ProcessTree.tsx +265 -0
- package/src/components/ProcessTree/index.ts +5 -0
- package/src/components/Timeline/Timeline.stories.tsx +326 -0
- package/src/components/Timeline/Timeline.tsx +534 -0
- package/src/components/Timeline/git.drawio.svg +821 -0
- package/src/components/Timeline/index.ts +7 -0
- package/src/components/Timeline/timeline-options.ts +27 -0
- package/src/components/Timeline/timeline-printer.test.ts +76 -0
- package/src/components/Timeline/timeline-printer.ts +223 -0
- package/src/components/TracePanel/TracePanel.tsx +248 -0
- package/src/components/TracePanel/dxn-extractor.test.ts +153 -0
- package/src/components/TracePanel/dxn-extractor.ts +166 -0
- package/src/components/TracePanel/index.ts +7 -0
- package/src/components/TracePanel/trace-filter.test.ts +137 -0
- package/src/components/TracePanel/trace-filter.ts +120 -0
- package/src/components/TracePanel/useTraceMenu.ts +95 -0
- package/src/components/index.ts +8 -0
- package/src/execution-graph/SPEC.md +749 -0
- package/src/execution-graph/execution-graph.test.ts +1205 -0
- package/src/execution-graph/execution-graph.ts +1076 -0
- package/src/execution-graph/index.ts +35 -0
- package/src/execution-graph/pending-block-status.test.ts +118 -0
- package/src/execution-graph/pending-block-status.ts +88 -0
- package/src/execution-graph/remote-snapshot.test.ts +116 -0
- package/src/execution-graph/span-tree.test.ts +370 -0
- package/src/execution-graph/span-tree.ts +404 -0
- package/src/execution-graph/sub-agent-delegation.test.ts +65 -0
- package/src/execution-graph/testing/collect-trace-events.ts +76 -0
- package/src/execution-graph/testing/index.ts +5 -0
- package/src/execution-graph/testing/sub-agent-delegation.json +1285 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useExecutionGraph.ts +107 -0
- package/src/hooks/useSessionTimeline.ts +53 -0
- package/src/hooks/useTraceMessages.ts +58 -0
- package/src/index.ts +8 -0
- package/src/session-timeline/index.ts +6 -0
- package/src/session-timeline/session-timeline.test.ts +662 -0
- package/src/session-timeline/session-timeline.ts +605 -0
- package/src/session-timeline/types.ts +100 -0
- package/src/testing/index.ts +8 -0
- package/src/testing/simulated-agent.ts +293 -0
- package/src/testing/snapshot-playback.ts +45 -0
- package/src/testing/test-trace.ts +22 -0
- package/src/translations.ts +29 -0
|
@@ -0,0 +1,2868 @@
|
|
|
1
|
+
import { translationKey } from "./translations.mjs";
|
|
2
|
+
import { format } from "date-fns";
|
|
3
|
+
import React, { forwardRef, memo, useCallback, useContext, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createContext, useComposedRefs } from "@dxos/react-hooks";
|
|
5
|
+
import { Accordion, HoverCard, Icon, IconButton, Panel, ScrollArea, ScrollContainer, Tooltip, composable, composableProps, useDynamicRef, useForwardedRef, useTranslation } from "@dxos/react-ui";
|
|
6
|
+
import { mx } from "@dxos/ui-theme";
|
|
7
|
+
import { Unit, trim } from "@dxos/util";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { RegistryContext } from "@effect/atom-react/RegistryContext";
|
|
10
|
+
import * as Match from "effect/Match";
|
|
11
|
+
import * as Option from "effect/Option";
|
|
12
|
+
import * as Process from "@dxos/compute/Process";
|
|
13
|
+
import { Tree, createStaticTreeModel } from "@dxos/react-ui-list";
|
|
14
|
+
import { addEventListener } from "@dxos/async";
|
|
15
|
+
import { LogLevel, log } from "@dxos/log";
|
|
16
|
+
import { Shimmer } from "@dxos/react-ui-components";
|
|
17
|
+
import { ActionToolbar, MenuBuilder, useMenuBuilder } from "@dxos/react-ui-menu";
|
|
18
|
+
import { JsonHighlighter } from "@dxos/react-ui-syntax-highlighter";
|
|
19
|
+
import * as Array$1 from "effect/Array";
|
|
20
|
+
import { pipe } from "effect/Function";
|
|
21
|
+
import * as Pipeable from "effect/Pipeable";
|
|
22
|
+
import * as Schema from "effect/Schema";
|
|
23
|
+
import * as Struct from "effect/Struct";
|
|
24
|
+
import { RUN_AGAIN_ERROR_CODE, RUN_AGAIN_MESSAGE } from "@dxos/compute";
|
|
25
|
+
import * as Trace from "@dxos/compute/Trace";
|
|
26
|
+
import { Annotation, Filter, Query } from "@dxos/echo";
|
|
27
|
+
import { EID } from "@dxos/keys";
|
|
28
|
+
import { useAtomValue } from "@effect/atom-react/Hooks";
|
|
29
|
+
import * as Duration from "effect/Duration";
|
|
30
|
+
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
31
|
+
import { FeedTraceSink } from "@dxos/compute-runtime";
|
|
32
|
+
import { Task } from "@dxos/types";
|
|
33
|
+
//#region src/components/Gantt/Gantt.tsx
|
|
34
|
+
var ROW_HEIGHT = 28;
|
|
35
|
+
var HEADER_HEIGHT = 20;
|
|
36
|
+
var PAD_X = 12;
|
|
37
|
+
var NODE_RADIUS = 5;
|
|
38
|
+
/** Upper bound on axis ticks; the count shrinks with the width so `HH:mm:ss` labels never overlap. */
|
|
39
|
+
var MAX_TICKS = 5;
|
|
40
|
+
var TICK_MIN_WIDTH = 72;
|
|
41
|
+
var BAR_HEIGHT = 15;
|
|
42
|
+
/**
|
|
43
|
+
* A bar reaches half its height past its first and last instant, so a node sitting on either one is
|
|
44
|
+
* as far from the bar's end as it is from its top and bottom.
|
|
45
|
+
*/
|
|
46
|
+
var BAR_OVERHANG = BAR_HEIGHT / 2;
|
|
47
|
+
/** Gap between adjacent session rectangles, halved on each. */
|
|
48
|
+
var GROUP_INSET = 2;
|
|
49
|
+
/** The rectangle's clearance around its bars, the same on every side, so its corners stay concentric with theirs. */
|
|
50
|
+
var GROUP_PAD = ROW_HEIGHT / 2 - BAR_HEIGHT / 2 - GROUP_INSET;
|
|
51
|
+
var GROUP_RADIUS = 12;
|
|
52
|
+
/** Radius of the delegation connector's bend from the drop into the child bar. */
|
|
53
|
+
var BEND_RADIUS = BAR_HEIGHT / 2;
|
|
54
|
+
/**
|
|
55
|
+
* Bar, node, thread and label colours per status: nodes and the thread through them are the bar's
|
|
56
|
+
* hue in a lighter shade.
|
|
57
|
+
*/
|
|
58
|
+
var STATUS_COLOR = {
|
|
59
|
+
pending: {
|
|
60
|
+
fill: "fill-neutral-500/40",
|
|
61
|
+
node: "fill-neutral-300",
|
|
62
|
+
thread: "stroke-neutral-300",
|
|
63
|
+
text: "text-neutral-400"
|
|
64
|
+
},
|
|
65
|
+
blocked: {
|
|
66
|
+
fill: "fill-orange-500/40",
|
|
67
|
+
node: "fill-orange-300",
|
|
68
|
+
thread: "stroke-orange-300",
|
|
69
|
+
text: "text-orange-500"
|
|
70
|
+
},
|
|
71
|
+
running: {
|
|
72
|
+
fill: "fill-sky-500/40",
|
|
73
|
+
node: "fill-sky-300",
|
|
74
|
+
thread: "stroke-sky-300",
|
|
75
|
+
text: "text-sky-500"
|
|
76
|
+
},
|
|
77
|
+
review: {
|
|
78
|
+
fill: "fill-cyan-500/40",
|
|
79
|
+
node: "fill-cyan-300",
|
|
80
|
+
thread: "stroke-cyan-300",
|
|
81
|
+
text: "text-cyan-500"
|
|
82
|
+
},
|
|
83
|
+
done: {
|
|
84
|
+
fill: "fill-green-500/40",
|
|
85
|
+
node: "fill-green-300",
|
|
86
|
+
thread: "stroke-green-300",
|
|
87
|
+
text: "text-green-500"
|
|
88
|
+
},
|
|
89
|
+
failed: {
|
|
90
|
+
fill: "fill-red-500/40",
|
|
91
|
+
node: "fill-red-300",
|
|
92
|
+
thread: "stroke-red-300",
|
|
93
|
+
text: "text-red-500"
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Rows grouped by the session that processes them: a session, then the tasks (and subtasks) it
|
|
98
|
+
* works itself, then — after that block — every session those tasks spawned, recursively. Keeping a
|
|
99
|
+
* session's own rows contiguous is what lets one rectangle enclose them.
|
|
100
|
+
*/
|
|
101
|
+
var orderRows = (lanes) => {
|
|
102
|
+
const rows = [];
|
|
103
|
+
const groups = [];
|
|
104
|
+
const childrenOf = (parentId) => lanes.filter((lane) => lane.parentId === parentId);
|
|
105
|
+
const visitSession = (session, depth) => {
|
|
106
|
+
const first = rows.length;
|
|
107
|
+
rows.push({
|
|
108
|
+
lane: session,
|
|
109
|
+
depth,
|
|
110
|
+
index: rows.length,
|
|
111
|
+
sessionId: session.id
|
|
112
|
+
});
|
|
113
|
+
const spawned = [];
|
|
114
|
+
const visitTasks = (parentId, taskDepth) => {
|
|
115
|
+
for (const child of childrenOf(parentId)) if (child.kind === "task") {
|
|
116
|
+
rows.push({
|
|
117
|
+
lane: child,
|
|
118
|
+
depth: taskDepth,
|
|
119
|
+
index: rows.length,
|
|
120
|
+
sessionId: session.id
|
|
121
|
+
});
|
|
122
|
+
visitTasks(child.id, taskDepth + 1);
|
|
123
|
+
} else spawned.push({
|
|
124
|
+
lane: child,
|
|
125
|
+
depth: taskDepth
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
visitTasks(session.id, depth + 1);
|
|
129
|
+
groups.push({
|
|
130
|
+
session,
|
|
131
|
+
first,
|
|
132
|
+
last: rows.length - 1
|
|
133
|
+
});
|
|
134
|
+
for (const child of spawned) visitSession(child.lane, child.depth);
|
|
135
|
+
};
|
|
136
|
+
for (const lane of childrenOf(void 0)) if (lane.kind === "session") visitSession(lane, 0);
|
|
137
|
+
else rows.push({
|
|
138
|
+
lane,
|
|
139
|
+
depth: 0,
|
|
140
|
+
index: rows.length,
|
|
141
|
+
sessionId: void 0
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
rows,
|
|
145
|
+
groups
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
var formatTokens = (tokens) => tokens.total >= 1e3 ? Unit.Thousand(tokens.total).toString() : String(tokens.total);
|
|
149
|
+
var [GanttProvider, useGanttContext] = createContext("Gantt");
|
|
150
|
+
/**
|
|
151
|
+
* Gantt view of sessions and tasks on a shared time axis. The root resolves the rows and the axis;
|
|
152
|
+
* the parts lay out side by side in the order given, sharing one row grid: `Legend` names the
|
|
153
|
+
* lanes, `Chart` draws them, `Meta` shows per-lane totals. A host that already lists the lanes
|
|
154
|
+
* renders the chart alone.
|
|
155
|
+
*/
|
|
156
|
+
var GanttRoot = composable(({ lanes, markers = [], range: rangeProp, now, showNow, onLaneSelect, onMarkerSelect, children, ...props }, forwardedRef) => {
|
|
157
|
+
const { rows, groups } = useMemo(() => orderRows(lanes), [lanes]);
|
|
158
|
+
const rowById = useMemo(() => new Map(rows.map((row) => [row.lane.id, row])), [rows]);
|
|
159
|
+
const markerById = useMemo(() => new Map(markers.map((marker) => [marker.id, marker])), [markers]);
|
|
160
|
+
const range = useMemo(() => {
|
|
161
|
+
if (rangeProp) return rangeProp;
|
|
162
|
+
const times = [...lanes.flatMap((lane) => [lane.start, lane.end]), ...markers.map((marker) => marker.timestamp)].filter((time) => time !== void 0).concat(now === void 0 ? [] : [now]);
|
|
163
|
+
const start = times.length > 0 ? Math.min(...times) : 0;
|
|
164
|
+
return {
|
|
165
|
+
start,
|
|
166
|
+
end: times.length > 0 ? Math.max(...times) : start
|
|
167
|
+
};
|
|
168
|
+
}, [
|
|
169
|
+
rangeProp,
|
|
170
|
+
lanes,
|
|
171
|
+
markers,
|
|
172
|
+
now
|
|
173
|
+
]);
|
|
174
|
+
return /* @__PURE__ */ jsx(GanttProvider, {
|
|
175
|
+
rows,
|
|
176
|
+
groups,
|
|
177
|
+
rowById,
|
|
178
|
+
markers,
|
|
179
|
+
markerById,
|
|
180
|
+
range,
|
|
181
|
+
now,
|
|
182
|
+
showNow,
|
|
183
|
+
onLaneSelect,
|
|
184
|
+
onMarkerSelect,
|
|
185
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
186
|
+
...composableProps(props, { classNames: "flex w-full items-start text-xs font-mono overflow-hidden" }),
|
|
187
|
+
ref: forwardedRef,
|
|
188
|
+
children
|
|
189
|
+
})
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
GanttRoot.displayName = "Gantt.Root";
|
|
193
|
+
/** The lane names, one per row, indented by depth; each row is the lane's keyboard path. */
|
|
194
|
+
var GanttLegend = composable((props, forwardedRef) => {
|
|
195
|
+
const { rows, onLaneSelect } = useGanttContext("Gantt.Legend");
|
|
196
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
197
|
+
...composableProps(props, { classNames: "shrink-0 w-[min(20rem,40%)] min-w-40 flex flex-col" }),
|
|
198
|
+
ref: forwardedRef,
|
|
199
|
+
children: [/* @__PURE__ */ jsx("div", { style: { height: HEADER_HEIGHT } }), rows.map(({ lane, depth }) => /* @__PURE__ */ jsxs("div", {
|
|
200
|
+
className: mx("flex items-center gap-2 truncate", onLaneSelect && "cursor-pointer hover:bg-hover-surface-subtle"),
|
|
201
|
+
style: {
|
|
202
|
+
height: ROW_HEIGHT,
|
|
203
|
+
paddingInlineStart: `${.5 + depth}rem`
|
|
204
|
+
},
|
|
205
|
+
...onLaneSelect && {
|
|
206
|
+
role: "button",
|
|
207
|
+
tabIndex: 0,
|
|
208
|
+
onClick: () => onLaneSelect(lane),
|
|
209
|
+
onKeyDown: (event) => {
|
|
210
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
onLaneSelect(lane);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
children: [/* @__PURE__ */ jsx("span", { className: mx("shrink-0 w-2 h-2 rounded-full bg-current", STATUS_COLOR[lane.status].text) }), /* @__PURE__ */ jsx("span", {
|
|
217
|
+
className: "truncate text-base-fg",
|
|
218
|
+
children: lane.label
|
|
219
|
+
})]
|
|
220
|
+
}, lane.id))]
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
GanttLegend.displayName = "Gantt.Legend";
|
|
224
|
+
/** Per-lane totals — tokens and tool calls — aligned to the rows. */
|
|
225
|
+
var GanttMeta = composable((props, forwardedRef) => {
|
|
226
|
+
const { rows } = useGanttContext("Gantt.Meta");
|
|
227
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
228
|
+
...composableProps(props, { classNames: "shrink-0 flex flex-col" }),
|
|
229
|
+
ref: forwardedRef,
|
|
230
|
+
children: [/* @__PURE__ */ jsx("div", { style: { height: HEADER_HEIGHT } }), rows.map(({ lane }) => /* @__PURE__ */ jsxs("div", {
|
|
231
|
+
className: "flex items-center justify-end gap-2 px-2 text-subdued whitespace-nowrap",
|
|
232
|
+
style: { height: ROW_HEIGHT },
|
|
233
|
+
children: [lane.tokens && /* @__PURE__ */ jsx("span", {
|
|
234
|
+
title: `${lane.tokens.input} in / ${lane.tokens.output} out`,
|
|
235
|
+
children: formatTokens(lane.tokens)
|
|
236
|
+
}), lane.toolCalls !== void 0 && /* @__PURE__ */ jsxs("span", { children: [lane.toolCalls, " tools"] })]
|
|
237
|
+
}, lane.id))]
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
GanttMeta.displayName = "Gantt.Meta";
|
|
241
|
+
/**
|
|
242
|
+
* The drawing: every lane is a rounded bar with its markers threaded through it as nodes, a session's
|
|
243
|
+
* rectangle encloses the tasks it works, and connectors draw dependencies and delegations.
|
|
244
|
+
*/
|
|
245
|
+
var GanttChart = forwardRef(({ classNames }, forwardedRef) => {
|
|
246
|
+
const { rows, groups, rowById, markers, markerById, range, now, showNow, onLaneSelect, onMarkerSelect } = useGanttContext("Gantt.Chart");
|
|
247
|
+
const svgRef = useRef(null);
|
|
248
|
+
const ref = useComposedRefs(svgRef, forwardedRef);
|
|
249
|
+
const [width, setWidth] = useState(600);
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
const element = svgRef.current;
|
|
252
|
+
if (!element) return;
|
|
253
|
+
const observer = new ResizeObserver(([entry]) => entry && setWidth(entry.contentRect.width));
|
|
254
|
+
observer.observe(element);
|
|
255
|
+
return () => observer.disconnect();
|
|
256
|
+
}, []);
|
|
257
|
+
const span = Math.max(range.end - range.start, 1);
|
|
258
|
+
const x = (time) => PAD_X + (time - range.start) / span * (width - 24);
|
|
259
|
+
const rowY = (index) => HEADER_HEIGHT + index * ROW_HEIGHT + ROW_HEIGHT / 2;
|
|
260
|
+
const laneTimes = (lane) => markers.filter((marker) => marker.laneId === lane.id).map((marker) => marker.timestamp);
|
|
261
|
+
const laneEnd = (lane) => {
|
|
262
|
+
const times = laneTimes(lane);
|
|
263
|
+
return times.length > 0 ? Math.max(...times) : lane.end ?? now;
|
|
264
|
+
};
|
|
265
|
+
const delegationSource = (lane) => lane.delegatedFrom && markerById.get(lane.delegatedFrom.markerId);
|
|
266
|
+
const barStart = (lane, start) => {
|
|
267
|
+
const source = delegationSource(lane);
|
|
268
|
+
const edge = x(start) - BAR_OVERHANG;
|
|
269
|
+
return source ? Math.max(edge, x(source.timestamp) + 2 * BEND_RADIUS) : edge;
|
|
270
|
+
};
|
|
271
|
+
const nodeX = (lane, time) => lane.start === void 0 ? x(time) : Math.max(x(time), barStart(lane, lane.start) + BAR_OVERHANG);
|
|
272
|
+
const height = HEADER_HEIGHT + rows.length * ROW_HEIGHT;
|
|
273
|
+
const tickCount = Math.max(2, Math.min(MAX_TICKS, Math.floor((width - 24) / TICK_MIN_WIDTH)));
|
|
274
|
+
const ticks = Array.from({ length: tickCount }, (_, index) => range.start + span * index / (tickCount - 1));
|
|
275
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
276
|
+
className: mx("flex-1 min-w-0", classNames),
|
|
277
|
+
ref,
|
|
278
|
+
style: { height },
|
|
279
|
+
children: [
|
|
280
|
+
ticks.map((tick, index) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
|
|
281
|
+
x1: x(tick),
|
|
282
|
+
x2: x(tick),
|
|
283
|
+
y1: HEADER_HEIGHT,
|
|
284
|
+
y2: height,
|
|
285
|
+
className: "stroke-separator"
|
|
286
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
287
|
+
x: x(tick),
|
|
288
|
+
y: 14,
|
|
289
|
+
textAnchor: index === 0 ? "start" : index === tickCount - 1 ? "end" : "middle",
|
|
290
|
+
className: "fill-current text-subdued",
|
|
291
|
+
children: format(tick, "HH:mm:ss")
|
|
292
|
+
})] }, index)),
|
|
293
|
+
now !== void 0 && showNow && /* @__PURE__ */ jsx("line", {
|
|
294
|
+
x1: x(now),
|
|
295
|
+
x2: x(now),
|
|
296
|
+
y1: 0,
|
|
297
|
+
y2: height,
|
|
298
|
+
strokeDasharray: "3 3",
|
|
299
|
+
className: STATUS_COLOR.running.thread
|
|
300
|
+
}),
|
|
301
|
+
groups.flatMap(({ session, first, last }) => {
|
|
302
|
+
const members = rows.slice(first, last + 1).map((row) => row.lane);
|
|
303
|
+
const starts = members.flatMap((lane) => lane.start === void 0 ? [] : [barStart(lane, lane.start)]);
|
|
304
|
+
const ends = members.flatMap((lane) => {
|
|
305
|
+
const end = lane.start === void 0 ? void 0 : laneEnd(lane);
|
|
306
|
+
return end === void 0 ? [] : [x(end) + BAR_OVERHANG];
|
|
307
|
+
});
|
|
308
|
+
if (starts.length === 0 || ends.length === 0) return [];
|
|
309
|
+
const left = Math.min(...starts) - GROUP_PAD;
|
|
310
|
+
const right = Math.max(...ends) + GROUP_PAD;
|
|
311
|
+
return [/* @__PURE__ */ jsx("rect", {
|
|
312
|
+
x: left,
|
|
313
|
+
y: rowY(first) - ROW_HEIGHT / 2 + GROUP_INSET,
|
|
314
|
+
width: Math.max(right - left, ROW_HEIGHT),
|
|
315
|
+
height: (last - first + 1) * ROW_HEIGHT - 4,
|
|
316
|
+
rx: GROUP_RADIUS,
|
|
317
|
+
className: "fill-input-surface"
|
|
318
|
+
}, `group:${session.id}`)];
|
|
319
|
+
}),
|
|
320
|
+
rows.flatMap(({ lane, index }) => (lane.blockedOn ?? []).flatMap((depId) => {
|
|
321
|
+
const dep = rowById.get(depId);
|
|
322
|
+
const anchor = dep && (laneEnd(dep.lane) ?? dep.lane.start);
|
|
323
|
+
if (!dep || anchor === void 0) return [];
|
|
324
|
+
const anchorX = nodeX(dep.lane, anchor);
|
|
325
|
+
return [/* @__PURE__ */ jsx("line", {
|
|
326
|
+
x1: anchorX,
|
|
327
|
+
x2: anchorX,
|
|
328
|
+
y1: rowY(dep.index),
|
|
329
|
+
y2: rowY(index),
|
|
330
|
+
strokeDasharray: "2 2",
|
|
331
|
+
className: STATUS_COLOR.blocked.thread
|
|
332
|
+
}, `${lane.id}:${depId}`), ...lane.start === void 0 ? [/* @__PURE__ */ jsx("circle", {
|
|
333
|
+
cx: anchorX,
|
|
334
|
+
cy: rowY(index),
|
|
335
|
+
r: NODE_RADIUS,
|
|
336
|
+
strokeDasharray: "2 2",
|
|
337
|
+
className: mx("fill-base-surface", STATUS_COLOR.blocked.thread)
|
|
338
|
+
}, `${lane.id}:${depId}:hold`)] : []];
|
|
339
|
+
})),
|
|
340
|
+
rows.flatMap(({ lane, index }) => {
|
|
341
|
+
const source = delegationSource(lane);
|
|
342
|
+
const sourceRow = lane.delegatedFrom && rowById.get(lane.delegatedFrom.laneId);
|
|
343
|
+
if (!source || !sourceRow || lane.start === void 0) return [];
|
|
344
|
+
const sourceX = x(source.timestamp);
|
|
345
|
+
const y = rowY(index);
|
|
346
|
+
const firstNode = Math.min(...laneTimes(lane));
|
|
347
|
+
const targetX = Number.isFinite(firstNode) ? nodeX(lane, firstNode) : barStart(lane, lane.start) + BAR_OVERHANG;
|
|
348
|
+
return [/* @__PURE__ */ jsx("path", {
|
|
349
|
+
d: `M ${sourceX} ${rowY(sourceRow.index)} V ${y - BEND_RADIUS} Q ${sourceX} ${y} ${sourceX + BEND_RADIUS} ${y} H ${targetX}`,
|
|
350
|
+
fill: "none",
|
|
351
|
+
className: "stroke-fuchsia-500"
|
|
352
|
+
}, `delegation:${lane.id}`)];
|
|
353
|
+
}),
|
|
354
|
+
rows.map(({ lane, index }) => {
|
|
355
|
+
const end = laneEnd(lane);
|
|
356
|
+
if (lane.start === void 0 || end === void 0) return null;
|
|
357
|
+
const bar = {
|
|
358
|
+
x: barStart(lane, lane.start),
|
|
359
|
+
y: rowY(index) - BAR_HEIGHT / 2,
|
|
360
|
+
width: Math.max(x(end) + BAR_OVERHANG - barStart(lane, lane.start), BAR_HEIGHT),
|
|
361
|
+
height: BAR_HEIGHT,
|
|
362
|
+
rx: BAR_HEIGHT / 2
|
|
363
|
+
};
|
|
364
|
+
return /* @__PURE__ */ jsxs("g", {
|
|
365
|
+
className: "cursor-pointer",
|
|
366
|
+
onClick: () => onLaneSelect?.(lane),
|
|
367
|
+
children: [/* @__PURE__ */ jsx("rect", {
|
|
368
|
+
...bar,
|
|
369
|
+
className: "fill-base-surface"
|
|
370
|
+
}), /* @__PURE__ */ jsx("rect", {
|
|
371
|
+
...bar,
|
|
372
|
+
className: STATUS_COLOR[lane.status].fill
|
|
373
|
+
})]
|
|
374
|
+
}, lane.id);
|
|
375
|
+
}),
|
|
376
|
+
rows.map(({ lane, index }) => {
|
|
377
|
+
const times = laneTimes(lane);
|
|
378
|
+
if (times.length < 2) return null;
|
|
379
|
+
return /* @__PURE__ */ jsx("line", {
|
|
380
|
+
x1: nodeX(lane, Math.min(...times)),
|
|
381
|
+
x2: nodeX(lane, Math.max(...times)),
|
|
382
|
+
y1: rowY(index),
|
|
383
|
+
y2: rowY(index),
|
|
384
|
+
className: STATUS_COLOR[lane.status].thread
|
|
385
|
+
}, `thread:${lane.id}`);
|
|
386
|
+
}),
|
|
387
|
+
markers.map((marker) => {
|
|
388
|
+
const row = rowById.get(marker.laneId);
|
|
389
|
+
return row ? /* @__PURE__ */ jsxs(HoverCard.Root, { children: [/* @__PURE__ */ jsx(HoverCard.Trigger, {
|
|
390
|
+
asChild: true,
|
|
391
|
+
children: /* @__PURE__ */ jsx("circle", {
|
|
392
|
+
cx: nodeX(row.lane, marker.timestamp),
|
|
393
|
+
cy: rowY(row.index),
|
|
394
|
+
r: NODE_RADIUS,
|
|
395
|
+
className: mx("cursor-pointer stroke-base-surface transition-[stroke-width] hover:stroke-[3px] hover:stroke-base-fg", STATUS_COLOR[row.lane.status].node),
|
|
396
|
+
onClick: () => onMarkerSelect?.(marker)
|
|
397
|
+
})
|
|
398
|
+
}), /* @__PURE__ */ jsx(HoverCard.Portal, { children: /* @__PURE__ */ jsxs(HoverCard.Content, {
|
|
399
|
+
classNames: "p-2 max-w-72 text-xs font-mono",
|
|
400
|
+
children: [
|
|
401
|
+
/* @__PURE__ */ jsx("div", {
|
|
402
|
+
className: "font-medium truncate",
|
|
403
|
+
children: marker.label
|
|
404
|
+
}),
|
|
405
|
+
/* @__PURE__ */ jsxs("div", {
|
|
406
|
+
className: "text-description",
|
|
407
|
+
children: [
|
|
408
|
+
marker.kind,
|
|
409
|
+
" · ",
|
|
410
|
+
format(marker.timestamp, "HH:mm:ss.SSS"),
|
|
411
|
+
marker.level && marker.level !== "info" && /* @__PURE__ */ jsx("span", {
|
|
412
|
+
className: mx("ms-2", marker.level === "error" ? "text-error-text" : "text-warning-text"),
|
|
413
|
+
children: marker.level
|
|
414
|
+
})
|
|
415
|
+
]
|
|
416
|
+
}),
|
|
417
|
+
/* @__PURE__ */ jsx("div", {
|
|
418
|
+
className: "text-description truncate",
|
|
419
|
+
children: row.lane.label
|
|
420
|
+
}),
|
|
421
|
+
/* @__PURE__ */ jsx(HoverCard.Arrow, {})
|
|
422
|
+
]
|
|
423
|
+
}) })] }, marker.id) : null;
|
|
424
|
+
})
|
|
425
|
+
]
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
GanttChart.displayName = "Gantt.Chart";
|
|
429
|
+
var Gantt = {
|
|
430
|
+
Root: GanttRoot,
|
|
431
|
+
Legend: GanttLegend,
|
|
432
|
+
Chart: GanttChart,
|
|
433
|
+
Meta: GanttMeta
|
|
434
|
+
};
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/components/ProcessTree/ProcessTree.tsx
|
|
437
|
+
var DEFAULT_DEPTH = 1;
|
|
438
|
+
/** Nested subprocess rows (level > 1) only surface still-active processes. */
|
|
439
|
+
var NESTED_ACTIVE_STATES = /* @__PURE__ */ new Set([
|
|
440
|
+
Process.State.RUNNING,
|
|
441
|
+
Process.State.HYBERNATING,
|
|
442
|
+
Process.State.TERMINATING
|
|
443
|
+
]);
|
|
444
|
+
/** Synthetic root; the tree renders its children, never the root itself. */
|
|
445
|
+
var ROOT_ID = "processes";
|
|
446
|
+
var NO_SELECTION$1 = [];
|
|
447
|
+
/**
|
|
448
|
+
* The agent's running processes as a tree, one row per process with its status glyph and metrics
|
|
449
|
+
* in trailing columns. The forest is rebuilt on every metrics tick, so open state lives outside the
|
|
450
|
+
* model and is re-seeded — a collapse must survive the next tick. Renders through `Tree`.
|
|
451
|
+
*/
|
|
452
|
+
var ProcessTree = React.memo(composable(({ processes, depth = DEFAULT_DEPTH, resolveLabel, selected = NO_SELECTION$1, onSelectedChange, onProcessTerminate, ...props }, forwardedRef) => {
|
|
453
|
+
const openRef = useRef(/* @__PURE__ */ new Map());
|
|
454
|
+
const registry = useContext(RegistryContext);
|
|
455
|
+
const root = useMemo(() => buildProcessForest(processes, depth), [processes, depth]);
|
|
456
|
+
const model = useMemo(() => createStaticTreeModel(root, {
|
|
457
|
+
getChildren: (node) => node.children,
|
|
458
|
+
getProps: (node) => ({ label: (node.process && resolveLabel?.(node.process)) ?? node.process?.params.name ?? node.id }),
|
|
459
|
+
isOpen: (_node, path) => openRef.current.get(path.join("/")) ?? true,
|
|
460
|
+
isCurrent: (node) => node.process !== void 0 && selected.includes(node.process.pid)
|
|
461
|
+
}), [
|
|
462
|
+
root,
|
|
463
|
+
resolveLabel,
|
|
464
|
+
selected
|
|
465
|
+
]);
|
|
466
|
+
const handleOpenChange = useCallback(({ path, open }) => {
|
|
467
|
+
openRef.current.set(path.join("/"), open);
|
|
468
|
+
const atom = model.stateAtom(path);
|
|
469
|
+
registry.set(atom, {
|
|
470
|
+
...registry.get(atom),
|
|
471
|
+
open
|
|
472
|
+
});
|
|
473
|
+
}, [model, registry]);
|
|
474
|
+
const handleSelect = useCallback(({ item, current, meta }) => {
|
|
475
|
+
if (!item.process) return;
|
|
476
|
+
const pid = item.process.pid.toString();
|
|
477
|
+
onSelectedChange?.(!meta ? [pid] : current ? [...selected, pid] : selected.filter((candidate) => candidate !== pid));
|
|
478
|
+
}, [selected, onSelectedChange]);
|
|
479
|
+
const renderIcon = useMemo(() => makeIconRenderer(), []);
|
|
480
|
+
const renderColumns = useMemo(() => makeColumnRenderer(onProcessTerminate), [onProcessTerminate]);
|
|
481
|
+
return /* @__PURE__ */ jsx(ScrollArea.Root, {
|
|
482
|
+
...composableProps(props),
|
|
483
|
+
thin: true,
|
|
484
|
+
ref: forwardedRef,
|
|
485
|
+
children: /* @__PURE__ */ jsx(ScrollArea.Viewport, { children: /* @__PURE__ */ jsx(Tree, {
|
|
486
|
+
id: ROOT_ID,
|
|
487
|
+
model,
|
|
488
|
+
density: "sm",
|
|
489
|
+
selectionMode: "multiple",
|
|
490
|
+
classNames: "text-sm tabular-nums font-thin",
|
|
491
|
+
gridTemplateColumns: "[tree-row-start] var(--dx-control) minmax(0, 1fr) min-content min-content [tree-row-end]",
|
|
492
|
+
renderIcon,
|
|
493
|
+
renderColumns,
|
|
494
|
+
onOpenChange: handleOpenChange,
|
|
495
|
+
onSelect: handleSelect
|
|
496
|
+
}) })
|
|
497
|
+
});
|
|
498
|
+
}));
|
|
499
|
+
/** Status glyph — animated, coloured and tooltipped per state, which `TreeItemDataProps.icon` cannot express. */
|
|
500
|
+
var makeIconRenderer = () => ({ item: { process } }) => process === void 0 ? null : /* @__PURE__ */ jsx(Tooltip.Trigger, {
|
|
501
|
+
content: process.state.toString(),
|
|
502
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
503
|
+
size: 4,
|
|
504
|
+
synchronized: true,
|
|
505
|
+
classNames: mx("shrink-0", process.state === Process.State.RUNNING && "animate-spin", process.state === Process.State.FAILED && "text-error-text", process.state === Process.State.SUCCEEDED && "text-success-text"),
|
|
506
|
+
icon: Match.value(process.state).pipe(Match.when(Process.State.RUNNING, () => "ph--spinner-gap--regular"), Match.when(Process.State.SUCCEEDED, () => "ph--check-circle--regular"), Match.when(Process.State.FAILED, () => "ph--warning--regular"), Match.when(Process.State.HYBERNATING, () => "ph--spinner--regular"), Match.when(Process.State.IDLE, () => "ph--moon-stars--regular"), Match.when(Process.State.TERMINATING, () => "ph--x-circle--regular"), Match.when(Process.State.TERMINATED, () => "ph--x-circle--regular"), Match.orElse(() => "ph--spinner-gap--regular"))
|
|
507
|
+
})
|
|
508
|
+
});
|
|
509
|
+
/** Trailing columns: elapsed time for finished processes, and the terminate control. */
|
|
510
|
+
var makeColumnRenderer = (onProcessTerminate) => ({ item: { process } }) => process === void 0 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
511
|
+
className: "flex items-center justify-end text-xs text-description tabular-nums",
|
|
512
|
+
children: [Process.State.FAILED, Process.State.SUCCEEDED].includes(process.state) && /* @__PURE__ */ jsx("span", {
|
|
513
|
+
className: "whitespace-nowrap",
|
|
514
|
+
children: Unit.Duration(process.metrics.wallTime).toString()
|
|
515
|
+
})
|
|
516
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
517
|
+
className: "flex items-center",
|
|
518
|
+
children: onProcessTerminate && /* @__PURE__ */ jsx(IconButton, {
|
|
519
|
+
classNames: "min-h-0 p-1",
|
|
520
|
+
icon: "ph--x--regular",
|
|
521
|
+
iconOnly: true,
|
|
522
|
+
density: "sm",
|
|
523
|
+
variant: "ghost",
|
|
524
|
+
label: "Actions",
|
|
525
|
+
onClick: (event) => {
|
|
526
|
+
event.stopPropagation();
|
|
527
|
+
onProcessTerminate(process);
|
|
528
|
+
}
|
|
529
|
+
})
|
|
530
|
+
})] });
|
|
531
|
+
var sortProcesses = (processes) => {
|
|
532
|
+
return [
|
|
533
|
+
...processes.filter((process) => [Process.State.RUNNING, Process.State.HYBERNATING].includes(process.state)),
|
|
534
|
+
...processes.filter((process) => [Process.State.IDLE].includes(process.state)).slice(0, 3),
|
|
535
|
+
...processes.filter((process) => [
|
|
536
|
+
Process.State.SUCCEEDED,
|
|
537
|
+
Process.State.FAILED,
|
|
538
|
+
Process.State.TERMINATED
|
|
539
|
+
].includes(process.state))
|
|
540
|
+
].sort((left, right) => {
|
|
541
|
+
const leftCompletedAt = Option.getOrElse(left.completedAt, () => Infinity);
|
|
542
|
+
return Option.getOrElse(right.completedAt, () => Infinity) - leftCompletedAt;
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
var sortNestedActive = (processes) => processes.filter((process) => NESTED_ACTIVE_STATES.has(process.state)).sort((left, right) => {
|
|
546
|
+
const priority = (state) => state === Process.State.RUNNING ? 0 : state === Process.State.HYBERNATING ? 1 : 2;
|
|
547
|
+
return priority(left.state) - priority(right.state);
|
|
548
|
+
});
|
|
549
|
+
/**
|
|
550
|
+
* Builds the process forest, pruned to `maxDepth` from each root. Nested levels surface only still-
|
|
551
|
+
* active processes, so a deep tree stays readable while completed work collapses out of view.
|
|
552
|
+
*/
|
|
553
|
+
var buildProcessForest = (processes, maxDepth) => {
|
|
554
|
+
const pidSet = new Set(processes.map((process) => String(process.pid)));
|
|
555
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
556
|
+
const roots = [];
|
|
557
|
+
for (const process of processes) {
|
|
558
|
+
const parent = process.parentPid;
|
|
559
|
+
if (parent === null || !pidSet.has(String(parent))) {
|
|
560
|
+
roots.push(process);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const key = String(parent);
|
|
564
|
+
const siblings = childrenByParent.get(key) ?? [];
|
|
565
|
+
siblings.push(process);
|
|
566
|
+
childrenByParent.set(key, siblings);
|
|
567
|
+
}
|
|
568
|
+
const visit = (process, level) => {
|
|
569
|
+
const children = level >= maxDepth ? [] : sortNestedActive(childrenByParent.get(String(process.pid)) ?? []);
|
|
570
|
+
return {
|
|
571
|
+
id: String(process.pid),
|
|
572
|
+
process,
|
|
573
|
+
children: children.map((child) => visit(child, level + 1))
|
|
574
|
+
};
|
|
575
|
+
};
|
|
576
|
+
return {
|
|
577
|
+
id: ROOT_ID,
|
|
578
|
+
children: sortProcesses(roots).map((process) => visit(process, 1))
|
|
579
|
+
};
|
|
580
|
+
};
|
|
581
|
+
//#endregion
|
|
582
|
+
//#region src/components/Timeline/timeline-options.ts
|
|
583
|
+
var defaultOptions = {
|
|
584
|
+
lineHeight: 24,
|
|
585
|
+
columnWidth: 14,
|
|
586
|
+
nodeRadius: 5,
|
|
587
|
+
lineStyle: "stroke-1"
|
|
588
|
+
};
|
|
589
|
+
var compactOptions = {
|
|
590
|
+
lineHeight: 20,
|
|
591
|
+
columnWidth: 12,
|
|
592
|
+
nodeRadius: 4,
|
|
593
|
+
lineStyle: "stroke-1"
|
|
594
|
+
};
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/components/Timeline/Timeline.tsx
|
|
597
|
+
var SHIMMER_EFFECT_TAG$1 = "effect:shimmer";
|
|
598
|
+
/** Wall-clock to the second: a trace line is read against its neighbours, and milliseconds only widen the column. */
|
|
599
|
+
var TIMESTAMP_FORMAT = "HH:mm:ss";
|
|
600
|
+
var hasShimmerEffect = (commit) => commit.tags?.includes(SHIMMER_EFFECT_TAG$1) ?? false;
|
|
601
|
+
var empty = Object.freeze([]);
|
|
602
|
+
/**
|
|
603
|
+
* GitGraph-style timeline.
|
|
604
|
+
*/
|
|
605
|
+
var Timeline = memo(composable(({ branches: branchesProp, branch, commits = empty, showTimestamp = false, showIcon = true, compact = false, options = compact ? compactOptions : defaultOptions, debug = false, onChange, onSelect, ...props }, forwardedRef) => {
|
|
606
|
+
const { t } = useTranslation(translationKey);
|
|
607
|
+
const containerRef = useForwardedRef(forwardedRef);
|
|
608
|
+
const branches = useMemo(() => {
|
|
609
|
+
if (branchesProp) return branchesProp;
|
|
610
|
+
return commits.reduce((branches, commit) => {
|
|
611
|
+
if (!branches.includes(commit.branch)) branches.push(commit.branch);
|
|
612
|
+
return branches;
|
|
613
|
+
}, []);
|
|
614
|
+
}, [branchesProp, commits]);
|
|
615
|
+
const getCommitIndex = (id) => commits.findIndex((c) => c.id === id);
|
|
616
|
+
const getBranch = (id) => commits.find((c) => c.id === id)?.branch;
|
|
617
|
+
/**
|
|
618
|
+
* Assign branches to lanes, reusing lanes once a branch has been merged.
|
|
619
|
+
* Unmerged branches keep their lane active (they may still be in progress).
|
|
620
|
+
*/
|
|
621
|
+
const { branchLane, laneCount } = useMemo(() => {
|
|
622
|
+
const visibleBranches = new Set(branches);
|
|
623
|
+
const commitBranch = /* @__PURE__ */ new Map();
|
|
624
|
+
for (const commit of commits) commitBranch.set(commit.id, commit.branch);
|
|
625
|
+
const mergeRow = /* @__PURE__ */ new Map();
|
|
626
|
+
for (let row = 0; row < commits.length; row++) {
|
|
627
|
+
const commit = commits[row];
|
|
628
|
+
for (const parentId of commit.parents ?? []) {
|
|
629
|
+
const parentBranch = commitBranch.get(parentId);
|
|
630
|
+
if (parentBranch && parentBranch !== commit.branch && visibleBranches.has(parentBranch)) mergeRow.set(parentBranch, Math.max(mergeRow.get(parentBranch) ?? row, row));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const branchLane = /* @__PURE__ */ new Map();
|
|
634
|
+
if (branches.length > 0) branchLane.set(branches[0], 0);
|
|
635
|
+
const activeLanes = /* @__PURE__ */ new Set([0]);
|
|
636
|
+
let maxLane = 0;
|
|
637
|
+
for (let row = 0; row < commits.length; row++) {
|
|
638
|
+
const commit = commits[row];
|
|
639
|
+
if (visibleBranches.has(commit.branch) && !branchLane.has(commit.branch)) {
|
|
640
|
+
let lane = 1;
|
|
641
|
+
while (activeLanes.has(lane)) lane++;
|
|
642
|
+
branchLane.set(commit.branch, lane);
|
|
643
|
+
activeLanes.add(lane);
|
|
644
|
+
maxLane = Math.max(maxLane, lane);
|
|
645
|
+
}
|
|
646
|
+
for (const [branch, endRow] of mergeRow) if (endRow === row && branch !== branches[0] && branchLane.has(branch)) activeLanes.delete(branchLane.get(branch));
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
branchLane,
|
|
650
|
+
laneCount: maxLane + 1
|
|
651
|
+
};
|
|
652
|
+
}, [commits, branches]);
|
|
653
|
+
const getBranchIndex = (branch) => branchLane.get(branch) ?? -1;
|
|
654
|
+
/**
|
|
655
|
+
* Create spans for each branch.
|
|
656
|
+
*/
|
|
657
|
+
const spans = useMemo(() => {
|
|
658
|
+
const spans = /* @__PURE__ */ new Map();
|
|
659
|
+
commits.forEach((commit, index) => {
|
|
660
|
+
let span = spans.get(commit.branch);
|
|
661
|
+
if (!span) {
|
|
662
|
+
span = {
|
|
663
|
+
start: index,
|
|
664
|
+
end: index
|
|
665
|
+
};
|
|
666
|
+
spans.set(commit.branch, span);
|
|
667
|
+
} else span.end = index;
|
|
668
|
+
const parents = commit.parents ?? [];
|
|
669
|
+
for (const parent of parents) {
|
|
670
|
+
const branch = getBranch(parent);
|
|
671
|
+
if (branch && branch !== commit.branch) {
|
|
672
|
+
span.start = Math.min(span.start, getCommitIndex(parent));
|
|
673
|
+
if (parents.length > 1) {
|
|
674
|
+
const parentSpan = spans.get(branch);
|
|
675
|
+
if (parentSpan) parentSpan.end = Math.max(parentSpan.end, index);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
return spans;
|
|
681
|
+
}, [commits, branches]);
|
|
682
|
+
const [current, setCurrent] = useState(void 0);
|
|
683
|
+
const currentRef = useDynamicRef(current);
|
|
684
|
+
const selectedRef = useDynamicRef(void 0);
|
|
685
|
+
const currentCommit = useMemo(() => current !== void 0 ? commits[current] : void 0, [current, commits]);
|
|
686
|
+
const highlightedBranch = branch ?? currentCommit?.branch;
|
|
687
|
+
useEffect(() => {
|
|
688
|
+
onChange?.({
|
|
689
|
+
current,
|
|
690
|
+
commit: current === void 0 ? void 0 : commits[current]
|
|
691
|
+
});
|
|
692
|
+
(containerRef.current?.querySelector(`[data-index="${current}"]`))?.scrollIntoView({
|
|
693
|
+
behavior: "instant",
|
|
694
|
+
block: "nearest"
|
|
695
|
+
});
|
|
696
|
+
}, [onChange, current]);
|
|
697
|
+
useEffect(() => {
|
|
698
|
+
if (!branch) return;
|
|
699
|
+
if (currentRef.current !== void 0 && commits[currentRef.current]?.branch === branch) return;
|
|
700
|
+
const index = commits.findIndex((commit) => commit.branch === branch);
|
|
701
|
+
if (index >= 0) setCurrent(index);
|
|
702
|
+
}, [branch]);
|
|
703
|
+
useEffect(() => {
|
|
704
|
+
if (!containerRef.current) return;
|
|
705
|
+
return addEventListener(containerRef.current, "keydown", (event) => {
|
|
706
|
+
switch (event.key) {
|
|
707
|
+
case "ArrowUp":
|
|
708
|
+
event.preventDefault();
|
|
709
|
+
if (event.metaKey || currentRef.current === void 0) setCurrent(0);
|
|
710
|
+
else setCurrent((selected) => {
|
|
711
|
+
if (event.shiftKey && selected !== void 0) {
|
|
712
|
+
const branch = commits[selected].branch;
|
|
713
|
+
for (let i = selected - 1; i >= 0; i--) if (commits[i].branch === branch) return i;
|
|
714
|
+
return selected;
|
|
715
|
+
} else return selected === void 0 ? commits.length - 1 : Math.max(0, selected - 1);
|
|
716
|
+
});
|
|
717
|
+
break;
|
|
718
|
+
case "ArrowDown":
|
|
719
|
+
event.preventDefault();
|
|
720
|
+
if (event.metaKey || currentRef.current === void 0) setCurrent(commits.length - 1);
|
|
721
|
+
else setCurrent((selected) => {
|
|
722
|
+
if (event.shiftKey && selected !== void 0) {
|
|
723
|
+
const branch = commits[selected].branch;
|
|
724
|
+
for (let i = selected + 1; i <= commits.length - 1; i++) if (commits[i].branch === branch) return i;
|
|
725
|
+
return selected;
|
|
726
|
+
} else return selected === void 0 ? 0 : Math.min(commits.length - 1, selected + 1);
|
|
727
|
+
});
|
|
728
|
+
break;
|
|
729
|
+
case "Enter":
|
|
730
|
+
event.preventDefault();
|
|
731
|
+
if (currentRef.current !== void 0) {
|
|
732
|
+
selectedRef.current = currentRef.current === selectedRef.current ? void 0 : currentRef.current;
|
|
733
|
+
onSelect?.(selectedRef.current !== void 0 ? commits[selectedRef.current] : void 0);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}, [commits, containerRef.current]);
|
|
738
|
+
return /* @__PURE__ */ jsx("div", {
|
|
739
|
+
...composableProps(props, { classNames: "grid auto-rows-min outline-none" }),
|
|
740
|
+
tabIndex: 0,
|
|
741
|
+
style: { gridTemplateColumns: [
|
|
742
|
+
"min-content",
|
|
743
|
+
showIcon && "1.25rem",
|
|
744
|
+
"1fr",
|
|
745
|
+
showTimestamp && "max-content"
|
|
746
|
+
].filter(Boolean).join(" ") },
|
|
747
|
+
ref: containerRef,
|
|
748
|
+
children: commits.length < 1 ? /* @__PURE__ */ jsx("p", {
|
|
749
|
+
className: "col-span-full text-description p-trim-md",
|
|
750
|
+
children: t("no-commits.message")
|
|
751
|
+
}) : commits.map((commit, index) => {
|
|
752
|
+
if (getBranchIndex(commit.branch) === -1) return null;
|
|
753
|
+
const hasLink = !!commit.link && !!onSelect;
|
|
754
|
+
const handleClick = () => {
|
|
755
|
+
if (selectedRef.current === index) {
|
|
756
|
+
selectedRef.current = void 0;
|
|
757
|
+
setCurrent(void 0);
|
|
758
|
+
onSelect?.(void 0);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
setCurrent(index);
|
|
762
|
+
selectedRef.current = index;
|
|
763
|
+
onSelect?.(commit);
|
|
764
|
+
};
|
|
765
|
+
const message = debug ? JSON.stringify({
|
|
766
|
+
id: commit.id,
|
|
767
|
+
parents: commit.parents
|
|
768
|
+
}) : commit.message;
|
|
769
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
770
|
+
"data-index": index,
|
|
771
|
+
"aria-current": current === index,
|
|
772
|
+
className: mx("group/row col-span-full grid grid-cols-subgrid gap-1 px-[2px] overflow-hidden items-center cursor-pointer pe-2", "aria-current:bg-current-surface! hover:bg-hover-surface-subtle"),
|
|
773
|
+
style: { height: `${options.lineHeight}px` },
|
|
774
|
+
onClick: handleClick,
|
|
775
|
+
children: [
|
|
776
|
+
/* @__PURE__ */ jsx("div", {
|
|
777
|
+
className: "px-2",
|
|
778
|
+
children: /* @__PURE__ */ jsx(LineVector, {
|
|
779
|
+
branchLane,
|
|
780
|
+
laneCount,
|
|
781
|
+
spans,
|
|
782
|
+
index,
|
|
783
|
+
commit,
|
|
784
|
+
highlightedBranch,
|
|
785
|
+
options
|
|
786
|
+
})
|
|
787
|
+
}),
|
|
788
|
+
showIcon && /* @__PURE__ */ jsx(CommitIcon, { commit }),
|
|
789
|
+
/* @__PURE__ */ jsx("div", {
|
|
790
|
+
className: mx("text-sm truncate cursor-pointer text-description font-thin group-aria-current/row:text-current-fg hover:text-current-fg", hasLink && "underline decoration-dotted underline-offset-2"),
|
|
791
|
+
children: hasShimmerEffect(commit) ? /* @__PURE__ */ jsx(Shimmer, { children: message }) : message
|
|
792
|
+
}),
|
|
793
|
+
showTimestamp && /* @__PURE__ */ jsx("div", {
|
|
794
|
+
className: "text-xs tabular-nums items-center text-description font-thin",
|
|
795
|
+
children: commit.timestamp && format(commit.timestamp, TIMESTAMP_FORMAT)
|
|
796
|
+
})
|
|
797
|
+
]
|
|
798
|
+
}, commit.id);
|
|
799
|
+
})
|
|
800
|
+
});
|
|
801
|
+
}));
|
|
802
|
+
Timeline.displayName = "Timeline";
|
|
803
|
+
var CommitIcon = memo(({ commit }) => {
|
|
804
|
+
if (!commit.icon) return /* @__PURE__ */ jsx("div", {});
|
|
805
|
+
return /* @__PURE__ */ jsx(Icon, {
|
|
806
|
+
icon: commit.icon,
|
|
807
|
+
size: 4,
|
|
808
|
+
synchronized: true,
|
|
809
|
+
classNames: mx(commit.icon === "ph--spinner-gap--regular" && "animate-spin", commit.level !== void 0 && levelColors[commit.level])
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
var colors = [
|
|
813
|
+
{
|
|
814
|
+
stroke: "stroke-orange-500",
|
|
815
|
+
fill: "group-aria-[current=true]:fill-orange-500"
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
stroke: "stroke-sky-500",
|
|
819
|
+
fill: "group-aria-[current=true]:fill-sky-500"
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
stroke: "stroke-green-500",
|
|
823
|
+
fill: "group-aria-[current=true]:fill-green-500"
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
stroke: "stroke-fuchsia-500",
|
|
827
|
+
fill: "group-aria-[current=true]:fill-fuchsia-500"
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
stroke: "stroke-cyan-500",
|
|
831
|
+
fill: "group-aria-[current=true]:fill-cyan-500"
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
stroke: "stroke-emerald-500",
|
|
835
|
+
fill: "group-aria-[current=true]:fill-emerald-500"
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
stroke: "stroke-violet-500",
|
|
839
|
+
fill: "group-aria-[current=true]:fill-violet-500"
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
stroke: "stroke-teal-500",
|
|
843
|
+
fill: "group-aria-[current=true]:fill-teal-500"
|
|
844
|
+
}
|
|
845
|
+
];
|
|
846
|
+
var levelColors = {
|
|
847
|
+
[LogLevel.TRACE]: "text-gray-500",
|
|
848
|
+
[LogLevel.DEBUG]: "text-gray-500",
|
|
849
|
+
[LogLevel.VERBOSE]: "text-gray-500",
|
|
850
|
+
[LogLevel.INFO]: "text-green-500",
|
|
851
|
+
[LogLevel.WARN]: "text-orange-500",
|
|
852
|
+
[LogLevel.ERROR]: "text-red-500"
|
|
853
|
+
};
|
|
854
|
+
/**
|
|
855
|
+
* SVG for node and connector paths.
|
|
856
|
+
*/
|
|
857
|
+
var LineVector = memo(({ branchLane, laneCount, spans, index, commit, highlightedBranch, options }) => {
|
|
858
|
+
const halfHeight = options.lineHeight / 2;
|
|
859
|
+
const cx = (c) => c * options.columnWidth + options.columnWidth / 2;
|
|
860
|
+
const getBranchIndex = (branch) => branchLane.get(branch) ?? -1;
|
|
861
|
+
const createPath = (index, commit, branch, span) => {
|
|
862
|
+
const parents = commit.parents ?? [];
|
|
863
|
+
const commitIndex = getBranchIndex(commit.branch);
|
|
864
|
+
const branchIndex = getBranchIndex(branch);
|
|
865
|
+
if (span.start < index && index < span.end) return `M ${cx(branchIndex)} 0 l 0 ${options.lineHeight}`;
|
|
866
|
+
else if (commit.branch === branch && parents.length > 0) return `M ${cx(branchIndex)} 0 l 0 ${halfHeight}`;
|
|
867
|
+
else if (commit.branch === branch && index < span.end) return `M ${cx(branchIndex)} ${halfHeight} l 0 ${options.lineHeight}`;
|
|
868
|
+
if (commit.branch !== branch && index === span.start) return trim`
|
|
869
|
+
M ${cx(commitIndex)} ${halfHeight}
|
|
870
|
+
L ${cx(branchIndex) - halfHeight} ${halfHeight}
|
|
871
|
+
a ${halfHeight} ${halfHeight} 0 0 1 ${halfHeight} ${halfHeight}
|
|
872
|
+
`;
|
|
873
|
+
if (commit.branch !== branch && index === span.end) return trim`
|
|
874
|
+
M ${cx(commitIndex)} ${halfHeight}
|
|
875
|
+
L ${cx(branchIndex) - halfHeight} ${halfHeight}
|
|
876
|
+
a ${halfHeight} ${halfHeight} -90 0 0 ${halfHeight} ${-halfHeight}
|
|
877
|
+
`;
|
|
878
|
+
};
|
|
879
|
+
const col = getBranchIndex(commit.branch);
|
|
880
|
+
const color = colors[col % colors.length];
|
|
881
|
+
const opacity = (branch) => ["duration-500 transition-opacity", highlightedBranch === void 0 || branch === highlightedBranch ? "opacity-100" : "opacity-50"];
|
|
882
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
883
|
+
width: laneCount * options.columnWidth,
|
|
884
|
+
height: options.lineHeight,
|
|
885
|
+
children: [
|
|
886
|
+
[...spans.entries()].map(([branch, span]) => {
|
|
887
|
+
const lane = getBranchIndex(branch);
|
|
888
|
+
if (lane < 0) return null;
|
|
889
|
+
const color = colors[lane % colors.length];
|
|
890
|
+
const path = createPath(index, commit, branch, span);
|
|
891
|
+
if (!path) return null;
|
|
892
|
+
return /* @__PURE__ */ jsx("path", {
|
|
893
|
+
d: path,
|
|
894
|
+
fill: "none",
|
|
895
|
+
className: mx(options.lineStyle, color.stroke, color.fill, opacity(branch))
|
|
896
|
+
}, branch);
|
|
897
|
+
}),
|
|
898
|
+
/* @__PURE__ */ jsx("circle", {
|
|
899
|
+
cx: cx(col),
|
|
900
|
+
cy: halfHeight,
|
|
901
|
+
r: options.nodeRadius,
|
|
902
|
+
className: mx("fill-base-surface stroke-base-surface")
|
|
903
|
+
}),
|
|
904
|
+
/* @__PURE__ */ jsx("circle", {
|
|
905
|
+
cx: cx(col),
|
|
906
|
+
cy: halfHeight,
|
|
907
|
+
r: options.nodeRadius,
|
|
908
|
+
className: mx("fill-base-surface", options.lineStyle, color?.stroke, color?.fill, opacity(commit.branch))
|
|
909
|
+
})
|
|
910
|
+
]
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region src/components/Timeline/timeline-printer.ts
|
|
915
|
+
var NODE = "●";
|
|
916
|
+
var MERGE_NODE = "◆";
|
|
917
|
+
var RAIL = "│";
|
|
918
|
+
var HORIZ = "─";
|
|
919
|
+
var COL_WIDTH = 3;
|
|
920
|
+
/**
|
|
921
|
+
* Renders commits as a monospace ASCII graph with rounded box-drawing arcs and vertical rails.
|
|
922
|
+
* Commits must be ordered chronologically (oldest first, newest last). Parents must refer to older commits.
|
|
923
|
+
*/
|
|
924
|
+
var renderTimelineAscii = (commits, branches) => {
|
|
925
|
+
if (commits.length === 0) return "";
|
|
926
|
+
const laneById = assignLanes(commits, branches);
|
|
927
|
+
const lanes = commits.map((commit) => laneById.get(commit.id));
|
|
928
|
+
const maxLane = Math.max(...lanes);
|
|
929
|
+
if (maxLane === 0) return commits.map((commit) => `${NODE} ${formatLabel(commit)}`).join("\n");
|
|
930
|
+
const active = computeActiveColumns(commits, lanes, laneById);
|
|
931
|
+
const graphParts = commits.map((commit, row) => renderRow(commit, lanes[row], maxLane, active[row], laneById));
|
|
932
|
+
const maxGraphWidth = Math.max(...graphParts.map((part) => part.length));
|
|
933
|
+
return commits.map((commit, row) => `${graphParts[row].padEnd(maxGraphWidth)} ${formatLabel(commit)}`).join("\n");
|
|
934
|
+
};
|
|
935
|
+
var renderRow = (commit, commitLane, maxLane, activeRow, laneById) => {
|
|
936
|
+
const totalChars = (maxLane + 1) * COL_WIDTH;
|
|
937
|
+
const chars = Array(totalChars).fill(" ");
|
|
938
|
+
const merge = isMergeCommit(commit);
|
|
939
|
+
const fork = !merge && isForkCommit(commit, laneById, commitLane);
|
|
940
|
+
let connectLane = -1;
|
|
941
|
+
if (fork) {
|
|
942
|
+
const parentLane = laneById.get(commit.parents[0]);
|
|
943
|
+
if (parentLane !== void 0) connectLane = parentLane;
|
|
944
|
+
} else if (merge) for (const parentId of commit.parents ?? []) {
|
|
945
|
+
const parentLane = laneById.get(parentId);
|
|
946
|
+
if (parentLane !== void 0 && parentLane !== commitLane) {
|
|
947
|
+
connectLane = parentLane;
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
for (let col = 0; col <= maxLane; col++) {
|
|
952
|
+
if (col === commitLane) continue;
|
|
953
|
+
if (activeRow[col]) chars[col * COL_WIDTH] = RAIL;
|
|
954
|
+
}
|
|
955
|
+
chars[commitLane * COL_WIDTH] = merge ? MERGE_NODE : NODE;
|
|
956
|
+
if (connectLane >= 0) {
|
|
957
|
+
const connectPos = connectLane * COL_WIDTH;
|
|
958
|
+
if (fork) chars[connectPos] = connectLane < commitLane ? "├" : "┤";
|
|
959
|
+
else chars[connectPos] = connectLane > commitLane ? "╯" : "╰";
|
|
960
|
+
const minCol = Math.min(commitLane, connectLane);
|
|
961
|
+
const maxCol = Math.max(commitLane, connectLane);
|
|
962
|
+
for (let pos = minCol * COL_WIDTH + 1; pos < maxCol * COL_WIDTH; pos++) if (pos % COL_WIDTH === 0) {
|
|
963
|
+
if (activeRow[pos / COL_WIDTH]) chars[pos] = "┼";
|
|
964
|
+
else chars[pos] = HORIZ;
|
|
965
|
+
} else chars[pos] = HORIZ;
|
|
966
|
+
}
|
|
967
|
+
return chars.join("").trimEnd();
|
|
968
|
+
};
|
|
969
|
+
var formatLabel = (commit) => {
|
|
970
|
+
const icon = commit.icon?.replace(/^ph--/, "").replace(/--regular$/, "").replace(/--bold$/, "").replace(/--fill$/, "");
|
|
971
|
+
return icon ? `[${icon}] ${commit.message}` : commit.message;
|
|
972
|
+
};
|
|
973
|
+
/**
|
|
974
|
+
* Assigns each commit to a lane (column), reusing lanes once a branch has been merged.
|
|
975
|
+
* A branch is considered "merged" when a commit on another branch references it as a parent.
|
|
976
|
+
* Unmerged branches keep their lane active (they may still be in progress).
|
|
977
|
+
* Lane 0 is reserved for the first branch (typically 'main').
|
|
978
|
+
*/
|
|
979
|
+
var assignLanes = (commits, branches) => {
|
|
980
|
+
const commitBranch = /* @__PURE__ */ new Map();
|
|
981
|
+
for (const commit of commits) commitBranch.set(commit.id, commit.branch);
|
|
982
|
+
const mergeRow = /* @__PURE__ */ new Map();
|
|
983
|
+
for (let row = 0; row < commits.length; row++) {
|
|
984
|
+
const commit = commits[row];
|
|
985
|
+
for (const parentId of commit.parents ?? []) {
|
|
986
|
+
const parentBranch = commitBranch.get(parentId);
|
|
987
|
+
if (parentBranch && parentBranch !== commit.branch) mergeRow.set(parentBranch, Math.max(mergeRow.get(parentBranch) ?? row, row));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const branchLane = /* @__PURE__ */ new Map();
|
|
991
|
+
if (branches.length > 0) branchLane.set(branches[0], 0);
|
|
992
|
+
const activeLanes = /* @__PURE__ */ new Set([0]);
|
|
993
|
+
const laneById = /* @__PURE__ */ new Map();
|
|
994
|
+
for (let row = 0; row < commits.length; row++) {
|
|
995
|
+
const commit = commits[row];
|
|
996
|
+
if (!branchLane.has(commit.branch)) {
|
|
997
|
+
let lane = 1;
|
|
998
|
+
while (activeLanes.has(lane)) lane++;
|
|
999
|
+
branchLane.set(commit.branch, lane);
|
|
1000
|
+
activeLanes.add(lane);
|
|
1001
|
+
}
|
|
1002
|
+
laneById.set(commit.id, branchLane.get(commit.branch));
|
|
1003
|
+
for (const [branch, endRow] of mergeRow) if (endRow === row && branch !== branches[0] && branchLane.has(branch)) activeLanes.delete(branchLane.get(branch));
|
|
1004
|
+
}
|
|
1005
|
+
return laneById;
|
|
1006
|
+
};
|
|
1007
|
+
var isMergeCommit = (commit) => (commit.parents?.length ?? 0) > 1;
|
|
1008
|
+
var isForkCommit = (commit, laneById, lane) => {
|
|
1009
|
+
const parents = commit.parents;
|
|
1010
|
+
if (!parents || parents.length !== 1) return false;
|
|
1011
|
+
const parentLane = laneById.get(parents[0]);
|
|
1012
|
+
return parentLane !== void 0 && parentLane !== lane;
|
|
1013
|
+
};
|
|
1014
|
+
/**
|
|
1015
|
+
* Column `col` is active on `row` when `row` falls inside the vertical span
|
|
1016
|
+
* of that column (the range of rows that include commits on that lane plus
|
|
1017
|
+
* merge rows referencing parents on that lane) AND the row's own commit
|
|
1018
|
+
* is not on that column.
|
|
1019
|
+
*/
|
|
1020
|
+
var computeActiveColumns = (commits, lanes, laneById) => {
|
|
1021
|
+
const rows = commits.length;
|
|
1022
|
+
const cols = Math.max(...lanes, 0) + 1;
|
|
1023
|
+
const rowIndicesByCol = Array.from({ length: cols }, () => []);
|
|
1024
|
+
for (let row = 0; row < rows; row++) rowIndicesByCol[lanes[row]].push(row);
|
|
1025
|
+
for (let row = 0; row < rows; row++) for (const parentId of commits[row]?.parents ?? []) {
|
|
1026
|
+
const parentLane = laneById.get(parentId);
|
|
1027
|
+
if (parentLane === void 0) continue;
|
|
1028
|
+
rowIndicesByCol[parentLane].push(row);
|
|
1029
|
+
}
|
|
1030
|
+
const active = Array.from({ length: rows }, () => Array(cols).fill(false));
|
|
1031
|
+
for (let col = 0; col < cols; col++) {
|
|
1032
|
+
const indices = rowIndicesByCol[col];
|
|
1033
|
+
if (indices.length === 0) continue;
|
|
1034
|
+
const minR = Math.min(...indices);
|
|
1035
|
+
const maxR = Math.max(...indices);
|
|
1036
|
+
for (let row = minR; row <= maxR; row++) if (lanes[row] !== col) active[row][col] = true;
|
|
1037
|
+
}
|
|
1038
|
+
return active;
|
|
1039
|
+
};
|
|
1040
|
+
//#endregion
|
|
1041
|
+
//#region src/components/TracePanel/trace-filter.ts
|
|
1042
|
+
/**
|
|
1043
|
+
* What a running process is working on behalf of, as a single bucket.
|
|
1044
|
+
*
|
|
1045
|
+
* Bucketed from the inherited {@link Process.Environment}, so filtering keeps whole subtrees together.
|
|
1046
|
+
*/
|
|
1047
|
+
var ProcessEnvironment = {
|
|
1048
|
+
/** Not scoped to a space: layout, navigation, identity, settings, space lifecycle. */
|
|
1049
|
+
App: "app",
|
|
1050
|
+
/** Scoped to a space, but not to a conversation within it. */
|
|
1051
|
+
Space: "space",
|
|
1052
|
+
/** Serving a conversation: an agent's turn and every tool call it spawns. */
|
|
1053
|
+
Conversation: "conversation"
|
|
1054
|
+
};
|
|
1055
|
+
/** Every environment, in menu order — widening scope, so the filter reads the same on every trace. */
|
|
1056
|
+
var ALL_PROCESS_ENVIRONMENTS = [
|
|
1057
|
+
ProcessEnvironment.App,
|
|
1058
|
+
ProcessEnvironment.Space,
|
|
1059
|
+
ProcessEnvironment.Conversation
|
|
1060
|
+
];
|
|
1061
|
+
/** App-level processes start hidden: that bucket fires on every click, whatever the user is watching. */
|
|
1062
|
+
var DEFAULT_PROCESS_ENVIRONMENTS = [ProcessEnvironment.Space, ProcessEnvironment.Conversation];
|
|
1063
|
+
var ENVIRONMENT_ICONS = {
|
|
1064
|
+
[ProcessEnvironment.App]: "ph--app-window--regular",
|
|
1065
|
+
[ProcessEnvironment.Space]: "ph--planet--regular",
|
|
1066
|
+
[ProcessEnvironment.Conversation]: "ph--chat-teardrop-text--regular"
|
|
1067
|
+
};
|
|
1068
|
+
var environmentIcon = (environment) => ENVIRONMENT_ICONS[environment];
|
|
1069
|
+
/** `conversation` outranks `space`, since a conversation always runs inside one. */
|
|
1070
|
+
var processEnvironment = (process) => {
|
|
1071
|
+
if (process.environment.conversation !== void 0) return ProcessEnvironment.Conversation;
|
|
1072
|
+
if (process.environment.space !== void 0) return ProcessEnvironment.Space;
|
|
1073
|
+
return ProcessEnvironment.App;
|
|
1074
|
+
};
|
|
1075
|
+
var filterProcesses = (processes, selected) => {
|
|
1076
|
+
if (selected.length === ALL_PROCESS_ENVIRONMENTS.length) return processes;
|
|
1077
|
+
const selection = new Set(selected);
|
|
1078
|
+
return processes.filter((process) => selection.has(processEnvironment(process)));
|
|
1079
|
+
};
|
|
1080
|
+
/** Toggles an environment, preserving the canonical order. */
|
|
1081
|
+
var toggleProcessEnvironment = (selected, environment) => ALL_PROCESS_ENVIRONMENTS.filter((candidate) => candidate === environment ? !selected.includes(environment) : selected.includes(candidate));
|
|
1082
|
+
/**
|
|
1083
|
+
* Narrows a persisted selection to the known environments, in canonical order.
|
|
1084
|
+
*
|
|
1085
|
+
* Settings outlive the vocabulary that wrote them; an unrecognized value would filter the panel to nothing.
|
|
1086
|
+
*/
|
|
1087
|
+
var parseProcessEnvironments = (selected) => selected === void 0 ? DEFAULT_PROCESS_ENVIRONMENTS : ALL_PROCESS_ENVIRONMENTS.filter((environment) => selected.includes(environment));
|
|
1088
|
+
/**
|
|
1089
|
+
* Narrows trace messages to the selected processes and their descendants: a tool call or sub-agent
|
|
1090
|
+
* runs as a child of the process it serves, so its events are that process's work. An empty
|
|
1091
|
+
* selection is no filter.
|
|
1092
|
+
*/
|
|
1093
|
+
var filterTraceMessages = (messages, selected) => {
|
|
1094
|
+
if (selected.length === 0) return messages;
|
|
1095
|
+
const parentByPid = /* @__PURE__ */ new Map();
|
|
1096
|
+
for (const message of messages) if (message.meta.pid !== void 0) parentByPid.set(message.meta.pid, message.meta.parentPid);
|
|
1097
|
+
const selection = new Set(selected);
|
|
1098
|
+
const matches = /* @__PURE__ */ new Map();
|
|
1099
|
+
const isSelected = (pid) => {
|
|
1100
|
+
const known = matches.get(pid);
|
|
1101
|
+
if (known !== void 0) return known;
|
|
1102
|
+
matches.set(pid, false);
|
|
1103
|
+
const parent = parentByPid.get(pid);
|
|
1104
|
+
const result = selection.has(pid) || parent !== void 0 && isSelected(parent);
|
|
1105
|
+
matches.set(pid, result);
|
|
1106
|
+
return result;
|
|
1107
|
+
};
|
|
1108
|
+
return messages.filter((message) => message.meta.pid !== void 0 && isSelected(message.meta.pid));
|
|
1109
|
+
};
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region src/components/TracePanel/useTraceMenu.ts
|
|
1112
|
+
/**
|
|
1113
|
+
* Environment filter menu for the trace panel's toolbar.
|
|
1114
|
+
* The group collapses to a funnel trigger, so the panel's own rows keep the width.
|
|
1115
|
+
*/
|
|
1116
|
+
var useTraceMenu = ({ selected, onSelectedChange, selectionCount = 0, onClearSelection }) => {
|
|
1117
|
+
const handleToggle = useCallback((environment) => onSelectedChange(toggleProcessEnvironment(selected, environment)), [selected, onSelectedChange]);
|
|
1118
|
+
return useMenuBuilder(() => MenuBuilder.make().action("clearSelection", {
|
|
1119
|
+
label: ["trace-clear-selection.label", { ns: translationKey }],
|
|
1120
|
+
icon: "ph--selection-slash--regular",
|
|
1121
|
+
iconOnly: true,
|
|
1122
|
+
hidden: selectionCount === 0,
|
|
1123
|
+
testId: "tracePanel.clearSelection"
|
|
1124
|
+
}, () => onClearSelection?.()).group("processEnvironments", {
|
|
1125
|
+
label: ["trace-filter.menu", { ns: translationKey }],
|
|
1126
|
+
icon: "ph--funnel--regular",
|
|
1127
|
+
iconOnly: true,
|
|
1128
|
+
variant: "dropdownMenu",
|
|
1129
|
+
selectCardinality: "multiple",
|
|
1130
|
+
value: [...selected],
|
|
1131
|
+
testId: "tracePanel.filter"
|
|
1132
|
+
}, (group) => {
|
|
1133
|
+
for (const environment of ALL_PROCESS_ENVIRONMENTS) group.action(environment, {
|
|
1134
|
+
label: [`trace-environment-${environment}.label`, { ns: translationKey }],
|
|
1135
|
+
icon: environmentIcon(environment),
|
|
1136
|
+
checked: selected.includes(environment)
|
|
1137
|
+
}, () => handleToggle(environment));
|
|
1138
|
+
group.separator("line");
|
|
1139
|
+
group.action("all", {
|
|
1140
|
+
label: ["trace-filter-all.label", { ns: translationKey }],
|
|
1141
|
+
icon: "ph--list-checks--regular"
|
|
1142
|
+
}, () => onSelectedChange([...ALL_PROCESS_ENVIRONMENTS]));
|
|
1143
|
+
group.action("none", {
|
|
1144
|
+
label: ["trace-filter-none.label", { ns: translationKey }],
|
|
1145
|
+
icon: "ph--prohibit--regular"
|
|
1146
|
+
}, () => onSelectedChange([]));
|
|
1147
|
+
}).build(), [
|
|
1148
|
+
handleToggle,
|
|
1149
|
+
selected,
|
|
1150
|
+
onSelectedChange,
|
|
1151
|
+
selectionCount,
|
|
1152
|
+
onClearSelection
|
|
1153
|
+
]);
|
|
1154
|
+
};
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region src/components/TracePanel/TracePanel.tsx
|
|
1157
|
+
/**
|
|
1158
|
+
* A space's runtime: its processes as a tree, its trace as a commit graph, and the picked commit's
|
|
1159
|
+
* event. Presentation only — the host resolves the process monitor, the trace feed, settings and
|
|
1160
|
+
* navigation and hands them in as props.
|
|
1161
|
+
*/
|
|
1162
|
+
var TracePanel = composable(({ classNames, processes, graph, environments, onEnvironmentsChange, debug = false, resolveLabel, selected = NO_SELECTION, onSelectedChange, onProcessTerminate, onOpenLink, ...props }, forwardedRef) => {
|
|
1163
|
+
const { t } = useTranslation(translationKey);
|
|
1164
|
+
const handleClearSelection = useCallback(() => onSelectedChange?.([]), [onSelectedChange]);
|
|
1165
|
+
const menu = useTraceMenu({
|
|
1166
|
+
selected: environments,
|
|
1167
|
+
onSelectedChange: onEnvironmentsChange,
|
|
1168
|
+
selectionCount: selected.length,
|
|
1169
|
+
onClearSelection: handleClearSelection
|
|
1170
|
+
});
|
|
1171
|
+
const { branches, commits, spanTree, details } = useDeferredValue(graph);
|
|
1172
|
+
const [selectedCommit, setSelectedCommit] = useState();
|
|
1173
|
+
const [openSections, setOpenSections] = useState(SECTIONS.map((section) => section.id));
|
|
1174
|
+
const handleCommitSelect = useCallback((commit) => {
|
|
1175
|
+
setSelectedCommit(commit);
|
|
1176
|
+
if (commit?.link) onOpenLink?.(commit.link);
|
|
1177
|
+
}, [onOpenLink]);
|
|
1178
|
+
const currentBranch = useMemo(() => {
|
|
1179
|
+
const last = selected.at(-1);
|
|
1180
|
+
return last !== void 0 && branches.includes(last) ? last : null;
|
|
1181
|
+
}, [selected, branches]);
|
|
1182
|
+
return /* @__PURE__ */ jsxs(Panel.Root, {
|
|
1183
|
+
...composableProps(props, { classNames: ["h-full", classNames] }),
|
|
1184
|
+
ref: forwardedRef,
|
|
1185
|
+
children: [/* @__PURE__ */ jsx(Panel.Toolbar, {
|
|
1186
|
+
asChild: true,
|
|
1187
|
+
children: /* @__PURE__ */ jsx(ActionToolbar, {
|
|
1188
|
+
...menu,
|
|
1189
|
+
alwaysActive: true,
|
|
1190
|
+
classNames: "justify-end"
|
|
1191
|
+
})
|
|
1192
|
+
}), /* @__PURE__ */ jsx(Panel.Content, { children: /* @__PURE__ */ jsx(Accordion.Root, {
|
|
1193
|
+
items: SECTIONS,
|
|
1194
|
+
value: openSections,
|
|
1195
|
+
onValueChange: setOpenSections,
|
|
1196
|
+
classNames: "h-full min-h-0 rounded-none border-y-0",
|
|
1197
|
+
children: ({ items }) => items.map((section) => {
|
|
1198
|
+
switch (section.id) {
|
|
1199
|
+
case "processes": return /* @__PURE__ */ jsxs(Accordion.Item, {
|
|
1200
|
+
item: section,
|
|
1201
|
+
classNames: "border-x-0",
|
|
1202
|
+
children: [/* @__PURE__ */ jsx(Accordion.ItemHeader, {
|
|
1203
|
+
hover: true,
|
|
1204
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1205
|
+
className: "text-sm text-description",
|
|
1206
|
+
children: t("trace-processes.label")
|
|
1207
|
+
})
|
|
1208
|
+
}), /* @__PURE__ */ jsx(Accordion.ItemBody, {
|
|
1209
|
+
classNames: "p-0",
|
|
1210
|
+
children: /* @__PURE__ */ jsx(ProcessTreeContainer, {
|
|
1211
|
+
classNames: "max-h-[8lh]",
|
|
1212
|
+
processes,
|
|
1213
|
+
environments,
|
|
1214
|
+
resolveLabel,
|
|
1215
|
+
selected,
|
|
1216
|
+
onSelectedChange,
|
|
1217
|
+
onProcessTerminate
|
|
1218
|
+
})
|
|
1219
|
+
})]
|
|
1220
|
+
}, section.id);
|
|
1221
|
+
case "trace": return /* @__PURE__ */ jsxs(Accordion.Item, {
|
|
1222
|
+
item: section,
|
|
1223
|
+
classNames: mx("border-x-0 dx-grow flex flex-col", "[&>[data-part=item-content]]:dx-grow [&>[data-part=item-content]]:flex [&>[data-part=item-content]]:flex-col [&>[data-part=item-content]]:animate-none"),
|
|
1224
|
+
children: [/* @__PURE__ */ jsx(Accordion.ItemHeader, {
|
|
1225
|
+
hover: true,
|
|
1226
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1227
|
+
className: "text-sm text-description",
|
|
1228
|
+
children: t("trace.label")
|
|
1229
|
+
})
|
|
1230
|
+
}), /* @__PURE__ */ jsx(Accordion.ItemBody, {
|
|
1231
|
+
classNames: "dx-grow grid grid-rows-[minmax(0,1fr)]",
|
|
1232
|
+
children: /* @__PURE__ */ jsx(ScrollContainer.Root, {
|
|
1233
|
+
pin: true,
|
|
1234
|
+
children: /* @__PURE__ */ jsxs(ScrollContainer.Content, {
|
|
1235
|
+
thin: true,
|
|
1236
|
+
children: [
|
|
1237
|
+
/* @__PURE__ */ jsx(ScrollContainer.Fade, {}),
|
|
1238
|
+
/* @__PURE__ */ jsx(ScrollContainer.Viewport, { children: debug ? /* @__PURE__ */ jsx(JsonHighlighter, {
|
|
1239
|
+
data: spanTree,
|
|
1240
|
+
classNames: "text-xs"
|
|
1241
|
+
}) : /* @__PURE__ */ jsx(Timeline, {
|
|
1242
|
+
branches,
|
|
1243
|
+
branch: currentBranch,
|
|
1244
|
+
commits,
|
|
1245
|
+
showTimestamp: true,
|
|
1246
|
+
onSelect: handleCommitSelect
|
|
1247
|
+
}) }),
|
|
1248
|
+
/* @__PURE__ */ jsx(ScrollContainer.ScrollDownButton, {})
|
|
1249
|
+
]
|
|
1250
|
+
})
|
|
1251
|
+
})
|
|
1252
|
+
})]
|
|
1253
|
+
}, section.id);
|
|
1254
|
+
case "details": {
|
|
1255
|
+
const commit = debug ? void 0 : selectedCommit;
|
|
1256
|
+
return /* @__PURE__ */ jsxs(Accordion.Item, {
|
|
1257
|
+
item: section,
|
|
1258
|
+
disabled: !commit,
|
|
1259
|
+
classNames: "border-x-0",
|
|
1260
|
+
children: [/* @__PURE__ */ jsx(Accordion.ItemHeader, {
|
|
1261
|
+
hover: true,
|
|
1262
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1263
|
+
className: "block truncate text-sm text-description",
|
|
1264
|
+
children: commit?.message ?? t("trace-details.label")
|
|
1265
|
+
})
|
|
1266
|
+
}), commit && /* @__PURE__ */ jsx(Accordion.ItemBody, {
|
|
1267
|
+
classNames: "p-0",
|
|
1268
|
+
children: /* @__PURE__ */ jsx(JsonHighlighter, {
|
|
1269
|
+
data: details[commit.id] ?? commit,
|
|
1270
|
+
classNames: "max-h-[20lh] text-xs"
|
|
1271
|
+
})
|
|
1272
|
+
})]
|
|
1273
|
+
}, section.id);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
})
|
|
1277
|
+
}) })]
|
|
1278
|
+
});
|
|
1279
|
+
});
|
|
1280
|
+
TracePanel.displayName = "TracePanel";
|
|
1281
|
+
var SECTIONS = [
|
|
1282
|
+
{ id: "processes" },
|
|
1283
|
+
{ id: "trace" },
|
|
1284
|
+
{ id: "details" }
|
|
1285
|
+
];
|
|
1286
|
+
var NO_SELECTION = [];
|
|
1287
|
+
var ProcessTreeContainer = ({ classNames, processes, environments, resolveLabel, selected, onSelectedChange, onProcessTerminate }) => {
|
|
1288
|
+
const processesDeferred = useDeferredValue(processes);
|
|
1289
|
+
const visibleProcesses = useMemo(() => filterProcesses(processesDeferred, environments), [processesDeferred, environments]);
|
|
1290
|
+
return /* @__PURE__ */ jsx(ProcessTree, {
|
|
1291
|
+
classNames,
|
|
1292
|
+
depth: 3,
|
|
1293
|
+
processes: visibleProcesses,
|
|
1294
|
+
resolveLabel,
|
|
1295
|
+
selected,
|
|
1296
|
+
onSelectedChange,
|
|
1297
|
+
onProcessTerminate
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
1300
|
+
//#endregion
|
|
1301
|
+
//#region src/execution-graph/span-tree.ts
|
|
1302
|
+
/**
|
|
1303
|
+
* Default duration a span may sit open with no new events before it's considered abandoned
|
|
1304
|
+
* (the runtime crashed, lost connectivity, or otherwise never wrote the closing event).
|
|
1305
|
+
*/
|
|
1306
|
+
var DEFAULT_SPAN_TIMEOUT_MS = 12e5;
|
|
1307
|
+
/**
|
|
1308
|
+
* Synthetic id assigned to the root span.
|
|
1309
|
+
* The root is a virtual container for top-level spans (those without a parent in the captured trace).
|
|
1310
|
+
*/
|
|
1311
|
+
var ROOT_SPAN_ID = "<main>";
|
|
1312
|
+
var BEGIN_EVENT_TYPES = /* @__PURE__ */ new Set([Trace.OperationStart.key, Trace.AgentRequestBegin.key]);
|
|
1313
|
+
var END_EVENT_TYPES = /* @__PURE__ */ new Set([Trace.OperationEnd.key, Trace.AgentRequestEnd.key]);
|
|
1314
|
+
var isSpanBeginEvent = (event) => BEGIN_EVENT_TYPES.has(event.type);
|
|
1315
|
+
var isSpanEndEvent = (event) => END_EVENT_TYPES.has(event.type);
|
|
1316
|
+
var computeMeta = (rawMeta) => ({
|
|
1317
|
+
pid: rawMeta.pid,
|
|
1318
|
+
parentPid: rawMeta.parentPid,
|
|
1319
|
+
processName: rawMeta.processName,
|
|
1320
|
+
conversationId: rawMeta.conversation ? EID.getEntityId(EID.parse(rawMeta.conversation.uri)) : void 0,
|
|
1321
|
+
triggerId: rawMeta.trigger ? EID.getEntityId(EID.parse(rawMeta.trigger.uri)) : void 0,
|
|
1322
|
+
toolCallId: rawMeta.toolCallId,
|
|
1323
|
+
runtimeName: rawMeta.runtimeName
|
|
1324
|
+
});
|
|
1325
|
+
var makeMutableSpan = (id, meta) => ({
|
|
1326
|
+
id,
|
|
1327
|
+
meta,
|
|
1328
|
+
events: [],
|
|
1329
|
+
children: [],
|
|
1330
|
+
firstTimestamp: Number.POSITIVE_INFINITY
|
|
1331
|
+
});
|
|
1332
|
+
var freezeSpan = (span) => ({
|
|
1333
|
+
id: span.id,
|
|
1334
|
+
meta: span.meta,
|
|
1335
|
+
events: span.events,
|
|
1336
|
+
children: span.children.map(freezeSpan)
|
|
1337
|
+
});
|
|
1338
|
+
/**
|
|
1339
|
+
* Builds a hierarchical span tree from a list of trace messages.
|
|
1340
|
+
*
|
|
1341
|
+
* Algorithm:
|
|
1342
|
+
* 1. Flatten messages into events and sort them chronologically.
|
|
1343
|
+
* 2. For each event:
|
|
1344
|
+
* - Begin events (`OperationStart`, `Trace.AgentRequestBegin`) open a new span whose parent
|
|
1345
|
+
* is the currently-open span of the event's `meta.parentPid`, or the root if there is
|
|
1346
|
+
* none. The begin event itself becomes the first event of the new span.
|
|
1347
|
+
* - End events (`OperationEnd`, `Trace.AgentRequestEnd`) close the currently-open span for
|
|
1348
|
+
* the event's `meta.pid` and become its last event.
|
|
1349
|
+
* - Other events attach to the currently-open span for their `meta.pid`, or to the
|
|
1350
|
+
* root span if no span is currently open for that pid (or the event has no pid).
|
|
1351
|
+
* 3. Sort each span's children by their earliest event timestamp.
|
|
1352
|
+
*/
|
|
1353
|
+
var buildSpanTree = (messages, options = {}) => {
|
|
1354
|
+
const allEvents = messages.flatMap((message) => Trace.flatten(message));
|
|
1355
|
+
allEvents.sort((a, b) => a.timestamp - b.timestamp);
|
|
1356
|
+
const events = applyEventLimit(allEvents, options.eventLimit);
|
|
1357
|
+
const root = makeMutableSpan(ROOT_SPAN_ID, {});
|
|
1358
|
+
const openSpans = /* @__PURE__ */ new Map();
|
|
1359
|
+
const lastSpanByPid = /* @__PURE__ */ new Map();
|
|
1360
|
+
const allSpans = [];
|
|
1361
|
+
let spanCounter = 0;
|
|
1362
|
+
const allocSpanId = (pid) => {
|
|
1363
|
+
const id = `${pid}#${spanCounter}`;
|
|
1364
|
+
spanCounter += 1;
|
|
1365
|
+
return id;
|
|
1366
|
+
};
|
|
1367
|
+
const noteTimestamp = (span, timestamp) => {
|
|
1368
|
+
if (timestamp < span.firstTimestamp) span.firstTimestamp = timestamp;
|
|
1369
|
+
};
|
|
1370
|
+
for (const event of events) {
|
|
1371
|
+
const pid = event.meta.pid;
|
|
1372
|
+
if (!pid) {
|
|
1373
|
+
root.events.push(event);
|
|
1374
|
+
noteTimestamp(root, event.timestamp);
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
if (BEGIN_EVENT_TYPES.has(event.type)) {
|
|
1378
|
+
const parentPid = event.meta.parentPid;
|
|
1379
|
+
const parent = parentPid && (openSpans.get(parentPid) ?? lastSpanByPid.get(parentPid)) || root;
|
|
1380
|
+
const span = makeMutableSpan(allocSpanId(pid), computeMeta(event.meta));
|
|
1381
|
+
span.events.push(event);
|
|
1382
|
+
noteTimestamp(span, event.timestamp);
|
|
1383
|
+
parent.children.push(span);
|
|
1384
|
+
allSpans.push(span);
|
|
1385
|
+
openSpans.set(pid, span);
|
|
1386
|
+
lastSpanByPid.set(pid, span);
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
if (END_EVENT_TYPES.has(event.type)) {
|
|
1390
|
+
const span = openSpans.get(pid);
|
|
1391
|
+
if (span) {
|
|
1392
|
+
span.events.push(event);
|
|
1393
|
+
noteTimestamp(span, event.timestamp);
|
|
1394
|
+
openSpans.delete(pid);
|
|
1395
|
+
} else {
|
|
1396
|
+
root.events.push(event);
|
|
1397
|
+
noteTimestamp(root, event.timestamp);
|
|
1398
|
+
}
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const open = openSpans.get(pid);
|
|
1402
|
+
if (open) {
|
|
1403
|
+
open.events.push(event);
|
|
1404
|
+
noteTimestamp(open, event.timestamp);
|
|
1405
|
+
} else {
|
|
1406
|
+
root.events.push(event);
|
|
1407
|
+
noteTimestamp(root, event.timestamp);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (options.now !== void 0) {
|
|
1411
|
+
const now = options.now;
|
|
1412
|
+
const spanTimeoutMs = options.spanTimeoutMs ?? 12e5;
|
|
1413
|
+
for (const span of allSpans) {
|
|
1414
|
+
const lastEvent = span.events[span.events.length - 1];
|
|
1415
|
+
if (isSpanEndEvent(lastEvent)) continue;
|
|
1416
|
+
const timeoutAt = lastEvent.timestamp + spanTimeoutMs;
|
|
1417
|
+
if (now < timeoutAt) continue;
|
|
1418
|
+
const endEvent = makeTimeoutEndEvent(span, timeoutAt);
|
|
1419
|
+
span.events.push(endEvent);
|
|
1420
|
+
noteTimestamp(span, endEvent.timestamp);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const sortChildren = (span) => {
|
|
1424
|
+
span.children.sort((a, b) => a.firstTimestamp - b.firstTimestamp);
|
|
1425
|
+
for (const child of span.children) sortChildren(child);
|
|
1426
|
+
};
|
|
1427
|
+
sortChildren(root);
|
|
1428
|
+
return freezeSpan(root);
|
|
1429
|
+
};
|
|
1430
|
+
/**
|
|
1431
|
+
* Message stamped on synthetic end events produced when a span times out.
|
|
1432
|
+
*/
|
|
1433
|
+
var SPAN_TIMEOUT_MESSAGE = "No end event was recorded for this span; it was closed after a timeout.";
|
|
1434
|
+
/**
|
|
1435
|
+
* Synthesizes the end event that closes an abandoned span, matching the event type its begin
|
|
1436
|
+
* event opened it with (`OperationStart` → `OperationEnd`, `Trace.AgentRequestBegin` → `Trace.AgentRequestEnd`).
|
|
1437
|
+
*/
|
|
1438
|
+
var makeTimeoutEndEvent = (span, timestamp) => {
|
|
1439
|
+
const beginEvent = span.events[0];
|
|
1440
|
+
if (beginEvent.type === Trace.AgentRequestBegin.key) return {
|
|
1441
|
+
type: Trace.AgentRequestEnd.key,
|
|
1442
|
+
timestamp,
|
|
1443
|
+
meta: beginEvent.meta,
|
|
1444
|
+
isEphemeral: false,
|
|
1445
|
+
data: {
|
|
1446
|
+
status: "interrupted",
|
|
1447
|
+
error: SPAN_TIMEOUT_MESSAGE
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
const fallback = { key: "unknown" };
|
|
1451
|
+
const beginData = Option.getOrElse(Schema.decodeUnknownOption(Trace.OperationStart.schema)(beginEvent.data), () => fallback);
|
|
1452
|
+
return {
|
|
1453
|
+
type: Trace.OperationEnd.key,
|
|
1454
|
+
timestamp,
|
|
1455
|
+
meta: beginEvent.meta,
|
|
1456
|
+
isEphemeral: false,
|
|
1457
|
+
data: {
|
|
1458
|
+
key: beginData.key,
|
|
1459
|
+
name: beginData.name,
|
|
1460
|
+
icon: beginData.icon,
|
|
1461
|
+
outcome: "failure",
|
|
1462
|
+
error: SPAN_TIMEOUT_MESSAGE
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
};
|
|
1466
|
+
/**
|
|
1467
|
+
* Caps the number of non-boundary events while retaining every span boundary event.
|
|
1468
|
+
*
|
|
1469
|
+
* Boundary events define the span tree's structure: dropping a begin event detaches every
|
|
1470
|
+
* descendant span (its `parentPid` lookup fails, so children fall back to the root), and
|
|
1471
|
+
* dropping an end event leaves a span open and corrupts the open-span map for sibling spans
|
|
1472
|
+
* that share a pid. Status/partial-block events are the noisy bulk and the only ones the
|
|
1473
|
+
* limit needs to control.
|
|
1474
|
+
*
|
|
1475
|
+
* The returned array stays in chronological order.
|
|
1476
|
+
*/
|
|
1477
|
+
var applyEventLimit = (events, eventLimit) => {
|
|
1478
|
+
if (eventLimit === void 0) return [...events];
|
|
1479
|
+
let nonBoundaryCount = 0;
|
|
1480
|
+
for (const event of events) if (!isBoundary(event)) nonBoundaryCount += 1;
|
|
1481
|
+
if (nonBoundaryCount <= eventLimit) return [...events];
|
|
1482
|
+
const dropCount = nonBoundaryCount - eventLimit;
|
|
1483
|
+
let dropped = 0;
|
|
1484
|
+
const result = [];
|
|
1485
|
+
for (const event of events) {
|
|
1486
|
+
if (!isBoundary(event) && dropped < dropCount) {
|
|
1487
|
+
dropped += 1;
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
result.push(event);
|
|
1491
|
+
}
|
|
1492
|
+
return result;
|
|
1493
|
+
};
|
|
1494
|
+
var isBoundary = (event) => BEGIN_EVENT_TYPES.has(event.type) || END_EVENT_TYPES.has(event.type);
|
|
1495
|
+
/**
|
|
1496
|
+
* Walks the span tree in depth-first pre-order, invoking `visit` for each span (root included).
|
|
1497
|
+
*/
|
|
1498
|
+
var walkSpanTree = (root, visit, depth = 0) => {
|
|
1499
|
+
visit(root, depth);
|
|
1500
|
+
for (const child of root.children) walkSpanTree(child, visit, depth + 1);
|
|
1501
|
+
};
|
|
1502
|
+
/**
|
|
1503
|
+
* Returns all spans in the tree in depth-first pre-order, root first.
|
|
1504
|
+
*/
|
|
1505
|
+
var flattenSpanTree = (root) => {
|
|
1506
|
+
const result = [];
|
|
1507
|
+
walkSpanTree(root, (span) => result.push(span));
|
|
1508
|
+
return result;
|
|
1509
|
+
};
|
|
1510
|
+
//#endregion
|
|
1511
|
+
//#region src/execution-graph/execution-graph.ts
|
|
1512
|
+
var __dxlog_file = "/Users/burdon/Code/dxos/dxos/.claude/worktrees/resume-project-tasks-6ed628/packages/ui/react-ui-trace/src/execution-graph/execution-graph.ts";
|
|
1513
|
+
/**
|
|
1514
|
+
* Branch name for top-level operation invocations.
|
|
1515
|
+
*/
|
|
1516
|
+
var MAIN_BRANCH = "main";
|
|
1517
|
+
/** Timeline tag that applies a shimmer message effect. */
|
|
1518
|
+
var SHIMMER_EFFECT_TAG = "effect:shimmer";
|
|
1519
|
+
var ICONS = {
|
|
1520
|
+
agentRequestBegin: {
|
|
1521
|
+
icon: "ph--atom--regular",
|
|
1522
|
+
level: LogLevel.VERBOSE
|
|
1523
|
+
},
|
|
1524
|
+
agentRequestEndSuccess: {
|
|
1525
|
+
icon: "ph--atom--regular",
|
|
1526
|
+
level: LogLevel.INFO
|
|
1527
|
+
},
|
|
1528
|
+
agentRequestEndError: {
|
|
1529
|
+
icon: "ph--atom--regular",
|
|
1530
|
+
level: LogLevel.ERROR
|
|
1531
|
+
},
|
|
1532
|
+
agentRequestEndInterrupted: {
|
|
1533
|
+
icon: "ph--atom--regular",
|
|
1534
|
+
level: LogLevel.WARN
|
|
1535
|
+
},
|
|
1536
|
+
userMessage: {
|
|
1537
|
+
icon: "ph--user--regular",
|
|
1538
|
+
level: LogLevel.VERBOSE
|
|
1539
|
+
},
|
|
1540
|
+
statusMessage: {
|
|
1541
|
+
icon: "ph--info--regular",
|
|
1542
|
+
level: LogLevel.VERBOSE
|
|
1543
|
+
},
|
|
1544
|
+
operationStart: {
|
|
1545
|
+
icon: "ph--function--regular",
|
|
1546
|
+
level: LogLevel.VERBOSE
|
|
1547
|
+
},
|
|
1548
|
+
operationEndSuccess: {
|
|
1549
|
+
icon: "ph--function--regular",
|
|
1550
|
+
level: LogLevel.INFO
|
|
1551
|
+
},
|
|
1552
|
+
operationEndError: {
|
|
1553
|
+
icon: "ph--function--regular",
|
|
1554
|
+
level: LogLevel.ERROR
|
|
1555
|
+
},
|
|
1556
|
+
operationEndIncomplete: {
|
|
1557
|
+
icon: "ph--arrows-clockwise--regular",
|
|
1558
|
+
level: LogLevel.WARN
|
|
1559
|
+
},
|
|
1560
|
+
toolCall: {
|
|
1561
|
+
icon: "ph--wrench--regular",
|
|
1562
|
+
level: LogLevel.VERBOSE
|
|
1563
|
+
},
|
|
1564
|
+
toolResultSuccess: {
|
|
1565
|
+
icon: "ph--wrench--regular",
|
|
1566
|
+
level: LogLevel.INFO
|
|
1567
|
+
},
|
|
1568
|
+
toolResultError: {
|
|
1569
|
+
icon: "ph--wrench--regular",
|
|
1570
|
+
level: LogLevel.ERROR
|
|
1571
|
+
},
|
|
1572
|
+
agentRequestRunning: {
|
|
1573
|
+
icon: "ph--spinner-gap--regular",
|
|
1574
|
+
level: LogLevel.VERBOSE
|
|
1575
|
+
},
|
|
1576
|
+
processRunning: {
|
|
1577
|
+
icon: "ph--spinner-gap--regular",
|
|
1578
|
+
level: LogLevel.VERBOSE
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
var buildExecutionGraph = ({ traceMessages, activeProcesses = [], collapseCompletedSpans = false, eventLimit = 500, spanTimeoutMs, now }) => {
|
|
1582
|
+
const spanTree = buildSpanTree(traceMessages, {
|
|
1583
|
+
eventLimit,
|
|
1584
|
+
spanTimeoutMs,
|
|
1585
|
+
now
|
|
1586
|
+
});
|
|
1587
|
+
const built = spanTreeToCommits(spanTree, activeProcesses, buildToolCallContext(traceMessages), collapseCompletedSpans);
|
|
1588
|
+
log("trace execution graph", {
|
|
1589
|
+
traceMessages: traceMessages.length,
|
|
1590
|
+
commits: built.commits.length,
|
|
1591
|
+
branches: built.branches.length,
|
|
1592
|
+
activeProcesses: activeProcesses.length
|
|
1593
|
+
}, {
|
|
1594
|
+
"~LogMeta": "~LogMeta",
|
|
1595
|
+
F: __dxlog_file,
|
|
1596
|
+
L: 144,
|
|
1597
|
+
S: void 0
|
|
1598
|
+
});
|
|
1599
|
+
return {
|
|
1600
|
+
...built,
|
|
1601
|
+
spanTree
|
|
1602
|
+
};
|
|
1603
|
+
};
|
|
1604
|
+
var buildToolCallContext = (messages) => {
|
|
1605
|
+
const callById = /* @__PURE__ */ new Map();
|
|
1606
|
+
const resultByCallId = /* @__PURE__ */ new Map();
|
|
1607
|
+
for (const message of messages) for (const event of message.events) {
|
|
1608
|
+
if (event.type !== Trace.CompleteBlock.key) continue;
|
|
1609
|
+
const block = event.data?.block;
|
|
1610
|
+
if (!block) continue;
|
|
1611
|
+
if (block._tag === "toolCall") callById.set(block.toolCallId, block);
|
|
1612
|
+
else if (block._tag === "toolResult") resultByCallId.set(block.toolCallId, block);
|
|
1613
|
+
}
|
|
1614
|
+
return {
|
|
1615
|
+
callById,
|
|
1616
|
+
resultByCallId
|
|
1617
|
+
};
|
|
1618
|
+
};
|
|
1619
|
+
/**
|
|
1620
|
+
* Parses `Trace.AgentRequestEnd` payload, accepting legacy traces that omitted `status`.
|
|
1621
|
+
*/
|
|
1622
|
+
var parseAgentRequestEnd = (data) => {
|
|
1623
|
+
if (Schema.is(Trace.AgentRequestEnd.schema)(data)) return data;
|
|
1624
|
+
if (data != null && typeof data === "object" && Object.keys(data).length === 0) return { status: "success" };
|
|
1625
|
+
log("invalid trace event", { type: Trace.AgentRequestEnd.key }, {
|
|
1626
|
+
"~LogMeta": "~LogMeta",
|
|
1627
|
+
F: __dxlog_file,
|
|
1628
|
+
L: 210,
|
|
1629
|
+
S: void 0
|
|
1630
|
+
});
|
|
1631
|
+
};
|
|
1632
|
+
var presentAgentRequestEnd = (data) => {
|
|
1633
|
+
switch (data.status) {
|
|
1634
|
+
case "error": return {
|
|
1635
|
+
icon: ICONS.agentRequestEndError.icon,
|
|
1636
|
+
level: ICONS.agentRequestEndError.level,
|
|
1637
|
+
message: data.error ? trimText(`Agent request failed: ${data.error}`) : "Agent request failed"
|
|
1638
|
+
};
|
|
1639
|
+
case "interrupted": return {
|
|
1640
|
+
icon: ICONS.agentRequestEndInterrupted.icon,
|
|
1641
|
+
level: ICONS.agentRequestEndInterrupted.level,
|
|
1642
|
+
message: "Agent request interrupted"
|
|
1643
|
+
};
|
|
1644
|
+
default: return {
|
|
1645
|
+
icon: ICONS.agentRequestEndSuccess.icon,
|
|
1646
|
+
level: ICONS.agentRequestEndSuccess.level,
|
|
1647
|
+
message: "Agent completed request"
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
var presentEvent = (event, toolCallContext) => {
|
|
1652
|
+
if (Trace.isOfType(Trace.AgentRequestBegin, event)) {
|
|
1653
|
+
if (!Schema.is(Trace.AgentRequestBegin.schema)(event.data)) {
|
|
1654
|
+
log("invalid trace event", { type: event.type }, {
|
|
1655
|
+
"~LogMeta": "~LogMeta",
|
|
1656
|
+
F: __dxlog_file,
|
|
1657
|
+
L: 240,
|
|
1658
|
+
S: void 0
|
|
1659
|
+
});
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
return {
|
|
1663
|
+
icon: ICONS.agentRequestBegin.icon,
|
|
1664
|
+
level: ICONS.agentRequestBegin.level,
|
|
1665
|
+
message: "Agent processing request..."
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
if (Trace.isOfType(Trace.AgentRequestEnd, event)) {
|
|
1669
|
+
const endData = parseAgentRequestEnd(event.data);
|
|
1670
|
+
if (!endData) return;
|
|
1671
|
+
return presentAgentRequestEnd(endData);
|
|
1672
|
+
}
|
|
1673
|
+
if (Trace.isOfType(Trace.CompleteBlock, event)) {
|
|
1674
|
+
if (!Schema.is(Trace.CompleteBlock.schema)(event.data)) {
|
|
1675
|
+
log("invalid trace event", { type: event.type }, {
|
|
1676
|
+
"~LogMeta": "~LogMeta",
|
|
1677
|
+
F: __dxlog_file,
|
|
1678
|
+
L: 258,
|
|
1679
|
+
S: void 0
|
|
1680
|
+
});
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
switch (event.data.block._tag) {
|
|
1684
|
+
case "text": return event.data.role === "user" ? {
|
|
1685
|
+
icon: ICONS.userMessage.icon,
|
|
1686
|
+
level: ICONS.userMessage.level,
|
|
1687
|
+
message: trimText(event.data.block.text)
|
|
1688
|
+
} : void 0;
|
|
1689
|
+
case "status": return {
|
|
1690
|
+
icon: ICONS.statusMessage.icon,
|
|
1691
|
+
level: ICONS.statusMessage.level,
|
|
1692
|
+
message: trimText(event.data.block.statusText)
|
|
1693
|
+
};
|
|
1694
|
+
case "toolCall":
|
|
1695
|
+
if (event.data.block.operationKey) return;
|
|
1696
|
+
if (toolCallContext.resultByCallId.has(event.data.block.toolCallId)) return;
|
|
1697
|
+
return {
|
|
1698
|
+
icon: ICONS.toolCall.icon,
|
|
1699
|
+
level: ICONS.toolCall.level,
|
|
1700
|
+
message: event.data.block.name,
|
|
1701
|
+
idSuffix: `toolCall:${event.data.block.toolCallId}`
|
|
1702
|
+
};
|
|
1703
|
+
case "toolResult": {
|
|
1704
|
+
const call = toolCallContext.callById.get(event.data.block.toolCallId);
|
|
1705
|
+
if (call?.operationKey) return;
|
|
1706
|
+
const success = event.data.block.error === void 0;
|
|
1707
|
+
const name = call?.name ?? event.data.block.name;
|
|
1708
|
+
return {
|
|
1709
|
+
icon: success ? ICONS.toolResultSuccess.icon : ICONS.toolResultError.icon,
|
|
1710
|
+
level: success ? ICONS.toolResultSuccess.level : ICONS.toolResultError.level,
|
|
1711
|
+
message: `${name}${!success ? " - Error" : ""}`,
|
|
1712
|
+
idSuffix: `toolResult:${event.data.block.toolCallId}`
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
default: return;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
if (Trace.isOfType(Trace.OperationStart, event)) {
|
|
1719
|
+
if (!Schema.is(Trace.OperationStart.schema)(event.data)) {
|
|
1720
|
+
log("invalid trace event", { type: event.type }, {
|
|
1721
|
+
"~LogMeta": "~LogMeta",
|
|
1722
|
+
F: __dxlog_file,
|
|
1723
|
+
L: 318,
|
|
1724
|
+
S: void 0
|
|
1725
|
+
});
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
return {
|
|
1729
|
+
icon: event.data.icon ?? ICONS.operationStart.icon,
|
|
1730
|
+
level: ICONS.operationStart.level,
|
|
1731
|
+
message: event.data.name ?? event.data.key,
|
|
1732
|
+
idSuffix: `${event.data.key}:start`
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
if (Trace.isOfType(Trace.OperationEnd, event)) {
|
|
1736
|
+
if (!Schema.is(Trace.OperationEnd.schema)(event.data)) {
|
|
1737
|
+
log("invalid trace event", { type: event.type }, {
|
|
1738
|
+
"~LogMeta": "~LogMeta",
|
|
1739
|
+
F: __dxlog_file,
|
|
1740
|
+
L: 330,
|
|
1741
|
+
S: void 0
|
|
1742
|
+
});
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
const incomplete = event.data.outcome === "failure" && (event.data.errorCode === RUN_AGAIN_ERROR_CODE || event.data.errorCode === void 0 && event.data.error === RUN_AGAIN_MESSAGE);
|
|
1746
|
+
const presentation = event.data.outcome === "success" ? {
|
|
1747
|
+
icon: ICONS.operationEndSuccess.icon,
|
|
1748
|
+
level: ICONS.operationEndSuccess.level,
|
|
1749
|
+
suffix: ""
|
|
1750
|
+
} : incomplete ? {
|
|
1751
|
+
icon: ICONS.operationEndIncomplete.icon,
|
|
1752
|
+
level: ICONS.operationEndIncomplete.level,
|
|
1753
|
+
suffix: " - Incomplete"
|
|
1754
|
+
} : {
|
|
1755
|
+
icon: ICONS.operationEndError.icon,
|
|
1756
|
+
level: ICONS.operationEndError.level,
|
|
1757
|
+
suffix: " - Error"
|
|
1758
|
+
};
|
|
1759
|
+
return {
|
|
1760
|
+
icon: event.data.icon ?? presentation.icon,
|
|
1761
|
+
level: presentation.level,
|
|
1762
|
+
message: `${event.data.name ?? event.data.key}${presentation.suffix}`,
|
|
1763
|
+
idSuffix: `${event.data.key}:end`
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
/**
|
|
1768
|
+
* Collects human-readable activity lines for a process subtree (root pid and descendants).
|
|
1769
|
+
* Used to surface live sub-agent status in the task list.
|
|
1770
|
+
*/
|
|
1771
|
+
var collectProcessActivityLines = (messages, rootPid, options = {}) => {
|
|
1772
|
+
const { conversationId, eventLimit = 50 } = options;
|
|
1773
|
+
const descendantPids = collectDescendantPids(messages, rootPid);
|
|
1774
|
+
const filtered = messages.filter((message) => {
|
|
1775
|
+
const pid = message.meta.pid;
|
|
1776
|
+
if (!pid || !descendantPids.has(pid)) return false;
|
|
1777
|
+
const metaConvId = message.meta.conversation ? EID.getEntityId(EID.parse(message.meta.conversation.uri)) : void 0;
|
|
1778
|
+
if (conversationId && metaConvId !== void 0 && metaConvId !== conversationId) return false;
|
|
1779
|
+
return true;
|
|
1780
|
+
});
|
|
1781
|
+
const events = filtered.flatMap((message) => Trace.flatten(message)).sort((left, right) => left.timestamp - right.timestamp).slice(-eventLimit);
|
|
1782
|
+
const toolCallContext = buildToolCallContext(filtered);
|
|
1783
|
+
const lines = [];
|
|
1784
|
+
for (const event of events) {
|
|
1785
|
+
const presentation = presentEvent(event, toolCallContext);
|
|
1786
|
+
if (presentation) lines.push(presentation.message);
|
|
1787
|
+
}
|
|
1788
|
+
return lines;
|
|
1789
|
+
};
|
|
1790
|
+
/**
|
|
1791
|
+
* Derives a single in-flight status line from durable trace when ephemeral subscription
|
|
1792
|
+
* is unavailable (e.g. between partial blocks or while a child operation is running).
|
|
1793
|
+
*/
|
|
1794
|
+
var deriveInFlightActivityLine = (messages, rootPid, options = {}) => {
|
|
1795
|
+
const { conversationId, eventLimit = 50 } = options;
|
|
1796
|
+
const descendantPids = collectDescendantPids(messages, rootPid);
|
|
1797
|
+
const events = messages.filter((message) => {
|
|
1798
|
+
const pid = message.meta.pid;
|
|
1799
|
+
if (!pid || !descendantPids.has(pid)) return false;
|
|
1800
|
+
const metaConvId = message.meta.conversation ? EID.getEntityId(EID.parse(message.meta.conversation.uri)) : void 0;
|
|
1801
|
+
if (conversationId && metaConvId !== void 0 && metaConvId !== conversationId) return false;
|
|
1802
|
+
return true;
|
|
1803
|
+
}).flatMap((message) => Trace.flatten(message)).sort((left, right) => left.timestamp - right.timestamp).slice(-eventLimit);
|
|
1804
|
+
const openOperations = /* @__PURE__ */ new Map();
|
|
1805
|
+
let agentRequestOpen = false;
|
|
1806
|
+
let lastLine;
|
|
1807
|
+
for (const event of events) {
|
|
1808
|
+
const pid = event.meta.pid ?? rootPid;
|
|
1809
|
+
if (Trace.isOfType(Trace.OperationStart, event)) {
|
|
1810
|
+
const key = `${pid}:${event.data.key}`;
|
|
1811
|
+
const name = event.data.name ?? event.data.key;
|
|
1812
|
+
openOperations.set(key, {
|
|
1813
|
+
name,
|
|
1814
|
+
timestamp: event.timestamp
|
|
1815
|
+
});
|
|
1816
|
+
lastLine = `${name}...`;
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
if (Trace.isOfType(Trace.OperationEnd, event)) {
|
|
1820
|
+
openOperations.delete(`${pid}:${event.data.key}`);
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
if (Trace.isOfType(Trace.AgentRequestBegin, event)) {
|
|
1824
|
+
agentRequestOpen = true;
|
|
1825
|
+
lastLine = "Generating...";
|
|
1826
|
+
continue;
|
|
1827
|
+
}
|
|
1828
|
+
if (Trace.isOfType(Trace.AgentRequestEnd, event)) agentRequestOpen = false;
|
|
1829
|
+
}
|
|
1830
|
+
if (agentRequestOpen) return "Generating...";
|
|
1831
|
+
const latestOperation = [...openOperations.values()].sort((left, right) => right.timestamp - left.timestamp)[0];
|
|
1832
|
+
if (latestOperation) return `${latestOperation.name}...`;
|
|
1833
|
+
return lastLine;
|
|
1834
|
+
};
|
|
1835
|
+
var collectDescendantPids = (messages, rootPid) => {
|
|
1836
|
+
const pids = /* @__PURE__ */ new Set([rootPid]);
|
|
1837
|
+
let expanded = true;
|
|
1838
|
+
while (expanded) {
|
|
1839
|
+
expanded = false;
|
|
1840
|
+
for (const message of messages) {
|
|
1841
|
+
const pid = message.meta.pid;
|
|
1842
|
+
const parentPid = message.meta.parentPid;
|
|
1843
|
+
if (pid && parentPid && pids.has(parentPid) && !pids.has(pid)) {
|
|
1844
|
+
pids.add(pid);
|
|
1845
|
+
expanded = true;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return pids;
|
|
1850
|
+
};
|
|
1851
|
+
var isPendingToolCallEvent = (event, toolCallContext) => {
|
|
1852
|
+
if (!Trace.isOfType(Trace.CompleteBlock, event)) return false;
|
|
1853
|
+
if (event.data.block._tag !== "toolCall") return false;
|
|
1854
|
+
return !toolCallContext.resultByCallId.has(event.data.block.toolCallId);
|
|
1855
|
+
};
|
|
1856
|
+
/**
|
|
1857
|
+
* A span is completed when its last recorded event is a matching end boundary.
|
|
1858
|
+
*/
|
|
1859
|
+
var isCompletedSpan = (span) => {
|
|
1860
|
+
if (span.id === "<main>") return false;
|
|
1861
|
+
const lastEvent = span.events[span.events.length - 1];
|
|
1862
|
+
return lastEvent !== void 0 && isSpanEndEvent(lastEvent);
|
|
1863
|
+
};
|
|
1864
|
+
/**
|
|
1865
|
+
* A span is "collapsible" when it would otherwise render as an empty fork-and-merge:
|
|
1866
|
+
* exactly two events that form a begin/end pair and no child sub-spans. In that case
|
|
1867
|
+
* we emit a single commit for the end event on the parent branch.
|
|
1868
|
+
*
|
|
1869
|
+
* When `collapseCompletedSpans` is enabled, any completed span collapses to that single
|
|
1870
|
+
* end commit — middle events, child sub-spans, and the begin fork are all omitted.
|
|
1871
|
+
*
|
|
1872
|
+
* A *pending* span (one whose end event has not yet been recorded) is never collapsed:
|
|
1873
|
+
* its trailing events should keep rendering as middle commits on the span's own branch
|
|
1874
|
+
* — never as a fake "end" commit on the parent branch.
|
|
1875
|
+
*
|
|
1876
|
+
* Both nested spans and top-level spans (whose parent is the synthetic root) are eligible
|
|
1877
|
+
* once they have completed: a span with no inner activity is collapsed regardless of where
|
|
1878
|
+
* it sits in the tree.
|
|
1879
|
+
*/
|
|
1880
|
+
var isCollapsibleSpan = (span, parent, collapseCompletedSpans) => {
|
|
1881
|
+
if (span.id === "<main>") return false;
|
|
1882
|
+
if (parent === null) return false;
|
|
1883
|
+
if (span.events.length === 2 && span.children.length === 0) {
|
|
1884
|
+
const firstEvent = span.events[0];
|
|
1885
|
+
const lastEvent = span.events[span.events.length - 1];
|
|
1886
|
+
if (isSpanBeginEvent(firstEvent) && isSpanEndEvent(lastEvent)) return true;
|
|
1887
|
+
}
|
|
1888
|
+
if (collapseCompletedSpans && isCompletedSpan(span)) return true;
|
|
1889
|
+
return false;
|
|
1890
|
+
};
|
|
1891
|
+
/**
|
|
1892
|
+
* Converts a span tree into a flat list of commits + branches suitable for `Timeline` rendering.
|
|
1893
|
+
*
|
|
1894
|
+
* For every span the algorithm:
|
|
1895
|
+
* - emits the **first** event as a "begin" commit on the parent span's branch (fork);
|
|
1896
|
+
* - emits **middle** events as commits on the span's own branch;
|
|
1897
|
+
* - emits the **last** event as an "end" commit on the parent span's branch with two parents
|
|
1898
|
+
* (the begin commit and the last commit on the span's own branch — a merge).
|
|
1899
|
+
*
|
|
1900
|
+
* Collapsible spans (`isCollapsibleSpan`) emit only the end commit on the parent branch with
|
|
1901
|
+
* a single parent — a tidier rendering for sub-operations that have no inner detail.
|
|
1902
|
+
*
|
|
1903
|
+
* Events are processed in categorical span-tree order: each span's begin events, its child
|
|
1904
|
+
* subtrees (depth-first), then its remaining events. Active-process running rows are inserted
|
|
1905
|
+
* at the end of each matching span block — not appended after the entire tree.
|
|
1906
|
+
*/
|
|
1907
|
+
var spanTreeToCommits = (root, activeProcesses, toolCallContext, collapseCompletedSpans) => {
|
|
1908
|
+
const builder = new GraphBuilder();
|
|
1909
|
+
const details = {};
|
|
1910
|
+
builder.addBranch(MAIN_BRANCH);
|
|
1911
|
+
const parentBySpan = /* @__PURE__ */ new Map();
|
|
1912
|
+
parentBySpan.set(root, null);
|
|
1913
|
+
const indexParents = (span) => {
|
|
1914
|
+
for (const child of span.children) {
|
|
1915
|
+
parentBySpan.set(child, span);
|
|
1916
|
+
indexParents(child);
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
indexParents(root);
|
|
1920
|
+
const findParentSpan = (span) => parentBySpan.get(span) ?? null;
|
|
1921
|
+
const collapsedSpanIds = /* @__PURE__ */ new Set();
|
|
1922
|
+
walkSpanTree(root, (span) => {
|
|
1923
|
+
if (isCollapsibleSpan(span, findParentSpan(span), collapseCompletedSpans)) collapsedSpanIds.add(span.id);
|
|
1924
|
+
});
|
|
1925
|
+
const isUnderCollapsedAncestor = (span) => {
|
|
1926
|
+
let ancestor = findParentSpan(span);
|
|
1927
|
+
while (ancestor) {
|
|
1928
|
+
if (collapsedSpanIds.has(ancestor.id)) return true;
|
|
1929
|
+
ancestor = findParentSpan(ancestor);
|
|
1930
|
+
}
|
|
1931
|
+
return false;
|
|
1932
|
+
};
|
|
1933
|
+
const branchOf = (span) => {
|
|
1934
|
+
if (!span || span.id === "<main>") return MAIN_BRANCH;
|
|
1935
|
+
return span.meta.pid ?? span.id;
|
|
1936
|
+
};
|
|
1937
|
+
const beginCommitIdBySpan = /* @__PURE__ */ new Map();
|
|
1938
|
+
const lastInSpanContext = (span) => {
|
|
1939
|
+
if (!span || span.id === "<main>") return CommitSelector.branch(MAIN_BRANCH).pipe(CommitSelector.compose(CommitSelector.last()));
|
|
1940
|
+
const ownBranch = branchOf(span);
|
|
1941
|
+
const beginId = beginCommitIdBySpan.get(span.id);
|
|
1942
|
+
return CommitSelector.firstOf(CommitSelector.branch(ownBranch).pipe(CommitSelector.compose(CommitSelector.last())), beginId ? CommitSelector.id(beginId) : CommitSelector.filter(() => false), lastInSpanContext(findParentSpan(span)));
|
|
1943
|
+
};
|
|
1944
|
+
const lastCommitIdBySpan = /* @__PURE__ */ new Map();
|
|
1945
|
+
let globalIndex = 0;
|
|
1946
|
+
const insertedRunningPids = /* @__PURE__ */ new Set();
|
|
1947
|
+
const activeProcessByPid = new Map(activeProcesses.map((process) => [process.pid, process]));
|
|
1948
|
+
const emitRunningIndicator = (pid) => {
|
|
1949
|
+
if (insertedRunningPids.has(pid)) return;
|
|
1950
|
+
const process = activeProcessByPid.get(pid);
|
|
1951
|
+
if (!process) return;
|
|
1952
|
+
if (Process.isHarnessHost(process) && Option.isSome(Annotation.getDictionary(process.params.annotations, Process.TargetAnnotation)) && (process.state === Process.State.RUNNING || process.state === Process.State.HYBERNATING)) {
|
|
1953
|
+
builder.addCommit({
|
|
1954
|
+
id: `running:${process.pid}`,
|
|
1955
|
+
branch: process.pid,
|
|
1956
|
+
parents: builder.computeParents(CommitSelector.firstOf(CommitSelector.branch(process.pid).pipe(CommitSelector.compose(CommitSelector.last())), CommitSelector.branch(MAIN_BRANCH).pipe(CommitSelector.compose(CommitSelector.last())))),
|
|
1957
|
+
icon: ICONS.agentRequestRunning.icon,
|
|
1958
|
+
level: ICONS.agentRequestRunning.level,
|
|
1959
|
+
message: "Generating...",
|
|
1960
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1961
|
+
tags: [SHIMMER_EFFECT_TAG]
|
|
1962
|
+
});
|
|
1963
|
+
insertedRunningPids.add(pid);
|
|
1964
|
+
} else if (process.state === Process.State.RUNNING && builder.hasBranch(process.pid)) {
|
|
1965
|
+
builder.addCommit({
|
|
1966
|
+
id: `running:${process.pid}`,
|
|
1967
|
+
branch: process.pid,
|
|
1968
|
+
parents: builder.computeParents(CommitSelector.branch(process.pid).pipe(CommitSelector.compose(CommitSelector.last()))),
|
|
1969
|
+
icon: ICONS.processRunning.icon,
|
|
1970
|
+
level: ICONS.processRunning.level,
|
|
1971
|
+
message: "Running...",
|
|
1972
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1973
|
+
tags: [SHIMMER_EFFECT_TAG]
|
|
1974
|
+
});
|
|
1975
|
+
insertedRunningPids.add(pid);
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
const emitEventCommit = (span, event, eventIndex) => {
|
|
1979
|
+
const presentation = presentEvent(event, toolCallContext);
|
|
1980
|
+
if (!presentation) return;
|
|
1981
|
+
const isBeginEvent = isSpanBeginEvent(event);
|
|
1982
|
+
const isEndEvent = isSpanEndEvent(event);
|
|
1983
|
+
const parentSpan = findParentSpan(span);
|
|
1984
|
+
const collapsible = isCollapsibleSpan(span, parentSpan, collapseCompletedSpans);
|
|
1985
|
+
if (isUnderCollapsedAncestor(span)) return;
|
|
1986
|
+
if (collapsible && !isEndEvent) return;
|
|
1987
|
+
const parentBranch = branchOf(parentSpan);
|
|
1988
|
+
const ownBranch = branchOf(span);
|
|
1989
|
+
const ownPid = span.meta.pid;
|
|
1990
|
+
const parentPid = parentSpan?.meta.pid;
|
|
1991
|
+
const spanHasMiddle = span.events.some((spanEvent) => !isSpanBeginEvent(spanEvent) && !isSpanEndEvent(spanEvent));
|
|
1992
|
+
const isProcessBoundary = ownPid != null && parentPid != null && ownPid !== parentPid && spanHasMiddle;
|
|
1993
|
+
let branch;
|
|
1994
|
+
let parents;
|
|
1995
|
+
if (span.id === "<main>") {
|
|
1996
|
+
branch = MAIN_BRANCH;
|
|
1997
|
+
parents = builder.computeParents(lastInSpanContext(null));
|
|
1998
|
+
} else if (collapsible) {
|
|
1999
|
+
branch = parentBranch;
|
|
2000
|
+
parents = builder.computeParents(lastInSpanContext(parentSpan));
|
|
2001
|
+
} else if (isBeginEvent) {
|
|
2002
|
+
branch = isProcessBoundary ? ownBranch : parentBranch;
|
|
2003
|
+
parents = builder.computeParents(lastInSpanContext(parentSpan));
|
|
2004
|
+
} else if (isEndEvent) {
|
|
2005
|
+
branch = isProcessBoundary ? MAIN_BRANCH : parentBranch;
|
|
2006
|
+
parents = builder.computeParents(CommitSelector.unionAll(CommitSelector.branch(branch).pipe(CommitSelector.compose(CommitSelector.last())), CommitSelector.branch(ownBranch).pipe(CommitSelector.compose(CommitSelector.last()))));
|
|
2007
|
+
} else {
|
|
2008
|
+
branch = ownBranch;
|
|
2009
|
+
parents = builder.computeParents(lastInSpanContext(span));
|
|
2010
|
+
}
|
|
2011
|
+
const commitId = formatCommitId(span, globalIndex++, presentation.idSuffix);
|
|
2012
|
+
const shimmer = isPendingToolCallEvent(event, toolCallContext);
|
|
2013
|
+
const commit = {
|
|
2014
|
+
id: commitId,
|
|
2015
|
+
branch,
|
|
2016
|
+
parents,
|
|
2017
|
+
timestamp: new Date(event.timestamp),
|
|
2018
|
+
icon: presentation.icon,
|
|
2019
|
+
level: presentation.level,
|
|
2020
|
+
message: presentation.message,
|
|
2021
|
+
...shimmer && { tags: [SHIMMER_EFFECT_TAG] }
|
|
2022
|
+
};
|
|
2023
|
+
builder.addCommit(commit);
|
|
2024
|
+
details[commitId] = event;
|
|
2025
|
+
lastCommitIdBySpan.set(span.id, commitId);
|
|
2026
|
+
if (isBeginEvent && !collapsible && span.id !== "<main>") beginCommitIdBySpan.set(span.id, commitId);
|
|
2027
|
+
};
|
|
2028
|
+
const finishSpan = (span) => {
|
|
2029
|
+
const pid = span.meta.pid;
|
|
2030
|
+
if (pid) emitRunningIndicator(Process.ID.make(pid));
|
|
2031
|
+
if (span.id === "<main>" || isCompletedSpan(span)) return;
|
|
2032
|
+
if (pid && insertedRunningPids.has(pid)) return;
|
|
2033
|
+
const lastCommitId = lastCommitIdBySpan.get(span.id);
|
|
2034
|
+
if (lastCommitId) builder.addTag(lastCommitId, SHIMMER_EFFECT_TAG);
|
|
2035
|
+
};
|
|
2036
|
+
const emitSpanCommits = (span) => {
|
|
2037
|
+
for (let eventIndex = 0; eventIndex < span.events.length; eventIndex += 1) {
|
|
2038
|
+
const event = span.events[eventIndex];
|
|
2039
|
+
if (isSpanBeginEvent(event)) emitEventCommit(span, event, eventIndex);
|
|
2040
|
+
}
|
|
2041
|
+
for (const child of span.children) emitSpanCommits(child);
|
|
2042
|
+
for (let eventIndex = 0; eventIndex < span.events.length; eventIndex += 1) {
|
|
2043
|
+
const event = span.events[eventIndex];
|
|
2044
|
+
if (!isSpanBeginEvent(event)) emitEventCommit(span, event, eventIndex);
|
|
2045
|
+
}
|
|
2046
|
+
finishSpan(span);
|
|
2047
|
+
};
|
|
2048
|
+
emitSpanCommits(root);
|
|
2049
|
+
for (const process of activeProcesses) emitRunningIndicator(process.pid);
|
|
2050
|
+
const { commits, branches } = builder.build();
|
|
2051
|
+
return {
|
|
2052
|
+
commits,
|
|
2053
|
+
branches,
|
|
2054
|
+
details
|
|
2055
|
+
};
|
|
2056
|
+
};
|
|
2057
|
+
var formatCommitId = (span, globalIndex, suffix) => {
|
|
2058
|
+
const base = `${span.id}:${globalIndex}`;
|
|
2059
|
+
return suffix ? `${base}:${suffix}` : base;
|
|
2060
|
+
};
|
|
2061
|
+
var trimText = (text) => text.slice(0, 100).trim().split("\n")[0];
|
|
2062
|
+
var CommitSelector = {
|
|
2063
|
+
make: (select) => ({
|
|
2064
|
+
select,
|
|
2065
|
+
pipe(...args) {
|
|
2066
|
+
return Pipeable.pipeArguments(this, args);
|
|
2067
|
+
}
|
|
2068
|
+
}),
|
|
2069
|
+
identity: () => CommitSelector.make((commits) => commits),
|
|
2070
|
+
/**
|
|
2071
|
+
* Selects commits that match the given predicate.
|
|
2072
|
+
*/
|
|
2073
|
+
filter: (predicate) => CommitSelector.make((commits) => commits.filter(predicate)),
|
|
2074
|
+
/**
|
|
2075
|
+
* Selects commits by id.
|
|
2076
|
+
*/
|
|
2077
|
+
id: (id) => CommitSelector.filter((commit) => commit.id === id),
|
|
2078
|
+
/**
|
|
2079
|
+
* Selects commits by tag.
|
|
2080
|
+
*/
|
|
2081
|
+
tag: (tag) => CommitSelector.filter((commit) => tag && commit.tags?.includes(tag)),
|
|
2082
|
+
/**
|
|
2083
|
+
* Selects commits by if any of the given tags are present.
|
|
2084
|
+
*/
|
|
2085
|
+
anyTags: (tags) => CommitSelector.filter((commit) => tags.some((tag) => tag && commit.tags?.includes(tag))),
|
|
2086
|
+
/**
|
|
2087
|
+
* Selects commits by branch.
|
|
2088
|
+
*/
|
|
2089
|
+
branch: (branch) => CommitSelector.filter((commit) => branch && commit.branch === branch),
|
|
2090
|
+
/**
|
|
2091
|
+
* Chains two selectors together, applying the second selector to the result of the first.
|
|
2092
|
+
*
|
|
2093
|
+
* Example:
|
|
2094
|
+
*
|
|
2095
|
+
* ```
|
|
2096
|
+
* CommitSelector.tag('tag').pipe(CommitSelector.compose(CommitSelector.branch('main')));
|
|
2097
|
+
* ```
|
|
2098
|
+
*/
|
|
2099
|
+
compose: (next) => (prev) => CommitSelector.make((commits) => next.select(prev.select(commits))),
|
|
2100
|
+
/**
|
|
2101
|
+
* If `prev` selector matches no commits, return `next` selector, otherwise return `prev` selector.
|
|
2102
|
+
* Same as `CommitSelector.firstOf(prev, next)`.
|
|
2103
|
+
*
|
|
2104
|
+
* Example:
|
|
2105
|
+
*
|
|
2106
|
+
* ```
|
|
2107
|
+
* CommitSelector.tag('tag').pipe(CommitSelector.orElse(CommitSelector.branch('main')));
|
|
2108
|
+
* ```
|
|
2109
|
+
*/
|
|
2110
|
+
orElse: (next) => (prev) => CommitSelector.make((commits) => {
|
|
2111
|
+
const selected = prev.select(commits);
|
|
2112
|
+
if (selected.length > 0) return selected;
|
|
2113
|
+
return next.select(commits);
|
|
2114
|
+
}),
|
|
2115
|
+
/**
|
|
2116
|
+
* Selects commits that match either of the given selectors.
|
|
2117
|
+
*/
|
|
2118
|
+
andAlso: (next) => (prev) => CommitSelector.unionAll(prev, next),
|
|
2119
|
+
/**
|
|
2120
|
+
* Selects the first n commits.
|
|
2121
|
+
*/
|
|
2122
|
+
first: (n = 1) => CommitSelector.make((commits) => commits.slice(0, n)),
|
|
2123
|
+
/**
|
|
2124
|
+
* Selects the last n commits (reverses the order of the commits).
|
|
2125
|
+
*/
|
|
2126
|
+
last: (n = 1) => CommitSelector.make((commits) => commits.toReversed().slice(0, n)),
|
|
2127
|
+
/**
|
|
2128
|
+
* Selects commits that do not match the given selector.
|
|
2129
|
+
*/
|
|
2130
|
+
not: (selector) => CommitSelector.make((commits) => {
|
|
2131
|
+
const selected = selector.select(commits);
|
|
2132
|
+
return commits.filter((commit) => !selected.includes(commit));
|
|
2133
|
+
}),
|
|
2134
|
+
/**
|
|
2135
|
+
* Selects commits that match any of the given selectors.
|
|
2136
|
+
*/
|
|
2137
|
+
unionAll: (...selectors) => CommitSelector.make((commits) => {
|
|
2138
|
+
return Array$1.dedupeWith(selectors.flatMap((selector) => selector.select(commits)), (a, b) => a.id === b.id);
|
|
2139
|
+
}),
|
|
2140
|
+
/**
|
|
2141
|
+
* Selects commits that match all of the given selectors.
|
|
2142
|
+
*/
|
|
2143
|
+
intersectAll: (...selectors) => CommitSelector.make((commits) => {
|
|
2144
|
+
const selected = selectors.map((selector) => selector.select(commits));
|
|
2145
|
+
if (selected.length === 0) return [];
|
|
2146
|
+
return selected[0].filter((commit) => selected.every((selected) => selected.some((c) => c.id === commit.id)));
|
|
2147
|
+
}),
|
|
2148
|
+
/**
|
|
2149
|
+
* Returns the result of the first selector that matches any commits.
|
|
2150
|
+
*/
|
|
2151
|
+
firstOf: (...selectors) => CommitSelector.make((commits) => {
|
|
2152
|
+
for (const selector of selectors) {
|
|
2153
|
+
const selected = selector.select(commits);
|
|
2154
|
+
if (selected.length > 0) return selected;
|
|
2155
|
+
}
|
|
2156
|
+
return [];
|
|
2157
|
+
})
|
|
2158
|
+
};
|
|
2159
|
+
var GraphBuilder = class {
|
|
2160
|
+
#commits = [];
|
|
2161
|
+
#branches = /* @__PURE__ */ new Set();
|
|
2162
|
+
findCommits(selector) {
|
|
2163
|
+
return selector.select(this.#commits);
|
|
2164
|
+
}
|
|
2165
|
+
hasBranch(branch) {
|
|
2166
|
+
return this.#branches.has(branch);
|
|
2167
|
+
}
|
|
2168
|
+
addCommit(commit) {
|
|
2169
|
+
this.addBranch(commit.branch);
|
|
2170
|
+
this.#commits.push(commit);
|
|
2171
|
+
}
|
|
2172
|
+
addTag(commitId, tag) {
|
|
2173
|
+
const index = this.#commits.findIndex((commit) => commit.id === commitId);
|
|
2174
|
+
if (index < 0) return;
|
|
2175
|
+
const commit = this.#commits[index];
|
|
2176
|
+
if (commit.tags?.includes(tag)) return;
|
|
2177
|
+
this.#commits[index] = {
|
|
2178
|
+
...commit,
|
|
2179
|
+
tags: [...commit.tags ?? [], tag]
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Computes parents — picks first matching commit's id.
|
|
2184
|
+
*/
|
|
2185
|
+
computeParents(selector) {
|
|
2186
|
+
return Array$1.dedupe(selector.select(this.#commits).map((commit) => commit.id));
|
|
2187
|
+
}
|
|
2188
|
+
addBranch(branch) {
|
|
2189
|
+
this.#branches.add(branch);
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Removes duplicate commits and dangling parents.
|
|
2193
|
+
*/
|
|
2194
|
+
doctor() {
|
|
2195
|
+
this.#commits = pipe(this.#commits, Array$1.dedupeWith((a, b) => a.id === b.id), Array$1.map(Struct.evolve({ parents: (parents) => parents ? Array$1.filter(parents, (id) => this.#commits.some((commit) => commit.id === id)) : void 0 })));
|
|
2196
|
+
}
|
|
2197
|
+
build() {
|
|
2198
|
+
this.doctor();
|
|
2199
|
+
return {
|
|
2200
|
+
commits: this.#commits,
|
|
2201
|
+
branches: [...this.#branches]
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
//#endregion
|
|
2206
|
+
//#region src/execution-graph/pending-block-status.ts
|
|
2207
|
+
/**
|
|
2208
|
+
* Approximate streamed token count from partial text (UTF-8 length / 4).
|
|
2209
|
+
*/
|
|
2210
|
+
var estimateTokenCount = (text) => Math.max(1, Math.ceil(new TextEncoder().encode(text).length / 4));
|
|
2211
|
+
var formatOperationInputStatus = (data) => {
|
|
2212
|
+
return `Calling ${data.name ?? data.key} (${new TextEncoder().encode(JSON.stringify(data.input)).length} bytes)...`;
|
|
2213
|
+
};
|
|
2214
|
+
/** Raw routine status updates (e.g. `Running 01KVB9WBRG…`) carry an opaque ULID, not a useful label. */
|
|
2215
|
+
var ROUTINE_ULID_STATUS = /^Running [0-9A-HJKMNP-TV-Z]{26}$/i;
|
|
2216
|
+
/**
|
|
2217
|
+
* Formats a pending assistant block for the delegated-task status line.
|
|
2218
|
+
*/
|
|
2219
|
+
var formatPendingBlockStatus = (block) => {
|
|
2220
|
+
if (!block.pending) return;
|
|
2221
|
+
if (block._tag === "text") return `Generating ${estimateTokenCount(block.text)} tokens....`;
|
|
2222
|
+
if (block._tag === "toolCall") {
|
|
2223
|
+
const bytes = new TextEncoder().encode(block.input).length;
|
|
2224
|
+
return `Calling ${block.operationName ?? block.name} (${bytes} bytes)...`;
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
/**
|
|
2228
|
+
* Reads the latest pending partial block from an ephemeral trace message.
|
|
2229
|
+
*/
|
|
2230
|
+
var pendingStatusFromEphemeralMessage = (message) => {
|
|
2231
|
+
for (const event of message.events) {
|
|
2232
|
+
if (Trace.isOfType(Trace.PartialBlock, event)) {
|
|
2233
|
+
const status = formatPendingBlockStatus(event.data.block);
|
|
2234
|
+
if (status) return status;
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
if (Trace.isOfType(Trace.OperationInput, event)) return formatOperationInputStatus(event.data);
|
|
2238
|
+
if (Trace.isOfType(Trace.StatusUpdate, event)) {
|
|
2239
|
+
const message = event.data.message?.trim();
|
|
2240
|
+
if (!message || ROUTINE_ULID_STATUS.test(message)) continue;
|
|
2241
|
+
return message;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
/**
|
|
2246
|
+
* Maps an ephemeral trace message to a task-status update.
|
|
2247
|
+
*
|
|
2248
|
+
* The status line is "sticky": a message without a pending line (e.g. a completed block during an
|
|
2249
|
+
* LLM-thinking gap) leaves the previous line in place rather than clearing it, so the row keeps
|
|
2250
|
+
* showing the latest meaningful activity until a new line arrives or the subscription ends.
|
|
2251
|
+
*/
|
|
2252
|
+
var resolveEphemeralStatusUpdate = (message) => {
|
|
2253
|
+
const line = pendingStatusFromEphemeralMessage(message);
|
|
2254
|
+
if (line) return { line };
|
|
2255
|
+
return "unchanged";
|
|
2256
|
+
};
|
|
2257
|
+
//#endregion
|
|
2258
|
+
//#region src/hooks/useTraceMessages.ts
|
|
2259
|
+
var atomEmpty$1 = Atom.make(() => []);
|
|
2260
|
+
/**
|
|
2261
|
+
* Atom of raw trace messages for a space — the input to `buildExecutionGraph`.
|
|
2262
|
+
*/
|
|
2263
|
+
var getTraceMessagesAtom = (space) => pipe(space.db.query(FeedTraceSink.query).atom, Atom.map((feeds) => space.db.query(feeds.length > 0 ? Query.type(Trace.Message).from([...feeds]) : Query.select(Filter.nothing())).atom), (atom) => Atom.make((get) => get(get(atom))));
|
|
2264
|
+
/**
|
|
2265
|
+
* Subscribes to the space invocation trace feed.
|
|
2266
|
+
*/
|
|
2267
|
+
var useTraceMessages = (space, { debounce } = {}) => {
|
|
2268
|
+
const atom = useMemo(() => {
|
|
2269
|
+
if (!space) return atomEmpty$1;
|
|
2270
|
+
const traces = getTraceMessagesAtom(space);
|
|
2271
|
+
return debounce ? traces.pipe(Atom.debounce(debounce)) : traces;
|
|
2272
|
+
}, [space, debounce]);
|
|
2273
|
+
return useAtomValue(atom);
|
|
2274
|
+
};
|
|
2275
|
+
//#endregion
|
|
2276
|
+
//#region src/hooks/useExecutionGraph.ts
|
|
2277
|
+
var atomEmpty = Atom.make(() => []);
|
|
2278
|
+
var NO_PIDS = [];
|
|
2279
|
+
var SPAN_TIMEOUT_CHECK_INTERVAL_MS = 6e4;
|
|
2280
|
+
/**
|
|
2281
|
+
* The commit graph of a space's trace, rebuilt as messages land and as the process tree changes.
|
|
2282
|
+
* `processesAtom` is the host's live process tree (a running agent draws a spinner commit); without
|
|
2283
|
+
* it the graph is the durable trace alone.
|
|
2284
|
+
*/
|
|
2285
|
+
var useExecutionGraph = (space, processesAtom, { collapseCompletedSpans, eventLimit, selectedPids = NO_PIDS } = {}) => {
|
|
2286
|
+
const [now, setNow] = useState(() => Date.now());
|
|
2287
|
+
useEffect(() => {
|
|
2288
|
+
const id = setInterval(() => setNow(Date.now()), SPAN_TIMEOUT_CHECK_INTERVAL_MS);
|
|
2289
|
+
return () => clearInterval(id);
|
|
2290
|
+
}, []);
|
|
2291
|
+
const atom = useMemo(() => getExecutionGraph(space, processesAtom ?? atomEmpty, {
|
|
2292
|
+
collapseCompletedSpans,
|
|
2293
|
+
eventLimit,
|
|
2294
|
+
selectedPids,
|
|
2295
|
+
now
|
|
2296
|
+
}), [
|
|
2297
|
+
space,
|
|
2298
|
+
processesAtom,
|
|
2299
|
+
collapseCompletedSpans,
|
|
2300
|
+
eventLimit,
|
|
2301
|
+
selectedPids,
|
|
2302
|
+
now
|
|
2303
|
+
]);
|
|
2304
|
+
return useAtomValue(atom);
|
|
2305
|
+
};
|
|
2306
|
+
/** Identity for the graph: only a process appearing, disappearing or changing state redraws it. */
|
|
2307
|
+
var sameProcesses = (left, right) => left.length === right.length && left.every((process, index) => process.pid === right[index].pid && process.state === right[index].state);
|
|
2308
|
+
var getExecutionGraph = (space, processesAtom, { collapseCompletedSpans = true, eventLimit = 100, selectedPids = NO_PIDS, now }) => {
|
|
2309
|
+
const traceMessages = getTraceMessagesAtom(space).pipe(Atom.map((messages) => filterTraceMessages(messages, selectedPids)));
|
|
2310
|
+
const activeProcesses = pipe(processesAtom, Atom.debounce(Duration.millis(500)), Atom.map((processes) => processes.filter((process) => (process.state === Process.State.RUNNING || process.state === Process.State.HYBERNATING) && (selectedPids.length === 0 || selectedPids.includes(process.pid)))), Atom.withEquality(sameProcesses));
|
|
2311
|
+
return Atom.make((get) => buildExecutionGraph({
|
|
2312
|
+
traceMessages: get(traceMessages),
|
|
2313
|
+
activeProcesses: get(activeProcesses),
|
|
2314
|
+
collapseCompletedSpans,
|
|
2315
|
+
eventLimit,
|
|
2316
|
+
now
|
|
2317
|
+
}));
|
|
2318
|
+
};
|
|
2319
|
+
//#endregion
|
|
2320
|
+
//#region src/session-timeline/session-timeline.ts
|
|
2321
|
+
var ACTIVE_STATES = /* @__PURE__ */ new Set([Process.State.RUNNING, Process.State.HYBERNATING]);
|
|
2322
|
+
/** How long an open lane may be silent before the axis stops following `now`. */
|
|
2323
|
+
var OPEN_LANE_STALE_MS = 6e5;
|
|
2324
|
+
var TASK_STATUS = {
|
|
2325
|
+
todo: "pending",
|
|
2326
|
+
backlog: "pending",
|
|
2327
|
+
blocked: "blocked",
|
|
2328
|
+
started: "running",
|
|
2329
|
+
review: "review",
|
|
2330
|
+
done: "done",
|
|
2331
|
+
failed: "failed"
|
|
2332
|
+
};
|
|
2333
|
+
/** Entity id of an ECHO URI, the join key between a process target, a trace meta and a chat. */
|
|
2334
|
+
var entityKey = (uri) => {
|
|
2335
|
+
const eid = EID.tryParse(uri);
|
|
2336
|
+
return (eid && EID.getEntityId(eid)) ?? uri;
|
|
2337
|
+
};
|
|
2338
|
+
var eventFeedKey = (event) => event.meta.conversation ? entityKey(event.meta.conversation.uri) : void 0;
|
|
2339
|
+
var decode = (schema, data) => Option.getOrUndefined(Schema.decodeUnknownOption(schema)(data));
|
|
2340
|
+
var sessionLaneId = (key) => `session:${key}`;
|
|
2341
|
+
var taskLaneId = (taskId) => `task:${taskId}`;
|
|
2342
|
+
/**
|
|
2343
|
+
* Cuts a session's events into one segment per task worked, from the status events its task tools
|
|
2344
|
+
* write. Only tasks in `taskIds` — the session's own checklist — take part: an agent is free to move
|
|
2345
|
+
* a task belonging to nothing on this chart, and such an event must not close the segment that is
|
|
2346
|
+
* open or move the boundary.
|
|
2347
|
+
*
|
|
2348
|
+
* A `started` event opens a segment and closes the one still open — the agent keeps exactly one task
|
|
2349
|
+
* in progress. Any other status ends the agent's work on the task: `todo` puts it down as surely as
|
|
2350
|
+
* `done`, and whatever the agent does next is no longer that task's. A task can also finish without
|
|
2351
|
+
* a start: delegation marks every task it hands over
|
|
2352
|
+
* `started` before the agent's first turn, so the run's only event for that task is the one closing
|
|
2353
|
+
* it. Such a task gets the stretch since the last boundary, which is where its work happened — but
|
|
2354
|
+
* only on the transition out of `started` and only while no other task holds the stretch: a task
|
|
2355
|
+
* merely dismissed (`todo` → `blocked`) claims nothing, a second close (`review` → `done`) mints
|
|
2356
|
+
* nothing, and a close arriving after the next task has started leaves that task's segment alone
|
|
2357
|
+
* rather than overlapping it.
|
|
2358
|
+
*/
|
|
2359
|
+
var buildTaskSegments = (events, sessionStart, taskIds) => {
|
|
2360
|
+
const segments = [];
|
|
2361
|
+
let open;
|
|
2362
|
+
let boundary = sessionStart;
|
|
2363
|
+
const close = (segment, timestamp) => {
|
|
2364
|
+
segment.end = timestamp;
|
|
2365
|
+
boundary = timestamp;
|
|
2366
|
+
if (open === segment) open = void 0;
|
|
2367
|
+
};
|
|
2368
|
+
for (const event of events) {
|
|
2369
|
+
if (event.type !== Trace.TaskStatusChanged.key) continue;
|
|
2370
|
+
const data = decode(Trace.TaskStatusChanged.schema, event.data);
|
|
2371
|
+
if (!data || !taskIds.has(data.taskId)) continue;
|
|
2372
|
+
if (data.status === "started") {
|
|
2373
|
+
if (open && open.taskId !== data.taskId) close(open, event.timestamp);
|
|
2374
|
+
if (!open) {
|
|
2375
|
+
open = {
|
|
2376
|
+
taskId: data.taskId,
|
|
2377
|
+
laneId: taskLaneId(data.taskId),
|
|
2378
|
+
start: event.timestamp
|
|
2379
|
+
};
|
|
2380
|
+
segments.push(open);
|
|
2381
|
+
boundary = event.timestamp;
|
|
2382
|
+
}
|
|
2383
|
+
} else {
|
|
2384
|
+
const started = segments.findLast((candidate) => candidate.taskId === data.taskId && candidate.end === void 0);
|
|
2385
|
+
if (started) close(started, event.timestamp);
|
|
2386
|
+
else if (data.previousStatus === "started" && open === void 0) {
|
|
2387
|
+
const segment = {
|
|
2388
|
+
taskId: data.taskId,
|
|
2389
|
+
laneId: taskLaneId(data.taskId),
|
|
2390
|
+
start: Math.min(boundary ?? event.timestamp, event.timestamp)
|
|
2391
|
+
};
|
|
2392
|
+
segments.push(segment);
|
|
2393
|
+
close(segment, event.timestamp);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
return segments;
|
|
2398
|
+
};
|
|
2399
|
+
/**
|
|
2400
|
+
* The segment an event belongs to. Segments meet where one task starts over another, so a
|
|
2401
|
+
* status event is matched to its own task's segment, and any other event at a shared boundary
|
|
2402
|
+
* goes to the later segment — the newcomer's start is the first thing the agent did on it.
|
|
2403
|
+
*/
|
|
2404
|
+
var segmentFor = (segments, event) => {
|
|
2405
|
+
if (!segments) return;
|
|
2406
|
+
const contains = (segment) => event.timestamp >= segment.start && event.timestamp <= (segment.end ?? Number.MAX_SAFE_INTEGER);
|
|
2407
|
+
if (event.type === Trace.TaskStatusChanged.key) {
|
|
2408
|
+
const data = decode(Trace.TaskStatusChanged.schema, event.data);
|
|
2409
|
+
return data && segments.findLast((segment) => segment.taskId === data.taskId && contains(segment));
|
|
2410
|
+
}
|
|
2411
|
+
return segments.findLast(contains);
|
|
2412
|
+
};
|
|
2413
|
+
/**
|
|
2414
|
+
* Builds the gantt-shaped view of one or more assistant sessions from their trace: session lanes
|
|
2415
|
+
* (supervisors and the sub-agents they delegate to), task lanes, and the markers on each.
|
|
2416
|
+
*/
|
|
2417
|
+
var buildSessionTimeline = ({ traceMessages, processes = [], sessions, tasks = [], now }) => {
|
|
2418
|
+
const spans = flattenSpanTree(buildSpanTree(traceMessages));
|
|
2419
|
+
const events = spans.flatMap((span) => span.events).sort((a, b) => a.timestamp - b.timestamp);
|
|
2420
|
+
const requestBegins = events.filter((event) => event.type === Trace.AgentRequestBegin.key && event.meta.pid !== void 0);
|
|
2421
|
+
const agentPidsByChat = /* @__PURE__ */ new Map();
|
|
2422
|
+
for (const process of processes) {
|
|
2423
|
+
if (!Process.isHarnessHost(process)) continue;
|
|
2424
|
+
const target = Option.getOrUndefined(Annotation.getDictionary(process.params.annotations, Process.TargetAnnotation));
|
|
2425
|
+
if (target !== void 0) {
|
|
2426
|
+
const key = entityKey(target.toString());
|
|
2427
|
+
agentPidsByChat.set(key, [...agentPidsByChat.get(key) ?? [], process.pid]);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const sources = [];
|
|
2431
|
+
if (sessions) for (const session of sessions) {
|
|
2432
|
+
if (session.taskIds.length === 0) continue;
|
|
2433
|
+
const feed = session.feedId;
|
|
2434
|
+
const pids = new Set(agentPidsByChat.get(entityKey(session.uri)) ?? []);
|
|
2435
|
+
for (const event of requestBegins) if (feed !== void 0 && eventFeedKey(event) === feed) pids.add(event.meta.pid);
|
|
2436
|
+
sources.push({
|
|
2437
|
+
key: session.id,
|
|
2438
|
+
label: session.label?.trim() || "Session",
|
|
2439
|
+
session,
|
|
2440
|
+
pids
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
else {
|
|
2444
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2445
|
+
for (const event of requestBegins) {
|
|
2446
|
+
const pid = event.meta.pid;
|
|
2447
|
+
const key = eventFeedKey(event) ?? pid;
|
|
2448
|
+
const source = byKey.get(key) ?? {
|
|
2449
|
+
key,
|
|
2450
|
+
label: eventFeedKey(event) ?? event.meta.processName ?? pid,
|
|
2451
|
+
pids: /* @__PURE__ */ new Set()
|
|
2452
|
+
};
|
|
2453
|
+
source.pids.add(pid);
|
|
2454
|
+
byKey.set(key, source);
|
|
2455
|
+
}
|
|
2456
|
+
sources.push(...byKey.values());
|
|
2457
|
+
}
|
|
2458
|
+
const processByPid = new Map(processes.map((process) => [process.pid, process]));
|
|
2459
|
+
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
2460
|
+
const laneByPid = /* @__PURE__ */ new Map();
|
|
2461
|
+
const lanes = [];
|
|
2462
|
+
const markers = [];
|
|
2463
|
+
const childSessions = [];
|
|
2464
|
+
const replacedTaskLanes = /* @__PURE__ */ new Map();
|
|
2465
|
+
/** Per session lane: the task segments its markers are attributed to. */
|
|
2466
|
+
const segmentsBySession = /* @__PURE__ */ new Map();
|
|
2467
|
+
for (const source of sources) {
|
|
2468
|
+
const laneId = sessionLaneId(source.key);
|
|
2469
|
+
const sessionEvents = events.filter((event) => event.meta.pid && source.pids.has(event.meta.pid));
|
|
2470
|
+
const begins = sessionEvents.filter((event) => event.type === Trace.AgentRequestBegin.key);
|
|
2471
|
+
const ends = sessionEvents.filter((event) => event.type === Trace.AgentRequestEnd.key);
|
|
2472
|
+
const requestOpen = begins.length > ends.length;
|
|
2473
|
+
const processActive = [...source.pids].some((pid) => {
|
|
2474
|
+
const process = processByPid.get(pid);
|
|
2475
|
+
return process !== void 0 && ACTIVE_STATES.has(process.state);
|
|
2476
|
+
});
|
|
2477
|
+
const lastEnd = ends.at(-1);
|
|
2478
|
+
const lastEndStatus = lastEnd ? decode(Trace.AgentRequestEnd.schema, lastEnd.data)?.status : void 0;
|
|
2479
|
+
const processKnown = [...source.pids].some((pid) => processByPid.has(pid));
|
|
2480
|
+
const died = requestOpen && processKnown && !processActive;
|
|
2481
|
+
const open = requestOpen && !died;
|
|
2482
|
+
const status = processActive || open ? "running" : died || lastEndStatus === "error" ? "failed" : "done";
|
|
2483
|
+
lanes.push({
|
|
2484
|
+
id: laneId,
|
|
2485
|
+
kind: "session",
|
|
2486
|
+
label: source.label,
|
|
2487
|
+
status,
|
|
2488
|
+
start: begins[0]?.timestamp,
|
|
2489
|
+
end: open ? void 0 : died ? sessionEvents.at(-1)?.timestamp : lastEnd?.timestamp,
|
|
2490
|
+
sessionId: source.session?.id,
|
|
2491
|
+
pid: [...source.pids].at(-1)
|
|
2492
|
+
});
|
|
2493
|
+
for (const pid of source.pids) laneByPid.set(pid, laneId);
|
|
2494
|
+
const taskByPid = /* @__PURE__ */ new Map();
|
|
2495
|
+
for (const event of sessionEvents) if (event.type === Trace.DelegationSpawned.key) {
|
|
2496
|
+
const data = decode(Trace.DelegationSpawned.schema, event.data);
|
|
2497
|
+
if (data) taskByPid.set(data.pid, data.taskId);
|
|
2498
|
+
}
|
|
2499
|
+
const subAgentSpans = spans.flatMap((span) => {
|
|
2500
|
+
const startEvent = span.events[0];
|
|
2501
|
+
const pid = span.meta.pid;
|
|
2502
|
+
if (pid === void 0 || span.meta.parentPid === void 0 || !source.pids.has(span.meta.parentPid) || startEvent?.type !== Trace.OperationStart.key) return [];
|
|
2503
|
+
return taskByPid.has(pid) || span.events.some((event) => event.type === Trace.CompleteBlock.key) ? [{
|
|
2504
|
+
span,
|
|
2505
|
+
pid,
|
|
2506
|
+
startEvent
|
|
2507
|
+
}] : [];
|
|
2508
|
+
});
|
|
2509
|
+
const chatTasks = (source.session?.taskIds ?? []).map((id) => taskById.get(id)).filter((task) => task !== void 0);
|
|
2510
|
+
const chatTaskIds = new Set(chatTasks.map((task) => task.id));
|
|
2511
|
+
const taskLanes = /* @__PURE__ */ new Map();
|
|
2512
|
+
for (const task of chatTasks) {
|
|
2513
|
+
const taskLane = {
|
|
2514
|
+
id: taskLaneId(task.id),
|
|
2515
|
+
kind: "task",
|
|
2516
|
+
label: task.title,
|
|
2517
|
+
status: taskLaneStatus(task, tasks),
|
|
2518
|
+
parentId: laneId,
|
|
2519
|
+
sessionId: source.session?.id,
|
|
2520
|
+
taskId: task.id
|
|
2521
|
+
};
|
|
2522
|
+
const blockedOn = (task.dependsOn ?? []).map((ref) => Task.refEntityId(ref)).filter((id) => id !== void 0 && chatTaskIds.has(id)).map(taskLaneId);
|
|
2523
|
+
if (blockedOn.length > 0) taskLane.blockedOn = blockedOn;
|
|
2524
|
+
taskLanes.set(task.id, taskLane);
|
|
2525
|
+
lanes.push(taskLane);
|
|
2526
|
+
}
|
|
2527
|
+
const subAgentPids = /* @__PURE__ */ new Set([...subAgentSpans.map(({ pid }) => pid), ...taskByPid.keys()]);
|
|
2528
|
+
const segments = buildTaskSegments(events.filter((event) => event.meta.pid !== void 0 && source.pids.has(event.meta.pid) || event.meta.parentPid !== void 0 && source.pids.has(event.meta.parentPid) && !(event.meta.pid !== void 0 && subAgentPids.has(event.meta.pid))), begins[0]?.timestamp, chatTaskIds);
|
|
2529
|
+
segmentsBySession.set(laneId, segments);
|
|
2530
|
+
for (const segment of segments) {
|
|
2531
|
+
const taskLane = taskLanes.get(segment.taskId);
|
|
2532
|
+
if (!taskLane) continue;
|
|
2533
|
+
taskLane.start = Math.min(taskLane.start ?? segment.start, segment.start);
|
|
2534
|
+
taskLane.end = segment.end === void 0 ? void 0 : Math.max(taskLane.end ?? segment.end, segment.end);
|
|
2535
|
+
}
|
|
2536
|
+
for (const subPid of subAgentPids) {
|
|
2537
|
+
const match = subAgentSpans.find((candidate) => candidate.pid === subPid);
|
|
2538
|
+
const process = processByPid.get(subPid);
|
|
2539
|
+
if (!match && !process) continue;
|
|
2540
|
+
const taskId = taskByPid.get(subPid);
|
|
2541
|
+
const task = taskId === void 0 ? void 0 : taskById.get(taskId);
|
|
2542
|
+
const startName = match ? decode(Trace.OperationStart.schema, match.startEvent.data)?.name : void 0;
|
|
2543
|
+
const endEvent = match?.span.events.find((event) => event.type === Trace.OperationEnd.key);
|
|
2544
|
+
const start = match?.startEvent.timestamp ?? process?.startedAt;
|
|
2545
|
+
const end = endEvent?.timestamp ?? (process ? Option.getOrUndefined(process.completedAt) : void 0);
|
|
2546
|
+
const subLane = {
|
|
2547
|
+
id: sessionLaneId(subPid),
|
|
2548
|
+
kind: "session",
|
|
2549
|
+
label: task?.title ?? startName ?? process?.params.name ?? subPid,
|
|
2550
|
+
status: subSessionStatus(endEvent, process),
|
|
2551
|
+
start,
|
|
2552
|
+
end,
|
|
2553
|
+
parentId: laneId,
|
|
2554
|
+
sessionId: source.session?.id,
|
|
2555
|
+
taskId,
|
|
2556
|
+
pid: subPid
|
|
2557
|
+
};
|
|
2558
|
+
laneByPid.set(subPid, subLane.id);
|
|
2559
|
+
childSessions.push({
|
|
2560
|
+
lane: subLane,
|
|
2561
|
+
sessionLaneId: laneId
|
|
2562
|
+
});
|
|
2563
|
+
const taskLane = taskId === void 0 ? void 0 : taskLanes.get(taskId);
|
|
2564
|
+
if (taskLane) {
|
|
2565
|
+
subLane.blockedOn = taskLane.blockedOn;
|
|
2566
|
+
lanes.splice(lanes.indexOf(taskLane), 1, subLane);
|
|
2567
|
+
replacedTaskLanes.set(taskLane.id, subLane.id);
|
|
2568
|
+
} else lanes.push(subLane);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
for (const [sessionId, segments] of segmentsBySession) segmentsBySession.set(sessionId, segments.filter((segment) => !replacedTaskLanes.has(segment.laneId)));
|
|
2572
|
+
for (const lane of lanes) if (lane.blockedOn) lane.blockedOn = lane.blockedOn.map((id) => replacedTaskLanes.get(id) ?? id);
|
|
2573
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
2574
|
+
const spawnMarkerByPid = /* @__PURE__ */ new Map();
|
|
2575
|
+
for (const event of events) {
|
|
2576
|
+
const laneId = (event.meta.pid && laneByPid.get(event.meta.pid)) ?? (event.meta.parentPid && laneByPid.get(event.meta.parentPid)) ?? void 0;
|
|
2577
|
+
if (laneId === void 0) continue;
|
|
2578
|
+
const markerLaneId = event.type === Trace.AgentRequestBegin.key || event.type === Trace.AgentRequestEnd.key ? laneId : segmentFor(segmentsBySession.get(laneId), event)?.laneId ?? laneId;
|
|
2579
|
+
const marker = toMarker(event, `${markerLaneId}:${markers.length}`, markerLaneId);
|
|
2580
|
+
if (marker) {
|
|
2581
|
+
markers.push(marker);
|
|
2582
|
+
if (marker.kind === "delegation") {
|
|
2583
|
+
const data = decode(Trace.DelegationSpawned.schema, event.data);
|
|
2584
|
+
if (data) spawnMarkerByPid.set(data.pid, marker.id);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
if (event.type === Trace.CompleteBlock.key) {
|
|
2588
|
+
const data = decode(Trace.CompleteBlock.schema, event.data);
|
|
2589
|
+
if (data?.block._tag === "stats") {
|
|
2590
|
+
const entry = tokens.get(laneId) ?? {
|
|
2591
|
+
usage: {
|
|
2592
|
+
input: 0,
|
|
2593
|
+
output: 0,
|
|
2594
|
+
total: 0
|
|
2595
|
+
},
|
|
2596
|
+
toolCalls: 0
|
|
2597
|
+
};
|
|
2598
|
+
const input = data.block.usage?.inputTokens ?? 0;
|
|
2599
|
+
const output = data.block.usage?.outputTokens ?? 0;
|
|
2600
|
+
const total = data.block.usage?.totalTokens ?? input + output;
|
|
2601
|
+
entry.usage = {
|
|
2602
|
+
input: entry.usage.input + input,
|
|
2603
|
+
output: entry.usage.output + output,
|
|
2604
|
+
total: entry.usage.total + total
|
|
2605
|
+
};
|
|
2606
|
+
entry.toolCalls += data.block.toolCalls ?? 0;
|
|
2607
|
+
tokens.set(laneId, entry);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
for (const lane of lanes) {
|
|
2612
|
+
const entry = tokens.get(lane.id);
|
|
2613
|
+
if (entry) {
|
|
2614
|
+
lane.tokens = entry.usage;
|
|
2615
|
+
lane.toolCalls = entry.toolCalls;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
for (const { lane, sessionLaneId } of childSessions) {
|
|
2619
|
+
const markerId = (lane.pid && spawnMarkerByPid.get(lane.pid)) ?? markers.filter((marker) => marker.laneId === sessionLaneId && marker.timestamp <= (lane.start ?? 0)).at(-1)?.id;
|
|
2620
|
+
if (markerId !== void 0) lane.delegatedFrom = {
|
|
2621
|
+
laneId: sessionLaneId,
|
|
2622
|
+
markerId
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
for (const session of lanes.filter((lane) => lane.kind === "session" && lane.parentId === void 0)) {
|
|
2626
|
+
const children = lanes.filter((lane) => lane.parentId === session.id);
|
|
2627
|
+
const [task] = children;
|
|
2628
|
+
if (children.length !== 1 || task.kind !== "task" || lanes.some((lane) => lane.blockedOn?.includes(task.id))) continue;
|
|
2629
|
+
lanes.splice(lanes.indexOf(task), 1);
|
|
2630
|
+
session.taskId = task.taskId;
|
|
2631
|
+
if (task.status === "blocked" || task.status === "review" || task.status === "pending") session.status = task.status;
|
|
2632
|
+
markers.forEach((marker, index) => {
|
|
2633
|
+
if (marker.laneId === task.id) markers[index] = {
|
|
2634
|
+
...marker,
|
|
2635
|
+
laneId: session.id
|
|
2636
|
+
};
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
const times = [...lanes.flatMap((lane) => [lane.start, lane.end]).filter((time) => time !== void 0), ...markers.map((marker) => marker.timestamp)];
|
|
2640
|
+
const hasOpen = lanes.some((lane) => lane.start !== void 0 && lane.end === void 0);
|
|
2641
|
+
const latest = times.length > 0 ? Math.max(...times) : void 0;
|
|
2642
|
+
if (hasOpen && now !== void 0 && (latest === void 0 || now - latest <= OPEN_LANE_STALE_MS)) times.push(now);
|
|
2643
|
+
const start = times.length > 0 ? Math.min(...times) : now ?? 0;
|
|
2644
|
+
return {
|
|
2645
|
+
lanes,
|
|
2646
|
+
markers,
|
|
2647
|
+
range: {
|
|
2648
|
+
start,
|
|
2649
|
+
end: times.length > 0 ? Math.max(...times) : start
|
|
2650
|
+
}
|
|
2651
|
+
};
|
|
2652
|
+
};
|
|
2653
|
+
var taskLaneStatus = (task, tasks) => {
|
|
2654
|
+
if (task.status === "todo" && !Task.isTaskReady(tasks, task)) return "blocked";
|
|
2655
|
+
return (task.status && TASK_STATUS[task.status]) ?? "pending";
|
|
2656
|
+
};
|
|
2657
|
+
var subSessionStatus = (endEvent, process) => {
|
|
2658
|
+
if (endEvent) return decode(Trace.OperationEnd.schema, endEvent.data)?.outcome === "failure" ? "failed" : "done";
|
|
2659
|
+
if (process && !ACTIVE_STATES.has(process.state)) return process.state === Process.State.FAILED ? "failed" : "done";
|
|
2660
|
+
return "running";
|
|
2661
|
+
};
|
|
2662
|
+
var toMarker = (event, id, laneId) => {
|
|
2663
|
+
const base = {
|
|
2664
|
+
id,
|
|
2665
|
+
laneId,
|
|
2666
|
+
timestamp: event.timestamp,
|
|
2667
|
+
pid: event.meta.pid
|
|
2668
|
+
};
|
|
2669
|
+
switch (event.type) {
|
|
2670
|
+
case Trace.AgentRequestBegin.key: return {
|
|
2671
|
+
...base,
|
|
2672
|
+
kind: "request",
|
|
2673
|
+
label: "Request started"
|
|
2674
|
+
};
|
|
2675
|
+
case Trace.AgentRequestEnd.key: {
|
|
2676
|
+
const data = decode(Trace.AgentRequestEnd.schema, event.data);
|
|
2677
|
+
return {
|
|
2678
|
+
...base,
|
|
2679
|
+
kind: "request",
|
|
2680
|
+
label: `Request ${data?.status ?? "ended"}`,
|
|
2681
|
+
level: data?.status === "error" ? "error" : data?.status === "interrupted" ? "warn" : void 0,
|
|
2682
|
+
detail: data?.error
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
case Trace.TaskStatusChanged.key: {
|
|
2686
|
+
const data = decode(Trace.TaskStatusChanged.schema, event.data);
|
|
2687
|
+
return {
|
|
2688
|
+
...base,
|
|
2689
|
+
kind: "task",
|
|
2690
|
+
label: data?.status === "started" ? "Task started" : `Task ${data?.status ?? "updated"}`,
|
|
2691
|
+
level: data?.status === "failed" ? "error" : void 0,
|
|
2692
|
+
detail: data
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
case Trace.DelegationSpawned.key: {
|
|
2696
|
+
const data = decode(Trace.DelegationSpawned.schema, event.data);
|
|
2697
|
+
return {
|
|
2698
|
+
...base,
|
|
2699
|
+
kind: "delegation",
|
|
2700
|
+
label: "Delegated",
|
|
2701
|
+
detail: data
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
case Trace.OperationStart.key: {
|
|
2705
|
+
const data = decode(Trace.OperationStart.schema, event.data);
|
|
2706
|
+
return {
|
|
2707
|
+
...base,
|
|
2708
|
+
kind: "operation",
|
|
2709
|
+
label: data?.name ?? data?.key ?? "Operation"
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
case Trace.OperationEnd.key: {
|
|
2713
|
+
const data = decode(Trace.OperationEnd.schema, event.data);
|
|
2714
|
+
const failed = data?.outcome === "failure";
|
|
2715
|
+
return {
|
|
2716
|
+
...base,
|
|
2717
|
+
kind: "operation",
|
|
2718
|
+
label: data?.name ?? data?.key ?? "Operation",
|
|
2719
|
+
level: failed ? "error" : void 0,
|
|
2720
|
+
detail: failed ? data?.error : void 0
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
case Trace.CompleteBlock.key: {
|
|
2724
|
+
const data = decode(Trace.CompleteBlock.schema, event.data);
|
|
2725
|
+
if (data?.block._tag === "toolCall") return {
|
|
2726
|
+
...base,
|
|
2727
|
+
kind: "tool",
|
|
2728
|
+
label: data.block.name
|
|
2729
|
+
};
|
|
2730
|
+
if (data?.block._tag === "text" && data.role === "user") return {
|
|
2731
|
+
...base,
|
|
2732
|
+
kind: "message",
|
|
2733
|
+
label: data.block.text.split("\n")[0] ?? ""
|
|
2734
|
+
};
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
default: return;
|
|
2738
|
+
}
|
|
2739
|
+
};
|
|
2740
|
+
//#endregion
|
|
2741
|
+
//#region src/session-timeline/types.ts
|
|
2742
|
+
var LaneKind = Schema.Literals(["session", "task"]);
|
|
2743
|
+
var LaneStatus = Schema.Literals([
|
|
2744
|
+
"pending",
|
|
2745
|
+
"blocked",
|
|
2746
|
+
"running",
|
|
2747
|
+
"review",
|
|
2748
|
+
"done",
|
|
2749
|
+
"failed"
|
|
2750
|
+
]);
|
|
2751
|
+
var TokenUsage = Schema.Struct({
|
|
2752
|
+
input: Schema.Number,
|
|
2753
|
+
output: Schema.Number,
|
|
2754
|
+
total: Schema.Number
|
|
2755
|
+
});
|
|
2756
|
+
/**
|
|
2757
|
+
* A conversation the timeline draws as a session lane: the chat, reduced to what the join needs so
|
|
2758
|
+
* the builder does not depend on the chat's schema. `uri` is what the agent process targets and
|
|
2759
|
+
* `feedId` what its trace meta names; `taskIds` is the checklist.
|
|
2760
|
+
*/
|
|
2761
|
+
var Session = Schema.Struct({
|
|
2762
|
+
id: Schema.String,
|
|
2763
|
+
label: Schema.optional(Schema.String),
|
|
2764
|
+
uri: Schema.String,
|
|
2765
|
+
feedId: Schema.optional(Schema.String),
|
|
2766
|
+
taskIds: Schema.Array(Schema.String)
|
|
2767
|
+
});
|
|
2768
|
+
/**
|
|
2769
|
+
* Node in the supervisor lane a child session was spawned from, so the connector can start at the
|
|
2770
|
+
* exact marker rather than at the lane's edge.
|
|
2771
|
+
*/
|
|
2772
|
+
var DelegationSource = Schema.Struct({
|
|
2773
|
+
laneId: Schema.String,
|
|
2774
|
+
markerId: Schema.String
|
|
2775
|
+
});
|
|
2776
|
+
/**
|
|
2777
|
+
* One row of the timeline: a session (supervisor or delegated sub-agent) rendered as a box spanning
|
|
2778
|
+
* `start`..`end`, or a task rendered as a thin line grouped under its session.
|
|
2779
|
+
*/
|
|
2780
|
+
var Lane = Schema.Struct({
|
|
2781
|
+
id: Schema.String,
|
|
2782
|
+
kind: LaneKind,
|
|
2783
|
+
label: Schema.String,
|
|
2784
|
+
status: LaneStatus,
|
|
2785
|
+
/** Epoch ms. */
|
|
2786
|
+
start: Schema.optional(Schema.Number),
|
|
2787
|
+
end: Schema.optional(Schema.Number),
|
|
2788
|
+
parentId: Schema.optional(Schema.String),
|
|
2789
|
+
sessionId: Schema.optional(Schema.String),
|
|
2790
|
+
taskId: Schema.optional(Schema.String),
|
|
2791
|
+
pid: Schema.optional(Schema.String),
|
|
2792
|
+
/** Lane ids this lane waits on (task dependencies). */
|
|
2793
|
+
blockedOn: Schema.optional(Schema.Array(Schema.String)),
|
|
2794
|
+
delegatedFrom: Schema.optional(DelegationSource),
|
|
2795
|
+
tokens: Schema.optional(TokenUsage),
|
|
2796
|
+
toolCalls: Schema.optional(Schema.Number)
|
|
2797
|
+
});
|
|
2798
|
+
var MarkerKind = Schema.Literals([
|
|
2799
|
+
"request",
|
|
2800
|
+
"operation",
|
|
2801
|
+
"tool",
|
|
2802
|
+
"message",
|
|
2803
|
+
"error",
|
|
2804
|
+
"delegation",
|
|
2805
|
+
"task"
|
|
2806
|
+
]);
|
|
2807
|
+
var MarkerLevel = Schema.Literals([
|
|
2808
|
+
"info",
|
|
2809
|
+
"warn",
|
|
2810
|
+
"error"
|
|
2811
|
+
]);
|
|
2812
|
+
/**
|
|
2813
|
+
* A point event on a lane, rendered as a node inside the session box.
|
|
2814
|
+
*/
|
|
2815
|
+
var Marker = Schema.Struct({
|
|
2816
|
+
id: Schema.String,
|
|
2817
|
+
laneId: Schema.String,
|
|
2818
|
+
kind: MarkerKind,
|
|
2819
|
+
timestamp: Schema.Number,
|
|
2820
|
+
label: Schema.String,
|
|
2821
|
+
level: Schema.optional(MarkerLevel),
|
|
2822
|
+
pid: Schema.optional(Schema.String),
|
|
2823
|
+
detail: Schema.optional(Schema.Unknown)
|
|
2824
|
+
});
|
|
2825
|
+
var TimeRange = Schema.Struct({
|
|
2826
|
+
start: Schema.Number,
|
|
2827
|
+
end: Schema.Number
|
|
2828
|
+
});
|
|
2829
|
+
var SessionTimeline = Schema.Struct({
|
|
2830
|
+
lanes: Schema.Array(Lane),
|
|
2831
|
+
markers: Schema.Array(Marker),
|
|
2832
|
+
range: TimeRange
|
|
2833
|
+
});
|
|
2834
|
+
//#endregion
|
|
2835
|
+
//#region src/hooks/useSessionTimeline.ts
|
|
2836
|
+
/** How often `now` advances while a session is open, so a running lane's range keeps growing. */
|
|
2837
|
+
var TICK_MS = 5e3;
|
|
2838
|
+
var TRACE_DEBOUNCE = Duration.millis(500);
|
|
2839
|
+
var NO_PROCESSES = [];
|
|
2840
|
+
/**
|
|
2841
|
+
* The live gantt-shaped view of the given sessions: rebuilt as trace events land on the space's
|
|
2842
|
+
* trace feed and as the process tree changes, and ticked so open lanes keep extending.
|
|
2843
|
+
*/
|
|
2844
|
+
var useSessionTimeline = (space, { sessions, tasks, processes = NO_PROCESSES }) => {
|
|
2845
|
+
const traceMessages = useTraceMessages(space, { debounce: TRACE_DEBOUNCE });
|
|
2846
|
+
const [now, setNow] = useState(() => Date.now());
|
|
2847
|
+
useEffect(() => {
|
|
2848
|
+
const interval = setInterval(() => setNow(Date.now()), TICK_MS);
|
|
2849
|
+
return () => clearInterval(interval);
|
|
2850
|
+
}, []);
|
|
2851
|
+
return useMemo(() => buildSessionTimeline({
|
|
2852
|
+
traceMessages,
|
|
2853
|
+
processes,
|
|
2854
|
+
sessions,
|
|
2855
|
+
tasks,
|
|
2856
|
+
now
|
|
2857
|
+
}), [
|
|
2858
|
+
traceMessages,
|
|
2859
|
+
processes,
|
|
2860
|
+
sessions,
|
|
2861
|
+
tasks,
|
|
2862
|
+
now
|
|
2863
|
+
]);
|
|
2864
|
+
};
|
|
2865
|
+
//#endregion
|
|
2866
|
+
export { ALL_PROCESS_ENVIRONMENTS, BEGIN_EVENT_TYPES, CommitSelector, DEFAULT_PROCESS_ENVIRONMENTS, DEFAULT_SPAN_TIMEOUT_MS, DelegationSource, END_EVENT_TYPES, Gantt, Lane, LaneKind, LaneStatus, Marker, MarkerKind, MarkerLevel, ProcessEnvironment, ProcessTree, ROOT_SPAN_ID, Session, SessionTimeline, TimeRange, Timeline, TokenUsage, TracePanel, buildExecutionGraph, buildSessionTimeline, buildSpanTree, collectProcessActivityLines, compactOptions, defaultOptions, deriveInFlightActivityLine, environmentIcon, estimateTokenCount, filterProcesses, filterTraceMessages, flattenSpanTree, formatPendingBlockStatus, getTraceMessagesAtom, isSpanBeginEvent, isSpanEndEvent, parseProcessEnvironments, pendingStatusFromEphemeralMessage, processEnvironment, renderTimelineAscii, resolveEphemeralStatusUpdate, toggleProcessEnvironment, useExecutionGraph, useSessionTimeline, useTraceMenu, useTraceMessages, walkSpanTree };
|
|
2867
|
+
|
|
2868
|
+
//# sourceMappingURL=index.mjs.map
|