@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
@@ -0,0 +1,118 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { Task } from '@dxos/types';
8
+
9
+ import { sessionTimelineToGantt } from './gantt-mapping.ts';
10
+ import { type SessionTimeline } from './types.ts';
11
+
12
+ describe('sessionTimelineToGantt', () => {
13
+ test('a session is the band around its tasks, with no row of its own', ({ expect }) => {
14
+ const timeline: SessionTimeline = {
15
+ lanes: [
16
+ { id: 'session:a', kind: 'session', label: 'Chat', status: 'running', start: 1, toolCalls: 2 },
17
+ { id: 'task:1', kind: 'task', label: 'Parent', status: 'running', start: 2, parentId: 'session:a' },
18
+ { id: 'task:2', kind: 'task', label: 'Child', status: 'done', start: 3, end: 4, parentId: 'task:1' },
19
+ ],
20
+ markers: [{ id: 'm', laneId: 'session:a', kind: 'request', timestamp: 1, label: 'Request started' }],
21
+ range: { start: 1, end: 4 },
22
+ };
23
+
24
+ const { groups, lanes, markers } = sessionTimelineToGantt(timeline);
25
+ // The session is the band alone: no row doubles it, and its own nodes have nowhere to go.
26
+ expect(groups).toEqual([{ id: 'session:a' }]);
27
+ expect(lanes.map(({ id, groupId, parentId }) => ({ id, groupId, parentId }))).toEqual([
28
+ { id: 'task:1', groupId: 'session:a', parentId: undefined },
29
+ { id: 'task:2', groupId: 'session:a', parentId: 'task:1' },
30
+ ]);
31
+ expect(markers).toEqual([]);
32
+ });
33
+
34
+ test('a session standing for one task is that task, drawn inside its own band', ({ expect }) => {
35
+ const timeline: SessionTimeline = {
36
+ lanes: [{ id: 'session:a', kind: 'session', label: 'Only', status: 'running', start: 1, taskId: 't' }],
37
+ markers: [{ id: 'm', laneId: 'session:a', kind: 'task', timestamp: 1, label: 'Task started' }],
38
+ range: { start: 1, end: 1 },
39
+ };
40
+
41
+ const { groups, lanes, markers } = sessionTimelineToGantt(timeline);
42
+ expect(groups).toEqual([{ id: 'session:a' }]);
43
+ expect(lanes).toMatchObject([{ id: 'session:a', groupId: 'session:a', label: 'Only' }]);
44
+ expect(markers?.map(({ laneId }) => laneId)).toEqual(['session:a']);
45
+ });
46
+
47
+ test('a lane breaks around each gap, so a question and its answer are separate runs', ({ expect }) => {
48
+ const timeline: SessionTimeline = {
49
+ lanes: [
50
+ { id: 'session:a', kind: 'session', label: 'Chat', status: 'running', start: 1 },
51
+ {
52
+ id: 'task:1',
53
+ kind: 'task',
54
+ label: 'Cup',
55
+ status: 'done',
56
+ start: 2,
57
+ end: 9,
58
+ gaps: [{ start: 4, end: 6 }],
59
+ parentId: 'session:a',
60
+ },
61
+ { id: 'task:2', kind: 'task', label: 'Open', status: 'running', start: 2, parentId: 'session:a' },
62
+ // Gaps reaching past either end are clipped to the lane, not dropped.
63
+ {
64
+ id: 'task:3',
65
+ kind: 'task',
66
+ label: 'Edges',
67
+ status: 'done',
68
+ start: 2,
69
+ end: 9,
70
+ gaps: [
71
+ { start: 2, end: 3 },
72
+ { start: 8, end: 12 },
73
+ ],
74
+ parentId: 'session:a',
75
+ },
76
+ ],
77
+ markers: [],
78
+ range: { start: 1, end: 9 },
79
+ };
80
+
81
+ const { lanes } = sessionTimelineToGantt(timeline);
82
+ expect(lanes.map(({ segments }) => segments)).toEqual([
83
+ [
84
+ { start: 2, end: 4 },
85
+ { start: 6, end: 9 },
86
+ ],
87
+ [{ start: 2 }],
88
+ [{ start: 3, end: 8 }],
89
+ ]);
90
+ });
91
+
92
+ test('an answered question waits until its answer; an open one has no wait yet', ({ expect }) => {
93
+ const task = Task.make({ title: 'Cup', status: 'blocked' });
94
+ const asked = Task.ask(task, { text: 'Which lot?' });
95
+ const open = Task.ask(task, { text: 'Which roast?' });
96
+ const answered = Task.answer(task, asked.id, 'Guji');
97
+ const timeline: SessionTimeline = {
98
+ lanes: [
99
+ { id: 'session:a', kind: 'session', label: 'Chat', status: 'running', start: 1 },
100
+ { id: 'task:1', kind: 'task', label: 'Cup', status: 'blocked', start: 2, parentId: 'session:a' },
101
+ ],
102
+ markers: [
103
+ { id: 'q1', laneId: 'task:1', kind: 'task', timestamp: 3, label: 'Which lot?', level: 'warn', detail: asked },
104
+ { id: 'q2', laneId: 'task:1', kind: 'task', timestamp: 4, label: 'Which roast?', level: 'warn', detail: open },
105
+ { id: 'a1', laneId: 'task:1', kind: 'task', timestamp: 5, label: 'Answered: Guji', detail: answered },
106
+ ],
107
+ range: { start: 1, end: 5 },
108
+ };
109
+
110
+ const byId = new Map((sessionTimelineToGantt(timeline).markers ?? []).map((marker) => [marker.id, marker]));
111
+ expect(byId.get('q1')?.wait).toEqual({ until: 'a1' });
112
+ expect(byId.get('q2')?.wait).toBeUndefined();
113
+ // The open one is pending until answered; the answered one is not.
114
+ expect(byId.get('q2')?.pending).toBe(true);
115
+ expect(byId.get('q1')?.pending).toBeUndefined();
116
+ expect(byId.get('a1')?.wait).toBeUndefined();
117
+ });
118
+ });
@@ -0,0 +1,158 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Schema from 'effect/Schema';
6
+
7
+ import { Task } from '@dxos/types';
8
+ import { Unit } from '@dxos/util';
9
+
10
+ import {
11
+ type GanttData,
12
+ type GanttGroup,
13
+ type GanttLane,
14
+ type GanttMarker,
15
+ type GanttMeta,
16
+ type GanttSegment,
17
+ } from '../components/Gantt/index.ts';
18
+ import { type Lane, type SessionTimeline, type TokenUsage } from './types.ts';
19
+
20
+ /**
21
+ * The one place the domain's nouns meet the chart's.
22
+ *
23
+ * The chart knows only groups, lanes, segments and markers; this file knows that a **process** is a
24
+ * group — the band around the lanes of the tasks it works, with no row of its own — a **task** is a
25
+ * lane inside it, a **subtask** nests under its parent task, and a **delegation** opens a lane out of
26
+ * the node that spawned it. Keeping the translation here is what lets either vocabulary change without
27
+ * dragging the other with it.
28
+ */
29
+ const formatTokens = (tokens: TokenUsage): string =>
30
+ tokens.total >= 1_000 ? Unit.Thousand(tokens.total).toString() : String(tokens.total);
31
+
32
+ /** Tokens and tool calls, formatted here because the chart renders `meta` without reading it. */
33
+ const laneMeta = (lane: Lane): GanttMeta[] => [
34
+ ...(lane.tokens
35
+ ? [{ label: formatTokens(lane.tokens), title: `${lane.tokens.input} in / ${lane.tokens.output} out` }]
36
+ : []),
37
+ ...(lane.toolCalls !== undefined ? [{ label: `${lane.toolCalls} tools` }] : []),
38
+ ];
39
+
40
+ /**
41
+ * A lane's stretch as segments: one run, split around each gap where its task was put down, so a
42
+ * question and its answer are separate runs with the wait between them left open.
43
+ */
44
+ const segmentsOf = (lane: Lane): GanttLane['segments'] => {
45
+ if (lane.start === undefined) {
46
+ return undefined;
47
+ }
48
+ const segments: GanttSegment[] = [];
49
+ let start = lane.start;
50
+ for (const gap of [...(lane.gaps ?? [])].sort((left, right) => left.start - right.start)) {
51
+ // Clipped to the lane: a folded session lane keeps its own span while taking its task's gaps.
52
+ const gapStart = Math.max(gap.start, start);
53
+ const gapEnd = lane.end === undefined ? gap.end : Math.min(gap.end, lane.end);
54
+ if (gapEnd <= gapStart) {
55
+ continue;
56
+ }
57
+ if (gapStart > start) {
58
+ segments.push({ start, end: gapStart });
59
+ }
60
+ start = gapEnd;
61
+ }
62
+ if (lane.end === undefined) {
63
+ segments.push({ start });
64
+ } else if (lane.end > start) {
65
+ segments.push({ start, end: lane.end });
66
+ }
67
+ return segments;
68
+ };
69
+
70
+ const isHistoryEntry = Schema.is(Task.HistoryEntry);
71
+
72
+ /**
73
+ * Maps a session timeline onto the chart's model.
74
+ *
75
+ * Every process becomes a band holding the lanes of the tasks it works; the process itself draws no
76
+ * row, so its own nodes (requests, tool calls no task claims) are dropped. A process standing for one
77
+ * task — a delegated sub-agent, or a session the builder folded its lone task into — is that task's
78
+ * lane, so it keeps its span, its nodes and the node it opened out of. A process spawned by another
79
+ * nests its band under its parent's — so causality is drawn by `openedFrom` while membership is
80
+ * `groupId` and nesting is `parentId`, three facts the timeline's single `parentId` used to carry.
81
+ */
82
+ export const sessionTimelineToGantt = (timeline: SessionTimeline): Pick<GanttData, 'groups' | 'lanes' | 'markers'> => {
83
+ const byId = new Map(timeline.lanes.map((lane) => [lane.id, lane]));
84
+ const groups: GanttGroup[] = [];
85
+ const lanes: GanttLane[] = [];
86
+
87
+ /** The band a lane belongs to: its own, for a process; its process's, for a task. */
88
+ const bandOf = (lane: Lane): string | undefined => {
89
+ if (lane.kind === 'session') {
90
+ return lane.id;
91
+ }
92
+ const parent = lane.parentId === undefined ? undefined : byId.get(lane.parentId);
93
+ return parent ? bandOf(parent) : undefined;
94
+ };
95
+
96
+ for (const lane of timeline.lanes) {
97
+ const meta = laneMeta(lane);
98
+ const facts = {
99
+ label: lane.label,
100
+ status: lane.status,
101
+ ...(segmentsOf(lane) ? { segments: segmentsOf(lane) } : {}),
102
+ ...(lane.delegatedFrom ? { openedFrom: lane.delegatedFrom } : {}),
103
+ ...(lane.returnedTo ? { closedInto: lane.returnedTo } : {}),
104
+ ...(lane.hue ? { hue: lane.hue } : {}),
105
+ ...(lane.blockedOn ? { blockedOn: lane.blockedOn } : {}),
106
+ ...(meta.length > 0 ? { meta } : {}),
107
+ };
108
+ const parent = lane.parentId === undefined ? undefined : byId.get(lane.parentId);
109
+
110
+ if (lane.kind === 'session') {
111
+ groups.push({
112
+ id: lane.id,
113
+ // A process spawned by another sits inside its parent's band; a task's parent resolves to the
114
+ // process working it, which is the band this one nests under.
115
+ ...(parent ? { parentId: bandOf(parent) } : {}),
116
+ });
117
+ if (lane.taskId === undefined) {
118
+ continue;
119
+ }
120
+ }
121
+
122
+ lanes.push({
123
+ id: lane.id,
124
+ groupId: bandOf(lane),
125
+ // A task nests under its parent task; one hanging off its process sits at the top of the band.
126
+ ...(lane.kind === 'task' && parent?.kind === 'task' ? { parentId: parent.id } : {}),
127
+ ...facts,
128
+ });
129
+ }
130
+
131
+ const drawn = new Set(lanes.map((lane) => lane.id));
132
+ // A question waits for its answer: each answer node names the question it closes, by the question's
133
+ // id in the task's history, so the pair is joined on the same lane.
134
+ const answerByQuestion = new Map<string, string>();
135
+ for (const marker of timeline.markers) {
136
+ if (isHistoryEntry(marker.detail) && Task.isAnswerEntry(marker.detail)) {
137
+ answerByQuestion.set(`${marker.laneId}:${marker.detail.questionId}`, marker.id);
138
+ }
139
+ }
140
+ const markers: GanttMarker[] = timeline.markers
141
+ .flatMap((marker) => (drawn.has(marker.laneId) ? [marker] : []))
142
+ .map((marker) => {
143
+ const question = isHistoryEntry(marker.detail) && Task.isQuestionEntry(marker.detail) ? marker.detail : undefined;
144
+ const until = question && answerByQuestion.get(`${marker.laneId}:${question.id}`);
145
+ return {
146
+ id: marker.id,
147
+ laneId: marker.laneId,
148
+ kind: marker.kind,
149
+ timestamp: marker.timestamp,
150
+ label: marker.label,
151
+ ...(marker.level ? { level: marker.level } : {}),
152
+ ...(until ? { wait: { until } } : {}),
153
+ ...(question && !until ? { pending: true } : {}),
154
+ };
155
+ });
156
+
157
+ return { groups, lanes, markers };
158
+ };
@@ -2,5 +2,6 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
+ export * from './gantt-mapping.ts';
5
6
  export * from './session-timeline.ts';
6
7
  export * from './types.ts';