Chat
A conversation about the workspace in the secondary side bar, answered by a pluggable ChatProvider
chat adds a conversation about the workspace to the secondary side bar. The file tree, the open
file and the conversation stay on screen together, instead of swapping the sidebar for a chat view.
import { Workbench, FileSystem } from "codelet/workbench";
import { chat } from "codelet/extensions/chat";
const workbench = new Workbench({
parent: document.getElementById("app")!,
fs: new FileSystem({ "/README.md": "# Hello" }),
extensions: [chat()],
});#Opening it
chat has no icon of its own in the activity bar — nothing in the secondary side bar does. Open
it from the command palette (Toggle Secondary Side Bar, or Chat: Focus Chat), or click the
chat icon at the right of the tab strip, above whatever file is open. Chat: New Chat is also in
the palette, and opens the bar too.
Another extension can ask a question on the reader's behalf:
vscode.commands.executeCommand("chat.ask", "What does this file do?");#In the pane
The message box takes focus when the pane opens, and again after you send. Enter sends; Shift+Enter starts a new line. The box grows with what you type, up to a few lines, then scrolls.
While an answer streams in, the transcript follows it to the bottom — until you scroll up yourself, so a new token can't drag you back down. A ↓ Latest button appears while you're scrolled away, and jumps you back.
Send disables on an empty box, and turns into Stop while a turn is being written. A model that needs downloading says which one, and what it costs, before you ask anything. A failure shows what happened with a Try again button next to it, which re-asks the same question rather than leaving it twice in the transcript.
Answers render as markdown — fences, lists, tables, links. It's parsed to elements, not to HTML,
so nothing a model writes can become markup in your page. A link is only followed for http:,
https: or mailto: URLs, and opens in a new tab.
#Options
| Option | Type | Default | Description |
|---|---|---|---|
provider | ChatProvider | webllm() | What answers the conversation. See The default provider and Writing your own provider. |
instructions | string | a short built-in system prompt | What the model is told it is, before its tools are described. Which file is open, and where bash starts, is appended either way. |
shell | string | the profile that looks like a shell, or the first one there is | Which contributed terminal profile the bash tool spawns, by id or title. See Tools. |
#The default provider
webllm() runs a model on the reader's own GPU. Nothing is fetched until the first question, and
there's no server behind it. Weights are cached in the browser's Cache Storage, so a second
question — even after a reload — reuses what already downloaded.
import { chat, webllm } from "codelet/extensions/chat";
chat({ provider: webllm({ model: "Qwen2.5-Coder-3B-Instruct-q4f16_1-MLC" }) });| Option | Type | Default | Description |
|---|---|---|---|
models | readonly { id: string; label: string; size?: string; note?: string }[] | a shortlist of 7 models | What the model picker offers. Ids are WebLLM's own. |
model | string | "Qwen3.5-4B-q4f16_1-MLC" | Which model loads before the reader picks another. |
cdn | string | "https://esm.sh" | Where the WebLLM engine is fetched from. |
Warning
webllm() needs WebGPU — Chrome, Edge or Safari 26. Without it, the
pane shows a plain "not supported" status instead of an answer. The default model is around 2.4 GB
to download on the first question, and wants roughly 3.9 GB of VRAM to run.
#Writing your own provider
Pass provider to run the conversation through your own backend. A ChatProvider is asked once
for a ChatSession, and never asked where it runs:
interface ChatProvider {
name: string;
open(context: ChatContext): ChatSession | PromiseLike<ChatSession>;
}
interface ChatContext {
tools: readonly ChatTool[];
instructions(): string;
}
interface ChatSession {
ask(messages: readonly ChatMessage[], reply: ChatReply, signal: AbortSignal): Promise<void>;
models?: readonly ChatModel[];
model?(): string;
select?(id: string): void;
close?(): void;
}ask answers one turn through reply: reply.text(delta) for each piece of the answer as it
arrives, reply.call(index, call) once as a tool call is made and again once its output is
filled in, and reply.status(status) for { phase: "loading" | "ready" | "unsupported" | "error", ... }.
A skeleton that calls a backend running its own tool loop:
import type { ChatProvider } from "codelet/extensions/chat";
export const myApi = (): ChatProvider => ({
name: "My API",
open(context) {
return {
async ask(messages, reply, signal) {
const res = await fetch("/api/chat", {
method: "POST",
signal,
body: JSON.stringify({ messages, instructions: context.instructions() }),
});
const { text, call } = await res.json();
reply.text(text);
if (call) {
const tool = context.tools.find((one) => one.name === call.name);
const output = tool ? await tool.run(JSON.parse(call.input)) : "error: no such tool";
reply.call(0, { ...call, output });
}
},
};
},
});A provider whose model can call more than one tool per turn loops over context.tools and feeds
each result back to the model, the way webllm() does — ask only has to resolve once the turn
is over.
#Tools
The model can act on the workspace through five tools:
read_file— read a text file.list_dir— list a directory's entries.find_files— find paths by glob, for when the model shouldn't guess at one.write_file— write a whole file, creating missing directories.bash— run one shell command and read what it printed. The shell stays open for the whole conversation, socdand exported variables carry across calls.
The first four work over workspace.fs, so a file the model writes shows up in the explorer
immediately — nothing is worked on in a copy off to one side. They take absolute workspace paths.
bash starts in /workspace, where those same files are mounted, and the model is told so along
with which file the reader has open.
bash spawns a contributed terminal profile — whatever this workbench registered through
contributes.terminal.profiles, such as justBash. shell picks which one, by id or title, when
there's more than one; without it, the profile that looks like a shell is picked, or whichever
there is. A workbench that contributes no terminal profile has no bash tool at all, rather than
one that always fails.