Skip to content

Add open-canvases snapshot tracking to the Java SDK#1606

Merged
edburns merged 3 commits into
mainfrom
jmoseley/java-open-canvases-snapshot
Jun 10, 2026
Merged

Add open-canvases snapshot tracking to the Java SDK#1606
edburns merged 3 commits into
mainfrom
jmoseley/java-open-canvases-snapshot

Conversation

@jmoseley

@jmoseley jmoseley commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What

Brings the Java SDK to parity with the other five SDK languages (Rust, Node, Python, Go, .NET) by maintaining an in-memory snapshot of the canvas instances currently open for a session. Java previously had none of this feature — only the generated event/data types existed — so this is net-new.

Behavior (mirrors the other 5 languages' contract exactly)

  • CopilotSession keeps a lock-guarded List<OpenCanvasInstance> and exposes it via a new public accessor getOpenCanvases() (returns an immutable defensive copy).
  • session.canvas.openedupsert by instanceId. A provider-unregister re-emit arrives as another opened event with availability=stale and replaces the prior entry rather than duplicating it.
  • session.canvas.closedremove by instanceId (idempotent — removing an absent id is a no-op).
  • Validation guardrails: opened requires non-empty instanceId/canvasId/extensionId and non-null availability; closed requires a non-empty instanceId. Invalid/empty/null payloads log the canonical failed to deserialize session.canvas.{opened,closed} payload warning and no-op. opened/stale events are never treated as removals.
  • The update runs inside handleBroadcastEventAsync alongside the existing capabilities.changed passive state update, before user handlers observe the event, wrapped best-effort so snapshot upkeep can never disrupt event delivery.
  • Resume/create seeding: the snapshot is seeded from the session.create / session.resume responses, which already carry openCanvases on the wire. Added the field to the hand-written CreateSessionResponse / ResumeSessionResponse records and seed via session.setOpenCanvases(...) after setCapabilities, matching .NET.

Tests

New SessionCanvasSnapshotTest (12 cases): open two → both present; close one → gone, other remains; idempotent absent close; empty + null instanceId no-op; missing-required-field opened ignored; stale re-emit replaces (no duplicate); getOpenCanvases() returns an immutable copy; seed via setOpenCanvases (incl. null-element filtering); null clears; plus Jackson deserialization tests for both response records. mvn spotless:apply && mvn verify is green.

Context

Constraints honored

Java only. No edits to generated files (java/src/generated/) or other languages.

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

Bring the Java SDK to parity with the other five SDK languages
(Rust, Node, Python, Go, .NET) by maintaining an in-memory snapshot of
the canvas instances currently open for a session.

- CopilotSession now keeps a lock-guarded List<OpenCanvasInstance> and
  exposes it via getOpenCanvases() (immutable defensive copy).
- session.canvas.opened upserts by instanceId (a stale re-emit from a
  provider unregister replaces the prior entry rather than duplicating
  it); session.canvas.closed removes by instanceId. Both are validated
  against the canonical contract and are best-effort so snapshot upkeep
  never disrupts event delivery. The update runs in
  handleBroadcastEventAsync alongside the capabilities.changed state
  update, before user handlers observe the event.
- The snapshot is seeded from the session.create / session.resume
  responses, which already carry openCanvases on the wire.

Mirrors PR #1604, which landed the same opened-upsert + closed-remove
behavior for Rust/Node/Python/Go/.NET, and the runtime events from
copilot-agent-runtime #9489 (CLI 1.0.60). The consumer is github-app's
sticky-canvas fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 9, 2026 04:36
@jmoseley jmoseley requested a review from a team as a code owner June 9, 2026 04:36

Copilot AI left a comment

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.

Pull request overview

Adds Java-side in-memory tracking of “open canvas instances” for a session, bringing the Java SDK in line with the existing snapshot behavior in the other language SDKs.

Changes:

  • Added a lock-guarded open-canvases snapshot to CopilotSession, updated via session.canvas.opened (upsert) and session.canvas.closed (remove), and exposed via getOpenCanvases().
  • Seeded the snapshot from session.create / session.resume RPC responses by adding openCanvases to the corresponding response records and wiring seeding in CopilotClient.
  • Added unit tests covering upsert/remove, guardrails, stale re-emit replacement, seeding, and response deserialization.
Show a summary per file
File Description
java/src/main/java/com/github/copilot/CopilotSession.java Maintains and exposes the open-canvases snapshot; updates it during event dispatch.
java/src/main/java/com/github/copilot/CopilotClient.java Seeds the session’s open-canvases snapshot from create/resume responses.
java/src/main/java/com/github/copilot/rpc/CreateSessionResponse.java Adds openCanvases to the create-session response shape for seeding.
java/src/main/java/com/github/copilot/rpc/ResumeSessionResponse.java Adds openCanvases to the resume-session response shape for seeding.
java/src/test/java/com/github/copilot/SessionCanvasSnapshotTest.java New unit coverage for snapshot behavior + response deserialization.

Copilot's findings

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

Comment thread java/src/main/java/com/github/copilot/CopilotSession.java
… test

- getOpenCanvases() @SInCE 1.0.0 -> 1.0.1 (new public API)
- Note openCanvases component added in 1.0.1 on Create/ResumeSessionResponse
  (the record types themselves predate this PR, so type-level @SInCE stays 1.0.0)
- getOpenCanvasesReturnsImmutableCopy now dispatches a later event and asserts
  the previously-returned list is unchanged, proving it is a point-in-time
  snapshot rather than a live view

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

This comment has been minimized.

@edburns

edburns commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Investigating the test failure now.

@github-actions

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅

This PR successfully brings Java to parity with all five other SDK implementations for the open-canvases snapshot feature. Here's the cross-SDK consistency assessment:

API surface — naming is idiomatic and consistent

SDK Public accessor Return type
Node.js session.openCanvases (getter) OpenCanvasInstance[] (copy)
Python session.open_canvases (property) list[OpenCanvasInstance] (copy)
Go session.OpenCanvases() []rpc.OpenCanvasInstance (copy)
.NET session.OpenCanvases (property) IReadOnlyList<OpenCanvasInstance>
Rust session.open_canvases() Vec<OpenCanvasInstance> (clone)
Java (this PR) session.getOpenCanvases() List<OpenCanvasInstance> (immutable copy)

All names follow language conventions. ✅

Behavior — all SDKs match

  • session.canvas.opened → upsert by instanceId (stale re-emit replaces, not duplicates) ✅
  • session.canvas.closed → idempotent remove by instanceId
  • Invalid/missing required fields → log warning, no-op ✅
  • Return value is an immutable/defensive copy ✅
  • Thread-safe snapshot (Java uses synchronized, Go uses sync.RWMutex, Rust uses parking_lot::RwLock, Python uses threading.Lock) ✅

One pre-existing inconsistency worth noting (not introduced by this PR)

This PR matches .NET's behavior of seeding openCanvases from both session.create and session.resume responses. However, Node.js, Python, Go, and Rust (as implemented in #1604) only seed from session.resume — their createSession implementations don't read openCanvases from the session.create response.

This gap predates this PR and is a minor behavioral difference: a freshly-created session in Node.js/Python/Go/Rust will always start with an empty canvas snapshot regardless of what the wire returns, while .NET and Java will correctly reflect any canvases the CLI reports at session creation time. Worth a follow-up to align Node.js/Python/Go/Rust with .NET+Java behavior.

This is not a blocker for merging this PR — Java's implementation is the more complete one here.

Generated by SDK Consistency Review Agent for issue #1606 · sonnet46 1.7M ·

@edburns edburns left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR is solid and I'm +1 on merging it for Java parity.

Suggested follow-up: now that Java has the canvas snapshot foundation in place, this is a good time to invest in canvas ergonomics holistically across the SDK — not just for Java, but as a coordinated cross-language effort.

Concretely, the gap between Node's per-canvas factory pattern (createCanvas + per-action handler closures) and the single session-level CanvasHandler that the other five languages expose (where the caller is responsible for multiplexing by canvasId) is the most impactful thing to close. A thin, additive canvas-router helper — a canvasId-to-handler/action map that sits in front of the existing CanvasHandler interface — would substantially reduce boilerplate for implementors without touching any wire contracts or generated types.

The same "additive ergonomics over stable wire contracts" pattern applies to other areas too (elicitation helpers, plan-mode policy objects, SessionFS provider boilerplate), so it is worth writing a short cross-SDK ergonomics design note that sets naming and behavioral guardrails before we start building. But canvas is the right place to start: it is the newest surface, the cross-SDK inconsistency is already documented, and this PR gives Java the baseline needed to participate.

@edburns edburns added this pull request to the merge queue Jun 10, 2026
Merged via the queue into main with commit 0efeb73 Jun 10, 2026
23 of 24 checks passed
@edburns edburns deleted the jmoseley/java-open-canvases-snapshot branch June 10, 2026 18:13
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.

3 participants