@workflow/web-shared 5.0.0-beta.1 → 5.0.0-beta.11

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 (298) hide show
  1. package/README.md +1 -1
  2. package/dist/components/error-boundary.js +1 -1
  3. package/dist/components/event-list-view.d.ts +18 -1
  4. package/dist/components/event-list-view.d.ts.map +1 -1
  5. package/dist/components/event-list-view.js +219 -125
  6. package/dist/components/hook-actions.js +1 -1
  7. package/dist/components/index.d.ts +5 -1
  8. package/dist/components/index.d.ts.map +1 -1
  9. package/dist/components/index.js +6 -2
  10. package/dist/components/new-trace-viewer/components/copy-button.d.ts +9 -0
  11. package/dist/components/new-trace-viewer/components/copy-button.d.ts.map +1 -0
  12. package/dist/components/new-trace-viewer/components/copy-button.js +27 -0
  13. package/dist/components/new-trace-viewer/components/event-list.d.ts +10 -0
  14. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -0
  15. package/dist/components/new-trace-viewer/components/event-list.js +44 -0
  16. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
  17. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
  18. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
  19. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
  20. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
  21. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
  22. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
  23. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
  24. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -0
  25. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts +20 -0
  26. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -0
  27. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +114 -0
  28. package/dist/components/new-trace-viewer/components/split-pane.d.ts +15 -0
  29. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -0
  30. package/dist/components/new-trace-viewer/components/split-pane.js +105 -0
  31. package/dist/components/new-trace-viewer/components/timeline.d.ts +34 -0
  32. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -0
  33. package/dist/components/new-trace-viewer/components/timeline.js +221 -0
  34. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts +2 -0
  35. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -0
  36. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.js +19 -0
  37. package/dist/components/new-trace-viewer/components/use-row-window.d.ts +20 -0
  38. package/dist/components/new-trace-viewer/components/use-row-window.d.ts.map +1 -0
  39. package/dist/components/new-trace-viewer/components/use-row-window.js +69 -0
  40. package/dist/components/new-trace-viewer/context.d.ts +15 -0
  41. package/dist/components/new-trace-viewer/context.d.ts.map +1 -0
  42. package/dist/components/new-trace-viewer/context.js +47 -0
  43. package/dist/components/new-trace-viewer/detail-panel.d.ts +10 -0
  44. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -0
  45. package/dist/components/new-trace-viewer/detail-panel.js +13 -0
  46. package/dist/components/new-trace-viewer/icons.d.ts +6 -0
  47. package/dist/components/new-trace-viewer/icons.d.ts.map +1 -0
  48. package/dist/components/new-trace-viewer/icons.js +15 -0
  49. package/dist/components/new-trace-viewer/search.d.ts +19 -0
  50. package/dist/components/new-trace-viewer/search.d.ts.map +1 -0
  51. package/dist/components/new-trace-viewer/search.js +134 -0
  52. package/dist/components/new-trace-viewer/trace-viewer.d.ts +11 -0
  53. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -0
  54. package/dist/components/new-trace-viewer/trace-viewer.js +400 -0
  55. package/dist/components/new-trace-viewer/types.d.ts +1 -0
  56. package/dist/components/new-trace-viewer/types.d.ts.map +1 -0
  57. package/dist/components/new-trace-viewer/types.js +2 -0
  58. package/dist/components/new-trace-viewer/util/constants.d.ts +1 -0
  59. package/dist/components/new-trace-viewer/util/constants.d.ts.map +1 -0
  60. package/dist/components/new-trace-viewer/util/constants.js +2 -0
  61. package/dist/components/new-trace-viewer/utils.d.ts +46 -0
  62. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -0
  63. package/dist/components/new-trace-viewer/utils.js +330 -0
  64. package/dist/components/run-trace-view.d.ts +2 -1
  65. package/dist/components/run-trace-view.d.ts.map +1 -1
  66. package/dist/components/run-trace-view.js +3 -3
  67. package/dist/components/sidebar/attribute-panel.d.ts +7 -1
  68. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  69. package/dist/components/sidebar/attribute-panel.js +131 -70
  70. package/dist/components/sidebar/conversation-view.js +1 -1
  71. package/dist/components/sidebar/copyable-data-block.d.ts +1 -0
  72. package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
  73. package/dist/components/sidebar/copyable-data-block.js +16 -15
  74. package/dist/components/sidebar/detail-card.d.ts +5 -6
  75. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  76. package/dist/components/sidebar/detail-card.js +32 -16
  77. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
  78. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  79. package/dist/components/sidebar/entity-detail-panel.js +20 -37
  80. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  81. package/dist/components/sidebar/events-list.js +21 -15
  82. package/dist/components/sidebar/resolve-hook-modal.js +1 -1
  83. package/dist/components/sidebar/sidebar-data-context.d.ts +29 -0
  84. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -0
  85. package/dist/components/sidebar/sidebar-data-context.js +19 -0
  86. package/dist/components/stream-viewer.d.ts +1 -1
  87. package/dist/components/stream-viewer.d.ts.map +1 -1
  88. package/dist/components/stream-viewer.js +3 -27
  89. package/dist/components/trace-viewer/components/map.js +1 -1
  90. package/dist/components/trace-viewer/components/markers.js +1 -1
  91. package/dist/components/trace-viewer/components/node.js +1 -1
  92. package/dist/components/trace-viewer/components/search-input.js +1 -1
  93. package/dist/components/trace-viewer/components/search.js +1 -1
  94. package/dist/components/trace-viewer/components/span-content.js +1 -1
  95. package/dist/components/trace-viewer/components/span-detail-panel.js +1 -1
  96. package/dist/components/trace-viewer/components/span-segments.js +1 -1
  97. package/dist/components/trace-viewer/components/span-strategies.js +1 -1
  98. package/dist/components/trace-viewer/components/ui.js +1 -1
  99. package/dist/components/trace-viewer/components/zoom-button.js +1 -1
  100. package/dist/components/trace-viewer/components/zoom-icons.js +1 -1
  101. package/dist/components/trace-viewer/context.js +1 -1
  102. package/dist/components/trace-viewer/index.js +1 -1
  103. package/dist/components/trace-viewer/trace-viewer.js +1 -1
  104. package/dist/components/trace-viewer/types.js +1 -1
  105. package/dist/components/trace-viewer/util/constants.js +1 -1
  106. package/dist/components/trace-viewer/util/scrollbar-width.js +1 -1
  107. package/dist/components/trace-viewer/util/timing.d.ts +1 -1
  108. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  109. package/dist/components/trace-viewer/util/timing.js +6 -9
  110. package/dist/components/trace-viewer/util/tree.js +1 -1
  111. package/dist/components/trace-viewer/util/use-immediate-style.js +1 -1
  112. package/dist/components/trace-viewer/util/use-streaming-spans.js +1 -1
  113. package/dist/components/trace-viewer/util/use-trackpad-zoom.js +1 -1
  114. package/dist/components/trace-viewer/worker.js +1 -1
  115. package/dist/components/trace-viewer-new.d.ts +14 -0
  116. package/dist/components/trace-viewer-new.d.ts.map +1 -0
  117. package/dist/components/trace-viewer-new.js +22 -0
  118. package/dist/components/ui/alert.js +1 -1
  119. package/dist/components/ui/button.d.ts +10 -0
  120. package/dist/components/ui/button.d.ts.map +1 -0
  121. package/dist/components/ui/button.js +46 -0
  122. package/dist/components/ui/card.js +1 -1
  123. package/dist/components/ui/data-inspector.d.ts +39 -1
  124. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  125. package/dist/components/ui/data-inspector.js +207 -22
  126. package/dist/components/ui/decrypt-button.js +1 -1
  127. package/dist/components/ui/error-card.js +1 -1
  128. package/dist/components/ui/error-stack-block.d.ts.map +1 -1
  129. package/dist/components/ui/error-stack-block.js +27 -19
  130. package/dist/components/ui/icon-button.d.ts +19 -0
  131. package/dist/components/ui/icon-button.d.ts.map +1 -0
  132. package/dist/components/ui/icon-button.js +29 -0
  133. package/dist/components/ui/inspector-theme.d.ts +9 -10
  134. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  135. package/dist/components/ui/inspector-theme.js +39 -72
  136. package/dist/components/ui/kbd.d.ts +6 -0
  137. package/dist/components/ui/kbd.d.ts.map +1 -0
  138. package/dist/components/ui/kbd.js +6 -0
  139. package/dist/components/ui/load-more-button.js +1 -1
  140. package/dist/components/ui/menu-dropdown.js +1 -1
  141. package/dist/components/ui/skeleton.js +1 -1
  142. package/dist/components/ui/spinner.js +1 -1
  143. package/dist/components/ui/timestamp-tooltip.js +1 -1
  144. package/dist/components/ui/tooltip.d.ts +10 -0
  145. package/dist/components/ui/tooltip.d.ts.map +1 -0
  146. package/dist/components/ui/tooltip.js +12 -0
  147. package/dist/components/workflow-trace-view.d.ts +3 -1
  148. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  149. package/dist/components/workflow-trace-view.js +10 -5
  150. package/dist/components/workflow-traces/event-colors.js +1 -1
  151. package/dist/components/workflow-traces/trace-colors.js +1 -1
  152. package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
  153. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  154. package/dist/components/workflow-traces/trace-span-construction.js +19 -6
  155. package/dist/components/workflow-traces/trace-time-utils.js +1 -1
  156. package/dist/hooks/use-dark-mode.js +1 -1
  157. package/dist/hooks/use-intersection-observer.d.ts +17 -0
  158. package/dist/hooks/use-intersection-observer.d.ts.map +1 -0
  159. package/dist/hooks/use-intersection-observer.js +29 -0
  160. package/dist/hooks/use-load-more-on-scroll.d.ts +21 -0
  161. package/dist/hooks/use-load-more-on-scroll.d.ts.map +1 -0
  162. package/dist/hooks/use-load-more-on-scroll.js +28 -0
  163. package/dist/hooks/use-reduced-motion.d.ts +10 -0
  164. package/dist/hooks/use-reduced-motion.d.ts.map +1 -0
  165. package/dist/hooks/use-reduced-motion.js +29 -0
  166. package/dist/index.d.ts +3 -1
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +3 -2
  169. package/dist/lib/event-analysis.js +1 -1
  170. package/dist/lib/event-materialization.js +1 -1
  171. package/dist/lib/exact-event-search-id.d.ts +25 -0
  172. package/dist/lib/exact-event-search-id.d.ts.map +1 -0
  173. package/dist/lib/exact-event-search-id.js +39 -0
  174. package/dist/lib/hydration.d.ts +14 -1
  175. package/dist/lib/hydration.d.ts.map +1 -1
  176. package/dist/lib/hydration.js +157 -4
  177. package/dist/lib/stream-display.d.ts +18 -0
  178. package/dist/lib/stream-display.d.ts.map +1 -0
  179. package/dist/lib/stream-display.js +30 -0
  180. package/dist/lib/toast.js +1 -1
  181. package/dist/lib/trace-builder.d.ts.map +1 -1
  182. package/dist/lib/trace-builder.js +3 -2
  183. package/dist/lib/utils.d.ts +9 -9
  184. package/dist/lib/utils.d.ts.map +1 -1
  185. package/dist/lib/utils.js +27 -32
  186. package/dist/styles.css +475 -1
  187. package/package.json +6 -4
  188. package/src/components/event-list-view.tsx +421 -262
  189. package/src/components/index.ts +13 -1
  190. package/src/components/new-trace-viewer/components/copy-button.tsx +68 -0
  191. package/src/components/new-trace-viewer/components/event-list.tsx +135 -0
  192. package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
  193. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
  194. package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
  195. package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
  196. package/src/components/new-trace-viewer/components/split-pane.tsx +198 -0
  197. package/src/components/new-trace-viewer/components/timeline.tsx +560 -0
  198. package/src/components/new-trace-viewer/components/trace-viewer-skeleton.tsx +94 -0
  199. package/src/components/new-trace-viewer/components/use-row-window.ts +93 -0
  200. package/src/components/new-trace-viewer/context.tsx +90 -0
  201. package/src/components/new-trace-viewer/detail-panel.tsx +55 -0
  202. package/src/components/new-trace-viewer/icons.tsx +79 -0
  203. package/src/components/new-trace-viewer/search.ts +178 -0
  204. package/src/components/new-trace-viewer/trace-viewer.tsx +762 -0
  205. package/src/components/new-trace-viewer/types.ts +0 -0
  206. package/src/components/new-trace-viewer/util/constants.ts +0 -0
  207. package/src/components/new-trace-viewer/utils.ts +482 -0
  208. package/src/components/run-trace-view.tsx +3 -0
  209. package/src/components/sidebar/attribute-panel.tsx +240 -196
  210. package/src/components/sidebar/copyable-data-block.tsx +49 -27
  211. package/src/components/sidebar/detail-card.tsx +119 -59
  212. package/src/components/sidebar/entity-detail-panel.tsx +92 -148
  213. package/src/components/sidebar/events-list.tsx +51 -76
  214. package/src/components/sidebar/sidebar-data-context.tsx +62 -0
  215. package/src/components/stream-viewer.tsx +18 -40
  216. package/src/components/trace-viewer/util/timing.ts +5 -9
  217. package/src/components/trace-viewer-new.tsx +57 -0
  218. package/src/components/ui/button.tsx +68 -0
  219. package/src/components/ui/data-inspector.tsx +427 -31
  220. package/src/components/ui/error-stack-block.tsx +39 -30
  221. package/src/components/ui/icon-button.tsx +53 -0
  222. package/src/components/ui/inspector-theme.ts +39 -81
  223. package/src/components/ui/kbd.tsx +21 -0
  224. package/src/components/ui/tooltip.tsx +47 -0
  225. package/src/components/workflow-trace-view.tsx +12 -2
  226. package/src/components/workflow-traces/trace-span-construction.ts +24 -6
  227. package/src/hooks/use-intersection-observer.ts +57 -0
  228. package/src/hooks/use-load-more-on-scroll.ts +49 -0
  229. package/src/hooks/use-reduced-motion.ts +34 -0
  230. package/src/index.ts +9 -0
  231. package/src/lib/exact-event-search-id.ts +67 -0
  232. package/src/lib/hydration.ts +169 -1
  233. package/src/lib/stream-display.ts +48 -0
  234. package/src/lib/trace-builder.ts +2 -1
  235. package/src/lib/utils.ts +27 -32
  236. package/src/styles.css +475 -1
  237. package/dist/components/error-boundary.js.map +0 -1
  238. package/dist/components/event-list-view.js.map +0 -1
  239. package/dist/components/hook-actions.js.map +0 -1
  240. package/dist/components/index.js.map +0 -1
  241. package/dist/components/run-trace-view.js.map +0 -1
  242. package/dist/components/sidebar/attribute-panel.js.map +0 -1
  243. package/dist/components/sidebar/conversation-view.js.map +0 -1
  244. package/dist/components/sidebar/copyable-data-block.js.map +0 -1
  245. package/dist/components/sidebar/detail-card.js.map +0 -1
  246. package/dist/components/sidebar/entity-detail-panel.js.map +0 -1
  247. package/dist/components/sidebar/events-list.js.map +0 -1
  248. package/dist/components/sidebar/resolve-hook-modal.js.map +0 -1
  249. package/dist/components/stream-viewer.js.map +0 -1
  250. package/dist/components/trace-viewer/components/map.js.map +0 -1
  251. package/dist/components/trace-viewer/components/markers.js.map +0 -1
  252. package/dist/components/trace-viewer/components/node.js.map +0 -1
  253. package/dist/components/trace-viewer/components/search-input.js.map +0 -1
  254. package/dist/components/trace-viewer/components/search.js.map +0 -1
  255. package/dist/components/trace-viewer/components/span-content.js.map +0 -1
  256. package/dist/components/trace-viewer/components/span-detail-panel.js.map +0 -1
  257. package/dist/components/trace-viewer/components/span-segments.js.map +0 -1
  258. package/dist/components/trace-viewer/components/span-strategies.js.map +0 -1
  259. package/dist/components/trace-viewer/components/ui.js.map +0 -1
  260. package/dist/components/trace-viewer/components/zoom-button.js.map +0 -1
  261. package/dist/components/trace-viewer/components/zoom-icons.js.map +0 -1
  262. package/dist/components/trace-viewer/context.js.map +0 -1
  263. package/dist/components/trace-viewer/index.js.map +0 -1
  264. package/dist/components/trace-viewer/trace-viewer.js.map +0 -1
  265. package/dist/components/trace-viewer/types.js.map +0 -1
  266. package/dist/components/trace-viewer/util/constants.js.map +0 -1
  267. package/dist/components/trace-viewer/util/scrollbar-width.js.map +0 -1
  268. package/dist/components/trace-viewer/util/timing.js.map +0 -1
  269. package/dist/components/trace-viewer/util/tree.js.map +0 -1
  270. package/dist/components/trace-viewer/util/use-immediate-style.js.map +0 -1
  271. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +0 -1
  272. package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +0 -1
  273. package/dist/components/trace-viewer/worker.js.map +0 -1
  274. package/dist/components/ui/alert.js.map +0 -1
  275. package/dist/components/ui/card.js.map +0 -1
  276. package/dist/components/ui/data-inspector.js.map +0 -1
  277. package/dist/components/ui/decrypt-button.js.map +0 -1
  278. package/dist/components/ui/error-card.js.map +0 -1
  279. package/dist/components/ui/error-stack-block.js.map +0 -1
  280. package/dist/components/ui/inspector-theme.js.map +0 -1
  281. package/dist/components/ui/load-more-button.js.map +0 -1
  282. package/dist/components/ui/menu-dropdown.js.map +0 -1
  283. package/dist/components/ui/skeleton.js.map +0 -1
  284. package/dist/components/ui/spinner.js.map +0 -1
  285. package/dist/components/ui/timestamp-tooltip.js.map +0 -1
  286. package/dist/components/workflow-trace-view.js.map +0 -1
  287. package/dist/components/workflow-traces/event-colors.js.map +0 -1
  288. package/dist/components/workflow-traces/trace-colors.js.map +0 -1
  289. package/dist/components/workflow-traces/trace-span-construction.js.map +0 -1
  290. package/dist/components/workflow-traces/trace-time-utils.js.map +0 -1
  291. package/dist/hooks/use-dark-mode.js.map +0 -1
  292. package/dist/index.js.map +0 -1
  293. package/dist/lib/event-analysis.js.map +0 -1
  294. package/dist/lib/event-materialization.js.map +0 -1
  295. package/dist/lib/hydration.js.map +0 -1
  296. package/dist/lib/toast.js.map +0 -1
  297. package/dist/lib/trace-builder.js.map +0 -1
  298. package/dist/lib/utils.js.map +0 -1
@@ -3,13 +3,24 @@
3
3
  import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
4
4
  import type { Event, WorkflowRun } from '@workflow/world';
5
5
  import { Check, ChevronRight, Copy } from 'lucide-react';
6
- import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
6
+ import type {
7
+ KeyboardEvent as ReactKeyboardEvent,
8
+ MouseEvent as ReactMouseEvent,
9
+ ReactNode,
10
+ } from 'react';
7
11
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
12
  import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
9
13
  import { isEncryptedMarker } from '../lib/hydration';
14
+ import {
15
+ parseExactWorkflowSearchId,
16
+ looksLikeWorkflowIdSearchInput,
17
+ type ExactIdSearchResult,
18
+ type ExactWorkflowSearchIdKind,
19
+ } from '../lib/exact-event-search-id';
10
20
  import { DecryptButton } from './ui/decrypt-button';
11
21
  import { formatDuration } from '../lib/utils';
12
- import { DataInspector } from './ui/data-inspector';
22
+ import { useToast } from '../lib/toast';
23
+ import { DataInspector, DecryptClickContext } from './ui/data-inspector';
13
24
  import {
14
25
  ErrorStackBlock,
15
26
  isStructuredErrorWithStack,
@@ -142,7 +153,7 @@ function buildNameMaps(
142
153
  return { correlationNameMap, workflowName };
143
154
  }
144
155
 
145
- interface DurationInfo {
156
+ export interface DurationInfo {
146
157
  /** Time from created → started (ms) */
147
158
  queued?: number;
148
159
  /** Time from started → completed/failed/cancelled (ms) */
@@ -154,19 +165,30 @@ interface DurationInfo {
154
165
  * created ↔ started (queued) and started ↔ completed/failed/cancelled (ran).
155
166
  * Also computes run-level durations under the key '__run__'.
156
167
  */
157
- function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
168
+ export function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
169
+ // Process events in chronological order so the result doesn't depend on
170
+ // the caller's sort direction. Retried steps emit multiple `step_started`
171
+ // events for the same correlationId; the queued duration must be measured
172
+ // against the first one, not the last.
173
+ const chronological = [...events].sort(
174
+ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
175
+ );
176
+
158
177
  const createdTimes = new Map<string, number>();
178
+ const firstStartedTimes = new Map<string, number>();
159
179
  const startedTimes = new Map<string, number>();
160
180
  const durations = new Map<string, DurationInfo>();
161
181
 
162
- for (const event of events) {
182
+ for (const event of chronological) {
163
183
  const ts = new Date(event.createdAt).getTime();
164
184
  const key = event.correlationId ?? '__run__';
165
185
  const type: string = event.eventType;
166
186
 
167
187
  // Track created times (first event for each correlation)
168
188
  if (type === 'step_created' || type === 'run_created') {
169
- createdTimes.set(key, ts);
189
+ if (!createdTimes.has(key)) {
190
+ createdTimes.set(key, ts);
191
+ }
170
192
  }
171
193
 
172
194
  // Track started times & compute queued duration
@@ -176,16 +198,21 @@ function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
176
198
  type === 'workflow_started'
177
199
  ) {
178
200
  startedTimes.set(key, ts);
179
- // If no explicit created event was seen, use the started time as created
180
- if (!createdTimes.has(key)) {
181
- createdTimes.set(key, ts);
182
- }
183
- const createdAt = createdTimes.get(key);
184
- const info = durations.get(key) ?? {};
185
- if (createdAt !== undefined) {
186
- info.queued = ts - createdAt;
201
+ // The queued duration is anchored on the first start event only
202
+ // subsequent step_started events come from retries.
203
+ if (!firstStartedTimes.has(key)) {
204
+ firstStartedTimes.set(key, ts);
205
+ // If no explicit created event was seen, use the started time as created
206
+ if (!createdTimes.has(key)) {
207
+ createdTimes.set(key, ts);
208
+ }
209
+ const createdAt = createdTimes.get(key);
210
+ const info = durations.get(key) ?? {};
211
+ if (createdAt !== undefined) {
212
+ info.queued = ts - createdAt;
213
+ }
214
+ durations.set(key, info);
187
215
  }
188
- durations.set(key, info);
189
216
  }
190
217
 
191
218
  // Compute ran duration on terminal events
@@ -703,6 +730,14 @@ interface EventsListProps {
703
730
  onDecrypt?: () => void;
704
731
  /** Whether the encryption key is currently being fetched. */
705
732
  isDecrypting?: boolean;
733
+ /** Run-level hint: the run contains encrypted data (from probe). */
734
+ hasEncryptedData?: boolean;
735
+ /** Fetch events for an exact correlation or event ID. */
736
+ onExactIdSearch?: (
737
+ id: string,
738
+ kind: ExactWorkflowSearchIdKind,
739
+ signal?: AbortSignal
740
+ ) => Promise<ExactIdSearchResult>;
706
741
  }
707
742
 
708
743
  function EventRow({
@@ -725,6 +760,7 @@ function EventRow({
725
760
  onCacheEventData,
726
761
  encryptionKey,
727
762
  onEncryptedDataDetected,
763
+ suppressGroupDimming = false,
728
764
  }: {
729
765
  event: Event;
730
766
  index: number;
@@ -745,6 +781,8 @@ function EventRow({
745
781
  onCacheEventData: (eventId: string, data: unknown) => void;
746
782
  encryptionKey?: Uint8Array;
747
783
  onEncryptedDataDetected?: () => void;
784
+ /** Exact-ID search results should not dim unrelated rows. */
785
+ suppressGroupDimming?: boolean;
748
786
  }) {
749
787
  const [isLoading, setIsLoading] = useState(false);
750
788
  const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
@@ -786,7 +824,7 @@ function EventRow({
786
824
 
787
825
  const hasActive = activeGroupKey !== undefined;
788
826
  const isRelated = rowGroupKey !== undefined && rowGroupKey === activeGroupKey;
789
- const isDimmed = hasActive && !isRelated;
827
+ const isDimmed = hasActive && !isRelated && !suppressGroupDimming;
790
828
  const isPulsing = hasActive && isRelated;
791
829
 
792
830
  // Gutter state derived from selectedGroupRange
@@ -1136,7 +1174,10 @@ export function EventListView({
1136
1174
  onSortOrderChange,
1137
1175
  onDecrypt,
1138
1176
  isDecrypting = false,
1177
+ hasEncryptedData: hasEncryptedDataProp = false,
1178
+ onExactIdSearch,
1139
1179
  }: EventsListProps) {
1180
+ const toast = useToast();
1140
1181
  const [internalSortOrder, setInternalSortOrder] = useState<'asc' | 'desc'>(
1141
1182
  'asc'
1142
1183
  );
@@ -1152,25 +1193,42 @@ export function EventListView({
1152
1193
  [onSortOrderChange]
1153
1194
  );
1154
1195
 
1196
+ const [searchQuery, setSearchQuery] = useState('');
1197
+ const [searchResults, setSearchResults] = useState<Event[] | null>(null);
1198
+ const [searchResultsTruncated, setSearchResultsTruncated] = useState(false);
1199
+ const [searchError, setSearchError] = useState<string | null>(null);
1200
+ const [searchLoading, setSearchLoading] = useState(false);
1201
+ const [searchNotFound, setSearchNotFound] = useState(false);
1202
+ const searchRequestRef = useRef(0);
1203
+ const virtuosoRef = useRef<VirtuosoHandle>(null);
1204
+
1205
+ const parsedSearchId = useMemo(
1206
+ () => parseExactWorkflowSearchId(searchQuery),
1207
+ [searchQuery]
1208
+ );
1209
+ const isExactSearchActive = searchResults !== null;
1210
+
1155
1211
  const sortedEvents = useMemo(() => {
1156
- if (!events || events.length === 0) return [];
1212
+ const sourceEvents = isExactSearchActive ? searchResults : (events ?? []);
1213
+ if (sourceEvents.length === 0) return [];
1157
1214
  const dir = effectiveSortOrder === 'desc' ? -1 : 1;
1158
- return [...events].sort(
1215
+ return [...sourceEvents].sort(
1159
1216
  (a, b) =>
1160
1217
  dir *
1161
1218
  (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
1162
1219
  );
1163
- }, [events, effectiveSortOrder]);
1220
+ }, [events, effectiveSortOrder, isExactSearchActive, searchResults]);
1164
1221
 
1165
1222
  // Detect encrypted fields across all loaded events (inline eventData).
1166
1223
  const hasEncryptedInlineData = useMemo(() => {
1167
- if (!events) return false;
1168
- for (const event of events) {
1224
+ const sourceEvents = isExactSearchActive ? searchResults : events;
1225
+ if (!sourceEvents) return false;
1226
+ for (const event of sourceEvents) {
1169
1227
  const ed = (event as Record<string, unknown>).eventData;
1170
1228
  if (hasEncryptedValues(ed)) return true;
1171
1229
  }
1172
1230
  return false;
1173
- }, [events]);
1231
+ }, [events, isExactSearchActive, searchResults]);
1174
1232
 
1175
1233
  // Tracks whether any expanded row's lazy-loaded data contained encrypted markers.
1176
1234
  // Set to true by EventRow via onEncryptedDataDetected; never reset (sticky).
@@ -1180,11 +1238,16 @@ export function EventListView({
1180
1238
  setFoundEncryptedInLazyData(true);
1181
1239
  }, []);
1182
1240
 
1183
- const hasEncryptedData = hasEncryptedInlineData || foundEncryptedInLazyData;
1241
+ const hasEncryptedData =
1242
+ hasEncryptedDataProp || hasEncryptedInlineData || foundEncryptedInLazyData;
1184
1243
 
1185
1244
  const { correlationNameMap, workflowName } = useMemo(
1186
- () => buildNameMaps(events ?? null, run ?? null),
1187
- [events, run]
1245
+ () =>
1246
+ buildNameMaps(
1247
+ isExactSearchActive ? searchResults : (events ?? null),
1248
+ run ?? null
1249
+ ),
1250
+ [events, isExactSearchActive, run, searchResults]
1188
1251
  );
1189
1252
 
1190
1253
  const durationMap = useMemo(
@@ -1274,68 +1337,139 @@ export function EventListView({
1274
1337
  return first >= 0 ? { first, last } : null;
1275
1338
  }, [activeGroupKey, sortedEvents]);
1276
1339
 
1277
- const [searchQuery, setSearchQuery] = useState('');
1278
- const virtuosoRef = useRef<VirtuosoHandle>(null);
1279
-
1280
- const searchIndex = useMemo(() => {
1281
- const entries: {
1282
- fields: string[];
1283
- groupKey?: string;
1284
- eventId: string;
1285
- index: number;
1286
- }[] = [];
1287
- for (let i = 0; i < sortedEvents.length; i++) {
1288
- const ev = sortedEvents[i];
1289
- const isRun = isRunLevel(ev.eventType);
1290
- const name = isRun
1291
- ? (workflowName ?? '')
1292
- : ev.correlationId
1293
- ? (correlationNameMap.get(ev.correlationId) ?? '')
1294
- : '';
1295
- entries.push({
1296
- fields: [
1297
- ev.eventId,
1298
- ev.correlationId ?? '',
1299
- ev.eventType,
1300
- formatEventType(ev.eventType),
1301
- name,
1302
- ].map((f) => f.toLowerCase()),
1303
- groupKey: ev.correlationId ?? (isRun ? '__run__' : undefined),
1304
- eventId: ev.eventId,
1305
- index: i,
1306
- });
1307
- }
1308
- return entries;
1309
- }, [sortedEvents, correlationNameMap, workflowName]);
1310
-
1311
1340
  useEffect(() => {
1312
- const q = searchQuery.trim().toLowerCase();
1313
- if (!q) {
1341
+ const trimmed = searchQuery.trim();
1342
+ if (!trimmed) {
1343
+ searchRequestRef.current += 1;
1344
+ setSearchResults(null);
1345
+ setSearchResultsTruncated(false);
1346
+ setSearchError(null);
1347
+ setSearchLoading(false);
1348
+ setSearchNotFound(false);
1314
1349
  setSelectedGroupKey(undefined);
1315
1350
  return;
1316
1351
  }
1317
- let bestMatch: (typeof searchIndex)[number] | null = null;
1318
- let bestScore = 0;
1319
- for (const entry of searchIndex) {
1320
- for (const field of entry.fields) {
1321
- if (field && field.includes(q)) {
1322
- const score = q.length / field.length;
1323
- if (score > bestScore) {
1324
- bestScore = score;
1325
- bestMatch = entry;
1352
+
1353
+ const parsed = parseExactWorkflowSearchId(trimmed);
1354
+ if (!parsed || !onExactIdSearch) {
1355
+ setSearchResults(null);
1356
+ setSearchLoading(false);
1357
+ setSearchNotFound(false);
1358
+ return;
1359
+ }
1360
+
1361
+ const requestId = ++searchRequestRef.current;
1362
+ setSearchLoading(true);
1363
+ setSearchNotFound(false);
1364
+ setSearchError(null);
1365
+
1366
+ const abortController = new AbortController();
1367
+
1368
+ const timer = setTimeout(() => {
1369
+ void (async () => {
1370
+ try {
1371
+ const results = await onExactIdSearch(
1372
+ parsed.id,
1373
+ parsed.kind,
1374
+ abortController.signal
1375
+ );
1376
+ if (
1377
+ abortController.signal.aborted ||
1378
+ searchRequestRef.current !== requestId
1379
+ ) {
1380
+ return;
1381
+ }
1382
+
1383
+ if (results.status === 'error') {
1384
+ setSearchResults([]);
1385
+ setSearchResultsTruncated(false);
1386
+ setSearchNotFound(false);
1387
+ setSearchError(results.message);
1388
+ setSelectedGroupKey(undefined);
1389
+ return;
1390
+ }
1391
+
1392
+ if (
1393
+ results.status === 'not_found' ||
1394
+ (results.status === 'ok' && results.events.length === 0)
1395
+ ) {
1396
+ setSearchResults([]);
1397
+ setSearchResultsTruncated(false);
1398
+ setSearchNotFound(true);
1399
+ setSearchError(null);
1400
+ setSelectedGroupKey(undefined);
1401
+ return;
1402
+ }
1403
+
1404
+ setSearchResults(results.events);
1405
+ setSearchResultsTruncated(Boolean(results.truncated));
1406
+ setSearchNotFound(false);
1407
+ setSearchError(null);
1408
+ setSelectedGroupKey(
1409
+ parsed.kind === 'event'
1410
+ ? (() => {
1411
+ const first = results.events[0];
1412
+ if (!first) return undefined;
1413
+ return isRunLevel(first.eventType)
1414
+ ? '__run__'
1415
+ : (first.correlationId ?? undefined);
1416
+ })()
1417
+ : parsed.id
1418
+ );
1419
+ virtuosoRef.current?.scrollToIndex({
1420
+ index: 0,
1421
+ align: 'start',
1422
+ behavior: 'smooth',
1423
+ });
1424
+ } catch {
1425
+ if (
1426
+ abortController.signal.aborted ||
1427
+ searchRequestRef.current !== requestId
1428
+ ) {
1429
+ return;
1430
+ }
1431
+ setSearchResults([]);
1432
+ setSearchResultsTruncated(false);
1433
+ setSearchNotFound(false);
1434
+ setSearchError('Failed to search events. Try again.');
1435
+ setSelectedGroupKey(undefined);
1436
+ } finally {
1437
+ if (
1438
+ searchRequestRef.current === requestId &&
1439
+ !abortController.signal.aborted
1440
+ ) {
1441
+ setSearchLoading(false);
1326
1442
  }
1327
1443
  }
1444
+ })();
1445
+ }, 300);
1446
+
1447
+ return () => {
1448
+ clearTimeout(timer);
1449
+ abortController.abort();
1450
+ };
1451
+ }, [searchQuery, onExactIdSearch]);
1452
+
1453
+ const handleSearchKeyDown = useCallback(
1454
+ (event: ReactKeyboardEvent<HTMLInputElement>) => {
1455
+ if (event.key !== 'Enter') {
1456
+ return;
1328
1457
  }
1329
- }
1330
- if (bestMatch) {
1331
- setSelectedGroupKey(bestMatch.groupKey);
1332
- virtuosoRef.current?.scrollToIndex({
1333
- index: bestMatch.index,
1334
- align: 'center',
1335
- behavior: 'smooth',
1336
- });
1337
- }
1338
- }, [searchQuery, searchIndex]);
1458
+
1459
+ const trimmed = searchQuery.trim();
1460
+ if (
1461
+ !trimmed ||
1462
+ parseExactWorkflowSearchId(trimmed) ||
1463
+ !onExactIdSearch ||
1464
+ !looksLikeWorkflowIdSearchInput(trimmed)
1465
+ ) {
1466
+ return;
1467
+ }
1468
+
1469
+ toast.info('Enter a full step ID, wait ID, hook ID, or event ID');
1470
+ },
1471
+ [searchQuery, onExactIdSearch, toast]
1472
+ );
1339
1473
 
1340
1474
  // Track whether we've ever had events to distinguish initial load from refetch
1341
1475
  const hasHadEventsRef = useRef(false);
@@ -1381,207 +1515,232 @@ export function EventListView({
1381
1515
  );
1382
1516
  }
1383
1517
 
1384
- if (!isLoading && (!events || events.length === 0)) {
1385
- return (
1386
- <div
1387
- className="flex items-center justify-center h-full text-sm"
1388
- style={{ color: 'var(--ds-gray-700)' }}
1389
- >
1390
- No events found
1391
- </div>
1392
- );
1393
- }
1394
-
1395
1518
  return (
1396
- <div className="h-full flex flex-col overflow-hidden">
1397
- <style>{`@keyframes workflow-dot-pulse{0%{transform:scale(1);opacity:.7}70%,100%{transform:scale(2.2);opacity:0}}`}</style>
1398
- {/* Search bar + sort */}
1399
- <div
1400
- style={{
1401
- padding: 6,
1402
- backgroundColor: 'var(--ds-background-100)',
1403
- display: 'flex',
1404
- gap: 6,
1405
- }}
1406
- >
1407
- <label
1519
+ <DecryptClickContext.Provider
1520
+ value={onDecrypt ? { onDecrypt, isDecrypting } : undefined}
1521
+ >
1522
+ <div className="h-full flex flex-col overflow-hidden">
1523
+ <style>{`@keyframes workflow-dot-pulse{0%{transform:scale(1);opacity:.7}70%,100%{transform:scale(2.2);opacity:0}}`}</style>
1524
+ {/* Search bar + sort */}
1525
+ <div
1408
1526
  style={{
1527
+ padding: 6,
1528
+ backgroundColor: 'var(--ds-background-100)',
1409
1529
  display: 'flex',
1410
- alignItems: 'center',
1411
- justifyContent: 'center',
1412
- borderRadius: 6,
1413
- boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
1414
- background: 'var(--ds-background-100)',
1415
- height: 40,
1416
- flex: 1,
1417
- minWidth: 0,
1530
+ gap: 6,
1418
1531
  }}
1419
1532
  >
1420
- <div
1533
+ <label
1421
1534
  style={{
1422
- width: 40,
1423
- height: 40,
1424
1535
  display: 'flex',
1425
1536
  alignItems: 'center',
1426
1537
  justifyContent: 'center',
1427
- color: 'var(--ds-gray-800)',
1428
- flexShrink: 0,
1538
+ borderRadius: 6,
1539
+ boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
1540
+ background: 'var(--ds-background-100)',
1541
+ height: 40,
1542
+ flex: 1,
1543
+ minWidth: 0,
1429
1544
  }}
1430
1545
  >
1431
- <svg
1432
- width={16}
1433
- height={16}
1434
- viewBox="0 0 16 16"
1435
- fill="none"
1436
- aria-hidden="true"
1437
- focusable="false"
1546
+ <div
1547
+ style={{
1548
+ width: 40,
1549
+ height: 40,
1550
+ display: 'flex',
1551
+ alignItems: 'center',
1552
+ justifyContent: 'center',
1553
+ color: 'var(--ds-gray-800)',
1554
+ flexShrink: 0,
1555
+ }}
1438
1556
  >
1439
- <circle
1440
- cx="7"
1441
- cy="7"
1442
- r="4.5"
1443
- stroke="currentColor"
1444
- strokeWidth="1.5"
1445
- />
1446
- <path
1447
- d="M11.5 11.5L14 14"
1448
- stroke="currentColor"
1449
- strokeWidth="1.5"
1450
- strokeLinecap="round"
1451
- />
1452
- </svg>
1453
- </div>
1454
- <input
1455
- type="search"
1456
- placeholder="Search by name, event type, or ID…"
1457
- value={searchQuery}
1458
- onChange={(e) => setSearchQuery(e.target.value)}
1459
- style={{
1460
- marginLeft: -16,
1461
- paddingInline: 12,
1462
- fontFamily: 'inherit',
1463
- fontSize: 14,
1464
- background: 'transparent',
1465
- border: 'none',
1466
- outline: 'none',
1467
- height: 40,
1468
- width: '100%',
1469
- }}
1470
- />
1471
- </label>
1472
- <MenuDropdown
1473
- options={SORT_OPTIONS}
1474
- value={effectiveSortOrder}
1475
- onChange={handleSortOrderChange}
1476
- />
1477
- {(hasEncryptedData || encryptionKey) && onDecrypt && (
1478
- <DecryptButton
1479
- decrypted={!!encryptionKey}
1480
- loading={isDecrypting}
1481
- onClick={onDecrypt}
1557
+ <svg
1558
+ width={16}
1559
+ height={16}
1560
+ viewBox="0 0 16 16"
1561
+ fill="none"
1562
+ aria-hidden="true"
1563
+ focusable="false"
1564
+ >
1565
+ <circle
1566
+ cx="7"
1567
+ cy="7"
1568
+ r="4.5"
1569
+ stroke="currentColor"
1570
+ strokeWidth="1.5"
1571
+ />
1572
+ <path
1573
+ d="M11.5 11.5L14 14"
1574
+ stroke="currentColor"
1575
+ strokeWidth="1.5"
1576
+ strokeLinecap="round"
1577
+ />
1578
+ </svg>
1579
+ </div>
1580
+ <input
1581
+ type="search"
1582
+ placeholder="Search by step ID, wait ID, hook ID, or event ID…"
1583
+ value={searchQuery}
1584
+ onChange={(e) => setSearchQuery(e.target.value)}
1585
+ onKeyDown={handleSearchKeyDown}
1586
+ disabled={!onExactIdSearch}
1587
+ title={
1588
+ onExactIdSearch
1589
+ ? undefined
1590
+ : 'Exact ID search is unavailable in this view.'
1591
+ }
1592
+ style={{
1593
+ marginLeft: -16,
1594
+ paddingInline: 12,
1595
+ fontFamily: 'inherit',
1596
+ fontSize: 14,
1597
+ background: 'transparent',
1598
+ border: 'none',
1599
+ outline: 'none',
1600
+ height: 40,
1601
+ width: '100%',
1602
+ opacity: onExactIdSearch ? 1 : 0.5,
1603
+ cursor: onExactIdSearch ? 'text' : 'not-allowed',
1604
+ }}
1605
+ />
1606
+ </label>
1607
+ <MenuDropdown
1608
+ options={SORT_OPTIONS}
1609
+ value={effectiveSortOrder}
1610
+ onChange={handleSortOrderChange}
1482
1611
  />
1483
- )}
1484
- </div>
1485
-
1486
- {/* Header */}
1487
- <div
1488
- className="flex items-center gap-0 text-[13px] font-medium h-10 border-b flex-shrink-0"
1489
- style={{
1490
- borderColor: 'var(--ds-gray-alpha-200)',
1491
- color: 'var(--ds-gray-900)',
1492
- backgroundColor: 'var(--ds-background-100)',
1493
- }}
1494
- >
1495
- <div className="flex-shrink-0" style={{ width: GUTTER_WIDTH }} />
1496
- <div className="w-5 flex-shrink-0" />
1497
- <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1498
- Time
1499
- </div>
1500
- <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1501
- Event Type
1502
- </div>
1503
- <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1504
- Name
1505
- </div>
1506
- <div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
1507
- Correlation ID
1508
- </div>
1509
- <div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
1510
- Event ID
1612
+ {(hasEncryptedData || encryptionKey) && onDecrypt && (
1613
+ <DecryptButton
1614
+ decrypted={!!encryptionKey}
1615
+ loading={isDecrypting}
1616
+ onClick={onDecrypt}
1617
+ />
1618
+ )}
1511
1619
  </div>
1512
- </div>
1513
1620
 
1514
- {/* Virtualized event rows or refetching skeleton */}
1515
- {isRefetching ? (
1516
- <RowsSkeleton />
1517
- ) : (
1518
- <Virtuoso
1519
- ref={virtuosoRef}
1520
- totalCount={sortedEvents.length}
1521
- overscan={20}
1522
- defaultItemHeight={40}
1523
- endReached={() => {
1524
- if (!hasMoreEvents || isLoadingMoreEvents) {
1525
- return;
1526
- }
1527
- void onLoadMoreEvents?.();
1528
- }}
1529
- itemContent={(index: number) => {
1530
- const ev = sortedEvents[index];
1531
- return (
1532
- <EventRow
1533
- event={ev}
1534
- index={index}
1535
- isFirst={index === 0}
1536
- isLast={index === sortedEvents.length - 1}
1537
- isExpanded={expandedEventIds.has(ev.eventId)}
1538
- onToggleExpand={toggleEventExpanded}
1539
- activeGroupKey={activeGroupKey}
1540
- selectedGroupKey={selectedGroupKey}
1541
- selectedGroupRange={selectedGroupRange}
1542
- correlationNameMap={correlationNameMap}
1543
- workflowName={workflowName}
1544
- durationMap={durationMap}
1545
- onSelectGroup={onSelectGroup}
1546
- onHoverGroup={onHoverGroup}
1547
- onLoadEventData={onLoadEventData}
1548
- cachedEventData={
1549
- eventDataCacheRef.current.get(ev.eventId) ?? null
1550
- }
1551
- onCacheEventData={cacheEventData}
1552
- encryptionKey={encryptionKey}
1553
- onEncryptedDataDetected={handleEncryptedDataDetected}
1554
- />
1555
- );
1621
+ {/* Header */}
1622
+ <div
1623
+ className="flex items-center gap-0 text-[13px] font-medium h-10 border-b flex-shrink-0"
1624
+ style={{
1625
+ borderColor: 'var(--ds-gray-alpha-200)',
1626
+ color: 'var(--ds-gray-900)',
1627
+ backgroundColor: 'var(--ds-background-100)',
1556
1628
  }}
1557
- style={{ flex: 1, minHeight: 0 }}
1558
- />
1559
- )}
1629
+ >
1630
+ <div className="flex-shrink-0" style={{ width: GUTTER_WIDTH }} />
1631
+ <div className="w-5 flex-shrink-0" />
1632
+ <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1633
+ Time
1634
+ </div>
1635
+ <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1636
+ Event Type
1637
+ </div>
1638
+ <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1639
+ Name
1640
+ </div>
1641
+ <div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
1642
+ Correlation ID
1643
+ </div>
1644
+ <div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
1645
+ Event ID
1646
+ </div>
1647
+ </div>
1560
1648
 
1561
- {/* Fixed footer count + load more */}
1562
- <div
1563
- className="relative flex-shrink-0 flex items-center h-10 border-t px-4 text-xs"
1564
- style={{
1565
- borderColor: 'var(--ds-gray-alpha-200)',
1566
- color: 'var(--ds-gray-900)',
1567
- backgroundColor: 'var(--ds-background-100)',
1568
- }}
1569
- >
1570
- <span>
1571
- {sortedEvents.length} event
1572
- {sortedEvents.length !== 1 ? 's' : ''} loaded
1573
- </span>
1574
- {hasMoreEvents && (
1575
- <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
1576
- <div className="pointer-events-auto">
1577
- <LoadMoreButton
1578
- loading={isLoadingMoreEvents}
1579
- onClick={() => void onLoadMoreEvents?.()}
1580
- />
1581
- </div>
1649
+ {/* Virtualized event rows or refetching skeleton */}
1650
+ {isRefetching || searchLoading ? (
1651
+ <RowsSkeleton />
1652
+ ) : sortedEvents.length === 0 ? (
1653
+ <div
1654
+ className="flex flex-1 items-center justify-center px-6 text-center text-sm"
1655
+ style={{ color: 'var(--ds-gray-700)' }}
1656
+ >
1657
+ {searchNotFound && searchQuery.trim()
1658
+ ? `No events found for ${searchQuery.trim()}`
1659
+ : searchError
1660
+ ? searchError
1661
+ : parsedSearchId && searchQuery.trim() && !onExactIdSearch
1662
+ ? 'Exact ID search is unavailable in this view.'
1663
+ : 'No events found'}
1582
1664
  </div>
1665
+ ) : (
1666
+ <Virtuoso
1667
+ ref={virtuosoRef}
1668
+ totalCount={sortedEvents.length}
1669
+ overscan={20}
1670
+ defaultItemHeight={40}
1671
+ endReached={() => {
1672
+ if (
1673
+ isExactSearchActive ||
1674
+ !hasMoreEvents ||
1675
+ isLoadingMoreEvents
1676
+ ) {
1677
+ return;
1678
+ }
1679
+ void onLoadMoreEvents?.();
1680
+ }}
1681
+ itemContent={(index: number) => {
1682
+ const ev = sortedEvents[index];
1683
+ return (
1684
+ <EventRow
1685
+ event={ev}
1686
+ index={index}
1687
+ isFirst={index === 0}
1688
+ isLast={index === sortedEvents.length - 1}
1689
+ isExpanded={expandedEventIds.has(ev.eventId)}
1690
+ onToggleExpand={toggleEventExpanded}
1691
+ activeGroupKey={activeGroupKey}
1692
+ selectedGroupKey={selectedGroupKey}
1693
+ selectedGroupRange={selectedGroupRange}
1694
+ correlationNameMap={correlationNameMap}
1695
+ workflowName={workflowName}
1696
+ durationMap={durationMap}
1697
+ onSelectGroup={onSelectGroup}
1698
+ onHoverGroup={onHoverGroup}
1699
+ onLoadEventData={onLoadEventData}
1700
+ cachedEventData={
1701
+ eventDataCacheRef.current.get(ev.eventId) ?? null
1702
+ }
1703
+ onCacheEventData={cacheEventData}
1704
+ encryptionKey={encryptionKey}
1705
+ onEncryptedDataDetected={handleEncryptedDataDetected}
1706
+ suppressGroupDimming={isExactSearchActive}
1707
+ />
1708
+ );
1709
+ }}
1710
+ style={{ flex: 1, minHeight: 0 }}
1711
+ />
1583
1712
  )}
1713
+
1714
+ {/* Fixed footer — count + load more */}
1715
+ <div
1716
+ className="relative flex-shrink-0 flex items-center h-10 border-t px-4 text-xs"
1717
+ style={{
1718
+ borderColor: 'var(--ds-gray-alpha-200)',
1719
+ color: 'var(--ds-gray-900)',
1720
+ backgroundColor: 'var(--ds-background-100)',
1721
+ }}
1722
+ >
1723
+ <span>
1724
+ {isExactSearchActive
1725
+ ? searchError
1726
+ ? searchError
1727
+ : searchNotFound
1728
+ ? `No events found for ${searchQuery.trim()}`
1729
+ : `${sortedEvents.length} event${sortedEvents.length !== 1 ? 's' : ''} for ${searchQuery.trim()}${searchResultsTruncated ? ' (results may be truncated)' : ''}`
1730
+ : `${sortedEvents.length} event${sortedEvents.length !== 1 ? 's' : ''} loaded`}
1731
+ </span>
1732
+ {!isExactSearchActive && hasMoreEvents && (
1733
+ <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
1734
+ <div className="pointer-events-auto">
1735
+ <LoadMoreButton
1736
+ loading={isLoadingMoreEvents}
1737
+ onClick={() => void onLoadMoreEvents?.()}
1738
+ />
1739
+ </div>
1740
+ </div>
1741
+ )}
1742
+ </div>
1584
1743
  </div>
1585
- </div>
1744
+ </DecryptClickContext.Provider>
1586
1745
  );
1587
1746
  }