ProgressIndicator Component

The ProgressIndicator component provides visual feedback for long-running operations. It supports both determinate (percentage-based) and indeterminate progress, with optional cancellation.

Usage

The primary way to show progress is through api.ui.withProgress():

const result = await api.ui.withProgress(options, task);

Parameters

ParameterTypeRequiredDescription
optionsobjectYesProgress configuration
taskfunctionYesTask function to execute

Options

interface ProgressOptions {
  location: 'notification' | 'window' | 'source-control';
  title?: string;
  cancellable?: boolean;
}

Available Options

OptionTypeDefaultDescription
locationstring'notification'Where to show the progress indicator
titlestring-Title text displayed above progress bar
cancellablebooleanfalseWhether user can cancel the operation

Location Types

LocationBehaviorUse Case
'notification'Bottom-right corner, smallBackground tasks, non-intrusive
'window'Center of window, modalCritical operations, requires attention
'source-control'Source control panelSCM operations

Task Function

The task function receives two parameters:

type TaskFunction<T> = (
  progress: Progress,
  token: CancellationToken
) => Promise<T>;

Progress Object

interface Progress {
  report(value: ProgressValue): void;
}
 
interface ProgressValue {
  message?: string;    // Status message
  increment?: number;  // Percentage increment (0-100)
}

CancellationToken

interface CancellationToken {
  isCancellationRequested: boolean;
  onCancellationRequested(listener: () => void): Disposable;
}

Basic Example

await api.ui.withProgress(
  {
    location: 'notification',
    title: 'Processing Files',
    cancellable: true
  },
  async (progress, token) => {
    for (let i = 0; i < 100; i++) {
      // Check for cancellation
      if (token.isCancellationRequested) {
        return null;
      }
 
      // Report progress
      progress.report({
        message: `Processing file ${i + 1}/100`,
        increment: 1
      });
 
      await processFile(i);
    }
 
    return 'Complete';
  }
);

Determinate Progress

Show exact progress percentage:

await api.ui.withProgress(
  {
    location: 'notification',
    title: 'Uploading Files'
  },
  async (progress) => {
    const files = await getFiles();
    const total = files.length;
 
    for (let i = 0; i < total; i++) {
      progress.report({
        message: `Uploading $\\{files[i].name\\}`,
        increment: (100 / total)  // Calculate percentage
      });
 
      await uploadFile(files[i]);
    }
  }
);

Indeterminate Progress

When you don’t know the total amount:

await api.ui.withProgress(
  {
    location: 'notification',
    title: 'Loading Data'
  },
  async (progress) => {
    progress.report({ message: 'Connecting to server...' });
    await connect();
 
    progress.report({ message: 'Fetching data...' });
    const data = await fetchData();
 
    progress.report({ message: 'Processing...' });
    return await process(data);
  }
);

Note: Omit the increment property for indeterminate progress (animated bar).

Cancellable Operations

Allow users to cancel long operations:

const result = await api.ui.withProgress(
  {
    location: 'window',
    title: 'Building Project',
    cancellable: true
  },
  async (progress, token) => {
    // Set up cancellation handler
    token.onCancellationRequested(() => {
      console.log('User cancelled build');
    });
 
    const steps = ['Clean', 'Compile', 'Test', 'Package'];
 
    for (let i = 0; i < steps.length; i++) {
      // Check cancellation before each step
      if (token.isCancellationRequested) {
        return { success: false, reason: 'cancelled' };
      }
 
      progress.report({
        message: `${steps[i]}...`,
        increment: 25
      });
 
      await executeStep(steps[i]);
    }
 
    return { success: true };
  }
);
 
if (result.success) {
  api.ui.showInformationMessage('Build complete!');
} else {
  api.ui.showWarningMessage('Build cancelled');
}

Window (Modal) Progress

For operations that require user attention:

await api.ui.withProgress(
  {
    location: 'window',
    title: 'Installing Dependencies',
    cancellable: false
  },
  async (progress) => {
    progress.report({ message: 'Downloading packages...' });
    await downloadPackages();
 
    progress.report({ message: 'Installing...' });
    await installPackages();
 
    progress.report({ message: 'Running post-install scripts...' });
    await runScripts();
  }
);

Multiple Progress Indicators

You can show multiple progress indicators simultaneously:

// First operation
api.ui.withProgress(
  {
    location: 'notification',
    title: 'Syncing Files'
  },
  async (progress) => {
    // ... sync logic
  }
);
 
// Second operation (runs concurrently)
api.ui.withProgress(
  {
    location: 'notification',
    title: 'Building Assets'
  },
  async (progress) => {
    // ... build logic
  }
);

Error Handling

Handle errors within the task function:

try {
  const result = await api.ui.withProgress(
    {
      location: 'notification',
      title: 'Processing'
    },
    async (progress) => {
      try {
        progress.report({ message: 'Step 1...' });
        await step1();
 
        progress.report({ message: 'Step 2...' });
        await step2();
 
        return { success: true };
      } catch (error) {
        // Handle error within task
        progress.report({ message: 'Failed!' });
        throw error;
      }
    }
  );
 
  api.ui.showInformationMessage('Operation completed');
} catch (error) {
  api.ui.showErrorMessage(`Operation failed: $\\{error.message\\}`);
}

Real-World Examples

File Download

async function downloadFile(api, url, destination) {
  await api.ui.withProgress(
    {
      location: 'notification',
      title: 'Downloading File',
      cancellable: true
    },
    async (progress, token) => {
      const response = await fetch(url);
      const total = parseInt(response.headers.get('content-length'), 10);
      let downloaded = 0;
 
      const reader = response.body.getReader();
      const chunks = [];
 
      while (true) {
        // Check cancellation
        if (token.isCancellationRequested) {
          reader.cancel();
          return null;
        }
 
        const { done, value } = await reader.read();
        if (done) break;
 
        chunks.push(value);
        downloaded += value.length;
 
        // Report progress
        const percentage = (downloaded / total) * 100;
        progress.report({
          message: `${(downloaded / 1024 / 1024).toFixed(2)} MB / ${(total / 1024 / 1024).toFixed(2)} MB`,
          increment: percentage - (downloaded - value.length) / total * 100
        });
      }
 
      // Save file
      const blob = new Blob(chunks);
      await saveFile(destination, blob);
      return destination;
    }
  );
}

Batch Processing

async function processBatch(api, items) {
  const results = await api.ui.withProgress(
    {
      location: 'window',
      title: 'Processing Items',
      cancellable: true
    },
    async (progress, token) => {
      const results = [];
      const total = items.length;
 
      for (let i = 0; i < total; i++) {
        if (token.isCancellationRequested) {
          break;
        }
 
        const item = items[i];
 
        progress.report({
          message: `Processing ${item.name} (${i + 1}/${total})`,
          increment: (100 / total)
        });
 
        try {
          const result = await processItem(item);
          results.push({ item, result, success: true });
        } catch (error) {
          results.push({ item, error, success: false });
        }
      }
 
      return results;
    }
  );
 
  // Show summary
  const succeeded = results.filter(r => r.success).length;
  const failed = results.filter(r => !r.success).length;
 
  api.ui.showInformationMessage(
    `Processed ${succeeded} items successfully, ${failed} failed`
  );
}

Search Operation

async function searchFiles(api, query) {
  return await api.ui.withProgress(
    {
      location: 'notification',
      title: 'Searching Files'
    },
    async (progress) => {
      progress.report({ message: 'Indexing files...' });
      const files = await api.workspace.findFiles('**/*');
 
      progress.report({ message: `Searching ${files.length} files...` });
      const results = [];
 
      for (let i = 0; i < files.length; i++) {
        const content = await api.workspace.readFile(files[i]);
        if (content.includes(query)) {
          results.push(files[i]);
        }
 
        // Update every 100 files
        if (i % 100 === 0) {
          progress.report({
            message: `Searched ${i}/${files.length} files`,
            increment: (100 / files.length) * 100
          });
        }
      }
 
      return results;
    }
  );
}

Styling

The ProgressIndicator uses the following CSS variables:

/* Container */
--panel                       /* Background color */
--border                      /* Border color */
--radius                      /* Border radius */
 
/* Text */
--text                        /* Title color */
--text-secondary              /* Message color */
--muted                       /* Percentage color */
 
/* Progress bar */
--border                      /* Bar background */
--accent                      /* Bar fill color */
 
/* Cancel button */
--text-secondary              /* Button color */
--danger                      /* Button hover color */

Location-Specific Styles

Different locations have different styling:

/* Notification (bottom-right) */
.progress-notification {
  min-width: 300px;
  max-width: 500px;
}
 
/* Window (modal, centered) */
.progress-window {
  min-width: 400px;
  max-width: 600px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
 
/* Source control (compact) */
.progress-source-control {
  min-width: 250px;
  padding: 8px;
}

Best Practices

1. Choose Appropriate Location

  • Notification: Non-critical background tasks
  • Window: Critical operations requiring user attention
  • Source Control: SCM-related operations

2. Provide Meaningful Messages

// Good
progress.report({ message: 'Compiling TypeScript (35/100 files)' });
 
// Less helpful
progress.report({ message: 'Processing...' });

3. Update Progress Regularly

Update at least every few seconds for long operations:

for (let i = 0; i < items.length; i++) {
  // Update every 10 items or every item for small batches
  if (i % 10 === 0 || items.length < 50) {
    progress.report({
      message: `Processing ${i + 1}/$\\{items.length\\}`,
      increment: (100 / items.length)
    });
  }
  await processItem(items[i]);
}

4. Handle Cancellation Properly

Always check isCancellationRequested and clean up:

async (progress, token) => {
  const cleanup = [];
 
  try {
    for (const item of items) {
      if (token.isCancellationRequested) {
        break;
      }
      const resource = await processItem(item);
      cleanup.push(resource);
    }
  } finally {
    // Clean up even if cancelled
    for (const resource of cleanup) {
      await resource.dispose();
    }
  }
}

5. Use Determinate When Possible

Determinate progress (with percentage) is more informative:

// Good: User knows progress
progress.report({
  message: 'Processing file 50/100',
  increment: 1
});
 
// Less informative
progress.report({
  message: 'Processing files...'
});

Performance Tips

1. Batch Progress Updates

Don’t update too frequently:

// Good: Update every 100ms
let lastUpdate = 0;
for (const item of items) {
  await process(item);
 
  const now = Date.now();
  if (now - lastUpdate > 100) {
    progress.report({ /* ... */ });
    lastUpdate = now;
  }
}
 
// Bad: Update every iteration
for (const item of items) {
  await process(item);
  progress.report({ /* ... */ });  // Too frequent!
}

2. Avoid Expensive Message Calculations

// Good: Calculate once
const total = items.length;
progress.report({
  message: `${i + 1}/$\\{total\\}`,
  increment: percentageIncrement
});
 
// Bad: Recalculate every time
progress.report({
  message: `${i + 1}/$\\{items.filter(x => x.valid).length\\}`,  // Expensive!
  increment: percentageIncrement
});

Troubleshooting

Progress not showing

Ensure withProgress is awaited and task function is async:

await api.ui.withProgress(options, async (progress) => { ... });

Percentage not updating

Make sure you’re using increment, not absolute values:

progress.report({ increment: 10 });  // Adds 10%

Cancellation not working

Check isCancellationRequested regularly and return early:

if (token.isCancellationRequested) {
  return null;
}

See Also