@dxos/react-ui-trace 0.11.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/lib/index.mjs +1722 -561
- package/dist/lib/index.mjs.map +1 -1
- package/dist/lib/testing.mjs +9 -8
- package/dist/lib/testing.mjs.map +1 -1
- package/dist/lib/translations.mjs +5 -0
- package/dist/lib/translations.mjs.map +1 -1
- package/dist/types/src/components/Gantt/Gantt.d.ts +109 -24
- package/dist/types/src/components/Gantt/Gantt.d.ts.map +1 -1
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts +73 -3
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts.map +1 -1
- package/dist/types/src/components/Gantt/gantt-rows.d.ts +26 -0
- package/dist/types/src/components/Gantt/gantt-rows.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/gantt-rows.test.d.ts +2 -0
- package/dist/types/src/components/Gantt/gantt-rows.test.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/gantt-scale.d.ts +57 -0
- package/dist/types/src/components/Gantt/gantt-scale.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/gantt-scale.test.d.ts +2 -0
- package/dist/types/src/components/Gantt/gantt-scale.test.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 -1
- package/dist/types/src/components/Gantt/useEnter.d.ts +13 -0
- package/dist/types/src/components/Gantt/useEnter.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/useRangeTween.d.ts +12 -0
- package/dist/types/src/components/Gantt/useRangeTween.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts +3 -3
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts.map +1 -1
- package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Timeline/Timeline.d.ts +10 -2
- package/dist/types/src/components/Timeline/Timeline.d.ts.map +1 -1
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts +8 -4
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts.map +1 -1
- package/dist/types/src/components/Timeline/timeline-layout.d.ts +35 -0
- package/dist/types/src/components/Timeline/timeline-layout.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-layout.test.d.ts +2 -0
- package/dist/types/src/components/Timeline/timeline-layout.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/SpanTreeView.d.ts +15 -0
- package/dist/types/src/components/TracePanel/SpanTreeView.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/TracePanel.d.ts +3 -3
- package/dist/types/src/components/TracePanel/TracePanel.d.ts.map +1 -1
- package/dist/types/src/components/TracePanel/TracePanel.stories.d.ts +46 -0
- package/dist/types/src/components/TracePanel/TracePanel.stories.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/trace-filter.d.ts +7 -2
- package/dist/types/src/components/TracePanel/trace-filter.d.ts.map +1 -1
- package/dist/types/src/components/TracePanel/useTraceMenu.d.ts.map +1 -1
- package/dist/types/src/execution-graph/execution-graph.d.ts +1 -1
- package/dist/types/src/execution-graph/execution-graph.d.ts.map +1 -1
- package/dist/types/src/hooks/useExecutionGraph.d.ts +2 -2
- package/dist/types/src/hooks/useExecutionGraph.d.ts.map +1 -1
- package/dist/types/src/hooks/useSessionTimeline.d.ts +1 -1
- package/dist/types/src/hooks/useSessionTimeline.d.ts.map +1 -1
- package/dist/types/src/hooks/useTraceMessages.d.ts +1 -1
- package/dist/types/src/hooks/useTraceMessages.d.ts.map +1 -1
- package/dist/types/src/session-timeline/TaskHistory.stories.d.ts +12 -0
- package/dist/types/src/session-timeline/TaskHistory.stories.d.ts.map +1 -0
- package/dist/types/src/session-timeline/gantt-mapping.d.ts +14 -0
- package/dist/types/src/session-timeline/gantt-mapping.d.ts.map +1 -0
- package/dist/types/src/session-timeline/gantt-mapping.test.d.ts +2 -0
- package/dist/types/src/session-timeline/gantt-mapping.test.d.ts.map +1 -0
- package/dist/types/src/session-timeline/index.d.ts +1 -0
- package/dist/types/src/session-timeline/index.d.ts.map +1 -1
- package/dist/types/src/session-timeline/session-timeline.d.ts +17 -2
- package/dist/types/src/session-timeline/session-timeline.d.ts.map +1 -1
- package/dist/types/src/session-timeline/types.d.ts +32 -0
- package/dist/types/src/session-timeline/types.d.ts.map +1 -1
- package/dist/types/src/testing/simulated-agent.d.ts +5 -4
- package/dist/types/src/testing/simulated-agent.d.ts.map +1 -1
- package/dist/types/src/testing/test-trace.d.ts +2 -2
- package/dist/types/src/testing/test-trace.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +5 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +30 -26
- package/src/components/Gantt/Gantt.stories.tsx +914 -96
- package/src/components/Gantt/Gantt.tsx +964 -261
- package/src/components/Gantt/gantt-rows.test.ts +123 -0
- package/src/components/Gantt/gantt-rows.ts +71 -0
- package/src/components/Gantt/gantt-scale.test.ts +105 -0
- package/src/components/Gantt/gantt-scale.ts +143 -0
- package/src/components/Gantt/index.ts +2 -0
- package/src/components/Gantt/useEnter.ts +33 -0
- package/src/components/Gantt/useRangeTween.ts +61 -0
- package/src/components/ProcessTree/ProcessTree.stories.tsx +2 -3
- package/src/components/ProcessTree/ProcessTree.tsx +99 -90
- package/src/components/Timeline/Timeline.stories.tsx +110 -22
- package/src/components/Timeline/Timeline.tsx +319 -271
- package/src/components/Timeline/timeline-layout.test.ts +177 -0
- package/src/components/Timeline/timeline-layout.ts +153 -0
- package/src/components/TracePanel/SpanTreeView.tsx +39 -0
- package/src/components/TracePanel/TracePanel.stories.tsx +106 -0
- package/src/components/TracePanel/TracePanel.tsx +113 -106
- package/src/components/TracePanel/trace-filter.test.ts +31 -2
- package/src/components/TracePanel/trace-filter.ts +28 -3
- package/src/components/TracePanel/useTraceMenu.ts +0 -1
- package/src/execution-graph/SPEC.md +10 -10
- package/src/execution-graph/execution-graph.test.ts +16 -15
- package/src/execution-graph/execution-graph.ts +108 -60
- package/src/execution-graph/pending-block-status.ts +1 -1
- package/src/execution-graph/sub-agent-delegation.test.ts +1 -1
- package/src/hooks/useExecutionGraph.ts +14 -11
- package/src/hooks/useSessionTimeline.ts +35 -9
- package/src/hooks/useTraceMessages.ts +1 -1
- package/src/session-timeline/TaskHistory.stories.tsx +142 -0
- package/src/session-timeline/gantt-mapping.test.ts +118 -0
- package/src/session-timeline/gantt-mapping.ts +158 -0
- package/src/session-timeline/index.ts +1 -0
- package/src/session-timeline/session-timeline.test.ts +585 -35
- package/src/session-timeline/session-timeline.ts +433 -62
- package/src/session-timeline/types.ts +14 -0
- package/src/testing/simulated-agent.ts +17 -16
- package/src/testing/test-trace.ts +14 -12
- package/src/translations.ts +8 -2
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts +0 -2
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts.map +0 -1
- package/src/execution-graph/remote-snapshot.test.ts +0 -116
package/dist/lib/index.mjs
CHANGED
|
@@ -1,165 +1,380 @@
|
|
|
1
1
|
import { translationKey } from "./translations.mjs";
|
|
2
2
|
import { format } from "date-fns";
|
|
3
|
-
import React, { forwardRef, memo, useCallback, useContext, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import React, { Fragment, forwardRef, memo, useCallback, useContext, useDeferredValue, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { createContext, useComposedRefs } from "@dxos/react-hooks";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
5
|
+
import * as Button from "@dxos/react-ui/Button";
|
|
6
|
+
import * as Hooks from "@dxos/react-ui/Hooks";
|
|
7
|
+
import * as HoverCard from "@dxos/react-ui/HoverCard";
|
|
8
|
+
import * as ScrollArea from "@dxos/react-ui/ScrollArea";
|
|
9
|
+
import * as Util from "@dxos/react-ui/Util";
|
|
10
|
+
import { getHashHue, mx, palette } from "@dxos/ui-theme";
|
|
11
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
9
12
|
import { RegistryContext } from "@effect/atom-react/RegistryContext";
|
|
10
13
|
import * as Match from "effect/Match";
|
|
11
14
|
import * as Option from "effect/Option";
|
|
12
15
|
import * as Process from "@dxos/compute/Process";
|
|
13
16
|
import { Tree, createStaticTreeModel } from "@dxos/react-ui-list";
|
|
17
|
+
import * as Icon from "@dxos/react-ui/Icon";
|
|
18
|
+
import * as Tooltip from "@dxos/react-ui/Tooltip";
|
|
19
|
+
import { Unit, safeStringify, trim } from "@dxos/util";
|
|
14
20
|
import { addEventListener } from "@dxos/async";
|
|
15
21
|
import { LogLevel, log } from "@dxos/log";
|
|
16
22
|
import { Shimmer } from "@dxos/react-ui-components";
|
|
23
|
+
import { useListModel, useWindow, windowRowProps } from "@dxos/react-ui-virtual";
|
|
17
24
|
import { ActionToolbar, MenuBuilder, useMenuBuilder } from "@dxos/react-ui-menu";
|
|
18
25
|
import { JsonHighlighter } from "@dxos/react-ui-syntax-highlighter";
|
|
26
|
+
import * as Accordion from "@dxos/react-ui/Accordion";
|
|
27
|
+
import * as Panel from "@dxos/react-ui/Panel";
|
|
28
|
+
import * as ScrollContainer from "@dxos/react-ui/ScrollContainer";
|
|
29
|
+
import { TextEditor } from "@dxos/react-ui-editor";
|
|
30
|
+
import { createBasicExtensions, createThemeExtensions, folding, json } from "@dxos/ui-editor";
|
|
19
31
|
import * as Array$1 from "effect/Array";
|
|
20
32
|
import { pipe } from "effect/Function";
|
|
21
33
|
import * as Pipeable from "effect/Pipeable";
|
|
22
34
|
import * as Schema from "effect/Schema";
|
|
23
35
|
import * as Struct from "effect/Struct";
|
|
24
|
-
import { RUN_AGAIN_ERROR_CODE, RUN_AGAIN_MESSAGE } from "@dxos/compute";
|
|
25
36
|
import * as Trace from "@dxos/compute/Trace";
|
|
26
|
-
import { Annotation, Filter, Query } from "@dxos/echo";
|
|
37
|
+
import { Annotation, Filter, Obj, Query } from "@dxos/echo";
|
|
27
38
|
import { EID } from "@dxos/keys";
|
|
28
39
|
import { useAtomValue } from "@effect/atom-react/Hooks";
|
|
29
40
|
import * as Duration from "effect/Duration";
|
|
30
|
-
import * as Atom from "effect/
|
|
41
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
31
42
|
import { FeedTraceSink } from "@dxos/compute-runtime";
|
|
32
43
|
import { Task } from "@dxos/types";
|
|
44
|
+
//#region src/components/Gantt/gantt-rows.ts
|
|
45
|
+
/**
|
|
46
|
+
* Rows in reading order: the ungrouped lanes, then each group's band, a nested group's band directly
|
|
47
|
+
* after its parent's. Within a band the lanes come in the order given, each nested under its
|
|
48
|
+
* `parentId`.
|
|
49
|
+
*
|
|
50
|
+
* A band is contiguous by construction, which is the whole reason the order is settled here rather
|
|
51
|
+
* than left to the caller: one rectangle can only enclose a run of adjacent rows.
|
|
52
|
+
*/
|
|
53
|
+
var orderRows = (groups, lanes) => {
|
|
54
|
+
const rows = [];
|
|
55
|
+
const bands = [];
|
|
56
|
+
/** A group's lanes, nested under one another by `parentId`; a parent in another group is ignored. */
|
|
57
|
+
const pushLanes = (members, parentId, depth) => {
|
|
58
|
+
for (const lane of members.filter((member) => member.parentId === parentId)) {
|
|
59
|
+
rows.push({
|
|
60
|
+
lane,
|
|
61
|
+
depth,
|
|
62
|
+
index: rows.length,
|
|
63
|
+
groupId: lane.groupId
|
|
64
|
+
});
|
|
65
|
+
pushLanes(members, lane.id, depth + 1);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const visitGroup = (group, depth) => {
|
|
69
|
+
const first = rows.length;
|
|
70
|
+
pushLanes(lanes.filter((lane) => lane.groupId === group.id), void 0, depth);
|
|
71
|
+
if (rows.length > first) bands.push({
|
|
72
|
+
group,
|
|
73
|
+
first,
|
|
74
|
+
last: rows.length - 1
|
|
75
|
+
});
|
|
76
|
+
for (const child of groups.filter((candidate) => candidate.parentId === group.id)) visitGroup(child, depth + 1);
|
|
77
|
+
};
|
|
78
|
+
pushLanes(lanes.filter((lane) => lane.groupId === void 0), void 0, 0);
|
|
79
|
+
for (const group of groups.filter((group) => group.parentId === void 0)) visitGroup(group, 0);
|
|
80
|
+
return {
|
|
81
|
+
rows,
|
|
82
|
+
bands
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/components/Gantt/gantt-scale.ts
|
|
87
|
+
/** Room a tick label needs; elapsed labels are short, so this also caps how many fit. */
|
|
88
|
+
var TIME_LABEL_WIDTH = 56;
|
|
89
|
+
var MAX_TIME_TICKS = 10;
|
|
90
|
+
/** Round tick intervals in milliseconds, so elapsed labels read `0s, 10s, 20s` rather than `13.4s`. */
|
|
91
|
+
var TICK_STEPS = [
|
|
92
|
+
100,
|
|
93
|
+
200,
|
|
94
|
+
500,
|
|
95
|
+
1e3,
|
|
96
|
+
2e3,
|
|
97
|
+
5e3,
|
|
98
|
+
1e4,
|
|
99
|
+
15e3,
|
|
100
|
+
3e4,
|
|
101
|
+
6e4,
|
|
102
|
+
12e4,
|
|
103
|
+
3e5,
|
|
104
|
+
6e5,
|
|
105
|
+
9e5,
|
|
106
|
+
18e5,
|
|
107
|
+
36e5,
|
|
108
|
+
72e5,
|
|
109
|
+
216e5,
|
|
110
|
+
432e5,
|
|
111
|
+
864e5
|
|
112
|
+
];
|
|
113
|
+
var pad2 = (value) => String(value).padStart(2, "0");
|
|
114
|
+
/**
|
|
115
|
+
* Elapsed time since the start of the axis. The span picks the form for every tick alike, so a
|
|
116
|
+
* run's labels read as one unit: `0.2s` below a second's step, `45s` below a minute, `1:05` below an
|
|
117
|
+
* hour, `1:02:05` beyond.
|
|
118
|
+
*/
|
|
119
|
+
var formatElapsed = (elapsed, { span, step }) => {
|
|
120
|
+
const seconds = Math.round(elapsed / 1e3);
|
|
121
|
+
if (span >= 36e5) return `${Math.floor(seconds / 3600)}:${pad2(Math.floor(seconds / 60) % 60)}:${pad2(seconds % 60)}`;
|
|
122
|
+
if (span >= 6e4) return `${Math.floor(seconds / 60)}:${pad2(seconds % 60)}`;
|
|
123
|
+
if (step < 1e3) return `${(elapsed / 1e3).toFixed(1)}s`;
|
|
124
|
+
return `${seconds}s`;
|
|
125
|
+
};
|
|
126
|
+
/** An event's ordinal is two or three glyphs, so its labels pack tighter than a clock's. */
|
|
127
|
+
var EVENT_LABEL_WIDTH = 40;
|
|
128
|
+
/** Duration against width, labelled with the time elapsed since the start of the range. */
|
|
129
|
+
var timeScale = ({ range, width, pad }) => {
|
|
130
|
+
const span = Math.max(range.end - range.start, 1);
|
|
131
|
+
const inner = Math.max(width - 2 * pad, 1);
|
|
132
|
+
const at = (time) => pad + (time - range.start) / span * inner;
|
|
133
|
+
const maxTicks = Math.max(2, Math.min(MAX_TIME_TICKS, Math.floor(inner / TIME_LABEL_WIDTH)));
|
|
134
|
+
const step = TICK_STEPS.find((candidate) => span / candidate <= maxTicks - 1) ?? Math.ceil(span / (maxTicks - 1) / TICK_STEPS[TICK_STEPS.length - 1]) * TICK_STEPS[TICK_STEPS.length - 1];
|
|
135
|
+
const ticks = [];
|
|
136
|
+
for (let elapsed = 0; elapsed <= span; elapsed += step) ticks.push({
|
|
137
|
+
at: at(range.start + elapsed),
|
|
138
|
+
label: formatElapsed(elapsed, {
|
|
139
|
+
span,
|
|
140
|
+
step
|
|
141
|
+
})
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
at,
|
|
145
|
+
width,
|
|
146
|
+
ticks
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* One step per event, in the order they happened. Ordinals are global rather than per lane: a
|
|
151
|
+
* delegation drops from a parent's node to its child's first node, and only a shared ordering keeps
|
|
152
|
+
* those two at the same offset.
|
|
153
|
+
*/
|
|
154
|
+
var unitScale = ({ times, step, pad }) => {
|
|
155
|
+
const events = [...new Set(times)].sort((left, right) => left - right);
|
|
156
|
+
const last = Math.max(events.length - 1, 0);
|
|
157
|
+
/** Index of the last event at or before `time`, or -1 when `time` precedes every event. */
|
|
158
|
+
const floorIndex = (time) => {
|
|
159
|
+
let low = 0;
|
|
160
|
+
let high = events.length - 1;
|
|
161
|
+
let found = -1;
|
|
162
|
+
while (low <= high) {
|
|
163
|
+
const mid = low + high >> 1;
|
|
164
|
+
if (events[mid] <= time) {
|
|
165
|
+
found = mid;
|
|
166
|
+
low = mid + 1;
|
|
167
|
+
} else high = mid - 1;
|
|
168
|
+
}
|
|
169
|
+
return found;
|
|
170
|
+
};
|
|
171
|
+
const unit = (time) => {
|
|
172
|
+
if (events.length === 0) return 0;
|
|
173
|
+
const index = floorIndex(time);
|
|
174
|
+
if (index < 0) return 0;
|
|
175
|
+
if (index >= last) return last;
|
|
176
|
+
const from = events[index];
|
|
177
|
+
return index + (time - from) / (events[index + 1] - from);
|
|
178
|
+
};
|
|
179
|
+
const stride = Math.max(1, Math.ceil(last / Math.max(Math.floor(last * step / EVENT_LABEL_WIDTH), 1)));
|
|
180
|
+
const ticks = [];
|
|
181
|
+
for (let index = 0; events.length > 0 && index <= last; index += stride) ticks.push({
|
|
182
|
+
at: pad + index * step,
|
|
183
|
+
label: `#${index + 1}`
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
at: (time) => pad + unit(time) * step,
|
|
187
|
+
width: 2 * pad + last * step,
|
|
188
|
+
ticks
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/components/Gantt/useEnter.ts
|
|
193
|
+
/**
|
|
194
|
+
* Which of `ids` arrived since the last frame.
|
|
195
|
+
*
|
|
196
|
+
* A caller draws an entering element at the place it came from — the event before it, the node that
|
|
197
|
+
* spawned its lane — and the next frame draws it where it belongs, leaving a CSS transition to carry
|
|
198
|
+
* it the rest of the way. That is all the bookkeeping an enter animation needs: everything else in
|
|
199
|
+
* the drawing animates from its own previous value, which the browser already has.
|
|
200
|
+
*
|
|
201
|
+
* Nothing present on mount ever counts as entering — a chart that animates its whole history open on
|
|
202
|
+
* first paint is noise, since the reader was not there to see it arrive.
|
|
203
|
+
*/
|
|
204
|
+
var useEnter = (ids) => {
|
|
205
|
+
const [settled, setSettled] = useState(() => new Set(ids));
|
|
206
|
+
const entering = ids.some((id) => !settled.has(id));
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (!entering) return;
|
|
209
|
+
const frame = requestAnimationFrame(() => setSettled(new Set(ids)));
|
|
210
|
+
return () => cancelAnimationFrame(frame);
|
|
211
|
+
});
|
|
212
|
+
return (id) => !settled.has(id);
|
|
213
|
+
};
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/components/Gantt/useRangeTween.ts
|
|
216
|
+
/** How long the axis takes to settle on new bounds. */
|
|
217
|
+
var TWEEN_DURATION = 400;
|
|
218
|
+
var easeOutCubic = (progress) => 1 - (1 - progress) ** 3;
|
|
219
|
+
/** Motion is skipped where nothing would see it, or where the reader asked for less of it. */
|
|
220
|
+
var shouldSnap = () => typeof document === "undefined" || document.hidden || typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
221
|
+
/**
|
|
222
|
+
* The range to draw with: `target`, reached over a short ease rather than in one step, so a
|
|
223
|
+
* time axis whose bounds move (a new event, `now` advancing) slides every span, node and tick to
|
|
224
|
+
* its new place together instead of jumping. A new target mid-flight eases on from wherever the
|
|
225
|
+
* drawing is, so a stream of updates reads as one continuous motion.
|
|
226
|
+
*/
|
|
227
|
+
var useRangeTween = (target, enabled) => {
|
|
228
|
+
const [shown, setShown] = useState(target);
|
|
229
|
+
const shownRef = useRef(target);
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const from = shownRef.current;
|
|
232
|
+
const settle = (range) => {
|
|
233
|
+
shownRef.current = range;
|
|
234
|
+
setShown(range);
|
|
235
|
+
};
|
|
236
|
+
if (!enabled || shouldSnap() || from.start === target.start && from.end === target.end) {
|
|
237
|
+
settle(target);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
let frame;
|
|
241
|
+
const startedAt = performance.now();
|
|
242
|
+
const step = (time) => {
|
|
243
|
+
const progress = Math.min((time - startedAt) / TWEEN_DURATION, 1);
|
|
244
|
+
const eased = easeOutCubic(progress);
|
|
245
|
+
settle({
|
|
246
|
+
start: from.start + (target.start - from.start) * eased,
|
|
247
|
+
end: from.end + (target.end - from.end) * eased
|
|
248
|
+
});
|
|
249
|
+
frame = progress < 1 ? requestAnimationFrame(step) : void 0;
|
|
250
|
+
};
|
|
251
|
+
frame = requestAnimationFrame(step);
|
|
252
|
+
return () => {
|
|
253
|
+
if (frame !== void 0) cancelAnimationFrame(frame);
|
|
254
|
+
};
|
|
255
|
+
}, [
|
|
256
|
+
enabled,
|
|
257
|
+
target.start,
|
|
258
|
+
target.end
|
|
259
|
+
]);
|
|
260
|
+
return enabled ? shown : target;
|
|
261
|
+
};
|
|
262
|
+
//#endregion
|
|
33
263
|
//#region src/components/Gantt/Gantt.tsx
|
|
34
|
-
var
|
|
264
|
+
var PAD_X = 16;
|
|
265
|
+
var ROW_HEIGHT = 24;
|
|
35
266
|
var HEADER_HEIGHT = 20;
|
|
36
|
-
|
|
37
|
-
var
|
|
38
|
-
/**
|
|
39
|
-
var
|
|
40
|
-
|
|
267
|
+
/** Pixels per event on the `unit` axis — wide enough that two adjacent nodes read as two. */
|
|
268
|
+
var UNIT_STEP = 32;
|
|
269
|
+
/** How long a newly arrived element takes to travel from where it came from to where it belongs. */
|
|
270
|
+
var ENTER_TRANSITION = "duration-500 ease-out";
|
|
271
|
+
/** Within this of the live edge the chart keeps following it; past it the reader is reading history. */
|
|
272
|
+
var FOLLOW_SLACK = 4;
|
|
273
|
+
/**
|
|
274
|
+
* How long the chart takes to drift to the live edge. Longer than the enter transition on purpose:
|
|
275
|
+
* the arriving event settles into place first, and the window follows it rather than racing it.
|
|
276
|
+
*/
|
|
277
|
+
var FOLLOW_DURATION = 600;
|
|
278
|
+
var NODE_RADIUS = 6;
|
|
41
279
|
var BAR_HEIGHT = 15;
|
|
280
|
+
/** The thread through a lane's nodes: a hairline, so the nodes remain what the row reads as. */
|
|
281
|
+
var THREAD_HEIGHT = 1;
|
|
42
282
|
/**
|
|
43
283
|
* A bar reaches half its height past its first and last instant, so a node sitting on either one is
|
|
44
284
|
* as far from the bar's end as it is from its top and bottom.
|
|
45
285
|
*/
|
|
46
286
|
var BAR_OVERHANG = BAR_HEIGHT / 2;
|
|
47
|
-
/** Gap between adjacent
|
|
287
|
+
/** Gap between adjacent band rectangles, halved on each. */
|
|
48
288
|
var GROUP_INSET = 2;
|
|
49
289
|
/** The rectangle's clearance around its bars, the same on every side, so its corners stay concentric with theirs. */
|
|
50
290
|
var GROUP_PAD = ROW_HEIGHT / 2 - BAR_HEIGHT / 2 - GROUP_INSET;
|
|
51
|
-
var GROUP_RADIUS =
|
|
52
|
-
/** Radius of the
|
|
291
|
+
var GROUP_RADIUS = 10;
|
|
292
|
+
/** Radius of the connector's bend from the drop into the lane it opens. */
|
|
53
293
|
var BEND_RADIUS = BAR_HEIGHT / 2;
|
|
54
294
|
/**
|
|
55
295
|
* Bar, node, thread and label colours per status: nodes and the thread through them are the bar's
|
|
56
|
-
* hue in a lighter shade
|
|
296
|
+
* hue in a lighter shade, and `edge` — the live end of a lane still running — is the stronger shade
|
|
297
|
+
* the legend's own status dot uses, so the two read as the same statement about the lane.
|
|
57
298
|
*/
|
|
299
|
+
/**
|
|
300
|
+
* A hued lane's colour: the theme's tag surface token, read as a variable because the hue is data, so
|
|
301
|
+
* the lane matches a `Tag` of the same hue.
|
|
302
|
+
*/
|
|
303
|
+
var hueColor = (hue) => `var(--color-${hue}-surface)`;
|
|
304
|
+
/** The hue's strong shade, for a node that needs attention (a question, an error) within its lane. */
|
|
305
|
+
var hueStrongColor = (hue) => `var(--color-${hue}-text)`;
|
|
58
306
|
var STATUS_COLOR = {
|
|
59
307
|
pending: {
|
|
60
308
|
fill: "fill-neutral-500/40",
|
|
61
309
|
node: "fill-neutral-300",
|
|
310
|
+
edge: "fill-neutral-400",
|
|
62
311
|
thread: "stroke-neutral-300",
|
|
63
312
|
text: "text-neutral-400"
|
|
64
313
|
},
|
|
65
314
|
blocked: {
|
|
66
315
|
fill: "fill-orange-500/40",
|
|
67
316
|
node: "fill-orange-300",
|
|
317
|
+
edge: "fill-orange-500",
|
|
68
318
|
thread: "stroke-orange-300",
|
|
69
319
|
text: "text-orange-500"
|
|
70
320
|
},
|
|
71
321
|
running: {
|
|
72
322
|
fill: "fill-sky-500/40",
|
|
73
323
|
node: "fill-sky-300",
|
|
324
|
+
edge: "fill-sky-500",
|
|
74
325
|
thread: "stroke-sky-300",
|
|
75
326
|
text: "text-sky-500"
|
|
76
327
|
},
|
|
77
328
|
review: {
|
|
78
329
|
fill: "fill-cyan-500/40",
|
|
79
330
|
node: "fill-cyan-300",
|
|
331
|
+
edge: "fill-cyan-500",
|
|
80
332
|
thread: "stroke-cyan-300",
|
|
81
333
|
text: "text-cyan-500"
|
|
82
334
|
},
|
|
83
335
|
done: {
|
|
84
336
|
fill: "fill-green-500/40",
|
|
85
337
|
node: "fill-green-300",
|
|
338
|
+
edge: "fill-green-500",
|
|
86
339
|
thread: "stroke-green-300",
|
|
87
340
|
text: "text-green-500"
|
|
88
341
|
},
|
|
89
342
|
failed: {
|
|
90
343
|
fill: "fill-red-500/40",
|
|
91
344
|
node: "fill-red-300",
|
|
345
|
+
edge: "fill-red-500",
|
|
92
346
|
thread: "stroke-red-300",
|
|
93
347
|
text: "text-red-500"
|
|
94
348
|
}
|
|
95
349
|
};
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
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);
|
|
350
|
+
/** A node that asks for attention: a question put to the reader, or something that went wrong. */
|
|
351
|
+
var isAlert = (marker) => marker.level === "warn" || marker.level === "error";
|
|
352
|
+
var CONNECTOR_CLASSNAME = "stroke-fuchsia-500";
|
|
149
353
|
var [GanttProvider, useGanttContext] = createContext("Gantt");
|
|
150
354
|
/**
|
|
151
|
-
* Gantt view of
|
|
152
|
-
* the parts lay out side by side in the order given, sharing one row grid: `Legend` names the
|
|
153
|
-
*
|
|
154
|
-
*
|
|
355
|
+
* Gantt view of lanes on a shared axis. The root resolves the rows and the axis and fills its host;
|
|
356
|
+
* the parts lay out side by side in the order given, sharing one row grid: `Legend` names the lanes
|
|
357
|
+
* and `Chart` draws them. A host that already lists the lanes renders the chart alone.
|
|
358
|
+
*
|
|
359
|
+
* The root owns the scrolling, on both axes at once: the legend's rows and the chart's move together
|
|
360
|
+
* vertically, the legend stays put while the drawing scrolls horizontally, and the horizontal bar is
|
|
361
|
+
* at the foot of the host rather than under the last lane.
|
|
155
362
|
*/
|
|
156
|
-
var GanttRoot = composable(({ lanes, markers = [], range: rangeProp, now, showNow, onLaneSelect, onMarkerSelect, children, ...props }, forwardedRef) => {
|
|
157
|
-
const
|
|
363
|
+
var GanttRoot = Util.composable(({ groups = [], lanes, markers = [], range: rangeProp, now, showNow, axis = "unit", unitStep, animate, onLaneSelect, onMarkerSelect, onAxisChange, legend: legendProp, onLegendChange, children, ...props }, forwardedRef) => {
|
|
364
|
+
const [legendState, setLegendState] = useState("title");
|
|
365
|
+
const legend = legendProp ?? legendState;
|
|
366
|
+
const setLegend = useCallback((next) => {
|
|
367
|
+
setLegendState(next);
|
|
368
|
+
onLegendChange?.(next);
|
|
369
|
+
}, [onLegendChange]);
|
|
370
|
+
const [viewport, setViewport] = useState(null);
|
|
371
|
+
const [legendWidth, setLegendWidth] = useState(0);
|
|
372
|
+
const { rows, bands } = useMemo(() => orderRows(groups, lanes), [groups, lanes]);
|
|
158
373
|
const rowById = useMemo(() => new Map(rows.map((row) => [row.lane.id, row])), [rows]);
|
|
159
374
|
const markerById = useMemo(() => new Map(markers.map((marker) => [marker.id, marker])), [markers]);
|
|
160
375
|
const range = useMemo(() => {
|
|
161
376
|
if (rangeProp) return rangeProp;
|
|
162
|
-
const times = [...lanes.flatMap((lane) =>
|
|
377
|
+
const times = [...lanes.flatMap((lane) => (lane.segments ?? []).flatMap((segment) => [segment.start, segment.end])), ...markers.map((marker) => marker.timestamp)].filter((time) => time !== void 0).concat(now === void 0 ? [] : [now]);
|
|
163
378
|
const start = times.length > 0 ? Math.min(...times) : 0;
|
|
164
379
|
return {
|
|
165
380
|
start,
|
|
@@ -173,35 +388,72 @@ var GanttRoot = composable(({ lanes, markers = [], range: rangeProp, now, showNo
|
|
|
173
388
|
]);
|
|
174
389
|
return /* @__PURE__ */ jsx(GanttProvider, {
|
|
175
390
|
rows,
|
|
176
|
-
|
|
391
|
+
bands,
|
|
177
392
|
rowById,
|
|
178
393
|
markers,
|
|
179
394
|
markerById,
|
|
180
395
|
range,
|
|
181
396
|
now,
|
|
182
397
|
showNow,
|
|
398
|
+
axis,
|
|
399
|
+
unitStep,
|
|
400
|
+
animate,
|
|
183
401
|
onLaneSelect,
|
|
184
402
|
onMarkerSelect,
|
|
185
|
-
|
|
186
|
-
|
|
403
|
+
onAxisChange,
|
|
404
|
+
legend,
|
|
405
|
+
setLegend,
|
|
406
|
+
viewport,
|
|
407
|
+
setLegendWidth,
|
|
408
|
+
children: /* @__PURE__ */ jsx(ScrollArea.Root, {
|
|
409
|
+
...Util.composableProps(props, { classNames: "dx-expand text-xs font-mono" }),
|
|
410
|
+
orientation: "all",
|
|
411
|
+
trackStart: legendWidth,
|
|
187
412
|
ref: forwardedRef,
|
|
188
|
-
children
|
|
413
|
+
children: /* @__PURE__ */ jsx(ScrollArea.Viewport, {
|
|
414
|
+
classNames: "flex items-start",
|
|
415
|
+
ref: setViewport,
|
|
416
|
+
children
|
|
417
|
+
})
|
|
189
418
|
})
|
|
190
419
|
});
|
|
191
420
|
});
|
|
192
421
|
GanttRoot.displayName = "Gantt.Root";
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
422
|
+
/**
|
|
423
|
+
* The lane names, one per row, indented by depth; each row is the lane's keyboard path. Children go
|
|
424
|
+
* in the header row above the names, beside the chart's axis labels (e.g. `Gantt.AxisToggle`).
|
|
425
|
+
*
|
|
426
|
+
* The column shows each lane's title or its stats (`legend` on the root), and whichever it is not
|
|
427
|
+
* showing is in a hover card over the row, so neither costs a column of its own.
|
|
428
|
+
*/
|
|
429
|
+
var GanttLegend = Util.composable(({ children, ...props }, forwardedRef) => {
|
|
430
|
+
const { rows, legend, onLaneSelect, setLegendWidth } = useGanttContext("Gantt.Legend");
|
|
431
|
+
const legendRef = useRef(null);
|
|
432
|
+
const ref = useComposedRefs(forwardedRef, legendRef);
|
|
433
|
+
useEffect(() => {
|
|
434
|
+
const element = legendRef.current;
|
|
435
|
+
if (!element) return;
|
|
436
|
+
const observer = new ResizeObserver(() => setLegendWidth(element.offsetWidth));
|
|
437
|
+
observer.observe(element);
|
|
438
|
+
return () => {
|
|
439
|
+
observer.disconnect();
|
|
440
|
+
setLegendWidth(0);
|
|
441
|
+
};
|
|
442
|
+
}, [setLegendWidth]);
|
|
196
443
|
return /* @__PURE__ */ jsxs("div", {
|
|
197
|
-
...composableProps(props, { classNames: "shrink-0 w-[min(
|
|
198
|
-
ref
|
|
199
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
200
|
-
className:
|
|
444
|
+
...Util.composableProps(props, { classNames: "sticky left-0 z-[1] shrink-0 w-[min(15rem,20cqw)] min-w-40 flex flex-col font-sans bg-(--surface-bg)" }),
|
|
445
|
+
ref,
|
|
446
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
447
|
+
className: "flex items-center",
|
|
448
|
+
style: { height: HEADER_HEIGHT },
|
|
449
|
+
children
|
|
450
|
+
}), rows.map(({ lane, depth }) => /* @__PURE__ */ jsxs("div", {
|
|
451
|
+
className: mx("flex items-center gap-2 min-w-0", onLaneSelect && "cursor-pointer hover:bg-hover-surface-subtle"),
|
|
201
452
|
style: {
|
|
202
453
|
height: ROW_HEIGHT,
|
|
203
454
|
paddingInlineStart: `${.5 + depth}rem`
|
|
204
455
|
},
|
|
456
|
+
"data-testid": "gantt.legend.row",
|
|
205
457
|
...onLaneSelect && {
|
|
206
458
|
role: "button",
|
|
207
459
|
tabIndex: 0,
|
|
@@ -213,82 +465,354 @@ var GanttLegend = composable((props, forwardedRef) => {
|
|
|
213
465
|
}
|
|
214
466
|
}
|
|
215
467
|
},
|
|
216
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
217
|
-
className: "
|
|
218
|
-
|
|
468
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
469
|
+
className: mx("shrink-0 w-2 h-2 rounded-full bg-current", !lane.hue && STATUS_COLOR[lane.status].text),
|
|
470
|
+
style: lane.hue ? { color: hueColor(lane.hue) } : void 0
|
|
471
|
+
}), /* @__PURE__ */ jsx(GanttLaneLabel, {
|
|
472
|
+
lane,
|
|
473
|
+
legend
|
|
219
474
|
})]
|
|
220
475
|
}, lane.id))]
|
|
221
476
|
});
|
|
222
477
|
});
|
|
223
478
|
GanttLegend.displayName = "Gantt.Legend";
|
|
224
|
-
/**
|
|
225
|
-
var
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
children:
|
|
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))]
|
|
479
|
+
/** A legend row's text, and a hover card holding what the column is not showing. */
|
|
480
|
+
var GanttLaneLabel = ({ lane, legend }) => {
|
|
481
|
+
const meta = lane.meta ?? [];
|
|
482
|
+
const title = /* @__PURE__ */ jsx("span", {
|
|
483
|
+
className: "truncate text-fg",
|
|
484
|
+
"data-testid": "gantt.legend.title",
|
|
485
|
+
children: lane.label
|
|
238
486
|
});
|
|
239
|
-
|
|
240
|
-
|
|
487
|
+
const stats = /* @__PURE__ */ jsx("span", {
|
|
488
|
+
className: "grow flex gap-2 truncate text-fg-muted whitespace-nowrap tabular-nums",
|
|
489
|
+
"data-testid": "gantt.legend.stats",
|
|
490
|
+
children: meta.map(({ label }, index) => /* @__PURE__ */ jsx("span", { children: label }, index))
|
|
491
|
+
});
|
|
492
|
+
if (legend === "title" && meta.length === 0) return title;
|
|
493
|
+
return /* @__PURE__ */ jsxs(HoverCard.Root, { children: [/* @__PURE__ */ jsx(HoverCard.Trigger, {
|
|
494
|
+
asChild: true,
|
|
495
|
+
children: legend === "title" ? title : stats
|
|
496
|
+
}), /* @__PURE__ */ jsx(HoverCard.Content, {
|
|
497
|
+
classNames: "p-2 max-w-72 text-xs",
|
|
498
|
+
"data-testid": "gantt.legend.card",
|
|
499
|
+
children: legend === "title" ? meta.map(({ label, title }, index) => /* @__PURE__ */ jsxs("div", {
|
|
500
|
+
className: "tabular-nums",
|
|
501
|
+
children: [label, title && /* @__PURE__ */ jsx("span", {
|
|
502
|
+
className: "text-fg-muted",
|
|
503
|
+
children: ` · ${title}`
|
|
504
|
+
})]
|
|
505
|
+
}, index)) : /* @__PURE__ */ jsx("div", {
|
|
506
|
+
className: "font-medium",
|
|
507
|
+
children: lane.label
|
|
508
|
+
})
|
|
509
|
+
})] });
|
|
510
|
+
};
|
|
241
511
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
512
|
+
* Switches the chart between its axes: `time`, the run fitted to the width, and `unit`, one step per
|
|
513
|
+
* event. Renders nothing unless the root was given `onAxisChange`, since the axis is the host's state.
|
|
514
|
+
*/
|
|
515
|
+
var GanttAxisToggle = (_) => {
|
|
516
|
+
const { t } = Hooks.useTranslation(translationKey);
|
|
517
|
+
const { axis = "unit", onAxisChange } = useGanttContext("Gantt.AxisToggle");
|
|
518
|
+
if (!onAxisChange) return null;
|
|
519
|
+
return /* @__PURE__ */ jsx(Button.Root, {
|
|
520
|
+
variant: "ghost",
|
|
521
|
+
size: "sm",
|
|
522
|
+
iconSize: "xs",
|
|
523
|
+
iconOnly: true,
|
|
524
|
+
icon: axis === "time" ? "ph--clock--regular" : "ph--dots-three-outline--regular",
|
|
525
|
+
label: t(axis === "time" ? "gantt-axis-unit.label" : "gantt-axis-time.label"),
|
|
526
|
+
onClick: () => onAxisChange(axis === "time" ? "unit" : "time"),
|
|
527
|
+
"data-testid": "gantt.axisToggle"
|
|
528
|
+
});
|
|
529
|
+
};
|
|
530
|
+
GanttAxisToggle.displayName = "Gantt.AxisToggle";
|
|
531
|
+
/** Switches the legend's column between the lanes' titles and their stats. */
|
|
532
|
+
var GanttLegendToggle = (_) => {
|
|
533
|
+
const { t } = Hooks.useTranslation(translationKey);
|
|
534
|
+
const { legend, setLegend } = useGanttContext("Gantt.LegendToggle");
|
|
535
|
+
return /* @__PURE__ */ jsx(Button.Root, {
|
|
536
|
+
variant: "ghost",
|
|
537
|
+
size: "sm",
|
|
538
|
+
iconSize: "xs",
|
|
539
|
+
iconOnly: true,
|
|
540
|
+
icon: legend === "title" ? "ph--text-aa--regular" : "ph--chart-bar--regular",
|
|
541
|
+
label: t(legend === "title" ? "gantt-legend-stats.label" : "gantt-legend-title.label"),
|
|
542
|
+
onClick: () => setLegend(legend === "title" ? "stats" : "title"),
|
|
543
|
+
"data-testid": "gantt.legendToggle"
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
GanttLegendToggle.displayName = "Gantt.LegendToggle";
|
|
547
|
+
/**
|
|
548
|
+
* The drawing: a lane is a rounded bar per segment with its markers threaded through as nodes, a
|
|
549
|
+
* group's rectangle encloses its band, and connectors draw dependencies and the node a lane opened
|
|
550
|
+
* out of.
|
|
551
|
+
*
|
|
552
|
+
* On the `unit` axis the drawing is as wide as the events need and follows the newest of them in the
|
|
553
|
+
* root's scroll frame, while the legend beside it stays where it is.
|
|
554
|
+
*
|
|
555
|
+
* The drawing is one keyboard stop: clicking a node focuses it, the arrows walk the nodes (left and
|
|
556
|
+
* right along a lane, up and down to the nearest node of the next lane), Space opens the current
|
|
557
|
+
* node's card and Enter selects it — the same keys a legend row answers to.
|
|
244
558
|
*/
|
|
245
559
|
var GanttChart = forwardRef(({ classNames }, forwardedRef) => {
|
|
246
|
-
const { rows,
|
|
560
|
+
const { rows, bands, rowById, markers, markerById, range, now, showNow, axis = "unit", unitStep = UNIT_STEP, animate, onLaneSelect, onMarkerSelect, viewport } = useGanttContext("Gantt.Chart");
|
|
561
|
+
const { t } = Hooks.useTranslation(translationKey);
|
|
247
562
|
const svgRef = useRef(null);
|
|
248
|
-
const ref = useComposedRefs(
|
|
563
|
+
const ref = useComposedRefs(forwardedRef, svgRef);
|
|
564
|
+
const nodeIdPrefix = useId();
|
|
565
|
+
/** The node the keyboard is on, and the node whose card is open (by hover or by Space). */
|
|
566
|
+
const [currentId, setCurrentId] = useState();
|
|
567
|
+
const [openId, setOpenId] = useState();
|
|
568
|
+
const followRef = useRef(true);
|
|
569
|
+
const glideRef = useRef(false);
|
|
570
|
+
const frameRef = useRef(void 0);
|
|
571
|
+
const drawnRef = useRef(void 0);
|
|
249
572
|
const [width, setWidth] = useState(600);
|
|
250
573
|
useEffect(() => {
|
|
251
|
-
const element =
|
|
574
|
+
const element = viewport;
|
|
252
575
|
if (!element) return;
|
|
253
|
-
const
|
|
576
|
+
const measure = () => {
|
|
577
|
+
const svg = svgRef.current;
|
|
578
|
+
const offset = svg ? svg.getBoundingClientRect().left - element.getBoundingClientRect().left + element.scrollLeft : 0;
|
|
579
|
+
setWidth(Math.max(element.clientWidth - offset, 0));
|
|
580
|
+
};
|
|
581
|
+
const observer = new ResizeObserver(measure);
|
|
254
582
|
observer.observe(element);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
583
|
+
const onScroll = () => {
|
|
584
|
+
const atEdge = element.scrollWidth - element.clientWidth - element.scrollLeft <= FOLLOW_SLACK;
|
|
585
|
+
if (glideRef.current) {
|
|
586
|
+
glideRef.current = !atEdge;
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
followRef.current = atEdge;
|
|
590
|
+
};
|
|
591
|
+
element.addEventListener("scroll", onScroll, { passive: true });
|
|
592
|
+
return () => {
|
|
593
|
+
observer.disconnect();
|
|
594
|
+
element.removeEventListener("scroll", onScroll);
|
|
595
|
+
};
|
|
596
|
+
}, [viewport]);
|
|
597
|
+
/** Each lane's markers in the order they happened: the threads, the bars and the enter origins read it. */
|
|
598
|
+
const laneMarkers = useMemo(() => {
|
|
599
|
+
const byLane = /* @__PURE__ */ new Map();
|
|
600
|
+
for (const marker of markers) {
|
|
601
|
+
const list = byLane.get(marker.laneId);
|
|
602
|
+
if (list) list.push(marker);
|
|
603
|
+
else byLane.set(marker.laneId, [marker]);
|
|
604
|
+
}
|
|
605
|
+
for (const list of byLane.values()) list.sort((left, right) => left.timestamp - right.timestamp);
|
|
606
|
+
return byLane;
|
|
607
|
+
}, [markers]);
|
|
608
|
+
const lastEvent = useMemo(() => {
|
|
609
|
+
const times = [...markers.map((marker) => marker.timestamp), ...rows.flatMap(({ lane }) => (lane.segments ?? []).flatMap(({ start, end }) => [start, end ?? start]))];
|
|
610
|
+
return times.length > 0 ? Math.max(...times) : void 0;
|
|
611
|
+
}, [markers, rows]);
|
|
612
|
+
const shownRange = useRangeTween(useMemo(() => lastEvent === void 0 ? range : {
|
|
613
|
+
start: range.start,
|
|
614
|
+
end: Math.max(lastEvent, range.start)
|
|
615
|
+
}, [
|
|
616
|
+
range.start,
|
|
617
|
+
range.end,
|
|
618
|
+
lastEvent
|
|
619
|
+
]), axis === "time");
|
|
620
|
+
const slide = animate === true && axis === "unit";
|
|
621
|
+
const scale = useMemo(() => axis === "unit" ? unitScale({
|
|
622
|
+
times: markers.map((marker) => marker.timestamp),
|
|
623
|
+
step: unitStep,
|
|
624
|
+
pad: PAD_X
|
|
625
|
+
}) : timeScale({
|
|
626
|
+
range: shownRange,
|
|
627
|
+
width,
|
|
628
|
+
pad: PAD_X
|
|
629
|
+
}), [
|
|
630
|
+
axis,
|
|
631
|
+
markers,
|
|
632
|
+
unitStep,
|
|
633
|
+
shownRange,
|
|
634
|
+
width
|
|
635
|
+
]);
|
|
636
|
+
const isEntering = useEnter(markers.map((marker) => marker.id));
|
|
637
|
+
const edge = axis === "time" ? scale.at(shownRange.end) : void 0;
|
|
638
|
+
const x = (time) => edge === void 0 ? scale.at(time) : Math.min(scale.at(time), edge);
|
|
259
639
|
const rowY = (index) => HEADER_HEIGHT + index * ROW_HEIGHT + ROW_HEIGHT / 2;
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
640
|
+
const openSource = (lane) => lane.openedFrom && markerById.get(lane.openedFrom.markerId);
|
|
641
|
+
const markerX = /* @__PURE__ */ new Map();
|
|
642
|
+
const activeEdges = /* @__PURE__ */ new Set();
|
|
643
|
+
for (const [laneId, list] of laneMarkers) {
|
|
644
|
+
const row = rowById.get(laneId);
|
|
645
|
+
if (!row) continue;
|
|
646
|
+
const newest = list[list.length - 1];
|
|
647
|
+
if (row.lane.status === "running" && newest) activeEdges.add(newest.id);
|
|
648
|
+
list.forEach((marker, index) => {
|
|
649
|
+
const previous = index > 0 ? list[index - 1] : void 0;
|
|
650
|
+
const source = previous ? void 0 : openSource(row.lane);
|
|
651
|
+
const origin = previous ? x(previous.timestamp) : source && x(source.timestamp);
|
|
652
|
+
const resting = x(marker.timestamp);
|
|
653
|
+
markerX.set(marker.id, slide && origin !== void 0 && isEntering(marker.id) ? origin : resting);
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* A lane's drawn stretches, one per segment. An open segment reaches its last node, taking the
|
|
658
|
+
* node's drawn position rather than its instant so the bar grows with an arriving event instead of
|
|
659
|
+
* jumping ahead of it — and on to the chart's newest event when that is later.
|
|
660
|
+
*/
|
|
661
|
+
const stretchesOf = (lane) => {
|
|
662
|
+
const list = laneMarkers.get(lane.id) ?? [];
|
|
663
|
+
const segments = lane.segments ?? [];
|
|
664
|
+
return segments.map((segment, index) => {
|
|
665
|
+
const upper = segment.end ?? segments[index + 1]?.start ?? Number.MAX_SAFE_INTEGER;
|
|
666
|
+
const inside = list.filter((marker) => marker.timestamp >= segment.start && marker.timestamp <= upper);
|
|
667
|
+
const first = inside[0];
|
|
668
|
+
const last = inside[inside.length - 1];
|
|
669
|
+
const from = x(segment.start);
|
|
670
|
+
const lastX = last ? markerX.get(last.id) ?? x(last.timestamp) : void 0;
|
|
671
|
+
const edgeX = lastEvent !== void 0 ? x(lastEvent) : void 0;
|
|
672
|
+
const to = segment.end !== void 0 ? x(segment.end) : lastX !== void 0 && edgeX !== void 0 ? Math.max(lastX, edgeX) : lastX ?? edgeX ?? from;
|
|
673
|
+
const threaded = inside.length > 1 && first && last;
|
|
674
|
+
return {
|
|
675
|
+
from,
|
|
676
|
+
to: Math.max(to, from),
|
|
677
|
+
threadFrom: threaded ? markerX.get(first.id) : void 0,
|
|
678
|
+
threadTo: threaded ? markerX.get(last.id) : void 0
|
|
679
|
+
};
|
|
680
|
+
});
|
|
264
681
|
};
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const edge = x(start) - BAR_OVERHANG;
|
|
269
|
-
return source ? Math.max(edge, x(source.timestamp) + 2 * BEND_RADIUS) : edge;
|
|
682
|
+
const isOpening = (lane) => {
|
|
683
|
+
const first = laneMarkers.get(lane.id)?.[0];
|
|
684
|
+
return slide && first !== void 0 && isEntering(first.id);
|
|
270
685
|
};
|
|
271
|
-
const nodeX = (lane, time) => lane.start === void 0 ? x(time) : Math.max(x(time), barStart(lane, lane.start) + BAR_OVERHANG);
|
|
272
686
|
const height = HEADER_HEIGHT + rows.length * ROW_HEIGHT;
|
|
273
|
-
const
|
|
274
|
-
const
|
|
687
|
+
const grow = slide && mx("transition-[width]", ENTER_TRANSITION);
|
|
688
|
+
const nodeIds = useMemo(() => new Map(markers.map((marker, index) => [marker.id, `${nodeIdPrefix}-node-${index}`])), [markers, nodeIdPrefix]);
|
|
689
|
+
const current = currentId === void 0 ? void 0 : markerById.get(currentId);
|
|
690
|
+
/** The node an arrow key moves to from `from`; `from` itself where there is nowhere further to go. */
|
|
691
|
+
const neighbour = (from, key) => {
|
|
692
|
+
if (key === "ArrowLeft" || key === "ArrowRight") {
|
|
693
|
+
const list = laneMarkers.get(from.laneId) ?? [];
|
|
694
|
+
return list[list.findIndex((marker) => marker.id === from.id) + (key === "ArrowLeft" ? -1 : 1)] ?? from;
|
|
695
|
+
}
|
|
696
|
+
const row = rowById.get(from.laneId);
|
|
697
|
+
if (!row) return from;
|
|
698
|
+
const fromX = markerX.get(from.id) ?? x(from.timestamp);
|
|
699
|
+
const distance = (marker) => Math.abs((markerX.get(marker.id) ?? x(marker.timestamp)) - fromX);
|
|
700
|
+
const direction = key === "ArrowUp" ? -1 : 1;
|
|
701
|
+
for (let index = row.index + direction; index >= 0 && index < rows.length; index += direction) {
|
|
702
|
+
const list = laneMarkers.get(rows[index].lane.id) ?? [];
|
|
703
|
+
if (list.length > 0) return list.reduce((best, marker) => distance(marker) < distance(best) ? marker : best);
|
|
704
|
+
}
|
|
705
|
+
return from;
|
|
706
|
+
};
|
|
707
|
+
/** Scrolls the frame just enough to show `marker` clear of the sticky legend and the frame's edges. */
|
|
708
|
+
const reveal = (marker) => {
|
|
709
|
+
const svg = svgRef.current;
|
|
710
|
+
const row = rowById.get(marker.laneId);
|
|
711
|
+
const cx = markerX.get(marker.id);
|
|
712
|
+
if (!viewport || !svg || !row || cx === void 0) return;
|
|
713
|
+
const frame = viewport.getBoundingClientRect();
|
|
714
|
+
const drawing = svg.getBoundingClientRect();
|
|
715
|
+
const legendWidth = drawing.left - frame.left + viewport.scrollLeft;
|
|
716
|
+
const nodeX = drawing.left - frame.left + cx;
|
|
717
|
+
const nodeY = drawing.top - frame.top + rowY(row.index);
|
|
718
|
+
const margin = ROW_HEIGHT;
|
|
719
|
+
const left = nodeX - margin < legendWidth ? nodeX - margin - legendWidth : nodeX + margin > viewport.clientWidth ? nodeX + margin - viewport.clientWidth : 0;
|
|
720
|
+
const top = nodeY - margin < 0 ? nodeY - margin : nodeY + margin > viewport.clientHeight ? nodeY + margin - viewport.clientHeight : 0;
|
|
721
|
+
if (left !== 0 || top !== 0) viewport.scrollBy({
|
|
722
|
+
left,
|
|
723
|
+
top
|
|
724
|
+
});
|
|
725
|
+
};
|
|
726
|
+
const moveTo = (marker) => {
|
|
727
|
+
setCurrentId(marker.id);
|
|
728
|
+
setOpenId((open) => open === void 0 ? void 0 : marker.id);
|
|
729
|
+
reveal(marker);
|
|
730
|
+
};
|
|
731
|
+
const handleKeyDown = (event) => {
|
|
732
|
+
switch (event.key) {
|
|
733
|
+
case "ArrowLeft":
|
|
734
|
+
case "ArrowRight":
|
|
735
|
+
case "ArrowUp":
|
|
736
|
+
case "ArrowDown": {
|
|
737
|
+
event.preventDefault();
|
|
738
|
+
const next = current ? neighbour(current, event.key) : rows.map(({ lane }) => laneMarkers.get(lane.id)?.at(-1)).find((marker) => marker !== void 0);
|
|
739
|
+
if (next) moveTo(next);
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
case " ":
|
|
743
|
+
event.preventDefault();
|
|
744
|
+
if (current) setOpenId((open) => open === current.id ? void 0 : current.id);
|
|
745
|
+
break;
|
|
746
|
+
case "Escape":
|
|
747
|
+
if (openId !== void 0) {
|
|
748
|
+
event.preventDefault();
|
|
749
|
+
setOpenId(void 0);
|
|
750
|
+
}
|
|
751
|
+
break;
|
|
752
|
+
case "Enter": if (current) {
|
|
753
|
+
event.preventDefault();
|
|
754
|
+
onMarkerSelect?.(current);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
useLayoutEffect(() => {
|
|
759
|
+
const element = viewport;
|
|
760
|
+
if (!element) return;
|
|
761
|
+
const previous = drawnRef.current;
|
|
762
|
+
drawnRef.current = scale.width;
|
|
763
|
+
const grown = previous !== void 0 && scale.width > previous;
|
|
764
|
+
if (previous === void 0 || grown && previous <= width) {
|
|
765
|
+
element.scrollLeft = element.scrollWidth;
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (!grown || !followRef.current) return;
|
|
769
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
|
|
770
|
+
element.scrollLeft = element.scrollWidth;
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const from = element.scrollLeft;
|
|
774
|
+
const to = element.scrollWidth - element.clientWidth;
|
|
775
|
+
if (to <= from) return;
|
|
776
|
+
const started = performance.now();
|
|
777
|
+
glideRef.current = true;
|
|
778
|
+
const step = (now) => {
|
|
779
|
+
const progress = Math.min((now - started) / FOLLOW_DURATION, 1);
|
|
780
|
+
element.scrollLeft = from + (to - from) * (1 - (1 - progress) ** 3);
|
|
781
|
+
if (progress < 1) frameRef.current = requestAnimationFrame(step);
|
|
782
|
+
else glideRef.current = false;
|
|
783
|
+
};
|
|
784
|
+
frameRef.current = requestAnimationFrame(step);
|
|
785
|
+
return () => {
|
|
786
|
+
if (frameRef.current !== void 0) cancelAnimationFrame(frameRef.current);
|
|
787
|
+
glideRef.current = false;
|
|
788
|
+
};
|
|
789
|
+
}, [scale.width, viewport]);
|
|
275
790
|
return /* @__PURE__ */ jsxs("svg", {
|
|
276
|
-
className: mx("
|
|
791
|
+
className: mx("grow shrink-0 dx-focus-ring", classNames),
|
|
792
|
+
style: {
|
|
793
|
+
width: scale.width,
|
|
794
|
+
height
|
|
795
|
+
},
|
|
796
|
+
tabIndex: 0,
|
|
797
|
+
role: "group",
|
|
798
|
+
"aria-label": t("gantt-chart.label"),
|
|
799
|
+
"aria-activedescendant": current ? nodeIds.get(current.id) : void 0,
|
|
800
|
+
onKeyDown: handleKeyDown,
|
|
801
|
+
"data-testid": "gantt.chart",
|
|
277
802
|
ref,
|
|
278
|
-
style: { height },
|
|
279
803
|
children: [
|
|
280
|
-
ticks.map((
|
|
281
|
-
x1:
|
|
282
|
-
x2:
|
|
804
|
+
scale.ticks.map(({ at, label }, index) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
|
|
805
|
+
x1: at,
|
|
806
|
+
x2: at,
|
|
283
807
|
y1: HEADER_HEIGHT,
|
|
284
808
|
y2: height,
|
|
285
809
|
className: "stroke-separator"
|
|
286
810
|
}), /* @__PURE__ */ jsx("text", {
|
|
287
|
-
x:
|
|
811
|
+
x: at,
|
|
288
812
|
y: 14,
|
|
289
|
-
textAnchor:
|
|
290
|
-
className: "fill-current text-
|
|
291
|
-
children:
|
|
813
|
+
textAnchor: at <= PAD_X ? "start" : at >= scale.width - PAD_X ? "end" : "middle",
|
|
814
|
+
className: "fill-current text-fg-subtle",
|
|
815
|
+
children: label
|
|
292
816
|
})] }, index)),
|
|
293
817
|
now !== void 0 && showNow && /* @__PURE__ */ jsx("line", {
|
|
294
818
|
x1: x(now),
|
|
@@ -298,30 +822,27 @@ var GanttChart = forwardRef(({ classNames }, forwardedRef) => {
|
|
|
298
822
|
strokeDasharray: "3 3",
|
|
299
823
|
className: STATUS_COLOR.running.thread
|
|
300
824
|
}),
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
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;
|
|
825
|
+
bands.flatMap(({ group, first, last }) => {
|
|
826
|
+
const stretches = rows.slice(first, last + 1).flatMap((row) => stretchesOf(row.lane));
|
|
827
|
+
if (stretches.length === 0) return [];
|
|
828
|
+
const left = Math.min(...stretches.map((stretch) => stretch.from)) - BAR_OVERHANG - GROUP_PAD;
|
|
829
|
+
const right = Math.max(...stretches.map((stretch) => stretch.to)) + BAR_OVERHANG + GROUP_PAD;
|
|
311
830
|
return [/* @__PURE__ */ jsx("rect", {
|
|
312
831
|
x: left,
|
|
313
832
|
y: rowY(first) - ROW_HEIGHT / 2 + GROUP_INSET,
|
|
314
833
|
width: Math.max(right - left, ROW_HEIGHT),
|
|
315
834
|
height: (last - first + 1) * ROW_HEIGHT - 4,
|
|
316
835
|
rx: GROUP_RADIUS,
|
|
317
|
-
className: "fill-input-surface"
|
|
318
|
-
}, `
|
|
836
|
+
className: mx("fill-input-surface", grow)
|
|
837
|
+
}, `band:${group.id}`)];
|
|
319
838
|
}),
|
|
320
839
|
rows.flatMap(({ lane, index }) => (lane.blockedOn ?? []).flatMap((depId) => {
|
|
321
840
|
const dep = rowById.get(depId);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
841
|
+
if (!dep) return [];
|
|
842
|
+
const stretches = stretchesOf(dep.lane);
|
|
843
|
+
const anchor = stretches[stretches.length - 1];
|
|
844
|
+
if (!anchor) return [];
|
|
845
|
+
const anchorX = anchor.to;
|
|
325
846
|
return [/* @__PURE__ */ jsx("line", {
|
|
326
847
|
x1: anchorX,
|
|
327
848
|
x2: anchorX,
|
|
@@ -329,35 +850,55 @@ var GanttChart = forwardRef(({ classNames }, forwardedRef) => {
|
|
|
329
850
|
y2: rowY(index),
|
|
330
851
|
strokeDasharray: "2 2",
|
|
331
852
|
className: STATUS_COLOR.blocked.thread
|
|
332
|
-
}, `${lane.id}:${depId}`), ...lane.
|
|
853
|
+
}, `${lane.id}:${depId}`), ...(lane.segments ?? []).length === 0 ? [/* @__PURE__ */ jsx("circle", {
|
|
333
854
|
cx: anchorX,
|
|
334
855
|
cy: rowY(index),
|
|
335
856
|
r: NODE_RADIUS,
|
|
336
|
-
strokeDasharray: "2 2",
|
|
337
857
|
className: mx("fill-base-surface", STATUS_COLOR.blocked.thread)
|
|
338
858
|
}, `${lane.id}:${depId}:hold`)] : []];
|
|
339
859
|
})),
|
|
340
860
|
rows.flatMap(({ lane, index }) => {
|
|
341
|
-
const source =
|
|
342
|
-
const sourceRow = lane.
|
|
343
|
-
|
|
861
|
+
const source = openSource(lane);
|
|
862
|
+
const sourceRow = lane.openedFrom && rowById.get(lane.openedFrom.laneId);
|
|
863
|
+
const stretches = stretchesOf(lane);
|
|
864
|
+
if (!source || !sourceRow || stretches.length === 0) return [];
|
|
344
865
|
const sourceX = x(source.timestamp);
|
|
345
866
|
const y = rowY(index);
|
|
346
|
-
const
|
|
347
|
-
const
|
|
867
|
+
const targetX = stretches[0].from;
|
|
868
|
+
const path = targetX > sourceX + BEND_RADIUS ? `M ${sourceX} ${rowY(sourceRow.index)} V ${y - BEND_RADIUS} Q ${sourceX} ${y} ${sourceX + BEND_RADIUS} ${y} H ${targetX}` : `M ${sourceX} ${rowY(sourceRow.index)} V ${y}`;
|
|
348
869
|
return [/* @__PURE__ */ jsx("path", {
|
|
349
|
-
d:
|
|
870
|
+
d: path,
|
|
350
871
|
fill: "none",
|
|
351
|
-
|
|
352
|
-
|
|
872
|
+
pathLength: 1,
|
|
873
|
+
strokeDasharray: 1,
|
|
874
|
+
strokeDashoffset: isOpening(lane) ? 1 : 0,
|
|
875
|
+
className: mx(animate && mx("transition-[stroke-dashoffset]", ENTER_TRANSITION), CONNECTOR_CLASSNAME)
|
|
876
|
+
}, `opened:${lane.id}`)];
|
|
353
877
|
}),
|
|
354
|
-
rows.
|
|
355
|
-
const
|
|
356
|
-
|
|
878
|
+
rows.flatMap(({ lane, index }) => {
|
|
879
|
+
const target = lane.closedInto && markerById.get(lane.closedInto.markerId);
|
|
880
|
+
const targetRow = lane.closedInto && rowById.get(lane.closedInto.laneId);
|
|
881
|
+
const stretches = stretchesOf(lane);
|
|
882
|
+
const last = stretches[stretches.length - 1];
|
|
883
|
+
if (!target || !targetRow || !last) return [];
|
|
884
|
+
const targetX = x(target.timestamp);
|
|
885
|
+
const y = rowY(index);
|
|
886
|
+
const targetY = rowY(targetRow.index);
|
|
887
|
+
const rise = Math.sign(targetY - y) * BEND_RADIUS;
|
|
888
|
+
const path = targetX > last.to + BEND_RADIUS ? `M ${last.to} ${y} H ${targetX - BEND_RADIUS} Q ${targetX} ${y} ${targetX} ${y + rise} V ${targetY}` : `M ${targetX} ${y} V ${targetY}`;
|
|
889
|
+
return [/* @__PURE__ */ jsx("path", {
|
|
890
|
+
d: path,
|
|
891
|
+
fill: "none",
|
|
892
|
+
strokeDasharray: "3 2",
|
|
893
|
+
className: CONNECTOR_CLASSNAME
|
|
894
|
+
}, `closed:${lane.id}`)];
|
|
895
|
+
}),
|
|
896
|
+
rows.flatMap(({ lane, index }) => stretchesOf(lane).map((stretch, segment) => {
|
|
897
|
+
const start = stretch.from - BAR_OVERHANG;
|
|
357
898
|
const bar = {
|
|
358
|
-
x:
|
|
899
|
+
x: start,
|
|
359
900
|
y: rowY(index) - BAR_HEIGHT / 2,
|
|
360
|
-
width: Math.max(
|
|
901
|
+
width: isOpening(lane) ? BAR_HEIGHT : Math.max(stretch.to + BAR_OVERHANG - start, BAR_HEIGHT),
|
|
361
902
|
height: BAR_HEIGHT,
|
|
362
903
|
rx: BAR_HEIGHT / 2
|
|
363
904
|
};
|
|
@@ -366,61 +907,104 @@ var GanttChart = forwardRef(({ classNames }, forwardedRef) => {
|
|
|
366
907
|
onClick: () => onLaneSelect?.(lane),
|
|
367
908
|
children: [/* @__PURE__ */ jsx("rect", {
|
|
368
909
|
...bar,
|
|
369
|
-
className: "fill-base-surface"
|
|
370
|
-
}), /* @__PURE__ */ jsx("rect", {
|
|
910
|
+
className: mx("fill-base-surface", grow)
|
|
911
|
+
}), lane.hue ? /* @__PURE__ */ jsx("rect", {
|
|
371
912
|
...bar,
|
|
372
|
-
className:
|
|
913
|
+
className: mx(grow),
|
|
914
|
+
style: {
|
|
915
|
+
fill: hueColor(lane.hue),
|
|
916
|
+
fillOpacity: .4
|
|
917
|
+
}
|
|
918
|
+
}) : /* @__PURE__ */ jsx("rect", {
|
|
919
|
+
...bar,
|
|
920
|
+
className: mx(STATUS_COLOR[lane.status].fill, grow)
|
|
373
921
|
})]
|
|
374
|
-
}, lane.id);
|
|
375
|
-
}),
|
|
376
|
-
rows.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
922
|
+
}, `${lane.id}:${segment}`);
|
|
923
|
+
})),
|
|
924
|
+
rows.flatMap(({ lane, index }) => stretchesOf(lane).flatMap((stretch, segment) => stretch.threadFrom === void 0 || stretch.threadTo === void 0 ? [] : [/* @__PURE__ */ jsx("rect", {
|
|
925
|
+
x: stretch.threadFrom,
|
|
926
|
+
y: rowY(index) - THREAD_HEIGHT / 2,
|
|
927
|
+
width: Math.max(stretch.threadTo - stretch.threadFrom, 0),
|
|
928
|
+
height: THREAD_HEIGHT,
|
|
929
|
+
className: mx(slide && mx("transition-[x,width]", ENTER_TRANSITION), !lane.hue && STATUS_COLOR[lane.status].node),
|
|
930
|
+
style: lane.hue ? { fill: hueColor(lane.hue) } : void 0
|
|
931
|
+
}, `thread:${lane.id}:${segment}`)])),
|
|
932
|
+
markers.flatMap((marker) => {
|
|
933
|
+
const row = marker.wait && rowById.get(marker.laneId);
|
|
934
|
+
const from = markerX.get(marker.id);
|
|
935
|
+
const to = marker.wait && markerX.get(marker.wait.until);
|
|
936
|
+
if (!row || from === void 0 || to === void 0 || to <= from) return [];
|
|
937
|
+
return [/* @__PURE__ */ jsx("line", {
|
|
938
|
+
x1: from,
|
|
939
|
+
x2: to,
|
|
940
|
+
y1: rowY(row.index),
|
|
941
|
+
y2: rowY(row.index),
|
|
942
|
+
strokeWidth: 2,
|
|
943
|
+
strokeDasharray: "4 3",
|
|
944
|
+
className: row.lane.hue ? void 0 : STATUS_COLOR[row.lane.status].thread,
|
|
945
|
+
style: row.lane.hue ? { stroke: hueStrongColor(row.lane.hue) } : void 0
|
|
946
|
+
}, `wait:${marker.id}`)];
|
|
386
947
|
}),
|
|
387
948
|
markers.map((marker) => {
|
|
388
949
|
const row = rowById.get(marker.laneId);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
950
|
+
const cx = markerX.get(marker.id);
|
|
951
|
+
return row && cx !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [marker.pending && /* @__PURE__ */ jsx("circle", {
|
|
952
|
+
cx,
|
|
953
|
+
cy: rowY(row.index),
|
|
954
|
+
r: NODE_RADIUS,
|
|
955
|
+
className: "animate-ping transform-fill origin-center pointer-events-none fill-error-500"
|
|
956
|
+
}), /* @__PURE__ */ jsxs(HoverCard.Root, {
|
|
957
|
+
open: openId === marker.id,
|
|
958
|
+
onOpenChange: ({ open }) => setOpenId((previous) => open ? marker.id : previous === marker.id ? void 0 : previous),
|
|
959
|
+
children: [/* @__PURE__ */ jsx(HoverCard.Trigger, {
|
|
960
|
+
asChild: true,
|
|
961
|
+
children: /* @__PURE__ */ jsx("circle", {
|
|
962
|
+
id: nodeIds.get(marker.id),
|
|
963
|
+
role: "img",
|
|
964
|
+
"aria-label": marker.label,
|
|
965
|
+
"data-marker-id": marker.id,
|
|
966
|
+
"data-lane-id": marker.laneId,
|
|
967
|
+
cx,
|
|
968
|
+
cy: rowY(row.index),
|
|
969
|
+
r: NODE_RADIUS,
|
|
970
|
+
className: mx("cursor-pointer hover:stroke-[3px] hover:stroke-fg", isAlert(marker) ? "stroke-red-500 stroke-2" : "stroke-base-surface", marker.level === "error" && row.lane.status === "failed" && "animate-pulse", slide ? mx("transition-[stroke-width,cx]", ENTER_TRANSITION) : "transition-[stroke-width]", activeEdges.has(marker.id) ? mx("animate-pulse", !row.lane.hue && STATUS_COLOR[row.lane.status].edge) : !row.lane.hue && (isAlert(marker) ? STATUS_COLOR[row.lane.status].edge : STATUS_COLOR[row.lane.status].node), marker.id === currentId && "stroke-[3px] stroke-fg"),
|
|
971
|
+
style: row.lane.hue ? { fill: isAlert(marker) ? hueStrongColor(row.lane.hue) : hueColor(row.lane.hue) } : void 0,
|
|
972
|
+
onClick: () => {
|
|
973
|
+
setCurrentId(marker.id);
|
|
974
|
+
svgRef.current?.focus({ preventScroll: true });
|
|
975
|
+
onMarkerSelect?.(marker);
|
|
976
|
+
}
|
|
977
|
+
})
|
|
978
|
+
}), /* @__PURE__ */ jsxs(HoverCard.Content, {
|
|
979
|
+
classNames: "p-2 max-w-72 text-xs",
|
|
980
|
+
"data-testid": "gantt.markerCard",
|
|
981
|
+
children: [
|
|
982
|
+
/* @__PURE__ */ jsx("div", {
|
|
983
|
+
className: "font-medium truncate",
|
|
984
|
+
children: marker.label
|
|
985
|
+
}),
|
|
986
|
+
/* @__PURE__ */ jsx("div", {
|
|
987
|
+
className: "text-fg-muted truncate",
|
|
988
|
+
children: row.lane.label
|
|
989
|
+
}),
|
|
990
|
+
/* @__PURE__ */ jsxs("div", {
|
|
991
|
+
className: "text-fg-muted tabular-nums",
|
|
992
|
+
children: [
|
|
993
|
+
marker.kind && `${marker.kind} · `,
|
|
994
|
+
format(marker.timestamp, "HH:mm:ss"),
|
|
995
|
+
` · +${formatElapsed(marker.timestamp - range.start, {
|
|
996
|
+
span: marker.timestamp - range.start,
|
|
997
|
+
step: 1e3
|
|
998
|
+
})}`,
|
|
999
|
+
marker.level && marker.level !== "info" && /* @__PURE__ */ jsx("span", {
|
|
1000
|
+
className: mx("ms-2", marker.level === "error" ? "text-error-text" : "text-warning-text"),
|
|
1001
|
+
children: marker.level
|
|
1002
|
+
})
|
|
1003
|
+
]
|
|
1004
|
+
})
|
|
1005
|
+
]
|
|
1006
|
+
})]
|
|
1007
|
+
})] }, marker.id) : null;
|
|
424
1008
|
})
|
|
425
1009
|
]
|
|
426
1010
|
});
|
|
@@ -429,8 +1013,9 @@ GanttChart.displayName = "Gantt.Chart";
|
|
|
429
1013
|
var Gantt = {
|
|
430
1014
|
Root: GanttRoot,
|
|
431
1015
|
Legend: GanttLegend,
|
|
432
|
-
|
|
433
|
-
|
|
1016
|
+
AxisToggle: GanttAxisToggle,
|
|
1017
|
+
LegendToggle: GanttLegendToggle,
|
|
1018
|
+
Chart: GanttChart
|
|
434
1019
|
};
|
|
435
1020
|
//#endregion
|
|
436
1021
|
//#region src/components/ProcessTree/ProcessTree.tsx
|
|
@@ -449,7 +1034,7 @@ var NO_SELECTION$1 = [];
|
|
|
449
1034
|
* in trailing columns. The forest is rebuilt on every metrics tick, so open state lives outside the
|
|
450
1035
|
* model and is re-seeded — a collapse must survive the next tick. Renders through `Tree`.
|
|
451
1036
|
*/
|
|
452
|
-
var ProcessTree = React.memo(composable(({ processes, depth = DEFAULT_DEPTH, resolveLabel, selected = NO_SELECTION$1, onSelectedChange, onProcessTerminate, ...props }, forwardedRef) => {
|
|
1037
|
+
var ProcessTree = React.memo(Util.composable(({ processes, depth = DEFAULT_DEPTH, resolveLabel, selected = NO_SELECTION$1, onSelectedChange, onProcessTerminate, ...props }, forwardedRef) => {
|
|
453
1038
|
const openRef = useRef(/* @__PURE__ */ new Map());
|
|
454
1039
|
const registry = useContext(RegistryContext);
|
|
455
1040
|
const root = useMemo(() => buildProcessForest(processes, depth), [processes, depth]);
|
|
@@ -476,58 +1061,64 @@ var ProcessTree = React.memo(composable(({ processes, depth = DEFAULT_DEPTH, res
|
|
|
476
1061
|
const pid = item.process.pid.toString();
|
|
477
1062
|
onSelectedChange?.(!meta ? [pid] : current ? [...selected, pid] : selected.filter((candidate) => candidate !== pid));
|
|
478
1063
|
}, [selected, onSelectedChange]);
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
1064
|
+
const renderRow = useCallback((node) => /* @__PURE__ */ jsx(ProcessRow, {
|
|
1065
|
+
node,
|
|
1066
|
+
onProcessTerminate
|
|
1067
|
+
}), [onProcessTerminate]);
|
|
1068
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1069
|
+
...Util.composableProps(props, { classNames: "flex flex-col min-h-0" }),
|
|
484
1070
|
ref: forwardedRef,
|
|
485
|
-
children: /* @__PURE__ */ jsx(
|
|
1071
|
+
children: /* @__PURE__ */ jsx(Tree.Root, {
|
|
486
1072
|
id: ROOT_ID,
|
|
487
1073
|
model,
|
|
488
|
-
|
|
1074
|
+
virtual: "fixed",
|
|
1075
|
+
size: "sm",
|
|
489
1076
|
selectionMode: "multiple",
|
|
490
|
-
|
|
491
|
-
gridTemplateColumns: "[tree-row-start] var(--dx-control) minmax(0, 1fr) min-content min-content [tree-row-end]",
|
|
492
|
-
renderIcon,
|
|
493
|
-
renderColumns,
|
|
1077
|
+
columns: COLUMNS,
|
|
494
1078
|
onOpenChange: handleOpenChange,
|
|
495
|
-
onSelect: handleSelect
|
|
496
|
-
|
|
1079
|
+
onSelect: handleSelect,
|
|
1080
|
+
children: /* @__PURE__ */ jsx(Tree.Content, { children: renderRow })
|
|
1081
|
+
})
|
|
497
1082
|
});
|
|
498
1083
|
}));
|
|
499
|
-
/**
|
|
500
|
-
var
|
|
1084
|
+
/** Disclosure, status glyph, label, then the elapsed time and the terminate control. */
|
|
1085
|
+
var COLUMNS = "var(--dx-half-block-size) var(--dx-block-size) minmax(0, 1fr) min-content min-content";
|
|
1086
|
+
/** One process: its status glyph (animated, coloured and tooltipped per state), elapsed time and terminate control. */
|
|
1087
|
+
var ProcessRow = ({ node, onProcessTerminate }) => {
|
|
1088
|
+
const process = node.item?.process;
|
|
1089
|
+
return /* @__PURE__ */ jsxs(Tree.Item, {
|
|
1090
|
+
node,
|
|
1091
|
+
children: [
|
|
1092
|
+
/* @__PURE__ */ jsx(Tree.ItemIndicator, {}),
|
|
1093
|
+
/* @__PURE__ */ jsx(Tree.ItemIcon, { children: process && /* @__PURE__ */ jsx(StatusIcon, { process }) }),
|
|
1094
|
+
/* @__PURE__ */ jsx(Tree.ItemText, {}),
|
|
1095
|
+
/* @__PURE__ */ jsx("span", {
|
|
1096
|
+
className: "text-end ps-1 text-xs text-fg-muted tabular-nums whitespace-nowrap",
|
|
1097
|
+
children: process && [Process.State.FAILED, Process.State.SUCCEEDED].includes(process.state) && Unit.Duration(process.metrics.wallTime).toString()
|
|
1098
|
+
}),
|
|
1099
|
+
/* @__PURE__ */ jsx("span", { children: process && onProcessTerminate && process.state !== Process.State.TERMINATED && /* @__PURE__ */ jsx(Button.Root, {
|
|
1100
|
+
icon: "ph--x--regular",
|
|
1101
|
+
iconOnly: true,
|
|
1102
|
+
size: "sm",
|
|
1103
|
+
variant: "ghost",
|
|
1104
|
+
label: "Actions",
|
|
1105
|
+
onClick: (event) => {
|
|
1106
|
+
event.stopPropagation();
|
|
1107
|
+
onProcessTerminate(process);
|
|
1108
|
+
}
|
|
1109
|
+
}) })
|
|
1110
|
+
]
|
|
1111
|
+
});
|
|
1112
|
+
};
|
|
1113
|
+
var StatusIcon = ({ process }) => /* @__PURE__ */ jsx(Tooltip.Trigger, {
|
|
501
1114
|
content: process.state.toString(),
|
|
502
|
-
children: /* @__PURE__ */ jsx(Icon, {
|
|
503
|
-
size:
|
|
504
|
-
|
|
505
|
-
|
|
1115
|
+
children: /* @__PURE__ */ jsx(Icon.Icon, {
|
|
1116
|
+
size: "md",
|
|
1117
|
+
spin: process.state === Process.State.RUNNING,
|
|
1118
|
+
valence: process.state === Process.State.FAILED ? "error" : process.state === Process.State.SUCCEEDED ? "success" : void 0,
|
|
506
1119
|
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
1120
|
})
|
|
508
1121
|
});
|
|
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
1122
|
var sortProcesses = (processes) => {
|
|
532
1123
|
return [
|
|
533
1124
|
...processes.filter((process) => [Process.State.RUNNING, Process.State.HYBERNATING].includes(process.state)),
|
|
@@ -579,6 +1170,103 @@ var buildProcessForest = (processes, maxDepth) => {
|
|
|
579
1170
|
};
|
|
580
1171
|
};
|
|
581
1172
|
//#endregion
|
|
1173
|
+
//#region src/components/Timeline/timeline-layout.ts
|
|
1174
|
+
/**
|
|
1175
|
+
* Lays a commit graph out into rows: which lane each branch draws in, how far each branch's line
|
|
1176
|
+
* runs, and which commits get a row at all.
|
|
1177
|
+
*
|
|
1178
|
+
* Pure and linear in the number of commits: every lookup a commit makes about a parent, and every
|
|
1179
|
+
* lane release, is served from an index built in one pass. Commits are assumed to be in topological
|
|
1180
|
+
* order — a parent always precedes its children.
|
|
1181
|
+
*/
|
|
1182
|
+
var layoutTimeline = (commits, branches) => {
|
|
1183
|
+
const visibleBranches = new Set(branches);
|
|
1184
|
+
const firstBranch = branches[0];
|
|
1185
|
+
const commitIndexById = /* @__PURE__ */ new Map();
|
|
1186
|
+
commits.forEach((commit, index) => {
|
|
1187
|
+
if (!commitIndexById.has(commit.id)) commitIndexById.set(commit.id, index);
|
|
1188
|
+
});
|
|
1189
|
+
const positionOf = (commitId) => {
|
|
1190
|
+
const index = commitIndexById.get(commitId);
|
|
1191
|
+
return index === void 0 ? void 0 : {
|
|
1192
|
+
index,
|
|
1193
|
+
branch: commits[index].branch
|
|
1194
|
+
};
|
|
1195
|
+
};
|
|
1196
|
+
const mergeRow = /* @__PURE__ */ new Map();
|
|
1197
|
+
commits.forEach((commit, row) => {
|
|
1198
|
+
for (const parentId of commit.parents ?? []) {
|
|
1199
|
+
const parent = positionOf(parentId);
|
|
1200
|
+
if (parent && parent.branch !== commit.branch && visibleBranches.has(parent.branch)) mergeRow.set(parent.branch, row);
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
const releasedAtRow = /* @__PURE__ */ new Map();
|
|
1204
|
+
for (const [branch, endRow] of mergeRow) {
|
|
1205
|
+
if (branch === firstBranch) continue;
|
|
1206
|
+
const released = releasedAtRow.get(endRow);
|
|
1207
|
+
if (released) released.push(branch);
|
|
1208
|
+
else releasedAtRow.set(endRow, [branch]);
|
|
1209
|
+
}
|
|
1210
|
+
const branchLane = /* @__PURE__ */ new Map();
|
|
1211
|
+
if (firstBranch !== void 0) branchLane.set(firstBranch, 0);
|
|
1212
|
+
const activeLanes = /* @__PURE__ */ new Set([0]);
|
|
1213
|
+
let maxLane = 0;
|
|
1214
|
+
commits.forEach((commit, row) => {
|
|
1215
|
+
if (visibleBranches.has(commit.branch) && !branchLane.has(commit.branch)) {
|
|
1216
|
+
let lane = 1;
|
|
1217
|
+
while (activeLanes.has(lane)) lane++;
|
|
1218
|
+
branchLane.set(commit.branch, lane);
|
|
1219
|
+
activeLanes.add(lane);
|
|
1220
|
+
maxLane = Math.max(maxLane, lane);
|
|
1221
|
+
}
|
|
1222
|
+
for (const branch of releasedAtRow.get(row) ?? []) {
|
|
1223
|
+
const lane = branchLane.get(branch);
|
|
1224
|
+
if (lane !== void 0) activeLanes.delete(lane);
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
const spans = /* @__PURE__ */ new Map();
|
|
1228
|
+
commits.forEach((commit, index) => {
|
|
1229
|
+
let span = spans.get(commit.branch);
|
|
1230
|
+
if (span) span.end = index;
|
|
1231
|
+
else {
|
|
1232
|
+
span = {
|
|
1233
|
+
start: index,
|
|
1234
|
+
end: index
|
|
1235
|
+
};
|
|
1236
|
+
spans.set(commit.branch, span);
|
|
1237
|
+
}
|
|
1238
|
+
const parents = commit.parents ?? [];
|
|
1239
|
+
for (const parentId of parents) {
|
|
1240
|
+
const parent = positionOf(parentId);
|
|
1241
|
+
if (!parent || parent.branch === commit.branch) continue;
|
|
1242
|
+
span.start = Math.min(span.start, parent.index);
|
|
1243
|
+
if (parents.length > 1) {
|
|
1244
|
+
const parentSpan = spans.get(parent.branch);
|
|
1245
|
+
if (parentSpan) parentSpan.end = Math.max(parentSpan.end, index);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
const rows = [];
|
|
1250
|
+
const rowByCommitIndex = /* @__PURE__ */ new Map();
|
|
1251
|
+
commits.forEach((commit, index) => {
|
|
1252
|
+
if (branchLane.has(commit.branch)) {
|
|
1253
|
+
rowByCommitIndex.set(index, rows.length);
|
|
1254
|
+
rows.push({
|
|
1255
|
+
commit,
|
|
1256
|
+
index
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
return {
|
|
1261
|
+
rows,
|
|
1262
|
+
branchLane,
|
|
1263
|
+
laneCount: maxLane + 1,
|
|
1264
|
+
spans,
|
|
1265
|
+
rowByCommitIndex,
|
|
1266
|
+
commitIndexById
|
|
1267
|
+
};
|
|
1268
|
+
};
|
|
1269
|
+
//#endregion
|
|
582
1270
|
//#region src/components/Timeline/timeline-options.ts
|
|
583
1271
|
var defaultOptions = {
|
|
584
1272
|
lineHeight: 24,
|
|
@@ -601,10 +1289,13 @@ var hasShimmerEffect = (commit) => commit.tags?.includes(SHIMMER_EFFECT_TAG$1) ?
|
|
|
601
1289
|
var empty = Object.freeze([]);
|
|
602
1290
|
/**
|
|
603
1291
|
* GitGraph-style timeline.
|
|
1292
|
+
*
|
|
1293
|
+
* Rows are windowed by `useWindow`, so rendering costs the viewport rather than the history. The
|
|
1294
|
+
* layout is still computed over the whole history, so a windowed row draws every lane crossing it.
|
|
604
1295
|
*/
|
|
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);
|
|
1296
|
+
var Timeline = memo(Util.composable(({ branches: branchesProp, branch, commits = empty, showTimestamp = false, showIcon = true, compact = false, options = compact ? compactOptions : defaultOptions, debug = false, scroller, onChange, onSelect, ...props }, forwardedRef) => {
|
|
1297
|
+
const { t } = Hooks.useTranslation(translationKey);
|
|
1298
|
+
const containerRef = Hooks.useForwardedRef(forwardedRef);
|
|
608
1299
|
const branches = useMemo(() => {
|
|
609
1300
|
if (branchesProp) return branchesProp;
|
|
610
1301
|
return commits.reduce((branches, commit) => {
|
|
@@ -612,87 +1303,18 @@ var Timeline = memo(composable(({ branches: branchesProp, branch, commits = empt
|
|
|
612
1303
|
return branches;
|
|
613
1304
|
}, []);
|
|
614
1305
|
}, [branchesProp, commits]);
|
|
615
|
-
const
|
|
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]);
|
|
1306
|
+
const layout = useMemo(() => layoutTimeline(commits, branches), [commits, branches]);
|
|
682
1307
|
const [current, setCurrent] = useState(void 0);
|
|
683
|
-
const currentRef = useDynamicRef(current);
|
|
684
|
-
const selectedRef = useDynamicRef(void 0);
|
|
1308
|
+
const currentRef = Hooks.useDynamicRef(current);
|
|
1309
|
+
const selectedRef = Hooks.useDynamicRef(void 0);
|
|
685
1310
|
const currentCommit = useMemo(() => current !== void 0 ? commits[current] : void 0, [current, commits]);
|
|
686
1311
|
const highlightedBranch = branch ?? currentCommit?.branch;
|
|
1312
|
+
const currentRow = current === void 0 ? void 0 : layout.rowByCommitIndex.get(current);
|
|
687
1313
|
useEffect(() => {
|
|
688
1314
|
onChange?.({
|
|
689
1315
|
current,
|
|
690
1316
|
commit: current === void 0 ? void 0 : commits[current]
|
|
691
1317
|
});
|
|
692
|
-
(containerRef.current?.querySelector(`[data-index="${current}"]`))?.scrollIntoView({
|
|
693
|
-
behavior: "instant",
|
|
694
|
-
block: "nearest"
|
|
695
|
-
});
|
|
696
1318
|
}, [onChange, current]);
|
|
697
1319
|
useEffect(() => {
|
|
698
1320
|
if (!branch) return;
|
|
@@ -735,78 +1357,147 @@ var Timeline = memo(composable(({ branches: branchesProp, branch, commits = empt
|
|
|
735
1357
|
}
|
|
736
1358
|
});
|
|
737
1359
|
}, [commits, containerRef.current]);
|
|
1360
|
+
const handleRowClick = useCallback((index) => {
|
|
1361
|
+
if (selectedRef.current === index) {
|
|
1362
|
+
selectedRef.current = void 0;
|
|
1363
|
+
setCurrent(void 0);
|
|
1364
|
+
onSelect?.(void 0);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
setCurrent(index);
|
|
1368
|
+
selectedRef.current = index;
|
|
1369
|
+
onSelect?.(commits[index]);
|
|
1370
|
+
}, [commits, onSelect]);
|
|
1371
|
+
const gridTemplateColumns = useMemo(() => [
|
|
1372
|
+
"min-content",
|
|
1373
|
+
showIcon && "1.25rem",
|
|
1374
|
+
"1fr",
|
|
1375
|
+
showTimestamp && "max-content"
|
|
1376
|
+
].filter(Boolean).join(" "), [showIcon, showTimestamp]);
|
|
738
1377
|
return /* @__PURE__ */ jsx("div", {
|
|
739
|
-
...composableProps(props, { classNames: "
|
|
1378
|
+
...Util.composableProps(props, { classNames: "relative outline-none" }),
|
|
1379
|
+
role: "list",
|
|
740
1380
|
tabIndex: 0,
|
|
741
|
-
style: { gridTemplateColumns: [
|
|
742
|
-
"min-content",
|
|
743
|
-
showIcon && "1.25rem",
|
|
744
|
-
"1fr",
|
|
745
|
-
showTimestamp && "max-content"
|
|
746
|
-
].filter(Boolean).join(" ") },
|
|
747
1381
|
ref: containerRef,
|
|
748
|
-
children:
|
|
749
|
-
className: "
|
|
1382
|
+
children: layout.rows.length < 1 ? /* @__PURE__ */ jsx("p", {
|
|
1383
|
+
className: "text-fg-muted p-trim-md",
|
|
750
1384
|
children: t("no-commits.message")
|
|
751
|
-
}) :
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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);
|
|
1385
|
+
}) : scroller && /* @__PURE__ */ jsx(TimelineWindow, {
|
|
1386
|
+
scroller,
|
|
1387
|
+
layout,
|
|
1388
|
+
options,
|
|
1389
|
+
currentRow,
|
|
1390
|
+
showIcon,
|
|
1391
|
+
showTimestamp,
|
|
1392
|
+
debug,
|
|
1393
|
+
highlightedBranch,
|
|
1394
|
+
linkable: !!onSelect,
|
|
1395
|
+
gridTemplateColumns,
|
|
1396
|
+
onRowClick: handleRowClick
|
|
799
1397
|
})
|
|
800
1398
|
});
|
|
801
1399
|
}));
|
|
802
1400
|
Timeline.displayName = "Timeline";
|
|
1401
|
+
var getRowId = (row) => row.commit.id;
|
|
1402
|
+
/**
|
|
1403
|
+
* The mounted rows: a sizer that gives the scrollbar the whole history's extent, and a window
|
|
1404
|
+
* holding the rows in view, translated to where they belong. Rendered only once the host's
|
|
1405
|
+
* scroller exists, because the placement binds to the element on mount.
|
|
1406
|
+
*/
|
|
1407
|
+
var TimelineWindow = ({ scroller, layout, options, currentRow, ...context }) => {
|
|
1408
|
+
const scrollerRef = useRef(scroller);
|
|
1409
|
+
scrollerRef.current = scroller;
|
|
1410
|
+
const model = useListModel(layout.rows, getRowId);
|
|
1411
|
+
const extents = useMemo(() => ({
|
|
1412
|
+
of: () => options.lineHeight,
|
|
1413
|
+
exact: true
|
|
1414
|
+
}), [options.lineHeight]);
|
|
1415
|
+
const controllerRef = useRef(null);
|
|
1416
|
+
const { layout: { visible }, windowRef, offset, sizerExtent, first, last } = useWindow({
|
|
1417
|
+
scrollerRef,
|
|
1418
|
+
model,
|
|
1419
|
+
extents,
|
|
1420
|
+
controllerRef
|
|
1421
|
+
});
|
|
1422
|
+
const visibleRef = useRef(visible);
|
|
1423
|
+
visibleRef.current = visible;
|
|
1424
|
+
useEffect(() => {
|
|
1425
|
+
if (currentRow === void 0) return;
|
|
1426
|
+
const { first, last } = visibleRef.current;
|
|
1427
|
+
if (currentRow < first) controllerRef.current?.scrollToIndex(currentRow, "start");
|
|
1428
|
+
else if (currentRow > last) controllerRef.current?.scrollToIndex(currentRow, "end");
|
|
1429
|
+
}, [currentRow]);
|
|
1430
|
+
const rows = [];
|
|
1431
|
+
for (let index = first; index <= last; index++) {
|
|
1432
|
+
const row = layout.rows[index];
|
|
1433
|
+
if (!row) continue;
|
|
1434
|
+
rows.push(/* @__PURE__ */ jsx(TimelineRowView, {
|
|
1435
|
+
index,
|
|
1436
|
+
row,
|
|
1437
|
+
current: index === currentRow,
|
|
1438
|
+
layout,
|
|
1439
|
+
options,
|
|
1440
|
+
...context
|
|
1441
|
+
}, row.commit.id));
|
|
1442
|
+
}
|
|
1443
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", { style: { height: sizerExtent } }), /* @__PURE__ */ jsx("div", {
|
|
1444
|
+
ref: windowRef,
|
|
1445
|
+
className: "absolute top-0 left-0 flex flex-col w-full",
|
|
1446
|
+
style: { transform: `translateY(${offset}px)` },
|
|
1447
|
+
children: rows
|
|
1448
|
+
})] });
|
|
1449
|
+
};
|
|
1450
|
+
var TimelineRowView = memo(({ index, row, current, layout, options, showIcon, showTimestamp, debug, highlightedBranch, linkable, gridTemplateColumns, onRowClick }) => {
|
|
1451
|
+
const { commit } = row;
|
|
1452
|
+
const handleClick = useCallback(() => onRowClick(row.index), [onRowClick, row.index]);
|
|
1453
|
+
const hasLink = !!commit.link && linkable;
|
|
1454
|
+
const message = debug ? JSON.stringify({
|
|
1455
|
+
id: commit.id,
|
|
1456
|
+
parents: commit.parents
|
|
1457
|
+
}) : commit.message;
|
|
1458
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1459
|
+
role: "listitem",
|
|
1460
|
+
"data-object-id": commit.id,
|
|
1461
|
+
...windowRowProps(index, commit.id),
|
|
1462
|
+
"data-commit-index": row.index,
|
|
1463
|
+
"aria-current": current ? "true" : void 0,
|
|
1464
|
+
className: "group/row grid gap-1 px-[2px] overflow-hidden items-center pe-2 dx-hover dx-current",
|
|
1465
|
+
style: {
|
|
1466
|
+
gridTemplateColumns,
|
|
1467
|
+
height: `${options.lineHeight}px`
|
|
1468
|
+
},
|
|
1469
|
+
onClick: handleClick,
|
|
1470
|
+
children: [
|
|
1471
|
+
/* @__PURE__ */ jsx("div", {
|
|
1472
|
+
className: "px-2",
|
|
1473
|
+
children: /* @__PURE__ */ jsx(LineVector, {
|
|
1474
|
+
layout,
|
|
1475
|
+
index: row.index,
|
|
1476
|
+
commit,
|
|
1477
|
+
highlightedBranch,
|
|
1478
|
+
options
|
|
1479
|
+
})
|
|
1480
|
+
}),
|
|
1481
|
+
showIcon && /* @__PURE__ */ jsx(CommitIcon, { commit }),
|
|
1482
|
+
/* @__PURE__ */ jsx("div", {
|
|
1483
|
+
className: mx("text-sm truncate cursor-pointer text-fg-muted font-thin group-aria-current/row:text-current-fg hover:text-current-fg", hasLink && "underline decoration-dotted underline-offset-2"),
|
|
1484
|
+
children: hasShimmerEffect(commit) ? /* @__PURE__ */ jsx(Shimmer, { children: message }) : message
|
|
1485
|
+
}),
|
|
1486
|
+
showTimestamp && /* @__PURE__ */ jsx("div", {
|
|
1487
|
+
className: "text-xs tabular-nums items-center text-fg-muted font-thin",
|
|
1488
|
+
children: commit.timestamp && format(commit.timestamp, TIMESTAMP_FORMAT)
|
|
1489
|
+
})
|
|
1490
|
+
]
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
TimelineRowView.displayName = "TimelineRowView";
|
|
803
1494
|
var CommitIcon = memo(({ commit }) => {
|
|
804
1495
|
if (!commit.icon) return /* @__PURE__ */ jsx("div", {});
|
|
805
|
-
return /* @__PURE__ */ jsx(Icon, {
|
|
1496
|
+
return /* @__PURE__ */ jsx(Icon.Icon, {
|
|
806
1497
|
icon: commit.icon,
|
|
807
|
-
size:
|
|
808
|
-
|
|
809
|
-
classNames:
|
|
1498
|
+
size: "md",
|
|
1499
|
+
spin: commit.icon === "ph--spinner-gap--regular",
|
|
1500
|
+
classNames: commit.level !== void 0 ? levelColors[commit.level] : void 0
|
|
810
1501
|
});
|
|
811
1502
|
});
|
|
812
1503
|
var colors = [
|
|
@@ -854,9 +1545,10 @@ var levelColors = {
|
|
|
854
1545
|
/**
|
|
855
1546
|
* SVG for node and connector paths.
|
|
856
1547
|
*/
|
|
857
|
-
var LineVector = memo(({
|
|
1548
|
+
var LineVector = memo(({ layout, index, commit, highlightedBranch, options }) => {
|
|
1549
|
+
const { branchLane, laneCount, spans } = layout;
|
|
858
1550
|
const halfHeight = options.lineHeight / 2;
|
|
859
|
-
const cx = (
|
|
1551
|
+
const cx = (column) => column * options.columnWidth + options.columnWidth / 2;
|
|
860
1552
|
const getBranchIndex = (branch) => branchLane.get(branch) ?? -1;
|
|
861
1553
|
const createPath = (index, commit, branch, span) => {
|
|
862
1554
|
const parents = commit.parents ?? [];
|
|
@@ -878,7 +1570,7 @@ var LineVector = memo(({ branchLane, laneCount, spans, index, commit, highlighte
|
|
|
878
1570
|
};
|
|
879
1571
|
const col = getBranchIndex(commit.branch);
|
|
880
1572
|
const color = colors[col % colors.length];
|
|
881
|
-
const opacity = (branch) => ["duration-500 transition-opacity", highlightedBranch === void 0 || branch === highlightedBranch ? "opacity-100" : "opacity-
|
|
1573
|
+
const opacity = (branch) => ["duration-500 transition-opacity", highlightedBranch === void 0 || branch === highlightedBranch ? "opacity-100" : "opacity-40"];
|
|
882
1574
|
return /* @__PURE__ */ jsxs("svg", {
|
|
883
1575
|
width: laneCount * options.columnWidth,
|
|
884
1576
|
height: options.lineHeight,
|
|
@@ -1038,6 +1730,38 @@ var computeActiveColumns = (commits, lanes, laneById) => {
|
|
|
1038
1730
|
return active;
|
|
1039
1731
|
};
|
|
1040
1732
|
//#endregion
|
|
1733
|
+
//#region src/components/TracePanel/SpanTreeView.tsx
|
|
1734
|
+
/**
|
|
1735
|
+
* The trace's span tree as read-only JSON.
|
|
1736
|
+
*
|
|
1737
|
+
* CodeMirror puts only the lines in view into the DOM and parses the rest in time-sliced chunks, so
|
|
1738
|
+
* showing a tree stops costing what the tree is worth — a highlighter renders every one of its lines
|
|
1739
|
+
* in one synchronous pass. `TextEditor` rather than `Editor.View`: this pane has no toolbar, so the
|
|
1740
|
+
* compound component's context would only be wiring nothing reads.
|
|
1741
|
+
*/
|
|
1742
|
+
var SpanTreeView = ({ classNames, spanTree }) => {
|
|
1743
|
+
const themeMode = Hooks.useThemeMode();
|
|
1744
|
+
const value = useMemo(() => safeStringify(spanTree, void 0, 2), [spanTree]);
|
|
1745
|
+
const extensions = useMemo(() => [
|
|
1746
|
+
createBasicExtensions({
|
|
1747
|
+
readOnly: true,
|
|
1748
|
+
lineWrapping: false,
|
|
1749
|
+
search: true
|
|
1750
|
+
}),
|
|
1751
|
+
createThemeExtensions({
|
|
1752
|
+
themeMode,
|
|
1753
|
+
syntaxHighlighting: true
|
|
1754
|
+
}),
|
|
1755
|
+
json(),
|
|
1756
|
+
folding()
|
|
1757
|
+
], [themeMode]);
|
|
1758
|
+
return /* @__PURE__ */ jsx(TextEditor, {
|
|
1759
|
+
classNames: ["text-xs", classNames],
|
|
1760
|
+
value,
|
|
1761
|
+
extensions
|
|
1762
|
+
});
|
|
1763
|
+
};
|
|
1764
|
+
//#endregion
|
|
1041
1765
|
//#region src/components/TracePanel/trace-filter.ts
|
|
1042
1766
|
/**
|
|
1043
1767
|
* What a running process is working on behalf of, as a single bucket.
|
|
@@ -1090,6 +1814,21 @@ var parseProcessEnvironments = (selected) => selected === void 0 ? DEFAULT_PROCE
|
|
|
1090
1814
|
* runs as a child of the process it serves, so its events are that process's work. An empty
|
|
1091
1815
|
* selection is no filter.
|
|
1092
1816
|
*/
|
|
1817
|
+
/**
|
|
1818
|
+
* Narrows the process tree to the selected processes and their descendants, the process-side twin of
|
|
1819
|
+
* {@link filterTraceMessages}: a running child of a picked process is that process's live work.
|
|
1820
|
+
*/
|
|
1821
|
+
var filterProcessesBySelection = (processes, selected) => {
|
|
1822
|
+
if (selected.length === 0) return processes;
|
|
1823
|
+
const parentByPid = new Map(processes.map((process) => [process.pid, process.parentPid]));
|
|
1824
|
+
const selection = new Set(selected);
|
|
1825
|
+
const isSelected = (pid, depth = 0) => {
|
|
1826
|
+
if (selection.has(pid)) return true;
|
|
1827
|
+
const parent = parentByPid.get(pid);
|
|
1828
|
+
return parent !== null && parent !== void 0 && depth < processes.length && isSelected(parent, depth + 1);
|
|
1829
|
+
};
|
|
1830
|
+
return processes.filter((process) => isSelected(process.pid));
|
|
1831
|
+
};
|
|
1093
1832
|
var filterTraceMessages = (messages, selected) => {
|
|
1094
1833
|
if (selected.length === 0) return messages;
|
|
1095
1834
|
const parentByPid = /* @__PURE__ */ new Map();
|
|
@@ -1159,8 +1898,8 @@ var useTraceMenu = ({ selected, onSelectedChange, selectionCount = 0, onClearSel
|
|
|
1159
1898
|
* event. Presentation only — the host resolves the process monitor, the trace feed, settings and
|
|
1160
1899
|
* navigation and hands them in as props.
|
|
1161
1900
|
*/
|
|
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);
|
|
1901
|
+
var TracePanel = Util.composable(({ classNames, processes, graph, environments, onEnvironmentsChange, debug = false, resolveLabel, selected = NO_SELECTION, onSelectedChange, onProcessTerminate, onOpenLink, ...props }, forwardedRef) => {
|
|
1902
|
+
const { t } = Hooks.useTranslation(translationKey);
|
|
1164
1903
|
const handleClearSelection = useCallback(() => onSelectedChange?.([]), [onSelectedChange]);
|
|
1165
1904
|
const menu = useTraceMenu({
|
|
1166
1905
|
selected: environments,
|
|
@@ -1169,8 +1908,10 @@ var TracePanel = composable(({ classNames, processes, graph, environments, onEnv
|
|
|
1169
1908
|
onClearSelection: handleClearSelection
|
|
1170
1909
|
});
|
|
1171
1910
|
const { branches, commits, spanTree, details } = useDeferredValue(graph);
|
|
1172
|
-
const [
|
|
1911
|
+
const [pickedCommit, setSelectedCommit] = useState();
|
|
1912
|
+
const selectedCommit = useMemo(() => pickedCommit && commits.some((commit) => commit.id === pickedCommit.id) ? pickedCommit : void 0, [pickedCommit, commits]);
|
|
1173
1913
|
const [openSections, setOpenSections] = useState(SECTIONS.map((section) => section.id));
|
|
1914
|
+
const [traceViewport, setTraceViewport] = useState(null);
|
|
1174
1915
|
const handleCommitSelect = useCallback((commit) => {
|
|
1175
1916
|
setSelectedCommit(commit);
|
|
1176
1917
|
if (commit?.link) onOpenLink?.(commit.link);
|
|
@@ -1180,32 +1921,25 @@ var TracePanel = composable(({ classNames, processes, graph, environments, onEnv
|
|
|
1180
1921
|
return last !== void 0 && branches.includes(last) ? last : null;
|
|
1181
1922
|
}, [selected, branches]);
|
|
1182
1923
|
return /* @__PURE__ */ jsxs(Panel.Root, {
|
|
1183
|
-
...composableProps(props, { classNames: ["h-full", classNames] }),
|
|
1924
|
+
...Util.composableProps(props, { classNames: ["h-full", classNames] }),
|
|
1184
1925
|
ref: forwardedRef,
|
|
1185
|
-
children: [/* @__PURE__ */ jsx(Panel.
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
classNames: "justify-end"
|
|
1191
|
-
})
|
|
1192
|
-
}), /* @__PURE__ */ jsx(Panel.Content, { children: /* @__PURE__ */ jsx(Accordion.Root, {
|
|
1193
|
-
items: SECTIONS,
|
|
1926
|
+
children: [/* @__PURE__ */ jsx(Panel.Header, { children: /* @__PURE__ */ jsx(ActionToolbar, {
|
|
1927
|
+
...menu,
|
|
1928
|
+
alwaysActive: true,
|
|
1929
|
+
classNames: "justify-end"
|
|
1930
|
+
}) }), /* @__PURE__ */ jsx(Panel.Body, { children: /* @__PURE__ */ jsx(Accordion.Root, {
|
|
1194
1931
|
value: openSections,
|
|
1195
1932
|
onValueChange: setOpenSections,
|
|
1196
|
-
classNames: "h-full min-h-0
|
|
1197
|
-
|
|
1933
|
+
classNames: "h-full min-h-0",
|
|
1934
|
+
border: false,
|
|
1935
|
+
children: SECTIONS.map((section) => {
|
|
1198
1936
|
switch (section.id) {
|
|
1199
1937
|
case "processes": return /* @__PURE__ */ jsxs(Accordion.Item, {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
className: "text-sm text-description",
|
|
1206
|
-
children: t("trace-processes.label")
|
|
1207
|
-
})
|
|
1208
|
-
}), /* @__PURE__ */ jsx(Accordion.ItemBody, {
|
|
1938
|
+
value: section.id,
|
|
1939
|
+
children: [/* @__PURE__ */ jsx(Accordion.ItemTrigger, { children: /* @__PURE__ */ jsx("span", {
|
|
1940
|
+
className: "text-sm text-fg-muted",
|
|
1941
|
+
children: t("trace-processes.label")
|
|
1942
|
+
}) }), /* @__PURE__ */ jsx(Accordion.ItemContent, {
|
|
1209
1943
|
classNames: "p-0",
|
|
1210
1944
|
children: /* @__PURE__ */ jsx(ProcessTreeContainer, {
|
|
1211
1945
|
classNames: "max-h-[8lh]",
|
|
@@ -1219,32 +1953,31 @@ var TracePanel = composable(({ classNames, processes, graph, environments, onEnv
|
|
|
1219
1953
|
})]
|
|
1220
1954
|
}, section.id);
|
|
1221
1955
|
case "trace": return /* @__PURE__ */ jsxs(Accordion.Item, {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
})
|
|
1230
|
-
}), /* @__PURE__ */ jsx(Accordion.ItemBody, {
|
|
1956
|
+
value: section.id,
|
|
1957
|
+
disabled: true,
|
|
1958
|
+
classNames: mx("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"),
|
|
1959
|
+
children: [/* @__PURE__ */ jsx(Accordion.ItemTrigger, { children: /* @__PURE__ */ jsx("span", {
|
|
1960
|
+
className: "text-sm text-fg-muted",
|
|
1961
|
+
children: t("trace.label")
|
|
1962
|
+
}) }), /* @__PURE__ */ jsx(Accordion.ItemContent, {
|
|
1231
1963
|
classNames: "dx-grow grid grid-rows-[minmax(0,1fr)]",
|
|
1232
1964
|
children: /* @__PURE__ */ jsx(ScrollContainer.Root, {
|
|
1233
1965
|
pin: true,
|
|
1234
1966
|
children: /* @__PURE__ */ jsxs(ScrollContainer.Content, {
|
|
1235
|
-
|
|
1967
|
+
width: "thin",
|
|
1236
1968
|
children: [
|
|
1237
|
-
/* @__PURE__ */ jsx(ScrollContainer.Fade, {}),
|
|
1238
|
-
/* @__PURE__ */ jsx(ScrollContainer.Viewport, {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1969
|
+
/* @__PURE__ */ jsx(ScrollContainer.Fade, { classNames: "h-8" }),
|
|
1970
|
+
/* @__PURE__ */ jsx(ScrollContainer.Viewport, {
|
|
1971
|
+
ref: setTraceViewport,
|
|
1972
|
+
children: debug ? /* @__PURE__ */ jsx(SpanTreeView, { spanTree }) : /* @__PURE__ */ jsx(Timeline, {
|
|
1973
|
+
branches,
|
|
1974
|
+
branch: currentBranch,
|
|
1975
|
+
commits,
|
|
1976
|
+
showTimestamp: true,
|
|
1977
|
+
scroller: traceViewport,
|
|
1978
|
+
onSelect: handleCommitSelect
|
|
1979
|
+
})
|
|
1980
|
+
}),
|
|
1248
1981
|
/* @__PURE__ */ jsx(ScrollContainer.ScrollDownButton, {})
|
|
1249
1982
|
]
|
|
1250
1983
|
})
|
|
@@ -1254,20 +1987,16 @@ var TracePanel = composable(({ classNames, processes, graph, environments, onEnv
|
|
|
1254
1987
|
case "details": {
|
|
1255
1988
|
const commit = debug ? void 0 : selectedCommit;
|
|
1256
1989
|
return /* @__PURE__ */ jsxs(Accordion.Item, {
|
|
1257
|
-
|
|
1990
|
+
value: section.id,
|
|
1258
1991
|
disabled: !commit,
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
className: "block truncate text-sm text-description",
|
|
1264
|
-
children: commit?.message ?? t("trace-details.label")
|
|
1265
|
-
})
|
|
1266
|
-
}), commit && /* @__PURE__ */ jsx(Accordion.ItemBody, {
|
|
1992
|
+
children: [/* @__PURE__ */ jsx(Accordion.ItemTrigger, { children: /* @__PURE__ */ jsx("span", {
|
|
1993
|
+
className: "flex items-center truncate text-sm text-fg-muted",
|
|
1994
|
+
children: t("trace-details.label")
|
|
1995
|
+
}) }), commit && /* @__PURE__ */ jsx(Accordion.ItemContent, {
|
|
1267
1996
|
classNames: "p-0",
|
|
1268
1997
|
children: /* @__PURE__ */ jsx(JsonHighlighter, {
|
|
1269
1998
|
data: details[commit.id] ?? commit,
|
|
1270
|
-
classNames: "max-h-[20lh] text-xs"
|
|
1999
|
+
classNames: "max-h-[20lh] text-xs p-1.5"
|
|
1271
2000
|
})
|
|
1272
2001
|
})]
|
|
1273
2002
|
}, section.id);
|
|
@@ -1509,7 +2238,7 @@ var flattenSpanTree = (root) => {
|
|
|
1509
2238
|
};
|
|
1510
2239
|
//#endregion
|
|
1511
2240
|
//#region src/execution-graph/execution-graph.ts
|
|
1512
|
-
var __dxlog_file = "/
|
|
2241
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-trace/src/execution-graph/execution-graph.ts";
|
|
1513
2242
|
/**
|
|
1514
2243
|
* Branch name for top-level operation invocations.
|
|
1515
2244
|
*/
|
|
@@ -1593,7 +2322,7 @@ var buildExecutionGraph = ({ traceMessages, activeProcesses = [], collapseComple
|
|
|
1593
2322
|
}, {
|
|
1594
2323
|
"~LogMeta": "~LogMeta",
|
|
1595
2324
|
F: __dxlog_file,
|
|
1596
|
-
L:
|
|
2325
|
+
L: 143,
|
|
1597
2326
|
S: void 0
|
|
1598
2327
|
});
|
|
1599
2328
|
return {
|
|
@@ -1625,7 +2354,7 @@ var parseAgentRequestEnd = (data) => {
|
|
|
1625
2354
|
log("invalid trace event", { type: Trace.AgentRequestEnd.key }, {
|
|
1626
2355
|
"~LogMeta": "~LogMeta",
|
|
1627
2356
|
F: __dxlog_file,
|
|
1628
|
-
L:
|
|
2357
|
+
L: 209,
|
|
1629
2358
|
S: void 0
|
|
1630
2359
|
});
|
|
1631
2360
|
};
|
|
@@ -1654,7 +2383,7 @@ var presentEvent = (event, toolCallContext) => {
|
|
|
1654
2383
|
log("invalid trace event", { type: event.type }, {
|
|
1655
2384
|
"~LogMeta": "~LogMeta",
|
|
1656
2385
|
F: __dxlog_file,
|
|
1657
|
-
L:
|
|
2386
|
+
L: 239,
|
|
1658
2387
|
S: void 0
|
|
1659
2388
|
});
|
|
1660
2389
|
return;
|
|
@@ -1675,7 +2404,7 @@ var presentEvent = (event, toolCallContext) => {
|
|
|
1675
2404
|
log("invalid trace event", { type: event.type }, {
|
|
1676
2405
|
"~LogMeta": "~LogMeta",
|
|
1677
2406
|
F: __dxlog_file,
|
|
1678
|
-
L:
|
|
2407
|
+
L: 257,
|
|
1679
2408
|
S: void 0
|
|
1680
2409
|
});
|
|
1681
2410
|
return;
|
|
@@ -1720,7 +2449,7 @@ var presentEvent = (event, toolCallContext) => {
|
|
|
1720
2449
|
log("invalid trace event", { type: event.type }, {
|
|
1721
2450
|
"~LogMeta": "~LogMeta",
|
|
1722
2451
|
F: __dxlog_file,
|
|
1723
|
-
L:
|
|
2452
|
+
L: 317,
|
|
1724
2453
|
S: void 0
|
|
1725
2454
|
});
|
|
1726
2455
|
return;
|
|
@@ -1737,12 +2466,12 @@ var presentEvent = (event, toolCallContext) => {
|
|
|
1737
2466
|
log("invalid trace event", { type: event.type }, {
|
|
1738
2467
|
"~LogMeta": "~LogMeta",
|
|
1739
2468
|
F: __dxlog_file,
|
|
1740
|
-
L:
|
|
2469
|
+
L: 329,
|
|
1741
2470
|
S: void 0
|
|
1742
2471
|
});
|
|
1743
2472
|
return;
|
|
1744
2473
|
}
|
|
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);
|
|
2474
|
+
const incomplete = event.data.outcome === "failure" && (event.data.errorCode === Process.RUN_AGAIN_ERROR_CODE || event.data.errorCode === void 0 && event.data.error === Process.RUN_AGAIN_MESSAGE);
|
|
1746
2475
|
const presentation = event.data.outcome === "success" ? {
|
|
1747
2476
|
icon: ICONS.operationEndSuccess.icon,
|
|
1748
2477
|
level: ICONS.operationEndSuccess.level,
|
|
@@ -1833,18 +2562,22 @@ var deriveInFlightActivityLine = (messages, rootPid, options = {}) => {
|
|
|
1833
2562
|
return lastLine;
|
|
1834
2563
|
};
|
|
1835
2564
|
var collectDescendantPids = (messages, rootPid) => {
|
|
1836
|
-
const
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
pids.add(pid);
|
|
1845
|
-
expanded = true;
|
|
1846
|
-
}
|
|
2565
|
+
const childPidsByParent = /* @__PURE__ */ new Map();
|
|
2566
|
+
for (const message of messages) {
|
|
2567
|
+
const { pid, parentPid } = message.meta;
|
|
2568
|
+
if (!pid || !parentPid) continue;
|
|
2569
|
+
let children = childPidsByParent.get(parentPid);
|
|
2570
|
+
if (!children) {
|
|
2571
|
+
children = /* @__PURE__ */ new Set();
|
|
2572
|
+
childPidsByParent.set(parentPid, children);
|
|
1847
2573
|
}
|
|
2574
|
+
children.add(pid);
|
|
2575
|
+
}
|
|
2576
|
+
const pids = /* @__PURE__ */ new Set([rootPid]);
|
|
2577
|
+
const pending = [rootPid];
|
|
2578
|
+
while (pending.length > 0) for (const childPid of childPidsByParent.get(pending.pop()) ?? []) if (!pids.has(childPid)) {
|
|
2579
|
+
pids.add(childPid);
|
|
2580
|
+
pending.push(childPid);
|
|
1848
2581
|
}
|
|
1849
2582
|
return pids;
|
|
1850
2583
|
};
|
|
@@ -1904,6 +2637,8 @@ var isCollapsibleSpan = (span, parent, collapseCompletedSpans) => {
|
|
|
1904
2637
|
* subtrees (depth-first), then its remaining events. Active-process running rows are inserted
|
|
1905
2638
|
* at the end of each matching span block — not appended after the entire tree.
|
|
1906
2639
|
*/
|
|
2640
|
+
/** Branch tips as a commit's parent list; a branch with no commit yet contributes nothing. */
|
|
2641
|
+
var parentIds = (...tips) => Array$1.dedupe(tips.filter((tip) => tip !== void 0).map((tip) => tip.id));
|
|
1907
2642
|
var spanTreeToCommits = (root, activeProcesses, toolCallContext, collapseCompletedSpans) => {
|
|
1908
2643
|
const builder = new GraphBuilder();
|
|
1909
2644
|
const details = {};
|
|
@@ -1936,10 +2671,12 @@ var spanTreeToCommits = (root, activeProcesses, toolCallContext, collapseComplet
|
|
|
1936
2671
|
};
|
|
1937
2672
|
const beginCommitIdBySpan = /* @__PURE__ */ new Map();
|
|
1938
2673
|
const lastInSpanContext = (span) => {
|
|
1939
|
-
if (!span || span.id === "<main>") return
|
|
1940
|
-
const
|
|
2674
|
+
if (!span || span.id === "<main>") return parentIds(builder.tipOf(MAIN_BRANCH));
|
|
2675
|
+
const ownTip = builder.tipOf(branchOf(span));
|
|
2676
|
+
if (ownTip) return parentIds(ownTip);
|
|
1941
2677
|
const beginId = beginCommitIdBySpan.get(span.id);
|
|
1942
|
-
|
|
2678
|
+
if (beginId && builder.hasCommit(beginId)) return [beginId];
|
|
2679
|
+
return lastInSpanContext(findParentSpan(span));
|
|
1943
2680
|
};
|
|
1944
2681
|
const lastCommitIdBySpan = /* @__PURE__ */ new Map();
|
|
1945
2682
|
let globalIndex = 0;
|
|
@@ -1953,7 +2690,7 @@ var spanTreeToCommits = (root, activeProcesses, toolCallContext, collapseComplet
|
|
|
1953
2690
|
builder.addCommit({
|
|
1954
2691
|
id: `running:${process.pid}`,
|
|
1955
2692
|
branch: process.pid,
|
|
1956
|
-
parents: builder.
|
|
2693
|
+
parents: parentIds(builder.firstTipOf(process.pid, MAIN_BRANCH)),
|
|
1957
2694
|
icon: ICONS.agentRequestRunning.icon,
|
|
1958
2695
|
level: ICONS.agentRequestRunning.level,
|
|
1959
2696
|
message: "Generating...",
|
|
@@ -1965,7 +2702,7 @@ var spanTreeToCommits = (root, activeProcesses, toolCallContext, collapseComplet
|
|
|
1965
2702
|
builder.addCommit({
|
|
1966
2703
|
id: `running:${process.pid}`,
|
|
1967
2704
|
branch: process.pid,
|
|
1968
|
-
parents: builder.
|
|
2705
|
+
parents: parentIds(builder.tipOf(process.pid)),
|
|
1969
2706
|
icon: ICONS.processRunning.icon,
|
|
1970
2707
|
level: ICONS.processRunning.level,
|
|
1971
2708
|
message: "Running...",
|
|
@@ -1994,19 +2731,19 @@ var spanTreeToCommits = (root, activeProcesses, toolCallContext, collapseComplet
|
|
|
1994
2731
|
let parents;
|
|
1995
2732
|
if (span.id === "<main>") {
|
|
1996
2733
|
branch = MAIN_BRANCH;
|
|
1997
|
-
parents =
|
|
2734
|
+
parents = lastInSpanContext(null);
|
|
1998
2735
|
} else if (collapsible) {
|
|
1999
2736
|
branch = parentBranch;
|
|
2000
|
-
parents =
|
|
2737
|
+
parents = lastInSpanContext(parentSpan);
|
|
2001
2738
|
} else if (isBeginEvent) {
|
|
2002
2739
|
branch = isProcessBoundary ? ownBranch : parentBranch;
|
|
2003
|
-
parents =
|
|
2740
|
+
parents = lastInSpanContext(parentSpan);
|
|
2004
2741
|
} else if (isEndEvent) {
|
|
2005
2742
|
branch = isProcessBoundary ? MAIN_BRANCH : parentBranch;
|
|
2006
|
-
parents = builder.
|
|
2743
|
+
parents = parentIds(builder.tipOf(branch), builder.tipOf(ownBranch));
|
|
2007
2744
|
} else {
|
|
2008
2745
|
branch = ownBranch;
|
|
2009
|
-
parents =
|
|
2746
|
+
parents = lastInSpanContext(span);
|
|
2010
2747
|
}
|
|
2011
2748
|
const commitId = formatCommitId(span, globalIndex++, presentation.idSuffix);
|
|
2012
2749
|
const shimmer = isPendingToolCallEvent(event, toolCallContext);
|
|
@@ -2156,22 +2893,44 @@ var CommitSelector = {
|
|
|
2156
2893
|
return [];
|
|
2157
2894
|
})
|
|
2158
2895
|
};
|
|
2896
|
+
/**
|
|
2897
|
+
* Accumulates the commit list. Every lookup the graph makes while building — the tip of a branch,
|
|
2898
|
+
* a commit by id — is served from an index, because each is asked once per commit and a scan would
|
|
2899
|
+
* make the whole build quadratic in the history.
|
|
2900
|
+
*/
|
|
2159
2901
|
var GraphBuilder = class {
|
|
2160
2902
|
#commits = [];
|
|
2161
2903
|
#branches = /* @__PURE__ */ new Set();
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
}
|
|
2904
|
+
#indexById = /* @__PURE__ */ new Map();
|
|
2905
|
+
#tipIndexByBranch = /* @__PURE__ */ new Map();
|
|
2165
2906
|
hasBranch(branch) {
|
|
2166
2907
|
return this.#branches.has(branch);
|
|
2167
2908
|
}
|
|
2909
|
+
hasCommit(commitId) {
|
|
2910
|
+
return this.#indexById.has(commitId);
|
|
2911
|
+
}
|
|
2912
|
+
/** The most recent commit on a branch, or undefined while the branch has none. */
|
|
2913
|
+
tipOf(branch) {
|
|
2914
|
+
if (!branch) return;
|
|
2915
|
+
const index = this.#tipIndexByBranch.get(branch);
|
|
2916
|
+
return index === void 0 ? void 0 : this.#commits[index];
|
|
2917
|
+
}
|
|
2918
|
+
/** The most recent commit on the first of `branches` that has one. */
|
|
2919
|
+
firstTipOf(...branches) {
|
|
2920
|
+
for (const branch of branches) {
|
|
2921
|
+
const tip = this.tipOf(branch);
|
|
2922
|
+
if (tip) return tip;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2168
2925
|
addCommit(commit) {
|
|
2169
2926
|
this.addBranch(commit.branch);
|
|
2170
|
-
this.#commits.push(commit);
|
|
2927
|
+
const index = this.#commits.push(commit) - 1;
|
|
2928
|
+
if (!this.#indexById.has(commit.id)) this.#indexById.set(commit.id, index);
|
|
2929
|
+
this.#tipIndexByBranch.set(commit.branch, index);
|
|
2171
2930
|
}
|
|
2172
2931
|
addTag(commitId, tag) {
|
|
2173
|
-
const index = this.#
|
|
2174
|
-
if (index
|
|
2932
|
+
const index = this.#indexById.get(commitId);
|
|
2933
|
+
if (index === void 0) return;
|
|
2175
2934
|
const commit = this.#commits[index];
|
|
2176
2935
|
if (commit.tags?.includes(tag)) return;
|
|
2177
2936
|
this.#commits[index] = {
|
|
@@ -2179,12 +2938,6 @@ var GraphBuilder = class {
|
|
|
2179
2938
|
tags: [...commit.tags ?? [], tag]
|
|
2180
2939
|
};
|
|
2181
2940
|
}
|
|
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
2941
|
addBranch(branch) {
|
|
2189
2942
|
this.#branches.add(branch);
|
|
2190
2943
|
}
|
|
@@ -2192,7 +2945,13 @@ var GraphBuilder = class {
|
|
|
2192
2945
|
* Removes duplicate commits and dangling parents.
|
|
2193
2946
|
*/
|
|
2194
2947
|
doctor() {
|
|
2195
|
-
|
|
2948
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2949
|
+
this.#commits = pipe(this.#commits, Array$1.filter((commit) => {
|
|
2950
|
+
if (seen.has(commit.id)) return false;
|
|
2951
|
+
seen.add(commit.id);
|
|
2952
|
+
return true;
|
|
2953
|
+
}), Array$1.map(Struct.evolve({ parents: (parents) => parents ? Array$1.filter(parents, (id) => seen.has(id)) : void 0 })));
|
|
2954
|
+
this.#reindex();
|
|
2196
2955
|
}
|
|
2197
2956
|
build() {
|
|
2198
2957
|
this.doctor();
|
|
@@ -2201,6 +2960,14 @@ var GraphBuilder = class {
|
|
|
2201
2960
|
branches: [...this.#branches]
|
|
2202
2961
|
};
|
|
2203
2962
|
}
|
|
2963
|
+
#reindex() {
|
|
2964
|
+
this.#indexById.clear();
|
|
2965
|
+
this.#tipIndexByBranch.clear();
|
|
2966
|
+
this.#commits.forEach((commit, index) => {
|
|
2967
|
+
if (!this.#indexById.has(commit.id)) this.#indexById.set(commit.id, index);
|
|
2968
|
+
this.#tipIndexByBranch.set(commit.branch, index);
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2204
2971
|
};
|
|
2205
2972
|
//#endregion
|
|
2206
2973
|
//#region src/execution-graph/pending-block-status.ts
|
|
@@ -2221,7 +2988,7 @@ var formatPendingBlockStatus = (block) => {
|
|
|
2221
2988
|
if (block._tag === "text") return `Generating ${estimateTokenCount(block.text)} tokens....`;
|
|
2222
2989
|
if (block._tag === "toolCall") {
|
|
2223
2990
|
const bytes = new TextEncoder().encode(block.input).length;
|
|
2224
|
-
return `Calling ${block.operationName ?? block.name} (${bytes} bytes)...`;
|
|
2991
|
+
return `Calling ${block.displayName ?? block.operationName ?? block.name} (${bytes} bytes)...`;
|
|
2225
2992
|
}
|
|
2226
2993
|
};
|
|
2227
2994
|
/**
|
|
@@ -2277,6 +3044,7 @@ var useTraceMessages = (space, { debounce } = {}) => {
|
|
|
2277
3044
|
var atomEmpty = Atom.make(() => []);
|
|
2278
3045
|
var NO_PIDS = [];
|
|
2279
3046
|
var SPAN_TIMEOUT_CHECK_INTERVAL_MS = 6e4;
|
|
3047
|
+
var REBUILD_DEBOUNCE = Duration.millis(500);
|
|
2280
3048
|
/**
|
|
2281
3049
|
* The commit graph of a space's trace, rebuilt as messages land and as the process tree changes.
|
|
2282
3050
|
* `processesAtom` is the host's live process tree (a running agent draws a spinner commit); without
|
|
@@ -2306,8 +3074,8 @@ var useExecutionGraph = (space, processesAtom, { collapseCompletedSpans, eventLi
|
|
|
2306
3074
|
/** Identity for the graph: only a process appearing, disappearing or changing state redraws it. */
|
|
2307
3075
|
var sameProcesses = (left, right) => left.length === right.length && left.every((process, index) => process.pid === right[index].pid && process.state === right[index].state);
|
|
2308
3076
|
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(
|
|
3077
|
+
const traceMessages = getTraceMessagesAtom(space).pipe(Atom.debounce(REBUILD_DEBOUNCE), Atom.map((messages) => filterTraceMessages(messages, selectedPids)));
|
|
3078
|
+
const activeProcesses = pipe(processesAtom, Atom.debounce(REBUILD_DEBOUNCE), Atom.map((processes) => filterProcessesBySelection(processes, selectedPids).filter((process) => process.state === Process.State.RUNNING || process.state === Process.State.HYBERNATING)), Atom.withEquality(sameProcesses));
|
|
2311
3079
|
return Atom.make((get) => buildExecutionGraph({
|
|
2312
3080
|
traceMessages: get(traceMessages),
|
|
2313
3081
|
activeProcesses: get(activeProcesses),
|
|
@@ -2317,10 +3085,171 @@ var getExecutionGraph = (space, processesAtom, { collapseCompletedSpans = true,
|
|
|
2317
3085
|
}));
|
|
2318
3086
|
};
|
|
2319
3087
|
//#endregion
|
|
3088
|
+
//#region src/session-timeline/gantt-mapping.ts
|
|
3089
|
+
/**
|
|
3090
|
+
* The one place the domain's nouns meet the chart's.
|
|
3091
|
+
*
|
|
3092
|
+
* The chart knows only groups, lanes, segments and markers; this file knows that a **process** is a
|
|
3093
|
+
* group — the band around the lanes of the tasks it works, with no row of its own — a **task** is a
|
|
3094
|
+
* lane inside it, a **subtask** nests under its parent task, and a **delegation** opens a lane out of
|
|
3095
|
+
* the node that spawned it. Keeping the translation here is what lets either vocabulary change without
|
|
3096
|
+
* dragging the other with it.
|
|
3097
|
+
*/
|
|
3098
|
+
var formatTokens = (tokens) => tokens.total >= 1e3 ? Unit.Thousand(tokens.total).toString() : String(tokens.total);
|
|
3099
|
+
/** Tokens and tool calls, formatted here because the chart renders `meta` without reading it. */
|
|
3100
|
+
var laneMeta = (lane) => [...lane.tokens ? [{
|
|
3101
|
+
label: formatTokens(lane.tokens),
|
|
3102
|
+
title: `${lane.tokens.input} in / ${lane.tokens.output} out`
|
|
3103
|
+
}] : [], ...lane.toolCalls !== void 0 ? [{ label: `${lane.toolCalls} tools` }] : []];
|
|
3104
|
+
/**
|
|
3105
|
+
* A lane's stretch as segments: one run, split around each gap where its task was put down, so a
|
|
3106
|
+
* question and its answer are separate runs with the wait between them left open.
|
|
3107
|
+
*/
|
|
3108
|
+
var segmentsOf = (lane) => {
|
|
3109
|
+
if (lane.start === void 0) return;
|
|
3110
|
+
const segments = [];
|
|
3111
|
+
let start = lane.start;
|
|
3112
|
+
for (const gap of [...lane.gaps ?? []].sort((left, right) => left.start - right.start)) {
|
|
3113
|
+
const gapStart = Math.max(gap.start, start);
|
|
3114
|
+
const gapEnd = lane.end === void 0 ? gap.end : Math.min(gap.end, lane.end);
|
|
3115
|
+
if (gapEnd <= gapStart) continue;
|
|
3116
|
+
if (gapStart > start) segments.push({
|
|
3117
|
+
start,
|
|
3118
|
+
end: gapStart
|
|
3119
|
+
});
|
|
3120
|
+
start = gapEnd;
|
|
3121
|
+
}
|
|
3122
|
+
if (lane.end === void 0) segments.push({ start });
|
|
3123
|
+
else if (lane.end > start) segments.push({
|
|
3124
|
+
start,
|
|
3125
|
+
end: lane.end
|
|
3126
|
+
});
|
|
3127
|
+
return segments;
|
|
3128
|
+
};
|
|
3129
|
+
var isHistoryEntry = Schema.is(Task.HistoryEntry);
|
|
3130
|
+
/**
|
|
3131
|
+
* Maps a session timeline onto the chart's model.
|
|
3132
|
+
*
|
|
3133
|
+
* Every process becomes a band holding the lanes of the tasks it works; the process itself draws no
|
|
3134
|
+
* row, so its own nodes (requests, tool calls no task claims) are dropped. A process standing for one
|
|
3135
|
+
* task — a delegated sub-agent, or a session the builder folded its lone task into — is that task's
|
|
3136
|
+
* lane, so it keeps its span, its nodes and the node it opened out of. A process spawned by another
|
|
3137
|
+
* nests its band under its parent's — so causality is drawn by `openedFrom` while membership is
|
|
3138
|
+
* `groupId` and nesting is `parentId`, three facts the timeline's single `parentId` used to carry.
|
|
3139
|
+
*/
|
|
3140
|
+
var sessionTimelineToGantt = (timeline) => {
|
|
3141
|
+
const byId = new Map(timeline.lanes.map((lane) => [lane.id, lane]));
|
|
3142
|
+
const groups = [];
|
|
3143
|
+
const lanes = [];
|
|
3144
|
+
/** The band a lane belongs to: its own, for a process; its process's, for a task. */
|
|
3145
|
+
const bandOf = (lane) => {
|
|
3146
|
+
if (lane.kind === "session") return lane.id;
|
|
3147
|
+
const parent = lane.parentId === void 0 ? void 0 : byId.get(lane.parentId);
|
|
3148
|
+
return parent ? bandOf(parent) : void 0;
|
|
3149
|
+
};
|
|
3150
|
+
for (const lane of timeline.lanes) {
|
|
3151
|
+
const meta = laneMeta(lane);
|
|
3152
|
+
const facts = {
|
|
3153
|
+
label: lane.label,
|
|
3154
|
+
status: lane.status,
|
|
3155
|
+
...segmentsOf(lane) ? { segments: segmentsOf(lane) } : {},
|
|
3156
|
+
...lane.delegatedFrom ? { openedFrom: lane.delegatedFrom } : {},
|
|
3157
|
+
...lane.returnedTo ? { closedInto: lane.returnedTo } : {},
|
|
3158
|
+
...lane.hue ? { hue: lane.hue } : {},
|
|
3159
|
+
...lane.blockedOn ? { blockedOn: lane.blockedOn } : {},
|
|
3160
|
+
...meta.length > 0 ? { meta } : {}
|
|
3161
|
+
};
|
|
3162
|
+
const parent = lane.parentId === void 0 ? void 0 : byId.get(lane.parentId);
|
|
3163
|
+
if (lane.kind === "session") {
|
|
3164
|
+
groups.push({
|
|
3165
|
+
id: lane.id,
|
|
3166
|
+
...parent ? { parentId: bandOf(parent) } : {}
|
|
3167
|
+
});
|
|
3168
|
+
if (lane.taskId === void 0) continue;
|
|
3169
|
+
}
|
|
3170
|
+
lanes.push({
|
|
3171
|
+
id: lane.id,
|
|
3172
|
+
groupId: bandOf(lane),
|
|
3173
|
+
...lane.kind === "task" && parent?.kind === "task" ? { parentId: parent.id } : {},
|
|
3174
|
+
...facts
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
const drawn = new Set(lanes.map((lane) => lane.id));
|
|
3178
|
+
const answerByQuestion = /* @__PURE__ */ new Map();
|
|
3179
|
+
for (const marker of timeline.markers) if (isHistoryEntry(marker.detail) && Task.isAnswerEntry(marker.detail)) answerByQuestion.set(`${marker.laneId}:${marker.detail.questionId}`, marker.id);
|
|
3180
|
+
return {
|
|
3181
|
+
groups,
|
|
3182
|
+
lanes,
|
|
3183
|
+
markers: timeline.markers.flatMap((marker) => drawn.has(marker.laneId) ? [marker] : []).map((marker) => {
|
|
3184
|
+
const question = isHistoryEntry(marker.detail) && Task.isQuestionEntry(marker.detail) ? marker.detail : void 0;
|
|
3185
|
+
const until = question && answerByQuestion.get(`${marker.laneId}:${question.id}`);
|
|
3186
|
+
return {
|
|
3187
|
+
id: marker.id,
|
|
3188
|
+
laneId: marker.laneId,
|
|
3189
|
+
kind: marker.kind,
|
|
3190
|
+
timestamp: marker.timestamp,
|
|
3191
|
+
label: marker.label,
|
|
3192
|
+
...marker.level ? { level: marker.level } : {},
|
|
3193
|
+
...until ? { wait: { until } } : {},
|
|
3194
|
+
...question && !until ? { pending: true } : {}
|
|
3195
|
+
};
|
|
3196
|
+
})
|
|
3197
|
+
};
|
|
3198
|
+
};
|
|
3199
|
+
//#endregion
|
|
2320
3200
|
//#region src/session-timeline/session-timeline.ts
|
|
3201
|
+
var isStatus = Schema.is(Task.Status);
|
|
3202
|
+
/**
|
|
3203
|
+
* The status moves in a task's edit history, oldest first; none for a task not stored in a database,
|
|
3204
|
+
* which keeps no history.
|
|
3205
|
+
*/
|
|
3206
|
+
var readTaskStatusChanges = (task) => Obj.getDatabase(task) === void 0 ? [] : Obj.getChanges(task, { property: "status" }).flatMap(({ time, before, after }) => isStatus(after) ? [{
|
|
3207
|
+
timestamp: time,
|
|
3208
|
+
status: after,
|
|
3209
|
+
...isStatus(before) ? { previousStatus: before } : {}
|
|
3210
|
+
}] : []);
|
|
3211
|
+
/** Statuses of a task not yet picked up. */
|
|
3212
|
+
var PENDING_STATUSES = /* @__PURE__ */ new Set(["todo", "backlog"]);
|
|
3213
|
+
/**
|
|
3214
|
+
* The edit history's status moves, each timed by the trace event recording the same move when there is
|
|
3215
|
+
* one. History times are whole seconds, rounded down, so on their own they start a lane up to a second
|
|
3216
|
+
* before the node that marks its start; everything derived from them — spans, nodes — reads these.
|
|
3217
|
+
*/
|
|
3218
|
+
var refineStatusChanges = (changes, events) => {
|
|
3219
|
+
const traced = /* @__PURE__ */ new Map();
|
|
3220
|
+
for (const event of events) if (event.type === Trace.TaskStatusChanged.key) {
|
|
3221
|
+
const data = decode(Trace.TaskStatusChanged.schema, event.data);
|
|
3222
|
+
if (data) traced.set(data.taskId, [...traced.get(data.taskId) ?? [], {
|
|
3223
|
+
timestamp: event.timestamp,
|
|
3224
|
+
status: data.status
|
|
3225
|
+
}]);
|
|
3226
|
+
}
|
|
3227
|
+
return new Map([...changes].map(([taskId, list]) => {
|
|
3228
|
+
const candidates = [...traced.get(taskId) ?? []];
|
|
3229
|
+
return [taskId, list.map((change) => {
|
|
3230
|
+
const index = candidates.findIndex(({ timestamp, status }) => status === change.status && Math.abs(timestamp - change.timestamp) <= HISTORY_MATCH_MS);
|
|
3231
|
+
if (index < 0) return change;
|
|
3232
|
+
const [match] = candidates.splice(index, 1);
|
|
3233
|
+
return {
|
|
3234
|
+
...change,
|
|
3235
|
+
timestamp: match.timestamp
|
|
3236
|
+
};
|
|
3237
|
+
})];
|
|
3238
|
+
}));
|
|
3239
|
+
};
|
|
3240
|
+
/** Statuses that end a stretch of work on a task. */
|
|
3241
|
+
var CLOSED_STATUSES = /* @__PURE__ */ new Set([
|
|
3242
|
+
"done",
|
|
3243
|
+
"review",
|
|
3244
|
+
"failed",
|
|
3245
|
+
"cancelled"
|
|
3246
|
+
]);
|
|
3247
|
+
var isStatusChange = (detail) => typeof detail === "object" && detail !== null && "status" in detail && isStatus(detail.status);
|
|
2321
3248
|
var ACTIVE_STATES = /* @__PURE__ */ new Set([Process.State.RUNNING, Process.State.HYBERNATING]);
|
|
2322
3249
|
/** How long an open lane may be silent before the axis stops following `now`. */
|
|
2323
3250
|
var OPEN_LANE_STALE_MS = 6e5;
|
|
3251
|
+
/** How far apart a task's recorded status move and the trace event recording the same move may land. */
|
|
3252
|
+
var HISTORY_MATCH_MS = 5e3;
|
|
2324
3253
|
var TASK_STATUS = {
|
|
2325
3254
|
todo: "pending",
|
|
2326
3255
|
backlog: "pending",
|
|
@@ -2340,66 +3269,62 @@ var decode = (schema, data) => Option.getOrUndefined(Schema.decodeUnknownOption(
|
|
|
2340
3269
|
var sessionLaneId = (key) => `session:${key}`;
|
|
2341
3270
|
var taskLaneId = (taskId) => `task:${taskId}`;
|
|
2342
3271
|
/**
|
|
2343
|
-
* Cuts a session's events into
|
|
2344
|
-
*
|
|
2345
|
-
*
|
|
2346
|
-
* open or move the boundary.
|
|
3272
|
+
* Cuts a session's events into segments of task work, from the status events its task tools write.
|
|
3273
|
+
* Only tasks in `taskIds` — the session's own checklist — take part: an agent is free to move a task
|
|
3274
|
+
* belonging to nothing on this chart, and such an event must not open or close anything here.
|
|
2347
3275
|
*
|
|
2348
|
-
*
|
|
2349
|
-
*
|
|
2350
|
-
*
|
|
2351
|
-
* a start: delegation marks every task it hands over
|
|
2352
|
-
*
|
|
2353
|
-
*
|
|
2354
|
-
*
|
|
2355
|
-
*
|
|
2356
|
-
*
|
|
2357
|
-
* rather than overlapping it.
|
|
3276
|
+
* An agent may hold several tasks in progress at once, and may put one down and pick it up again as
|
|
3277
|
+
* it discovers what depends on what: a `started` event opens a segment for its task and leaves every
|
|
3278
|
+
* other open segment alone, and any other status closes that task's open segment. A task can also
|
|
3279
|
+
* finish without a start: delegation marks every task it hands over `started` before the agent's
|
|
3280
|
+
* first turn, so the run's only event for that task is the one closing it. Such a task gets the
|
|
3281
|
+
* stretch since the last boundary, which is where its work happened — but only on the transition out
|
|
3282
|
+
* of `started` and only while no other task holds the stretch: a task merely dismissed
|
|
3283
|
+
* (`todo` → `blocked`) claims nothing, a second close (`review` → `done`) mints nothing, and a close
|
|
3284
|
+
* arriving while other work is open leaves that work alone rather than overlapping it.
|
|
2358
3285
|
*/
|
|
2359
3286
|
var buildTaskSegments = (events, sessionStart, taskIds) => {
|
|
2360
3287
|
const segments = [];
|
|
2361
|
-
|
|
3288
|
+
const open = /* @__PURE__ */ new Map();
|
|
2362
3289
|
let boundary = sessionStart;
|
|
2363
|
-
const close = (segment, timestamp) => {
|
|
2364
|
-
segment.end = timestamp;
|
|
2365
|
-
boundary = timestamp;
|
|
2366
|
-
if (open === segment) open = void 0;
|
|
2367
|
-
};
|
|
2368
3290
|
for (const event of events) {
|
|
2369
3291
|
if (event.type !== Trace.TaskStatusChanged.key) continue;
|
|
2370
3292
|
const data = decode(Trace.TaskStatusChanged.schema, event.data);
|
|
2371
3293
|
if (!data || !taskIds.has(data.taskId)) continue;
|
|
3294
|
+
const current = open.get(data.taskId);
|
|
2372
3295
|
if (data.status === "started") {
|
|
2373
|
-
if (
|
|
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) {
|
|
3296
|
+
if (!current) {
|
|
2387
3297
|
const segment = {
|
|
2388
3298
|
taskId: data.taskId,
|
|
2389
3299
|
laneId: taskLaneId(data.taskId),
|
|
2390
|
-
start:
|
|
3300
|
+
start: event.timestamp
|
|
2391
3301
|
};
|
|
2392
3302
|
segments.push(segment);
|
|
2393
|
-
|
|
3303
|
+
open.set(data.taskId, segment);
|
|
2394
3304
|
}
|
|
3305
|
+
boundary = event.timestamp;
|
|
3306
|
+
} else if (current) {
|
|
3307
|
+
current.end = event.timestamp;
|
|
3308
|
+
open.delete(data.taskId);
|
|
3309
|
+
boundary = event.timestamp;
|
|
3310
|
+
} else if (data.previousStatus === "started" && open.size === 0) {
|
|
3311
|
+
segments.push({
|
|
3312
|
+
taskId: data.taskId,
|
|
3313
|
+
laneId: taskLaneId(data.taskId),
|
|
3314
|
+
start: Math.min(boundary ?? event.timestamp, event.timestamp),
|
|
3315
|
+
end: event.timestamp
|
|
3316
|
+
});
|
|
3317
|
+
boundary = event.timestamp;
|
|
2395
3318
|
}
|
|
2396
3319
|
}
|
|
2397
3320
|
return segments;
|
|
2398
3321
|
};
|
|
2399
3322
|
/**
|
|
2400
|
-
* The segment an event belongs to.
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
3323
|
+
* The segment an event belongs to. A status event goes to its own task's segment. Any other event
|
|
3324
|
+
* goes to the one segment in progress when it happened; where segments merely meet, the one ending
|
|
3325
|
+
* there yields to the one beginning — the newcomer's start is the first thing the agent did on it.
|
|
3326
|
+
* With several tasks genuinely in progress at once the event belongs to none of them in particular,
|
|
3327
|
+
* so it stays with the session rather than being credited to whichever started last.
|
|
2403
3328
|
*/
|
|
2404
3329
|
var segmentFor = (segments, event) => {
|
|
2405
3330
|
if (!segments) return;
|
|
@@ -2408,15 +3333,85 @@ var segmentFor = (segments, event) => {
|
|
|
2408
3333
|
const data = decode(Trace.TaskStatusChanged.schema, event.data);
|
|
2409
3334
|
return data && segments.findLast((segment) => segment.taskId === data.taskId && contains(segment));
|
|
2410
3335
|
}
|
|
2411
|
-
|
|
3336
|
+
const candidates = segments.filter(contains);
|
|
3337
|
+
const inside = candidates.filter((segment) => segment.end === void 0 || event.timestamp < segment.end);
|
|
3338
|
+
if (inside.length > 1) return;
|
|
3339
|
+
return inside[0] ?? candidates.at(-1);
|
|
3340
|
+
};
|
|
3341
|
+
/**
|
|
3342
|
+
* The stretch a task's edit history says it was worked: from its first move to `started` to the last
|
|
3343
|
+
* move out of it. `end` is absent while the history leaves it `started`; `last` is its last move.
|
|
3344
|
+
* Absent when the task was never started.
|
|
3345
|
+
*/
|
|
3346
|
+
var historySpan = (changes) => {
|
|
3347
|
+
let start;
|
|
3348
|
+
let end;
|
|
3349
|
+
let open = false;
|
|
3350
|
+
for (const change of changes) if (change.status === "started") {
|
|
3351
|
+
start ??= change.timestamp;
|
|
3352
|
+
open = true;
|
|
3353
|
+
} else if (open) {
|
|
3354
|
+
end = change.timestamp;
|
|
3355
|
+
open = false;
|
|
3356
|
+
}
|
|
3357
|
+
const last = changes.at(-1)?.timestamp;
|
|
3358
|
+
return start === void 0 || last === void 0 ? void 0 : {
|
|
3359
|
+
start,
|
|
3360
|
+
end: open ? void 0 : end,
|
|
3361
|
+
last
|
|
3362
|
+
};
|
|
3363
|
+
};
|
|
3364
|
+
/**
|
|
3365
|
+
* The stretches a task was put down between two runs of work: from a move out of `started` that did
|
|
3366
|
+
* not finish it (a question blocking it, say) to the next move back. One still open is no gap — the
|
|
3367
|
+
* lane simply ends there — and nothing before the first start is one either.
|
|
3368
|
+
*/
|
|
3369
|
+
var workGaps = (changes) => {
|
|
3370
|
+
const gaps = [];
|
|
3371
|
+
let started = false;
|
|
3372
|
+
let pausedAt;
|
|
3373
|
+
for (const change of changes) if (change.status === "started") {
|
|
3374
|
+
if (pausedAt !== void 0 && change.timestamp > pausedAt) gaps.push({
|
|
3375
|
+
start: pausedAt,
|
|
3376
|
+
end: change.timestamp
|
|
3377
|
+
});
|
|
3378
|
+
started = true;
|
|
3379
|
+
pausedAt = void 0;
|
|
3380
|
+
} else if (started && pausedAt === void 0 && !CLOSED_STATUSES.has(change.status)) pausedAt = change.timestamp;
|
|
3381
|
+
return gaps;
|
|
3382
|
+
};
|
|
3383
|
+
/** A question or answer in the task's log; other entries are edits the edit history already holds. */
|
|
3384
|
+
var exchangeMarker = (entry, id, laneId) => {
|
|
3385
|
+
const timestamp = Date.parse(entry.date);
|
|
3386
|
+
if (Number.isNaN(timestamp)) return;
|
|
3387
|
+
const base = {
|
|
3388
|
+
id,
|
|
3389
|
+
laneId,
|
|
3390
|
+
kind: "task",
|
|
3391
|
+
timestamp,
|
|
3392
|
+
detail: entry
|
|
3393
|
+
};
|
|
3394
|
+
switch (entry.event) {
|
|
3395
|
+
case "question": return {
|
|
3396
|
+
...base,
|
|
3397
|
+
label: entry.text,
|
|
3398
|
+
level: "warn"
|
|
3399
|
+
};
|
|
3400
|
+
case "answer": return {
|
|
3401
|
+
...base,
|
|
3402
|
+
label: `Answered: ${entry.answer}`
|
|
3403
|
+
};
|
|
3404
|
+
default: return;
|
|
3405
|
+
}
|
|
2412
3406
|
};
|
|
2413
3407
|
/**
|
|
2414
3408
|
* Builds the gantt-shaped view of one or more assistant sessions from their trace: session lanes
|
|
2415
3409
|
* (supervisors and the sub-agents they delegate to), task lanes, and the markers on each.
|
|
2416
3410
|
*/
|
|
2417
|
-
var buildSessionTimeline = ({ traceMessages, processes = [], sessions, tasks = [], now }) => {
|
|
3411
|
+
var buildSessionTimeline = ({ traceMessages, processes = [], sessions, tasks = [], taskStatusChanges: recordedChanges, now }) => {
|
|
2418
3412
|
const spans = flattenSpanTree(buildSpanTree(traceMessages));
|
|
2419
3413
|
const events = spans.flatMap((span) => span.events).sort((a, b) => a.timestamp - b.timestamp);
|
|
3414
|
+
const taskStatusChanges = recordedChanges && refineStatusChanges(recordedChanges, events);
|
|
2420
3415
|
const requestBegins = events.filter((event) => event.type === Trace.AgentRequestBegin.key && event.meta.pid !== void 0);
|
|
2421
3416
|
const agentPidsByChat = /* @__PURE__ */ new Map();
|
|
2422
3417
|
for (const process of processes) {
|
|
@@ -2510,12 +3505,13 @@ var buildSessionTimeline = ({ traceMessages, processes = [], sessions, tasks = [
|
|
|
2510
3505
|
const chatTaskIds = new Set(chatTasks.map((task) => task.id));
|
|
2511
3506
|
const taskLanes = /* @__PURE__ */ new Map();
|
|
2512
3507
|
for (const task of chatTasks) {
|
|
3508
|
+
const parentTask = Task.getParentTask(task);
|
|
2513
3509
|
const taskLane = {
|
|
2514
3510
|
id: taskLaneId(task.id),
|
|
2515
3511
|
kind: "task",
|
|
2516
3512
|
label: task.title,
|
|
2517
3513
|
status: taskLaneStatus(task, tasks),
|
|
2518
|
-
parentId: laneId,
|
|
3514
|
+
parentId: parentTask && chatTaskIds.has(parentTask.id) ? taskLaneId(parentTask.id) : laneId,
|
|
2519
3515
|
sessionId: source.session?.id,
|
|
2520
3516
|
taskId: task.id
|
|
2521
3517
|
};
|
|
@@ -2533,6 +3529,18 @@ var buildSessionTimeline = ({ traceMessages, processes = [], sessions, tasks = [
|
|
|
2533
3529
|
taskLane.start = Math.min(taskLane.start ?? segment.start, segment.start);
|
|
2534
3530
|
taskLane.end = segment.end === void 0 ? void 0 : Math.max(taskLane.end ?? segment.end, segment.end);
|
|
2535
3531
|
}
|
|
3532
|
+
for (const task of chatTasks) {
|
|
3533
|
+
const taskLane = taskLanes.get(task.id);
|
|
3534
|
+
const span = historySpan(taskStatusChanges?.get(task.id) ?? []);
|
|
3535
|
+
if (!taskLane || !span) continue;
|
|
3536
|
+
const tracedEnd = taskLane.start === void 0 ? void 0 : taskLane.end;
|
|
3537
|
+
taskLane.start = Math.min(taskLane.start ?? span.start, span.start);
|
|
3538
|
+
if (span.end === void 0 && task.status === "started") taskLane.end = void 0;
|
|
3539
|
+
else {
|
|
3540
|
+
const end = span.end ?? tracedEnd ?? span.last;
|
|
3541
|
+
taskLane.end = tracedEnd === void 0 ? end : Math.max(tracedEnd, end);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
2536
3544
|
for (const subPid of subAgentPids) {
|
|
2537
3545
|
const match = subAgentSpans.find((candidate) => candidate.pid === subPid);
|
|
2538
3546
|
const process = processByPid.get(subPid);
|
|
@@ -2568,20 +3576,83 @@ var buildSessionTimeline = ({ traceMessages, processes = [], sessions, tasks = [
|
|
|
2568
3576
|
} else lanes.push(subLane);
|
|
2569
3577
|
}
|
|
2570
3578
|
}
|
|
3579
|
+
const lanedTaskIds = new Set(lanes.flatMap((lane) => lane.taskId === void 0 ? [] : [lane.taskId]));
|
|
3580
|
+
const unownedLanes = /* @__PURE__ */ new Map();
|
|
3581
|
+
for (const task of tasks) {
|
|
3582
|
+
const span = lanedTaskIds.has(task.id) ? void 0 : historySpan(taskStatusChanges?.get(task.id) ?? []);
|
|
3583
|
+
if (!span) continue;
|
|
3584
|
+
unownedLanes.set(task.id, {
|
|
3585
|
+
id: taskLaneId(task.id),
|
|
3586
|
+
kind: "task",
|
|
3587
|
+
label: task.title,
|
|
3588
|
+
status: taskLaneStatus(task, tasks),
|
|
3589
|
+
start: span.start,
|
|
3590
|
+
end: span.end === void 0 && task.status === "started" ? void 0 : span.end ?? span.last,
|
|
3591
|
+
taskId: task.id
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
for (const [taskId, lane] of unownedLanes) {
|
|
3595
|
+
const blockedOn = (taskById.get(taskId)?.dependsOn ?? []).map((ref) => Task.refEntityId(ref)).filter((id) => id !== void 0 && unownedLanes.has(id)).map(taskLaneId);
|
|
3596
|
+
if (blockedOn.length > 0) lane.blockedOn = blockedOn;
|
|
3597
|
+
lanes.push(lane);
|
|
3598
|
+
}
|
|
2571
3599
|
for (const [sessionId, segments] of segmentsBySession) segmentsBySession.set(sessionId, segments.filter((segment) => !replacedTaskLanes.has(segment.laneId)));
|
|
3600
|
+
const tracedChanges = /* @__PURE__ */ new Map();
|
|
3601
|
+
for (const event of events) if ((event.meta.pid !== void 0 && laneByPid.has(event.meta.pid) || event.meta.parentPid !== void 0 && laneByPid.has(event.meta.parentPid)) && event.type === Trace.TaskStatusChanged.key) {
|
|
3602
|
+
const data = decode(Trace.TaskStatusChanged.schema, event.data);
|
|
3603
|
+
if (data) tracedChanges.set(data.taskId, [...tracedChanges.get(data.taskId) ?? [], event]);
|
|
3604
|
+
}
|
|
3605
|
+
const mergedEvents = /* @__PURE__ */ new Set();
|
|
3606
|
+
for (const lane of lanes) {
|
|
3607
|
+
const task = lane.taskId === void 0 ? void 0 : taskById.get(lane.taskId);
|
|
3608
|
+
if (!task) continue;
|
|
3609
|
+
const traced = tracedChanges.get(task.id) ?? [];
|
|
3610
|
+
const changes = taskStatusChanges?.get(task.id) ?? [];
|
|
3611
|
+
const firstStart = changes.findIndex((change) => change.status === "started");
|
|
3612
|
+
for (const [index, change] of changes.entries()) {
|
|
3613
|
+
if (PENDING_STATUSES.has(change.status) && (firstStart < 0 || index < firstStart)) continue;
|
|
3614
|
+
const match = traced.find((event) => !mergedEvents.has(event) && decode(Trace.TaskStatusChanged.schema, event.data)?.status === change.status && Math.abs(event.timestamp - change.timestamp) <= HISTORY_MATCH_MS);
|
|
3615
|
+
if (match) mergedEvents.add(match);
|
|
3616
|
+
markers.push({
|
|
3617
|
+
id: `${lane.id}:${markers.length}`,
|
|
3618
|
+
laneId: lane.id,
|
|
3619
|
+
kind: "task",
|
|
3620
|
+
timestamp: match?.timestamp ?? change.timestamp,
|
|
3621
|
+
label: `Task ${change.status}`,
|
|
3622
|
+
level: change.status === "failed" ? "error" : void 0,
|
|
3623
|
+
pid: match?.meta.pid,
|
|
3624
|
+
detail: change
|
|
3625
|
+
});
|
|
3626
|
+
}
|
|
3627
|
+
for (const entry of task.history ?? []) {
|
|
3628
|
+
const marker = exchangeMarker(entry, `${lane.id}:${markers.length}`, lane.id);
|
|
3629
|
+
if (marker) markers.push(marker);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
2572
3632
|
for (const lane of lanes) if (lane.blockedOn) lane.blockedOn = lane.blockedOn.map((id) => replacedTaskLanes.get(id) ?? id);
|
|
2573
3633
|
const tokens = /* @__PURE__ */ new Map();
|
|
2574
3634
|
const spawnMarkerByPid = /* @__PURE__ */ new Map();
|
|
3635
|
+
const returnMarkerByPid = /* @__PURE__ */ new Map();
|
|
2575
3636
|
for (const event of events) {
|
|
2576
3637
|
const laneId = (event.meta.pid && laneByPid.get(event.meta.pid)) ?? (event.meta.parentPid && laneByPid.get(event.meta.parentPid)) ?? void 0;
|
|
2577
3638
|
if (laneId === void 0) continue;
|
|
2578
3639
|
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);
|
|
3640
|
+
const marker = mergedEvents.has(event) ? void 0 : toMarker(event, `${markerLaneId}:${markers.length}`, markerLaneId);
|
|
2580
3641
|
if (marker) {
|
|
2581
3642
|
markers.push(marker);
|
|
2582
|
-
if (
|
|
3643
|
+
if (event.type === Trace.DelegationSpawned.key) {
|
|
2583
3644
|
const data = decode(Trace.DelegationSpawned.schema, event.data);
|
|
2584
|
-
if (data) spawnMarkerByPid.set(data.pid,
|
|
3645
|
+
if (data) spawnMarkerByPid.set(data.pid, {
|
|
3646
|
+
laneId: marker.laneId,
|
|
3647
|
+
markerId: marker.id
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3650
|
+
if (event.type === Trace.DelegationCompleted.key) {
|
|
3651
|
+
const data = decode(Trace.DelegationCompleted.schema, event.data);
|
|
3652
|
+
if (data) returnMarkerByPid.set(data.pid, {
|
|
3653
|
+
laneId: marker.laneId,
|
|
3654
|
+
markerId: marker.id
|
|
3655
|
+
});
|
|
2585
3656
|
}
|
|
2586
3657
|
}
|
|
2587
3658
|
if (event.type === Trace.CompleteBlock.key) {
|
|
@@ -2616,18 +3687,22 @@ var buildSessionTimeline = ({ traceMessages, processes = [], sessions, tasks = [
|
|
|
2616
3687
|
}
|
|
2617
3688
|
}
|
|
2618
3689
|
for (const { lane, sessionLaneId } of childSessions) {
|
|
2619
|
-
const
|
|
2620
|
-
|
|
3690
|
+
const fallbackId = markers.filter((marker) => marker.laneId === sessionLaneId && marker.timestamp <= (lane.start ?? 0)).at(-1)?.id;
|
|
3691
|
+
const spawn = (lane.pid === void 0 ? void 0 : spawnMarkerByPid.get(lane.pid)) ?? (fallbackId === void 0 ? void 0 : {
|
|
2621
3692
|
laneId: sessionLaneId,
|
|
2622
|
-
markerId
|
|
2623
|
-
};
|
|
3693
|
+
markerId: fallbackId
|
|
3694
|
+
});
|
|
3695
|
+
if (spawn) lane.delegatedFrom = spawn;
|
|
3696
|
+
const returned = lane.pid === void 0 ? void 0 : returnMarkerByPid.get(lane.pid);
|
|
3697
|
+
if (returned) lane.returnedTo = returned;
|
|
2624
3698
|
}
|
|
2625
3699
|
for (const session of lanes.filter((lane) => lane.kind === "session" && lane.parentId === void 0)) {
|
|
2626
3700
|
const children = lanes.filter((lane) => lane.parentId === session.id);
|
|
2627
3701
|
const [task] = children;
|
|
2628
|
-
if (children.length !== 1 || task.kind !== "task" || lanes.some((lane) => lane.blockedOn?.includes(task.id))) continue;
|
|
3702
|
+
if (children.length !== 1 || task.kind !== "task" || lanes.some((lane) => lane.parentId === task.id || lane.blockedOn?.includes(task.id))) continue;
|
|
2629
3703
|
lanes.splice(lanes.indexOf(task), 1);
|
|
2630
3704
|
session.taskId = task.taskId;
|
|
3705
|
+
if (task.start !== void 0) session.start = session.start === void 0 ? task.start : Math.min(session.start, task.start);
|
|
2631
3706
|
if (task.status === "blocked" || task.status === "review" || task.status === "pending") session.status = task.status;
|
|
2632
3707
|
markers.forEach((marker, index) => {
|
|
2633
3708
|
if (marker.laneId === task.id) markers[index] = {
|
|
@@ -2636,6 +3711,49 @@ var buildSessionTimeline = ({ traceMessages, processes = [], sessions, tasks = [
|
|
|
2636
3711
|
};
|
|
2637
3712
|
});
|
|
2638
3713
|
}
|
|
3714
|
+
const laneById = new Map(lanes.map((lane) => [lane.id, lane]));
|
|
3715
|
+
const laneByTaskId = new Map(lanes.flatMap((lane) => lane.taskId === void 0 ? [] : [[lane.taskId, lane]]));
|
|
3716
|
+
const ancestorLanesOf = (lane) => {
|
|
3717
|
+
const task = lane.taskId === void 0 ? void 0 : taskById.get(lane.taskId);
|
|
3718
|
+
const ancestors = [];
|
|
3719
|
+
const seen = new Set(task ? [task.id] : []);
|
|
3720
|
+
for (let parent = task && Task.getParentTask(task); parent && !seen.has(parent.id); parent = Task.getParentTask(parent)) {
|
|
3721
|
+
seen.add(parent.id);
|
|
3722
|
+
const parentLane = laneByTaskId.get(parent.id);
|
|
3723
|
+
if (parentLane?.kind === "task") ancestors.push(parentLane);
|
|
3724
|
+
}
|
|
3725
|
+
return ancestors;
|
|
3726
|
+
};
|
|
3727
|
+
for (const marker of [...markers]) {
|
|
3728
|
+
const lane = laneById.get(marker.laneId);
|
|
3729
|
+
if (lane?.kind !== "task" || !isStatusChange(marker.detail)) continue;
|
|
3730
|
+
const { status } = marker.detail;
|
|
3731
|
+
if (status !== "started" && !CLOSED_STATUSES.has(status)) continue;
|
|
3732
|
+
for (const ancestor of ancestorLanesOf(lane)) markers.push({
|
|
3733
|
+
...marker,
|
|
3734
|
+
id: `${ancestor.id}:sub:${marker.id}`,
|
|
3735
|
+
laneId: ancestor.id,
|
|
3736
|
+
label: `${lane.label}: ${status}`
|
|
3737
|
+
});
|
|
3738
|
+
}
|
|
3739
|
+
for (const lane of lanes) {
|
|
3740
|
+
const task = lane.taskId === void 0 ? void 0 : taskById.get(lane.taskId);
|
|
3741
|
+
if (!task || lane.start === void 0) continue;
|
|
3742
|
+
const seen = /* @__PURE__ */ new Set([task.id]);
|
|
3743
|
+
for (let parent = Task.getParentTask(task); parent && !seen.has(parent.id); parent = Task.getParentTask(parent)) {
|
|
3744
|
+
seen.add(parent.id);
|
|
3745
|
+
const parentLane = laneByTaskId.get(parent.id);
|
|
3746
|
+
if (parentLane?.start !== void 0 && lane.start < parentLane.start) parentLane.start = lane.start;
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
for (const lane of lanes) {
|
|
3750
|
+
const task = lane.taskId === void 0 ? void 0 : taskById.get(lane.taskId);
|
|
3751
|
+
if (task) {
|
|
3752
|
+
lane.hue = getHashHue(Obj.getMnemonic(task));
|
|
3753
|
+
const gaps = workGaps(taskStatusChanges?.get(task.id) ?? []);
|
|
3754
|
+
if (gaps.length > 0) lane.gaps = gaps;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
2639
3757
|
const times = [...lanes.flatMap((lane) => [lane.start, lane.end]).filter((time) => time !== void 0), ...markers.map((marker) => marker.timestamp)];
|
|
2640
3758
|
const hasOpen = lanes.some((lane) => lane.start !== void 0 && lane.end === void 0);
|
|
2641
3759
|
const latest = times.length > 0 ? Math.max(...times) : void 0;
|
|
@@ -2701,6 +3819,16 @@ var toMarker = (event, id, laneId) => {
|
|
|
2701
3819
|
detail: data
|
|
2702
3820
|
};
|
|
2703
3821
|
}
|
|
3822
|
+
case Trace.DelegationCompleted.key: {
|
|
3823
|
+
const data = decode(Trace.DelegationCompleted.schema, event.data);
|
|
3824
|
+
return {
|
|
3825
|
+
...base,
|
|
3826
|
+
kind: "delegation",
|
|
3827
|
+
label: data?.status === "failure" ? "Sub-agent failed" : `Returned${data?.result ? `: ${data.result}` : ""}`,
|
|
3828
|
+
level: data?.status === "failure" ? "error" : void 0,
|
|
3829
|
+
detail: data
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
2704
3832
|
case Trace.OperationStart.key: {
|
|
2705
3833
|
const data = decode(Trace.OperationStart.schema, event.data);
|
|
2706
3834
|
return {
|
|
@@ -2740,6 +3868,7 @@ var toMarker = (event, id, laneId) => {
|
|
|
2740
3868
|
//#endregion
|
|
2741
3869
|
//#region src/session-timeline/types.ts
|
|
2742
3870
|
var LaneKind = Schema.Literals(["session", "task"]);
|
|
3871
|
+
var LaneHue = Schema.Literals([palette.neutral.hue, ...palette.hues.map((style) => style.hue)]);
|
|
2743
3872
|
var LaneStatus = Schema.Literals([
|
|
2744
3873
|
"pending",
|
|
2745
3874
|
"blocked",
|
|
@@ -2785,13 +3914,25 @@ var Lane = Schema.Struct({
|
|
|
2785
3914
|
/** Epoch ms. */
|
|
2786
3915
|
start: Schema.optional(Schema.Number),
|
|
2787
3916
|
end: Schema.optional(Schema.Number),
|
|
3917
|
+
/**
|
|
3918
|
+
* Stretches inside `start`..`end` when the lane's task was put down — blocked on a question, say —
|
|
3919
|
+
* and later picked up again; the lane is drawn as separate runs around them.
|
|
3920
|
+
*/
|
|
3921
|
+
gaps: Schema.optional(Schema.Array(Schema.Struct({
|
|
3922
|
+
start: Schema.Number,
|
|
3923
|
+
end: Schema.Number
|
|
3924
|
+
}))),
|
|
2788
3925
|
parentId: Schema.optional(Schema.String),
|
|
2789
3926
|
sessionId: Schema.optional(Schema.String),
|
|
2790
3927
|
taskId: Schema.optional(Schema.String),
|
|
3928
|
+
/** The task's hue, hashed from its mnemonic, so the lane matches the task's mnemonic chip. */
|
|
3929
|
+
hue: Schema.optional(LaneHue),
|
|
2791
3930
|
pid: Schema.optional(Schema.String),
|
|
2792
3931
|
/** Lane ids this lane waits on (task dependencies). */
|
|
2793
3932
|
blockedOn: Schema.optional(Schema.Array(Schema.String)),
|
|
2794
3933
|
delegatedFrom: Schema.optional(DelegationSource),
|
|
3934
|
+
/** Node in the supervisor lane this lane's result was folded back into. */
|
|
3935
|
+
returnedTo: Schema.optional(DelegationSource),
|
|
2795
3936
|
tokens: Schema.optional(TokenUsage),
|
|
2796
3937
|
toolCalls: Schema.optional(Schema.Number)
|
|
2797
3938
|
});
|
|
@@ -2838,6 +3979,21 @@ var TICK_MS = 5e3;
|
|
|
2838
3979
|
var TRACE_DEBOUNCE = Duration.millis(500);
|
|
2839
3980
|
var NO_PROCESSES = [];
|
|
2840
3981
|
/**
|
|
3982
|
+
* A task's status moves, re-read only when its document has moved on: reading them diffs every change
|
|
3983
|
+
* in the document, and the timeline rebuilds on every trace emission and tick.
|
|
3984
|
+
*/
|
|
3985
|
+
var cachedStatusChanges = (cache, task) => {
|
|
3986
|
+
const heads = (Obj.version(task).automergeHeads ?? []).join(",");
|
|
3987
|
+
const cached = cache.get(task.id);
|
|
3988
|
+
if (cached?.heads === heads) return cached.changes;
|
|
3989
|
+
const changes = readTaskStatusChanges(task);
|
|
3990
|
+
cache.set(task.id, {
|
|
3991
|
+
heads,
|
|
3992
|
+
changes
|
|
3993
|
+
});
|
|
3994
|
+
return changes;
|
|
3995
|
+
};
|
|
3996
|
+
/**
|
|
2841
3997
|
* The live gantt-shaped view of the given sessions: rebuilt as trace events land on the space's
|
|
2842
3998
|
* trace feed and as the process tree changes, and ticked so open lanes keep extending.
|
|
2843
3999
|
*/
|
|
@@ -2848,13 +4004,18 @@ var useSessionTimeline = (space, { sessions, tasks, processes = NO_PROCESSES })
|
|
|
2848
4004
|
const interval = setInterval(() => setNow(Date.now()), TICK_MS);
|
|
2849
4005
|
return () => clearInterval(interval);
|
|
2850
4006
|
}, []);
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
4007
|
+
const statusChangeCache = useRef(/* @__PURE__ */ new Map());
|
|
4008
|
+
return useMemo(() => {
|
|
4009
|
+
const taskStatusChanges = new Map((tasks ?? []).map((task) => [task.id, cachedStatusChanges(statusChangeCache.current, task)]));
|
|
4010
|
+
return buildSessionTimeline({
|
|
4011
|
+
traceMessages,
|
|
4012
|
+
processes,
|
|
4013
|
+
sessions,
|
|
4014
|
+
tasks,
|
|
4015
|
+
taskStatusChanges,
|
|
4016
|
+
now
|
|
4017
|
+
});
|
|
4018
|
+
}, [
|
|
2858
4019
|
traceMessages,
|
|
2859
4020
|
processes,
|
|
2860
4021
|
sessions,
|
|
@@ -2863,6 +4024,6 @@ var useSessionTimeline = (space, { sessions, tasks, processes = NO_PROCESSES })
|
|
|
2863
4024
|
]);
|
|
2864
4025
|
};
|
|
2865
4026
|
//#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 };
|
|
4027
|
+
export { ALL_PROCESS_ENVIRONMENTS, BEGIN_EVENT_TYPES, CommitSelector, DEFAULT_PROCESS_ENVIRONMENTS, DEFAULT_SPAN_TIMEOUT_MS, DelegationSource, END_EVENT_TYPES, Gantt, Lane, LaneHue, 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, filterProcessesBySelection, filterTraceMessages, flattenSpanTree, formatElapsed, formatPendingBlockStatus, getTraceMessagesAtom, isSpanBeginEvent, isSpanEndEvent, orderRows, parseProcessEnvironments, pendingStatusFromEphemeralMessage, processEnvironment, readTaskStatusChanges, renderTimelineAscii, resolveEphemeralStatusUpdate, sessionTimelineToGantt, timeScale, toggleProcessEnvironment, unitScale, useExecutionGraph, useSessionTimeline, useTraceMenu, useTraceMessages, walkSpanTree };
|
|
2867
4028
|
|
|
2868
4029
|
//# sourceMappingURL=index.mjs.map
|