@dxos/react-ui-trace 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +105 -0
- package/README.md +3 -0
- package/dist/lib/index.mjs +2868 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing.mjs +338 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/translations.mjs +19 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/types/src/components/Gantt/Gantt.d.ts +68 -0
- package/dist/types/src/components/Gantt/Gantt.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts +30 -0
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/index.d.ts +2 -0
- package/dist/types/src/components/Gantt/index.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts +32 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts +8 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/index.d.ts +2 -0
- package/dist/types/src/components/ProcessTree/index.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/Timeline.d.ts +50 -0
- package/dist/types/src/components/Timeline/Timeline.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts +24 -0
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/index.d.ts +4 -0
- package/dist/types/src/components/Timeline/index.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-options.d.ts +9 -0
- package/dist/types/src/components/Timeline/timeline-options.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-printer.d.ts +7 -0
- package/dist/types/src/components/Timeline/timeline-printer.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-printer.test.d.ts +2 -0
- package/dist/types/src/components/Timeline/timeline-printer.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/TracePanel.d.ts +28 -0
- package/dist/types/src/components/TracePanel/TracePanel.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.d.ts +19 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.test.d.ts +2 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/index.d.ts +4 -0
- package/dist/types/src/components/TracePanel/index.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/trace-filter.d.ts +39 -0
- package/dist/types/src/components/TracePanel/trace-filter.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/trace-filter.test.d.ts +2 -0
- package/dist/types/src/components/TracePanel/trace-filter.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/useTraceMenu.d.ts +15 -0
- package/dist/types/src/components/TracePanel/useTraceMenu.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +5 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/execution-graph/execution-graph.d.ts +139 -0
- package/dist/types/src/execution-graph/execution-graph.d.ts.map +1 -0
- package/dist/types/src/execution-graph/execution-graph.test.d.ts +2 -0
- package/dist/types/src/execution-graph/execution-graph.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/index.d.ts +4 -0
- package/dist/types/src/execution-graph/index.d.ts.map +1 -0
- package/dist/types/src/execution-graph/pending-block-status.d.ts +26 -0
- package/dist/types/src/execution-graph/pending-block-status.d.ts.map +1 -0
- package/dist/types/src/execution-graph/pending-block-status.test.d.ts +2 -0
- package/dist/types/src/execution-graph/pending-block-status.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts +2 -0
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/span-tree.d.ts +106 -0
- package/dist/types/src/execution-graph/span-tree.d.ts.map +1 -0
- package/dist/types/src/execution-graph/span-tree.test.d.ts +2 -0
- package/dist/types/src/execution-graph/span-tree.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/sub-agent-delegation.test.d.ts +2 -0
- package/dist/types/src/execution-graph/sub-agent-delegation.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/testing/collect-trace-events.d.ts +40 -0
- package/dist/types/src/execution-graph/testing/collect-trace-events.d.ts.map +1 -0
- package/dist/types/src/execution-graph/testing/index.d.ts +2 -0
- package/dist/types/src/execution-graph/testing/index.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +4 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useExecutionGraph.d.ts +17 -0
- package/dist/types/src/hooks/useExecutionGraph.d.ts.map +1 -0
- package/dist/types/src/hooks/useSessionTimeline.d.ts +17 -0
- package/dist/types/src/hooks/useSessionTimeline.d.ts.map +1 -0
- package/dist/types/src/hooks/useTraceMessages.d.ts +17 -0
- package/dist/types/src/hooks/useTraceMessages.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +5 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/session-timeline/index.d.ts +3 -0
- package/dist/types/src/session-timeline/index.d.ts.map +1 -0
- package/dist/types/src/session-timeline/session-timeline.d.ts +19 -0
- package/dist/types/src/session-timeline/session-timeline.d.ts.map +1 -0
- package/dist/types/src/session-timeline/session-timeline.test.d.ts +2 -0
- package/dist/types/src/session-timeline/session-timeline.test.d.ts.map +1 -0
- package/dist/types/src/session-timeline/types.d.ts +129 -0
- package/dist/types/src/session-timeline/types.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +5 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/simulated-agent.d.ts +63 -0
- package/dist/types/src/testing/simulated-agent.d.ts.map +1 -0
- package/dist/types/src/testing/snapshot-playback.d.ts +12 -0
- package/dist/types/src/testing/snapshot-playback.d.ts.map +1 -0
- package/dist/types/src/testing/test-trace.d.ts +5 -0
- package/dist/types/src/testing/test-trace.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +19 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +77 -0
- package/src/components/Gantt/Gantt.stories.tsx +241 -0
- package/src/components/Gantt/Gantt.tsx +589 -0
- package/src/components/Gantt/index.ts +5 -0
- package/src/components/ProcessTree/ProcessTree.stories.tsx +95 -0
- package/src/components/ProcessTree/ProcessTree.tsx +265 -0
- package/src/components/ProcessTree/index.ts +5 -0
- package/src/components/Timeline/Timeline.stories.tsx +326 -0
- package/src/components/Timeline/Timeline.tsx +534 -0
- package/src/components/Timeline/git.drawio.svg +821 -0
- package/src/components/Timeline/index.ts +7 -0
- package/src/components/Timeline/timeline-options.ts +27 -0
- package/src/components/Timeline/timeline-printer.test.ts +76 -0
- package/src/components/Timeline/timeline-printer.ts +223 -0
- package/src/components/TracePanel/TracePanel.tsx +248 -0
- package/src/components/TracePanel/dxn-extractor.test.ts +153 -0
- package/src/components/TracePanel/dxn-extractor.ts +166 -0
- package/src/components/TracePanel/index.ts +7 -0
- package/src/components/TracePanel/trace-filter.test.ts +137 -0
- package/src/components/TracePanel/trace-filter.ts +120 -0
- package/src/components/TracePanel/useTraceMenu.ts +95 -0
- package/src/components/index.ts +8 -0
- package/src/execution-graph/SPEC.md +749 -0
- package/src/execution-graph/execution-graph.test.ts +1205 -0
- package/src/execution-graph/execution-graph.ts +1076 -0
- package/src/execution-graph/index.ts +35 -0
- package/src/execution-graph/pending-block-status.test.ts +118 -0
- package/src/execution-graph/pending-block-status.ts +88 -0
- package/src/execution-graph/remote-snapshot.test.ts +116 -0
- package/src/execution-graph/span-tree.test.ts +370 -0
- package/src/execution-graph/span-tree.ts +404 -0
- package/src/execution-graph/sub-agent-delegation.test.ts +65 -0
- package/src/execution-graph/testing/collect-trace-events.ts +76 -0
- package/src/execution-graph/testing/index.ts +5 -0
- package/src/execution-graph/testing/sub-agent-delegation.json +1285 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useExecutionGraph.ts +107 -0
- package/src/hooks/useSessionTimeline.ts +53 -0
- package/src/hooks/useTraceMessages.ts +58 -0
- package/src/index.ts +8 -0
- package/src/session-timeline/index.ts +6 -0
- package/src/session-timeline/session-timeline.test.ts +662 -0
- package/src/session-timeline/session-timeline.ts +605 -0
- package/src/session-timeline/types.ts +100 -0
- package/src/testing/index.ts +8 -0
- package/src/testing/simulated-agent.ts +293 -0
- package/src/testing/snapshot-playback.ts +45 -0
- package/src/testing/test-trace.ts +22 -0
- package/src/translations.ts +29 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// Kept out of `Timeline.tsx`: react-refresh only fast-refreshes a module whose exports are all
|
|
6
|
+
// components, so constants exported beside them force a full page reload on every edit.
|
|
7
|
+
|
|
8
|
+
export type TimelineOptions = {
|
|
9
|
+
lineHeight: number;
|
|
10
|
+
columnWidth: number;
|
|
11
|
+
nodeRadius: number;
|
|
12
|
+
lineStyle: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const defaultOptions: TimelineOptions = {
|
|
16
|
+
lineHeight: 24,
|
|
17
|
+
columnWidth: 14,
|
|
18
|
+
nodeRadius: 5,
|
|
19
|
+
lineStyle: 'stroke-1',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const compactOptions: TimelineOptions = {
|
|
23
|
+
lineHeight: 20,
|
|
24
|
+
columnWidth: 12,
|
|
25
|
+
nodeRadius: 4,
|
|
26
|
+
lineStyle: 'stroke-1',
|
|
27
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { renderTimelineAscii } from './timeline-printer.ts';
|
|
8
|
+
import { type Commit } from './Timeline.tsx';
|
|
9
|
+
|
|
10
|
+
describe('renderTimelineAscii', () => {
|
|
11
|
+
test('linear single branch', () => {
|
|
12
|
+
const commits: Commit[] = [
|
|
13
|
+
{ id: 'c1', branch: 'main', message: 'first', parents: [] },
|
|
14
|
+
{ id: 'c2', branch: 'main', message: 'second', parents: ['c1'] },
|
|
15
|
+
{ id: 'c3', branch: 'main', message: 'third', parents: ['c2'] },
|
|
16
|
+
];
|
|
17
|
+
expect(`\n${renderTimelineAscii(commits, ['main'])}\n`).toMatchInlineSnapshot(`
|
|
18
|
+
"
|
|
19
|
+
● first
|
|
20
|
+
● second
|
|
21
|
+
● third
|
|
22
|
+
"
|
|
23
|
+
`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('fork from main, merge back', () => {
|
|
27
|
+
const commits: Commit[] = [
|
|
28
|
+
{ id: 'b', branch: 'main', message: 'baseline on main', parents: [] },
|
|
29
|
+
{ id: 'f0', branch: 'feature', message: 'start feature', parents: ['b'] },
|
|
30
|
+
{ id: 'f1', branch: 'feature', message: 'implement', parents: ['f0'] },
|
|
31
|
+
{ id: 'f2', branch: 'feature', message: 'polish', parents: ['f1'] },
|
|
32
|
+
{
|
|
33
|
+
id: 'm',
|
|
34
|
+
branch: 'main',
|
|
35
|
+
message: 'merge feature into main',
|
|
36
|
+
parents: ['f2', 'b'],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
expect(`\n${renderTimelineAscii(commits, ['main', 'feature'])}\n`).toMatchInlineSnapshot(`
|
|
40
|
+
"
|
|
41
|
+
● baseline on main
|
|
42
|
+
├──● start feature
|
|
43
|
+
│ ● implement
|
|
44
|
+
│ ● polish
|
|
45
|
+
◆──╯ merge feature into main
|
|
46
|
+
"
|
|
47
|
+
`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('two interleaved feature branches', () => {
|
|
51
|
+
const commits: Commit[] = [
|
|
52
|
+
{ id: 'b', branch: 'main', message: 'baseline', parents: [] },
|
|
53
|
+
{ id: 'a1', branch: 'alpha', message: 'alpha start', parents: ['b'] },
|
|
54
|
+
{ id: 'b1', branch: 'beta', message: 'beta start', parents: ['b'] },
|
|
55
|
+
{ id: 'a2', branch: 'alpha', message: 'alpha work', parents: ['a1'] },
|
|
56
|
+
{ id: 'b2', branch: 'beta', message: 'beta work', parents: ['b1'] },
|
|
57
|
+
{ id: 'a3', branch: 'alpha', message: 'alpha done', parents: ['a2'] },
|
|
58
|
+
{ id: 'ma', branch: 'main', message: 'merge alpha', parents: ['a3', 'b'] },
|
|
59
|
+
{ id: 'b3', branch: 'beta', message: 'beta done', parents: ['b2'] },
|
|
60
|
+
{ id: 'mb', branch: 'main', message: 'merge beta', parents: ['b3', 'ma'] },
|
|
61
|
+
];
|
|
62
|
+
expect(`\n${renderTimelineAscii(commits, ['main', 'alpha', 'beta'])}\n`).toMatchInlineSnapshot(`
|
|
63
|
+
"
|
|
64
|
+
● baseline
|
|
65
|
+
├──● alpha start
|
|
66
|
+
├──┼──● beta start
|
|
67
|
+
│ ● │ alpha work
|
|
68
|
+
│ │ ● beta work
|
|
69
|
+
│ ● │ alpha done
|
|
70
|
+
◆──╯ │ merge alpha
|
|
71
|
+
│ ● beta done
|
|
72
|
+
◆─────╯ merge beta
|
|
73
|
+
"
|
|
74
|
+
`);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Commit } from './Timeline.tsx';
|
|
6
|
+
|
|
7
|
+
const NODE = '●';
|
|
8
|
+
const MERGE_NODE = '◆';
|
|
9
|
+
const RAIL = '│';
|
|
10
|
+
const HORIZ = '─';
|
|
11
|
+
const COL_WIDTH = 3;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Renders commits as a monospace ASCII graph with rounded box-drawing arcs and vertical rails.
|
|
15
|
+
* Commits must be ordered chronologically (oldest first, newest last). Parents must refer to older commits.
|
|
16
|
+
*/
|
|
17
|
+
export const renderTimelineAscii = (commits: Commit[], branches: string[]): string => {
|
|
18
|
+
if (commits.length === 0) {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const laneById = assignLanes(commits, branches);
|
|
23
|
+
const lanes = commits.map((commit) => laneById.get(commit.id)!);
|
|
24
|
+
const maxLane = Math.max(...lanes);
|
|
25
|
+
|
|
26
|
+
if (maxLane === 0) {
|
|
27
|
+
return commits.map((commit) => `${NODE} ${formatLabel(commit)}`).join('\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const active = computeActiveColumns(commits, lanes, laneById);
|
|
31
|
+
|
|
32
|
+
const graphParts = commits.map((commit, row) => renderRow(commit, lanes[row]!, maxLane, active[row]!, laneById));
|
|
33
|
+
|
|
34
|
+
const maxGraphWidth = Math.max(...graphParts.map((part) => part.length));
|
|
35
|
+
return commits.map((commit, row) => `${graphParts[row]!.padEnd(maxGraphWidth)} ${formatLabel(commit)}`).join('\n');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const renderRow = (
|
|
39
|
+
commit: Commit,
|
|
40
|
+
commitLane: number,
|
|
41
|
+
maxLane: number,
|
|
42
|
+
activeRow: boolean[],
|
|
43
|
+
laneById: Map<string, number>,
|
|
44
|
+
): string => {
|
|
45
|
+
const totalChars = (maxLane + 1) * COL_WIDTH;
|
|
46
|
+
const chars: string[] = Array(totalChars).fill(' ');
|
|
47
|
+
|
|
48
|
+
const merge = isMergeCommit(commit);
|
|
49
|
+
const fork = !merge && isForkCommit(commit, laneById, commitLane);
|
|
50
|
+
|
|
51
|
+
let connectLane = -1;
|
|
52
|
+
if (fork) {
|
|
53
|
+
const parentLane = laneById.get(commit.parents![0]!);
|
|
54
|
+
if (parentLane !== undefined) {
|
|
55
|
+
connectLane = parentLane;
|
|
56
|
+
}
|
|
57
|
+
} else if (merge) {
|
|
58
|
+
for (const parentId of commit.parents ?? []) {
|
|
59
|
+
const parentLane = laneById.get(parentId);
|
|
60
|
+
if (parentLane !== undefined && parentLane !== commitLane) {
|
|
61
|
+
connectLane = parentLane;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (let col = 0; col <= maxLane; col++) {
|
|
68
|
+
if (col === commitLane) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (activeRow[col]) {
|
|
72
|
+
chars[col * COL_WIDTH] = RAIL;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
chars[commitLane * COL_WIDTH] = merge ? MERGE_NODE : NODE;
|
|
77
|
+
if (connectLane >= 0) {
|
|
78
|
+
const connectPos = connectLane * COL_WIDTH;
|
|
79
|
+
if (fork) {
|
|
80
|
+
chars[connectPos] = connectLane < commitLane ? '├' : '┤';
|
|
81
|
+
} else {
|
|
82
|
+
chars[connectPos] = connectLane > commitLane ? '╯' : '╰';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const minCol = Math.min(commitLane, connectLane);
|
|
86
|
+
const maxCol = Math.max(commitLane, connectLane);
|
|
87
|
+
for (let pos = minCol * COL_WIDTH + 1; pos < maxCol * COL_WIDTH; pos++) {
|
|
88
|
+
if (pos % COL_WIDTH === 0) {
|
|
89
|
+
const col = pos / COL_WIDTH;
|
|
90
|
+
if (activeRow[col]) {
|
|
91
|
+
chars[pos] = '┼';
|
|
92
|
+
} else {
|
|
93
|
+
chars[pos] = HORIZ;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
chars[pos] = HORIZ;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return chars.join('').trimEnd();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const formatLabel = (commit: Commit): string => {
|
|
105
|
+
const icon = commit.icon
|
|
106
|
+
?.replace(/^ph--/, '')
|
|
107
|
+
.replace(/--regular$/, '')
|
|
108
|
+
.replace(/--bold$/, '')
|
|
109
|
+
.replace(/--fill$/, '');
|
|
110
|
+
return icon ? `[${icon}] ${commit.message}` : commit.message;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Assigns each commit to a lane (column), reusing lanes once a branch has been merged.
|
|
115
|
+
* A branch is considered "merged" when a commit on another branch references it as a parent.
|
|
116
|
+
* Unmerged branches keep their lane active (they may still be in progress).
|
|
117
|
+
* Lane 0 is reserved for the first branch (typically 'main').
|
|
118
|
+
*/
|
|
119
|
+
const assignLanes = (commits: Commit[], branches: string[]): Map<string, number> => {
|
|
120
|
+
// Build commit-id to branch lookup.
|
|
121
|
+
const commitBranch = new Map<string, string>();
|
|
122
|
+
for (const commit of commits) {
|
|
123
|
+
commitBranch.set(commit.id, commit.branch);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Find the row at which each branch is merged (closed) by another branch.
|
|
127
|
+
const mergeRow = new Map<string, number>();
|
|
128
|
+
for (let row = 0; row < commits.length; row++) {
|
|
129
|
+
const commit = commits[row]!;
|
|
130
|
+
for (const parentId of commit.parents ?? []) {
|
|
131
|
+
const parentBranch = commitBranch.get(parentId);
|
|
132
|
+
if (parentBranch && parentBranch !== commit.branch) {
|
|
133
|
+
mergeRow.set(parentBranch, Math.max(mergeRow.get(parentBranch) ?? row, row));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const branchLane = new Map<string, number>();
|
|
139
|
+
if (branches.length > 0) {
|
|
140
|
+
branchLane.set(branches[0]!, 0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const activeLanes = new Set<number>([0]);
|
|
144
|
+
const laneById = new Map<string, number>();
|
|
145
|
+
|
|
146
|
+
for (let row = 0; row < commits.length; row++) {
|
|
147
|
+
const commit = commits[row]!;
|
|
148
|
+
|
|
149
|
+
if (!branchLane.has(commit.branch)) {
|
|
150
|
+
let lane = 1;
|
|
151
|
+
while (activeLanes.has(lane)) {
|
|
152
|
+
lane++;
|
|
153
|
+
}
|
|
154
|
+
branchLane.set(commit.branch, lane);
|
|
155
|
+
activeLanes.add(lane);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
laneById.set(commit.id, branchLane.get(commit.branch)!);
|
|
159
|
+
|
|
160
|
+
// Release lanes for branches that have been merged at this row.
|
|
161
|
+
for (const [branch, endRow] of mergeRow) {
|
|
162
|
+
if (endRow === row && branch !== branches[0] && branchLane.has(branch)) {
|
|
163
|
+
activeLanes.delete(branchLane.get(branch)!);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return laneById;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const isMergeCommit = (commit: Commit): boolean => (commit.parents?.length ?? 0) > 1;
|
|
172
|
+
|
|
173
|
+
const isForkCommit = (commit: Commit, laneById: Map<string, number>, lane: number): boolean => {
|
|
174
|
+
const parents = commit.parents;
|
|
175
|
+
if (!parents || parents.length !== 1) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
const parentLane = laneById.get(parents[0]!);
|
|
179
|
+
return parentLane !== undefined && parentLane !== lane;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Column `col` is active on `row` when `row` falls inside the vertical span
|
|
184
|
+
* of that column (the range of rows that include commits on that lane plus
|
|
185
|
+
* merge rows referencing parents on that lane) AND the row's own commit
|
|
186
|
+
* is not on that column.
|
|
187
|
+
*/
|
|
188
|
+
const computeActiveColumns = (commits: Commit[], lanes: number[], laneById: Map<string, number>): boolean[][] => {
|
|
189
|
+
const rows = commits.length;
|
|
190
|
+
const cols = Math.max(...lanes, 0) + 1;
|
|
191
|
+
const rowIndicesByCol: number[][] = Array.from({ length: cols }, () => []);
|
|
192
|
+
|
|
193
|
+
for (let row = 0; row < rows; row++) {
|
|
194
|
+
rowIndicesByCol[lanes[row]!]!.push(row);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (let row = 0; row < rows; row++) {
|
|
198
|
+
for (const parentId of commits[row]?.parents ?? []) {
|
|
199
|
+
const parentLane = laneById.get(parentId);
|
|
200
|
+
if (parentLane === undefined) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
rowIndicesByCol[parentLane]!.push(row);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const active: boolean[][] = Array.from({ length: rows }, () => Array(cols).fill(false));
|
|
208
|
+
for (let col = 0; col < cols; col++) {
|
|
209
|
+
const indices = rowIndicesByCol[col]!;
|
|
210
|
+
if (indices.length === 0) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const minR = Math.min(...indices);
|
|
214
|
+
const maxR = Math.max(...indices);
|
|
215
|
+
for (let row = minR; row <= maxR; row++) {
|
|
216
|
+
if (lanes[row] !== col) {
|
|
217
|
+
active[row]![col] = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return active;
|
|
223
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback, useDeferredValue, useMemo, useState } from 'react';
|
|
6
|
+
|
|
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
|
+
import { ActionToolbar } from '@dxos/react-ui-menu';
|
|
18
|
+
import { JsonHighlighter } from '@dxos/react-ui-syntax-highlighter';
|
|
19
|
+
import { mx } from '@dxos/ui-theme';
|
|
20
|
+
|
|
21
|
+
import { type ExecutionGraph } from '../../execution-graph/index.ts';
|
|
22
|
+
import { translationKey } from '../../translations.ts';
|
|
23
|
+
import { ProcessTree, type ProcessTreeProps } from '../ProcessTree/index.ts';
|
|
24
|
+
import { type Commit, Timeline } from '../Timeline/index.ts';
|
|
25
|
+
|
|
26
|
+
import { type ProcessEnvironment, filterProcesses } from './trace-filter.ts';
|
|
27
|
+
import { useTraceMenu } from './useTraceMenu.ts';
|
|
28
|
+
|
|
29
|
+
export type TracePanelProps = ThemedClassName<
|
|
30
|
+
Pick<ProcessTreeProps, 'resolveLabel' | 'selected' | 'onSelectedChange' | 'onProcessTerminate'> & {
|
|
31
|
+
/** The live process tree; narrowed here by `environments`. */
|
|
32
|
+
processes: readonly Process.Info[];
|
|
33
|
+
/** The trace as a commit graph, built by `useExecutionGraph` (already narrowed to `selected`). */
|
|
34
|
+
graph: ExecutionGraph;
|
|
35
|
+
/** Process environments shown; the toolbar's funnel edits it. */
|
|
36
|
+
environments: readonly ProcessEnvironment[];
|
|
37
|
+
onEnvironmentsChange: (environments: ProcessEnvironment[]) => void;
|
|
38
|
+
/** Shows the span tree as JSON in place of the timeline. */
|
|
39
|
+
debug?: boolean;
|
|
40
|
+
/** A commit whose message links an object was picked; the host decides what opening it means. */
|
|
41
|
+
onOpenLink?: (uri: string) => void;
|
|
42
|
+
}
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A space's runtime: its processes as a tree, its trace as a commit graph, and the picked commit's
|
|
47
|
+
* event. Presentation only — the host resolves the process monitor, the trace feed, settings and
|
|
48
|
+
* navigation and hands them in as props.
|
|
49
|
+
*/
|
|
50
|
+
export const TracePanel = composable<HTMLDivElement, TracePanelProps>(
|
|
51
|
+
(
|
|
52
|
+
{
|
|
53
|
+
classNames,
|
|
54
|
+
processes,
|
|
55
|
+
graph,
|
|
56
|
+
environments,
|
|
57
|
+
onEnvironmentsChange,
|
|
58
|
+
debug = false,
|
|
59
|
+
resolveLabel,
|
|
60
|
+
selected = NO_SELECTION,
|
|
61
|
+
onSelectedChange,
|
|
62
|
+
onProcessTerminate,
|
|
63
|
+
onOpenLink,
|
|
64
|
+
...props
|
|
65
|
+
},
|
|
66
|
+
forwardedRef,
|
|
67
|
+
) => {
|
|
68
|
+
const { t } = useTranslation(translationKey);
|
|
69
|
+
const handleClearSelection = useCallback(() => onSelectedChange?.([]), [onSelectedChange]);
|
|
70
|
+
const menu = useTraceMenu({
|
|
71
|
+
selected: environments,
|
|
72
|
+
onSelectedChange: onEnvironmentsChange,
|
|
73
|
+
selectionCount: selected.length,
|
|
74
|
+
onClearSelection: handleClearSelection,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// `useDeferredValue` batches update bursts, works together with `React.memo`.
|
|
78
|
+
// See the comment in `ProcessTreeContainer` for more details.
|
|
79
|
+
const { branches, commits, spanTree, details } = useDeferredValue(graph);
|
|
80
|
+
|
|
81
|
+
const [selectedCommit, setSelectedCommit] = useState<Commit | undefined>();
|
|
82
|
+
// Remembered across selections, so collapsing a section once keeps it collapsed.
|
|
83
|
+
const [openSections, setOpenSections] = useState<string[]>(SECTIONS.map((section) => section.id));
|
|
84
|
+
const handleCommitSelect = useCallback(
|
|
85
|
+
(commit: Commit | undefined) => {
|
|
86
|
+
setSelectedCommit(commit);
|
|
87
|
+
if (commit?.link) {
|
|
88
|
+
onOpenLink?.(commit.link);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
[onOpenLink],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// The most recently picked process is the highlighted branch, while it is still picked.
|
|
95
|
+
const currentBranch = useMemo(() => {
|
|
96
|
+
const last = selected.at(-1);
|
|
97
|
+
return last !== undefined && branches.includes(last) ? last : null;
|
|
98
|
+
}, [selected, branches]);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Panel.Root {...composableProps(props, { classNames: ['h-full', classNames] })} ref={forwardedRef}>
|
|
102
|
+
<Panel.Toolbar asChild>
|
|
103
|
+
<ActionToolbar {...menu} alwaysActive classNames='justify-end' />
|
|
104
|
+
</Panel.Toolbar>
|
|
105
|
+
|
|
106
|
+
<Panel.Content>
|
|
107
|
+
<Accordion.Root<TraceSection>
|
|
108
|
+
items={SECTIONS}
|
|
109
|
+
value={openSections}
|
|
110
|
+
onValueChange={setOpenSections}
|
|
111
|
+
classNames='h-full min-h-0 rounded-none border-y-0'
|
|
112
|
+
>
|
|
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}
|
|
131
|
+
/>
|
|
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
|
+
}
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
</Accordion.Root>
|
|
197
|
+
</Panel.Content>
|
|
198
|
+
</Panel.Root>
|
|
199
|
+
);
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
TracePanel.displayName = 'TracePanel';
|
|
204
|
+
|
|
205
|
+
type TraceSection = { id: 'processes' | 'trace' | 'details' };
|
|
206
|
+
|
|
207
|
+
const SECTIONS: TraceSection[] = [{ id: 'processes' }, { id: 'trace' }, { id: 'details' }];
|
|
208
|
+
|
|
209
|
+
const NO_SELECTION: readonly string[] = [];
|
|
210
|
+
|
|
211
|
+
type ProcessTreeContainerProps = ThemedClassName<
|
|
212
|
+
Pick<ProcessTreeProps, 'resolveLabel' | 'selected' | 'onSelectedChange' | 'onProcessTerminate'> & {
|
|
213
|
+
processes: readonly Process.Info[];
|
|
214
|
+
environments: readonly ProcessEnvironment[];
|
|
215
|
+
}
|
|
216
|
+
>;
|
|
217
|
+
|
|
218
|
+
// Isolate `ProcessTree` updates from the rest of the panel.
|
|
219
|
+
const ProcessTreeContainer = ({
|
|
220
|
+
classNames,
|
|
221
|
+
processes,
|
|
222
|
+
environments,
|
|
223
|
+
resolveLabel,
|
|
224
|
+
selected,
|
|
225
|
+
onSelectedChange,
|
|
226
|
+
onProcessTerminate,
|
|
227
|
+
}: ProcessTreeContainerProps) => {
|
|
228
|
+
// `processes` updates in bursts (about 14 updates per navigation).
|
|
229
|
+
// `useDeferredValue` will debounce update propagation, returning stale value for short periods.
|
|
230
|
+
// NOTE: `ProcessTree` MUST use `React.memo`, otherwise this will not work.
|
|
231
|
+
const processesDeferred = useDeferredValue(processes);
|
|
232
|
+
const visibleProcesses = useMemo(
|
|
233
|
+
() => filterProcesses(processesDeferred, environments),
|
|
234
|
+
[processesDeferred, environments],
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<ProcessTree
|
|
239
|
+
classNames={classNames}
|
|
240
|
+
depth={3}
|
|
241
|
+
processes={visibleProcesses}
|
|
242
|
+
resolveLabel={resolveLabel}
|
|
243
|
+
selected={selected}
|
|
244
|
+
onSelectedChange={onSelectedChange}
|
|
245
|
+
onProcessTerminate={onProcessTerminate}
|
|
246
|
+
/>
|
|
247
|
+
);
|
|
248
|
+
};
|