Skip to content

Commit 51043b1

Browse files
jmoseleyCopilot
andauthored
Add SDK canvas runtime support (#1401)
* Add canvas runtime support to SDK 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> * Add canvas provider RPC tracing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add extension info session option 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> * Expose canvas resume durability fields Add CanvasInstanceAvailability, OpenCanvasInstance availability, and resume openCanvases seeding support to the Rust SDK. Validation: cargo +nightly-2026-04-14 fmt --check; cargo clippy --all-features --all-targets -- -D warnings; cargo test --all-features. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address canvas SDK review feedback Validate canvas provider request payloads before routing, surface Rust canvas serialization and builder errors, and clarify list_open RPC behavior. Validation: nodejs typecheck/lint/vitest; rust fmt/clippy/test --all-features. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Format Rust session imports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Sync canvas tool surface docs 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> * Require canvas descriptions 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> * Require canvas instance availability 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 Node canvas open handler 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> * Drop canvas toolbar and focus/reload surface 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> * Restore optional onClose handler on Node canvas options 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> * Support per-action handlers on Node canvas actions 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> * Drop top-level onAction fallback on Node canvases 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> * Normalize JSDoc style on CanvasAgentActionDeclaration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Drop stale wire-rename guards from canvas declaration test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document canvas action dispatch divergence in Rust SDK Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Require handler on canvas actions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Collapse CanvasAgentActionDeclaration into CanvasAction Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Drop debug log for inbound JSON-RPC requests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Drop canvas debug logs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove canvas tools field 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> * rust: slim canvas surface to wire types + CanvasHandler 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> * rust: drop canvas wire types duplicated by codegen 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> * Fix broken intra-doc link to renamed CanvasAction type 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> * go: add canvas runtime support 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> * python: add canvas runtime support 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> * dotnet: add canvas runtime support 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> * Address PR review: openCanvases parity + Node API divergence note - Node: add openCanvases accessor on CopilotSession and OpenCanvases field on ResumeSessionConfig so callers can both rehydrate from the resume response and pre-populate canvas state on resume. - Node: document why createCanvas/Canvas intentionally diverges from the per-session CanvasHandler pattern used by Rust/Python/Go/.NET. - Go: add ResumeSessionConfig.OpenCanvases, thread through to the resume request wire payload, and add a serialization test. - .NET: add ResumeSessionConfig.OpenCanvases, thread through to the internal ResumeSessionRequest record, and add a serialization test. Mirrors what Rust and Python already do, fixing wire-protocol parity across SDKs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 08f0521 commit 51043b1

34 files changed

Lines changed: 4083 additions & 62 deletions

dotnet/src/Canvas.cs

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using GitHub.Copilot.Rpc;
12+
13+
namespace GitHub.Copilot;
14+
15+
/// <summary>
16+
/// Declarative metadata for a single canvas, sent over the wire on
17+
/// <c>session.create</c> / <c>session.resume</c>.
18+
/// </summary>
19+
[Experimental(Diagnostics.Experimental)]
20+
public sealed class CanvasDeclaration
21+
{
22+
/// <summary>Canvas identifier, unique within the declaring connection.</summary>
23+
[JsonPropertyName("id")]
24+
public string Id { get; set; } = string.Empty;
25+
26+
/// <summary>Human-readable name shown in host UI and canvas pickers.</summary>
27+
[JsonPropertyName("displayName")]
28+
public string DisplayName { get; set; } = string.Empty;
29+
30+
/// <summary>Short, single-sentence description shown to the agent in canvas catalogs.</summary>
31+
[JsonPropertyName("description")]
32+
public string Description { get; set; } = string.Empty;
33+
34+
/// <summary>JSON Schema for the <c>input</c> payload accepted by <c>canvas.open</c>.</summary>
35+
[JsonPropertyName("inputSchema")]
36+
public JsonElement? InputSchema { get; set; }
37+
38+
/// <summary>Agent-callable actions this canvas exposes.</summary>
39+
[JsonPropertyName("actions")]
40+
public IList<CanvasAction>? Actions { get; set; }
41+
}
42+
43+
/// <summary>
44+
/// Stable extension identity for session participants that provide canvases.
45+
/// </summary>
46+
[Experimental(Diagnostics.Experimental)]
47+
public sealed class ExtensionInfo
48+
{
49+
/// <summary>Extension namespace/source, e.g. <c>"github-app"</c>.</summary>
50+
[JsonPropertyName("source")]
51+
public string Source { get; set; } = string.Empty;
52+
53+
/// <summary>Stable provider name within the source namespace.</summary>
54+
[JsonPropertyName("name")]
55+
public string Name { get; set; } = string.Empty;
56+
}
57+
58+
/// <summary>Response returned from <see cref="ICanvasHandler.OnOpenAsync"/>.</summary>
59+
[Experimental(Diagnostics.Experimental)]
60+
public sealed class CanvasOpenResponse
61+
{
62+
/// <summary>URL the host should render. Optional for canvases with no visual surface.</summary>
63+
[JsonPropertyName("url")]
64+
public string? Url { get; set; }
65+
66+
/// <summary>Provider-supplied title shown in host chrome.</summary>
67+
[JsonPropertyName("title")]
68+
public string? Title { get; set; }
69+
70+
/// <summary>Provider-supplied status text shown in host chrome.</summary>
71+
[JsonPropertyName("status")]
72+
public string? Status { get; set; }
73+
}
74+
75+
/// <summary>Host capabilities passed to canvas provider callbacks.</summary>
76+
[Experimental(Diagnostics.Experimental)]
77+
public sealed class CanvasHostContext
78+
{
79+
/// <summary>Host capability details.</summary>
80+
[JsonPropertyName("capabilities")]
81+
public CanvasHostCapabilities Capabilities { get; set; } = new();
82+
}
83+
84+
/// <summary>Host capability details passed to canvas provider callbacks.</summary>
85+
[Experimental(Diagnostics.Experimental)]
86+
public sealed class CanvasHostCapabilities
87+
{
88+
/// <summary>Whether the host supports canvas rendering.</summary>
89+
[JsonPropertyName("canvases")]
90+
public bool Canvases { get; set; }
91+
}
92+
93+
/// <summary>Context handed to <see cref="ICanvasHandler.OnOpenAsync"/>.</summary>
94+
[Experimental(Diagnostics.Experimental)]
95+
public sealed class CanvasOpenContext
96+
{
97+
/// <summary>Session that requested the canvas.</summary>
98+
public string SessionId { get; init; } = string.Empty;
99+
100+
/// <summary>Owning provider identifier.</summary>
101+
public string ExtensionId { get; init; } = string.Empty;
102+
103+
/// <summary>Canvas id from the declaring <see cref="CanvasDeclaration"/>.</summary>
104+
public string CanvasId { get; init; } = string.Empty;
105+
106+
/// <summary>Stable instance id supplied by the runtime.</summary>
107+
public string InstanceId { get; init; } = string.Empty;
108+
109+
/// <summary>Validated input payload.</summary>
110+
public JsonElement Input { get; init; }
111+
112+
/// <summary>Host capabilities supplied by the runtime.</summary>
113+
public CanvasHostContext? Host { get; init; }
114+
}
115+
116+
/// <summary>Context handed to <see cref="ICanvasHandler.OnActionAsync"/>.</summary>
117+
[Experimental(Diagnostics.Experimental)]
118+
public sealed class CanvasActionContext
119+
{
120+
/// <summary>Session that invoked the action.</summary>
121+
public string SessionId { get; init; } = string.Empty;
122+
123+
/// <summary>Owning provider identifier.</summary>
124+
public string ExtensionId { get; init; } = string.Empty;
125+
126+
/// <summary>Canvas id targeted by the action.</summary>
127+
public string CanvasId { get; init; } = string.Empty;
128+
129+
/// <summary>Instance id targeted by the action.</summary>
130+
public string InstanceId { get; init; } = string.Empty;
131+
132+
/// <summary>Action name from <see cref="CanvasAction.Name"/>.</summary>
133+
public string ActionName { get; init; } = string.Empty;
134+
135+
/// <summary>Validated input payload.</summary>
136+
public JsonElement Input { get; init; }
137+
138+
/// <summary>Host capabilities supplied by the runtime.</summary>
139+
public CanvasHostContext? Host { get; init; }
140+
}
141+
142+
/// <summary>Context handed to a canvas's close lifecycle hook.</summary>
143+
[Experimental(Diagnostics.Experimental)]
144+
public sealed class CanvasLifecycleContext
145+
{
146+
/// <summary>Session owning the canvas instance.</summary>
147+
public string SessionId { get; init; } = string.Empty;
148+
149+
/// <summary>Owning provider identifier.</summary>
150+
public string ExtensionId { get; init; } = string.Empty;
151+
152+
/// <summary>Canvas id from the declaring <see cref="CanvasDeclaration"/>.</summary>
153+
public string CanvasId { get; init; } = string.Empty;
154+
155+
/// <summary>Instance id this lifecycle event applies to.</summary>
156+
public string InstanceId { get; init; } = string.Empty;
157+
158+
/// <summary>Host capabilities supplied by the runtime.</summary>
159+
public CanvasHostContext? Host { get; init; }
160+
}
161+
162+
/// <summary>Structured error returned from canvas handlers.</summary>
163+
/// <remarks>
164+
/// Throw this from <see cref="ICanvasHandler"/> implementations to surface a
165+
/// machine-readable error code to the runtime. Any other exception is wrapped
166+
/// in a generic <c>canvas_handler_error</c> envelope.
167+
/// </remarks>
168+
[Experimental(Diagnostics.Experimental)]
169+
public sealed class CanvasError : Exception
170+
{
171+
/// <summary>Initializes a new <see cref="CanvasError"/>.</summary>
172+
/// <param name="code">Machine-readable error code.</param>
173+
/// <param name="message">Human-readable message.</param>
174+
public CanvasError(string code, string message) : base(message)
175+
{
176+
Code = code;
177+
}
178+
179+
/// <summary>Machine-readable error code.</summary>
180+
public string Code { get; }
181+
182+
/// <summary>
183+
/// Default error returned when a custom action has no handler.
184+
/// </summary>
185+
public static CanvasError NoHandler() => new(
186+
"canvas_action_no_handler",
187+
"No handler implemented for this canvas action");
188+
}
189+
190+
/// <summary>
191+
/// Internal helpers used by the session runtime to translate <see cref="CanvasError"/>
192+
/// (and other handler-thrown exceptions) into structured JSON-RPC error responses.
193+
/// </summary>
194+
internal static class CanvasErrorHelpers
195+
{
196+
private const int InternalError = -32603;
197+
198+
public static LocalRpcInvocationException HandlerUnset() => Build(
199+
"canvas_handler_unset",
200+
"No canvas handler is registered on this session");
201+
202+
public static LocalRpcInvocationException HandlerError(string message) => Build(
203+
"canvas_handler_error",
204+
message);
205+
206+
public static LocalRpcInvocationException ToRpcException(CanvasError error) => Build(error.Code, error.Message);
207+
208+
private static LocalRpcInvocationException Build(string code, string message)
209+
{
210+
var json = JsonSerializer.Serialize(
211+
new CanvasErrorPayload { Code = code, Message = message },
212+
CanvasJsonContext.Default.CanvasErrorPayload);
213+
using var doc = JsonDocument.Parse(json);
214+
return new LocalRpcInvocationException(InternalError, message, doc.RootElement.Clone());
215+
}
216+
217+
internal sealed class CanvasErrorPayload
218+
{
219+
[JsonPropertyName("code")]
220+
public string Code { get; set; } = string.Empty;
221+
222+
[JsonPropertyName("message")]
223+
public string Message { get; set; } = string.Empty;
224+
}
225+
}
226+
227+
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
228+
[JsonSerializable(typeof(CanvasErrorHelpers.CanvasErrorPayload))]
229+
internal partial class CanvasJsonContext : JsonSerializerContext;
230+
231+
/// <summary>
232+
/// Provider-side canvas lifecycle handler.
233+
/// </summary>
234+
/// <remarks>
235+
/// A session installs a single <see cref="ICanvasHandler"/> via
236+
/// <c>SessionConfigBase.CanvasHandler</c>. The handler receives every
237+
/// inbound <c>canvas.open</c> / <c>canvas.close</c> / <c>canvas.action.invoke</c>
238+
/// JSON-RPC request the runtime issues for this session and decides — typically
239+
/// by inspecting <see cref="CanvasOpenContext.CanvasId"/> — which
240+
/// application-side canvas should handle the call.
241+
/// <para>
242+
/// The SDK does not maintain a per-canvas registry; multiplexing across
243+
/// declared canvases is the implementor's responsibility.
244+
/// </para>
245+
/// <para>
246+
/// Implementations targeting <c>netstandard2.0</c> cannot rely on default
247+
/// interface methods; derive from <see cref="CanvasHandlerBase"/> to inherit
248+
/// sensible defaults for <see cref="OnCloseAsync"/> and <see cref="OnActionAsync"/>.
249+
/// </para>
250+
/// </remarks>
251+
[Experimental(Diagnostics.Experimental)]
252+
public interface ICanvasHandler
253+
{
254+
/// <summary>Open a new canvas instance.</summary>
255+
Task<CanvasOpenResponse> OnOpenAsync(CanvasOpenContext context, CancellationToken cancellationToken);
256+
257+
/// <summary>Canvas was closed by the user or agent. Default: no-op.</summary>
258+
Task OnCloseAsync(CanvasLifecycleContext context, CancellationToken cancellationToken);
259+
260+
/// <summary>
261+
/// Handle a non-lifecycle action declared by the canvas.
262+
/// Default: throws <see cref="CanvasError.NoHandler"/>.
263+
/// </summary>
264+
Task<object?> OnActionAsync(CanvasActionContext context, CancellationToken cancellationToken);
265+
}
266+
267+
/// <summary>
268+
/// Convenience base class for <see cref="ICanvasHandler"/> that supplies
269+
/// default no-op / no-handler implementations of the optional callbacks.
270+
/// </summary>
271+
[Experimental(Diagnostics.Experimental)]
272+
public abstract class CanvasHandlerBase : ICanvasHandler
273+
{
274+
/// <inheritdoc />
275+
public abstract Task<CanvasOpenResponse> OnOpenAsync(CanvasOpenContext context, CancellationToken cancellationToken);
276+
277+
/// <inheritdoc />
278+
public virtual Task OnCloseAsync(CanvasLifecycleContext context, CancellationToken cancellationToken)
279+
#if NET8_0_OR_GREATER
280+
=> Task.CompletedTask;
281+
#else
282+
=> Task.FromResult<object?>(null);
283+
#endif
284+
285+
/// <inheritdoc />
286+
public virtual Task<object?> OnActionAsync(CanvasActionContext context, CancellationToken cancellationToken)
287+
=> Task.FromException<object?>(CanvasError.NoHandler());
288+
}

0 commit comments

Comments
 (0)