@dxos/react-ui-trace 0.11.1 → 0.13.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 +3 -3
  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 +3 -2
  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 +5 -6
  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 +567 -19
  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 +3 -2
  111. package/src/testing/test-trace.ts +2 -2
  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,177 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import * as Trace from '@dxos/compute/Trace';
8
+
9
+ import { buildExecutionGraph } from '../../execution-graph/execution-graph.ts';
10
+ import subAgentFixture from '../../execution-graph/testing/sub-agent-delegation.json';
11
+ import { type TimelineLayout, layoutTimeline } from './timeline-layout.ts';
12
+ import { type Commit } from './Timeline.tsx';
13
+
14
+ describe('layoutTimeline', () => {
15
+ test('linear history keeps every commit in lane 0', ({ expect }) => {
16
+ const commits: Commit[] = [
17
+ { id: 'c1', branch: 'main', message: 'first' },
18
+ { id: 'c2', branch: 'main', message: 'second', parents: ['c1'] },
19
+ { id: 'c3', branch: 'main', message: 'third', parents: ['c2'] },
20
+ ];
21
+ expect(`\n${format(layoutTimeline(commits, ['main']))}\n`).toMatchInlineSnapshot(`
22
+ "
23
+ 0: lane 0 first
24
+ 1: lane 0 second
25
+ 2: lane 0 third
26
+ lanes (1): main -> 0
27
+ spans: main: 0..2
28
+ "
29
+ `);
30
+ });
31
+
32
+ test('a fork takes its own lane and the merge closes its span', ({ expect }) => {
33
+ const commits: Commit[] = [
34
+ { id: 'b', branch: 'main', message: 'baseline' },
35
+ { id: 'f0', branch: 'feature', message: 'start feature', parents: ['b'] },
36
+ { id: 'f1', branch: 'feature', message: 'implement', parents: ['f0'] },
37
+ { id: 'm', branch: 'main', message: 'merge feature', parents: ['f1', 'b'] },
38
+ ];
39
+ expect(`\n${format(layoutTimeline(commits, ['main', 'feature']))}\n`).toMatchInlineSnapshot(`
40
+ "
41
+ 0: lane 0 baseline
42
+ 1: lane 1 start feature
43
+ 2: lane 1 implement
44
+ 3: lane 0 merge feature
45
+ lanes (2): main -> 0, feature -> 1
46
+ spans: main: 0..3, feature: 0..3
47
+ "
48
+ `);
49
+ });
50
+
51
+ test('a lane is reused once its branch has merged', ({ expect }) => {
52
+ const commits: Commit[] = [
53
+ { id: 'b', branch: 'main', message: 'baseline' },
54
+ { id: 'x0', branch: 'first', message: 'first work', parents: ['b'] },
55
+ { id: 'm0', branch: 'main', message: 'merge first', parents: ['x0', 'b'] },
56
+ { id: 'y0', branch: 'second', message: 'second work', parents: ['m0'] },
57
+ { id: 'm1', branch: 'main', message: 'merge second', parents: ['y0', 'm0'] },
58
+ ];
59
+ expect(`\n${format(layoutTimeline(commits, ['main', 'first', 'second']))}\n`).toMatchInlineSnapshot(`
60
+ "
61
+ 0: lane 0 baseline
62
+ 1: lane 1 first work
63
+ 2: lane 0 merge first
64
+ 3: lane 1 second work
65
+ 4: lane 0 merge second
66
+ lanes (2): main -> 0, first -> 1, second -> 1
67
+ spans: main: 0..4, first: 0..2, second: 2..4
68
+ "
69
+ `);
70
+ });
71
+
72
+ test('two branches open at once take separate lanes', ({ expect }) => {
73
+ const commits: Commit[] = [
74
+ { id: 'b', branch: 'main', message: 'baseline' },
75
+ { id: 'x0', branch: 'first', message: 'first work', parents: ['b'] },
76
+ { id: 'y0', branch: 'second', message: 'second work', parents: ['b'] },
77
+ { id: 'm0', branch: 'main', message: 'merge first', parents: ['x0', 'b'] },
78
+ { id: 'm1', branch: 'main', message: 'merge second', parents: ['y0', 'm0'] },
79
+ ];
80
+ expect(`\n${format(layoutTimeline(commits, ['main', 'first', 'second']))}\n`).toMatchInlineSnapshot(`
81
+ "
82
+ 0: lane 0 baseline
83
+ 1: lane 1 first work
84
+ 2: lane 2 second work
85
+ 3: lane 0 merge first
86
+ 4: lane 0 merge second
87
+ lanes (3): main -> 0, first -> 1, second -> 2
88
+ spans: main: 0..4, first: 0..3, second: 0..4
89
+ "
90
+ `);
91
+ });
92
+
93
+ test('a branch outside the whitelist gets no lane and no row', ({ expect }) => {
94
+ const commits: Commit[] = [
95
+ { id: 'b', branch: 'main', message: 'baseline' },
96
+ { id: 'h0', branch: 'hidden', message: 'hidden work', parents: ['b'] },
97
+ { id: 'c1', branch: 'main', message: 'more', parents: ['b'] },
98
+ ];
99
+ const layout = layoutTimeline(commits, ['main']);
100
+ expect(`\n${format(layout)}\n`).toMatchInlineSnapshot(`
101
+ "
102
+ 0: lane 0 baseline
103
+ 2: lane 0 more
104
+ lanes (1): main -> 0
105
+ spans: main: 0..2, hidden: 0..1
106
+ "
107
+ `);
108
+ // The keyboard navigates in commit indices, so the hidden commit maps to no row.
109
+ expect([...layout.rowByCommitIndex.entries()]).toEqual([
110
+ [0, 0],
111
+ [2, 1],
112
+ ]);
113
+ });
114
+
115
+ test('a parent that is not in the list is ignored', ({ expect }) => {
116
+ const commits: Commit[] = [
117
+ { id: 'c1', branch: 'main', message: 'first', parents: ['pruned'] },
118
+ { id: 'f0', branch: 'feature', message: 'fork', parents: ['c1', 'pruned'] },
119
+ ];
120
+ expect(`\n${format(layoutTimeline(commits, ['main', 'feature']))}\n`).toMatchInlineSnapshot(`
121
+ "
122
+ 0: lane 0 first
123
+ 1: lane 1 fork
124
+ lanes (2): main -> 0, feature -> 1
125
+ spans: main: 0..1, feature: 0..1
126
+ "
127
+ `);
128
+ });
129
+
130
+ test('the sub-agent delegation fixture', ({ expect }) => {
131
+ // External JSON → typed at this boundary; `buildExecutionGraph` reads only `meta`/`events`.
132
+ const messages = (subAgentFixture as unknown as Trace.Message[])
133
+ .slice()
134
+ .sort((left, right) => (left.events[0]?.timestamp ?? 0) - (right.events[0]?.timestamp ?? 0));
135
+ const { commits, branches } = buildExecutionGraph({ traceMessages: messages });
136
+ const layout = layoutTimeline(commits, branches);
137
+
138
+ expect(layout.rows).toHaveLength(commits.length);
139
+ expect(`\n${layout.rows.map(({ commit }) => `lane ${layout.branchLane.get(commit.branch)}`).join('\n')}\n`)
140
+ .toMatchInlineSnapshot(`
141
+ "
142
+ lane 0
143
+ lane 1
144
+ lane 1
145
+ lane 1
146
+ lane 1
147
+ lane 1
148
+ lane 1
149
+ lane 2
150
+ lane 2
151
+ lane 2
152
+ lane 2
153
+ lane 2
154
+ lane 2
155
+ lane 2
156
+ lane 2
157
+ lane 2
158
+ lane 2
159
+ lane 0
160
+ lane 1
161
+ lane 0
162
+ lane 0
163
+ "
164
+ `);
165
+ });
166
+ });
167
+
168
+ /** One line per row (`lane message`), then the lanes and spans the pass assigned. */
169
+ const format = (layout: TimelineLayout): string => {
170
+ const rows = layout.rows.map(({ commit, index }) => {
171
+ const lane = layout.branchLane.get(commit.branch) ?? -1;
172
+ return `${index}: lane ${lane} ${commit.message}`;
173
+ });
174
+ const lanes = [...layout.branchLane.entries()].map(([branch, lane]) => `${branch} -> ${lane}`);
175
+ const spans = [...layout.spans.entries()].map(([branch, span]) => `${branch}: ${span.start}..${span.end}`);
176
+ return [...rows, `lanes (${layout.laneCount}): ${lanes.join(', ')}`, `spans: ${spans.join(', ')}`].join('\n');
177
+ };
@@ -0,0 +1,153 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Commit } from './Timeline.tsx';
6
+
7
+ /** The row range a branch's line covers, in commit indices. */
8
+ export type TimelineSpan = {
9
+ start: number;
10
+ end: number;
11
+ };
12
+
13
+ export type TimelineRow = {
14
+ commit: Commit;
15
+ /** Index into the commit list the layout was built from; `spans` share these coordinates. */
16
+ index: number;
17
+ };
18
+
19
+ export type TimelineLayout = {
20
+ /** One row per commit on a whitelisted branch, in commit order. */
21
+ rows: TimelineRow[];
22
+ /** Lane per branch; a branch outside the whitelist gets none, and so gets no rows. */
23
+ branchLane: Map<string, number>;
24
+ /** Lanes the row graphic has to be wide enough for. */
25
+ laneCount: number;
26
+ /** The span each branch's line covers. */
27
+ spans: Map<string, TimelineSpan>;
28
+ /** Row index per commit index — the keyboard navigates in commit indices but scrolls rows. */
29
+ rowByCommitIndex: Map<number, number>;
30
+ /** Commit index per commit id — a click names a commit, everything else speaks in indices. */
31
+ commitIndexById: Map<string, number>;
32
+ };
33
+
34
+ /**
35
+ * Lays a commit graph out into rows: which lane each branch draws in, how far each branch's line
36
+ * runs, and which commits get a row at all.
37
+ *
38
+ * Pure and linear in the number of commits: every lookup a commit makes about a parent, and every
39
+ * lane release, is served from an index built in one pass. Commits are assumed to be in topological
40
+ * order — a parent always precedes its children.
41
+ */
42
+ export const layoutTimeline = (commits: readonly Commit[], branches: readonly string[]): TimelineLayout => {
43
+ const visibleBranches = new Set(branches);
44
+ const firstBranch = branches[0];
45
+
46
+ const commitIndexById = new Map<string, number>();
47
+ commits.forEach((commit, index) => {
48
+ if (!commitIndexById.has(commit.id)) {
49
+ commitIndexById.set(commit.id, index);
50
+ }
51
+ });
52
+
53
+ const positionOf = (commitId: string): { index: number; branch: string } | undefined => {
54
+ const index = commitIndexById.get(commitId);
55
+ return index === undefined ? undefined : { index, branch: commits[index].branch };
56
+ };
57
+
58
+ // The row at which each branch is merged by another branch, and so may hand its lane back.
59
+ const mergeRow = new Map<string, number>();
60
+ commits.forEach((commit, row) => {
61
+ for (const parentId of commit.parents ?? []) {
62
+ const parent = positionOf(parentId);
63
+ if (parent && parent.branch !== commit.branch && visibleBranches.has(parent.branch)) {
64
+ mergeRow.set(parent.branch, row);
65
+ }
66
+ }
67
+ });
68
+
69
+ // Inverted so a row releases lanes with one lookup instead of a scan of every merge.
70
+ const releasedAtRow = new Map<number, string[]>();
71
+ for (const [branch, endRow] of mergeRow) {
72
+ if (branch === firstBranch) {
73
+ continue;
74
+ }
75
+ const released = releasedAtRow.get(endRow);
76
+ if (released) {
77
+ released.push(branch);
78
+ } else {
79
+ releasedAtRow.set(endRow, [branch]);
80
+ }
81
+ }
82
+
83
+ // Assign branches to lanes, reusing a lane once its branch has been merged. Unmerged branches
84
+ // keep their lane active — they may still be in progress.
85
+ const branchLane = new Map<string, number>();
86
+ if (firstBranch !== undefined) {
87
+ branchLane.set(firstBranch, 0);
88
+ }
89
+
90
+ const activeLanes = new Set<number>([0]);
91
+ let maxLane = 0;
92
+
93
+ commits.forEach((commit, row) => {
94
+ if (visibleBranches.has(commit.branch) && !branchLane.has(commit.branch)) {
95
+ let lane = 1;
96
+ while (activeLanes.has(lane)) {
97
+ lane++;
98
+ }
99
+ branchLane.set(commit.branch, lane);
100
+ activeLanes.add(lane);
101
+ maxLane = Math.max(maxLane, lane);
102
+ }
103
+
104
+ for (const branch of releasedAtRow.get(row) ?? []) {
105
+ const lane = branchLane.get(branch);
106
+ if (lane !== undefined) {
107
+ activeLanes.delete(lane);
108
+ }
109
+ }
110
+ });
111
+
112
+ // How far each branch's line runs: from its first commit (or the fork it came off) to its last
113
+ // commit (or the merge that absorbed it).
114
+ const spans = new Map<string, TimelineSpan>();
115
+ commits.forEach((commit, index) => {
116
+ let span = spans.get(commit.branch);
117
+ if (span) {
118
+ span.end = index;
119
+ } else {
120
+ span = { start: index, end: index };
121
+ spans.set(commit.branch, span);
122
+ }
123
+
124
+ const parents = commit.parents ?? [];
125
+ for (const parentId of parents) {
126
+ const parent = positionOf(parentId);
127
+ if (!parent || parent.branch === commit.branch) {
128
+ continue;
129
+ }
130
+
131
+ span.start = Math.min(span.start, parent.index);
132
+
133
+ // A merge extends the branch being merged in, down to this row.
134
+ if (parents.length > 1) {
135
+ const parentSpan = spans.get(parent.branch);
136
+ if (parentSpan) {
137
+ parentSpan.end = Math.max(parentSpan.end, index);
138
+ }
139
+ }
140
+ }
141
+ });
142
+
143
+ const rows: TimelineRow[] = [];
144
+ const rowByCommitIndex = new Map<number, number>();
145
+ commits.forEach((commit, index) => {
146
+ if (branchLane.has(commit.branch)) {
147
+ rowByCommitIndex.set(index, rows.length);
148
+ rows.push({ commit, index });
149
+ }
150
+ });
151
+
152
+ return { rows, branchLane, laneCount: maxLane + 1, spans, rowByCommitIndex, commitIndexById };
153
+ };
@@ -0,0 +1,39 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { useMemo } from 'react';
6
+
7
+ import { TextEditor } from '@dxos/react-ui-editor';
8
+ import * as Hooks from '@dxos/react-ui/Hooks';
9
+ import type * as Util from '@dxos/react-ui/Util';
10
+ import { createBasicExtensions, createThemeExtensions, folding, json } from '@dxos/ui-editor';
11
+ import { safeStringify } from '@dxos/util';
12
+
13
+ export type SpanTreeViewProps = Util.ThemedClassName<{
14
+ spanTree: unknown;
15
+ }>;
16
+
17
+ /**
18
+ * The trace's span tree as read-only JSON.
19
+ *
20
+ * CodeMirror puts only the lines in view into the DOM and parses the rest in time-sliced chunks, so
21
+ * showing a tree stops costing what the tree is worth — a highlighter renders every one of its lines
22
+ * in one synchronous pass. `TextEditor` rather than `Editor.View`: this pane has no toolbar, so the
23
+ * compound component's context would only be wiring nothing reads.
24
+ */
25
+ export const SpanTreeView = ({ classNames, spanTree }: SpanTreeViewProps) => {
26
+ const themeMode = Hooks.useThemeMode();
27
+ const value = useMemo(() => safeStringify(spanTree, undefined, 2), [spanTree]);
28
+ const extensions = useMemo(
29
+ () => [
30
+ createBasicExtensions({ readOnly: true, lineWrapping: false, search: true }),
31
+ createThemeExtensions({ themeMode, syntaxHighlighting: true }),
32
+ json(),
33
+ folding(),
34
+ ],
35
+ [themeMode],
36
+ );
37
+
38
+ return <TextEditor classNames={['text-xs', classNames]} value={value} extensions={extensions} />;
39
+ };
@@ -0,0 +1,106 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Option from 'effect/Option';
7
+ import React, { useMemo, useState } from 'react';
8
+
9
+ import * as Process from '@dxos/compute/Process';
10
+ import type * as Trace from '@dxos/compute/Trace';
11
+ import { Annotation } from '@dxos/echo';
12
+ import { URI } from '@dxos/keys';
13
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
14
+
15
+ import { buildExecutionGraph } from '../../execution-graph/index.ts';
16
+ import { makeProcess, subAgentDelegationFixture } from '../../testing/index.ts';
17
+ import { translations } from '../../translations.ts';
18
+ import { ALL_PROCESS_ENVIRONMENTS, type ProcessEnvironment, filterTraceMessages } from './trace-filter.ts';
19
+ import { TracePanel, type TracePanelProps } from './TracePanel.tsx';
20
+
21
+ // Raw Trace.Message[] captured from a live sub-agent delegation via `dxosDumpTrace()`. External
22
+ // JSON → typed at this boundary; `buildExecutionGraph` only reads `meta`/`events`.
23
+ const messages = (subAgentDelegationFixture as unknown as Trace.Message[])
24
+ .slice()
25
+ .sort((a, b) => (a.events[0]?.timestamp ?? 0) - (b.events[0]?.timestamp ?? 0));
26
+
27
+ /** One process per top-level pid in the fixture, so the tree has something to pick. */
28
+ const processes: Process.Process[] = [...new Set(messages.map((message) => message.meta.pid))]
29
+ .filter((pid): pid is string => pid !== undefined)
30
+ .map((pid, index) => {
31
+ const own = messages.filter((message) => message.meta.pid === pid);
32
+ const parentPid = own[0]?.meta.parentPid;
33
+ const name = own[0]?.meta.processName ?? `Process ${index + 1}`;
34
+ // The fixture's one agent: the other top-level pid is the chat-naming operation the UI ran.
35
+ const isAgent = parentPid === undefined && name === 'Agent';
36
+ return makeProcess({
37
+ pid: Process.ID.make(pid),
38
+ parentPid: parentPid === undefined ? null : Process.ID.make(parentPid),
39
+ name,
40
+ state: Process.State.SUCCEEDED,
41
+ startedAt: own[0]?.events[0]?.timestamp ?? 0,
42
+ completedAt: Option.some(own.at(-1)?.events.at(-1)?.timestamp ?? 0),
43
+ metrics: { wallTime: 1_200 * (index + 1), inputCount: 1, outputCount: 1 },
44
+ params: {
45
+ name,
46
+ annotations: Annotation.buildDictionary((dictionary) => {
47
+ if (isAgent) {
48
+ Annotation.setDictionary(dictionary, Process.HarnessHostAnnotation, true);
49
+ Annotation.setDictionary(dictionary, Process.TargetAnnotation, URI.make('dxn:echo:@:chat'));
50
+ }
51
+ }),
52
+ },
53
+ });
54
+ });
55
+
56
+ type StoryProps = Pick<TracePanelProps, 'debug'>;
57
+
58
+ /** The story holds what the app's container would: the selection, the environment filter, the graph. */
59
+ const DefaultStory = ({ debug }: StoryProps) => {
60
+ const [selected, setSelected] = useState<readonly string[]>([]);
61
+ const [environments, setEnvironments] = useState<readonly ProcessEnvironment[]>(ALL_PROCESS_ENVIRONMENTS);
62
+ const graph = useMemo(
63
+ () => buildExecutionGraph({ traceMessages: filterTraceMessages(messages, selected), activeProcesses: [] }),
64
+ [selected],
65
+ );
66
+
67
+ return (
68
+ <TracePanel
69
+ processes={processes}
70
+ graph={graph}
71
+ environments={environments}
72
+ onEnvironmentsChange={setEnvironments}
73
+ debug={debug}
74
+ selected={selected}
75
+ onSelectedChange={setSelected}
76
+ resolveLabel={(process) => (Process.isHarnessHost(process) ? 'Agent (sub-agent delegation)' : undefined)}
77
+ onProcessTerminate={() => {}}
78
+ onOpenLink={(uri) => {
79
+ // eslint-disable-next-line no-console
80
+ console.log('open', uri);
81
+ }}
82
+ />
83
+ );
84
+ };
85
+
86
+ const meta = {
87
+ title: 'ui/react-ui-trace/TracePanel',
88
+ component: DefaultStory,
89
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'w-(--dx-complementary-sidebar-size)' })],
90
+ parameters: { layout: 'fullscreen', translations },
91
+ } satisfies Meta<StoryProps>;
92
+
93
+ export default meta;
94
+
95
+ type Story = StoryObj<typeof meta>;
96
+
97
+ /**
98
+ * A captured sub-agent delegation: the supervisor, its tool calls and the spawned sub-agent, as the
99
+ * process tree and the commit graph. Click a process to narrow the trace to it; ⌘-click adds one.
100
+ */
101
+ export const Default: Story = { args: {} };
102
+
103
+ /** The span tree the graph was built from, in place of the timeline. */
104
+ export const Debug: Story = {
105
+ args: { debug: true },
106
+ };