@saifymatteo/pi-x-ide 0.0.0-stage → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +328 -0
- package/CONTRIBUTING.md +119 -0
- package/CONTRIBUTING.zh-CN.md +106 -0
- package/LICENSE +201 -0
- package/README.md +33 -2
- package/README.zh-CN.md +34 -0
- package/RELEASE.md +213 -0
- package/config.example.json +9 -0
- package/dist/src/pi/chunks/chunk-4RHJAL4S.js +200 -0
- package/dist/src/pi/chunks/chunk-UUXVCTOS.js +819 -0
- package/dist/src/pi/chunks/config-ui-LO3DVQFC.js +8073 -0
- package/dist/src/pi/chunks/runtime-services-TUOTPMCH.js +19569 -0
- package/dist/src/pi/commands.js +126 -0
- package/dist/src/pi/config-ui.js +127 -0
- package/dist/src/pi/connection.js +237 -0
- package/dist/src/pi/context.js +30 -0
- package/dist/src/pi/diagnostic-renderer.js +326 -0
- package/dist/src/pi/diagnostic-types.js +1 -0
- package/dist/src/pi/diagnostics.js +104 -0
- package/dist/src/pi/discovery.js +135 -0
- package/dist/src/pi/effect-boundary.js +5 -0
- package/dist/src/pi/ide-host.js +68 -0
- package/dist/src/pi/index.js +636 -0
- package/dist/src/pi/install.js +308 -0
- package/dist/src/pi/reconnect.js +57 -0
- package/dist/src/pi/runtime-loader.js +38 -0
- package/dist/src/pi/runtime-services.js +356 -0
- package/dist/src/pi/safety.js +61 -0
- package/dist/src/pi/selection.js +18 -0
- package/dist/src/pi/state.js +13 -0
- package/dist/src/pi/ui.js +304 -0
- package/dist/src/pi/zed.js +409 -0
- package/dist/src/shared/config-options.js +45 -0
- package/dist/src/shared/config.js +183 -0
- package/dist/src/shared/display-width.js +99 -0
- package/dist/src/shared/effect-errors.js +36 -0
- package/dist/src/shared/effect-runtime.js +36 -0
- package/dist/src/shared/effect-schema.js +120 -0
- package/dist/src/shared/errors.js +48 -0
- package/dist/src/shared/format.js +92 -0
- package/dist/src/shared/ide-server.js +167 -0
- package/dist/src/shared/jsonrpc-guard.js +12 -0
- package/dist/src/shared/lock-file-heartbeat.js +58 -0
- package/dist/src/shared/lock-file.js +50 -0
- package/dist/src/shared/paths.js +79 -0
- package/dist/src/shared/platform.js +57 -0
- package/dist/src/shared/protocol.js +3 -0
- package/dist/src/shared/schema.js +37 -0
- package/dist/src/shared/version.js +1 -0
- package/dist/src/shared/ws.js +8 -0
- package/docs/explanation/architecture.md +123 -0
- package/docs/explanation/discovery.md +113 -0
- package/docs/explanation/effect.md +127 -0
- package/docs/how-to/install-jetbrains.md +58 -0
- package/docs/how-to/install-neovim.md +138 -0
- package/docs/how-to/install-vscode.md +75 -0
- package/docs/how-to/install-zed.md +70 -0
- package/docs/how-to/troubleshoot-connection.md +103 -0
- package/docs/how-to/use-wsl2.md +90 -0
- package/docs/reference/commands.md +59 -0
- package/docs/reference/configuration.md +101 -0
- package/docs/reference/feature-parity.md +26 -0
- package/docs/reference/protocol.md +71 -0
- package/docs/specs/ide-protocol.md +215 -0
- package/docs/tutorials/get-started.md +133 -0
- package/docs/zh-CN/explanation/architecture.md +93 -0
- package/docs/zh-CN/explanation/discovery.md +71 -0
- package/docs/zh-CN/explanation/effect.md +90 -0
- package/docs/zh-CN/how-to/install-jetbrains.md +49 -0
- package/docs/zh-CN/how-to/install-neovim.md +126 -0
- package/docs/zh-CN/how-to/install-vscode.md +67 -0
- package/docs/zh-CN/how-to/install-zed.md +56 -0
- package/docs/zh-CN/how-to/troubleshoot-connection.md +85 -0
- package/docs/zh-CN/how-to/use-wsl2.md +75 -0
- package/docs/zh-CN/reference/commands.md +54 -0
- package/docs/zh-CN/reference/configuration.md +94 -0
- package/docs/zh-CN/reference/feature-parity.md +21 -0
- package/docs/zh-CN/reference/protocol.md +54 -0
- package/docs/zh-CN/tutorials/get-started.md +101 -0
- package/package.json +86 -4
- package/schemas/config.json +77 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { LOCK_FILE_EXTENSION } from "./protocol.js";
|
|
5
|
+
import { resolveLockDir } from "./paths.js";
|
|
6
|
+
export function createAuthToken() {
|
|
7
|
+
return randomBytes(32).toString("hex");
|
|
8
|
+
}
|
|
9
|
+
export function createIdeLockFilePath(source, port, pid = process.pid, lockDir) {
|
|
10
|
+
return join(lockDir ?? resolveLockDir(), `${source}-${pid}-${port}${LOCK_FILE_EXTENSION}`);
|
|
11
|
+
}
|
|
12
|
+
export function createIdeLockFile(options) {
|
|
13
|
+
const now = (options.now ?? new Date()).toISOString();
|
|
14
|
+
return {
|
|
15
|
+
version: 1,
|
|
16
|
+
ide: options.ide,
|
|
17
|
+
name: options.name,
|
|
18
|
+
transport: "ws",
|
|
19
|
+
host: "127.0.0.1",
|
|
20
|
+
port: options.port,
|
|
21
|
+
authToken: options.authToken,
|
|
22
|
+
workspaceFolders: options.workspaceFolders,
|
|
23
|
+
pid: options.pid ?? process.pid,
|
|
24
|
+
runningInWindows: options.runningInWindows ?? process.platform === "win32",
|
|
25
|
+
createdAt: now,
|
|
26
|
+
updatedAt: now,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function writeIdeLockFile(path, lock, lockDir) {
|
|
30
|
+
const dir = lockDir ?? resolveLockDir();
|
|
31
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
32
|
+
await chmod(dir, 0o700).catch(() => undefined);
|
|
33
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
34
|
+
await writeFile(tmp, `${JSON.stringify(lock, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
35
|
+
await chmod(tmp, 0o600).catch(() => undefined);
|
|
36
|
+
await rename(tmp, path);
|
|
37
|
+
await chmod(path, 0o600).catch(() => undefined);
|
|
38
|
+
}
|
|
39
|
+
export async function removeIdeLockFile(path) {
|
|
40
|
+
if (!path)
|
|
41
|
+
return;
|
|
42
|
+
await rm(path, { force: true }).catch(() => undefined);
|
|
43
|
+
}
|
|
44
|
+
export function refreshIdeLockFile(lock, workspaceFolders, now = new Date()) {
|
|
45
|
+
return {
|
|
46
|
+
...lock,
|
|
47
|
+
workspaceFolders,
|
|
48
|
+
updatedAt: now.toISOString(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { CONFIG_DIR_NAME, EXT_CONFIG_NAME } from "./config.js";
|
|
5
|
+
import { normalizePathForHost, windowsUserProfileDirs } from "./platform.js";
|
|
6
|
+
export function resolveLockDir() {
|
|
7
|
+
return resolve(homedir(), CONFIG_DIR_NAME, EXT_CONFIG_NAME, "lock");
|
|
8
|
+
}
|
|
9
|
+
export function resolveLockDirs(options = {}) {
|
|
10
|
+
if (options.lockDir)
|
|
11
|
+
return [resolve(options.lockDir)];
|
|
12
|
+
const dirs = [resolve(options.homeLockDir ?? resolveLockDir())];
|
|
13
|
+
for (const profileDir of windowsUserProfileDirs(options.windowsUsersRoot, options.env)) {
|
|
14
|
+
dirs.push(resolve(profileDir, CONFIG_DIR_NAME, EXT_CONFIG_NAME, "lock"));
|
|
15
|
+
}
|
|
16
|
+
return dedupePaths(dirs);
|
|
17
|
+
}
|
|
18
|
+
function dedupePaths(paths) {
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
const deduped = [];
|
|
21
|
+
for (const path of paths) {
|
|
22
|
+
const resolved = resolve(path);
|
|
23
|
+
let key = resolved;
|
|
24
|
+
try {
|
|
25
|
+
key = realpathSync(resolved);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
}
|
|
29
|
+
if (seen.has(key))
|
|
30
|
+
continue;
|
|
31
|
+
seen.add(key);
|
|
32
|
+
deduped.push(resolved);
|
|
33
|
+
}
|
|
34
|
+
return deduped;
|
|
35
|
+
}
|
|
36
|
+
export function normalizePath(input) {
|
|
37
|
+
const resolved = resolve(input);
|
|
38
|
+
if (process.platform === "win32" && resolved.length >= 2 && resolved[1] === ":") {
|
|
39
|
+
return resolved[0].toUpperCase() + resolved.slice(1);
|
|
40
|
+
}
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
export function normalizePathForComparison(input, env = process.env) {
|
|
44
|
+
return normalizePath(normalizePathForHost(input, env));
|
|
45
|
+
}
|
|
46
|
+
export function isPathInsideOrEqual(parent, child) {
|
|
47
|
+
const normalizedParent = normalizePath(parent);
|
|
48
|
+
const normalizedChild = normalizePath(child);
|
|
49
|
+
if (normalizedParent === normalizedChild)
|
|
50
|
+
return true;
|
|
51
|
+
const rel = relative(normalizedParent, normalizedChild);
|
|
52
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
53
|
+
}
|
|
54
|
+
export function hasDirectWorkspaceMatch(workspaceFolders, cwd, env = process.env) {
|
|
55
|
+
const normalizedCwd = normalizePathForComparison(cwd, env);
|
|
56
|
+
return workspaceFolders.some((workspaceFolder) => isPathInsideOrEqual(normalizePathForComparison(workspaceFolder, env), normalizedCwd));
|
|
57
|
+
}
|
|
58
|
+
export function relationshipMatchLength(workspaceFolder, cwd, env = process.env) {
|
|
59
|
+
const workspace = normalizePathForComparison(workspaceFolder, env);
|
|
60
|
+
const active = normalizePathForComparison(cwd, env);
|
|
61
|
+
if (isPathInsideOrEqual(workspace, active)) {
|
|
62
|
+
return workspace.length + 10_000;
|
|
63
|
+
}
|
|
64
|
+
if (isPathInsideOrEqual(active, workspace)) {
|
|
65
|
+
return active.length;
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
export function toRelativeDisplayPath(filePath, workspaceFolder, cwd, env = process.env) {
|
|
70
|
+
const base = workspaceFolder ?? cwd;
|
|
71
|
+
if (!base)
|
|
72
|
+
return normalizePathForHost(filePath, env);
|
|
73
|
+
const normalizedFilePath = normalizePathForHost(filePath, env);
|
|
74
|
+
const normalizedBase = normalizePathForHost(base, env);
|
|
75
|
+
const rel = relative(normalizedBase, normalizedFilePath);
|
|
76
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel))
|
|
77
|
+
return normalizedFilePath;
|
|
78
|
+
return rel.split(sep).join("/");
|
|
79
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { isProcessEnvOrPiConfigOverlay, resolvePiConfigEnv } from "./config.js";
|
|
4
|
+
const WINDOWS_SYSTEM_PROFILES = new Set(["public", "default", "default user", "all users"]);
|
|
5
|
+
export function isWsl(env = process.env) {
|
|
6
|
+
const configuredEnv = resolvePiConfigEnv(env);
|
|
7
|
+
if (configuredEnv.WSL_DISTRO_NAME || configuredEnv.WSL_INTEROP)
|
|
8
|
+
return true;
|
|
9
|
+
if (!isProcessEnvOrPiConfigOverlay(configuredEnv))
|
|
10
|
+
return false;
|
|
11
|
+
try {
|
|
12
|
+
return /microsoft|wsl/i.test(readFileSync("/proc/version", "utf8"));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function normalizePathForHost(input, env = process.env) {
|
|
19
|
+
const configuredEnv = resolvePiConfigEnv(env);
|
|
20
|
+
if (!input || !isWsl(configuredEnv))
|
|
21
|
+
return input;
|
|
22
|
+
const driveMatch = input.match(/^([a-zA-Z]):[\\/](.*)$/);
|
|
23
|
+
if (driveMatch) {
|
|
24
|
+
const drive = driveMatch[1].toLowerCase();
|
|
25
|
+
const rest = driveMatch[2].replaceAll("\\", "/");
|
|
26
|
+
return `/mnt/${drive}/${rest}`;
|
|
27
|
+
}
|
|
28
|
+
const uncMatch = input.match(/^[\\/]{2}(?:wsl\$|wsl\.localhost)[\\/]([^\\/]+)[\\/](.*)$/i);
|
|
29
|
+
if (uncMatch) {
|
|
30
|
+
const distro = uncMatch[1];
|
|
31
|
+
const rest = uncMatch[2].replaceAll("\\", "/");
|
|
32
|
+
const currentDistro = configuredEnv.WSL_DISTRO_NAME;
|
|
33
|
+
if (!currentDistro || distro.toLowerCase() === currentDistro.toLowerCase()) {
|
|
34
|
+
return `/${rest}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return input;
|
|
38
|
+
}
|
|
39
|
+
export function windowsUserProfileDirs(usersRoot = "/mnt/c/Users", env = process.env) {
|
|
40
|
+
if (!isWsl(env))
|
|
41
|
+
return [];
|
|
42
|
+
try {
|
|
43
|
+
return readdirSync(usersRoot, { withFileTypes: true })
|
|
44
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !WINDOWS_SYSTEM_PROFILES.has(entry.name.toLowerCase()))
|
|
45
|
+
.map((entry) => resolve(usersRoot, entry.name));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function normalizeEditorSelectionSnapshotForHost(snapshot, env = process.env) {
|
|
52
|
+
return {
|
|
53
|
+
...snapshot,
|
|
54
|
+
filePath: normalizePathForHost(snapshot.filePath, env),
|
|
55
|
+
workspaceFolder: snapshot.workspaceFolder ? normalizePathForHost(snapshot.workspaceFolder, env) : undefined,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { decodeAtMentionedParams, decodeDiagnosticFixRequestedParams, decodeEditorSelectionSnapshot, decodeLockFile, decodeSelectionChangedParams, decodeSelectionClearedParams, } from "./effect-schema.js";
|
|
2
|
+
import { isJsonRpcRequest } from "./jsonrpc-guard.js";
|
|
3
|
+
export { isJsonRpcRequest };
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
export function isIdeLockFile(value) {
|
|
8
|
+
return decodeLockFile(value) !== undefined;
|
|
9
|
+
}
|
|
10
|
+
export function isEditorSelectionSnapshot(value) {
|
|
11
|
+
return decodeEditorSelectionSnapshot(value) !== undefined;
|
|
12
|
+
}
|
|
13
|
+
export function isSelectionChangedParams(value) {
|
|
14
|
+
return decodeSelectionChangedParams(value) !== undefined;
|
|
15
|
+
}
|
|
16
|
+
export function isSelectionClearedParams(value) {
|
|
17
|
+
return decodeSelectionClearedParams(value) !== undefined;
|
|
18
|
+
}
|
|
19
|
+
export function isAtMentionedParams(value) {
|
|
20
|
+
return decodeAtMentionedParams(value) !== undefined;
|
|
21
|
+
}
|
|
22
|
+
export function isDiagnosticFixRequestedParams(value) {
|
|
23
|
+
return decodeDiagnosticFixRequestedParams(value) !== undefined;
|
|
24
|
+
}
|
|
25
|
+
export function parseJsonObject(input) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(input);
|
|
28
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function parseLockFileContent(content) {
|
|
35
|
+
const parsed = parseJsonObject(content);
|
|
36
|
+
return parsed === undefined ? undefined : decodeLockFile(parsed);
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PI_X_IDE_VERSION = "1.20.1";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Pi × IDE bridges two processes that normally know nothing about each other: your
|
|
4
|
+
IDE and the Pi TUI. This page explains the moving parts, the data flow, and why
|
|
5
|
+
each editor connects differently. It is understanding-oriented — for exact
|
|
6
|
+
message shapes and field semantics, see the
|
|
7
|
+
[protocol specification](../specs/ide-protocol.md).
|
|
8
|
+
|
|
9
|
+
## The three roles
|
|
10
|
+
|
|
11
|
+
| Role | Who | Responsibility |
|
|
12
|
+
| ---------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
13
|
+
| **IDE plugin** | VS Code extension, JetBrains plugin, Neovim Lua plugin + sidecar | Observe the active editor and selection, expose them over a local WebSocket. |
|
|
14
|
+
| **Lock file** | Written by the IDE plugin, read by Pi | Advertise the WebSocket endpoint (host, port, auth token) and workspace folders. |
|
|
15
|
+
| **Pi extension** | `pi-x-ide` loaded into the Pi TUI | Discover lock files, connect, receive selections, inject them as LLM context. |
|
|
16
|
+
|
|
17
|
+
Zed is the exception: it has no plugin, so Pi reads Zed's local state database
|
|
18
|
+
directly. See [Why each editor connects differently](#why-each-editor-connects-differently).
|
|
19
|
+
|
|
20
|
+
## Data flow
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
User selects text in the IDE
|
|
24
|
+
│
|
|
25
|
+
▼
|
|
26
|
+
┌──────────────────────────────────────────────────────────┐
|
|
27
|
+
│ IDE plugin (VS Code / JetBrains / Neovim sidecar) │
|
|
28
|
+
│ │
|
|
29
|
+
│ Captures editor + selection, broadcasts a │
|
|
30
|
+
│ JSON-RPC "selection_changed" notification. │
|
|
31
|
+
└──────────────────────┬───────────────────────────────────┘
|
|
32
|
+
│ WebSocket ws://127.0.0.1:<port>
|
|
33
|
+
│ Auth: x-pi-x-ide-authorization header
|
|
34
|
+
▼
|
|
35
|
+
┌──────────────────────────────────────────────────────────┐
|
|
36
|
+
│ Pi extension (src/pi/connection.ts) │
|
|
37
|
+
│ │
|
|
38
|
+
│ Receives the notification, caches it as │
|
|
39
|
+
│ runtime.latestSelection, marks attachState = "pending", │
|
|
40
|
+
│ updates the TUI widget (⇡ marker). │
|
|
41
|
+
└──────────────────────┬───────────────────────────────────┘
|
|
42
|
+
│ on next user message
|
|
43
|
+
▼
|
|
44
|
+
┌──────────────────────────────────────────────────────────┐
|
|
45
|
+
│ Pi prompt lifecycle (src/pi/context.ts) │
|
|
46
|
+
│ │
|
|
47
|
+
│ before_agent_start: snapshots the pending selection. │
|
|
48
|
+
│ Adds a visible custom message containing only IDE, file, │
|
|
49
|
+
│ and selected line range metadata. Prompt stays intact; │
|
|
50
|
+
│ agent can read source from disk as needed. │
|
|
51
|
+
│ attachState = "sent". │
|
|
52
|
+
└──────────────────────────────────────────────────────────┘
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The attach shortcut (`Ctrl+Alt+K` in the TUI, or the IDE's attach command) sends
|
|
56
|
+
an `at_mentioned` notification instead. That inserts the `@file#Lx-Ly` mention
|
|
57
|
+
into the Pi input box and caches the selection text so it is ready for the next
|
|
58
|
+
turn.
|
|
59
|
+
|
|
60
|
+
## Why each editor connects differently
|
|
61
|
+
|
|
62
|
+
Each IDE's extension API dictates the transport.
|
|
63
|
+
|
|
64
|
+
| Editor | Mechanism | Reason |
|
|
65
|
+
| ------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| **VS Code** | WebSocket server inside the extension host | Rich extension API: `onDidChangeTextEditorSelection`, file-system access, terminal creation. A lightweight server runs in-process — no separate process needed. |
|
|
67
|
+
| **JetBrains** | WebSocket server inside the IDE process | Same rationale: the `com.intellij` API provides editor listeners and project-service lifecycle. The Kotlin plugin runs the server in-process. |
|
|
68
|
+
| **Neovim** | Separate **sidecar** process | Lua cannot run a WebSocket server natively. The sidecar is a standalone process that talks to Neovim over stdin/stdout (JSON lines) and exposes the WebSocket to Pi. |
|
|
69
|
+
| **Zed** | **Pi-side SQLite polling** | Zed has no extension API. It stores editor state in a local SQLite database, which Pi reads directly every second. The polling runs on the Pi side, not in Zed. |
|
|
70
|
+
|
|
71
|
+
### The Neovim sidecar
|
|
72
|
+
|
|
73
|
+
Because Neovim cannot host a WebSocket server, the Lua plugin spawns a sidecar
|
|
74
|
+
process (`src/nvim/sidecar.ts`) that owns the server and the lock file. The
|
|
75
|
+
plugin captures selections via Neovim autocmds (`CursorMoved`, `BufEnter`,
|
|
76
|
+
visual-mode marks), debounces them, and writes JSON messages to the sidecar's
|
|
77
|
+
stdin. The sidecar broadcasts them as JSON-RPC notifications to Pi.
|
|
78
|
+
|
|
79
|
+
The sidecar is resolved in this order (`ide-plugins/nvim/lua/pi_x_ide/download.lua`):
|
|
80
|
+
|
|
81
|
+
1. A platform binary bundled in `ide-plugins/nvim/bin/`.
|
|
82
|
+
2. A cached binary downloaded from GitHub Releases, verified by SHA256.
|
|
83
|
+
3. A fallback Node.js script (`pi-x-ide-nvim-sidecar.cjs`).
|
|
84
|
+
|
|
85
|
+
This is why the Neovim plugin "just works" on first start: it downloads the right
|
|
86
|
+
binary for your platform and falls back to Node if the download is unavailable.
|
|
87
|
+
|
|
88
|
+
### Zed polling
|
|
89
|
+
|
|
90
|
+
Pi reads Zed's state database directly (`src/pi/zed.ts`). On each poll (default
|
|
91
|
+
1000 ms, clampable to 100–2000 ms via `PI_X_IDE_ZED_POLL_INTERVAL_MS`):
|
|
92
|
+
|
|
93
|
+
1. Check the SQLite WAL file's mtime — skip the expensive read if nothing changed
|
|
94
|
+
(this matters especially under WSL, where a full snapshot would otherwise be
|
|
95
|
+
copied each tick).
|
|
96
|
+
2. Query the active editor by joining Zed's `items`, `panes`, `workspaces`, and
|
|
97
|
+
`editors` tables.
|
|
98
|
+
3. Score candidates by workspace match and pick the best.
|
|
99
|
+
4. Convert UTF-8 byte offsets to VS Code/LSP-style line/character positions.
|
|
100
|
+
|
|
101
|
+
## Authentication model
|
|
102
|
+
|
|
103
|
+
Each IDE process generates a fresh token per session using
|
|
104
|
+
`randomBytes(32).toString("hex")` — a 64-character hex string
|
|
105
|
+
(`src/shared/lock-file.ts`). JetBrains generates the equivalent with
|
|
106
|
+
`SecureRandom`. The token is written to the lock file (mode `0600`) and sent only
|
|
107
|
+
during the WebSocket upgrade handshake, in the `x-pi-x-ide-authorization` header.
|
|
108
|
+
The server's `verifyClient` callback rejects any connection whose header does not
|
|
109
|
+
match (`src/shared/ide-server.ts`).
|
|
110
|
+
|
|
111
|
+
Tokens never leave localhost except across the WSL boundary, where the WebSocket
|
|
112
|
+
still targets the resolved Windows host.
|
|
113
|
+
|
|
114
|
+
## Related
|
|
115
|
+
|
|
116
|
+
- [How discovery works](discovery.md) — how Pi finds lock files and picks a
|
|
117
|
+
connection.
|
|
118
|
+
- [Effect adoption](effect.md) — how Pi-side TypeScript may use Effect internally
|
|
119
|
+
for errors, schema validation, and IO boundaries.
|
|
120
|
+
- [Protocol specification](../specs/ide-protocol.md) — exact message shapes,
|
|
121
|
+
handshake, and notifications.
|
|
122
|
+
- [Feature parity](../reference/feature-parity.md) — which capabilities each
|
|
123
|
+
editor supports.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# How discovery works
|
|
2
|
+
|
|
3
|
+
Pi discovers IDE connections through **lock files**: small JSON files that each
|
|
4
|
+
running IDE plugin writes to advertise its WebSocket endpoint. This page explains
|
|
5
|
+
where they live, how Pi picks the right one, and how the WSL boundary is crossed.
|
|
6
|
+
For the exact file schema, see the
|
|
7
|
+
[protocol specification](../specs/ide-protocol.md).
|
|
8
|
+
|
|
9
|
+
## The lock directory
|
|
10
|
+
|
|
11
|
+
The primary lock directory is always:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
~/.pi/pi-x-ide/lock/
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Each file is named `<ide>-<pid>-<port>.lock`, for example:
|
|
18
|
+
|
|
19
|
+
- `vscode-12345-48123.lock`
|
|
20
|
+
- `nvim-98765-50001.lock`
|
|
21
|
+
- `jetbrains-4242-51234.lock`
|
|
22
|
+
|
|
23
|
+
A lock file contains the IDE source, WebSocket host and port, an auth token, the
|
|
24
|
+
workspace folders, the process PID, and timestamps. Writers create a temp file and
|
|
25
|
+
rename it into place atomically; the directory is mode `0700` and files are mode
|
|
26
|
+
`0600`.
|
|
27
|
+
|
|
28
|
+
## How Pi picks a connection
|
|
29
|
+
|
|
30
|
+
When Pi starts, it scans every lock directory it knows about, parses each file,
|
|
31
|
+
and ranks the candidates. The ranking prefers a connection whose workspace
|
|
32
|
+
**contains** your terminal's current working directory (`relationshipMatchLength`
|
|
33
|
+
in `src/shared/paths.ts`):
|
|
34
|
+
|
|
35
|
+
1. A workspace folder that contains your `cwd` scores highest.
|
|
36
|
+
2. A workspace folder contained _by_ your `cwd` (you started Pi in a parent
|
|
37
|
+
directory) scores lower.
|
|
38
|
+
3. No relationship scores zero.
|
|
39
|
+
|
|
40
|
+
This is why the recommended workflow is to start Pi from **inside your project
|
|
41
|
+
directory**: the match is exact and Pi connects automatically. If your `cwd` is
|
|
42
|
+
only a parent (for example `~/`), no candidate wins outright - run `/ide` to
|
|
43
|
+
choose one manually, or `/ide auto` to re-attempt matching.
|
|
44
|
+
|
|
45
|
+
## Lock file lifecycle
|
|
46
|
+
|
|
47
|
+
Lock files are ephemeral. Pi parses each lock before it applies stale-file policy.
|
|
48
|
+
When Pi proves that a producer is live, it keeps the lock at any age.
|
|
49
|
+
|
|
50
|
+
- **Live local PID:** a usable local PID is a positive safe integer. A live usable
|
|
51
|
+
PID keeps the lock. A dead usable PID removes the lock.
|
|
52
|
+
- **Age-only cleanup:** if the PID is missing or unusable, or if PID checking is
|
|
53
|
+
disabled, Pi uses age. Files older than 24 hours are then deleted.
|
|
54
|
+
- **WSL Windows locks:** for `runningInWindows: true` when Pi runs in WSL, TCP
|
|
55
|
+
reachability is authoritative. Pi keeps a reachable lock and removes an
|
|
56
|
+
unreachable lock.
|
|
57
|
+
- **Malformed:** unparseable files are removed.
|
|
58
|
+
|
|
59
|
+
VS Code, Neovim, and JetBrains refresh owned locks every 15 minutes while they
|
|
60
|
+
are active. Refresh keeps the endpoint identity and the auth token, and it updates
|
|
61
|
+
freshness data. If the file is deleted while the producer is still active, the
|
|
62
|
+
lock can stay missing until the next heartbeat.
|
|
63
|
+
|
|
64
|
+
Zed does not write this lock file. Pi reads Zed state from SQLite.
|
|
65
|
+
|
|
66
|
+
When you close your IDE, the plugin removes its lock file. If the IDE crashes, Pi
|
|
67
|
+
reclaims the file on the next scan when liveness fails or age-only cleanup applies.
|
|
68
|
+
|
|
69
|
+
## WSL2: discovering an IDE on Windows
|
|
70
|
+
|
|
71
|
+
When Pi runs inside WSL and your IDE runs on native Windows, the IDE writes its
|
|
72
|
+
lock file to the Windows user profile. Pi scans both the Linux home lock directory
|
|
73
|
+
**and** Windows user profiles such as
|
|
74
|
+
`/mnt/c/Users/<user>/.pi/pi-x-ide/lock/`, skipping Windows system profiles.
|
|
75
|
+
|
|
76
|
+
A Windows lock file sets `runningInWindows: true`. When Pi runs in WSL,
|
|
77
|
+
TCP reachability to the Windows producer is authoritative. Pi does not use
|
|
78
|
+
the Windows PID as Linux process-liveness evidence.
|
|
79
|
+
|
|
80
|
+
### Resolving the connection host
|
|
81
|
+
|
|
82
|
+
The lock file's `host` is always `127.0.0.1`, which is correct for the IDE
|
|
83
|
+
process but **not** reachable from inside WSL. Pi resolves a usable host in this
|
|
84
|
+
order (`src/pi/ide-host.ts`):
|
|
85
|
+
|
|
86
|
+
1. **`PI_X_IDE_HOST_OVERRIDE`** - if set (in the environment or
|
|
87
|
+
`~/.pi/pi-x-ide/config.json` under `env`), use it directly.
|
|
88
|
+
2. **WSL default gateway** - if the lock has `runningInWindows: true` and Pi is in
|
|
89
|
+
WSL, parse the gateway from `ip route show` and use it only if a short TCP
|
|
90
|
+
probe to `<gateway>:<port>` succeeds (500 ms timeout).
|
|
91
|
+
3. **The lock file `host` value** - `127.0.0.1` for same-host connections.
|
|
92
|
+
4. **`127.0.0.1`** - if `host` is empty.
|
|
93
|
+
|
|
94
|
+
Most WSL setups need no configuration: step 2 handles it automatically. If your
|
|
95
|
+
WSL networking mode, firewall, or endpoint security blocks the probe, set
|
|
96
|
+
`PI_X_IDE_HOST_OVERRIDE` explicitly. See [Use Pi × IDE over WSL2](../how-to/use-wsl2.md).
|
|
97
|
+
|
|
98
|
+
## Path normalization
|
|
99
|
+
|
|
100
|
+
Editor paths arriving from a Windows IDE are Windows-style. Pi normalizes them to
|
|
101
|
+
the Linux host before workspace matching, UI display, `@file#Lx-Ly` mention
|
|
102
|
+
formatting, and prompt context formatting. For example, a WSL UNC path like
|
|
103
|
+
`\\wsl.localhost\Ubuntu\home\user\project` becomes `/home/user/project` when Pi is
|
|
104
|
+
running in the `Ubuntu` distro (`src/shared/platform.ts`).
|
|
105
|
+
|
|
106
|
+
## Related
|
|
107
|
+
|
|
108
|
+
- [Architecture](architecture.md) - the end-to-end data flow and per-editor
|
|
109
|
+
transports.
|
|
110
|
+
- [Protocol specification](../specs/ide-protocol.md) - lock file schema, handshake,
|
|
111
|
+
and notification semantics.
|
|
112
|
+
- [Troubleshoot a missing connection](../how-to/troubleshoot-connection.md) - what
|
|
113
|
+
to do when discovery does not connect.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Effect adoption
|
|
2
|
+
|
|
3
|
+
Pi × IDE uses [Effect](https://effect.website/) as an **implementation detail**
|
|
4
|
+
for typed failures, Schema validation, and composable async work on the Pi side.
|
|
5
|
+
Public APIs that VS Code, tests, and Pi host callbacks call stay Promise- or
|
|
6
|
+
sync-friendly. This page is for contributors who touch `src/shared/*` or
|
|
7
|
+
`src/pi/*`.
|
|
8
|
+
|
|
9
|
+
## Why Effect here
|
|
10
|
+
|
|
11
|
+
- **Structured errors** — tagged failures (`LockFileParseError`, connect/install
|
|
12
|
+
errors) carry fields without free-form string parsing.
|
|
13
|
+
- **Schema decode** — lock files, selections, and diagnostic payloads share one
|
|
14
|
+
validation path (`effect-schema` → `schema.ts` adapters).
|
|
15
|
+
- **Boundary control** — IO programs (discovery, host resolve, install, connect,
|
|
16
|
+
reconnect, Zed poll) can fail closed or log-and-swallow at process edges.
|
|
17
|
+
|
|
18
|
+
Effect is not a product feature. End users never configure it.
|
|
19
|
+
|
|
20
|
+
## Boundary rule
|
|
21
|
+
|
|
22
|
+
- **Never** throw Effect failures into Pi host callbacks or VS Code activation.
|
|
23
|
+
- Exit Effects with `Effect.runPromise` / `Effect.runSync`, Phase 1 runners
|
|
24
|
+
(`runEffect` / `runEffectSync`), or Pi helpers (`runPiEffect`).
|
|
25
|
+
- On failure at a Pi boundary: report via `logExtensionError` / `containPiError`
|
|
26
|
+
(pi `ui.notify`, not `console`); optionally set `runtime.connectionStatus = "error"`.
|
|
27
|
+
- **Interrupt** (reconnect / Zed poll fibers) is cancellation, not a UI error.
|
|
28
|
+
|
|
29
|
+
Auth tokens stay in WebSocket upgrade headers only. Do not put `authToken` or
|
|
30
|
+
full lock JSON into logs, Effect error messages, or `scope` strings.
|
|
31
|
+
|
|
32
|
+
## Module map
|
|
33
|
+
|
|
34
|
+
| Module | Role |
|
|
35
|
+
| ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| `src/shared/effect-errors.ts` | `Data.TaggedError` domain errors with readable `message` getters |
|
|
37
|
+
| `src/shared/effect-runtime.ts` | `runEffect` / `runEffectSync` (log-and-swallow); `runEffectOrThrow` (tests) |
|
|
38
|
+
| `src/shared/effect-schema.ts` | Effect Schema definitions + `decode*` helpers |
|
|
39
|
+
| `src/shared/schema.ts` | Stable `is*` / `parse*` facades over Schema decode |
|
|
40
|
+
| `src/shared/jsonrpc-guard.ts` | Effect-free `isJsonRpcRequest` for the VS Code `ide-server` import graph |
|
|
41
|
+
| `src/pi/index.ts`, `commands.ts`, `context.ts`, `ui.ts`, `state.ts`, `safety.ts`, `diagnostic-renderer.ts` | Lightweight static shell: registration, status UI, local diagnostic renderer, error containment (no Effect, no pi-tui) |
|
|
42
|
+
| `src/pi/config-ui.ts` | Settings dialog (pi-tui); loaded only via dynamic import from `/ide settings` |
|
|
43
|
+
| `src/pi/runtime-loader.ts` | Heavy-runtime dynamic boundary: cached `import("./runtime-services.js")` |
|
|
44
|
+
| `src/pi/runtime-services.ts` | Heavy lifecycle: discovery/install/connect/reconnect/Zed (may import Effect) |
|
|
45
|
+
| `src/pi/effect-boundary.ts` | `runPiEffect` → `containPiError` + UI status (Effect runtime import) |
|
|
46
|
+
| `src/pi/safety.ts` | Effect-free `containPiError`, `runPiBoundary`, `runPiBoundaryAsync` |
|
|
47
|
+
| `src/pi/discovery.ts`, `ide-host.ts`, `install.ts` | Effect programs + Promise facades |
|
|
48
|
+
| `src/pi/connection.ts` | Connect handshake Effect; maps tagged timeout → `IdeConnectionTimeoutError` |
|
|
49
|
+
| `src/pi/reconnect.ts`, `zed.ts` | Interruptible fibers for reconnect delay and Zed poll |
|
|
50
|
+
|
|
51
|
+
VS Code still imports `@shared/*` pieces that must stay Effect-free on the hot
|
|
52
|
+
path (`jsonrpc-guard`, protocol, lock-file helpers). Pulling `effect-schema` into
|
|
53
|
+
the VS Code bundle is an escape-hatch failure: keep decode on the Pi path or
|
|
54
|
+
split guards if bundle size grows.
|
|
55
|
+
|
|
56
|
+
### Pi startup shell vs heavy runtime
|
|
57
|
+
|
|
58
|
+
- The published Pi entry is an esbuild code-split ESM shell (`dist/src/pi/index.js`)
|
|
59
|
+
with Effect/`ws`/`node:sqlite` only on **dynamic-import** edges into lazy chunks.
|
|
60
|
+
- `runtime-loader.ts` is the only dynamic boundary for the heavy lifecycle
|
|
61
|
+
(`runtime-services.ts`). `effect-boundary.ts` may import Effect; the static
|
|
62
|
+
shell must not.
|
|
63
|
+
- `/ide settings` is a second lazy boundary: `commands.ts` dynamically imports
|
|
64
|
+
`config-ui.ts` only when the settings subcommand runs. That keeps `pi-tui`
|
|
65
|
+
(SettingsList and friends) out of the static shell graph. Because the lazy
|
|
66
|
+
chunk bundles its own pi-tui code, it seeds that copy with the keybinding
|
|
67
|
+
manager injected by `ctx.ui.custom()` so user remappings still apply.
|
|
68
|
+
- The diagnostic message renderer is a **local** Component-shaped helper
|
|
69
|
+
(`render` / `invalidate`) so the shell never takes a runtime edge into
|
|
70
|
+
`@earendil-works/pi-tui`. Config UI uses a local `DynamicBorder` for the same
|
|
71
|
+
reason — no coding-agent value import for borders.
|
|
72
|
+
- **Host package runtime edge ban:** every generated Pi entry JS output (static
|
|
73
|
+
entry and lazy chunks) must have **zero** runtime external imports of
|
|
74
|
+
`@earendil-works/pi-coding-agent` or `@earendil-works/pi-tui`. Type-only imports
|
|
75
|
+
are fine; value imports of host packages in native ESM bypass Pi's jiti aliases
|
|
76
|
+
and can double-load host copies. The bundler metafile gate fails the build on
|
|
77
|
+
any remaining host external edge. `pi-tui` is bundled into the settings lazy
|
|
78
|
+
chunk; coding-agent stays listed as esbuild `external` for future checks but
|
|
79
|
+
must not appear as a runtime edge in outputs.
|
|
80
|
+
- Adding a static Effect, heavy-runtime, or host-package value import to the
|
|
81
|
+
shell is a **startup regression**. Topology/size gates fail the build if the
|
|
82
|
+
static entry gains a heavy static dependency or if any output reintroduces a
|
|
83
|
+
host runtime external edge.
|
|
84
|
+
|
|
85
|
+
## Import style
|
|
86
|
+
|
|
87
|
+
Prefer Effect subpath imports for tree-shaking:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import * as Effect from "effect/Effect";
|
|
91
|
+
import * as Schema from "effect/Schema";
|
|
92
|
+
import * as Data from "effect/Data";
|
|
93
|
+
import * as Fiber from "effect/Fiber";
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Error tagging
|
|
97
|
+
|
|
98
|
+
- Use tags listed in `effect-errors.ts` (or add a tag in the phase that needs it).
|
|
99
|
+
- Override `message` so default logging is useful without dumping secrets.
|
|
100
|
+
- Promise facades that existing code checks with `instanceof` (e.g.
|
|
101
|
+
`IdeConnectionTimeoutError`) must map tagged failures back to those classes.
|
|
102
|
+
|
|
103
|
+
## What stays Promise / sync based
|
|
104
|
+
|
|
105
|
+
- Exported discovery, install, connect, and host-resolve entry points.
|
|
106
|
+
- `IdeConnection` class methods and callback model.
|
|
107
|
+
- VS Code extension activation and shared IDE host WebSocket server.
|
|
108
|
+
- Pure helpers: ranking, version compare, path matching, Zed snapshot SQL.
|
|
109
|
+
|
|
110
|
+
Call sites in the heavy runtime (`runtime-services.ts` and deeper) should keep
|
|
111
|
+
using Promise facades unless a path explicitly opts into `runPiEffect` from
|
|
112
|
+
`effect-boundary.ts`. The lightweight shell must not import `effect-boundary.ts`
|
|
113
|
+
or Effect modules.
|
|
114
|
+
|
|
115
|
+
## Non-goals
|
|
116
|
+
|
|
117
|
+
- Full `Layer` DI rewrite of the extension entrypoint.
|
|
118
|
+
- JetBrains Kotlin / Neovim Lua rewrites.
|
|
119
|
+
- Replacing `node:test` with `@effect/vitest` as a requirement.
|
|
120
|
+
- On-wire protocol or lock-file JSON shape changes.
|
|
121
|
+
- Mock modes or fake Effect data paths.
|
|
122
|
+
|
|
123
|
+
## Related
|
|
124
|
+
|
|
125
|
+
- [Architecture](architecture.md) — process roles and data flow.
|
|
126
|
+
- [How discovery works](discovery.md) — lock ranking (behavior unchanged by Effect).
|
|
127
|
+
- Implementation plans: [effect-adoption-plan](../plans/effect-adoption-plan/).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Install the JetBrains plugin
|
|
2
|
+
|
|
3
|
+
Pi × IDE integrates with IntelliJ IDEA, PyCharm, WebStorm, and other JetBrains
|
|
4
|
+
IDEs through a marketplace plugin.
|
|
5
|
+
|
|
6
|
+
## Prerequisites
|
|
7
|
+
|
|
8
|
+
- The [`pi`](https://github.com/earendil-works/pi-coding-agent) CLI installed.
|
|
9
|
+
- The `pi-x-ide` Pi package installed: `pi install npm:pi-x-ide`.
|
|
10
|
+
- A JetBrains IDE (2024.2 or later recommended).
|
|
11
|
+
|
|
12
|
+
## Install from the JetBrains Marketplace
|
|
13
|
+
|
|
14
|
+
1. In your IDE, open **Settings / Preferences -> Plugins -> Marketplace**.
|
|
15
|
+
2. Search for `balaenis.pi-x-ide`.
|
|
16
|
+
3. Click **Install**, then restart the IDE when prompted.
|
|
17
|
+
|
|
18
|
+
The plugin is also listed on the
|
|
19
|
+
[JetBrains Marketplace](https://plugins.jetbrains.com/plugin/32664-pi-x-ide).
|
|
20
|
+
|
|
21
|
+
## Verify the connection
|
|
22
|
+
|
|
23
|
+
1. Open a project in your JetBrains IDE.
|
|
24
|
+
2. Open a terminal in that project and start Pi:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pi
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. Open a source file and select some text. The Pi widget should show:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
⧉ ⇡ foo.ts#L10-L20
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If the widget does not appear, see
|
|
37
|
+
[Troubleshoot a missing connection](troubleshoot-connection.md).
|
|
38
|
+
|
|
39
|
+
## Attach a selection
|
|
40
|
+
|
|
41
|
+
With text selected in the editor, do either of the following:
|
|
42
|
+
|
|
43
|
+
- Press `Ctrl+Alt+K` (Linux/Windows) or `Cmd+Alt+K` (macOS).
|
|
44
|
+
- Run the **Pi x IDE: Attach Selection** action from the Find Action dialog
|
|
45
|
+
(`Ctrl+Shift+A` / `Cmd+Shift+A`).
|
|
46
|
+
|
|
47
|
+
Pi inserts `@relative/path#Lx-Ly` into the input box. Submit your message to send
|
|
48
|
+
the selected text as LLM context.
|
|
49
|
+
|
|
50
|
+
## Notes
|
|
51
|
+
|
|
52
|
+
- JetBrains sends selection notifications for both non-empty selections and an
|
|
53
|
+
active file with no selected text.
|
|
54
|
+
- Diagnostic Quick Fix and auto-install are VS Code-family features and are not
|
|
55
|
+
available for JetBrains. See the
|
|
56
|
+
[feature parity](../reference/feature-parity.md) table.
|
|
57
|
+
- For local development and sandbox testing, see
|
|
58
|
+
[CONTRIBUTING.md](../../CONTRIBUTING.md).
|