Tells Para Code what is in the Claude Code prompt box (needs Claude Code 2.1.300 or later; older versions skip it).

Para Code は、AI コーディングエージェント(Claude Code・Codex など)と一緒に開発することを前提に作られたエディタです。Visual Studio Code を fork し、複数リポジトリの並行作業・常駐ターミナル・エージェントセッション管理・内蔵ブラウザ連携・SSH リモート開発・モバイルからの遠隔操作といった機能を継ぎ足してあります。
このリポジトリは microsoft/vscode を fork した独自エディタです。エディタ本体の土台は本家 VS Code のままで、今後も定期的に upstream の新しいリリースを取り込み続けます。そのうえに、複数のエージェントを並行して走らせながら開発する働き方に合わせた機能を追加しています。
<img alt="Para Code" src=".github/readme/screenshot.png">
各バージョンで実際に何が変わったかは、アプリ内の歯車メニュー →「更新履歴」から確認できます。
Para Code は本家 VS Code のソースをベースにしているため、ビルド・デバッグの基本的な流れは How to Contribute や Coding Guidelines がそのまま参考になります。そのうえで、この fork 特有の実装ルール(新機能の置き場所・既存ファイルへのパッチ方針・upstream 取り込み手順など)は CLAUDE.md と NOTES.md にまとめてあるので、変更を加える前に一読してください。
Para Code は microsoft/vscode を upstream として fetch し続けている fork です。エディタのコア機能(言語サポート、デバッグ、拡張機能 API など)は本家の開発にそのまま追従し、Para Code 独自の機能はなるべく新規ファイル・薄いフックポイントだけで完結させる方針で実装しています。本家の関連プロジェクト一覧は Related Projects を参照してください。
extensions フォルダには、多言語対応の文法・スニペットなどを提供する組み込み拡張機能が含まれます(本家由来)。加えて Para Code では、Open VSX に未公開のいくつかのサードパーティ拡張機能を VSIX として同梱し、起動時に自動インストールする仕組みを備えています。拡張機能の取得元は Open VSX Registry です。
このリポジトリには Visual Studio Code Dev Containers / GitHub Codespaces 用の開発コンテナ設定が含まれています。
フルビルドには最低でも 4 コア・6 GB の RAM(推奨 8 GB) が必要です。詳細は development container README を参照してください。
このプロジェクトに参加するすべての人は、互いに敬意を持って接してください。攻撃的な言動やハラスメントは認められません。問題があれば Issue で報告してください。
Copyright (c) 2015 - present Microsoft Corporation. Para Code による追加・変更部分も含め、MIT ライセンスの下で公開されています。
hooks/draft.ts 217 lines1/*---------------------------------------------------------------------------------------------
2 * Copyright (c) Microsoft Corporation. All rights reserved.
3 * Licensed under the MIT License. See License.txt in the project root for license information.
4 *--------------------------------------------------------------------------------------------*/
5// allow-any-unicode-comment-file
6// PARA-CODE: fork-owned file (Para Code) — not present in upstream microsoft/vscode. See CLAUDE.md.
7
8// The prompt box's text for Para Code (the person's draft in the Claude Code prompt box).
9//
10// `prompt.edit` carries the typed edits and `prompt.change` (Claude Code 2.1.300) everything else: undo, a clear, a
11// recalled history entry, the clear after a submit, a plugin's fill. Without `prompt.change` a submit is never seen, so
12// the text would stay stale. Claude Code refuses to load a hooks module that names an event it does not know, so this
13// lives in its own module: older Claude Code skips this file and keeps register.ts. Para Code treats the box as unknown
14// until a `draft` event arrives, and then keeps everything else as before.
15//
16// Rules: the same as register.ts (only `next` and `$` are waited on; every failed request is dropped quietly).
17
18import type { EngineInterface, Register } from 'claude-code';
19
20type Engine = EngineInterface;
21type Json = Record<string, unknown>;
22
23const MOD_PROTOCOL = '1';
24/** The box's text is sent at most this often (the last text of a burst is always sent). */
25const DRAFT_FLUSH_MS = 150;
26/** Longer text is sent cut and marked `truncated` (Para Code then does not put it back). */
27const MAX_DRAFT_CHARS = 20_000;
28/** A send Para Code refused (or could not take) is tried again after this, twice as long after each further refusal. */
29const DRAFT_RETRY_MS = 1_000;
30/** The longest wait between two tries. */
31const DRAFT_RETRY_MAX_MS = 30_000;
32/** Tries after refusals in a row, after which the mod stops trying until the box changes again. */
33const DRAFT_RETRY_LIMIT = 8;
34
35interface IEndpoint {
36 readonly url: string;
37 readonly token: string;
38}
39
40let endpoint: IEndpoint | undefined;
41/** Connected to an interactive session in a Para Code terminal. */
42let active = false;
43/** The box's text as the last `prompt.edit` / `prompt.change` left it; undefined until one came this session. */
44let draft: string | undefined;
45/** The text Para Code last took, whether a send waits for {@link DRAFT_FLUSH_MS}, one is under way, or a retry waits. */
46let draftSent: string | undefined;
47let draftScheduled = false;
48let sending = false;
49let retryScheduled = false;
50/** Sends refused in a row since Para Code last took one. */
51let refusals = 0;
52/** Counts the sessions, so a send scheduled before a new session.start is not sent for the new one. */
53let generation = 0;
54
55function rec(value: unknown): Json | undefined {
56 return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Json : undefined;
57}
58
59function curlQuote(value: string): string {
60 return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r/g, '\\r').replace(/\n/g, '\\n')}"`;
61}
62
63async function resolveEndpoint($: Engine): Promise<IEndpoint | undefined> {
64 const token = await $.env.get('PARA_CODE_TERMINAL_PANE_ID');
65 const portFile = await $.env.get('PARA_CODE_MCP_PORT_FILE');
66 if (token === undefined || token.length === 0 || portFile === undefined || portFile.length === 0) {
67 return undefined;
68 }
69 let port: unknown;
70 try {
71 port = rec(JSON.parse(String(await $.fs.read(portFile))))?.port;
72 } catch {
73 return undefined;
74 }
75 if (typeof port !== 'number' || !Number.isInteger(port) || port <= 0 || port > 65_535) {
76 return undefined;
77 }
78 return { url: `http://127.0.0.1:${port}/claude-mod/v1`, token };
79}
80
81/**
82 * Sends the events through `$.http.fetch`, or curl when the fetch is refused (the token and body go on stdin). True only
83 * when Para Code took them (a 2xx answer): a refusal (429, 403) is not retried here but by the caller later.
84 */
85async function post($: Engine, body: string): Promise<boolean> {
86 // Para Code may have restarted on another port since the last send: read the port file again after a failure.
87 for (let attempt = 0; attempt < 2; attempt++) {
88 const target = endpoint ?? await resolveEndpoint($);
89 if (target === undefined) {
90 return false;
91 }
92 endpoint = target;
93 try {
94 const response = await $.http.fetch(`${target.url}/event`, {
95 method: 'POST',
96 headers: { 'authorization': `Bearer ${target.token}`, 'content-type': 'application/json', 'x-para-code-mod': MOD_PROTOCOL },
97 body,
98 });
99 return response.status >= 200 && response.status < 300;
100 } catch {
101 // fall through to curl
102 }
103 try {
104 const config = [
105 `url = ${curlQuote(`${target.url}/event`)}`,
106 'request = "POST"',
107 `header = ${curlQuote(`Authorization: Bearer ${target.token}`)}`,
108 'header = "Content-Type: application/json"',
109 `header = ${curlQuote(`X-Para-Code-Mod: ${MOD_PROTOCOL}`)}`,
110 `data-binary = ${curlQuote(body)}`,
111 'silent',
112 'fail',
113 'max-time = 10',
114 '',
115 ].join('\n');
116 const result = await $.process.run(['curl', '--config', '-'], { stdin: config, timeoutMs: 15_000 });
117 if (result.exitCode === 0) {
118 return true;
119 }
120 if (result.exitCode === 22) {
121 // Para Code answered with an error status: the port is right, so do not read the port file again
122 return false;
123 }
124 } catch {
125 // try the port file once more
126 }
127 endpoint = undefined;
128 }
129 return false;
130}
131
132/**
133 * Sends the box's text, one request at a time so a later text never arrives before an earlier one. A text counts as
134 * sent only once Para Code took it; a refused send is tried again with the latest text, after {@link DRAFT_RETRY_MS}
135 * and then twice as long each time (at most {@link DRAFT_RETRY_MAX_MS}), and at most {@link DRAFT_RETRY_LIMIT} times in a row.
136 */
137async function pump($: Engine, mine: number): Promise<void> {
138 if (sending) {
139 return;
140 }
141 sending = true;
142 try {
143 while (active && mine === generation && draft !== undefined && draft !== draftSent) {
144 const text = draft;
145 const sessionId = await $.session.id();
146 const event = { type: 'draft', text: text.slice(0, MAX_DRAFT_CHARS), ...(text.length > MAX_DRAFT_CHARS ? { truncated: true } : {}), at: Date.now() };
147 if (!await post($, JSON.stringify({ sessionId, events: [event] }))) {
148 if (!retryScheduled && refusals < DRAFT_RETRY_LIMIT) {
149 retryScheduled = true;
150 const delay = Math.min(DRAFT_RETRY_MS * 2 ** refusals, DRAFT_RETRY_MAX_MS);
151 refusals++;
152 $.clock.after(delay, () => {
153 retryScheduled = false;
154 void pump($, mine).catch(() => undefined);
155 });
156 }
157 return;
158 }
159 refusals = 0;
160 draftSent = text;
161 }
162 } finally {
163 sending = false;
164 }
165}
166
167/**
168 * The box changed: sends its text to Para Code, at most every {@link DRAFT_FLUSH_MS}. While a retry waits, the retry
169 * sends the latest text; after the mod gave up, a change starts the tries afresh.
170 */
171function noteDraft($: Engine, text: string): void {
172 draft = text;
173 if (!active || draftScheduled || retryScheduled) {
174 return;
175 }
176 if (refusals >= DRAFT_RETRY_LIMIT) {
177 refusals = 0;
178 }
179 draftScheduled = true;
180 const mine = generation;
181 $.clock.after(DRAFT_FLUSH_MS, () => {
182 draftScheduled = false;
183 void pump($, mine).catch(() => undefined);
184 });
185}
186
187export const register: Register = on => {
188 on('session.start', async ($, e, next) => {
189 const started = await next(e);
190 generation++;
191 // The box is unknown until it changes (a reload of this module may find text in it).
192 draft = undefined;
193 draftSent = undefined;
194 draftScheduled = false;
195 retryScheduled = false;
196 refusals = 0;
197 endpoint = e.isInteractive ? await resolveEndpoint($) : undefined;
198 active = endpoint !== undefined;
199 return started;
200 });
201
202 on('prompt.change', ($, e, next) => {
203 if (active && typeof e.text === 'string') {
204 noteDraft($, e.text);
205 }
206 return next(e);
207 });
208
209 on('prompt.edit', async ($, e, next) => {
210 const edited = await next(e);
211 if (active) {
212 noteDraft($, typeof edited?.text === 'string' ? edited.text : `${e.text.slice(0, e.start)}${e.inputText}${e.text.slice(e.end)}`);
213 }
214 return edited;
215 });
216};
217