Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions .github/skills/vscode-messenger/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<R>`. 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`.

Expand Down Expand Up @@ -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.
Expand Down
52 changes: 52 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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).
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
53 changes: 53 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -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<P>` / `RequestType<P, R>` 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).
Loading