Warning
Pre-1.0.0, breaking changes may happen in any minor release. SemVer guarantees will begin at 1.0.0.
I recommend using only the Progress.all and Progress.forEach APIs for now, as they will likely change the least. The lower-level APIs for manual progress bar control are more likely to see breaking changes as I iterate on the design.
Please open an issue or reach out if you have any questions or want to contribute! Feedback and contributions are very welcome!
effective-progress is an Effect-native CLI progress bar library with:
- multiple nested tree-like progress bars
- spinner support for “we have no idea how long this takes” work
- keep using
Console.log/Effect.logInfowhile progress rendering is active - familiar
.alland.forEachAPIs — swapEffectforProgress, get progress bars basically for free - flicker-free rendering with Ink
bun add effective-progressIterate items with a single progress bar.
import { Console, Effect } from "effect";
import * as Progress from "effective-progress";
const program = Progress.all(
Array.from({ length: 5 }).map((_, i) =>
Effect.gen(function* () {
yield* Effect.sleep("1 second");
yield* Console.log(`Completed task ${i + 1}`);
}),
),
{ description: "Running tasks in parallel", concurrency: 2 },
);
Effect.runPromise(program);Nested progress bars with tree-style rendering that highlights parent tasks and their subtasks
import { Effect } from "effect";
import * as Progress from "effective-progress";
const program = Progress.all(
Array.from({ length: 5 }).map((_, i) =>
Effect.asVoid(
Progress.all(
Array.from({ length: 15 }).map((_) => Effect.sleep("100 millis")),
{ description: `Running subtasks for task ${i + 1}` },
),
),
),
{ description: "Running tasks in parallel", concurrency: 2 },
);
Effect.runPromise(program);Support for either/validate modes of Effect.all and render the amount of sucesses/failures.
Progress.allin default mode (mode: "default") remains fail-fast.- In fail-fast runs, unresolved units remain unprocessed.
mode: "either"andmode: "validate"run all effects and keep mixed outcomes in the task counters.- Mixed outcomes can still finalize as
donewhen all units are accounted for. - Empty collections are valid inputs for
Progress.all/Progress.forEachand render as0/0instead of failing.
examples/simpleExample.ts- low-boilerplate real-world flowexamples/advancedExample.ts- full API usage and manual task controlexamples/mixedOutcomes.ts- fail-fast vseither/validatewith mixed success/failure countersexamples/cliProgressSemantics.ts- zero totals, negative totals clearing to unknown totals, overflow counts, and emptyall/forEachexamples/unknownTotalCounting.ts- count successes/failures without a known total and renderprocessed/?examples/showcase.ts- nested concurrent tasks, spinner workloads, and mixed Effect/Console loggingexamples/performance.ts- stress-style run with high log volume and deeply nested progress updatesexamples/performanceLong.ts- longer-running stress run with roughly 10x the work ofperformance.tsexamples/performanceComparison.ts- bare vs progress comparison for theperformance.tsworkloadexamples/performanceComparisonLong.ts- longer bare vs progress comparison for theperformanceLong.tsworkload
- The Ink renderer runs with
patchConsole: true, so console output is patched by Ink while the app is mounted. Progress.task,Progress.all, andProgress.forEachwrite through the currently provided EffectConsoleimplementation.- Formatting is controlled by the API consumer's logger/console implementation.
- Rendering is powered by Ink.
- Built-in columns are: description, bar, amount/spinner, elapsed, and ETA.
- Determinate bars are segmented by outcome: succeeded (green), failed (red), and remaining (neutral).
- Determinate amount text shows counters without prefixes:
<succeeded> <failed> <processed>/<total>. - Counts can exceed
total; the amount text keeps those raw values (for example12/10) while the bar stays visually clamped at full. total: 0is valid for determinate tasks and renders as a full bar by default.- Column widths are measured and allocated per frame from a shared column tree, so rows stay aligned.
- Elapsed and ETA reserve stable widths to reduce jitter while tasks transition states.
- Sticky width can keep selected columns stable until the frame empties.
- On narrow terminals, layout compacts to fit available width and tree prefixes are suppressed when description space is too tight.
For manual usage, task still provides the current Task context, while logs continue through your outer Console:
const program = Progress.task(
Effect.gen(function* () {
const progress = yield* Progress.Progress;
const currentTask = yield* Progress.Task;
yield* Console.log("This log is handled by the outer Console", { taskId: currentTask });
// Manual determinate updates:
yield* progress.incrementSucceeded(currentTask, 3);
yield* progress.incrementFailed(currentTask, 1);
yield* Effect.sleep("1 second");
}),
{ description: "Manual task", total: 10 },
);Manual total behavior:
- negative totals on task creation clear the total and switch to indeterminate rendering
- negative totals on later
updateTaskcalls also clear the total - explicit
total: undefinedonupdateTaskclears the total and switches back to indeterminate rendering
Custom column APIs are not part of the first Ink release. The renderer ships with built-in columns only, and old renderer config APIs (RendererConfig, ProgressBarConfig, custom column definitions) are intentionally removed in this iteration.
- As Effect 4.0 is around the corner with some changes to logging, there may be some adjustments needed to align with the new Effect APIs.



