diff --git a/.github/skills/vscode-messenger/SKILL.md b/.github/skills/vscode-messenger/SKILL.md index 8b8e9a1..a94bcad 100644 --- a/.github/skills/vscode-messenger/SKILL.md +++ b/.github/skills/vscode-messenger/SKILL.md @@ -81,7 +81,7 @@ class ColorsViewProvider implements vscode.WebviewViewProvider { - `new Messenger(options?)` — `MessengerOptions`: `ignoreHiddenViews` (default `true`), `uniqueHandlers` (throws if a handler for the same method is registered twice — incompatible with sender-scoped handlers; see that section below), `debugLog`. - `registerWebviewView(view, options?)` / `registerWebviewPanel(panel, options?)` — returns a `WebviewIdMessageParticipant` with the assigned `webviewId`. Use this returned participant to address that **specific instance** (vs. the `webviewType` string which addresses **all instances** of that type). The library auto-unregisters on `onDidDispose`. -- `onRequest(type, handler, { sender? })` / `onNotification(type, handler, { sender? })` — return a `Disposable`. **Extension side:** multiple handlers for the same method stack and all fire (for notifications; for requests, multiple matching handlers is an error — see below). **Webview side:** registering a new handler for the same method **replaces** the previous one (last-write-wins). The optional `sender` (extension side only) filters the handler so it only fires for messages from that participant. +- `onRequest(type, handler, { sender? })` / `onNotification(type, handler, { sender? })` — return a `Disposable`. **Extension side:** for **notifications**, multiple handlers for the same method stack and all fire. For **requests**, registering more than one handler for the same method is invalid — it causes a runtime error response ("Multiple matching request handlers") when that request arrives; use `sender`-scoped handlers instead if you need per-webview logic for the same method (see below). **Webview side:** registering a new handler for the same method **replaces** the previous one (last-write-wins). The optional `sender` (extension side only) filters the handler so it only fires for messages from that participant. - `sendRequest(type, receiver, params?, cancelable?)` — returns `Promise`. Receiver is a `MessageParticipant` (webview by id, webview by type, or — once supported — another extension). Cannot be `BROADCAST` — throws immediately on both sides. - `sendNotification(type, receiver, params?)` — fire-and-forget. Receiver may be `BROADCAST`. @@ -190,20 +190,27 @@ const colorsView = messenger.registerWebviewView(view); // { type: 'webview', we messenger.sendNotification(ColorModify, colorsView, 'clear'); ``` -If you address by `webviewType` instead and multiple instances of that type are registered, `sendRequest` sends the request to **all** registered instances and uses `Promise.race` — the first response wins, others are discarded. Hidden instances (when `ignoreHiddenViews` is `true`, the default) produce an immediate rejection that participates in the race; if any visible instance responds successfully, its result is returned. If all instances are hidden, the request is rejected. `sendNotification` is sent to all instances of that type (hidden ones are silently skipped when `ignoreHiddenViews` is `true`). +If you address by `webviewType` instead and multiple instances of that type are registered, `sendRequest` behaves as follows: + +1. The request is sent to **all** registered instances of that type. +2. `Promise.race` is used — the first response wins, others are discarded. +3. With `ignoreHiddenViews: true` (the default): hidden instances immediately reject, and that rejection participates in the race; if at least one visible instance responds successfully, its result is returned; if all instances are hidden, the request rejects. +4. To avoid this behavior entirely, address by `webviewId` instead of `webviewType`. + +`sendNotification` is sent to all instances of that type (hidden ones are silently skipped when `ignoreHiddenViews` is `true`). ## Common gotchas ### Setup prerequisites (must be correct before any message works) -- **Webview drops messages until `start()`** — the webview-side `Messenger` only attaches its `window.addEventListener('message', ...)` inside `start()`. Forgetting to call it makes every incoming message disappear silently. Call `start()` once, synchronously after all `onRequest`/`onNotification` registrations in the same top-level script execution (before any async work), so no incoming messages are missed. +- **Webview drops messages until `start()`** — the webview-side `Messenger` only attaches its `window.addEventListener('message', ...)` inside `start()`. Forgetting to call it makes every incoming message disappear silently. Call `start()` once, after all `onRequest`/`onNotification` calls in your entry-point script, before any `await` expression or async callback. Do not defer `start()` to a later microtask or event handler. - **Hidden views are skipped by default** — `ignoreHiddenViews: true` causes `sendNotification` / `sendRequest` to a non-visible webview to be skipped (notification) or rejected (request). Either ensure the view is visible, set `ignoreHiddenViews: false` in `MessengerOptions`, or enable `retainContextWhenHidden` on the webview itself when constructing it. - **Broadcast requires opt-in per view** — a view receives a broadcast notification only if its `ViewOptions.broadcastMethods` contains the method string. Without that, the broadcast looks like it works but the view never sees it. - **Extension may send before webview is ready** — if the extension sends a request immediately after `registerWebviewView`, the webview may not yet have called `messenger.start()`. Guard against this by having the webview send an initialization notification to the extension once `start()` is called, and only then begin sending from the extension side. ### Runtime and targeting pitfalls -- **Webview handlers are last-write-wins** — on the webview side, `onRequest`/`onNotification` for the same method **replace** the previous handler. On the extension side, notification handlers stack and all fire; request handlers also stack but having multiple matching handlers for the same request method results in an error response ("Multiple matching request handlers"). Use `uniqueHandlers: true` to catch accidental duplicate registrations at registration time (throws immediately). If you re-register on the webview during HMR or re-mount, dispose the old `Disposable` first. +- **Webview handlers are last-write-wins** — on the webview side, `onRequest`/`onNotification` for the same method **replace** the previous handler. On the extension side, notification handlers stack and all fire, but registering more than one **request** handler for the same method is invalid usage: it results in a runtime error response ("Multiple matching request handlers") rather than being silently accepted. Use `uniqueHandlers: true` to catch accidental duplicate registrations at registration time (throws immediately). If you re-register on the webview during HMR or re-mount, dispose the old `Disposable` first. - **`webviewType` with multiple instances races requests** — if you really want to broadcast a question and aggregate, you have to do it yourself (iterate instances by id and `Promise.all`). The library only returns the first response. - **`extensionId` is reserved for future use** — `ExtensionMessageParticipant.extensionId` is in the type but cross-extension messaging isn't implemented. `sendRequest` to `{ type: 'extension', extensionId }` throws. Use `HOST_EXTENSION` (no `extensionId`) for the host extension. - **`webviewId` changes on every register** — the id is generated fresh each time `registerWebviewView` / `registerWebviewPanel` is called. Don't persist it across sessions; capture the returned participant and use it for the lifetime of that view. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..55af622 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,52 @@ +# vscode-messenger + +Typed RPC library for message-passing between a VS Code extension host and its webviews. TypeScript monorepo managed with npm workspaces (npm ^10.8.0, Node 18+ — CI pins Node 18). + +## Commands + +```sh +npm install # ~2s warm +npm run build # tsc -b (typecheck+compile all packages) + browserify bundles + devtools-ui vite build + lint, ~8.5s +npm test # jest: vscode-messenger + vscode-messenger-webview, 44 tests, ~2.4s +npx jest packages/vscode-messenger/tests/messenger.test.ts # single test file +npm run lint # eslint --workspaces, ~3.7s +npm run watch # tsc -b -w, incremental rebuild across all packages +``` + +There is no separate typecheck script: `tsc` doubles as build+typecheck per package, so `npm run build` (or the "Workspace TypeScript watch" task) is the typecheck. + +## Why and where + +- `packages/vscode-messenger-common/` — shared types: `NotificationType`, `RequestType`, `HOST_EXTENSION`, `BROADCAST`, cancellation. Every other package depends on it. +- `packages/vscode-messenger/` — extension-host side: `Messenger` class, webview registration, diagnostic API (consumed by devtools). +- `packages/vscode-messenger-webview/` — webview-side `Messenger` + `createCancellationToken`. +- `packages/vscode-messenger-devtools/` — VS Code extension that visualizes messenger traffic via `Messenger.diagnosticApi()`. `src/` is the extension host code; `webview-ui/` is a separate React + Vite + `baukasten-ui` app with its own `package.json`, lint and build. +- `examples/calico-colors/` — sample extension exercising the library end-to-end; not published. + +See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full module index. + +## Conventions + +- Keep all `NotificationType`/`RequestType` declarations in one shared module imported by both the extension and webview side — the same `method` string and generic types must match on both ends, and nothing enforces this beyond a shared import. +- ESLint already enforces most style (single quotes, semicolons, `no-explicit-any`, `no-var`, arrow/paren spacing — see `eslint.config.js`); don't restate those as review comments. +- Package build order follows the project references in `tsconfig.build.json`: `vscode-messenger-common` → `vscode-messenger-webview` / `vscode-messenger` → `vscode-messenger-devtools` → its `webview-ui` → `examples/*`. Don't add an import that runs against this direction (e.g. `vscode-messenger-common` importing from `vscode-messenger`). + +## Boundaries and definition of done + +- Never hand-edit `packages/*/lib/` or `packages/*/webview-ui/build/` — these are build outputs regenerated by `npm run build`. +- Never hand-edit `examples/*/media/web-view-bundle-*.js` — generated by the `browserify` step of `npm run build`. +- The three publishable packages (`vscode-messenger`, `vscode-messenger-webview`, `vscode-messenger-common`) must keep matching version ranges on each other — see [releasing.md](releasing.md) before touching any `package.json` version or dependency range. +- Done means `npm run build`, `npm test`, and `npm run lint` all pass locally; include the final line(s) of each command's output in your response to confirm. +- A change to a package's public API includes an updated usage example in [README.md](README.md) in the same change — the README is this library's primary consumer-facing contract. +- For non-publishable packages (devtools, examples), update the relevant section of [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) instead of README.md when their public API changes. +- If reality contradicts this file or `docs/ARCHITECTURE.md`, fix the doc as part of the change — never silently work around it. + +## PR conventions + +- No enforced commit format (history mixes free-form and `type: ...` messages); PRs merge with the PR number in the message (`... (#60)`). + +## Pointers + +- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — package layering and module index. +- [releasing.md](releasing.md) — npm, Open VSX, and VS Code Marketplace release steps. +- [.github/skills/vscode-messenger/SKILL.md](.github/skills/vscode-messenger/SKILL.md) — consumer-facing skill for downstream users of this library (message-type patterns, cancellation, devtools wiring). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md index 8be651e..9f7ddfb 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ RPC messaging library for the VS Code extension platform. Makes the communicatio [![License](https://img.shields.io/github/license/TypeFox/vscode-messenger?color=green)](https://github.com/TypeFox/vscode-messenger/blob/main/LICENSE) [![Codespaces](https://img.shields.io/badge/Codespaces-Open-blue?logo=github)](https://codespaces.new/TypeFox/vscode-messenger) [![Copilot Skill](https://img.shields.io/badge/Copilot-Skill-blue?logo=github)](https://github.com/TypeFox/vscode-messenger/blob/main/.github/skills/vscode-messenger/SKILL.md) +[![AGENTS.md](https://img.shields.io/badge/AGENTS.md-guide-blue?logo=github)](https://github.com/TypeFox/vscode-messenger/blob/main/AGENTS.md) ## Features diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..9a6c956 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,53 @@ +# Architecture + +`vscode-messenger` is a typed RPC layer over `postMessage` for the three-way channel between a VS Code extension host, its webviews, and (for the devtools package) other extensions inspecting that traffic. It ships as three small packages plus one VS Code extension that consumes them; there is no server, database, or network layer. + +## Package layering + +Build/type order follows the project references in [tsconfig.build.json](../tsconfig.build.json). A package may only depend on ones to its left: + +``` +vscode-messenger-common + │ + ├── vscode-messenger (extension host) + └── vscode-messenger-webview (webview script) + │ + └── vscode-messenger-devtools (VS Code extension) + └── webview-ui (React app, separate build) + +examples/calico-colors (consumes vscode-messenger + vscode-messenger-webview + vscode-messenger-common) +``` + +### `packages/vscode-messenger-common/` + +Runtime-agnostic types shared by both ends of the channel. + +- `messages.ts` — `NotificationType

` / `RequestType` type tags, `MessageParticipant` variants, `HOST_EXTENSION`, `BROADCAST`, the wire message shapes. +- `cancellation.ts` — cancellation message types shared between the extension's `vscode.CancellationToken` and the webview's `AbortSignal` bridge. + +### `packages/vscode-messenger/` + +Extension-host runtime. + +- `messenger.ts` — the `Messenger` class: `registerWebviewView` / `registerWebviewPanel`, `sendNotification` / `sendRequest`, `onNotification` / `onRequest`, routing by participant (single view, view-type group, broadcast), view disposal cleanup. +- `diagnostic-api.ts` — `MessengerDiagnostic` / `isMessengerDiagnostic`: an opt-in introspection surface (`extensionInfo()`, `addEventListener`) that lets another extension (namely devtools) observe a host's message traffic without coupling to its internals. + +### `packages/vscode-messenger-webview/` + +Webview-side mirror of the `Messenger` API (`messenger.ts`), plus `vscode-api.ts` wrapping `acquireVsCodeApi()` and `createCancellationToken` for bridging an `AbortSignal` to the extension side. `messenger.start()` must be called before any message is received — see [AGENTS.md](../AGENTS.md). + +### `packages/vscode-messenger-devtools/` + +A VS Code extension, not a library. `devtool-ext.ts` activates a webview panel (`panels/MessagesPanel.ts`) and, for every other installed extension, checks `isMessengerDiagnostic(ext.exports)` to attach a listener and stream its message traffic into the panel via `PushDataNotification`. `webview-ui/` is an independently built React + Vite app (uses `baukasten-ui`, `@tanstack/react-table`, `zustand`) rendering that traffic: +- `components/` — `data-table.tsx`, `messenger-chart.tsx`, `visualization.tsx`, `extension-info.tsx`, `view-header.tsx`. +- `model/` — `messenger-types.ts`, the shared request/notification contract with the extension host side (mirrors `packages/vscode-messenger-devtools/src/messenger-types.ts`). + +### `examples/calico-colors/` + +Reference extension (two webview types: `calico-colors-view.ts`, `cat-coding-view.ts`) showing end-to-end usage of the library. Not published; exists to keep the README examples and the SKILL.md honest against a real build. + +## Invariants + +- `vscode-messenger-common` has no dependency on `vscode` or DOM APIs — it must stay usable from both the extension host and the webview sandbox. +- The devtools introspection contract (`MessengerDiagnostic`) is the only sanctioned way to observe another extension's messenger traffic; it must not require the observed extension to import `vscode-messenger-devtools`. +- `webview-ui` is a `noEmit` TypeScript project (typecheck-only via the root `tsc -b` graph); its actual bundle is produced separately by its own `tsc && vite build` script, invoked from the root `npm run build` — see [AGENTS.md](../AGENTS.md#commands).