@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
@@ -5,31 +5,27 @@
5
5
  import React, { useCallback, useDeferredValue, useMemo, useState } from 'react';
6
6
 
7
7
  import type * as Process from '@dxos/compute/Process';
8
- import {
9
- Accordion,
10
- Panel,
11
- ScrollContainer,
12
- type ThemedClassName,
13
- composable,
14
- composableProps,
15
- useTranslation,
16
- } from '@dxos/react-ui';
17
8
  import { ActionToolbar } from '@dxos/react-ui-menu';
18
9
  import { JsonHighlighter } from '@dxos/react-ui-syntax-highlighter';
10
+ import * as Accordion from '@dxos/react-ui/Accordion';
11
+ import * as Hooks from '@dxos/react-ui/Hooks';
12
+ import * as Panel from '@dxos/react-ui/Panel';
13
+ import * as ScrollContainer from '@dxos/react-ui/ScrollContainer';
14
+ import * as Util from '@dxos/react-ui/Util';
19
15
  import { mx } from '@dxos/ui-theme';
20
16
 
21
17
  import { type ExecutionGraph } from '../../execution-graph/index.ts';
22
18
  import { translationKey } from '../../translations.ts';
23
19
  import { ProcessTree, type ProcessTreeProps } from '../ProcessTree/index.ts';
24
20
  import { type Commit, Timeline } from '../Timeline/index.ts';
25
-
21
+ import { SpanTreeView } from './SpanTreeView.tsx';
26
22
  import { type ProcessEnvironment, filterProcesses } from './trace-filter.ts';
27
23
  import { useTraceMenu } from './useTraceMenu.ts';
28
24
 
29
- export type TracePanelProps = ThemedClassName<
25
+ export type TracePanelProps = Util.ThemedClassName<
30
26
  Pick<ProcessTreeProps, 'resolveLabel' | 'selected' | 'onSelectedChange' | 'onProcessTerminate'> & {
31
27
  /** The live process tree; narrowed here by `environments`. */
32
- processes: readonly Process.Info[];
28
+ processes: readonly Process.Process[];
33
29
  /** The trace as a commit graph, built by `useExecutionGraph` (already narrowed to `selected`). */
34
30
  graph: ExecutionGraph;
35
31
  /** Process environments shown; the toolbar's funnel edits it. */
@@ -47,7 +43,7 @@ export type TracePanelProps = ThemedClassName<
47
43
  * event. Presentation only — the host resolves the process monitor, the trace feed, settings and
48
44
  * navigation and hands them in as props.
49
45
  */
50
- export const TracePanel = composable<HTMLDivElement, TracePanelProps>(
46
+ export const TracePanel = Util.composable<HTMLDivElement, TracePanelProps>(
51
47
  (
52
48
  {
53
49
  classNames,
@@ -65,7 +61,7 @@ export const TracePanel = composable<HTMLDivElement, TracePanelProps>(
65
61
  },
66
62
  forwardedRef,
67
63
  ) => {
68
- const { t } = useTranslation(translationKey);
64
+ const { t } = Hooks.useTranslation(translationKey);
69
65
  const handleClearSelection = useCallback(() => onSelectedChange?.([]), [onSelectedChange]);
70
66
  const menu = useTraceMenu({
71
67
  selected: environments,
@@ -78,9 +74,16 @@ export const TracePanel = composable<HTMLDivElement, TracePanelProps>(
78
74
  // See the comment in `ProcessTreeContainer` for more details.
79
75
  const { branches, commits, spanTree, details } = useDeferredValue(graph);
80
76
 
81
- const [selectedCommit, setSelectedCommit] = useState<Commit | undefined>();
77
+ // A commit the graph no longer holds (the process selection moved) is no longer picked.
78
+ const [pickedCommit, setSelectedCommit] = useState<Commit | undefined>();
79
+ const selectedCommit = useMemo(
80
+ () => (pickedCommit && commits.some((commit) => commit.id === pickedCommit.id) ? pickedCommit : undefined),
81
+ [pickedCommit, commits],
82
+ );
82
83
  // Remembered across selections, so collapsing a section once keeps it collapsed.
83
84
  const [openSections, setOpenSections] = useState<string[]>(SECTIONS.map((section) => section.id));
85
+ // The timeline windows its rows against this scroller.
86
+ const [traceViewport, setTraceViewport] = useState<HTMLDivElement | null>(null);
84
87
  const handleCommitSelect = useCallback(
85
88
  (commit: Commit | undefined) => {
86
89
  setSelectedCommit(commit);
@@ -98,103 +101,107 @@ export const TracePanel = composable<HTMLDivElement, TracePanelProps>(
98
101
  }, [selected, branches]);
99
102
 
100
103
  return (
101
- <Panel.Root {...composableProps(props, { classNames: ['h-full', classNames] })} ref={forwardedRef}>
102
- <Panel.Toolbar asChild>
104
+ <Panel.Root {...Util.composableProps(props, { classNames: ['h-full', classNames] })} ref={forwardedRef}>
105
+ <Panel.Header>
103
106
  <ActionToolbar {...menu} alwaysActive classNames='justify-end' />
104
- </Panel.Toolbar>
107
+ </Panel.Header>
105
108
 
106
- <Panel.Content>
107
- <Accordion.Root<TraceSection>
108
- items={SECTIONS}
109
+ <Panel.Body>
110
+ <Accordion.Root
109
111
  value={openSections}
110
112
  onValueChange={setOpenSections}
111
- classNames='h-full min-h-0 rounded-none border-y-0'
113
+ classNames='h-full min-h-0'
114
+ border={false}
112
115
  >
113
- {({ items }) =>
114
- items.map((section) => {
115
- switch (section.id) {
116
- case 'processes':
117
- return (
118
- <Accordion.Item key={section.id} item={section} classNames='border-x-0'>
119
- <Accordion.ItemHeader hover>
120
- <span className='text-sm text-description'>{t('trace-processes.label')}</span>
121
- </Accordion.ItemHeader>
122
- <Accordion.ItemBody classNames='p-0'>
123
- <ProcessTreeContainer
124
- classNames='max-h-[8lh]'
125
- processes={processes}
126
- environments={environments}
127
- resolveLabel={resolveLabel}
128
- selected={selected}
129
- onSelectedChange={onSelectedChange}
130
- onProcessTerminate={onProcessTerminate}
116
+ {SECTIONS.map((section) => {
117
+ switch (section.id) {
118
+ case 'processes':
119
+ return (
120
+ <Accordion.Item key={section.id} value={section.id}>
121
+ <Accordion.ItemTrigger>
122
+ <span className='text-sm text-fg-muted'>{t('trace-processes.label')}</span>
123
+ </Accordion.ItemTrigger>
124
+ <Accordion.ItemContent classNames='p-0'>
125
+ <ProcessTreeContainer
126
+ classNames='max-h-[8lh]'
127
+ processes={processes}
128
+ environments={environments}
129
+ resolveLabel={resolveLabel}
130
+ selected={selected}
131
+ onSelectedChange={onSelectedChange}
132
+ onProcessTerminate={onProcessTerminate}
133
+ />
134
+ </Accordion.ItemContent>
135
+ </Accordion.Item>
136
+ );
137
+ case 'trace':
138
+ return (
139
+ // The trace takes the slack: item and body are flex columns so the scroll
140
+ // container inside gets a definite height to scroll within. The body's slide
141
+ // animation is off here — it ramps to a measured height, and this body's height
142
+ // comes from the flex slack, not its content.
143
+ <Accordion.Item
144
+ key={section.id}
145
+ value={section.id}
146
+ disabled
147
+ classNames={mx(
148
+ 'dx-grow flex flex-col',
149
+ '[&>[data-part=item-content]]:dx-grow [&>[data-part=item-content]]:flex [&>[data-part=item-content]]:flex-col [&>[data-part=item-content]]:animate-none',
150
+ )}
151
+ >
152
+ <Accordion.ItemTrigger>
153
+ <span className='text-sm text-fg-muted'>{t('trace.label')}</span>
154
+ </Accordion.ItemTrigger>
155
+ <Accordion.ItemContent classNames='dx-grow grid grid-rows-[minmax(0,1fr)]'>
156
+ {/* Opens at the tail and follows new entries while pinned; scrolling up unpins. */}
157
+ <ScrollContainer.Root pin>
158
+ <ScrollContainer.Content width='thin'>
159
+ <ScrollContainer.Fade classNames='h-8' />
160
+ <ScrollContainer.Viewport ref={setTraceViewport}>
161
+ {debug ? (
162
+ <SpanTreeView spanTree={spanTree} />
163
+ ) : (
164
+ <Timeline
165
+ branches={branches}
166
+ branch={currentBranch}
167
+ commits={commits}
168
+ showTimestamp
169
+ scroller={traceViewport}
170
+ onSelect={handleCommitSelect}
171
+ />
172
+ )}
173
+ </ScrollContainer.Viewport>
174
+ <ScrollContainer.ScrollDownButton />
175
+ </ScrollContainer.Content>
176
+ </ScrollContainer.Root>
177
+ </Accordion.ItemContent>
178
+ </Accordion.Item>
179
+ );
180
+ case 'details': {
181
+ const commit = debug ? undefined : selectedCommit;
182
+ return (
183
+ // With nothing selected the section stays as a plain, closed row.
184
+ <Accordion.Item key={section.id} value={section.id} disabled={!commit}>
185
+ <Accordion.ItemTrigger>
186
+ <span className='flex items-center truncate text-sm text-fg-muted'>
187
+ {t('trace-details.label')}
188
+ </span>
189
+ </Accordion.ItemTrigger>
190
+ {commit && (
191
+ <Accordion.ItemContent classNames='p-0'>
192
+ <JsonHighlighter
193
+ data={details[commit.id] ?? commit}
194
+ classNames='max-h-[20lh] text-xs p-1.5'
131
195
  />
132
- </Accordion.ItemBody>
133
- </Accordion.Item>
134
- );
135
- case 'trace':
136
- return (
137
- // The trace takes the slack: item and body are flex columns so the scroll
138
- // container inside gets a definite height to scroll within. The body's slide
139
- // animation is off here — it ramps to a measured height, and this body's height
140
- // comes from the flex slack, not its content.
141
- <Accordion.Item
142
- key={section.id}
143
- item={section}
144
- classNames={mx(
145
- 'border-x-0 dx-grow flex flex-col',
146
- '[&>[data-part=item-content]]:dx-grow [&>[data-part=item-content]]:flex [&>[data-part=item-content]]:flex-col [&>[data-part=item-content]]:animate-none',
147
- )}
148
- >
149
- <Accordion.ItemHeader hover>
150
- <span className='text-sm text-description'>{t('trace.label')}</span>
151
- </Accordion.ItemHeader>
152
- <Accordion.ItemBody classNames='dx-grow grid grid-rows-[minmax(0,1fr)]'>
153
- <ScrollContainer.Root pin>
154
- <ScrollContainer.Content thin>
155
- <ScrollContainer.Fade />
156
- <ScrollContainer.Viewport>
157
- {debug ? (
158
- <JsonHighlighter data={spanTree} classNames='text-xs' />
159
- ) : (
160
- <Timeline
161
- branches={branches}
162
- branch={currentBranch}
163
- commits={commits}
164
- showTimestamp
165
- onSelect={handleCommitSelect}
166
- />
167
- )}
168
- </ScrollContainer.Viewport>
169
- <ScrollContainer.ScrollDownButton />
170
- </ScrollContainer.Content>
171
- </ScrollContainer.Root>
172
- </Accordion.ItemBody>
173
- </Accordion.Item>
174
- );
175
- case 'details': {
176
- const commit = debug ? undefined : selectedCommit;
177
- return (
178
- // With nothing selected the section stays as a plain, closed row.
179
- <Accordion.Item key={section.id} item={section} disabled={!commit} classNames='border-x-0'>
180
- <Accordion.ItemHeader hover>
181
- <span className='block truncate text-sm text-description'>
182
- {commit?.message ?? t('trace-details.label')}
183
- </span>
184
- </Accordion.ItemHeader>
185
- {commit && (
186
- <Accordion.ItemBody classNames='p-0'>
187
- <JsonHighlighter data={details[commit.id] ?? commit} classNames='max-h-[20lh] text-xs' />
188
- </Accordion.ItemBody>
189
- )}
190
- </Accordion.Item>
191
- );
192
- }
196
+ </Accordion.ItemContent>
197
+ )}
198
+ </Accordion.Item>
199
+ );
193
200
  }
194
- })
195
- }
201
+ }
202
+ })}
196
203
  </Accordion.Root>
197
- </Panel.Content>
204
+ </Panel.Body>
198
205
  </Panel.Root>
199
206
  );
200
207
  },
@@ -208,9 +215,9 @@ const SECTIONS: TraceSection[] = [{ id: 'processes' }, { id: 'trace' }, { id: 'd
208
215
 
209
216
  const NO_SELECTION: readonly string[] = [];
210
217
 
211
- type ProcessTreeContainerProps = ThemedClassName<
218
+ type ProcessTreeContainerProps = Util.ThemedClassName<
212
219
  Pick<ProcessTreeProps, 'resolveLabel' | 'selected' | 'onSelectedChange' | 'onProcessTerminate'> & {
213
- processes: readonly Process.Info[];
220
+ processes: readonly Process.Process[];
214
221
  environments: readonly ProcessEnvironment[];
215
222
  }
216
223
  >;
@@ -10,12 +10,12 @@ import { Obj } from '@dxos/echo';
10
10
  import { SpaceId, URI } from '@dxos/keys';
11
11
 
12
12
  import { makeProcess } from '../../testing/index.ts';
13
-
14
13
  import {
15
14
  ALL_PROCESS_ENVIRONMENTS,
16
15
  DEFAULT_PROCESS_ENVIRONMENTS,
17
16
  ProcessEnvironment,
18
17
  filterProcesses,
18
+ filterProcessesBySelection,
19
19
  filterTraceMessages,
20
20
  parseProcessEnvironments,
21
21
  processEnvironment,
@@ -99,7 +99,7 @@ describe('parseProcessEnvironments', () => {
99
99
  });
100
100
  });
101
101
 
102
- const makeInfo = (name: string, environment: Process.Environment): Process.Info =>
102
+ const makeInfo = (name: string, environment: Process.Environment): Process.Process =>
103
103
  makeProcess({ pid: Process.ID.make(name), name, state: Process.State.RUNNING, environment });
104
104
 
105
105
  describe('filterTraceMessages', () => {
@@ -135,3 +135,32 @@ describe('filterTraceMessages', () => {
135
135
  expect(filterTraceMessages(messages, ['z'])).toEqual([]);
136
136
  });
137
137
  });
138
+
139
+ describe('filterProcessesBySelection', () => {
140
+ const process = (pid: string, parentPid?: string): Process.Process =>
141
+ makeProcess({
142
+ pid: Process.ID.make(pid),
143
+ parentPid: parentPid === undefined ? null : Process.ID.make(parentPid),
144
+ name: pid,
145
+ state: Process.State.RUNNING,
146
+ });
147
+
148
+ test('an empty selection is no filter', ({ expect }) => {
149
+ const processes = [process('a'), process('b')];
150
+ expect(filterProcessesBySelection(processes, [])).toBe(processes);
151
+ });
152
+
153
+ test('a selected process keeps its descendants and drops the rest', ({ expect }) => {
154
+ const processes = [process('agent'), process('tool', 'agent'), process('nested', 'tool'), process('other')];
155
+ expect(filterProcessesBySelection(processes, ['agent']).map((info) => info.pid)).toEqual([
156
+ 'agent',
157
+ 'tool',
158
+ 'nested',
159
+ ]);
160
+ });
161
+
162
+ test('a cyclic parent chain terminates', ({ expect }) => {
163
+ const processes = [process('a', 'b'), process('b', 'a')];
164
+ expect(filterProcessesBySelection(processes, ['z'])).toEqual([]);
165
+ });
166
+ });
@@ -10,6 +10,7 @@ import type * as Trace from '@dxos/compute/Trace';
10
10
  *
11
11
  * Bucketed from the inherited {@link Process.Environment}, so filtering keeps whole subtrees together.
12
12
  */
13
+ // TODO(burdon): Factor out.
13
14
  export const ProcessEnvironment = {
14
15
  /** Not scoped to a space: layout, navigation, identity, settings, space lifecycle. */
15
16
  App: 'app',
@@ -43,7 +44,7 @@ const ENVIRONMENT_ICONS: Record<ProcessEnvironment, string> = {
43
44
  export const environmentIcon = (environment: ProcessEnvironment): string => ENVIRONMENT_ICONS[environment];
44
45
 
45
46
  /** `conversation` outranks `space`, since a conversation always runs inside one. */
46
- export const processEnvironment = (process: Process.Info): ProcessEnvironment => {
47
+ export const processEnvironment = (process: Process.Process): ProcessEnvironment => {
47
48
  if (process.environment.conversation !== undefined) {
48
49
  return ProcessEnvironment.Conversation;
49
50
  }
@@ -54,9 +55,9 @@ export const processEnvironment = (process: Process.Info): ProcessEnvironment =>
54
55
  };
55
56
 
56
57
  export const filterProcesses = (
57
- processes: readonly Process.Info[],
58
+ processes: readonly Process.Process[],
58
59
  selected: readonly ProcessEnvironment[],
59
- ): readonly Process.Info[] => {
60
+ ): readonly Process.Process[] => {
60
61
  // Identity when nothing is excluded, so `ProcessTree`'s `React.memo` still holds.
61
62
  if (selected.length === ALL_PROCESS_ENVIRONMENTS.length) {
62
63
  return processes;
@@ -89,6 +90,30 @@ export const parseProcessEnvironments = (selected: readonly string[] | undefined
89
90
  * runs as a child of the process it serves, so its events are that process's work. An empty
90
91
  * selection is no filter.
91
92
  */
93
+ /**
94
+ * Narrows the process tree to the selected processes and their descendants, the process-side twin of
95
+ * {@link filterTraceMessages}: a running child of a picked process is that process's live work.
96
+ */
97
+ export const filterProcessesBySelection = (
98
+ processes: readonly Process.Process[],
99
+ selected: readonly string[],
100
+ ): readonly Process.Process[] => {
101
+ if (selected.length === 0) {
102
+ return processes;
103
+ }
104
+ const parentByPid = new Map<string, string | null>(processes.map((process) => [process.pid, process.parentPid]));
105
+ const selection = new Set(selected);
106
+ const isSelected = (pid: string, depth = 0): boolean => {
107
+ if (selection.has(pid)) {
108
+ return true;
109
+ }
110
+ const parent = parentByPid.get(pid);
111
+ // Bounded so a cyclic parent chain (a corrupt tree) terminates.
112
+ return parent !== null && parent !== undefined && depth < processes.length && isSelected(parent, depth + 1);
113
+ };
114
+ return processes.filter((process) => isSelected(process.pid));
115
+ };
116
+
92
117
  export const filterTraceMessages = (
93
118
  messages: readonly Trace.Message[],
94
119
  selected: readonly string[],
@@ -7,7 +7,6 @@ import { useCallback } from 'react';
7
7
  import { MenuBuilder, useMenuBuilder } from '@dxos/react-ui-menu';
8
8
 
9
9
  import { translationKey } from '../../translations.ts';
10
-
11
10
  import {
12
11
  ALL_PROCESS_ENVIRONMENTS,
13
12
  type ProcessEnvironment,
@@ -1,13 +1,13 @@
1
1
  # Trace Execution Graph — Specification
2
2
 
3
3
  This document specifies the pure-data layer that powers the `TracePanel` Timeline view.
4
- Source: `packages/plugins/plugin-assistant/src/execution-graph/`.
4
+ Source: `packages/ui/react-ui-trace/src/execution-graph/`.
5
5
 
6
6
  ## 1. Purpose & scope
7
7
 
8
8
  The module converts a flat list of `Trace.Message` records (and a snapshot of active
9
9
  processes) into a Mercurial/Git-style commit graph consumable by the
10
- `@dxos/react-ui-components` `Timeline` component.
10
+ `Timeline` component (`../components/Timeline`).
11
11
 
12
12
  In scope:
13
13
 
@@ -25,7 +25,7 @@ Out of scope:
25
25
  - Filtering / search of commits.
26
26
  - Mutating, ack-ing, or replaying trace events.
27
27
 
28
- Inputs: `traceMessages: Trace.Message[]`, `activeProcesses?: Process.Info[]`, `eventLimit?: number`,
28
+ Inputs: `traceMessages: Trace.Message[]`, `activeProcesses?: Process.Process[]`, `eventLimit?: number`,
29
29
  `spanTimeoutMs?: number`, `now?: number`.
30
30
  Output: `ExecutionGraph = { branches, commits, spanTree, details }`.
31
31
 
@@ -57,7 +57,7 @@ forwarded as `SpanMeta` for downstream display but never branch or anchor logic.
57
57
  `isEphemeral` is preserved on `FlatEvent` but is not consulted; ephemeral events
58
58
  participate exactly like persistent events.
59
59
 
60
- ### 2.2 `Process.Info` (active processes)
60
+ ### 2.2 `Process.Process` (active processes)
61
61
 
62
62
  Defined in `packages/core/compute/compute/src/Process.ts#L365`.
63
63
  Optional input via `activeProcesses` (defaults to `[]`). Fields the builder reads:
@@ -106,7 +106,7 @@ export type ExecutionGraphDetailsMap = Record<string, Trace.FlatEvent | undefine
106
106
 
107
107
  ### 3.2 `Commit` (set fields only)
108
108
 
109
- From `packages/ui/react-ui-components/src/components/Timeline/Timeline.tsx#L40`. The
109
+ From `packages/ui/react-ui-trace/src/components/Timeline/Timeline.tsx#L40`. The
110
110
  builder sets:
111
111
 
112
112
  - `id` — see §9.
@@ -681,13 +681,13 @@ Each entry: **scenario → expected output → rationale**.
681
681
 
682
682
  | Symbol | File | Line |
683
683
  | ------------------------------------------------------- | ---------------------------------------------------------------------------- | ----------- |
684
- | `buildSpanTree` | `packages/plugins/plugin-assistant/src/execution-graph/span-tree.ts` | L112 |
684
+ | `buildSpanTree` | `packages/ui/react-ui-trace/src/execution-graph/span-tree.ts` | L112 |
685
685
  | `applyEventLimit` | same | L211 |
686
686
  | `makeTimeoutEndEvent` / `DEFAULT_SPAN_TIMEOUT_MS` | same | L252 / L13 |
687
687
  | `ROOT_SPAN_ID` | same | L12 |
688
688
  | `BEGIN_EVENT_TYPES`/`END_EVENT_TYPES` | same | L47 |
689
689
  | `walkSpanTree`/`flattenSpanTree` | same | L245 |
690
- | `buildExecutionGraph` | `packages/plugins/plugin-assistant/src/execution-graph/execution-graph.ts` | L91 |
690
+ | `buildExecutionGraph` | `packages/ui/react-ui-trace/src/execution-graph/execution-graph.ts` | L91 |
691
691
  | `presentEvent` | same | L118 |
692
692
  | `isCollapsibleSpan` | same | L206 |
693
693
  | `spanTreeToCommits` | same | L239 |
@@ -696,10 +696,10 @@ Each entry: **scenario → expected output → rationale**.
696
696
  | `CommitSelector` | same | L446 |
697
697
  | `GraphBuilder` / `doctor` | same | L579 / L610 |
698
698
  | `Trace.Meta` / `Trace.flatten` | `packages/core/compute/compute/src/Trace.ts` | L102 / L178 |
699
- | `Process.Info` / `Process.State` | `packages/core/compute/compute/src/Process.ts` | L365 / L321 |
699
+ | `Process.Process` / `Process.State` | `packages/core/compute/compute/src/Process.ts` | L365 / L321 |
700
700
  | `AGENT_PROCESS_KEY` | `packages/core/compute/functions-runtime/src/agent-service/agent-process.ts` | L54 |
701
- | `Commit` type | `packages/ui/react-ui-components/src/components/Timeline/Timeline.tsx` | L40 |
702
- | `TracePanel` `useExecutionGraph` / default `eventLimit` | `packages/plugins/plugin-assistant/src/containers/TracePanel/TracePanel.tsx` | L138 / L153 |
701
+ | `Commit` type | `packages/ui/react-ui-trace/src/components/Timeline/Timeline.tsx` | L40 |
702
+ | `TracePanel` `useExecutionGraph` / default `eventLimit` | `packages/ui/react-ui-trace/src/hooks/useExecutionGraph.ts` | L138 / L153 |
703
703
 
704
704
  ## Appendix B: Visual contract examples
705
705
 
@@ -6,14 +6,14 @@ import * as Effect from 'effect/Effect';
6
6
  import * as Option from 'effect/Option';
7
7
  import { describe, test } from 'vitest';
8
8
 
9
- import { RUN_AGAIN_ERROR_CODE, RUN_AGAIN_MESSAGE } from '@dxos/compute';
9
+ import { makeTestProcess } from '@dxos/compute-runtime/testing';
10
10
  import * as Process from '@dxos/compute/Process';
11
11
  import * as Trace from '@dxos/compute/Trace';
12
12
  import { Annotation } from '@dxos/echo';
13
13
  import { EntityId, URI } from '@dxos/keys';
14
14
  import { LogLevel } from '@dxos/log';
15
- import { type Commit, renderTimelineAscii } from '../components/Timeline/index.ts';
16
15
 
16
+ import { type Commit, renderTimelineAscii } from '../components/Timeline/index.ts';
17
17
  import { CommitSelector, buildExecutionGraph } from './execution-graph.ts';
18
18
  import { collectTraceEvents, withMeta } from './testing/index.ts';
19
19
 
@@ -189,17 +189,18 @@ const MESSAGE_ID = '01HQ0000000000000000000000';
189
189
  const SHIMMER_EFFECT_TAG = 'effect:shimmer';
190
190
 
191
191
  const makeActiveProcess = (
192
- overrides: Partial<Process.Info> & Pick<Process.Info, 'pid' | 'key' | 'state'>,
193
- ): Process.Info => ({
194
- parentPid: null,
195
- params: { name: null, annotations: {} },
196
- environment: {},
197
- error: null,
198
- startedAt: 0,
199
- completedAt: Option.none(),
200
- metrics: { wallTime: 0, inputCount: 0, outputCount: 0 },
201
- ...overrides,
202
- });
192
+ overrides: Partial<Process.Data> & Pick<Process.Data, 'pid' | 'key' | 'state'>,
193
+ ): Process.Process =>
194
+ makeTestProcess({
195
+ parentPid: null,
196
+ params: { name: null, annotations: {} },
197
+ environment: {},
198
+ error: null,
199
+ startedAt: 0,
200
+ completedAt: Option.none(),
201
+ metrics: { wallTime: 0, inputCount: 0, outputCount: 0 },
202
+ ...overrides,
203
+ });
203
204
 
204
205
  describe('buildExecutionGraph (span-tree based)', () => {
205
206
  test('empty input → no commits', ({ expect }) => {
@@ -930,7 +931,7 @@ describe('buildExecutionGraph scenarios', () => {
930
931
  name: 'Sync Google Mail',
931
932
  outcome: 'failure',
932
933
  error: 'Run again',
933
- errorCode: RUN_AGAIN_ERROR_CODE,
934
+ errorCode: Process.RUN_AGAIN_ERROR_CODE,
934
935
  });
935
936
  }),
936
937
  ),
@@ -956,7 +957,7 @@ describe('buildExecutionGraph scenarios', () => {
956
957
  key: 'sync',
957
958
  name: 'Sync Google Mail',
958
959
  outcome: 'failure',
959
- error: RUN_AGAIN_MESSAGE,
960
+ error: Process.RUN_AGAIN_MESSAGE,
960
961
  });
961
962
  }),
962
963
  ),