Codelet logoCodelet

Prompts, notifications and UI

Quick picks, input boxes, messages, the status bar, progress and output channels

Everything an extension can put in front of the reader that is not a view or an editor: a question, a message, a line in the status bar, a progress spinner, an output channel. All of it hangs off vscode.window and vscode.env, handed to activate the same way the rest of the namespace is.

#Asking a question

showQuickPick puts a list of choices to the reader and resolves with the one they took:

const choice = await vscode.window.showQuickPick(
  [
    { label: "Small", description: "2 spaces" },
    { label: "Large", description: "4 spaces" },
  ],
  { placeHolder: "Indent size" },
);

Escape or a click outside resolves undefined. Pass canPickMany: true for a tick per row and the whole selection as the answer:

const chosen = await vscode.window.showQuickPick(items, {
  canPickMany: true,
  placeHolder: "Which files to include",
});

showInputBox asks for a line of text. validateInput runs on every keystroke; returning a string keeps Enter from doing anything until the field says something else:

const name = await vscode.window.showInputBox({
  prompt: "Branch name",
  placeHolder: "feature/…",
  validateInput: (value) => (/^[\w./-]+$/.test(value) ? undefined : "No spaces or punctuation."),
});

Nothing typed and Enter pressed answers ""; escape answers undefined — worth telling apart if an empty value means something.

showWorkspaceFolderPick asks about the one folder there is:

const folder = await vscode.window.showWorkspaceFolderPick({ placeHolder: "Confirm the folder" });

#Holding a prompt open

showQuickPick and showInputBox are one call each: they show, wait for an answer, and hide. createQuickPick() and createInputBox() give you the same field without any of that — you own its whole lifecycle and write to it as things change:

const picker = vscode.window.createQuickPick<vscode.QuickPickItem>();
picker.placeholder = "Search files…";
picker.busy = true;

picker.onDidChangeValue(async (query) => {
  picker.busy = true;
  picker.items = await search(query); // fills the list as results come back
  picker.busy = false;
});

picker.onDidAccept(() => {
  const [item] = picker.selectedItems;
  if (item) open(item);
  picker.hide();
});

picker.show();

Reach for these over the one-shot calls whenever the list depends on what has been typed, whenever you need a spinner while something loads, or whenever you want buttons on the field itself (buttons, onDidTriggerButton) or on a row (QuickPickItem.buttons, onDidTriggerItemButton). createInputBox() is the same idea with no list under it — value, password, prompt, validationMessage written directly instead of read through options. onDidHide fires however the field closes, and dispose() when you are done with it for good.

#Messages

Three calls, one per severity, each returning the button the reader clicked:

const answer = await vscode.window.showWarningMessage(
  "Discard 3 unsaved files?",
  { modal: true, detail: "This cannot be undone." },
  "Discard",
);
if (answer === "Discard") discard();

Without items, the call still resolves — undefined if the reader dismisses it, whatever string they clicked otherwise. Without { modal: true } a message is a toast in the corner the reader can read past; with it, a dialog over the whole shell they have to answer before doing anything else. Reserve modal for something that cannot be undone — detail is its second line and only shows in a modal. Messages queue one at a time, oldest first, and if the extension that raised one is stopped before it is answered, the promise resolves undefined rather than hanging.

#The status bar

const item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
item.text = "$(check) All saved";
item.tooltip = "Every file is written to disk";
item.command = "myExtension.showDetails";
item.show();
context.subscriptions.push(item);

Higher priority sits further left within its group (Left or Right). text reads $(name) as one of codelet's icons, so "$(sync~spin) Syncing…" is a spinning icon before the words. tooltip and color (a CSS string or a ThemeColor) are both honoured; backgroundColor takes only statusBarItem.errorBackground or statusBarItem.warningBackground. command runs on click — a string id, or a Command object if you need to pass arguments. show(), hide() and dispose() are yours to call; nothing shows an item you have not called show() on.

For a message that should say itself once and go, setStatusBarMessage(text, hideAfterTimeout?) is a status bar item and a timer in one call, returning a Disposable that takes it down early.

#Progress

await vscode.window.withProgress(
  { location: vscode.ProgressLocation.Window, title: "Publishing" },
  async (progress) => {
    progress.report({ message: "Uploading assets", increment: 20 });
    await uploadAssets();
    progress.report({ message: "Writing manifest", increment: 80 });
    await writeManifest();
  },
);

ProgressLocation.Window is the only location — a line in the status bar, replaced by whatever the task last reported. increment is a percentage added to what came before; report none and the line shows a spinner with no bar, which is the honest shape for work that cannot say how far along it is. There is no cancellation: the task runs to completion or throws, and nothing here gives the reader a button to stop it early.

#Output channels

const output = vscode.window.createOutputChannel("My Extension", { log: true });
output.info("Starting up");
output.warn("Config missing a value, using the default");
output.error(new Error("Could not reach the server"));

createOutputChannel(name) alone gives you an OutputChannel: append, appendLine, replace, clear, show, hide, dispose. Passing { log: true } gives you a LogOutputChannel with trace, debug, info, warn and error instead — trace lands as debug, there being no fifth row to draw it at. A channel's lines show in the Logs panel (Logs) when a workbench mounts it, and print to the console when none is mounted — an extension does not need to know which.

#The theme

if (vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark) {
  // …
}

context.subscriptions.push(vscode.window.onDidChangeActiveColorTheme(() => rebuildWebview()));

activeColorTheme carries one member, kindColorThemeKind.Light or .Dark (.HighContrast and .HighContrastLight are declared for an extension that compares against them, but neither is ever answered). A webview is a whole document that styles itself, and it is rebuilt whenever the theme changes rather than told about it — this event is how an extension notices and does the same to anything else it drew that read the theme once.

#Icons and colors

new vscode.ThemeIcon(name) names one of codelet's own glyphs rather than drawing one — ThemeIcon.File and ThemeIcon.Folder are built in, and a second argument colors it with a ThemeColor. A name outside the set draws nothing, the same way an unknown id does in VS Code. codelet ships a few dozen glyphs of its own — files, search, folder, gear, trash, star, scm, terminal, chat, run, refresh, check, error, warning, info, add, edit, copy, download, lock, plug, and more — plus a set of VS Code's own codicon names aliased onto the same shapes (settings → gear, source-control → scm, play → run, star-full → star, and so on). There is no published list to import; the fastest way to see what is available is to look at the workbench's own chrome — every glyph in the activity bar, the status bar and a tree row is one of these names, $(name) in a status bar item's text included.

new vscode.ThemeColor(id) names a color from a fixed table rather than VS Code's full theme — an id outside it paints nothing. What is in the table:

IdWhat it colors
gitDecoration.addedResourceForegroundAn added file's badge and name.
gitDecoration.modifiedResourceForegroundA modified file's badge and name.
gitDecoration.deletedResourceForegroundA deleted file's badge and name.
gitDecoration.renamedResourceForegroundA renamed file's badge and name.
gitDecoration.untrackedResourceForegroundAn untracked file's badge and name.
gitDecoration.ignoredResourceForegroundAn ignored file's badge and name.
gitDecoration.conflictingResourceForegroundA conflicting file's badge and name.
list.deemphasizedForegroundMuted row text.
list.errorForeground / list.warningForegroundRow text at that severity.
statusBarItem.errorBackgroundStatusBarItem.backgroundColor.
statusBarItem.warningBackgroundStatusBarItem.backgroundColor.
problemsErrorIcon.foregroundAn error glyph.
problemsWarningIcon.foregroundA warning glyph.
problemsInfoIcon.foregroundAn info glyph.
editorGutter.addedBackgroundThe quick diff gutter's added bar.
editorGutter.modifiedBackgroundThe quick diff gutter's modified bar.
editorGutter.deletedBackgroundThe quick diff gutter's deleted wedge.
await vscode.env.clipboard.writeText(url);
const copied = await vscode.env.clipboard.readText();

const opened = await vscode.env.openExternal(vscode.Uri.parse("https://example.com"));
if (!opened) {
  // a popup the browser blocked, with no gesture behind it
}

env.clipboard wraps navigator.clipboard; a browser that offers none, or a read the reader has not allowed, rejects rather than answering with nothing. env.openExternal opens a new tab and resolves whether it actually opened — false is a blocked popup, worth telling the reader about rather than swallowing.