@workflow/web-shared 5.0.0-beta.35 → 5.0.0-beta.37

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 (31) hide show
  1. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -1
  2. package/dist/components/new-trace-viewer/components/event-list.js +10 -7
  3. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -1
  4. package/dist/components/new-trace-viewer/components/split-pane.js +5 -5
  5. package/dist/components/new-trace-viewer/components/timeline.d.ts +8 -2
  6. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
  7. package/dist/components/new-trace-viewer/components/timeline.js +95 -26
  8. package/dist/components/new-trace-viewer/components/trace-shortcut-helper.js +2 -2
  9. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
  10. package/dist/components/new-trace-viewer/trace-viewer.js +24 -22
  11. package/dist/components/new-trace-viewer/utils.d.ts +29 -0
  12. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
  13. package/dist/components/new-trace-viewer/utils.js +46 -3
  14. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  15. package/dist/components/sidebar/attribute-panel.js +7 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/lib/hydration.d.ts.map +1 -1
  20. package/dist/lib/hydration.js +13 -5
  21. package/package.json +5 -5
  22. package/src/components/new-trace-viewer/components/event-list.tsx +21 -11
  23. package/src/components/new-trace-viewer/components/split-pane.tsx +14 -4
  24. package/src/components/new-trace-viewer/components/timeline.tsx +158 -47
  25. package/src/components/new-trace-viewer/components/trace-shortcut-helper.tsx +1 -1
  26. package/src/components/new-trace-viewer/trace-viewer.tsx +54 -50
  27. package/src/components/new-trace-viewer/utils.test.ts +199 -0
  28. package/src/components/new-trace-viewer/utils.ts +79 -2
  29. package/src/components/sidebar/attribute-panel.tsx +6 -0
  30. package/src/index.ts +1 -4
  31. package/src/lib/hydration.ts +12 -6
@@ -3,6 +3,7 @@ import type { Span, SpanEvent } from './types';
3
3
  import {
4
4
  clampViewportToRoot,
5
5
  computeOffscreenMarkers,
6
+ computeSpanDelta,
6
7
  computeSpanMarkers,
7
8
  computeSpanSegments,
8
9
  computeTimeMarkers,
@@ -257,3 +258,201 @@ describe('getResourceColor', () => {
257
258
  });
258
259
  });
259
260
  });
261
+
262
+ describe('computeSpanDelta', () => {
263
+ function span(id: string, startMs: number, endMs: number): Span {
264
+ return {
265
+ name: id,
266
+ kind: 0,
267
+ resource: 'step',
268
+ library: { name: 'workflow' },
269
+ spanId: id,
270
+ status: { code: 1 },
271
+ traceFlags: 0,
272
+ attributes: {},
273
+ links: [],
274
+ events: [],
275
+ startTime: ts(startMs),
276
+ endTime: ts(endMs),
277
+ duration: ts(endMs - startMs),
278
+ };
279
+ }
280
+
281
+ it('measures the gap between disjoint spans (anchor earlier)', () => {
282
+ const delta = computeSpanDelta(
283
+ span('a', 0, 100),
284
+ span('b', 300, 400),
285
+ 0,
286
+ 1000
287
+ );
288
+ expect(delta).toEqual({
289
+ deltaMs: 200,
290
+ anchorFrac: 0.1,
291
+ hoveredFrac: 0.3,
292
+ anchorEdge: 'end',
293
+ hoveredOffscreen: null,
294
+ });
295
+ });
296
+
297
+ it('is direction-agnostic: anchor later gives the same delta with edges swapped', () => {
298
+ const delta = computeSpanDelta(
299
+ span('b', 300, 400),
300
+ span('a', 0, 100),
301
+ 0,
302
+ 1000
303
+ );
304
+ expect(delta).toEqual({
305
+ deltaMs: 200,
306
+ anchorFrac: 0.3,
307
+ hoveredFrac: 0.1,
308
+ anchorEdge: 'start',
309
+ hoveredOffscreen: null,
310
+ });
311
+ });
312
+
313
+ it('measures start-to-start offset for overlapping spans', () => {
314
+ const delta = computeSpanDelta(
315
+ span('a', 0, 500),
316
+ span('b', 200, 800),
317
+ 0,
318
+ 1000
319
+ );
320
+ expect(delta).toEqual({
321
+ deltaMs: 200,
322
+ anchorFrac: 0,
323
+ hoveredFrac: 0.2,
324
+ anchorEdge: 'start',
325
+ hoveredOffscreen: null,
326
+ });
327
+ });
328
+
329
+ it('measures start-to-start offset for contained spans (step inside run)', () => {
330
+ const delta = computeSpanDelta(
331
+ span('run', 0, 1000),
332
+ span('step', 400, 600),
333
+ 0,
334
+ 1000
335
+ );
336
+ expect(delta).toEqual({
337
+ deltaMs: 400,
338
+ anchorFrac: 0,
339
+ hoveredFrac: 0.4,
340
+ anchorEdge: 'start',
341
+ hoveredOffscreen: null,
342
+ });
343
+ });
344
+
345
+ it('returns a zero delta for identical starts', () => {
346
+ const delta = computeSpanDelta(
347
+ span('a', 200, 500),
348
+ span('b', 200, 300),
349
+ 0,
350
+ 1000
351
+ );
352
+ expect(delta).toEqual({
353
+ deltaMs: 0,
354
+ anchorFrac: 0.2,
355
+ hoveredFrac: 0.2,
356
+ anchorEdge: 'start',
357
+ hoveredOffscreen: null,
358
+ });
359
+ });
360
+
361
+ it('keeps a zero delta sitting exactly on the viewport edge (root span anchor at default zoom)', () => {
362
+ const delta = computeSpanDelta(
363
+ span('run', 0, 1000),
364
+ span('step', 0, 400),
365
+ 0,
366
+ 1000
367
+ );
368
+ expect(delta).toEqual({
369
+ deltaMs: 0,
370
+ anchorFrac: 0,
371
+ hoveredFrac: 0,
372
+ anchorEdge: 'start',
373
+ hoveredOffscreen: null,
374
+ });
375
+ });
376
+
377
+ it('treats touching spans as a zero gap, not an overlap', () => {
378
+ const delta = computeSpanDelta(
379
+ span('a', 0, 300),
380
+ span('b', 300, 400),
381
+ 0,
382
+ 1000
383
+ );
384
+ expect(delta).toEqual({
385
+ deltaMs: 0,
386
+ anchorFrac: 0.3,
387
+ hoveredFrac: 0.3,
388
+ anchorEdge: 'end',
389
+ hoveredOffscreen: null,
390
+ });
391
+ });
392
+
393
+ it('handles zero-duration spans as points', () => {
394
+ const delta = computeSpanDelta(
395
+ span('a', 100, 100),
396
+ span('b', 400, 400),
397
+ 0,
398
+ 1000
399
+ );
400
+ expect(delta).toEqual({
401
+ deltaMs: 300,
402
+ anchorFrac: 0.1,
403
+ hoveredFrac: 0.4,
404
+ anchorEdge: 'end',
405
+ hoveredOffscreen: null,
406
+ });
407
+ });
408
+
409
+ it('clamps edges to the viewport but keeps the true delta', () => {
410
+ const delta = computeSpanDelta(
411
+ span('a', 0, 100),
412
+ span('b', 900, 950),
413
+ 500,
414
+ 800
415
+ );
416
+ expect(delta).toEqual({
417
+ deltaMs: 800,
418
+ anchorFrac: 0,
419
+ hoveredFrac: 1,
420
+ anchorEdge: 'end',
421
+ hoveredOffscreen: 'right',
422
+ });
423
+ });
424
+
425
+ it('flags a hovered span lying fully left of the viewport', () => {
426
+ const delta = computeSpanDelta(
427
+ span('anchor', 600, 700),
428
+ span('hovered', 0, 100),
429
+ 200,
430
+ 1200
431
+ );
432
+ expect(delta).toEqual({
433
+ deltaMs: 500,
434
+ anchorFrac: 0.4,
435
+ hoveredFrac: 0,
436
+ anchorEdge: 'start',
437
+ hoveredOffscreen: 'left',
438
+ });
439
+ });
440
+
441
+ it('returns null when the measurement is entirely left of the viewport', () => {
442
+ expect(
443
+ computeSpanDelta(span('a', 0, 100), span('b', 200, 300), 500, 1000)
444
+ ).toBeNull();
445
+ });
446
+
447
+ it('returns null when the measurement is entirely right of the viewport', () => {
448
+ expect(
449
+ computeSpanDelta(span('a', 600, 700), span('b', 900, 950), 0, 500)
450
+ ).toBeNull();
451
+ });
452
+
453
+ it('returns null for an empty viewport range', () => {
454
+ expect(
455
+ computeSpanDelta(span('a', 0, 100), span('b', 200, 300), 500, 500)
456
+ ).toBeNull();
457
+ });
458
+ });
@@ -174,6 +174,11 @@ export interface SpanGap {
174
174
  rowIndex: number;
175
175
  }
176
176
 
177
+ /** Project an absolute time onto the viewport as a clamped [0, 1] fraction. */
178
+ function toViewportFrac(ms: number, viewStart: number, range: number): number {
179
+ return Math.min(Math.max((ms - viewStart) / range, 0), 1);
180
+ }
181
+
177
182
  export function computeSpanGaps(
178
183
  spans: Span[],
179
184
  viewStart: number,
@@ -189,8 +194,8 @@ export function computeSpanGaps(
189
194
  const gapMs = startTime - endTime;
190
195
  if (gapMs <= 0) continue;
191
196
 
192
- const leftFrac = Math.min(Math.max((endTime - viewStart) / range, 0), 1);
193
- const rightFrac = Math.min(Math.max((startTime - viewStart) / range, 0), 1);
197
+ const leftFrac = toViewportFrac(endTime, viewStart, range);
198
+ const rightFrac = toViewportFrac(startTime, viewStart, range);
194
199
  if (rightFrac - leftFrac < 0.001) continue;
195
200
 
196
201
  gaps.push({ gapMs, leftFrac, rightFrac, rowIndex: i });
@@ -198,6 +203,78 @@ export function computeSpanGaps(
198
203
  return gaps;
199
204
  }
200
205
 
206
+ export interface SpanDelta {
207
+ /** True measured time between the two spans, unaffected by clamping. */
208
+ deltaMs: number;
209
+ /** Viewport-clamped fraction of the anchor span's measured edge. */
210
+ anchorFrac: number;
211
+ /** Viewport-clamped fraction of the hovered span's measured edge. */
212
+ hoveredFrac: number;
213
+ /**
214
+ * Which edge of the anchor span the measurement runs from: its end when the
215
+ * spans are disjoint and the anchor comes first, otherwise its start.
216
+ */
217
+ anchorEdge: 'start' | 'end';
218
+ /**
219
+ * Set when the hovered span lies entirely outside the viewport on one side,
220
+ * i.e. its row renders an off-screen arrow the line should stop short of.
221
+ */
222
+ hoveredOffscreen: 'left' | 'right' | null;
223
+ }
224
+
225
+ /**
226
+ * Measure the temporal delta between an anchor (selected) span and a hovered
227
+ * span for the Alt+hover measurement overlay. Ordering the two spans by start
228
+ * time (E = earlier-starting, L = later-starting), the measurement runs to
229
+ * L's start from E's end when the spans are disjoint ("how long after E ended
230
+ * did L start"), or from E's start when they overlap ("how long after E
231
+ * started did L start"). Fractions are clamped to the viewport like
232
+ * `computeSpanGaps`; `deltaMs` is always the true duration. Returns null when
233
+ * the measurement lies entirely outside the viewport.
234
+ */
235
+ export function computeSpanDelta(
236
+ anchor: Span,
237
+ hovered: Span,
238
+ viewStart: number,
239
+ viewEnd: number
240
+ ): SpanDelta | null {
241
+ const range = viewEnd - viewStart;
242
+ if (range <= 0) return null;
243
+
244
+ const anchorStart = getHighResInMs(anchor.startTime);
245
+ const hoveredStart = getHighResInMs(hovered.startTime);
246
+ const anchorIsEarlier = anchorStart <= hoveredStart;
247
+ const earlierStart = anchorIsEarlier ? anchorStart : hoveredStart;
248
+ const earlierEnd = getHighResInMs(
249
+ anchorIsEarlier ? anchor.endTime : hovered.endTime
250
+ );
251
+ const laterStart = anchorIsEarlier ? hoveredStart : anchorStart;
252
+
253
+ const originMs = earlierEnd <= laterStart ? earlierEnd : earlierStart;
254
+ const deltaMs = laterStart - originMs;
255
+
256
+ // Entirely outside the viewport. Compared in time-space (not clamped
257
+ // fractions) so a zero-delta point sitting exactly on a viewport edge —
258
+ // e.g. the root span selected at default zoom — still renders.
259
+ if (laterStart < viewStart || originMs > viewEnd) {
260
+ return null;
261
+ }
262
+
263
+ const originFrac = toViewportFrac(originMs, viewStart, range);
264
+ const laterFrac = toViewportFrac(laterStart, viewStart, range);
265
+
266
+ const hoveredEnd = getHighResInMs(hovered.endTime);
267
+
268
+ return {
269
+ deltaMs,
270
+ anchorFrac: anchorIsEarlier ? originFrac : laterFrac,
271
+ hoveredFrac: anchorIsEarlier ? laterFrac : originFrac,
272
+ anchorEdge: anchorIsEarlier && earlierEnd <= laterStart ? 'end' : 'start',
273
+ hoveredOffscreen:
274
+ hoveredStart > viewEnd ? 'right' : hoveredEnd < viewStart ? 'left' : null,
275
+ };
276
+ }
277
+
201
278
  // ---------------------------------------------------------------------------
202
279
  // Resource colors
203
280
  // ---------------------------------------------------------------------------
@@ -417,6 +417,8 @@ const attributeToDisplayFn: Record<
417
417
  receivedCount: (value: unknown) => String(value),
418
418
  lastReceivedAt: localMillisecondTimeOrNull,
419
419
  disposedAt: localMillisecondTimeOrNull,
420
+ // Internal resume plumbing — not surfaced in the UI
421
+ resumeContext: (_value: unknown) => null,
420
422
  // Event details
421
423
  eventType: (value: unknown) => String(value),
422
424
  correlationId: (value: unknown) => String(value),
@@ -638,6 +640,10 @@ const attributeToDisplayFn: Record<
638
640
  if (typeof value !== 'string' || value.length === 0) return null;
639
641
  return String(value);
640
642
  },
643
+ // Internal encryption plumbing: the run's X25519 public key, used by
644
+ // cross-run writers to seal payloads to this run. Not actionable for users
645
+ // and not secret — hidden rather than rendered as 44 opaque base64 chars.
646
+ encryptionPublicKey: (_value: unknown) => null,
641
647
  };
642
648
 
643
649
  const resolvableAttributes = [
package/src/index.ts CHANGED
@@ -1,7 +1,4 @@
1
- export type {
2
- HealthCheckEndpoint,
3
- HealthCheckResult,
4
- } from '@workflow/core/runtime';
1
+ export type { HealthCheckResult } from '@workflow/core/runtime';
5
2
  export {
6
3
  parseStepName,
7
4
  parseWorkflowName,
@@ -487,7 +487,7 @@ export async function hydrateResourceIOAsync<T>(
487
487
  resource: T,
488
488
  key?: Uint8Array
489
489
  ): Promise<T> {
490
- const { hydrateDataWithKey } = await import(
490
+ const { hydrateDataWithKey, deriveRunPayloadKeys } = await import(
491
491
  '@workflow/core/serialization-format'
492
492
  );
493
493
  // Payloads may be zstd-compressed (the Web DecompressionStream has no zstd);
@@ -497,11 +497,17 @@ export async function hydrateResourceIOAsync<T>(
497
497
  './zstd-browser-decoder.js'
498
498
  );
499
499
  ensureZstdDecoderRegistered();
500
- const cryptoKey = key
501
- ? await import('@workflow/core/encryption').then(({ importKey }) =>
502
- importKey(key)
503
- )
504
- : undefined;
500
+ // Resolve the *full* key capability, not just the symmetric key: a run's
501
+ // event log can contain sealed ('encp') payloads that another run wrote to
502
+ // it (a cross-deployment hook resumption, say), and opening those needs the
503
+ // run's X25519 scalar in addition to its AES key. Both are derived from the
504
+ // same 32 bytes the key-retrieval endpoint returns.
505
+ // Resolve the *full* key capability, not just the symmetric key: a run's
506
+ // event log can contain sealed ('encp') payloads that another run wrote to
507
+ // it (a cross-deployment hook resumption, say), and opening those needs the
508
+ // run's X25519 scalar in addition to its AES key. Both derive from the same
509
+ // 32 bytes the key-retrieval endpoint returns.
510
+ const cryptoKey = key ? await deriveRunPayloadKeys(key) : undefined;
505
511
  const revivers = getRevivers();
506
512
 
507
513
  async function hydrateField(value: unknown): Promise<unknown> {