@workflow/web-shared 5.0.0-beta.2 → 5.0.0-beta.3

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 (127) hide show
  1. package/dist/components/index.d.ts +2 -0
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/index.js +2 -0
  4. package/dist/components/index.js.map +1 -1
  5. package/dist/components/new-trace-viewer/components/copy-button.d.ts +9 -0
  6. package/dist/components/new-trace-viewer/components/copy-button.d.ts.map +1 -0
  7. package/dist/components/new-trace-viewer/components/copy-button.js +27 -0
  8. package/dist/components/new-trace-viewer/components/copy-button.js.map +1 -0
  9. package/dist/components/new-trace-viewer/components/event-list.d.ts +8 -0
  10. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -0
  11. package/dist/components/new-trace-viewer/components/event-list.js +35 -0
  12. package/dist/components/new-trace-viewer/components/event-list.js.map +1 -0
  13. package/dist/components/new-trace-viewer/components/split-pane.d.ts +14 -0
  14. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -0
  15. package/dist/components/new-trace-viewer/components/split-pane.js +100 -0
  16. package/dist/components/new-trace-viewer/components/split-pane.js.map +1 -0
  17. package/dist/components/new-trace-viewer/components/timeline.d.ts +32 -0
  18. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -0
  19. package/dist/components/new-trace-viewer/components/timeline.js +134 -0
  20. package/dist/components/new-trace-viewer/components/timeline.js.map +1 -0
  21. package/dist/components/new-trace-viewer/context.d.ts +15 -0
  22. package/dist/components/new-trace-viewer/context.d.ts.map +1 -0
  23. package/dist/components/new-trace-viewer/context.js +49 -0
  24. package/dist/components/new-trace-viewer/context.js.map +1 -0
  25. package/dist/components/new-trace-viewer/detail-panel.d.ts +10 -0
  26. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -0
  27. package/dist/components/new-trace-viewer/detail-panel.js +11 -0
  28. package/dist/components/new-trace-viewer/detail-panel.js.map +1 -0
  29. package/dist/components/new-trace-viewer/icons.d.ts +6 -0
  30. package/dist/components/new-trace-viewer/icons.d.ts.map +1 -0
  31. package/dist/components/new-trace-viewer/icons.js +15 -0
  32. package/dist/components/new-trace-viewer/icons.js.map +1 -0
  33. package/dist/components/new-trace-viewer/trace-viewer.d.ts +8 -0
  34. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -0
  35. package/dist/components/new-trace-viewer/trace-viewer.js +329 -0
  36. package/dist/components/new-trace-viewer/trace-viewer.js.map +1 -0
  37. package/dist/components/new-trace-viewer/types.d.ts +1 -0
  38. package/dist/components/new-trace-viewer/types.d.ts.map +1 -0
  39. package/dist/components/new-trace-viewer/types.js +2 -0
  40. package/dist/components/new-trace-viewer/types.js.map +1 -0
  41. package/dist/components/new-trace-viewer/util/constants.d.ts +1 -0
  42. package/dist/components/new-trace-viewer/util/constants.d.ts.map +1 -0
  43. package/dist/components/new-trace-viewer/util/constants.js +2 -0
  44. package/dist/components/new-trace-viewer/util/constants.js.map +1 -0
  45. package/dist/components/new-trace-viewer/utils.d.ts +47 -0
  46. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -0
  47. package/dist/components/new-trace-viewer/utils.js +358 -0
  48. package/dist/components/new-trace-viewer/utils.js.map +1 -0
  49. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  50. package/dist/components/sidebar/attribute-panel.js +42 -23
  51. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  52. package/dist/components/sidebar/copyable-data-block.js +1 -1
  53. package/dist/components/sidebar/copyable-data-block.js.map +1 -1
  54. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  55. package/dist/components/sidebar/detail-card.js +1 -1
  56. package/dist/components/sidebar/detail-card.js.map +1 -1
  57. package/dist/components/sidebar/entity-detail-panel.d.ts +1 -3
  58. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  59. package/dist/components/sidebar/entity-detail-panel.js +3 -25
  60. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  61. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  62. package/dist/components/sidebar/events-list.js +2 -2
  63. package/dist/components/sidebar/events-list.js.map +1 -1
  64. package/dist/components/sidebar/sidebar-data-context.d.ts +29 -0
  65. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -0
  66. package/dist/components/sidebar/sidebar-data-context.js +19 -0
  67. package/dist/components/sidebar/sidebar-data-context.js.map +1 -0
  68. package/dist/components/stream-viewer.d.ts +1 -1
  69. package/dist/components/stream-viewer.d.ts.map +1 -1
  70. package/dist/components/stream-viewer.js +2 -26
  71. package/dist/components/stream-viewer.js.map +1 -1
  72. package/dist/components/trace-viewer-new.d.ts +10 -0
  73. package/dist/components/trace-viewer-new.d.ts.map +1 -0
  74. package/dist/components/trace-viewer-new.js +23 -0
  75. package/dist/components/trace-viewer-new.js.map +1 -0
  76. package/dist/components/ui/data-inspector.d.ts +21 -0
  77. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  78. package/dist/components/ui/data-inspector.js +99 -4
  79. package/dist/components/ui/data-inspector.js.map +1 -1
  80. package/dist/components/ui/inspector-theme.d.ts +9 -10
  81. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  82. package/dist/components/ui/inspector-theme.js +38 -71
  83. package/dist/components/ui/inspector-theme.js.map +1 -1
  84. package/dist/components/workflow-trace-view.d.ts +1 -3
  85. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  86. package/dist/components/workflow-trace-view.js +4 -4
  87. package/dist/components/workflow-trace-view.js.map +1 -1
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/lib/stream-display.d.ts +18 -0
  92. package/dist/lib/stream-display.d.ts.map +1 -0
  93. package/dist/lib/stream-display.js +30 -0
  94. package/dist/lib/stream-display.js.map +1 -0
  95. package/dist/lib/utils.d.ts +7 -9
  96. package/dist/lib/utils.d.ts.map +1 -1
  97. package/dist/lib/utils.js +17 -22
  98. package/dist/lib/utils.js.map +1 -1
  99. package/dist/styles.css +470 -1
  100. package/package.json +3 -2
  101. package/src/components/index.ts +5 -0
  102. package/src/components/new-trace-viewer/components/copy-button.tsx +68 -0
  103. package/src/components/new-trace-viewer/components/event-list.tsx +109 -0
  104. package/src/components/new-trace-viewer/components/split-pane.tsx +186 -0
  105. package/src/components/new-trace-viewer/components/timeline.tsx +378 -0
  106. package/src/components/new-trace-viewer/context.tsx +94 -0
  107. package/src/components/new-trace-viewer/detail-panel.tsx +57 -0
  108. package/src/components/new-trace-viewer/icons.tsx +79 -0
  109. package/src/components/new-trace-viewer/trace-viewer.tsx +585 -0
  110. package/src/components/new-trace-viewer/types.ts +0 -0
  111. package/src/components/new-trace-viewer/util/constants.ts +0 -0
  112. package/src/components/new-trace-viewer/utils.ts +524 -0
  113. package/src/components/sidebar/attribute-panel.tsx +43 -32
  114. package/src/components/sidebar/copyable-data-block.tsx +1 -1
  115. package/src/components/sidebar/detail-card.tsx +1 -19
  116. package/src/components/sidebar/entity-detail-panel.tsx +0 -71
  117. package/src/components/sidebar/events-list.tsx +9 -6
  118. package/src/components/sidebar/sidebar-data-context.tsx +62 -0
  119. package/src/components/stream-viewer.tsx +18 -40
  120. package/src/components/trace-viewer-new.tsx +49 -0
  121. package/src/components/ui/data-inspector.tsx +235 -4
  122. package/src/components/ui/inspector-theme.ts +39 -81
  123. package/src/components/workflow-trace-view.tsx +2 -6
  124. package/src/index.ts +1 -0
  125. package/src/lib/stream-display.ts +48 -0
  126. package/src/lib/utils.ts +18 -23
  127. package/src/styles.css +470 -1
@@ -1,17 +1,16 @@
1
1
  /**
2
2
  * Shared theme configuration for react-inspector's ObjectInspector.
3
3
  *
4
- * Color choices are inspired by Node.js `util.inspect()` ANSI stylizing:
5
- * - property names: unstyled (default foreground dark text on light, light text on dark)
6
- * - strings / symbols: green
7
- * - numbers / booleans / bigint: yellow
8
- * - null: bold (foreground)
9
- * - undefined: grey
10
- * - regexp: red
11
- * - functions (special): cyan
12
- * - date: magenta
4
+ * Colors follow Geist's Shiki JSON palette so the inspector reads the same
5
+ * as highlighted code blocks across the product:
6
+ * - property names / punctuation: --ds-gray-1000 (default foreground)
7
+ * - strings / numbers / booleans: --ds-green-900
8
+ * - null / undefined: --ds-gray-900 (muted)
9
+ * - regexp / function: --ds-purple-900
10
+ * - date: --ds-pink-900
13
11
  *
14
- * See: https://github.com/nodejs/node/blob/main/lib/internal/util/inspect.js
12
+ * Because the `--ds-*` tokens adapt to theme automatically, the light and
13
+ * dark objects are intentionally identical.
15
14
  */
16
15
 
17
16
  // ---------------------------------------------------------------------------
@@ -25,11 +24,11 @@ export interface InspectorThemeExtended {
25
24
  }
26
25
 
27
26
  export const inspectorThemeExtendedLight: InspectorThemeExtended = {
28
- OBJECT_VALUE_DATE_COLOR: '#a21caf', // fuchsia-700
27
+ OBJECT_VALUE_DATE_COLOR: 'var(--ds-pink-900)',
29
28
  };
30
29
 
31
30
  export const inspectorThemeExtendedDark: InspectorThemeExtended = {
32
- OBJECT_VALUE_DATE_COLOR: '#e879f9', // fuchsia-400
31
+ OBJECT_VALUE_DATE_COLOR: 'var(--ds-pink-900)',
33
32
  };
34
33
 
35
34
  // ---------------------------------------------------------------------------
@@ -57,96 +56,55 @@ const shared = {
57
56
  // Light theme
58
57
  // ---------------------------------------------------------------------------
59
58
 
60
- export const inspectorThemeLight = {
59
+ const geistTheme = {
61
60
  ...shared,
62
61
 
63
62
  // Base text
64
63
  BASE_COLOR: 'var(--ds-gray-1000)',
65
64
 
66
- // Property names — unstyled, same as base foreground (Node: no style)
67
- OBJECT_NAME_COLOR: 'var(--ds-gray-900)',
65
+ // Property names — default foreground (matches JSON key color in Geist Shiki)
66
+ OBJECT_NAME_COLOR: 'var(--ds-gray-1000)',
68
67
 
69
- // Strings & symbols — green (Node: 'green')
70
- OBJECT_VALUE_STRING_COLOR: '#16a34a', // green-600
71
- OBJECT_VALUE_SYMBOL_COLOR: '#16a34a',
68
+ // Strings & symbols — green
69
+ OBJECT_VALUE_STRING_COLOR: 'var(--ds-green-900)',
70
+ OBJECT_VALUE_SYMBOL_COLOR: 'var(--ds-green-900)',
72
71
 
73
- // Numbers & booleans — yellow/amber (Node: 'yellow')
74
- OBJECT_VALUE_NUMBER_COLOR: '#b45309', // amber-700 (readable on white)
75
- OBJECT_VALUE_BOOLEAN_COLOR: '#b45309',
72
+ // Numbers & booleans — green (Geist JSON tokens)
73
+ OBJECT_VALUE_NUMBER_COLOR: 'var(--ds-green-900)',
74
+ OBJECT_VALUE_BOOLEAN_COLOR: 'var(--ds-green-900)',
76
75
 
77
- // null — bold foreground (Node: 'bold')
76
+ // null — muted foreground
78
77
  OBJECT_VALUE_NULL_COLOR: 'var(--ds-gray-900)',
79
78
 
80
- // undefined — grey (Node: 'grey')
81
- OBJECT_VALUE_UNDEFINED_COLOR: 'var(--ds-gray-500)',
79
+ // undefined — muted foreground
80
+ OBJECT_VALUE_UNDEFINED_COLOR: 'var(--ds-gray-900)',
82
81
 
83
- // RegExp — red (Node regexp base uses green/red/yellow palette)
84
- OBJECT_VALUE_REGEXP_COLOR: '#dc2626', // red-600
82
+ // RegExp — purple
83
+ OBJECT_VALUE_REGEXP_COLOR: 'var(--ds-purple-900)',
85
84
 
86
- // Functions — cyan (Node: 'special' → 'cyan')
87
- OBJECT_VALUE_FUNCTION_PREFIX_COLOR: '#0891b2', // cyan-600
85
+ // Functions — purple
86
+ OBJECT_VALUE_FUNCTION_PREFIX_COLOR: 'var(--ds-purple-900)',
88
87
 
89
- // HTML (less relevant for data inspection, but reasonable defaults)
90
- HTML_TAG_COLOR: 'var(--ds-gray-500)',
91
- HTML_TAGNAME_COLOR: '#0891b2',
92
- HTML_ATTRIBUTE_NAME_COLOR: '#b45309',
93
- HTML_ATTRIBUTE_VALUE_COLOR: '#16a34a',
94
- HTML_COMMENT_COLOR: 'var(--ds-gray-400)',
95
- HTML_DOCTYPE_COLOR: 'var(--ds-gray-400)',
88
+ // HTML (rarely used here, kept consistent with the palette)
89
+ HTML_TAG_COLOR: 'var(--ds-gray-900)',
90
+ HTML_TAGNAME_COLOR: 'var(--ds-blue-900)',
91
+ HTML_ATTRIBUTE_NAME_COLOR: 'var(--ds-amber-900)',
92
+ HTML_ATTRIBUTE_VALUE_COLOR: 'var(--ds-green-900)',
93
+ HTML_COMMENT_COLOR: 'var(--ds-gray-700)',
94
+ HTML_DOCTYPE_COLOR: 'var(--ds-gray-700)',
96
95
 
97
96
  // Structural
98
- ARROW_COLOR: 'var(--ds-gray-500)',
97
+ ARROW_COLOR: 'var(--ds-gray-700)',
99
98
  TABLE_BORDER_COLOR: 'var(--ds-gray-300)',
100
99
  TABLE_TH_BACKGROUND_COLOR: 'var(--ds-gray-100)',
101
100
  TABLE_TH_HOVER_COLOR: 'var(--ds-gray-200)',
102
- TABLE_SORT_ICON_COLOR: 'var(--ds-gray-500)',
101
+ TABLE_SORT_ICON_COLOR: 'var(--ds-gray-700)',
103
102
  };
104
103
 
104
+ export const inspectorThemeLight = geistTheme;
105
+
105
106
  // ---------------------------------------------------------------------------
106
107
  // Dark theme
107
108
  // ---------------------------------------------------------------------------
108
109
 
109
- export const inspectorThemeDark = {
110
- ...shared,
111
-
112
- // Base text
113
- BASE_COLOR: 'var(--ds-gray-1000)',
114
-
115
- // Property names — white/light foreground (Node: unstyled = white in dark terminal)
116
- OBJECT_NAME_COLOR: 'var(--ds-gray-900)',
117
-
118
- // Strings & symbols — green (Node: 'green')
119
- OBJECT_VALUE_STRING_COLOR: '#4ade80', // green-400
120
- OBJECT_VALUE_SYMBOL_COLOR: '#4ade80',
121
-
122
- // Numbers & booleans — yellow (Node: 'yellow')
123
- OBJECT_VALUE_NUMBER_COLOR: '#facc15', // yellow-400
124
- OBJECT_VALUE_BOOLEAN_COLOR: '#facc15',
125
-
126
- // null — bold foreground / white (Node: 'bold')
127
- OBJECT_VALUE_NULL_COLOR: 'var(--ds-gray-1000)',
128
-
129
- // undefined — grey (Node: 'grey')
130
- OBJECT_VALUE_UNDEFINED_COLOR: 'var(--ds-gray-500)',
131
-
132
- // RegExp — red (Node regexp palette)
133
- OBJECT_VALUE_REGEXP_COLOR: '#f87171', // red-400
134
-
135
- // Functions — cyan (Node: 'special' → 'cyan')
136
- OBJECT_VALUE_FUNCTION_PREFIX_COLOR: '#22d3ee', // cyan-400
137
-
138
- // HTML
139
- HTML_TAG_COLOR: 'var(--ds-gray-500)',
140
- HTML_TAGNAME_COLOR: '#22d3ee',
141
- HTML_ATTRIBUTE_NAME_COLOR: '#facc15',
142
- HTML_ATTRIBUTE_VALUE_COLOR: '#4ade80',
143
- HTML_COMMENT_COLOR: 'var(--ds-gray-500)',
144
- HTML_DOCTYPE_COLOR: 'var(--ds-gray-500)',
145
-
146
- // Structural
147
- ARROW_COLOR: 'var(--ds-gray-500)',
148
- TABLE_BORDER_COLOR: 'var(--ds-gray-300)',
149
- TABLE_TH_BACKGROUND_COLOR: 'var(--ds-gray-100)',
150
- TABLE_TH_HOVER_COLOR: 'var(--ds-gray-200)',
151
- TABLE_SORT_ICON_COLOR: 'var(--ds-gray-500)',
152
- };
110
+ export const inspectorThemeDark = geistTheme;
@@ -17,6 +17,7 @@ import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
17
17
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
18
18
  import { createPortal } from 'react-dom';
19
19
  import { useToast } from '../lib/toast';
20
+ import { buildTrace, type TraceWithMeta } from '../lib/trace-builder';
20
21
  import { ErrorBoundary } from './error-boundary';
21
22
  import {
22
23
  EntityDetailPanel,
@@ -36,7 +37,6 @@ import {
36
37
  getCustomSpanClassName,
37
38
  getCustomSpanEventClassName,
38
39
  } from './workflow-traces/trace-colors';
39
- import { buildTrace, type TraceWithMeta } from '../lib/trace-builder';
40
40
 
41
41
  /**
42
42
  * While a run is live, continuously grow root.duration and rescale so the
@@ -776,7 +776,6 @@ export const WorkflowTraceViewer = ({
776
776
  encryptionKey,
777
777
  onDecrypt,
778
778
  isDecrypting = false,
779
- hasEncryptedData = false,
780
779
  }: {
781
780
  run: WorkflowRun;
782
781
  events: Event[];
@@ -819,8 +818,6 @@ export const WorkflowTraceViewer = ({
819
818
  onDecrypt?: () => void;
820
819
  /** Whether the encryption key is currently being fetched */
821
820
  isDecrypting?: boolean;
822
- /** Run-level hint: the run contains encrypted data (from probe). */
823
- hasEncryptedData?: boolean;
824
821
  }) => {
825
822
  const toast = useToast();
826
823
  const [selectedSpan, setSelectedSpan] = useState<SelectedSpanInfo | null>(
@@ -977,7 +974,7 @@ export const WorkflowTraceViewer = ({
977
974
  }
978
975
 
979
976
  return (
980
- <div className="relative w-full h-full flex">
977
+ <div className="relative w-full h-full flex flex-col">
981
978
  {/* Timeline (takes remaining space) */}
982
979
  <div className="flex-1 min-w-0 relative">
983
980
  <TraceViewerContextProvider
@@ -1164,7 +1161,6 @@ export const WorkflowTraceViewer = ({
1164
1161
  onDecrypt={onDecrypt}
1165
1162
  isDecrypting={isDecrypting}
1166
1163
  selectedSpan={selectedSpan}
1167
- hasEncryptedData={hasEncryptedData}
1168
1164
  />
1169
1165
  </ErrorBoundary>
1170
1166
  </div>
package/src/index.ts CHANGED
@@ -50,6 +50,7 @@ export {
50
50
  STREAM_REF_TYPE,
51
51
  truncateId,
52
52
  } from './lib/hydration';
53
+ export type { DecodedStreamChunkSource } from './lib/stream-display';
53
54
  export type { ToastAdapter } from './lib/toast';
54
55
  export { ToastProvider, useToast } from './lib/toast';
55
56
  export type { StreamStep } from './lib/utils';
@@ -0,0 +1,48 @@
1
+ export interface DecodedStreamChunkSource {
2
+ type: string;
3
+ encoding: 'utf-8';
4
+ rawSummary: string;
5
+ }
6
+
7
+ export interface FormattedStreamChunkDisplay {
8
+ text: string;
9
+ decodedFrom?: DecodedStreamChunkSource;
10
+ }
11
+
12
+ export function summarizeArrayBufferView(value: ArrayBufferView): string {
13
+ const ta = value as unknown as {
14
+ length: number;
15
+ constructor: { name: string };
16
+ } & ArrayLike<number>;
17
+ const name = ta.constructor.name;
18
+ const preview = Array.from(
19
+ { length: Math.min(ta.length, 8) },
20
+ (_, i) => ta[i]
21
+ );
22
+ const suffix = ta.length > 8 ? ', …' : '';
23
+ return `${name}(${ta.length}) [${preview.join(', ')}${suffix}]`;
24
+ }
25
+
26
+ /**
27
+ * Decode a typed array as UTF-8 text when valid, otherwise return a compact
28
+ * raw-byte summary. Used by `DataInspector`'s `collapseRefs` pipeline so
29
+ * hydrated `Uint8Array` chunks (e.g. AI SDK text deltas) render as readable
30
+ * text while still exposing the underlying byte layout.
31
+ */
32
+ export function formatArrayBufferViewForDisplay(
33
+ value: ArrayBufferView
34
+ ): FormattedStreamChunkDisplay {
35
+ try {
36
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(value);
37
+ return {
38
+ text,
39
+ decodedFrom: {
40
+ type: value.constructor.name,
41
+ encoding: 'utf-8',
42
+ rawSummary: summarizeArrayBufferView(value),
43
+ },
44
+ };
45
+ } catch {
46
+ return { text: summarizeArrayBufferView(value) };
47
+ }
48
+ }
package/src/lib/utils.ts CHANGED
@@ -7,7 +7,12 @@ export function cn(...inputs: ClassValue[]) {
7
7
  return twMerge(clsx(inputs));
8
8
  }
9
9
 
10
- const durationFormatter = new Intl.NumberFormat(undefined, {
10
+ // const compactFormatter = new Intl.NumberFormat(undefined, {
11
+ // maximumFractionDigits: 2,
12
+ // });
13
+
14
+ const secondsFormatter = new Intl.NumberFormat(undefined, {
15
+ minimumFractionDigits: 2,
11
16
  maximumFractionDigits: 2,
12
17
  });
13
18
 
@@ -20,33 +25,25 @@ const MS_IN_DAY = 24 * MS_IN_HOUR;
20
25
  * Formats a duration in milliseconds to a human-readable string.
21
26
  *
22
27
  * @param ms - Duration in milliseconds
23
- * @param compact - If true, returns a single-unit format (e.g., "45s", "2.5m").
24
- * If false (default), returns multi-part format (e.g., "1h 30m", "2d 5h").
28
+ * @param compact - If true, returns a single-unit format (e.g., "0.38s", "2.5m").
29
+ * If false (default), returns multi-part format (e.g., "1m 13.21s", "2d 5h 3m 12.00s").
25
30
  *
26
- * Compact format:
27
- * - < 1s: shows milliseconds (e.g., "500ms")
28
- * - < 1m: shows seconds (e.g., "45s")
29
- * - < 1h: shows minutes (e.g., "45m")
31
+ * Compact format (timeline markers):
32
+ * - < 1m: shows seconds (e.g., "0.38s", "45s")
33
+ * - < 1h: shows minutes (e.g., "2.5m")
30
34
  * - >= 1h: shows hours (e.g., "2.5h")
31
35
  *
32
36
  * Full format:
33
- * - < 1s: shows milliseconds (e.g., "500ms")
34
- * - < 1m: shows seconds (e.g., "45.5s")
35
- * - >= 1m: shows human-readable format (e.g., "1h 30m", "2d 5h")
37
+ * - < 1m: shows seconds (e.g., "0.38s", "2.71s")
38
+ * - >= 1m: shows decomposed format with fractional seconds (e.g., "1m 13.21s", "2m 13.04s")
36
39
  */
37
40
  export function formatDuration(ms: number, compact = false): string {
38
41
  if (ms === 0) {
39
- return '0';
40
- }
41
-
42
- // For durations less than 1 second, show milliseconds
43
- if (ms < MS_IN_SECOND) {
44
- return `${durationFormatter.format(ms)}ms`;
42
+ return '0s';
45
43
  }
46
44
 
47
- // For durations less than 1 minute, show seconds
48
45
  if (ms < MS_IN_MINUTE) {
49
- return `${durationFormatter.format(ms / MS_IN_SECOND)}s`;
46
+ return `${secondsFormatter.format(ms / MS_IN_SECOND)}s`;
50
47
  }
51
48
 
52
49
  // Compact format: multi-unit without decimals (e.g. "8m 20s", "2h 30m")
@@ -61,11 +58,11 @@ export function formatDuration(ms: number, compact = false): string {
61
58
  return m > 0 ? `${h}h ${m}m` : `${h}h`;
62
59
  }
63
60
 
64
- // Full format: human-readable multi-part
61
+ // Full format: decompose into larger units + fractional seconds
65
62
  const days = Math.floor(ms / MS_IN_DAY);
66
63
  const hours = Math.floor((ms % MS_IN_DAY) / MS_IN_HOUR);
67
64
  const minutes = Math.floor((ms % MS_IN_HOUR) / MS_IN_MINUTE);
68
- const seconds = Math.floor((ms % MS_IN_MINUTE) / MS_IN_SECOND);
65
+ const seconds = (ms % MS_IN_MINUTE) / MS_IN_SECOND;
69
66
 
70
67
  const parts: string[] = [];
71
68
 
@@ -78,9 +75,7 @@ export function formatDuration(ms: number, compact = false): string {
78
75
  if (minutes > 0) {
79
76
  parts.push(`${minutes}m`);
80
77
  }
81
- if (hours <= 1 && (seconds > 0 || parts.length === 0)) {
82
- parts.push(`${seconds}s`);
83
- }
78
+ parts.push(`${secondsFormatter.format(seconds)}s`);
84
79
 
85
80
  return parts.join(' ');
86
81
  }