Skip to content

Add SDK canvas runtime support#1401

Open
jmoseley wants to merge 28 commits into
mainfrom
jmoseley/sdk-canvas-runtime-support
Open

Add SDK canvas runtime support#1401
jmoseley wants to merge 28 commits into
mainfrom
jmoseley/sdk-canvas-runtime-support

Conversation

@jmoseley
Copy link
Copy Markdown
Contributor

@jmoseley jmoseley commented May 23, 2026

What changed

Adds canvas runtime support across the Rust, Node, Python, Go, and .NET SDKs so consumers can declare canvases on session.create / session.resume, install a single CanvasHandler, and receive routed canvas.open / canvas.close / canvas.action.invoke JSON-RPC requests from the runtime. Resume surfaces openCanvases for rehydration, and host code can call session.canvas.* for native UI / chrome actions.

Per-language surface

  • Rust (rust/src/canvas.rs): CanvasDeclaration, CanvasHandler trait, CanvasHostContext/CanvasOpenContext/CanvasActionContext/CanvasLifecycleContext, CanvasError, CanvasOpenResponse, ExtensionInfo. Wire types (OpenCanvasInstance, CanvasAction, CanvasInstanceAvailability, CanvasInvokeActionRequest/Result, etc.) consumed directly from generated::api_types — no duplication. Session::open_canvases() accessor; host APIs via session.rpc().canvas().
  • Node (nodejs/src/canvas.ts): createCanvas, canvas declarations, handler contexts, errors, exports. Session create/resume thread canvases, requestCanvasRenderer, requestExtensions, extensionInfo.
  • Python (python/copilot/canvas.py): CanvasHandler ABC with default on_close / on_action (raises canvas_action_no_handler). _jsonrpc loosened to allow arbitrary JSON return values for action results.
  • Go (go/canvas.go): CanvasHandler interface + CanvasHandlerDefaults embeddable for partial impls. Inbound RPC registered via jsonrpc2.RequestHandlerFor with {code,message} error envelope.
  • .NET (dotnet/src/Canvas.cs): idiomatic ICanvasHandler with async methods; same dispatch shape.

Java is intentionally not included in this PR and will follow separately.

Shared design

  • App-side dispatch: SDK exposes a single CanvasHandler per session. Consumers switch on canvas_id themselves — no per-canvas registry inside the SDK.
  • Generated wire types are imported directly from each language's codegen module; the hand-written canvas API is purely additive.
  • Inbound RPC without a handler installed returns {"code":"canvas_handler_unset","message":"..."}.
  • ExtensionInfo { source, name } lets consumers opt into stable agent-facing extension IDs (github-app:<provider-name>).

Notes for reviewers

Additive on top of the existing permission / user-input / tool handler models — no broad handler refactor. Canvas declarations require displayName. capabilities.ui.canvases is boolean. session.canvas.listOpen is a real RPC.

Validation

Per language:

  • Rust: cargo +nightly-2026-04-14 fmt --check, cargo clippy --all-features --all-targets -- -D warnings, cargo test --all-features, cargo doc --no-deps --all-features
  • Node: npm run typecheck, npm run lint, npx vitest run
  • Python: uv run ruff format/check, uv run ty check copilot, uv run pytest
  • Go: gofmt, go build/vet/test ./...
  • .NET: dotnet format --verify-no-changes, dotnet build, dotnet test

@jmoseley jmoseley requested a review from a team as a code owner May 23, 2026 04:50
Copilot AI review requested due to automatic review settings May 23, 2026 04:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds cross-SDK “canvas runtime” support, enabling SDK consumers to declare canvases, receive direct provider callbacks (canvas.*), and use host-side session.canvas.* RPCs with stable extension identity (extensionInfo)—without requiring the larger handler refactor from the prototype branch.

Changes:

  • Rust: Introduces a new canvas module (declarations, handler traits, dispatch, host RPC types) and wires SessionConfig / ResumeSessionConfig to serialize canvas-related fields on session.create / session.resume.
  • Rust: Adds Session::canvas() host API and updates the session event loop/router to handle direct provider callback requests (canvas.open|focus|reload|close|action.invoke).
  • Node.js: Adds a canvas surface (createCanvas, types, dispatch), forwards canvas declarations + request flags + extensionInfo on session create/resume, and routes direct canvas.* JSON-RPC requests to registered canvases with tests.
Show a summary per file
File Description
rust/tests/session_test.rs Adds coverage for Rust canvas wire fields, provider dispatch routing, and SessionCanvas host API behavior.
rust/tests/e2e/elicitation.rs Updates capability struct test to include the new ui.canvases field.
rust/src/wire.rs Extends session create/resume wire payloads with canvases, request flags, and extension_info.
rust/src/types.rs Adds ExtensionInfo, canvas config fields on session configs, resume result open-canvas capture, and UiCapabilities.canvases.
rust/src/session.rs Adds SessionCanvas host API + open-canvas tracking and routes canvas.* provider callbacks through a per-session registry.
rust/src/router.rs Adds debug logging when routing requests to a registered session.
rust/src/lib.rs Exposes the new canvas module publicly.
rust/src/jsonrpc.rs Adds debug logging for incoming JSON-RPC requests from the runtime.
rust/src/canvas.rs New Rust canvas module: declarations, handler traits, registry + dispatch, request/response types, and unit tests.
nodejs/test/extension.test.ts Verifies createCanvas is exported from the extension surface.
nodejs/test/client.test.ts Adds tests for forwarding canvas fields on create/resume and for direct provider dispatch behavior.
nodejs/src/types.ts Adds SessionCapabilities.ui.canvases, introduces ExtensionInfo, and extends session configs with canvas fields.
nodejs/src/session.ts Adds per-session canvas registration and lookup to support direct dispatch routing.
nodejs/src/index.ts Exports canvas APIs/types and ExtensionInfo from the main entrypoint.
nodejs/src/extension.ts Exports canvas APIs/types and ExtensionInfo from the extension surface.
nodejs/src/client.ts Forwards canvas fields on create/resume and registers handlers for direct canvas.* provider callbacks.
nodejs/src/canvas.ts New Node canvas module: canvas declaration/types, createCanvas, and provider request dispatch helper.

Copilot's findings

  • Files reviewed: 17/17 changed files
  • Comments generated: 5

Comment thread rust/src/session.rs Outdated
Comment thread rust/src/canvas.rs Outdated
Comment thread rust/src/canvas.rs Outdated
Comment thread nodejs/src/client.ts Outdated
Comment thread nodejs/src/client.ts Outdated
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@jmoseley jmoseley force-pushed the jmoseley/sdk-canvas-runtime-support branch from 1202bc1 to c50b728 Compare May 24, 2026 07:24
@jmoseley jmoseley changed the base branch from main to update-copilot-1.0.53-2 May 24, 2026 07:25
@github-actions

This comment has been minimized.

Base automatically changed from update-copilot-1.0.53-2 to main May 24, 2026 07:48
@jmoseley jmoseley enabled auto-merge May 24, 2026 07:54
jmoseley and others added 3 commits May 24, 2026 07:54
Add Node extension canvas APIs and direct canvas provider callback routing. Add Rust canvas declarations, provider handlers, create/resume wiring, and host session.canvas APIs aligned with the runtime schema.

Validation: nodejs typecheck/lint/tests; rust fmt/check/clippy; cargo test --all-features.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expose stable extension identity metadata on Node and Rust session create/resume options and forward extensionInfo on the wire for canvas providers.

Validation: nodejs typecheck/lint/vitest; rust fmt/clippy/test --all-features.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jmoseley and others added 18 commits May 24, 2026 07:54
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove stale focus/close/reload canvas agent-tool references and cover custom-tool permission payload passthrough for open_canvas.

Validation: nodejs typecheck; cargo test --all-features permission_request_data_extracts_typed_kind.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Align canvas contribution and discovered canvas descriptions with the runtime schema, update canvas tool-surface docs, and cover open_canvas custom-tool permission payloads.

Validation: nodejs typecheck/lint/vitest client+extension; rust fmt/clippy; cargo check --all-features --all-targets; targeted canvas and permission tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Align OpenCanvasInstance with the runtime schema by making availability required and updating canvas host/resume tests.

Validation: cargo check --all-features --all-targets; cargo test --all-features canvas; targeted session canvas tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename the Node canvas provider option from onOpen to open and remove lifecycle handler options from the extension canvas API.

Validation: nodejs typecheck; vitest client and extension tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Aligns the SDK canvas contract with copilot-agent-runtime
jmoseley/adr-implementation-plan commits 85b23bc264 and acdefc1bc1:

- Rename agentActions to actions on CanvasDeclaration and
  DiscoveredCanvas (Rust + Node).
- Drop toolbar from CanvasContribution and CanvasOpenResponse, and
  remove CanvasToolbarItemDeclaration / CanvasToolbarItem entirely.
- Drop SessionCanvas::focus and SessionCanvas::reload host APIs;
  re-opening with the same instanceId now drives focus via
  session.canvas.opened { reopen: true }, and reload is renderer-only.
- Drop canvas.focus / canvas.reload provider JSON-RPC routes and the
  matching CanvasHandler::on_focus / on_reload hooks; canvas.close keeps
  its dedicated dispatch path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lets extension authors observe canvas instance close events without
adding back the dropped onFocus/onReload hooks. Fire-and-forget: the
handler's return value is ignored and the provider response is still
undefined.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Each entry in createCanvas({ actions }) may now carry its own optional
handler, co-located with the action's metadata. The top-level onAction
remains as a fallback for actions that don't define their own handler.

Dispatch order:
  1. Per-action handler when set.
  2. Top-level onAction otherwise.
  3. canvas_action_no_handler if neither is wired.

The handler closure is stripped from the wire CanvasDeclaration sent on
session.create / session.resume; only the action's name, description,
and inputSchema reach the runtime. A new CanvasAction authoring type
sits on top of the existing CanvasAgentActionDeclaration wire type.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per-action handlers are now the only dispatch path. Declared actions
without a handler fall through to canvas_action_no_handler. Keeps the
action's metadata and behavior co-located and removes a second
indirection that always boiled down to a switch on actionName.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop CanvasToolDefinition, CanvasToolDefinitionDefer, and the
CanvasOpenResponse.tools / OpenCanvasInstance.tools fields from both
the Node and Rust SDKs. The CLI side is being removed in lockstep, so
the wire contract no longer carries this field.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move per-canvas registry, Canvas builder, dispatch helpers, and the
SessionCanvas host helper out of the SDK. The Rust canvas surface now
matches the other typed extension points (PermissionHandler /
UserInputHandler / HookHandler):

  SessionConfig
    .with_canvases([CanvasDeclaration, ...])
    .with_canvas_handler(Arc::new(MyHandler))

Removed:
  - canvas::Canvas, CanvasBuilder (declaration+handler bundle)
  - canvas::CanvasRegistry, build_registry, dispatch_canvas_*
  - session::SessionCanvas + Session::canvas() accessor
    (callers move to session.rpc().canvas().*)

Kept (the wire boundary + typed extension point):
  - All wire types (CanvasDeclaration, OpenCanvasInstance, ...)
  - CanvasHandler trait + on_open/on_action/on_close
  - SessionConfig/ResumeSessionConfig.canvases (now Vec<CanvasDeclaration>)
  - SessionConfig/ResumeSessionConfig.canvas_handler

handle_request dispatches canvas.open/close/action.invoke directly to
the handler; the per-canvas registry now lives in the app layer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jmoseley jmoseley force-pushed the jmoseley/sdk-canvas-runtime-support branch from 3a8b85f to 3c0850d Compare May 24, 2026 14:55
@github-actions

This comment has been minimized.

Removed CanvasInstanceAvailability, OpenCanvasInstance,
CanvasAgentActionDeclaration (-> CanvasAction), CanvasDiscoverResult,
DiscoveredCanvas, CanvasListOpenResult, CanvasOpenRequest,
CanvasCloseRequest, CanvasInvokeActionRequest, and
CanvasInvokeActionResult from canvas.rs; consumers import these from
crate::generated::api_types directly. The remaining hand-written types
(CanvasDeclaration, CanvasOpenResponse, handler trait, contexts,
CanvasError) are genuinely additive provider-authoring contracts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

The canvas wire types were deduplicated against generated/api_types.rs,
renaming CanvasAgentActionDeclaration to CanvasAction. A doc comment
in canvas.rs still referenced the old name, which broke cargo doc on CI
(broken_intra_doc_links is denied).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

jmoseley and others added 2 commits May 24, 2026 09:14
Mirrors the Rust SDK canvas surface in rust/src/canvas.rs:

- CanvasDeclaration, CanvasOpenResponse, CanvasHostContext,
  CanvasOpenContext / CanvasActionContext / CanvasLifecycleContext,
  CanvasError, CanvasHandler interface + CanvasHandlerDefaults, and
  ExtensionInfo.
- SessionConfig / ResumeSessionConfig: Canvases, RequestCanvasRenderer,
  RequestExtensions, CanvasHandler, ExtensionInfo.
- Inbound JSON-RPC dispatch for canvas.open, canvas.close, and
  canvas.action.invoke, with a canvas_handler_unset error envelope when
  no handler is installed and a canvas_handler_error envelope when a
  handler returns a non-CanvasError error.
- Session.OpenCanvases() surfaces the openCanvases snapshot from the
  session.resume response.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirrors the Rust SDK design: callers declare canvases on session.create
/ session.resume, install a single CanvasHandler, and the SDK dispatches
inbound canvas.open / canvas.close / canvas.action.invoke JSON-RPC
requests to that handler. Resume populates session.open_canvases from
the response. JSON-RPC dispatch was loosened to allow handlers to return
any JSON value (canvas.action.invoke result is arbitrary JSON).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Ports the canvas runtime surface from the Rust SDK to the .NET SDK so
.NET hosts can declare canvases on session create/resume, advertise an
extension identity, and handle inbound canvas.open / canvas.close /
canvas.action.invoke RPC calls.

* New public Canvas.cs surface (CanvasDeclaration, ExtensionInfo,
  CanvasOpenResponse, CanvasHostContext, lifecycle/action/open contexts,
  CanvasError, ICanvasHandler, CanvasHandlerBase). All marked
  [Experimental(GHCP001)].
* SessionConfigBase gains Canvases, RequestCanvasRenderer,
  RequestExtensions, ExtensionInfo, CanvasHandler.
* CreateSession/ResumeSession requests forward the new fields and
  surface OpenCanvases on the response. CopilotSession exposes the
  returned canvases via OpenCanvases.
* CopilotClient registers canvas.open / canvas.close /
  canvas.action.invoke handlers and dispatches them to the session,
  which invokes the user's ICanvasHandler and returns structured
  CanvasError data via a new JsonRpc LocalRpcInvocationException path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review

Great addition of canvas runtime support across five SDKs! The implementation is thorough and the per-language idioms are generally well-respected. I found three consistency gaps worth discussing:


1. Canvas handler design: Node.js diverges from Python / Go / .NET / Rust

The most significant inconsistency is the handler architecture:

SDK Pattern
Node.js createCanvas() factory — one Canvas object per canvas id with co-located open, onClose, and per-action handler closures. SDK routes by canvasId automatically.
Python / Go / .NET / Rust Single CanvasHandler per session — one handler dispatches all canvas_ids, switching internally.

The Node.js approach is arguably more ergonomic (similar to how tools co-locate their schema + handler) and easier to use when declaring multiple canvases. If this better design was adopted in other SDKs, the API surface would be more consistent. Alternatively, if the single-handler approach is preferred for the other SDKs, the Node.js rationale for diverging should be documented.

Suggestion: consider whether Python/Go/.NET/Rust should adopt the createCanvas factory pattern for API parity, or document explicitly that Node.js intentionally diverges here.


2. Node.js session doesn't expose openCanvases

All other SDKs populate and expose the open-canvas snapshot from the session.resume response:

  • Python: session.open_canvases property
  • Go: session.OpenCanvases() method
  • .NET: session.OpenCanvases property
  • Rust: Session::open_canvases() accessor (noted in PR description)
  • Node.js: ❌ not present on CopilotSession

Consumers who need to rehydrate canvas state on resume won't be able to access this from the Node.js SDK. This looks like an unintentional omission.


3. Python resume_session sends openCanvases in the request payload; Go / .NET do not

In python/copilot/client.py, resume_session accepts open_canvases as an input parameter and includes it in the wire payload:

if open_canvases:
    payload["openCanvases"] = [inst.to_dict() for inst in open_canvases]

Neither Go nor .NET include openCanvases in the resumeSessionRequest. If this is an intentional part of the session.resume wire protocol (e.g., for rehydration hints), Go and .NET should expose the same parameter. If it isn't part of the spec, the Python parameter should be removed.


Not an issue

  • Java being deferred is called out in the PR and is fine.
  • Minor differences in optionality (CanvasHostContext.capabilities being optional in Node.js vs always-present in Python/Go/.NET) are reasonable given language idioms.

Generated by SDK Consistency Review Agent for issue #1401 · ● 11M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1401 · ● 11M

Comment thread nodejs/src/session.ts
}

/**
* Registers command handlers for this session.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing openCanvases property — all other SDKs (Python, Go, .NET, Rust) expose the open-canvas snapshot from the session.resume response on the session object. Consider adding something like:

private openCanvasInstances: OpenCanvasInstance[] = [];

/** Open canvas instances reported by the most recent `session.resume` response. */
get openCanvases(): OpenCanvasInstance[] {
    return [...this.openCanvasInstances];
}

/** `@internal` */
setOpenCanvases(instances: OpenCanvasInstance[]): void {
    this.openCanvasInstances = instances;
}

Without this, Node.js SDK consumers can't rehydrate canvas state on resume, unlike every other SDK in this PR.

Comment thread nodejs/src/canvas.ts
}

/** A registered canvas: declarative metadata + in-process handler closures. */
export class Canvas {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Design divergence from other SDKs — Node.js uses a per-canvas Canvas class with co-located open/onClose handlers and per-action handler closures (dispatched automatically by canvas ID). Python, Go, .NET, and Rust all use a single CanvasHandler per session that switches on canvas_id internally.

The Node.js pattern is arguably more ergonomic (mirrors the DefineTool co-location pattern), but the divergence is significant enough that consumers reading across SDK docs may be confused. Please either:

  1. Document why Node.js intentionally diverges (e.g., in a comment on Canvas or createCanvas), or
  2. Consider whether the createCanvas factory pattern could be added to the other SDKs for API parity.

Comment thread python/copilot/client.py
request_extensions: bool | None = None,
extension_info: ExtensionInfo | None = None,
canvas_handler: CanvasHandler | None = None,
open_canvases: list[OpenCanvasInstance] | None = None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

open_canvases sent in resume request payload — not present in Go or .NET — Go's ResumeSessionConfig and resumeSessionRequest don't include open_canvases / OpenCanvases, nor does .NET's SessionConfig copy on resume.

If sending openCanvases in the session.resume request is intentional wire-protocol behavior (e.g., to hint the runtime for rehydration), Go and .NET should be updated to support it too. If it's not part of the spec, this parameter should be removed to avoid sending unrecognized fields.

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