Shows progress for background tasks in Visual Studio extensions. Covers Task Status Center, status bar, threaded wait dialog, and WorkProgress for out-of-process and in-process approaches.
How this skill is triggered — by the user, by Claude, or both
Slash command
/vs-extensibility-skills:showing-background-progressThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
When your extension performs long-running work, you must show progress to the user so they know the operation is in progress and how far along it is. Visual Studio provides several progress UI mechanisms — choosing the right one depends on how much attention the task demands.
When your extension performs long-running work, you must show progress to the user so they know the operation is in progress and how far along it is. Visual Studio provides several progress UI mechanisms — choosing the right one depends on how much attention the task demands.
Without progress feedback, users assume the IDE is frozen and may force-quit Visual Studio, losing unsaved work. The VS progress APIs handle threading, cancellation, and UI placement automatically — building your own progress UI from scratch means duplicating all of that. The Task Status Center is the standard location users have learned to check (NuGet restore, project load, and indexing all use it), so extensions that report progress there feel native rather than bolted on.
When to use this vs. alternatives:
| Mechanism | Blocking? | When to use | User attention |
|---|---|---|---|
| Status bar progress | No | Quick background tasks (< 10 s) | Low — user doesn't need to act |
| Task Status Center | No | Long-running background tasks (indexing, analysis, sync) | Low — appears in bottom-left corner, user can check anytime |
| Threaded Wait Dialog | Semi-blocking | Tasks the user must wait for but shouldn't freeze the IDE | Medium — modal dialog appears after a delay |
| ProgressReporter (Extensibility) | No | Out-of-process extensions that need non-blocking progress | Low — displayed in the Task Status Center |
Rule of thumb: Prefer non-blocking progress (status bar or Task Status Center). Use the Threaded Wait Dialog only when the user initiated an action and must wait for the result before continuing.
Use ShellExtensibility.StartProgressReportingAsync() to start progress reporting. It returns a ProgressReporter (implements IProgress<ProgressStatus> and IDisposable) that displays progress in the VS Task Status Center.
NuGet package: Microsoft.VisualStudio.Extensibility
Key namespaces: Microsoft.VisualStudio.Extensibility.Shell, Microsoft.VisualStudio.RpcContracts.ProgressReporting
[VisualStudioContribution]
internal class AnalyzeCommand : Command
{
public AnalyzeCommand(VisualStudioExtensibility extensibility)
: base(extensibility) { }
public override CommandConfiguration CommandConfiguration => new("Analyze Solution")
{
Placements = [CommandPlacement.KnownPlacements.ToolsMenu],
Icon = new(ImageMoniker.KnownValues.Search, IconSettings.IconAndText),
};
public override async Task ExecuteCommandAsync(
IClientContext context, CancellationToken ct)
{
using ProgressReporter progress = await this.Extensibility.Shell()
.StartProgressReportingAsync("Analyzing solution", ct);
progress.Report(new ProgressStatus(percentComplete: 0, "Starting analysis..."));
await Task.Delay(1000, ct); // Step 1
progress.Report(new ProgressStatus(percentComplete: 33, "Scanning files..."));
await Task.Delay(1000, ct); // Step 2
progress.Report(new ProgressStatus(percentComplete: 66, "Processing results..."));
await Task.Delay(1000, ct); // Step 3
progress.Report(new ProgressStatus(percentComplete: 100, "Complete"));
}
}
Pass ProgressReporterOptions to make the task cancellable by the user:
using ProgressReporter progress = await this.Extensibility.Shell()
.StartProgressReportingAsync(
"Long running task",
new ProgressReporterOptions(isWorkCancellable: true),
ct);
progress.Report(new ProgressStatus(percentComplete: 0, "Working..."));
Pass null for percentComplete to show an indeterminate spinner:
progress.Report(new ProgressStatus(percentComplete: null, "Scanning..."));
The Community Toolkit provides three progress mechanisms via simple static APIs.
NuGet package: Community.VisualStudio.Toolkit
Key namespace: Community.VisualStudio.Toolkit
The simplest option. Shows a progress bar integrated into the status bar at the bottom of the VS window. Non-blocking — the user can continue working.
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
var totalSteps = 5;
for (int currentStep = 1; currentStep <= totalSteps; currentStep++)
{
await VS.StatusBar.ShowProgressAsync(
$"Processing step {currentStep}/{totalSteps}",
currentStep,
totalSteps);
await Task.Delay(1000); // Simulate work
}
// Progress automatically clears when currentStep == totalSteps
}
To show a simple text message without a progress bar:
await VS.StatusBar.ShowMessageAsync("Operation completed successfully.");
To start and stop a status bar animation:
await VS.StatusBar.StartAnimationAsync(StatusAnimation.Build);
// ... do work ...
await VS.StatusBar.EndAnimationAsync(StatusAnimation.Build);
The Task Status Center (TSC) is the icon area at the bottom-left of the status bar. It's designed for long-running background tasks (like indexing, NuGet restores, etc.). Tasks are listed when the user clicks the icon. Non-blocking and supports cancellation.
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
await StartBackgroundTaskAsync();
}
private async Task StartBackgroundTaskAsync()
{
IVsTaskStatusCenterService tsc = await VS.Services.GetTaskStatusCenterAsync();
var options = default(TaskHandlerOptions);
options.Title = "Analyzing solution";
options.ActionsAfterCompletion = CompletionActions.None;
TaskProgressData data = default;
data.CanBeCanceled = true;
ITaskHandler handler = tsc.PreRegister(options, data);
Task task = DoLongRunningWorkAsync(data, handler);
handler.RegisterTask(task);
}
private async Task DoLongRunningWorkAsync(TaskProgressData data, ITaskHandler handler)
{
float totalSteps = 5;
for (float currentStep = 1; currentStep <= totalSteps; currentStep++)
{
// Check for cancellation
if (handler.UserCancellation.IsCancellationRequested)
{
data.PercentComplete = (int)(currentStep / totalSteps * 100);
data.ProgressText = "Cancelled";
handler.Progress.Report(data);
return;
}
await Task.Delay(1000);
data.PercentComplete = (int)(currentStep / totalSteps * 100);
data.ProgressText = $"Step {currentStep} of {totalSteps} completed";
handler.Progress.Report(data);
}
}
Task Status Center — key concepts:
| Concept | Description |
|---|---|
TaskHandlerOptions.Title | Text shown in the TSC list |
TaskProgressData.CanBeCanceled | If true, user can cancel via the TSC UI |
TaskProgressData.PercentComplete | 0–100 integer for the progress bar |
TaskProgressData.ProgressText | Dynamic text shown under the title |
CompletionActions.None | Task disappears from the list when done |
CompletionActions.RetainOnFaulted | Keep the entry visible if the task throws |
handler.UserCancellation | CancellationToken triggered when user clicks Cancel |
handler.PreRegister / RegisterTask | Pre-register shows the task immediately; RegisterTask links the actual Task |
A modal dialog that appears only after a configurable delay (e.g., 1 second). It writes progress to the status bar while waiting for the delay, then shows a dialog. Use this when the user initiated an action and must wait for it to finish.
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
var factory = await VS.Services.GetThreadedWaitDialogAsync() as IVsThreadedWaitDialogFactory;
IVsThreadedWaitDialog4 dialog = factory.CreateInstance();
// Show the dialog after 1 second of waiting
dialog.StartWaitDialog(
szWaitCaption: "Processing",
szWaitMessage: "Please wait while the operation completes...",
szProgressText: "",
varStatusBmpAnim: null,
szStatusBarText: "Processing...",
iDelayToShowDialog: 1, // seconds before the dialog appears
fIsCancelable: true,
fShowMarqueeProgress: false);
var totalSteps = 5;
for (int currentStep = 1; currentStep <= totalSteps; currentStep++)
{
dialog.UpdateProgress(
szUpdatedWaitMessage: "Please wait...",
szProgressText: $"Step {currentStep}/{totalSteps}",
szStatusBarText: $"Step {currentStep}/{totalSteps}",
iCurrentStep: currentStep,
iTotalSteps: totalSteps,
fDisableCancel: false,
out bool cancelled);
if (cancelled) break;
await Task.Delay(1000); // Simulate work
}
// Dismiss the dialog
(dialog as IDisposable).Dispose();
}
Important: The Threaded Wait Dialog keeps the UI thread responsive (unlike a raw
Thread.Sleep) — VS continues pumping messages. But the modal dialog blocks user interaction with the IDE.
The raw VSSDK APIs for the same three mechanisms. Use these when you don't have the Community Toolkit.
NuGet package: Microsoft.VisualStudio.SDK
Key namespaces: Microsoft.VisualStudio.Shell, Microsoft.VisualStudio.Shell.Interop
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
IVsStatusbar statusBar = (IVsStatusbar)Package.GetGlobalService(typeof(SVsStatusbar));
uint cookie = 0;
int totalSteps = 5;
// Initialize the progress bar
statusBar.Progress(ref cookie, 1, "", 0, 0);
for (uint currentStep = 1; currentStep <= totalSteps; currentStep++)
{
statusBar.Progress(ref cookie, 1, $"Step {currentStep}/{totalSteps}", currentStep, (uint)totalSteps);
await Task.Delay(1000);
}
// Clear the progress bar
statusBar.Progress(ref cookie, 0, "", 0, 0);
Status bar text only (no progress bar):
IVsStatusbar statusBar = (IVsStatusbar)Package.GetGlobalService(typeof(SVsStatusbar));
statusBar.SetText("Operation completed.");
private async Task StartBackgroundTaskAsync()
{
var tsc = (IVsTaskStatusCenterService)await AsyncServiceProvider.GlobalProvider
.GetServiceAsync(typeof(SVsTaskStatusCenterService));
var options = default(TaskHandlerOptions);
options.Title = "Indexing project files";
options.ActionsAfterCompletion = CompletionActions.None;
TaskProgressData data = default;
data.CanBeCanceled = true;
ITaskHandler handler = tsc.PreRegister(options, data);
Task task = DoLongRunningWorkAsync(data, handler);
handler.RegisterTask(task);
}
private async Task DoLongRunningWorkAsync(TaskProgressData data, ITaskHandler handler)
{
float totalSteps = 10;
for (float currentStep = 1; currentStep <= totalSteps; currentStep++)
{
if (handler.UserCancellation.IsCancellationRequested) return;
await Task.Delay(500);
data.PercentComplete = (int)(currentStep / totalSteps * 100);
data.ProgressText = $"Indexed {currentStep} of {totalSteps} files";
handler.Progress.Report(data);
}
}
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
var factory = (IVsThreadedWaitDialogFactory)Package.GetGlobalService(typeof(SVsThreadedWaitDialogFactory));
IVsThreadedWaitDialog2 dialog;
factory.CreateInstance(out dialog);
dialog.StartWaitDialog(
"Processing", // caption
"Working on it...", // message
"", // progress text
null, // status bar animation
"", // status bar text
1, // delay in seconds
true, // cancelable
true); // show marquee
int totalSteps = 5;
for (int i = 1; i <= totalSteps; i++)
{
bool cancelled;
dialog.HasCanceled(out cancelled);
if (cancelled) break;
dialog.UpdateProgress(
"Working...",
$"Step {i}/{totalSteps}",
$"Step {i}/{totalSteps}",
i,
totalSteps,
false, // disable cancel
out cancelled);
await Task.Delay(1000);
}
dialog.EndWaitDialog(out int cancelledResult);
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
// Start a Task Status Center entry for the background work
IVsTaskStatusCenterService tsc = await VS.Services.GetTaskStatusCenterAsync();
var options = default(TaskHandlerOptions);
options.Title = "Deploying extension";
options.ActionsAfterCompletion = CompletionActions.RetainOnFaulted;
TaskProgressData data = default;
data.CanBeCanceled = true;
ITaskHandler handler = tsc.PreRegister(options, data);
Task task = Task.Run(async () =>
{
try
{
float totalSteps = 3;
data.ProgressText = "Packaging...";
data.PercentComplete = 0;
handler.Progress.Report(data);
await PackageAsync(handler.UserCancellation);
data.ProgressText = "Uploading...";
data.PercentComplete = 33;
handler.Progress.Report(data);
await UploadAsync(handler.UserCancellation);
data.ProgressText = "Verifying...";
data.PercentComplete = 66;
handler.Progress.Report(data);
await VerifyAsync(handler.UserCancellation);
data.ProgressText = "Done!";
data.PercentComplete = 100;
handler.Progress.Report(data);
await VS.StatusBar.ShowMessageAsync("Deployment completed successfully.");
}
catch (OperationCanceledException)
{
await VS.StatusBar.ShowMessageAsync("Deployment cancelled.");
}
catch (Exception ex)
{
await ex.LogAsync();
await VS.StatusBar.ShowMessageAsync("Deployment failed. See Output window.");
}
});
handler.RegisterTask(task);
}
Thread.Sleep or synchronous waits during long operations. Use async/await and one of the progress APIs above.CompletionActions.RetainOnFaulted to keep failed tasks visible in the Task Status Center so the user can see the error.progress.Report() with the current step.StartProgressReportingAsync and then progress.Report(...) with incremented step values. The Task Status Center won't display until the first Report call.ProgressReporter was disposed (or the using block ended) before the work completed. Ensure the using block wraps the entire async operation.CancellationToken from the progress API through your entire call chain and checking it between steps with cancellationToken.ThrowIfCancellationRequested().VS.StatusBar.ClearAsync() (Toolkit) or reset the status bar after the operation completes.Do NOT use
Thread.Sleep()or synchronous blocking waits — they freeze the UI thread. Useawait Task.Delay()and async patterns.
Do NOT use the Threaded Wait Dialog for operations the user didn't trigger — it's semi-blocking modal. For background tasks, use the Task Status Center.
Do NOT show status bar progress for operations >10 seconds — the status bar is easy to miss and has no cancellation. Use the Task Status Center.
Do NOT forget cancellation support for any operation >a few seconds — always pass
CancellationTokenthrough the call chain.
Do NOT silently swallow task failures — use
CompletionActions.RetainOnFaultedso failed tasks remain visible.
npx claudepluginhub madskristensen/vs-agent-plugins --plugin vs-extensibility-skillsCorrectly handle async/await patterns, thread switching, and JoinableTaskFactory usage in Visual Studio extensions, covering VisualStudio.Extensibility, VSIX Community Toolkit, and legacy VSSDK approaches.
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.