Add open-canvases snapshot tracking to the Java SDK#1606
Conversation
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>
There was a problem hiding this comment.
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 viasession.canvas.opened(upsert) andsession.canvas.closed(remove), and exposed viagetOpenCanvases(). - Seeded the snapshot from
session.create/session.resumeRPC responses by addingopenCanvasesto the corresponding response records and wiring seeding inCopilotClient. - 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
… 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>
Cross-SDK Consistency Review ✅This PR correctly brings the Java SDK to full parity with the other five SDK implementations. I reviewed the implementation against the current Feature parity across all SDKs
API naming consistencyJava uses No inconsistencies found. 🎉
|
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)
CopilotSessionkeeps a lock-guardedList<OpenCanvasInstance>and exposes it via a new public accessorgetOpenCanvases()(returns an immutable defensive copy).session.canvas.opened→ upsert byinstanceId. A provider-unregister re-emit arrives as anotheropenedevent withavailability=staleand replaces the prior entry rather than duplicating it.session.canvas.closed→ remove byinstanceId(idempotent — removing an absent id is a no-op).openedrequires non-emptyinstanceId/canvasId/extensionIdand non-nullavailability;closedrequires a non-emptyinstanceId. Invalid/empty/nullpayloads log the canonicalfailed to deserialize session.canvas.{opened,closed} payloadwarning and no-op.opened/stale events are never treated as removals.handleBroadcastEventAsyncalongside the existingcapabilities.changedpassive state update, before user handlers observe the event, wrapped best-effort so snapshot upkeep can never disrupt event delivery.session.create/session.resumeresponses, which already carryopenCanvaseson the wire. Added the field to the hand-writtenCreateSessionResponse/ResumeSessionResponserecords and seed viasession.setOpenCanvases(...)aftersetCapabilities, matching .NET.Tests
New
SessionCanvasSnapshotTest(12 cases): open two → both present; close one → gone, other remains; idempotent absent close; empty +nullinstanceIdno-op; missing-required-fieldopenedignored; stale re-emit replaces (no duplicate);getOpenCanvases()returns an immutable copy; seed viasetOpenCanvases(incl. null-element filtering);nullclears; plus Jackson deserialization tests for both response records.mvn spotless:apply && mvn verifyis 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