@saifymatteo/pi-x-ide 0.0.0-stage → 1.20.2

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/CONTRIBUTING.md +119 -0
  3. package/CONTRIBUTING.zh-CN.md +106 -0
  4. package/LICENSE +201 -0
  5. package/README.md +33 -2
  6. package/README.zh-CN.md +34 -0
  7. package/RELEASE.md +213 -0
  8. package/config.example.json +9 -0
  9. package/dist/src/pi/chunks/chunk-4RHJAL4S.js +200 -0
  10. package/dist/src/pi/chunks/chunk-UUXVCTOS.js +819 -0
  11. package/dist/src/pi/chunks/config-ui-LO3DVQFC.js +8073 -0
  12. package/dist/src/pi/chunks/runtime-services-BVQBAAWT.js +19569 -0
  13. package/dist/src/pi/commands.js +126 -0
  14. package/dist/src/pi/config-ui.js +127 -0
  15. package/dist/src/pi/connection.js +237 -0
  16. package/dist/src/pi/context.js +30 -0
  17. package/dist/src/pi/diagnostic-renderer.js +326 -0
  18. package/dist/src/pi/diagnostic-types.js +1 -0
  19. package/dist/src/pi/diagnostics.js +104 -0
  20. package/dist/src/pi/discovery.js +135 -0
  21. package/dist/src/pi/effect-boundary.js +5 -0
  22. package/dist/src/pi/ide-host.js +68 -0
  23. package/dist/src/pi/index.js +636 -0
  24. package/dist/src/pi/install.js +308 -0
  25. package/dist/src/pi/reconnect.js +57 -0
  26. package/dist/src/pi/runtime-loader.js +38 -0
  27. package/dist/src/pi/runtime-services.js +356 -0
  28. package/dist/src/pi/safety.js +61 -0
  29. package/dist/src/pi/selection.js +18 -0
  30. package/dist/src/pi/state.js +13 -0
  31. package/dist/src/pi/ui.js +304 -0
  32. package/dist/src/pi/zed.js +409 -0
  33. package/dist/src/shared/config-options.js +45 -0
  34. package/dist/src/shared/config.js +183 -0
  35. package/dist/src/shared/display-width.js +99 -0
  36. package/dist/src/shared/effect-errors.js +36 -0
  37. package/dist/src/shared/effect-runtime.js +36 -0
  38. package/dist/src/shared/effect-schema.js +120 -0
  39. package/dist/src/shared/errors.js +48 -0
  40. package/dist/src/shared/format.js +92 -0
  41. package/dist/src/shared/ide-server.js +167 -0
  42. package/dist/src/shared/jsonrpc-guard.js +12 -0
  43. package/dist/src/shared/lock-file-heartbeat.js +58 -0
  44. package/dist/src/shared/lock-file.js +50 -0
  45. package/dist/src/shared/paths.js +79 -0
  46. package/dist/src/shared/platform.js +57 -0
  47. package/dist/src/shared/protocol.js +3 -0
  48. package/dist/src/shared/schema.js +37 -0
  49. package/dist/src/shared/version.js +1 -0
  50. package/dist/src/shared/ws.js +8 -0
  51. package/docs/explanation/architecture.md +123 -0
  52. package/docs/explanation/discovery.md +113 -0
  53. package/docs/explanation/effect.md +127 -0
  54. package/docs/how-to/install-jetbrains.md +58 -0
  55. package/docs/how-to/install-neovim.md +138 -0
  56. package/docs/how-to/install-vscode.md +75 -0
  57. package/docs/how-to/install-zed.md +70 -0
  58. package/docs/how-to/troubleshoot-connection.md +103 -0
  59. package/docs/how-to/use-wsl2.md +90 -0
  60. package/docs/reference/commands.md +59 -0
  61. package/docs/reference/configuration.md +101 -0
  62. package/docs/reference/feature-parity.md +26 -0
  63. package/docs/reference/protocol.md +71 -0
  64. package/docs/specs/ide-protocol.md +215 -0
  65. package/docs/tutorials/get-started.md +133 -0
  66. package/docs/zh-CN/explanation/architecture.md +93 -0
  67. package/docs/zh-CN/explanation/discovery.md +71 -0
  68. package/docs/zh-CN/explanation/effect.md +90 -0
  69. package/docs/zh-CN/how-to/install-jetbrains.md +49 -0
  70. package/docs/zh-CN/how-to/install-neovim.md +126 -0
  71. package/docs/zh-CN/how-to/install-vscode.md +67 -0
  72. package/docs/zh-CN/how-to/install-zed.md +56 -0
  73. package/docs/zh-CN/how-to/troubleshoot-connection.md +85 -0
  74. package/docs/zh-CN/how-to/use-wsl2.md +75 -0
  75. package/docs/zh-CN/reference/commands.md +54 -0
  76. package/docs/zh-CN/reference/configuration.md +94 -0
  77. package/docs/zh-CN/reference/feature-parity.md +21 -0
  78. package/docs/zh-CN/reference/protocol.md +54 -0
  79. package/docs/zh-CN/tutorials/get-started.md +101 -0
  80. package/package.json +86 -4
  81. 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,3 @@
1
+ export const PROTOCOL_VERSION = 1;
2
+ export const AUTH_HEADER = "x-pi-x-ide-authorization";
3
+ export const LOCK_FILE_EXTENSION = ".lock";
@@ -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.2";
@@ -0,0 +1,8 @@
1
+ import { Buffer } from "node:buffer";
2
+ export function decodeRawData(raw) {
3
+ if (Array.isArray(raw))
4
+ return Buffer.concat(raw).toString("utf8");
5
+ if (Buffer.isBuffer(raw))
6
+ return raw.toString("utf8");
7
+ return Buffer.from(raw).toString("utf8");
8
+ }
@@ -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).