# JetBrains Plugin — Wire Compatibility Invariants

**Protocol version:** `1.1.0` (sourced from `src/version.ts` `BRIDGE_PROTOCOL_VERSION`, confirmed matching `EXTENSION_PROTOCOL_VERSION` in `vscode-extension/src/constants.ts`)

---

## 1. Exact Wire Method Names

All methods use the `extension/` namespace. There is no `editor/` or `workspace/` or `clipboard/` namespace on the wire. Any planning doc that says `editor/getSelection` is shorthand only — the wire name is `extension/getSelection`.

### Requests (bridge → plugin, plugin must respond)

| Wire method | MVP disposition | Notes |
|---|---|---|
| `extension/getSelection` | Full impl | no params |
| `extension/getOpenFiles` | Full impl | no params |
| `extension/getFileContent` | Full impl | params: `{ file: string }` |
| `extension/openFile` | Full impl | params: `{ file: string, line?: number }` |
| `extension/isDirty` | Full impl | params: `{ file: string }` |
| `extension/getWorkspaceFolders` | Full impl | no params |
| `extension/getDiagnostics` | Full impl | params: `{ file?: string }` |
| `extension/readClipboard` | Full impl | no params |
| `extension/writeClipboard` | Full impl | params: `{ text: string }` |
| `extension/saveFile` | Full impl | params: `{ file: string }` |
| `extension/closeTab` | Full impl | params: `{ file: string }` |
| `extension/getAIComments` | Stub | |
| `extension/createFile` | Full impl | params: `{ filePath, content?, isDirectory?, overwrite?, openAfterCreate? }` |
| `extension/deleteFile` | Full impl | params: `{ filePath, recursive?, useTrash? }` |
| `extension/renameFile` | Full impl | params: `{ oldPath, newPath, overwrite? }` |
| `extension/editText` | Full impl | |
| `extension/replaceBlock` | Full impl | |
| `extension/listTerminals` | Full impl | |
| `extension/getTerminalOutput` | Full impl (capability stub) | always `{available:false}` — IJ API limitation |
| `extension/createTerminal` | Full impl | |
| `extension/disposeTerminal` | Full impl | |
| `extension/sendTerminalCommand` | Full impl | |
| `extension/executeInTerminal` | Full impl | runs via GeneralCommandLine, not in a tab |
| `extension/waitForTerminalOutput` | Full impl (capability stub) | always `{matched:false}` — IJ API limitation |
| `extension/formatDocument` | Stub | params: `{ file: string }` |
| `extension/fixAllLintErrors` | Stub | params: `{ file: string }` |
| `extension/organizeImports` | Stub | params: `{ file: string }` |
| `extension/goToDefinition` | Stub | |
| `extension/findReferences` | Stub | |
| `extension/findImplementations` | Stub | |
| `extension/goToTypeDefinition` | Stub | |
| `extension/goToDeclaration` | Stub | |
| `extension/getHover` | Stub | |
| `extension/getCodeActions` | Stub | |
| `extension/applyCodeAction` | Stub | |
| `extension/previewCodeAction` | Stub | |
| `extension/renameSymbol` | Stub | |
| `extension/searchSymbols` | Stub | |
| `extension/prepareRename` | Stub | |
| `extension/formatRange` | Stub | |
| `extension/signatureHelp` | Stub | |
| `extension/foldingRanges` | Stub | |
| `extension/selectionRanges` | Stub | |
| `extension/watchFiles` | Stub | params: `{ id, pattern }` |
| `extension/unwatchFiles` | Stub | params: `{ id }` |
| `extension/captureScreenshot` | Stub | |
| `extension/listTasks` | Stub | |
| `extension/runTask` | Stub | |
| `extension/getWorkspaceSettings` | Stub | |
| `extension/setWorkspaceSetting` | Stub | |
| `extension/executeVSCodeCommand` | Stub | |
| `extension/listVSCodeCommands` | Stub | |
| `extension/getInlayHints` | Stub | |
| `extension/getTypeHierarchy` | Stub | |
| `extension/getSemanticTokens` | Stub | |
| `extension/getCodeLens` | Stub | |
| `extension/getDocumentLinks` | Stub | |
| `extension/getDocumentSymbols` | Stub | |
| `extension/getCallHierarchy` | Stub | |
| `extension/getDebugState` | Stub | |
| `extension/evaluateInDebugger` | Stub | |
| `extension/setDebugBreakpoints` | Stub | |
| `extension/startDebugging` | Stub | |
| `extension/stopDebugging` | Stub | |
| `extension/setDecorations` | Stub | |
| `extension/clearDecorations` | Stub | |

### Notifications (plugin → bridge, no response)

| Wire method | Buffered across reconnect? |
|---|---|
| `extension/hello` | No — first message on every connect |
| `extension/lspReady` | Yes |
| `extension/diagnosticsChanged` | Yes |
| `extension/fileChanged` | Yes |
| `extension/aiCommentsChanged` | Yes |
| `extension/selectionChanged` | No |
| `extension/activeFileChanged` | No |
| `extension/fileSaved` | No |
| `extension/debugSessionChanged` | No |
| `extension/rttUpdate` | No — stale latency is meaningless |

### Notifications (bridge → plugin, plugin must handle)

| Wire method | Params |
|---|---|
| `bridge/claudeConnectionChanged` | `{ connected: boolean, stats?: { callCount, errorCount, durationMs } }` |
| `bridge/claudeTaskOutput` | `{ taskId, chunk?, done?, status? }` |
| `extension/bridgeLiveState` | `{ contextCachedAt?, preCompactArmed?, debugSessionActive? }` |

---

## 2. Position and Path Conventions

**Line/column:** 1-based. Sourced from `vscode-extension/src/handlers/selection.ts`: `sel.start.line + 1`, `sel.start.character + 1`. No exceptions.

**File paths:** Absolute filesystem paths (not `file://` URIs). The `extension/getSelection` response uses `editor.document.uri.fsPath`. All `file` and `filePath` params throughout handlers expect absolute filesystem paths.

**`extension/getSelection` — no active editor:** Returns `{ "error": "No active editor" }` — not `null`, not a JSON-RPC error, not `{ "success": false }`. This is a result-level object.

**`extension/getOpenFiles` path field:** The field name is `filePath` (not `path`, not `uri`). Shape per item:
```json
{ "filePath": "/abs/path/to/file.kt", "isActive": true, "isDirty": false, "languageId": "kotlin" }
```

**`extension/readClipboard` shape:**
```json
{ "text": "...", "byteLength": 17, "truncated": false }
```
`byteLength` is the original byte length before truncation. Truncated at 100 KB.

**`extension/writeClipboard` shape:**
```json
{ "written": true, "byteLength": 17 }
```
or `{ "written": false, "error": "description" }`. Text > 1 MB returns JSON-RPC `-32602`.

**`extension/getDiagnostics` — two shapes:**
- File-scoped (`file` param present): plain array of diagnostic objects, 1-based line/column.
- Workspace-scoped (no `file` param): `{ "diagnostics": [{ "file": "...", "diagnostics": [...] }], "truncated": false }`. Cap at 500 total; set `truncated: true` when hit.

**`extension/saveFile` shape:**
```json
true
```
Returns a bare JSON boolean `true` on success (same as `extension/openFile`). Returns `{ "success": false, "error": "..." }` for null project, file not open in editor, or untitled buffer.

**`extension/closeTab` shape:**
```json
{ "success": true, "promptedToSave": false }
```
`promptedToSave` reflects whether the document had unsaved changes at close time. Returns `{ "success": false, "error": "Tab not found" }` if file not open.

**`extension/createFile` shape:**
```json
{ "success": true, "filePath": "/abs/path", "isDirectory": false, "created": true }
```
`created: false` when file already exists and `overwrite: false` (default). `openAfterCreate` defaults to `true`.

**`extension/deleteFile` shape:**
```json
{ "success": true, "filePath": "/abs/path" }
```
Non-recursive delete of non-empty directory returns `{ "success": false, "error": "..." }`.

**`extension/renameFile` shape:**
```json
{ "success": true, "oldPath": "...", "newPath": "..." }
```

**`extension/editText` shape:**
```json
{ "success": true, "editCount": 3, "saved": false }
```
`save` param defaults to `false`. All edits are pre-validated in `runReadAction` before the write action is opened — if any offset is out of range, the entire request fails with no partial mutations. Wire coordinates are 1-based; converted to 0-based internally.

**`extension/replaceBlock` shape:**
```json
{ "success": true, "saved": true, "source": "intellij-buffer" }
```
`save` param defaults to `true` (differs from `editText`). `source` is `"intellij-buffer"` (VS Code returns `"vscode-buffer"`). Exact error texts on failure:
- Not found: `"oldContent not found in file — verify the exact text including whitespace and line endings"`
- Ambiguous: `"oldContent matches N locations — add more surrounding context to make it unique"`

**`extension/listTerminals` shape:**
```json
{ "terminals": [{ "name": "Terminal", "index": 0, "isActive": false, "hasOutputCapture": false }], "count": 1, "outputCaptureAvailable": false }
```
`outputCaptureAvailable` is always `false` — IJ terminal plugin has no public output buffer API. `isActive` is always `false` — no reliable activeTerminal API in IJ.

**`extension/createTerminal` shape:**
```json
{ "success": true, "name": "Terminal", "index": 0 }
```
Uses `TerminalView.createLocalShellWidget(cwd, name)` via reflection (falls back to `createNewSession` on older IJ). `show` param defaults to `true`.

**`extension/disposeTerminal` shape:**
```json
{ "success": true, "terminalName": "my-term" }
```
Not-found returns: `{ "success": false, "error": "Terminal not found...", "availableTerminals": ["..."] }`.

**`extension/sendTerminalCommand` shape:**
```json
{ "success": true, "terminalName": "my-term" }
```
Metacharacter validation rejects `[;&|` + `$()<>{}!\\\n\r]`. `addNewline` defaults to `true`. Not-found returns `availableTerminals` array same as `disposeTerminal`.

**`extension/getTerminalOutput` shape (IJ limitation):**
```json
{ "available": false, "error": "Terminal output capture is not available in the JetBrains plugin..." }
```
IJ terminal API has no public output buffer. Always returns `available: false`. Callers should use `executeInTerminal` for output capture.

**`extension/waitForTerminalOutput` shape (IJ limitation):**
```json
{ "matched": false, "error": "Terminal output capture is not available in the JetBrains plugin..." }
```
Same limitation as `getTerminalOutput`. `pattern` param is validated (non-empty string required) but cannot be matched.

**`extension/executeInTerminal` shape:**
```json
{ "success": true, "exitCode": 0, "output": "hello\n" }
```
Runs via `GeneralCommandLine(/bin/sh -c <command>)` — not in a visible terminal tab. Output capped at 512 KB (`truncated: true` when hit). ANSI escape codes stripped. `timeoutMs` clamped to 1000–300000 ms (default 30000). Timeout response: `{ "success": false, "error": "Command timed out after Xms", "timedOut": true, "output": "<partial output>" }`. Metacharacter validation same as `sendTerminalCommand` **except** newlines — newlines tested separately before metachar check with error `"Command must not contain newlines"`.

---

## 3. Active Project Resolution

These rules are JetBrains-specific (VS Code is single-workspace). All handlers snapshot the active project on request arrival and do not re-resolve mid-handler.

| Condition | Resolution |
|---|---|
| Single project open | Use it |
| Multiple projects open | Use last-focused (tracked via `FrameStateListener`) |
| No focused frame / welcome screen / tool window | Use last-focused project if any was ever focused |
| No project at all | Return `{ "success": false, "error": "No project open" }` |
| Focus switches during in-flight request | Use project active at request receipt — snapshot on arrival |
| Null `basePath` (default project) | Treat as no project |

**WorkspaceGuard containment check:**
- Get content roots: `ProjectRootManager.getInstance(project).contentRoots`
- A file is in-scope if its canonical path starts with any content root's canonical path
- Resolve symlinks via `Files.realPath()` before comparison
- MVP single-root exception: if exactly one content root exists, `basePath` is sufficient
- Containment failure: `{ "success": false, "error": "path outside workspace" }`

---

## 4. Connection Lifecycle Invariants

All values sourced from `vscode-extension/src/constants.ts` and `vscode-extension/src/connection.ts`.

**WebSocket auth header:** `x-claude-ide-extension: <authToken>` (confirmed `connection.ts` line 320). The field name `x-claude-code-ide-authorization` is **wrong** — it does not authenticate.

**Lock file selection** (matches `lockfiles.ts`):
1. List all `*.lock` files in `${CLAUDE_CONFIG_DIR:-~/.claude}/ide/`
2. Sort by `mtime` descending (newest first)
3. For each: parse JSON, require `isBridge: true`, valid `port`, non-empty `authToken`
4. Check `startedAt` freshness: missing or non-numeric `startedAt` → treat as `0` (epoch) → `now - 0 > 24h` → rejected. Drop if `now - startedAt > 24h`.
5. Check PID liveness: `ProcessHandle.of(pid).isPresent`; treat `SecurityException` as alive
6. Prefer candidate whose `workspace` field (canonical) matches the active project path
7. Fall back to first remaining candidate

| Constant | Value | Source |
|---|---|---|
| `RECONNECT_BASE_DELAY` | 1000 ms | `constants.ts` |
| `RECONNECT_MAX_DELAY` | 30000 ms | `constants.ts` |
| `HANDLER_TIMEOUT` | 30000 ms | `constants.ts` |
| WebSocket open timeout | 30000 ms | `connection.ts` |
| Bridge unresponsive timeout | 120000 ms | `connection.ts` |
| Heartbeat check interval | 45000 ms | `connection.ts` |
| Notification buffer cap | 20 items | `connection.ts` |

**Reconnect backoff formula:** `jitteredDelay = round(500 + random() * currentDelay)`, then `currentDelay = min(currentDelay * 2, 30000)`. Resets to `RECONNECT_BASE_DELAY` on successful connect.

**Generation guard:** Each `connect()` call increments a monotonic integer `generation`. Every async callback captures `gen = this.generation` at creation time and silently returns without acting if `gen !== this.generation` when it fires. Required — prevents stale reconnect callbacks from corrupting new connection state.

**Heartbeat / liveness:** Liveness is tracked via **WebSocket ping frames from the bridge**, not JSON messages. Bridge sends ping frames ~every 30s. Plugin must:
- Override `onWebsocketPing` (Java-WebSocket), record `lastBridgePingMs = now`, then call `super` to send the pong frame
- Check every 45s: if `now - lastBridgePingMs > 120s`, close socket and reconnect
- Do NOT use `onMessage` timestamps for liveness — idle connections send no JSON but the bridge keeps pinging

**Active project snapshot:** Capture `val project = activeProject` in `onMessage` **before** `scheduler.submit {}`. The submit delay allows a focus switch to change `activeProject`; the snapshot pins the project to the moment the request arrived.

**Notification replay on reconnect:** On successful reconnect, flush buffered notifications in order before processing new messages. Buffer cap 20 items; drop oldest when full. Buffered methods: `extension/diagnosticsChanged`, `extension/fileChanged`, `extension/aiCommentsChanged`, `extension/lspReady`.

**`extension/hello` on connect:** Must be the first message sent after WebSocket open, before any other notifications.
```json
{ "extensionVersion": "1.1.0", "packageVersion": "<plugin-version>" }
```
The bridge reads only `extensionVersion` (major-version check) and `packageVersion` (stored for logging). All other fields — including VS Code's `vscodeVersion` — are ignored. The JetBrains plugin may include an `ideVersion` field for diagnostics but must not rely on it being read.

---

## 5. `extension/lspReady` Exact Contract

- **Wire method:** `extension/lspReady` (notification — no `id`, no response)
- **Trigger (JetBrains):** Subscribe to `DaemonCodeAnalyzer.DaemonListener` via `project.messageBus.connect()`. On `daemonFinished(fileEditor)`, derive the language for the finished file and mark it ready — mirroring VS Code's `onDidChangeDiagnostics` pattern. Also start a 30 s fallback timer per project on connect; when it fires, send `lspReady` for all languages found in currently open files that haven't been marked ready yet. This handles projects with no diagnostics where the daemon may not fire prominently.
- **Language ID derivation:** Enumerate `FileEditorManager.getInstance(project).openFiles`, call `LanguageUtil.getFileLanguage(vFile)?.id?.lowercase()`, filter nulls, deduplicate into a `Set<String>`. Emit one notification per unique language.
- **Params shape:** `{ "languageId": "kotlin", "timestamp": <epoch-ms> }` — one notification per language, not batched.
- **Deduplication:** Each language sent at most once per session. Track in a `readyLanguages: MutableSet<String>`.
- **Re-sent after reconnect:** Yes — replay all entries in `readyLanguages` on every successful reconnect (these are bufferable methods).

---

## 6. Stub and Error Semantics

| Situation | Response type | Shape |
|---|---|---|
| Known method, not yet implemented | Result | `{ "success": false, "error": "Not implemented in JetBrains plugin MVP" }` |
| Unknown method (not in handler registry) | JSON-RPC error | `{ "code": -32601, "message": "Method not found: <method>" }` |
| Invalid params (missing required field, wrong type) | JSON-RPC error | `{ "code": -32602, "message": "<description>" }` |
| Internal error (exception during handler) | JSON-RPC error | `{ "code": -32603, "message": "<sanitized message>" }` — no stack trace on wire |
| Too many concurrent pending handlers (> 50) | JSON-RPC error | `{ "code": -32000, "message": "Too many pending handlers — try again later" }` |

**Result response wire format:**
```json
{ "jsonrpc": "2.0", "id": <request-id>, "result": <value> }
```

**Error response wire format:**
```json
{ "jsonrpc": "2.0", "id": <request-id>, "error": { "code": -32601, "message": "Method not found: extension/foo" } }
```

`result` and `error` are mutually exclusive. Never send both. Never omit `id` on a response to a request. Never send a response for a notification (a message with no `id`).
