@workflow/web-shared 4.1.0-beta.55 → 4.1.0-beta.56

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 (42) hide show
  1. package/dist/components/event-list-view.d.ts.map +1 -1
  2. package/dist/components/event-list-view.js +34 -2
  3. package/dist/components/event-list-view.js.map +1 -1
  4. package/dist/components/run-trace-view.d.ts +4 -1
  5. package/dist/components/run-trace-view.d.ts.map +1 -1
  6. package/dist/components/run-trace-view.js +2 -2
  7. package/dist/components/run-trace-view.js.map +1 -1
  8. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  9. package/dist/components/sidebar/attribute-panel.js +122 -45
  10. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  11. package/dist/components/sidebar/copyable-data-block.d.ts +4 -0
  12. package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -0
  13. package/dist/components/sidebar/copyable-data-block.js +33 -0
  14. package/dist/components/sidebar/copyable-data-block.js.map +1 -0
  15. package/dist/components/sidebar/detail-card.d.ts +7 -1
  16. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  17. package/dist/components/sidebar/detail-card.js +12 -3
  18. package/dist/components/sidebar/detail-card.js.map +1 -1
  19. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  20. package/dist/components/sidebar/entity-detail-panel.js +30 -16
  21. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  22. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  23. package/dist/components/sidebar/events-list.js +37 -7
  24. package/dist/components/sidebar/events-list.js.map +1 -1
  25. package/dist/components/ui/error-stack-block.d.ts +19 -0
  26. package/dist/components/ui/error-stack-block.d.ts.map +1 -0
  27. package/dist/components/ui/error-stack-block.js +39 -0
  28. package/dist/components/ui/error-stack-block.js.map +1 -0
  29. package/dist/components/workflow-trace-view.d.ts +7 -1
  30. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  31. package/dist/components/workflow-trace-view.js +137 -24
  32. package/dist/components/workflow-trace-view.js.map +1 -1
  33. package/package.json +3 -3
  34. package/src/components/event-list-view.tsx +53 -2
  35. package/src/components/run-trace-view.tsx +9 -0
  36. package/src/components/sidebar/attribute-panel.tsx +285 -127
  37. package/src/components/sidebar/copyable-data-block.tsx +51 -0
  38. package/src/components/sidebar/detail-card.tsx +28 -2
  39. package/src/components/sidebar/entity-detail-panel.tsx +138 -81
  40. package/src/components/sidebar/events-list.tsx +72 -21
  41. package/src/components/ui/error-stack-block.tsx +80 -0
  42. package/src/components/workflow-trace-view.tsx +208 -28
@@ -156,20 +156,14 @@ export function EntityDetailPanel({
156
156
  if (resource !== 'sleep' || !rawEvents) return false;
157
157
  const terminalStates = ['completed', 'failed', 'cancelled'];
158
158
  if (terminalStates.includes(run.status)) return false;
159
+ const hasWaitCreated = rawEvents.some(
160
+ (e) => e.eventType === 'wait_created'
161
+ );
162
+ if (!hasWaitCreated) return false;
159
163
  const hasWaitCompleted = rawEvents.some(
160
164
  (e) => e.eventType === 'wait_completed'
161
165
  );
162
- if (hasWaitCompleted) return false;
163
- const waitCreatedEvent = rawEvents.find(
164
- (e) => e.eventType === 'wait_created'
165
- );
166
- const eventData = (waitCreatedEvent as any)?.eventData as
167
- | { resumeAt?: string | Date }
168
- | undefined;
169
- const resumeAt = eventData?.resumeAt;
170
- if (!resumeAt) return false;
171
- const resumeAtDate = new Date(resumeAt);
172
- return resumeAtDate.getTime() > Date.now();
166
+ return !hasWaitCompleted;
173
167
  }, [resource, rawEvents, rawEventsLength, run.status]);
174
168
 
175
169
  // Check if this hook can be resolved
@@ -328,70 +322,146 @@ export function EntityDetailPanel({
328
322
  return undefined;
329
323
  }, [displayData, run.workflowName]);
330
324
 
325
+ const resourceLabel = resource.charAt(0).toUpperCase() + resource.slice(1);
326
+ const hasPendingActions =
327
+ (resource === 'sleep' && canWakeUp) ||
328
+ (resource === 'hook' && canResolveHook);
329
+ const runStateLabel = run.completedAt ? 'Completed' : 'Live';
330
+
331
331
  return (
332
- <div
333
- className={clsx('flex flex-col px-3')}
334
- style={{ paddingTop: 12, gap: 16 }}
335
- >
336
- {/* Wake up button for pending sleep calls */}
337
- {resource === 'sleep' && canWakeUp && (
338
- <div
339
- className="mb-3 pb-3"
340
- style={{ borderBottom: '1px solid var(--ds-gray-alpha-400)' }}
341
- >
342
- <button
343
- type="button"
344
- onClick={handleWakeUp}
345
- disabled={stoppingSleep}
346
- className={clsx(
347
- 'flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md w-full',
348
- 'disabled:opacity-50 disabled:cursor-not-allowed',
349
- 'transition-colors',
350
- stoppingSleep ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
351
- )}
352
- style={{
353
- background: 'var(--ds-amber-200)',
354
- color: 'var(--ds-amber-900)',
355
- }}
356
- >
357
- <Zap className="h-4 w-4" />
358
- {stoppingSleep ? 'Waking up...' : 'Wake up'}
359
- </button>
360
- <p className="mt-1.5 text-xs" style={{ color: 'var(--ds-gray-900)' }}>
361
- Interrupt this sleep call and wake up the run.
362
- </p>
332
+ <div className="flex h-full flex-col">
333
+ <div
334
+ className="border-b px-3 py-3"
335
+ style={{ borderColor: 'var(--ds-gray-200)' }}
336
+ >
337
+ <div className="flex items-start justify-between gap-2">
338
+ <div className="min-w-0">
339
+ <div className="flex items-center gap-2">
340
+ <span
341
+ className="inline-flex items-center rounded-full border px-2 py-0.5 text-[13px] font-medium"
342
+ style={{
343
+ borderColor: 'var(--ds-gray-300)',
344
+ color: 'var(--ds-gray-900)',
345
+ backgroundColor: 'var(--ds-background-100)',
346
+ }}
347
+ >
348
+ {resourceLabel}
349
+ </span>
350
+ <span
351
+ className="text-[13px]"
352
+ style={{
353
+ color: run.completedAt
354
+ ? 'var(--ds-gray-700)'
355
+ : 'var(--ds-green-800)',
356
+ }}
357
+ >
358
+ {runStateLabel}
359
+ </span>
360
+ </div>
361
+ <p
362
+ className="mt-1 truncate font-mono text-[13px]"
363
+ style={{ color: 'var(--ds-gray-700)' }}
364
+ title={resourceId}
365
+ >
366
+ {resourceId}
367
+ </p>
368
+ </div>
363
369
  </div>
364
- )}
370
+ </div>
365
371
 
366
- {/* Resolve hook button for pending hooks */}
367
- {resource === 'hook' && canResolveHook && (
368
- <div
369
- className="mb-3 pb-3"
370
- style={{ borderBottom: '1px solid var(--ds-gray-alpha-400)' }}
371
- >
372
- <button
373
- type="button"
374
- onClick={() => setShowResolveHookModal(true)}
375
- disabled={resolvingHook}
376
- className={clsx(
377
- 'flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md w-full',
378
- 'disabled:opacity-50 disabled:cursor-not-allowed',
379
- 'transition-colors',
380
- resolvingHook ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
381
- )}
372
+ <div className="flex-1 overflow-y-auto px-3 pt-3 pb-8">
373
+ {hasPendingActions && (
374
+ <div
375
+ className="mb-4 rounded-lg border p-2"
382
376
  style={{
383
- background: 'var(--ds-gray-1000)',
384
- color: 'var(--ds-background-100)',
377
+ borderColor: 'var(--ds-gray-300)',
378
+ backgroundColor: 'var(--ds-gray-100)',
385
379
  }}
386
380
  >
387
- <Send className="h-4 w-4" />
388
- Resolve Hook
389
- </button>
390
- <p className="mt-1.5 text-xs" style={{ color: 'var(--ds-gray-900)' }}>
391
- Send a JSON payload to resolve this hook.
392
- </p>
381
+ <p
382
+ className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide"
383
+ style={{ color: 'var(--ds-gray-700)' }}
384
+ >
385
+ Actions
386
+ </p>
387
+ <div className="flex flex-col gap-2">
388
+ {/* Wake up button for pending sleep calls */}
389
+ {resource === 'sleep' && canWakeUp && (
390
+ <button
391
+ type="button"
392
+ onClick={handleWakeUp}
393
+ disabled={stoppingSleep}
394
+ className={clsx(
395
+ 'flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium',
396
+ 'disabled:opacity-50 disabled:cursor-not-allowed transition-colors',
397
+ stoppingSleep
398
+ ? 'opacity-50 cursor-not-allowed'
399
+ : 'cursor-pointer'
400
+ )}
401
+ style={{
402
+ background: 'var(--ds-amber-200)',
403
+ color: 'var(--ds-amber-900)',
404
+ }}
405
+ >
406
+ <Zap className="h-4 w-4" />
407
+ {stoppingSleep ? 'Waking up...' : 'Wake Up Sleep'}
408
+ </button>
409
+ )}
410
+
411
+ {/* Resolve hook button for pending hooks */}
412
+ {resource === 'hook' && canResolveHook && (
413
+ <button
414
+ type="button"
415
+ onClick={() => setShowResolveHookModal(true)}
416
+ disabled={resolvingHook}
417
+ className={clsx(
418
+ 'flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium',
419
+ 'disabled:opacity-50 disabled:cursor-not-allowed transition-colors',
420
+ resolvingHook
421
+ ? 'opacity-50 cursor-not-allowed'
422
+ : 'cursor-pointer'
423
+ )}
424
+ style={{
425
+ background: 'var(--ds-gray-1000)',
426
+ color: 'var(--ds-background-100)',
427
+ }}
428
+ >
429
+ <Send className="h-4 w-4" />
430
+ Resolve Hook
431
+ </button>
432
+ )}
433
+ </div>
434
+ </div>
435
+ )}
436
+
437
+ <div className="space-y-4">
438
+ <section>
439
+ <h3
440
+ className="mb-2 text-[13px] font-medium uppercase tracking-wide"
441
+ style={{ color: 'var(--ds-gray-700)' }}
442
+ >
443
+ Details
444
+ </h3>
445
+ <AttributePanel
446
+ data={displayData}
447
+ moduleSpecifier={moduleSpecifier}
448
+ expiredAt={run.expiredAt}
449
+ isLoading={loading}
450
+ error={error ?? undefined}
451
+ onStreamClick={onStreamClick}
452
+ />
453
+ </section>
454
+
455
+ {resource !== 'run' && rawEvents && (
456
+ <section>
457
+ <EventsList
458
+ events={rawEvents}
459
+ onLoadEventData={onLoadEventData}
460
+ />
461
+ </section>
462
+ )}
393
463
  </div>
394
- )}
464
+ </div>
395
465
 
396
466
  {/* Resolve Hook Modal */}
397
467
  <ResolveHookModal
@@ -400,19 +470,6 @@ export function EntityDetailPanel({
400
470
  onSubmit={handleResolveHook}
401
471
  isSubmitting={resolvingHook}
402
472
  />
403
-
404
- {/* Content display */}
405
- <AttributePanel
406
- data={displayData}
407
- moduleSpecifier={moduleSpecifier}
408
- expiredAt={run.expiredAt}
409
- isLoading={loading}
410
- error={error ?? undefined}
411
- onStreamClick={onStreamClick}
412
- />
413
- {resource !== 'run' && rawEvents && (
414
- <EventsList events={rawEvents} onLoadEventData={onLoadEventData} />
415
- )}
416
473
  </div>
417
474
  );
418
475
  }
@@ -2,10 +2,20 @@
2
2
 
3
3
  import type { Event } from '@workflow/world';
4
4
  import { useCallback, useMemo, useState } from 'react';
5
- import { DataInspector } from '../ui/data-inspector';
5
+ import {
6
+ ErrorStackBlock,
7
+ isStructuredErrorWithStack,
8
+ } from '../ui/error-stack-block';
9
+ import { Skeleton } from '../ui/skeleton';
6
10
  import { localMillisecondTime } from './attribute-panel';
11
+ import { CopyableDataBlock } from './copyable-data-block';
7
12
  import { DetailCard } from './detail-card';
8
13
 
14
+ /**
15
+ * Event types whose eventData contains an error field with a StructuredError.
16
+ */
17
+ const ERROR_EVENT_TYPES = new Set(['step_failed', 'step_retrying']);
18
+
9
19
  /**
10
20
  * Event types that carry user-serialized data in their eventData field.
11
21
  */
@@ -69,6 +79,7 @@ function EventItem({
69
79
 
70
80
  return (
71
81
  <DetailCard
82
+ summaryClassName="text-base py-2"
72
83
  summary={
73
84
  <>
74
85
  <span
@@ -100,36 +111,35 @@ function EventItem({
100
111
  }}
101
112
  >
102
113
  <div
103
- className="flex items-center justify-between px-2.5 py-1.5"
114
+ className="flex min-h-[32px] items-center justify-between gap-4 px-2.5 py-1.5"
104
115
  style={{ borderColor: 'var(--ds-gray-300)' }}
105
116
  >
106
- <span
107
- className="text-[11px] font-medium"
108
- style={{ color: 'var(--ds-gray-700)' }}
109
- >
117
+ <span className="text-[14px]" style={{ color: 'var(--ds-gray-700)' }}>
110
118
  eventId
111
119
  </span>
112
120
  <span
113
- className="text-[11px] font-mono"
121
+ className="max-w-[70%] truncate text-right text-[13px] font-mono"
114
122
  style={{ color: 'var(--ds-gray-1000)' }}
123
+ title={event.eventId}
115
124
  >
116
125
  {event.eventId}
117
126
  </span>
118
127
  </div>
119
128
  {event.correlationId && (
120
129
  <div
121
- className="flex items-center justify-between px-2.5 py-1.5"
130
+ className="flex min-h-[32px] items-center justify-between gap-4 px-2.5 py-1.5"
122
131
  style={{ borderColor: 'var(--ds-gray-300)' }}
123
132
  >
124
133
  <span
125
- className="text-[11px] font-medium"
134
+ className="text-[14px]"
126
135
  style={{ color: 'var(--ds-gray-700)' }}
127
136
  >
128
137
  correlationId
129
138
  </span>
130
139
  <span
131
- className="text-[11px] font-mono"
140
+ className="max-w-[70%] truncate text-right text-[13px] font-mono"
132
141
  style={{ color: 'var(--ds-gray-1000)' }}
142
+ title={event.correlationId}
133
143
  >
134
144
  {event.correlationId}
135
145
  </span>
@@ -140,20 +150,21 @@ function EventItem({
140
150
  {/* Loading state */}
141
151
  {isLoading && (
142
152
  <div
143
- className="mt-2 text-xs rounded-md border p-2"
153
+ className="mt-2 rounded-md border p-3"
144
154
  style={{
145
155
  borderColor: 'var(--ds-gray-300)',
146
- color: 'var(--ds-gray-600)',
147
156
  }}
148
157
  >
149
- Loading event data...
158
+ <Skeleton className="h-4 w-[35%]" />
159
+ <Skeleton className="mt-2 h-4 w-[90%]" />
160
+ <Skeleton className="mt-2 h-4 w-[75%]" />
150
161
  </div>
151
162
  )}
152
163
 
153
164
  {/* Error state */}
154
165
  {loadError && (
155
166
  <div
156
- className="mt-2 text-xs rounded-md border p-2"
167
+ className="mt-2 rounded-md border p-2 text-sm"
157
168
  style={{
158
169
  borderColor: 'var(--ds-red-300)',
159
170
  color: 'var(--ds-red-700)',
@@ -165,17 +176,51 @@ function EventItem({
165
176
 
166
177
  {/* Event data */}
167
178
  {displayData != null && (
168
- <div
169
- className="mt-2 overflow-x-auto rounded-md border p-3"
170
- style={{ borderColor: 'var(--ds-gray-300)' }}
171
- >
172
- <DataInspector data={displayData} />
179
+ <div className="mt-2">
180
+ <EventDataBlock eventType={event.eventType} data={displayData} />
173
181
  </div>
174
182
  )}
175
183
  </DetailCard>
176
184
  );
177
185
  }
178
186
 
187
+ /**
188
+ * Renders event data, using ErrorStackBlock for error events that contain
189
+ * a structured error with a stack trace, and CopyableDataBlock otherwise.
190
+ */
191
+ function EventDataBlock({
192
+ eventType,
193
+ data,
194
+ }: {
195
+ eventType: string;
196
+ data: unknown;
197
+ }) {
198
+ // For error events (step_failed, step_retrying), the eventData has the shape
199
+ // { error: StructuredError, stack?: string, ... }. Check both the top-level
200
+ // value and the nested `error` field for a stack trace.
201
+ if (
202
+ ERROR_EVENT_TYPES.has(eventType) &&
203
+ data != null &&
204
+ typeof data === 'object'
205
+ ) {
206
+ const record = data as Record<string, unknown>;
207
+
208
+ // Check the nested `error` field first (the StructuredError)
209
+ if (isStructuredErrorWithStack(record.error)) {
210
+ return <ErrorStackBlock value={record.error} />;
211
+ }
212
+
213
+ // Some error formats put the stack at the top level of eventData
214
+ if (isStructuredErrorWithStack(record)) {
215
+ return <ErrorStackBlock value={record} />;
216
+ }
217
+ }
218
+
219
+ // For non-error events or errors without a stack, fall back to the
220
+ // generic JSON viewer.
221
+ return <CopyableDataBlock data={data} />;
222
+ }
223
+
179
224
  export function EventsList({
180
225
  events,
181
226
  isLoading = false,
@@ -208,12 +253,18 @@ export function EventsList({
208
253
  >
209
254
  Events {!isLoading && `(${sortedEvents.length})`}
210
255
  </h3>
211
- {isLoading ? <div>Loading events...</div> : null}
256
+ {isLoading ? (
257
+ <div className="flex flex-col gap-3">
258
+ <Skeleton className="h-[48px] w-full rounded-lg border" />
259
+ <Skeleton className="h-[48px] w-full rounded-lg border" />
260
+ <Skeleton className="h-[48px] w-full rounded-lg border" />
261
+ </div>
262
+ ) : null}
212
263
  {!isLoading && !error && sortedEvents.length === 0 && (
213
264
  <div className="text-sm">No events found</div>
214
265
  )}
215
266
  {sortedEvents.length > 0 && !error ? (
216
- <div className="flex flex-col gap-2">
267
+ <div className="flex flex-col gap-4">
217
268
  {sortedEvents.map((event) => (
218
269
  <EventItem
219
270
  key={event.eventId}
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+
3
+ import { Copy } from 'lucide-react';
4
+ import { toast } from 'sonner';
5
+
6
+ /**
7
+ * Check whether `value` looks like a structured error object with a `stack`
8
+ * field that we can render as pre-formatted text.
9
+ */
10
+ export function isStructuredErrorWithStack(
11
+ value: unknown
12
+ ): value is Record<string, unknown> & { stack: string } {
13
+ return (
14
+ value != null &&
15
+ typeof value === 'object' &&
16
+ 'stack' in value &&
17
+ typeof (value as Record<string, unknown>).stack === 'string'
18
+ );
19
+ }
20
+
21
+ /**
22
+ * Renders an error with a `stack` field as readable pre-formatted text,
23
+ * styled to match the CopyableDataBlock component. The error message is
24
+ * displayed at the top with a visual separator from the stack trace.
25
+ * The entire block is copyable via a copy button.
26
+ */
27
+ export function ErrorStackBlock({
28
+ value,
29
+ }: {
30
+ value: Record<string, unknown> & { stack: string };
31
+ }) {
32
+ const stack = value.stack;
33
+ const message = typeof value.message === 'string' ? value.message : undefined;
34
+ const copyText = message ? `${message}\n\n${stack}` : stack;
35
+
36
+ return (
37
+ <div
38
+ className="relative overflow-x-auto rounded-md border p-3 pt-9"
39
+ style={{ borderColor: 'var(--ds-gray-300)' }}
40
+ >
41
+ <button
42
+ type="button"
43
+ aria-label="Copy error"
44
+ title="Copy"
45
+ className="!absolute !right-2 !top-2 !flex !h-6 !w-6 !items-center !justify-center !rounded-md !border !bg-[var(--ds-background-100)] !text-[var(--ds-gray-800)] transition-transform transition-colors duration-100 hover:!bg-[var(--ds-gray-alpha-200)] active:!scale-95 active:!bg-[var(--ds-gray-alpha-300)]"
46
+ style={{ borderColor: 'var(--ds-gray-300)' }}
47
+ onClick={() => {
48
+ navigator.clipboard
49
+ .writeText(copyText)
50
+ .then(() => {
51
+ toast.success('Copied to clipboard');
52
+ })
53
+ .catch(() => {
54
+ toast.error('Failed to copy');
55
+ });
56
+ }}
57
+ >
58
+ <Copy size={12} />
59
+ </button>
60
+
61
+ {message && (
62
+ <p
63
+ className="pb-2 mb-2 text-xs font-semibold font-mono"
64
+ style={{
65
+ color: 'var(--ds-red-900)',
66
+ borderBottom: '1px solid var(--ds-gray-300)',
67
+ }}
68
+ >
69
+ {message}
70
+ </p>
71
+ )}
72
+ <pre
73
+ className="text-xs font-mono whitespace-pre-wrap break-words overflow-auto m-0"
74
+ style={{ color: 'var(--ds-gray-1000)' }}
75
+ >
76
+ {stack}
77
+ </pre>
78
+ </div>
79
+ );
80
+ }