BrowserToolHandler:Puppeteer ブラウザ自動化
役割
BrowserToolHandler は Cline がブラウザを操作するための入口であり、ClineDefaultTool.BROWSER ツール名に対応する。モデルは action パラメータで具体的な操作を選ぶ:launch(URL を開く)、click(座標クリック)、type(現在のフォーカスにテキスト入力)、scroll_up/scroll_down(ページスクロール)、close(ブラウザを閉じる)(class declaration:12-13)。各アクションの実行後、スクリーンショットとそのウィンドウの console logs を返し、モデルが視覚情報でページ状態を理解できるようにする。
handler 自体は「ファサード」に過ぎない:tool_use を受け取った後にパラメータ検証、承認、hook を行い、実際の动作を config.services.browserSession という BrowserSession インスタンスに移譲する。本物のブラウザプロセス、Puppeteer API、スクリーンショットのエンコード、リモートブラウザ接続はすべて BrowserSession の中にある(class BrowserSession:38)。BrowserSession は puppeteer-core でローカル Chrome またはリモートブラウザに接続し、connect/launch で Browser/Page オブジェクトを取得する。
このツールは Cline の中で「能力系ツール (capability tool)」という位置付けで、file ops や web fetch と同列に、ToolExecutor が起動する使い捨て実行ユニットである。特殊な点はステートフルであること:1 つの Task 内でブラウザセッションは 1 つしか開かれず、launch 後の click/type/scroll はすべて同じ Page を再利用し、close またはエラー発生まで維持される。だから handler はどのエラー分岐でも closeBrowser() を呼ぶ —— ブラウザプロセスを残してはいけない。
設計動機
- 単一セッション再利用:launch 後の page は close まで再利用され、click のたびにブラウザを開き直すことを避ける。モデルも複数 action 間でログイン状態やスクロール位置を保持できる(
launchBrowser:155)。 - スクリーンショット + コンソールログの二重チャネル:各 action 完了後、
doActionはコンソールが静默 500ms になってからスクリーンショットを撮り、視覚情報とログを一緒にモデルに返す。純粋な視覚だけでは JS エラーを捉えられないという穴を補う(doAction:378)。 - launch の個別承認:ブラウザを開くのは高リスクな动作なので、launch は
browser_action_launchという独立した ask タイプを通る。他の action(click/type/scroll)は launch が既に承認されていればデフォルトで通す(launch flow:81)。 - リモートブラウザのフォールバック:リモートブラウザ接続が失敗した時はローカルモードに降格し、ツール全体がクラッシュしないようにする(
remote fallback:181-198)。 - click 後のナビゲーション待ち:クリックがページ遷移をトリガーする可能性があるため、doAction は click 時に
requestイベントを監視してネットワーク活動があるかを判断し、あればwaitForNavigationを呼ぶ(click:528-561). - ブラウザを閉じてから別ツールへ:戻り値に「REMEMBER ... browser_action 以外のツールを使うには先にブラウザを close しなければならない」という一文を付け、ブラウザを開いたままファイル変更に切り替えないようモデルに促す(
result reminder:190-194)。
主要ファイル
class declaration:12—export class BrowserToolHandler implements IFullyManagedTool、name = ClineDefaultTool.BROWSER。handlePartialBlock:19— ストリーミング分岐:launch はbrowser_action_launchask を通り、他の action は現在の座標/テキストを直接 say する。execute:62— 主流程:action 検証 → launch 分岐または click/type/scroll/close 分岐 → screenshot + logs を返す。launch flow:81-127— launch の完全な流れ:url 検証、承認、PreToolUse hook、applyLatestBrowserSettingsで session を更新、launchBrowser+navigateToUrl。click/type param check:130-145— click で coordinate が無い、type で text が無い場合にそれぞれエラーを投げ closeBrowser する。action dispatch:163-179— switch でbrowserSession.click/type/scrollDown/scrollUp/closeBrowserに振り分ける。result format:184-203— launch/click/type/scroll は screenshot + logs を返し、close は平文のテキストプロンプトを返す。error close:205-208— 例外は何であれcloseBrowserを呼び、ブラウザプロセスを残さない。launchBrowser:155— リモート優先、失敗時はローカルに降格、その後browser.newPage()で新規 tab を開く。doAction:378— console/pageerror リスナーを装着、action を実行、500ms の静默を待つ、スクリーンショット、listener のクリーンアップ。navigateToUrl:481—page.gotoはdomcontentloaded + networkidle2を使い、さらにwaitTillHTMLStableで HTML サイズをポーリングする。click:528— 座標を分解、page.mouse.click、request リスナーでナビゲーション待ちを判断。screenshot:441— webp 優先、失敗時は png にフォールバック、両方失敗したらエラーを投げテレメトリを打つ。
データフロー
launch 分岐は BrowserToolHandler の中で最も完全なフロー:url 検証 → 承認 → hook → browserSession 更新 → ブラウザ起動 → ナビゲーション → スクリーンショットとログを返す。キーコード:
// apps/vscode/src/core/task/tools/handlers/BrowserToolHandler.ts
if (action === "launch") {
if (!url) {
config.taskState.consecutiveMistakeCount++
const errorResult = await config.callbacks.sayAndCreateMissingParamError(this.name, "url")
await config.services.browserSession.closeBrowser()
return errorResult
}
config.taskState.consecutiveMistakeCount = 0
// ... approval + PreToolUse hook ...
await config.callbacks.say("browser_action_result", "")
config.services.browserSession = await config.callbacks.applyLatestBrowserSettings()
await config.services.browserSession.launchBrowser()
browserActionResult = await config.services.browserSession.navigateToUrl(url)
}config.services.browserSession = await config.callbacks.applyLatestBrowserSettings() という行に注意:設定を更新するだけでなく、ToolExecutor が持つ browserSession の参照を新しいものに差し替える。これで後続の browser tool 呼び出しが同じ新 session を取得する(applyLatestBrowserSettings:125)。
navigateToUrl 内部は doAction を通り、他の非 launch action もすべて doAction を通る。doAction はスクリーンショット + コンソールログを組み合わせて BrowserActionResult にする:
// apps/vscode/src/services/browser/BrowserSession.ts
const screenshotType = this.useWebp ? "webp" : "png"
let screenshotBase64 = await this.page.screenshot({ ...options, type: screenshotType })
let screenshot = `data:image/${screenshotType};base64,${screenshotBase64}`
if (!screenshotBase64) {
screenshotBase64 = await this.page.screenshot({ ...options, type: "png" })
screenshot = `data:image/png;base64,${screenshotBase64}`
}
return {
screenshot,
logs: logs.join("\n"),
currentUrl: this.page.url(),
currentMousePosition: this.currentMousePosition,
}handler はこの result を formatResponse.toolResult に包んで返し、さらに「ブラウザを close しないと他のツールに切り替えられない」という強いリマインダーを付ける。
境界と失敗
- action が不正:block 完了時に action が
browserActionsリストに無い場合、consecutiveMistakeCount+++sayAndCreateMissingParamError+closeBrowser(invalid action:69-75)。 - launch で url が無い:launch は url を必須とする。無い場合は同様に mistake++ して closeBrowser する(
missing url:82-87)。 - click で coordinate 無し / type で text 無し:対応するパラメータが欠けている場合、エラーを投げ closeBrowser し、session を残さない(
param checks:131-145)。 - リモートブラウザ接続失敗:
launchRemoteBrowserがエラーを投げた時、catch してlaunchLocalBrowserに降格し、テレメトリに remote_browser_launch_error を記録する(remote fallback:181-198)。 - スクリーンショット失敗:webp が失敗した時は png に切り替えてリトライする。両方失敗したら "Failed to take screenshot." を投げ、screenshot_error テレメトリを打つ(
screenshot retry:448-466)。 - page が未起動:
doActionはthis.pageが存在しない時にエラーを投げ、「Browser is not launched」と提示する。この状況は通常、非 browser_action ツール(実際には権限が無い)か、前回の close 後にまた呼び出された場合(page check:379-383)。 - どんな例外でもブラウザを閉じる:handler の try/catch の最後は
browserSession.closeBrowser()で、Chrome プロセスがリソースを占有したまま残らないようにする(error close:205-208)。
まとめ
BrowserToolHandler は薄い殻であり、本当の仕事は BrowserSession が行う。モデルは action パラメータで launch/click/type/scroll/close を選び、handler はパラメータ検証と承認を行って、动作を BrowserSession の Puppeteer 呼び出しに移譲し、スクリーンショットと console ログをパッケージしてモデルに返す。launch だけが個別承認を通り、他の action は既に開いているセッションを再利用する。エラー時には必ずブラウザを閉じる。
さらに掘り下げるなら次へ:
- ファイルとコマンド系ツール:
/cap-tools/file-ops - Web fetch/search:
/cap-tools/web - ツール调度の経路:
/agent-loop/task-class