@workflow/web-shared 5.0.0-beta.41 → 5.0.0-beta.43

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 (96) hide show
  1. package/dist/components/event-list-view.d.ts +10 -2
  2. package/dist/components/event-list-view.d.ts.map +1 -1
  3. package/dist/components/event-list-view.js +52 -26
  4. package/dist/components/sidebar/attributes-block.d.ts.map +1 -1
  5. package/dist/components/sidebar/attributes-block.js +3 -6
  6. package/dist/components/sidebar/conversation-view.d.ts.map +1 -1
  7. package/dist/components/sidebar/conversation-view.js +19 -36
  8. package/dist/components/sidebar/entity-detail-panel.d.ts +5 -0
  9. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  10. package/dist/components/sidebar/entity-detail-panel.js +2 -2
  11. package/dist/components/sidebar/events-list.d.ts +7 -1
  12. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  13. package/dist/components/sidebar/events-list.js +6 -5
  14. package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -1
  15. package/dist/components/sidebar/resolve-hook-modal.js +4 -145
  16. package/dist/components/sidebar/sidebar-data-context.d.ts +6 -0
  17. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -1
  18. package/dist/components/sidebar/sidebar-data-context.js +1 -1
  19. package/dist/components/stream-viewer-skeleton.js +6 -6
  20. package/dist/components/stream-viewer.js +2 -2
  21. package/dist/components/trace-viewer/components/detail-panel.d.ts.map +1 -1
  22. package/dist/components/trace-viewer/components/detail-panel.js +2 -1
  23. package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.d.ts +9 -0
  24. package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.d.ts.map +1 -0
  25. package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.js +29 -0
  26. package/dist/components/trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -1
  27. package/dist/components/trace-viewer/components/middle-truncate/middle-truncate.js +16 -8
  28. package/dist/components/trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -1
  29. package/dist/components/trace-viewer/components/middle-truncate/use-middle-truncate.js +83 -19
  30. package/dist/components/trace-viewer/components/timeline.d.ts.map +1 -1
  31. package/dist/components/trace-viewer/components/timeline.js +13 -22
  32. package/dist/components/trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -1
  33. package/dist/components/trace-viewer/components/trace-viewer-skeleton.js +58 -18
  34. package/dist/components/trace-viewer/icons.js +5 -5
  35. package/dist/components/trace-viewer/utils.d.ts +6 -10
  36. package/dist/components/trace-viewer/utils.d.ts.map +1 -1
  37. package/dist/components/trace-viewer/utils.js +14 -24
  38. package/dist/components/trace-viewer.d.ts.map +1 -1
  39. package/dist/components/trace-viewer.js +12 -4
  40. package/dist/components/ui/context-card.d.ts.map +1 -1
  41. package/dist/components/ui/context-card.js +9 -21
  42. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  43. package/dist/components/ui/data-inspector.js +8 -35
  44. package/dist/components/ui/duplicate-event-tooltip.d.ts +15 -0
  45. package/dist/components/ui/duplicate-event-tooltip.d.ts.map +1 -0
  46. package/dist/components/ui/duplicate-event-tooltip.js +19 -0
  47. package/dist/components/ui/menu-dropdown.d.ts.map +1 -1
  48. package/dist/components/ui/menu-dropdown.js +3 -19
  49. package/dist/components/ui/skeleton.js +2 -2
  50. package/dist/components/ui/spinner.d.ts.map +1 -1
  51. package/dist/components/ui/spinner.js +5 -11
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +2 -1
  55. package/dist/lib/duplicate-events.d.ts +27 -0
  56. package/dist/lib/duplicate-events.d.ts.map +1 -0
  57. package/dist/lib/duplicate-events.js +166 -0
  58. package/dist/lib/event-materialization.d.ts +15 -2
  59. package/dist/lib/event-materialization.d.ts.map +1 -1
  60. package/dist/lib/event-materialization.js +19 -4
  61. package/dist/lib/trace-builder.d.ts +16 -1
  62. package/dist/lib/trace-builder.d.ts.map +1 -1
  63. package/dist/lib/trace-builder.js +23 -4
  64. package/package.json +4 -4
  65. package/src/components/event-list-view.tsx +70 -32
  66. package/src/components/sidebar/attributes-block.tsx +4 -11
  67. package/src/components/sidebar/conversation-view.tsx +25 -79
  68. package/src/components/sidebar/entity-detail-panel.tsx +6 -0
  69. package/src/components/sidebar/events-list.tsx +21 -3
  70. package/src/components/sidebar/resolve-hook-modal.tsx +23 -186
  71. package/src/components/sidebar/sidebar-data-context.tsx +6 -0
  72. package/src/components/stream-viewer-skeleton.tsx +5 -5
  73. package/src/components/stream-viewer.tsx +1 -1
  74. package/src/components/trace-viewer/components/detail-panel.tsx +1 -0
  75. package/src/components/trace-viewer/components/middle-truncate/measurement-batch.ts +47 -0
  76. package/src/components/trace-viewer/components/middle-truncate/middle-truncate.tsx +15 -7
  77. package/src/components/trace-viewer/components/middle-truncate/use-middle-truncate.ts +132 -38
  78. package/src/components/trace-viewer/components/timeline.tsx +21 -39
  79. package/src/components/trace-viewer/components/trace-viewer-skeleton.tsx +63 -35
  80. package/src/components/trace-viewer/icons.tsx +4 -4
  81. package/src/components/trace-viewer/utils.test.ts +5 -7
  82. package/src/components/trace-viewer/utils.ts +17 -31
  83. package/src/components/trace-viewer.tsx +15 -3
  84. package/src/components/ui/context-card.tsx +16 -26
  85. package/src/components/ui/data-inspector.tsx +20 -65
  86. package/src/components/ui/duplicate-event-tooltip.tsx +44 -0
  87. package/src/components/ui/menu-dropdown.tsx +8 -22
  88. package/src/components/ui/skeleton.tsx +2 -2
  89. package/src/components/ui/spinner.tsx +4 -8
  90. package/src/index.ts +4 -0
  91. package/src/lib/duplicate-events.test.ts +339 -0
  92. package/src/lib/duplicate-events.ts +188 -0
  93. package/src/lib/event-materialization.test.ts +117 -0
  94. package/src/lib/event-materialization.ts +23 -3
  95. package/src/lib/trace-builder.test.ts +74 -0
  96. package/src/lib/trace-builder.ts +30 -3
@@ -8,6 +8,7 @@
8
8
  import { isHookLifecycleEventType, isStepEventType, isWaitEventType, } from '@workflow/world';
9
9
  import { getEventTimestamp, hookToSpan, runToSpan, stepToSpan, WORKFLOW_LIBRARY, waitToSpan, } from '../components/workflow-traces/trace-span-construction';
10
10
  import { otelTimeToMs } from '../components/workflow-traces/trace-time-utils';
11
+ import { findDuplicateEventIds } from './duplicate-events';
11
12
  /**
12
13
  * Events that belong to the run root span rather than a child entity span.
13
14
  * Mirrors the fallthrough logic in {@link groupEventsByCorrelation}: events
@@ -122,9 +123,26 @@ function cascadeSpans(runSpan, spans) {
122
123
  };
123
124
  });
124
125
  }
125
- export function buildTrace(run, events, now) {
126
- const groupedEvents = groupEventsByCorrelation(events);
127
- const latestKnownTime = computeLatestKnownTime(events, run);
126
+ export function buildTrace(run, events, now,
127
+ /**
128
+ * Whether `events` is the run's whole log. Defaults to false, which builds
129
+ * the trace from every event: on a subset there is no way to tell a repeat
130
+ * from the only copy the caller was given, and dropping the wrong one moves
131
+ * a span. See {@link findDuplicateEventIds}.
132
+ */
133
+ { isCompleteHistory = false } = {}) {
134
+ // Span geometry comes from what the run acted on. A repeat of a class the
135
+ // log already records is read past by every replay, and letting one through
136
+ // here would stretch a span to whenever a concurrent replay committed it.
137
+ // The event lists still show them, marked as repeats.
138
+ const duplicateEventIds = findDuplicateEventIds(events, {
139
+ isCompleteHistory,
140
+ });
141
+ const actedOnEvents = duplicateEventIds.size === 0
142
+ ? events
143
+ : events.filter((event) => !duplicateEventIds.has(event.eventId));
144
+ const groupedEvents = groupEventsByCorrelation(actedOnEvents);
145
+ const latestKnownTime = computeLatestKnownTime(actedOnEvents, run);
128
146
  const { runSpan, spans } = buildSpans(run, groupedEvents, now, latestKnownTime);
129
147
  const sortedCascadingSpans = cascadeSpans(runSpan, spans);
130
148
  // Compute known duration relative to trace start
@@ -143,6 +161,7 @@ export function buildTrace(run, events, now) {
143
161
  },
144
162
  ],
145
163
  knownDurationMs: Math.max(0, knownDurationMs),
164
+ duplicateEventIds,
146
165
  };
147
166
  }
148
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhY2UtYnVpbGRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9saWIvdHJhY2UtYnVpbGRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7O0dBTUc7QUFFSCxPQUFPLEVBRUwsd0JBQXdCLEVBQ3hCLGVBQWUsRUFDZixlQUFlLEdBRWhCLE1BQU0saUJBQWlCLENBQUM7QUFDekIsT0FBTyxFQUNMLGlCQUFpQixFQUNqQixVQUFVLEVBQ1YsU0FBUyxFQUNULFVBQVUsRUFDVixnQkFBZ0IsRUFDaEIsVUFBVSxHQUNYLE1BQU0sdURBQXVELENBQUM7QUFDL0QsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGdEQUFnRCxDQUFDO0FBRzlFOzs7Ozs7R0FNRztBQUNILE1BQU0sQ0FBQyxNQUFNLGVBQWUsR0FBRyxDQUFDLEtBQVksRUFBVyxFQUFFLENBQ3ZELENBQUMsS0FBSyxDQUFDLGFBQWE7SUFDcEIsQ0FBQyxDQUFDLGVBQWUsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDO1FBQ2hDLENBQUMsd0JBQXdCLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQztRQUMxQyxDQUFDLGVBQWUsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQztBQUV2Qzs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSxtQkFBbUIsQ0FDakMsTUFBZSxFQUNmLFFBQTRCLEVBQzVCLE1BQTBCO0lBRTFCLElBQUksUUFBUSxLQUFLLEtBQUssRUFBRSxDQUFDO1FBQ3ZCLE9BQU8sTUFBTSxDQUFDLE1BQU0sQ0FBQyxlQUFlLENBQUMsQ0FBQztJQUN4QyxDQUFDO0lBQ0QsSUFBSSxDQUFDLE1BQU07UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUN2QixPQUFPLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxhQUFhLEtBQUssTUFBTSxDQUFDLENBQUM7QUFDMUQsQ0FBQztBQWFELFNBQVMsU0FBUyxD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QUFLO1FBQ2xCLFVBQVUsRUFBRSxHQUFHLENBQUMsS0FBSztRQUNyQixLQUFLLEVBQUUsb0JBQW9CO1FBQzNCLFNBQVMsRUFBRTtZQUNUO2dCQUNFLElBQUksRUFBRSxVQUFVO2dCQUNoQixVQUFVLEVBQUU7b0JBQ1YsY0FBYyxFQUFFLGdCQUFnQixDQUFDLElBQUk7aUJBQ3RDO2FBQ0Y7U0FDRjtRQUNELGVBQWUsRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxlQUFlLENBQUM7S0FDOUMsQ0FBQztBQUNKLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIEJ1aWxkcyBhIGNvbXBsZXRlIHRyYWNlIGZyb20gYSBXb3JrZmxvd1J1biBhbmQgaXRzIEV2ZW50cy5cbiAqXG4gKiBUaGlzIG1vZHVsZSBncm91cHMgcmF3IGV2ZW50cyBieSBjb3JyZWxhdGlvbiBJRCBhbmQgZW50aXR5IHR5cGUsXG4gKiBjb252ZXJ0cyBlYWNoIGdyb3VwIGludG8gYW4gT3BlblRlbGVtZXRyeS1zdHlsZSBTcGFuLCBhbmQgcmV0dXJuc1xuICogYSBmdWxseS1mb3JtZWQgVHJhY2UgcmVhZHkgZm9yIHRoZSB0cmFjZSB2aWV3ZXIuXG4gKi9cblxuaW1wb3J0IHtcbiAgdHlwZSBFdmVudCxcbiAgaXNIb29rTGlmZWN5Y2xlRXZlbnRUeXBlLFxuICBpc1N0ZXBFdmVudFR5cGUsXG4gIGlzV2FpdEV2ZW50VHlwZSxcbiAgdHlwZSBXb3JrZmxvd1J1bixcbn0gZnJvbSAnQHdvcmtmbG93L3dvcmxkJztcbmltcG9ydCB7XG4gIGdldEV2ZW50VGltZXN0YW1wLFxuICBob29rVG9TcGFuLFxuICBydW5Ub1NwYW4sXG4gIHN0ZXBUb1NwYW4sXG4gIFdPUktGTE9XX0xJQlJBUlksXG4gIHdhaXRUb1NwYW4sXG59IGZyb20gJy4uL2NvbXBvbmVudHMvd29ya2Zsb3ctdHJhY2VzL3RyYWNlLXNwYW4tY29uc3RydWN0aW9uJztcbmltcG9ydCB7IG90ZWxUaW1lVG9NcyB9IGZyb20gJy4uL2NvbXBvbmVudHMvd29ya2Zsb3ctdHJhY2VzL3RyYWNlLXRpbWUtdXRpbHMnO1xuaW1wb3J0IHR5cGUgeyBTcGFuIH0gZnJvbSAnLi90cmFjZS10eXBlcyc7XG5cbi8qKlxuICogRXZlbnRzIHRoYXQgYmVsb25nIHRvIHRoZSBydW4gcm9vdCBzcGFuIHJhdGhlciB0aGFuIGEgY2hpbGQgZW50aXR5IHNwYW4uXG4gKiBNaXJyb3JzIHRoZSBmYWxsdGhyb3VnaCBsb2dpYyBpbiB7QGxpbmsgZ3JvdXBFdmVudHNCeUNvcnJlbGF0aW9ufTogZXZlbnRzXG4gKiB3aXRob3V0IGEgY29ycmVsYXRpb25JZCAocnVuIGxpZmVjeWNsZSkgcGx1cyBjb3JyZWxhdGVkIGV2ZW50cyB0aGF0IGFyZW4ndFxuICogc3RlcC90aW1lci9ob29rIGV2ZW50cyAoZS5nLiBgYXR0cl9zZXRgLCB3aG9zZSBjb3JyZWxhdGlvbklkIGlzIGEgZGVkdXBcbiAqIHRva2VuIHJhdGhlciB0aGFuIGEgY2hpbGQgZW50aXR5IElEKS5cbiAqL1xuZXhwb3J0IGNvbnN0IGlzUnVuTGV2ZWxFdmVudCA9IChldmVudDogRXZlbnQpOiBib29sZWFuID0+XG4gICFldmVudC5jb3JyZWxhdGlvbklkIHx8XG4gICghaXNXYWl0RXZlbnRUeXBlKGV2ZW50LmV2ZW50VHlwZSkgJiZcbiAgICAhaXNIb29rTGlmZWN5Y2xlRXZlbnRUeXBlKGV2ZW50LmV2ZW50VHlwZSkgJiZcbiAgICAhaXNTdGVwRXZlbnRUeXBlKGV2ZW50LmV2ZW50VHlwZSkpO1xuXG4vKipcbiAqIEZpbHRlciB0aGUgcmF3IGV2ZW50IGxpc3QgZG93biB0byB0aGUgZXZlbnRzIGZvciBhIHNlbGVjdGVkIHNwYW4uXG4gKiBGb3IgdGhlIHJ1biByb290IHNwYW4gdGhpcyByZXR1cm5zIHJ1bi1sZXZlbCBldmVudHMgKHNlZVxuICoge0BsaW5rIGlzUnVuTGV2ZWxFdmVudH0pOyBmb3IgY2hpbGQgc3BhbnMgaXQgcmV0dXJucyB0aGUgZXZlbnRzXG4gKiBjb3JyZWxhdGVkIHRvIHRoYXQgc3BhbidzIElELlxuICovXG5leHBvcnQgZnVuY3Rpb24gZmlsdGVyU3BhblJhd0V2ZW50cyhcbiAgZXZlbnRzOiBFdmVudFtdLFxuICByZXNvdXJjZTogc3RyaW5nIHwgdW5kZWZpbmVkLFxuICBzcGFuSWQ6IHN0cmluZyB8IHVuZGVmaW5lZFxuKTogRXZlbnRbXSB7XG4gIGlmIChyZXNvdXJjZSA9PT0gJ3J1bicpIHtcbiAgICByZXR1cm4gZXZlbnRzLmZpbHRlcihpc1J1bkxldmVsRXZlbnQpO1xuICB9XG4gIGlmICghc3BhbklkKSByZXR1cm4gW107XG4gIHJldHVybiBldmVudHMuZmlsdGVyKChlKSA9PiBlLmNvcnJlbGF0aW9uSWQgPT09IHNwYW5JZCk7XG59XG5cbi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuLy8gRXZlbnQgZ3JvdXBpbmdcbi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuXG5leHBvcnQgdHlwZSBHcm91cGVkRXZlbnRzID0ge1xuICBldmVudHNCeVN0ZXBJZDogTWFwPHN0cmluZywgRXZlbnRbXT47XG4gIHJ1bkxldmVsRXZlbnRzOiBFdmVudFtdO1xuICB0aW1lckV2ZW50czogTWFwPHN0cmluZywgRXZlbnRbXT47XG4gIGhvb2tFdmVudHM6IE1hcDxzdHJpbmcsIEV2ZW50W10+O1xufTtcblxuZnVuY3Rpb24gcHVzaEV2ZW50KFxuICBtYXA6IE1hcDxzdHJpbmcsIEV2ZW50W10+LFxuICBjb3JyZWxhdGlvbklkOiBzdHJpbmcsXG4gIGV2ZW50OiBFdmVudFxuKSB7XG4gIGNvbnN0IGV4aXN0aW5nID0gbWFwLmdldChjb3JyZWxhdGlvbklkKTtcbiAgaWYgKGV4aXN0aW5nKSB7XG4gICAgZXhpc3RpbmcucHVzaChldmVudCk7XG4gICAgcmV0dXJuO1xuICB9XG4gIG1hcC5zZXQoY29ycmVsYXRpb25JZCwgW2V2ZW50XSk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBncm91cEV2ZW50c0J5Q29ycmVsYXRpb24oZXZlbnRzOiBFdmVudFtdKTogR3JvdXBlZEV2ZW50cyB7XG4gIGNvbnN0IGV2ZW50c0J5U3RlcElkID0gbmV3IE1hcDxzdHJpbmcsIEV2ZW50W10+KCk7XG4gIGNvbnN0IHJ1bkxldmVsRXZlbnRzOiBFdmVudFtdID0gW107XG4gIGNvbnN0IHRpbWVyRXZlbnRzID0gbmV3IE1hcDxzdHJpbmcsIEV2ZW50W10+KCk7XG4gIGNvbnN0IGhvb2tFdmVudHMgPSBuZXcgTWFwPHN0cmluZywgRXZlbnRbXT4oKTtcblxuICBmb3IgKGNvbnN0IGV2ZW50IG9mIGV2ZW50cykge1xuICAgIGNvbnN0IGNvcnJlbGF0aW9uSWQgPSBldmVudC5jb3JyZWxhdGlvbklkO1xuICAgIGlmICghY29ycmVsYXRpb25JZCkge1xuICAgICAgcnVuTGV2ZWxFdmVudHMucHVzaChldmVudCk7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG5cbiAgICBpZiAoaXNXYWl0RXZlbnRUeXBlKGV2ZW50LmV2ZW50VHlwZSkpIHtcbiAgICAgIHB1c2hFdmVudCh0aW1lckV2ZW50cywgY29ycmVsYXRpb25JZCwgZXZlbnQpO1xuICAgICAgY29udGludWU7XG4gICAgfVxuXG4gICAgaWYgKGlzSG9va0xpZmVjeWNsZUV2ZW50VHlwZShldmVudC5ldmVudFR5cGUpKSB7XG4gICAgICBwdXNoRXZlbnQoaG9va0V2ZW50cywgY29ycmVsYXRpb25JZCwgZXZlbnQpO1xuICAgICAgY29udGludWU7XG4gICAgfVxuXG4gICAgaWYgKGlzU3RlcEV2ZW50VHlwZShldmVudC5ldmVudFR5cGUpKSB7XG4gICAgICBwdXNoRXZlbnQoZXZlbnRzQnlTdGVwSWQsIGNvcnJlbGF0aW9uSWQsIGV2ZW50KTtcbiAgICAgIGNvbnRpbnVlO1xuICAgIH1cblxuICAgIHJ1bkxldmVsRXZlbnRzLnB1c2goZXZlbnQpO1xuICB9XG5cbiAgcmV0dXJuIHsgZXZlbnRzQnlTdGVwSWQsIHJ1bkxldmVsRXZlbnRzLCB0aW1lckV2ZW50cywgaG9va0V2ZW50cyB9O1xufVxuXG4vLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbi8vIFRyYWNlIGNvbnN0cnVjdGlvblxuLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbi8qKlxuICogQ29tcHV0ZXMgdGhlIGxhdGVzdCBrbm93biBldmVudCB0aW1lIGZyb20gdGhlIGV2ZW50cyBsaXN0LlxuICogQWN0aXZlIChpbi1wcm9ncmVzcykgY2hpbGQgc3BhbnMgYXJlIGNhcHBlZCBhdCB0aGlzIHRpbWUgaW5zdGVhZCBvZiBgbm93YCxcbiAqIHNvIHRoZXkgb25seSBleHRlbmQgYXMgZmFyIGFzIG91ciBkYXRhIGdvZXMuXG4gKi9cbmZ1bmN0aW9uIGNvbXB1dGVMYXRlc3RLbm93blRpbWUoZXZlbnRzOiBFdmVudFtdLCBydW46IFdvcmtmbG93UnVuKTogRGF0ZSB7XG4gIGxldCBsYXRlc3QgPSBuZXcgRGF0ZShydW4uY3JlYXRlZEF0KS5nZXRUaW1lKCk7XG4gIGZvciAoY29uc3QgZXZlbnQgb2YgZXZlbnRzKSB7XG4gICAgY29uc3QgdCA9IChnZXRFdmVudFRpbWVzdGFtcChldmVudCkgPz8gbmV3IERhdGUoZXZlbnQuY3JlYXRlZEF0KSkuZ2V0VGltZSgpO1xuICAgIGlmICh0ID4gbGF0ZXN0KSBsYXRlc3QgPSB0O1xuICB9XG4gIHJldHVybiBuZXcgRGF0ZShsYXRlc3QpO1xufVxuXG5mdW5jdGlvbiBidWlsZFNwYW5zKFxuICBydW46IFdvcmtmbG93UnVuLFxuICBncm91cGVkRXZlbnRzOiBHcm91cGVkRXZlbnRzLFxuICBub3c6IERhdGUsXG4gIGxhdGVzdEtub3duVGltZTogRGF0ZVxuKSB7XG4gIC8vIEFjdGl2ZSBjaGlsZCBzcGFucyBjYXAgYXQgbGF0ZXN0S25vd25UaW1lIHNvIHRoZXkgZG9uJ3QgZXh0ZW5kIGludG9cbiAgLy8gdW5rbm93biB0ZXJyaXRvcnkuIEV2ZW4gd2hlbiB0aGUgcnVuIGlzIGNvbXBsZXRlZCwgd2UgbWF5IG5vdCBoYXZlIGxvYWRlZFxuICAvLyBhbGwgZXZlbnRzIHlldCwgc28gYWx3YXlzIHVzZSB0aGUgbGF0ZXN0IGV2ZW50IHdlJ3ZlIGFjdHVhbGx5IHNlZW4uXG4gIGNvbnN0IGNoaWxkTWF4RW5kID0gbGF0ZXN0S25vd25UaW1lO1xuICBjb25zdCBydW5NYXhFbmQgPSBydW4uY29tcGxldGVkQXQgPz8gbm93O1xuXG4gIGNvbnN0IHN0ZXBTcGFucyA9IEFycmF5LmZyb20oZ3JvdXBlZEV2ZW50cy5ldmVudHNCeVN0ZXBJZC52YWx1ZXMoKSlcbiAgICAubWFwKChldmVudHMpID0+IHN0ZXBUb1NwYW4oZXZlbnRzLCBjaGlsZE1heEVuZCkpXG4gICAgLmZpbHRlcigoc3Bhbik6IHNwYW4gaXMgU3BhbiA9PiBzcGFuICE9PSBudWxsKTtcblxuICBjb25zdCBob29rU3BhbnMgPSBBcnJheS5mcm9tKGdyb3VwZWRFdmVudHMuaG9va0V2ZW50cy52YWx1ZXMoKSlcbiAgICAubWFwKChldmVudHMpID0+IGhvb2tUb1NwYW4oZXZlbnRzLCBjaGlsZE1heEVuZCkpXG4gICAgLmZpbHRlcigoc3Bhbik6IHNwYW4gaXMgU3BhbiA9PiBzcGFuICE9PSBudWxsKTtcblxuICBjb25zdCB3YWl0U3BhbnMgPSBBcnJheS5mcm9tKGdyb3VwZWRFdmVudHMudGltZXJFdmVudHMudmFsdWVzKCkpXG4gICAgLm1hcCgoZXZlbnRzKSA9PiB3YWl0VG9TcGFuKGV2ZW50cywgY2hpbGRNYXhFbmQsIHJ1bk1heEVuZCkpXG4gICAgLmZpbHRlcigoc3Bhbik6IHNwYW4gaXMgU3BhbiA9PiBzcGFuICE9PSBudWxsKTtcblxuICByZXR1cm4ge1xuICAgIHJ1blNwYW46IHJ1blRvU3BhbihydW4sIGdyb3VwZWRFdmVudHMucnVuTGV2ZWxFdmVudHMsIG5vdyksXG4gICAgc3BhbnM6IFsuLi5zdGVwU3BhbnMsIC4uLmhvb2tTcGFucywgLi4ud2FpdFNwYW5zXSxcbiAgfTtcbn1cblxuZnVuY3Rpb24gY2FzY2FkZVNwYW5zKHJ1blNwYW46IFNwYW4sIHNwYW5zOiBTcGFuW10pIHtcbiAgY29uc3Qgc29ydGVkU3BhbnMgPSBbXG4gICAgcnVuU3BhbixcbiAgICAuLi5zcGFucy5zbGljZSgpLnNvcnQoKGEsIGIpID0+IHtcbiAgICAgIGNvbnN0IGFTdGFydCA9IG90ZWxUaW1lVG9NcyhhLnN0YXJ0VGltZSk7XG4gICAgICBjb25zdCBiU3RhcnQgPSBvdGVsVGltZVRvTXMoYi5zdGFydFRpbWUpO1xuICAgICAgcmV0dXJuIGFTdGFydCAtIGJTdGFydDtcbiAgICB9KSxcbiAgXTtcblxuICByZXR1cm4gc29ydGVkU3BhbnMubWFwKChzcGFuLCBpbmRleCkgPT4ge1xuICAgIGNvbnN0IHBhcmVudFNwYW5JZCA9XG4gICAgICBpbmRleCA9PT0gMCA/IHVuZGVmaW5lZCA6IFN0cmluZyhzb3J0ZWRTcGFuc1tpbmRleCAtIDFdLnNwYW5JZCk7XG4gICAgcmV0dXJuIHtcbiAgICAgIC4uLnNwYW4sXG4gICAgICBwYXJlbnRTcGFuSWQsXG4gICAgfTtcbiAgfSk7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVHJhY2VXaXRoTWV0YSB7XG4gIHRyYWNlSWQ6IHN0cmluZztcbiAgcm9vdFNwYW5JZDogc3RyaW5nO1xuICBzcGFuczogU3BhbltdO1xuICByZXNvdXJjZXM6IHsgbmFtZTogc3RyaW5nOyBhdHRyaWJ1dGVzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+IH1bXTtcbiAgLyoqIER1cmF0aW9uIGluIG1zIGZyb20gdHJhY2Ugc3RhcnQgdG8gdGhlIGxhdGVzdCBrbm93biBldmVudC4gKi9cbiAga25vd25EdXJhdGlvbk1zOiBudW1iZXI7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBidWlsZFRyYWNlKFxuICBydW46IFdvcmtmbG93UnVuLFxuICBldmVudHM6IEV2ZW50W10sXG4gIG5vdzogRGF0ZVxuKTogVHJhY2VXaXRoTWV0YSB7XG4gIGNvbnN0IGdyb3VwZWRFdmVudHMgPSBncm91cEV2ZW50c0J5Q29ycmVsYXRpb24oZXZlbnRzKTtcbiAgY29uc3QgbGF0ZXN0S25vd25UaW1lID0gY29tcHV0ZUxhdGVzdEtub3duVGltZShldmVudHMsIHJ1bik7XG4gIGNvbnN0IHsgcnVuU3Bhbiwgc3BhbnMgfSA9IGJ1aWxkU3BhbnMoXG4gICAgcnVuLFxuICAgIGdyb3VwZWRFdmVudHMsXG4gICAgbm93LFxuICAgIGxhdGVzdEtub3duVGltZVxuICApO1xuICBjb25zdCBzb3J0ZWRDYXNjYWRpbmdTcGFucyA9IGNhc2NhZGVTcGFucyhydW5TcGFuLCBzcGFucyk7XG5cbiAgLy8gQ29tcHV0ZSBrbm93biBkdXJhdGlvbiByZWxhdGl2ZSB0byB0cmFjZSBzdGFydFxuICBjb25zdCB0cmFjZVN0YXJ0TXMgPSBvdGVsVGltZVRvTXMocnVuU3Bhbi5zdGFydFRpbWUpO1xuICBjb25zdCBrbm93bkR1cmF0aW9uTXMgPSBsYXRlc3RLbm93blRpbWUuZ2V0VGltZSgpIC0gdHJhY2VTdGFydE1zO1xuXG4gIHJldHVybiB7XG4gICAgdHJhY2VJZDogcnVuLnJ1bklkLFxuICAgIHJvb3RTcGFuSWQ6IHJ1bi5ydW5JZCxcbiAgICBzcGFuczogc29ydGVkQ2FzY2FkaW5nU3BhbnMsXG4gICAgcmVzb3VyY2VzOiBbXG4gICAgICB7XG4gICAgICAgIG5hbWU6ICd3b3JrZmxvdycsXG4gICAgICAgIGF0dHJpYnV0ZXM6IHtcbiAgICAgICAgICAnc2VydmljZS5uYW1lJzogV09SS0ZMT1dfTElCUkFSWS5uYW1lLFxuICAgICAgICB9LFxuICAgICAgfSxcbiAgICBdLFxuICAgIGtub3duRHVyYXRpb25NczogTWF0aC5tYXgoMCwga25vd25EdXJhdGlvbk1zKSxcbiAgfTtcbn1cbiJdfQ==
167
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhY2UtYnVpbGRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9saWIvdHJhY2UtYnVpbGRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7O0dBTUc7QUFFSCxPQUFPLEVBRUwsd0JBQXdCLEVBQ3hCLGVBQWUsRUFDZixlQUFlLEdBRWhCLE1BQU0saUJBQWlCLENBQUM7QUFDekIsT0FBTyxFQUNMLGlCQUFpQixFQUNqQixVQUFVLEVBQ1YsU0FBUyxFQUNULFVBQVUsRUFDVixnQkFBZ0IsRUFDaEIsVUFBVSxHQUNYLE1BQU0sdURBQXVELENBQUM7QUFDL0QsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGdEQUFnRCxDQUFDO0FBQzlFLE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRzNEOzs7Ozs7R0FNRztBQUNILE1BQU0sQ0FBQyxNQUFNLGVBQWUsR0FBRyxDQUFDLEtBQVksRUFBVyxFQUFFLENBQ3ZELENBQUMsS0FBSyxDQUFDLGFBQWE7SUFDcEIsQ0FBQyxDQUFDLGVBQWUsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDO1FBQ2hDLENBQUMsd0JBQXdCLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQztRQUMxQyxDQUFDLGVBQWUsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQztBQUV2Qzs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSxtQkFBbUIsQ0FDakMsTUFBZSxFQUNmLFFBQTRCLEVBQzVCLE1BQTBCO0lBRTFCLElBQUksUUFBUSxLQUFLLEtBQUssRUFBRSxDQUFDO1FBQ3ZCLE9BQU8sTUFBTSxDQUFDLE1BQU0sQ0FBQyxlQUFlLENBQUMsQ0FBQztJQUN4QyxDQUFDO0lBQ0QsSUFBSSxDQUFDLE1BQU07UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUN2QixPQUFPLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxhQUFhLEtB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QUFhLEdBQUcsd0JBQXdCLENBQUMsYUFBYSxDQUFDLENBQUM7SUFDOUQsTUFBTSxlQUFlLEdBQUcsc0JBQXNCLENBQUMsYUFBYSxFQUFFLEdBQUcsQ0FBQyxDQUFDO0lBQ25FLE1BQU0sRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLEdBQUcsVUFBVSxDQUNuQyxHQUFHLEVBQ0gsYUFBYSxFQUNiLEdBQUcsRUFDSCxlQUFlLENBQ2hCLENBQUM7SUFDRixNQUFNLG9CQUFvQixHQUFHLFlBQVksQ0FBQyxPQUFPLEVBQUUsS0FBSyxDQUFDLENBQUM7SUFFMUQsaURBQWlEO0lBQ2pELE1BQU0sWUFBWSxHQUFHLFlBQVksQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDckQsTUFBTSxlQUFlLEdBQUcsZUFBZSxDQUFDLE9BQU8sRUFBRSxHQUFHLFlBQVksQ0FBQztJQUVqRSxPQUFPO1FBQ0wsT0FBTyxFQUFFLEdBQUcsQ0FBQyxLQUFLO1FBQ2xCLFVBQVUsRUFBRSxHQUFHLENBQUMsS0FBSztRQUNyQixLQUFLLEVBQUUsb0JBQW9CO1FBQzNCLFNBQVMsRUFBRTtZQUNUO2dCQUNFLElBQUksRUFBRSxVQUFVO2dCQUNoQixVQUFVLEVBQUU7b0JBQ1YsY0FBYyxFQUFFLGdCQUFnQixDQUFDLElBQUk7aUJBQ3RDO2FBQ0Y7U0FDRjtRQUNELGVBQWUsRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxlQUFlLENBQUM7UUFDN0MsaUJBQWlCO0tBQ2xCLENBQUM7QUFDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBCdWlsZHMgYSBjb21wbGV0ZSB0cmFjZSBmcm9tIGEgV29ya2Zsb3dSdW4gYW5kIGl0cyBFdmVudHMuXG4gKlxuICogVGhpcyBtb2R1bGUgZ3JvdXBzIHJhdyBldmVudHMgYnkgY29ycmVsYXRpb24gSUQgYW5kIGVudGl0eSB0eXBlLFxuICogY29udmVydHMgZWFjaCBncm91cCBpbnRvIGFuIE9wZW5UZWxlbWV0cnktc3R5bGUgU3BhbiwgYW5kIHJldHVybnNcbiAqIGEgZnVsbHktZm9ybWVkIFRyYWNlIHJlYWR5IGZvciB0aGUgdHJhY2Ugdmlld2VyLlxuICovXG5cbmltcG9ydCB7XG4gIHR5cGUgRXZlbnQsXG4gIGlzSG9va0xpZmVjeWNsZUV2ZW50VHlwZSxcbiAgaXNTdGVwRXZlbnRUeXBlLFxuICBpc1dhaXRFdmVudFR5cGUsXG4gIHR5cGUgV29ya2Zsb3dSdW4sXG59IGZyb20gJ0B3b3JrZmxvdy93b3JsZCc7XG5pbXBvcnQge1xuICBnZXRFdmVudFRpbWVzdGFtcCxcbiAgaG9va1RvU3BhbixcbiAgcnVuVG9TcGFuLFxuICBzdGVwVG9TcGFuLFxuICBXT1JLRkxPV19MSUJSQVJZLFxuICB3YWl0VG9TcGFuLFxufSBmcm9tICcuLi9jb21wb25lbnRzL3dvcmtmbG93LXRyYWNlcy90cmFjZS1zcGFuLWNvbnN0cnVjdGlvbic7XG5pbXBvcnQgeyBvdGVsVGltZVRvTXMgfSBmcm9tICcuLi9jb21wb25lbnRzL3dvcmtmbG93LXRyYWNlcy90cmFjZS10aW1lLXV0aWxzJztcbmltcG9ydCB7IGZpbmREdXBsaWNhdGVFdmVudElkcyB9IGZyb20gJy4vZHVwbGljYXRlLWV2ZW50cyc7XG5pbXBvcnQgdHlwZSB7IFNwYW4gfSBmcm9tICcuL3RyYWNlLXR5cGVzJztcblxuLyoqXG4gKiBFdmVudHMgdGhhdCBiZWxvbmcgdG8gdGhlIHJ1biByb290IHNwYW4gcmF0aGVyIHRoYW4gYSBjaGlsZCBlbnRpdHkgc3Bhbi5cbiAqIE1pcnJvcnMgdGhlIGZhbGx0aHJvdWdoIGxvZ2ljIGluIHtAbGluayBncm91cEV2ZW50c0J5Q29ycmVsYXRpb259OiBldmVudHNcbiAqIHdpdGhvdXQgYSBjb3JyZWxhdGlvbklkIChydW4gbGlmZWN5Y2xlKSBwbHVzIGNvcnJlbGF0ZWQgZXZlbnRzIHRoYXQgYXJlbid0XG4gKiBzdGVwL3RpbWVyL2hvb2sgZXZlbnRzIChlLmcuIGBhdHRyX3NldGAsIHdob3NlIGNvcnJlbGF0aW9uSWQgaXMgYSBkZWR1cFxuICogdG9rZW4gcmF0aGVyIHRoYW4gYSBjaGlsZCBlbnRpdHkgSUQpLlxuICovXG5leHBvcnQgY29uc3QgaXNSdW5MZXZlbEV2ZW50ID0gKGV2ZW50OiBFdmVudCk6IGJvb2xlYW4gPT5cbiAgIWV2ZW50LmNvcnJlbGF0aW9uSWQgfHxcbiAgKCFpc1dhaXRFdmVudFR5cGUoZXZlbnQuZXZlbnRUeXBlKSAmJlxuICAgICFpc0hvb2tMaWZlY3ljbGVFdmVudFR5cGUoZXZlbnQuZXZlbnRUeXBlKSAmJlxuICAgICFpc1N0ZXBFdmVudFR5cGUoZXZlbnQuZXZlbnRUeXBlKSk7XG5cbi8qKlxuICogRmlsdGVyIHRoZSByYXcgZXZlbnQgbGlzdCBkb3duIHRvIHRoZSBldmVudHMgZm9yIGEgc2VsZWN0ZWQgc3Bhbi5cbiAqIEZvciB0aGUgcnVuIHJvb3Qgc3BhbiB0aGlzIHJldHVybnMgcnVuLWxldmVsIGV2ZW50cyAoc2VlXG4gKiB7QGxpbmsgaXNSdW5MZXZlbEV2ZW50fSk7IGZvciBjaGlsZCBzcGFucyBpdCByZXR1cm5zIHRoZSBldmVudHNcbiAqIGNvcnJlbGF0ZWQgdG8gdGhhdCBzcGFuJ3MgSUQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBmaWx0ZXJTcGFuUmF3RXZlbnRzKFxuICBldmVudHM6IEV2ZW50W10sXG4gIHJlc291cmNlOiBzdHJpbmcgfCB1bmRlZmluZWQsXG4gIHNwYW5JZDogc3RyaW5nIHwgdW5kZWZpbmVkXG4pOiBFdmVudFtdIHtcbiAgaWYgKHJlc291cmNlID09PSAncnVuJykge1xuICAgIHJldHVybiBldmVudHMuZmlsdGVyKGlzUnVuTGV2ZWxFdmVudCk7XG4gIH1cbiAgaWYgKCFzcGFuSWQpIHJldHVybiBbXTtcbiAgcmV0dXJuIGV2ZW50cy5maWx0ZXIoKGUpID0+IGUuY29ycmVsYXRpb25JZCA9PT0gc3BhbklkKTtcbn1cblxuLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG4vLyBFdmVudCBncm91cGluZ1xuLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbmV4cG9ydCB0eXBlIEdyb3VwZWRFdmVudHMgPSB7XG4gIGV2ZW50c0J5U3RlcElkOiBNYXA8c3RyaW5nLCBFdmVudFtdPjtcbiAgcnVuTGV2ZWxFdmVudHM6IEV2ZW50W107XG4gIHRpbWVyRXZlbnRzOiBNYXA8c3RyaW5nLCBFdmVudFtdPjtcbiAgaG9va0V2ZW50czogTWFwPHN0cmluZywgRXZlbnRbXT47XG59O1xuXG5mdW5jdGlvbiBwdXNoRXZlbnQoXG4gIG1hcDogTWFwPHN0cmluZywgRXZlbnRbXT4sXG4gIGNvcnJlbGF0aW9uSWQ6IHN0cmluZyxcbiAgZXZlbnQ6IEV2ZW50XG4pIHtcbiAgY29uc3QgZXhpc3RpbmcgPSBtYXAuZ2V0KGNvcnJlbGF0aW9uSWQpO1xuICBpZiAoZXhpc3RpbmcpIHtcbiAgICBleGlzdGluZy5wdXNoKGV2ZW50KTtcbiAgICByZXR1cm47XG4gIH1cbiAgbWFwLnNldChjb3JyZWxhdGlvbklkLCBbZXZlbnRdKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGdyb3VwRXZlbnRzQnlDb3JyZWxhdGlvbihldmVudHM6IEV2ZW50W10pOiBHcm91cGVkRXZlbnRzIHtcbiAgY29uc3QgZXZlbnRzQnlTdGVwSWQgPSBuZXcgTWFwPHN0cmluZywgRXZlbnRbXT4oKTtcbiAgY29uc3QgcnVuTGV2ZWxFdmVudHM6IEV2ZW50W10gPSBbXTtcbiAgY29uc3QgdGltZXJFdmVudHMgPSBuZXcgTWFwPHN0cmluZywgRXZlbnRbXT4oKTtcbiAgY29uc3QgaG9va0V2ZW50cyA9IG5ldyBNYXA8c3RyaW5nLCBFdmVudFtdPigpO1xuXG4gIGZvciAoY29uc3QgZXZlbnQgb2YgZXZlbnRzKSB7XG4gICAgY29uc3QgY29ycmVsYXRpb25JZCA9IGV2ZW50LmNvcnJlbGF0aW9uSWQ7XG4gICAgaWYgKCFjb3JyZWxhdGlvbklkKSB7XG4gICAgICBydW5MZXZlbEV2ZW50cy5wdXNoKGV2ZW50KTtcbiAgICAgIGNvbnRpbnVlO1xuICAgIH1cblxuICAgIGlmIChpc1dhaXRFdmVudFR5cGUoZXZlbnQuZXZlbnRUeXBlKSkge1xuICAgICAgcHVzaEV2ZW50KHRpbWVyRXZlbnRzLCBjb3JyZWxhdGlvbklkLCBldmVudCk7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG5cbiAgICBpZiAoaXNIb29rTGlmZWN5Y2xlRXZlbnRUeXBlKGV2ZW50LmV2ZW50VHlwZSkpIHtcbiAgICAgIHB1c2hFdmVudChob29rRXZlbnRzLCBjb3JyZWxhdGlvbklkLCBldmVudCk7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG5cbiAgICBpZiAoaXNTdGVwRXZlbnRUeXBlKGV2ZW50LmV2ZW50VHlwZSkpIHtcbiAgICAgIHB1c2hFdmVudChldmVudHNCeVN0ZXBJZCwgY29ycmVsYXRpb25JZCwgZXZlbnQpO1xuICAgICAgY29udGludWU7XG4gICAgfVxuXG4gICAgcnVuTGV2ZWxFdmVudHMucHVzaChldmVudCk7XG4gIH1cblxuICByZXR1cm4geyBldmVudHNCeVN0ZXBJZCwgcnVuTGV2ZWxFdmVudHMsIHRpbWVyRXZlbnRzLCBob29rRXZlbnRzIH07XG59XG5cbi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuLy8gVHJhY2UgY29uc3RydWN0aW9uXG4vLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cblxuLyoqXG4gKiBDb21wdXRlcyB0aGUgbGF0ZXN0IGtub3duIGV2ZW50IHRpbWUgZnJvbSB0aGUgZXZlbnRzIGxpc3QuXG4gKiBBY3RpdmUgKGluLXByb2dyZXNzKSBjaGlsZCBzcGFucyBhcmUgY2FwcGVkIGF0IHRoaXMgdGltZSBpbnN0ZWFkIG9mIGBub3dgLFxuICogc28gdGhleSBvbmx5IGV4dGVuZCBhcyBmYXIgYXMgb3VyIGRhdGEgZ29lcy5cbiAqL1xuZnVuY3Rpb24gY29tcHV0ZUxhdGVzdEtub3duVGltZShldmVudHM6IEV2ZW50W10sIHJ1bjogV29ya2Zsb3dSdW4pOiBEYXRlIHtcbiAgbGV0IGxhdGVzdCA9IG5ldyBEYXRlKHJ1bi5jcmVhdGVkQXQpLmdldFRpbWUoKTtcbiAgZm9yIChjb25zdCBldmVudCBvZiBldmVudHMpIHtcbiAgICBjb25zdCB0ID0gKGdldEV2ZW50VGltZXN0YW1wKGV2ZW50KSA/PyBuZXcgRGF0ZShldmVudC5jcmVhdGVkQXQpKS5nZXRUaW1lKCk7XG4gICAgaWYgKHQgPiBsYXRlc3QpIGxhdGVzdCA9IHQ7XG4gIH1cbiAgcmV0dXJuIG5ldyBEYXRlKGxhdGVzdCk7XG59XG5cbmZ1bmN0aW9uIGJ1aWxkU3BhbnMoXG4gIHJ1bjogV29ya2Zsb3dSdW4sXG4gIGdyb3VwZWRFdmVudHM6IEdyb3VwZWRFdmVudHMsXG4gIG5vdzogRGF0ZSxcbiAgbGF0ZXN0S25vd25UaW1lOiBEYXRlXG4pIHtcbiAgLy8gQWN0aXZlIGNoaWxkIHNwYW5zIGNhcCBhdCBsYXRlc3RLbm93blRpbWUgc28gdGhleSBkb24ndCBleHRlbmQgaW50b1xuICAvLyB1bmtub3duIHRlcnJpdG9yeS4gRXZlbiB3aGVuIHRoZSBydW4gaXMgY29tcGxldGVkLCB3ZSBtYXkgbm90IGhhdmUgbG9hZGVkXG4gIC8vIGFsbCBldmVudHMgeWV0LCBzbyBhbHdheXMgdXNlIHRoZSBsYXRlc3QgZXZlbnQgd2UndmUgYWN0dWFsbHkgc2Vlbi5cbiAgY29uc3QgY2hpbGRNYXhFbmQgPSBsYXRlc3RLbm93blRpbWU7XG4gIGNvbnN0IHJ1bk1heEVuZCA9IHJ1bi5jb21wbGV0ZWRBdCA/PyBub3c7XG5cbiAgY29uc3Qgc3RlcFNwYW5zID0gQXJyYXkuZnJvbShncm91cGVkRXZlbnRzLmV2ZW50c0J5U3RlcElkLnZhbHVlcygpKVxuICAgIC5tYXAoKGV2ZW50cykgPT4gc3RlcFRvU3BhbihldmVudHMsIGNoaWxkTWF4RW5kKSlcbiAgICAuZmlsdGVyKChzcGFuKTogc3BhbiBpcyBTcGFuID0+IHNwYW4gIT09IG51bGwpO1xuXG4gIGNvbnN0IGhvb2tTcGFucyA9IEFycmF5LmZyb20oZ3JvdXBlZEV2ZW50cy5ob29rRXZlbnRzLnZhbHVlcygpKVxuICAgIC5tYXAoKGV2ZW50cykgPT4gaG9va1RvU3BhbihldmVudHMsIGNoaWxkTWF4RW5kKSlcbiAgICAuZmlsdGVyKChzcGFuKTogc3BhbiBpcyBTcGFuID0+IHNwYW4gIT09IG51bGwpO1xuXG4gIGNvbnN0IHdhaXRTcGFucyA9IEFycmF5LmZyb20oZ3JvdXBlZEV2ZW50cy50aW1lckV2ZW50cy52YWx1ZXMoKSlcbiAgICAubWFwKChldmVudHMpID0+IHdhaXRUb1NwYW4oZXZlbnRzLCBjaGlsZE1heEVuZCwgcnVuTWF4RW5kKSlcbiAgICAuZmlsdGVyKChzcGFuKTogc3BhbiBpcyBTcGFuID0+IHNwYW4gIT09IG51bGwpO1xuXG4gIHJldHVybiB7XG4gICAgcnVuU3BhbjogcnVuVG9TcGFuKHJ1biwgZ3JvdXBlZEV2ZW50cy5ydW5MZXZlbEV2ZW50cywgbm93KSxcbiAgICBzcGFuczogWy4uLnN0ZXBTcGFucywgLi4uaG9va1NwYW5zLCAuLi53YWl0U3BhbnNdLFxuICB9O1xufVxuXG5mdW5jdGlvbiBjYXNjYWRlU3BhbnMocnVuU3BhbjogU3Bhbiwgc3BhbnM6IFNwYW5bXSkge1xuICBjb25zdCBzb3J0ZWRTcGFucyA9IFtcbiAgICBydW5TcGFuLFxuICAgIC4uLnNwYW5zLnNsaWNlKCkuc29ydCgoYSwgYikgPT4ge1xuICAgICAgY29uc3QgYVN0YXJ0ID0gb3RlbFRpbWVUb01zKGEuc3RhcnRUaW1lKTtcbiAgICAgIGNvbnN0IGJTdGFydCA9IG90ZWxUaW1lVG9NcyhiLnN0YXJ0VGltZSk7XG4gICAgICByZXR1cm4gYVN0YXJ0IC0gYlN0YXJ0O1xuICAgIH0pLFxuICBdO1xuXG4gIHJldHVybiBzb3J0ZWRTcGFucy5tYXAoKHNwYW4sIGluZGV4KSA9PiB7XG4gICAgY29uc3QgcGFyZW50U3BhbklkID1cbiAgICAgIGluZGV4ID09PSAwID8gdW5kZWZpbmVkIDogU3RyaW5nKHNvcnRlZFNwYW5zW2luZGV4IC0gMV0uc3BhbklkKTtcbiAgICByZXR1cm4ge1xuICAgICAgLi4uc3BhbixcbiAgICAgIHBhcmVudFNwYW5JZCxcbiAgICB9O1xuICB9KTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBUcmFjZVdpdGhNZXRhIHtcbiAgdHJhY2VJZDogc3RyaW5nO1xuICByb290U3BhbklkOiBzdHJpbmc7XG4gIHNwYW5zOiBTcGFuW107XG4gIHJlc291cmNlczogeyBuYW1lOiBzdHJpbmc7IGF0dHJpYnV0ZXM6IFJlY29yZDxzdHJpbmcsIHN0cmluZz4gfVtdO1xuICAvKiogRHVyYXRpb24gaW4gbXMgZnJvbSB0cmFjZSBzdGFydCB0byB0aGUgbGF0ZXN0IGtub3duIGV2ZW50LiAqL1xuICBrbm93bkR1cmF0aW9uTXM6IG51bWJlcjtcbiAgLyoqXG4gICAqIFRoZSBldmVudHMgbGVmdCBvdXQgb2YgdGhlIHNwYW4gZ2VvbWV0cnkgYXMgcmVwZWF0cy4gRW1wdHkgdW5sZXNzIHRoZVxuICAgKiBjYWxsZXIgdm91Y2hlZCBmb3IgdGhlIGxvZyBiZWluZyBjb21wbGV0ZS4gU2VlXG4gICAqIHtAbGluayBmaW5kRHVwbGljYXRlRXZlbnRJZHN9LlxuICAgKi9cbiAgZHVwbGljYXRlRXZlbnRJZHM6IFJlYWRvbmx5U2V0PHN0cmluZz47XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBidWlsZFRyYWNlKFxuICBydW46IFdvcmtmbG93UnVuLFxuICBldmVudHM6IEV2ZW50W10sXG4gIG5vdzogRGF0ZSxcbiAgLyoqXG4gICAqIFdoZXRoZXIgYGV2ZW50c2AgaXMgdGhlIHJ1bidzIHdob2xlIGxvZy4gRGVmYXVsdHMgdG8gZmFsc2UsIHdoaWNoIGJ1aWxkc1xuICAgKiB0aGUgdHJhY2UgZnJvbSBldmVyeSBldmVudDogb24gYSBzdWJzZXQgdGhlcmUgaXMgbm8gd2F5IHRvIHRlbGwgYSByZXBlYXRcbiAgICogZnJvbSB0aGUgb25seSBjb3B5IHRoZSBjYWxsZXIgd2FzIGdpdmVuLCBhbmQgZHJvcHBpbmcgdGhlIHdyb25nIG9uZSBtb3Zlc1xuICAgKiBhIHNwYW4uIFNlZSB7QGxpbmsgZmluZER1cGxpY2F0ZUV2ZW50SWRzfS5cbiAgICovXG4gIHsgaXNDb21wbGV0ZUhpc3RvcnkgPSBmYWxzZSB9OiB7IGlzQ29tcGxldGVIaXN0b3J5PzogYm9vbGVhbiB9ID0ge31cbik6IFRyYWNlV2l0aE1ldGEge1xuICAvLyBTcGFuIGdlb21ldHJ5IGNvbWVzIGZyb20gd2hhdCB0aGUgcnVuIGFjdGVkIG9uLiBBIHJlcGVhdCBvZiBhIGNsYXNzIHRoZVxuICAvLyBsb2cgYWxyZWFkeSByZWNvcmRzIGlzIHJlYWQgcGFzdCBieSBldmVyeSByZXBsYXksIGFuZCBsZXR0aW5nIG9uZSB0aHJvdWdoXG4gIC8vIGhlcmUgd291bGQgc3RyZXRjaCBhIHNwYW4gdG8gd2hlbmV2ZXIgYSBjb25jdXJyZW50IHJlcGxheSBjb21taXR0ZWQgaXQuXG4gIC8vIFRoZSBldmVudCBsaXN0cyBzdGlsbCBzaG93IHRoZW0sIG1hcmtlZCBhcyByZXBlYXRzLlxuICBjb25zdCBkdXBsaWNhdGVFdmVudElkcyA9IGZpbmREdXBsaWNhdGVFdmVudElkcyhldmVudHMsIHtcbiAgICBpc0NvbXBsZXRlSGlzdG9yeSxcbiAgfSk7XG4gIGNvbnN0IGFjdGVkT25FdmVudHMgPVxuICAgIGR1cGxpY2F0ZUV2ZW50SWRzLnNpemUgPT09IDBcbiAgICAgID8gZXZlbnRzXG4gICAgICA6IGV2ZW50cy5maWx0ZXIoKGV2ZW50KSA9PiAhZHVwbGljYXRlRXZlbnRJZHMuaGFzKGV2ZW50LmV2ZW50SWQpKTtcblxuICBjb25zdCBncm91cGVkRXZlbnRzID0gZ3JvdXBFdmVudHNCeUNvcnJlbGF0aW9uKGFjdGVkT25FdmVudHMpO1xuICBjb25zdCBsYXRlc3RLbm93blRpbWUgPSBjb21wdXRlTGF0ZXN0S25vd25UaW1lKGFjdGVkT25FdmVudHMsIHJ1bik7XG4gIGNvbnN0IHsgcnVuU3Bhbiwgc3BhbnMgfSA9IGJ1aWxkU3BhbnMoXG4gICAgcnVuLFxuICAgIGdyb3VwZWRFdmVudHMsXG4gICAgbm93LFxuICAgIGxhdGVzdEtub3duVGltZVxuICApO1xuICBjb25zdCBzb3J0ZWRDYXNjYWRpbmdTcGFucyA9IGNhc2NhZGVTcGFucyhydW5TcGFuLCBzcGFucyk7XG5cbiAgLy8gQ29tcHV0ZSBrbm93biBkdXJhdGlvbiByZWxhdGl2ZSB0byB0cmFjZSBzdGFydFxuICBjb25zdCB0cmFjZVN0YXJ0TXMgPSBvdGVsVGltZVRvTXMocnVuU3Bhbi5zdGFydFRpbWUpO1xuICBjb25zdCBrbm93bkR1cmF0aW9uTXMgPSBsYXRlc3RLbm93blRpbWUuZ2V0VGltZSgpIC0gdHJhY2VTdGFydE1zO1xuXG4gIHJldHVybiB7XG4gICAgdHJhY2VJZDogcnVuLnJ1bklkLFxuICAgIHJvb3RTcGFuSWQ6IHJ1bi5ydW5JZCxcbiAgICBzcGFuczogc29ydGVkQ2FzY2FkaW5nU3BhbnMsXG4gICAgcmVzb3VyY2VzOiBbXG4gICAgICB7XG4gICAgICAgIG5hbWU6ICd3b3JrZmxvdycsXG4gICAgICAgIGF0dHJpYnV0ZXM6IHtcbiAgICAgICAgICAnc2VydmljZS5uYW1lJzogV09SS0ZMT1dfTElCUkFSWS5uYW1lLFxuICAgICAgICB9LFxuICAgICAgfSxcbiAgICBdLFxuICAgIGtub3duRHVyYXRpb25NczogTWF0aC5tYXgoMCwga25vd25EdXJhdGlvbk1zKSxcbiAgICBkdXBsaWNhdGVFdmVudElkcyxcbiAgfTtcbn1cbiJdfQ==
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@workflow/web-shared",
3
3
  "description": "Shared components for Workflow Observability UI",
4
- "version": "5.0.0-beta.41",
4
+ "version": "5.0.0-beta.43",
5
5
  "private": false,
6
6
  "files": [
7
7
  "dist",
@@ -55,9 +55,9 @@
55
55
  "streamdown": "2.3.0",
56
56
  "tailwind-merge": "3.5.0",
57
57
  "tailwindcss": "4",
58
- "@workflow/core": "5.0.0-beta.41",
58
+ "@workflow/core": "5.0.0-beta.43",
59
59
  "@workflow/utils": "5.0.0-beta.8",
60
- "@workflow/world": "5.0.0-beta.26"
60
+ "@workflow/world": "5.0.0-beta.28"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@biomejs/biome": "^2.4.4",
@@ -67,7 +67,7 @@
67
67
  "ai": "6.0.116",
68
68
  "typescript": "^6.0.3",
69
69
  "vitest": "^4.1.10",
70
- "@workflow/errors": "5.0.0-beta.16",
70
+ "@workflow/errors": "5.0.0-beta.17",
71
71
  "@workflow/tsconfig": "5.0.0-beta.0"
72
72
  },
73
73
  "scripts": {
@@ -10,6 +10,7 @@ import type {
10
10
  } from 'react';
11
11
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
12
12
  import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
13
+ import { findDuplicateEventIds } from '../lib/duplicate-events';
13
14
  import {
14
15
  type ExactIdSearchResult,
15
16
  type ExactWorkflowSearchIdKind,
@@ -23,6 +24,7 @@ import { AttrSetEventBlock } from './sidebar/attributes-block';
23
24
  import { ContextCardProvider } from './ui/context-card';
24
25
  import { DataInspector, DecryptClickContext } from './ui/data-inspector';
25
26
  import { DecryptButton } from './ui/decrypt-button';
27
+ import { DuplicateEventTooltip } from './ui/duplicate-event-tooltip';
26
28
  import {
27
29
  ErrorStackBlock,
28
30
  isStructuredError,
@@ -190,15 +192,24 @@ export interface DurationInfo {
190
192
  * Build a map from correlationId → duration info by diffing
191
193
  * created ↔ started (queued) and started ↔ completed/failed/cancelled (ran).
192
194
  * Also computes run-level durations under the key '__run__'.
195
+ *
196
+ * Events every replay reads past as repeats are excluded: a second
197
+ * `step_completed` written by a concurrent replay would otherwise stretch the
198
+ * step's measured runtime to whenever that replay happened to commit. The
199
+ * caller supplies them, because whether an event is a repeat is a property of
200
+ * the whole log and this function may be handed a page of it.
193
201
  */
194
- export function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
202
+ export function buildDurationMap(
203
+ events: Event[],
204
+ duplicateEventIds: ReadonlySet<string> = new Set()
205
+ ): Map<string, DurationInfo> {
195
206
  // Process events in chronological order so the result doesn't depend on
196
207
  // the caller's sort direction. Retried steps emit multiple `step_started`
197
208
  // events for the same correlationId; the queued duration must be measured
198
209
  // against the first one, not the last.
199
- const chronological = [...events].sort(
200
- (a, b) => getEffectiveEventTime(a) - getEffectiveEventTime(b)
201
- );
210
+ const chronological = [...events]
211
+ .filter((event) => !duplicateEventIds.has(event.eventId))
212
+ .sort((a, b) => getEffectiveEventTime(a) - getEffectiveEventTime(b));
202
213
 
203
214
  const createdTimes = new Map<string, number>();
204
215
  const firstStartedTimes = new Map<string, number>();
@@ -832,6 +843,7 @@ export function EventRow({
832
843
  onEncryptedDataDetected,
833
844
  suppressGroupDimming = false,
834
845
  showSeparateEventOccurrenceTimestamps = false,
846
+ isDuplicate = false,
835
847
  }: {
836
848
  event: Event;
837
849
  index: number;
@@ -856,6 +868,8 @@ export function EventRow({
856
868
  suppressGroupDimming?: boolean;
857
869
  /** Show occurredAt separately instead of folding it into the Created timestamp. */
858
870
  showSeparateEventOccurrenceTimestamps?: boolean;
871
+ /** The event repeats a class already in the log, so the runtime ignored it. */
872
+ isDuplicate?: boolean;
859
873
  }) {
860
874
  const [isLoading, setIsLoading] = useState(false);
861
875
  const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
@@ -1095,43 +1109,49 @@ export function EventRow({
1095
1109
 
1096
1110
  {/* Event Type */}
1097
1111
  <div className="font-medium min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1098
- <span
1099
- className="inline-flex items-center gap-1.5"
1100
- style={{ color: 'var(--ds-gray-900)' }}
1101
- >
1112
+ <DuplicateEventTooltip isDuplicate={isDuplicate}>
1102
1113
  <span
1114
+ className="inline-flex items-center gap-1.5"
1103
1115
  style={{
1104
- position: 'relative',
1105
- display: 'inline-flex',
1106
- width: 6,
1107
- height: 6,
1108
- flexShrink: 0,
1116
+ color: isDuplicate
1117
+ ? 'var(--ds-gray-700)'
1118
+ : 'var(--ds-gray-900)',
1109
1119
  }}
1110
1120
  >
1111
- {isPulsing && (
1112
- <span
1113
- style={{
1114
- position: 'absolute',
1115
- inset: 0,
1116
- borderRadius: '50%',
1117
- backgroundColor: statusDotColor,
1118
- opacity: 0.75,
1119
- animation: DOT_PULSE_ANIMATION,
1120
- }}
1121
- />
1122
- )}
1123
1121
  <span
1124
1122
  style={{
1125
1123
  position: 'relative',
1124
+ display: 'inline-flex',
1126
1125
  width: 6,
1127
1126
  height: 6,
1128
- borderRadius: '50%',
1129
- backgroundColor: statusDotColor,
1127
+ flexShrink: 0,
1130
1128
  }}
1131
- />
1129
+ >
1130
+ {isPulsing && (
1131
+ <span
1132
+ style={{
1133
+ position: 'absolute',
1134
+ inset: 0,
1135
+ borderRadius: '50%',
1136
+ backgroundColor: statusDotColor,
1137
+ opacity: 0.75,
1138
+ animation: DOT_PULSE_ANIMATION,
1139
+ }}
1140
+ />
1141
+ )}
1142
+ <span
1143
+ style={{
1144
+ position: 'relative',
1145
+ width: 6,
1146
+ height: 6,
1147
+ borderRadius: '50%',
1148
+ backgroundColor: statusDotColor,
1149
+ }}
1150
+ />
1151
+ </span>
1152
+ {formatEventType(event.eventType)}
1132
1153
  </span>
1133
- {formatEventType(event.eventType)}
1134
- </span>
1154
+ </DuplicateEventTooltip>
1135
1155
  </div>
1136
1156
 
1137
1157
  {/* Name */}
@@ -1310,6 +1330,23 @@ function EventListViewInner({
1310
1330
  );
1311
1331
  }, [events, effectiveSortOrder, isExactSearchActive, searchResults]);
1312
1332
 
1333
+ // Events every replay reads past as repeats. Computed from the source list
1334
+ // rather than `sortedEvents` because which occurrence counted is a property
1335
+ // of the log, not of the direction the table happens to be sorted in.
1336
+ //
1337
+ // A page short of the whole log, or an exact-ID search that returns one
1338
+ // event, cannot answer that question: the event a repeat lost to may be
1339
+ // outside the window, and reading it the other way round would mark the
1340
+ // event the run acted on and drop it from the durations. Both cases classify
1341
+ // nothing.
1342
+ const duplicateEventIds = useMemo(
1343
+ () =>
1344
+ findDuplicateEventIds(events ?? [], {
1345
+ isCompleteHistory: !hasMoreEvents && !isExactSearchActive,
1346
+ }),
1347
+ [events, hasMoreEvents, isExactSearchActive]
1348
+ );
1349
+
1313
1350
  // Detect encrypted fields across all loaded events (inline eventData).
1314
1351
  const hasEncryptedInlineData = useMemo(() => {
1315
1352
  const sourceEvents = isExactSearchActive ? searchResults : events;
@@ -1342,8 +1379,8 @@ function EventListViewInner({
1342
1379
  );
1343
1380
 
1344
1381
  const durationMap = useMemo(
1345
- () => buildDurationMap(sortedEvents),
1346
- [sortedEvents]
1382
+ () => buildDurationMap(sortedEvents, duplicateEventIds),
1383
+ [sortedEvents, duplicateEventIds]
1347
1384
  );
1348
1385
 
1349
1386
  const [selectedGroupKey, setSelectedGroupKey] = useState<string | undefined>(
@@ -1804,6 +1841,7 @@ function EventListViewInner({
1804
1841
  encryptionKey={encryptionKey}
1805
1842
  onEncryptedDataDetected={handleEncryptedDataDetected}
1806
1843
  suppressGroupDimming={isExactSearchActive}
1844
+ isDuplicate={duplicateEventIds.has(ev.eventId)}
1807
1845
  showSeparateEventOccurrenceTimestamps={
1808
1846
  showSeparateEventOccurrenceTimestamps
1809
1847
  }
@@ -92,8 +92,7 @@ function DetailKeyValueRowBase({
92
92
  >
93
93
  <MiddleTruncate
94
94
  value={copyText}
95
- className="min-w-0 text-right"
96
- style={{ gridTemplateColumns: 'minmax(0, 1fr)' }}
95
+ className="min-w-0 grid-cols-[minmax(0,1fr)] text-right"
97
96
  />
98
97
  <ArrowUpRight aria-hidden className="h-3 w-3 shrink-0" />
99
98
  </a>
@@ -101,9 +100,9 @@ function DetailKeyValueRowBase({
101
100
  <MiddleTruncate
102
101
  value={copyText}
103
102
  className={cn(
104
- rowValueVariants({ variant, className: 'text-right' })
103
+ rowValueVariants({ variant, className: 'text-right' }),
104
+ 'grid-cols-[minmax(0,1fr)]'
105
105
  )}
106
- style={{ gridTemplateColumns: 'minmax(0, 1fr)' }}
107
106
  />
108
107
  )}
109
108
  <CopyButton
@@ -311,13 +310,7 @@ export function AttrSetEventBlock({ data }: { data: unknown }) {
311
310
  ))}
312
311
  </div>
313
312
  {writerLabel && (
314
- <div
315
- className="border-t py-1.5 text-label-12"
316
- style={{
317
- borderColor: 'var(--ds-gray-alpha-400)',
318
- color: 'var(--ds-gray-700)',
319
- }}
320
- >
313
+ <div className="border-gray-alpha-400 border-t py-1.5 text-gray-700 text-label-12">
321
314
  {writerLabel}
322
315
  </div>
323
316
  )}
@@ -9,10 +9,7 @@ interface ConversationViewProps {
9
9
  export function ConversationView({ messages }: ConversationViewProps) {
10
10
  if (messages.length === 0) {
11
11
  return (
12
- <div
13
- className="text-center py-8 text-[11px]"
14
- style={{ color: 'var(--ds-gray-600)' }}
15
- >
12
+ <div className="py-8 text-center text-[11px] text-gray-600">
16
13
  No messages
17
14
  </div>
18
15
  );
@@ -30,23 +27,13 @@ export function ConversationView({ messages }: ConversationViewProps) {
30
27
  function MessageBubble({ message }: { message: ModelMessage }) {
31
28
  const role = message.role;
32
29
  const parts = parseContent(message.content);
33
- const style = getRoleStyle(role);
30
+ const roleClasses = getRoleClasses(role);
34
31
 
35
32
  return (
36
- <div
37
- className="rounded-md border text-[11px]"
38
- style={{
39
- backgroundColor: style.bg,
40
- borderColor: style.border,
41
- }}
42
- >
33
+ <div className={`rounded-md border text-[11px] ${roleClasses.bubble}`}>
43
34
  {/* Role header */}
44
35
  <div
45
- className="px-2.5 py-1 border-b text-[10px] font-medium uppercase tracking-wide"
46
- style={{
47
- borderColor: style.border,
48
- color: style.label,
49
- }}
36
+ className={`border-b px-2.5 py-1 font-medium text-[10px] uppercase tracking-wide ${roleClasses.label}`}
50
37
  >
51
38
  {role}
52
39
  </div>
@@ -76,20 +63,14 @@ function ContentPart({ part, role }: { part: ParsedPart; role: string }) {
76
63
  // Use Streamdown for assistant messages (they often contain markdown)
77
64
  if (role === 'assistant') {
78
65
  return (
79
- <div
80
- className="prose prose-sm max-w-none text-[11px] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
81
- style={{ color: 'var(--ds-gray-1000)' }}
82
- >
66
+ <div className="prose prose-sm max-w-none text-[11px] text-gray-1000 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
83
67
  <Streamdown>{part.text}</Streamdown>
84
68
  </div>
85
69
  );
86
70
  }
87
71
 
88
72
  return (
89
- <div
90
- className="whitespace-pre-wrap break-words"
91
- style={{ color: 'var(--ds-gray-1000)' }}
92
- >
73
+ <div className="whitespace-pre-wrap break-words text-gray-1000">
93
74
  {part.text}
94
75
  </div>
95
76
  );
@@ -97,29 +78,15 @@ function ContentPart({ part, role }: { part: ParsedPart; role: string }) {
97
78
 
98
79
  if (part.type === 'tool-call') {
99
80
  return (
100
- <div
101
- className="rounded border px-2 py-1.5"
102
- style={{
103
- backgroundColor: 'var(--ds-purple-100)',
104
- borderColor: 'var(--ds-purple-300)',
105
- }}
106
- >
81
+ <div className="rounded border border-purple-300 bg-purple-100 px-2 py-1.5">
107
82
  <div className="flex items-center gap-1.5 text-[10px] font-medium">
108
83
  <span>🔧</span>
109
- <span style={{ color: 'var(--ds-purple-900)' }}>{part.toolName}</span>
84
+ <span className="text-purple-900">{part.toolName}</span>
110
85
  </div>
111
86
  {part.input != null && (
112
- <div
113
- className="mt-1.5 overflow-x-auto p-1.5 rounded"
114
- style={{ backgroundColor: 'var(--ds-gray-100)' }}
115
- >
87
+ <div className="mt-1.5 overflow-x-auto rounded bg-gray-100 p-1.5">
116
88
  {typeof part.input === 'string' ? (
117
- <pre
118
- className="text-[10px]"
119
- style={{ color: 'var(--ds-gray-800)' }}
120
- >
121
- {part.input}
122
- </pre>
89
+ <pre className="text-[10px] text-gray-800">{part.input}</pre>
123
90
  ) : (
124
91
  <DataInspector data={part.input} />
125
92
  )}
@@ -131,31 +98,15 @@ function ContentPart({ part, role }: { part: ParsedPart; role: string }) {
131
98
 
132
99
  if (part.type === 'tool-result') {
133
100
  return (
134
- <div
135
- className="rounded border px-2 py-1.5"
136
- style={{
137
- backgroundColor: 'var(--ds-green-100)',
138
- borderColor: 'var(--ds-green-300)',
139
- }}
140
- >
101
+ <div className="rounded border border-green-300 bg-green-100 px-2 py-1.5">
141
102
  <div className="flex items-center gap-1.5 text-[10px] font-medium">
142
103
  <span>✓</span>
143
- <span style={{ color: 'var(--ds-green-900)' }}>
144
- {part.toolName} result
145
- </span>
104
+ <span className="text-green-900">{part.toolName} result</span>
146
105
  </div>
147
106
  {part.output != null && (
148
- <div
149
- className="mt-1.5 overflow-x-auto max-h-[200px] overflow-y-auto p-1.5 rounded"
150
- style={{ backgroundColor: 'var(--ds-gray-100)' }}
151
- >
107
+ <div className="mt-1.5 max-h-[200px] overflow-x-auto overflow-y-auto rounded bg-gray-100 p-1.5">
152
108
  {typeof part.output === 'string' ? (
153
- <pre
154
- className="text-[10px]"
155
- style={{ color: 'var(--ds-gray-800)' }}
156
- >
157
- {part.output}
158
- </pre>
109
+ <pre className="text-[10px] text-gray-800">{part.output}</pre>
159
110
  ) : (
160
111
  <DataInspector data={part.output} expandLevel={1} />
161
112
  )}
@@ -168,37 +119,32 @@ function ContentPart({ part, role }: { part: ParsedPart; role: string }) {
168
119
  return null;
169
120
  }
170
121
 
171
- function getRoleStyle(role: string) {
122
+ function getRoleClasses(role: string): { bubble: string; label: string } {
172
123
  switch (role) {
173
124
  case 'user':
174
125
  return {
175
- bg: 'var(--ds-blue-100)',
176
- border: 'var(--ds-blue-300)',
177
- label: 'var(--ds-blue-700)',
126
+ bubble: 'border-blue-300 bg-blue-100',
127
+ label: 'border-blue-300 text-blue-700',
178
128
  };
179
129
  case 'assistant':
180
130
  return {
181
- bg: 'var(--ds-gray-100)',
182
- border: 'var(--ds-gray-300)',
183
- label: 'var(--ds-gray-700)',
131
+ bubble: 'border-gray-300 bg-gray-100',
132
+ label: 'border-gray-300 text-gray-700',
184
133
  };
185
134
  case 'system':
186
135
  return {
187
- bg: 'var(--ds-amber-100)',
188
- border: 'var(--ds-amber-300)',
189
- label: 'var(--ds-amber-700)',
136
+ bubble: 'border-amber-300 bg-amber-100',
137
+ label: 'border-amber-300 text-amber-700',
190
138
  };
191
139
  case 'tool':
192
140
  return {
193
- bg: 'var(--ds-green-50)',
194
- border: 'var(--ds-green-300)',
195
- label: 'var(--ds-green-700)',
141
+ bubble: 'border-green-300 bg-green-100',
142
+ label: 'border-green-300 text-green-700',
196
143
  };
197
144
  default:
198
145
  return {
199
- bg: 'var(--ds-gray-100)',
200
- border: 'var(--ds-gray-300)',
201
- label: 'var(--ds-gray-700)',
146
+ bubble: 'border-gray-300 bg-gray-100',
147
+ label: 'border-gray-300 text-gray-700',
202
148
  };
203
149
  }
204
150
  }
@@ -47,6 +47,11 @@ export interface SelectedSpanInfo {
47
47
  spanId?: string;
48
48
  /** Raw correlated events from the store (NOT from the trace worker pipeline) */
49
49
  rawEvents?: Event[];
50
+ /**
51
+ * Events every replay reads past as repeats, computed from the whole log.
52
+ * `rawEvents` is one entity's slice, which cannot answer that on its own.
53
+ */
54
+ duplicateEventIds?: ReadonlySet<string>;
50
55
  }
51
56
 
52
57
  /**
@@ -402,6 +407,7 @@ export function EntityDetailPanel({
402
407
  showSeparateEventOccurrenceTimestamps={
403
408
  showSeparateEventOccurrenceTimestamps
404
409
  }
410
+ duplicateEventIds={selectedSpan?.duplicateEventIds}
405
411
  />
406
412
  )}
407
413
  </div>
@@ -10,6 +10,7 @@ import {
10
10
  CollapsibleTrigger,
11
11
  } from '../ui/collapsible';
12
12
  import { RunClickContext, StreamClickContext } from '../ui/data-inspector';
13
+ import { DuplicateEventTooltip } from '../ui/duplicate-event-tooltip';
13
14
  import { ErrorCard } from '../ui/error-card';
14
15
  import { ErrorStackBlock, isStructuredError } from '../ui/error-stack-block';
15
16
  import { Skeleton } from '../ui/skeleton';
@@ -47,6 +48,7 @@ function EventItem({
47
48
  onLoadEventData,
48
49
  encryptionKey,
49
50
  showSeparateEventOccurrenceTimestamps = false,
51
+ isDuplicate = false,
50
52
  }: {
51
53
  event: Event;
52
54
  onLoadEventData?: (event: Event) => Promise<unknown | null>;
@@ -54,6 +56,8 @@ function EventItem({
54
56
  encryptionKey?: Uint8Array;
55
57
  /** Show occurredAt separately instead of folding it into the Created timestamp. */
56
58
  showSeparateEventOccurrenceTimestamps?: boolean;
59
+ /** The event repeats a class already in the log, so the runtime ignored it. */
60
+ isDuplicate?: boolean;
57
61
  }) {
58
62
  const [loadedData, setLoadedData] = useState<unknown | null>(null);
59
63
  const [isLoading, setIsLoading] = useState(false);
@@ -135,9 +139,15 @@ function EventItem({
135
139
  >
136
140
  <CollapsibleTrigger className="px-3 py-2">
137
141
  <div className="flex w-full items-center justify-between gap-3">
138
- <span className="text-gray-1000 text-label-12 font-mono">
139
- {event.eventType}
140
- </span>
142
+ <DuplicateEventTooltip isDuplicate={isDuplicate}>
143
+ <span
144
+ className={`text-label-12 font-mono ${
145
+ isDuplicate ? 'text-gray-700' : 'text-gray-1000'
146
+ }`}
147
+ >
148
+ {event.eventType}
149
+ </span>
150
+ </DuplicateEventTooltip>
141
151
  <span className="shrink-0 text-label-13 text-gray-900">
142
152
  {displayedCreatedAtTime}
143
153
  </span>
@@ -294,6 +304,7 @@ export function EventsList({
294
304
  onRunClick,
295
305
  encryptionKey,
296
306
  showSeparateEventOccurrenceTimestamps = false,
307
+ duplicateEventIds,
297
308
  }: {
298
309
  events: Event[];
299
310
  isLoading?: boolean;
@@ -305,6 +316,12 @@ export function EventsList({
305
316
  encryptionKey?: Uint8Array;
306
317
  /** Show occurredAt separately instead of folding it into the Created timestamp. */
307
318
  showSeparateEventOccurrenceTimestamps?: boolean;
319
+ /**
320
+ * Events every replay reads past as repeats, from the caller that holds the
321
+ * whole log. `events` here is one entity's slice of it, which cannot answer
322
+ * the question on its own.
323
+ */
324
+ duplicateEventIds?: ReadonlySet<string>;
308
325
  }) {
309
326
  // Sort by the timestamp shown as Created by default.
310
327
  const sortedEvents = useMemo(
@@ -352,6 +369,7 @@ export function EventsList({
352
369
  showSeparateEventOccurrenceTimestamps={
353
370
  showSeparateEventOccurrenceTimestamps
354
371
  }
372
+ isDuplicate={duplicateEventIds?.has(event.eventId)}
355
373
  />
356
374
  ))}
357
375
  </div>