Skip to content

Proposal: Canvas — declared, instanced, agent-openable UI surfaces on a dedicated channel #297

Description

@colbylwilliams

Summary

AHP has no Canvas concept on the wire. There is no state slot, no action family, no method, and no capability flag for it — grep -ri canvas against main returns nothing in types/, docs/, schema/, or any language client.

Several JSON-RPC agent runtimes ship a Canvas surface: declarative, interactive UI surfaces (an editor, a live browser, a terminal, or arbitrary extension-authored HTML) that the agent can open, drive, and close through a small set of tools, and that a client renders in a side panel. A runtime fronted by an AHP client has nowhere to put those surfaces today — the agent's "open a canvas" call reaches the runtime and then dies, because AHP gives it no way to reach the renderer.

This proposal adds Canvas to AHP by following the two patterns AHP already uses for exactly this class of problem:

  1. The MCP Apps pattern — a client capability opt-in plus a small discovery surface on session state — for "here is an opaque, client-rendered UI surface and here is what the host will do for it."
  2. The stateful per-resource channel pattern used by terminals, changesets, and the resource-watch channel — one channel per open instance, carrying that instance's live state and its lifecycle traffic.

Crucially, the runtime→provider callbacks that back a canvas (open / invoke action / close) are request/response. AHP already models bidirectional request/response: the resource* family and createResourceWatch appear in both CommandMap and ServerCommandMap and may be initiated by either peer (see types/common/messages.ts). Canvas provider callbacks map onto that existing machinery directly, instead of being simulated with correlated state.

The result is a purely additive surface: one client capability, a small session-state registry, a per-instance ahp-canvas:/<id> channel, the provider request methods, and a small set of channel actions — plus an in-band content path (canvasReadResource + the canvas/message bridge, §10) so a canvas renders and stays interactive even when the client has no direct route to the host, as in a relayed / broker-fronted deployment. Clients that do not opt in see nothing.


Motivation

What "Canvas" means

The shape below is reverse-engineered from a real, shipping JSON-RPC agent runtime, so maintainers don't have to invent the semantics. The runtime splits Canvas across three roles:

  • Provider — a connection that declares zero or more canvases and installs request/response callbacks for canvas.open, canvas.close, and canvas.action.invoke. A provider may be the runtime host process itself (a built-in or in-process extension) or a separate connection (e.g. an extension child process). A session may have several providers.
  • Runtime — maintains the aggregated registry across all connected providers, routes the agent's canvas calls to the owning provider, and tells consumers which canvases are open.
  • Agent — the model. Discovers canvases, and opens / drives / closes them through runtime-owned tools. The agent is opaque to AHP; it never sees the wire format.

The runtime's own wire surface, for reference shape only:

Direction Call Returns
Agent → Runtime list / list-open / open / close / invoke-action discovery + open-instance results
Runtime → Provider canvas.open { url?, title?, status? } | error
Runtime → Provider canvas.action.invoke provider-defined value | error
Runtime → Provider canvas.close () | error
Runtime → Consumer open / update / close / registry-changed events live + durable-resume state

Three orthogonal nouns AHP has to model:

Noun Identity Cardinality Lifecycle
Declaration (extensionId, canvasId) Many per session; varies as providers come and go Lives as long as the declaring provider is connected
Action (canvasId, actionName) Many per declaration Same as declaration
Open instance instanceId (caller-minted) Many per declaration Persists until closed; on provider disconnect the runtime keeps the entry and marks it stale

Two provider locations matter to AHP, and the design hinges on separating them:

  • Server-side provider — a provider that lives inside the host process. The agent's open resolves host-internally; the provider returns a render URL; the client's only job is to render that URL. No AHP request leaves the host for the provider callback.
  • Client-side provider — the AHP renderer itself declares native canvases (e.g. editor, browser, terminal) that it both provides and renders. The agent's open must be routed to that client as a request; the client renders natively and returns render metadata.

The provider callbacks are request/response — the runtime blocks on the provider's return value (or an error envelope with code / message). The rendered content behind url is opaque to the protocol, exactly like an MCP App View. Open instances are durable: they survive session resume.

Where AHP is blocked today

  1. A client that is the natural canvas renderer has no wire to publish its own canvases on. A desktop client that can host an editor / browser / terminal surface can't declare those anywhere. They aren't tools (the agent doesn't invoke them by name with JSON-Schema input; it opens them by id and drives them over a separate action family).
  2. The agent's open cannot reach a client renderer/provider at all. There is no AHP channel or request by which "open this canvas" reaches the client, and no way for a client-declared provider to answer the open.
  3. Session state carries no canvas state. A subscriber can't see what's declared or open; a reconnecting client can't rebuild it.
  4. Canvas content has no in-band delivery path. A client is handed a url and expected to load it directly. When the host can't expose an endpoint the client can reach — a relayed or broker-fronted deployment — the content is unreachable, so the canvas renders blank even though every control message arrived.

Why a channel, and why request/response

The two things Canvas needs — (a) a durable, resumable, per-instance record of an open surface, and (b) real request/response for the provider callbacks — are both things AHP already does, just not for canvases:

  • (a) is exactly what TerminalState / ChangesetState / ResourceWatchState are: per-resource channel state, delivered as a snapshot on subscribe/reconnect and mutated by channel actions.
  • (b) is exactly what the symmetric resource* + createResourceWatch requests are: methods in ServerCommandMap that the server initiates against the client and awaits a typed result on.

Modeling Canvas any other way would mean re-deriving primitives AHP already ships.


Proposed change

1. Client capability

// types/common/commands.ts — ClientCapabilities
export interface ClientCapabilities {
  // …existing…

  /**
   * Client can render canvases and host client-declared canvas providers —
   * i.e. it can render an opaque canvas URL in an isolated surface, and it
   * can answer canvasOpen / canvasInvokeAction / canvasClose requests for
   * canvases it declares via SessionActiveClient.canvasProviders.
   *
   * Hosts SHOULD only populate SessionState.canvases / SessionState.openCanvases
   * and only route canvas requests to a client that declared this capability.
   * Clients that omit it see no canvas surface.
   */
  canvas?: Record<string, never>;
}

Mirrors mcpApps exactly: a presence flag, empty object = supported, absence = not supported.

2. Discovery surface (session state)

Two small fields; the heavy per-instance state lives on the channel (§4), not here.

// types/channels-session/state.ts — SessionState
export interface SessionState {
  // …existing…

  /**
   * Aggregated canvas registry currently exposed to the agent — the union
   * of every connected provider (server-side and client-declared).
   * Full-replacement via `session/canvasesChanged`.
   */
  canvases?: SessionCanvasDeclaration[];

  /**
   * Lightweight catalogue of currently-open canvas instances. Each entry
   * carries the instance's `ahp-canvas:/<id>` channel URI so a subscriber
   * can subscribe to the full CanvasState and render it. Full-replacement
   * via `session/openCanvasesChanged`.
   *
   * Analogous to how RootState.terminals catalogues live terminals whose
   * full state lives on each terminal channel.
   */
  openCanvases?: OpenCanvasRef[];
}

// SessionActiveClient — how a client advertises the canvases it provides.
export interface SessionActiveClient {
  // …existing…

  /**
   * Canvas declarations this client contributes. Updated atomically with
   * `session/activeClientChanged`, identical to the `tools` pattern — there
   * is no separate change action. The host folds these into
   * SessionState.canvases with source = { kind: 'client', clientId }.
   */
  canvasProviders?: ClientCanvasDeclaration[];
}
// Supporting types — types/channels-session/state.ts

/** One entry in the aggregated registry. */
export interface SessionCanvasDeclaration {
  /** Owning provider id. Stable across declarations and instances. */
  extensionId: string;
  extensionName?: string;
  /** Provider-local canvas id. Unique within extensionId. */
  canvasId: string;
  displayName: string;
  description: string;
  /** JSON Schema for open input. Opaque to AHP; mirrors ToolDefinition.inputSchema shape. */
  inputSchema?: Record<string, unknown>;
  actions?: SessionCanvasAction[];
  /** Where the declaration came from — for routing and cleanup. */
  source: CanvasProviderSource;
}

export interface SessionCanvasAction {
  /** Unique within (extensionId, canvasId). */
  name: string;
  description?: string;
  inputSchema?: Record<string, unknown>;
}

/** Discriminated union — no `clientId` on the server variant (invalid states unrepresentable). */
export type CanvasProviderSource =
  | { kind: 'server' }
  | { kind: 'client'; clientId: string };

/** Lighter shape a client publishes; the host derives extensionId + source. */
export interface ClientCanvasDeclaration {
  canvasId: string;
  displayName: string;
  description: string;
  inputSchema?: Record<string, unknown>;
  actions?: SessionCanvasAction[];
}

/** Lightweight open-instance catalogue entry. Full state is on the channel. */
export interface OpenCanvasRef {
  instanceId: string;
  /** The instance's channel URI (`ahp-canvas:/<id>`). Subscribe for CanvasState. */
  channel: URI;
  canvasId: string;
  extensionId: string;
  extensionName?: string;
  title?: string;
  availability: CanvasAvailability;
}

export const enum CanvasAvailability {
  Ready = 'ready',
  Stale = 'stale',
}

3. New URI scheme

Add to the URI-scheme table in subscriptions.md:

URI State type Description
ahp-canvas:/<id> CanvasState Per-open-canvas-instance state. Server-assigned id; clients MUST treat it as opaque. Advertised on SessionState.openCanvases.

4. Canvas channel (ahp-canvas:/<id>)

One channel per open instance (the same "one channel per resource" convention as terminal / changeset / resource-watch).

State — snapshot on subscribe/reconnect, mutated by channel actions:

// types/channels-canvas/state.ts
export interface CanvasState {
  instanceId: string;
  canvasId: string;
  extensionId: string;
  extensionName?: string;
  displayName?: string;
  /** Input the agent supplied when opening. Retained for resume/rebind. */
  input?: Record<string, unknown>;
  title?: string;
  status?: string;
  /** Renderer-targeted address for the opaque canvas content — a directly-loadable URL or a channel-served `ahp-canvas-content:` URI. See §9 (policy) and §10 (transport). */
  url?: string;
  availability: CanvasAvailability;
  /** Who owns the provider callbacks for this instance. */
  provider: CanvasProviderSource;
}

Actions on the channel (carry channel: "ahp-canvas:/<id>"):

Action Client-dispatchable Reducer effect
canvas/updated No Partial-merge title / status / url / availability. null clears an optional value; absent preserves it.
canvas/closeRequested Yes No-op. Pure client→host signal ("user hit ✕"); the host then runs the close flow (§5). Mirrors how terminal/input is a side-effect-only client action.
canvas/message Yes No-op. Bidirectional opaque { payload } bridge between the rendered View and the instance's provider — the relay-carried analogue of postMessage. See §10.

5. Provider request family (server → client)

The provider callbacks are real JSON-RPC requests added to ServerCommandMap. They are sent only to a client that (a) declared capabilities.canvas and (b) is the provider of the target canvas (i.e. declared it via canvasProviders). For server-side providers the host resolves these host-internally and emits no request.

// types/channels-canvas/commands.ts

export interface CanvasOpenParams extends BaseParams {
  channel: URI;            // the owning session URI (ahp-session:/<uuid>)
  canvasId: string;
  extensionId: string;
  instanceId: string;      // caller-minted handle for this panel
  input?: Record<string, unknown>;
}
export interface CanvasOpenResult {
  url?: string;
  title?: string;
  status?: string;
}

export interface CanvasInvokeActionParams extends BaseParams {
  channel: URI;            // ahp-session:/<uuid>
  instanceId: string;
  canvasId: string;
  extensionId: string;
  actionName: string;
  input?: Record<string, unknown>;
}
export interface CanvasInvokeActionResult {
  value?: unknown;         // opaque, provider-defined
}

export interface CanvasCloseParams extends BaseParams {
  channel: URI;            // ahp-session:/<uuid>
  instanceId: string;
  canvasId: string;
  extensionId: string;
}
// result: null

Registered symmetrically, following the resource* precedent:

// ServerCommandMap (server → client)  — and mirrored in CommandMap for symmetry
'canvasOpen':         { params: CanvasOpenParams;         result: CanvasOpenResult };
'canvasInvokeAction': { params: CanvasInvokeActionParams; result: CanvasInvokeActionResult };
'canvasClose':        { params: CanvasCloseParams;        result: null };

Provider errors are returned as ordinary JSON-RPC errors. Define an application error code — e.g. CanvasProviderError — whose data carries { code: string; message: string } (observed provider codes include canvas_action_no_handler and canvas_provider_unavailable). Cancellation and timeouts are ordinary request-lifecycle concerns — there is no cancellation action or in-state deadline to model.

Note

Rendering is state-driven, not request-driven. A client renders a canvas by subscribing to its ahp-canvas:/<id> channel and reading CanvasState.url — resolving it directly or over the channel per §9/§10; it never receives a "render this" request. The canvasOpen / canvasInvokeAction / canvasClose requests exist only to drive a client-declared provider. This cleanly separates "who renders" (any capable subscriber) from "who provides" (the declaring client, or the host).

6. Lifecycle

sequenceDiagram
  participant Agent as Agent (opaque, in host)
  participant Host as AHP Host
  participant Client as AHP client (capabilities.canvas)

  Note over Host,Client: initialize → client declares capabilities.canvas
  Note over Host,Client: session → SessionActiveClient.canvasProviders folded into SessionState.canvases

  Agent->>Host: open canvas (canvasId, instanceId, input)
  alt client-declared provider
    Host->>Client: canvasOpen { channel: session, canvasId, extensionId, instanceId, input }
    Client-->>Host: { url?, title?, status? }
  else server-side provider
    Host->>Host: resolve internally → { url, title, status }
  end
  Host->>Host: mint ahp-canvas:/<id>, build CanvasState
  Host->>Client: action session/openCanvasesChanged (catalogue gains the ref)
  Client->>Host: subscribe ahp-canvas:/<id>
  Host-->>Client: snapshot CanvasState → render url
  Agent->>Host: invoke action (instanceId, actionName, input)
  alt client-declared provider
    Host->>Client: canvasInvokeAction {...} → { value? }
  else server-side provider
    Host->>Host: resolve internally → value
  end
  Host->>Client: action canvas/updated (title/status/url deltas)
  Client->>Host: dispatchAction canvas/closeRequested (user hit ✕)
  alt client-declared provider
    Host->>Client: canvasClose {...} → null
  else server-side provider
    Host->>Host: resolve internally
  end
  Host->>Client: action session/openCanvasesChanged (ref removed)
  Note over Client: unsubscribe ahp-canvas:/<id>
Loading
  • Provider disconnect. The host dispatches canvas/updated { availability: 'stale' } and keeps the entry. In-flight canvasOpen/canvasInvokeAction/canvasClose requests fail with an ordinary JSON-RPC error. On reconnect the provider re-opens and the host flips availability back to 'ready'.
  • Resume. SessionState.canvases and SessionState.openCanvases replay in the session snapshot; the client re-subscribes to each ahp-canvas:/<id> and gets a fresh CanvasState. For server-side providers whose url is ephemeral, the entry may be 'stale' (and url absent) until the provider re-opens.

7. Session actions

Two new session-channel actions (both full-replacement, mirroring session/activeClientToolsChanged and the customizations registry):

Action Direction Reducer
session/canvasesChanged Server state.canvases = action.canvases
session/openCanvasesChanged Server state.openCanvases = action.openCanvases
// types/channels-session/actions.ts
export interface SessionCanvasesChangedAction {
  type: ActionType.SessionCanvasesChanged;
  canvases: SessionCanvasDeclaration[];
}
export interface SessionOpenCanvasesChangedAction {
  type: ActionType.SessionOpenCanvasesChanged;
  openCanvases: OpenCanvasRef[];
}

8. Server validation of client-dispatched actions

Append to the validation table in session-channel.md (session action) and add a Canvas section to a new channel page:

Action / request Condition Server behaviour
canvas/closeRequested No open instance with matching instanceId SHOULD silently ignore (parallels stale tool-call denial)
canvas/closeRequested Dispatched by a client that is not a subscriber/renderer of the instance SHOULD reject
canvasOpen/canvasInvokeAction/canvasClose (if a client ever initiates) Target canvas is not a client-declared provider owned by the peer SHOULD reject
canvasReadResource uri is not an ahp-canvas-content: URI scoped to an open instance the peer renders, or the instance is closed SHOULD reject
canvas/message No open instance with matching instanceId, or peer is not a subscriber/renderer of it SHOULD reject

9. URL / render-target policy

CanvasState.url is a plain string; the renderer enforces policy. The spec SHOULD require renderers to implement at least the first two:

  1. Scheme allow-list. Refuse schemes outside an explicit allow-list. Baseline: https, file, data, plus implementation-defined in-process schemes (vscode-webview://, tauri-app://, …). http MAY be allowed only for localhost / 127.0.0.1. The ahp-canvas-content: scheme (§10) is always permitted — the renderer resolves it over the channel rather than loading it as a network URL.
  2. Origin isolation. Sandbox canvas content (iframe sandbox, isolated webview/web-contents) so it cannot read application-scope state or credentials.
  3. Credential handling. Strip Authorization, Cookie, and other ambient credentials on navigation to a canvas URL.

10. Content transport & relayed deployments

Everything above puts the control plane on the wire — discovery, the provider requests (§5), the CanvasState snapshot, and lifecycle. But a canvas is only useful once its content renders, and §9 addresses that content by url. If that url is an endpoint the renderer dials directly (https://…, http://localhost, an in-process scheme), the client must be able to reach it. In a relayed deployment — the host sits behind a broker and has no address the renderer can open a socket to — a direct url is unreachable, and the canvas is blank even though every control message arrived.

So content is addressed one of two ways, and the renderer dispatches on the url scheme:

Mode url scheme How the renderer gets the bytes When
Direct https:, in-process (vscode-webview://, host-native), http://localhost Loads the URL directly in its sandboxed surface Renderer can reach the content origin. Fast path; §9 policy applies.
Channel-served ahp-canvas-content:/<instanceId>/<path> Reads it over the owning ahp-canvas:/<id> channel via canvasReadResource Renderer cannot reach the host directly (relayed / broker-fronted).

ahp-canvas-content: is a content-address scheme, not a subscribable channel — it never appears in the §3 channel table; it is resolved by request against the instance's existing ahp-canvas:/<id> channel. The <instanceId> segment tells the renderer which channel to read from.

Static content — canvasReadResource (client → server). A client→host request registered in CommandMap, modeled on MCP's resources/read:

// types/channels-canvas/commands.ts
export interface CanvasReadResourceParams extends BaseParams {
  channel: URI;   // the owning ahp-canvas:/<id>
  uri: string;    // an ahp-canvas-content:/<instanceId>/... URI
}
export interface CanvasReadResourceResult {
  contents: CanvasResourceContent[];
}
export interface CanvasResourceContent {
  uri: string;
  mimeType?: string;
  text?: string;   // for text payloads
  blob?: string;   // base64 for binary payloads
}
// CommandMap (client → server)
'canvasReadResource': { params: CanvasReadResourceParams; result: CanvasReadResourceResult };

The renderer resolves the top-level ahp-canvas-content: url through this request, then serves the bytes into its sandboxed surface (a custom scheme handler / service worker / data: rehydration). Sub-resources resolve the same way: any relative or ahp-canvas-content: reference the document makes is intercepted by the renderer and satisfied by another canvasReadResource, so the surface never touches the network and needs no host endpoint it can reach.

Interactive traffic — the canvas/message bridge. Static reads cover a self-contained document; an interactive View that talks back to its provider (the relay-carried analogue of a postMessage bridge) needs a bidirectional path. The canvas/message channel action (§4) carries it both ways as an opaque { payload }: client→host is a View→provider message, host→client is a provider→View message, routed to the provider resolved per §5 (host-internal for a server-side provider). Like terminal/input and canvas/closeRequested it is a pure signal with a no-op reducer, so it never bloats channel state.

This is additive, and the direct path stays first-class. Channel-served transport is a supported content mode, never a required one:

  • The host chooses the addressing mode per canvas from its own reachability: a host that can expose a reachable endpoint returns a direct url; a relayed host returns an ahp-canvas-content: url. A renderer that supports Canvas MUST handle both, so it never has to know which deployment it is in.
  • Optionally a client MAY advertise that it can reach host endpoints directly (a flag on its canvasProviders declaration or the capability), letting the host prefer a direct url when the renderer can use it.
  • A direct desktop client never pays the channel-transport cost; a relayed client never needs a reachable endpoint. The channel path only adds relay support — it changes nothing for direct rendering.

Implementation scope scales with ambition, so implementors can size it: a self-contained document is a single canvasReadResource round-trip; a rich app that loads its own sub-resources and holds a live bridge additionally needs the renderer to intercept its surface's loads and the host to serve/route them — both ends build that together. Neither is on the path for a host that only ever emits directly-reachable URLs.

11. Versioning

The current PROTOCOL_VERSION is 0.5.1. This surface is additive; introduce it at the next version (e.g. 0.6.0):

  • Add ACTION_INTRODUCED_IN entries for session/canvasesChanged, session/openCanvasesChanged, canvas/updated, canvas/closeRequested, canvas/message.
  • Track the new canvasOpen / canvasInvokeAction / canvasClose (server→client) and canvasReadResource (client→server) request methods per the repo's method-versioning convention; they are only exercised when both peers negotiate the version and the client declares capabilities.canvas.
  • Bump PROTOCOL_VERSION and prepend it to SUPPORTED_PROTOCOL_VERSIONS.

12. Files to touch

  • types/common/commands.ts — add ClientCapabilities.canvas.
  • types/channels-session/state.tsSessionState.canvases, SessionState.openCanvases, SessionActiveClient.canvasProviders, and the supporting declaration/ref types + CanvasAvailability / CanvasProviderSource.
  • types/channels-session/actions.ts + types/common/actions.ts — the two new ActionType entries + interfaces; extend the session action union.
  • types/channels-session/reducer.ts — two full-replacement arms.
  • New types/channels-canvas/state.ts (CanvasState), actions.ts (canvas/updated, canvas/closeRequested, canvas/message), commands.ts (the three server→client provider methods + the client→server canvasReadResource), reducer.ts (merge arm + no-op arms). Register the channel in types/index.ts and the message maps in types/common/messages.ts (canvasReadResource in CommandMap; the provider methods in ServerCommandMap, mirrored in CommandMap).
  • types/version/registry.tsACTION_INTRODUCED_IN entries + version bump.
  • schema/*.json — regenerate.
  • docs/specification/ — new canvas-channel.md (covering the ahp-canvas-content: content-address scheme and the in-band transport of §10); URI-scheme row in subscriptions.md for ahp-canvas:/<id> (the subscribable channel only); overview channel list; session-channel validation additions.
  • docs/guide/ — a canvases.md guide (concepts: declaration vs instance vs provider; the server-vs-client-provider split; direct vs channel-served content and relayed deployments; end-to-end flows; security policy) + sidebar entry.
  • clients/{rust,kotlin,swift,go}/… — regenerate mirrors + hand-port the reducer arms; clients/typescript CHANGELOG.
  • types/test-cases/reducers/ — fixtures: registry replace, open-catalogue replace, canvas/updated merge (+ null-clear, absent-preserve, unknown-instance no-op), canvas/closeRequested no-op, canvas/message no-op. (canvasReadResource is a request, not an action — cover it in request/round-trip fixtures rather than reducer fixtures.)

Compatibility

Purely additive.

  • All new state fields are optional; all new actions/requests are unknown to older consumers and ignored per the additive-evolution rule.
  • No changes to existing types, methods, or transport.
  • Renderer capability is negotiated via capabilities.canvas on initialize — a host offers the canvas surface only to clients that declared it. A client that never declares it, or a host that never populates canvas state, behaves exactly as today.

Considered alternatives

  • Model provider callbacks as session state + a correlated action pair (requestCreated / requestCompleted / requestCancelled with an in-state deadline and a per-target completion-direction rule). This simulates request/response on top of one-way action notifications. Rejected: AHP already has native bidirectional request/response (ServerCommandMap, the symmetric resource* + createResourceWatch), so the simulation adds a correlation protocol, a shared-state timestamp, and a validation matrix to re-implement something the transport already does. It also broadcasts every in-flight request payload to every session subscriber.
  • Put the full open-instance state (and all in-flight requests) directly on SessionState. Rejected: it forces every session subscriber — including clients that never render a canvas — to carry the canvas reducer arms and receive all canvas traffic. The per-instance channel + capability opt-in scopes the state and traffic to the clients that render it, matching terminals/changesets/resource-watch.
  • Tunnel canvases through customizations. Mechanical fit but semantic mismatch — customizations are agent-augmentation sources (plugins, skills, MCP servers) with their own parse/load lifecycle, and conflating them loses the per-instance state Canvas needs.
  • Leave Canvas as a vendor _meta extension. Fine as a prototype vehicle, not a protocol answer: every client that renders canvases would grow bespoke handling, defeating the point of the protocol.

Open questions

  1. One channel per instance vs. one canvas channel per session. This proposal uses one channel per open instance (matching terminal/changeset/resource-watch). A single per-session ahp-canvas:/<sessionId> channel carrying all instances is simpler to subscribe to and closer to the mcp:// "one side-channel per owner" shape, at the cost of per-instance subscription granularity. Which convention do maintainers prefer?
  2. Render targeting for server-side providers. When several capabilities.canvas clients are connected, which renders a server-side-provider canvas? Options: all capable subscribers render; the host pins a preferred renderer on the OpenCanvasRef; or the requesting client is pinned. (MCP Apps sidesteps this because the App is bound to a specific tool call on a specific client.)
  3. Typed View → provider calls. The canvas/message bridge (§10) already gives a rendered View an opaque channel to its provider. Open question: should a View also get the typed, request/response canvasInvokeAction in the client→server direction (a structured call with a return value, closer to mcp://'s tools/call), or is the opaque bridge sufficient? Confirm before adding a client→server canvasInvokeAction form.
  4. Where the open-instance catalogue livesSessionState.openCanvases (as proposed) vs a RootState-style catalogue vs a uriTemplate (like changesets). Session-scoped placement is the most natural, but maintainers may prefer consistency with one of the others.
  5. canvasProviders on the active client vs. a dedicated client-declaration action. This proposal updates it atomically with activeClientChanged (like tools); a dedicated change action is the alternative.

References

  • The mcp:// channel and MCP Apps — the capability-opt-in + discovery-surface + dedicated-channel pattern this proposal follows for an opaque, client-rendered UI surface; MCP's resources/read is the model for the in-band canvasReadResource content path (§10).
  • Terminal channel and Resource Watch channel — the stateful per-resource channel pattern, and (createResourceWatch) the "mint a channel URI and return it" precedent.
  • ServerCommandMap / symmetric resource* — the existing bidirectional server→client request machinery the provider callbacks reuse.
  • Channels & Subscriptions — the channel model and the universal channel: URI routing key.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions