SLOPSHOPPER

para-code-draft

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

newpromptprocessnetworktimer
A shopper browsing a rack in a slop shop
README

Para Code

License Issues

Para Code は、AI コーディングエージェント(Claude Code・Codex など)と一緒に開発することを前提に作られたエディタです。Visual Studio Code を fork し、複数リポジトリの並行作業・常駐ターミナル・エージェントセッション管理・内蔵ブラウザ連携・SSH リモート開発・モバイルからの遠隔操作といった機能を継ぎ足してあります。

Para Code とは

このリポジトリは microsoft/vscode を fork した独自エディタです。エディタ本体の土台は本家 VS Code のままで、今後も定期的に upstream の新しいリリースを取り込み続けます。そのうえに、複数のエージェントを並行して走らせながら開発する働き方に合わせた機能を追加しています。

<img alt="Para Code" src=".github/readme/screenshot.png">

主な機能

スペース(ワークスペース)管理

  • 複数のリポジトリ・ワークツリーを常駐させたまま、Extension Host を再起動せずに瞬時に切り替え
  • リモートエクスプローラーの「Para ホスト」ビューで、手元と SSH 先を同じツリーに並べて操作
  • SSH 先とのファイル送受信・ドラッグ&ドロップ・複数ファイルのアップロードにも対応

ターミナル

  • 縦横自由な田の字型(2D グリッド)分割
  • 「常駐ターミナル」でウィンドウや Para Code 自体を閉じても実行中のプロセスが動き続け、次回起動時にそのまま繋ぎ直せる(macOS / Linux)
  • Superset 相当のターミナル履歴サジェストなど、日々のシェル操作を補う拡張

AI エージェント連携

  • Claude Code・Codex など複数のエージェントセッションを一覧・絞り込み・履歴検索
  • 通知(音声報告・サウンド・おやすみモード)とユーザー辞書
  • 使用量ダッシュボード(コスト/トークン切り替え)や RTK のトークン節約ダッシュボード
  • 実行環境(手元/特定の SSH 接続先など)に応じて出し分けられるコマンドプリセット

内蔵ブラウザ

  • CDP で繋いだブラウザタブとエージェントセッションを紐づけ、エージェントが見ている画面を可視化・共有
  • 複数ペインの一覧化・絞り込み・ズーム操作

ファイルビューア / Git

  • Markdown・HTML・PDF・Excel・Word の独自プレビュー(相対パスの画像・実行中の読み込みにも対応)
  • Excel・Word の変更点を左右に並べて見られる差分ビュー
  • ソース管理に分岐元ブランチからの差分を独立して表示する「ブランチの変更点」ビュー、GitHub Issue 連携

モバイル(Para Code Mobile)

  • iPhone / iPad から、PC 上で動いているエージェントセッションを遠隔で確認・操作

各バージョンで実際に何が変わったかは、アプリ内の歯車メニュー →「更新履歴」から確認できます。

開発に参加する

Para Code は本家 VS Code のソースをベースにしているため、ビルド・デバッグの基本的な流れは How to Contribute や Coding Guidelines がそのまま参考になります。そのうえで、この fork 特有の実装ルール(新機能の置き場所・既存ファイルへのパッチ方針・upstream 取り込み手順など)は CLAUDE.md と NOTES.md にまとめてあるので、変更を加える前に一読してください。

フィードバック

本家 VS Code との関係

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 用の開発コンテナ設定が含まれています。

  • Dev Containers を使う場合は、Docker と VS Code(または Para Code)をインストールしたうえで Dev Containers: Clone Repository in Container Volume... コマンドを実行してください。
  • Codespaces を使う場合は、GitHub Codespaces 拡張機能をインストールし、Codespaces: Create New Codespace コマンドを実行してください。

フルビルドには最低でも 4 コア・6 GB の RAM(推奨 8 GB) が必要です。詳細は development container README を参照してください。

行動規範

このプロジェクトに参加するすべての人は、互いに敬意を持って接してください。攻撃的な言動やハラスメントは認められません。問題があれば Issue で報告してください。

ライセンス

Copyright (c) 2015 - present Microsoft Corporation. Para Code による追加・変更部分も含め、MIT ライセンスの下で公開されています。

Source 1 files
hooks/draft.ts 217 lines
1/*---------------------------------------------------------------------------------------------
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