俺たちのVS Code用Chat Extensionの明日はどっちだ

# TL;DR

- VSCodeには、Language ModelやChatを利用するためのAPIが生えており、このAPIを利用することで、生成AI・GitHub Copilotを利用したExtensionの開発が可能
- TISとして[Promptis](https://marketplace.visualstudio.com/items?itemName=tis.promptis)というExtensionをVS Code Marketplaceに公開、[OSSとしても公開](https://github.com/Fintan-contents/promptis/)したものの、メンテできる人を増やさないといけないので、ここに学び方みたいなやつを書いた
- 俺たちの明日はどっちだ

# VS Code Chat Extension を作るためには

まずは公式ドキュメントを読みましょう。
一次情報源にあたれっていうのはおじさんが新入社員の頃からよく言われました。おじさんとの約束だぞ。

## 読むべきドキュメント

前提として、VS Codeは開発者用のドキュメントも充実しています。こういうQiitaとかより、まずは本家にあたりましょう。[Extension Guide](https://code.visualstudio.com/api/extension-guides/overview)にExtensionに関するドキュメントがあるので、まずはこの辺りを読み進めると良いでしょう。

その中でも、GitHub Copilotを利用していくために重要なのは、Language Modelの扱い方と、GitHub Copilot ChatのようなChatを実現するアーキテクチャ。
それぞれのドキュメントはここにまとまっています。

| テーマ | ドキュメント | チュートリアル |
| :- | :- | :- |
| Language Model | [Language Model API](https://code.visualstudio.com/api/extension-guides/language-model) | [Tutorial: Generate AI-powered code annotations by using the Language Model API](https://code.visualstudio.com/api/extension-guides/language-model-tutorial) |
| Chat | [Chat extensions](https://code.visualstudio.com/api/extension-guides/chat) | [Tutorial: Build a code tutorial chat participant with the Chat API](https://code.visualstudio.com/api/extension-guides/chat-tutorial) |

## Language Model

生成AIを利用するということは大体にしてモデルを利用することになるわけですが、[VS Code API](https://code.visualstudio.com/api/references/vscode-api)の1つ、 [`selectChatModels`](https://code.visualstudio.com/api/references/vscode-api#lm)を呼び出すことで利用可能なモデルを取得できます。
モデルは[`LanguageModelChat`](https://code.visualstudio.com/api/references/vscode-api#LanguageModelChat)として表現されており、そのクラスが持つメソッド`sendRequest`経由で、モデルに対してプロンプトや会話履歴が送信できます。
この辺りは、[チュートリアル](https://code.visualstudio.com/api/extension-guides/language-model-tutorial)にある以下のスニペットでイメージが掴めると思います。力強く掴もう、未来をその手で。

```typescript
const disposable = vscode.commands.registerTextEditorCommand(
  'code-tutor.annotate',
  async (textEditor: vscode.TextEditor) => {
    // Get the code with line numbers from the current editor
    const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor);

    // select the 4o chat model
    let [model] = await vscode.lm.selectChatModels({
      vendor: 'copilot',
      family: 'gpt-4o'
    });

    // init the chat message
    const messages = [
      vscode.LanguageModelChatMessage.User(ANNOTATION_PROMPT),
      vscode.LanguageModelChatMessage.User(codeWithLineNumbers)
    ];

    // make sure the model is available
    if (model) {
      // send the messages array to the model and get the response
      let chatResponse = await model.sendRequest(
        messages,
        {},
        new vscode.CancellationTokenSource().token
      );

      // handle chat response
      await parseChatResponse(chatResponse, textEditor);
    }
  }
);
```

## Chat

チャットについても様々な観点がありますが、ユーザからチャット経由でリクエストが送信されたら、それに応答しないといけません。これは、はい、あなたの想像通りリクエストハンドラとして実装する形になります。

### Chatのリクエストハンドラ

ハンドラの型は[`ChatRequestHandler`](https://code.visualstudio.com/api/references/vscode-api#ChatRequestHandler)として定義されているので、これをそのまま実装することになります。ユーザがチャットで入力した内容は、ハンドラの第一引数である[`request`](https://code.visualstudio.com/api/references/vscode-api#ChatRequest)に含まれているし、その応答は第3引数である[stream](https://code.visualstudio.com/api/references/vscode-api#ChatResponseStream)に書き出すことでユーザに届けられます。基本的には、requestをゴニョゴニョして結果をstreamに書き出せば良いわけで、その"ゴニョゴニョ"がLLMの呼び出しとかになるわけですね。

もちろんハンドラを定義するだけでは、VS Codeにハンドラの存在を認識させられません。ハンドラの登録メソッドも当然用意されていて、その勇猛たる名称を[`createChatParticipant`](https://code.visualstudio.com/api/references/vscode-api#chat)といいます。このメソッドを利用して、[こんな感じ](https://github.com/Fintan-contents/promptis/blob/v1.0.1/src/extension.ts#L9
)で、VS Codeに登録すれば良い。

```typescript
  // Participantと、当該Participantへのメンションに反応するハンドラを設定
  const promptis = vscode.chat.createChatParticipant(PARTICIPANT_ID, chatHandler);
```

## Participantってなんだよ

ここでいきなり`Participant`というキーワードが現れるけど、これはチャットのメンション先を表現します。例えばみなさんがよくお使いの`@workspace`とか`@vscode`とかがParticipantに該当します。ChatタイプのVS Code Exntensionは、基本的にこのParticipantを新たに定義することになり、Promptisなら`@promptis`というParticipantを定義しています。

では定義はどこにあるかというと、`package.json`にあります。Promptisでは[ここ](https://github.com/Fintan-contents/promptis/blob/v1.0.1/package.json#L29)ですね。

これに限らず、Extensionに必要な設定項目や、チャット上で利用できるコマンド（`/tests`や`/explain`)など、VS Codeに認識させないといけないような内容は、package.json上で定義する仕様になっています。
この辺りのリファレンスは[Extension Manifest](https://code.visualstudio.com/api/references/extension-manifest)にあるので読んでみると良いでしょう。

## 参考になるソースはあるんか？

この手のChat Extensionについては、Microsoftが公開している[chat-example](https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample)を見るのが早いです。
VS Codeは進化が早く、月次リリースに伴って様々なAPIが生えていきますが、上記exampleも頻繁にアップデートされていきます。素晴らしい。前に読んだ内容が今や消失しています。苦しい。

例えば最近だと、VS Codeに[Tool Calling](https://code.visualstudio.com/api/extension-guides/tools)のAPIが生えたわけですが、chat-exampleにはもう反映されています。ソースを読むと使い方のイメージがわかりやすくて良いですね。

# Extensionのテストどうするのよ

Extensionのテストどうやるんか、という疑問を呈する諸兄に朗報ですが、テスト実行方法の整備は[Testing Extensions](https://code.visualstudio.com/api/working-with-extensions/testing-extension)に書いてあるので、まずはこれを読みましょう。

## テストの仕組み

テスト自体は、別のVS Codeインスタンスを起動した後、そこに開発したExtensionを読み込ませて、テストコードを実行するという仕組みなっています。この VS Codeインスタンスは「Extension Development Host」と呼ばれています。

まぁ、Microsoftが[vscode-test-cli](https://github.com/microsoft/vscode-test-cli)というCLIツールを用意してくれているので、難しいことはあまり考えなくて良いでしょう。
実際、Promptisでも[単に`vscode-test`を呼び出す](https://github.com/Fintan-contents/promptis/blob/v1.0.1/package.json#L120-L121)ことで、用意したテストコードを実行するだけとなっています。以下のような感じ。実行したら、指定したバージョンのVS Codeをダウンロードしてくれて、それをExtension Development Hostとして起動してくれるという感じ。

```json
    "test:linux": "xvfb-run -a vscode-test --coverage --coverage-output --coverage-reporter html json-summary",
    "test:win32:darwin": "vscode-test --coverage --coverage-output --coverage-reporter html json-summary",
```

上記npm scriptを見ていただければわかるように、coverageを計測しレポートしてくれる機能も内包しているので至れり尽くせりですね。

# 苦労したところ

実は結構苦労したという実感があるのは、カバレッジのバッジ実装でした。
以下の画像はREADME.mdにあるバッジだけど、Testのバッジと比べて、coverageのバッジの解像度の低さがお分かりになると思います。実は`Test`のバッジはSVGなんだけど、coverageのバッジはPNGになっている。

![badge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29311/b4d7e971-fe68-1b71-419d-c327cb719f44.png)

これには悲しい裏話がありまして、VS Code Marketplace上でSVGを表示できるのは、Marketplace側のホワイトリストで許可されたドメイン配下のみという仕様になっています。もしホワイトリスト以外のドメイン配下でSVGをホストし、それをREADMEに配置しようとすると、ビルドが失敗する。この仕様に気づくのに、結構かかった覚えがある。

- [Constraints for rendering SVG images](https://github.com/Microsoft/vscode-docs/blob/vnext/release-notes/v1_14.md#constraints-for-rendering-svg-images)

coverageのバッジはgithub.io配下に配置しているんだけど、github.ioはホワイトリストに入っていない。なので、GitHub Actionsの中でSVGをPNGに変換するという謎の処理を挟むことになりました。

Coverallsなどの外部サービスを利用するとこの辺り楽になるはずなのだけれど、突貫で整備が必要だったので、そういった外部サービスのリスクアセスメントをし会社に認めてもらうよりも自分で変換したほうが早かった。助けてくれ誰か…。

# 蛇足ですが

先に述べたようにVS Codeの進化は相当早くて、Extension用のTool CallingのAPIは生えたし、Terminalの内容も読み取れるので、ターミナル上のエラーを読んでコードを自動修正したりするエージェント的な拡張が次々に出てくるでしょう。すでに出てきてますね。はい。
で、生成AIを中心にしたこのようなExtensionには、現時点では人の介在・AIへのフィードバックは必須となっており、そのインタフェースとしてチャットが用いられる時代はまだまだ続くと思われます。

この辺りはMicrosoftも意識しているようで、今回エントリで書いたようなChat Extensionを簡単に作れるようなユーテリティが公開されておりました。これでさらにChat Extensionの数は増えていくのではないでしょうか。

- [@vscode/chat-extension-utils](https://github.com/microsoft/vscode-chat-extension-utils/blob/main/README.md)

Promptisもこちらを利用するようにしないといつしか追随ができなくなると思われますので、Pull Requestお待ちしております。