Zenn
😊

200行のTypeScriptでmin-clineを実装する

2025/02/27に公開
25

はじめに

VSCode拡張のClineがシンプルなアーキテクチャで実現されていることを説明するために、VSCode拡張のAPIだけを使った「ワークスペース内のエラーが発生している箇所をLLMで自動修正する」タスクのみを実行する最小のClineを作りました。筆者はこれら知見を元に自作のコーディングエージェントのPoCをしています。

この記事のソースコード全文は以下のリンクにあります。

https://gist.github.com/laiso/df56c55f2b7f0581fcfebed4751499a0?2

同じ拡張を最初から作る場合はまずプロジェクトを新規に始めてください。

❯ yo code
# 自由に選択

min-cline概要

このVSCode拡張はMainコマンド「Run: mini-cline」が1つ定義されています。

min-clineの基本的な流れ

  1. Cmd+Shift+Pで"Run mini-cline"を実行する
  2. エディタのワークスペースで検出されているPROBLEMSを取得しプレーンテキストに整形
  3. これをソースコードと共にプロンプトに含めることで、修正パッチをVSCodeのLM APIモデル(gpt-4o)に生成させます
  4. ユーザーが許可をしたらパッチをソースコードに適用します

この時のモデルはユーザーが契約しているCopilotアカウントを利用するので追加の認証は不要です(許可ダイアログだけ最初に表示されます)。LM APIの導入は以前の記事に書きました。

https://zenn.dev/laiso/articles/2b6136dbd3cba5

診断情報(Diagnostics)の取得と整形

診断情報はVSCodeで研修されたPROBLEMSの欄に出る内部情報でコンパイラのエラーや警告など、コード内の問題点を示すものです。

これはvscode.languages.getDiagnostics()で簡単に取得できます。
診断オブジェクトには、問題が発生しているrange(範囲)、問題の説明であるmessage、severity(警告、エラーなどの種類)などのプロパティが含まれます。これをテキスト形式に整形します。そして以下のようにファイルごとにコンテンツと診断情報を含めて一連のプレーンテキストにします。

File: /Users/username/projects/myapp/src/index.ts
--------------------------------------------------------------------------------
Content:
import { Component } from './component';

function main() {
  const comp = new Componnt();
  comp.init();
  
  console.log("Application started");
}

main();


Diagnostics:
--------------------------------------------------------------------------------
[Error] Line 4, Column 18: Cannot find name 'Componnt'. Did you mean 'Component'?

プロンプトの送信とツールの定義

上記で得られたファイルコンテンツと診断情報を含むプレーンテキストをLM APIへ送信します。この時にパッチを受け取れるようにToolsの仕組みを使います。

https://code.visualstudio.com/api/extension-guides/tools

Toolsはモデルに与えられたコンテキストと設定情報に基づいて、モデル自身が呼び出し対象のTool(関数)を選択して、引数として構造化データを付与します。この引数の中にパッチ用の置き換え文字列が含まれています。

下記のreplace_in_fileは実際のClineのコアツールの1つを移植したものです。範囲置き換えをプレーンテキストベースで行う汎用的なフォーマットで、この方式はいろいろなツールで採用されています。

tool: replace_in_fileで生成される生成されたパッチの例:

path: /tmp/gomi.ts
diff: <<<<<<< SEARCH
console.log("The sum is: " + result.toUpperCase());
=======
console.log("The sum is: " + result.toString().toUpperCase());
>>>>>>> REPLACE

Toolは設定オブジェクトもしくは独自クラスで定義し、LM APIへのメッセージ送信のオプションとして引数に渡します。replace_in_fileは設定オブジェクトを使います。

onst [model] = await vscode.lm.selectChatModels({
			vendor: 'copilot',
			family: 'gpt-4o'
		});
const response = await model.sendRequest(prompt, {
    tools: [{
            name: "replace_in_file",
            description: "Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file.",
            inputSchema: {
                type: "object",
                properties: {
                    path: {
                        type: "string",
                        description: "The path of the file to modify (relative to the current working directory ${cwd.toPosix()})"
                    },
                    diff: {
                        type: "string",
                        description: "One or more SEARCH/REPLACE blocks following this exact format: <<<<<<< SEARCH [exact content to find] ======= [new content to replace with] >>>>>>> REPLACE"
                    }
                },
                required: ["path", "diff"]
            }
        }
    ]
});

ツール呼び出し結果の処理

モデルからのレスポンスを受け取り、パッチ情報を出力チャンネルに表示します。そして変更を適用するかどうかをユーザーに尋ねます。

const applyAction = 'Apply Changes';
    const userChoice = await vscode.window.showInformationMessage(
        'Do you want to apply the suggested changes?',
        applyAction
    );

    if (userChoice === applyAction) {
        let appliedCount = 0;

        for (const result of toolCallResults) {
            const success = await applyChanges(result.path, result.diff);
            if (success) {
                appliedCount++;
            }
        }

        vscode.window.showInformationMessage(
            `Applied changes to ${appliedCount} of ${toolCallResults.length} files.`
        );
    }

生成されたパッチは、デバッグ用にレスポンスをvscode.window.createOutputChannel()でOUTPUTパネル(初期はエディタ下側)に表示しています(Clineではこれはチャットビューの内部に埋め込んでいる部分です)。

これを適用するかどうかをダイアログで尋ねます。適用を選ぶと該当のパスのSEARCHがマッチされ、REPLACEに順次書き換わります。

おわりに

以上、Clineの最小実装の説明をしましたが、Clineではこの基本アーキテクチャの入力/出力にさらに多層的に要素が加わります。重要なパーツとしてはフィードバックループがあります(むしろ本体?)。

入力の例:

  • ユーザーの入力したメッセージやカスタムテキストの命令
  • 設定経由で埋め込まれるプロンプト(MCP等)
  • 実行中に読み込んだファイルの内容や関数の名前
  • 会話の途中でパッチを適用してファイルが書き換わった結果発生するエディタイベント(テスト結果や静的なエラー)
  • スクリーンキャプチャ完了のイベント

この「ユーザの入力」を受け付けるためにサイドバーパネルにWebViewでチャットUIを構築し、設定ViewでAPIキーなどを登録できるようになっています。

コードベースのファイル情報収集においてもモデルの入力トークン制限(コンテキストウィンドウ)があるので大規模なリポジトリではソースコード全文を1つのリクエストに埋め込むのは不可能です。なので型定義の選出やtree-sitterでのパースなど工夫する部分もあります。

https://zenn.dev/laiso/articles/c0adf16df23d39

出力の例:

  • ファイルシステムの作成・削除などの操作
  • コマンドの実行Tool
  • ヘッドレスブラウザの内部起動
  • 再び入力に戻るフィードバックループ

パッチによる変更内容の差分もグラフィカルな一時表示を行うためのウィンドウが独自に実装されています。

さらにClineはマルチLLMプロバイダに対応しているのでVSCodeのLM APIだけでなく、OpenAIやGeminiなどのAPIを共通化して切り替えています。

ということを考えるとだんだん巨大な拡張となっていくわけですが「入力→出力を繰り返してパッチを適用して完了」という基本戦略を説明するには十分なサンプルコードかと思います。

https://gist.github.com/laiso/df56c55f2b7f0581fcfebed4751499a0?2

25

Discussion

ログインするとコメントできます