@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.
Files changed (115) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/index.mjs +1722 -561
  3. package/dist/lib/index.mjs.map +1 -1
  4. package/dist/lib/testing.mjs +9 -8
  5. package/dist/lib/testing.mjs.map +1 -1
  6. package/dist/lib/translations.mjs +5 -0
  7. package/dist/lib/translations.mjs.map +1 -1
  8. package/dist/types/src/components/Gantt/Gantt.d.ts +109 -24
  9. package/dist/types/src/components/Gantt/Gantt.d.ts.map +1 -1
  10. package/dist/types/src/components/Gantt/Gantt.stories.d.ts +73 -3
  11. package/dist/types/src/components/Gantt/Gantt.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Gantt/gantt-rows.d.ts +26 -0
  13. package/dist/types/src/components/Gantt/gantt-rows.d.ts.map +1 -0
  14. package/dist/types/src/components/Gantt/gantt-rows.test.d.ts +2 -0
  15. package/dist/types/src/components/Gantt/gantt-rows.test.d.ts.map +1 -0
  16. package/dist/types/src/components/Gantt/gantt-scale.d.ts +57 -0
  17. package/dist/types/src/components/Gantt/gantt-scale.d.ts.map +1 -0
  18. package/dist/types/src/components/Gantt/gantt-scale.test.d.ts +2 -0
  19. package/dist/types/src/components/Gantt/gantt-scale.test.d.ts.map +1 -0
  20. package/dist/types/src/components/Gantt/index.d.ts +2 -0
  21. package/dist/types/src/components/Gantt/index.d.ts.map +1 -1
  22. package/dist/types/src/components/Gantt/useEnter.d.ts +13 -0
  23. package/dist/types/src/components/Gantt/useEnter.d.ts.map +1 -0
  24. package/dist/types/src/components/Gantt/useRangeTween.d.ts +12 -0
  25. package/dist/types/src/components/Gantt/useRangeTween.d.ts.map +1 -0
  26. package/dist/types/src/components/ProcessTree/ProcessTree.d.ts +3 -3
  27. package/dist/types/src/components/ProcessTree/ProcessTree.d.ts.map +1 -1
  28. package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Timeline/Timeline.d.ts +10 -2
  30. package/dist/types/src/components/Timeline/Timeline.d.ts.map +1 -1
  31. package/dist/types/src/components/Timeline/Timeline.stories.d.ts +8 -4
  32. package/dist/types/src/components/Timeline/Timeline.stories.d.ts.map +1 -1
  33. package/dist/types/src/components/Timeline/timeline-layout.d.ts +35 -0
  34. package/dist/types/src/components/Timeline/timeline-layout.d.ts.map +1 -0
  35. package/dist/types/src/components/Timeline/timeline-layout.test.d.ts +2 -0
  36. package/dist/types/src/components/Timeline/timeline-layout.test.d.ts.map +1 -0
  37. package/dist/types/src/components/TracePanel/SpanTreeView.d.ts +15 -0
  38. package/dist/types/src/components/TracePanel/SpanTreeView.d.ts.map +1 -0
  39. package/dist/types/src/components/TracePanel/TracePanel.d.ts +3 -3
  40. package/dist/types/src/components/TracePanel/TracePanel.d.ts.map +1 -1
  41. package/dist/types/src/components/TracePanel/TracePanel.stories.d.ts +46 -0
  42. package/dist/types/src/components/TracePanel/TracePanel.stories.d.ts.map +1 -0
  43. package/dist/types/src/components/TracePanel/trace-filter.d.ts +7 -2
  44. package/dist/types/src/components/TracePanel/trace-filter.d.ts.map +1 -1
  45. package/dist/types/src/components/TracePanel/useTraceMenu.d.ts.map +1 -1
  46. package/dist/types/src/execution-graph/execution-graph.d.ts +1 -1
  47. package/dist/types/src/execution-graph/execution-graph.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useExecutionGraph.d.ts +2 -2
  49. package/dist/types/src/hooks/useExecutionGraph.d.ts.map +1 -1
  50. package/dist/types/src/hooks/useSessionTimeline.d.ts +1 -1
  51. package/dist/types/src/hooks/useSessionTimeline.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useTraceMessages.d.ts +1 -1
  53. package/dist/types/src/hooks/useTraceMessages.d.ts.map +1 -1
  54. package/dist/types/src/session-timeline/TaskHistory.stories.d.ts +12 -0
  55. package/dist/types/src/session-timeline/TaskHistory.stories.d.ts.map +1 -0
  56. package/dist/types/src/session-timeline/gantt-mapping.d.ts +14 -0
  57. package/dist/types/src/session-timeline/gantt-mapping.d.ts.map +1 -0
  58. package/dist/types/src/session-timeline/gantt-mapping.test.d.ts +2 -0
  59. package/dist/types/src/session-timeline/gantt-mapping.test.d.ts.map +1 -0
  60. package/dist/types/src/session-timeline/index.d.ts +1 -0
  61. package/dist/types/src/session-timeline/index.d.ts.map +1 -1
  62. package/dist/types/src/session-timeline/session-timeline.d.ts +17 -2
  63. package/dist/types/src/session-timeline/session-timeline.d.ts.map +1 -1
  64. package/dist/types/src/session-timeline/types.d.ts +32 -0
  65. package/dist/types/src/session-timeline/types.d.ts.map +1 -1
  66. package/dist/types/src/testing/simulated-agent.d.ts +5 -4
  67. package/dist/types/src/testing/simulated-agent.d.ts.map +1 -1
  68. package/dist/types/src/testing/test-trace.d.ts +2 -2
  69. package/dist/types/src/testing/test-trace.d.ts.map +1 -1
  70. package/dist/types/src/translations.d.ts +5 -0
  71. package/dist/types/src/translations.d.ts.map +1 -1
  72. package/dist/types/tsconfig.tsbuildinfo +1 -1
  73. package/package.json +30 -26
  74. package/src/components/Gantt/Gantt.stories.tsx +914 -96
  75. package/src/components/Gantt/Gantt.tsx +964 -261
  76. package/src/components/Gantt/gantt-rows.test.ts +123 -0
  77. package/src/components/Gantt/gantt-rows.ts +71 -0
  78. package/src/components/Gantt/gantt-scale.test.ts +105 -0
  79. package/src/components/Gantt/gantt-scale.ts +143 -0
  80. package/src/components/Gantt/index.ts +2 -0
  81. package/src/components/Gantt/useEnter.ts +33 -0
  82. package/src/components/Gantt/useRangeTween.ts +61 -0
  83. package/src/components/ProcessTree/ProcessTree.stories.tsx +2 -3
  84. package/src/components/ProcessTree/ProcessTree.tsx +99 -90
  85. package/src/components/Timeline/Timeline.stories.tsx +110 -22
  86. package/src/components/Timeline/Timeline.tsx +319 -271
  87. package/src/components/Timeline/timeline-layout.test.ts +177 -0
  88. package/src/components/Timeline/timeline-layout.ts +153 -0
  89. package/src/components/TracePanel/SpanTreeView.tsx +39 -0
  90. package/src/components/TracePanel/TracePanel.stories.tsx +106 -0
  91. package/src/components/TracePanel/TracePanel.tsx +113 -106
  92. package/src/components/TracePanel/trace-filter.test.ts +31 -2
  93. package/src/components/TracePanel/trace-filter.ts +28 -3
  94. package/src/components/TracePanel/useTraceMenu.ts +0 -1
  95. package/src/execution-graph/SPEC.md +10 -10
  96. package/src/execution-graph/execution-graph.test.ts +16 -15
  97. package/src/execution-graph/execution-graph.ts +108 -60
  98. package/src/execution-graph/pending-block-status.ts +1 -1
  99. package/src/execution-graph/sub-agent-delegation.test.ts +1 -1
  100. package/src/hooks/useExecutionGraph.ts +14 -11
  101. package/src/hooks/useSessionTimeline.ts +35 -9
  102. package/src/hooks/useTraceMessages.ts +1 -1
  103. package/src/session-timeline/TaskHistory.stories.tsx +142 -0
  104. package/src/session-timeline/gantt-mapping.test.ts +118 -0
  105. package/src/session-timeline/gantt-mapping.ts +158 -0
  106. package/src/session-timeline/index.ts +1 -0
  107. package/src/session-timeline/session-timeline.test.ts +585 -35
  108. package/src/session-timeline/session-timeline.ts +433 -62
  109. package/src/session-timeline/types.ts +14 -0
  110. package/src/testing/simulated-agent.ts +17 -16
  111. package/src/testing/test-trace.ts +14 -12
  112. package/src/translations.ts +8 -2
  113. package/dist/types/src/execution-graph/remote-snapshot.test.d.ts +0 -2
  114. package/dist/types/src/execution-graph/remote-snapshot.test.d.ts.map +0 -1
  115. package/src/execution-graph/remote-snapshot.test.ts +0 -116
@@ -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 { Accordion, HoverCard, Icon, IconButton, Panel, ScrollArea, ScrollContainer, Tooltip, composable, composableProps, useDynamicRef, useForwardedRef, useTranslation } from "@dxos/react-ui";
6
- import { mx } from "@dxos/ui-theme";
7
- import { Unit, trim } from "@dxos/util";
8
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
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/unstable/reactivity/Atom";
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 ROW_HEIGHT = 28;
264
+ var PAD_X = 16;
265
+ var ROW_HEIGHT = 24;
35
266
  var HEADER_HEIGHT = 20;
36
- var PAD_X = 12;
37
- var NODE_RADIUS = 5;
38
- /** Upper bound on axis ticks; the count shrinks with the width so `HH:mm:ss` labels never overlap. */
39
- var MAX_TICKS = 5;
40
- var TICK_MIN_WIDTH = 72;
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 session rectangles, halved on each. */
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 = 12;
52
- /** Radius of the delegation connector's bend from the drop into the child bar. */
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
- * Rows grouped by the session that processes them: a session, then the tasks (and subtasks) it
98
- * works itself, then — after that block — every session those tasks spawned, recursively. Keeping a
99
- * session's own rows contiguous is what lets one rectangle enclose them.
100
- */
101
- var orderRows = (lanes) => {
102
- const rows = [];
103
- const groups = [];
104
- const childrenOf = (parentId) => lanes.filter((lane) => lane.parentId === parentId);
105
- const visitSession = (session, depth) => {
106
- const first = rows.length;
107
- rows.push({
108
- lane: session,
109
- depth,
110
- index: rows.length,
111
- sessionId: session.id
112
- });
113
- const spawned = [];
114
- const visitTasks = (parentId, taskDepth) => {
115
- for (const child of childrenOf(parentId)) if (child.kind === "task") {
116
- rows.push({
117
- lane: child,
118
- depth: taskDepth,
119
- index: rows.length,
120
- sessionId: session.id
121
- });
122
- visitTasks(child.id, taskDepth + 1);
123
- } else spawned.push({
124
- lane: child,
125
- depth: taskDepth
126
- });
127
- };
128
- visitTasks(session.id, depth + 1);
129
- groups.push({
130
- session,
131
- first,
132
- last: rows.length - 1
133
- });
134
- for (const child of spawned) visitSession(child.lane, child.depth);
135
- };
136
- for (const lane of childrenOf(void 0)) if (lane.kind === "session") visitSession(lane, 0);
137
- else rows.push({
138
- lane,
139
- depth: 0,
140
- index: rows.length,
141
- sessionId: void 0
142
- });
143
- return {
144
- rows,
145
- groups
146
- };
147
- };
148
- var formatTokens = (tokens) => tokens.total >= 1e3 ? Unit.Thousand(tokens.total).toString() : String(tokens.total);
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 sessions and tasks on a shared time axis. The root resolves the rows and the axis;
152
- * the parts lay out side by side in the order given, sharing one row grid: `Legend` names the
153
- * lanes, `Chart` draws them, `Meta` shows per-lane totals. A host that already lists the lanes
154
- * renders the chart alone.
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 { rows, groups } = useMemo(() => orderRows(lanes), [lanes]);
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) => [lane.start, lane.end]), ...markers.map((marker) => marker.timestamp)].filter((time) => time !== void 0).concat(now === void 0 ? [] : [now]);
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
- groups,
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
- children: /* @__PURE__ */ jsx("div", {
186
- ...composableProps(props, { classNames: "flex w-full items-start text-xs font-mono overflow-hidden" }),
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
- /** The lane names, one per row, indented by depth; each row is the lane's keyboard path. */
194
- var GanttLegend = composable((props, forwardedRef) => {
195
- const { rows, onLaneSelect } = useGanttContext("Gantt.Legend");
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(20rem,40%)] min-w-40 flex flex-col" }),
198
- ref: forwardedRef,
199
- children: [/* @__PURE__ */ jsx("div", { style: { height: HEADER_HEIGHT } }), rows.map(({ lane, depth }) => /* @__PURE__ */ jsxs("div", {
200
- className: mx("flex items-center gap-2 truncate", onLaneSelect && "cursor-pointer hover:bg-hover-surface-subtle"),
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", { className: mx("shrink-0 w-2 h-2 rounded-full bg-current", STATUS_COLOR[lane.status].text) }), /* @__PURE__ */ jsx("span", {
217
- className: "truncate text-base-fg",
218
- children: lane.label
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
- /** Per-lane totals — tokens and tool calls — aligned to the rows. */
225
- var GanttMeta = composable((props, forwardedRef) => {
226
- const { rows } = useGanttContext("Gantt.Meta");
227
- return /* @__PURE__ */ jsxs("div", {
228
- ...composableProps(props, { classNames: "shrink-0 flex flex-col" }),
229
- ref: forwardedRef,
230
- children: [/* @__PURE__ */ jsx("div", { style: { height: HEADER_HEIGHT } }), rows.map(({ lane }) => /* @__PURE__ */ jsxs("div", {
231
- className: "flex items-center justify-end gap-2 px-2 text-subdued whitespace-nowrap",
232
- style: { height: ROW_HEIGHT },
233
- children: [lane.tokens && /* @__PURE__ */ jsx("span", {
234
- title: `${lane.tokens.input} in / ${lane.tokens.output} out`,
235
- children: formatTokens(lane.tokens)
236
- }), lane.toolCalls !== void 0 && /* @__PURE__ */ jsxs("span", { children: [lane.toolCalls, " tools"] })]
237
- }, lane.id))]
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
- GanttMeta.displayName = "Gantt.Meta";
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
- * The drawing: every lane is a rounded bar with its markers threaded through it as nodes, a session's
243
- * rectangle encloses the tasks it works, and connectors draw dependencies and delegations.
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, groups, rowById, markers, markerById, range, now, showNow, onLaneSelect, onMarkerSelect } = useGanttContext("Gantt.Chart");
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(svgRef, forwardedRef);
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 = svgRef.current;
574
+ const element = viewport;
252
575
  if (!element) return;
253
- const observer = new ResizeObserver(([entry]) => entry && setWidth(entry.contentRect.width));
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
- return () => observer.disconnect();
256
- }, []);
257
- const span = Math.max(range.end - range.start, 1);
258
- const x = (time) => PAD_X + (time - range.start) / span * (width - 24);
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 laneTimes = (lane) => markers.filter((marker) => marker.laneId === lane.id).map((marker) => marker.timestamp);
261
- const laneEnd = (lane) => {
262
- const times = laneTimes(lane);
263
- return times.length > 0 ? Math.max(...times) : lane.end ?? now;
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 delegationSource = (lane) => lane.delegatedFrom && markerById.get(lane.delegatedFrom.markerId);
266
- const barStart = (lane, start) => {
267
- const source = delegationSource(lane);
268
- const edge = x(start) - BAR_OVERHANG;
269
- return source ? Math.max(edge, x(source.timestamp) + 2 * BEND_RADIUS) : edge;
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 tickCount = Math.max(2, Math.min(MAX_TICKS, Math.floor((width - 24) / TICK_MIN_WIDTH)));
274
- const ticks = Array.from({ length: tickCount }, (_, index) => range.start + span * index / (tickCount - 1));
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("flex-1 min-w-0", classNames),
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((tick, index) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
281
- x1: x(tick),
282
- x2: x(tick),
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: x(tick),
811
+ x: at,
288
812
  y: 14,
289
- textAnchor: index === 0 ? "start" : index === tickCount - 1 ? "end" : "middle",
290
- className: "fill-current text-subdued",
291
- children: format(tick, "HH:mm:ss")
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
- groups.flatMap(({ session, first, last }) => {
302
- const members = rows.slice(first, last + 1).map((row) => row.lane);
303
- const starts = members.flatMap((lane) => lane.start === void 0 ? [] : [barStart(lane, lane.start)]);
304
- const ends = members.flatMap((lane) => {
305
- const end = lane.start === void 0 ? void 0 : laneEnd(lane);
306
- return end === void 0 ? [] : [x(end) + BAR_OVERHANG];
307
- });
308
- if (starts.length === 0 || ends.length === 0) return [];
309
- const left = Math.min(...starts) - GROUP_PAD;
310
- const right = Math.max(...ends) + GROUP_PAD;
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
- }, `group:${session.id}`)];
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
- const anchor = dep && (laneEnd(dep.lane) ?? dep.lane.start);
323
- if (!dep || anchor === void 0) return [];
324
- const anchorX = nodeX(dep.lane, anchor);
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.start === void 0 ? [/* @__PURE__ */ jsx("circle", {
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 = delegationSource(lane);
342
- const sourceRow = lane.delegatedFrom && rowById.get(lane.delegatedFrom.laneId);
343
- if (!source || !sourceRow || lane.start === void 0) return [];
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 firstNode = Math.min(...laneTimes(lane));
347
- const targetX = Number.isFinite(firstNode) ? nodeX(lane, firstNode) : barStart(lane, lane.start) + BAR_OVERHANG;
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: `M ${sourceX} ${rowY(sourceRow.index)} V ${y - BEND_RADIUS} Q ${sourceX} ${y} ${sourceX + BEND_RADIUS} ${y} H ${targetX}`,
870
+ d: path,
350
871
  fill: "none",
351
- className: "stroke-fuchsia-500"
352
- }, `delegation:${lane.id}`)];
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.map(({ lane, index }) => {
355
- const end = laneEnd(lane);
356
- if (lane.start === void 0 || end === void 0) return null;
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: barStart(lane, lane.start),
899
+ x: start,
359
900
  y: rowY(index) - BAR_HEIGHT / 2,
360
- width: Math.max(x(end) + BAR_OVERHANG - barStart(lane, lane.start), BAR_HEIGHT),
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: STATUS_COLOR[lane.status].fill
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.map(({ lane, index }) => {
377
- const times = laneTimes(lane);
378
- if (times.length < 2) return null;
379
- return /* @__PURE__ */ jsx("line", {
380
- x1: nodeX(lane, Math.min(...times)),
381
- x2: nodeX(lane, Math.max(...times)),
382
- y1: rowY(index),
383
- y2: rowY(index),
384
- className: STATUS_COLOR[lane.status].thread
385
- }, `thread:${lane.id}`);
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
- return row ? /* @__PURE__ */ jsxs(HoverCard.Root, { children: [/* @__PURE__ */ jsx(HoverCard.Trigger, {
390
- asChild: true,
391
- children: /* @__PURE__ */ jsx("circle", {
392
- cx: nodeX(row.lane, marker.timestamp),
393
- cy: rowY(row.index),
394
- r: NODE_RADIUS,
395
- className: mx("cursor-pointer stroke-base-surface transition-[stroke-width] hover:stroke-[3px] hover:stroke-base-fg", STATUS_COLOR[row.lane.status].node),
396
- onClick: () => onMarkerSelect?.(marker)
397
- })
398
- }), /* @__PURE__ */ jsx(HoverCard.Portal, { children: /* @__PURE__ */ jsxs(HoverCard.Content, {
399
- classNames: "p-2 max-w-72 text-xs font-mono",
400
- children: [
401
- /* @__PURE__ */ jsx("div", {
402
- className: "font-medium truncate",
403
- children: marker.label
404
- }),
405
- /* @__PURE__ */ jsxs("div", {
406
- className: "text-description",
407
- children: [
408
- marker.kind,
409
- " · ",
410
- format(marker.timestamp, "HH:mm:ss.SSS"),
411
- marker.level && marker.level !== "info" && /* @__PURE__ */ jsx("span", {
412
- className: mx("ms-2", marker.level === "error" ? "text-error-text" : "text-warning-text"),
413
- children: marker.level
414
- })
415
- ]
416
- }),
417
- /* @__PURE__ */ jsx("div", {
418
- className: "text-description truncate",
419
- children: row.lane.label
420
- }),
421
- /* @__PURE__ */ jsx(HoverCard.Arrow, {})
422
- ]
423
- }) })] }, marker.id) : null;
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
- Chart: GanttChart,
433
- Meta: GanttMeta
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 renderIcon = useMemo(() => makeIconRenderer(), []);
480
- const renderColumns = useMemo(() => makeColumnRenderer(onProcessTerminate), [onProcessTerminate]);
481
- return /* @__PURE__ */ jsx(ScrollArea.Root, {
482
- ...composableProps(props),
483
- thin: true,
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(ScrollArea.Viewport, { children: /* @__PURE__ */ jsx(Tree, {
1071
+ children: /* @__PURE__ */ jsx(Tree.Root, {
486
1072
  id: ROOT_ID,
487
1073
  model,
488
- density: "sm",
1074
+ virtual: "fixed",
1075
+ size: "sm",
489
1076
  selectionMode: "multiple",
490
- classNames: "text-sm tabular-nums font-thin",
491
- gridTemplateColumns: "[tree-row-start] var(--dx-control) minmax(0, 1fr) min-content min-content [tree-row-end]",
492
- renderIcon,
493
- renderColumns,
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
- /** Status glyph — animated, coloured and tooltipped per state, which `TreeItemDataProps.icon` cannot express. */
500
- var makeIconRenderer = () => ({ item: { process } }) => process === void 0 ? null : /* @__PURE__ */ jsx(Tooltip.Trigger, {
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: 4,
504
- synchronized: true,
505
- classNames: mx("shrink-0", process.state === Process.State.RUNNING && "animate-spin", process.state === Process.State.FAILED && "text-error-text", process.state === Process.State.SUCCEEDED && "text-success-text"),
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 getCommitIndex = (id) => commits.findIndex((c) => c.id === id);
616
- const getBranch = (id) => commits.find((c) => c.id === id)?.branch;
617
- /**
618
- * Assign branches to lanes, reusing lanes once a branch has been merged.
619
- * Unmerged branches keep their lane active (they may still be in progress).
620
- */
621
- const { branchLane, laneCount } = useMemo(() => {
622
- const visibleBranches = new Set(branches);
623
- const commitBranch = /* @__PURE__ */ new Map();
624
- for (const commit of commits) commitBranch.set(commit.id, commit.branch);
625
- const mergeRow = /* @__PURE__ */ new Map();
626
- for (let row = 0; row < commits.length; row++) {
627
- const commit = commits[row];
628
- for (const parentId of commit.parents ?? []) {
629
- const parentBranch = commitBranch.get(parentId);
630
- if (parentBranch && parentBranch !== commit.branch && visibleBranches.has(parentBranch)) mergeRow.set(parentBranch, Math.max(mergeRow.get(parentBranch) ?? row, row));
631
- }
632
- }
633
- const branchLane = /* @__PURE__ */ new Map();
634
- if (branches.length > 0) branchLane.set(branches[0], 0);
635
- const activeLanes = /* @__PURE__ */ new Set([0]);
636
- let maxLane = 0;
637
- for (let row = 0; row < commits.length; row++) {
638
- const commit = commits[row];
639
- if (visibleBranches.has(commit.branch) && !branchLane.has(commit.branch)) {
640
- let lane = 1;
641
- while (activeLanes.has(lane)) lane++;
642
- branchLane.set(commit.branch, lane);
643
- activeLanes.add(lane);
644
- maxLane = Math.max(maxLane, lane);
645
- }
646
- for (const [branch, endRow] of mergeRow) if (endRow === row && branch !== branches[0] && branchLane.has(branch)) activeLanes.delete(branchLane.get(branch));
647
- }
648
- return {
649
- branchLane,
650
- laneCount: maxLane + 1
651
- };
652
- }, [commits, branches]);
653
- const getBranchIndex = (branch) => branchLane.get(branch) ?? -1;
654
- /**
655
- * Create spans for each branch.
656
- */
657
- const spans = useMemo(() => {
658
- const spans = /* @__PURE__ */ new Map();
659
- commits.forEach((commit, index) => {
660
- let span = spans.get(commit.branch);
661
- if (!span) {
662
- span = {
663
- start: index,
664
- end: index
665
- };
666
- spans.set(commit.branch, span);
667
- } else span.end = index;
668
- const parents = commit.parents ?? [];
669
- for (const parent of parents) {
670
- const branch = getBranch(parent);
671
- if (branch && branch !== commit.branch) {
672
- span.start = Math.min(span.start, getCommitIndex(parent));
673
- if (parents.length > 1) {
674
- const parentSpan = spans.get(branch);
675
- if (parentSpan) parentSpan.end = Math.max(parentSpan.end, index);
676
- }
677
- }
678
- }
679
- });
680
- return spans;
681
- }, [commits, branches]);
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: "grid auto-rows-min outline-none" }),
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: commits.length < 1 ? /* @__PURE__ */ jsx("p", {
749
- className: "col-span-full text-description p-trim-md",
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
- }) : commits.map((commit, index) => {
752
- if (getBranchIndex(commit.branch) === -1) return null;
753
- const hasLink = !!commit.link && !!onSelect;
754
- const handleClick = () => {
755
- if (selectedRef.current === index) {
756
- selectedRef.current = void 0;
757
- setCurrent(void 0);
758
- onSelect?.(void 0);
759
- return;
760
- }
761
- setCurrent(index);
762
- selectedRef.current = index;
763
- onSelect?.(commit);
764
- };
765
- const message = debug ? JSON.stringify({
766
- id: commit.id,
767
- parents: commit.parents
768
- }) : commit.message;
769
- return /* @__PURE__ */ jsxs("div", {
770
- "data-index": index,
771
- "aria-current": current === index,
772
- className: mx("group/row col-span-full grid grid-cols-subgrid gap-1 px-[2px] overflow-hidden items-center cursor-pointer pe-2", "aria-current:bg-current-surface! hover:bg-hover-surface-subtle"),
773
- style: { height: `${options.lineHeight}px` },
774
- onClick: handleClick,
775
- children: [
776
- /* @__PURE__ */ jsx("div", {
777
- className: "px-2",
778
- children: /* @__PURE__ */ jsx(LineVector, {
779
- branchLane,
780
- laneCount,
781
- spans,
782
- index,
783
- commit,
784
- highlightedBranch,
785
- options
786
- })
787
- }),
788
- showIcon && /* @__PURE__ */ jsx(CommitIcon, { commit }),
789
- /* @__PURE__ */ jsx("div", {
790
- className: mx("text-sm truncate cursor-pointer text-description font-thin group-aria-current/row:text-current-fg hover:text-current-fg", hasLink && "underline decoration-dotted underline-offset-2"),
791
- children: hasShimmerEffect(commit) ? /* @__PURE__ */ jsx(Shimmer, { children: message }) : message
792
- }),
793
- showTimestamp && /* @__PURE__ */ jsx("div", {
794
- className: "text-xs tabular-nums items-center text-description font-thin",
795
- children: commit.timestamp && format(commit.timestamp, TIMESTAMP_FORMAT)
796
- })
797
- ]
798
- }, commit.id);
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: 4,
808
- synchronized: true,
809
- classNames: mx(commit.icon === "ph--spinner-gap--regular" && "animate-spin", commit.level !== void 0 && levelColors[commit.level])
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(({ branchLane, laneCount, spans, index, commit, highlightedBranch, options }) => {
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 = (c) => c * options.columnWidth + options.columnWidth / 2;
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-50"];
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 [selectedCommit, setSelectedCommit] = useState();
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.Toolbar, {
1186
- asChild: true,
1187
- children: /* @__PURE__ */ jsx(ActionToolbar, {
1188
- ...menu,
1189
- alwaysActive: true,
1190
- classNames: "justify-end"
1191
- })
1192
- }), /* @__PURE__ */ jsx(Panel.Content, { children: /* @__PURE__ */ jsx(Accordion.Root, {
1193
- items: SECTIONS,
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 rounded-none border-y-0",
1197
- children: ({ items }) => items.map((section) => {
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
- item: section,
1201
- classNames: "border-x-0",
1202
- children: [/* @__PURE__ */ jsx(Accordion.ItemHeader, {
1203
- hover: true,
1204
- children: /* @__PURE__ */ jsx("span", {
1205
- className: "text-sm text-description",
1206
- children: t("trace-processes.label")
1207
- })
1208
- }), /* @__PURE__ */ jsx(Accordion.ItemBody, {
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
- item: section,
1223
- classNames: mx("border-x-0 dx-grow flex flex-col", "[&>[data-part=item-content]]:dx-grow [&>[data-part=item-content]]:flex [&>[data-part=item-content]]:flex-col [&>[data-part=item-content]]:animate-none"),
1224
- children: [/* @__PURE__ */ jsx(Accordion.ItemHeader, {
1225
- hover: true,
1226
- children: /* @__PURE__ */ jsx("span", {
1227
- className: "text-sm text-description",
1228
- children: t("trace.label")
1229
- })
1230
- }), /* @__PURE__ */ jsx(Accordion.ItemBody, {
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
- thin: true,
1967
+ width: "thin",
1236
1968
  children: [
1237
- /* @__PURE__ */ jsx(ScrollContainer.Fade, {}),
1238
- /* @__PURE__ */ jsx(ScrollContainer.Viewport, { children: debug ? /* @__PURE__ */ jsx(JsonHighlighter, {
1239
- data: spanTree,
1240
- classNames: "text-xs"
1241
- }) : /* @__PURE__ */ jsx(Timeline, {
1242
- branches,
1243
- branch: currentBranch,
1244
- commits,
1245
- showTimestamp: true,
1246
- onSelect: handleCommitSelect
1247
- }) }),
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
- item: section,
1990
+ value: section.id,
1258
1991
  disabled: !commit,
1259
- classNames: "border-x-0",
1260
- children: [/* @__PURE__ */ jsx(Accordion.ItemHeader, {
1261
- hover: true,
1262
- children: /* @__PURE__ */ jsx("span", {
1263
- className: "block truncate text-sm text-description",
1264
- children: commit?.message ?? t("trace-details.label")
1265
- })
1266
- }), commit && /* @__PURE__ */ jsx(Accordion.ItemBody, {
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 = "/Users/burdon/Code/dxos/dxos/.claude/worktrees/resume-project-tasks-6ed628/packages/ui/react-ui-trace/src/execution-graph/execution-graph.ts";
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: 144,
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: 210,
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: 240,
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: 258,
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: 318,
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: 330,
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 pids = /* @__PURE__ */ new Set([rootPid]);
1837
- let expanded = true;
1838
- while (expanded) {
1839
- expanded = false;
1840
- for (const message of messages) {
1841
- const pid = message.meta.pid;
1842
- const parentPid = message.meta.parentPid;
1843
- if (pid && parentPid && pids.has(parentPid) && !pids.has(pid)) {
1844
- pids.add(pid);
1845
- expanded = true;
1846
- }
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 CommitSelector.branch(MAIN_BRANCH).pipe(CommitSelector.compose(CommitSelector.last()));
1940
- const ownBranch = branchOf(span);
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
- return CommitSelector.firstOf(CommitSelector.branch(ownBranch).pipe(CommitSelector.compose(CommitSelector.last())), beginId ? CommitSelector.id(beginId) : CommitSelector.filter(() => false), lastInSpanContext(findParentSpan(span)));
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.computeParents(CommitSelector.firstOf(CommitSelector.branch(process.pid).pipe(CommitSelector.compose(CommitSelector.last())), CommitSelector.branch(MAIN_BRANCH).pipe(CommitSelector.compose(CommitSelector.last())))),
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.computeParents(CommitSelector.branch(process.pid).pipe(CommitSelector.compose(CommitSelector.last()))),
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 = builder.computeParents(lastInSpanContext(null));
2734
+ parents = lastInSpanContext(null);
1998
2735
  } else if (collapsible) {
1999
2736
  branch = parentBranch;
2000
- parents = builder.computeParents(lastInSpanContext(parentSpan));
2737
+ parents = lastInSpanContext(parentSpan);
2001
2738
  } else if (isBeginEvent) {
2002
2739
  branch = isProcessBoundary ? ownBranch : parentBranch;
2003
- parents = builder.computeParents(lastInSpanContext(parentSpan));
2740
+ parents = lastInSpanContext(parentSpan);
2004
2741
  } else if (isEndEvent) {
2005
2742
  branch = isProcessBoundary ? MAIN_BRANCH : parentBranch;
2006
- parents = builder.computeParents(CommitSelector.unionAll(CommitSelector.branch(branch).pipe(CommitSelector.compose(CommitSelector.last())), CommitSelector.branch(ownBranch).pipe(CommitSelector.compose(CommitSelector.last()))));
2743
+ parents = parentIds(builder.tipOf(branch), builder.tipOf(ownBranch));
2007
2744
  } else {
2008
2745
  branch = ownBranch;
2009
- parents = builder.computeParents(lastInSpanContext(span));
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
- findCommits(selector) {
2163
- return selector.select(this.#commits);
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.#commits.findIndex((commit) => commit.id === commitId);
2174
- if (index < 0) return;
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
- this.#commits = pipe(this.#commits, Array$1.dedupeWith((a, b) => a.id === b.id), Array$1.map(Struct.evolve({ parents: (parents) => parents ? Array$1.filter(parents, (id) => this.#commits.some((commit) => commit.id === id)) : void 0 })));
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(Duration.millis(500)), Atom.map((processes) => processes.filter((process) => (process.state === Process.State.RUNNING || process.state === Process.State.HYBERNATING) && (selectedPids.length === 0 || selectedPids.includes(process.pid)))), Atom.withEquality(sameProcesses));
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 one segment per task worked, from the status events its task tools
2344
- * write. Only tasks in `taskIds` — the session's own checklist — take part: an agent is free to move
2345
- * a task belonging to nothing on this chart, and such an event must not close the segment that is
2346
- * open or move the boundary.
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
- * A `started` event opens a segment and closes the one still open — the agent keeps exactly one task
2349
- * in progress. Any other status ends the agent's work on the task: `todo` puts it down as surely as
2350
- * `done`, and whatever the agent does next is no longer that task's. A task can also finish without
2351
- * a start: delegation marks every task it hands over
2352
- * `started` before the agent's first turn, so the run's only event for that task is the one closing
2353
- * it. Such a task gets the stretch since the last boundary, which is where its work happened — but
2354
- * only on the transition out of `started` and only while no other task holds the stretch: a task
2355
- * merely dismissed (`todo` → `blocked`) claims nothing, a second close (`review` → `done`) mints
2356
- * nothing, and a close arriving after the next task has started leaves that task's segment alone
2357
- * rather than overlapping it.
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
- let open;
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 (open && open.taskId !== data.taskId) close(open, event.timestamp);
2374
- if (!open) {
2375
- open = {
2376
- taskId: data.taskId,
2377
- laneId: taskLaneId(data.taskId),
2378
- start: event.timestamp
2379
- };
2380
- segments.push(open);
2381
- boundary = event.timestamp;
2382
- }
2383
- } else {
2384
- const started = segments.findLast((candidate) => candidate.taskId === data.taskId && candidate.end === void 0);
2385
- if (started) close(started, event.timestamp);
2386
- else if (data.previousStatus === "started" && open === void 0) {
3296
+ if (!current) {
2387
3297
  const segment = {
2388
3298
  taskId: data.taskId,
2389
3299
  laneId: taskLaneId(data.taskId),
2390
- start: Math.min(boundary ?? event.timestamp, event.timestamp)
3300
+ start: event.timestamp
2391
3301
  };
2392
3302
  segments.push(segment);
2393
- close(segment, event.timestamp);
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. Segments meet where one task starts over another, so a
2401
- * status event is matched to its own task's segment, and any other event at a shared boundary
2402
- * goes to the later segment — the newcomer's start is the first thing the agent did on it.
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
- return segments.findLast(contains);
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 (marker.kind === "delegation") {
3643
+ if (event.type === Trace.DelegationSpawned.key) {
2583
3644
  const data = decode(Trace.DelegationSpawned.schema, event.data);
2584
- if (data) spawnMarkerByPid.set(data.pid, marker.id);
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 markerId = (lane.pid && spawnMarkerByPid.get(lane.pid)) ?? markers.filter((marker) => marker.laneId === sessionLaneId && marker.timestamp <= (lane.start ?? 0)).at(-1)?.id;
2620
- if (markerId !== void 0) lane.delegatedFrom = {
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
- return useMemo(() => buildSessionTimeline({
2852
- traceMessages,
2853
- processes,
2854
- sessions,
2855
- tasks,
2856
- now
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