@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.
- package/README.md +1 -1
- package/dist/lib/index.mjs +1722 -561
- package/dist/lib/index.mjs.map +1 -1
- package/dist/lib/testing.mjs +3 -3
- package/dist/lib/testing.mjs.map +1 -1
- package/dist/lib/translations.mjs +5 -0
- package/dist/lib/translations.mjs.map +1 -1
- package/dist/types/src/components/Gantt/Gantt.d.ts +109 -24
- package/dist/types/src/components/Gantt/Gantt.d.ts.map +1 -1
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts +73 -3
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts.map +1 -1
- package/dist/types/src/components/Gantt/gantt-rows.d.ts +26 -0
- package/dist/types/src/components/Gantt/gantt-rows.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/gantt-rows.test.d.ts +2 -0
- package/dist/types/src/components/Gantt/gantt-rows.test.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/gantt-scale.d.ts +57 -0
- package/dist/types/src/components/Gantt/gantt-scale.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/gantt-scale.test.d.ts +2 -0
- package/dist/types/src/components/Gantt/gantt-scale.test.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 -1
- package/dist/types/src/components/Gantt/useEnter.d.ts +13 -0
- package/dist/types/src/components/Gantt/useEnter.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/useRangeTween.d.ts +12 -0
- package/dist/types/src/components/Gantt/useRangeTween.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts +3 -3
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts.map +1 -1
- package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Timeline/Timeline.d.ts +10 -2
- package/dist/types/src/components/Timeline/Timeline.d.ts.map +1 -1
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts +8 -4
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts.map +1 -1
- package/dist/types/src/components/Timeline/timeline-layout.d.ts +35 -0
- package/dist/types/src/components/Timeline/timeline-layout.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-layout.test.d.ts +2 -0
- package/dist/types/src/components/Timeline/timeline-layout.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/SpanTreeView.d.ts +15 -0
- package/dist/types/src/components/TracePanel/SpanTreeView.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/TracePanel.d.ts +3 -3
- package/dist/types/src/components/TracePanel/TracePanel.d.ts.map +1 -1
- package/dist/types/src/components/TracePanel/TracePanel.stories.d.ts +46 -0
- package/dist/types/src/components/TracePanel/TracePanel.stories.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/trace-filter.d.ts +7 -2
- package/dist/types/src/components/TracePanel/trace-filter.d.ts.map +1 -1
- package/dist/types/src/components/TracePanel/useTraceMenu.d.ts.map +1 -1
- package/dist/types/src/execution-graph/execution-graph.d.ts +1 -1
- package/dist/types/src/execution-graph/execution-graph.d.ts.map +1 -1
- package/dist/types/src/hooks/useExecutionGraph.d.ts +2 -2
- package/dist/types/src/hooks/useExecutionGraph.d.ts.map +1 -1
- package/dist/types/src/hooks/useSessionTimeline.d.ts +1 -1
- package/dist/types/src/hooks/useSessionTimeline.d.ts.map +1 -1
- package/dist/types/src/hooks/useTraceMessages.d.ts +1 -1
- package/dist/types/src/hooks/useTraceMessages.d.ts.map +1 -1
- package/dist/types/src/session-timeline/TaskHistory.stories.d.ts +12 -0
- package/dist/types/src/session-timeline/TaskHistory.stories.d.ts.map +1 -0
- package/dist/types/src/session-timeline/gantt-mapping.d.ts +14 -0
- package/dist/types/src/session-timeline/gantt-mapping.d.ts.map +1 -0
- package/dist/types/src/session-timeline/gantt-mapping.test.d.ts +2 -0
- package/dist/types/src/session-timeline/gantt-mapping.test.d.ts.map +1 -0
- package/dist/types/src/session-timeline/index.d.ts +1 -0
- package/dist/types/src/session-timeline/index.d.ts.map +1 -1
- package/dist/types/src/session-timeline/session-timeline.d.ts +17 -2
- package/dist/types/src/session-timeline/session-timeline.d.ts.map +1 -1
- package/dist/types/src/session-timeline/types.d.ts +32 -0
- package/dist/types/src/session-timeline/types.d.ts.map +1 -1
- package/dist/types/src/testing/simulated-agent.d.ts +3 -2
- package/dist/types/src/testing/simulated-agent.d.ts.map +1 -1
- package/dist/types/src/testing/test-trace.d.ts +2 -2
- package/dist/types/src/testing/test-trace.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +5 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +30 -26
- package/src/components/Gantt/Gantt.stories.tsx +914 -96
- package/src/components/Gantt/Gantt.tsx +964 -261
- package/src/components/Gantt/gantt-rows.test.ts +123 -0
- package/src/components/Gantt/gantt-rows.ts +71 -0
- package/src/components/Gantt/gantt-scale.test.ts +105 -0
- package/src/components/Gantt/gantt-scale.ts +143 -0
- package/src/components/Gantt/index.ts +2 -0
- package/src/components/Gantt/useEnter.ts +33 -0
- package/src/components/Gantt/useRangeTween.ts +61 -0
- package/src/components/ProcessTree/ProcessTree.stories.tsx +2 -3
- package/src/components/ProcessTree/ProcessTree.tsx +99 -90
- package/src/components/Timeline/Timeline.stories.tsx +110 -22
- package/src/components/Timeline/Timeline.tsx +319 -271
- package/src/components/Timeline/timeline-layout.test.ts +177 -0
- package/src/components/Timeline/timeline-layout.ts +153 -0
- package/src/components/TracePanel/SpanTreeView.tsx +39 -0
- package/src/components/TracePanel/TracePanel.stories.tsx +106 -0
- package/src/components/TracePanel/TracePanel.tsx +113 -106
- package/src/components/TracePanel/trace-filter.test.ts +31 -2
- package/src/components/TracePanel/trace-filter.ts +28 -3
- package/src/components/TracePanel/useTraceMenu.ts +0 -1
- package/src/execution-graph/SPEC.md +10 -10
- package/src/execution-graph/execution-graph.test.ts +5 -6
- package/src/execution-graph/execution-graph.ts +108 -60
- package/src/execution-graph/pending-block-status.ts +1 -1
- package/src/execution-graph/sub-agent-delegation.test.ts +1 -1
- package/src/hooks/useExecutionGraph.ts +14 -11
- package/src/hooks/useSessionTimeline.ts +35 -9
- package/src/hooks/useTraceMessages.ts +1 -1
- package/src/session-timeline/TaskHistory.stories.tsx +142 -0
- package/src/session-timeline/gantt-mapping.test.ts +118 -0
- package/src/session-timeline/gantt-mapping.ts +158 -0
- package/src/session-timeline/index.ts +1 -0
- package/src/session-timeline/session-timeline.test.ts +567 -19
- package/src/session-timeline/session-timeline.ts +433 -62
- package/src/session-timeline/types.ts +14 -0
- package/src/testing/simulated-agent.ts +3 -2
- package/src/testing/test-trace.ts +2 -2
- package/src/translations.ts +8 -2
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts +0 -2
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts.map +0 -1
- package/src/execution-graph/remote-snapshot.test.ts +0 -116
|
@@ -9,15 +9,14 @@ import * as Pipeable from 'effect/Pipeable';
|
|
|
9
9
|
import * as Schema from 'effect/Schema';
|
|
10
10
|
import * as Struct from 'effect/Struct';
|
|
11
11
|
|
|
12
|
-
import { RUN_AGAIN_ERROR_CODE, RUN_AGAIN_MESSAGE } from '@dxos/compute';
|
|
13
12
|
import * as Process from '@dxos/compute/Process';
|
|
14
13
|
import * as Trace from '@dxos/compute/Trace';
|
|
15
14
|
import { Annotation } from '@dxos/echo';
|
|
16
15
|
import { EID } from '@dxos/keys';
|
|
17
16
|
import { LogLevel, log } from '@dxos/log';
|
|
18
|
-
import { type Commit } from '../components/Timeline/index.ts';
|
|
19
17
|
import { type ContentBlock } from '@dxos/types';
|
|
20
18
|
|
|
19
|
+
import { type Commit } from '../components/Timeline/index.ts';
|
|
21
20
|
import { ROOT_SPAN_ID, type Span, buildSpanTree, isSpanBeginEvent, isSpanEndEvent, walkSpanTree } from './span-tree.ts';
|
|
22
21
|
|
|
23
22
|
/**
|
|
@@ -93,7 +92,7 @@ const ICONS = {
|
|
|
93
92
|
|
|
94
93
|
export interface BuildExecutionGraphParams {
|
|
95
94
|
traceMessages: readonly Trace.Message[];
|
|
96
|
-
activeProcesses?: readonly Process.
|
|
95
|
+
activeProcesses?: readonly Process.Process[];
|
|
97
96
|
collapseCompletedSpans?: boolean;
|
|
98
97
|
eventLimit?: number;
|
|
99
98
|
/**
|
|
@@ -336,8 +335,8 @@ const presentEvent = (event: Trace.FlatEvent, toolCallContext: ToolCallContext):
|
|
|
336
335
|
// the legacy message for events persisted before `errorCode` existed.
|
|
337
336
|
const incomplete =
|
|
338
337
|
event.data.outcome === 'failure' &&
|
|
339
|
-
(event.data.errorCode === RUN_AGAIN_ERROR_CODE ||
|
|
340
|
-
(event.data.errorCode === undefined && event.data.error === RUN_AGAIN_MESSAGE));
|
|
338
|
+
(event.data.errorCode === Process.RUN_AGAIN_ERROR_CODE ||
|
|
339
|
+
(event.data.errorCode === undefined && event.data.error === Process.RUN_AGAIN_MESSAGE));
|
|
341
340
|
const presentation =
|
|
342
341
|
event.data.outcome === 'success'
|
|
343
342
|
? { icon: ICONS.operationEndSuccess.icon, level: ICONS.operationEndSuccess.level, suffix: '' }
|
|
@@ -478,16 +477,28 @@ export const deriveInFlightActivityLine = (
|
|
|
478
477
|
};
|
|
479
478
|
|
|
480
479
|
const collectDescendantPids = (messages: readonly Trace.Message[], rootPid: string): Set<string> => {
|
|
480
|
+
// Indexed first so the walk costs one pass over the messages rather than one per tree level.
|
|
481
|
+
const childPidsByParent = new Map<string, Set<string>>();
|
|
482
|
+
for (const message of messages) {
|
|
483
|
+
const { pid, parentPid } = message.meta;
|
|
484
|
+
if (!pid || !parentPid) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
let children = childPidsByParent.get(parentPid);
|
|
488
|
+
if (!children) {
|
|
489
|
+
children = new Set();
|
|
490
|
+
childPidsByParent.set(parentPid, children);
|
|
491
|
+
}
|
|
492
|
+
children.add(pid);
|
|
493
|
+
}
|
|
494
|
+
|
|
481
495
|
const pids = new Set([rootPid]);
|
|
482
|
-
|
|
483
|
-
while (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (pid && parentPid && pids.has(parentPid) && !pids.has(pid)) {
|
|
489
|
-
pids.add(pid);
|
|
490
|
-
expanded = true;
|
|
496
|
+
const pending = [rootPid];
|
|
497
|
+
while (pending.length > 0) {
|
|
498
|
+
for (const childPid of childPidsByParent.get(pending.pop()!) ?? []) {
|
|
499
|
+
if (!pids.has(childPid)) {
|
|
500
|
+
pids.add(childPid);
|
|
501
|
+
pending.push(childPid);
|
|
491
502
|
}
|
|
492
503
|
}
|
|
493
504
|
}
|
|
@@ -567,9 +578,13 @@ const isCollapsibleSpan = (span: Span, parent: Span | null, collapseCompletedSpa
|
|
|
567
578
|
* subtrees (depth-first), then its remaining events. Active-process running rows are inserted
|
|
568
579
|
* at the end of each matching span block — not appended after the entire tree.
|
|
569
580
|
*/
|
|
581
|
+
/** Branch tips as a commit's parent list; a branch with no commit yet contributes nothing. */
|
|
582
|
+
const parentIds = (...tips: (Commit | undefined)[]): string[] =>
|
|
583
|
+
Array.dedupe(tips.filter((tip) => tip !== undefined).map((tip) => tip.id));
|
|
584
|
+
|
|
570
585
|
const spanTreeToCommits = (
|
|
571
586
|
root: Span,
|
|
572
|
-
activeProcesses: readonly Process.
|
|
587
|
+
activeProcesses: readonly Process.Process[],
|
|
573
588
|
toolCallContext: ToolCallContext,
|
|
574
589
|
collapseCompletedSpans: boolean,
|
|
575
590
|
): { branches: string[]; commits: Commit[]; details: ExecutionGraphDetailsMap } => {
|
|
@@ -621,7 +636,7 @@ const spanTreeToCommits = (
|
|
|
621
636
|
// Track the id of the begin commit for each non-collapsible span so the end commit can merge into it.
|
|
622
637
|
const beginCommitIdBySpan = new Map<string, string>();
|
|
623
638
|
|
|
624
|
-
//
|
|
639
|
+
// The parents for "the most recent commit in this span's structural context".
|
|
625
640
|
//
|
|
626
641
|
// The walk is structural (pid/parentPid), not topological: at each ancestor, we prefer
|
|
627
642
|
// 1) the last commit on that span's own branch, then
|
|
@@ -633,17 +648,19 @@ const spanTreeToCommits = (
|
|
|
633
648
|
// to A.begin — not to whatever unrelated commit happens to be the latest on main.
|
|
634
649
|
// This keeps fork edges visually attached to the correct span even when sibling top-level
|
|
635
650
|
// spans (different pids) overlap in time on main.
|
|
636
|
-
const lastInSpanContext = (span: Span | null):
|
|
651
|
+
const lastInSpanContext = (span: Span | null): string[] => {
|
|
637
652
|
if (!span || span.id === ROOT_SPAN_ID) {
|
|
638
|
-
return
|
|
653
|
+
return parentIds(builder.tipOf(MAIN_BRANCH));
|
|
654
|
+
}
|
|
655
|
+
const ownTip = builder.tipOf(branchOf(span));
|
|
656
|
+
if (ownTip) {
|
|
657
|
+
return parentIds(ownTip);
|
|
639
658
|
}
|
|
640
|
-
const ownBranch = branchOf(span);
|
|
641
659
|
const beginId = beginCommitIdBySpan.get(span.id);
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
);
|
|
660
|
+
if (beginId && builder.hasCommit(beginId)) {
|
|
661
|
+
return [beginId];
|
|
662
|
+
}
|
|
663
|
+
return lastInSpanContext(findParentSpan(span));
|
|
647
664
|
};
|
|
648
665
|
|
|
649
666
|
const lastCommitIdBySpan = new Map<string, string>();
|
|
@@ -671,12 +688,7 @@ const spanTreeToCommits = (
|
|
|
671
688
|
branch: process.pid,
|
|
672
689
|
// Falls back to the tail of main: a completed request collapses onto main, leaving the
|
|
673
690
|
// agent's own branch empty, and a parentless spinner draws as a second, disconnected root.
|
|
674
|
-
parents: builder.
|
|
675
|
-
CommitSelector.firstOf(
|
|
676
|
-
CommitSelector.branch(process.pid).pipe(CommitSelector.compose(CommitSelector.last())),
|
|
677
|
-
CommitSelector.branch(MAIN_BRANCH).pipe(CommitSelector.compose(CommitSelector.last())),
|
|
678
|
-
),
|
|
679
|
-
),
|
|
691
|
+
parents: parentIds(builder.firstTipOf(process.pid, MAIN_BRANCH)),
|
|
680
692
|
icon: ICONS.agentRequestRunning.icon,
|
|
681
693
|
level: ICONS.agentRequestRunning.level,
|
|
682
694
|
message: 'Generating...',
|
|
@@ -688,9 +700,7 @@ const spanTreeToCommits = (
|
|
|
688
700
|
builder.addCommit({
|
|
689
701
|
id: `running:${process.pid}`,
|
|
690
702
|
branch: process.pid,
|
|
691
|
-
parents: builder.
|
|
692
|
-
CommitSelector.branch(process.pid).pipe(CommitSelector.compose(CommitSelector.last())),
|
|
693
|
-
),
|
|
703
|
+
parents: parentIds(builder.tipOf(process.pid)),
|
|
694
704
|
icon: ICONS.processRunning.icon,
|
|
695
705
|
level: ICONS.processRunning.level,
|
|
696
706
|
message: 'Running...',
|
|
@@ -746,19 +756,19 @@ const spanTreeToCommits = (
|
|
|
746
756
|
if (span.id === ROOT_SPAN_ID) {
|
|
747
757
|
// Root-level events (no pid) attach sequentially to main.
|
|
748
758
|
branch = MAIN_BRANCH;
|
|
749
|
-
parents =
|
|
759
|
+
parents = lastInSpanContext(null);
|
|
750
760
|
} else if (collapsible) {
|
|
751
761
|
// Only the end event reaches here for collapsible spans.
|
|
752
762
|
// Anchor to the parent span's structural context so the fork attaches to the parent
|
|
753
763
|
// — not to a sibling span's commit that happens to be the latest on the shared ancestor.
|
|
754
764
|
branch = parentBranch;
|
|
755
|
-
parents =
|
|
765
|
+
parents = lastInSpanContext(parentSpan);
|
|
756
766
|
} else if (isBeginEvent) {
|
|
757
767
|
// Begin commit, anchored to the parent's structural context. A nested sub-span stays on the
|
|
758
768
|
// parent lane (only its middle events fork). A concurrent child process forks onto its own
|
|
759
769
|
// branch from this first event so it gets its own lane immediately.
|
|
760
770
|
branch = isProcessBoundary ? ownBranch : parentBranch;
|
|
761
|
-
parents =
|
|
771
|
+
parents = lastInSpanContext(parentSpan);
|
|
762
772
|
} else if (isEndEvent) {
|
|
763
773
|
// End commit: continues the parent branch chronologically and merges in own-branch work.
|
|
764
774
|
//
|
|
@@ -773,18 +783,13 @@ const spanTreeToCommits = (
|
|
|
773
783
|
// to main) would otherwise gain commits after its own merge and never re-collapse — leaving a
|
|
774
784
|
// dangling lane after the child finishes.
|
|
775
785
|
branch = isProcessBoundary ? MAIN_BRANCH : parentBranch;
|
|
776
|
-
parents = builder.
|
|
777
|
-
CommitSelector.unionAll(
|
|
778
|
-
CommitSelector.branch(branch).pipe(CommitSelector.compose(CommitSelector.last())),
|
|
779
|
-
CommitSelector.branch(ownBranch).pipe(CommitSelector.compose(CommitSelector.last())),
|
|
780
|
-
),
|
|
781
|
-
);
|
|
786
|
+
parents = parentIds(builder.tipOf(branch), builder.tipOf(ownBranch));
|
|
782
787
|
} else {
|
|
783
788
|
// Middle event: continue the span's own branch; fall back to span's own context so the
|
|
784
789
|
// first middle event of a span forks from the span's begin commit, not from a sibling's
|
|
785
790
|
// commit on an ancestor branch.
|
|
786
791
|
branch = ownBranch;
|
|
787
|
-
parents =
|
|
792
|
+
parents = lastInSpanContext(span);
|
|
788
793
|
}
|
|
789
794
|
|
|
790
795
|
const commitIndex = globalIndex++;
|
|
@@ -1010,26 +1015,58 @@ export const CommitSelector = {
|
|
|
1010
1015
|
}),
|
|
1011
1016
|
};
|
|
1012
1017
|
|
|
1018
|
+
/**
|
|
1019
|
+
* Accumulates the commit list. Every lookup the graph makes while building — the tip of a branch,
|
|
1020
|
+
* a commit by id — is served from an index, because each is asked once per commit and a scan would
|
|
1021
|
+
* make the whole build quadratic in the history.
|
|
1022
|
+
*/
|
|
1013
1023
|
class GraphBuilder {
|
|
1014
1024
|
#commits: Commit[] = [];
|
|
1015
1025
|
#branches = new Set<string>();
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
return selector.select(this.#commits);
|
|
1019
|
-
}
|
|
1026
|
+
#indexById = new Map<string, number>();
|
|
1027
|
+
#tipIndexByBranch = new Map<string, number>();
|
|
1020
1028
|
|
|
1021
1029
|
hasBranch(branch: string): boolean {
|
|
1022
1030
|
return this.#branches.has(branch);
|
|
1023
1031
|
}
|
|
1024
1032
|
|
|
1033
|
+
hasCommit(commitId: string): boolean {
|
|
1034
|
+
return this.#indexById.has(commitId);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** The most recent commit on a branch, or undefined while the branch has none. */
|
|
1038
|
+
tipOf(branch: string | Falsy): Commit | undefined {
|
|
1039
|
+
if (!branch) {
|
|
1040
|
+
return undefined;
|
|
1041
|
+
}
|
|
1042
|
+
const index = this.#tipIndexByBranch.get(branch);
|
|
1043
|
+
return index === undefined ? undefined : this.#commits[index];
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/** The most recent commit on the first of `branches` that has one. */
|
|
1047
|
+
firstTipOf(...branches: (string | Falsy)[]): Commit | undefined {
|
|
1048
|
+
for (const branch of branches) {
|
|
1049
|
+
const tip = this.tipOf(branch);
|
|
1050
|
+
if (tip) {
|
|
1051
|
+
return tip;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return undefined;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1025
1057
|
addCommit(commit: Commit) {
|
|
1026
1058
|
this.addBranch(commit.branch);
|
|
1027
|
-
this.#commits.push(commit);
|
|
1059
|
+
const index = this.#commits.push(commit) - 1;
|
|
1060
|
+
// Duplicate ids survive until `doctor` drops them, and it keeps the first — so does this index.
|
|
1061
|
+
if (!this.#indexById.has(commit.id)) {
|
|
1062
|
+
this.#indexById.set(commit.id, index);
|
|
1063
|
+
}
|
|
1064
|
+
this.#tipIndexByBranch.set(commit.branch, index);
|
|
1028
1065
|
}
|
|
1029
1066
|
|
|
1030
1067
|
addTag(commitId: string, tag: string) {
|
|
1031
|
-
const index = this.#
|
|
1032
|
-
if (index
|
|
1068
|
+
const index = this.#indexById.get(commitId);
|
|
1069
|
+
if (index === undefined) {
|
|
1033
1070
|
return;
|
|
1034
1071
|
}
|
|
1035
1072
|
const commit = this.#commits[index]!;
|
|
@@ -1039,13 +1076,6 @@ class GraphBuilder {
|
|
|
1039
1076
|
this.#commits[index] = { ...commit, tags: [...(commit.tags ?? []), tag] };
|
|
1040
1077
|
}
|
|
1041
1078
|
|
|
1042
|
-
/**
|
|
1043
|
-
* Computes parents — picks first matching commit's id.
|
|
1044
|
-
*/
|
|
1045
|
-
computeParents(selector: CommitSelector): string[] {
|
|
1046
|
-
return Array.dedupe(selector.select(this.#commits).map((commit) => commit.id));
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
1079
|
addBranch(branch: string) {
|
|
1050
1080
|
this.#branches.add(branch);
|
|
1051
1081
|
}
|
|
@@ -1054,16 +1084,23 @@ class GraphBuilder {
|
|
|
1054
1084
|
* Removes duplicate commits and dangling parents.
|
|
1055
1085
|
*/
|
|
1056
1086
|
doctor() {
|
|
1087
|
+
const seen = new Set<string>();
|
|
1057
1088
|
this.#commits = pipe(
|
|
1058
1089
|
this.#commits,
|
|
1059
|
-
Array.
|
|
1090
|
+
Array.filter((commit) => {
|
|
1091
|
+
if (seen.has(commit.id)) {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
seen.add(commit.id);
|
|
1095
|
+
return true;
|
|
1096
|
+
}),
|
|
1060
1097
|
Array.map(
|
|
1061
1098
|
Struct.evolve({
|
|
1062
|
-
parents: (parents) =>
|
|
1063
|
-
parents ? Array.filter(parents, (id) => this.#commits.some((commit) => commit.id === id)) : undefined,
|
|
1099
|
+
parents: (parents) => (parents ? Array.filter(parents, (id) => seen.has(id)) : undefined),
|
|
1064
1100
|
}),
|
|
1065
1101
|
),
|
|
1066
1102
|
);
|
|
1103
|
+
this.#reindex();
|
|
1067
1104
|
}
|
|
1068
1105
|
|
|
1069
1106
|
build() {
|
|
@@ -1073,4 +1110,15 @@ class GraphBuilder {
|
|
|
1073
1110
|
branches: [...this.#branches],
|
|
1074
1111
|
};
|
|
1075
1112
|
}
|
|
1113
|
+
|
|
1114
|
+
#reindex() {
|
|
1115
|
+
this.#indexById.clear();
|
|
1116
|
+
this.#tipIndexByBranch.clear();
|
|
1117
|
+
this.#commits.forEach((commit, index) => {
|
|
1118
|
+
if (!this.#indexById.has(commit.id)) {
|
|
1119
|
+
this.#indexById.set(commit.id, index);
|
|
1120
|
+
}
|
|
1121
|
+
this.#tipIndexByBranch.set(commit.branch, index);
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1076
1124
|
}
|
|
@@ -34,7 +34,7 @@ export const formatPendingBlockStatus = (block: ContentBlock.Any): string | unde
|
|
|
34
34
|
|
|
35
35
|
if (block._tag === 'toolCall') {
|
|
36
36
|
const bytes = new TextEncoder().encode(block.input).length;
|
|
37
|
-
const name = block.operationName ?? block.name;
|
|
37
|
+
const name = block.displayName ?? block.operationName ?? block.name;
|
|
38
38
|
return `Calling ${name} (${bytes} bytes)...`;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import { describe, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import * as Trace from '@dxos/compute/Trace';
|
|
8
|
-
import { renderTimelineAscii } from '../components/Timeline/index.ts';
|
|
9
8
|
|
|
9
|
+
import { renderTimelineAscii } from '../components/Timeline/index.ts';
|
|
10
10
|
import { buildExecutionGraph, collectProcessActivityLines, deriveInFlightActivityLine } from './execution-graph.ts';
|
|
11
11
|
import subAgentFixture from './testing/sub-agent-delegation.json';
|
|
12
12
|
|
|
@@ -5,15 +5,14 @@
|
|
|
5
5
|
import { useAtomValue } from '@effect/atom-react/Hooks';
|
|
6
6
|
import * as Duration from 'effect/Duration';
|
|
7
7
|
import { pipe } from 'effect/Function';
|
|
8
|
-
import * as Atom from 'effect/
|
|
8
|
+
import * as Atom from 'effect/reactivity/Atom';
|
|
9
9
|
import { useEffect, useMemo, useState } from 'react';
|
|
10
10
|
|
|
11
11
|
import * as Process from '@dxos/compute/Process';
|
|
12
12
|
import { type Space } from '@dxos/react-client/echo';
|
|
13
13
|
|
|
14
|
-
import { filterTraceMessages } from '../components/TracePanel/trace-filter.ts';
|
|
14
|
+
import { filterProcessesBySelection, filterTraceMessages } from '../components/TracePanel/trace-filter.ts';
|
|
15
15
|
import { type ExecutionGraph, buildExecutionGraph } from '../execution-graph/index.ts';
|
|
16
|
-
|
|
17
16
|
import { getTraceMessagesAtom } from './useTraceMessages.ts';
|
|
18
17
|
|
|
19
18
|
// Stable refs.
|
|
@@ -25,6 +24,11 @@ const NO_PIDS: readonly string[] = [];
|
|
|
25
24
|
// benefit to re-deriving the graph more often than this just to catch the timeout crossing.
|
|
26
25
|
const SPAN_TIMEOUT_CHECK_INTERVAL_MS = 60_000;
|
|
27
26
|
|
|
27
|
+
// The trace feed emits per message while anything runs, and the process monitor re-polls on its own
|
|
28
|
+
// clock; every emission rebuilds the graph from the whole history, so the rebuild rate is capped
|
|
29
|
+
// rather than either input's, as `useSessionTimeline` already does.
|
|
30
|
+
const REBUILD_DEBOUNCE = Duration.millis(500);
|
|
31
|
+
|
|
28
32
|
export type UseExecutionGraphOptions = {
|
|
29
33
|
collapseCompletedSpans?: boolean;
|
|
30
34
|
eventLimit?: number;
|
|
@@ -39,7 +43,7 @@ export type UseExecutionGraphOptions = {
|
|
|
39
43
|
*/
|
|
40
44
|
export const useExecutionGraph = (
|
|
41
45
|
space: Space,
|
|
42
|
-
processesAtom: Atom.Atom<readonly Process.
|
|
46
|
+
processesAtom: Atom.Atom<readonly Process.Process[]> | undefined,
|
|
43
47
|
{ collapseCompletedSpans, eventLimit, selectedPids = NO_PIDS }: UseExecutionGraphOptions = {},
|
|
44
48
|
): ExecutionGraph => {
|
|
45
49
|
// Ticks periodically so spans that are still open purely because no new trace event has
|
|
@@ -62,13 +66,13 @@ export const useExecutionGraph = (
|
|
|
62
66
|
};
|
|
63
67
|
|
|
64
68
|
/** Identity for the graph: only a process appearing, disappearing or changing state redraws it. */
|
|
65
|
-
const sameProcesses = (left: readonly Process.
|
|
69
|
+
const sameProcesses = (left: readonly Process.Process[], right: readonly Process.Process[]): boolean =>
|
|
66
70
|
left.length === right.length &&
|
|
67
71
|
left.every((process, index) => process.pid === right[index].pid && process.state === right[index].state);
|
|
68
72
|
|
|
69
73
|
const getExecutionGraph = (
|
|
70
74
|
space: Space,
|
|
71
|
-
processesAtom: Atom.Atom<readonly Process.
|
|
75
|
+
processesAtom: Atom.Atom<readonly Process.Process[]>,
|
|
72
76
|
{
|
|
73
77
|
collapseCompletedSpans = true,
|
|
74
78
|
eventLimit = 100,
|
|
@@ -77,17 +81,16 @@ const getExecutionGraph = (
|
|
|
77
81
|
}: UseExecutionGraphOptions & { now: number },
|
|
78
82
|
): Atom.Atom<ExecutionGraph> => {
|
|
79
83
|
const traceMessages = getTraceMessagesAtom(space).pipe(
|
|
84
|
+
Atom.debounce(REBUILD_DEBOUNCE),
|
|
80
85
|
Atom.map((messages) => filterTraceMessages(messages, selectedPids)),
|
|
81
86
|
);
|
|
82
87
|
|
|
83
88
|
const activeProcesses = pipe(
|
|
84
89
|
processesAtom,
|
|
85
|
-
Atom.debounce(
|
|
90
|
+
Atom.debounce(REBUILD_DEBOUNCE),
|
|
86
91
|
Atom.map((processes) =>
|
|
87
|
-
processes.filter(
|
|
88
|
-
(process) =>
|
|
89
|
-
(process.state === Process.State.RUNNING || process.state === Process.State.HYBERNATING) &&
|
|
90
|
-
(selectedPids.length === 0 || selectedPids.includes(process.pid)),
|
|
92
|
+
filterProcessesBySelection(processes, selectedPids).filter(
|
|
93
|
+
(process) => process.state === Process.State.RUNNING || process.state === Process.State.HYBERNATING,
|
|
91
94
|
),
|
|
92
95
|
),
|
|
93
96
|
// The monitor rebuilds the process list on every poll, so without a structural comparison the
|
|
@@ -3,14 +3,20 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import * as Duration from 'effect/Duration';
|
|
6
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
6
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import type * as Process from '@dxos/compute/Process';
|
|
9
|
+
import { Obj } from '@dxos/echo';
|
|
9
10
|
import { type Space } from '@dxos/react-client/echo';
|
|
10
11
|
import { type Task } from '@dxos/types';
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
13
|
+
import {
|
|
14
|
+
type Session,
|
|
15
|
+
type SessionTimeline,
|
|
16
|
+
type TaskStatusChange,
|
|
17
|
+
buildSessionTimeline,
|
|
18
|
+
readTaskStatusChanges,
|
|
19
|
+
} from '../session-timeline/index.ts';
|
|
14
20
|
import { useTraceMessages } from './useTraceMessages.ts';
|
|
15
21
|
|
|
16
22
|
/** How often `now` advances while a session is open, so a running lane's range keeps growing. */
|
|
@@ -20,14 +26,31 @@ const TICK_MS = 5_000;
|
|
|
20
26
|
// timeline from the full message history — so the rebuild rate is capped rather than the feed's.
|
|
21
27
|
const TRACE_DEBOUNCE = Duration.millis(500);
|
|
22
28
|
|
|
23
|
-
const NO_PROCESSES: readonly Process.
|
|
29
|
+
const NO_PROCESSES: readonly Process.Process[] = [];
|
|
30
|
+
|
|
31
|
+
type StatusChangeCache = Map<string, { heads: string; changes: TaskStatusChange[] }>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A task's status moves, re-read only when its document has moved on: reading them diffs every change
|
|
35
|
+
* in the document, and the timeline rebuilds on every trace emission and tick.
|
|
36
|
+
*/
|
|
37
|
+
const cachedStatusChanges = (cache: StatusChangeCache, task: Task.Task): TaskStatusChange[] => {
|
|
38
|
+
const heads = (Obj.version(task).automergeHeads ?? []).join(',');
|
|
39
|
+
const cached = cache.get(task.id);
|
|
40
|
+
if (cached?.heads === heads) {
|
|
41
|
+
return cached.changes;
|
|
42
|
+
}
|
|
43
|
+
const changes = readTaskStatusChanges(task);
|
|
44
|
+
cache.set(task.id, { heads, changes });
|
|
45
|
+
return changes;
|
|
46
|
+
};
|
|
24
47
|
|
|
25
48
|
export type UseSessionTimelineOptions = {
|
|
26
49
|
/** The sessions shown; each contributes a session lane and its checklist's task lanes. */
|
|
27
50
|
sessions: readonly Session[];
|
|
28
51
|
tasks?: readonly Task.Task[];
|
|
29
52
|
/** The live process tree, from whichever monitor the host has; without it a session's liveness is read from the trace alone. */
|
|
30
|
-
processes?: readonly Process.
|
|
53
|
+
processes?: readonly Process.Process[];
|
|
31
54
|
};
|
|
32
55
|
|
|
33
56
|
/**
|
|
@@ -46,8 +69,11 @@ export const useSessionTimeline = (
|
|
|
46
69
|
return () => clearInterval(interval);
|
|
47
70
|
}, []);
|
|
48
71
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
const statusChangeCache = useRef<StatusChangeCache>(new Map());
|
|
73
|
+
return useMemo(() => {
|
|
74
|
+
const taskStatusChanges = new Map(
|
|
75
|
+
(tasks ?? []).map((task) => [task.id, cachedStatusChanges(statusChangeCache.current, task)]),
|
|
76
|
+
);
|
|
77
|
+
return buildSessionTimeline({ traceMessages, processes, sessions, tasks, taskStatusChanges, now });
|
|
78
|
+
}, [traceMessages, processes, sessions, tasks, now]);
|
|
53
79
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { useAtomValue } from '@effect/atom-react/Hooks';
|
|
6
6
|
import * as Duration from 'effect/Duration';
|
|
7
7
|
import { pipe } from 'effect/Function';
|
|
8
|
-
import * as Atom from 'effect/
|
|
8
|
+
import * as Atom from 'effect/reactivity/Atom';
|
|
9
9
|
import { useMemo } from 'react';
|
|
10
10
|
|
|
11
11
|
import { FeedTraceSink } from '@dxos/compute-runtime';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
|
+
|
|
8
|
+
import * as Trace from '@dxos/compute/Trace';
|
|
9
|
+
import { Feed, Obj, Ref } from '@dxos/echo';
|
|
10
|
+
import { EID, EntityId } from '@dxos/keys';
|
|
11
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
12
|
+
import { Task } from '@dxos/types';
|
|
13
|
+
|
|
14
|
+
import { Gantt } from '../components/index.ts';
|
|
15
|
+
import { sessionTimelineToGantt } from './gantt-mapping.ts';
|
|
16
|
+
import { type TaskStatusChange, buildSessionTimeline } from './session-timeline.ts';
|
|
17
|
+
import { type Session } from './types.ts';
|
|
18
|
+
|
|
19
|
+
const T0 = Date.UTC(2026, 8, 25, 12, 5, 22);
|
|
20
|
+
const MINUTE = 60_000;
|
|
21
|
+
const minute = (minutes: number): number => T0 + minutes * MINUTE;
|
|
22
|
+
const at = (minutes: number): string => new Date(minute(minutes)).toISOString();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One session's checklist, and the status moves each task's edit history records — the story's tasks
|
|
26
|
+
* live in no database, so their moves are given rather than read. With `history` off, the chart sees
|
|
27
|
+
* only the trace.
|
|
28
|
+
*/
|
|
29
|
+
const makeTasks = (history: boolean) => {
|
|
30
|
+
const audit = Task.make({ title: 'Audit current artifact storage', status: 'done' });
|
|
31
|
+
const design = Task.make({ title: 'Design history entry schema', status: 'done' });
|
|
32
|
+
const migrate = Task.make({ title: 'Write the migration', status: 'done' });
|
|
33
|
+
const backfill = Task.make({
|
|
34
|
+
title: 'Backfill existing tasks',
|
|
35
|
+
status: 'todo',
|
|
36
|
+
dependsOn: [Ref.make(migrate), Ref.make(design)],
|
|
37
|
+
});
|
|
38
|
+
const worked = (start: number, end: number): TaskStatusChange[] => [
|
|
39
|
+
{ timestamp: minute(start), status: 'started', previousStatus: 'todo' },
|
|
40
|
+
{ timestamp: minute(end), status: 'done', previousStatus: 'started' },
|
|
41
|
+
];
|
|
42
|
+
if (history) {
|
|
43
|
+
const question = Task.ask(design, {
|
|
44
|
+
text: 'Keep artifacts as refs or fold them into history?',
|
|
45
|
+
actor: { name: 'Scout', role: 'assistant' },
|
|
46
|
+
date: at(36),
|
|
47
|
+
});
|
|
48
|
+
Task.answer(design, question.id, 'Keep refs', { date: at(47) });
|
|
49
|
+
}
|
|
50
|
+
const taskStatusChanges = new Map<string, TaskStatusChange[]>(
|
|
51
|
+
history
|
|
52
|
+
? [
|
|
53
|
+
[audit.id, worked(1, 26)],
|
|
54
|
+
[design.id, worked(28, 55)],
|
|
55
|
+
[migrate.id, worked(57, 111)],
|
|
56
|
+
]
|
|
57
|
+
: [],
|
|
58
|
+
);
|
|
59
|
+
return { tasks: [audit, design, migrate, backfill], taskStatusChanges };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The session's own trace: two requests and the tool calls between them, with no task status events. */
|
|
63
|
+
const makeTrace = (feed: Ref.Ref<Feed.Feed>): Trace.Message[] => {
|
|
64
|
+
const event = (minutes: number, type: string, data: unknown) => ({ timestamp: T0 + minutes * MINUTE, type, data });
|
|
65
|
+
const toolCall = (minutes: number, name: string) =>
|
|
66
|
+
event(minutes, Trace.CompleteBlock.key, {
|
|
67
|
+
messageId: EntityId.random(),
|
|
68
|
+
role: 'assistant',
|
|
69
|
+
block: { _tag: 'toolCall', toolCallId: name, name, input: '{}', providerExecuted: false },
|
|
70
|
+
});
|
|
71
|
+
return [
|
|
72
|
+
Obj.make(Trace.Message, {
|
|
73
|
+
meta: { pid: 'agent', conversation: feed },
|
|
74
|
+
isEphemeral: false,
|
|
75
|
+
events: [
|
|
76
|
+
event(0, Trace.AgentRequestBegin.key, {}),
|
|
77
|
+
toolCall(4, 'Search code'),
|
|
78
|
+
toolCall(12, 'Read file'),
|
|
79
|
+
toolCall(20, 'Read file'),
|
|
80
|
+
toolCall(33, 'Write document'),
|
|
81
|
+
toolCall(42, 'Ask question'),
|
|
82
|
+
event(58, Trace.AgentRequestEnd.key, { status: 'success' }),
|
|
83
|
+
event(104, Trace.AgentRequestBegin.key, {}),
|
|
84
|
+
toolCall(106, 'Edit file'),
|
|
85
|
+
toolCall(109, 'Run tests'),
|
|
86
|
+
event(113, Trace.AgentRequestEnd.key, { status: 'success' }),
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const Timeline = ({ title, history }: { title: string; history: boolean }) => {
|
|
93
|
+
const timeline = useMemo(() => {
|
|
94
|
+
const feed = Feed.make();
|
|
95
|
+
const { tasks, taskStatusChanges } = makeTasks(history);
|
|
96
|
+
const id = EntityId.random();
|
|
97
|
+
const session: Session = {
|
|
98
|
+
id,
|
|
99
|
+
label: 'Schema Migration: Task Artifacts to History',
|
|
100
|
+
uri: EID.make({ entityId: id }).toString(),
|
|
101
|
+
feedId: feed.id,
|
|
102
|
+
taskIds: tasks.map((task) => task.id),
|
|
103
|
+
};
|
|
104
|
+
return buildSessionTimeline({
|
|
105
|
+
traceMessages: makeTrace(Ref.make(feed)),
|
|
106
|
+
sessions: [session],
|
|
107
|
+
tasks,
|
|
108
|
+
taskStatusChanges,
|
|
109
|
+
});
|
|
110
|
+
}, [history]);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className='flex flex-col gap-1'>
|
|
114
|
+
<h2 className='px-2 text-sm text-fg-subtle'>{title}</h2>
|
|
115
|
+
<Gantt.Root {...sessionTimelineToGantt(timeline)} range={timeline.range} classNames='p-2'>
|
|
116
|
+
<Gantt.Legend>
|
|
117
|
+
<Gantt.LegendToggle />
|
|
118
|
+
</Gantt.Legend>
|
|
119
|
+
<Gantt.Chart />
|
|
120
|
+
</Gantt.Root>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const DefaultStory = () => (
|
|
126
|
+
<div className='flex flex-col gap-6 p-4'>
|
|
127
|
+
<Timeline title='Trace only — task lanes have no span' history={false} />
|
|
128
|
+
<Timeline title='With edit history — begin, end, status moves and questions as nodes' history={true} />
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const meta = {
|
|
133
|
+
title: 'ui/react-ui-trace/SessionTimeline',
|
|
134
|
+
render: DefaultStory,
|
|
135
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
136
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
137
|
+
|
|
138
|
+
export default meta;
|
|
139
|
+
|
|
140
|
+
type Story = StoryObj<typeof meta>;
|
|
141
|
+
|
|
142
|
+
export const TaskHistory: Story = {};
|