Repository: promptfoo/promptfoo
Pull request #3768: fix(cli): improve progress bar visualization with thread grouping
Author: login: AISimplyExplained
Created: 2025-04-21T14:30:47Z | Merged: 2025-05-02T05:31:25Z

This PR improves the visualization of progress bars when running with many threads by introducing thread grouping.

### Changes
- Add thread grouping for better visualization when using many threads
- Group threads dynamically based on concurrency level:
  - 1-10 threads: Individual bars (no change from current behavior)
  - 11-30 threads: Groups of 5 threads
  - 31-100 threads: Groups of 10 threads
  - 100+ threads: Groups of 20 threads
- Show active threads per group and cleaner progress information

### Example Output for concurrency 50
![image](https://github.com/user-attachments/assets/d4575535-9e8b-4e03-895b-ce4bd9aa9b97)


### Problem
When running with many threads (e.g. 50), the current visualization shows too many progress bars and becomes black (mentioned in #3752) , making it hard to track progress and causing visual clutter.


<!-- ELLIPSIS_HIDDEN -->

----

> [!IMPORTANT]
> Improves progress bar visualization by dynamically grouping threads based on concurrency in `Evaluator` class, with new `calculateThreadsPerBar` function and corresponding tests.
> 
>   - **Behavior**:
>     - Introduces thread grouping for progress bars in `Evaluator` class in `src/evaluator.ts`.
>     - Groups threads dynamically based on concurrency: 1-10 (individual), 11-30 (groups of 5), 31-100 (groups of 10), 100+ (groups of 20).
>     - Displays active threads per group in progress bars.
>   - **Functions**:
>     - Adds `calculateThreadsPerBar()` in `src/evaluator.ts` to calculate thread allocation per progress bar.
>   - **Tests**:
>     - Adds tests for `calculateThreadsPerBar()` in `test/evaluator.test.ts` to verify thread distribution logic.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=promptfoo%2Fpromptfoo&utm_source=github&utm_medium=referral)<sup> for c132fdae1f03f38420a8f63427eba675f2684375. You can [customize](https://app.ellipsis.dev/promptfoo/settings/summaries) this summary. It will automatically update as commits are pushed.</sup>

<!-- ELLIPSIS_HIDDEN -->

## Summary by Sourcery

Improve progress bar visualization for multi-threaded evaluations by introducing dynamic thread grouping

New Features:
- Implement dynamic thread grouping for progress bars based on concurrency level

Bug Fixes:
- Resolve issue with excessive progress bars when running with many threads

Enhancements:
- Reduce visual clutter by limiting the number of progress bars
- Distribute threads evenly across progress bar groups

Tests:
- Add comprehensive test cases for thread distribution calculation


Diff:
diff --git a/src/evaluator.ts b/src/evaluator.ts
index 747a8e2e7775..4f8aadff99ef 100644
--- a/src/evaluator.ts
+++ b/src/evaluator.ts
@@ -1,6 +1,7 @@
 import async from 'async';
 import chalk from 'chalk';
 import type { MultiBar, SingleBar } from 'cli-progress';
+import cliProgress from 'cli-progress';
 import { randomUUID } from 'crypto';
 import { globSync } from 'glob';
 import * as path from 'path';
@@ -426,6 +427,23 @@ export async function runEval({
   }
 }
 
+/**
+ * Calculates the number of threads allocated to a specific progress bar.
+ * @param concurrency Total number of concurrent threads
+ * @param numProgressBars Total number of progress bars
+ * @param barIndex Index of the progress bar (0-based)
+ * @returns Number of threads allocated to this progress bar
+ */
+export function calculateThreadsPerBar(
+  concurrency: number,
+  numProgressBars: number,
+  barIndex: number,
+): number {
+  const minThreadsPerBar = Math.floor(concurrency / numProgressBars);
+  const extraThreads = concurrency % numProgressBars;
+  return barIndex < extraThreads ? minThreadsPerBar + 1 : minThreadsPerBar;
+}
+
 export function generateVarCombinations(
   vars: Record<string, string | string[] | any>,
 ): Record<string, string | any[]>[] {
@@ -984,8 +1002,19 @@ class Evaluator {
           .replace(/\n/g, ' ');
         logger.info(`[${numComplete}/${total}] Running ${provider} with vars: ${vars}`);
       } else if (multibar && evalStep) {
-        const threadIndex = index % concurrency;
-        const progressbar = multiProgressBars[threadIndex];
+        const numProgressBars = Math.min(concurrency, 20);
+
+        // Calculate which progress bar to use
+        const progressBarIndex = index % numProgressBars;
+        const progressbar = multiProgressBars[progressBarIndex];
+
+        // Calculate how many threads are assigned to this progress bar
+        const threadsForThisBar = calculateThreadsPerBar(
+          concurrency,
+          numProgressBars,
+          progressBarIndex,
+        );
+
         progressbar.increment({
           provider: evalStep.provider.label || evalStep.provider.id(),
           prompt: evalStep.prompt.raw.slice(0, 10).replace(/\n/g, ' '),
@@ -994,6 +1023,7 @@ class Evaluator {
             .join(' ')
             .slice(0, 10)
             .replace(/\n/g, ' '),
+          activeThreads: threadsForThisBar,
         });
       } else {
         logger.debug(`Eval #${index + 1} complete (${numComplete} of ${runEvalOptions.length})`);
@@ -1006,25 +1036,41 @@ class Evaluator {
         return;
       }
 
-      const cliProgress = await import('cli-progress');
+      const numProgressBars = Math.min(concurrency, 20);
+
+      const showThreadCounts = concurrency > numProgressBars;
+
       multibar = new cliProgress.MultiBar(
         {
-          format:
-            '[{bar}] {percentage}% | ETA: {eta}s | {value}/{total} | {provider} "{prompt}" {vars}',
+          format: showThreadCounts
+            ? 'Group {groupId} [{bar}] {percentage}% | {value}/{total} | {activeThreads}/{maxThreads} threads | {provider} "{prompt}" {vars}'
+            : 'Group {groupId} [{bar}] {percentage}% | {value}/{total} | {provider} "{prompt}" {vars}',
           hideCursor: true,
         },
         cliProgress.Presets.shades_classic,
       );
-      const stepsPerThread = Math.floor(evalOptions.length / concurrency);
-      const remainingSteps = evalOptions.length % concurrency;
+
+      if (!multibar) {
+        return;
+      }
+
+      const stepsPerProgressBar = Math.floor(evalOptions.length / numProgressBars);
+      const remainingSteps = evalOptions.length % numProgressBars;
       multiProgressBars = [];
-      for (let i = 0; i < concurrency; i++) {
-        const totalSteps = i < remainingSteps ? stepsPerThread + 1 : stepsPerThread;
+
+      for (let i = 0; i < numProgressBars; i++) {
+        const totalSteps = i < remainingSteps ? stepsPerProgressBar + 1 : stepsPerProgressBar;
         if (totalSteps > 0) {
+          // Calculate how many threads are assigned to this progress bar
+          const threadsForThisBar = calculateThreadsPerBar(concurrency, numProgressBars, i);
+
           const progressbar = multibar.create(totalSteps, 0, {
+            groupId: `${i + 1}/${numProgressBars}`,
             provider: '',
             prompt: '',
             vars: '',
+            activeThreads: 0,
+            maxThreads: threadsForThisBar,
           });
           multiProgressBars.push(progressbar);
         }
diff --git a/test/evaluator.test.ts b/test/evaluator.test.ts
index 48acb2e6282f..7653f2a856f9 100644
--- a/test/evaluator.test.ts
+++ b/test/evaluator.test.ts
@@ -1,6 +1,12 @@
 import { randomUUID } from 'crypto';
 import glob from 'glob';
-import { evaluate, generateVarCombinations, isAllowedPrompt, runEval } from '../src/evaluator';
+import {
+  calculateThreadsPerBar,
+  evaluate,
+  generateVarCombinations,
+  isAllowedPrompt,
+  runEval,
+} from '../src/evaluator';
 import { runExtensionHook } from '../src/evaluatorHelpers';
 import logger from '../src/logger';
 import { runDbMigrations } from '../src/migrate';
@@ -2377,3 +2383,46 @@ describe('runEval', () => {
     });
   });
 });
+
+describe('calculateThreadsPerBar', () => {
+  it('should evenly distribute threads when concurrency is a multiple of numProgressBars', () => {
+    // 10 threads, 5 progress bars = 2 threads per bar
+    expect(calculateThreadsPerBar(10, 5, 0)).toBe(2);
+    expect(calculateThreadsPerBar(10, 5, 1)).toBe(2);
+    expect(calculateThreadsPerBar(10, 5, 4)).toBe(2);
+
+    // 12 threads, 6 progress bars = 2 threads per bar
+    expect(calculateThreadsPerBar(12, 6, 0)).toBe(2);
+    expect(calculateThreadsPerBar(12, 6, 5)).toBe(2);
+  });
+
+  it('should correctly distribute extra threads for the first N bars', () => {
+    // 11 threads, 5 progress bars = 2 threads for first bar, 2 for the rest
+    // floor(11/5) = 2 with 1 extra thread
+    expect(calculateThreadsPerBar(11, 5, 0)).toBe(3); // First bar gets 2+1
+    expect(calculateThreadsPerBar(11, 5, 1)).toBe(2);
+    expect(calculateThreadsPerBar(11, 5, 4)).toBe(2);
+
+    // 17 threads, 6 progress bars = 2 threads for first 5 bars, 2 for the last
+    // floor(17/6) = 2 with 5 extra threads
+    expect(calculateThreadsPerBar(17, 6, 0)).toBe(3); // First bar gets 2+1
+    expect(calculateThreadsPerBar(17, 6, 4)).toBe(3); // Fifth bar gets 2+1
+    expect(calculateThreadsPerBar(17, 6, 5)).toBe(2); // Last bar gets just 2
+  });
+
+  it('should handle edge cases', () => {
+    // 1 thread, 1 progress bar
+    expect(calculateThreadsPerBar(1, 1, 0)).toBe(1);
+
+    // More progress bars than threads (1 thread per bar until we run out)
+    expect(calculateThreadsPerBar(3, 5, 0)).toBe(1);
+    expect(calculateThreadsPerBar(3, 5, 1)).toBe(1);
+    expect(calculateThreadsPerBar(3, 5, 2)).toBe(1);
+    expect(calculateThreadsPerBar(3, 5, 3)).toBe(0);
+    expect(calculateThreadsPerBar(3, 5, 4)).toBe(0);
+
+    // Large numbers
+    expect(calculateThreadsPerBar(101, 20, 0)).toBe(6); // 5 with 1 extra
+    expect(calculateThreadsPerBar(101, 20, 19)).toBe(5); // Last bar gets no extra
+  });
+});


Reviews:
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T14:33:14Z
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T17:17:55Z
user: login: sourcery-ai[bot]
body: Hey @AISimplyExplained - I've reviewed your changes - here's some feedback:

- Consider making the maximum number of progress bars (currently hardcoded to 20) configurable.
- The PR description mentions dynamic grouping rules based on thread count ranges, but the implementation primarily caps the bars at 20; consider aligning the description with the implementation logic.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 1 issue found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T17:24:46Z
user: login: ellipsis-dev[bot]
body: > [!CAUTION]
> **Changes requested  :x:**
> <details>
> <summary>Reviewed everything up to c132fdae1f03f38420a8f63427eba675f2684375 in <ellipsistime>2 minutes and 53 seconds</ellipsistime>. Click for details.</summary> 
> 
> - Reviewed `180` lines of code in `2` files
> - Skipped `0` files when reviewing.
> - Skipped posting `6` draft comments. View those below.
> - Modify your [settings](https://app.ellipsis.dev/promptfoo/settings/code-review) and [rules](https://app.ellipsis.dev/promptfoo/code-review/rules) to customize what types of comments Ellipsis leaves. And don't forget to react with :+1: or :-1: to teach Ellipsis.
> 
> <details>
> <summary>1. <code>test/evaluator.test.ts:2388</code></summary>
> 
> - **Draft comment:** 
> The calculateThreadsPerBar tests are comprehensive and cover normal, extra thread, and edge cases. Good job ensuring correct thread grouping behavior.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>2. <code>test/evaluator.test.ts:1050</code></summary>
> 
> - **Draft comment:** 
> The tests provide excellent coverage of evaluator behavior (provider transforms, conversation histories, multiple providers, caching, metadata merging, assert scoring, and transform orders). This extensive test suite is valuable for ensuring progress bar grouping and progress callbacks work correctly.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>3. <code>test/evaluator.test.ts:1920</code></summary>
> 
> - **Draft comment:** 
> Tests for provider delay (and skipping delay for cached responses) are clear. Ensure that the sleep function used is behaving as expected. No issues noted.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>4. <code>test/evaluator.test.ts:2217</code></summary>
> 
> - **Draft comment:** 
> The conversation tests are well done; they validate separate conversation histories based on metadata.conversationId. Consider adding inline comments to describe the expected prompt structure for clarity.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>5. <code>test/evaluator.test.ts:1755</code></summary>
> 
> - **Draft comment:** 
> Good use of beforeEach and afterEach for clearing mocks. The extensive tests for metadata merging and transform ordering are very thorough.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>6. <code>test/evaluator.test.ts:2388</code></summary>
> 
> - **Draft comment:** 
> While the unit tests for calculateThreadsPerBar verify the distribution function, there are no integration tests verifying that the overall number of progress bars (and their associated thread counts) matches the documented dynamic grouping behavior. Please add tests for different concurrency levels (e.g. 15, 50, 150) to ensure the progress bar visualization adheres to the intended thresholds.
> - **Reason this comment was not posted:**
> Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% The existing unit tests are quite thorough and test the core functionality of thread distribution. Integration tests would only verify that this function is being called correctly, which is a simpler concern. The comment seems to be asking for tests that would be redundant with the existing unit tests. The visual clutter problem is already addressed by the distribution logic that's being tested. I could be wrong about the value of integration tests - they might catch issues with how the progress bars are actually rendered or how the distribution interacts with the UI layer. While integration tests can be valuable, in this case the core functionality (thread distribution) is well-tested, and the UI rendering is a separate concern that should be tested separately. The current unit tests provide strong confidence in the distribution logic. Delete the comment. The existing unit tests thoroughly verify the thread distribution logic at multiple concurrency levels. Additional integration tests would be redundant and wouldn't meaningfully increase confidence in the code.
> 
> 
> </details>
> 
> 
> Workflow ID: <workflowid>`wflow_cjDRbeMneoW17Y4a`</workflowid>
> </details>
> 
> <sup>You can customize </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=promptfoo%2Fpromptfoo&utm_source=github&utm_medium=referral)<sup> by changing your [verbosity settings](https://app.ellipsis.dev/promptfoo/settings/code-review), reacting with :+1: or :-1:, [replying to comments](https://docs.ellipsis.dev/features/code-review#teaching-ellipsis), or adding [code review rules](https://app.ellipsis.dev/promptfoo/code-review/rules).</sup>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T17:26:43Z

Review comments:
diff_hunk: @@ -463,6 +464,19 @@ export function generateVarCombinations(
   return combinations;
 }
 
+// Helper function to calculate group size and count based on concurrency
+function calculateProgressGroups(concurrency: number): { groupSize: number; numGroups: number } {
+  if (concurrency <= 10) {
path: src/evaluator.ts
user: login: ellipsis-dev[bot]
body: Consider adding an invariant (or similar check) to ensure that `concurrency` is greater than 0 in `calculateProgressGroups` to guard against edge cases.

created_at: 2025-04-21T14:33:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1006,25 +1019,44 @@ class Evaluator {
         return;
       }
 
-      const cliProgress = await import('cli-progress');
+      // Determine number of progress bars (max 20)
+      const numProgressBars = Math.min(concurrency, 20);
+
+      const showThreadCounts = concurrency > numProgressBars;
+
       multibar = new cliProgress.MultiBar(
         {
-          format:
-            '[{bar}] {percentage}% | ETA: {eta}s | {value}/{total} | {provider} "{prompt}" {vars}',
+          format: showThreadCounts
+            ? 'Group {groupId} [{bar}] {percentage}% | {value}/{total} | {activeThreads}/{maxThreads} threads | {provider} "{prompt}" {vars}'
+            : 'Group {groupId} [{bar}] {percentage}% | {value}/{total} | {provider} "{prompt}" {vars}',
           hideCursor: true,
         },
         cliProgress.Presets.shades_classic,
       );
-      const stepsPerThread = Math.floor(evalOptions.length / concurrency);
-      const remainingSteps = evalOptions.length % concurrency;
+
+      if (!multibar) {
+        return;
+      }
+
+      const stepsPerProgressBar = Math.floor(evalOptions.length / numProgressBars);
+      const remainingSteps = evalOptions.length % numProgressBars;
       multiProgressBars = [];
-      for (let i = 0; i < concurrency; i++) {
-        const totalSteps = i < remainingSteps ? stepsPerThread + 1 : stepsPerThread;
+
+      for (let i = 0; i < numProgressBars; i++) {
+        const totalSteps = i < remainingSteps ? stepsPerProgressBar + 1 : stepsPerProgressBar;
         if (totalSteps > 0) {
+          // Calculate how many threads are assigned to this progress bar
+          const minThreadsPerBar = Math.floor(concurrency / numProgressBars);
path: src/evaluator.ts
user: login: ellipsis-dev[bot]
body: The calculation for threads per progress bar is repeated in both `progressCallback` and `createMultiBars`. Consider extracting it into a common helper to minimize duplication.

created_at: 2025-05-01T17:17:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -984,8 +1002,19 @@ class Evaluator {
           .replace(/\n/g, ' ');
         logger.info(`[${numComplete}/${total}] Running ${provider} with vars: ${vars}`);
       } else if (multibar && evalStep) {
-        const threadIndex = index % concurrency;
-        const progressbar = multiProgressBars[threadIndex];
+        const numProgressBars = Math.min(concurrency, 20);
path: src/evaluator.ts
user: login: sourcery-ai[bot]
body: **suggestion:** Magic number 20 appears; consider using a named constant.

Use a named constant (e.g., MAX_PROGRESS_BARS) instead of the literal 20 to clarify its purpose and simplify future updates.

Suggested implementation:

```typescript
const MAX_PROGRESS_BARS = 20;

```

```typescript
        const numProgressBars = Math.min(concurrency, MAX_PROGRESS_BARS);

```

Make sure that the MAX_PROGRESS_BARS constant is placed at an appropriate location depending on your file structure (e.g., at the top of the file with other constants or within a configuration object if applicable).
created_at: 2025-05-01T17:24:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -984,8 +1002,19 @@ class Evaluator {
           .replace(/\n/g, ' ');
         logger.info(`[${numComplete}/${total}] Running ${provider} with vars: ${vars}`);
       } else if (multibar && evalStep) {
-        const threadIndex = index % concurrency;
-        const progressbar = multiProgressBars[threadIndex];
+        const numProgressBars = Math.min(concurrency, 20);
path: src/evaluator.ts
user: login: ellipsis-dev[bot]
body: The progress bar grouping logic uses `const numProgressBars = Math.min(concurrency, 20)` which always limits the number of bars to 20. However, the PR description specifies dynamic group sizes (e.g. groups of 5 for 11–30 threads, 10 for 31–100, and 20 for 100+). Please update this logic so that for example a concurrency of 50 produces 5 progress bars (groups of 10 threads) rather than 20.
```suggestion
        const numProgressBars = concurrency <= 10 ? 1 : concurrency <= 30 ? Math.ceil(concurrency / 5) : concurrency <= 100 ? Math.ceil(concurrency / 10) : Math.ceil(concurrency / 20);
```

created_at: 2025-05-01T17:26:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: gru-agent[bot]
created_at: 2025-04-21T14:31:07Z
body: ## TestGru Assignment

### Summary

| Link | CommitId | Status | Reason |
| ---- | -------- | ------ | ------ |
| [Detail](https://gru.ai/:test/promptfoo@github/promptfoo/5c8f45ed-cd16-4854-b13a-bfca40690176) | b3976344e88cbf51c7d7da5366f0d7d1adbe0006 | 🚫 Skipped | No files need to be tested {"src/evaluator.ts":"target file(src/evaluator.ts) not in work scope \n include: src/app/src/**/*.ts \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js"} |

> [!TIP]
> You can `@gru-agent` and leave your feedback. TestGru will make adjustments based on your input

author_association: CONTRIBUTOR
user: login: gru-agent[bot]
created_at: 2025-04-21T14:31:10Z
body: ## TestGru Assignment

### Summary

| Link | CommitId | Status | Reason |
| ---- | -------- | ------ | ------ |
| [Detail](https://gru.ai/:test/promptfoo@github/promptfoo/dce411d7-39bf-449c-949d-108ad8ea93ee) | b3976344e88cbf51c7d7da5366f0d7d1adbe0006 | 🚫 Skipped | No files need to be tested {"src/evaluator.ts":"source code too long"} |

> [!TIP]
> You can `@gru-agent` and leave your feedback. TestGru will make adjustments based on your input

author_association: CONTRIBUTOR
user: login: faizanminhas
created_at: 2025-04-21T14:41:56Z
body: I was wondering if we could / should use something like github.com/vadimdemedes/ink here, to make this prettier?
author_association: CONTRIBUTOR
user: login: AISimplyExplained
created_at: 2025-04-21T15:15:19Z
body: What do you think it should look like? Should it also have some color?
author_association: CONTRIBUTOR
user: login: faizanminhas
created_at: 2025-04-21T15:21:55Z
body: I think this should actually be able to display all 50+ threads and it should be scrollable and refresh like a fancy terminal app like htop. So maybe it'll just look similar to how it does today?
author_association: CONTRIBUTOR
user: login: mldangelo
created_at: 2025-05-01T17:23:39Z
body: @ellipsis-dev review 
author_association: MEMBER
user: login: sourcery-ai[bot]
created_at: 2025-05-01T17:24:45Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

This pull request implements thread grouping for progress bars to enhance visualization when using high concurrency. It introduces logic to calculate thread distribution across a limited number of bars (max 20) and updates the display format to show threads per group.

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Implemented thread grouping logic for progress bars. | <ul><li>Limited the maximum number of displayed progress bars to 20.</li><li>Mapped evaluation tasks to progress bars using modulo operator based on the number of bars.</li><li>Adjusted the progress bar creation loop to initialize a limited number of bars.</li></ul> | `src/evaluator.ts` |
| Added calculation and display logic for threads per progress bar group. | <ul><li>Introduced the `calculateThreadsPerBar` function to distribute threads across bars.</li><li>Updated the progress bar format string to conditionally display thread counts per group (`activeThreads`/`maxThreads`).</li><li>Passed group ID and thread count data during progress bar creation and updates.</li></ul> | `src/evaluator.ts` |
| Added unit tests for thread distribution calculation. | <ul><li>Created a new test suite for the `calculateThreadsPerBar` function.</li><li>Added test cases covering even distribution, uneven distribution, and edge scenarios.</li></ul> | `test/evaluator.test.ts` |

### Possibly linked issues

- **#3752**: The PR improves CLI progress bar visualization by grouping threads to address the issue of poor visuals with many threads.

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR