@silvery/ui 0.3.0

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 (87) hide show
  1. package/package.json +71 -0
  2. package/src/animation/easing.ts +38 -0
  3. package/src/animation/index.ts +18 -0
  4. package/src/animation/useAnimation.ts +143 -0
  5. package/src/animation/useInterval.ts +39 -0
  6. package/src/animation/useLatest.ts +35 -0
  7. package/src/animation/useTimeout.ts +65 -0
  8. package/src/animation/useTransition.ts +110 -0
  9. package/src/animation.ts +24 -0
  10. package/src/ansi/index.ts +43 -0
  11. package/src/canvas/index.ts +169 -0
  12. package/src/cli/ansi.ts +85 -0
  13. package/src/cli/index.ts +39 -0
  14. package/src/cli/multi-progress.ts +340 -0
  15. package/src/cli/progress-bar.ts +222 -0
  16. package/src/cli/spinner.ts +275 -0
  17. package/src/components/Badge.tsx +54 -0
  18. package/src/components/Breadcrumb.tsx +72 -0
  19. package/src/components/Button.tsx +73 -0
  20. package/src/components/CommandPalette.tsx +186 -0
  21. package/src/components/Console.tsx +79 -0
  22. package/src/components/CursorLine.tsx +71 -0
  23. package/src/components/Divider.tsx +67 -0
  24. package/src/components/EditContextDisplay.tsx +164 -0
  25. package/src/components/ErrorBoundary.tsx +179 -0
  26. package/src/components/Form.tsx +86 -0
  27. package/src/components/GridCell.tsx +42 -0
  28. package/src/components/HorizontalVirtualList.tsx +375 -0
  29. package/src/components/ModalDialog.tsx +179 -0
  30. package/src/components/PickerDialog.tsx +208 -0
  31. package/src/components/PickerList.tsx +93 -0
  32. package/src/components/ProgressBar.tsx +126 -0
  33. package/src/components/Screen.tsx +78 -0
  34. package/src/components/ScrollbackList.tsx +92 -0
  35. package/src/components/ScrollbackView.tsx +390 -0
  36. package/src/components/SelectList.tsx +176 -0
  37. package/src/components/Skeleton.tsx +87 -0
  38. package/src/components/Spinner.tsx +64 -0
  39. package/src/components/SplitView.tsx +199 -0
  40. package/src/components/Table.tsx +139 -0
  41. package/src/components/Tabs.tsx +203 -0
  42. package/src/components/TextArea.tsx +264 -0
  43. package/src/components/TextInput.tsx +240 -0
  44. package/src/components/Toast.tsx +216 -0
  45. package/src/components/Toggle.tsx +73 -0
  46. package/src/components/Tooltip.tsx +60 -0
  47. package/src/components/TreeView.tsx +212 -0
  48. package/src/components/Typography.tsx +233 -0
  49. package/src/components/VirtualList.tsx +318 -0
  50. package/src/components/VirtualView.tsx +221 -0
  51. package/src/components/useReadline.ts +213 -0
  52. package/src/components/useTextArea.ts +648 -0
  53. package/src/components.ts +133 -0
  54. package/src/display/Table.tsx +179 -0
  55. package/src/display/index.ts +13 -0
  56. package/src/hooks/useTea.ts +133 -0
  57. package/src/image/Image.tsx +187 -0
  58. package/src/image/index.ts +15 -0
  59. package/src/image/kitty-graphics.ts +161 -0
  60. package/src/image/sixel-encoder.ts +194 -0
  61. package/src/images.ts +22 -0
  62. package/src/index.ts +34 -0
  63. package/src/input/Select.tsx +155 -0
  64. package/src/input/TextInput.tsx +227 -0
  65. package/src/input/index.ts +25 -0
  66. package/src/progress/als-context.ts +160 -0
  67. package/src/progress/declarative.ts +519 -0
  68. package/src/progress/index.ts +54 -0
  69. package/src/progress/step-node.ts +152 -0
  70. package/src/progress/steps.ts +425 -0
  71. package/src/progress/task.ts +138 -0
  72. package/src/progress/tasks.ts +216 -0
  73. package/src/react/ProgressBar.tsx +146 -0
  74. package/src/react/Spinner.tsx +74 -0
  75. package/src/react/Tasks.tsx +144 -0
  76. package/src/react/context.tsx +145 -0
  77. package/src/react/index.ts +30 -0
  78. package/src/types.ts +252 -0
  79. package/src/utils/eta.ts +155 -0
  80. package/src/utils/index.ts +13 -0
  81. package/src/wrappers/index.ts +36 -0
  82. package/src/wrappers/with-progress.ts +250 -0
  83. package/src/wrappers/with-select.ts +194 -0
  84. package/src/wrappers/with-spinner.ts +108 -0
  85. package/src/wrappers/with-text-input.ts +388 -0
  86. package/src/wrappers/wrap-emitter.ts +158 -0
  87. package/src/wrappers/wrap-generator.ts +143 -0
@@ -0,0 +1,519 @@
1
+ /**
2
+ * Declarative steps implementation
3
+ *
4
+ * Provides the declarative overload for steps() that accepts an object
5
+ * structure and shows all steps upfront before execution.
6
+ */
7
+
8
+ import { MultiProgress, type TaskHandle } from "../cli/multi-progress"
9
+ import { step as getStepContext, createStepContext, runWithStepContext, type InternalStepContext } from "./als-context"
10
+ import { parseStepsDef, flattenStepNodes, getLeafNodes, type StepNode, type StepsDef } from "./step-node"
11
+
12
+ // Re-export step() for convenience
13
+ export { step } from "./als-context"
14
+
15
+ // Node.js globals for yielding to event loop
16
+ declare function setImmediate(callback: (value?: unknown) => void): unknown
17
+ declare function setTimeout(callback: (value?: unknown) => void, ms: number): unknown
18
+
19
+ /**
20
+ * Options for run() and pipe() execution
21
+ */
22
+ export interface ExecuteOptions {
23
+ /** Clear progress display after completion (default: false) */
24
+ clear?: boolean
25
+ }
26
+
27
+ /**
28
+ * Extract the return type from a generator or async generator
29
+ */
30
+ type GeneratorReturn<T> =
31
+ T extends Generator<unknown, infer R, unknown> ? R : T extends AsyncGenerator<unknown, infer R, unknown> ? R : T
32
+
33
+ /**
34
+ * Unwrap the result type, handling generators specially
35
+ */
36
+ type UnwrapResult<T> = Awaited<GeneratorReturn<Awaited<T>>>
37
+
38
+ /**
39
+ * Result type: maps step keys to their return values
40
+ */
41
+ type StepResults<T extends StepsDef> = {
42
+ [K in keyof T]: T[K] extends (...args: unknown[]) => infer R
43
+ ? UnwrapResult<R>
44
+ : T[K] extends [string, (...args: unknown[]) => infer R]
45
+ ? UnwrapResult<R>
46
+ : T[K] extends StepsDef
47
+ ? StepResults<T[K]>
48
+ : unknown
49
+ }
50
+
51
+ /**
52
+ * The runner object returned by steps()
53
+ */
54
+ export interface StepsRunner<T extends StepsDef> {
55
+ /** Internal: the parsed step nodes (for testing) */
56
+ readonly _steps: StepNode[]
57
+
58
+ /**
59
+ * Execute all steps sequentially
60
+ * @returns Results keyed by step name
61
+ */
62
+ run(options?: ExecuteOptions): Promise<StepResults<T>>
63
+
64
+ /**
65
+ * Execute all steps in a pipeline (each receives previous result)
66
+ * @returns Final step's result
67
+ */
68
+ pipe(options?: ExecuteOptions): Promise<unknown>
69
+
70
+ /**
71
+ * Manually signal completion (for manual execution mode)
72
+ */
73
+ done(options?: { clear?: boolean }): void
74
+ }
75
+
76
+ /**
77
+ * Create a declarative steps runner
78
+ *
79
+ * @param def - Object structure defining steps
80
+ * @returns StepsRunner with run(), pipe(), and done() methods
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * const loader = stepsDeclarative({
85
+ * loadModules, // "Load modules"
86
+ * loadRepo: { // "Load repo" (group)
87
+ * discover, // "Discover"
88
+ * parse, // "Parse"
89
+ * },
90
+ * });
91
+ *
92
+ * const results = await loader.run({ clear: true });
93
+ * ```
94
+ */
95
+ export function stepsDeclarative<T extends StepsDef>(def: T): StepsRunner<T> {
96
+ const rootNodes = parseStepsDef(def)
97
+ const allNodes = flattenStepNodes(rootNodes)
98
+
99
+ let multi: MultiProgress | null = null
100
+ const handles = new Map<StepNode, TaskHandle>()
101
+
102
+ // Build group tracking: map each group to its leaf nodes
103
+ const groupLeaves = new Map<StepNode, StepNode[]>()
104
+ const leafToGroups = new Map<StepNode, StepNode[]>()
105
+
106
+ for (const node of allNodes) {
107
+ if (node.children) {
108
+ const leaves = getLeafNodes([node])
109
+ groupLeaves.set(node, leaves)
110
+ for (const leaf of leaves) {
111
+ const groups = leafToGroups.get(leaf) ?? []
112
+ groups.push(node)
113
+ leafToGroups.set(leaf, groups)
114
+ }
115
+ }
116
+ }
117
+
118
+ return {
119
+ get _steps() {
120
+ return rootNodes
121
+ },
122
+
123
+ async run(options?: ExecuteOptions): Promise<StepResults<T>> {
124
+ multi = new MultiProgress()
125
+
126
+ // Register all steps upfront (shows pending state)
127
+ registerAllSteps(allNodes, multi, handles)
128
+
129
+ // Group timing tracking
130
+ const groupStartTimes = new Map<StepNode, number>()
131
+ const completedLeaves = new Set<StepNode>()
132
+
133
+ multi.start()
134
+
135
+ // Yield to event loop to ensure initial render is displayed
136
+ // before we start modifying task states
137
+ await new Promise((resolve) => setImmediate(resolve))
138
+
139
+ const results: Record<string, unknown> = {}
140
+
141
+ try {
142
+ // Execute each step with work
143
+ for (const node of allNodes) {
144
+ if (node.work) {
145
+ // Start parent groups if not started
146
+ const groups = leafToGroups.get(node) ?? []
147
+ for (const group of groups) {
148
+ if (!groupStartTimes.has(group)) {
149
+ groupStartTimes.set(group, Date.now())
150
+ handles.get(group)?.start()
151
+ }
152
+ }
153
+
154
+ const result = await executeStep(node, handles, multi)
155
+ setNestedResult(results, node.key, result)
156
+
157
+ // Mark leaf as complete and check group completion
158
+ completedLeaves.add(node)
159
+ for (const group of groups) {
160
+ const leaves = groupLeaves.get(group) ?? []
161
+ if (leaves.every((l) => completedLeaves.has(l))) {
162
+ const elapsed = Date.now() - groupStartTimes.get(group)!
163
+ handles.get(group)?.complete(elapsed)
164
+ }
165
+ }
166
+ }
167
+ }
168
+ } finally {
169
+ multi.stop(options?.clear ?? false)
170
+ }
171
+
172
+ return results as StepResults<T>
173
+ },
174
+
175
+ async pipe(options?: ExecuteOptions): Promise<unknown> {
176
+ multi = new MultiProgress()
177
+
178
+ // Register all steps upfront
179
+ registerAllSteps(allNodes, multi, handles)
180
+
181
+ // Group timing tracking
182
+ const groupStartTimes = new Map<StepNode, number>()
183
+ const completedLeaves = new Set<StepNode>()
184
+
185
+ multi.start()
186
+
187
+ // Yield to event loop to ensure initial render is displayed
188
+ // before we start modifying task states
189
+ await new Promise((resolve) => setImmediate(resolve))
190
+
191
+ let previousResult: unknown = undefined
192
+
193
+ try {
194
+ // Execute each step, passing previous result
195
+ for (const node of allNodes) {
196
+ if (node.work) {
197
+ // Start parent groups if not started
198
+ const groups = leafToGroups.get(node) ?? []
199
+ for (const group of groups) {
200
+ if (!groupStartTimes.has(group)) {
201
+ groupStartTimes.set(group, Date.now())
202
+ handles.get(group)?.start()
203
+ }
204
+ }
205
+
206
+ previousResult = await executeStep(node, handles, multi, previousResult)
207
+
208
+ // Mark leaf as complete and check group completion
209
+ completedLeaves.add(node)
210
+ for (const group of groups) {
211
+ const leaves = groupLeaves.get(group) ?? []
212
+ if (leaves.every((l) => completedLeaves.has(l))) {
213
+ const elapsed = Date.now() - groupStartTimes.get(group)!
214
+ handles.get(group)?.complete(elapsed)
215
+ }
216
+ }
217
+ }
218
+ }
219
+ } finally {
220
+ multi.stop(options?.clear ?? false)
221
+ }
222
+
223
+ return previousResult
224
+ },
225
+
226
+ done(options?: { clear?: boolean }) {
227
+ if (multi) {
228
+ multi.stop(options?.clear ?? false)
229
+ multi = null
230
+ }
231
+ },
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Register all steps with MultiProgress upfront
237
+ */
238
+ function registerAllSteps(nodes: StepNode[], multi: MultiProgress, handles: Map<StepNode, TaskHandle>): void {
239
+ // Register in order without insertAfter - simpler and correct
240
+ for (const node of nodes) {
241
+ const isGroup = node.children && !node.work
242
+ const handle = multi.add(node.label, {
243
+ type: isGroup ? "group" : "spinner",
244
+ indent: node.indent,
245
+ })
246
+ handles.set(node, handle)
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Execute a single step
252
+ */
253
+ async function executeStep(
254
+ node: StepNode,
255
+ handles: Map<StepNode, TaskHandle>,
256
+ multi: MultiProgress,
257
+ input?: unknown,
258
+ ): Promise<unknown> {
259
+ const handle = handles.get(node)!
260
+ const startTime = Date.now()
261
+
262
+ // Yield to event loop before starting
263
+ await new Promise((resolve) => setImmediate(resolve))
264
+
265
+ // Create step context for ALS
266
+ const ctx = createStepContext(node.label, handle, (subLabel) => {
267
+ // Create sub-step handle when step().sub() is called
268
+ return multi.add(subLabel, {
269
+ type: "spinner",
270
+ indent: node.indent + 1,
271
+ insertAfter: handle.id,
272
+ })
273
+ })
274
+
275
+ handle.start()
276
+
277
+ try {
278
+ // Run work function with ALS context
279
+ const result = await runWithStepContext(ctx, () => {
280
+ if (input !== undefined) {
281
+ return (node.work as (input: unknown) => unknown)(input)
282
+ }
283
+ return node.work!()
284
+ })
285
+
286
+ // Handle generator results
287
+ if (isGenerator(result)) {
288
+ return await runGenerator(result, ctx, node, multi)
289
+ }
290
+
291
+ if (isAsyncGenerator(result)) {
292
+ return await runAsyncGenerator(result, ctx, node, multi)
293
+ }
294
+
295
+ // Complete any remaining sub-step
296
+ ctx._completeSubStep()
297
+
298
+ // Complete the step with timing
299
+ const elapsed = Date.now() - startTime
300
+ handle.complete(elapsed)
301
+
302
+ return result
303
+ } catch (error) {
304
+ handle.fail()
305
+ throw error
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Run a sync generator step
311
+ */
312
+ async function runGenerator<T>(
313
+ gen: Generator<unknown, T, unknown>,
314
+ ctx: InternalStepContext,
315
+ node: StepNode,
316
+ multi: MultiProgress,
317
+ ): Promise<T> {
318
+ const startTime = Date.now()
319
+ let result = gen.next()
320
+ let hasSubSteps = false
321
+ // Track last inserted handle to maintain correct order
322
+ // Each new sub-step inserts after the previous one, not after parent
323
+ let lastInsertedId = ctx.handle.id
324
+
325
+ while (!result.done) {
326
+ const value = result.value
327
+
328
+ // Handle yielded values
329
+ if (isDeclareSteps(value)) {
330
+ // Declare all sub-steps upfront (show as pending)
331
+ if (!hasSubSteps) {
332
+ hasSubSteps = true
333
+ ctx.handle.setType("group")
334
+ }
335
+ for (const label of value.declare) {
336
+ const subHandle = multi.add(label, {
337
+ type: "spinner",
338
+ indent: node.indent + 1,
339
+ insertAfter: lastInsertedId,
340
+ })
341
+ lastInsertedId = subHandle.id
342
+ ctx._addSubHandle(label, subHandle)
343
+ }
344
+ } else if (typeof value === "string") {
345
+ // String = start a sub-step with this label
346
+ ctx._completeSubStep()
347
+
348
+ // First sub-step: change parent from spinner to group (no animation)
349
+ if (!hasSubSteps) {
350
+ hasSubSteps = true
351
+ ctx.handle.setType("group")
352
+ }
353
+
354
+ // Check if already declared, otherwise create new
355
+ const existingHandle = ctx._getSubHandle?.(value)
356
+ if (existingHandle) {
357
+ ctx._setCurrentSubHandle(value, existingHandle)
358
+ existingHandle.start()
359
+ } else {
360
+ const subHandle = multi.add(value, {
361
+ type: "spinner",
362
+ indent: node.indent + 1,
363
+ insertAfter: lastInsertedId,
364
+ })
365
+ lastInsertedId = subHandle.id
366
+ ctx._addSubHandle(value, subHandle)
367
+ subHandle.start()
368
+ }
369
+ } else if (isProgressUpdate(value)) {
370
+ // Progress update
371
+ ctx.progress(value.current ?? 0, value.total ?? 0)
372
+ }
373
+
374
+ // Yield to event loop for animation
375
+ await new Promise((resolve) => setTimeout(resolve, 0))
376
+
377
+ result = gen.next()
378
+ }
379
+
380
+ // Complete any remaining sub-step
381
+ ctx._completeSubStep()
382
+
383
+ // Complete the step with timing
384
+ const elapsed = Date.now() - startTime
385
+ ctx.handle.complete(elapsed)
386
+
387
+ return result.value
388
+ }
389
+
390
+ /**
391
+ * Run an async generator step
392
+ */
393
+ async function runAsyncGenerator<T>(
394
+ gen: AsyncGenerator<unknown, T, unknown>,
395
+ ctx: InternalStepContext,
396
+ node: StepNode,
397
+ multi: MultiProgress,
398
+ ): Promise<T> {
399
+ const startTime = Date.now()
400
+ let result = await gen.next()
401
+ let hasSubSteps = false
402
+ // Track last inserted handle to maintain correct order
403
+ // Each new sub-step inserts after the previous one, not after parent
404
+ let lastInsertedId = ctx.handle.id
405
+
406
+ while (!result.done) {
407
+ const value = result.value
408
+
409
+ // Handle yielded values
410
+ if (isDeclareSteps(value)) {
411
+ // Declare all sub-steps upfront (show as pending)
412
+ if (!hasSubSteps) {
413
+ hasSubSteps = true
414
+ ctx.handle.setType("group")
415
+ }
416
+ for (const label of value.declare) {
417
+ const subHandle = multi.add(label, {
418
+ type: "spinner",
419
+ indent: node.indent + 1,
420
+ insertAfter: lastInsertedId,
421
+ })
422
+ lastInsertedId = subHandle.id
423
+ ctx._addSubHandle(label, subHandle)
424
+ }
425
+ } else if (typeof value === "string") {
426
+ // String = start a sub-step with this label
427
+ ctx._completeSubStep()
428
+
429
+ // First sub-step: change parent from spinner to group (no animation)
430
+ if (!hasSubSteps) {
431
+ hasSubSteps = true
432
+ ctx.handle.setType("group")
433
+ }
434
+
435
+ // Check if already declared, otherwise create new
436
+ const existingHandle = ctx._getSubHandle(value)
437
+ if (existingHandle) {
438
+ ctx._setCurrentSubHandle(value, existingHandle)
439
+ existingHandle.start()
440
+ } else {
441
+ const subHandle = multi.add(value, {
442
+ type: "spinner",
443
+ indent: node.indent + 1,
444
+ insertAfter: lastInsertedId,
445
+ })
446
+ lastInsertedId = subHandle.id
447
+ ctx._addSubHandle(value, subHandle)
448
+ subHandle.start()
449
+ }
450
+ } else if (isProgressUpdate(value)) {
451
+ // Progress update
452
+ ctx.progress(value.current ?? 0, value.total ?? 0)
453
+ }
454
+
455
+ // Yield to event loop for animation
456
+ await new Promise((resolve) => setTimeout(resolve, 0))
457
+
458
+ result = await gen.next()
459
+ }
460
+
461
+ // Complete any remaining sub-step
462
+ ctx._completeSubStep()
463
+
464
+ // Complete the step with timing
465
+ const elapsed = Date.now() - startTime
466
+ ctx.handle.complete(elapsed)
467
+
468
+ return result.value
469
+ }
470
+
471
+ /**
472
+ * Set a nested result value by key path
473
+ */
474
+ function setNestedResult(results: Record<string, unknown>, key: string, value: unknown): void {
475
+ // For now, flat keys only - nested groups would need path handling
476
+ results[key] = value
477
+ }
478
+
479
+ /**
480
+ * Type guards
481
+ */
482
+ function isGenerator(value: unknown): value is Generator<unknown, unknown, unknown> {
483
+ return (
484
+ value !== null &&
485
+ typeof value === "object" &&
486
+ typeof (value as Generator).next === "function" &&
487
+ typeof (value as Generator)[Symbol.iterator] === "function"
488
+ )
489
+ }
490
+
491
+ function isAsyncGenerator(value: unknown): value is AsyncGenerator<unknown, unknown, unknown> {
492
+ return (
493
+ value !== null &&
494
+ typeof value === "object" &&
495
+ typeof (value as AsyncGenerator).next === "function" &&
496
+ typeof (value as AsyncGenerator)[Symbol.asyncIterator] === "function"
497
+ )
498
+ }
499
+
500
+ interface ProgressUpdate {
501
+ current?: number
502
+ total?: number
503
+ }
504
+
505
+ interface DeclareSteps {
506
+ declare: string[]
507
+ }
508
+
509
+ function isProgressUpdate(value: unknown): value is ProgressUpdate {
510
+ return (
511
+ value !== null && typeof value === "object" && !Array.isArray(value) && ("current" in value || "total" in value)
512
+ )
513
+ }
514
+
515
+ function isDeclareSteps(value: unknown): value is DeclareSteps {
516
+ return (
517
+ value !== null && typeof value === "object" && "declare" in value && Array.isArray((value as DeclareSteps).declare)
518
+ )
519
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Progress utilities for CLI applications
3
+ *
4
+ * Provides declarative and fluent APIs for displaying progress during async operations.
5
+ *
6
+ * @example Declarative mode (recommended)
7
+ * ```typescript
8
+ * import { steps, step } from "@silvery/ui/progress";
9
+ *
10
+ * const loader = steps({
11
+ * loadModules, // Auto-named: "Load modules"
12
+ * loadRepo: { // Group: "Load repo"
13
+ * discover, // "Discover"
14
+ * parse, // "Parse"
15
+ * },
16
+ * });
17
+ *
18
+ * const results = await loader.run({ clear: true });
19
+ * ```
20
+ *
21
+ * @example Fluent mode (legacy)
22
+ * ```typescript
23
+ * await steps()
24
+ * .run("Loading", loadModules)
25
+ * .run("Building", buildView)
26
+ * .execute({ clear: true });
27
+ * ```
28
+ */
29
+
30
+ // Modern API (recommended)
31
+ export {
32
+ steps,
33
+ step,
34
+ type StepBuilder,
35
+ type ExecuteOptions,
36
+ type StepsRunner,
37
+ type StepsDef,
38
+ type StepNode,
39
+ type StepContext,
40
+ } from "./steps"
41
+
42
+ // Legacy task wrappers (deprecated - use steps() instead)
43
+ /** @deprecated Use steps() instead */
44
+ export { task, type TaskWrapper } from "./task"
45
+ /** @deprecated Use steps() instead */
46
+ export { tasks, type TaskBuilder, type RunOptions } from "./tasks"
47
+
48
+ // Re-export CLI progress components
49
+ export { Spinner, createSpinner, type CallableSpinner } from "../cli/spinner"
50
+ export { ProgressBar } from "../cli/progress-bar"
51
+ export { MultiProgress, type TaskHandle } from "../cli/multi-progress"
52
+
53
+ // Re-export types
54
+ export type { ProgressInfo, StepProgress } from "../types.js"