
# Chat

`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.

```ts
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:

```ts
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](#the-default-provider) and [Writing your own provider](#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](#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.

```ts
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](https://caniuse.com/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:

```ts
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:

```ts
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, so `cd` and 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.

:read-more{to="/extensions/terminal"}
