Skip to content

Commit bdb687f

Browse files
jmoseleyCopilot
andcommitted
Add openCanvasInstances to ResumeSessionConfig
Threads agent-canvas instance rehydrate from the host through both Rust and Node SDKs to the runtime's session.resume RPC. Rust: - New CanvasInstanceRehydrate struct in canvas.rs (camelCase serde). - ResumeSessionConfig gains open_canvas_instances: Vec<CanvasInstanceRehydrate> with a with_open_canvas_instances() builder; serializes via the existing serde_json::to_value(&config) wire path in resume_session. - Debug impl includes the new field. Node: - CanvasInstanceRehydrate interface mirrored in canvas.ts, re-exported from index.ts. - ResumeSessionConfig.openCanvasInstances?: CanvasInstanceRehydrate[]. - client.ts session.resume payload forwards the field. The runtime side (copilot-agent-runtime PR #8441) consumes this via SessionResumeRequest.openCanvasInstances and rehydrateCanvasInstances(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7655d0b commit bdb687f

6 files changed

Lines changed: 86 additions & 1 deletion

File tree

nodejs/src/canvas.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,25 @@ export interface CanvasOpenResponse {
9797
instanceId?: string;
9898
}
9999

100+
/**
101+
* Identifies an extension canvas instance that the host believes is still open
102+
* across a runtime restart. Supplied via `ResumeSessionConfig.openCanvasInstances`
103+
* so the runtime can re-populate its in-memory instance map without re-invoking
104+
* the extension's `onOpen`. Orphans (no matching extension/canvas in the active
105+
* extension set) trigger a `session.canvas.closed` event with
106+
* `reason: "rehydrate_failed"` so the host can drop the stale UI.
107+
*/
108+
export interface CanvasInstanceRehydrate {
109+
/** Extension id that originally opened the canvas. */
110+
extensionId: string;
111+
/** Canvas id (matches the declaring `CanvasDeclaration.id`). */
112+
canvasId: string;
113+
/** Agent-supplied stable instance id from the original open. */
114+
instanceId: string;
115+
/** Extension-owned URL the host last rendered, if any. */
116+
url?: string;
117+
}
118+
100119
/** Context handed to a canvas's `onOpen` handler. */
101120
export interface CanvasOpenContext {
102121
/** Session that requested the canvas. */

nodejs/src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,7 @@ export class CopilotClient {
990990
infiniteSessions: config.infiniteSessions,
991991
disableResume: config.disableResume,
992992
continuePendingWork: config.continuePendingWork,
993+
openCanvasInstances: config.openCanvasInstances,
993994
gitHubToken: config.gitHubToken,
994995
remoteSession: config.remoteSession,
995996
});

nodejs/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export {
1717
type CanvasActionContext,
1818
type CanvasAgentActionDeclaration,
1919
type CanvasDeclaration,
20+
type CanvasInstanceRehydrate,
2021
type CanvasLifecycleContext,
2122
type CanvasOpenContext,
2223
type CanvasOpenResponse,

nodejs/src/types.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
// Import and re-export generated session event types
10-
import type { Canvas } from "./canvas.js";
10+
import type { Canvas, CanvasInstanceRehydrate } from "./canvas.js";
1111
import type { SessionFsProvider } from "./sessionFsProvider.js";
1212
import type { SessionEvent as GeneratedSessionEvent } from "./generated/session-events.js";
1313
import type { CopilotSession } from "./session.js";
@@ -1639,6 +1639,17 @@ export type ResumeSessionConfig = Pick<
16391639
* @default false
16401640
*/
16411641
continuePendingWork?: boolean;
1642+
/**
1643+
* Extension canvas instances the host believes are still open from a prior
1644+
* runtime process. Supplied on resume so the runtime can re-populate its
1645+
* in-memory canvas instance map without re-invoking each extension's
1646+
* `onOpen`. Instances whose `(extensionId, canvasId)` don't resolve in the
1647+
* active extension set produce a `session.canvas.closed` event with
1648+
* `reason: "rehydrate_failed"` so the host can drop the stale UI. Native
1649+
* host-implemented canvases (e.g. `host.*` ids) should be omitted — the
1650+
* host owns their lifecycle end-to-end without the runtime instance record.
1651+
*/
1652+
openCanvasInstances?: CanvasInstanceRehydrate[];
16421653
};
16431654

16441655
/**

rust/src/canvas.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,31 @@ pub struct CanvasOpenResponse {
111111
pub instance_id: Option<String>,
112112
}
113113

114+
/// Per-instance resume hint sent on `session.resume` to rebuild the runtime's
115+
/// canvas-instance registry. The host persists open canvases across CLI
116+
/// process restarts and hands them back here so subsequent
117+
/// `invoke_canvas_action` dispatches find the existing instance instead of
118+
/// erroring with `canvas_instance_not_found`.
119+
///
120+
/// The handler's `on_open` is **not** re-invoked on rehydrate — the extension
121+
/// keeps whatever state it had in its own process. Entries the runtime cannot
122+
/// bind to a currently-declared canvas trigger a `session.canvas.closed`
123+
/// event with `reason: "rehydrate_failed"`.
124+
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
125+
#[serde(rename_all = "camelCase")]
126+
pub struct CanvasInstanceRehydrate {
127+
/// Canonical extension id that owns the canvas.
128+
pub extension_id: String,
129+
/// Canvas declaration id within that extension.
130+
pub canvas_id: String,
131+
/// Stable instance id the host originally opened the canvas under.
132+
pub instance_id: String,
133+
/// Optional URL recorded at the original open. Populated as-is into the
134+
/// rebuilt instance record; not re-validated by the runtime.
135+
#[serde(default, skip_serializing_if = "Option::is_none")]
136+
pub url: Option<String>,
137+
}
138+
114139
/// Context handed to [`CanvasHandler::on_open`].
115140
#[derive(Debug, Clone)]
116141
pub struct CanvasOpenContext {

rust/src/types.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1823,6 +1823,18 @@ pub struct ResumeSessionConfig {
18231823
/// the prior entry on the runtime side.
18241824
#[serde(default, skip_serializing_if = "Vec::is_empty", skip_deserializing)]
18251825
pub canvases: Vec<crate::canvas::Canvas>,
1826+
/// Host-supplied list of canvas instances that should still be considered
1827+
/// open from a prior CLI process run, scoped to this session. The runtime
1828+
/// rebuilds its in-memory canvas-instance registry from these entries so
1829+
/// subsequent `invoke_canvas_action` dispatches succeed without the host
1830+
/// re-issuing `canvas.open`. Handler `on_open` is **not** re-invoked.
1831+
///
1832+
/// Entries that fail to bind to a currently-declared canvas (extension
1833+
/// not loaded this session, or contribution removed) trigger a
1834+
/// `session.canvas.closed` event with `reason: "rehydrate_failed"` so the
1835+
/// host can clean up the stale panel.
1836+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
1837+
pub open_canvas_instances: Vec<crate::canvas::CanvasInstanceRehydrate>,
18261838
/// Custom session filesystem provider. Required on resume when the
18271839
/// [`Client`](crate::Client) was started with
18281840
/// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs).
@@ -1897,6 +1909,7 @@ impl std::fmt::Debug for ResumeSessionConfig {
18971909
)
18981910
.field("commands", &self.commands)
18991911
.field("canvases", &self.canvases)
1912+
.field("open_canvas_instances", &self.open_canvas_instances)
19001913
.field(
19011914
"session_fs_provider",
19021915
&self.session_fs_provider.as_ref().map(|_| "<set>"),
@@ -1956,6 +1969,7 @@ impl ResumeSessionConfig {
19561969
include_sub_agent_streaming_events: None,
19571970
commands: None,
19581971
canvases: Vec::new(),
1972+
open_canvas_instances: Vec::new(),
19591973
session_fs_provider: None,
19601974
disable_resume: None,
19611975
continue_pending_work: None,
@@ -1992,6 +2006,20 @@ impl ResumeSessionConfig {
19922006
self
19932007
}
19942008

2009+
/// Supply the list of canvas instances the host still considers open
2010+
/// from a prior CLI process run. The runtime resolves each entry's
2011+
/// `(extension_id, canvas_id)` against the canvases declared on this
2012+
/// resume and rebuilds its in-memory instance registry, so subsequent
2013+
/// `invoke_canvas_action` dispatches succeed without re-issuing
2014+
/// `canvas.open`. Handler `on_open` is **not** re-invoked.
2015+
pub fn with_open_canvas_instances(
2016+
mut self,
2017+
instances: Vec<crate::canvas::CanvasInstanceRehydrate>,
2018+
) -> Self {
2019+
self.open_canvas_instances = instances;
2020+
self
2021+
}
2022+
19952023
/// Install a [`SessionFsProvider`] backing the resumed session's
19962024
/// filesystem. See [`SessionConfig::with_session_fs_provider`].
19972025
pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {

0 commit comments

Comments
 (0)