Przeglądaj źródła

Changed communication between UI <-> IDE to HTTP + SSE

paviko 2 tygodni temu
rodzic
commit
d262e69a68
2 zmienionych plików z 161 dodań i 165 usunięć
  1. 0 165
      hosts/IDE_BRIDGE.md
  2. 161 0
      hosts/IDE_BRIDGE_HTTP_SSE.md

+ 0 - 165
hosts/IDE_BRIDGE.md

@@ -1,165 +0,0 @@
-# ideBridge: Unified Host ↔ UI Messaging
-
-This document explains the unified `ideBridge` used by IDE hosts (JetBrains, VSCode) and the web UI (packages/opencode/webgui). It standardizes bidirectional communication, supports request/response (RPC), centralizes delivery, and handles lifecycle queueing.
-
-## Goals
-
-- Unify the bridge API under `window.ideBridge`
-- Request/response semantics (id/replyTo)
-- Centralize host→UI delivery (single injection point)
-- Use only message-based interaction (no scattered globals)
-- Queueing and lifecycle resilience on both sides
-
-We intentionally defer: strict origin checks, schemas, and runtime validation for now.
-
----
-
-## Message shape
-
-- Common fields:
-  - `type: string` – message kind
-  - `payload?: any` – arbitrary data per type
-  - `id?: string` – present for requests
-  - `replyTo?: string` – present for responses
-  - `ok?: boolean` and `error?: string` – optional result flags in responses
-  - `timestamp?: number`
-
-Example request:
-
-```json
-{ "id": "abc123", "type": "openFile", "payload": { "path": "/p/file.ts", "line": 42 }, "timestamp": 1731390000000 }
-```
-
-Example response:
-
-```json
-{ "replyTo": "abc123", "ok": true }
-```
-
----
-
-## JetBrains host (JCEF)
-
-- Entry point: `paviko.opencode.ui.IdeBridge`
-  - Creates a single `JBCefJSQuery` to receive UI→host JSON strings
-  - Injects two shims into the page:
-    - `__ideBridgeSend(string)` – UI calls this; routes into the JSQuery
-    - `__ideBridgeDeliver(any)` – Host uses this to deliver messages to UI
-  - Provides host API:
-    - `IdeBridge.install(browser, project)` – set up bridge and queues
-    - `IdeBridge.send(type, payload)` – host→UI message
-    - Handles inbound requests (sample): `openFile { path, line }` -> opens in IDE, replies `{ ok: true }`
-  - Queues host→UI messages until WebView is ready; flushes automatically
-
-Converted usages:
-
-- All `executeJavaScript(window.postMessage(...))` replaced by `IdeBridge.send(type, payload)`
-- Components updated:
-  - `PathInserter` → `IdeBridge.send("insertPaths"|"pastePath", ...)`
-  - `IdeOpenFilesUpdater` → `IdeBridge.send("updateOpenedFiles", ...)`
-  - `FontSizeSynchronizer` (kept for compatibility) → `IdeBridge.send("setFontSize", { size })`
-- Removed legacy per-feature JSQuery bridges and direct observers. `OpenInIdeHandler` is superseded by `IdeBridge` request handler.
-
----
-
-## Web UI (packages/opencode/webgui)
-
-- File: `src/lib/ideBridge.ts`
-  - Exposes:
-    - `ideBridge.init()` – binds dispatching to `window.postMessage` and host shims
-    - `ideBridge.isInstalled()` – returns `true` when running inside an IDE host (JCEF/VSCode iframe); use to avoid `window.open()` which hangs JCEF
-    - `ideBridge.send(msg)` – UI→host fire-and-forget
-    - `ideBridge.request(type, payload)` – returns Promise; resolves on `{ replyTo }`
-    - `ideBridge.on(handler)` – subscribe to host→UI messages
-    - Outbound queue until `__ideBridgeSend` exists, then `flush()`
-- Initialize in `src/main.tsx` before React mount
-- App code subscribes once and routes messages by `type`
-
-Minimal UI usage:
-
-```ts
-import { ideBridge } from "./lib/ideBridge"
-
-ideBridge.init()
-ideBridge.on((msg) => {
-  switch (msg.type) {
-    case "updateOpenedFiles":
-      /* update state */ break
-    case "insertPaths":
-      /* route into UI */ break
-  }
-})
-
-// Request/response example
-await ideBridge.request("openFile", { path: "/p/file.ts", line: 10 })
-
-// Opening URLs safely (avoids window.open() which hangs JCEF)
-if (ideBridge.isInstalled()) {
-  ideBridge.send({ type: "openUrl", payload: { url } })
-} else {
-  window.open(url, "_blank")
-}
-```
-
----
-
-## VSCode host (iframe-ready)
-
-- The UI expects two shims on the iframe window:
-  - `__ideBridgeSend(string)` – UI→extension delivery
-  - `__ideBridgeOnMessage(any)` – extension→UI delivery entry
-- The extension should forward UI requests to its internal logic and respond with `{ replyTo, ok, ... }`
-- No changes needed in `webgui` beyond `ideBridge.init()`
-
----
-
-## Adding new features
-
-1. Define a new `type` and payload contract
-2. UI side:
-   - Send: `ideBridge.send({ type, payload })` or `ideBridge.request(type, payload)`
-   - Receive: `ideBridge.on(msg => { if (msg.type === 'newType') ... })`
-3. Host side (JetBrains):
-   - In `IdeBridge.handleInbound`, add case for `type`
-   - Perform work, then `replyOk(id)` or `replyError(id, message)`
-   - For proactive host→UI updates, call `IdeBridge.send(type, payload)`
-
----
-
-## Migration notes
-
-- Old direct DOM/JS injection and multiple JSQuery bridges are replaced by one bridge
-- Remove any remaining references to `WebViewLoadHandler`, `WebViewScripts`, and `OpenInIdeHandler` in new code paths
-- Prefer routing all state/UI sync through message types so VSCode and JetBrains remain aligned
-
----
-
-## Troubleshooting
-
-- If UI isn’t receiving messages:
-  - Ensure `ideBridge.init()` runs before app logic
-  - Confirm host injected shims (`__ideBridgeSend`, `__ideBridgeDeliver`) – JetBrains is handled by `IdeBridge.install`
-- If requests never resolve:
-  - Verify host replies include `replyTo` equal to request `id`
-
----
-
-## Message types (from implementation)
-
-### JetBrains host (JCEF) → Web UI
-
-- **insertPaths** — payload: `{ paths: string[] }`
-- **pastePath** — payload: `{ path: string }`
-- **updateOpenedFiles** — payload: `{ openedFiles: string[], currentFile: string | null }`
-- **setTooltipPolyfill** — payload: `{ enabled: boolean }` — JetBrains-only: enables CSS tooltip polyfill for `title` tooltips
-
-### Web UI → JetBrains host (handled)
-
-- **openFile** — payload: `{ path: string, line?: number }` → opens file in IDE, responds with `{ replyTo, ok }` or `{ replyTo, ok: false, error }`
-- **openUrl** — payload: `{ url: string }` → opens URL in default browser, responds with `{ replyTo, ok }`
-- **reloadPath** — payload: `{ path: string, operation: "write" | "edit" }` → reloads file from disk after AI agent modifies it, responds with `{ replyTo, ok }`
-
-### Protocol notes
-
-- **Responses**: `{ replyTo, ok, error? }`
-- **Transport shims**: UI uses `__ideBridgeSend` to send and `__ideBridgeOnMessage`/host `__ideBridgeDeliver` to receive

+ 161 - 0
hosts/IDE_BRIDGE_HTTP_SSE.md

@@ -0,0 +1,161 @@
+# ideBridge v2: Local HTTP + SSE Transport
+
+This document describes the current IDE <-> Web UI communication mechanism used by:
+
+- JetBrains host: `hosts/jetbrains-plugin`
+- VSCode host: `hosts/vscode-plugin`
+- Web UI: `packages/opencode/webgui`
+
+This replaces the older CEF/JSQuery/router injection approach and avoids VSCode-specific `postMessage` tunnels for ideBridge traffic.
+
+## Goals
+
+- One transport and one message contract across JetBrains + VSCode
+- No CEF `cefQuery_*` bindings, no JS injection ordering sensitivity
+- Works with VSCode Remote-SSH via `vscode.env.asExternalUri(...)`
+
+## Message contract
+
+All messages are JSON objects.
+
+Common fields:
+
+- `type: string`
+- `payload?: any`
+- `timestamp?: number`
+
+Request/response:
+
+- Requests include `id: string`
+- Responses include `replyTo: string` matching the request `id`
+- Responses include `ok: boolean` and optional `error: string`
+
+Example request:
+
+```json
+{ "id": "abc123", "type": "openFile", "payload": { "path": "/p/file.ts", "line": 42 }, "timestamp": 1731390000000 }
+```
+
+Example response:
+
+```json
+{ "replyTo": "abc123", "ok": true, "timestamp": 1731390000100 }
+```
+
+## Transport
+
+The IDE host runs a small HTTP server bound to `127.0.0.1` on an ephemeral port and creates a per-webview session.
+
+Session identifiers:
+
+- `sessionId`: random UUID used in the URL path
+- `token`: random UUID used as a query parameter for auth
+
+Base URL shape:
+
+```
+http://127.0.0.1:{port}/idebridge/{sessionId}
+```
+
+Endpoints:
+
+- `GET  {baseUrl}/events?token=...`
+  - SSE stream (`text/event-stream`)
+  - Server pushes messages as `event: message` with `data: <json>`
+  - Server sends periodic keepalive pings (`: ping`) to prevent proxy/tunnel timeouts
+
+- `POST {baseUrl}/send?token=...`
+  - Body: JSON message
+  - Response: `204` on success, `401` if token mismatch, `400` on malformed body
+
+Notes:
+
+- CORS is permissive (`Access-Control-Allow-Origin: *`) because the token is unguessable and scoped per session.
+- SSE headers include `Cache-Control: no-cache, no-transform` and `X-Accel-Buffering: no` to reduce buffering by proxies.
+
+## Web UI (packages/opencode/webgui)
+
+File: `packages/opencode/webgui/src/lib/ideBridge.ts`
+
+The UI reads two query params from `window.location.search`:
+
+- `ideBridge`: base URL (string)
+- `ideBridgeToken`: token
+
+Behavior:
+
+- `ideBridge.init()` opens an `EventSource` to `{ideBridge}/events?token=...`
+- UI -> host messages arrive via SSE and are dispatched to subscribers
+- UI -> IDE messages are sent via `fetch(POST {ideBridge}/send?token=...)`
+- Requests use `id/replyTo` to implement Promise-based RPC
+- Includes reconnect logic + bounded retries for transient send failures
+
+If params are missing, `ideBridge.isInstalled()` is `false` and requests are rejected.
+
+## JetBrains host (hosts/jetbrains-plugin)
+
+Server + routing:
+
+- `hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/ui/IdeBridge.kt`
+  - Starts `HttpServer` on `127.0.0.1:0`
+  - Creates sessions per project (`createSession(project)`), maps `Project -> sessionId`
+  - Implements SSE fan-out to all connected clients per session
+  - Implements inbound handlers for `openFile`, `openUrl`, `reloadPath`
+
+URL wiring:
+
+- `hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/ui/ChatToolWindowFactory.kt`
+  - Creates a session and appends `ideBridge` and `ideBridgeToken` to the UI URL
+
+Host -> UI updates:
+
+- Utilities call `IdeBridge.send(project, type, payload)` (session is looked up automatically)
+
+## VSCode host (hosts/vscode-plugin)
+
+Server + routing:
+
+- `hosts/vscode-plugin/src/ui/IdeBridgeServer.ts`
+  - Starts a Node HTTP server on `127.0.0.1:0`
+  - Creates sessions per webview (`createSession(handlers)`)
+  - SSE stream + keepalive pings
+  - Inbound handlers call into VSCode logic (`openFile`, `openUrl`, `reloadPath`)
+
+Remote-SSH support:
+
+- `hosts/vscode-plugin/src/ui/WebviewController.ts`
+  - Uses `vscode.env.asExternalUri(...)` for both:
+    - the backend UI base URL
+    - the ideBridge server base URL
+  - Passes the externalized URLs into the iframe as query params
+
+Host -> UI updates:
+
+- `CommunicationBridge` can route ideBridge-type host messages via the bridge server once `setBridgeSession(...)` is called.
+
+## Implemented message types
+
+Web UI -> host (handled by both hosts):
+
+- `openFile` payload: `{ path: string, line?: number }`
+- `openUrl` payload: `{ url: string }`
+- `reloadPath` payload: `{ path: string, operation?: "write" | "edit" | "apply_patch" }`
+
+Host -> Web UI (sent by hosts):
+
+- `insertPaths` payload: `{ paths: string[] }`
+- `pastePath` payload: `{ path: string }`
+- `updateOpenedFiles` payload: `{ openedFiles: string[], currentFile?: string | null }`
+
+## Troubleshooting
+
+- UI never connects:
+  - Confirm the loaded URL includes `ideBridge=` and `ideBridgeToken=`
+  - Confirm the host server is started and reachable (ephemeral port on localhost)
+
+- Requests never resolve:
+  - Host must reply with `{ replyTo: <id>, ok: true|false }` over SSE
+
+- Remote-SSH drops the connection:
+  - Keepalives should prevent idle timeouts; verify they are flowing (`: ping` frames)
+  - Ensure `asExternalUri` is applied to the bridge URL in VSCode