-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add SDK canvas runtime support #1401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1cffb3e
4d5bd11
36397a8
458c6f3
9eb4523
076a288
68616d8
cc4bccc
d0accc4
5985b50
4ebff84
efd7aef
6710b30
cdd4a5e
b60c9cb
86b2cc9
0f597e9
f5c76f8
37c8d36
5115ce8
c1146c0
3475390
3c0850d
cd0f66f
f47d4c5
2dc1f90
c6d1f00
97dc612
04fc748
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,288 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Text.Json; | ||
| using System.Text.Json.Serialization; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using GitHub.Copilot.Rpc; | ||
|
|
||
| namespace GitHub.Copilot; | ||
|
|
||
| /// <summary> | ||
| /// Declarative metadata for a single canvas, sent over the wire on | ||
| /// <c>session.create</c> / <c>session.resume</c>. | ||
| /// </summary> | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public sealed class CanvasDeclaration | ||
| { | ||
| /// <summary>Canvas identifier, unique within the declaring connection.</summary> | ||
| [JsonPropertyName("id")] | ||
| public string Id { get; set; } = string.Empty; | ||
|
|
||
| /// <summary>Human-readable name shown in host UI and canvas pickers.</summary> | ||
| [JsonPropertyName("displayName")] | ||
| public string DisplayName { get; set; } = string.Empty; | ||
|
|
||
| /// <summary>Short, single-sentence description shown to the agent in canvas catalogs.</summary> | ||
| [JsonPropertyName("description")] | ||
| public string Description { get; set; } = string.Empty; | ||
|
|
||
| /// <summary>JSON Schema for the <c>input</c> payload accepted by <c>canvas.open</c>.</summary> | ||
| [JsonPropertyName("inputSchema")] | ||
| public JsonElement? InputSchema { get; set; } | ||
|
|
||
| /// <summary>Agent-callable actions this canvas exposes.</summary> | ||
| [JsonPropertyName("actions")] | ||
| public IList<CanvasAction>? Actions { get; set; } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Stable extension identity for session participants that provide canvases. | ||
| /// </summary> | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public sealed class ExtensionInfo | ||
| { | ||
| /// <summary>Extension namespace/source, e.g. <c>"github-app"</c>.</summary> | ||
| [JsonPropertyName("source")] | ||
| public string Source { get; set; } = string.Empty; | ||
|
|
||
| /// <summary>Stable provider name within the source namespace.</summary> | ||
| [JsonPropertyName("name")] | ||
| public string Name { get; set; } = string.Empty; | ||
| } | ||
|
|
||
| /// <summary>Response returned from <see cref="ICanvasHandler.OnOpenAsync"/>.</summary> | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public sealed class CanvasOpenResponse | ||
| { | ||
| /// <summary>URL the host should render. Optional for canvases with no visual surface.</summary> | ||
| [JsonPropertyName("url")] | ||
| public string? Url { get; set; } | ||
|
|
||
| /// <summary>Provider-supplied title shown in host chrome.</summary> | ||
| [JsonPropertyName("title")] | ||
| public string? Title { get; set; } | ||
|
|
||
| /// <summary>Provider-supplied status text shown in host chrome.</summary> | ||
| [JsonPropertyName("status")] | ||
| public string? Status { get; set; } | ||
| } | ||
|
|
||
| /// <summary>Host capabilities passed to canvas provider callbacks.</summary> | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public sealed class CanvasHostContext | ||
| { | ||
| /// <summary>Host capability details.</summary> | ||
| [JsonPropertyName("capabilities")] | ||
| public CanvasHostCapabilities Capabilities { get; set; } = new(); | ||
| } | ||
|
|
||
| /// <summary>Host capability details passed to canvas provider callbacks.</summary> | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public sealed class CanvasHostCapabilities | ||
| { | ||
| /// <summary>Whether the host supports canvas rendering.</summary> | ||
| [JsonPropertyName("canvases")] | ||
| public bool Canvases { get; set; } | ||
| } | ||
|
|
||
| /// <summary>Context handed to <see cref="ICanvasHandler.OnOpenAsync"/>.</summary> | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public sealed class CanvasOpenContext | ||
| { | ||
| /// <summary>Session that requested the canvas.</summary> | ||
| public string SessionId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Owning provider identifier.</summary> | ||
| public string ExtensionId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Canvas id from the declaring <see cref="CanvasDeclaration"/>.</summary> | ||
| public string CanvasId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Stable instance id supplied by the runtime.</summary> | ||
| public string InstanceId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Validated input payload.</summary> | ||
| public JsonElement Input { get; init; } | ||
|
|
||
| /// <summary>Host capabilities supplied by the runtime.</summary> | ||
| public CanvasHostContext? Host { get; init; } | ||
|
jmoseley marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /// <summary>Context handed to <see cref="ICanvasHandler.OnActionAsync"/>.</summary> | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public sealed class CanvasActionContext | ||
| { | ||
| /// <summary>Session that invoked the action.</summary> | ||
| public string SessionId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Owning provider identifier.</summary> | ||
| public string ExtensionId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Canvas id targeted by the action.</summary> | ||
| public string CanvasId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Instance id targeted by the action.</summary> | ||
| public string InstanceId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Action name from <see cref="CanvasAction.Name"/>.</summary> | ||
| public string ActionName { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Validated input payload.</summary> | ||
| public JsonElement Input { get; init; } | ||
|
|
||
| /// <summary>Host capabilities supplied by the runtime.</summary> | ||
| public CanvasHostContext? Host { get; init; } | ||
| } | ||
|
|
||
| /// <summary>Context handed to a canvas's close lifecycle hook.</summary> | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public sealed class CanvasLifecycleContext | ||
| { | ||
| /// <summary>Session owning the canvas instance.</summary> | ||
| public string SessionId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Owning provider identifier.</summary> | ||
| public string ExtensionId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Canvas id from the declaring <see cref="CanvasDeclaration"/>.</summary> | ||
| public string CanvasId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Instance id this lifecycle event applies to.</summary> | ||
| public string InstanceId { get; init; } = string.Empty; | ||
|
|
||
| /// <summary>Host capabilities supplied by the runtime.</summary> | ||
| public CanvasHostContext? Host { get; init; } | ||
| } | ||
|
|
||
| /// <summary>Structured error returned from canvas handlers.</summary> | ||
| /// <remarks> | ||
| /// Throw this from <see cref="ICanvasHandler"/> implementations to surface a | ||
| /// machine-readable error code to the runtime. Any other exception is wrapped | ||
| /// in a generic <c>canvas_handler_error</c> envelope. | ||
| /// </remarks> | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public sealed class CanvasError : Exception | ||
|
jmoseley marked this conversation as resolved.
|
||
| { | ||
| /// <summary>Initializes a new <see cref="CanvasError"/>.</summary> | ||
| /// <param name="code">Machine-readable error code.</param> | ||
| /// <param name="message">Human-readable message.</param> | ||
| public CanvasError(string code, string message) : base(message) | ||
| { | ||
| Code = code; | ||
| } | ||
|
|
||
| /// <summary>Machine-readable error code.</summary> | ||
| public string Code { get; } | ||
|
|
||
| /// <summary> | ||
| /// Default error returned when a custom action has no handler. | ||
| /// </summary> | ||
| public static CanvasError NoHandler() => new( | ||
| "canvas_action_no_handler", | ||
| "No handler implemented for this canvas action"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Internal helpers used by the session runtime to translate <see cref="CanvasError"/> | ||
| /// (and other handler-thrown exceptions) into structured JSON-RPC error responses. | ||
| /// </summary> | ||
| internal static class CanvasErrorHelpers | ||
| { | ||
| private const int InternalError = -32603; | ||
|
|
||
| public static LocalRpcInvocationException HandlerUnset() => Build( | ||
| "canvas_handler_unset", | ||
| "No canvas handler is registered on this session"); | ||
|
|
||
| public static LocalRpcInvocationException HandlerError(string message) => Build( | ||
| "canvas_handler_error", | ||
| message); | ||
|
|
||
| public static LocalRpcInvocationException ToRpcException(CanvasError error) => Build(error.Code, error.Message); | ||
|
|
||
| private static LocalRpcInvocationException Build(string code, string message) | ||
| { | ||
| var json = JsonSerializer.Serialize( | ||
| new CanvasErrorPayload { Code = code, Message = message }, | ||
| CanvasJsonContext.Default.CanvasErrorPayload); | ||
| using var doc = JsonDocument.Parse(json); | ||
| return new LocalRpcInvocationException(InternalError, message, doc.RootElement.Clone()); | ||
| } | ||
|
|
||
| internal sealed class CanvasErrorPayload | ||
| { | ||
| [JsonPropertyName("code")] | ||
| public string Code { get; set; } = string.Empty; | ||
|
|
||
| [JsonPropertyName("message")] | ||
| public string Message { get; set; } = string.Empty; | ||
| } | ||
| } | ||
|
|
||
| [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] | ||
| [JsonSerializable(typeof(CanvasErrorHelpers.CanvasErrorPayload))] | ||
| internal partial class CanvasJsonContext : JsonSerializerContext; | ||
|
jmoseley marked this conversation as resolved.
|
||
|
|
||
| /// <summary> | ||
| /// Provider-side canvas lifecycle handler. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// A session installs a single <see cref="ICanvasHandler"/> via | ||
| /// <c>SessionConfigBase.CanvasHandler</c>. The handler receives every | ||
| /// inbound <c>canvas.open</c> / <c>canvas.close</c> / <c>canvas.action.invoke</c> | ||
| /// JSON-RPC request the runtime issues for this session and decides — typically | ||
| /// by inspecting <see cref="CanvasOpenContext.CanvasId"/> — which | ||
| /// application-side canvas should handle the call. | ||
| /// <para> | ||
| /// The SDK does not maintain a per-canvas registry; multiplexing across | ||
| /// declared canvases is the implementor's responsibility. | ||
| /// </para> | ||
| /// <para> | ||
| /// Implementations targeting <c>netstandard2.0</c> cannot rely on default | ||
| /// interface methods; derive from <see cref="CanvasHandlerBase"/> to inherit | ||
| /// sensible defaults for <see cref="OnCloseAsync"/> and <see cref="OnActionAsync"/>. | ||
| /// </para> | ||
| /// </remarks> | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public interface ICanvasHandler | ||
| { | ||
| /// <summary>Open a new canvas instance.</summary> | ||
| Task<CanvasOpenResponse> OnOpenAsync(CanvasOpenContext context, CancellationToken cancellationToken); | ||
|
|
||
| /// <summary>Canvas was closed by the user or agent. Default: no-op.</summary> | ||
| Task OnCloseAsync(CanvasLifecycleContext context, CancellationToken cancellationToken); | ||
|
|
||
| /// <summary> | ||
| /// Handle a non-lifecycle action declared by the canvas. | ||
| /// Default: throws <see cref="CanvasError.NoHandler"/>. | ||
| /// </summary> | ||
| Task<object?> OnActionAsync(CanvasActionContext context, CancellationToken cancellationToken); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Convenience base class for <see cref="ICanvasHandler"/> that supplies | ||
| /// default no-op / no-handler implementations of the optional callbacks. | ||
| /// </summary> | ||
| [Experimental(Diagnostics.Experimental)] | ||
| public abstract class CanvasHandlerBase : ICanvasHandler | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this necessary? Doesn't seem like it provides much in the way of convenience.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| { | ||
| /// <inheritdoc /> | ||
| public abstract Task<CanvasOpenResponse> OnOpenAsync(CanvasOpenContext context, CancellationToken cancellationToken); | ||
|
|
||
| /// <inheritdoc /> | ||
| public virtual Task OnCloseAsync(CanvasLifecycleContext context, CancellationToken cancellationToken) | ||
| #if NET8_0_OR_GREATER | ||
| => Task.CompletedTask; | ||
| #else | ||
| => Task.FromResult<object?>(null); | ||
| #endif | ||
|
|
||
| /// <inheritdoc /> | ||
| public virtual Task<object?> OnActionAsync(CanvasActionContext context, CancellationToken cancellationToken) | ||
| => Task.FromException<object?>(CanvasError.NoHandler()); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This name confuses me a bit. Should this be specific to rendering, as elaborated on in the comment? It's not clear to me from "Canvases" that it's specific to rendering, and presumably there are other capabilities a canvas host will be capable of related to canvases.