Skip to content

refactor: shared webview IPC helpers with enforced exhaustiveness#911

Merged
EhabY merged 6 commits intomainfrom
feat/webview-ipc-refactor
Apr 30, 2026
Merged

refactor: shared webview IPC helpers with enforced exhaustiveness#911
EhabY merged 6 commits intomainfrom
feat/webview-ipc-refactor

Conversation

@EhabY
Copy link
Copy Markdown
Collaborator

@EhabY EhabY commented Apr 20, 2026

  • Shared extension-side dispatch helpers (isIpcCommand, isIpcRequest, notifyWebview, dispatchCommand, dispatchRequest, onWhileVisible) live in src/webviews/dispatch.ts; HTML scaffolding (getWebviewHtml, getWebviewAssetUris, buildWebviewCsp) in src/webviews/html.ts. All three panels dispatch through them.
  • Shared vanilla-webview helpers sendCommand and subscribeNotifications in @repo/webview-shared. subscribeNotifications is exhaustive over the API's notifications: a new defineNotification without a handler is a build error. Speedtest and chat use it; React webviews keep useIpc.
  • Every panel calls both buildCommandHandlers and buildRequestHandlers (empty {} is fine). New defineCommand / defineRequest entries fail the build at any panel missing a handler, so the extension can't silently drop a future action.
  • Chat panel migrated off its ad-hoc {type, ...} protocol onto ChatApi. The iframe shim was extracted into a typed bundle at packages/chat/. A single source-gated window.message listener bridges the iframe's {type, payload} contract and ChatApi; iframe messages can never reach typed notification handlers.
  • packages/webview-shared/README.md rewritten as a navigation map with a "Where each handler lives" table and pointers to the canonical reference packages. CONTRIBUTING.md and AGENTS.md point at it as the single source of truth.

@EhabY EhabY force-pushed the feat/speedtest-visualization-888 branch from 781bd7a to 8c6204a Compare April 20, 2026 15:20
@EhabY EhabY force-pushed the feat/webview-ipc-refactor branch from df4afc3 to abfbedc Compare April 20, 2026 18:06
@EhabY EhabY force-pushed the feat/speedtest-visualization-888 branch 2 times, most recently from 4e7be71 to 973e57a Compare April 21, 2026 13:13
@EhabY EhabY force-pushed the feat/webview-ipc-refactor branch from abfbedc to f890b00 Compare April 21, 2026 13:28
@EhabY EhabY force-pushed the feat/speedtest-visualization-888 branch from 973e57a to 83a1ca1 Compare April 21, 2026 13:50
@EhabY EhabY force-pushed the feat/webview-ipc-refactor branch from f890b00 to 30594e4 Compare April 21, 2026 13:50
@EhabY EhabY force-pushed the feat/speedtest-visualization-888 branch from 83a1ca1 to 1ce0401 Compare April 21, 2026 14:22
@EhabY EhabY force-pushed the feat/webview-ipc-refactor branch from 30594e4 to 6647e51 Compare April 21, 2026 14:22
@EhabY EhabY self-assigned this Apr 27, 2026
@EhabY EhabY force-pushed the feat/speedtest-visualization-888 branch 2 times, most recently from ae111a5 to 7b451c4 Compare April 28, 2026 12:09
@EhabY EhabY force-pushed the feat/webview-ipc-refactor branch from 6647e51 to 9c3ac9d Compare April 28, 2026 12:10
@EhabY EhabY force-pushed the feat/speedtest-visualization-888 branch from 7b451c4 to 26fc3d2 Compare April 28, 2026 12:11
@EhabY EhabY force-pushed the feat/webview-ipc-refactor branch from 9c3ac9d to f260351 Compare April 28, 2026 12:11
@EhabY EhabY force-pushed the feat/speedtest-visualization-888 branch from 26fc3d2 to 04e4c55 Compare April 28, 2026 12:19
@EhabY EhabY force-pushed the feat/webview-ipc-refactor branch from f260351 to 76a78b6 Compare April 28, 2026 12:19
@EhabY EhabY force-pushed the feat/speedtest-visualization-888 branch from 04e4c55 to 13ee0b6 Compare April 28, 2026 12:47
@EhabY EhabY force-pushed the feat/webview-ipc-refactor branch from 76a78b6 to 30e9c13 Compare April 28, 2026 12:48
@EhabY EhabY force-pushed the feat/speedtest-visualization-888 branch 2 times, most recently from 9fbd977 to bfb8ce3 Compare April 29, 2026 15:45
@EhabY EhabY force-pushed the feat/webview-ipc-refactor branch from 30e9c13 to 47ed5e2 Compare April 29, 2026 16:03
@EhabY
Copy link
Copy Markdown
Collaborator Author

EhabY commented Apr 30, 2026

/coder-agents-review

Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid infrastructure PR. The buildCommandHandlers / buildRequestHandlers pattern is a strong approach to eliminating silently-dropped messages at compile time. The README is well-written reference documentation with the error-semantics table, concrete code examples, and the "no dropped events" explanation. The chat shim decomposition into named functions (toIframe / handleFromIframe / handleFromExtension / showRetry) is a clear improvement over the monolithic handler. The migration is systematic: all three panels moved to the shared helpers, tests updated to match the new wire format.

1 P0, 1 P2, 11 P3, 1 P4, 4 Nit.

The P0 is a regression: the speedtest ready command calls window.postMessage (DOM global) instead of the VS Code IPC because the postMessage import was removed but one call site was not migrated. The chart is blank on initial open. Fix: sendCommand(SpeedtestApi.ready).

The P2 is architectural: the chat shim's switch statements are raw JS inside a template literal, outside the exhaustiveness guarantee. Adding a ChatApi entry compiles cleanly, but the shim silently drops the new message. Seven reviewers flagged this independently.

Most P3s are documentation accuracy (dead file path, missing scaffolding steps, WebviewPanel vs WebviewView), one behavioral default change (speedtest error toasts), one code duplication (useIpc.command vs sendCommand), and test coverage gaps.

PS. Mafu-san's process audit found zero unverified claims in the PR description, all six structural claims hold against the diff. Pariston tried four framings and could not construct a simpler alternative. Takumi traced every concurrency path and found no issues. Ging-TS had no TypeScript modernization findings.

"This also violates the PR's own AGENTS.md rule: 'Never hand-roll ... postMessage({ method, params }). Use onNotification / sendCommand." (Melody)


packages/speedtest/src/index.ts:32

P0 [DEREM-4] The postMessage import was removed (line 2), but this call site was not migrated. postMessage now resolves to the DOM global window.postMessage(message, options?), which TypeScript compiles without error (the DOM lib provides the overload). At runtime, window.postMessage({method:"speedtest/ready"}) dispatches a MessageEvent on the webview's own window, not to the extension host. The extension's ready handler (the only path that calls sendData() on initial open) never fires. The chart is blank until the user switches tabs and back.

"Confirmed in the built artifact: sendCommand is inlined as o(...) / s(...), but line 32 emits bare postMessage(...), a global call." (Mafuuu)

Fix: sendCommand(SpeedtestApi.ready);

🤖

packages/webview-shared/src/react/useIpc.ts:119

P3 [DEREM-11] useIpc.command is a line-for-line copy of sendCommand from ipc.ts. Both take a CommandDef<P>, spread variadic args, and call postMessage({ method: def.method, params: args[0] }). The onNotification path was already migrated to delegate to the shared helper (line 146: return subscribe(definition, callback)). The symmetric command path was not. One-line fix: import { sendCommand } from "../ipc" and delegate. (Robin)

🤖

test/unit/webviews/speedtest/speedtestPanelFactory.test.ts:128

P4 [DEREM-16] "ignores unknown message methods" no longer matches actual behavior. The old code silently dropped unknown methods. The new code routes through dispatchCommand, which throws, logs a warning, and pops showErrorMessage (default showErrorToUser: () => true). The test asserts .not.toThrow() synchronously on an async dispatch, so the error toast is invisible. The test name is misleading and the assertion is vacuous for this path. (Bisky)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread src/webviews/chat/chatPanelProvider.ts Outdated
Comment thread src/webviews/chat/chatPanelProvider.ts Outdated
Comment thread test/webview/shared/ipc.test.ts Outdated
Comment thread src/webviews/speedtest/speedtestPanelFactory.ts
Comment thread packages/webview-shared/README.md Outdated
Comment thread src/webviews/util.ts Outdated
Comment thread src/webviews/util.ts Outdated
Comment thread src/webviews/util.ts Outdated
Comment thread packages/webview-shared/src/ipc.ts Outdated
Comment thread packages/webview-shared/src/ipc.ts Outdated
Base automatically changed from feat/speedtest-visualization-888 to main April 30, 2026 12:09
EhabY added 2 commits April 30, 2026 15:10
…ness

- Add isIpcCommand/Request, notifyWebview, dispatchCommand/Request, and
  onWhileVisible in src/webviews/util.ts; migrate tasksPanelProvider and
  speedtestPanel off their local copies.
- Add sendCommand/onNotification in @repo/webview-shared for vanilla
  webviews; useIpc stays for React.
- speedtestPanel calls buildCommandHandlers AND buildRequestHandlers,
  so adding a new action to SpeedtestApi is a compile error until a
  handler is wired.
- Document the contract and the visibility/theme re-send guarantee in
  packages/webview-shared/README.md; AGENTS.md and CONTRIBUTING.md
  point at it.
- Expand speedtestPanel tests (payload, visibility/theme re-send,
  viewJson dispatch, cleanup) and add ipc.test.ts for the new helpers.
Define ChatApi in shared and replace the ad-hoc {type, ...} message
handling with buildCommandHandlers / buildRequestHandlers plus
dispatchCommand / dispatchRequest. Outgoing notifications route
through a private notify() wrapper, mirroring tasksPanelProvider.

The iframe shim in the inline HTML keeps its own {type, payload}
contract with the Coder server, but its shim-to-extension side now
speaks the IPC wire format. Split the handler into handleFromIframe /
handleFromExtension + a toIframe helper + a showRetry builder so the
dispatch reads straight through.

Tests updated to the new wire format.
@EhabY EhabY force-pushed the feat/webview-ipc-refactor branch from 47ed5e2 to 23c96b9 Compare April 30, 2026 12:10
Split src/webviews/util.ts into dispatch.ts (IPC runtime) and html.ts
(scaffolding). Default dispatchCommand to not show errors, matching
dispatchRequest, and let panels opt in per method. Tasks panel adds
viewInCoder and viewLogs to its user-action set so dialogs still pop for
clicked items. Chat shim now uses ?? for data fallback, validates URL
before forwarding navigate, and references ChatApi sync requirements in a
short comment block. Drop the duplicate notify wrapper in chat and tasks
since notifyWebview accepts undefined webview. Rename "Unknown method" to
"Unknown request" for symmetry. sendCommand omits params for void
commands. Tests cover the dispatch error branches, void notifications,
and the auth-error retry path. README tightened and clarified that the
visibility resend only matters when retainContextWhenHidden is unset.
@EhabY EhabY force-pushed the feat/webview-ipc-refactor branch from 55ad7ce to f11c7f9 Compare April 30, 2026 12:48
@EhabY EhabY marked this pull request as ready for review April 30, 2026 12:58
Replaces the inline JS shim in chatPanelProvider with a real webview
package at packages/chat/. The shim now imports ChatApi and uses
sendCommand and subscribeNotifications, so renaming an entry on the
API breaks the build at every callsite. Iframe and status div are
still pre-rendered in the panel HTML so the iframe loads as soon as
the page parses, matching the original code path; the bundle just
attaches listeners.

Adds subscribeNotifications to webview-shared, an exhaustive
notification subscriber driven by a single window message listener
and a Map dispatcher. Removes the public onNotification export;
useIpc still uses the lower-level subscribeOne for per-call React
subscriptions. Speedtest migrates to subscribeNotifications too.

Pulls a buildWebviewCsp helper out of html.ts so the chat panel and
the standard getWebviewHtml share one CSP source of truth, and adds
getWebviewAssetUris for panels that compose their own HTML.

README rewritten as a navigation map with a "Where each handler
lives" table and pointers to the canonical reference packages.
@EhabY
Copy link
Copy Markdown
Collaborator Author

EhabY commented Apr 30, 2026

/coder-agents-review

Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 of 20 R1 findings addressed. The author fixed the P0, the P2, all P3s with inline threads, and all Nits. Thorough response.

Two findings remain unaddressed. Both were auto-folded into the review body (their lines fell outside the diff context), so the author had no threaded comment to reply to. Listing them here so they have a reply target.

DEREM-11 (P3) packages/webview-shared/src/react/useIpc.ts:119: useIpc.command is a line-for-line copy of sendCommand from ipc.ts. The onNotification path was already migrated to delegate to the shared helper (line 146: return subscribe(definition, callback)). The symmetric command path was not. One-line fix: import { sendCommand } from "../ipc" and delegate.

DEREM-16 (P4) test/unit/webviews/speedtest/speedtestPanelFactory.test.ts:128: "ignores unknown message methods" test name is misleading. After the dispatch migration, unknown commands throw internally, get caught, and log a warning (no longer silently ignored). With the default flip from DEREM-8 (55ad7ce), the user-facing behavior is closer to the old silent behavior (no error toast), but the internal path changed. The test assertion (.not.toThrow()) is vacuous because it checks synchronously against an async dispatch.

Further review is blocked until these are responded to (fix, acknowledge, or contest). The panel has not yet reviewed the new commits.

🤖 This review was automatically generated with Coder Agents.

DEREM-11: useIpc.command no longer hand-rolls postMessage. Delegates
to sendCommand, which also brings in the void-command params-omission
behavior so React webviews are consistent with vanilla.

DEREM-16: speedtest panel test "ignores unknown message methods" was
asserting against a sync throw on an async dispatch, which always
passed regardless of behavior. Renamed to reflect the user-facing
contract (no error dialog) and asserts on showErrorMessage after
the dispatch settles.
@EhabY
Copy link
Copy Markdown
Collaborator Author

EhabY commented Apr 30, 2026

Pushed 699e733 addressing the two remaining R1 findings:

  • DEREM-11: useIpc.command now delegates to sendCommand. As a side benefit, the React side picks up the void-command params-omission fix from DEREM-20.
  • DEREM-16: renamed ignores unknown message methods to does not surface an error dialog for unknown command methods and replaced the vacuous .not.toThrow() (sync against an async dispatch) with an awaited assertion that vscode.window.showErrorMessage was not called.

@EhabY
Copy link
Copy Markdown
Collaborator Author

EhabY commented Apr 30, 2026

/coder-agents-review

Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 20 R1 findings verified fixed. The author's response was thorough: the P0 speedtest bug, the P2 shim exhaustiveness gap, all P3s, and all Nits were addressed across three commits. Melody walked every API pairing end-to-end and confirmed all chains agree. Mafuuu verified the four highest-severity fixes against the code and ran adversarial scenarios. The dispatch.test.ts and auth-retry tests are genuine and well-structured.

The fix commits introduced significant refactoring beyond the R1 fixes: util.ts split into dispatch.ts + html.ts, chat shim extracted into packages/chat/, subscribeNotifications added for compile-checked notification handling, dispatchCommand default flipped. The panel reviewed these changes.

1 P2, 3 P3, 2 Nit (1 from Netero, 1 new).

The P2 is a fix-chain regression from DEREM-5: subscribeNotifications in the chat shim is source-blind. The old inline script had a single message listener with event.source gating that ensured notification handlers only processed extension messages. The extraction into two independent listeners removed that invariant. If the Coder embed ever posts a message with a notification type string, the handler crashes with TypeError.

"If the Coder server embed ever posts a message with type: 'coder:set-theme', listener 1 fires, extracts (msg as { data }).data (which is undefined), and the handler ({ theme }) => toIframe(...) throws TypeError." (Hisoka)

🤖 This review was automatically generated with Coder Agents.

Comment thread packages/chat/src/index.ts Outdated
Comment thread src/webviews/dispatch.ts
Comment thread src/webviews/html.ts
Comment thread src/webviews/chat/chatPanelProvider.ts Outdated
Comment thread packages/chat/src/index.ts Outdated
Comment thread packages/webview-shared/src/react/useIpc.ts Outdated
DEREM-23: Chat shim now uses a single source-gated message listener.
Iframe messages route to the foreign-protocol switch; extension
messages dispatch through `buildNotificationRouter` (extracted from
`subscribeNotifications`) so the exhaustiveness check is preserved
and the dispatch loop is shared. An iframe message whose `type`
matches a notification can no longer reach the typed handler.

DEREM-24: Direct tests for `isIpcRequest` and `isIpcCommand` covering
numeric `requestId`/`method`, missing fields, command vs request
disambiguation, and null/non-object inputs.

DEREM-25: Tests for `buildWebviewCsp`. Base CSP omits `frame-src`;
the option injects it.

DEREM-26: `embedUrl` now flows through `escapeHtml` before the iframe
`src` interpolation, so a `chatId` containing `"` can't break out of
the attribute.

DEREM-21: New `test/webview/chat/index.test.ts` covers iframe
forwarding (vscodeReady, chatReady, navigate with and without url),
extension push (setTheme), source isolation against
notification-typed iframe messages, and the auth-error retry flow.

DEREM-22: Reworded the `useIpc` comment to describe the role without
naming the helper.
Copy link
Copy Markdown

@jakehwll jakehwll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🙂

@EhabY EhabY merged commit 20072d8 into main Apr 30, 2026
10 checks passed
@EhabY EhabY deleted the feat/webview-ipc-refactor branch April 30, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants