Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
89 changes: 89 additions & 0 deletions java/src/main/java/com/github/copilot/CopilotSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,18 @@
import com.github.copilot.generated.SessionErrorEvent;
import com.github.copilot.generated.SessionEvent;
import com.github.copilot.generated.SessionIdleEvent;
import com.github.copilot.generated.rpc.CanvasActionInvokeParams;
import com.github.copilot.generated.rpc.CanvasCloseParams;
import com.github.copilot.generated.rpc.CanvasOpenParams;
import com.github.copilot.generated.rpc.CanvasOpenResult;
import com.github.copilot.generated.rpc.OpenCanvasInstance;
import com.github.copilot.rpc.AgentInfo;
import com.github.copilot.rpc.AutoModeSwitchHandler;
import com.github.copilot.rpc.AutoModeSwitchInvocation;
import com.github.copilot.rpc.AutoModeSwitchRequest;
import com.github.copilot.rpc.AutoModeSwitchResponse;
import com.github.copilot.rpc.CanvasException;
import com.github.copilot.rpc.CanvasHandler;
import com.github.copilot.rpc.CommandContext;
import com.github.copilot.rpc.CommandDefinition;
import com.github.copilot.rpc.CommandHandler;
Expand Down Expand Up @@ -182,6 +188,7 @@ public final class CopilotSession implements AutoCloseable {
private final AtomicReference<ElicitationHandler> elicitationHandler = new AtomicReference<>();
private final AtomicReference<ExitPlanModeHandler> exitPlanModeHandler = new AtomicReference<>();
private final AtomicReference<AutoModeSwitchHandler> autoModeSwitchHandler = new AtomicReference<>();
private final AtomicReference<CanvasHandler> canvasHandler = new AtomicReference<>();
private final AtomicReference<SessionHooks> hooksHandler = new AtomicReference<>();
private volatile EventErrorHandler eventErrorHandler;
private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS;
Expand Down Expand Up @@ -1479,6 +1486,87 @@ void registerAutoModeSwitchHandler(AutoModeSwitchHandler handler) {
autoModeSwitchHandler.set(handler);
}

/**
* Registers the canvas lifecycle handler for this session.
* <p>
* Called internally when creating or resuming a session that declares canvases.
* The handler receives inbound {@code canvas.open} / {@code canvas.close} /
* {@code canvas.action.invoke} requests.
*
* @param handler
* the handler to invoke for inbound canvas requests
*/
void registerCanvasHandler(CanvasHandler handler) {
canvasHandler.set(handler);
}

/**
* Routes an inbound {@code canvas.open} request to the registered
* {@link CanvasHandler}.
* <p>
* Called internally by the RPC dispatcher.
*
* @param params
* the open request from the runtime
* @return a future that completes with the open result
*/
CompletableFuture<CanvasOpenResult> handleCanvasOpen(CanvasOpenParams params) {
CanvasHandler handler = canvasHandler.get();
if (handler == null) {
return CompletableFuture.failedFuture(
new CanvasException("canvas_no_handler", "No canvas handler registered for this session"));
}
try {
return handler.onOpen(params);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}

/**
* Routes an inbound {@code canvas.action.invoke} request to the registered
* {@link CanvasHandler}.
* <p>
* Called internally by the RPC dispatcher.
*
* @param params
* the action-invoke request from the runtime
* @return a future that completes with the JSON-serializable action result
*/
CompletableFuture<Object> handleCanvasAction(CanvasActionInvokeParams params) {
CanvasHandler handler = canvasHandler.get();
if (handler == null) {
return CompletableFuture.failedFuture(CanvasException.noHandler());
}
try {
return handler.onAction(params);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}

/**
* Routes an inbound {@code canvas.close} request to the registered
* {@link CanvasHandler}.
* <p>
* Called internally by the RPC dispatcher.
*
* @param params
* the close request from the runtime
* @return a future that completes when the close has been handled
*/
CompletableFuture<Void> handleCanvasClose(CanvasCloseParams params) {
CanvasHandler handler = canvasHandler.get();
if (handler == null) {
return CompletableFuture.completedFuture(null);
}
try {
return handler.onClose(params);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}

/**
* Sets the capabilities reported by the host for this session.
* <p>
Expand Down Expand Up @@ -2245,6 +2333,7 @@ public void close() {
elicitationHandler.set(null);
exitPlanModeHandler.set(null);
autoModeSwitchHandler.set(null);
canvasHandler.set(null);
hooksHandler.set(null);
}

Expand Down
22 changes: 22 additions & 0 deletions java/src/main/java/com/github/copilot/JsonRpcClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,34 @@ public void sendResponse(Object id, Object result) throws IOException {
* Sends a JSON-RPC error response to a server request.
*/
public void sendErrorResponse(Object id, int code, String message) throws IOException {
sendErrorResponse(id, code, message, null);
}

/**
* Sends a JSON-RPC error response with structured {@code data} to a server
* request.
*
* @param id
* the request id being responded to
* @param code
* the JSON-RPC error code
* @param message
* the human-readable error message
* @param data
* optional structured error data, or {@code null} to omit
* @throws IOException
* if the response cannot be written
*/
public void sendErrorResponse(Object id, int code, String message, Object data) throws IOException {
var response = new JsonRpcResponse();
response.setJsonrpc("2.0");
response.setId(id);
var error = new JsonRpcError();
error.setCode(code);
error.setMessage(message);
if (data != null) {
error.setData(data);
}
response.setError(error);
sendMessage(response);
}
Expand Down
116 changes: 116 additions & 0 deletions java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.copilot.generated.SessionEvent;
import com.github.copilot.generated.rpc.CanvasActionInvokeParams;
import com.github.copilot.generated.rpc.CanvasCloseParams;
import com.github.copilot.generated.rpc.CanvasOpenParams;
import com.github.copilot.rpc.AutoModeSwitchRequest;
import com.github.copilot.rpc.CanvasException;
import com.github.copilot.rpc.ExitPlanModeRequest;
import com.github.copilot.rpc.BearerTokenProvider;
import com.github.copilot.rpc.ProviderTokenArgs;
Expand Down Expand Up @@ -92,6 +96,10 @@ void registerHandlers(JsonRpcClient rpc) {
(requestId, params) -> handleSystemMessageTransform(rpc, requestId, params));
rpc.registerMethodHandler("providerToken.getToken",
(requestId, params) -> handleProviderTokenGetToken(rpc, requestId, params));
rpc.registerMethodHandler("canvas.open", (requestId, params) -> handleCanvasOpen(rpc, requestId, params));
rpc.registerMethodHandler("canvas.close", (requestId, params) -> handleCanvasClose(rpc, requestId, params));
rpc.registerMethodHandler("canvas.action.invoke",
(requestId, params) -> handleCanvasActionInvoke(rpc, requestId, params));
}

private void handleSessionEvent(JsonNode params) {
Expand Down Expand Up @@ -465,6 +473,114 @@ private void handleAutoModeSwitchRequest(JsonRpcClient rpc, String requestId, Js
});
}

private void handleCanvasOpen(JsonRpcClient rpc, String requestId, JsonNode params) {
runAsync(() -> {
final long requestIdLong = parseRequestId(requestId, "canvas.open");
if (requestIdLong == -1) {
return;
}
try {
CanvasOpenParams openParams = MAPPER.treeToValue(params, CanvasOpenParams.class);
CopilotSession session = sessions.get(openParams.sessionId());
if (session == null) {
rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + openParams.sessionId());
return;
}
session.handleCanvasOpen(openParams).thenAccept(result -> {
try {
rpc.sendResponse(requestIdLong, result);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending canvas open response", e);
}
}).exceptionally(ex -> {
sendCanvasError(rpc, requestIdLong, "canvas.open", ex);
return null;
});
} catch (Exception e) {
LOG.log(Level.SEVERE, "Error handling canvas open request", e);
sendCanvasError(rpc, requestIdLong, "canvas.open", e);
}
Comment thread
jmoseley marked this conversation as resolved.
});
}

private void handleCanvasClose(JsonRpcClient rpc, String requestId, JsonNode params) {
runAsync(() -> {
final long requestIdLong = parseRequestId(requestId, "canvas.close");
if (requestIdLong == -1) {
return;
}
try {
CanvasCloseParams closeParams = MAPPER.treeToValue(params, CanvasCloseParams.class);
CopilotSession session = sessions.get(closeParams.sessionId());
if (session == null) {
rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + closeParams.sessionId());
return;
}
session.handleCanvasClose(closeParams).thenAccept(ignored -> {
try {
rpc.sendResponse(requestIdLong, null);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending canvas close response", e);
}
}).exceptionally(ex -> {
sendCanvasError(rpc, requestIdLong, "canvas.close", ex);
return null;
});
} catch (Exception e) {
LOG.log(Level.SEVERE, "Error handling canvas close request", e);
sendCanvasError(rpc, requestIdLong, "canvas.close", e);
}
Comment thread
jmoseley marked this conversation as resolved.
});
}

private void handleCanvasActionInvoke(JsonRpcClient rpc, String requestId, JsonNode params) {
runAsync(() -> {
final long requestIdLong = parseRequestId(requestId, "canvas.action.invoke");
if (requestIdLong == -1) {
return;
}
try {
CanvasActionInvokeParams actionParams = MAPPER.treeToValue(params, CanvasActionInvokeParams.class);
CopilotSession session = sessions.get(actionParams.sessionId());
if (session == null) {
rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + actionParams.sessionId());
return;
}
session.handleCanvasAction(actionParams).thenAccept(result -> {
try {
rpc.sendResponse(requestIdLong, result);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending canvas action response", e);
}
}).exceptionally(ex -> {
sendCanvasError(rpc, requestIdLong, "canvas.action.invoke", ex);
return null;
});
} catch (Exception e) {
LOG.log(Level.SEVERE, "Error handling canvas action invoke request", e);
sendCanvasError(rpc, requestIdLong, "canvas.action.invoke", e);
}
Comment thread
jmoseley marked this conversation as resolved.
});
}

/**
* Sends a structured error response for a failed canvas callback. A
* {@link CanvasException} carries a machine-readable {@code code}; any other
* exception is wrapped in a generic {@code canvas_handler_error} envelope.
*/
private void sendCanvasError(JsonRpcClient rpc, long requestIdLong, String label, Throwable ex) {
Throwable cause = (ex instanceof java.util.concurrent.CompletionException && ex.getCause() != null)
? ex.getCause()
: ex;
String code = cause instanceof CanvasException ? ((CanvasException) cause).getCode() : "canvas_handler_error";
String message = cause.getMessage() != null ? cause.getMessage() : cause.toString();
try {
rpc.sendErrorResponse(requestIdLong, -32603, message, Map.of("code", code, "message", message));
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending " + label + " error", e);
}
}

private void handleHooksInvoke(JsonRpcClient rpc, String requestId, JsonNode params) {
runAsync(() -> {
final long requestIdLong = parseRequestId(requestId, "hooks.invoke");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
request.setRemoteSession(config.getRemoteSession());
request.setCloud(config.getCloud());
request.setExpAssignments(config.getExpAssignments());
request.setCanvases(config.getCanvases());
request.setRequestCanvasRenderer(config.getRequestCanvasRenderer());
request.setRequestExtensions(config.getRequestExtensions());
request.setExtensionSdkPath(config.getExtensionSdkPath());
request.setExtensionInfo(config.getExtensionInfo());
request.setCanvasProvider(config.getCanvasProvider());

return request;
}
Expand Down Expand Up @@ -300,6 +306,13 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
request.setGitHubToken(config.getGitHubToken());
request.setRemoteSession(config.getRemoteSession());
request.setExpAssignments(config.getExpAssignments());
request.setCanvases(config.getCanvases());
request.setOpenCanvases(config.getOpenCanvases());
request.setRequestCanvasRenderer(config.getRequestCanvasRenderer());
request.setRequestExtensions(config.getRequestExtensions());
request.setExtensionSdkPath(config.getExtensionSdkPath());
request.setExtensionInfo(config.getExtensionInfo());
request.setCanvasProvider(config.getCanvasProvider());

return request;
}
Expand Down Expand Up @@ -349,6 +362,9 @@ static void configureSession(CopilotSession session, SessionConfig config) {
if (config.getOnAutoModeSwitch() != null) {
session.registerAutoModeSwitchHandler(config.getOnAutoModeSwitch());
}
if (config.getCanvasHandler() != null) {
session.registerCanvasHandler(config.getCanvasHandler());
}
if (config.getOnEvent() != null) {
session.on(config.getOnEvent());
}
Expand Down Expand Up @@ -399,6 +415,9 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config)
if (config.getOnAutoModeSwitch() != null) {
session.registerAutoModeSwitchHandler(config.getOnAutoModeSwitch());
}
if (config.getCanvasHandler() != null) {
session.registerCanvasHandler(config.getCanvasHandler());
}
if (config.getOnEvent() != null) {
session.on(config.getOnEvent());
}
Expand Down
Loading
Loading