Codelet logoCodelet

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: