Tasks, source control and terminals
Registering tasks, a source control and a terminal profile, and who draws them
Three subsystems share one shape. vscode is the producing half — the calls any extension uses
to say "here is a task", "here is what changed", "here is a shell you can open." codelet is the
consuming half: a small, separate namespace that reads back what was registered, used only by an
extension that draws a pane for it. Writing a task provider, a source control or a terminal
profile needs nothing from codelet at all; the built-in Tasks pane, Source Control pane and
terminal each use it to become the thing that shows what you registered.
#Tasks
vscode.tasks.registerTaskProvider("greet", {
provideTasks: () => [
new vscode.Task(
{ type: "greet" },
vscode.TaskScope.Workspace,
"Say hi",
"greet",
new vscode.CustomExecution(async () => {
const shell = codelet.terminal.profiles().at(-1);
const pty = (await shell?.provide())?.options.pty;
if (!pty) throw new Error("Nothing here can run a shell.");
return {
onDidWrite: pty.onDidWrite,
onDidClose: pty.onDidClose,
open: (dimensions) => {
pty.open(dimensions);
pty.handleInput?.("echo hi\n");
},
close: () => pty.close(),
};
}),
),
],
resolveTask: (task) => task,
});A Task is a TaskDefinition (your own type plus whatever else identifies one of yours), a
TaskScope or WorkspaceFolder saying whose it is, a name, a source string shown beside it,
and an optional execution. TaskGroup.Build, .Test, .Clean and .Rebuild are there for a
task to say what it is for. CustomExecution is the only execution codelet answers — a page has
nothing to spawn a shell command or a program with, so the callback hands back a Pseudoterminal
instead of naming one. In practice that means running a task is typing a command line into a
shell another extension contributed: the example above reaches for whatever
codelet.terminal.profiles() last offered and writes the line into it once the tab opens.
vscode.tasks.executeTask(task) runs one and resolves with a TaskExecution — .terminate()
ends it by closing its terminal, the same as the reader closing the tab. fetchTasks(filter?)
asks every registered provider and returns what they offer; taskExecutions is what is
currently running, and onDidStartTask / onDidEndTask fire around each.
codelet/extensions/npm reads a workspace's package.json scripts as tasks this way, typing
npm run <script> (or whichever manager the lockfile implies) into a shell and reporting when
the script's own line finishes rather than when the tab closes. codelet/extensions/tasks is the
activity bar pane that lists whatever every provider offers and runs one on a click — it reads
vscode.tasks the same way any other extension would, with no codelet namespace of its own.
#Source control
const control = vscode.scm.createSourceControl("git", "Git", vscode.Uri.file("/"));
control.inputBox.placeholder = "Message (⌘Enter to commit)";
control.acceptInputCommand = { command: "git.commit", title: "Commit" };
const changes = control.createResourceGroup("changes", "Changes");
changes.resourceStates = [
{
resourceUri: vscode.Uri.file("/src/index.ts"),
decorations: {
tooltip: "Modified",
iconPath: new vscode.ThemeIcon(
"scm",
new vscode.ThemeColor("gitDecoration.modifiedResourceForeground"),
),
},
},
];
context.subscriptions.push(control);createSourceControl(id, label, rootUri?) returns a SourceControl: the input box above the
rows, one or more SourceControlResourceGroups each holding SourceControlResourceStates, a
count for the activity bar badge, and statusBarCommands for a branch name in the footer.
Writing any of it — group.resourceStates = [...], control.count = 3 — repaints whatever is
drawing it, with nothing further to call.
Nothing draws a source control by itself. codelet.scm.sources() is the read side — every
registered SourceControl with its groups attached — and onDidChangeSources is when to ask
again; codelet.scm.actions(menu, control, group?, state?) resolves the buttons contributed to
one of the four scm/* menus (scm/title, scm/inputBox, scm/resourceGroup/context,
scm/resourceState/context) for whatever is being drawn. codelet/extensions/scm
(Scm) is the pane that calls both — mount it for a registered
source control to show anywhere at all.
quickDiffProvider is here: answer it with where a file's unedited text comes from and the
editor paints a quick diff gutter beside the lines that changed, the same three bars VS Code
draws. The one member missing is scm.inputBox, VS Code's own deprecated global for the box of
whichever source control was made last — SourceControl.inputBox is the box.
#Terminals
vscode.window.registerTerminalProfileProvider("my-shell", {
provideTerminalProfile: () => new vscode.TerminalProfile({ name: "My Shell", pty: makeShell() }),
});paired with a manifest declaring it:
contributes: { terminal: { profiles: [{ id: "my-shell", title: "My Shell" }] } },is what a normal extension needs — a Pseudoterminal you implement (onDidWrite, open,
close, handleInput), answered for the id your manifest named. window.createTerminal(options)
makes a Terminal out of a Pseudoterminal you already hold, for a shell that is yours from the
start rather than opened by the reader: sendText, show, hide, dispose, and
onDidCloseTerminal for when it ends, whichever end that was.
The consuming half is for the extension that draws terminal tabs — one ships,
codelet/extensions/terminal (Terminal) — and nothing else needs
it. codelet.terminal.profiles() and .terminals() are what a manifest declared and what
createTerminal made; .tabs() is a different list again, what the terminal extension is
actually showing, published through .present(tabs) and read back by anything else that cares
what tab is open. .expect() / .expecting() let another extension hold the panel open for tabs
that are still arriving rather than let the terminal open one of its own first —
codelet/extensions/live uses this for a guest whose shared shells are still crossing the wire.
.focus(id) and onDidShowTerminal / onDidHideTerminal round out which tab is active and
whether the panel itself is open.
#Worked example
A Pseudoterminal that echoes what is typed, registered as a profile:
import { defineExtension } from "codelet/extensions";
export const echo = defineExtension({
manifest: {
name: "echo-terminal",
contributes: { terminal: { profiles: [{ id: "echo", title: "Echo" }] } },
},
activate(context, vscode) {
context.subscriptions.push(
vscode.window.registerTerminalProfileProvider("echo", {
provideTerminalProfile() {
const written = new vscode.EventEmitter<string>();
let line = "";
return new vscode.TerminalProfile({
name: "Echo",
pty: {
onDidWrite: written.event,
open: () => written.fire("Type something.\r\n$ "),
close: () => written.dispose(),
handleInput: (data) => {
if (data === "\r") {
written.fire(`\r\n${line}\r\n$ `);
line = "";
} else if (data === "\u007f") {
line = line.slice(0, -1);
written.fire("\b \b");
} else {
line += data;
written.fire(data);
}
},
},
});
},
}),
);
},
});Mount it alongside codelet/extensions/terminal and "Echo" shows up beside every other shell in
the New Terminal picker. Below is codelet/extensions/terminal itself running just-bash, a real
Pseudoterminal over the workbench's own files — the same interface the example above
implements: