Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1cffb3e
Add canvas runtime support to SDK
jmoseley May 22, 2026
4d5bd11
Add canvas provider RPC tracing
jmoseley May 23, 2026
36397a8
Add extension info session option
jmoseley May 23, 2026
458c6f3
Expose canvas resume durability fields
jmoseley May 23, 2026
9eb4523
Address canvas SDK review feedback
jmoseley May 23, 2026
076a288
Format Rust session imports
jmoseley May 23, 2026
68616d8
Sync canvas tool surface docs
jmoseley May 23, 2026
cc4bccc
Require canvas descriptions
jmoseley May 23, 2026
d0accc4
Require canvas instance availability
jmoseley May 23, 2026
5985b50
Rename Node canvas open handler
jmoseley May 23, 2026
4ebff84
Drop canvas toolbar and focus/reload surface
jmoseley May 23, 2026
efd7aef
Restore optional onClose handler on Node canvas options
jmoseley May 23, 2026
6710b30
Support per-action handlers on Node canvas actions
jmoseley May 23, 2026
cdd4a5e
Drop top-level onAction fallback on Node canvases
jmoseley May 24, 2026
b60c9cb
Normalize JSDoc style on CanvasAgentActionDeclaration
jmoseley May 24, 2026
86b2cc9
Drop stale wire-rename guards from canvas declaration test
jmoseley May 24, 2026
0f597e9
Document canvas action dispatch divergence in Rust SDK
jmoseley May 24, 2026
f5c76f8
Require handler on canvas actions
jmoseley May 24, 2026
37c8d36
Collapse CanvasAgentActionDeclaration into CanvasAction
jmoseley May 24, 2026
5115ce8
Drop debug log for inbound JSON-RPC requests
jmoseley May 24, 2026
c1146c0
Drop canvas debug logs
jmoseley May 24, 2026
3475390
Remove canvas tools field
jmoseley May 24, 2026
3c0850d
rust: slim canvas surface to wire types + CanvasHandler
jmoseley May 24, 2026
cd0f66f
rust: drop canvas wire types duplicated by codegen
jmoseley May 24, 2026
f47d4c5
Fix broken intra-doc link to renamed CanvasAction type
jmoseley May 24, 2026
2dc1f90
go: add canvas runtime support
jmoseley May 24, 2026
c6d1f00
python: add canvas runtime support
jmoseley May 24, 2026
97dc612
dotnet: add canvas runtime support
jmoseley May 24, 2026
04fc748
Address PR review: openCanvases parity + Node API divergence note
jmoseley May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
288 changes: 288 additions & 0 deletions dotnet/src/Canvas.cs
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; }
}

/// <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
{
/// <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;

/// <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
{
/// <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());
}
Loading
Loading